@hyperframes/share 0.0.0-stage → 0.9.22
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 +51 -2
- package/dist/appOwnedPaths.d.mts +6 -0
- package/dist/fromReview.d.mts +15 -0
- package/dist/grantRefusal.d.mts +8 -0
- package/dist/host.d.mts +29 -0
- package/dist/hosts.d.ts +39 -0
- package/dist/index.d.mts +21 -0
- package/dist/projectLock.d.mts +17 -0
- package/dist/projectSnapshot.d.mts +20 -0
- package/dist/reviewAudience.d.mts +3 -0
- package/dist/reviewClient.d.mts +122 -0
- package/dist/reviewExport.d.mts +172 -0
- package/dist/reviewFiles.d.mts +43 -0
- package/dist/reviewLinkStates.d.mts +18 -0
- package/dist/reviewOpenCalls.d.mts +16 -0
- package/dist/reviewShare.d.mts +106 -0
- package/dist/reviewSwitch.d.mts +53 -0
- package/dist/reviewUpload.d.mts +16 -0
- package/dist/reviewVersionSentences.d.mts +2 -0
- package/dist/signedInRequest.d.mts +56 -0
- package/dist/storage.d.mts +20 -0
- package/dist/types/access.d.ts +12 -0
- package/dist/types/review.d.ts +59 -0
- package/dist/types/reviewWire.d.ts +200 -0
- package/dist/types/share.d.ts +119 -0
- package/dist/types/shareOpen.d.ts +96 -0
- package/package.json +112 -3
- package/scripts/copy-types.mjs +3 -0
- package/src/appOwnedPaths.mjs +27 -0
- package/src/fromReview.mjs +27 -0
- package/src/grantRefusal.mjs +28 -0
- package/src/host.mjs +46 -0
- package/src/hosts.d.ts +39 -0
- package/src/index.mjs +82 -0
- package/src/projectLock.mjs +120 -0
- package/src/projectSnapshot.mjs +120 -0
- package/src/reviewAudience.mjs +24 -0
- package/src/reviewClient.mjs +260 -0
- package/src/reviewExport.mjs +827 -0
- package/src/reviewFiles.mjs +98 -0
- package/src/reviewLinkStates.mjs +47 -0
- package/src/reviewOpenCalls.mjs +40 -0
- package/src/reviewShare.mjs +645 -0
- package/src/reviewSwitch.mjs +430 -0
- package/src/reviewUpload.mjs +98 -0
- package/src/reviewVersionSentences.mjs +48 -0
- package/src/signedInRequest.mjs +125 -0
- package/src/storage.mjs +116 -0
- package/src/types/access.d.ts +12 -0
- package/src/types/review.d.ts +59 -0
- package/src/types/reviewWire.d.ts +200 -0
- package/src/types/share.d.ts +119 -0
- package/src/types/shareOpen.d.ts +96 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
/** The sign-in exists but its renewal could not reach HeyGen: a reader throws it, and a request reads it as offline,
|
|
4
|
+
* never as signed out. */
|
|
5
|
+
export class AccountUnreachable extends Error {
|
|
6
|
+
constructor() {
|
|
7
|
+
super("the saved sign-in could not be renewed: HeyGen was unreachable");
|
|
8
|
+
this.name = "AccountUnreachable";
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** What a sign-in read yields, through readOrUnreachable, when its renewal could not reach HeyGen. */
|
|
13
|
+
export const UNREACHABLE = /** @type {"unreachable"} */ ("unreachable");
|
|
14
|
+
|
|
15
|
+
/** `access(refused)`, or UNREACHABLE when the saved sign-in's renewal could not reach HeyGen; other errors rethrow.
|
|
16
|
+
* @template T @param {(refused?: string) => Promise<T>} access @param {string} [refused] */
|
|
17
|
+
export async function readOrUnreachable(access, refused) {
|
|
18
|
+
try {
|
|
19
|
+
return await access(refused);
|
|
20
|
+
} catch (error) {
|
|
21
|
+
if (error instanceof AccountUnreachable) return UNREACHABLE;
|
|
22
|
+
throw error;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** `run`, answering `value` when the saved sign-in's renewal could not reach HeyGen; other errors rethrow. A probe's
|
|
27
|
+
* `access` is `orIfUnreachable(access, null)`. @template {any[]} A, R @param {(...args: A) => Promise<R>} run
|
|
28
|
+
* @param {R} value */
|
|
29
|
+
export const orIfUnreachable =
|
|
30
|
+
(run, value) =>
|
|
31
|
+
async (/** @type {A} */ ...args) => {
|
|
32
|
+
try {
|
|
33
|
+
return await run(...args);
|
|
34
|
+
} catch (error) {
|
|
35
|
+
if (error instanceof AccountUnreachable) return value;
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/** The sign-in each send reads again: the bearer, the backend it is for and the account it is; null signed out, and
|
|
41
|
+
* a reader may throw AccountUnreachable. A host's reader may take a bearer a server refused, which these requests
|
|
42
|
+
* never pass.
|
|
43
|
+
* @typedef {(refused?: string) => Promise<{ token: string, heygen: string, heygenApi: string, owner?: string | null }
|
|
44
|
+
* | null>} SignedInAccess */
|
|
45
|
+
|
|
46
|
+
/** `host` picks the sign-in host or HeyGen's /v3 API; `trace` marks `answers` as answers, not failures.
|
|
47
|
+
* `headers` go on every send, beneath a call's own.
|
|
48
|
+
* @typedef {{ access: SignedInAccess, fetchImpl?: typeof fetch, timeoutMs: number, tag: string, offline: () => Error,
|
|
49
|
+
* headers?: Record<string, string>,
|
|
50
|
+
* signedOut: () => Error, host?: "heygen" | "heygenApi",
|
|
51
|
+
* trace?: (init: RequestInit, answers: number[]) => RequestInit }} SignedInDeps */
|
|
52
|
+
|
|
53
|
+
/** Requests to a HeyGen host as the signed-in person. A 401 reads the sign-in again and retries only with a
|
|
54
|
+
* renewed token; a second 401 comes back as the answer. Signed out, or the same token refused, throws `signedOut()`;
|
|
55
|
+
* an unreachable server throws `offline()`. @param {SignedInDeps} deps */
|
|
56
|
+
export function signedInRequest({
|
|
57
|
+
access,
|
|
58
|
+
fetchImpl = fetch,
|
|
59
|
+
timeoutMs,
|
|
60
|
+
tag,
|
|
61
|
+
offline,
|
|
62
|
+
signedOut,
|
|
63
|
+
host = "heygen",
|
|
64
|
+
headers: always = {},
|
|
65
|
+
trace = (init) => init,
|
|
66
|
+
}) {
|
|
67
|
+
/** @param {{ token: string, heygen: string, heygenApi: string }} signedIn
|
|
68
|
+
* @param {string} method @param {string} route @param {unknown} body @param {Record<string, string>} headers
|
|
69
|
+
* @param {number[] | null} answers @param {string} label */
|
|
70
|
+
const send = (signedIn, method, route, body, headers, answers, label) => {
|
|
71
|
+
/** @type {RequestInit} */
|
|
72
|
+
const init = {
|
|
73
|
+
method,
|
|
74
|
+
headers: {
|
|
75
|
+
// Never a caller's Authorization, in any case: fetch would join it to the bearer.
|
|
76
|
+
...Object.fromEntries(
|
|
77
|
+
Object.entries({ ...always, ...headers }).filter(
|
|
78
|
+
([name]) => name.toLowerCase() !== "authorization",
|
|
79
|
+
),
|
|
80
|
+
),
|
|
81
|
+
authorization: `Bearer ${signedIn.token}`,
|
|
82
|
+
...(body === undefined ? {} : { "content-type": "application/json" }),
|
|
83
|
+
},
|
|
84
|
+
// A redirect would drop the bearer.
|
|
85
|
+
redirect: "error",
|
|
86
|
+
body: body === undefined ? null : JSON.stringify(body),
|
|
87
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
88
|
+
};
|
|
89
|
+
return fetchImpl(`${signedIn[host]}${route}`, answers ? trace(init, answers) : init).catch(
|
|
90
|
+
(error) => {
|
|
91
|
+
console.error(`[${tag}] ${label} failed:`, /** @type {Error} */ (error).name);
|
|
92
|
+
throw offline();
|
|
93
|
+
},
|
|
94
|
+
);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/** `answers`: a background check's own "no" (offline is then one too), not reported as an API failure. `label`
|
|
98
|
+
* names the call in the log in place of its route, for a route that carries a secret (a share token).
|
|
99
|
+
* @param {string} method @param {string} route @param {unknown} [body] @param {Record<string, string>} [headers]
|
|
100
|
+
* @param {number[] | null} [answers] @param {string} [label]
|
|
101
|
+
* @returns {Promise<{ reply: Response, heygen: string, owner: string | null }>} */
|
|
102
|
+
return async (
|
|
103
|
+
method,
|
|
104
|
+
route,
|
|
105
|
+
body,
|
|
106
|
+
headers = {},
|
|
107
|
+
answers = null,
|
|
108
|
+
label = `${method} ${route}`,
|
|
109
|
+
) => {
|
|
110
|
+
let refused = "";
|
|
111
|
+
for (;;) {
|
|
112
|
+
// The refused token is passed on: the account renews only past a token it is told the server refused.
|
|
113
|
+
const signedIn = await access(refused || undefined).catch((error) => {
|
|
114
|
+
throw error instanceof AccountUnreachable ? offline() : error;
|
|
115
|
+
});
|
|
116
|
+
if (!signedIn || signedIn.token === refused) throw signedOut();
|
|
117
|
+
const sending = typeof body === "function" ? await body() : body;
|
|
118
|
+
const reply = await send(signedIn, method, route, sending, headers, answers, label);
|
|
119
|
+
if (reply.status !== 401 || refused)
|
|
120
|
+
return { reply, heygen: signedIn.heygen, owner: signedIn.owner ?? null };
|
|
121
|
+
refused = signedIn.token;
|
|
122
|
+
await reply.body?.cancel().catch(() => {});
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
}
|
package/src/storage.mjs
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import {
|
|
3
|
+
lstat,
|
|
4
|
+
mkdir,
|
|
5
|
+
readFile,
|
|
6
|
+
writeFile,
|
|
7
|
+
rename as renameFile,
|
|
8
|
+
rm as remove,
|
|
9
|
+
} from "node:fs/promises";
|
|
10
|
+
import { basename, join, sep } from "node:path";
|
|
11
|
+
import { randomUUID } from "node:crypto";
|
|
12
|
+
|
|
13
|
+
/** @param {unknown} error */
|
|
14
|
+
export const errorCode = (error) =>
|
|
15
|
+
error instanceof Error && "code" in error ? error.code : undefined;
|
|
16
|
+
|
|
17
|
+
/** @template T @param {() => Promise<T>} run */
|
|
18
|
+
async function retryBusy(run) {
|
|
19
|
+
const waits = [10, 25, 50, 100, 200];
|
|
20
|
+
for (let attempt = 0; ; attempt++) {
|
|
21
|
+
try {
|
|
22
|
+
return await run();
|
|
23
|
+
} catch (error) {
|
|
24
|
+
const delay = waits[attempt];
|
|
25
|
+
if (
|
|
26
|
+
process.platform !== "win32" ||
|
|
27
|
+
!["EPERM", "EBUSY", "EACCES", "ENOTEMPTY"].includes(String(errorCode(error))) ||
|
|
28
|
+
delay === undefined
|
|
29
|
+
)
|
|
30
|
+
throw error;
|
|
31
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** @param {string} from @param {string} to */
|
|
36
|
+
export const rename = (from, to) => retryBusy(() => renameFile(from, to));
|
|
37
|
+
/** @param {string} path @param {import("node:fs").RmOptions} [options] */
|
|
38
|
+
export const rm = (path, options) => retryBusy(() => remove(path, options));
|
|
39
|
+
|
|
40
|
+
/** Create metadata folders only inside an existing project. @param {string} root @param {string} rel */
|
|
41
|
+
export async function mkdirInside(root, rel) {
|
|
42
|
+
let dir = root;
|
|
43
|
+
for (const part of rel.split(sep).filter(Boolean)) {
|
|
44
|
+
if (part === ".." || part === ".") throw new Error("Invalid metadata directory.");
|
|
45
|
+
dir = join(dir, part);
|
|
46
|
+
await mkdir(dir).catch((error) => {
|
|
47
|
+
if (errorCode(error) !== "EEXIST") throw error;
|
|
48
|
+
});
|
|
49
|
+
if (!(await lstat(dir)).isDirectory())
|
|
50
|
+
throw new Error("The destination must be a real folder, not a link.");
|
|
51
|
+
}
|
|
52
|
+
return dir;
|
|
53
|
+
}
|
|
54
|
+
/** @type {Map<string, Promise<void>>} */
|
|
55
|
+
const queued = new Map();
|
|
56
|
+
/** @param {string} path @param {unknown} data */
|
|
57
|
+
export function writeJsonAtomic(path, data) {
|
|
58
|
+
const text = JSON.stringify(data, null, 2);
|
|
59
|
+
const save = (queued.get(path) ?? Promise.resolve()).then(async () => {
|
|
60
|
+
const temp = `${path}.${randomUUID()}.tmp`;
|
|
61
|
+
try {
|
|
62
|
+
await writeFile(temp, text, { mode: 0o600 });
|
|
63
|
+
await rename(temp, path);
|
|
64
|
+
} finally {
|
|
65
|
+
await rm(temp, { force: true });
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
const settled = save.catch(() => {});
|
|
69
|
+
queued.set(path, settled);
|
|
70
|
+
void settled.then(() => queued.get(path) === settled && queued.delete(path));
|
|
71
|
+
return save;
|
|
72
|
+
}
|
|
73
|
+
export class SharingStateError extends Error {
|
|
74
|
+
/** @param {string} path @param {unknown} [cause] */
|
|
75
|
+
constructor(path, cause) {
|
|
76
|
+
super(
|
|
77
|
+
`Sharing state in .hyperframes/${basename(path)} could not be read. Restore this file from a backup, or contact support to reconnect the existing share. Keep the file; deleting it can lose recovery state or create a duplicate share.`,
|
|
78
|
+
{ cause },
|
|
79
|
+
);
|
|
80
|
+
this.name = "SharingStateError";
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** @template T @param {string} path @param {T} fallback @returns {Promise<Record<string, unknown> | Exclude<T, object>>} */
|
|
85
|
+
export async function readJsonOr(path, fallback) {
|
|
86
|
+
try {
|
|
87
|
+
return JSON.parse(await readFile(path, "utf8"));
|
|
88
|
+
} catch (error) {
|
|
89
|
+
if (errorCode(error) !== "ENOENT") throw new SharingStateError(path, error);
|
|
90
|
+
return /** @type {Record<string, unknown> | Exclude<T, object>} */ (fallback);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** @param {unknown} value @returns {value is Record<string, unknown>} */
|
|
95
|
+
export const isJsonObject = (value) =>
|
|
96
|
+
value !== null && typeof value === "object" && !Array.isArray(value);
|
|
97
|
+
|
|
98
|
+
/** @param {unknown} value @returns {value is string} */
|
|
99
|
+
export const isSavedId = (value) => typeof value === "string" && value.trim() !== "";
|
|
100
|
+
|
|
101
|
+
/** @param {unknown} value @returns {value is string | null} */
|
|
102
|
+
export const isNullableSavedId = (value) => value === null || isSavedId(value);
|
|
103
|
+
|
|
104
|
+
/** @param {unknown} value @returns {value is string | null | undefined} */
|
|
105
|
+
export const isOptionalSavedId = (value) => value === undefined || isNullableSavedId(value);
|
|
106
|
+
|
|
107
|
+
/** @param {unknown} value @returns {value is boolean | undefined} */
|
|
108
|
+
export const isOptionalBoolean = (value) => value === undefined || typeof value === "boolean";
|
|
109
|
+
|
|
110
|
+
/** An absent file is the only source of an empty record. @param {string} path */
|
|
111
|
+
export async function readJsonRecord(path) {
|
|
112
|
+
const value = await readJsonOr(path, undefined);
|
|
113
|
+
if (value === undefined) return null;
|
|
114
|
+
if (!isJsonObject(value)) throw new SharingStateError(path);
|
|
115
|
+
return value;
|
|
116
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Read for each request, so a share that outlives a token meets the renewed one; pass a bearer a server refused.
|
|
2
|
+
* `owner` is the account the token is (accountPrincipal), for what is counted per account. */
|
|
3
|
+
/** `orOffline`: a saved sign-in whose renewal could not reach HeyGen throws AccountUnreachable instead of null. */
|
|
4
|
+
export type ApiAccess = (
|
|
5
|
+
refused?: string,
|
|
6
|
+
orOffline?: boolean,
|
|
7
|
+
) => Promise<{
|
|
8
|
+
token: string;
|
|
9
|
+
owner: string | null;
|
|
10
|
+
heygen: string;
|
|
11
|
+
heygenApi: string;
|
|
12
|
+
} | null>;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export type ReviewPoint = [number, number];
|
|
2
|
+
/** Where on the video a comment points, as the share page sends it. */
|
|
3
|
+
export type ReviewAnchor =
|
|
4
|
+
| { version: 1; kind: "moment"; time: number; at?: ReviewPoint | null; from?: ReviewPoint | null }
|
|
5
|
+
| { version: 1; kind: "range"; start: number; end: number }
|
|
6
|
+
| { version: 1; kind: "region"; time: number; points: ReviewPoint[]; elements?: string[] }
|
|
7
|
+
| { version: 1; kind: "element"; time: number; selector: string; label?: string | null }
|
|
8
|
+
| { version: 1; kind: "track"; time: number; track: number };
|
|
9
|
+
|
|
10
|
+
/** A review comment as the window sees it: no signed URL, no author id. */
|
|
11
|
+
export interface ReviewComment {
|
|
12
|
+
id: string;
|
|
13
|
+
parentId: string | null;
|
|
14
|
+
body: string;
|
|
15
|
+
anchor: ReviewAnchor | null;
|
|
16
|
+
/** The export the comment was left on. */
|
|
17
|
+
origin: { versionId: string; exportedAt: string; width: number; height: number } | null;
|
|
18
|
+
/** Project-relative path of the saved frame once it is on disk; null until then, when there is none, or once the
|
|
19
|
+
* thread is resolved. */
|
|
20
|
+
framePath: string | null;
|
|
21
|
+
/** The comment carries a saved frame (the share page's capture can fail, leaving an anchored comment without one). */
|
|
22
|
+
hasFrame: boolean;
|
|
23
|
+
author: string;
|
|
24
|
+
authorRole: "owner" | "editor" | "commenter" | "viewer" | "guest" | null;
|
|
25
|
+
status: "open" | "resolved" | null;
|
|
26
|
+
/** `completed`: accepted, and the edit it asked for is done (the `resolve` action). */
|
|
27
|
+
resolution: "accepted" | "dismissed" | "completed" | null;
|
|
28
|
+
/** The thread was forwarded for follow-up. Defaults to false when absent. */
|
|
29
|
+
forwarded: boolean;
|
|
30
|
+
revision: number;
|
|
31
|
+
statusRevision: number | null;
|
|
32
|
+
createdAt: string;
|
|
33
|
+
deleted: boolean;
|
|
34
|
+
canResolve: boolean;
|
|
35
|
+
/** The server's hashed, project-scoped ref for a root comment, sent only to those who can decide; absent from older
|
|
36
|
+
* servers. Usage events alone carry it: it is never shown and never goes into a request to the agent. */
|
|
37
|
+
analyticsRef?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The thread decisions the review server takes on its status route. */
|
|
41
|
+
export type ReviewAction = "accept" | "dismiss" | "reopen" | "resolve";
|
|
42
|
+
|
|
43
|
+
/** A thread write's answer: the thread's new status revision, or why it was refused. */
|
|
44
|
+
export type ReviewWriteResult =
|
|
45
|
+
| { ok: true; statusRevision: number | null }
|
|
46
|
+
| {
|
|
47
|
+
ok: false;
|
|
48
|
+
reason: "conflict" | "forwarded" | "offline" | "signed-out" | "denied" | "gone" | "failed";
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/** The project's review comments, kept current while the project is in view. */
|
|
52
|
+
export interface ReviewFeedView {
|
|
53
|
+
/** off: the flag is off, or the project was never shared for review. */
|
|
54
|
+
state: "off" | "loading" | "ready" | "reconnecting" | "signed-out" | "denied" | "gone";
|
|
55
|
+
comments: ReviewComment[];
|
|
56
|
+
latestVersionId: string | null;
|
|
57
|
+
reviewUrl: string | null;
|
|
58
|
+
syncedAt: number | null;
|
|
59
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
// Review API request and response shapes. Nullable fields use `| null`.
|
|
2
|
+
|
|
3
|
+
import type { AccessMode, LinkRole } from "./share.js";
|
|
4
|
+
|
|
5
|
+
export interface ErrorEnvelope {
|
|
6
|
+
code: number;
|
|
7
|
+
message: string;
|
|
8
|
+
data: {
|
|
9
|
+
reason: string;
|
|
10
|
+
current_revision?: number;
|
|
11
|
+
/** On a 403 `workspace_membership_required`: the team workspace the link is for, absent when it has no name of
|
|
12
|
+
* its own to show (a personal space never has one). */
|
|
13
|
+
space?: { name: string; kind: "team" };
|
|
14
|
+
workspace_id?: string;
|
|
15
|
+
project_limit?: number;
|
|
16
|
+
projects_used_up_to_limit?: number;
|
|
17
|
+
requested_feature?: string;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface RegisterProjectRequest {
|
|
22
|
+
studio_project_id: string;
|
|
23
|
+
title: string;
|
|
24
|
+
client_operation_id: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface Capabilities {
|
|
28
|
+
can_view: true;
|
|
29
|
+
can_comment: boolean;
|
|
30
|
+
can_export: boolean;
|
|
31
|
+
can_resolve: boolean;
|
|
32
|
+
can_manage_sharing: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface ProjectDTO {
|
|
36
|
+
review_entitlements?: ReviewEntitlements | null;
|
|
37
|
+
project_id: string;
|
|
38
|
+
title: string;
|
|
39
|
+
access_mode: AccessMode;
|
|
40
|
+
latest_ready_version_id: string | null;
|
|
41
|
+
review_url: string | null;
|
|
42
|
+
authenticated_review_url: string;
|
|
43
|
+
capabilities: Capabilities;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// GET /v1/hyperframes/review-spaces: personal first, then team spaces by last use. The data
|
|
47
|
+
// is `{ items: SpaceDTO[], version_sharing?: boolean, share_files?: boolean }`: the server's per-account flags for
|
|
48
|
+
// shared reviews and for files on their versions, and anything but `true` (absent from an older server too) is off.
|
|
49
|
+
export interface SpaceDTO {
|
|
50
|
+
review_entitlements?: ReviewEntitlements | null;
|
|
51
|
+
space_id: string;
|
|
52
|
+
name: string;
|
|
53
|
+
kind: "personal" | "team";
|
|
54
|
+
can_create_review: boolean;
|
|
55
|
+
last_used_at: string | null;
|
|
56
|
+
/** The workspace this token is bound to; absent from servers before it. */
|
|
57
|
+
bound?: boolean;
|
|
58
|
+
/** Someone other than the calling user is a member; absent when the server could not count, or
|
|
59
|
+
* predates it. */
|
|
60
|
+
has_other_members?: boolean;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// The workspace a project was registered in.
|
|
64
|
+
interface SpaceRef {
|
|
65
|
+
space_id: string;
|
|
66
|
+
name: string;
|
|
67
|
+
kind: SpaceDTO["kind"];
|
|
68
|
+
/** Someone other than the project's owner is a member; absent as on SpaceDTO. */
|
|
69
|
+
has_other_members?: boolean;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface ShareDTO {
|
|
73
|
+
review_entitlements?: ReviewEntitlements | null;
|
|
74
|
+
sharing_state?: ReviewSharingState;
|
|
75
|
+
project_id: string;
|
|
76
|
+
access_mode: AccessMode;
|
|
77
|
+
default_role: LinkRole;
|
|
78
|
+
comments_enabled: boolean;
|
|
79
|
+
revision: number;
|
|
80
|
+
link_generation: number;
|
|
81
|
+
/** Null when the link is off or the frontend base is unset. */
|
|
82
|
+
review_url: string | null;
|
|
83
|
+
authenticated_review_url: string;
|
|
84
|
+
revoked: boolean;
|
|
85
|
+
space?: SpaceRef | null;
|
|
86
|
+
/** While a moderation hold is active, link viewers and guests receive 404. */
|
|
87
|
+
moderation_hold?: boolean;
|
|
88
|
+
/** Which hold: a staff takedown admits only the owner and refuses any reshare with 409 moderation_hold. */
|
|
89
|
+
moderation_hold_kind?: "takedown" | "auto_reports" | null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface UpdateProjectShareRequest {
|
|
93
|
+
access_mode: AccessMode;
|
|
94
|
+
default_role: LinkRole;
|
|
95
|
+
comments_enabled: boolean;
|
|
96
|
+
expected_revision: number;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface ProjectOperationRequest {
|
|
100
|
+
expected_link_generation: number;
|
|
101
|
+
client_operation_id: string;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// GET …/feedback: only the final page carries change_cursor.
|
|
105
|
+
export interface FeedbackPage {
|
|
106
|
+
items: unknown[];
|
|
107
|
+
next_cursor: string | null;
|
|
108
|
+
change_cursor: string | null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// POST /v1/hyperframes/review-projects/{id}/exports with a local MP4 (contract "Export a new video version").
|
|
112
|
+
export interface ExportRequest {
|
|
113
|
+
client_operation_id: string;
|
|
114
|
+
title_snapshot: string;
|
|
115
|
+
coordinate_width: number;
|
|
116
|
+
coordinate_height: number;
|
|
117
|
+
duration_seconds: number;
|
|
118
|
+
render_settings: Record<string, never>;
|
|
119
|
+
source_revision_id: null;
|
|
120
|
+
source_fingerprint: string | null;
|
|
121
|
+
local_upload: { size_bytes: number; checksum_sha256: string; content_type: "video/mp4" };
|
|
122
|
+
/** The synced snapshot the video was rendered from, sent only while the server's `share_files` is on. The server
|
|
123
|
+
* refuses one it cannot match with 422 `snapshot_mismatch`. */
|
|
124
|
+
snapshot?: SnapshotRef;
|
|
125
|
+
/** Whether this version addresses review feedback, when the host can tell. */
|
|
126
|
+
from_review?: "yes" | "no";
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** A composition-sync snapshot a review version carries, named in the snapshot's own workspace, which may differ from
|
|
130
|
+
* the review project's. Also the body of PUT …/versions/{version_id}/snapshot, which attaches one later. */
|
|
131
|
+
export interface SnapshotRef {
|
|
132
|
+
space_id: string;
|
|
133
|
+
composition_id: string;
|
|
134
|
+
snapshot_id: string;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// A version is playable only after screening completes.
|
|
138
|
+
export interface ExportDTO {
|
|
139
|
+
video_id: string;
|
|
140
|
+
version_id: string;
|
|
141
|
+
status: "upload_pending" | "processing" | "screening" | "ready" | "failed";
|
|
142
|
+
exported_at: string;
|
|
143
|
+
upload: { put_url: string; headers: Record<string, string> } | null;
|
|
144
|
+
error: { reason: string; message: string } | null;
|
|
145
|
+
/** Owner reads only (absent from an older backend); status stays "screening" for both, null when not screening.
|
|
146
|
+
* "checking" is any screening not held for a person at HeyGen. */
|
|
147
|
+
screening_state?: "checking" | "held" | null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export interface ReviewEntitlements {
|
|
151
|
+
policy_version: 1;
|
|
152
|
+
workspace_id: string;
|
|
153
|
+
plan: "free" | "subscribed";
|
|
154
|
+
project_limit: 5 | null;
|
|
155
|
+
projects_used_up_to_limit: number | null;
|
|
156
|
+
can_register_project: boolean;
|
|
157
|
+
allowed_collaborative_access_modes: Array<"public" | "team" | "invite">;
|
|
158
|
+
can_manage_email_grants: boolean;
|
|
159
|
+
/** Whether the owner's plan can give Can edit; sent only while `share_files` is on, false on Free. */
|
|
160
|
+
can_offer_edit?: boolean;
|
|
161
|
+
}
|
|
162
|
+
interface ReviewSharingState {
|
|
163
|
+
state: "available" | "closed" | "subscription_required";
|
|
164
|
+
can_close_access: boolean;
|
|
165
|
+
can_create_email_grants?: boolean;
|
|
166
|
+
can_update_email_grants?: boolean;
|
|
167
|
+
can_rotate_link?: boolean;
|
|
168
|
+
can_create_link?: boolean;
|
|
169
|
+
can_revoke_link?: boolean;
|
|
170
|
+
allowed_collaborative_access_modes?: Array<"public" | "team" | "invite">;
|
|
171
|
+
allowed_default_roles?: LinkRole[];
|
|
172
|
+
can_enable_comments?: boolean;
|
|
173
|
+
}
|
|
174
|
+
export interface AccessGrantDTO {
|
|
175
|
+
effective?: boolean;
|
|
176
|
+
can_revoke?: boolean;
|
|
177
|
+
allowed_roles?: Array<"viewer" | "commenter" | "editor">;
|
|
178
|
+
id: string;
|
|
179
|
+
email: string;
|
|
180
|
+
role: "viewer" | "commenter" | "editor";
|
|
181
|
+
revision: number;
|
|
182
|
+
revoked_at: string | null;
|
|
183
|
+
}
|
|
184
|
+
export interface CreateGrantRequest {
|
|
185
|
+
client_operation_id: string;
|
|
186
|
+
email: string;
|
|
187
|
+
role: AccessGrantDTO["role"];
|
|
188
|
+
expected_revision: number | null;
|
|
189
|
+
}
|
|
190
|
+
export interface ManagedReviewPage {
|
|
191
|
+
items: Array<{
|
|
192
|
+
project_id: string;
|
|
193
|
+
title: string;
|
|
194
|
+
created_at: string;
|
|
195
|
+
owner_user_id: string;
|
|
196
|
+
can_remove: boolean;
|
|
197
|
+
}>;
|
|
198
|
+
next_cursor: string | null;
|
|
199
|
+
review_entitlements: ReviewEntitlements | null;
|
|
200
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/** Who a review link lets in, and as what, as the shared-reviews server names them. */
|
|
2
|
+
export type AccessMode = "public" | "invite" | "team" | "private";
|
|
3
|
+
export type LinkRole = "viewer" | "commenter" | "editor";
|
|
4
|
+
|
|
5
|
+
/** Whether the version on the page, or the one a press would add, carries the project's synced files. `included`: it
|
|
6
|
+
* does. `not_synced`: the render is the project as it is now, but that is not what was last synced. `changed`: the
|
|
7
|
+
* project moved past the render. `other_workspace`: the project syncs to `workspace`, not the one Desktop is signed
|
|
8
|
+
* into, whose sign-in can't read its snapshots. `off`: share_files or sync is off, or there is no render to speak of. */
|
|
9
|
+
export type ReviewFiles =
|
|
10
|
+
| { at: "off" }
|
|
11
|
+
| { at: "included" }
|
|
12
|
+
| { at: "not_synced" }
|
|
13
|
+
| { at: "changed" }
|
|
14
|
+
| { at: "other_workspace"; workspace: string };
|
|
15
|
+
export type ReviewFilesState = ReviewFiles["at"];
|
|
16
|
+
|
|
17
|
+
/** An upload under way: bytes sent of the archive's total (0 while the archive is made). */
|
|
18
|
+
export interface ShareProgress {
|
|
19
|
+
sent: number;
|
|
20
|
+
total: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** A project's shared link, as the Share popover reads it. */
|
|
24
|
+
export interface ShareStatus {
|
|
25
|
+
/** Null until the first share; changedFiles counts the project's files changed since that link was made. */
|
|
26
|
+
link: { url: string; sharedAt: number; changedFiles: number } | null;
|
|
27
|
+
/** The project's files in bytes, and the most a shared project may be. */
|
|
28
|
+
bytes: number;
|
|
29
|
+
limit: number;
|
|
30
|
+
publishing: ShareProgress | null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** A project's review link, as the server holds it. */
|
|
34
|
+
export interface ReviewShare {
|
|
35
|
+
projectId?: string;
|
|
36
|
+
entitlements?: import("./reviewWire.js").ReviewEntitlements | null | undefined;
|
|
37
|
+
sharingState?: import("./reviewWire.js").ShareDTO["sharing_state"];
|
|
38
|
+
removed?: boolean;
|
|
39
|
+
/** Null while the link is off. */
|
|
40
|
+
url: string | null;
|
|
41
|
+
access: AccessMode;
|
|
42
|
+
role: LinkRole;
|
|
43
|
+
commentsOn: boolean;
|
|
44
|
+
/** The space the link belongs to; null from a server that does not say. */
|
|
45
|
+
space: ReviewSpaceRef | null;
|
|
46
|
+
/** The link's space is not the one Desktop is signed into. Everything still works there; this only names it. */
|
|
47
|
+
elsewhere: boolean;
|
|
48
|
+
/** A report paused access through the link: only the owner, invited people and workspace members get in. */
|
|
49
|
+
held: boolean;
|
|
50
|
+
/** Staff took the review down: only the owner can open it, at `page`, and it can't be shared again. */
|
|
51
|
+
takedown?: { page: string };
|
|
52
|
+
/** Where the page's versions stand; every answer over IPC carries it. */
|
|
53
|
+
version?: ReviewVersion;
|
|
54
|
+
/** After a change of space: the old link not yet turned off, and whether retries keep failing. */
|
|
55
|
+
oldLink?: { space: string; stuck: boolean } | null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** An upload to the review page in flight. `checking` is the server's screening, which can take a while. */
|
|
59
|
+
export interface ReviewExportProgress {
|
|
60
|
+
step: "preparing" | "uploading" | "processing" | "checking";
|
|
61
|
+
sent: number;
|
|
62
|
+
total: number;
|
|
63
|
+
/** Checking for over 2 minutes: staff review can take a while. */
|
|
64
|
+
slow?: boolean;
|
|
65
|
+
/** How screening stands, from a server that says: held for a person at HeyGen (no bar, polled slowly, up to a day)
|
|
66
|
+
* or checking. Absent from an older server, which leaves `slow` to say a long check. */
|
|
67
|
+
screening?: "checking" | "held";
|
|
68
|
+
/** Checking for over 15 minutes since the upload completed, by this computer's clock. */
|
|
69
|
+
late?: boolean;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface ReviewVersion {
|
|
73
|
+
/** The newest version this app put on the page; `current` when it is the project as it is now. */
|
|
74
|
+
last: { exportedAt: string; current: boolean } | null;
|
|
75
|
+
/** The newest export: ready to go up, none, not MP4, or older than the project now. */
|
|
76
|
+
source: "ready" | "none" | "not-mp4" | "stale";
|
|
77
|
+
exporting: ReviewExportProgress | null;
|
|
78
|
+
/** The last run's sentence when it did not go up. */
|
|
79
|
+
failure: string | null;
|
|
80
|
+
/** False when the same video would be refused again (blocked, rejected, not available): no Try again. */
|
|
81
|
+
retryable?: boolean;
|
|
82
|
+
/** With `retryable` false: a smaller or shorter export can go up, so offer Export MP4. */
|
|
83
|
+
exportHelps?: boolean;
|
|
84
|
+
/** The watch ran out while the video was held for review: not a refusal, so it is never shown as one. */
|
|
85
|
+
held?: boolean;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface ReviewSpaceRef {
|
|
89
|
+
id: string;
|
|
90
|
+
name: string;
|
|
91
|
+
kind: "personal" | "team";
|
|
92
|
+
/** Someone besides the link's owner (on a space list: besides the caller) is in the space; absent when the server
|
|
93
|
+
* does not say, which Share reads as yes. */
|
|
94
|
+
hasOtherMembers?: boolean;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** A space the account is in. v1 shares into the signed-in one; the later workspace switch lists the rest. */
|
|
98
|
+
export interface ReviewSpace extends ReviewSpaceRef {
|
|
99
|
+
/** Existing native registration verified by reading its owner share; a retry needs no new slot. */
|
|
100
|
+
existingProjectId?: string;
|
|
101
|
+
/** A durable registration request can be replayed; the server still decides admission. */
|
|
102
|
+
resumingRegistration?: boolean;
|
|
103
|
+
sharingState?: ReviewShare["sharingState"];
|
|
104
|
+
entitlements?: import("./reviewWire.js").ReviewEntitlements | null | undefined;
|
|
105
|
+
lastUsedAt: string | null;
|
|
106
|
+
canCreate: boolean;
|
|
107
|
+
/** The workspace Desktop is signed into, where a new link goes. */
|
|
108
|
+
signedIn: boolean;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export type ReviewAccessChange = { access: ReviewShare["access"] } | { role: ReviewShare["role"] };
|
|
112
|
+
|
|
113
|
+
/** A share link as Settings' Cloud list shows it (packages/agent-runtime/src/share/reviewLinkStates.mjs). */
|
|
114
|
+
export interface ReviewLinkState {
|
|
115
|
+
state: "on" | "off" | "paused" | "taken-down";
|
|
116
|
+
access: import("./share.js").AccessMode;
|
|
117
|
+
/** Why a paused link is paused: a report HyperFrames looks into, or a plan that no longer includes it. */
|
|
118
|
+
pausedFor?: "report" | "plan";
|
|
119
|
+
}
|