@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.
Files changed (70) hide show
  1. package/CHANGELOG.md +184 -0
  2. package/LICENSE +15 -39
  3. package/OPERATIONS.md +223 -0
  4. package/PROTOCOL.md +312 -0
  5. package/README.md +532 -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 +90 -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 +431 -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 +126 -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 +102 -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/magic.d.ts +30 -5
  38. package/dist/lib/magic.d.ts.map +1 -1
  39. package/dist/lib/magic.js +51 -14
  40. package/dist/lib/magic.js.map +1 -1
  41. package/dist/lib/options.d.ts +39 -0
  42. package/dist/lib/options.d.ts.map +1 -0
  43. package/dist/lib/options.js +103 -0
  44. package/dist/lib/options.js.map +1 -0
  45. package/dist/lib/pulsevaultTus.d.ts +52 -26
  46. package/dist/lib/pulsevaultTus.d.ts.map +1 -1
  47. package/dist/lib/pulsevaultTus.js +111 -43
  48. package/dist/lib/pulsevaultTus.js.map +1 -1
  49. package/dist/lib/request.d.ts +24 -0
  50. package/dist/lib/request.d.ts.map +1 -0
  51. package/dist/lib/request.js +16 -0
  52. package/dist/lib/request.js.map +1 -0
  53. package/dist/lib/uuid.d.ts.map +1 -1
  54. package/dist/lib/uuid.js +1 -1
  55. package/dist/lib/uuid.js.map +1 -1
  56. package/dist/routes/pulsevault.d.ts +18 -16
  57. package/dist/routes/pulsevault.d.ts.map +1 -1
  58. package/dist/routes/pulsevault.js +120 -251
  59. package/dist/routes/pulsevault.js.map +1 -1
  60. package/dist/storage/local.d.ts +31 -10
  61. package/dist/storage/local.d.ts.map +1 -1
  62. package/dist/storage/local.js +166 -82
  63. package/dist/storage/local.js.map +1 -1
  64. package/dist/storage/s3.d.ts +109 -0
  65. package/dist/storage/s3.d.ts.map +1 -0
  66. package/dist/storage/s3.js +432 -0
  67. package/dist/storage/s3.js.map +1 -0
  68. package/dist/storage/types.d.ts +69 -17
  69. package/dist/storage/types.d.ts.map +1 -1
  70. package/package.json +35 -7
