@lumenkey/cli 0.3.0 → 0.4.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -2
- package/dist/cli/copied.js +86 -85
- package/dist/cli/index.js +56 -44
- package/dist/cli/render.js +5 -1
- package/dist/core/asset-stream.d.ts +16 -0
- package/dist/core/asset-stream.js +64 -0
- package/dist/core/atomic-config-file.js +8 -7
- package/dist/core/chinese-messages.d.ts +4 -0
- package/dist/core/chinese-messages.js +907 -0
- package/dist/core/chinese-script.d.ts +7 -0
- package/dist/core/chinese-script.js +28 -0
- package/dist/core/client.js +2 -1
- package/dist/core/config-dir.js +2 -1
- package/dist/core/config.js +7 -6
- package/dist/core/download-root.d.ts +5 -0
- package/dist/core/download-root.js +64 -0
- package/dist/core/download.js +15 -12
- package/dist/core/generation-input.js +3 -2
- package/dist/core/generation.d.ts +8 -1
- package/dist/core/generation.js +13 -10
- package/dist/core/job-display.js +7 -6
- package/dist/core/locale.d.ts +2 -0
- package/dist/core/locale.js +172 -142
- package/dist/core/outcome.d.ts +6 -0
- package/dist/core/outcome.js +17 -4
- package/dist/core/output-path.d.ts +3 -1
- package/dist/core/output-path.js +5 -2
- package/dist/core/params.js +26 -25
- package/dist/core/profiles.js +5 -4
- package/dist/core/set-values.d.ts +0 -1
- package/dist/core/set-values.js +7 -7
- package/dist/core/submission-journal.js +43 -17
- package/dist/core/version.js +2 -3
- package/dist/mcp/server.js +30 -29
- package/dist/studio/asset-integrity-hash.d.ts +28 -0
- package/dist/studio/asset-integrity-hash.js +93 -0
- package/dist/studio/asset-integrity-queue.d.ts +24 -0
- package/dist/studio/asset-integrity-queue.js +94 -0
- package/dist/studio/asset-store.d.ts +52 -0
- package/dist/studio/asset-store.js +352 -0
- package/dist/studio/canvas/LICENSES.txt +1590 -0
- package/dist/studio/canvas/dependencies.json +862 -0
- package/dist/studio/canvas/director-source-manifest.json +47 -0
- package/dist/studio/canvas/editor.css +239 -0
- package/dist/studio/canvas/editor.js +4613 -0
- package/dist/studio/canvas/integration-source-manifest.json +384 -0
- package/dist/studio/canvas/source-manifest.json +240 -0
- package/dist/studio/canvas-store.d.ts +1254 -0
- package/dist/studio/canvas-store.js +205 -0
- package/dist/studio/capture-placement.d.ts +11 -0
- package/dist/studio/capture-placement.js +85 -0
- package/dist/studio/cloud-contract.d.ts +128 -0
- package/dist/studio/cloud-contract.js +229 -0
- package/dist/studio/cloud-service.d.ts +178 -0
- package/dist/studio/cloud-service.js +202 -0
- package/dist/studio/director-captures.d.ts +59 -0
- package/dist/studio/director-captures.js +141 -0
- package/dist/studio/director-contract.d.ts +1489 -0
- package/dist/studio/director-contract.js +288 -0
- package/dist/studio/director-png.d.ts +6 -0
- package/dist/studio/director-png.js +41 -0
- package/dist/studio/director-relations.d.ts +40 -0
- package/dist/studio/director-relations.js +103 -0
- package/dist/studio/document-authority.d.ts +7 -0
- package/dist/studio/document-authority.js +50 -0
- package/dist/studio/draft-store.d.ts +95 -0
- package/dist/studio/draft-store.js +100 -0
- package/dist/studio/errors.d.ts +85 -0
- package/dist/studio/errors.js +94 -0
- package/dist/studio/launch.d.ts +5 -0
- package/dist/studio/launch.js +31 -0
- package/dist/studio/media.d.ts +9 -0
- package/dist/studio/media.js +50 -0
- package/dist/studio/output-save-contract.d.ts +1321 -0
- package/dist/studio/output-save-contract.js +26 -0
- package/dist/studio/output-save-journal.d.ts +971 -0
- package/dist/studio/output-save-journal.js +287 -0
- package/dist/studio/output-save-recovery.d.ts +18 -0
- package/dist/studio/output-save-recovery.js +161 -0
- package/dist/studio/output-save-service.d.ts +436 -0
- package/dist/studio/output-save-service.js +251 -0
- package/dist/studio/output-save-worker.d.ts +40 -0
- package/dist/studio/output-save-worker.js +360 -0
- package/dist/studio/paths.d.ts +6 -0
- package/dist/studio/paths.js +68 -0
- package/dist/studio/run-journal.d.ts +993 -0
- package/dist/studio/run-journal.js +125 -0
- package/dist/studio/server.d.ts +17 -0
- package/dist/studio/server.js +440 -0
- package/dist/studio/session.d.ts +6 -0
- package/dist/studio/session.js +26 -0
- package/dist/studio/shell.d.ts +3 -0
- package/dist/studio/shell.js +23 -0
- package/dist/studio/style.d.ts +2 -0
- package/dist/studio/style.js +29 -0
- package/dist/studio/types.d.ts +26 -0
- package/dist/studio/types.js +2 -0
- package/dist/studio/web/app.d.ts +2 -0
- package/dist/studio/web/app.js +662 -0
- package/dist/studio/web/asset-controller.d.ts +37 -0
- package/dist/studio/web/asset-controller.js +191 -0
- package/dist/studio/web/asset-model.d.ts +29 -0
- package/dist/studio/web/asset-model.js +51 -0
- package/dist/studio/web/controller.d.ts +113 -0
- package/dist/studio/web/controller.js +401 -0
- package/dist/studio/web/messages.d.ts +2 -0
- package/dist/studio/web/messages.js +74 -0
- package/dist/studio/web/model.d.ts +75 -0
- package/dist/studio/web/model.js +182 -0
- package/dist/studio/web/save-controller.d.ts +85 -0
- package/dist/studio/web/save-controller.js +495 -0
- package/dist/studio/web/save-model.d.ts +63 -0
- package/dist/studio/web/save-model.js +64 -0
- package/dist/studio/web/save-queue.d.ts +7 -0
- package/dist/studio/web/save-queue.js +62 -0
- package/dist/studio/web/save-view.d.ts +9 -0
- package/dist/studio/web/save-view.js +19 -0
- package/dist/studio/writer-lock.d.ts +5 -0
- package/dist/studio/writer-lock.js +88 -0
- package/package.json +11 -5
- package/dist/cli/copied.d.ts.map +0 -1
- package/dist/cli/copied.js.map +0 -1
- package/dist/cli/index.d.ts.map +0 -1
- package/dist/cli/index.js.map +0 -1
- package/dist/cli/render.d.ts.map +0 -1
- package/dist/cli/render.js.map +0 -1
- package/dist/core/atomic-config-file.d.ts.map +0 -1
- package/dist/core/atomic-config-file.js.map +0 -1
- package/dist/core/chat.d.ts.map +0 -1
- package/dist/core/chat.js.map +0 -1
- package/dist/core/client.d.ts.map +0 -1
- package/dist/core/client.js.map +0 -1
- package/dist/core/config-dir.d.ts.map +0 -1
- package/dist/core/config-dir.js.map +0 -1
- package/dist/core/config-error.d.ts.map +0 -1
- package/dist/core/config-error.js.map +0 -1
- package/dist/core/config.d.ts.map +0 -1
- package/dist/core/config.js.map +0 -1
- package/dist/core/credit-cap.d.ts.map +0 -1
- package/dist/core/credit-cap.js.map +0 -1
- package/dist/core/download.d.ts.map +0 -1
- package/dist/core/download.js.map +0 -1
- package/dist/core/error-result.d.ts.map +0 -1
- package/dist/core/error-result.js.map +0 -1
- package/dist/core/explain.d.ts.map +0 -1
- package/dist/core/explain.js.map +0 -1
- package/dist/core/generation-input.d.ts.map +0 -1
- package/dist/core/generation-input.js.map +0 -1
- package/dist/core/generation-result.d.ts.map +0 -1
- package/dist/core/generation-result.js.map +0 -1
- package/dist/core/generation.d.ts.map +0 -1
- package/dist/core/generation.js.map +0 -1
- package/dist/core/job-display.d.ts.map +0 -1
- package/dist/core/job-display.js.map +0 -1
- package/dist/core/locale.d.ts.map +0 -1
- package/dist/core/locale.js.map +0 -1
- package/dist/core/origin.d.ts.map +0 -1
- package/dist/core/origin.js.map +0 -1
- package/dist/core/outcome.d.ts.map +0 -1
- package/dist/core/outcome.js.map +0 -1
- package/dist/core/output-path.d.ts.map +0 -1
- package/dist/core/output-path.js.map +0 -1
- package/dist/core/params.d.ts.map +0 -1
- package/dist/core/params.js.map +0 -1
- package/dist/core/poller.d.ts.map +0 -1
- package/dist/core/poller.js.map +0 -1
- package/dist/core/profiles.d.ts.map +0 -1
- package/dist/core/profiles.js.map +0 -1
- package/dist/core/set-values.d.ts.map +0 -1
- package/dist/core/set-values.js.map +0 -1
- package/dist/core/submission-journal.d.ts.map +0 -1
- package/dist/core/submission-journal.js.map +0 -1
- package/dist/core/types.d.ts.map +0 -1
- package/dist/core/types.js.map +0 -1
- package/dist/core/version.d.ts.map +0 -1
- package/dist/core/version.js.map +0 -1
- package/dist/core/wait.d.ts.map +0 -1
- package/dist/core/wait.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/mcp/render.d.ts.map +0 -1
- package/dist/mcp/render.js.map +0 -1
- package/dist/mcp/server.d.ts.map +0 -1
- package/dist/mcp/server.js.map +0 -1
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import type { LumenKeyConfig } from '../core/types.js';
|
|
2
|
+
import type { StudioAssetStore } from './asset-store.js';
|
|
3
|
+
export declare function studioBindingDigest(origin: string, key: string): string;
|
|
4
|
+
export declare function createStudioCloudService(options: {
|
|
5
|
+
store?: StudioAssetStore;
|
|
6
|
+
loadConfig: () => LumenKeyConfig | Promise<LumenKeyConfig>;
|
|
7
|
+
}): {
|
|
8
|
+
status: () => {
|
|
9
|
+
gateway?: string | undefined;
|
|
10
|
+
connected: boolean;
|
|
11
|
+
};
|
|
12
|
+
connect: () => Promise<{
|
|
13
|
+
connected: boolean;
|
|
14
|
+
gateway: string;
|
|
15
|
+
}>;
|
|
16
|
+
disconnect: () => Promise<{
|
|
17
|
+
connected: boolean;
|
|
18
|
+
}>;
|
|
19
|
+
models: () => Promise<import("../core/types.js").ModelItem[]>;
|
|
20
|
+
balance: () => Promise<{
|
|
21
|
+
availableCredits: number;
|
|
22
|
+
reservedCredits: number;
|
|
23
|
+
updatedAt: string;
|
|
24
|
+
}>;
|
|
25
|
+
list: () => Promise<{
|
|
26
|
+
createdAt: string;
|
|
27
|
+
updatedAt: string;
|
|
28
|
+
failure?: {
|
|
29
|
+
code: string;
|
|
30
|
+
message: string;
|
|
31
|
+
type: string;
|
|
32
|
+
httpStatus?: number | undefined;
|
|
33
|
+
submission?: "created" | "none" | "unknown" | undefined;
|
|
34
|
+
recovery?: "expired" | "rerun" | "resume-command" | "ledger" | undefined;
|
|
35
|
+
resumeDeadline?: string | undefined;
|
|
36
|
+
} | undefined;
|
|
37
|
+
lastErrorCode?: string | undefined;
|
|
38
|
+
job?: {
|
|
39
|
+
status: "accepted" | "policy_checking" | "queued" | "submitting" | "provider_running" | "output_checking" | "succeeded" | "rejected" | "failed" | "cancelled" | "expired";
|
|
40
|
+
jobId: string;
|
|
41
|
+
outputs: {
|
|
42
|
+
url: string;
|
|
43
|
+
type?: string | undefined;
|
|
44
|
+
assetId?: string | undefined;
|
|
45
|
+
outputIndex?: number | undefined;
|
|
46
|
+
}[];
|
|
47
|
+
error?: {
|
|
48
|
+
code: string;
|
|
49
|
+
message: string;
|
|
50
|
+
} | undefined;
|
|
51
|
+
identityReason?: "LEGACY_AWAITING_FRESH_GET" | "JOB_UNAVAILABLE" | "NO_VERIFIABLE_OUTPUTS" | "IDENTITY_CONFLICT" | undefined;
|
|
52
|
+
} | undefined;
|
|
53
|
+
runId: string;
|
|
54
|
+
modelId: string;
|
|
55
|
+
state: "QUOTED" | "SUBMISSION_UNKNOWN" | "SUBMISSION_REJECTED" | "ACCEPTED" | "TERMINAL";
|
|
56
|
+
quote: {
|
|
57
|
+
credits: number;
|
|
58
|
+
quoteId: string;
|
|
59
|
+
modelId: string;
|
|
60
|
+
expiresAt: string;
|
|
61
|
+
};
|
|
62
|
+
}[]>;
|
|
63
|
+
quote: (value: unknown) => Promise<{
|
|
64
|
+
createdAt: string;
|
|
65
|
+
updatedAt: string;
|
|
66
|
+
failure?: {
|
|
67
|
+
code: string;
|
|
68
|
+
message: string;
|
|
69
|
+
type: string;
|
|
70
|
+
httpStatus?: number | undefined;
|
|
71
|
+
submission?: "created" | "none" | "unknown" | undefined;
|
|
72
|
+
recovery?: "expired" | "rerun" | "resume-command" | "ledger" | undefined;
|
|
73
|
+
resumeDeadline?: string | undefined;
|
|
74
|
+
} | undefined;
|
|
75
|
+
lastErrorCode?: string | undefined;
|
|
76
|
+
job?: {
|
|
77
|
+
status: "accepted" | "policy_checking" | "queued" | "submitting" | "provider_running" | "output_checking" | "succeeded" | "rejected" | "failed" | "cancelled" | "expired";
|
|
78
|
+
jobId: string;
|
|
79
|
+
outputs: {
|
|
80
|
+
url: string;
|
|
81
|
+
type?: string | undefined;
|
|
82
|
+
assetId?: string | undefined;
|
|
83
|
+
outputIndex?: number | undefined;
|
|
84
|
+
}[];
|
|
85
|
+
error?: {
|
|
86
|
+
code: string;
|
|
87
|
+
message: string;
|
|
88
|
+
} | undefined;
|
|
89
|
+
identityReason?: "LEGACY_AWAITING_FRESH_GET" | "JOB_UNAVAILABLE" | "NO_VERIFIABLE_OUTPUTS" | "IDENTITY_CONFLICT" | undefined;
|
|
90
|
+
} | undefined;
|
|
91
|
+
runId: string;
|
|
92
|
+
modelId: string;
|
|
93
|
+
state: "QUOTED" | "SUBMISSION_UNKNOWN" | "SUBMISSION_REJECTED" | "ACCEPTED" | "TERMINAL";
|
|
94
|
+
quote: {
|
|
95
|
+
credits: number;
|
|
96
|
+
quoteId: string;
|
|
97
|
+
modelId: string;
|
|
98
|
+
expiresAt: string;
|
|
99
|
+
};
|
|
100
|
+
}>;
|
|
101
|
+
confirm: (runId: string, confirm: unknown) => Promise<{
|
|
102
|
+
createdAt: string;
|
|
103
|
+
updatedAt: string;
|
|
104
|
+
failure?: {
|
|
105
|
+
code: string;
|
|
106
|
+
message: string;
|
|
107
|
+
type: string;
|
|
108
|
+
httpStatus?: number | undefined;
|
|
109
|
+
submission?: "created" | "none" | "unknown" | undefined;
|
|
110
|
+
recovery?: "expired" | "rerun" | "resume-command" | "ledger" | undefined;
|
|
111
|
+
resumeDeadline?: string | undefined;
|
|
112
|
+
} | undefined;
|
|
113
|
+
lastErrorCode?: string | undefined;
|
|
114
|
+
job?: {
|
|
115
|
+
status: "accepted" | "policy_checking" | "queued" | "submitting" | "provider_running" | "output_checking" | "succeeded" | "rejected" | "failed" | "cancelled" | "expired";
|
|
116
|
+
jobId: string;
|
|
117
|
+
outputs: {
|
|
118
|
+
url: string;
|
|
119
|
+
type?: string | undefined;
|
|
120
|
+
assetId?: string | undefined;
|
|
121
|
+
outputIndex?: number | undefined;
|
|
122
|
+
}[];
|
|
123
|
+
error?: {
|
|
124
|
+
code: string;
|
|
125
|
+
message: string;
|
|
126
|
+
} | undefined;
|
|
127
|
+
identityReason?: "LEGACY_AWAITING_FRESH_GET" | "JOB_UNAVAILABLE" | "NO_VERIFIABLE_OUTPUTS" | "IDENTITY_CONFLICT" | undefined;
|
|
128
|
+
} | undefined;
|
|
129
|
+
runId: string;
|
|
130
|
+
modelId: string;
|
|
131
|
+
state: "QUOTED" | "SUBMISSION_UNKNOWN" | "SUBMISSION_REJECTED" | "ACCEPTED" | "TERMINAL";
|
|
132
|
+
quote: {
|
|
133
|
+
credits: number;
|
|
134
|
+
quoteId: string;
|
|
135
|
+
modelId: string;
|
|
136
|
+
expiresAt: string;
|
|
137
|
+
};
|
|
138
|
+
}>;
|
|
139
|
+
refresh: (runId: string) => Promise<{
|
|
140
|
+
createdAt: string;
|
|
141
|
+
updatedAt: string;
|
|
142
|
+
failure?: {
|
|
143
|
+
code: string;
|
|
144
|
+
message: string;
|
|
145
|
+
type: string;
|
|
146
|
+
httpStatus?: number | undefined;
|
|
147
|
+
submission?: "created" | "none" | "unknown" | undefined;
|
|
148
|
+
recovery?: "expired" | "rerun" | "resume-command" | "ledger" | undefined;
|
|
149
|
+
resumeDeadline?: string | undefined;
|
|
150
|
+
} | undefined;
|
|
151
|
+
lastErrorCode?: string | undefined;
|
|
152
|
+
job?: {
|
|
153
|
+
status: "accepted" | "policy_checking" | "queued" | "submitting" | "provider_running" | "output_checking" | "succeeded" | "rejected" | "failed" | "cancelled" | "expired";
|
|
154
|
+
jobId: string;
|
|
155
|
+
outputs: {
|
|
156
|
+
url: string;
|
|
157
|
+
type?: string | undefined;
|
|
158
|
+
assetId?: string | undefined;
|
|
159
|
+
outputIndex?: number | undefined;
|
|
160
|
+
}[];
|
|
161
|
+
error?: {
|
|
162
|
+
code: string;
|
|
163
|
+
message: string;
|
|
164
|
+
} | undefined;
|
|
165
|
+
identityReason?: "LEGACY_AWAITING_FRESH_GET" | "JOB_UNAVAILABLE" | "NO_VERIFIABLE_OUTPUTS" | "IDENTITY_CONFLICT" | undefined;
|
|
166
|
+
} | undefined;
|
|
167
|
+
runId: string;
|
|
168
|
+
modelId: string;
|
|
169
|
+
state: "QUOTED" | "SUBMISSION_UNKNOWN" | "SUBMISSION_REJECTED" | "ACCEPTED" | "TERMINAL";
|
|
170
|
+
quote: {
|
|
171
|
+
credits: number;
|
|
172
|
+
quoteId: string;
|
|
173
|
+
modelId: string;
|
|
174
|
+
expiresAt: string;
|
|
175
|
+
};
|
|
176
|
+
}>;
|
|
177
|
+
};
|
|
178
|
+
//# sourceMappingURL=cloud-service.d.ts.map
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { LumenKeyClient, LumenKeyError } from '../core/client.js';
|
|
3
|
+
import { ConfigError } from '../core/config-error.js';
|
|
4
|
+
import { buildGenerationInput } from '../core/generation-input.js';
|
|
5
|
+
import { classifyCreateFailure } from '../core/generation.js';
|
|
6
|
+
import { normalizeGatewayOrigin } from '../core/origin.js';
|
|
7
|
+
import { StudioError } from './errors.js';
|
|
8
|
+
import { assertNoCredential, cloudDiagnostic, projectJob, projectModels, publicRun, quoteRequest, safeCloudCode, safeCloudFailure, validateInput } from './cloud-contract.js';
|
|
9
|
+
import { isoDate, quoteSchema, uuid } from './run-journal.js';
|
|
10
|
+
export function studioBindingDigest(origin, key) { return createHash('sha256').update(JSON.stringify([origin, key.trim()])).digest('hex'); }
|
|
11
|
+
const terminal = new Set(['succeeded', 'failed', 'rejected', 'cancelled', 'expired']);
|
|
12
|
+
function canonical(value) {
|
|
13
|
+
if (Array.isArray(value))
|
|
14
|
+
return `[${value.map(canonical).join(',')}]`;
|
|
15
|
+
if (value && typeof value === 'object')
|
|
16
|
+
return `{${Object.keys(value).sort().map(k => `${JSON.stringify(k)}:${canonical(value[k])}`).join(',')}}`;
|
|
17
|
+
return JSON.stringify(value);
|
|
18
|
+
}
|
|
19
|
+
export function createStudioCloudService(options) {
|
|
20
|
+
let connection;
|
|
21
|
+
let queue = Promise.resolve();
|
|
22
|
+
function serial(fn) {
|
|
23
|
+
const next = queue.then(async () => {
|
|
24
|
+
try {
|
|
25
|
+
const result = await fn();
|
|
26
|
+
if (connection)
|
|
27
|
+
assertNoCredential(result, connection.secret);
|
|
28
|
+
return result;
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
throw safeCloudFailure(error, connection?.secret);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
queue = next.catch(() => { });
|
|
35
|
+
return next;
|
|
36
|
+
}
|
|
37
|
+
function connected() { if (!connection)
|
|
38
|
+
throw new StudioError('STUDIO_CLOUD_DISCONNECTED', 409); return connection; }
|
|
39
|
+
function journal() { if (!options.store)
|
|
40
|
+
throw new StudioError('STUDIO_OUTPUT_ROOT_REQUIRED', 409); return options.store.runs; }
|
|
41
|
+
function find(data, runId, c) {
|
|
42
|
+
if (!uuid.safeParse(runId).success)
|
|
43
|
+
throw new StudioError('STUDIO_INPUT_INVALID');
|
|
44
|
+
const run = data.runs.find(r => r.runId === runId && r.binding === c.binding);
|
|
45
|
+
if (!run)
|
|
46
|
+
throw new StudioError('STUDIO_RUN_NOT_FOUND', 404);
|
|
47
|
+
return run;
|
|
48
|
+
}
|
|
49
|
+
async function persist(data, run) {
|
|
50
|
+
await journal().write({ ...data, revision: data.revision + 1, runs: data.runs.map(r => r.runId === run.runId ? run : r) });
|
|
51
|
+
}
|
|
52
|
+
async function models(c) { return projectModels(await c.client.listModels(), c.secret); }
|
|
53
|
+
async function balance(c) {
|
|
54
|
+
const b = await c.client.getBalance();
|
|
55
|
+
const projected = { availableCredits: b.availableCredits, reservedCredits: b.reservedCredits, updatedAt: b.updatedAt };
|
|
56
|
+
if (!isoDate.safeParse(b.updatedAt).success)
|
|
57
|
+
throw new StudioError('STUDIO_CLOUD_RESPONSE_INVALID', 502);
|
|
58
|
+
assertNoCredential(projected, c.secret);
|
|
59
|
+
return projected;
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
status: () => ({ connected: !!connection, ...(connection ? { gateway: connection.origin } : {}) }),
|
|
63
|
+
connect: () => serial(async () => {
|
|
64
|
+
connection = undefined;
|
|
65
|
+
let config;
|
|
66
|
+
let client;
|
|
67
|
+
try {
|
|
68
|
+
config = { ...await options.loadConfig() };
|
|
69
|
+
config.apiKey = typeof config.apiKey === 'string' ? config.apiKey.trim() : undefined;
|
|
70
|
+
config.baseUrl = normalizeGatewayOrigin(config.baseUrl);
|
|
71
|
+
config.projectId = config.projectId?.trim().toLowerCase() || undefined;
|
|
72
|
+
client = new LumenKeyClient(config);
|
|
73
|
+
client.requireKey();
|
|
74
|
+
if (config.projectId && !uuid.safeParse(config.projectId).success)
|
|
75
|
+
throw new Error();
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
if (error instanceof ConfigError)
|
|
79
|
+
throw new LumenKeyError(error.code, 409);
|
|
80
|
+
throw new StudioError('STUDIO_CLOUD_CONFIG_INVALID', 409);
|
|
81
|
+
}
|
|
82
|
+
const origin = new URL(config.baseUrl).origin;
|
|
83
|
+
// Project selection does not change who pays: unresolved submission gate is credential-scoped.
|
|
84
|
+
const candidate = { client, origin, projectId: config.projectId, secret: config.apiKey, binding: studioBindingDigest(origin, config.apiKey) };
|
|
85
|
+
try {
|
|
86
|
+
await balance(candidate);
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
throw safeCloudFailure(error, candidate.secret);
|
|
90
|
+
}
|
|
91
|
+
connection = candidate;
|
|
92
|
+
return { connected: true, gateway: origin };
|
|
93
|
+
}),
|
|
94
|
+
disconnect: () => serial(async () => { connection = undefined; return { connected: false }; }),
|
|
95
|
+
models: () => serial(async () => models(connected())),
|
|
96
|
+
balance: () => serial(async () => balance(connected())),
|
|
97
|
+
list: () => serial(async () => { const c = connected(); return (await journal().read()).runs.filter(r => r.binding === c.binding).map(publicRun); }),
|
|
98
|
+
quote: (value) => serial(async () => {
|
|
99
|
+
const c = connected();
|
|
100
|
+
const parsed = quoteRequest.safeParse(value);
|
|
101
|
+
if (!parsed.success)
|
|
102
|
+
throw new StudioError('STUDIO_INPUT_INVALID');
|
|
103
|
+
const req = parsed.data;
|
|
104
|
+
assertNoCredential(req, c.secret);
|
|
105
|
+
const data = await journal().read();
|
|
106
|
+
const existing = data.runs.find(r => r.runId === req.requestId);
|
|
107
|
+
if (existing) {
|
|
108
|
+
if (existing.binding !== c.binding || existing.modelId !== req.modelId || canonical(existing.requestedInput ?? existing.input) !== canonical(req.input))
|
|
109
|
+
throw new StudioError('STUDIO_RUN_CONFLICT', 409);
|
|
110
|
+
return publicRun(existing);
|
|
111
|
+
}
|
|
112
|
+
if (data.runs.length >= 1000)
|
|
113
|
+
throw new StudioError('STUDIO_JOURNAL_INVALID', 409);
|
|
114
|
+
const model = (await models(c)).find(m => m.id === req.modelId);
|
|
115
|
+
if (!model)
|
|
116
|
+
throw new StudioError('STUDIO_MODEL_NOT_AVAILABLE', 400);
|
|
117
|
+
let input;
|
|
118
|
+
try {
|
|
119
|
+
input = (await buildGenerationInput(c.client, req.modelId, req.input, [], model)).input;
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
if (error instanceof LumenKeyError)
|
|
123
|
+
throw safeCloudFailure(new LumenKeyError(safeCloudCode(error, c.secret), error.status ?? 400), c.secret, { submission: 'none' });
|
|
124
|
+
throw error;
|
|
125
|
+
}
|
|
126
|
+
validateInput(model, input);
|
|
127
|
+
assertNoCredential(input, c.secret);
|
|
128
|
+
const result = quoteSchema.safeParse(await c.client.getQuote({ modelId: req.modelId, input }));
|
|
129
|
+
if (!result.success || Date.parse(result.data.expiresAt) <= Date.now())
|
|
130
|
+
throw new StudioError('STUDIO_CLOUD_RESPONSE_INVALID', 502);
|
|
131
|
+
assertNoCredential(result.data, c.secret);
|
|
132
|
+
const now = new Date().toISOString();
|
|
133
|
+
const run = { runId: req.requestId, binding: c.binding, modelId: req.modelId, input, requestedInput: req.input, projectId: c.projectId ?? randomUUID(), idempotencyKey: randomUUID(), quote: result.data, state: 'QUOTED', createdAt: now, updatedAt: now };
|
|
134
|
+
await journal().write({ ...data, revision: data.revision + 1, runs: [...data.runs, run] });
|
|
135
|
+
return publicRun(run);
|
|
136
|
+
}),
|
|
137
|
+
confirm: (runId, confirm) => serial(async () => {
|
|
138
|
+
const c = connected();
|
|
139
|
+
if (confirm !== true)
|
|
140
|
+
throw new StudioError('STUDIO_CONFIRM_REQUIRED', 409);
|
|
141
|
+
let data = await journal().read();
|
|
142
|
+
let run = find(data, runId, c);
|
|
143
|
+
if (run.job)
|
|
144
|
+
return publicRun(run);
|
|
145
|
+
if (data.runs.some(r => r.binding === c.binding && ['SUBMITTING', 'SUBMISSION_UNKNOWN'].includes(r.state)))
|
|
146
|
+
throw new StudioError('STUDIO_SUBMISSION_UNRESOLVED', 409);
|
|
147
|
+
if (run.state !== 'QUOTED')
|
|
148
|
+
throw new StudioError('STUDIO_RUN_CONFLICT', 409);
|
|
149
|
+
if (Date.parse(run.quote.expiresAt) <= Date.now())
|
|
150
|
+
throw new StudioError('STUDIO_QUOTE_EXPIRED', 409);
|
|
151
|
+
run = { ...run, state: 'SUBMITTING', updatedAt: new Date().toISOString() };
|
|
152
|
+
await persist(data, run);
|
|
153
|
+
data = await journal().read();
|
|
154
|
+
if (Date.parse(run.quote.expiresAt) <= Date.now()) {
|
|
155
|
+
// No request was made: restore the expired quote rather than inventing an unknown charge.
|
|
156
|
+
await persist(data, { ...run, state: 'QUOTED', updatedAt: new Date().toISOString() });
|
|
157
|
+
throw new StudioError('STUDIO_QUOTE_EXPIRED', 409);
|
|
158
|
+
}
|
|
159
|
+
let postAnswered = false;
|
|
160
|
+
try {
|
|
161
|
+
const created = await c.client.createJob({ quoteId: run.quote.quoteId, projectId: run.projectId, input: run.input }, run.idempotencyKey);
|
|
162
|
+
postAnswered = true;
|
|
163
|
+
const job = projectJob(created, c.secret);
|
|
164
|
+
run = { ...run, state: terminal.has(job.status) ? 'TERMINAL' : 'ACCEPTED', job, updatedAt: new Date().toISOString() };
|
|
165
|
+
await persist(data, run);
|
|
166
|
+
return publicRun(run);
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
// Only a failed first POST can prove no job was created. Projection or
|
|
170
|
+
// persistence after a response must never be reclassified as rejection.
|
|
171
|
+
if (!postAnswered && classifyCreateFailure(error, false) === 'none') {
|
|
172
|
+
await persist(data, { ...run, state: 'SUBMISSION_REJECTED', lastErrorCode: safeCloudCode(error, c.secret), lastFailure: cloudDiagnostic(error, c.secret, { submission: 'none' }), updatedAt: new Date().toISOString() });
|
|
173
|
+
throw safeCloudFailure(error, c.secret, { submission: 'none' });
|
|
174
|
+
}
|
|
175
|
+
// A failed response/persist cannot prove that the gateway did not accept.
|
|
176
|
+
const unknown = { ...run, state: 'SUBMISSION_UNKNOWN', lastErrorCode: safeCloudCode(error, c.secret), lastFailure: cloudDiagnostic(error, c.secret, { submission: run.job ? 'created' : 'unknown', recovery: run.job ? undefined : 'ledger' }), updatedAt: new Date().toISOString() };
|
|
177
|
+
await persist(data, unknown);
|
|
178
|
+
return publicRun(unknown);
|
|
179
|
+
}
|
|
180
|
+
}),
|
|
181
|
+
refresh: (runId) => serial(async () => {
|
|
182
|
+
const c = connected();
|
|
183
|
+
const data = await journal().read();
|
|
184
|
+
const run = find(data, runId, c);
|
|
185
|
+
if (!run.job)
|
|
186
|
+
throw new StudioError('STUDIO_SUBMISSION_UNRESOLVED', 409);
|
|
187
|
+
let job;
|
|
188
|
+
try {
|
|
189
|
+
job = projectJob(await c.client.getJob(run.job.jobId), c.secret, run.job, true);
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
await persist(data, { ...run, job: { ...run.job, identityReason: run.job.identityReason === 'IDENTITY_CONFLICT' ? 'IDENTITY_CONFLICT' : 'JOB_UNAVAILABLE' }, updatedAt: new Date().toISOString() });
|
|
193
|
+
throw error;
|
|
194
|
+
}
|
|
195
|
+
const { lastFailure: _failure, lastErrorCode: _code, ...before } = run;
|
|
196
|
+
const next = { ...before, job, state: terminal.has(job.status) ? 'TERMINAL' : 'ACCEPTED', updatedAt: new Date().toISOString() };
|
|
197
|
+
await persist(data, next);
|
|
198
|
+
return publicRun(next);
|
|
199
|
+
}),
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
//# sourceMappingURL=cloud-service.js.map
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { type CaptureBinding, type CaptureDelta } from './director-relations.js';
|
|
2
|
+
import { StudioError } from './errors.js';
|
|
3
|
+
import type { StudioCanvas, StudioCanvasAuthority } from './canvas-store.js';
|
|
4
|
+
import type { StudioAsset } from './types.js';
|
|
5
|
+
export interface CaptureView {
|
|
6
|
+
captureId: string;
|
|
7
|
+
assetId: string;
|
|
8
|
+
canvasId: string;
|
|
9
|
+
nodeId: string;
|
|
10
|
+
sceneId: string;
|
|
11
|
+
sceneRevision: number;
|
|
12
|
+
cameraId: string;
|
|
13
|
+
expectedCanvasRevision: number;
|
|
14
|
+
createdAt: string;
|
|
15
|
+
width: number;
|
|
16
|
+
height: number;
|
|
17
|
+
state: 'BOUND' | 'BINDING_PENDING' | 'BINDING_CONFLICT' | 'ASSET_UNAVAILABLE';
|
|
18
|
+
binding: CaptureBinding;
|
|
19
|
+
fileState: 'NOT_CHECKED' | 'READY' | 'MISSING' | 'CORRUPT';
|
|
20
|
+
retryable: boolean;
|
|
21
|
+
}
|
|
22
|
+
export interface CaptureReceipt {
|
|
23
|
+
status: 200 | 201 | 202;
|
|
24
|
+
capture: CaptureView;
|
|
25
|
+
canvas?: StudioCanvas;
|
|
26
|
+
relationDelta?: CaptureDelta;
|
|
27
|
+
replayed: boolean;
|
|
28
|
+
}
|
|
29
|
+
export declare class StudioCaptureError extends StudioError {
|
|
30
|
+
readonly capture: CaptureView;
|
|
31
|
+
constructor(code: 'STUDIO_CAPTURE_BINDING_CONFLICT' | 'STUDIO_CAPTURE_ASSET_UNAVAILABLE', capture: CaptureView);
|
|
32
|
+
}
|
|
33
|
+
export interface DirectorCaptureStore {
|
|
34
|
+
create(canvasId: string, input: unknown): Promise<CaptureReceipt>;
|
|
35
|
+
list(canvasId: string, input: {
|
|
36
|
+
limit?: number;
|
|
37
|
+
cursor?: string;
|
|
38
|
+
}): Promise<{
|
|
39
|
+
items: CaptureView[];
|
|
40
|
+
nextCursor: string | null;
|
|
41
|
+
}>;
|
|
42
|
+
read(canvasId: string, captureId: string): Promise<{
|
|
43
|
+
capture: CaptureView;
|
|
44
|
+
}>;
|
|
45
|
+
retry(canvasId: string, captureId: string): Promise<CaptureReceipt>;
|
|
46
|
+
}
|
|
47
|
+
export interface CapturePorts {
|
|
48
|
+
maxBytes: number;
|
|
49
|
+
canvas: StudioCanvasAuthority;
|
|
50
|
+
assets(): Promise<StudioAsset[]>;
|
|
51
|
+
save(asset: StudioAsset, bytes: Uint8Array): Promise<StudioAsset>;
|
|
52
|
+
verify(asset: StudioAsset): Promise<'READY' | 'MISSING' | 'CORRUPT'>;
|
|
53
|
+
faults?: {
|
|
54
|
+
beforeCaptureCanvas?(): void | Promise<void>;
|
|
55
|
+
afterCaptureCanvas?(): void | Promise<void>;
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
export declare function createDirectorCaptureStore(ports: CapturePorts, serial: <T>(work: () => Promise<T>) => Promise<T>): DirectorCaptureStore;
|
|
59
|
+
//# sourceMappingURL=director-captures.d.ts.map
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { directorUuid, parseDirectorScene, cameraDimensions, isDirectorJson } from './director-contract.js';
|
|
4
|
+
import { DIRECTOR_MAX_PNG_BYTES, validateDirectorPng } from './director-png.js';
|
|
5
|
+
import { captureBinding, canBindOriginal, withCaptureRelation } from './director-relations.js';
|
|
6
|
+
import { StudioError } from './errors.js';
|
|
7
|
+
export class StudioCaptureError extends StudioError {
|
|
8
|
+
capture;
|
|
9
|
+
constructor(code, capture) {
|
|
10
|
+
super(code, 409);
|
|
11
|
+
this.capture = capture;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
const revision = z.number().int().nonnegative().safe();
|
|
15
|
+
const inputSchema = z.object({ revision, nodeId: z.string().min(1).max(160).regex(/^[a-zA-Z0-9_-]+$/), sceneId: directorUuid, sceneRevision: revision, cameraId: directorUuid, captureId: directorUuid,
|
|
16
|
+
pngBase64: z.string().min(4).max(Math.ceil(DIRECTOR_MAX_PNG_BYTES / 3) * 4).regex(/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/),
|
|
17
|
+
}).strict();
|
|
18
|
+
function validId(id) { if (!directorUuid.safeParse(id).success)
|
|
19
|
+
throw new StudioError('STUDIO_INPUT_INVALID'); }
|
|
20
|
+
const sha = (bytes) => createHash('sha256').update(bytes).digest('hex');
|
|
21
|
+
export function createDirectorCaptureStore(ports, serial) {
|
|
22
|
+
async function readCanvas(id) { try {
|
|
23
|
+
return await ports.canvas.read(id);
|
|
24
|
+
}
|
|
25
|
+
catch (e) {
|
|
26
|
+
if (e instanceof StudioError && e.code === 'STUDIO_CANVAS_NOT_FOUND')
|
|
27
|
+
return undefined;
|
|
28
|
+
throw e;
|
|
29
|
+
} }
|
|
30
|
+
function project(asset, canvas, fileState = 'NOT_CHECKED') {
|
|
31
|
+
const p = asset.directorCapture, { binding, history } = captureBinding(canvas, asset);
|
|
32
|
+
const bound = history && binding.reference && binding.assetNode && binding.connection;
|
|
33
|
+
const pending = !bound && canBindOriginal(canvas, asset);
|
|
34
|
+
const unavailable = fileState === 'MISSING' || fileState === 'CORRUPT';
|
|
35
|
+
return { captureId: p.captureId, assetId: asset.assetId, canvasId: p.canvasId, nodeId: p.nodeId, sceneId: p.sceneId, sceneRevision: p.sceneRevision, cameraId: p.cameraId, expectedCanvasRevision: p.expectedCanvasRevision,
|
|
36
|
+
createdAt: asset.savedAt, width: p.width, height: p.height, binding, fileState, state: unavailable ? 'ASSET_UNAVAILABLE' : bound ? 'BOUND' : pending ? 'BINDING_PENDING' : 'BINDING_CONFLICT', retryable: pending && !unavailable };
|
|
37
|
+
}
|
|
38
|
+
async function find(canvasId, captureId) { validId(canvasId); validId(captureId); const asset = (await ports.assets()).find(a => a.directorCapture?.captureId === captureId && a.directorCapture.canvasId === canvasId); if (!asset)
|
|
39
|
+
throw new StudioError('STUDIO_CAPTURE_NOT_FOUND', 404); return asset; }
|
|
40
|
+
async function bind(asset, replayed) {
|
|
41
|
+
const p = asset.directorCapture, canvas = await readCanvas(p.canvasId), view = project(asset, canvas, await ports.verify(asset));
|
|
42
|
+
if (view.state === 'ASSET_UNAVAILABLE')
|
|
43
|
+
throw new StudioCaptureError('STUDIO_CAPTURE_ASSET_UNAVAILABLE', view);
|
|
44
|
+
if (view.state === 'BOUND')
|
|
45
|
+
return { status: 200, capture: view, canvas, replayed: true };
|
|
46
|
+
if (!view.retryable || !canvas)
|
|
47
|
+
throw new StudioCaptureError('STUDIO_CAPTURE_BINDING_CONFLICT', view);
|
|
48
|
+
let saved, relationDelta;
|
|
49
|
+
try {
|
|
50
|
+
const relation = withCaptureRelation(canvas, asset);
|
|
51
|
+
relationDelta = relation.delta;
|
|
52
|
+
await ports.faults?.beforeCaptureCanvas?.();
|
|
53
|
+
saved = await ports.canvas.save(canvas.id, { revision: canvas.revision, name: canvas.name, snapshot: relation.snapshot });
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
// Determine disk authority after a failed commit. A malformed/unreadable
|
|
57
|
+
// authority cannot be advertised as a known partial success.
|
|
58
|
+
await ports.assets().catch(() => { throw new StudioError('STUDIO_CAPTURE_OUTCOME_UNKNOWN', 503); });
|
|
59
|
+
const current = await readCanvas(p.canvasId).catch(() => { throw new StudioError('STUDIO_CAPTURE_OUTCOME_UNKNOWN', 503); });
|
|
60
|
+
const check = project(asset, current, await ports.verify(asset));
|
|
61
|
+
if (check.state === 'BOUND')
|
|
62
|
+
return { status: 200, capture: check, canvas: current, replayed: true };
|
|
63
|
+
if (check.state === 'BINDING_PENDING')
|
|
64
|
+
return { status: 202, capture: check, replayed };
|
|
65
|
+
if (check.state === 'ASSET_UNAVAILABLE')
|
|
66
|
+
throw new StudioCaptureError('STUDIO_CAPTURE_ASSET_UNAVAILABLE', check);
|
|
67
|
+
throw new StudioCaptureError('STUDIO_CAPTURE_BINDING_CONFLICT', check);
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
await ports.faults?.afterCaptureCanvas?.();
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
throw new StudioError('STUDIO_CAPTURE_OUTCOME_UNKNOWN', 503);
|
|
74
|
+
}
|
|
75
|
+
return { status: replayed ? 200 : 201, capture: project(asset, saved, 'READY'), canvas: saved, relationDelta, replayed };
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
create: (canvasId, value) => serial(async () => {
|
|
79
|
+
validId(canvasId);
|
|
80
|
+
if (!isDirectorJson(value))
|
|
81
|
+
throw new StudioError('STUDIO_INPUT_INVALID');
|
|
82
|
+
const parsed = inputSchema.safeParse(value);
|
|
83
|
+
if (!parsed.success)
|
|
84
|
+
throw new StudioError('STUDIO_INPUT_INVALID');
|
|
85
|
+
const input = parsed.data;
|
|
86
|
+
const bytes = Buffer.from(input.pngBase64, 'base64');
|
|
87
|
+
if (bytes.length > DIRECTOR_MAX_PNG_BYTES || bytes.toString('base64') !== input.pngBase64)
|
|
88
|
+
throw new StudioError('STUDIO_INPUT_INVALID');
|
|
89
|
+
const requestHash = sha(JSON.stringify([canvasId, input.revision, input.nodeId, input.sceneId, input.sceneRevision, input.cameraId, input.captureId, sha(bytes)]));
|
|
90
|
+
const existing = (await ports.assets()).find(a => a.directorCapture?.captureId === input.captureId);
|
|
91
|
+
if (existing) {
|
|
92
|
+
if (existing.directorCapture.requestHash !== requestHash)
|
|
93
|
+
throw new StudioError('STUDIO_CAPTURE_ID_CONFLICT', 409);
|
|
94
|
+
return bind(existing, true);
|
|
95
|
+
}
|
|
96
|
+
if (bytes.length > ports.maxBytes)
|
|
97
|
+
throw new StudioError('STUDIO_MEDIA_TOO_LARGE', 413);
|
|
98
|
+
const canvas = await ports.canvas.read(canvasId), n = canvas.snapshot.nodes.find(n => n.id === input.nodeId && ['image', 'video'].includes(n.type));
|
|
99
|
+
if (!n)
|
|
100
|
+
throw new StudioError('STUDIO_CANVAS_NOT_FOUND', 404);
|
|
101
|
+
const scene = n.metadata.studioDirector === undefined ? undefined : parseDirectorScene(n.metadata.studioDirector), camera = scene?.cameras.find(c => c.id === input.cameraId);
|
|
102
|
+
if (canvas.revision !== input.revision || scene?.sceneId !== input.sceneId || scene.sceneRevision !== input.sceneRevision || !camera)
|
|
103
|
+
throw new StudioError('STUDIO_CAPTURE_BINDING_CONFLICT', 409);
|
|
104
|
+
const dimensions = cameraDimensions(camera.aspect);
|
|
105
|
+
validateDirectorPng(bytes, dimensions);
|
|
106
|
+
const provenance = { captureId: input.captureId, requestHash, canvasId, expectedCanvasRevision: input.revision, nodeId: input.nodeId, sceneId: input.sceneId, sceneRevision: input.sceneRevision, cameraId: input.cameraId, ...dimensions };
|
|
107
|
+
const assetId = randomUUID(), asset = { assetId, displayName: '镜头截图.png', mediaType: 'image/png', relativePath: `assets/${assetId}.png`, byteLength: bytes.length, sha256: sha(bytes), savedAt: new Date().toISOString(), directorCapture: provenance };
|
|
108
|
+
const { snapshot } = withCaptureRelation(canvas, asset);
|
|
109
|
+
await ports.canvas.previewSave(canvasId, { revision: canvas.revision, name: canvas.name, snapshot }, [asset]);
|
|
110
|
+
try {
|
|
111
|
+
await ports.save(asset, bytes);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
throw new StudioError('STUDIO_CAPTURE_OUTCOME_UNKNOWN', 503);
|
|
115
|
+
}
|
|
116
|
+
return bind(asset, false);
|
|
117
|
+
}),
|
|
118
|
+
list: (canvasId, input) => serial(async () => {
|
|
119
|
+
validId(canvasId);
|
|
120
|
+
const parsed = z.object({ limit: z.number().int().min(1).max(100).optional(), cursor: z.string().min(1).max(256).regex(/^[A-Za-z0-9_-]+$/).optional() }).strict().safeParse(input);
|
|
121
|
+
if (!parsed.success)
|
|
122
|
+
throw new StudioError('STUDIO_INPUT_INVALID');
|
|
123
|
+
const rows = (await ports.assets()).filter(a => a.directorCapture?.canvasId === canvasId).sort((a, b) => a.savedAt.localeCompare(b.savedAt) || a.directorCapture.captureId.localeCompare(b.directorCapture.captureId)), canvas = await readCanvas(canvasId);
|
|
124
|
+
if (!canvas && !rows.length)
|
|
125
|
+
throw new StudioError('STUDIO_CANVAS_NOT_FOUND', 404);
|
|
126
|
+
let start = 0;
|
|
127
|
+
const cursor = parsed.data.cursor;
|
|
128
|
+
if (cursor) {
|
|
129
|
+
const at = rows.findIndex(a => Buffer.from(JSON.stringify([a.savedAt, a.directorCapture.captureId])).toString('base64url') === cursor);
|
|
130
|
+
if (at < 0)
|
|
131
|
+
throw new StudioError('STUDIO_INPUT_INVALID');
|
|
132
|
+
start = at + 1;
|
|
133
|
+
}
|
|
134
|
+
const selected = rows.slice(start, start + (parsed.data.limit ?? 20)), last = selected.at(-1);
|
|
135
|
+
return { items: selected.map(a => project(a, canvas)), nextCursor: start + selected.length < rows.length && last ? Buffer.from(JSON.stringify([last.savedAt, last.directorCapture.captureId])).toString('base64url') : null };
|
|
136
|
+
}),
|
|
137
|
+
read: (canvasId, captureId) => serial(async () => { const asset = await find(canvasId, captureId); return { capture: project(asset, await readCanvas(canvasId), await ports.verify(asset)) }; }),
|
|
138
|
+
retry: (canvasId, captureId) => serial(async () => bind(await find(canvasId, captureId), true)),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
//# sourceMappingURL=director-captures.js.map
|