@hyperframes/share 0.9.28-alpha.0 → 0.9.28
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/LICENSE +190 -0
- package/README.md +1 -58
- package/dist/index.d.mts +0 -1
- package/dist/projectLock.d.mts +2 -2
- package/dist/projectSnapshot.d.mts +1 -3
- package/dist/reviewClient.d.mts +16 -13
- package/dist/reviewExport.d.mts +1 -11
- package/dist/reviewUpload.d.mts +0 -2
- package/package.json +1 -1
- package/scripts/copy-types.mjs +0 -3
- package/src/index.mjs +0 -6
- package/src/projectLock.mjs +2 -9
- package/src/projectSnapshot.mjs +4 -11
- package/src/reviewClient.mjs +83 -48
- package/src/reviewExport.mjs +37 -145
- package/src/reviewUpload.mjs +16 -18
- package/dist/shareSources.d.mts +0 -24
- package/dist/sources/adapterTypes.d.ts +0 -39
- package/dist/sources/api.d.mts +0 -29
- package/dist/sources/capture.d.mts +0 -24
- package/dist/sources/contract.d.mts +0 -2
- package/dist/sources/errors.d.mts +0 -24
- package/dist/sources/limits.d.mts +0 -12
- package/dist/sources/manifest.d.mts +0 -23
- package/dist/sources/pathPolicy.d.mts +0 -25
- package/dist/sources/projectState.d.mts +0 -9
- package/dist/sources/store.d.mts +0 -3
- package/dist/sources/transfer.d.mts +0 -45
- package/dist/sources/types.d.ts +0 -353
- package/dist/sources/unicode.d.mts +0 -15
- package/dist/sources/upload.d.mts +0 -9
- package/dist/sources/uploadFiles.d.mts +0 -31
- package/dist/sources/wire.d.mts +0 -21
- package/src/shareSources.mjs +0 -236
- package/src/sources/adapterTypes.d.ts +0 -39
- package/src/sources/api.mjs +0 -114
- package/src/sources/capture.mjs +0 -262
- package/src/sources/contract/LICENSE-Unicode.txt +0 -39
- package/src/sources/contract/path-policy.json +0 -311
- package/src/sources/contract/unicode-15.1.0.json +0 -6179
- package/src/sources/contract.mjs +0 -13
- package/src/sources/errors.mjs +0 -77
- package/src/sources/limits.mjs +0 -48
- package/src/sources/manifest.mjs +0 -154
- package/src/sources/pathPolicy.mjs +0 -197
- package/src/sources/projectState.mjs +0 -59
- package/src/sources/store.mjs +0 -82
- package/src/sources/transfer.mjs +0 -196
- package/src/sources/types.d.ts +0 -353
- package/src/sources/unicode.mjs +0 -163
- package/src/sources/upload.mjs +0 -201
- package/src/sources/uploadFiles.mjs +0 -263
- package/src/sources/wire.mjs +0 -205
package/src/reviewClient.mjs
CHANGED
|
@@ -92,6 +92,7 @@ export function reviewClient({ access, fetchImpl = fetch, signedInEmail = async
|
|
|
92
92
|
* @returns {Promise<{ versionSharing: boolean, shareFiles: boolean }>} */
|
|
93
93
|
reviewFlags: async () => {
|
|
94
94
|
const data = await call("GET", SPACES, undefined, {}, SHARING_REFUSALS);
|
|
95
|
+
if (!Array.isArray(data?.items)) throw new ReviewFailed(502, "malformed_spaces");
|
|
95
96
|
return {
|
|
96
97
|
versionSharing: data?.version_sharing === true,
|
|
97
98
|
shareFiles: data?.share_files === true,
|
|
@@ -152,9 +153,8 @@ function threadWrites(call, project) {
|
|
|
152
153
|
};
|
|
153
154
|
}
|
|
154
155
|
|
|
155
|
-
/** An invite to an email whose grant was revoked: the server
|
|
156
|
-
*
|
|
157
|
-
* the conflict's. Null for any other refusal, or one the same revision already got.
|
|
156
|
+
/** An invite to an email whose grant was revoked: the server takes the row back only at its revision with a fresh
|
|
157
|
+
* operation ID, here the conflict's revision. Null for any other refusal, or one the same revision already got.
|
|
158
158
|
* @param {unknown} error @param {import("./types/reviewWire.js").CreateGrantRequest} body
|
|
159
159
|
* @returns {import("./types/reviewWire.js").CreateGrantRequest | null} */
|
|
160
160
|
function regrantOf(error, body) {
|
|
@@ -166,17 +166,24 @@ function regrantOf(error, body) {
|
|
|
166
166
|
return { ...body, client_operation_id: randomUUID(), expected_revision: revision };
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
-
/**
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
* exactly when the window drops its own operation: on an answer, on a refusal `rejectedGrantRequest` (the window's own
|
|
173
|
-
* test) calls final, or when the email is invited afresh. Any other refusal, a moderation hold among them, keeps it.
|
|
169
|
+
/** Grants listed with their revoked rows, whose revisions let a re-invite go straight out as the re-grant; a server
|
|
170
|
+
* that ignores `include_revoked` lists none, and the re-invite meets `revision_conflict` first. A re-grant is kept as
|
|
171
|
+
* its invitation's retry, sent again as itself until an answer or a refusal `rejectedGrantRequest` calls final.
|
|
174
172
|
* @param {(method: string, route: string, body?: unknown) => Promise<any>} call @param {(id: string) => string} project
|
|
175
173
|
* @param {import("./types/access.js").ApiAccess} access */
|
|
176
|
-
function
|
|
174
|
+
function grantCalls(call, project, access) {
|
|
177
175
|
/** Re-grants awaiting a final answer, by site, account, project and the operation the window sent.
|
|
178
176
|
* @type {Map<string, { email: string, regrant: import("./types/reviewWire.js").CreateGrantRequest }>} */
|
|
179
177
|
const sent = new Map();
|
|
178
|
+
/** The revision of each revoked grant the last list read, by site, account and project, then email.
|
|
179
|
+
* @type {Map<string, Map<string, number>>} */
|
|
180
|
+
const revoked = new Map();
|
|
181
|
+
/** @param {string} id */
|
|
182
|
+
const scopeOf = async (id) => {
|
|
183
|
+
// expected: signed out, the call itself refuses as login_required; the scope is only a key.
|
|
184
|
+
const who = await access().catch(() => null);
|
|
185
|
+
return [who?.heygen ?? "", who?.owner ?? "", id].join("\n");
|
|
186
|
+
};
|
|
180
187
|
/** @param {string} route @param {string} key @param {import("./types/reviewWire.js").CreateGrantRequest} body */
|
|
181
188
|
const settle = async (route, key, body) => {
|
|
182
189
|
try {
|
|
@@ -188,29 +195,76 @@ function grantCreator(call, project, access) {
|
|
|
188
195
|
throw error;
|
|
189
196
|
}
|
|
190
197
|
};
|
|
191
|
-
/** @param {string}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
const key = `${scope}\n${body.client_operation_id}`;
|
|
199
|
-
const retained = sent.get(key);
|
|
200
|
-
if (retained) return settle(route, key, retained.regrant);
|
|
198
|
+
/** @param {string} route @param {string} key @param {import("./types/reviewWire.js").CreateGrantRequest} body */
|
|
199
|
+
const regranting = (route, key, body) => {
|
|
200
|
+
sent.set(key, { email: body.email, regrant: body });
|
|
201
|
+
return settle(route, key, body);
|
|
202
|
+
};
|
|
203
|
+
/** @param {string} scope @param {string} email */
|
|
204
|
+
const forgetEarlierInvites = (scope, email) => {
|
|
201
205
|
for (const [other, kept] of sent)
|
|
202
|
-
if (other.startsWith(`${scope}\n`) && kept.email ===
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
206
|
+
if (other.startsWith(`${scope}\n`) && kept.email === email) sent.delete(other);
|
|
207
|
+
};
|
|
208
|
+
return {
|
|
209
|
+
/** The project's live grants. @param {string} id
|
|
210
|
+
* @returns {Promise<{items: import("./types/reviewWire.js").AccessGrantDTO[]}>} */
|
|
211
|
+
grants: async (id) => {
|
|
212
|
+
const scope = await scopeOf(id);
|
|
213
|
+
const items = await everyGrant(call, `${project(id)}/access-grants`);
|
|
214
|
+
revoked.set(
|
|
215
|
+
scope,
|
|
216
|
+
new Map(
|
|
217
|
+
items.filter((grant) => grant.revoked_at).map((grant) => [grant.email, grant.revision]),
|
|
218
|
+
),
|
|
219
|
+
);
|
|
220
|
+
return { items: items.filter((grant) => !grant.revoked_at) };
|
|
221
|
+
},
|
|
222
|
+
/** @param {string} id @param {import("./types/reviewWire.js").CreateGrantRequest} body
|
|
223
|
+
* @returns {Promise<import("./types/reviewWire.js").AccessGrantDTO>} */
|
|
224
|
+
createGrant: async (id, body) => {
|
|
225
|
+
const route = `${project(id)}/access-grants`;
|
|
226
|
+
const scope = await scopeOf(id);
|
|
227
|
+
const key = `${scope}\n${body.client_operation_id}`;
|
|
228
|
+
const retained = sent.get(key);
|
|
229
|
+
if (retained) return settle(route, key, retained.regrant);
|
|
230
|
+
forgetEarlierInvites(scope, body.email);
|
|
231
|
+
const known =
|
|
232
|
+
body.expected_revision === null ? revoked.get(scope)?.get(body.email) : undefined;
|
|
233
|
+
revoked.get(scope)?.delete(body.email);
|
|
234
|
+
const first =
|
|
235
|
+
known === undefined
|
|
236
|
+
? body
|
|
237
|
+
: { ...body, client_operation_id: randomUUID(), expected_revision: known };
|
|
238
|
+
try {
|
|
239
|
+
return await (first === body ? call("POST", route, body) : regranting(route, key, first));
|
|
240
|
+
} catch (error) {
|
|
241
|
+
const regrant = regrantOf(error, first);
|
|
242
|
+
if (!regrant) throw error;
|
|
243
|
+
return regranting(route, key, regrant);
|
|
244
|
+
}
|
|
245
|
+
},
|
|
211
246
|
};
|
|
212
247
|
}
|
|
213
248
|
|
|
249
|
+
/** Every page of a grant list, revoked rows included where the server lists them.
|
|
250
|
+
* @param {(method: string, route: string) => Promise<any>} call @param {string} route
|
|
251
|
+
* @returns {Promise<import("./types/reviewWire.js").AccessGrantDTO[]>} */
|
|
252
|
+
async function everyGrant(call, route) {
|
|
253
|
+
const items = [];
|
|
254
|
+
const cursors = new Set();
|
|
255
|
+
let cursor = null;
|
|
256
|
+
do {
|
|
257
|
+
const query = new URLSearchParams({ include_revoked: "true", ...(cursor ? { cursor } : {}) });
|
|
258
|
+
const page = await call("GET", `${route}?${query}`);
|
|
259
|
+
items.push(...page.items);
|
|
260
|
+
cursor = page.next_cursor;
|
|
261
|
+
if (cursor && (cursors.has(cursor) || cursors.size >= 1000))
|
|
262
|
+
throw new ReviewFailed(502, "malformed_grant_pagination");
|
|
263
|
+
cursors.add(cursor);
|
|
264
|
+
} while (cursor);
|
|
265
|
+
return items;
|
|
266
|
+
}
|
|
267
|
+
|
|
214
268
|
/** @typedef {ReturnType<typeof reviewClient>} ReviewClient
|
|
215
269
|
* @typedef {import("./types/reviewWire.js").ExportRequest} ExportRequest */
|
|
216
270
|
|
|
@@ -229,23 +283,7 @@ function managementClient(call, project, access) {
|
|
|
229
283
|
}),
|
|
230
284
|
/** @param {string} id @returns {Promise<null>} */
|
|
231
285
|
removeProject: (id) => call("DELETE", project(id)),
|
|
232
|
-
|
|
233
|
-
grants: async (id) => {
|
|
234
|
-
const items = [];
|
|
235
|
-
const cursors = new Set();
|
|
236
|
-
let cursor = null;
|
|
237
|
-
do {
|
|
238
|
-
const query = new URLSearchParams(cursor ? { cursor } : {});
|
|
239
|
-
const page = await call("GET", `${project(id)}/access-grants?${query}`);
|
|
240
|
-
items.push(...page.items);
|
|
241
|
-
cursor = page.next_cursor;
|
|
242
|
-
if (cursor && (cursors.has(cursor) || cursors.size >= 1000))
|
|
243
|
-
throw new ReviewFailed(502, "malformed_grant_pagination");
|
|
244
|
-
cursors.add(cursor);
|
|
245
|
-
} while (cursor);
|
|
246
|
-
return { items };
|
|
247
|
-
},
|
|
248
|
-
createGrant: grantCreator(call, project, access),
|
|
286
|
+
...grantCalls(call, project, access),
|
|
249
287
|
/** @param {string} id @param {string} grantId @param {{role: import("./types/reviewWire.js").AccessGrantDTO["role"],
|
|
250
288
|
* expected_revision: number}} body @returns {Promise<import("./types/reviewWire.js").AccessGrantDTO>} */
|
|
251
289
|
updateGrant: (id, grantId, body) =>
|
|
@@ -255,6 +293,3 @@ function managementClient(call, project, access) {
|
|
|
255
293
|
call("DELETE", `${project(id)}/access-grants/${encodeURIComponent(grantId)}`),
|
|
256
294
|
};
|
|
257
295
|
}
|
|
258
|
-
|
|
259
|
-
/** @param {Awaited<ReturnType<import("./types/access.js").ApiAccess>>} who @param {string} id */
|
|
260
|
-
const grantScope = (who, id) => [who?.heygen ?? "", who?.owner ?? "", id].join("\n");
|
package/src/reviewExport.mjs
CHANGED
|
@@ -39,10 +39,9 @@ import { versionSentence } from "./reviewVersionSentences.mjs";
|
|
|
39
39
|
* definitive result; `reviewSent`: cleanup cutoff, with attribution only when the accepted attempt is
|
|
40
40
|
* known; `last`: the newest version.
|
|
41
41
|
* @typedef {{ pending: { body: ExportRequest, videoId: string | null, projectId?: string | null,
|
|
42
|
-
*
|
|
43
|
-
* last: { fingerprint: string, versionId: string, exportedAt: string, projectId: string, files?: boolean,
|
|
44
|
-
*
|
|
45
|
-
* | null, failure?: (Failure & { projectId: string }) | null }} VersionRecord */
|
|
42
|
+
* checkingSince?: number, snapshotRefused?: boolean, unanswered?: boolean, reviewSent?: ReviewSent } | null,
|
|
43
|
+
* last: { fingerprint: string, versionId: string, exportedAt: string, projectId: string, files?: boolean, checksum?: string }
|
|
44
|
+
* | null }} VersionRecord */
|
|
46
45
|
|
|
47
46
|
/** `onAdded`: a version went ready, with whether it carries files and whether it is the page's first from here.
|
|
48
47
|
* @typedef {{ onProgress?: (progress: ReviewExportProgress | null) => void, onUploaded?: () => Promise<void>,
|
|
@@ -50,11 +49,10 @@ import { versionSentence } from "./reviewVersionSentences.mjs";
|
|
|
50
49
|
|
|
51
50
|
/** `projectId` overrides the recorded review project; `open`: a held video is watched only while the project is open.
|
|
52
51
|
* `filesForExport`: the synced snapshot a new version carries, null for
|
|
53
|
-
* none.
|
|
52
|
+
* none.
|
|
54
53
|
* @typedef {VersionHooks & { projectPath: string, title: string, client: ReviewClient, put?: typeof putFile,
|
|
55
54
|
* sleep?: (ms: number) => Promise<void>, probe?: (file: string) => Promise<VideoFacts | null>, now?: () => number,
|
|
56
|
-
* projectId?: string | null, filesForExport?: FilesForRender, fromReview?: FromReview | null
|
|
57
|
-
* allowPending?: boolean, requireSource?: boolean, omitSource?: boolean }} VersionRequest */
|
|
55
|
+
* projectId?: string | null, filesForExport?: FilesForRender, fromReview?: FromReview | null }} VersionRequest */
|
|
58
56
|
|
|
59
57
|
/** The snapshot of `rendered`, the project as the export was rendered; null when the folder is not that render.
|
|
60
58
|
* @typedef {(projectPath: string, rendered: string | null) => Promise<import("./types/reviewWire.js").SnapshotRef | null>}
|
|
@@ -179,36 +177,18 @@ function isLast(value) {
|
|
|
179
177
|
isSavedId(value.versionId) &&
|
|
180
178
|
isSavedId(value.exportedAt) &&
|
|
181
179
|
isSavedId(value.projectId) &&
|
|
182
|
-
isLastMetadata(value)
|
|
183
|
-
);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/** @param {Record<string, unknown>} value */
|
|
187
|
-
function isLastMetadata(value) {
|
|
188
|
-
return (
|
|
189
180
|
isOptionalBoolean(value.files) &&
|
|
190
|
-
isOptionalSnapshot(value.snapshot) &&
|
|
191
181
|
(value.checksum === undefined || isSavedId(value.checksum))
|
|
192
182
|
);
|
|
193
183
|
}
|
|
194
184
|
|
|
195
185
|
/** @param {Record<string, unknown>} saved
|
|
196
|
-
* @returns {saved is {pending?: VersionRecord["pending"], last?: SavedLast | null
|
|
186
|
+
* @returns {saved is {pending?: VersionRecord["pending"], last?: SavedLast | null}} */
|
|
197
187
|
const isExportState = (saved) =>
|
|
198
188
|
("pending" in saved || "last" in saved) &&
|
|
199
189
|
(saved.pending == null || isPending(saved.pending)) &&
|
|
200
190
|
(saved.last == null || isLast(saved.last));
|
|
201
191
|
|
|
202
|
-
/** @param {unknown} value @returns {value is Failure & { projectId: string }} */
|
|
203
|
-
const isSavedFailure = (value) =>
|
|
204
|
-
isJsonObject(value) &&
|
|
205
|
-
isSavedId(value.projectId) &&
|
|
206
|
-
typeof value.exportKey === "string" &&
|
|
207
|
-
typeof value.sentence === "string" &&
|
|
208
|
-
typeof value.final === "boolean" &&
|
|
209
|
-
typeof value.exportHelps === "boolean" &&
|
|
210
|
-
isOptionalBoolean(value.held);
|
|
211
|
-
|
|
212
192
|
/** @param {string} dir @returns {Promise<VersionRecord>} */
|
|
213
193
|
async function readState(dir) {
|
|
214
194
|
const file = statePath(dir);
|
|
@@ -218,7 +198,6 @@ async function readState(dir) {
|
|
|
218
198
|
const { last, pending } = saved;
|
|
219
199
|
return {
|
|
220
200
|
pending: pending ?? null,
|
|
221
|
-
failure: isSavedFailure(saved.failure) ? saved.failure : null,
|
|
222
201
|
last: typeof last?.fingerprint === "string" ? { ...last, fingerprint: last.fingerprint } : null,
|
|
223
202
|
};
|
|
224
203
|
}
|
|
@@ -255,7 +234,7 @@ const failures = new Map();
|
|
|
255
234
|
/** Which export a failure was about: the newest export file and the project as it was rendered. @param {string} dir */
|
|
256
235
|
const exportKeyOf = (dir) => {
|
|
257
236
|
const latest = latestExportFile(dir);
|
|
258
|
-
return latest ? `${latest.path}\n${latest.fingerprint ?? ""}
|
|
237
|
+
return latest ? `${latest.path}\n${latest.fingerprint ?? ""}` : "";
|
|
259
238
|
};
|
|
260
239
|
|
|
261
240
|
/** Whether the newest export can go up now: there is one, it is MP4, and the project has not moved past it.
|
|
@@ -280,8 +259,7 @@ export async function reviewVersion(projectPath, server = {}) {
|
|
|
280
259
|
const last = state.last?.projectId === projectId ? state.last : null;
|
|
281
260
|
const onServer =
|
|
282
261
|
server.latestReadyVersionId === undefined || server.latestReadyVersionId === last?.versionId;
|
|
283
|
-
const
|
|
284
|
-
const failed = savedFailure ?? failures.get(key);
|
|
262
|
+
const failed = failures.get(key);
|
|
285
263
|
return {
|
|
286
264
|
last: last
|
|
287
265
|
? { exportedAt: last.exportedAt, current: last.fingerprint === now && onServer }
|
|
@@ -301,8 +279,8 @@ export async function reviewVersion(projectPath, server = {}) {
|
|
|
301
279
|
|
|
302
280
|
/** The newest MP4 and the body that adds it; a stale, non-MP4, too big or too long export never sends anything.
|
|
303
281
|
* @param {string} dir @param {string} title @param {(file: string) => Promise<VideoFacts | null>} probe
|
|
304
|
-
* @param {FilesForRender} filesForExport
|
|
305
|
-
async function exportSource(dir, title, probe, filesForExport
|
|
282
|
+
* @param {FilesForRender} filesForExport */
|
|
283
|
+
async function exportSource(dir, title, probe, filesForExport) {
|
|
306
284
|
const latest = latestExportFile(dir);
|
|
307
285
|
if (!latest || sourceOf(dir, await projectFingerprint(dir)) !== "ready")
|
|
308
286
|
return { failure: local("Export an MP4 of the project as it is now first.") };
|
|
@@ -329,32 +307,16 @@ async function exportSource(dir, title, probe, filesForExport, requireSource) {
|
|
|
329
307
|
content_type: "video/mp4",
|
|
330
308
|
},
|
|
331
309
|
};
|
|
332
|
-
const snapshot = await
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
if (missingRequiredSource(requireSource, snapshot))
|
|
339
|
-
return {
|
|
340
|
-
failure: local(
|
|
341
|
-
"Source changed after preparation. Retry sharing, or choose video-only explicitly.",
|
|
342
|
-
),
|
|
343
|
-
};
|
|
344
|
-
if (snapshot) body.snapshot = snapshot;
|
|
345
|
-
return { file: latest.path, body };
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
/** @param {boolean} required @param {import("./types/reviewWire.js").SnapshotRef | null} snapshot */
|
|
349
|
-
const missingRequiredSource = (required, snapshot) => required && !snapshot;
|
|
350
|
-
|
|
351
|
-
/** @param {string} dir @param {string | null} rendered @param {FilesForRender} filesForExport @param {boolean} requireSource */
|
|
352
|
-
async function sourceSnapshot(dir, rendered, filesForExport, requireSource) {
|
|
353
|
-
return filesForExport(dir, rendered).catch((error) => {
|
|
354
|
-
if (requireSource) throw error;
|
|
355
|
-
console.error("[review files] could not read source; adding video only:", error);
|
|
310
|
+
const snapshot = await filesForExport(dir, body.source_fingerprint).catch((error) => {
|
|
311
|
+
// expected: the version still goes up, and Share's files line says it has no files and offers Sync now.
|
|
312
|
+
console.error(
|
|
313
|
+
"[review files] couldn't read the synced snapshot; the version goes up without files:",
|
|
314
|
+
error,
|
|
315
|
+
);
|
|
356
316
|
return null;
|
|
357
317
|
});
|
|
318
|
+
if (snapshot) body.snapshot = snapshot;
|
|
319
|
+
return { file: latest.path, body };
|
|
358
320
|
}
|
|
359
321
|
|
|
360
322
|
/** @param {ExportRequest["snapshot"]} a @param {ExportRequest["snapshot"]} b */
|
|
@@ -406,7 +368,7 @@ async function createdExport(r, id, pending, kept) {
|
|
|
406
368
|
return await sendCreate(r, id, pending, kept);
|
|
407
369
|
} catch (error) {
|
|
408
370
|
const refused = error instanceof ReviewFailed && error.reason === "snapshot_mismatch";
|
|
409
|
-
if (!refused || !pending.body.snapshot || pending.unanswered
|
|
371
|
+
if (!refused || !pending.body.snapshot || pending.unanswered) throw error;
|
|
410
372
|
console.error("[review] snapshot refused; the version goes up without files:", error);
|
|
411
373
|
const body = { ...pending.body, client_operation_id: `export-${randomUUID()}` };
|
|
412
374
|
delete body.snapshot;
|
|
@@ -602,31 +564,17 @@ const spent = (error, pending) =>
|
|
|
602
564
|
* @param {string} id @param {ExportDTO} done @param {FromReview | null} fromReview @returns {Promise<Said | null>} */
|
|
603
565
|
async function finish(dir, state, pending, id, done, fromReview) {
|
|
604
566
|
const ready = done.status === "ready";
|
|
605
|
-
const last =
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
});
|
|
567
|
+
const last = {
|
|
568
|
+
fingerprint: pending.body.source_fingerprint ?? "",
|
|
569
|
+
versionId: done.version_id,
|
|
570
|
+
exportedAt: done.exported_at,
|
|
571
|
+
projectId: id,
|
|
572
|
+
files: Boolean(pending.body.snapshot),
|
|
573
|
+
checksum: pending.body.local_upload.checksum_sha256,
|
|
574
|
+
};
|
|
575
|
+
await writeState(dir, { pending: null, last: ready ? last : state.last });
|
|
614
576
|
if (ready) await sharedFromReview(fromReview, dir, pending.reviewSent?.since);
|
|
615
|
-
return
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
/** @param {string} dir @param {NonNullable<VersionRecord["pending"]>} pending */
|
|
619
|
-
async function failedExportKey(dir, pending) {
|
|
620
|
-
if (typeof pending.renderKey === "string") return pending.renderKey;
|
|
621
|
-
const latest = latestExportFile(dir);
|
|
622
|
-
if (!latest) return "";
|
|
623
|
-
const checksum =
|
|
624
|
-
latest.fingerprint === pending.body.source_fingerprint
|
|
625
|
-
? await fileSha256Base64(latest.path).catch(() => null)
|
|
626
|
-
: null;
|
|
627
|
-
return checksum === pending.body.local_upload.checksum_sha256
|
|
628
|
-
? exportKeyOf(dir)
|
|
629
|
-
: `pending:${pending.body.client_operation_id}`;
|
|
577
|
+
return ready ? null : said(new ReviewFailed(200, done.error?.reason ?? ""));
|
|
630
578
|
}
|
|
631
579
|
|
|
632
580
|
/** @param {Required<VersionRequest>} r @param {(progress: ReviewExportProgress) => void} report
|
|
@@ -634,37 +582,17 @@ async function failedExportKey(dir, pending) {
|
|
|
634
582
|
async function run(r, report) {
|
|
635
583
|
report({ step: "preparing", sent: 0, total: 0 });
|
|
636
584
|
const state = await readState(r.projectPath);
|
|
637
|
-
const source = await exportSource(
|
|
638
|
-
r.projectPath,
|
|
639
|
-
r.title,
|
|
640
|
-
r.probe,
|
|
641
|
-
r.omitSource ? noSource : r.filesForExport,
|
|
642
|
-
r.requireSource,
|
|
643
|
-
);
|
|
585
|
+
const source = await exportSource(r.projectPath, r.title, r.probe, r.filesForExport);
|
|
644
586
|
if ("failure" in source) return source.failure ?? null;
|
|
645
587
|
const id = r.projectId ?? (await reviewProjectId(r.projectPath));
|
|
646
588
|
if (!id) return local("Share the project first, then add a version.");
|
|
647
|
-
if (
|
|
648
|
-
sameSourceMode(r, state.last, source.body.snapshot) &&
|
|
649
|
-
(await alreadyUploaded(r.client, id, state, source.body))
|
|
650
|
-
)
|
|
651
|
-
return null;
|
|
589
|
+
if (await alreadyUploaded(r.client, id, state, source.body)) return null;
|
|
652
590
|
return sendVersion(r, report, source, id, state);
|
|
653
591
|
}
|
|
654
|
-
const noSource = async () => null;
|
|
655
|
-
|
|
656
|
-
/** @param {Required<VersionRequest>} r @param {VersionRecord["last"]} last @param {ExportRequest["snapshot"]} snapshot */
|
|
657
|
-
function sameSourceMode(r, last, snapshot) {
|
|
658
|
-
if (r.requireSource) return sameSnapshot(last?.snapshot, snapshot);
|
|
659
|
-
if (r.omitSource) return last?.files !== true;
|
|
660
|
-
return true;
|
|
661
|
-
}
|
|
662
|
-
|
|
663
592
|
/** @param {ReviewClient} client @param {string} id @param {VersionRecord} state @param {ExportRequest} body */
|
|
664
593
|
async function alreadyUploaded(client, id, state, body) {
|
|
665
594
|
const last = state.last;
|
|
666
|
-
if (state.pending ||
|
|
667
|
-
return false;
|
|
595
|
+
if (state.pending || !last || last.projectId !== id) return false;
|
|
668
596
|
if (
|
|
669
597
|
last.fingerprint !== body.source_fingerprint ||
|
|
670
598
|
last.checksum !== body.local_upload.checksum_sha256
|
|
@@ -676,10 +604,8 @@ async function alreadyUploaded(client, id, state, body) {
|
|
|
676
604
|
* @param {{file: string, body: ExportRequest}} source @param {string} id @param {VersionRecord} state */
|
|
677
605
|
async function sendVersion(r, report, source, id, state) {
|
|
678
606
|
const first = state.last?.projectId !== id;
|
|
679
|
-
let pending =
|
|
680
|
-
|
|
681
|
-
if (sourceConflict) return local(sourceConflict);
|
|
682
|
-
await writeState(r.projectPath, (state = { ...state, pending, failure: null }));
|
|
607
|
+
let pending = bodyToSend(state, source.body, id);
|
|
608
|
+
await writeState(r.projectPath, (state = { ...state, pending }));
|
|
683
609
|
try {
|
|
684
610
|
const answer = await createdExport(r, id, pending, (next) =>
|
|
685
611
|
writeState(r.projectPath, (state = { ...state, pending: next })),
|
|
@@ -711,21 +637,10 @@ async function sendVersion(r, report, source, id, state) {
|
|
|
711
637
|
}
|
|
712
638
|
}
|
|
713
639
|
|
|
714
|
-
/** @param {Required<VersionRequest>} r @param {NonNullable<VersionRecord["pending"]>} pending @param {ExportRequest} fresh */
|
|
715
|
-
function pendingSourceConflict(r, pending, fresh) {
|
|
716
|
-
if (r.requireSource && !pending.body.snapshot)
|
|
717
|
-
return "A pending video-only upload must finish before attaching source. Retry with --video-only, then share source again.";
|
|
718
|
-
if (r.omitSource && pending.body.snapshot)
|
|
719
|
-
return "A pending upload includes source. Check its status or retry with source before creating a video-only version.";
|
|
720
|
-
if (r.requireSource && !sameSnapshot(pending.body.snapshot, fresh.snapshot))
|
|
721
|
-
return "A pending upload includes a different source snapshot. Check its status before sharing again.";
|
|
722
|
-
return null;
|
|
723
|
-
}
|
|
724
|
-
|
|
725
640
|
/** @param {Required<VersionRequest>} r @param {VersionRecord} state
|
|
726
641
|
* @param {NonNullable<VersionRecord["pending"]>} pending @param {string} id @param {ExportDTO} done @param {boolean} first */
|
|
727
642
|
async function finishRun(r, state, pending, id, done, first) {
|
|
728
|
-
if (going(done)) return
|
|
643
|
+
if (going(done)) return stillGoing(done);
|
|
729
644
|
const failed = await finish(r.projectPath, state, pending, id, done, r.fromReview);
|
|
730
645
|
if (!failed) {
|
|
731
646
|
const fromReview = pending.reviewSent?.value;
|
|
@@ -748,11 +663,8 @@ export async function addVersion(request) {
|
|
|
748
663
|
now: () => Date.now(),
|
|
749
664
|
onProgress: () => {},
|
|
750
665
|
open: () => true,
|
|
751
|
-
|
|
752
|
-
filesForExport: noSource,
|
|
666
|
+
filesForExport: async () => null,
|
|
753
667
|
fromReview: null,
|
|
754
|
-
requireSource: false,
|
|
755
|
-
omitSource: false,
|
|
756
668
|
onAdded: () => {},
|
|
757
669
|
...request,
|
|
758
670
|
projectId: request.projectId ?? null,
|
|
@@ -843,7 +755,7 @@ async function settleSavedExport(dir, client, fromReview) {
|
|
|
843
755
|
const id = await reviewProjectId(dir);
|
|
844
756
|
if (!pending?.videoId || !id || pending.projectId !== id) return;
|
|
845
757
|
const done = await client.readExport(id, pending.videoId);
|
|
846
|
-
if (
|
|
758
|
+
if (going(done)) return;
|
|
847
759
|
const failure = await finish(dir, state, pending, id, done, fromReview);
|
|
848
760
|
const key = normalize(dir);
|
|
849
761
|
if (failure) failures.set(key, { ...failure, exportKey: exportKeyOf(dir) });
|
|
@@ -913,23 +825,3 @@ export async function pendingReviewExport(projectPath) {
|
|
|
913
825
|
checksum: pending.body.local_upload.checksum_sha256,
|
|
914
826
|
};
|
|
915
827
|
}
|
|
916
|
-
|
|
917
|
-
/** @param {NonNullable<VersionRecord["pending"]>} pending @param {string} id @param {ExportDTO} done */
|
|
918
|
-
function readyRecord(pending, id, done) {
|
|
919
|
-
return {
|
|
920
|
-
fingerprint: pending.body.source_fingerprint ?? "",
|
|
921
|
-
versionId: done.version_id,
|
|
922
|
-
exportedAt: done.exported_at,
|
|
923
|
-
projectId: id,
|
|
924
|
-
files: Boolean(pending.body.snapshot),
|
|
925
|
-
...(pending.body.snapshot && { snapshot: pending.body.snapshot }),
|
|
926
|
-
checksum: pending.body.local_upload.checksum_sha256,
|
|
927
|
-
};
|
|
928
|
-
}
|
|
929
|
-
|
|
930
|
-
/** @param {NonNullable<VersionRecord["pending"]>} pending @param {string} dir */
|
|
931
|
-
function bindRenderKey(pending, dir) {
|
|
932
|
-
return typeof pending.renderKey === "string"
|
|
933
|
-
? pending
|
|
934
|
-
: { ...pending, renderKey: exportKeyOf(dir) };
|
|
935
|
-
}
|
package/src/reviewUpload.mjs
CHANGED
|
@@ -18,22 +18,6 @@ export function fileSha256Base64(file) {
|
|
|
18
18
|
|
|
19
19
|
/** @typedef {"sent" | "expired" | "refused" | "offline"} PutOutcome */
|
|
20
20
|
|
|
21
|
-
/** @param {string} url */
|
|
22
|
-
export function trustedUploadUrl(url) {
|
|
23
|
-
if (!URL.canParse(url)) return null;
|
|
24
|
-
const target = new URL(url);
|
|
25
|
-
const loopback = target.protocol === "http:" && target.hostname === "127.0.0.1";
|
|
26
|
-
if (
|
|
27
|
-
(target.protocol !== "https:" && !loopback) ||
|
|
28
|
-
target.username ||
|
|
29
|
-
target.password ||
|
|
30
|
-
target.hash ||
|
|
31
|
-
!reviewUploadOrigins().includes(target.origin)
|
|
32
|
-
)
|
|
33
|
-
return null;
|
|
34
|
-
return target;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
21
|
/** Streams the file to the signed URL through main's fetch (the app's proxy), the server's headers as given plus
|
|
38
22
|
* the length, and no bearer. Nothing sent or answered for `stallMs` ends it as offline, never "Uploading" forever.
|
|
39
23
|
* @param {{ put_url: string, headers: Record<string, string> }} upload @param {string} file @param {number} size
|
|
@@ -45,8 +29,22 @@ export async function putFile(
|
|
|
45
29
|
onSent,
|
|
46
30
|
{ stallMs = 60_000, fetchImpl = fetch } = {},
|
|
47
31
|
) {
|
|
48
|
-
|
|
49
|
-
|
|
32
|
+
let target;
|
|
33
|
+
try {
|
|
34
|
+
target = new URL(upload.put_url);
|
|
35
|
+
} catch {
|
|
36
|
+
return "refused";
|
|
37
|
+
}
|
|
38
|
+
const loopback = target.protocol === "http:" && target.hostname === "127.0.0.1";
|
|
39
|
+
if (
|
|
40
|
+
(target.protocol !== "https:" && !loopback) ||
|
|
41
|
+
target.username ||
|
|
42
|
+
target.password ||
|
|
43
|
+
target.hash ||
|
|
44
|
+
!reviewUploadOrigins().includes(target.origin)
|
|
45
|
+
) {
|
|
46
|
+
return "refused";
|
|
47
|
+
}
|
|
50
48
|
const stall = new AbortController();
|
|
51
49
|
/** @type {ReturnType<typeof setTimeout> | undefined} */
|
|
52
50
|
let timer;
|
package/dist/shareSources.d.mts
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
/** Content identity for sharing, including portable hidden render inputs. The Desktop watch inventory stays separate.
|
|
2
|
-
* @param {string} projectPath @returns {Promise<string | null>} */
|
|
3
|
-
export function shareSourceFingerprint(projectPath: string): Promise<string | null>;
|
|
4
|
-
/** Prepare source explicitly before adding the video version. No unexpected network or filesystem failure is
|
|
5
|
-
* converted to video-only. `filesForExport` revalidates the prepared bytes and never starts another upload.
|
|
6
|
-
* @param {import("./sources/adapterTypes.js").SourceOptions} options */
|
|
7
|
-
export function shareSources(options: import("./sources/adapterTypes.js").SourceOptions): {
|
|
8
|
-
prepare: (projectPath: string, rendered: string | null) => Promise<SourceResult>;
|
|
9
|
-
filesForExport: (projectPath: string, rendered: string | null) => Promise<import("./types/reviewWire.js").SnapshotRef | null>;
|
|
10
|
-
};
|
|
11
|
-
export { createProjectShareSourceStore } from "./sources/store.mjs";
|
|
12
|
-
export type Preparation = {
|
|
13
|
-
binding: Binding;
|
|
14
|
-
own: boolean;
|
|
15
|
-
external: boolean;
|
|
16
|
-
api: import("./sources/api.mjs").SourceApi;
|
|
17
|
-
store: import("./sources/adapterTypes.js").SourceStore;
|
|
18
|
-
options: import("./sources/adapterTypes.js").SourceOptions;
|
|
19
|
-
signal: AbortSignal;
|
|
20
|
-
fingerprint: (path: string) => Promise<string | null>;
|
|
21
|
-
rendered: string;
|
|
22
|
-
};
|
|
23
|
-
export type SourceResult = import("./sources/adapterTypes.js").SourceResult;
|
|
24
|
-
export type Binding = import("./sources/types.js").Binding;
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import type { Binding, BindingKey, Manifest } from "./types.js";
|
|
2
|
-
import type { SnapshotRef } from "../types/reviewWire.js";
|
|
3
|
-
import type { SignedInAccess } from "../signedInRequest.mjs";
|
|
4
|
-
import type { SourceApi } from "./api.mjs";
|
|
5
|
-
|
|
6
|
-
export interface SourceStore {
|
|
7
|
-
read(key: BindingKey): Promise<Binding | null>;
|
|
8
|
-
write(binding: Binding): Promise<void>;
|
|
9
|
-
}
|
|
10
|
-
export type SourceResult =
|
|
11
|
-
| { at: "ready"; snapshot: SnapshotRef }
|
|
12
|
-
| { at: "unavailable"; reason: string };
|
|
13
|
-
export interface SourceOptions {
|
|
14
|
-
access: SignedInAccess;
|
|
15
|
-
spaceId: string;
|
|
16
|
-
store?: SourceStore;
|
|
17
|
-
enabled?: () => boolean;
|
|
18
|
-
fingerprint?: (path: string) => Promise<string | null>;
|
|
19
|
-
/** Host-owned Desktop bindings may be read for exact reuse, never overwritten. */
|
|
20
|
-
existingBinding?: (key: BindingKey) => Promise<Binding | null>;
|
|
21
|
-
fetchImpl?: typeof fetch;
|
|
22
|
-
signal?: AbortSignal;
|
|
23
|
-
verificationTimeoutMs?: number;
|
|
24
|
-
onProgress?: (bytes: number) => void;
|
|
25
|
-
/** Test/host seams; the default is the authenticated v3 client. */
|
|
26
|
-
api?: SourceApi;
|
|
27
|
-
wait?: (ms: number, signal?: AbortSignal) => Promise<void>;
|
|
28
|
-
}
|
|
29
|
-
export interface SourceRun {
|
|
30
|
-
binding: Binding;
|
|
31
|
-
manifest: Manifest;
|
|
32
|
-
api: SourceApi;
|
|
33
|
-
store: SourceStore;
|
|
34
|
-
fetchImpl: typeof fetch;
|
|
35
|
-
signal: AbortSignal;
|
|
36
|
-
wait: (ms: number, signal?: AbortSignal) => Promise<void>;
|
|
37
|
-
timeoutMs: number;
|
|
38
|
-
onProgress: (bytes: number) => void;
|
|
39
|
-
}
|
package/dist/sources/api.d.mts
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
/** @param {{ access: import("../signedInRequest.mjs").SignedInAccess, fetchImpl?: typeof fetch }} deps */
|
|
2
|
-
export function sourceApi({ access, fetchImpl }: {
|
|
3
|
-
access: import("../signedInRequest.mjs").SignedInAccess;
|
|
4
|
-
fetchImpl?: typeof fetch;
|
|
5
|
-
}): {
|
|
6
|
-
/** @param {string} space */
|
|
7
|
-
limits: (space: string) => Promise<{
|
|
8
|
-
at: "read";
|
|
9
|
-
syncAllowed: boolean;
|
|
10
|
-
maxCompositions: number;
|
|
11
|
-
compositions: number;
|
|
12
|
-
push: import("./types.js").Limits;
|
|
13
|
-
}>;
|
|
14
|
-
/** @param {string} space @param {string} composition @param {string} title */
|
|
15
|
-
create: (space: string, composition: string, title: string) => Promise<import("./types.js").Composition>;
|
|
16
|
-
/** @param {string} space @param {string} composition @param {string} snapshot */
|
|
17
|
-
snapshot: (space: string, composition: string, snapshot: string) => Promise<import("./types.js").Snapshot>;
|
|
18
|
-
/** @param {string} space @param {string} composition @param {import("./types.js").BeginRequest} body */
|
|
19
|
-
begin: (space: string, composition: string, body: import("./types.js").BeginRequest) => Promise<import("./types.js").BeginResult>;
|
|
20
|
-
/** @param {string} space @param {string} composition @param {string} upload @param {import("./types.js").GrantRequest[]} files */
|
|
21
|
-
uploadUrls: (space: string, composition: string, upload: string, files: import("./types.js").GrantRequest[]) => Promise<import("./types.js").FileGrant[]>;
|
|
22
|
-
/** @param {string} space @param {string} composition @param {string} upload */
|
|
23
|
-
status: (space: string, composition: string, upload: string) => Promise<import("./types.js").UploadReceipt>;
|
|
24
|
-
/** @param {string} space @param {string} composition @param {string} upload */
|
|
25
|
-
cancel: (space: string, composition: string, upload: string) => Promise<import("./types.js").UploadReceipt>;
|
|
26
|
-
/** @param {string} space @param {string} composition @param {string} upload */
|
|
27
|
-
submit: (space: string, composition: string, upload: string) => Promise<import("./types.js").UploadReceipt>;
|
|
28
|
-
};
|
|
29
|
-
export type SourceApi = ReturnType<typeof sourceApi>;
|