@mieweb/pulsevault 0.0.2 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/CHANGELOG.md +258 -0
  2. package/LICENSE +15 -39
  3. package/OPERATIONS.md +223 -0
  4. package/PROTOCOL.md +345 -0
  5. package/README.md +548 -122
  6. package/augment.d.ts +1 -0
  7. package/augment.js +3 -0
  8. package/core.d.ts +1 -0
  9. package/core.js +7 -0
  10. package/dist/app.d.ts +91 -34
  11. package/dist/app.d.ts.map +1 -1
  12. package/dist/app.js +23 -33
  13. package/dist/app.js.map +1 -1
  14. package/dist/augment.d.ts +14 -7
  15. package/dist/augment.d.ts.map +1 -1
  16. package/dist/augment.js.map +1 -1
  17. package/dist/core.d.ts +108 -0
  18. package/dist/core.d.ts.map +1 -0
  19. package/dist/core.js +494 -0
  20. package/dist/core.js.map +1 -0
  21. package/dist/lib/authorize.d.ts +19 -0
  22. package/dist/lib/authorize.d.ts.map +1 -0
  23. package/dist/lib/authorize.js +2 -0
  24. package/dist/lib/authorize.js.map +1 -0
  25. package/dist/lib/capability-token.d.ts +80 -0
  26. package/dist/lib/capability-token.d.ts.map +1 -0
  27. package/dist/lib/capability-token.js +134 -0
  28. package/dist/lib/capability-token.js.map +1 -0
  29. package/dist/lib/checksum.d.ts +45 -0
  30. package/dist/lib/checksum.d.ts.map +1 -0
  31. package/dist/lib/checksum.js +105 -0
  32. package/dist/lib/checksum.js.map +1 -0
  33. package/dist/lib/deeplinks.d.ts +37 -21
  34. package/dist/lib/deeplinks.d.ts.map +1 -1
  35. package/dist/lib/deeplinks.js +54 -13
  36. package/dist/lib/deeplinks.js.map +1 -1
  37. package/dist/lib/errors.d.ts +7 -0
  38. package/dist/lib/errors.d.ts.map +1 -1
  39. package/dist/lib/errors.js +18 -0
  40. package/dist/lib/errors.js.map +1 -1
  41. package/dist/lib/magic.d.ts +30 -5
  42. package/dist/lib/magic.d.ts.map +1 -1
  43. package/dist/lib/magic.js +51 -14
  44. package/dist/lib/magic.js.map +1 -1
  45. package/dist/lib/options.d.ts +41 -0
  46. package/dist/lib/options.d.ts.map +1 -0
  47. package/dist/lib/options.js +111 -0
  48. package/dist/lib/options.js.map +1 -0
  49. package/dist/lib/pulsevaultTus.d.ts +53 -26
  50. package/dist/lib/pulsevaultTus.d.ts.map +1 -1
  51. package/dist/lib/pulsevaultTus.js +174 -55
  52. package/dist/lib/pulsevaultTus.js.map +1 -1
  53. package/dist/lib/request.d.ts +26 -0
  54. package/dist/lib/request.d.ts.map +1 -0
  55. package/dist/lib/request.js +22 -0
  56. package/dist/lib/request.js.map +1 -0
  57. package/dist/lib/uuid.d.ts.map +1 -1
  58. package/dist/lib/uuid.js +1 -1
  59. package/dist/lib/uuid.js.map +1 -1
  60. package/dist/routes/pulsevault.d.ts +18 -16
  61. package/dist/routes/pulsevault.d.ts.map +1 -1
  62. package/dist/routes/pulsevault.js +121 -251
  63. package/dist/routes/pulsevault.js.map +1 -1
  64. package/dist/storage/local.d.ts +31 -10
  65. package/dist/storage/local.d.ts.map +1 -1
  66. package/dist/storage/local.js +205 -84
  67. package/dist/storage/local.js.map +1 -1
  68. package/dist/storage/s3.d.ts +109 -0
  69. package/dist/storage/s3.d.ts.map +1 -0
  70. package/dist/storage/s3.js +438 -0
  71. package/dist/storage/s3.js.map +1 -0
  72. package/dist/storage/types.d.ts +80 -17
  73. package/dist/storage/types.d.ts.map +1 -1
  74. package/dist/storage/types.js +13 -1
  75. package/dist/storage/types.js.map +1 -1
  76. package/package.json +29 -7
