@ai-matrx/data 0.19.2 → 0.21.0
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/CHANGELOG.md +24 -0
- package/dist/files/tus.cjs +245 -0
- package/dist/files/tus.cjs.map +1 -0
- package/dist/files/tus.d.cts +106 -0
- package/dist/files/tus.d.ts +106 -0
- package/dist/files/tus.js +214 -0
- package/dist/files/tus.js.map +1 -0
- package/dist/files.cjs +47 -5
- package/dist/files.cjs.map +1 -1
- package/dist/files.d.cts +70 -3
- package/dist/files.d.ts +70 -3
- package/dist/files.js +47 -5
- package/dist/files.js.map +1 -1
- package/package.json +12 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.21.0 — the one upload transport
|
|
4
|
+
|
|
5
|
+
- New subpath `@ai-matrx/data/files/tus`: `tusTransfer(file, { endpoint, filePath, metadata,
|
|
6
|
+
idempotencyKey, headers, onProgress, signal })` — the ONE resumable upload wire (fresh headers on
|
|
7
|
+
every request, idempotency key on creation only, `mtx-tus-urls` resume store, `X-Cld-File-Id`
|
|
8
|
+
capture with completed-session recovery), plus `buildUploadMetadataEnvelope`,
|
|
9
|
+
`createTusUrlStorage`, `listStoredTusUploads`, `TUS_CHUNK_SIZE_BYTES`, `TUS_UPLOAD_PATH`. Moved from
|
|
10
|
+
`@ai-matrx/media` (architecture step 14, D1); its own subpath so `./files` never loads tus-js-client.
|
|
11
|
+
- `@ai-matrx/data/files` gains the upload dedup guard (`claimUpload`, `uploadDedupKey`), moved from media.
|
|
12
|
+
- `fileUrls(filesBaseUrl, fileId)` is renamed `buildFileUrls(filesBaseUrl, fileId)`. `fileUrls(fileId)`
|
|
13
|
+
is the one caller-facing signature (media's host-bound builder); the old name stays as a deprecated
|
|
14
|
+
alias for this release only.
|
|
15
|
+
|
|
16
|
+
Consumer action: replace `fileUrls(base, id)` from `@ai-matrx/data/files` with `buildFileUrls(base, id)`
|
|
17
|
+
(no consumer app imports it today). A host needing resumable uploads injects `tusTransfer` instead of its own TUS client.
|
|
18
|
+
|
|
19
|
+
## 0.20.0 — the one file error taxonomy
|
|
20
|
+
|
|
21
|
+
- `FileErrorCode` gains media's codes (`cors_blocked`, `mime_unknown`, `upload_cancelled`,
|
|
22
|
+
`quota_exceeded`, `in_flight`), so `@ai-matrx/data/files`' `FileError` family is the one file
|
|
23
|
+
error taxonomy; media re-exports it (architecture step 14).
|
|
24
|
+
|
|
25
|
+
Consumer action: none (additive).
|
|
26
|
+
|
|
3
27
|
## 0.19.2
|
|
4
28
|
|
|
5
29
|
- Re-release of 0.19.1 (its publish stopped at the frozen-lockfile install: the tagged commit's
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/files/tus.ts
|
|
31
|
+
var tus_exports = {};
|
|
32
|
+
__export(tus_exports, {
|
|
33
|
+
TUS_CHUNK_SIZE_BYTES: () => TUS_CHUNK_SIZE_BYTES,
|
|
34
|
+
TUS_UPLOAD_PATH: () => TUS_UPLOAD_PATH,
|
|
35
|
+
buildUploadMetadataEnvelope: () => buildUploadMetadataEnvelope,
|
|
36
|
+
createTusUrlStorage: () => createTusUrlStorage,
|
|
37
|
+
listStoredTusUploads: () => listStoredTusUploads,
|
|
38
|
+
tusTransfer: () => tusTransfer
|
|
39
|
+
});
|
|
40
|
+
module.exports = __toCommonJS(tus_exports);
|
|
41
|
+
var tus = __toESM(require("tus-js-client"), 1);
|
|
42
|
+
var TUS_CHUNK_SIZE_BYTES = 16 * 1024 * 1024;
|
|
43
|
+
var TUS_UPLOAD_PATH = "/files/upload/tus";
|
|
44
|
+
function buildUploadMetadataEnvelope(metadata) {
|
|
45
|
+
const envelope = {
|
|
46
|
+
// Origin tag aids backend log triage when something goes wrong.
|
|
47
|
+
origin: "cloudUpload",
|
|
48
|
+
...metadata ?? {}
|
|
49
|
+
};
|
|
50
|
+
if (process.env.NODE_ENV !== "production" && !hasUploadOrganization(envelope)) {
|
|
51
|
+
console.warn(
|
|
52
|
+
"[buildUploadMetadataEnvelope] upload carries no scope.organization_id \u2014 it will be filed in an organization nobody chose. Route this upload through the host's organization gate before the bytes move."
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
return envelope;
|
|
56
|
+
}
|
|
57
|
+
function hasUploadOrganization(envelope) {
|
|
58
|
+
if (typeof envelope.organization_id === "string" && envelope.organization_id) return true;
|
|
59
|
+
const scope = envelope.scope;
|
|
60
|
+
return Boolean(
|
|
61
|
+
scope && typeof scope === "object" && typeof scope.organization_id === "string" && scope.organization_id
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
var URL_DB_NAME = "mtx-tus-urls";
|
|
65
|
+
var URL_STORE = "urls";
|
|
66
|
+
var urlDbPromise = null;
|
|
67
|
+
function openUrlDb() {
|
|
68
|
+
if (urlDbPromise) return urlDbPromise;
|
|
69
|
+
urlDbPromise = new Promise((resolve, reject) => {
|
|
70
|
+
if (typeof indexedDB === "undefined") {
|
|
71
|
+
reject(new Error("[tusUpload] IndexedDB unavailable"));
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const req = indexedDB.open(URL_DB_NAME, 1);
|
|
75
|
+
req.onupgradeneeded = () => {
|
|
76
|
+
const db = req.result;
|
|
77
|
+
if (!db.objectStoreNames.contains(URL_STORE)) {
|
|
78
|
+
const store = db.createObjectStore(URL_STORE, { keyPath: "urlStorageKey" });
|
|
79
|
+
store.createIndex("fingerprint", "fingerprint", { unique: false });
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
req.onsuccess = () => resolve(req.result);
|
|
83
|
+
req.onerror = () => reject(req.error ?? new Error("[tusUpload] failed to open mtx-tus-urls"));
|
|
84
|
+
});
|
|
85
|
+
urlDbPromise.catch(() => {
|
|
86
|
+
urlDbPromise = null;
|
|
87
|
+
});
|
|
88
|
+
return urlDbPromise;
|
|
89
|
+
}
|
|
90
|
+
function idbRequest(req) {
|
|
91
|
+
return new Promise((resolve, reject) => {
|
|
92
|
+
req.onsuccess = () => resolve(req.result);
|
|
93
|
+
req.onerror = () => reject(req.error ?? new Error("IndexedDB failed"));
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
function createTusUrlStorage() {
|
|
97
|
+
async function write(record, deleteKey) {
|
|
98
|
+
const db = await openUrlDb();
|
|
99
|
+
const tx = db.transaction(URL_STORE, "readwrite");
|
|
100
|
+
if (record) tx.objectStore(URL_STORE).put(record);
|
|
101
|
+
else if (deleteKey) tx.objectStore(URL_STORE).delete(deleteKey);
|
|
102
|
+
await new Promise((resolve, reject) => {
|
|
103
|
+
tx.oncomplete = () => resolve();
|
|
104
|
+
tx.onerror = () => reject(tx.error ?? new Error("IndexedDB tx failed"));
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
async findAllUploads() {
|
|
109
|
+
const db = await openUrlDb();
|
|
110
|
+
const tx = db.transaction(URL_STORE, "readonly");
|
|
111
|
+
return idbRequest(tx.objectStore(URL_STORE).getAll());
|
|
112
|
+
},
|
|
113
|
+
async findUploadsByFingerprint(fingerprint) {
|
|
114
|
+
const db = await openUrlDb();
|
|
115
|
+
const tx = db.transaction(URL_STORE, "readonly");
|
|
116
|
+
return idbRequest(
|
|
117
|
+
tx.objectStore(URL_STORE).index("fingerprint").getAll(fingerprint)
|
|
118
|
+
);
|
|
119
|
+
},
|
|
120
|
+
async removeUpload(urlStorageKey) {
|
|
121
|
+
await write(null, urlStorageKey);
|
|
122
|
+
},
|
|
123
|
+
async addUpload(fingerprint, upload) {
|
|
124
|
+
const urlStorageKey = `tus::${fingerprint}::${Date.now().toString(36)}`;
|
|
125
|
+
await write({
|
|
126
|
+
urlStorageKey,
|
|
127
|
+
fingerprint,
|
|
128
|
+
size: upload.size,
|
|
129
|
+
metadata: upload.metadata,
|
|
130
|
+
creationTime: upload.creationTime,
|
|
131
|
+
uploadUrl: upload.uploadUrl,
|
|
132
|
+
parallelUploadUrls: upload.parallelUploadUrls
|
|
133
|
+
});
|
|
134
|
+
return urlStorageKey;
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
async function listStoredTusUploads() {
|
|
139
|
+
try {
|
|
140
|
+
const db = await openUrlDb();
|
|
141
|
+
const tx = db.transaction(URL_STORE, "readonly");
|
|
142
|
+
const all = await idbRequest(tx.objectStore(URL_STORE).getAll());
|
|
143
|
+
return all.map((r) => ({
|
|
144
|
+
urlStorageKey: r.urlStorageKey,
|
|
145
|
+
fingerprint: r.fingerprint,
|
|
146
|
+
size: r.size,
|
|
147
|
+
metadata: r.metadata,
|
|
148
|
+
creationTime: r.creationTime,
|
|
149
|
+
uploadUrl: r.uploadUrl
|
|
150
|
+
}));
|
|
151
|
+
} catch (err) {
|
|
152
|
+
console.error("[tusUpload] listStoredTusUploads failed:", err);
|
|
153
|
+
return [];
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
var FORWARDED_HEADERS = ["Authorization", "X-Guest-Fingerprint", "X-Organization-Id"];
|
|
157
|
+
function errorText(err) {
|
|
158
|
+
if (err instanceof Error) return err.message;
|
|
159
|
+
return typeof err === "string" ? err : String(err);
|
|
160
|
+
}
|
|
161
|
+
async function tusTransfer(file, opts) {
|
|
162
|
+
const fileName = typeof File !== "undefined" && file instanceof File ? file.name : opts.filePath.split("/").pop() ?? "upload";
|
|
163
|
+
const metadataEnvelope = buildUploadMetadataEnvelope(opts.metadata);
|
|
164
|
+
let cldFileId = null;
|
|
165
|
+
let aborted = false;
|
|
166
|
+
try {
|
|
167
|
+
await new Promise((resolve, reject) => {
|
|
168
|
+
const upload = new tus.Upload(file, {
|
|
169
|
+
endpoint: opts.endpoint,
|
|
170
|
+
chunkSize: TUS_CHUNK_SIZE_BYTES,
|
|
171
|
+
retryDelays: [0, 1e3, 3e3, 5e3, 1e4],
|
|
172
|
+
removeFingerprintOnSuccess: true,
|
|
173
|
+
urlStorage: opts.urlStorage ?? createTusUrlStorage(),
|
|
174
|
+
...opts.httpStack ? { httpStack: opts.httpStack } : {},
|
|
175
|
+
...opts.fileReader ? { fileReader: opts.fileReader } : {},
|
|
176
|
+
metadata: {
|
|
177
|
+
filename: fileName,
|
|
178
|
+
filepath: opts.filePath,
|
|
179
|
+
// tus-js-client base64-encodes the value per the Upload-Metadata
|
|
180
|
+
// spec; the server merges it exactly like the buffered form field.
|
|
181
|
+
metadata_json: JSON.stringify(metadataEnvelope),
|
|
182
|
+
...opts.visibility ? { visibility: opts.visibility } : {}
|
|
183
|
+
},
|
|
184
|
+
onBeforeRequest: async (req) => {
|
|
185
|
+
const headers = await opts.headers();
|
|
186
|
+
for (const name of FORWARDED_HEADERS) {
|
|
187
|
+
const value = headers[name];
|
|
188
|
+
if (value) req.setHeader(name, value);
|
|
189
|
+
}
|
|
190
|
+
if (req.getMethod() === "POST") req.setHeader("X-Idempotency-Key", opts.idempotencyKey);
|
|
191
|
+
},
|
|
192
|
+
onAfterResponse: (_req, res) => {
|
|
193
|
+
const id = res.getHeader("X-Cld-File-Id");
|
|
194
|
+
if (id) cldFileId = id;
|
|
195
|
+
},
|
|
196
|
+
onProgress: (bytesSent, bytesTotal) => {
|
|
197
|
+
opts.onProgress?.(bytesSent, bytesTotal);
|
|
198
|
+
},
|
|
199
|
+
onSuccess: () => resolve(),
|
|
200
|
+
onError: (error) => reject(error)
|
|
201
|
+
});
|
|
202
|
+
if (opts.signal) {
|
|
203
|
+
if (opts.signal.aborted) {
|
|
204
|
+
aborted = true;
|
|
205
|
+
reject(new Error("Upload cancelled"));
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
opts.signal.addEventListener(
|
|
209
|
+
"abort",
|
|
210
|
+
() => {
|
|
211
|
+
aborted = true;
|
|
212
|
+
void upload.abort();
|
|
213
|
+
reject(new Error("Upload cancelled"));
|
|
214
|
+
},
|
|
215
|
+
{ once: true }
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
void upload.findPreviousUploads().then((previous) => {
|
|
219
|
+
const first = previous[0];
|
|
220
|
+
if (first) upload.resumeFromPreviousUpload(first);
|
|
221
|
+
upload.start();
|
|
222
|
+
}).catch((err) => {
|
|
223
|
+
console.warn("[tusUpload] previous-upload lookup failed \u2014 starting fresh:", err);
|
|
224
|
+
upload.start();
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
} catch (err) {
|
|
228
|
+
if (cldFileId && !aborted) {
|
|
229
|
+
console.warn(
|
|
230
|
+
`[tusUpload] transfer reported an error but the server exposed X-Cld-File-Id=${cldFileId} \u2014 recovering the completed session.`
|
|
231
|
+
);
|
|
232
|
+
} else {
|
|
233
|
+
return { ok: false, error: errorText(err), errorCode: aborted ? "upload_cancelled" : "tus_upload_failed" };
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
if (!cldFileId) {
|
|
237
|
+
return {
|
|
238
|
+
ok: false,
|
|
239
|
+
error: "TUS upload finished but the server never exposed X-Cld-File-Id \u2014 the file cannot be resolved. Check server CORS expose_headers.",
|
|
240
|
+
errorCode: "tus_missing_file_id"
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
return { ok: true, fileId: cldFileId };
|
|
244
|
+
}
|
|
245
|
+
//# sourceMappingURL=tus.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/files/tus.ts"],"sourcesContent":["/**\n * @ai-matrx/data/files/tus — the ONE resumable (TUS) upload transport.\n *\n * Origin: `@ai-matrx/media/files/engine/upload/tusUpload.ts` (architecture\n * step 14, D1: only byte transport moves to data). Media keeps the Redux\n * side and the record hydration; every host — the app, the extension, the\n * desktop — rides this one wire:\n *\n * - creation POST carries `X-Idempotency-Key` (one intended upload, one key);\n * - FRESH headers on EVERY request (long uploads outlive a JWT; the\n * organization header must reach every chunk — aidream 8e5ee0b93 400s an\n * unscoped upload);\n * - one validated `metadata_json` envelope, byte-identical to the buffered\n * form field ({@link buildUploadMetadataEnvelope});\n * - automatic resume from the dedicated `mtx-tus-urls` IndexedDB store;\n * - the created file id comes back in `X-Cld-File-Id`, including the\n * completed-session recovery when the final response was lost.\n *\n * Its own subpath so `@ai-matrx/data/files` never loads tus-js-client.\n */\n\nimport * as tus from \"tus-js-client\";\n\n/** Server-bounded chunk size (8–32 MiB allowed): 16 MiB. */\nexport const TUS_CHUNK_SIZE_BYTES = 16 * 1024 * 1024;\n\n/** The TUS creation route on the files service. */\nexport const TUS_UPLOAD_PATH = \"/files/upload/tus\";\n\n// ─── Shared metadata envelope (buffered ↔ TUS parity) ────────────────────────\n\n/**\n * The ONE builder for the upload metadata JSON object. The buffered path\n * serializes it into the `metadata_json` form field; the TUS path serializes\n * the SAME object into the `metadata_json` Upload-Metadata key. One builder ⇒\n * the two transports can never drift.\n *\n * `scope.organization_id` is the OWNING WORKSPACE of the uploaded file. The\n * server stamps `files.files.organization_id` from it (membership-checked);\n * omitted, the server falls back to the uploader's own organization — a silent\n * mis-filing (2026-08-30). Loud in development, never a hard failure.\n */\nexport function buildUploadMetadataEnvelope(\n metadata: Record<string, unknown> | undefined,\n): Record<string, unknown> {\n const envelope: Record<string, unknown> = {\n // Origin tag aids backend log triage when something goes wrong.\n origin: \"cloudUpload\",\n ...(metadata ?? {}),\n };\n if (process.env.NODE_ENV !== \"production\" && !hasUploadOrganization(envelope)) {\n console.warn(\n \"[buildUploadMetadataEnvelope] upload carries no scope.organization_id — \" +\n \"it will be filed in an organization nobody chose. Route this \" +\n \"upload through the host's organization gate before the bytes move.\",\n );\n }\n return envelope;\n}\n\nfunction hasUploadOrganization(envelope: Record<string, unknown>): boolean {\n if (typeof envelope.organization_id === \"string\" && envelope.organization_id) return true;\n const scope = envelope.scope;\n return Boolean(\n scope &&\n typeof scope === \"object\" &&\n typeof (scope as Record<string, unknown>).organization_id === \"string\" &&\n (scope as Record<string, unknown>).organization_id,\n );\n}\n\n// ─── Dedicated resume-URL storage (mtx-tus-urls) ─────────────────────────────\n\nconst URL_DB_NAME = \"mtx-tus-urls\";\nconst URL_STORE = \"urls\";\n\ninterface StoredTusUpload {\n urlStorageKey: string;\n fingerprint: string;\n size: number | null;\n metadata: Record<string, string>;\n creationTime: string;\n uploadUrl: string | null;\n parallelUploadUrls: string[] | null;\n}\n\nlet urlDbPromise: Promise<IDBDatabase> | null = null;\n\nfunction openUrlDb(): Promise<IDBDatabase> {\n if (urlDbPromise) return urlDbPromise;\n urlDbPromise = new Promise<IDBDatabase>((resolve, reject) => {\n if (typeof indexedDB === \"undefined\") {\n reject(new Error(\"[tusUpload] IndexedDB unavailable\"));\n return;\n }\n const req = indexedDB.open(URL_DB_NAME, 1);\n req.onupgradeneeded = () => {\n const db = req.result;\n if (!db.objectStoreNames.contains(URL_STORE)) {\n const store = db.createObjectStore(URL_STORE, { keyPath: \"urlStorageKey\" });\n store.createIndex(\"fingerprint\", \"fingerprint\", { unique: false });\n }\n };\n req.onsuccess = () => resolve(req.result);\n req.onerror = () => reject(req.error ?? new Error(\"[tusUpload] failed to open mtx-tus-urls\"));\n });\n urlDbPromise.catch(() => {\n urlDbPromise = null;\n });\n return urlDbPromise;\n}\n\nfunction idbRequest<T>(req: IDBRequest<T>): Promise<T> {\n return new Promise<T>((resolve, reject) => {\n req.onsuccess = () => resolve(req.result);\n req.onerror = () => reject(req.error ?? new Error(\"IndexedDB failed\"));\n });\n}\n\ntype PreviousUpload = Awaited<ReturnType<tus.Upload[\"findPreviousUploads\"]>>[number];\n\n/** tus-js-client HttpStack port (tests inject a mock wire). */\nexport type TusHttpStack = tus.HttpStack;\n/** tus-js-client file reader port (tests: Node builds cannot slice browser Files). */\nexport type TusFileReader = ConstructorParameters<typeof tus.Upload>[1][\"fileReader\"];\n\n/** tus-js-client UrlStorage port. */\nexport type TusUrlStorage = NonNullable<ConstructorParameters<typeof tus.Upload>[1][\"urlStorage\"]>;\n\n/** tus-js-client UrlStorage backed by the dedicated `mtx-tus-urls` DB —\n * deliberately SEPARATE from any recorder chunk journal. */\nexport function createTusUrlStorage(): TusUrlStorage {\n async function write(record: StoredTusUpload | null, deleteKey?: string): Promise<void> {\n const db = await openUrlDb();\n const tx = db.transaction(URL_STORE, \"readwrite\");\n if (record) tx.objectStore(URL_STORE).put(record);\n else if (deleteKey) tx.objectStore(URL_STORE).delete(deleteKey);\n await new Promise<void>((resolve, reject) => {\n tx.oncomplete = () => resolve();\n tx.onerror = () => reject(tx.error ?? new Error(\"IndexedDB tx failed\"));\n });\n }\n return {\n async findAllUploads(): Promise<PreviousUpload[]> {\n const db = await openUrlDb();\n const tx = db.transaction(URL_STORE, \"readonly\");\n return idbRequest(tx.objectStore(URL_STORE).getAll() as IDBRequest<StoredTusUpload[]>);\n },\n async findUploadsByFingerprint(fingerprint: string): Promise<PreviousUpload[]> {\n const db = await openUrlDb();\n const tx = db.transaction(URL_STORE, \"readonly\");\n return idbRequest(\n tx.objectStore(URL_STORE).index(\"fingerprint\").getAll(fingerprint) as IDBRequest<StoredTusUpload[]>,\n );\n },\n async removeUpload(urlStorageKey: string): Promise<void> {\n await write(null, urlStorageKey);\n },\n async addUpload(fingerprint: string, upload: PreviousUpload): Promise<string> {\n const urlStorageKey = `tus::${fingerprint}::${Date.now().toString(36)}`;\n await write({\n urlStorageKey,\n fingerprint,\n size: upload.size,\n metadata: upload.metadata,\n creationTime: upload.creationTime,\n uploadUrl: upload.uploadUrl,\n parallelUploadUrls: upload.parallelUploadUrls,\n });\n return urlStorageKey;\n },\n };\n}\n\n/** Read-only summary of a stored TUS resume session (diagnostics surfaces). */\nexport interface StoredTusUploadSummary {\n urlStorageKey: string;\n fingerprint: string;\n size: number | null;\n metadata: Record<string, string>;\n creationTime: string;\n uploadUrl: string | null;\n}\n\n/**\n * List every stored TUS resume-URL entry. Read-only — resuming happens inside\n * {@link tusTransfer}; this only lets management surfaces SHOW a pending\n * session. Returns [] where IndexedDB is unavailable (never throws).\n */\nexport async function listStoredTusUploads(): Promise<StoredTusUploadSummary[]> {\n try {\n const db = await openUrlDb();\n const tx = db.transaction(URL_STORE, \"readonly\");\n const all = await idbRequest(tx.objectStore(URL_STORE).getAll() as IDBRequest<StoredTusUpload[]>);\n return all.map((r) => ({\n urlStorageKey: r.urlStorageKey,\n fingerprint: r.fingerprint,\n size: r.size,\n metadata: r.metadata,\n creationTime: r.creationTime,\n uploadUrl: r.uploadUrl,\n }));\n } catch (err) {\n console.error(\"[tusUpload] listStoredTusUploads failed:\", err);\n return [];\n }\n}\n\n// ─── The transport ───────────────────────────────────────────────────────────\n\nexport interface TusTransferOptions {\n /** Full creation URL (`{filesBase}${TUS_UPLOAD_PATH}`). */\n endpoint: string;\n /** Destination path including the file name. */\n filePath: string;\n /** Caller metadata; wrapped by {@link buildUploadMetadataEnvelope}. */\n metadata?: Record<string, unknown> | undefined;\n /** Optional Upload-Metadata `visibility` (transitional wire input). */\n visibility?: string | undefined;\n /** One intended upload, one key — sent on the creation POST only. */\n idempotencyKey: string;\n /**\n * FRESH request headers, read before EVERY request. Only `Authorization`,\n * `X-Guest-Fingerprint` and `X-Organization-Id` are forwarded.\n */\n headers: () => Promise<Record<string, string | undefined>>;\n onProgress?: ((loaded: number, total: number) => void) | undefined;\n signal?: AbortSignal | undefined;\n /** Injected HttpStack (tests: mock wire, no XHR). */\n httpStack?: TusHttpStack | undefined;\n /** Injected file reader (tests: Node builds cannot slice browser Files). */\n fileReader?: TusFileReader | undefined;\n /** Override URL storage (tests). Default: {@link createTusUrlStorage}. */\n urlStorage?: TusUrlStorage | undefined;\n}\n\nexport type TusTransferErrorCode = \"upload_cancelled\" | \"tus_upload_failed\" | \"tus_missing_file_id\";\n\nexport type TusTransferResult =\n | { ok: true; fileId: string }\n | { ok: false; error: string; errorCode: TusTransferErrorCode };\n\nconst FORWARDED_HEADERS = [\"Authorization\", \"X-Guest-Fingerprint\", \"X-Organization-Id\"] as const;\n\nfunction errorText(err: unknown): string {\n if (err instanceof Error) return err.message;\n return typeof err === \"string\" ? err : String(err);\n}\n\n/**\n * Resumable upload of one file. Never throws — `{ ok: false, errorCode }` on\n * failure. Resumes a previous session for the same file automatically; when the\n * transfer errors but the server already exposed `X-Cld-File-Id`, the completed\n * session is recovered (loudly) instead of failing or re-uploading.\n */\nexport async function tusTransfer(file: File | Blob, opts: TusTransferOptions): Promise<TusTransferResult> {\n const fileName = typeof File !== \"undefined\" && file instanceof File ? file.name : opts.filePath.split(\"/\").pop() ?? \"upload\";\n const metadataEnvelope = buildUploadMetadataEnvelope(opts.metadata);\n let cldFileId: string | null = null;\n let aborted = false;\n\n try {\n await new Promise<void>((resolve, reject) => {\n const upload: tus.Upload = new tus.Upload(file, {\n endpoint: opts.endpoint,\n chunkSize: TUS_CHUNK_SIZE_BYTES,\n retryDelays: [0, 1000, 3000, 5000, 10000],\n removeFingerprintOnSuccess: true,\n urlStorage: opts.urlStorage ?? createTusUrlStorage(),\n ...(opts.httpStack ? { httpStack: opts.httpStack } : {}),\n ...(opts.fileReader ? { fileReader: opts.fileReader } : {}),\n metadata: {\n filename: fileName,\n filepath: opts.filePath,\n // tus-js-client base64-encodes the value per the Upload-Metadata\n // spec; the server merges it exactly like the buffered form field.\n metadata_json: JSON.stringify(metadataEnvelope),\n ...(opts.visibility ? { visibility: opts.visibility } : {}),\n },\n onBeforeRequest: async (req) => {\n const headers = await opts.headers();\n for (const name of FORWARDED_HEADERS) {\n const value = headers[name];\n if (value) req.setHeader(name, value);\n }\n if (req.getMethod() === \"POST\") req.setHeader(\"X-Idempotency-Key\", opts.idempotencyKey);\n },\n onAfterResponse: (_req, res) => {\n const id = res.getHeader(\"X-Cld-File-Id\");\n if (id) cldFileId = id;\n },\n onProgress: (bytesSent, bytesTotal) => {\n opts.onProgress?.(bytesSent, bytesTotal);\n },\n onSuccess: () => resolve(),\n onError: (error) => reject(error),\n });\n\n if (opts.signal) {\n if (opts.signal.aborted) {\n aborted = true;\n reject(new Error(\"Upload cancelled\"));\n return;\n }\n opts.signal.addEventListener(\n \"abort\",\n () => {\n aborted = true;\n void upload.abort();\n reject(new Error(\"Upload cancelled\"));\n },\n { once: true },\n );\n }\n\n void upload\n .findPreviousUploads()\n .then((previous) => {\n const first = previous[0];\n if (first) upload.resumeFromPreviousUpload(first);\n upload.start();\n })\n .catch((err) => {\n console.warn(\"[tusUpload] previous-upload lookup failed — starting fresh:\", err);\n upload.start();\n });\n });\n } catch (err) {\n if (cldFileId && !aborted) {\n console.warn(\n `[tusUpload] transfer reported an error but the server exposed ` +\n `X-Cld-File-Id=${cldFileId} — recovering the completed session.`,\n );\n } else {\n return { ok: false, error: errorText(err), errorCode: aborted ? \"upload_cancelled\" : \"tus_upload_failed\" };\n }\n }\n\n if (!cldFileId) {\n return {\n ok: false,\n error:\n \"TUS upload finished but the server never exposed X-Cld-File-Id — \" +\n \"the file cannot be resolved. Check server CORS expose_headers.\",\n errorCode: \"tus_missing_file_id\",\n };\n }\n return { ok: true, fileId: cldFileId };\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import * as tus from 'tus-js-client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @ai-matrx/data/files/tus — the ONE resumable (TUS) upload transport.
|
|
5
|
+
*
|
|
6
|
+
* Origin: `@ai-matrx/media/files/engine/upload/tusUpload.ts` (architecture
|
|
7
|
+
* step 14, D1: only byte transport moves to data). Media keeps the Redux
|
|
8
|
+
* side and the record hydration; every host — the app, the extension, the
|
|
9
|
+
* desktop — rides this one wire:
|
|
10
|
+
*
|
|
11
|
+
* - creation POST carries `X-Idempotency-Key` (one intended upload, one key);
|
|
12
|
+
* - FRESH headers on EVERY request (long uploads outlive a JWT; the
|
|
13
|
+
* organization header must reach every chunk — aidream 8e5ee0b93 400s an
|
|
14
|
+
* unscoped upload);
|
|
15
|
+
* - one validated `metadata_json` envelope, byte-identical to the buffered
|
|
16
|
+
* form field ({@link buildUploadMetadataEnvelope});
|
|
17
|
+
* - automatic resume from the dedicated `mtx-tus-urls` IndexedDB store;
|
|
18
|
+
* - the created file id comes back in `X-Cld-File-Id`, including the
|
|
19
|
+
* completed-session recovery when the final response was lost.
|
|
20
|
+
*
|
|
21
|
+
* Its own subpath so `@ai-matrx/data/files` never loads tus-js-client.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/** Server-bounded chunk size (8–32 MiB allowed): 16 MiB. */
|
|
25
|
+
declare const TUS_CHUNK_SIZE_BYTES: number;
|
|
26
|
+
/** The TUS creation route on the files service. */
|
|
27
|
+
declare const TUS_UPLOAD_PATH = "/files/upload/tus";
|
|
28
|
+
/**
|
|
29
|
+
* The ONE builder for the upload metadata JSON object. The buffered path
|
|
30
|
+
* serializes it into the `metadata_json` form field; the TUS path serializes
|
|
31
|
+
* the SAME object into the `metadata_json` Upload-Metadata key. One builder ⇒
|
|
32
|
+
* the two transports can never drift.
|
|
33
|
+
*
|
|
34
|
+
* `scope.organization_id` is the OWNING WORKSPACE of the uploaded file. The
|
|
35
|
+
* server stamps `files.files.organization_id` from it (membership-checked);
|
|
36
|
+
* omitted, the server falls back to the uploader's own organization — a silent
|
|
37
|
+
* mis-filing (2026-08-30). Loud in development, never a hard failure.
|
|
38
|
+
*/
|
|
39
|
+
declare function buildUploadMetadataEnvelope(metadata: Record<string, unknown> | undefined): Record<string, unknown>;
|
|
40
|
+
/** tus-js-client HttpStack port (tests inject a mock wire). */
|
|
41
|
+
type TusHttpStack = tus.HttpStack;
|
|
42
|
+
/** tus-js-client file reader port (tests: Node builds cannot slice browser Files). */
|
|
43
|
+
type TusFileReader = ConstructorParameters<typeof tus.Upload>[1]["fileReader"];
|
|
44
|
+
/** tus-js-client UrlStorage port. */
|
|
45
|
+
type TusUrlStorage = NonNullable<ConstructorParameters<typeof tus.Upload>[1]["urlStorage"]>;
|
|
46
|
+
/** tus-js-client UrlStorage backed by the dedicated `mtx-tus-urls` DB —
|
|
47
|
+
* deliberately SEPARATE from any recorder chunk journal. */
|
|
48
|
+
declare function createTusUrlStorage(): TusUrlStorage;
|
|
49
|
+
/** Read-only summary of a stored TUS resume session (diagnostics surfaces). */
|
|
50
|
+
interface StoredTusUploadSummary {
|
|
51
|
+
urlStorageKey: string;
|
|
52
|
+
fingerprint: string;
|
|
53
|
+
size: number | null;
|
|
54
|
+
metadata: Record<string, string>;
|
|
55
|
+
creationTime: string;
|
|
56
|
+
uploadUrl: string | null;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* List every stored TUS resume-URL entry. Read-only — resuming happens inside
|
|
60
|
+
* {@link tusTransfer}; this only lets management surfaces SHOW a pending
|
|
61
|
+
* session. Returns [] where IndexedDB is unavailable (never throws).
|
|
62
|
+
*/
|
|
63
|
+
declare function listStoredTusUploads(): Promise<StoredTusUploadSummary[]>;
|
|
64
|
+
interface TusTransferOptions {
|
|
65
|
+
/** Full creation URL (`{filesBase}${TUS_UPLOAD_PATH}`). */
|
|
66
|
+
endpoint: string;
|
|
67
|
+
/** Destination path including the file name. */
|
|
68
|
+
filePath: string;
|
|
69
|
+
/** Caller metadata; wrapped by {@link buildUploadMetadataEnvelope}. */
|
|
70
|
+
metadata?: Record<string, unknown> | undefined;
|
|
71
|
+
/** Optional Upload-Metadata `visibility` (transitional wire input). */
|
|
72
|
+
visibility?: string | undefined;
|
|
73
|
+
/** One intended upload, one key — sent on the creation POST only. */
|
|
74
|
+
idempotencyKey: string;
|
|
75
|
+
/**
|
|
76
|
+
* FRESH request headers, read before EVERY request. Only `Authorization`,
|
|
77
|
+
* `X-Guest-Fingerprint` and `X-Organization-Id` are forwarded.
|
|
78
|
+
*/
|
|
79
|
+
headers: () => Promise<Record<string, string | undefined>>;
|
|
80
|
+
onProgress?: ((loaded: number, total: number) => void) | undefined;
|
|
81
|
+
signal?: AbortSignal | undefined;
|
|
82
|
+
/** Injected HttpStack (tests: mock wire, no XHR). */
|
|
83
|
+
httpStack?: TusHttpStack | undefined;
|
|
84
|
+
/** Injected file reader (tests: Node builds cannot slice browser Files). */
|
|
85
|
+
fileReader?: TusFileReader | undefined;
|
|
86
|
+
/** Override URL storage (tests). Default: {@link createTusUrlStorage}. */
|
|
87
|
+
urlStorage?: TusUrlStorage | undefined;
|
|
88
|
+
}
|
|
89
|
+
type TusTransferErrorCode = "upload_cancelled" | "tus_upload_failed" | "tus_missing_file_id";
|
|
90
|
+
type TusTransferResult = {
|
|
91
|
+
ok: true;
|
|
92
|
+
fileId: string;
|
|
93
|
+
} | {
|
|
94
|
+
ok: false;
|
|
95
|
+
error: string;
|
|
96
|
+
errorCode: TusTransferErrorCode;
|
|
97
|
+
};
|
|
98
|
+
/**
|
|
99
|
+
* Resumable upload of one file. Never throws — `{ ok: false, errorCode }` on
|
|
100
|
+
* failure. Resumes a previous session for the same file automatically; when the
|
|
101
|
+
* transfer errors but the server already exposed `X-Cld-File-Id`, the completed
|
|
102
|
+
* session is recovered (loudly) instead of failing or re-uploading.
|
|
103
|
+
*/
|
|
104
|
+
declare function tusTransfer(file: File | Blob, opts: TusTransferOptions): Promise<TusTransferResult>;
|
|
105
|
+
|
|
106
|
+
export { type StoredTusUploadSummary, TUS_CHUNK_SIZE_BYTES, TUS_UPLOAD_PATH, type TusFileReader, type TusHttpStack, type TusTransferErrorCode, type TusTransferOptions, type TusTransferResult, type TusUrlStorage, buildUploadMetadataEnvelope, createTusUrlStorage, listStoredTusUploads, tusTransfer };
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import * as tus from 'tus-js-client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @ai-matrx/data/files/tus — the ONE resumable (TUS) upload transport.
|
|
5
|
+
*
|
|
6
|
+
* Origin: `@ai-matrx/media/files/engine/upload/tusUpload.ts` (architecture
|
|
7
|
+
* step 14, D1: only byte transport moves to data). Media keeps the Redux
|
|
8
|
+
* side and the record hydration; every host — the app, the extension, the
|
|
9
|
+
* desktop — rides this one wire:
|
|
10
|
+
*
|
|
11
|
+
* - creation POST carries `X-Idempotency-Key` (one intended upload, one key);
|
|
12
|
+
* - FRESH headers on EVERY request (long uploads outlive a JWT; the
|
|
13
|
+
* organization header must reach every chunk — aidream 8e5ee0b93 400s an
|
|
14
|
+
* unscoped upload);
|
|
15
|
+
* - one validated `metadata_json` envelope, byte-identical to the buffered
|
|
16
|
+
* form field ({@link buildUploadMetadataEnvelope});
|
|
17
|
+
* - automatic resume from the dedicated `mtx-tus-urls` IndexedDB store;
|
|
18
|
+
* - the created file id comes back in `X-Cld-File-Id`, including the
|
|
19
|
+
* completed-session recovery when the final response was lost.
|
|
20
|
+
*
|
|
21
|
+
* Its own subpath so `@ai-matrx/data/files` never loads tus-js-client.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/** Server-bounded chunk size (8–32 MiB allowed): 16 MiB. */
|
|
25
|
+
declare const TUS_CHUNK_SIZE_BYTES: number;
|
|
26
|
+
/** The TUS creation route on the files service. */
|
|
27
|
+
declare const TUS_UPLOAD_PATH = "/files/upload/tus";
|
|
28
|
+
/**
|
|
29
|
+
* The ONE builder for the upload metadata JSON object. The buffered path
|
|
30
|
+
* serializes it into the `metadata_json` form field; the TUS path serializes
|
|
31
|
+
* the SAME object into the `metadata_json` Upload-Metadata key. One builder ⇒
|
|
32
|
+
* the two transports can never drift.
|
|
33
|
+
*
|
|
34
|
+
* `scope.organization_id` is the OWNING WORKSPACE of the uploaded file. The
|
|
35
|
+
* server stamps `files.files.organization_id` from it (membership-checked);
|
|
36
|
+
* omitted, the server falls back to the uploader's own organization — a silent
|
|
37
|
+
* mis-filing (2026-08-30). Loud in development, never a hard failure.
|
|
38
|
+
*/
|
|
39
|
+
declare function buildUploadMetadataEnvelope(metadata: Record<string, unknown> | undefined): Record<string, unknown>;
|
|
40
|
+
/** tus-js-client HttpStack port (tests inject a mock wire). */
|
|
41
|
+
type TusHttpStack = tus.HttpStack;
|
|
42
|
+
/** tus-js-client file reader port (tests: Node builds cannot slice browser Files). */
|
|
43
|
+
type TusFileReader = ConstructorParameters<typeof tus.Upload>[1]["fileReader"];
|
|
44
|
+
/** tus-js-client UrlStorage port. */
|
|
45
|
+
type TusUrlStorage = NonNullable<ConstructorParameters<typeof tus.Upload>[1]["urlStorage"]>;
|
|
46
|
+
/** tus-js-client UrlStorage backed by the dedicated `mtx-tus-urls` DB —
|
|
47
|
+
* deliberately SEPARATE from any recorder chunk journal. */
|
|
48
|
+
declare function createTusUrlStorage(): TusUrlStorage;
|
|
49
|
+
/** Read-only summary of a stored TUS resume session (diagnostics surfaces). */
|
|
50
|
+
interface StoredTusUploadSummary {
|
|
51
|
+
urlStorageKey: string;
|
|
52
|
+
fingerprint: string;
|
|
53
|
+
size: number | null;
|
|
54
|
+
metadata: Record<string, string>;
|
|
55
|
+
creationTime: string;
|
|
56
|
+
uploadUrl: string | null;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* List every stored TUS resume-URL entry. Read-only — resuming happens inside
|
|
60
|
+
* {@link tusTransfer}; this only lets management surfaces SHOW a pending
|
|
61
|
+
* session. Returns [] where IndexedDB is unavailable (never throws).
|
|
62
|
+
*/
|
|
63
|
+
declare function listStoredTusUploads(): Promise<StoredTusUploadSummary[]>;
|
|
64
|
+
interface TusTransferOptions {
|
|
65
|
+
/** Full creation URL (`{filesBase}${TUS_UPLOAD_PATH}`). */
|
|
66
|
+
endpoint: string;
|
|
67
|
+
/** Destination path including the file name. */
|
|
68
|
+
filePath: string;
|
|
69
|
+
/** Caller metadata; wrapped by {@link buildUploadMetadataEnvelope}. */
|
|
70
|
+
metadata?: Record<string, unknown> | undefined;
|
|
71
|
+
/** Optional Upload-Metadata `visibility` (transitional wire input). */
|
|
72
|
+
visibility?: string | undefined;
|
|
73
|
+
/** One intended upload, one key — sent on the creation POST only. */
|
|
74
|
+
idempotencyKey: string;
|
|
75
|
+
/**
|
|
76
|
+
* FRESH request headers, read before EVERY request. Only `Authorization`,
|
|
77
|
+
* `X-Guest-Fingerprint` and `X-Organization-Id` are forwarded.
|
|
78
|
+
*/
|
|
79
|
+
headers: () => Promise<Record<string, string | undefined>>;
|
|
80
|
+
onProgress?: ((loaded: number, total: number) => void) | undefined;
|
|
81
|
+
signal?: AbortSignal | undefined;
|
|
82
|
+
/** Injected HttpStack (tests: mock wire, no XHR). */
|
|
83
|
+
httpStack?: TusHttpStack | undefined;
|
|
84
|
+
/** Injected file reader (tests: Node builds cannot slice browser Files). */
|
|
85
|
+
fileReader?: TusFileReader | undefined;
|
|
86
|
+
/** Override URL storage (tests). Default: {@link createTusUrlStorage}. */
|
|
87
|
+
urlStorage?: TusUrlStorage | undefined;
|
|
88
|
+
}
|
|
89
|
+
type TusTransferErrorCode = "upload_cancelled" | "tus_upload_failed" | "tus_missing_file_id";
|
|
90
|
+
type TusTransferResult = {
|
|
91
|
+
ok: true;
|
|
92
|
+
fileId: string;
|
|
93
|
+
} | {
|
|
94
|
+
ok: false;
|
|
95
|
+
error: string;
|
|
96
|
+
errorCode: TusTransferErrorCode;
|
|
97
|
+
};
|
|
98
|
+
/**
|
|
99
|
+
* Resumable upload of one file. Never throws — `{ ok: false, errorCode }` on
|
|
100
|
+
* failure. Resumes a previous session for the same file automatically; when the
|
|
101
|
+
* transfer errors but the server already exposed `X-Cld-File-Id`, the completed
|
|
102
|
+
* session is recovered (loudly) instead of failing or re-uploading.
|
|
103
|
+
*/
|
|
104
|
+
declare function tusTransfer(file: File | Blob, opts: TusTransferOptions): Promise<TusTransferResult>;
|
|
105
|
+
|
|
106
|
+
export { type StoredTusUploadSummary, TUS_CHUNK_SIZE_BYTES, TUS_UPLOAD_PATH, type TusFileReader, type TusHttpStack, type TusTransferErrorCode, type TusTransferOptions, type TusTransferResult, type TusUrlStorage, buildUploadMetadataEnvelope, createTusUrlStorage, listStoredTusUploads, tusTransfer };
|