@@ -1,32 +1,34 @@
1
- import type { FastifyPluginAsync, FastifyPluginOptions, FastifyRequest } from "fastify";
2
- import type { PulseVaultCacheOptions } from "../app.js";
3
- import { type PulseVaultOnUploadComplete } from "../lib/pulsevaultTus.js";
4
- import type { PulseVaultValidatePayload } from "../lib/magic.js";
5
- import type { PulseVaultStorage } from "../storage/types.js";
6
- declare module "fastify" {
1
+ import type { FastifyPluginAsync, FastifyPluginOptions } from 'fastify';
2
+ import type { PulseVaultCacheOptions } from '../app.js';
3
+ import { PROTOCOL_VERSION } from '../core.js';
4
+ import type { PulseVaultOnUploadComplete, PulseVaultOnArtifactEvent } from '../lib/pulsevaultTus.js';
5
+ import type { PulseVaultValidatePayload } from '../lib/magic.js';
6
+ import { type PulseVaultAuthorize } from '../lib/authorize.js';
7
+ import type { PulseVaultAllowedExtensions } from '../lib/options.js';
8
+ import type { PulseVaultStorage, UploadKind } from '../storage/types.js';
9
+ export type { PulseVaultAuthorize, PulseVaultAuthorizeContext, PulseVaultAuthorizePhase, } from '../lib/authorize.js';
10
+ declare module 'fastify' {
7
11
  interface FastifyRequest {
8
12
  pulseVault?: {
9
- videoid: string;
13
+ artifactId: string;
14
+ kind: UploadKind;
15
+ relatedTo?: string;
10
16
  };
11
17
  }
12
18
  }
13
- export type PulseVaultAuthorizePhase = "create" | "patch" | "resolve" | "delete";
14
- export type PulseVaultAuthorizeContext = {
15
- phase: PulseVaultAuthorizePhase;
16
- videoid: string;
17
- /** Bearer / query-string token forwarded from the watch URL, if present. Only populated during the `"resolve"` phase. */
18
- token?: string;
19
- };
20
- export type PulseVaultAuthorize = (request: FastifyRequest, ctx: PulseVaultAuthorizeContext) => void | Promise<void>;
21
19
  export type PulseVaultRoutesOptions = {
22
20
  storage: PulseVaultStorage;
23
21
  maxUploadSize: number;
24
- allowedExtensions: readonly string[];
22
+ allowedExtensions: PulseVaultAllowedExtensions;
23
+ /** Advertised via `GET /capabilities` so the client knows which upload strategy this server expects. */
24
+ uploadUnit: 'beat' | 'merged';
25
25
  cache?: PulseVaultCacheOptions;
26
26
  authorize?: PulseVaultAuthorize;
27
27
  validatePayload?: PulseVaultValidatePayload;
28
28
  onUploadComplete?: PulseVaultOnUploadComplete;
29
+ onArtifactEvent?: PulseVaultOnArtifactEvent;
29
30
  } & FastifyPluginOptions;
30
31
  declare const pulseVaultRoutes: FastifyPluginAsync<PulseVaultRoutesOptions>;
31
32
  export default pulseVaultRoutes;
33
+ export { PROTOCOL_VERSION };
32
34
  //# sourceMappingURL=pulsevault.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"pulsevault.d.ts","sourceRoot":"","sources":["../../src/routes/pulsevault.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,kBAAkB,EAClB,oBAAoB,EAEpB,cAAc,EAEf,MAAM,SAAS,CAAC;AAEjB,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,WAAW,CAAC;AACxD,OAAO,EAGL,KAAK,0BAA0B,EAChC,MAAM,yBAAyB,CAAC;AACjC,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,iBAAiB,CAAC;AAGjE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAM7D,OAAO,QAAQ,SAAS,CAAC;IACvB,UAAU,cAAc;QACtB,UAAU,CAAC,EAAE;YAAE,OAAO,EAAE,MAAM,CAAA;SAAE,CAAC;KAClC;CACF;AAED,MAAM,MAAM,wBAAwB,GAChC,QAAQ,GACR,OAAO,GACP,SAAS,GACT,QAAQ,CAAC;AAEb,MAAM,MAAM,0BAA0B,GAAG;IACvC,KAAK,EAAE,wBAAwB,CAAC;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,yHAAyH;IACzH,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG,CAChC,OAAO,EAAE,cAAc,EACvB,GAAG,EAAE,0BAA0B,KAC5B,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAE1B,MAAM,MAAM,uBAAuB,GAAG;IACpC,OAAO,EAAE,iBAAiB,CAAC;IAC3B,aAAa,EAAE,MAAM,CAAC;IACtB,iBAAiB,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC,KAAK,CAAC,EAAE,sBAAsB,CAAC;IAC/B,SAAS,CAAC,EAAE,mBAAmB,CAAC;IAChC,eAAe,CAAC,EAAE,yBAAyB,CAAC;IAC5C,gBAAgB,CAAC,EAAE,0BAA0B,CAAC;CAC/C,GAAG,oBAAoB,CAAC;AAwKzB,QAAA,MAAM,gBAAgB,EAAE,kBAAkB,CAAC,uBAAuB,CA+NjE,CAAC;AAEF,eAAe,gBAAgB,CAAC"}
1
+ {"version":3,"file":"pulsevault.d.ts","sourceRoot":"","sources":["../../src/routes/pulsevault.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,kBAAkB,EAClB,oBAAoB,EAIrB,MAAM,SAAS,CAAC;AACjB,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,WAAW,CAAC;AACxD,OAAO,EAAwB,gBAAgB,EAAE,MAAM,YAAY,CAAC;AACpE,OAAO,KAAK,EACV,0BAA0B,EAC1B,yBAAyB,EAC1B,MAAM,yBAAyB,CAAC;AACjC,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,iBAAiB,CAAC;AACjE,OAAO,EAAE,KAAK,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAC/D,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,mBAAmB,CAAC;AACrE,OAAO,KAAK,EAAE,iBAAiB,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAEzE,YAAY,EACV,mBAAmB,EACnB,0BAA0B,EAC1B,wBAAwB,GACzB,MAAM,qBAAqB,CAAC;AAK7B,OAAO,QAAQ,SAAS,CAAC;IACvB,UAAU,cAAc;QACtB,UAAU,CAAC,EAAE;YAAE,UAAU,EAAE,MAAM,CAAC;YAAC,IAAI,EAAE,UAAU,CAAC;YAAC,SAAS,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;KAC3E;CACF;AAED,MAAM,MAAM,uBAAuB,GAAG;IACpC,OAAO,EAAE,iBAAiB,CAAC;IAC3B,aAAa,EAAE,MAAM,CAAC;IACtB,iBAAiB,EAAE,2BAA2B,CAAC;IAC/C,wGAAwG;IACxG,UAAU,EAAE,MAAM,GAAG,QAAQ,CAAC;IAC9B,KAAK,CAAC,EAAE,sBAAsB,CAAC;IAC/B,SAAS,CAAC,EAAE,mBAAmB,CAAC;IAChC,eAAe,CAAC,EAAE,yBAAyB,CAAC;IAC5C,gBAAgB,CAAC,EAAE,0BAA0B,CAAC;IAC9C,eAAe,CAAC,EAAE,yBAAyB,CAAC;CAC7C,GAAG,oBAAoB,CAAC;AA4JzB,QAAA,MAAM,gBAAgB,EAAE,kBAAkB,CAAC,uBAAuB,CA8EjE,CAAC;AAEF,eAAe,gBAAgB,CAAC;AAChC,OAAO,EAAE,gBAAgB,EAAE,CAAC"}
@@ -1,326 +1,195 @@
1
- import send from "@fastify/send";
2
- import { createPulsevaultTusServer, pulseVaultTusContext, } from "../lib/pulsevaultTus.js";
3
- import { pulseVaultError } from "../lib/errors.js";
4
- import { isUuid } from "../lib/uuid.js";
5
- /**
6
- * Pull `videoid` out of a raw `Upload-Metadata` header. Format is a
7
- * comma-separated list of `<key> <base64-value>` pairs (tus v1 creation
8
- * extension). We only care about `videoid`; everything else is left to the
9
- * tus server proper.
10
- */
11
- function parseVideoidFromMetadata(header) {
12
- for (const pair of header.split(",")) {
13
- const trimmed = pair.trim();
14
- if (!trimmed)
15
- continue;
16
- const sep = trimmed.indexOf(" ");
17
- if (sep < 0)
18
- continue;
19
- const key = trimmed.slice(0, sep);
20
- if (key !== "videoid")
21
- continue;
22
- const value = trimmed.slice(sep + 1).trim();
23
- if (!value)
24
- return undefined;
25
- try {
26
- const decoded = Buffer.from(value, "base64").toString("utf8");
27
- return isUuid(decoded) ? decoded : undefined;
28
- }
29
- catch {
30
- return undefined;
31
- }
32
- }
33
- return undefined;
34
- }
35
- /**
36
- * Decode the last URL segment of a tus PATCH/HEAD/DELETE (base64url-encoded
37
- * id) and extract the first path component, which the plugin always shapes as
38
- * the videoid (see `pulseVaultTus.ts`).
39
- */
40
- function videoidFromTusUrl(url) {
41
- const match = url.match(/\/upload\/([^/?#]+)/);
42
- if (!match?.[1])
43
- return undefined;
44
- let decoded;
45
- try {
46
- decoded = Buffer.from(match[1], "base64url").toString("utf8");
47
- }
48
- catch {
49
- return undefined;
50
- }
51
- const first = decoded.split("/", 1)[0];
52
- return isUuid(first) ? first : undefined;
53
- }
54
- function extractAuthzStatus(err) {
55
- const e = err;
56
- if (typeof e?.statusCode === "number")
57
- return e.statusCode;
58
- if (typeof e?.status_code === "number")
59
- return e.status_code;
60
- return 403;
61
- }
62
- function extractAuthzMessage(err) {
63
- if (err instanceof Error && err.message)
64
- return err.message;
65
- return "Forbidden";
66
- }
1
+ import { createPulseVaultCore, PROTOCOL_VERSION } from '../core.js';
2
+ import {} from '../lib/authorize.js';
67
3
  const pulseVaultErrorResponse = {
68
- type: "object",
4
+ type: 'object',
69
5
  properties: {
70
- ok: { type: "boolean" },
71
- error: { type: "string" },
6
+ ok: { type: 'boolean' },
7
+ error: { type: 'string' },
72
8
  },
73
- required: ["ok", "error"],
9
+ required: ['ok', 'error'],
74
10
  };
75
11
  const tusRouteSchema = {
76
- tags: ["pulsevault"],
77
- summary: "TUS resumable upload endpoint",
78
- description: "TUS v1 resumable upload protocol.\n\n" +
79
- "- `POST` creates a new upload. The `Upload-Metadata` header must include a base64-encoded `videoid` pair.\n" +
80
- "- `PATCH` appends a chunk at the offset given by `Upload-Offset`, with `Content-Type: application/offset+octet-stream`.\n" +
81
- "- `HEAD` returns the current offset for a resumable upload.\n" +
82
- "- `DELETE` cancels an in-flight upload.\n\n" +
83
- "See https://tus.io/protocols/resumable-upload for the full protocol.",
12
+ tags: ['pulsevault'],
13
+ summary: 'TUS resumable upload endpoint',
14
+ description: 'TUS v1 resumable upload protocol.\n\n' +
15
+ '- `POST` creates a new upload. The `Upload-Metadata` header must include base64-encoded key/value pairs:\n' +
16
+ ' - `artifactId` (or the legacy `videoid`/`projectid` aliases) — a UUID generated by your server.\n' +
17
+ " - `filename` — original filename; the extension must match the kind's allowed list.\n" +
18
+ ' - `kind` — `video` (default), `project`, or `captions`. Determines the storage subdir and which completion hooks fire.\n' +
19
+ " - `relatedTo` — optional UUID of another artifact this one belongs to (e.g. a video's captions, or a beat belonging to a pulse manifest's session).\n" +
20
+ ' - `checksum` — optional `<algorithm>:<hex digest>` of the finished file, verified post-upload if a checksum validator is configured.\n' +
21
+ '- `PATCH` appends a chunk at the offset given by `Upload-Offset`, with `Content-Type: application/offset+octet-stream`.\n' +
22
+ '- `HEAD` returns the current offset for a resumable upload.\n' +
23
+ '- `DELETE` cancels an in-flight upload.\n\n' +
24
+ 'See https://tus.io/protocols/resumable-upload for the full protocol.',
84
25
  response: {
85
- 400: { description: "Invalid request.", ...pulseVaultErrorResponse },
26
+ 400: { description: 'Invalid request.', ...pulseVaultErrorResponse },
86
27
  403: {
87
- description: "Authorize hook rejected the request.",
28
+ description: 'Authorize hook rejected the request.',
88
29
  ...pulseVaultErrorResponse,
89
30
  },
90
31
  },
91
32
  };
92
- const videoDeleteSchema = {
93
- tags: ["pulsevault"],
94
- summary: "Delete an uploaded video",
95
- description: "Deletes all storage for a videoid (bytes + sidecar metadata). Runs the `authorize` hook with `phase: \"delete\"` before the adapter's `remove` is called. Returns 204 on success, 404 if the videoid was unknown, 501 if the adapter does not implement `remove`.",
33
+ const artifactDeleteSchema = {
34
+ tags: ['pulsevault'],
35
+ summary: 'Delete an uploaded artifact',
36
+ description: 'Deletes all storage for an artifactId (bytes + sidecar metadata), regardless of kind. Runs the `authorize` hook with `phase: "delete"` before the adapter\'s `remove` is called. Returns 204 on success, 404 if the artifactId was unknown, 501 if the adapter does not implement `remove`.',
96
37
  params: {
97
- type: "object",
38
+ type: 'object',
98
39
  properties: {
99
- videoid: {
100
- type: "string",
101
- format: "uuid",
102
- description: "UUID of the upload to delete.",
40
+ artifactId: {
41
+ type: 'string',
42
+ format: 'uuid',
43
+ description: 'UUID of the upload to delete.',
103
44
  },
104
45
  },
105
- required: ["videoid"],
46
+ required: ['artifactId'],
106
47
  },
107
48
  response: {
108
49
  400: {
109
- description: "`videoid` is not a valid UUID.",
50
+ description: '`artifactId` is not a valid UUID.',
110
51
  ...pulseVaultErrorResponse,
111
52
  },
112
53
  403: {
113
- description: "Authorize hook rejected the request.",
54
+ description: 'Authorize hook rejected the request.',
114
55
  ...pulseVaultErrorResponse,
115
56
  },
116
- 404: { description: "Video not found.", ...pulseVaultErrorResponse },
57
+ 404: { description: 'Artifact not found.', ...pulseVaultErrorResponse },
117
58
  501: {
118
- description: "Storage adapter does not implement delete.",
59
+ description: 'Storage adapter does not implement delete.',
119
60
  ...pulseVaultErrorResponse,
120
61
  },
121
62
  },
122
63
  };
123
- const videoGetSchema = {
124
- tags: ["pulsevault"],
125
- summary: "Serve a previously uploaded video",
126
- description: "Resolves the `videoid` through the configured storage adapter and either streams the bytes or redirects (for CDN-backed adapters). Runs the `authorize` hook before resolve.",
64
+ const artifactGetSchema = {
65
+ tags: ['pulsevault'],
66
+ summary: 'Serve a previously uploaded artifact',
67
+ description: "Resolves the `artifactId` through the configured storage adapter and either streams the bytes or redirects (for CDN-backed adapters). The artifact's kind (video, project, or captions) is resolved from storage, not the URL. Runs the `authorize` hook before resolve.",
127
68
  params: {
128
- type: "object",
69
+ type: 'object',
129
70
  properties: {
130
- videoid: {
131
- type: "string",
132
- format: "uuid",
133
- description: "UUID returned from the reserve/TUS-create flow.",
71
+ artifactId: {
72
+ type: 'string',
73
+ format: 'uuid',
74
+ description: 'UUID returned from the upload flow.',
134
75
  },
135
76
  },
136
- required: ["videoid"],
77
+ required: ['artifactId'],
137
78
  },
138
79
  querystring: {
139
- type: "object",
80
+ type: 'object',
140
81
  properties: {
141
82
  token: {
142
- type: "string",
143
- description: "Optional bearer token for pre-authenticated watch links. Forwarded to the `authorize` hook as `ctx.token` so parent servers can validate it without a separate login step.",
83
+ type: 'string',
84
+ description: 'Optional bearer token for pre-authenticated watch links. Forwarded to the `authorize` hook as `ctx.token` so parent servers can validate it without a separate login step.',
144
85
  },
145
86
  },
146
87
  },
147
88
  response: {
148
89
  400: {
149
- description: "`videoid` is not a valid UUID.",
90
+ description: '`artifactId` is not a valid UUID.',
150
91
  ...pulseVaultErrorResponse,
151
92
  },
152
93
  403: {
153
- description: "Authorize hook rejected the request.",
94
+ description: 'Authorize hook rejected the request.',
154
95
  ...pulseVaultErrorResponse,
155
96
  },
156
- 404: { description: "Video not found.", ...pulseVaultErrorResponse },
97
+ 404: { description: 'Artifact not found.', ...pulseVaultErrorResponse },
157
98
  },
158
99
  };
100
+ const capabilitiesSchema = {
101
+ tags: ['pulsevault'],
102
+ summary: "Discover this deployment's protocol version and configuration",
103
+ description: 'Unauthenticated — the response carries no secrets. Lets a client detect protocol compatibility before pairing, and which upload strategy (`uploadUnit`) this server expects.',
104
+ response: {
105
+ 200: {
106
+ type: 'object',
107
+ properties: {
108
+ protocolVersion: { type: 'number' },
109
+ minSupportedVersion: { type: 'number' },
110
+ maxSupportedVersion: { type: 'number' },
111
+ uploadUnit: { type: 'string', enum: ['beat', 'merged'] },
112
+ kinds: { type: 'array', items: { type: 'string' } },
113
+ allowedExtensions: {
114
+ type: 'object',
115
+ properties: {
116
+ video: { type: 'array', items: { type: 'string' } },
117
+ project: { type: 'array', items: { type: 'string' } },
118
+ captions: { type: 'array', items: { type: 'string' } },
119
+ },
120
+ },
121
+ maxUploadSize: { type: 'number' },
122
+ checksum: {
123
+ type: 'object',
124
+ properties: { algorithms: { type: 'array', items: { type: 'string' } } },
125
+ },
126
+ },
127
+ },
128
+ },
129
+ };
130
+ /** Coerce the raw route param to a string; an invalid UUID is rejected by the core's own check either way. */
131
+ function paramToString(value) {
132
+ return typeof value === 'string' ? value : '';
133
+ }
159
134
  const pulseVaultRoutes = async (fastify, opts) => {
160
- const { storage, maxUploadSize, allowedExtensions, cache, authorize, validatePayload, onUploadComplete, } = opts;
161
- // `fastify.prefix` is `""` when the plugin is mounted at the root.
162
- const tusPath = `${fastify.prefix}/upload`;
163
- const tusServer = createPulsevaultTusServer({
135
+ const { storage, maxUploadSize, allowedExtensions, uploadUnit, cache, authorize, validatePayload, onUploadComplete, onArtifactEvent, } = opts;
136
+ // All hook orchestration, tus glue, and artifact GET/DELETE logic lives in
137
+ // the framework-agnostic core (`../core.js`); this file only wires Fastify's
138
+ // routing/schema/lifecycle around it, so behavior can't drift from the
139
+ // `./core` entry point non-Fastify hosts use.
140
+ const core = createPulseVaultCore({
164
141
  storage,
165
- tusPath,
166
- maxSize: maxUploadSize,
142
+ // `fastify.prefix` is `""` when the plugin is mounted at the root.
143
+ basePath: fastify.prefix,
144
+ maxUploadSize,
145
+ uploadUnit,
167
146
  allowedExtensions,
147
+ cache,
148
+ authorize,
168
149
  validatePayload,
169
150
  onUploadComplete,
151
+ onArtifactEvent,
152
+ logger: fastify.log,
170
153
  });
171
- fastify.addContentTypeParser("application/offset+octet-stream", (_request, _payload, done) => {
154
+ fastify.addContentTypeParser('application/offset+octet-stream', (_request, _payload, done) => {
172
155
  done(null);
173
156
  });
174
- /**
175
- * Run the consumer's `authorize` hook (if any) for a TUS request. Returns
176
- * `true` iff the request may proceed; on rejection, this function already
177
- * wrote the response.
178
- */
179
- const runAuthorize = async (request, reply, phase) => {
180
- let videoid;
181
- if (phase === "create") {
182
- const meta = request.headers["upload-metadata"];
183
- if (typeof meta === "string") {
184
- videoid = parseVideoidFromMetadata(meta);
185
- }
186
- }
187
- else {
188
- videoid = videoidFromTusUrl(request.url);
189
- }
190
- if (videoid) {
191
- request.pulseVault = { videoid };
192
- }
193
- if (!authorize) {
194
- return { ok: true, videoid };
195
- }
196
- // If we can't extract a videoid, let tus produce its own 4xx for malformed
197
- // input rather than synthesize a fake authorize failure.
198
- if (!videoid) {
199
- return { ok: true, videoid };
200
- }
201
- try {
202
- await authorize(request, { phase, videoid });
203
- return { ok: true, videoid };
204
- }
205
- catch (err) {
206
- const statusCode = extractAuthzStatus(err);
207
- const message = extractAuthzMessage(err);
208
- request.log.info({ err, videoid, phase, statusCode }, "pulsevault authorize rejected");
209
- await reply.code(statusCode).send(pulseVaultError(message));
210
- return { ok: false };
211
- }
212
- };
157
+ const tusMethods = ['POST', 'PATCH', 'HEAD', 'DELETE', 'OPTIONS'];
213
158
  const tusHandler = async (request, reply) => {
214
- const phase = request.method === "POST" ? "create" : "patch";
215
- const authz = await runAuthorize(request, reply, phase);
216
- if (!authz.ok)
217
- return;
218
- // Once hijacked, Fastify will not write to this reply on our behalf, so we
219
- // must translate any unexpected throw from `@tus/server` into a response
220
- // ourselves — otherwise the socket stays open until the client times out.
159
+ // The core writes directly to the raw response (tus dispatch, authorize
160
+ // rejections, error translation) — hijack so Fastify doesn't also try to
161
+ // manage this reply afterward.
221
162
  reply.hijack();
222
- try {
223
- await pulseVaultTusContext.run({ request, videoid: authz.videoid }, () => tusServer.handle(request.raw, reply.raw));
224
- }
225
- catch (err) {
226
- request.log.error({ err }, "pulsevault tus handler failed");
227
- if (reply.raw.headersSent || reply.raw.writableEnded) {
228
- reply.raw.destroy();
229
- return;
230
- }
231
- reply.raw.statusCode = 500;
232
- reply.raw.setHeader("content-type", "text/plain; charset=utf-8");
233
- reply.raw.end("Internal Server Error");
234
- }
163
+ await core.handleTus(request.raw, reply.raw);
235
164
  };
236
- const tusMethods = ["POST", "PATCH", "HEAD", "DELETE", "OPTIONS"];
237
165
  fastify.route({
238
166
  method: [...tusMethods],
239
- url: "/upload",
167
+ url: '/upload',
240
168
  schema: tusRouteSchema,
241
169
  handler: tusHandler,
242
170
  });
243
171
  fastify.route({
244
172
  method: [...tusMethods],
245
- url: "/upload/*",
173
+ url: '/upload/*',
246
174
  schema: tusRouteSchema,
247
175
  handler: tusHandler,
248
176
  });
249
- fastify.delete("/:videoid", { schema: videoDeleteSchema }, async (request, reply) => {
250
- const videoid = request.params?.videoid;
251
- if (!isUuid(videoid)) {
252
- return reply
253
- .code(400)
254
- .send(pulseVaultError("`videoid` must be a valid UUID"));
255
- }
256
- request.pulseVault = { videoid };
257
- if (authorize) {
258
- try {
259
- await authorize(request, { phase: "delete", videoid });
260
- }
261
- catch (err) {
262
- const statusCode = extractAuthzStatus(err);
263
- const message = extractAuthzMessage(err);
264
- request.log.info({ err, videoid, phase: "delete", statusCode }, "pulsevault authorize rejected");
265
- return reply.code(statusCode).send(pulseVaultError(message));
266
- }
267
- }
268
- if (typeof storage.remove !== "function") {
269
- return reply
270
- .code(501)
271
- .send(pulseVaultError("Storage adapter does not support delete"));
272
- }
273
- const removed = await storage.remove(videoid);
274
- if (!removed) {
275
- return reply.code(404).send(pulseVaultError("Video not found"));
276
- }
277
- return reply.code(204).send();
177
+ fastify.get('/capabilities', { schema: capabilitiesSchema }, async (request, reply) => {
178
+ reply.hijack();
179
+ await core.handleCapabilities(request.raw, reply.raw);
278
180
  });
279
- fastify.get("/:videoid", { schema: videoGetSchema }, async (request, reply) => {
280
- const videoid = request.params?.videoid;
281
- if (!isUuid(videoid)) {
282
- return reply
283
- .code(400)
284
- .send(pulseVaultError("`videoid` must be a valid UUID"));
285
- }
286
- request.pulseVault = { videoid };
287
- // Extract the optional token forwarded by the mobile app in the watch URL.
181
+ fastify.delete('/artifacts/:artifactId', { schema: artifactDeleteSchema }, async (request, reply) => {
182
+ const artifactId = paramToString(request.params?.artifactId);
183
+ reply.hijack();
184
+ await core.handleArtifactDelete(request.raw, reply.raw, artifactId);
185
+ });
186
+ fastify.get('/artifacts/:artifactId', { schema: artifactGetSchema }, async (request, reply) => {
187
+ const artifactId = paramToString(request.params?.artifactId);
288
188
  const token = request.query?.token;
289
- // Run authorize *before* resolve so consumers can reject without the
290
- // response leaking "this videoid exists but you don't own it" vs. "no such
291
- // videoid".
292
- if (authorize) {
293
- try {
294
- await authorize(request, { phase: "resolve", videoid, token });
295
- }
296
- catch (err) {
297
- const statusCode = extractAuthzStatus(err);
298
- const message = extractAuthzMessage(err);
299
- request.log.info({ err, videoid, phase: "resolve", statusCode }, "pulsevault authorize rejected");
300
- return reply.code(statusCode).send(pulseVaultError(message));
301
- }
302
- }
303
- const resolved = await storage.resolve(videoid);
304
- if (!resolved) {
305
- return reply.code(404).send(pulseVaultError("Video not found"));
306
- }
307
- if (resolved.kind === "redirect") {
308
- return reply.redirect(resolved.url, resolved.statusCode ?? 302);
309
- }
310
- const result = await send(request.raw, resolved.filename, {
311
- root: resolved.root,
312
- ...cache,
313
- });
314
- if (result.type === "error") {
315
- return reply
316
- .code(result.statusCode)
317
- .send(pulseVaultError(result.metadata.error.message));
318
- }
319
- for (const [name, value] of Object.entries(result.headers)) {
320
- reply.header(name, value);
321
- }
322
- return reply.code(result.statusCode).send(result.stream);
189
+ reply.hijack();
190
+ await core.handleArtifactGet(request.raw, reply.raw, artifactId, token);
323
191
  });
324
192
  };
325
193
  export default pulseVaultRoutes;
194
+ export { PROTOCOL_VERSION };
326
195
  //# sourceMappingURL=pulsevault.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"pulsevault.js","sourceRoot":"","sources":["../../src/routes/pulsevault.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"pulsevault.js","sourceRoot":"","sources":["../../src/routes/pulsevault.ts"],"names":[],"mappings":"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"}
@@ -1,34 +1,55 @@
1
- import type { PulseVaultStorage } from "./types.js";
1
+ import type { PulseVaultStorage, UploadKind } from './types.js';
2
2
  export type LocalStorageOptions = {
3
- /** Directory where per-video subdirectories are stored. Resolved against CWD if relative. */
3
+ /** Directory where uploads are stored (flat kind-scoped subdirs). Resolved against CWD if relative. */
4
4
  workspaceDir: string;
5
+ /**
6
+ * Max number of entries kept in the in-memory metadata cache before the
7
+ * oldest (by insertion order) is evicted. A cache miss falls back to
8
+ * reading the sidecar from disk, so eviction only costs an extra read, not
9
+ * correctness. Defaults to 10,000.
10
+ */
11
+ metaCacheLimit?: number;
5
12
  };
6
13
  /**
7
14
  * Local adapter storage. Layout contract (stable; downstream tools may rely
8
15
  * on it):
9
16
  *
10
17
  * ```text
11
- * <workspaceRoot>/<videoid>/
12
- * .pulsevault.json # our sidecar: { version, ext, filename, status }
13
- * video/<videoid><ext> # finalized upload bytes
14
- * video/<videoid><ext>.json # @tus/file-store's offset/metadata sidecar
18
+ * <workspaceRoot>/
19
+ * .pulsevault/<artifactId>.json # sidecar: { version, ext, filename, status, kind, relatedTo }
20
+ * video/<artifactId><ext> # finalized video bytes
21
+ * video/<artifactId><ext>.json # @tus/file-store offset/metadata sidecar
22
+ * project/<artifactId><ext> # finalized project bytes
23
+ * project/<artifactId><ext>.json # @tus/file-store offset/metadata sidecar
24
+ * captions/<artifactId><ext> # finalized captions bytes
25
+ * captions/<artifactId><ext>.json # @tus/file-store offset/metadata sidecar
15
26
  * ```
16
27
  *
17
28
  * `workspaceRoot` is exposed so consumers can layer post-processing (e.g.
18
29
  * hydrate an ArtiPod with `video/`, `transcripts/`, `frames/` mounts) against
19
30
  * the same on-disk tree from an `onUploadComplete` hook. `getLocalPath` is
20
31
  * exposed for `validatePayload` helpers that need to sniff the bytes before
21
- * the video is marked ready.
32
+ * the upload is marked ready.
22
33
  */
23
34
  export type LocalStorage = PulseVaultStorage & {
24
35
  readonly workspaceRoot: string;
25
36
  /**
26
- * Return the absolute path to the upload bytes for `videoid`, regardless
37
+ * Return the absolute path to the upload bytes for `artifactId`, regardless
27
38
  * of ready state. Falls back to reading the sidecar if the in-memory cache
28
39
  * is cold (so it works even after a server restart mid-upload). Returns
29
- * `null` if the videoid is unknown.
40
+ * `null` if the artifactId is unknown.
41
+ */
42
+ getLocalPath(artifactId: string): Promise<string | null>;
43
+ /**
44
+ * Return the artifact kind for a known artifactId without a full resolve.
45
+ * Returns `null` if the artifactId is unknown. Satisfies the optional
46
+ * `PulseVaultStorage.getKind` contract.
30
47
  */
31
- getLocalPath(videoid: string): Promise<string | null>;
48
+ getKind(artifactId: string): Promise<UploadKind | null>;
49
+ /** Satisfies the optional `PulseVaultStorage.getRelatedTo` contract. */
50
+ getRelatedTo(artifactId: string): Promise<string | null>;
51
+ /** Satisfies the optional `PulseVaultStorage.getChecksum` contract. */
52
+ getChecksum(artifactId: string): Promise<string | null>;
32
53
  };
33
54
  export declare function createLocalStorage(opts: LocalStorageOptions): LocalStorage;
34
55
  //# sourceMappingURL=local.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"local.d.ts","sourceRoot":"","sources":["../../src/storage/local.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAEV,iBAAiB,EAElB,MAAM,YAAY,CAAC;AA6BpB,MAAM,MAAM,mBAAmB,GAAG;IAChC,6FAA6F;IAC7F,YAAY,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,MAAM,YAAY,GAAG,iBAAiB,GAAG;IAC7C,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B;;;;;OAKG;IACH,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CACvD,CAAC;AAEF,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,mBAAmB,GAAG,YAAY,CAkL1E"}
1
+ {"version":3,"file":"local.d.ts","sourceRoot":"","sources":["../../src/storage/local.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAEV,iBAAiB,EAEjB,UAAU,EACX,MAAM,YAAY,CAAC;AAwEpB,MAAM,MAAM,mBAAmB,GAAG;IAChC,uGAAuG;IACvG,YAAY,EAAE,MAAM,CAAC;IACrB;;;;;OAKG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,MAAM,YAAY,GAAG,iBAAiB,GAAG;IAC7C,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B;;;;;OAKG;IACH,YAAY,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACzD;;;;OAIG;IACH,OAAO,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC;IACxD,wEAAwE;IACxE,YAAY,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACzD,uEAAuE;IACvE,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CACzD,CAAC;AAEF,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,mBAAmB,GAAG,YAAY,CAuO1E"}