@hyperframes/share 0.9.28-alpha.1 → 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.
Files changed (56) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +1 -58
  3. package/dist/index.d.mts +0 -1
  4. package/dist/projectLock.d.mts +2 -2
  5. package/dist/projectSnapshot.d.mts +1 -3
  6. package/dist/reviewClient.d.mts +16 -13
  7. package/dist/reviewExport.d.mts +1 -11
  8. package/dist/reviewUpload.d.mts +0 -2
  9. package/package.json +1 -1
  10. package/scripts/copy-types.mjs +0 -3
  11. package/src/index.mjs +0 -6
  12. package/src/projectLock.mjs +2 -9
  13. package/src/projectSnapshot.mjs +6 -22
  14. package/src/reviewClient.mjs +83 -48
  15. package/src/reviewExport.mjs +41 -156
  16. package/src/reviewUpload.mjs +16 -18
  17. package/src/reviewVersionSentences.mjs +1 -2
  18. package/dist/shareSources.d.mts +0 -24
  19. package/dist/sourceIgnore.d.mts +0 -3
  20. package/dist/sources/adapterTypes.d.ts +0 -39
  21. package/dist/sources/api.d.mts +0 -29
  22. package/dist/sources/capture.d.mts +0 -24
  23. package/dist/sources/contract.d.mts +0 -2
  24. package/dist/sources/errors.d.mts +0 -24
  25. package/dist/sources/limits.d.mts +0 -12
  26. package/dist/sources/manifest.d.mts +0 -23
  27. package/dist/sources/pathPolicy.d.mts +0 -25
  28. package/dist/sources/projectState.d.mts +0 -9
  29. package/dist/sources/store.d.mts +0 -3
  30. package/dist/sources/transfer.d.mts +0 -45
  31. package/dist/sources/types.d.ts +0 -353
  32. package/dist/sources/unicode.d.mts +0 -15
  33. package/dist/sources/upload.d.mts +0 -9
  34. package/dist/sources/uploadFiles.d.mts +0 -31
  35. package/dist/sources/wire.d.mts +0 -21
  36. package/src/shareSources.mjs +0 -236
  37. package/src/sourceIgnore.mjs +0 -69
  38. package/src/sources/adapterTypes.d.ts +0 -39
  39. package/src/sources/api.mjs +0 -114
  40. package/src/sources/capture.mjs +0 -264
  41. package/src/sources/contract/LICENSE-Unicode.txt +0 -39
  42. package/src/sources/contract/path-policy.json +0 -311
  43. package/src/sources/contract/unicode-15.1.0.json +0 -6179
  44. package/src/sources/contract.mjs +0 -13
  45. package/src/sources/errors.mjs +0 -77
  46. package/src/sources/limits.mjs +0 -48
  47. package/src/sources/manifest.mjs +0 -154
  48. package/src/sources/pathPolicy.mjs +0 -197
  49. package/src/sources/projectState.mjs +0 -59
  50. package/src/sources/store.mjs +0 -82
  51. package/src/sources/transfer.mjs +0 -196
  52. package/src/sources/types.d.ts +0 -353
  53. package/src/sources/unicode.mjs +0 -163
  54. package/src/sources/upload.mjs +0 -201
  55. package/src/sources/uploadFiles.mjs +0 -263
  56. package/src/sources/wire.mjs +0 -205