package/dist/core.js ADDED
@@ -0,0 +1,494 @@
1
+ import send from '@fastify/send';
2
+ import { createPulsevaultTusServer, pulseVaultTusContext, artifactIdFromUploadId, } from './lib/pulsevaultTus.js';
3
+ import { pulseVaultError, statusCodeOf } from './lib/errors.js';
4
+ import { isUuid } from './lib/uuid.js';
5
+ import { consoleLogger } from './lib/request.js';
6
+ import { parseUploadKind, UPLOAD_KINDS } from './storage/types.js';
7
+ import { normalizeAllowedExtensions, validateBasePath, validateMaxUploadSize, validateUploadUnit, validateAllowedExtensions, warnIfUsingDeprecatedProjectHooks, composeValidatePayload, composeOnUploadComplete, } from './lib/options.js';
8
+ /** Wire protocol version this release implements. See `/capabilities` and `PROTOCOL.md`. */
9
+ export const PROTOCOL_VERSION = 1;
10
+ const MIN_SUPPORTED_PROTOCOL_VERSION = 1;
11
+ const MAX_SUPPORTED_PROTOCOL_VERSION = 1;
12
+ /**
13
+ * Pull `artifactId` (or the legacy `videoid`/`projectid` aliases), `kind`,
14
+ * and `relatedTo` out of a raw `Upload-Metadata` header. Format is a
15
+ * comma-separated list of `<key> <base64-value>` pairs (tus v1 creation
16
+ * extension).
17
+ *
18
+ * Alias precedence is a fixed priority (`artifactId` beats `videoid` beats
19
+ * `projectid`, regardless of header order) so this always agrees with
20
+ * `namingFunction` in `lib/pulsevaultTus.ts` — which uses the same
21
+ * `?? `-chain precedence to decide what's actually reserved/written to
22
+ * storage. If these two disagreed, `authorize()` could validate ownership of
23
+ * a different artifactId than the one the upload actually lands under.
24
+ */
25
+ function parseUploadMetadata(header) {
26
+ let artifactIdRaw;
27
+ let videoidRaw;
28
+ let projectidRaw;
29
+ let kind = 'video';
30
+ let relatedTo;
31
+ for (const pair of header.split(',')) {
32
+ const trimmed = pair.trim();
33
+ if (!trimmed)
34
+ continue;
35
+ const sep = trimmed.indexOf(' ');
36
+ if (sep < 0)
37
+ continue;
38
+ const key = trimmed.slice(0, sep);
39
+ const value = trimmed.slice(sep + 1).trim();
40
+ if (!value)
41
+ continue;
42
+ try {
43
+ const decoded = Buffer.from(value, 'base64').toString('utf8');
44
+ if (key === 'artifactId') {
45
+ artifactIdRaw ??= decoded;
46
+ }
47
+ else if (key === 'videoid') {
48
+ videoidRaw ??= decoded;
49
+ }
50
+ else if (key === 'projectid') {
51
+ projectidRaw ??= decoded;
52
+ }
53
+ else if (key === 'kind') {
54
+ kind = parseUploadKind(decoded);
55
+ }
56
+ else if (key === 'relatedTo' && !relatedTo) {
57
+ relatedTo = isUuid(decoded) ? decoded : undefined;
58
+ }
59
+ }
60
+ catch {
61
+ // ignore malformed base64
62
+ }
63
+ }
64
+ const candidate = (artifactIdRaw ?? videoidRaw ?? projectidRaw ?? '').trim();
65
+ const artifactId = isUuid(candidate) ? candidate : undefined;
66
+ return { artifactId, kind, relatedTo };
67
+ }
68
+ /**
69
+ * `@tus/server`'s `BaseHandler.getFileIdFromRequest` — the function that
70
+ * ultimately decides which upload a PATCH/HEAD/DELETE actually operates on —
71
+ * extracts its file id from the request URL's *last* `/`-delimited segment
72
+ * (`reExtractFileID = /([^/]+)\/?$/`), not from the first segment after
73
+ * `/upload/`. This MUST mirror that exact regex: a URL with extra path
74
+ * segments after the real id (Fastify's `/upload/*` route accepts them) would
75
+ * otherwise let `authorize()` see and approve one artifactId (whichever this
76
+ * function resolved) while `@tus/server` writes the request body against a
77
+ * *different* one (whichever it resolved) — an attacker holding a valid
78
+ * token for their own artifact could smuggle a second, victim artifactId as
79
+ * a trailing path segment and have their bytes land there instead, fully
80
+ * bypassing authorization for the artifact actually written to.
81
+ *
82
+ * Implemented with plain string ops (not the regex itself) because the
83
+ * unanchored-start regex is polynomial on adversarial inputs (CodeQL
84
+ * js/polynomial-redos). Semantics are identical: after stripping at most one
85
+ * trailing `/`, the match is the non-empty run of non-`/` characters at the
86
+ * end of the string, or no match if that run is empty (e.g. `a//`).
87
+ */
88
+ function tusLastUrlSegment(url) {
89
+ const trimmed = url.endsWith('/') ? url.slice(0, -1) : url;
90
+ const segment = trimmed.slice(trimmed.lastIndexOf('/') + 1);
91
+ return segment.length > 0 ? segment : undefined;
92
+ }
93
+ /**
94
+ * Decode the tus file id (base64url-encoded, shaped `<kind>/<artifactId><ext>`
95
+ * by `namingFunction` in `lib/pulsevaultTus.ts`) that `@tus/server` itself
96
+ * will resolve a PATCH/HEAD/DELETE request to, and recover the artifactId via
97
+ * the exact same parser `onUploadFinish` uses — so this can never drift from
98
+ * what `@tus/server` actually operates on. See `tusLastUrlSegment` above
99
+ * for why this must match the *last* URL segment, not the first one after
100
+ * `/upload/`.
101
+ */
102
+ function artifactIdFromTusUrl(url) {
103
+ const rawSegment = tusLastUrlSegment(url);
104
+ if (!rawSegment)
105
+ return undefined;
106
+ let lastSegment;
107
+ try {
108
+ lastSegment = decodeURIComponent(rawSegment);
109
+ }
110
+ catch {
111
+ return undefined;
112
+ }
113
+ let decoded;
114
+ try {
115
+ decoded = Buffer.from(lastSegment, 'base64url').toString('utf8');
116
+ }
117
+ catch {
118
+ return undefined;
119
+ }
120
+ return artifactIdFromUploadId(decoded);
121
+ }
122
+ /**
123
+ * Resolve the artifact kind for an artifactId from storage. Duck-typed so it
124
+ * works with any adapter (those without `getKind` return `"video"`).
125
+ */
126
+ async function resolveStorageKind(storage, artifactId) {
127
+ const candidate = storage.getKind;
128
+ if (typeof candidate !== 'function')
129
+ return 'video';
130
+ const result = await candidate(artifactId);
131
+ return result ?? 'video';
132
+ }
133
+ /** Resolve the `relatedTo` artifact for an artifactId from storage, if the adapter supports it. */
134
+ async function resolveStorageRelatedTo(storage, artifactId) {
135
+ const candidate = storage.getRelatedTo;
136
+ if (typeof candidate !== 'function')
137
+ return undefined;
138
+ const result = await candidate(artifactId);
139
+ return result ?? undefined;
140
+ }
141
+ function extractAuthzMessage(err) {
142
+ if (err instanceof Error && err.message)
143
+ return err.message;
144
+ return 'Forbidden';
145
+ }
146
+ function writeJson(res, statusCode, body) {
147
+ res.writeHead(statusCode, { 'content-type': 'application/json; charset=utf-8' });
148
+ res.end(JSON.stringify(body));
149
+ }
150
+ /**
151
+ * Every response from a pulsevault route carries the wire protocol version
152
+ * it implements, so a client can detect "this server is too old/new for me"
153
+ * without a dedicated round-trip. Called at the top of each granular handler
154
+ * (rather than only in the aggregate `handler`) so it's also applied when a
155
+ * host framework — e.g. the Fastify adapter, which hijacks the reply and
156
+ * calls these directly — bypasses the aggregate router entirely. Must run
157
+ * before any `writeHead`/`end` call below: `setHeader` values merge into
158
+ * whatever headers object a later `writeHead` call passes.
159
+ */
160
+ function stampProtocolVersion(res) {
161
+ res.setHeader('Protocol-Version', String(PROTOCOL_VERSION));
162
+ }
163
+ /** Same augmentation as `augment.ts`'s `FastifyRequest.pulseVault`, applied to a raw request. */
164
+ function stashPulseVaultContext(req, ctx) {
165
+ req.pulseVault = ctx;
166
+ }
167
+ /**
168
+ * Build the framework-agnostic core: the same authorize/validatePayload/
169
+ * onUploadComplete/onArtifactEvent orchestration, tus glue, capabilities
170
+ * payload, and artifact GET/DELETE logic the Fastify plugin uses, operating
171
+ * on raw `(req, res)` instead of Fastify's `request`/`reply` wrappers. Both
172
+ * the Fastify plugin and this factory ultimately call into the same
173
+ * `lib/pulsevaultTus.js` tus server, so behavior can't drift between them.
174
+ */
175
+ export function createPulseVaultCore(options) {
176
+ validateBasePath(options.basePath, 'basePath');
177
+ validateMaxUploadSize(options.maxUploadSize);
178
+ validateUploadUnit(options.uploadUnit);
179
+ validateAllowedExtensions(options.allowedExtensions);
180
+ warnIfUsingDeprecatedProjectHooks(options);
181
+ const { storage, basePath, maxUploadSize, cache, authorize, onArtifactEvent } = options;
182
+ const stripBasePath = options.stripBasePath ?? true;
183
+ const uploadUnit = options.uploadUnit ?? 'segment';
184
+ const allowedExtensions = normalizeAllowedExtensions(options.allowedExtensions);
185
+ const validatePayload = composeValidatePayload(options.validatePayload, options.validateProjectPayload);
186
+ const onUploadComplete = composeOnUploadComplete(options.onUploadComplete, options.onProjectUploadComplete);
187
+ const logger = options.logger ?? consoleLogger;
188
+ const tusPath = `${basePath}/upload`;
189
+ const tusServer = createPulsevaultTusServer({
190
+ storage,
191
+ tusPath,
192
+ maxSize: maxUploadSize,
193
+ allowedExtensions,
194
+ validatePayload,
195
+ onUploadComplete,
196
+ onArtifactEvent,
197
+ logger,
198
+ });
199
+ /**
200
+ * Run the consumer's `authorize` hook (if any) for a TUS request. Returns
201
+ * `true` iff the request may proceed; on rejection, this function already
202
+ * wrote the response.
203
+ */
204
+ const runAuthorize = async (req, res, phase) => {
205
+ let artifactId;
206
+ let kind = 'video';
207
+ let relatedTo;
208
+ if (phase === 'create') {
209
+ const meta = req.headers['upload-metadata'];
210
+ if (typeof meta === 'string') {
211
+ ({ artifactId, kind, relatedTo } = parseUploadMetadata(meta));
212
+ }
213
+ }
214
+ else {
215
+ artifactId = artifactIdFromTusUrl(req.url ?? '');
216
+ if (artifactId) {
217
+ kind = await resolveStorageKind(storage, artifactId);
218
+ relatedTo = await resolveStorageRelatedTo(storage, artifactId);
219
+ }
220
+ }
221
+ if (artifactId) {
222
+ stashPulseVaultContext(req, { artifactId, kind, relatedTo });
223
+ }
224
+ if (!authorize) {
225
+ return { ok: true, artifactId, kind, relatedTo };
226
+ }
227
+ if (!artifactId && phase === 'patch') {
228
+ // PROTOCOL.md §5.2: failing to resolve the artifactId for an in-flight
229
+ // upload request is an authorization failure — reject, don't fall
230
+ // through to "no artifactId to check, so allow".
231
+ logger.info({ url: req.url, phase }, 'pulsevault authorize rejected: unresolvable artifactId');
232
+ writeJson(res, 403, pulseVaultError('Unable to resolve artifact for authorization'));
233
+ return { ok: false };
234
+ }
235
+ if (!artifactId) {
236
+ return { ok: true, artifactId, kind, relatedTo };
237
+ }
238
+ try {
239
+ await authorize(req, { phase, artifactId, kind, relatedTo });
240
+ return { ok: true, artifactId, kind, relatedTo };
241
+ }
242
+ catch (err) {
243
+ const statusCode = statusCodeOf(err, 403);
244
+ const message = extractAuthzMessage(err);
245
+ logger.info({ err, artifactId, phase, statusCode }, 'pulsevault authorize rejected');
246
+ if (phase === 'create') {
247
+ await onArtifactEvent?.({ phase: 'authorize', artifactId, kind, reason: message });
248
+ }
249
+ writeJson(res, statusCode, pulseVaultError(message));
250
+ return { ok: false };
251
+ }
252
+ };
253
+ /**
254
+ * Fail closed on an unexpected handler error. Host frameworks call these
255
+ * handlers on a *hijacked* socket (e.g. Fastify's `reply.hijack()`), so a
256
+ * thrown error is never turned into a response by the framework — without
257
+ * this the socket would hang until the client/server timeout. If headers are
258
+ * already on the wire we can only destroy the socket; otherwise emit a 500.
259
+ */
260
+ const failClosed = (res, err, context) => {
261
+ logger.error({ err }, `pulsevault ${context} failed`);
262
+ if (res.headersSent || res.writableEnded) {
263
+ res.destroy();
264
+ return;
265
+ }
266
+ res.statusCode = 500;
267
+ res.setHeader('content-type', 'text/plain; charset=utf-8');
268
+ res.end('Internal Server Error');
269
+ };
270
+ const handleTus = async (req, res) => {
271
+ stampProtocolVersion(res);
272
+ // A client that drops the connection mid-request — a mobile app killed during a PATCH, a
273
+ // network reset, a cancelled upload — makes Node emit an 'error' ('aborted' / ECONNRESET) on
274
+ // the raw request (and sometimes response) stream. Node treats an unhandled 'error' event as
275
+ // fatal: with no listener it re-throws and crashes the whole process, killing every other
276
+ // in-flight upload. It fires from the socket-close handler on a later tick, so the try/catch
277
+ // below never sees it. Attach no-op listeners so an aborted connection is just a dropped
278
+ // request: @tus/server sees the truncated body, the stored offset simply doesn't advance, and
279
+ // the client resumes from the last persisted byte on its next PATCH.
280
+ req.on('error', () => { });
281
+ res.on('error', () => { });
282
+ const phase = req.method === 'POST' ? 'create' : 'patch';
283
+ try {
284
+ // Inside the try: runAuthorize does storage I/O (kind/relatedTo resolution)
285
+ // and header writes of its own — an adapter fault or a consumer error with
286
+ // a bogus statusCode there must fail closed too, not hang the hijacked socket.
287
+ const authz = await runAuthorize(req, res, phase);
288
+ if (!authz.ok)
289
+ return;
290
+ await pulseVaultTusContext.run({ request: req, artifactId: authz.artifactId }, () => tusServer.handle(req, res));
291
+ }
292
+ catch (err) {
293
+ failClosed(res, err, 'tus handler');
294
+ }
295
+ };
296
+ const handleCapabilities = async (_req, res) => {
297
+ stampProtocolVersion(res);
298
+ try {
299
+ writeJson(res, 200, {
300
+ protocolVersion: PROTOCOL_VERSION,
301
+ minSupportedVersion: MIN_SUPPORTED_PROTOCOL_VERSION,
302
+ maxSupportedVersion: MAX_SUPPORTED_PROTOCOL_VERSION,
303
+ uploadUnit,
304
+ kinds: [...UPLOAD_KINDS],
305
+ allowedExtensions,
306
+ maxUploadSize,
307
+ checksum: { algorithms: ['sha256', 'sha1', 'md5'] },
308
+ });
309
+ }
310
+ catch (err) {
311
+ failClosed(res, err, 'capabilities');
312
+ }
313
+ };
314
+ /**
315
+ * Shared prelude for the artifact GET/DELETE handlers: resolve `kind`/
316
+ * `relatedTo` from storage, stash the request context, then run `authorize`
317
+ * (if configured) for the given phase. Returns `undefined` (having already
318
+ * written the response) when validation fails or `authorize` rejects.
319
+ */
320
+ const prepareArtifactRequest = async (req, res, artifactId, phase, token) => {
321
+ if (!isUuid(artifactId)) {
322
+ writeJson(res, 400, pulseVaultError('`artifactId` must be a valid UUID'));
323
+ return undefined;
324
+ }
325
+ const kind = await resolveStorageKind(storage, artifactId);
326
+ const relatedTo = await resolveStorageRelatedTo(storage, artifactId);
327
+ stashPulseVaultContext(req, { artifactId, kind, relatedTo });
328
+ if (!authorize)
329
+ return { kind, relatedTo };
330
+ try {
331
+ await authorize(req, { phase, artifactId, kind, relatedTo, token });
332
+ return { kind, relatedTo };
333
+ }
334
+ catch (err) {
335
+ const statusCode = statusCodeOf(err, 403);
336
+ const message = extractAuthzMessage(err);
337
+ logger.info({ err, artifactId, phase, statusCode }, 'pulsevault authorize rejected');
338
+ await onArtifactEvent?.({ phase: 'authorize', artifactId, kind, reason: message });
339
+ writeJson(res, statusCode, pulseVaultError(message));
340
+ return undefined;
341
+ }
342
+ };
343
+ const handleArtifactDelete = async (req, res, artifactId) => {
344
+ stampProtocolVersion(res);
345
+ try {
346
+ const prepared = await prepareArtifactRequest(req, res, artifactId, 'delete');
347
+ if (!prepared)
348
+ return;
349
+ if (typeof storage.remove !== 'function') {
350
+ writeJson(res, 501, pulseVaultError('Storage adapter does not support delete'));
351
+ return;
352
+ }
353
+ const removed = await storage.remove(artifactId);
354
+ if (!removed) {
355
+ writeJson(res, 404, pulseVaultError('Artifact not found'));
356
+ return;
357
+ }
358
+ res.writeHead(204);
359
+ res.end();
360
+ }
361
+ catch (err) {
362
+ failClosed(res, err, 'artifact delete');
363
+ }
364
+ };
365
+ const handleArtifactGet = async (req, res, artifactId, token) => {
366
+ stampProtocolVersion(res);
367
+ try {
368
+ const prepared = await prepareArtifactRequest(req, res, artifactId, 'resolve', token);
369
+ if (!prepared)
370
+ return;
371
+ const resolved = await storage.resolve(artifactId);
372
+ if (!resolved) {
373
+ writeJson(res, 404, pulseVaultError('Artifact not found'));
374
+ return;
375
+ }
376
+ if (resolved.kind === 'redirect') {
377
+ res.writeHead(resolved.statusCode ?? 302, { Location: resolved.url });
378
+ res.end();
379
+ return;
380
+ }
381
+ const result = await send(req, resolved.filename, { root: resolved.root, ...cache });
382
+ if (result.type === 'error') {
383
+ writeJson(res, result.statusCode, pulseVaultError(result.metadata.error.message));
384
+ return;
385
+ }
386
+ const headers = { ...result.headers };
387
+ // If the storage adapter provided an explicit content type (e.g. for
388
+ // non-standard extensions like `.pulse`), override what @fastify/send
389
+ // would otherwise infer from the filename.
390
+ if (resolved.contentType) {
391
+ headers['content-type'] = resolved.contentType;
392
+ }
393
+ res.writeHead(result.statusCode, headers);
394
+ // Headers are on the wire now, so a mid-stream read error (file removed
395
+ // after stat, disk error) can't become a 500 — destroy the socket instead
396
+ // of letting an unhandled 'error' crash the process. Destroy the source on
397
+ // client disconnect so the file descriptor is never leaked.
398
+ result.stream.on('error', (err) => failClosed(res, err, 'artifact stream'));
399
+ // A client that aborts mid-download makes the *response* stream emit its own
400
+ // 'error' (ECONNRESET / EPIPE). `.pipe` doesn't forward that, and an unhandled
401
+ // 'error' on `res` is fatal to the process — so swallow it (the 'close' handler
402
+ // above already tears down the source and reclaims the fd).
403
+ res.on('error', () => { });
404
+ res.on('close', () => result.stream.destroy());
405
+ result.stream.pipe(res);
406
+ }
407
+ catch (err) {
408
+ failClosed(res, err, 'artifact get');
409
+ }
410
+ };
411
+ const handler = async (req, res, next) => {
412
+ // (Protocol-Version is stamped by each granular handler below, so it's
413
+ // applied consistently whether reached through this router or called
414
+ // directly by a host framework's own routing.)
415
+ const notFound = () => {
416
+ if (next) {
417
+ next();
418
+ return;
419
+ }
420
+ res.writeHead(404, { 'content-type': 'text/plain; charset=utf-8' });
421
+ res.end('Not Found');
422
+ };
423
+ // A raw socket can present a request-target `new URL` refuses to parse
424
+ // (e.g. an absolute-form or garbage target from a non-HTTP client probing
425
+ // the port) — that's the client's malformed request, not our 500, and it
426
+ // must not escape as an uncaught throw on a hijacked/raw response.
427
+ let url;
428
+ try {
429
+ url = new URL(req.url ?? '/', 'http://internal');
430
+ }
431
+ catch {
432
+ res.writeHead(400, { 'content-type': 'text/plain; charset=utf-8' });
433
+ res.end('Bad Request');
434
+ return;
435
+ }
436
+ let pathname = url.pathname;
437
+ if (stripBasePath && basePath !== '') {
438
+ if (pathname === basePath) {
439
+ pathname = '';
440
+ }
441
+ else if (pathname.startsWith(`${basePath}/`)) {
442
+ pathname = pathname.slice(basePath.length);
443
+ }
444
+ else {
445
+ notFound();
446
+ return;
447
+ }
448
+ }
449
+ if (pathname === '')
450
+ pathname = '/';
451
+ if (pathname === '/upload' || pathname.startsWith('/upload/')) {
452
+ await handleTus(req, res);
453
+ return;
454
+ }
455
+ if (pathname === '/capabilities' && req.method === 'GET') {
456
+ await handleCapabilities(req, res);
457
+ return;
458
+ }
459
+ const artifactMatch = pathname.match(/^\/artifacts\/([^/]+)$/);
460
+ if (artifactMatch?.[1]) {
461
+ const artifactId = artifactMatch[1];
462
+ if (req.method === 'GET') {
463
+ await handleArtifactGet(req, res, artifactId, url.searchParams.get('token') ?? undefined);
464
+ return;
465
+ }
466
+ if (req.method === 'DELETE') {
467
+ await handleArtifactDelete(req, res, artifactId);
468
+ return;
469
+ }
470
+ }
471
+ notFound();
472
+ };
473
+ return {
474
+ handler,
475
+ shutdown: async () => {
476
+ await storage.shutdown?.();
477
+ },
478
+ handleTus,
479
+ handleCapabilities,
480
+ handleArtifactGet,
481
+ handleArtifactDelete,
482
+ };
483
+ }
484
+ // Re-exported so a non-Fastify consumer never needs to import from both `.`
485
+ // and `./core` for a normal setup — these are already framework-agnostic
486
+ // and identical to what the `.` (Fastify) entry point re-exports.
487
+ export { createLocalStorage } from './storage/local.js';
488
+ export { createS3Storage } from './storage/s3.js';
489
+ export { sniffMp4, createMp4Sniffer, createS3Mp4Sniffer } from './lib/magic.js';
490
+ export { buildUploadLink } from './lib/deeplinks.js';
491
+ export { issueCapabilityToken, verifyCapabilityToken, createCapabilityAuthorize, } from './lib/capability-token.js';
492
+ export { createChecksumValidator, createS3ChecksumValidator, parseChecksumMetadata, } from './lib/checksum.js';
493
+ export {} from './lib/request.js';
494
+ //# sourceMappingURL=core.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"core.js","sourceRoot":"","sources":["../src/core.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,19 @@
1
+ import type { PulseVaultRequest } from './request.js';
2
+ import type { UploadKind } from '../storage/types.js';
3
+ export type PulseVaultAuthorizePhase = 'create' | 'patch' | 'resolve' | 'delete';
4
+ export type PulseVaultAuthorizeContext = {
5
+ phase: PulseVaultAuthorizePhase;
6
+ artifactId: string;
7
+ /** Artifact kind: `"video"`, `"project"`, `"captions"`, or `"thumbnail"`. Always present. */
8
+ kind: UploadKind;
9
+ /** Bearer / query-string token forwarded from the watch URL, if present. Only populated during the `"resolve"` phase. */
10
+ token?: string;
11
+ /**
12
+ * The session-anchor artifact this one declared via `Upload-Metadata.relatedTo`,
13
+ * if any. Lets `createCapabilityAuthorize` authorize an artifact against a
14
+ * token scoped to the session it belongs to rather than its own id.
15
+ */
16
+ relatedTo?: string;
17
+ };
18
+ export type PulseVaultAuthorize = (request: PulseVaultRequest, ctx: PulseVaultAuthorizeContext) => void | Promise<void>;
19
+ //# sourceMappingURL=authorize.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"authorize.d.ts","sourceRoot":"","sources":["../../src/lib/authorize.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACtD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAEtD,MAAM,MAAM,wBAAwB,GAAG,QAAQ,GAAG,OAAO,GAAG,SAAS,GAAG,QAAQ,CAAC;AAEjF,MAAM,MAAM,0BAA0B,GAAG;IACvC,KAAK,EAAE,wBAAwB,CAAC;IAChC,UAAU,EAAE,MAAM,CAAC;IACnB,6FAA6F;IAC7F,IAAI,EAAE,UAAU,CAAC;IACjB,yHAAyH;IACzH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG,CAChC,OAAO,EAAE,iBAAiB,EAC1B,GAAG,EAAE,0BAA0B,KAC5B,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=authorize.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"authorize.js","sourceRoot":"","sources":["../../src/lib/authorize.ts"],"names":[],"mappings":""}
@@ -0,0 +1,80 @@
1
+ import type { PulseVaultAuthorize } from './authorize.js';
2
+ /**
3
+ * Claims signed into a capability token. `kid` lets a secret be rotated with
4
+ * an overlap window (old + new key both verify) instead of instantly
5
+ * invalidating every outstanding token; `iat` closes a clock-skew gap where
6
+ * signing only `exp` would let a slow server clock accept an already-expired
7
+ * token; `issuer` binds the token to the issuing deployment's identity so a
8
+ * secret accidentally shared between two independent orgs can't be replayed
9
+ * across them.
10
+ */
11
+ export type CapabilityTokenClaims = {
12
+ /** The artifact (or session-anchor artifact — see `relatedTo`) this token authorizes. */
13
+ artifactId: string;
14
+ /** Issued-at, seconds since epoch. */
15
+ iat: number;
16
+ /** Expiry, seconds since epoch. */
17
+ exp: number;
18
+ /** Key id, looked up via `lookupSecret` at verify time. */
19
+ kid: string;
20
+ /** Issuing deployment's identity, e.g. `"https://vault.acme-hospital.org"`. */
21
+ issuer: string;
22
+ };
23
+ export type IssueCapabilityTokenOptions = {
24
+ /** Key id this token is signed under. Required so rotation has an explicit overlap window. */
25
+ keyId: string;
26
+ /** Issuing deployment's identity. Required so cross-issuer replay can be rejected at verify time. */
27
+ issuer: string;
28
+ /** Token lifetime in seconds. Defaults to 1800 (30 minutes). */
29
+ expirySeconds?: number;
30
+ };
31
+ /**
32
+ * Mint a stateless, HMAC-signed capability token scoped to one artifact (or
33
+ * session-anchor artifact). No server-side session table — verification is a
34
+ * pure function of `(token, secret)`, so the issuing deployment can rotate
35
+ * keys, change its TTL policy, or revoke at the artifact level entirely on
36
+ * its own schedule. See `createCapabilityAuthorize` to use this as the
37
+ * plugin's `authorize` hook directly.
38
+ */
39
+ export declare function issueCapabilityToken(artifactId: string, secret: string, opts: IssueCapabilityTokenOptions): string;
40
+ /** Look up the signing secret for a given key id. Return `null`/`undefined` for an unrecognized `kid`. */
41
+ export type LookupSecret = (keyId: string) => string | null | undefined;
42
+ export type VerifyCapabilityTokenOptions = {
43
+ /** Must match the token's `issuer` claim exactly. */
44
+ issuer: string;
45
+ /** Clock-skew tolerance in seconds applied to both `iat` and `exp` checks. Defaults to 30. */
46
+ clockToleranceSeconds?: number;
47
+ };
48
+ /**
49
+ * Verify a token minted by `issueCapabilityToken`. Returns the authorized
50
+ * `artifactId` on success, or `null` for any failure (malformed token,
51
+ * unknown `kid`, bad signature, expired, issued too far in the future, or
52
+ * issuer mismatch) — deliberately collapsed to one failure shape so callers
53
+ * can't accidentally branch on *why* a token failed and leak which part was
54
+ * wrong to an attacker.
55
+ */
56
+ export declare function verifyCapabilityToken(token: string, lookupSecret: LookupSecret, opts: VerifyCapabilityTokenOptions): {
57
+ artifactId: string;
58
+ } | null;
59
+ /**
60
+ * Ready-made `authorize` hook backed by `verifyCapabilityToken`. Pass this
61
+ * directly as the plugin's `authorize` option for a secure-by-default setup
62
+ * with no custom auth code:
63
+ *
64
+ * ```ts
65
+ * await app.register(pulseVault, {
66
+ * // ...
67
+ * authorize: createCapabilityAuthorize((kid) => keys[kid] ?? null, {
68
+ * issuer: "https://vault.acme-hospital.org",
69
+ * }),
70
+ * });
71
+ * ```
72
+ *
73
+ * Authorizes the request if the token's `artifactId` matches either the
74
+ * artifact being acted on, or that artifact's `relatedTo` session anchor —
75
+ * so one token issued for a video also covers its captions/manifest/thumbnail
76
+ * and (under `uploadUnit: "segment"`) every clip and the ordering manifest
77
+ * uploaded in the same session, without minting a token per artifact.
78
+ */
79
+ export declare function createCapabilityAuthorize(lookupSecret: LookupSecret, opts: VerifyCapabilityTokenOptions): PulseVaultAuthorize;
80
+ //# sourceMappingURL=capability-token.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capability-token.d.ts","sourceRoot":"","sources":["../../src/lib/capability-token.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,mBAAmB,EAA8B,MAAM,gBAAgB,CAAC;AAEtF;;;;;;;;GAQG;AACH,MAAM,MAAM,qBAAqB,GAAG;IAClC,yFAAyF;IACzF,UAAU,EAAE,MAAM,CAAC;IACnB,sCAAsC;IACtC,GAAG,EAAE,MAAM,CAAC;IACZ,mCAAmC;IACnC,GAAG,EAAE,MAAM,CAAC;IACZ,2DAA2D;IAC3D,GAAG,EAAE,MAAM,CAAC;IACZ,+EAA+E;IAC/E,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAqBF,MAAM,MAAM,2BAA2B,GAAG;IACxC,8FAA8F;IAC9F,KAAK,EAAE,MAAM,CAAC;IACd,qGAAqG;IACrG,MAAM,EAAE,MAAM,CAAC;IACf,gEAAgE;IAChE,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC;AAEF;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAClC,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,2BAA2B,GAChC,MAAM,CAWR;AAED,0GAA0G;AAC1G,MAAM,MAAM,YAAY,GAAG,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;AAExE,MAAM,MAAM,4BAA4B,GAAG;IACzC,qDAAqD;IACrD,MAAM,EAAE,MAAM,CAAC;IACf,8FAA8F;IAC9F,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAChC,CAAC;AAEF;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,MAAM,EACb,YAAY,EAAE,YAAY,EAC1B,IAAI,EAAE,4BAA4B,GACjC;IAAE,UAAU,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAuC/B;AAgBD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,yBAAyB,CACvC,YAAY,EAAE,YAAY,EAC1B,IAAI,EAAE,4BAA4B,GACjC,mBAAmB,CAcrB"}