@mieweb/pulsevault 0.0.2 → 0.1.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 +184 -0
- package/LICENSE +15 -39
- package/OPERATIONS.md +223 -0
- package/PROTOCOL.md +312 -0
- package/README.md +532 -122
- package/augment.d.ts +1 -0
- package/augment.js +3 -0
- package/core.d.ts +1 -0
- package/core.js +7 -0
- package/dist/app.d.ts +90 -34
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +23 -33
- package/dist/app.js.map +1 -1
- package/dist/augment.d.ts +14 -7
- package/dist/augment.d.ts.map +1 -1
- package/dist/augment.js.map +1 -1
- package/dist/core.d.ts +108 -0
- package/dist/core.d.ts.map +1 -0
- package/dist/core.js +431 -0
- package/dist/core.js.map +1 -0
- package/dist/lib/authorize.d.ts +19 -0
- package/dist/lib/authorize.d.ts.map +1 -0
- package/dist/lib/authorize.js +2 -0
- package/dist/lib/authorize.js.map +1 -0
- package/dist/lib/capability-token.d.ts +80 -0
- package/dist/lib/capability-token.d.ts.map +1 -0
- package/dist/lib/capability-token.js +126 -0
- package/dist/lib/capability-token.js.map +1 -0
- package/dist/lib/checksum.d.ts +45 -0
- package/dist/lib/checksum.d.ts.map +1 -0
- package/dist/lib/checksum.js +102 -0
- package/dist/lib/checksum.js.map +1 -0
- package/dist/lib/deeplinks.d.ts +37 -21
- package/dist/lib/deeplinks.d.ts.map +1 -1
- package/dist/lib/deeplinks.js +54 -13
- package/dist/lib/deeplinks.js.map +1 -1
- package/dist/lib/magic.d.ts +30 -5
- package/dist/lib/magic.d.ts.map +1 -1
- package/dist/lib/magic.js +51 -14
- package/dist/lib/magic.js.map +1 -1
- package/dist/lib/options.d.ts +39 -0
- package/dist/lib/options.d.ts.map +1 -0
- package/dist/lib/options.js +103 -0
- package/dist/lib/options.js.map +1 -0
- package/dist/lib/pulsevaultTus.d.ts +52 -26
- package/dist/lib/pulsevaultTus.d.ts.map +1 -1
- package/dist/lib/pulsevaultTus.js +111 -43
- package/dist/lib/pulsevaultTus.js.map +1 -1
- package/dist/lib/request.d.ts +24 -0
- package/dist/lib/request.d.ts.map +1 -0
- package/dist/lib/request.js +16 -0
- package/dist/lib/request.js.map +1 -0
- package/dist/lib/uuid.d.ts.map +1 -1
- package/dist/lib/uuid.js +1 -1
- package/dist/lib/uuid.js.map +1 -1
- package/dist/routes/pulsevault.d.ts +18 -16
- package/dist/routes/pulsevault.d.ts.map +1 -1
- package/dist/routes/pulsevault.js +120 -251
- package/dist/routes/pulsevault.js.map +1 -1
- package/dist/storage/local.d.ts +31 -10
- package/dist/storage/local.d.ts.map +1 -1
- package/dist/storage/local.js +166 -82
- package/dist/storage/local.js.map +1 -1
- package/dist/storage/s3.d.ts +109 -0
- package/dist/storage/s3.d.ts.map +1 -0
- package/dist/storage/s3.js +432 -0
- package/dist/storage/s3.js.map +1 -0
- package/dist/storage/types.d.ts +69 -17
- package/dist/storage/types.d.ts.map +1 -1
- package/package.json +35 -7
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
const SIDECAR_VERSION = 1;
|
|
3
|
+
/** Key prefix inside the bucket that holds the per-upload sidecar objects. */
|
|
4
|
+
const PULSEVAULT_META_PREFIX = '.pulsevault';
|
|
5
|
+
/** Default presigned playback URL lifetime (15 minutes). */
|
|
6
|
+
const DEFAULT_PRESIGN_TTL_SECONDS = 900;
|
|
7
|
+
/** Default cap on the in-memory metadata cache before evicting the oldest entry. */
|
|
8
|
+
const DEFAULT_META_CACHE_LIMIT = 10_000;
|
|
9
|
+
/** Map a file extension to the `Content-Type` the playback URL should return. */
|
|
10
|
+
function extToContentType(ext) {
|
|
11
|
+
switch (ext) {
|
|
12
|
+
case '.mp4':
|
|
13
|
+
return 'video/mp4';
|
|
14
|
+
case '.zip':
|
|
15
|
+
return 'application/zip';
|
|
16
|
+
case '.srt':
|
|
17
|
+
return 'application/x-subrip';
|
|
18
|
+
default:
|
|
19
|
+
return 'application/octet-stream';
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/** Project a `Sidecar` into the shape kept in the in-memory cache. */
|
|
23
|
+
function sidecarToCachedMeta(sidecar, ready) {
|
|
24
|
+
return {
|
|
25
|
+
ext: sidecar.ext,
|
|
26
|
+
ready,
|
|
27
|
+
kind: sidecar.kind ?? 'video',
|
|
28
|
+
relatedTo: sidecar.relatedTo,
|
|
29
|
+
checksum: sidecar.checksum,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Build an S3/R2-backed storage adapter.
|
|
34
|
+
*
|
|
35
|
+
* Async because it lazily imports the optional `@aws-sdk/*` and
|
|
36
|
+
* `@tus/s3-store` packages — `await` it before registering the plugin:
|
|
37
|
+
*
|
|
38
|
+
* ```ts
|
|
39
|
+
* const storage = await createS3Storage({
|
|
40
|
+
* bucket: "pulse-videos",
|
|
41
|
+
* endpoint: `https://${ACCOUNT}.r2.cloudflarestorage.com`,
|
|
42
|
+
* accessKeyId: process.env.R2_ACCESS_KEY,
|
|
43
|
+
* secretAccessKey: process.env.R2_SECRET_KEY,
|
|
44
|
+
* });
|
|
45
|
+
* await app.register(pulseVault, { storage, prefix: "/pulsevault", maxUploadSize: Infinity });
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
export async function createS3Storage(opts) {
|
|
49
|
+
let s3;
|
|
50
|
+
let presigner;
|
|
51
|
+
let s3store;
|
|
52
|
+
try {
|
|
53
|
+
[s3, presigner, s3store] = await Promise.all([
|
|
54
|
+
import('@aws-sdk/client-s3'),
|
|
55
|
+
import('@aws-sdk/s3-request-presigner'),
|
|
56
|
+
import('@tus/s3-store'),
|
|
57
|
+
]);
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
throw new Error('createS3Storage requires the optional packages `@aws-sdk/client-s3`, ' +
|
|
61
|
+
'`@aws-sdk/s3-request-presigner`, and `@tus/s3-store`. Install them with:\n' +
|
|
62
|
+
' npm install @aws-sdk/client-s3 @aws-sdk/s3-request-presigner @tus/s3-store\n' +
|
|
63
|
+
`(original error: ${err instanceof Error ? err.message : String(err)})`);
|
|
64
|
+
}
|
|
65
|
+
const { S3Client, GetObjectCommand, PutObjectCommand, DeleteObjectCommand } = s3;
|
|
66
|
+
const { getSignedUrl } = presigner;
|
|
67
|
+
const { S3Store } = s3store;
|
|
68
|
+
const bucket = opts.bucket;
|
|
69
|
+
const presignTtl = opts.presignTtlSeconds ?? DEFAULT_PRESIGN_TTL_SECONDS;
|
|
70
|
+
const metaCacheLimit = opts.metaCacheLimit ?? DEFAULT_META_CACHE_LIMIT;
|
|
71
|
+
const credentials = opts.accessKeyId && opts.secretAccessKey
|
|
72
|
+
? {
|
|
73
|
+
accessKeyId: opts.accessKeyId,
|
|
74
|
+
secretAccessKey: opts.secretAccessKey,
|
|
75
|
+
...(opts.sessionToken ? { sessionToken: opts.sessionToken } : {}),
|
|
76
|
+
}
|
|
77
|
+
: undefined;
|
|
78
|
+
const clientConfig = {
|
|
79
|
+
region: opts.region ?? (opts.endpoint ? 'auto' : undefined),
|
|
80
|
+
...(opts.endpoint ? { endpoint: opts.endpoint } : {}),
|
|
81
|
+
forcePathStyle: opts.forcePathStyle ?? Boolean(opts.endpoint),
|
|
82
|
+
...(credentials ? { credentials } : {}),
|
|
83
|
+
...opts.clientConfig,
|
|
84
|
+
};
|
|
85
|
+
// Our own client, used for sidecar I/O, ranged header reads, deletes, and
|
|
86
|
+
// presigning. The TUS datastore creates its own client from the same config.
|
|
87
|
+
const client = new S3Client(clientConfig);
|
|
88
|
+
const datastore = new S3Store({
|
|
89
|
+
...(opts.partSize ? { partSize: opts.partSize } : {}),
|
|
90
|
+
s3ClientConfig: { ...clientConfig, bucket },
|
|
91
|
+
});
|
|
92
|
+
// Metadata cache keyed by artifactId, mirroring the local adapter: populated
|
|
93
|
+
// eagerly on reserve and lazily from the sidecar on a cache-miss, so the GET
|
|
94
|
+
// hot path avoids a per-request round-trip to the bucket. Bounded with
|
|
95
|
+
// simple insertion-order eviction, same rationale as the local adapter.
|
|
96
|
+
const metaCache = new Map();
|
|
97
|
+
const cacheSet = (artifactId, meta) => {
|
|
98
|
+
metaCache.delete(artifactId);
|
|
99
|
+
metaCache.set(artifactId, meta);
|
|
100
|
+
if (metaCache.size > metaCacheLimit) {
|
|
101
|
+
const oldest = metaCache.keys().next().value;
|
|
102
|
+
if (oldest !== undefined)
|
|
103
|
+
metaCache.delete(oldest);
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
const sidecarKey = (artifactId) => `${PULSEVAULT_META_PREFIX}/${artifactId}.json`;
|
|
107
|
+
/** Object key for the artifact bytes — also the TUS file id / multipart key. */
|
|
108
|
+
const artifactKey = (artifactId, kind, ext) => `${kind}/${artifactId}${ext}`;
|
|
109
|
+
const writeSidecar = async (artifactId, sidecar) => {
|
|
110
|
+
await client.send(new PutObjectCommand({
|
|
111
|
+
Bucket: bucket,
|
|
112
|
+
Key: sidecarKey(artifactId),
|
|
113
|
+
Body: JSON.stringify(sidecar),
|
|
114
|
+
ContentType: 'application/json',
|
|
115
|
+
}));
|
|
116
|
+
};
|
|
117
|
+
const readSidecar = async (artifactId) => {
|
|
118
|
+
let raw;
|
|
119
|
+
try {
|
|
120
|
+
const res = await client.send(new GetObjectCommand({ Bucket: bucket, Key: sidecarKey(artifactId) }));
|
|
121
|
+
raw = (await bodyToBuffer(res.Body)).toString('utf8');
|
|
122
|
+
}
|
|
123
|
+
catch (err) {
|
|
124
|
+
if (isNotFound(err))
|
|
125
|
+
return null;
|
|
126
|
+
throw err;
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
const parsed = JSON.parse(raw);
|
|
130
|
+
if (typeof parsed.ext !== 'string')
|
|
131
|
+
return null;
|
|
132
|
+
if (typeof parsed.filename !== 'string')
|
|
133
|
+
return null;
|
|
134
|
+
const status = parsed.status === 'uploading' ? 'uploading' : 'ready';
|
|
135
|
+
const kind = parsed.kind === 'project' ? 'project' : parsed.kind === 'captions' ? 'captions' : 'video';
|
|
136
|
+
return {
|
|
137
|
+
version: SIDECAR_VERSION,
|
|
138
|
+
ext: parsed.ext,
|
|
139
|
+
filename: parsed.filename,
|
|
140
|
+
status,
|
|
141
|
+
kind,
|
|
142
|
+
relatedTo: typeof parsed.relatedTo === 'string' ? parsed.relatedTo : undefined,
|
|
143
|
+
checksum: typeof parsed.checksum === 'string' ? parsed.checksum : undefined,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
// Malformed sidecar — treat as absent; `reserveUpload` rewrites it.
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
const loadMeta = async (artifactId) => {
|
|
152
|
+
const cached = metaCache.get(artifactId);
|
|
153
|
+
if (cached)
|
|
154
|
+
return cached;
|
|
155
|
+
const sidecar = await readSidecar(artifactId);
|
|
156
|
+
if (!sidecar)
|
|
157
|
+
return null;
|
|
158
|
+
const meta = sidecarToCachedMeta(sidecar, sidecar.status === 'ready');
|
|
159
|
+
cacheSet(artifactId, meta);
|
|
160
|
+
return meta;
|
|
161
|
+
};
|
|
162
|
+
const reserveUpload = async ({ artifactId, filename, ext, kind, relatedTo, checksum, }) => {
|
|
163
|
+
const sidecar = {
|
|
164
|
+
version: SIDECAR_VERSION,
|
|
165
|
+
ext,
|
|
166
|
+
filename,
|
|
167
|
+
status: 'uploading',
|
|
168
|
+
kind,
|
|
169
|
+
relatedTo,
|
|
170
|
+
checksum,
|
|
171
|
+
};
|
|
172
|
+
// Fast-path rejection for the common case. Not atomic by itself (two concurrent
|
|
173
|
+
// requests can both observe no existing meta before either writes) — the conditional
|
|
174
|
+
// write below closes that race on backends that support it — but it's also the only
|
|
175
|
+
// enforcement on S3-compatible backends that silently ignore `IfNoneMatch` instead of
|
|
176
|
+
// erroring (e.g. the `s3rver` mock this repo's own test suite runs against).
|
|
177
|
+
const existing = await loadMeta(artifactId);
|
|
178
|
+
if (existing) {
|
|
179
|
+
throw Object.assign(new Error(`artifactId ${artifactId} already has an upload`), {
|
|
180
|
+
statusCode: 409,
|
|
181
|
+
status_code: 409,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
// Collision guard: `IfNoneMatch: "*"` makes the write itself atomically fail
|
|
185
|
+
// (PreconditionFailed) if a sidecar object already exists for this artifactId, closing
|
|
186
|
+
// the race the check above can't close by itself. Surfaces as HTTP 409 via @tus/server's
|
|
187
|
+
// error path, same as before.
|
|
188
|
+
try {
|
|
189
|
+
await client.send(new PutObjectCommand({
|
|
190
|
+
Bucket: bucket,
|
|
191
|
+
Key: sidecarKey(artifactId),
|
|
192
|
+
Body: JSON.stringify(sidecar),
|
|
193
|
+
ContentType: 'application/json',
|
|
194
|
+
IfNoneMatch: '*',
|
|
195
|
+
}));
|
|
196
|
+
}
|
|
197
|
+
catch (err) {
|
|
198
|
+
if (isPreconditionFailed(err)) {
|
|
199
|
+
throw Object.assign(new Error(`artifactId ${artifactId} already has an upload`), {
|
|
200
|
+
statusCode: 409,
|
|
201
|
+
status_code: 409,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
if (!isConditionalWriteUnsupported(err))
|
|
205
|
+
throw err;
|
|
206
|
+
// Some S3-compatible backends don't support conditional writes — fall back to the
|
|
207
|
+
// previous check-then-write. Weaker (the original TOCTOU window reopens) but keeps
|
|
208
|
+
// reserve working on those backends instead of hard-failing every upload. Surface
|
|
209
|
+
// this degraded mode once per process so operators know their backend can't fully
|
|
210
|
+
// guarantee collision safety under concurrent/retried creates for the same artifactId.
|
|
211
|
+
warnAboutConditionalWriteFallbackOnce();
|
|
212
|
+
const meta = await loadMeta(artifactId);
|
|
213
|
+
if (meta) {
|
|
214
|
+
throw Object.assign(new Error(`artifactId ${artifactId} already has an upload`), {
|
|
215
|
+
statusCode: 409,
|
|
216
|
+
status_code: 409,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
await writeSidecar(artifactId, sidecar);
|
|
220
|
+
}
|
|
221
|
+
cacheSet(artifactId, { ext, ready: false, kind, relatedTo, checksum });
|
|
222
|
+
// @tus/s3-store uses this as the object key for the multipart upload, so
|
|
223
|
+
// the finished object lands at `<kind>/<artifactId><ext>`.
|
|
224
|
+
return artifactKey(artifactId, kind, ext);
|
|
225
|
+
};
|
|
226
|
+
const resolve = async (artifactId) => {
|
|
227
|
+
const meta = await loadMeta(artifactId);
|
|
228
|
+
// Only serve ready uploads — an object that exists but is mid-upload or
|
|
229
|
+
// failed validation stays hidden.
|
|
230
|
+
if (!meta || !meta.ready)
|
|
231
|
+
return null;
|
|
232
|
+
const key = artifactKey(artifactId, meta.kind, meta.ext);
|
|
233
|
+
const url = await getSignedUrl(client, new GetObjectCommand({
|
|
234
|
+
Bucket: bucket,
|
|
235
|
+
Key: key,
|
|
236
|
+
// Force the right Content-Type on the redirected download regardless
|
|
237
|
+
// of what was stored on the object.
|
|
238
|
+
ResponseContentType: extToContentType(meta.ext),
|
|
239
|
+
}), { expiresIn: presignTtl });
|
|
240
|
+
return { kind: 'redirect', url, statusCode: 302 };
|
|
241
|
+
};
|
|
242
|
+
const markReady = async (artifactId) => {
|
|
243
|
+
const sidecar = await readSidecar(artifactId);
|
|
244
|
+
if (!sidecar) {
|
|
245
|
+
throw new Error(`markReady: no sidecar for artifactId ${artifactId} (was reserveUpload called?)`);
|
|
246
|
+
}
|
|
247
|
+
const next = sidecarToCachedMeta(sidecar, true);
|
|
248
|
+
if (sidecar.status === 'ready') {
|
|
249
|
+
// Idempotent: already ready, just keep the cache consistent.
|
|
250
|
+
cacheSet(artifactId, next);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
await writeSidecar(artifactId, { ...sidecar, status: 'ready' });
|
|
254
|
+
cacheSet(artifactId, next);
|
|
255
|
+
};
|
|
256
|
+
const remove = async (artifactId) => {
|
|
257
|
+
const meta = await loadMeta(artifactId);
|
|
258
|
+
// Evict before deleting so a racing `resolve` can't hand back a stale key.
|
|
259
|
+
metaCache.delete(artifactId);
|
|
260
|
+
if (!meta)
|
|
261
|
+
return false;
|
|
262
|
+
const key = artifactKey(artifactId, meta.kind, meta.ext);
|
|
263
|
+
// Abort any still-in-progress multipart upload (no-op / best-effort once
|
|
264
|
+
// the upload has completed) so we never orphan an open multipart session.
|
|
265
|
+
await Promise.allSettled([
|
|
266
|
+
datastore.remove?.(key),
|
|
267
|
+
]);
|
|
268
|
+
// Delete the finalized object, the @tus/s3-store `.info` sidecar, and our
|
|
269
|
+
// metadata sidecar. DeleteObject is idempotent, so this is safe whether or
|
|
270
|
+
// not the multipart abort above already removed some of them.
|
|
271
|
+
await Promise.all([
|
|
272
|
+
client.send(new DeleteObjectCommand({ Bucket: bucket, Key: key })),
|
|
273
|
+
client.send(new DeleteObjectCommand({ Bucket: bucket, Key: `${key}.info` })),
|
|
274
|
+
client.send(new DeleteObjectCommand({ Bucket: bucket, Key: sidecarKey(artifactId) })),
|
|
275
|
+
]);
|
|
276
|
+
return true;
|
|
277
|
+
};
|
|
278
|
+
const readHeader = async (artifactId, n) => {
|
|
279
|
+
const meta = await loadMeta(artifactId);
|
|
280
|
+
if (!meta)
|
|
281
|
+
return null;
|
|
282
|
+
const key = artifactKey(artifactId, meta.kind, meta.ext);
|
|
283
|
+
try {
|
|
284
|
+
const res = await client.send(new GetObjectCommand({
|
|
285
|
+
Bucket: bucket,
|
|
286
|
+
Key: key,
|
|
287
|
+
Range: `bytes=0-${Math.max(0, n - 1)}`,
|
|
288
|
+
}));
|
|
289
|
+
return await bodyToBuffer(res.Body);
|
|
290
|
+
}
|
|
291
|
+
catch (err) {
|
|
292
|
+
if (isNotFound(err))
|
|
293
|
+
return null;
|
|
294
|
+
throw err;
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
const readAll = async (artifactId) => {
|
|
298
|
+
const meta = await loadMeta(artifactId);
|
|
299
|
+
if (!meta)
|
|
300
|
+
return null;
|
|
301
|
+
const key = artifactKey(artifactId, meta.kind, meta.ext);
|
|
302
|
+
try {
|
|
303
|
+
const res = await client.send(new GetObjectCommand({ Bucket: bucket, Key: key }));
|
|
304
|
+
return await bodyToBuffer(res.Body);
|
|
305
|
+
}
|
|
306
|
+
catch (err) {
|
|
307
|
+
if (isNotFound(err))
|
|
308
|
+
return null;
|
|
309
|
+
throw err;
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
const digestAll = async (artifactId, algorithm) => {
|
|
313
|
+
const meta = await loadMeta(artifactId);
|
|
314
|
+
if (!meta)
|
|
315
|
+
return null;
|
|
316
|
+
const key = artifactKey(artifactId, meta.kind, meta.ext);
|
|
317
|
+
try {
|
|
318
|
+
const res = await client.send(new GetObjectCommand({ Bucket: bucket, Key: key }));
|
|
319
|
+
return await digestBody(res.Body, algorithm);
|
|
320
|
+
}
|
|
321
|
+
catch (err) {
|
|
322
|
+
if (isNotFound(err))
|
|
323
|
+
return null;
|
|
324
|
+
throw err;
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
const getKind = async (artifactId) => {
|
|
328
|
+
const meta = await loadMeta(artifactId);
|
|
329
|
+
return meta ? meta.kind : null;
|
|
330
|
+
};
|
|
331
|
+
const getRelatedTo = async (artifactId) => {
|
|
332
|
+
const meta = await loadMeta(artifactId);
|
|
333
|
+
return meta?.relatedTo ?? null;
|
|
334
|
+
};
|
|
335
|
+
const getChecksum = async (artifactId) => {
|
|
336
|
+
const meta = await loadMeta(artifactId);
|
|
337
|
+
return meta?.checksum ?? null;
|
|
338
|
+
};
|
|
339
|
+
const shutdown = async () => {
|
|
340
|
+
client.destroy();
|
|
341
|
+
};
|
|
342
|
+
return {
|
|
343
|
+
datastore,
|
|
344
|
+
bucket,
|
|
345
|
+
reserveUpload,
|
|
346
|
+
resolve,
|
|
347
|
+
markReady,
|
|
348
|
+
remove,
|
|
349
|
+
readHeader,
|
|
350
|
+
readAll,
|
|
351
|
+
digestAll,
|
|
352
|
+
getKind,
|
|
353
|
+
getRelatedTo,
|
|
354
|
+
getChecksum,
|
|
355
|
+
shutdown,
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
function hasTransformToByteArray(body) {
|
|
359
|
+
return (!!body &&
|
|
360
|
+
typeof body.transformToByteArray === 'function');
|
|
361
|
+
}
|
|
362
|
+
/** Collect an AWS SDK response body into a Buffer. */
|
|
363
|
+
async function bodyToBuffer(body) {
|
|
364
|
+
if (hasTransformToByteArray(body)) {
|
|
365
|
+
return Buffer.from(await body.transformToByteArray());
|
|
366
|
+
}
|
|
367
|
+
const chunks = [];
|
|
368
|
+
for await (const chunk of body) {
|
|
369
|
+
chunks.push(Buffer.from(chunk));
|
|
370
|
+
}
|
|
371
|
+
return Buffer.concat(chunks);
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Stream an AWS SDK response body through a hash digest without buffering the
|
|
375
|
+
* whole thing into memory first — the streaming counterpart to
|
|
376
|
+
* `bodyToBuffer`, used by `digestAll`.
|
|
377
|
+
*/
|
|
378
|
+
async function digestBody(body, algorithm) {
|
|
379
|
+
const hash = createHash(algorithm);
|
|
380
|
+
if (hasTransformToByteArray(body)) {
|
|
381
|
+
hash.update(await body.transformToByteArray());
|
|
382
|
+
return hash.digest('hex');
|
|
383
|
+
}
|
|
384
|
+
for await (const chunk of body) {
|
|
385
|
+
hash.update(chunk);
|
|
386
|
+
}
|
|
387
|
+
return hash.digest('hex');
|
|
388
|
+
}
|
|
389
|
+
/** Whether an AWS SDK error represents a missing key/object (404-ish). */
|
|
390
|
+
function isNotFound(err) {
|
|
391
|
+
const e = err;
|
|
392
|
+
return (e?.name === 'NoSuchKey' ||
|
|
393
|
+
e?.name === 'NotFound' ||
|
|
394
|
+
e?.Code === 'NoSuchKey' ||
|
|
395
|
+
e?.Code === 'NotFound' ||
|
|
396
|
+
e?.$metadata?.httpStatusCode === 404);
|
|
397
|
+
}
|
|
398
|
+
/** Whether a conditional `PutObjectCommand` (`IfNoneMatch`) failed because the object already exists. */
|
|
399
|
+
function isPreconditionFailed(err) {
|
|
400
|
+
const e = err;
|
|
401
|
+
return (e?.name === 'PreconditionFailed' ||
|
|
402
|
+
e?.Code === 'PreconditionFailed' ||
|
|
403
|
+
e?.$metadata?.httpStatusCode === 412);
|
|
404
|
+
}
|
|
405
|
+
/** Whether the backend rejected `IfNoneMatch` itself as unsupported, rather than the condition failing. */
|
|
406
|
+
function isConditionalWriteUnsupported(err) {
|
|
407
|
+
const e = err;
|
|
408
|
+
return (e?.name === 'NotImplemented' ||
|
|
409
|
+
e?.Code === 'NotImplemented' ||
|
|
410
|
+
e?.$metadata?.httpStatusCode === 501);
|
|
411
|
+
}
|
|
412
|
+
let warnedAboutConditionalWriteFallback = false;
|
|
413
|
+
/**
|
|
414
|
+
* One-time-per-process warning when `reserveUpload` falls back to
|
|
415
|
+
* check-then-write because the configured S3-compatible backend doesn't
|
|
416
|
+
* support `IfNoneMatch`. That fallback reopens a genuine TOCTOU window
|
|
417
|
+
* (two concurrent/retried `reserveUpload` calls for the same artifactId can
|
|
418
|
+
* both pass the check before either writes, silently clobbering one
|
|
419
|
+
* another's sidecar) — there's no way to close it without a real distributed
|
|
420
|
+
* lock, so the best this library can do is make the degraded mode visible.
|
|
421
|
+
*/
|
|
422
|
+
function warnAboutConditionalWriteFallbackOnce() {
|
|
423
|
+
if (warnedAboutConditionalWriteFallback)
|
|
424
|
+
return;
|
|
425
|
+
warnedAboutConditionalWriteFallback = true;
|
|
426
|
+
console.warn('[pulsevault] S3-compatible backend does not support conditional writes (IfNoneMatch); ' +
|
|
427
|
+
'falling back to check-then-write for reserveUpload. This reopens a collision race ' +
|
|
428
|
+
'between concurrent/retried creates for the same artifactId. If this matters for your ' +
|
|
429
|
+
'deployment, use a backend that supports IfNoneMatch, or serialize artifactId creation ' +
|
|
430
|
+
'in front of pulsevault (e.g. in your own /reserve endpoint).');
|
|
431
|
+
}
|
|
432
|
+
//# sourceMappingURL=s3.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"s3.js","sourceRoot":"","sources":["../../src/storage/s3.ts"],"names":[],"mappings":"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"}
|
package/dist/storage/types.d.ts
CHANGED
|
@@ -1,28 +1,61 @@
|
|
|
1
|
-
import type { DataStore } from
|
|
1
|
+
import type { DataStore } from '@tus/server';
|
|
2
2
|
/**
|
|
3
|
-
* How the GET route should serve a resolved
|
|
4
|
-
* reads directly from a local path; Redirect issues an HTTP
|
|
5
|
-
* adapter produced (e.g. a pre-signed object storage URL).
|
|
3
|
+
* How the GET route should serve a resolved artifact. Stream means
|
|
4
|
+
* `@fastify/send` reads directly from a local path; Redirect issues an HTTP
|
|
5
|
+
* 3xx to a URL the adapter produced (e.g. a pre-signed object storage URL).
|
|
6
6
|
*/
|
|
7
7
|
export type PulseVaultResolution = {
|
|
8
|
-
kind:
|
|
8
|
+
kind: 'stream';
|
|
9
9
|
/** Root directory `@fastify/send` should jail to. */
|
|
10
10
|
root: string;
|
|
11
11
|
/** Filename relative to `root`. */
|
|
12
12
|
filename: string;
|
|
13
|
+
/**
|
|
14
|
+
* Explicit `Content-Type` to set on the response. When present the GET
|
|
15
|
+
* route overrides whatever `@fastify/send` infers from the extension.
|
|
16
|
+
* Populated by adapters for non-standard extensions (e.g. `.pulse`).
|
|
17
|
+
*/
|
|
18
|
+
contentType?: string;
|
|
13
19
|
} | {
|
|
14
|
-
kind:
|
|
20
|
+
kind: 'redirect';
|
|
15
21
|
url: string;
|
|
16
22
|
/** Defaults to 302. */
|
|
17
23
|
statusCode?: number;
|
|
18
24
|
};
|
|
25
|
+
export type UploadKind = 'video' | 'project' | 'captions';
|
|
19
26
|
export type ReserveUploadParams = {
|
|
20
|
-
/** UUID from `Upload-Metadata.videoid
|
|
21
|
-
|
|
27
|
+
/** UUID from `Upload-Metadata.artifactId` (or the `videoid`/`projectid` legacy aliases). */
|
|
28
|
+
artifactId: string;
|
|
22
29
|
/** Raw filename from `Upload-Metadata.filename`. */
|
|
23
30
|
filename: string;
|
|
24
31
|
/** Lowercase extension including the leading dot, validated upstream. */
|
|
25
32
|
ext: string;
|
|
33
|
+
/** Artifact kind derived from `Upload-Metadata.kind`. Defaults to `"video"`. */
|
|
34
|
+
kind: UploadKind;
|
|
35
|
+
/**
|
|
36
|
+
* Optional UUID of another artifact this one belongs to, from
|
|
37
|
+
* `Upload-Metadata.relatedTo`. Lets a single capability token scoped to one
|
|
38
|
+
* "session" artifact (e.g. a video) authorize related artifacts uploaded in
|
|
39
|
+
* the same session (its captions, or — under `uploadUnit: "beat"` — each
|
|
40
|
+
* beat plus the pulse manifest) without minting a token per artifact.
|
|
41
|
+
* The storage layer itself only stores and returns this value — it does not
|
|
42
|
+
* validate that the referenced artifact exists or was created by the same
|
|
43
|
+
* caller. The actual authorization check lives in `createCapabilityAuthorize`
|
|
44
|
+
* (`lib/capability-token.ts`), which treats a request's `relatedTo` as
|
|
45
|
+
* authorized whenever it matches the presented token's own signed
|
|
46
|
+
* `artifactId` claim. That's safe only because a token can't be forged or
|
|
47
|
+
* guessed (HMAC-signed) — do not read this field as "unauthenticated
|
|
48
|
+
* metadata with no security relevance."
|
|
49
|
+
*/
|
|
50
|
+
relatedTo?: string;
|
|
51
|
+
/**
|
|
52
|
+
* Optional client-supplied integrity digest from `Upload-Metadata.checksum`
|
|
53
|
+
* (`<algorithm>:<hex>`), persisted so `createChecksumValidator` can read it
|
|
54
|
+
* at completion time even when completion happens on a different request
|
|
55
|
+
* than creation (chunked uploads, or even a single-PATCH upload sent as
|
|
56
|
+
* two separate HTTP requests).
|
|
57
|
+
*/
|
|
58
|
+
checksum?: string;
|
|
26
59
|
};
|
|
27
60
|
/**
|
|
28
61
|
* Storage backend contract. Keep the surface small: one write hook, one read
|
|
@@ -46,12 +79,12 @@ export interface PulseVaultStorage {
|
|
|
46
79
|
*/
|
|
47
80
|
reserveUpload(params: ReserveUploadParams): Promise<string>;
|
|
48
81
|
/**
|
|
49
|
-
* Called by the GET route. Returns how to serve the
|
|
50
|
-
* the
|
|
82
|
+
* Called by the GET route. Returns how to serve the artifact, or `null` if
|
|
83
|
+
* the artifactId is unknown *or* the upload is still in progress — adapters
|
|
51
84
|
* that implement `markReady` should only return non-null once the bytes
|
|
52
85
|
* have been promoted to "ready."
|
|
53
86
|
*/
|
|
54
|
-
resolve(
|
|
87
|
+
resolve(artifactId: string): Promise<PulseVaultResolution | null>;
|
|
55
88
|
/**
|
|
56
89
|
* Mark an upload as fully written and safe to serve. Called by the plugin
|
|
57
90
|
* after the final byte lands and any `validatePayload` + `onUploadComplete`
|
|
@@ -60,13 +93,32 @@ export interface PulseVaultStorage {
|
|
|
60
93
|
* may omit this method — `resolve` will be trusted to only return finished
|
|
61
94
|
* uploads in that case.
|
|
62
95
|
*/
|
|
63
|
-
markReady?(
|
|
96
|
+
markReady?(artifactId: string): Promise<void>;
|
|
97
|
+
/**
|
|
98
|
+
* Delete all storage associated with an artifactId. Returns `true` if
|
|
99
|
+
* something was removed, `false` if the artifactId was already absent.
|
|
100
|
+
* Called both from the `DELETE /artifacts/:artifactId` route and from the
|
|
101
|
+
* plugin's cleanup path when `validatePayload` rejects a completed upload.
|
|
102
|
+
*/
|
|
103
|
+
remove?(artifactId: string): Promise<boolean>;
|
|
104
|
+
/**
|
|
105
|
+
* Return the artifact kind for a known artifactId, or `null` if the
|
|
106
|
+
* artifactId is not found. Used by the generic artifact route to resolve
|
|
107
|
+
* which kind to serve without it being encoded in the URL.
|
|
108
|
+
*/
|
|
109
|
+
getKind?(artifactId: string): Promise<UploadKind | null>;
|
|
110
|
+
/**
|
|
111
|
+
* Return the `relatedTo` artifact id stored at `reserveUpload` time, or
|
|
112
|
+
* `null` if the artifactId is unknown or has no relation recorded. Used by
|
|
113
|
+
* `createCapabilityAuthorize` to authorize an artifact against a token
|
|
114
|
+
* scoped to the session it belongs to, not just its own id.
|
|
115
|
+
*/
|
|
116
|
+
getRelatedTo?(artifactId: string): Promise<string | null>;
|
|
64
117
|
/**
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
* cleanup path when `validatePayload` rejects a completed upload.
|
|
118
|
+
* Return the `checksum` metadata stored at `reserveUpload` time, or `null`
|
|
119
|
+
* if the artifactId is unknown or no checksum was supplied. Used by
|
|
120
|
+
* `createChecksumValidator`/`createS3ChecksumValidator` at completion time.
|
|
69
121
|
*/
|
|
70
|
-
|
|
122
|
+
getChecksum?(artifactId: string): Promise<string | null>;
|
|
71
123
|
}
|
|
72
124
|
//# sourceMappingURL=types.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/storage/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAE7C;;;;GAIG;AACH,MAAM,MAAM,oBAAoB,GAC5B;IACE,IAAI,EAAE,QAAQ,CAAC;IACf,qDAAqD;IACrD,IAAI,EAAE,MAAM,CAAC;IACb,mCAAmC;IACnC,QAAQ,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/storage/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAE7C;;;;GAIG;AACH,MAAM,MAAM,oBAAoB,GAC5B;IACE,IAAI,EAAE,QAAQ,CAAC;IACf,qDAAqD;IACrD,IAAI,EAAE,MAAM,CAAC;IACb,mCAAmC;IACnC,QAAQ,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,GACD;IACE,IAAI,EAAE,UAAU,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,uBAAuB;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAEN,MAAM,MAAM,UAAU,GAAG,OAAO,GAAG,SAAS,GAAG,UAAU,CAAC;AAE1D,MAAM,MAAM,mBAAmB,GAAG;IAChC,4FAA4F;IAC5F,UAAU,EAAE,MAAM,CAAC;IACnB,oDAAoD;IACpD,QAAQ,EAAE,MAAM,CAAC;IACjB,yEAAyE;IACzE,GAAG,EAAE,MAAM,CAAC;IACZ,gFAAgF;IAChF,IAAI,EAAE,UAAU,CAAC;IACjB;;;;;;;;;;;;;;OAcG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF;;;GAGG;AACH,MAAM,WAAW,iBAAiB;IAChC,gDAAgD;IAChD,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAC;IAE9B,sDAAsD;IACtD,UAAU,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAE7B;;;;OAIG;IACH,QAAQ,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAE3B;;;;OAIG;IACH,aAAa,CAAC,MAAM,EAAE,mBAAmB,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAE5D;;;;;OAKG;IACH,OAAO,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC,CAAC;IAElE;;;;;;;OAOG;IACH,SAAS,CAAC,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAE9C;;;;;OAKG;IACH,MAAM,CAAC,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAE9C;;;;OAIG;IACH,OAAO,CAAC,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC;IAEzD;;;;;OAKG;IACH,YAAY,CAAC,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAE1D;;;;OAIG;IACH,WAAW,CAAC,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CAC1D"}
|