@@ -1,236 +0,0 @@
1
- // @ts-check
2
- import { realpath } from "node:fs/promises";
3
- import { randomUUID } from "node:crypto";
4
- import { basename } from "node:path";
5
- import { projectFingerprint } from "./projectSnapshot.mjs";
6
- import { withProjectShareLock } from "./projectLock.mjs";
7
- import { captureProject } from "./sources/capture.mjs";
8
- import { sourceApi } from "./sources/api.mjs";
9
- import { createProjectShareSourceStore } from "./sources/store.mjs";
10
- import { realWait } from "./sources/transfer.mjs";
11
- import { recoverSourceUpload, uploadSource } from "./sources/upload.mjs";
12
-
13
- /** @typedef {import("./sources/adapterTypes.js").SourceResult} SourceResult
14
- * @typedef {import("./sources/types.js").Binding} Binding */
15
- /** @param {string} reason @returns {SourceResult} */
16
- const unavailable = (reason) => ({ at: "unavailable", reason });
17
- /** Content identity for sharing, including portable hidden render inputs. The Desktop watch inventory stays separate.
18
- * @param {string} projectPath @returns {Promise<string | null>} */
19
- export async function shareSourceFingerprint(projectPath) {
20
- const capture = await captureProject(await realpath(projectPath));
21
- return capture.at === "captured" && !captureRefusal(capture) ? capture.manifest.sha256 : null;
22
- }
23
- /** @param {Binding} binding @param {string} space */
24
- function bindingRefusal(binding, space) {
25
- if (binding.ownerSpaceId !== space) return "source_other_workspace";
26
- if (binding.sharedFrom) return "source_owned_by_someone_else";
27
- if (binding.gettingFromCloud || binding.duplicate) return "source_operation_in_progress";
28
- return null;
29
- }
30
- /** @param {import("./sources/types.js").Capture} capture */
31
- function captureRefusal(capture) {
32
- if (capture.at === "too-big") return `source_${capture.over.limit}_exceeded`;
33
- if (capture.at === "problems") return `source_${capture.problems[0]?.problem ?? "invalid"}`;
34
- if (capture.settled.agentTurnOpen || capture.settled.projectLocked || capture.waiting.length)
35
- return "source_edit_in_progress";
36
- return null;
37
- }
38
-
39
- /** Prepare source explicitly before adding the video version. No unexpected network or filesystem failure is
40
- * converted to video-only. `filesForExport` revalidates the prepared bytes and never starts another upload.
41
- * @param {import("./sources/adapterTypes.js").SourceOptions} options */
42
- export function shareSources(options) {
43
- const store = options.store ?? createProjectShareSourceStore();
44
- const fingerprint = options.fingerprint ?? projectFingerprint;
45
- const signal = options.signal ?? new AbortController().signal;
46
- /** @type {Map<string, { rendered: string, digest: string, snapshot: import("./types/reviewWire.js").SnapshotRef, account: string, backend: string }>} */
47
- const prepared = new Map();
48
- /** @param {string} projectPath @param {string | null} rendered @returns {Promise<SourceResult>} */
49
- const prepare = async (projectPath, rendered) => {
50
- const root = await realpath(projectPath);
51
- prepared.delete(root);
52
- if (!enabled(options)) return unavailable("source_disabled");
53
- if (!rendered || (await fingerprint(root)) !== rendered)
54
- return unavailable("source_render_changed");
55
- return withProjectShareLock(
56
- root,
57
- "source",
58
- async () => {
59
- const signed = await options.access();
60
- if (!signed?.owner) return unavailable("source_signed_out");
61
- const key = { backend: signed.heygenApi, account: signed.owner, projectPath: root };
62
- const guardedAccess = async (/** @type {string | undefined} */ refused) => {
63
- const next = await options.access(refused);
64
- if (!next || next.owner !== key.account || next.heygenApi !== key.backend)
65
- throw new Error("Source account or backend changed while sharing.");
66
- return next;
67
- };
68
- const api =
69
- options.api ?? sourceApi({ access: guardedAccess, fetchImpl: options.fetchImpl });
70
- const { own, existing, binding } = await selectBinding(key, options, store);
71
- const refused = bindingRefusal(binding, options.spaceId);
72
- if (refused) return unavailable(refused);
73
- const result = await prepareBinding({
74
- binding,
75
- own: Boolean(own),
76
- external: Boolean(existing),
77
- api,
78
- store,
79
- options,
80
- signal,
81
- fingerprint,
82
- rendered,
83
- });
84
- const manifest = result.manifest;
85
- await guardedAccess(undefined);
86
- if (result.at !== "ready" || !manifest) return result;
87
- if (!(await stillMatches(root, rendered, manifest.sha256, fingerprint)))
88
- return unavailable("source_render_changed");
89
- prepared.set(root, {
90
- rendered,
91
- digest: manifest.sha256,
92
- snapshot: result.snapshot,
93
- account: key.account,
94
- backend: key.backend,
95
- });
96
- return { at: "ready", snapshot: result.snapshot };
97
- },
98
- { signal },
99
- );
100
- };
101
- /** @param {string} projectPath @param {string | null} rendered */
102
- const filesForExport = async (projectPath, rendered) => {
103
- const root = await realpath(projectPath);
104
- const held = prepared.get(root);
105
- const signed = await options.access();
106
- if (
107
- !held ||
108
- !rendered ||
109
- held.rendered !== rendered ||
110
- !sameOwner(signed, held) ||
111
- !enabled(options)
112
- )
113
- return null;
114
- return (await stillMatches(root, rendered, held.digest, fingerprint)) ? held.snapshot : null;
115
- };
116
- return { prepare, filesForExport };
117
- }
118
-
119
- /** @param {import("./sources/adapterTypes.js").SourceOptions} options */
120
- const enabled = (options) => options.enabled?.() ?? true;
121
- /** @param {Awaited<ReturnType<import("./signedInRequest.mjs").SignedInAccess>>} signed
122
- * @param {{account: string, backend: string}} held */
123
- const sameOwner = (signed, held) =>
124
- signed?.owner === held.account && signed?.heygenApi === held.backend;
125
- /** @typedef {{ binding: Binding, own: boolean, external: boolean, api: import("./sources/api.mjs").SourceApi,
126
- * store: import("./sources/adapterTypes.js").SourceStore, options: import("./sources/adapterTypes.js").SourceOptions,
127
- * signal: AbortSignal, fingerprint: (path: string) => Promise<string | null>, rendered: string }} Preparation */
128
- /** @param {Preparation} job @returns {Promise<SourceResult & {manifest?: import("./sources/types.js").Manifest}>} */
129
- async function prepareBinding(job) {
130
- const { binding, api, fingerprint, rendered } = job;
131
- const limits = await api.limits(binding.ownerSpaceId);
132
- if (!limits.syncAllowed) return unavailable("source_plan_required");
133
- const capture = await captureProject(binding.projectPath, { limits: limits.push });
134
- const bad = captureRefusal(capture);
135
- if (bad) return unavailable(bad);
136
- if (capture.at !== "captured") return unavailable("source_invalid");
137
- if ((await fingerprint(binding.projectPath)) !== rendered)
138
- return unavailable("source_render_changed");
139
- const result = await snapshotFor(job, capture.manifest, limits);
140
- return { ...result, manifest: capture.manifest };
141
- }
142
- /** @param {Preparation} job @param {import("./sources/types.js").Manifest} manifest
143
- * @param {Extract<import("./sources/types.js").SyncLimitsRead, {at: "read"}>} limits @returns {Promise<SourceResult>} */
144
- async function snapshotFor(job, manifest, limits) {
145
- const { binding, api, store, options, signal } = job;
146
- const blocked = await recoverEarlierCapture(job, manifest.sha256);
147
- if (blocked) return blocked;
148
- if (binding.base?.manifestSha256 === manifest.sha256 && !binding.inflight)
149
- return reuseSnapshot(api, binding, manifest.sha256);
150
- if (job.external) return unavailable("source_sync_required");
151
- if (!job.own && limits.compositions >= limits.maxCompositions)
152
- return unavailable("source_composition_limit");
153
- await store.write(binding);
154
- binding.composition = await api.create(
155
- binding.ownerSpaceId,
156
- binding.compositionId,
157
- basename(binding.projectPath),
158
- );
159
- await store.write(binding);
160
- return uploadSource({ binding, manifest, api, store, signal, ...uploadOptions(options) });
161
- }
162
- /** @param {Preparation} job @param {string} digest */
163
- function recoverEarlierCapture({ external, binding, api, store }, digest) {
164
- const inflight = binding.inflight;
165
- if (external || !inflight || inflight.manifestSha256 === digest) return null;
166
- return recoverSourceUpload({ binding, api, store });
167
- }
168
-
169
- /** @param {import("./sources/adapterTypes.js").SourceOptions} options */
170
- function uploadOptions(options) {
171
- return {
172
- fetchImpl: options.fetchImpl ?? fetch,
173
- wait: options.wait ?? realWait,
174
- timeoutMs: options.verificationTimeoutMs ?? 120_000,
175
- onProgress: options.onProgress ?? (() => {}),
176
- };
177
- }
178
-
179
- /** @param {import("./sources/types.js").BindingKey} key @param {import("./sources/adapterTypes.js").SourceOptions} options
180
- * @param {import("./sources/adapterTypes.js").SourceStore} store */
181
- async function selectBinding(key, options, store) {
182
- const own = await store.read(key);
183
- const existing = own ? null : await options.existingBinding?.(key);
184
- return { own, existing, binding: own ?? existing ?? newBinding(key, options.spaceId) };
185
- }
186
- /** @param {import("./sources/types.js").BindingKey} key @param {string} space @returns {Binding} */
187
- function newBinding(key, space) {
188
- return {
189
- ...key,
190
- version: 1,
191
- compositionId: randomUUID(),
192
- ownerSpaceId: space,
193
- boundAt: Date.now(),
194
- composition: null,
195
- etag: null,
196
- base: null,
197
- inflight: null,
198
- };
199
- }
200
- /** @param {import("./sources/api.mjs").SourceApi} api @param {Binding} binding @param {string} digest @returns {Promise<SourceResult>} */
201
- async function reuseSnapshot(api, binding, digest) {
202
- if (!binding.base) return unavailable("source_sync_required");
203
- const remote = await api.snapshot(
204
- binding.ownerSpaceId,
205
- binding.compositionId,
206
- binding.base.snapshotId,
207
- );
208
- if (
209
- remote.id !== binding.base.snapshotId ||
210
- remote.compositionId !== binding.compositionId ||
211
- remote.manifestSha256 !== digest ||
212
- remote.manifest?.sha256 !== digest ||
213
- remote.publicationOutcome !== "published"
214
- )
215
- return unavailable("source_snapshot_mismatch");
216
- return {
217
- at: "ready",
218
- snapshot: {
219
- space_id: binding.ownerSpaceId,
220
- composition_id: binding.compositionId,
221
- snapshot_id: remote.id,
222
- },
223
- };
224
- }
225
- /** @param {string} root @param {string} rendered @param {string} digest @param {(path: string) => Promise<string | null>} fingerprint */
226
- async function stillMatches(root, rendered, digest, fingerprint) {
227
- if ((await fingerprint(root)) !== rendered) return false;
228
- const capture = await captureProject(root);
229
- return (
230
- !captureRefusal(capture) &&
231
- capture.at === "captured" &&
232
- capture.manifest.sha256 === digest &&
233
- (await fingerprint(root)) === rendered
234
- );
235
- }
236
- export { createProjectShareSourceStore } from "./sources/store.mjs";
@@ -1,69 +0,0 @@
1
- // @ts-check
2
- import { constants } from "node:fs";
3
- import { lstat, open } from "node:fs/promises";
4
- import { join } from "node:path";
5
-
6
- /** Explicit, project-relative exclusions. This only removes files; it cannot override safety exclusions.
7
- * @param {string} root @returns {Promise<(path: string) => boolean>} */
8
- export async function readSourceIgnore(root) {
9
- const file = join(root, ".hyperframesignore");
10
- const invalid = () =>
11
- new Error(".hyperframesignore must be a regular file inside the project, smaller than 64 KiB.");
12
- const handle = await open(
13
- file,
14
- constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0),
15
- ).catch(async (/** @type {NodeJS.ErrnoException} */ error) => {
16
- if (error.code !== "ENOENT") throw invalid();
17
- // An absent file means no exclusions; a dangling symlink is invalid.
18
- const named = await lstat(file).catch((/** @type {NodeJS.ErrnoException} */ missing) => {
19
- if (missing.code === "ENOENT") return null;
20
- throw invalid();
21
- });
22
- if (named) throw invalid();
23
- return null;
24
- });
25
- if (!handle) return () => false;
26
- let text;
27
- try {
28
- // Validate the opened descriptor before reading. lstat additionally rejects links on
29
- // platforms without O_NOFOLLOW; it never authorizes a later open of this path.
30
- const [stat, named] = await Promise.all([handle.stat(), lstat(file)]);
31
- if (
32
- !stat.isFile() ||
33
- stat.size >= 65536 ||
34
- !named.isFile() ||
35
- stat.ino !== named.ino ||
36
- stat.dev !== named.dev
37
- )
38
- throw invalid();
39
- const buffer = Buffer.alloc(65536);
40
- let length = 0;
41
- while (length < buffer.length) {
42
- const { bytesRead } = await handle.read(buffer, length, buffer.length - length, length);
43
- if (!bytesRead) break;
44
- length += bytesRead;
45
- }
46
- if (length === buffer.length) throw invalid();
47
- text = buffer.subarray(0, length).toString("utf8");
48
- } finally {
49
- await handle.close();
50
- }
51
- const paths = text
52
- .split(/\r?\n/)
53
- .map((line) => line.trim())
54
- .filter((line) => line && !line.startsWith("#"));
55
- const entries = paths.map((entry) => {
56
- const path = entry.replace(/\/$/, "");
57
- if (
58
- path === "index.html" ||
59
- /[\\*?![\]:]/.test(path) ||
60
- path.includes("\0") ||
61
- path.split("/").some((part) => !part || part === "." || part === "..")
62
- )
63
- throw new Error(
64
- ".hyperframesignore accepts literal project-relative file or folder paths, one per line. Wildcards and excluding index.html are not supported.",
65
- );
66
- return path;
67
- });
68
- return (path) => entries.some((entry) => path === entry || path.startsWith(`${entry}/`));
69
- }
@@ -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
- }
@@ -1,114 +0,0 @@
1
- // @ts-check
2
- import { signedInRequest } from "../signedInRequest.mjs";
3
- import { SyncApiError, refusalOf } from "./errors.mjs";
4
- import {
5
- beginOf,
6
- grantsOf,
7
- objectOf,
8
- uploadOf,
9
- syncLimitsOf,
10
- compositionOf,
11
- snapshotOf,
12
- } from "./wire.mjs";
13
-
14
- /** @param {{ access: import("../signedInRequest.mjs").SignedInAccess, fetchImpl?: typeof fetch }} deps */
15
- export function sourceApi({ access, fetchImpl = fetch }) {
16
- const request = signedInRequest({
17
- access,
18
- fetchImpl,
19
- timeoutMs: 120_000,
20
- tag: "share-source",
21
- host: "heygenApi",
22
- offline: () => new SyncApiError("offline"),
23
- signedOut: () => new SyncApiError("signed-out"),
24
- });
25
- /** @param {string} method @param {string} route @param {unknown} [body] @param {Record<string,string>} [headers] */
26
- const send = async (method, route, body, headers) => {
27
- const { reply } = await request(method, route, body, headers);
28
- if (!reply.ok) throw await refusalOf(reply);
29
- return objectOf(await reply.json(), "envelope").data;
30
- };
31
- /** @param {string} space @param {string} composition */
32
- const path = (space, composition) =>
33
- `/v3/hyperframes/workspaces/${encodeURIComponent(space)}/compositions/${encodeURIComponent(composition)}`;
34
- return {
35
- /** @param {string} space */
36
- limits: async (space) =>
37
- syncLimitsOf(
38
- await send(
39
- "GET",
40
- `/v3/hyperframes/workspaces/${encodeURIComponent(space)}/composition-sync/limits`,
41
- ),
42
- ),
43
- /** @param {string} space @param {string} composition @param {string} title */
44
- create: async (space, composition, title) =>
45
- compositionOf(await send("PUT", path(space, composition), { title })),
46
- /** @param {string} space @param {string} composition @param {string} snapshot */
47
- snapshot: async (space, composition, snapshot) =>
48
- snapshotOf(
49
- await send(
50
- "GET",
51
- `${path(space, composition)}/snapshots/${encodeURIComponent(snapshot)}?include=manifest`,
52
- ),
53
- ),
54
- /** @param {string} space @param {string} composition @param {import("./types.js").BeginRequest} body */
55
- begin: async (space, composition, body) =>
56
- beginOf(
57
- await send(
58
- "POST",
59
- `${path(space, composition)}/uploads`,
60
- {
61
- base_snapshot_id: body.baseSnapshotId,
62
- resolves_conflict_snapshot_ids: [],
63
- client_type: "cli",
64
- manifest_sha256: body.manifest.sha256,
65
- manifest: body.manifest.document,
66
- },
67
- { "Idempotency-Key": body.idempotencyKey },
68
- ),
69
- false,
70
- ),
71
- /** @param {string} space @param {string} composition @param {string} upload @param {import("./types.js").GrantRequest[]} files */
72
- uploadUrls: async (space, composition, upload, files) =>
73
- grantsOf(
74
- await send(
75
- "POST",
76
- `${path(space, composition)}/uploads/${encodeURIComponent(upload)}/upload-urls`,
77
- {
78
- files: files.map((file) => ({
79
- sha256: file.sha256,
80
- ...(file.multipartUploadId && { multipart_upload_id: file.multipartUploadId }),
81
- ...(file.parts && {
82
- parts: file.parts.map((part) => ({
83
- part_number: part.partNumber,
84
- sha256: part.sha256,
85
- })),
86
- }),
87
- })),
88
- },
89
- ),
90
- ),
91
- /** @param {string} space @param {string} composition @param {string} upload */
92
- status: async (space, composition, upload) =>
93
- uploadOf(
94
- await send("GET", `${path(space, composition)}/uploads/${encodeURIComponent(upload)}`),
95
- ),
96
- /** @param {string} space @param {string} composition @param {string} upload */
97
- cancel: async (space, composition, upload) =>
98
- uploadOf(
99
- await send(
100
- "POST",
101
- `${path(space, composition)}/uploads/${encodeURIComponent(upload)}/cancel`,
102
- ),
103
- ),
104
- /** @param {string} space @param {string} composition @param {string} upload */
105
- submit: async (space, composition, upload) =>
106
- uploadOf(
107
- await send(
108
- "POST",
109
- `${path(space, composition)}/uploads/${encodeURIComponent(upload)}/verification`,
110
- ),
111
- ),
112
- };
113
- }
114
- /** @typedef {ReturnType<typeof sourceApi>} SourceApi */