feat: integrate platform backend and application interfaces
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "@miragenflow/contracts",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,462 @@
|
||||
export const CONTRACT_VERSION = "2026-08-20.v1" as const;
|
||||
export const API_PREFIX = "/api/v1" as const;
|
||||
export const WS_PATH = "/api/v1/ws/tasks" as const;
|
||||
|
||||
export const ErrorCodes = {
|
||||
Unauthorized: "UNAUTHORIZED",
|
||||
Forbidden: "FORBIDDEN",
|
||||
ValidationFailed: "VALIDATION_FAILED",
|
||||
RateLimited: "RATE_LIMITED",
|
||||
NotFound: "NOT_FOUND",
|
||||
Conflict: "CONFLICT",
|
||||
InsufficientBalance: "INSUFFICIENT_BALANCE",
|
||||
CapabilityNotEnabled: "CAPABILITY_NOT_ENABLED",
|
||||
ModelGroupUnavailable: "MODEL_GROUP_UNAVAILABLE",
|
||||
Internal: "INTERNAL_ERROR",
|
||||
VerificationRequired: "VERIFICATION_REQUIRED",
|
||||
VerificationExpired: "VERIFICATION_EXPIRED",
|
||||
SessionReused: "SESSION_REUSED",
|
||||
IdempotencyConflict: "IDEMPOTENCY_CONFLICT",
|
||||
ObjectNotFound: "OBJECT_NOT_FOUND",
|
||||
ObjectOwnershipDenied: "OBJECT_OWNERSHIP_DENIED",
|
||||
FileRejected: "FILE_REJECTED",
|
||||
StorageUnavailable: "STORAGE_UNAVAILABLE",
|
||||
ApprovalRequired: "APPROVAL_REQUIRED",
|
||||
VersionConflict: "VERSION_CONFLICT",
|
||||
AdminMfaRequired: "ADMIN_MFA_REQUIRED",
|
||||
} as const;
|
||||
|
||||
export type ErrorCode = (typeof ErrorCodes)[keyof typeof ErrorCodes];
|
||||
|
||||
export type ApiError = {
|
||||
code: ErrorCode;
|
||||
message: string;
|
||||
requestId: string;
|
||||
details?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type ApiEnvelope<T> =
|
||||
| { ok: true; data: T; requestId: string }
|
||||
| { ok: false; error: ApiError };
|
||||
|
||||
export type AuthUser = {
|
||||
id: string;
|
||||
username?: string;
|
||||
email?: string;
|
||||
phone?: string;
|
||||
displayName: string;
|
||||
status: "pending_verification" | "active" | "frozen" | "closing";
|
||||
mfaRequired: boolean;
|
||||
mfaEnabled: boolean;
|
||||
mfaSetupRequired: boolean;
|
||||
};
|
||||
|
||||
export type SessionResponse = {
|
||||
user: AuthUser;
|
||||
accessToken: string;
|
||||
accessTokenExpiresAt: string;
|
||||
};
|
||||
|
||||
export type MfaEnrollment = {
|
||||
secret: string;
|
||||
provisioningUri: string;
|
||||
recoveryCodes: string[];
|
||||
};
|
||||
|
||||
export type UserMfaChallengeResponse =
|
||||
| { mfaRequired: true; challengeId: string; challengeType: "verify" }
|
||||
| { mfaRequired: true; challengeId: string; challengeType: "setup"; mfaEnrollment: MfaEnrollment };
|
||||
|
||||
export type UserMfaSetupResponse = {
|
||||
enabled: boolean;
|
||||
challengeId: string;
|
||||
mfaEnrollment: MfaEnrollment;
|
||||
};
|
||||
|
||||
export type UserMfaVerifyResponse =
|
||||
| (SessionResponse & { mfaRequired: false })
|
||||
| { enabled: true; user: AuthUser };
|
||||
|
||||
export type CaptchaAction = "register" | "login" | "verify" | "reset" | "admin-login";
|
||||
export type CaptchaChallengeResponse = { challengeId: string; question: string; expiresAt: string };
|
||||
|
||||
export type ModelCapability = "image" | "text" | "audio" | "utility" | "3d" | "reverse-prompt" | "multi-angle" | "matting" | "split" | "upscale" | "expand";
|
||||
|
||||
export type CapabilitySchema = {
|
||||
capability: ModelCapability;
|
||||
inputTypes: Array<"prompt" | "image" | "mask" | "audio" | "text">;
|
||||
maxInputBytes?: number;
|
||||
parameters?: Record<string, { type: string; required?: boolean; enum?: string[]; min?: number; max?: number }>;
|
||||
resultTypes: Array<"image" | "text" | "audio" | "archive" | "3d">;
|
||||
async: boolean;
|
||||
};
|
||||
|
||||
export type PublicResolution = {
|
||||
id: string;
|
||||
label: string;
|
||||
width: number;
|
||||
height: number;
|
||||
priceMultiplier: number;
|
||||
/** Standard image ratios enabled for this billing preset; `auto` is implicit. */
|
||||
ratios?: string[];
|
||||
};
|
||||
|
||||
export type ReferenceImageInput = {
|
||||
objectId: string;
|
||||
seq: number;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type ReferenceImageGroup = {
|
||||
id?: string;
|
||||
images: ReferenceImageInput[];
|
||||
};
|
||||
|
||||
export type ImageOutputMetadata = {
|
||||
width?: number;
|
||||
height?: number;
|
||||
format?: "png" | "jpeg" | "webp" | string;
|
||||
size?: string;
|
||||
/** Public resolution tier selected for this task, when applicable. */
|
||||
resolution?: "1K" | "2K" | "4K" | string;
|
||||
revisedPrompt?: string;
|
||||
usage?: Record<string, number>;
|
||||
source?: "base64" | "url";
|
||||
};
|
||||
|
||||
export type PublicModelProduct = {
|
||||
id: string;
|
||||
publicModelId: string;
|
||||
name: string;
|
||||
tier: "basic" | "advanced" | "flagship";
|
||||
capabilities: ModelCapability[];
|
||||
resolutions: PublicResolution[];
|
||||
basePrice: number;
|
||||
currencyUnit: string;
|
||||
enabled: boolean;
|
||||
version?: number;
|
||||
capabilitiesSchema?: CapabilitySchema[];
|
||||
publicErrorMessages?: Partial<Record<ErrorCode, string>>;
|
||||
maxCount?: number;
|
||||
maxConcurrent?: number;
|
||||
};
|
||||
|
||||
export type InternalModelProduct = PublicModelProduct & {
|
||||
channelGroupId?: string;
|
||||
};
|
||||
|
||||
export type BalanceSummary = {
|
||||
available: number;
|
||||
reserved: number;
|
||||
unit: { name: string; shortCode: string; icon: string; precision: number; version: number };
|
||||
/** Reserved for future organization/team balances; V1 always omits it. */
|
||||
organizationId?: string;
|
||||
};
|
||||
|
||||
/** Organization/team scope is deliberately a nullable contract extension in
|
||||
* V1. No organization records or sharing endpoints are enabled yet. */
|
||||
export type OrganizationScope = {
|
||||
organizationId?: string;
|
||||
workspaceOwnerId?: string;
|
||||
objectScope?: "user" | "organization";
|
||||
};
|
||||
|
||||
export type BalanceLedgerEntry = {
|
||||
id: string;
|
||||
userId: string;
|
||||
type: "recharge" | "reserve" | "settle" | "release" | "refund" | "adjustment";
|
||||
amount: number;
|
||||
balanceUnitVersion: number;
|
||||
idempotencyKey: string;
|
||||
createdAt: string;
|
||||
referenceId?: string;
|
||||
bucketId?: string;
|
||||
taskId?: string;
|
||||
outputId?: string;
|
||||
expiresAt?: string;
|
||||
status?: "posted" | "voided";
|
||||
};
|
||||
|
||||
export type BalanceBucket = OrganizationScope & {
|
||||
id: string;
|
||||
userId: string;
|
||||
source: "recharge" | "plan" | "manual" | "bonus";
|
||||
remaining: number;
|
||||
expiresAt?: string;
|
||||
priority: number;
|
||||
};
|
||||
|
||||
export type BalanceUnitConfig = {
|
||||
name: string;
|
||||
shortCode: string;
|
||||
icon: string;
|
||||
precision: number;
|
||||
version: number;
|
||||
currencyCode: string;
|
||||
conversionRate: number;
|
||||
rounding: "floor" | "ceil" | "round";
|
||||
};
|
||||
|
||||
export type MembershipPlan = {
|
||||
id: string;
|
||||
code: string;
|
||||
name: string;
|
||||
icon?: string;
|
||||
description: string;
|
||||
durationDays: number;
|
||||
purchaseAmount: number;
|
||||
includedBalance: number;
|
||||
published: boolean;
|
||||
allowedModelProductIds: string[];
|
||||
allowedChannelGroupIds: string[];
|
||||
maxConcurrent: number;
|
||||
queuePriority: number;
|
||||
retentionDays: number;
|
||||
maxRetentionExtensionDays: number;
|
||||
version: number;
|
||||
};
|
||||
|
||||
export type TaskType = "image" | "text" | "audio" | "reverse-prompt" | "multi-angle" | "matting" | "split" | "upscale" | "expand" | "3d";
|
||||
export type TaskStatus = "queued" | "running" | "succeeded" | "partial" | "failed" | "unknown" | "canceled";
|
||||
|
||||
export type CreateTaskRequest = OrganizationScope & {
|
||||
modelProductId: string;
|
||||
resolutionPresetId?: string;
|
||||
prompt?: string;
|
||||
references?: string[];
|
||||
referenceGroup?: ReferenceImageGroup;
|
||||
maskObjectId?: string;
|
||||
count?: number;
|
||||
canvasProjectId?: string;
|
||||
params?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type GenerationTask = OrganizationScope & {
|
||||
id: string;
|
||||
ownerId: string;
|
||||
taskType: TaskType;
|
||||
modelProductId: string;
|
||||
status: TaskStatus;
|
||||
estimatedCost: number;
|
||||
reservedCost: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
eventSequence: number;
|
||||
channelGroupId: string;
|
||||
routeSnapshotVersion: number;
|
||||
attempts: GenerationAttempt[];
|
||||
outputs: Array<{ id: string; mimeType: string; url?: string; status: "succeeded" | "failed"; unitPriceSnapshot?: number; chargedAmount?: number; metadata?: ImageOutputMetadata }>;
|
||||
publicErrorCode?: ErrorCode;
|
||||
prompt?: string;
|
||||
resolutionPresetId?: string;
|
||||
count?: number;
|
||||
pricingSnapshot?: { basePrice: number; multiplier: number; unitVersion: number };
|
||||
modelSnapshot?: { publicModelId: string; version: number };
|
||||
routeSnapshot?: { groupId: string; version: number; channelIds: string[]; retryBudget?: number; channelRetryBudgets?: Record<string, number> };
|
||||
planSnapshot?: { planId?: string; version?: number; queuePriority: number; maxConcurrent: number };
|
||||
leaseExpiresAt?: string;
|
||||
/** Fences a late worker/provider response after a lease is recovered. */
|
||||
leaseToken?: string;
|
||||
providerSubmitted?: boolean;
|
||||
idempotencyKey?: string;
|
||||
requestFingerprint?: string;
|
||||
retentionExpiresAt?: string;
|
||||
retentionState?: "active" | "archiving" | "archive_failed" | "archived" | "deleted";
|
||||
retentionExtensionDays?: number;
|
||||
reserveExpiresAt?: string;
|
||||
inputSnapshot?: { references: string[]; referenceGroup?: ReferenceImageGroup; maskObjectId?: string; params: Record<string, unknown>; originalPrompt?: string };
|
||||
};
|
||||
|
||||
export type GenerationAttempt = {
|
||||
id: string;
|
||||
channelId: string;
|
||||
sequence: number;
|
||||
status: "started" | "succeeded" | "failed" | "unknown";
|
||||
errorCode?: string;
|
||||
providerRequestId?: string;
|
||||
platformIdempotencyKey?: string;
|
||||
startedAt: string;
|
||||
finishedAt?: string;
|
||||
leaseExpiresAt?: string;
|
||||
retryable?: boolean;
|
||||
reconciliationStatus?: "pending" | "confirmed_success" | "confirmed_failure" | "manual_review";
|
||||
};
|
||||
|
||||
export type PublicGenerationAttempt = {
|
||||
id: string;
|
||||
sequence: number;
|
||||
status: GenerationAttempt["status"];
|
||||
startedAt: string;
|
||||
finishedAt?: string;
|
||||
retryable?: boolean;
|
||||
reconciliationStatus?: GenerationAttempt["reconciliationStatus"];
|
||||
};
|
||||
|
||||
export type PublicGenerationTask = {
|
||||
id: string;
|
||||
taskType: TaskType;
|
||||
modelProductId: string;
|
||||
status: TaskStatus;
|
||||
estimatedCost: number;
|
||||
reservedCost: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
eventSequence: number;
|
||||
publicErrorCode?: ErrorCode;
|
||||
prompt?: string;
|
||||
resolutionPresetId?: string;
|
||||
count?: number;
|
||||
modelSnapshot?: GenerationTask["modelSnapshot"];
|
||||
retentionExpiresAt?: string;
|
||||
retentionState?: GenerationTask["retentionState"];
|
||||
retentionExtensionDays?: number;
|
||||
inputSnapshot?: GenerationTask["inputSnapshot"];
|
||||
attempts: PublicGenerationAttempt[];
|
||||
outputs: GenerationTask["outputs"];
|
||||
};
|
||||
|
||||
export type TaskEvent = {
|
||||
eventId: string;
|
||||
sequence: number;
|
||||
taskId: string;
|
||||
attemptId?: string;
|
||||
type: "task.queued" | "task.running" | "task.attempt" | "task.progress" | "task.succeeded" | "task.failed" | "task.canceled" | "task.unknown";
|
||||
occurredAt: string;
|
||||
payload: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type PaymentWebhookRequest = { orderId: string; eventId: string; status: "paid" | "failed" | "refunded"; amount?: number; currency?: string };
|
||||
|
||||
export type UploadObject = OrganizationScope & {
|
||||
id: string;
|
||||
idempotencyKey?: string;
|
||||
ownerId: string;
|
||||
mimeType: string;
|
||||
size: number;
|
||||
checksum: string;
|
||||
status: "pending" | "scanning" | "ready" | "rejected" | "deleted";
|
||||
stagingKey?: string;
|
||||
expiresAt: string;
|
||||
};
|
||||
|
||||
export type AssetRecord = OrganizationScope & {
|
||||
id: string;
|
||||
ownerId: string;
|
||||
type: "image" | "text" | "audio" | "archive" | "3d";
|
||||
objectId?: string;
|
||||
taskId?: string;
|
||||
sourceOutputId?: string;
|
||||
name: string;
|
||||
tags: string[];
|
||||
deletedAt?: string;
|
||||
retentionExpiresAt?: string;
|
||||
pinned?: boolean;
|
||||
objectUrl?: string;
|
||||
mimeType?: string;
|
||||
};
|
||||
|
||||
export type WebDavConfigStatus = {
|
||||
configured: boolean;
|
||||
url?: string;
|
||||
username?: string;
|
||||
directory?: string;
|
||||
lastSyncedAt?: string;
|
||||
state: "unconfigured" | "ready" | "syncing" | "error";
|
||||
};
|
||||
|
||||
export type WsClientMessage =
|
||||
| { type: "subscribe"; taskId: string; cursor?: number }
|
||||
| { type: "unsubscribe"; taskId: string }
|
||||
| { type: "ping" };
|
||||
|
||||
export type WsServerMessage =
|
||||
| { type: "ready"; contractVersion: string; heartbeatIntervalMs: number }
|
||||
| { type: "pong"; at: string }
|
||||
| { type: "event"; event: TaskEvent }
|
||||
| { type: "error"; error: ApiError };
|
||||
|
||||
export type HealthResponse = {
|
||||
status: "ok";
|
||||
service: "miragenflow-server";
|
||||
contractVersion: string;
|
||||
now: string;
|
||||
dependencies?: { persistence: { adapter: string; status: string; detail?: string }; queue: { adapter: string; status: string; detail?: string } };
|
||||
};
|
||||
|
||||
export type AdminRole = "super_admin" | "operator" | "finance" | "support" | "auditor";
|
||||
|
||||
export type AdminScope = "admin:read" | "admin:write" | "admin:finance" | "admin:security" | "admin:audit";
|
||||
|
||||
export type AdminUser = {
|
||||
id: string;
|
||||
email: string;
|
||||
roles: AdminRole[];
|
||||
scopes: AdminScope[];
|
||||
mfaRequired: boolean;
|
||||
};
|
||||
|
||||
export type AdminSessionResponse = {
|
||||
admin: AdminUser;
|
||||
accessToken?: string;
|
||||
accessTokenExpiresAt?: string;
|
||||
mfaRequired?: boolean;
|
||||
challengeId?: string;
|
||||
};
|
||||
|
||||
export type AdminPage<T> = {
|
||||
items: T[];
|
||||
total?: number;
|
||||
nextCursor?: string;
|
||||
};
|
||||
|
||||
export type AdminChannelSummary = {
|
||||
id: string;
|
||||
label: string;
|
||||
enabled: boolean;
|
||||
priority: number;
|
||||
health: "healthy" | "degraded" | "open";
|
||||
version: number;
|
||||
retryBudget: number;
|
||||
consecutiveFailures: number;
|
||||
providerType?: "generic" | "openai-images";
|
||||
providerName?: string;
|
||||
providerModelId?: string;
|
||||
enabledModelIds?: string[];
|
||||
modelMappings?: Array<{ displayModelId: string; requestModelId: string }>;
|
||||
resolutionModelMap?: Record<string, string>;
|
||||
baseUrl?: string;
|
||||
secretConfigured?: boolean;
|
||||
};
|
||||
|
||||
export type AdminTaskSummary = {
|
||||
id: string;
|
||||
ownerId: string;
|
||||
taskType: TaskType;
|
||||
modelProductId: string;
|
||||
status: TaskStatus;
|
||||
estimatedCost: number;
|
||||
reservedCost: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
attemptCount: number;
|
||||
outputCount: number;
|
||||
publicErrorCode?: ErrorCode;
|
||||
};
|
||||
|
||||
export type AdminAuditEntry = {
|
||||
id: string;
|
||||
actorId?: string;
|
||||
action: string;
|
||||
category?: string;
|
||||
actionLabel?: string;
|
||||
actorName?: string;
|
||||
actorType?: "admin" | "user" | "system" | "payment" | "unknown";
|
||||
objectType: string;
|
||||
objectId?: string;
|
||||
requestId: string;
|
||||
createdAt: string;
|
||||
statusCode?: number;
|
||||
level?: "normal" | "error";
|
||||
before?: unknown;
|
||||
after?: unknown;
|
||||
};
|
||||
Reference in New Issue
Block a user