@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.
- package/CHANGELOG.md +258 -0
- package/LICENSE +15 -39
- package/OPERATIONS.md +223 -0
- package/PROTOCOL.md +345 -0
- package/README.md +548 -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 +91 -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 +494 -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 +134 -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 +105 -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/errors.d.ts +7 -0
- package/dist/lib/errors.d.ts.map +1 -1
- package/dist/lib/errors.js +18 -0
- package/dist/lib/errors.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 +41 -0
- package/dist/lib/options.d.ts.map +1 -0
- package/dist/lib/options.js +111 -0
- package/dist/lib/options.js.map +1 -0
- package/dist/lib/pulsevaultTus.d.ts +53 -26
- package/dist/lib/pulsevaultTus.d.ts.map +1 -1
- package/dist/lib/pulsevaultTus.js +174 -55
- package/dist/lib/pulsevaultTus.js.map +1 -1
- package/dist/lib/request.d.ts +26 -0
- package/dist/lib/request.d.ts.map +1 -0
- package/dist/lib/request.js +22 -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 +121 -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 +205 -84
- 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 +438 -0
- package/dist/storage/s3.js.map +1 -0
- package/dist/storage/types.d.ts +80 -17
- package/dist/storage/types.d.ts.map +1 -1
- package/dist/storage/types.js +13 -1
- package/dist/storage/types.js.map +1 -1
- package/package.json +29 -7
|
@@ -1,326 +1,196 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
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:
|
|
4
|
+
type: 'object',
|
|
69
5
|
properties: {
|
|
70
|
-
ok: { type:
|
|
71
|
-
error: { type:
|
|
6
|
+
ok: { type: 'boolean' },
|
|
7
|
+
error: { type: 'string' },
|
|
72
8
|
},
|
|
73
|
-
required: [
|
|
9
|
+
required: ['ok', 'error'],
|
|
74
10
|
};
|
|
75
11
|
const tusRouteSchema = {
|
|
76
|
-
tags: [
|
|
77
|
-
summary:
|
|
78
|
-
description:
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
"- `
|
|
82
|
-
|
|
83
|
-
"
|
|
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`, `captions`, or `thumbnail`. Determines the storage subdir and which completion hooks fire.\n' +
|
|
19
|
+
" - `relatedTo` — optional UUID of another artifact this one belongs to (e.g. a merged video's captions/beat-manifest/thumbnail, or a clip belonging to a segment ordering 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:
|
|
26
|
+
400: { description: 'Invalid request.', ...pulseVaultErrorResponse },
|
|
86
27
|
403: {
|
|
87
|
-
description:
|
|
28
|
+
description: 'Authorize hook rejected the request.',
|
|
88
29
|
...pulseVaultErrorResponse,
|
|
89
30
|
},
|
|
90
31
|
},
|
|
91
32
|
};
|
|
92
|
-
const
|
|
93
|
-
tags: [
|
|
94
|
-
summary:
|
|
95
|
-
description:
|
|
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:
|
|
38
|
+
type: 'object',
|
|
98
39
|
properties: {
|
|
99
|
-
|
|
100
|
-
type:
|
|
101
|
-
format:
|
|
102
|
-
description:
|
|
40
|
+
artifactId: {
|
|
41
|
+
type: 'string',
|
|
42
|
+
format: 'uuid',
|
|
43
|
+
description: 'UUID of the upload to delete.',
|
|
103
44
|
},
|
|
104
45
|
},
|
|
105
|
-
required: [
|
|
46
|
+
required: ['artifactId'],
|
|
106
47
|
},
|
|
107
48
|
response: {
|
|
108
49
|
400: {
|
|
109
|
-
description:
|
|
50
|
+
description: '`artifactId` is not a valid UUID.',
|
|
110
51
|
...pulseVaultErrorResponse,
|
|
111
52
|
},
|
|
112
53
|
403: {
|
|
113
|
-
description:
|
|
54
|
+
description: 'Authorize hook rejected the request.',
|
|
114
55
|
...pulseVaultErrorResponse,
|
|
115
56
|
},
|
|
116
|
-
404: { description:
|
|
57
|
+
404: { description: 'Artifact not found.', ...pulseVaultErrorResponse },
|
|
117
58
|
501: {
|
|
118
|
-
description:
|
|
59
|
+
description: 'Storage adapter does not implement delete.',
|
|
119
60
|
...pulseVaultErrorResponse,
|
|
120
61
|
},
|
|
121
62
|
},
|
|
122
63
|
};
|
|
123
|
-
const
|
|
124
|
-
tags: [
|
|
125
|
-
summary:
|
|
126
|
-
description: "Resolves the `
|
|
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:
|
|
69
|
+
type: 'object',
|
|
129
70
|
properties: {
|
|
130
|
-
|
|
131
|
-
type:
|
|
132
|
-
format:
|
|
133
|
-
description:
|
|
71
|
+
artifactId: {
|
|
72
|
+
type: 'string',
|
|
73
|
+
format: 'uuid',
|
|
74
|
+
description: 'UUID returned from the upload flow.',
|
|
134
75
|
},
|
|
135
76
|
},
|
|
136
|
-
required: [
|
|
77
|
+
required: ['artifactId'],
|
|
137
78
|
},
|
|
138
79
|
querystring: {
|
|
139
|
-
type:
|
|
80
|
+
type: 'object',
|
|
140
81
|
properties: {
|
|
141
82
|
token: {
|
|
142
|
-
type:
|
|
143
|
-
description:
|
|
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:
|
|
90
|
+
description: '`artifactId` is not a valid UUID.',
|
|
150
91
|
...pulseVaultErrorResponse,
|
|
151
92
|
},
|
|
152
93
|
403: {
|
|
153
|
-
description:
|
|
94
|
+
description: 'Authorize hook rejected the request.',
|
|
154
95
|
...pulseVaultErrorResponse,
|
|
155
96
|
},
|
|
156
|
-
404: { description:
|
|
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: ['segment', '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
|
+
thumbnail: { type: 'array', items: { type: 'string' } },
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
maxUploadSize: { type: 'number' },
|
|
123
|
+
checksum: {
|
|
124
|
+
type: 'object',
|
|
125
|
+
properties: { algorithms: { type: 'array', items: { type: 'string' } } },
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
/** Coerce the raw route param to a string; an invalid UUID is rejected by the core's own check either way. */
|
|
132
|
+
function paramToString(value) {
|
|
133
|
+
return typeof value === 'string' ? value : '';
|
|
134
|
+
}
|
|
159
135
|
const pulseVaultRoutes = async (fastify, opts) => {
|
|
160
|
-
const { storage, maxUploadSize, allowedExtensions, cache, authorize, validatePayload, onUploadComplete, } = opts;
|
|
161
|
-
//
|
|
162
|
-
|
|
163
|
-
|
|
136
|
+
const { storage, maxUploadSize, allowedExtensions, uploadUnit, cache, authorize, validatePayload, onUploadComplete, onArtifactEvent, } = opts;
|
|
137
|
+
// All hook orchestration, tus glue, and artifact GET/DELETE logic lives in
|
|
138
|
+
// the framework-agnostic core (`../core.js`); this file only wires Fastify's
|
|
139
|
+
// routing/schema/lifecycle around it, so behavior can't drift from the
|
|
140
|
+
// `./core` entry point non-Fastify hosts use.
|
|
141
|
+
const core = createPulseVaultCore({
|
|
164
142
|
storage,
|
|
165
|
-
|
|
166
|
-
|
|
143
|
+
// `fastify.prefix` is `""` when the plugin is mounted at the root.
|
|
144
|
+
basePath: fastify.prefix,
|
|
145
|
+
maxUploadSize,
|
|
146
|
+
uploadUnit,
|
|
167
147
|
allowedExtensions,
|
|
148
|
+
cache,
|
|
149
|
+
authorize,
|
|
168
150
|
validatePayload,
|
|
169
151
|
onUploadComplete,
|
|
152
|
+
onArtifactEvent,
|
|
153
|
+
logger: fastify.log,
|
|
170
154
|
});
|
|
171
|
-
fastify.addContentTypeParser(
|
|
155
|
+
fastify.addContentTypeParser('application/offset+octet-stream', (_request, _payload, done) => {
|
|
172
156
|
done(null);
|
|
173
157
|
});
|
|
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
|
-
};
|
|
158
|
+
const tusMethods = ['POST', 'PATCH', 'HEAD', 'DELETE', 'OPTIONS'];
|
|
213
159
|
const tusHandler = async (request, reply) => {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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.
|
|
160
|
+
// The core writes directly to the raw response (tus dispatch, authorize
|
|
161
|
+
// rejections, error translation) — hijack so Fastify doesn't also try to
|
|
162
|
+
// manage this reply afterward.
|
|
221
163
|
reply.hijack();
|
|
222
|
-
|
|
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
|
-
}
|
|
164
|
+
await core.handleTus(request.raw, reply.raw);
|
|
235
165
|
};
|
|
236
|
-
const tusMethods = ["POST", "PATCH", "HEAD", "DELETE", "OPTIONS"];
|
|
237
166
|
fastify.route({
|
|
238
167
|
method: [...tusMethods],
|
|
239
|
-
url:
|
|
168
|
+
url: '/upload',
|
|
240
169
|
schema: tusRouteSchema,
|
|
241
170
|
handler: tusHandler,
|
|
242
171
|
});
|
|
243
172
|
fastify.route({
|
|
244
173
|
method: [...tusMethods],
|
|
245
|
-
url:
|
|
174
|
+
url: '/upload/*',
|
|
246
175
|
schema: tusRouteSchema,
|
|
247
176
|
handler: tusHandler,
|
|
248
177
|
});
|
|
249
|
-
fastify.
|
|
250
|
-
|
|
251
|
-
|
|
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();
|
|
178
|
+
fastify.get('/capabilities', { schema: capabilitiesSchema }, async (request, reply) => {
|
|
179
|
+
reply.hijack();
|
|
180
|
+
await core.handleCapabilities(request.raw, reply.raw);
|
|
278
181
|
});
|
|
279
|
-
fastify.
|
|
280
|
-
const
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
request.pulseVault = { videoid };
|
|
287
|
-
// Extract the optional token forwarded by the mobile app in the watch URL.
|
|
182
|
+
fastify.delete('/artifacts/:artifactId', { schema: artifactDeleteSchema }, async (request, reply) => {
|
|
183
|
+
const artifactId = paramToString(request.params?.artifactId);
|
|
184
|
+
reply.hijack();
|
|
185
|
+
await core.handleArtifactDelete(request.raw, reply.raw, artifactId);
|
|
186
|
+
});
|
|
187
|
+
fastify.get('/artifacts/:artifactId', { schema: artifactGetSchema }, async (request, reply) => {
|
|
188
|
+
const artifactId = paramToString(request.params?.artifactId);
|
|
288
189
|
const token = request.query?.token;
|
|
289
|
-
|
|
290
|
-
|
|
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);
|
|
190
|
+
reply.hijack();
|
|
191
|
+
await core.handleArtifactGet(request.raw, reply.raw, artifactId, token);
|
|
323
192
|
});
|
|
324
193
|
};
|
|
325
194
|
export default pulseVaultRoutes;
|
|
195
|
+
export { PROTOCOL_VERSION };
|
|
326
196
|
//# sourceMappingURL=pulsevault.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pulsevault.js","sourceRoot":"","sources":["../../src/routes/pulsevault.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"pulsevault.js","sourceRoot":"","sources":["../../src/routes/pulsevault.ts"],"names":[],"mappings":"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"}
|
package/dist/storage/local.d.ts
CHANGED
|
@@ -1,34 +1,55 @@
|
|
|
1
|
-
import type { PulseVaultStorage } from
|
|
1
|
+
import type { PulseVaultStorage, UploadKind } from './types.js';
|
|
2
2
|
export type LocalStorageOptions = {
|
|
3
|
-
/** Directory where
|
|
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
|
|
12
|
-
* .pulsevault
|
|
13
|
-
* video/<
|
|
14
|
-
* video/<
|
|
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
|
|
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 `
|
|
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
|
|
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
|
-
|
|
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":"
|
|
1
|
+
{"version":3,"file":"local.d.ts","sourceRoot":"","sources":["../../src/storage/local.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAEV,iBAAiB,EAEjB,UAAU,EACX,MAAM,YAAY,CAAC;AA8EpB,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,CAmQ1E"}
|