@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.
- 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 +6 -22
- package/src/reviewClient.mjs +83 -48
- package/src/reviewExport.mjs +41 -156
- package/src/reviewUpload.mjs +16 -18
- package/src/reviewVersionSentences.mjs +1 -2
- package/dist/shareSources.d.mts +0 -24
- package/dist/sourceIgnore.d.mts +0 -3
- 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/sourceIgnore.mjs +0 -69
- package/src/sources/adapterTypes.d.ts +0 -39
- package/src/sources/api.mjs +0 -114
- package/src/sources/capture.mjs +0 -264
- 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/sources/transfer.mjs
DELETED
|
@@ -1,196 +0,0 @@
|
|
|
1
|
-
// @ts-check
|
|
2
|
-
// Bytes to signed storage URLs: one file or multipart range, streamed and hashed as it is sent.
|
|
3
|
-
// Large media costs one chunk at a time. Storage URLs carry their own signature, never an OAuth bearer.
|
|
4
|
-
import { createHash } from "node:crypto";
|
|
5
|
-
import { constants, createReadStream } from "node:fs";
|
|
6
|
-
import { open, realpath } from "node:fs/promises";
|
|
7
|
-
import { dirname } from "node:path";
|
|
8
|
-
import { setTimeout as sleep } from "node:timers/promises";
|
|
9
|
-
import { trustedUploadUrl } from "../reviewUpload.mjs";
|
|
10
|
-
|
|
11
|
-
/** Why a transfer stopped. `expired`: the signed URL no longer works (ask for a new one). `exists`: a create-only PUT
|
|
12
|
-
* found the object already written. `changed`: the file's bytes are not the ones captured. `unreadable`: the file
|
|
13
|
-
* could not be read (a drive unplugged, a file deleted). `corrupt`: downloaded bytes do not hash to what was asked.
|
|
14
|
-
* `transient`: the network or the store failed; the same request may be sent again. */
|
|
15
|
-
export class TransferError extends Error {
|
|
16
|
-
/** @param {"expired" | "exists" | "changed" | "unreadable" | "corrupt" | "transient" | "refused"} kind
|
|
17
|
-
* @param {{ status?: number, path?: string }} [about] */
|
|
18
|
-
constructor(kind, about = {}) {
|
|
19
|
-
super(`composition sync transfer: ${kind}${about.status ? ` (${about.status})` : ""}`);
|
|
20
|
-
this.name = "TransferError";
|
|
21
|
-
this.kind = kind;
|
|
22
|
-
this.status = about.status ?? 0;
|
|
23
|
-
this.path = about.path ?? null;
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/** @param {unknown} error */
|
|
28
|
-
export const isAbort = (error) =>
|
|
29
|
-
error instanceof Error && (error.name === "AbortError" || error.name === "TimeoutError");
|
|
30
|
-
|
|
31
|
-
/** @param {number} ms @param {AbortSignal} [signal] */
|
|
32
|
-
export const realWait = (ms, signal) => sleep(ms, undefined, { signal });
|
|
33
|
-
|
|
34
|
-
/** S3 answers 403 for a URL past its expiry; a 5xx or 429 is worth sending again. @param {number} status
|
|
35
|
-
* @returns {TransferError} */
|
|
36
|
-
function refusalOf(status) {
|
|
37
|
-
if (status === 403) return new TransferError("expired", { status });
|
|
38
|
-
if (status === 412) return new TransferError("exists", { status });
|
|
39
|
-
if (status === 429 || status >= 500) return new TransferError("transient", { status });
|
|
40
|
-
return new TransferError("refused", { status });
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/** @param {string} file */
|
|
44
|
-
async function openSource(file) {
|
|
45
|
-
const handle = await open(file, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
46
|
-
try {
|
|
47
|
-
const stat = await handle.stat();
|
|
48
|
-
if (!stat.isFile() || (await realpath(dirname(file))) !== dirname(file))
|
|
49
|
-
throw new TransferError("unreadable", { path: file });
|
|
50
|
-
return handle;
|
|
51
|
-
} catch (error) {
|
|
52
|
-
await handle.close();
|
|
53
|
-
throw error;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/** @param {string} file @param {number} start @param {number} length @param {(bytes: number) => void} onBytes */
|
|
58
|
-
async function hashedBody(file, start, length, onBytes) {
|
|
59
|
-
const handle = await openSource(file);
|
|
60
|
-
const hash = createHash("sha256");
|
|
61
|
-
const seen = { bytes: 0, ended: false, readError: /** @type {unknown} */ (null) };
|
|
62
|
-
const chunks =
|
|
63
|
-
length === 0
|
|
64
|
-
? null
|
|
65
|
-
: handle
|
|
66
|
-
.createReadStream({ start, end: start + length - 1, autoClose: false })
|
|
67
|
-
[Symbol.asyncIterator]();
|
|
68
|
-
const body = new ReadableStream(
|
|
69
|
-
{
|
|
70
|
-
async pull(controller) {
|
|
71
|
-
if (!chunks) return controller.close();
|
|
72
|
-
try {
|
|
73
|
-
const { value, done } = await chunks.next();
|
|
74
|
-
if (done) return ((seen.ended = true), controller.close());
|
|
75
|
-
hash.update(value);
|
|
76
|
-
seen.bytes += value.length;
|
|
77
|
-
onBytes(value.length);
|
|
78
|
-
controller.enqueue(value);
|
|
79
|
-
} catch (error) {
|
|
80
|
-
seen.readError = error;
|
|
81
|
-
controller.error(error);
|
|
82
|
-
}
|
|
83
|
-
},
|
|
84
|
-
cancel: () => void chunks?.return?.(),
|
|
85
|
-
},
|
|
86
|
-
{ highWaterMark: 0 },
|
|
87
|
-
);
|
|
88
|
-
return { body, seen, digest: () => hash.digest("hex"), close: () => handle.close() };
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/** What the bytes taken from the file say about it once a PUT ended: unreadable, changed (shorter than captured,
|
|
92
|
-
* or another hash), or null when they are the captured bytes, all of them or a prefix the network cut short.
|
|
93
|
-
* @param {Awaited<ReturnType<typeof hashedBody>>} sent @param {{ length: number, sha256: string, path: string }} expected */
|
|
94
|
-
function fileProblem({ seen, digest }, { length, sha256, path }) {
|
|
95
|
-
if (seen.readError) return new TransferError("unreadable", { path });
|
|
96
|
-
const short = seen.ended && seen.bytes < length;
|
|
97
|
-
if (short || (seen.bytes === length && digest() !== sha256))
|
|
98
|
-
return new TransferError("changed", { path });
|
|
99
|
-
return null;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/** PUTs bytes of `file` to a signed URL with exactly the headers the grant signed. Resolves once storage keeps them;
|
|
103
|
-
* the bytes sent must hash to `sha256`, or it throws `changed` whatever storage answered.
|
|
104
|
-
* @param {{ fetchImpl: typeof fetch, url: string, headers: Record<string, string>, file: string, path: string,
|
|
105
|
-
* start: number, length: number, sha256: string, signal: AbortSignal, onBytes?: (bytes: number) => void, stallMs?: number }} put */
|
|
106
|
-
export async function putRange(put) {
|
|
107
|
-
const { fetchImpl, url, headers, file, start, length, signal } = put;
|
|
108
|
-
const target = trustedUploadUrl(url);
|
|
109
|
-
if (!target) throw new TransferError("refused", { path: put.path });
|
|
110
|
-
signal.throwIfAborted();
|
|
111
|
-
const stall = new AbortController();
|
|
112
|
-
const sent = await hashedBody(file, start, length, (bytes) => {
|
|
113
|
-
timer.refresh();
|
|
114
|
-
put.onBytes?.(bytes);
|
|
115
|
-
});
|
|
116
|
-
const timer = setTimeout(() => stall.abort(), put.stallMs ?? 60_000);
|
|
117
|
-
// Node's fetch needs `duplex` for a streamed body; lib.dom's RequestInit does not name it.
|
|
118
|
-
const init = {
|
|
119
|
-
method: "PUT",
|
|
120
|
-
headers,
|
|
121
|
-
body: sent.body,
|
|
122
|
-
duplex: "half",
|
|
123
|
-
redirect: "error",
|
|
124
|
-
signal: AbortSignal.any([signal, stall.signal]),
|
|
125
|
-
};
|
|
126
|
-
/** @type {Response} */
|
|
127
|
-
let reply;
|
|
128
|
-
try {
|
|
129
|
-
reply = await fetchImpl(target.href, /** @type {RequestInit} */ (init));
|
|
130
|
-
} catch (error) {
|
|
131
|
-
signal.throwIfAborted();
|
|
132
|
-
if (!stall.signal.aborted && isAbort(error)) throw error;
|
|
133
|
-
throw fileProblem(sent, put) ?? new TransferError("transient", { path: put.path });
|
|
134
|
-
} finally {
|
|
135
|
-
clearTimeout(timer);
|
|
136
|
-
await sent.close();
|
|
137
|
-
}
|
|
138
|
-
await reply.body?.cancel().catch(() => {});
|
|
139
|
-
const problem = fileProblem(sent, put);
|
|
140
|
-
if (problem) throw problem;
|
|
141
|
-
if (!reply.ok) throw refusalOf(reply.status);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/** Each part's SHA-256 (parts of `partBytes`, the last one shorter) and the whole file's, in one read.
|
|
145
|
-
* @param {string} file @param {number} size @param {number} partBytes @param {AbortSignal} signal */
|
|
146
|
-
export async function partDigests(file, size, partBytes, signal) {
|
|
147
|
-
const whole = createHash("sha256");
|
|
148
|
-
/** @type {string[]} */
|
|
149
|
-
const parts = [];
|
|
150
|
-
let part = createHash("sha256");
|
|
151
|
-
let inPart = 0;
|
|
152
|
-
for await (const chunk of createReadStream(file, { highWaterMark: 1 << 20, signal })) {
|
|
153
|
-
whole.update(chunk);
|
|
154
|
-
for (let at = 0; at < chunk.length; ) {
|
|
155
|
-
const take = Math.min(partBytes - inPart, chunk.length - at);
|
|
156
|
-
part.update(chunk.subarray(at, at + take));
|
|
157
|
-
inPart += take;
|
|
158
|
-
at += take;
|
|
159
|
-
if (inPart === partBytes) {
|
|
160
|
-
parts.push(part.digest("hex"));
|
|
161
|
-
part = createHash("sha256");
|
|
162
|
-
inPart = 0;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
if (inPart > 0 || parts.length === 0) parts.push(part.digest("hex"));
|
|
167
|
-
return {
|
|
168
|
-
parts,
|
|
169
|
-
whole: whole.digest("hex"),
|
|
170
|
-
complete: parts.length === Math.ceil(size / partBytes),
|
|
171
|
-
};
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/** Runs `work` over `items`, `limit` at a time. The first failure stops new work; the rest finish, then it throws.
|
|
175
|
-
* @template T @param {readonly T[]} items @param {number} limit @param {(item: T) => Promise<void>} work */
|
|
176
|
-
export async function eachLimit(items, limit, work) {
|
|
177
|
-
let next = 0;
|
|
178
|
-
const failure = { failed: false, error: /** @type {unknown} */ (null) };
|
|
179
|
-
const worker = async () => {
|
|
180
|
-
while (!failure.failed && next < items.length) {
|
|
181
|
-
const item = /** @type {T} */ (items[next++]);
|
|
182
|
-
try {
|
|
183
|
-
await work(item);
|
|
184
|
-
} catch (error) {
|
|
185
|
-
if (!failure.failed) Object.assign(failure, { failed: true, error });
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
};
|
|
189
|
-
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
|
|
190
|
-
if (failure.failed) throw failure.error;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
/** The wait before attempt `attempt` (from 1): doubling from `baseMs`, at most `maxMs`, or what the server asked.
|
|
194
|
-
* @param {number} attempt @param {number | null} [askedMs] */
|
|
195
|
-
export const backoffMs = (attempt, askedMs = null, baseMs = 1_000, maxMs = 30_000) =>
|
|
196
|
-
askedMs ?? Math.min(maxMs, baseMs * 2 ** (attempt - 1));
|
package/src/sources/types.d.ts
DELETED
|
@@ -1,353 +0,0 @@
|
|
|
1
|
-
export interface SyncTooBig {
|
|
2
|
-
limit: "file_count" | "file_size" | "project_size" | "manifest_size";
|
|
3
|
-
actual: number;
|
|
4
|
-
maximum: number;
|
|
5
|
-
biggest: { path: string; size: number }[];
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
/** A file that keeps a project from being captured; it is never left out silently. */
|
|
9
|
-
export interface SyncCaptureProblem {
|
|
10
|
-
path: string;
|
|
11
|
-
problem: "symlink" | "special-file" | "unreadable" | "invalid-name" | "changed" | "no-entrypoint";
|
|
12
|
-
/** For `invalid-name`, the path policy's code (`reserved_name`, `path_collision`, ...). */
|
|
13
|
-
code?: string;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export interface ManifestFile {
|
|
17
|
-
path: string;
|
|
18
|
-
sha256: string;
|
|
19
|
-
size: number;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface ManifestDocument {
|
|
23
|
-
format_version: 1;
|
|
24
|
-
entrypoint: "index.html";
|
|
25
|
-
files: ManifestFile[];
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/** A valid manifest: its document in canonical order, the canonical bytes and their SHA-256. */
|
|
29
|
-
export interface Manifest {
|
|
30
|
-
document: ManifestDocument;
|
|
31
|
-
canonical: Buffer;
|
|
32
|
-
sha256: string;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export interface Violation {
|
|
36
|
-
code: string;
|
|
37
|
-
path?: string;
|
|
38
|
-
rule?: string;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export type LimitName = SyncTooBig["limit"];
|
|
42
|
-
|
|
43
|
-
export interface Limits {
|
|
44
|
-
maxFiles: number;
|
|
45
|
-
maxFileBytes: number;
|
|
46
|
-
maxProjectBytes: number;
|
|
47
|
-
maxManifestBytes: number;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export type OverLimit = SyncTooBig;
|
|
51
|
-
|
|
52
|
-
type ClientAction = "include" | "exclude" | "wait_then_exclude";
|
|
53
|
-
|
|
54
|
-
export interface PathRule {
|
|
55
|
-
id: string;
|
|
56
|
-
regex: string;
|
|
57
|
-
client_action: ClientAction;
|
|
58
|
-
server_action: "allow" | "reject";
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/** The fields of contract/path-policy.json this client reads. */
|
|
62
|
-
export interface PolicyDocument {
|
|
63
|
-
policy_version: string;
|
|
64
|
-
stored_manifest: { legacy_agent_state_regex: string };
|
|
65
|
-
unicode: { version: string };
|
|
66
|
-
rules: PathRule[];
|
|
67
|
-
path: {
|
|
68
|
-
max_path_bytes: number;
|
|
69
|
-
max_segment_bytes: number;
|
|
70
|
-
forbidden_characters: string;
|
|
71
|
-
reserved_segment_regex: string;
|
|
72
|
-
canonical_spelling_roots: string[];
|
|
73
|
-
};
|
|
74
|
-
client_prefilter: { names: string[] };
|
|
75
|
-
client_symlinks: { skip_prefixes: string[] };
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export type CaptureProblem = SyncCaptureProblem;
|
|
79
|
-
|
|
80
|
-
/** What an owner workspace's plan allows composition sync, as the limits route answered it. `unread` when it could not
|
|
81
|
-
* be read: `not-deployed` is a server without the route (a 404), and nothing is refused on an unread answer. */
|
|
82
|
-
export type SyncLimitsRead =
|
|
83
|
-
| {
|
|
84
|
-
at: "read";
|
|
85
|
-
syncAllowed: boolean;
|
|
86
|
-
maxCompositions: number;
|
|
87
|
-
/** Live compositions in the workspace when it was read. */
|
|
88
|
-
compositions: number;
|
|
89
|
-
push: Limits;
|
|
90
|
-
}
|
|
91
|
-
| { at: "unread"; why: "not-deployed" | "offline" | "failed" };
|
|
92
|
-
|
|
93
|
-
/** A file a capture lists, before hashing. */
|
|
94
|
-
export interface Found {
|
|
95
|
-
path: string;
|
|
96
|
-
full: string;
|
|
97
|
-
size: number;
|
|
98
|
-
mtimeMs: number;
|
|
99
|
-
ino: number;
|
|
100
|
-
dev: number;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
export interface Walk {
|
|
104
|
-
files: Found[];
|
|
105
|
-
problems: CaptureProblem[];
|
|
106
|
-
waiting: string[];
|
|
107
|
-
skippedLinks: string[];
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
export type Capture =
|
|
111
|
-
| {
|
|
112
|
-
at: "captured";
|
|
113
|
-
manifest: Manifest;
|
|
114
|
-
settled: { agentTurnOpen: boolean; projectLocked: boolean };
|
|
115
|
-
waiting: string[];
|
|
116
|
-
skippedLinks: string[];
|
|
117
|
-
}
|
|
118
|
-
| { at: "problems"; problems: CaptureProblem[] }
|
|
119
|
-
| { at: "too-big"; over: OverLimit };
|
|
120
|
-
|
|
121
|
-
export type SyncErrorKind =
|
|
122
|
-
| "offline"
|
|
123
|
-
| "signed-out"
|
|
124
|
-
| "malformed"
|
|
125
|
-
| "not-enabled"
|
|
126
|
-
| "byok"
|
|
127
|
-
| "no-write-role"
|
|
128
|
-
| "forbidden"
|
|
129
|
-
| "composition-not-found"
|
|
130
|
-
| "not-found"
|
|
131
|
-
| "too-large"
|
|
132
|
-
| "reference-invalid"
|
|
133
|
-
| "upload-not-pending"
|
|
134
|
-
| "active-uploads-exceeded"
|
|
135
|
-
| "file-not-writable"
|
|
136
|
-
| "idempotency-key-reused"
|
|
137
|
-
| "request-in-progress"
|
|
138
|
-
| "snapshot-not-downloadable"
|
|
139
|
-
| "conflict-not-open"
|
|
140
|
-
| "download-expired"
|
|
141
|
-
| "invalid-parameter"
|
|
142
|
-
| "plan-required"
|
|
143
|
-
| "limit-reached"
|
|
144
|
-
| "rate-limited"
|
|
145
|
-
| "unavailable"
|
|
146
|
-
| "server-error"
|
|
147
|
-
| "refused";
|
|
148
|
-
|
|
149
|
-
export interface Composition {
|
|
150
|
-
id: string;
|
|
151
|
-
spaceId: string;
|
|
152
|
-
title: string;
|
|
153
|
-
createdBy: string;
|
|
154
|
-
/** Null before the first publish. */
|
|
155
|
-
latestSnapshotId: string | null;
|
|
156
|
-
createdAt: string;
|
|
157
|
-
updatedAt: string;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
export interface Snapshot {
|
|
161
|
-
id: string;
|
|
162
|
-
compositionId: string;
|
|
163
|
-
baseSnapshotId: string | null;
|
|
164
|
-
publicationOutcome: "published" | "conflict_saved";
|
|
165
|
-
conflictedWithSnapshotId: string | null;
|
|
166
|
-
conflictState: "open" | "resolved" | "discarded" | null;
|
|
167
|
-
resolvedBySnapshotId: string | null;
|
|
168
|
-
createdBy: string;
|
|
169
|
-
createdByName?: string | null;
|
|
170
|
-
clientType: string;
|
|
171
|
-
summary: string | null;
|
|
172
|
-
summarySource: string;
|
|
173
|
-
manifestSha256: string;
|
|
174
|
-
createdAt: string;
|
|
175
|
-
/** Only with `include=manifest`, and only once its bytes hash to manifestSha256. */
|
|
176
|
-
manifest: Manifest | null;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
type UploadStatus = "pending" | "verifying" | "completed" | "failed" | "expired";
|
|
180
|
-
|
|
181
|
-
export interface UploadReceipt {
|
|
182
|
-
uploadId: string;
|
|
183
|
-
compositionId: string;
|
|
184
|
-
snapshotId: string;
|
|
185
|
-
status: UploadStatus;
|
|
186
|
-
/** Why a failed upload failed: cancelled, content_incomplete, verification_timeout, access_revoked. */
|
|
187
|
-
errorCode: string | null;
|
|
188
|
-
baseSnapshotId: string | null;
|
|
189
|
-
resolvesConflictSnapshotIds: string[];
|
|
190
|
-
manifestSha256: string;
|
|
191
|
-
uploadDeadline: string;
|
|
192
|
-
verificationDeadline: string | null;
|
|
193
|
-
completedAt: string | null;
|
|
194
|
-
result: {
|
|
195
|
-
snapshotId: string;
|
|
196
|
-
publicationOutcome: "published" | "conflict_saved";
|
|
197
|
-
conflictedWithSnapshotId: string | null;
|
|
198
|
-
} | null;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
interface BlobRef {
|
|
202
|
-
sha256: string;
|
|
203
|
-
size: number;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
export interface BeginResult extends UploadReceipt {
|
|
207
|
-
/** Already saved in the base or a named conflict: never uploaded. */
|
|
208
|
-
present: BlobRef[];
|
|
209
|
-
/** To upload through upload-urls before submitting. */
|
|
210
|
-
needed: BlobRef[];
|
|
211
|
-
/** False for a replay of the same push under the same Idempotency-Key. */
|
|
212
|
-
created: boolean;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
export interface BeginRequest {
|
|
216
|
-
idempotencyKey: string;
|
|
217
|
-
baseSnapshotId: string | null;
|
|
218
|
-
resolvesConflictSnapshotIds: string[];
|
|
219
|
-
summary: { text: string; source: "user" | "agent" } | null;
|
|
220
|
-
manifest: Manifest;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
interface PartGrant {
|
|
224
|
-
partNumber: number;
|
|
225
|
-
length: number;
|
|
226
|
-
url: string;
|
|
227
|
-
headers: Record<string, string>;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
export interface FileGrant {
|
|
231
|
-
sha256: string;
|
|
232
|
-
size: number;
|
|
233
|
-
expiresAt: string;
|
|
234
|
-
/** Files up to 64 MiB: one PUT. */
|
|
235
|
-
url: string | null;
|
|
236
|
-
headers: Record<string, string>;
|
|
237
|
-
/** Files over 64 MiB: the file's one multipart upload, and the requested parts. */
|
|
238
|
-
multipartUploadId: string | null;
|
|
239
|
-
parts: PartGrant[];
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
export interface GrantRequest {
|
|
243
|
-
sha256: string;
|
|
244
|
-
multipartUploadId?: string;
|
|
245
|
-
parts?: { partNumber: number; sha256: string }[];
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
export interface DownloadBatch {
|
|
249
|
-
snapshotId: string;
|
|
250
|
-
manifestSha256: string;
|
|
251
|
-
continuationToken: string;
|
|
252
|
-
continuationExpiresAt: string;
|
|
253
|
-
urls: { sha256: string; size: number; url: string; expiresAt: string }[];
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
export interface Page<T> {
|
|
257
|
-
items: T[];
|
|
258
|
-
nextToken: string | null;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
export type CompositionRead =
|
|
262
|
-
| { at: "fresh"; composition: Composition; etag: string | null }
|
|
263
|
-
| { at: "unchanged"; etag: string };
|
|
264
|
-
|
|
265
|
-
/** Whose binding: the backend's API host, the signed-in account (accountPrincipal) and the project's real path. */
|
|
266
|
-
export interface BindingKey {
|
|
267
|
-
backend: string;
|
|
268
|
-
account: string;
|
|
269
|
-
projectPath: string;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
/** A push recorded before its Begin is sent, so a lost answer is recovered by sending the same body under the same
|
|
273
|
-
* Idempotency-Key; the upload's ids and deadlines land once an answer comes back. */
|
|
274
|
-
export interface InflightUpload {
|
|
275
|
-
idempotencyKey: string;
|
|
276
|
-
baseSnapshotId: string | null;
|
|
277
|
-
resolvesConflictSnapshotIds: string[];
|
|
278
|
-
summary: { text: string; source: "user" | "agent" } | null;
|
|
279
|
-
manifest: ManifestDocument;
|
|
280
|
-
manifestSha256: string;
|
|
281
|
-
uploadId: string | null;
|
|
282
|
-
snapshotId: string | null;
|
|
283
|
-
uploadDeadline: string | null;
|
|
284
|
-
verificationDeadline: string | null;
|
|
285
|
-
begunAt: number;
|
|
286
|
-
/** Files whose bytes reached staging, by SHA-256, so a resumed send does not write them again. */
|
|
287
|
-
sent?: string[];
|
|
288
|
-
/** Each multipart file's pinned upload and the parts already written, by the file's SHA-256. */
|
|
289
|
-
multipart?: Record<string, { uploadId: string; partsDone: number[] }>;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
export interface Binding extends BindingKey {
|
|
293
|
-
version: 1;
|
|
294
|
-
/** Client-generated, kept before the first request so a retried create names the same composition. */
|
|
295
|
-
compositionId: string;
|
|
296
|
-
ownerSpaceId: string;
|
|
297
|
-
/** The owner workspace as the account's list last named it, so a project whose workspace the account left can
|
|
298
|
-
* still be named. Absent from bindings made before it was kept, and until a list names the workspace. */
|
|
299
|
-
owner?: { name: string; kind: "personal" | "team" };
|
|
300
|
-
boundAt: number;
|
|
301
|
-
/** The composition as last read, and the ETag it came with; null before the create was answered. */
|
|
302
|
-
composition: Composition | null;
|
|
303
|
-
etag: string | null;
|
|
304
|
-
/** The snapshot this copy was last sent as or got from, with its manifest, so local changes need no network. */
|
|
305
|
-
base: { snapshotId: string; manifest: ManifestDocument; manifestSha256: string } | null;
|
|
306
|
-
inflight: InflightUpload | null;
|
|
307
|
-
/** Set while Get from the cloud is filling a folder it made, until the folder is added to Home. Until then the
|
|
308
|
-
* folder is not a project on this Mac, and the next Get of the composition finishes it. */
|
|
309
|
-
gettingFromCloud?: true;
|
|
310
|
-
/** What the marker that Get put in its folder holds: the only proof a file of that name there is not the person's. */
|
|
311
|
-
getToken?: string;
|
|
312
|
-
/** Set on the folder a Duplicate fills from the cloud: it is bound only while it downloads, so no Get ever opens
|
|
313
|
-
* it, and its binding goes once latest is in (duplicate.mjs). */
|
|
314
|
-
duplicate?: true;
|
|
315
|
-
/** The conflicts this Mac's sends saved, newest last: Compare names them as this project's, not another
|
|
316
|
-
* computer's. */
|
|
317
|
-
savedHere?: string[];
|
|
318
|
-
/** Set on a binding Work on {owner}'s project made from a share: the account reaches the owner's workspace through
|
|
319
|
-
* that share's grant, not membership, so every request names the review project (sync-backed sharing C5). */
|
|
320
|
-
sharedFrom?: SharedFrom;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
/** The share a binding came from: its review project, the owner's name and their workspace's, as the share named
|
|
324
|
-
* them when it was opened. */
|
|
325
|
-
export interface SharedFrom {
|
|
326
|
-
reviewProjectId: string;
|
|
327
|
-
owner: string;
|
|
328
|
-
workspace: string | null;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
/** A workspace's last refusal of a new composition: not admitted to sync yet, or encrypted (BYOK). */
|
|
332
|
-
export interface SpaceRefusal {
|
|
333
|
-
kind: "not-enabled" | "byok";
|
|
334
|
-
at: number;
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
/** One send or get under way for a project: the binding it owns until it ends, and where its states go. */
|
|
338
|
-
export interface UploadJob {
|
|
339
|
-
api: import("./api.mjs").SourceApi;
|
|
340
|
-
space: string;
|
|
341
|
-
composition: string;
|
|
342
|
-
uploadId: string;
|
|
343
|
-
inflight: InflightUpload;
|
|
344
|
-
local: Map<string, { path: string; full: string }>;
|
|
345
|
-
needed: { sha256: string; size: number }[];
|
|
346
|
-
fetchImpl: typeof fetch;
|
|
347
|
-
wait: (ms: number, signal?: AbortSignal) => Promise<void>;
|
|
348
|
-
signal: AbortSignal;
|
|
349
|
-
now: () => number;
|
|
350
|
-
persist: () => void;
|
|
351
|
-
onBytes: (bytes: number) => void;
|
|
352
|
-
onFile: () => void;
|
|
353
|
-
}
|