@nestm/storage 0.1.0-alpha.3 → 0.1.0-alpha.5

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 (92) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/README.md +200 -3
  3. package/dist/artifacts/artifact-storage.d.ts +161 -0
  4. package/dist/artifacts/artifact-storage.d.ts.map +1 -0
  5. package/dist/artifacts/artifact-storage.js +452 -0
  6. package/dist/artifacts/artifact-storage.js.map +1 -0
  7. package/dist/artifacts/crypto/codec.d.ts +44 -0
  8. package/dist/artifacts/crypto/codec.d.ts.map +1 -0
  9. package/dist/artifacts/crypto/codec.js +273 -0
  10. package/dist/artifacts/crypto/codec.js.map +1 -0
  11. package/dist/artifacts/crypto/context.d.ts +48 -0
  12. package/dist/artifacts/crypto/context.d.ts.map +1 -0
  13. package/dist/artifacts/crypto/context.js +37 -0
  14. package/dist/artifacts/crypto/context.js.map +1 -0
  15. package/dist/artifacts/crypto/dek-cache.d.ts +38 -0
  16. package/dist/artifacts/crypto/dek-cache.d.ts.map +1 -0
  17. package/dist/artifacts/crypto/dek-cache.js +137 -0
  18. package/dist/artifacts/crypto/dek-cache.js.map +1 -0
  19. package/dist/artifacts/crypto/index.d.ts +25 -0
  20. package/dist/artifacts/crypto/index.d.ts.map +1 -0
  21. package/dist/artifacts/crypto/index.js +29 -0
  22. package/dist/artifacts/crypto/index.js.map +1 -0
  23. package/dist/artifacts/crypto/key-provider.d.ts +44 -0
  24. package/dist/artifacts/crypto/key-provider.d.ts.map +1 -0
  25. package/dist/artifacts/crypto/key-provider.js +36 -0
  26. package/dist/artifacts/crypto/key-provider.js.map +1 -0
  27. package/dist/artifacts/crypto/kms-provider.d.ts +16 -0
  28. package/dist/artifacts/crypto/kms-provider.d.ts.map +1 -0
  29. package/dist/artifacts/crypto/kms-provider.js +90 -0
  30. package/dist/artifacts/crypto/kms-provider.js.map +1 -0
  31. package/dist/artifacts/crypto/local-provider.d.ts +17 -0
  32. package/dist/artifacts/crypto/local-provider.d.ts.map +1 -0
  33. package/dist/artifacts/crypto/local-provider.js +58 -0
  34. package/dist/artifacts/crypto/local-provider.js.map +1 -0
  35. package/dist/artifacts/env-config.d.ts +15 -0
  36. package/dist/artifacts/env-config.d.ts.map +1 -0
  37. package/dist/artifacts/env-config.js +95 -0
  38. package/dist/artifacts/env-config.js.map +1 -0
  39. package/dist/artifacts/index.d.ts +6 -0
  40. package/dist/artifacts/index.d.ts.map +1 -0
  41. package/dist/artifacts/index.js +6 -0
  42. package/dist/artifacts/index.js.map +1 -0
  43. package/dist/artifacts/nest/index.d.ts +29 -0
  44. package/dist/artifacts/nest/index.d.ts.map +1 -0
  45. package/dist/artifacts/nest/index.js +129 -0
  46. package/dist/artifacts/nest/index.js.map +1 -0
  47. package/dist/artifacts/object-store.d.ts +27 -0
  48. package/dist/artifacts/object-store.d.ts.map +1 -0
  49. package/dist/artifacts/object-store.js +198 -0
  50. package/dist/artifacts/object-store.js.map +1 -0
  51. package/dist/artifacts/storage-driver.d.ts +48 -0
  52. package/dist/artifacts/storage-driver.d.ts.map +1 -0
  53. package/dist/artifacts/storage-driver.js +89 -0
  54. package/dist/artifacts/storage-driver.js.map +1 -0
  55. package/dist/files-sdk/files-sdk.driver.d.ts +18 -18
  56. package/dist/files-sdk/files-sdk.driver.d.ts.map +1 -1
  57. package/dist/files-sdk/fs/index.d.ts +21 -0
  58. package/dist/files-sdk/fs/index.d.ts.map +1 -0
  59. package/dist/files-sdk/fs/index.js +20 -0
  60. package/dist/files-sdk/fs/index.js.map +1 -0
  61. package/dist/files-sdk/provider/index.d.ts +54 -0
  62. package/dist/files-sdk/provider/index.d.ts.map +1 -0
  63. package/dist/files-sdk/provider/index.js +80 -0
  64. package/dist/files-sdk/provider/index.js.map +1 -0
  65. package/dist/files-sdk/s3/index.d.ts +13 -2
  66. package/dist/files-sdk/s3/index.d.ts.map +1 -1
  67. package/dist/files-sdk/s3/index.js +19 -8
  68. package/dist/files-sdk/s3/index.js.map +1 -1
  69. package/dist/gateway/storage-gateway-fastify-parser.d.ts.map +1 -1
  70. package/dist/gateway/storage-gateway-fastify-parser.js.map +1 -1
  71. package/dist/gateway/storage-gateway.controller.d.ts.map +1 -1
  72. package/dist/gateway/storage-gateway.controller.js.map +1 -1
  73. package/dist/gateway/storage-gateway.guard.d.ts.map +1 -1
  74. package/dist/gateway/storage-gateway.guard.js.map +1 -1
  75. package/dist/gateway/storage-gateway.module.d.ts.map +1 -1
  76. package/dist/gateway/storage-gateway.module.js.map +1 -1
  77. package/dist/gateway/storage-gateway.types.d.ts +10 -10
  78. package/dist/gateway/storage-gateway.types.d.ts.map +1 -1
  79. package/dist/inject-storage.decorator.js.map +1 -1
  80. package/dist/storage-upload-control.d.ts.map +1 -1
  81. package/dist/storage.client.d.ts.map +1 -1
  82. package/dist/storage.client.js.map +1 -1
  83. package/dist/storage.error.d.ts +10 -10
  84. package/dist/storage.error.d.ts.map +1 -1
  85. package/dist/storage.error.js.map +1 -1
  86. package/dist/storage.module.d.ts.map +1 -1
  87. package/dist/storage.module.js.map +1 -1
  88. package/dist/storage.service.d.ts.map +1 -1
  89. package/dist/storage.service.js.map +1 -1
  90. package/dist/storage.tokens.js.map +1 -1
  91. package/dist/testing/index.js.map +1 -1
  92. package/package.json +34 -10
@@ -0,0 +1,452 @@
1
+ import { StorageErrorCode, isStorageError, } from '../core/index.js';
2
+ import AdmZip from 'adm-zip';
3
+ import { extname } from 'node:path';
4
+ import { ENVELOPE_MAX_OVERHEAD_BYTES, open, seal, SCOPE_FROM_HEADER, } from './crypto/index.js';
5
+ import { createArtifactStorageClient, DEFAULT_OBJECT_NAMESPACES, resolveObjectNamespaces, } from './storage-driver.js';
6
+ export { assertStorageProvider, defaultFsRoot, } from './storage-driver.js';
7
+ /** Permanent, address-independent read rejection safe for callers to map to a bounded 4xx. */
8
+ export class ArtifactReadLimitError extends Error {
9
+ maxPlainBytes;
10
+ constructor(maxPlainBytes) {
11
+ super(`artifact plaintext exceeds the ${maxPlainBytes} byte read limit`);
12
+ this.maxPlainBytes = maxPlainBytes;
13
+ this.name = 'ArtifactReadLimitError';
14
+ }
15
+ }
16
+ /** Permanent rejection for a ZIP whose declared expansion exceeds the public protocol limits. */
17
+ export class ArtifactBundleLimitError extends Error {
18
+ reason;
19
+ constructor(reason) {
20
+ super(`artifact bundle exceeds the ${reason} limit`);
21
+ this.reason = reason;
22
+ this.name = 'ArtifactBundleLimitError';
23
+ }
24
+ }
25
+ export const ARTIFACT_BUNDLE_MAX_ENTRIES = 1_000;
26
+ export const ARTIFACT_BUNDLE_MAX_ENTRY_BYTES = 64 * 1024 * 1024;
27
+ export const ARTIFACT_BUNDLE_MAX_TOTAL_BYTES = 256 * 1024 * 1024;
28
+ /** Reserved storage path for the generated PNG preview, served by the sandbox as `<id>/__thumb.png`. */
29
+ export const THUMBNAIL_KEY = '__thumb.png';
30
+ export async function createArtifactStorage(cfg, crypto) {
31
+ return createArtifactStorageWithClient(cfg, crypto, await createArtifactStorageClient(cfg));
32
+ }
33
+ /** Framework-neutral domain adapter used by both direct consumers and the Nest composition entry. */
34
+ export function createArtifactStorageWithClient(cfg, crypto, client) {
35
+ return new ClientArtifactStorage(crypto, client, resolveObjectNamespaces(cfg.objectNamespaces ?? DEFAULT_OBJECT_NAMESPACES), cfg.provider === 'fs');
36
+ }
37
+ /**
38
+ * Storage path prefix for a version's objects (AP-002): `""` for a backfilled `canonical_v1`
39
+ * version (its bytes ARE the canonical root objects) and `v/<versionId>/` for a `versioned` one.
40
+ */
41
+ export function versionPrefix(layout, versionId) {
42
+ assertVersionId(versionId);
43
+ return layout === 'canonical_v1' ? '' : `v/${versionId}/`;
44
+ }
45
+ /**
46
+ * Envelope `ctx.version` for a version's objects (AP-002): `null` for `canonical_v1` (root AAD —
47
+ * the serving plane is version-free forever) and the version UUID, byte-identical to the path
48
+ * segment, for `versioned`.
49
+ */
50
+ export function versionAad(layout, versionId) {
51
+ assertVersionId(versionId);
52
+ return layout === 'canonical_v1' ? null : versionId;
53
+ }
54
+ /** Artifact ids are one storage segment; `_objects` is the filesystem object-store namespace. */
55
+ export function assertArtifactId(artifactId) {
56
+ assertStorageSegment(artifactId, 'artifact id');
57
+ if (artifactId.toLowerCase() === '_objects') {
58
+ throw new Error('storage: artifact id "_objects" is reserved');
59
+ }
60
+ }
61
+ /** Version ids are one storage segment because they are interpolated under `v/<id>/`. */
62
+ export function assertVersionId(versionId) {
63
+ assertStorageSegment(versionId, 'version id');
64
+ }
65
+ function assertStorageSegment(value, label) {
66
+ if (value === '' ||
67
+ value === '.' ||
68
+ value === '..' ||
69
+ value.includes('/') ||
70
+ value.includes('\\') ||
71
+ value.includes('\0') ||
72
+ Buffer.byteLength(value, 'utf8') > 255) {
73
+ throw new Error(`storage: invalid ${label} ${JSON.stringify(value)}`);
74
+ }
75
+ }
76
+ function normalizeRelativePath(path) {
77
+ return path.replace(/\\/g, '/');
78
+ }
79
+ function isUnsafeRelativePath(path, allowEmpty = false, allowTrailingSlash = false) {
80
+ if (path === '')
81
+ return !allowEmpty;
82
+ if (path.startsWith('/') || path.includes('\0')) {
83
+ return true;
84
+ }
85
+ const segments = path.split('/');
86
+ if (allowTrailingSlash && segments.at(-1) === '')
87
+ segments.pop();
88
+ return segments.some((segment) => segment === '' || segment === '.' || segment === '..');
89
+ }
90
+ /**
91
+ * Is this artifact-relative path inside the namespaces VER-02 reserves for the platform — the
92
+ * `v/` version prefix and the `__`-internal families (meta/thumb/source/snapshot/overflow) —
93
+ * with the one serving exemption, top-level single-segment `__vendor/<name>` pool assets?
94
+ * User-supplied bundle entries matching this are SKIPPED at write time (writeBundle), exactly
95
+ * like zip-slip entries: the sandbox deny-guard will never serve them, and letting them through
96
+ * to the seal chokepoint would turn a hostile/quirky zip into a mid-write throw.
97
+ */
98
+ export function isReservedArtifactPath(relPath) {
99
+ const segments = relPath.split('/');
100
+ if (segments[0]?.toLowerCase() === 'v')
101
+ return true;
102
+ for (let i = 0; i < segments.length; i++) {
103
+ const seg = segments[i];
104
+ if (seg === undefined || !seg.startsWith('__'))
105
+ continue;
106
+ if (i === 0 &&
107
+ seg === '__vendor' &&
108
+ segments.length === 2 &&
109
+ segments[1] !== '')
110
+ continue;
111
+ return true;
112
+ }
113
+ return false;
114
+ }
115
+ /**
116
+ * AP-002 storage rule 2: `path.startsWith("v/" + version + "/") ⇔ version !== null`. Root objects
117
+ * never live under the reserved `v/` namespace and versioned objects never live anywhere else —
118
+ * asserted at the single ctx chokepoint (covers reads and writes, both backends), so a
119
+ * mis-addressed artifact object is impossible rather than unlikely. Throws (fail-closed): this is
120
+ * a programming error, never user input — every USER-named path (zip entries) is screened by
121
+ * `isReservedArtifactPath` before it can reach this assertion.
122
+ */
123
+ export function assertVersionPath(path, version) {
124
+ if (isUnsafeRelativePath(path)) {
125
+ throw new Error(`storage: invalid artifact path ${JSON.stringify(path)}`);
126
+ }
127
+ if (version !== null) {
128
+ assertVersionId(version);
129
+ if (!path.startsWith(`v/${version}/`)) {
130
+ throw new Error(`storage: versioned object path "${path}" must live under "v/${version}/" (AP-002 rule 2)`);
131
+ }
132
+ }
133
+ else if (path.split('/', 1)[0]?.toLowerCase() === 'v') {
134
+ throw new Error(`storage: root object path "${path}" is inside the reserved v/ namespace (AP-002 rule 2)`);
135
+ }
136
+ }
137
+ function bundleLimit(requested, hardLimit, name) {
138
+ const value = requested ?? hardLimit;
139
+ if (!Number.isSafeInteger(value) || value < 1 || value > hardLimit) {
140
+ throw new RangeError(`${name} must be an integer between 1 and ${hardLimit}`);
141
+ }
142
+ return value;
143
+ }
144
+ function prepareBundle(zipBuffer, limits) {
145
+ const maxEntries = bundleLimit(limits?.maxEntries, ARTIFACT_BUNDLE_MAX_ENTRIES, 'maxEntries');
146
+ const maxEntryBytes = bundleLimit(limits?.maxEntryBytes, ARTIFACT_BUNDLE_MAX_ENTRY_BYTES, 'maxEntryBytes');
147
+ const maxTotalBytes = bundleLimit(limits?.maxTotalBytes, ARTIFACT_BUNDLE_MAX_TOTAL_BYTES, 'maxTotalBytes');
148
+ const entries = new AdmZip(zipBuffer).getEntries();
149
+ if (entries.length > maxEntries) {
150
+ throw new ArtifactBundleLimitError('entries');
151
+ }
152
+ const accepted = [];
153
+ const names = new Set();
154
+ let declaredTotal = 0;
155
+ for (const entry of entries) {
156
+ if (entry.isDirectory)
157
+ continue;
158
+ const name = normalizeRelativePath(entry.entryName).replace(/^\/+/, '');
159
+ if (isUnsafeRelativePath(name) || isReservedArtifactPath(name)) {
160
+ continue;
161
+ }
162
+ const portableName = name.toLowerCase();
163
+ if (names.has(portableName)) {
164
+ throw new Error(`artifact bundle contains duplicate entry "${name}"`);
165
+ }
166
+ names.add(portableName);
167
+ const declaredSize = entry.header.size;
168
+ if (!Number.isSafeInteger(declaredSize) ||
169
+ declaredSize < 0 ||
170
+ declaredSize > maxEntryBytes) {
171
+ throw new ArtifactBundleLimitError('entry-bytes');
172
+ }
173
+ declaredTotal += declaredSize;
174
+ if (!Number.isSafeInteger(declaredTotal) || declaredTotal > maxTotalBytes) {
175
+ throw new ArtifactBundleLimitError('total-bytes');
176
+ }
177
+ accepted.push({ entry, name });
178
+ }
179
+ // Inflate and validate every accepted entry before the first storage write. This bounds peak
180
+ // memory by maxTotalBytes and prevents a late malformed/oversized entry from leaving a partial
181
+ // bundle behind.
182
+ const prepared = [];
183
+ let actualTotal = 0;
184
+ for (const { entry, name } of accepted) {
185
+ const body = entry.getData();
186
+ if (body.length > maxEntryBytes) {
187
+ throw new ArtifactBundleLimitError('entry-bytes');
188
+ }
189
+ actualTotal += body.length;
190
+ if (!Number.isSafeInteger(actualTotal) || actualTotal > maxTotalBytes) {
191
+ throw new ArtifactBundleLimitError('total-bytes');
192
+ }
193
+ prepared.push({ name, body });
194
+ }
195
+ return prepared;
196
+ }
197
+ /** Envelope context for an artifact-relative path. */
198
+ function artifactCtx(artifactId, path, ref) {
199
+ assertArtifactId(artifactId);
200
+ const version = ref.version ?? null;
201
+ assertVersionPath(path, version);
202
+ return { scope: ref.scope, artifactId, version, path };
203
+ }
204
+ /** Structured, greppable trace of a legacy plaintext pass-through (SEC-04 migration window). */
205
+ export function warnLegacyRead(where, key) {
206
+ // `console` is deliberate: this package is consumed by apps/sandbox as well as the Nest API,
207
+ // so a framework logger is not available here. It is the only portable sink.
208
+ // eslint-disable-next-line no-console
209
+ console.warn(`[storage] legacy plaintext read (${where}): ${key} — pending SEC-04 backfill`);
210
+ }
211
+ // ── storage-client backend ───────────────────────────────────────────────────
212
+ /**
213
+ * The one implementation, for every provider. It reaches storage only through `StorageClient`,
214
+ * so filesystem, S3, GCS, Azure, R2 and the rest differ by driver, not by code path here. The
215
+ * filesystem once had a parallel implementation that resolved real paths and probed with
216
+ * `existsSync`; it is gone, and with it the chance of the two drifting on reserved namespaces,
217
+ * version prefixes, or traversal guards.
218
+ */
219
+ class ClientArtifactStorage {
220
+ crypto;
221
+ client;
222
+ objectNamespaces;
223
+ filesystemLayout;
224
+ constructor(crypto, client, objectNamespaces, filesystemLayout) {
225
+ this.crypto = crypto;
226
+ this.client = client;
227
+ this.objectNamespaces = objectNamespaces;
228
+ this.filesystemLayout = filesystemLayout;
229
+ }
230
+ assertArtifactId(artifactId) {
231
+ assertArtifactId(artifactId);
232
+ if (this.objectNamespaces.has(artifactId.toLowerCase())) {
233
+ throw new Error(`storage: artifact id ${JSON.stringify(artifactId)} is a reserved object namespace`);
234
+ }
235
+ }
236
+ async writeHtml(artifactId, buffer, ref) {
237
+ await this.put(artifactId, 'index.html', buffer, ref, 'text/html; charset=utf-8');
238
+ }
239
+ async writeBundle(artifactId, zipBuffer, ref, opts) {
240
+ this.assertArtifactId(artifactId);
241
+ // Versioned mode (VER-03): mirror the fs backend — prefix + derived ctx.version, entry-name
242
+ // screening unchanged.
243
+ const versionId = opts?.versionId;
244
+ if (versionId !== undefined)
245
+ assertVersionId(versionId);
246
+ const prefix = versionId === undefined ? '' : `v/${versionId}/`;
247
+ const effRef = versionId === undefined ? ref : { ...ref, version: versionId };
248
+ const entries = prepareBundle(zipBuffer, opts?.limits);
249
+ for (const { name, body } of entries) {
250
+ // `__vendor/<name>` stays ROOT-scoped in versioned mode — see the fs backend.
251
+ if (prefix !== '' && /^__vendor\/[^/]+$/.test(name)) {
252
+ await this.put(artifactId, name, body, { scope: ref.scope }, contentTypeFor(name));
253
+ continue;
254
+ }
255
+ await this.put(artifactId, prefix + name, body, effRef, contentTypeFor(name));
256
+ }
257
+ }
258
+ async writeFile(artifactId, relPath, buffer, ref, contentType) {
259
+ const name = normalizeRelativePath(relPath);
260
+ // containment guard at the key level: reject absolute paths, empty, or any traversal segment.
261
+ if (isUnsafeRelativePath(name)) {
262
+ throw new Error(`writeFile: path escapes artifact directory: ${relPath}`);
263
+ }
264
+ await this.put(artifactId, name, buffer, ref, contentType ?? contentTypeFor(name));
265
+ }
266
+ async writeThumbnail(artifactId, pngBuffer, ref) {
267
+ await this.put(artifactId, THUMBNAIL_KEY, pngBuffer, ref, 'image/png');
268
+ }
269
+ async remove(artifactId) {
270
+ this.assertArtifactId(artifactId);
271
+ for await (const object of this.client.listAll({
272
+ prefix: `${artifactId}/`,
273
+ })) {
274
+ await this.client.delete(object.key);
275
+ }
276
+ }
277
+ async listFiles(artifactId, subPrefix = '') {
278
+ this.assertArtifactId(artifactId);
279
+ const normalizedPrefix = normalizeRelativePath(subPrefix);
280
+ if (isUnsafeRelativePath(normalizedPrefix, true, true)) {
281
+ throw new Error(`storage: invalid artifact list prefix ${JSON.stringify(subPrefix)}`);
282
+ }
283
+ const base = `${artifactId}/`;
284
+ const out = [];
285
+ for await (const object of this.client.listAll({
286
+ prefix: `${base}${normalizedPrefix}`,
287
+ })) {
288
+ if (!object.key.startsWith(base))
289
+ continue;
290
+ out.push({
291
+ path: object.key.slice(base.length),
292
+ lastModified: object.lastModified ?? new Date(0),
293
+ });
294
+ }
295
+ return out.sort((a, b) => (a.path < b.path ? -1 : 1));
296
+ }
297
+ async read(artifactId, relPath, ref, options) {
298
+ const out = await this.readWithInfo(artifactId, relPath, ref, options);
299
+ return out === null ? null : out.plain;
300
+ }
301
+ /**
302
+ * Raw bytes for one artifact-relative path. Reports a plain miss and a provider failure
303
+ * separately so the caller can retry a directory-shaped key without ever turning a genuine
304
+ * backend failure into a 404.
305
+ */
306
+ async fetchRaw(artifactId, rel, options, preflight = true) {
307
+ const key = `${artifactId}/${rel}`;
308
+ if (preflight && !(await preflightRawRead(this.client, key, options)))
309
+ return { kind: 'miss' };
310
+ try {
311
+ const bytes = await this.client.downloadBytes(key, {
312
+ maxBytes: rawReadLimit(options),
313
+ });
314
+ return { kind: 'hit', raw: Buffer.from(bytes), rel };
315
+ }
316
+ catch (error) {
317
+ if (isStorageError(error) && error.code === StorageErrorCode.NOT_FOUND) {
318
+ return { kind: 'miss' };
319
+ }
320
+ // A read limit is the caller's own bound, not a backend condition: it must surface as
321
+ // itself rather than as a miss the index.html rewrite would paper over.
322
+ if (isStorageError(error) &&
323
+ error.code === StorageErrorCode.LIMIT_EXCEEDED) {
324
+ throw new ArtifactReadLimitError(requirePlainReadLimit(options));
325
+ }
326
+ return { kind: 'error', error };
327
+ }
328
+ }
329
+ async readWithInfo(artifactId, relPath, ref, options) {
330
+ this.assertArtifactId(artifactId);
331
+ const normalized = normalizeRelativePath(relPath)
332
+ .replace(/\/{2,}/g, '/')
333
+ .replace(/^\/+|\/+$/g, '');
334
+ const rel = normalized === '' ? 'index.html' : normalized;
335
+ if (isUnsafeRelativePath(rel))
336
+ return null;
337
+ // A directory-shaped request ("" or "docs") serves that directory's index.html. Providers
338
+ // disagree on how a directory key fails — an object store has no such key at all, while the
339
+ // filesystem reports reading a directory as its own error — so the rewrite is driven by the
340
+ // miss, not by probing the backend. The AAD binds to the path actually read, never to the
341
+ // one requested (ADR D6.1).
342
+ // A filesystem HEAD cannot distinguish a file from a directory candidate. Skip that one
343
+ // preflight so a directory inode size cannot trigger the caller's plaintext limit before we
344
+ // try its bounded `index.html`; downloadBytes still enforces the raw-byte ceiling.
345
+ const direct = await this.fetchRaw(artifactId, rel, options, !this.filesystemLayout);
346
+ if (direct.kind === 'error' && !this.filesystemLayout) {
347
+ throw direct.error;
348
+ }
349
+ let resolved = direct;
350
+ if (resolved.kind !== 'hit') {
351
+ const fallback = await this.fetchRaw(artifactId, `${rel}/index.html`, options);
352
+ if (fallback.kind === 'error')
353
+ throw fallback.error;
354
+ if (fallback.kind === 'miss') {
355
+ if (direct.kind === 'error')
356
+ throw direct.error;
357
+ return null;
358
+ }
359
+ resolved = fallback;
360
+ }
361
+ const out = await open(resolved.raw, artifactCtx(artifactId, resolved.rel, ref), this.crypto.keyProvider, this.crypto.allowLegacyPlaintext === undefined
362
+ ? {}
363
+ : { allowLegacyPlaintext: this.crypto.allowLegacyPlaintext });
364
+ enforcePlainReadLimit(out.plain, options);
365
+ if (out.legacy)
366
+ warnLegacyRead('storage', `${artifactId}/${resolved.rel}`);
367
+ return {
368
+ plain: out.plain,
369
+ ...(out.contentType === undefined
370
+ ? {}
371
+ : { contentType: out.contentType }),
372
+ };
373
+ }
374
+ async deleteFiles(artifactId, relPaths) {
375
+ this.assertArtifactId(artifactId);
376
+ for (const relPath of relPaths) {
377
+ const name = normalizeRelativePath(relPath);
378
+ // containment at the key level: reject absolute/empty paths and traversal segments.
379
+ if (isUnsafeRelativePath(name)) {
380
+ continue;
381
+ }
382
+ await this.client.delete(`${artifactId}/${name}`);
383
+ }
384
+ }
385
+ /** The single s3 write primitive; enveloped objects are opaque octet streams to the bucket. */
386
+ async put(artifactId, relPath, body, ref, contentType) {
387
+ this.assertArtifactId(artifactId);
388
+ const sealed = await seal(body, artifactCtx(artifactId, relPath, ref), this.crypto.keyProvider, contentType);
389
+ await this.client.upload(`${artifactId}/${relPath}`, sealed, {
390
+ // The real content type is authenticated inside CAE1 and must not leak through S3 metadata.
391
+ contentType: 'application/octet-stream',
392
+ });
393
+ }
394
+ }
395
+ function requirePlainReadLimit(options) {
396
+ const limit = options?.maxPlainBytes;
397
+ if (limit === undefined || !Number.isSafeInteger(limit) || limit < 0) {
398
+ throw new Error('maxPlainBytes must be a non-negative safe integer');
399
+ }
400
+ return limit;
401
+ }
402
+ function rawReadLimit(options) {
403
+ if (options?.maxPlainBytes === undefined)
404
+ return Infinity;
405
+ return requirePlainReadLimit(options) + ENVELOPE_MAX_OVERHEAD_BYTES;
406
+ }
407
+ async function preflightRawRead(client, key, options) {
408
+ if (options?.maxPlainBytes === undefined)
409
+ return true;
410
+ try {
411
+ const metadata = await client.head(key);
412
+ if (metadata.size > rawReadLimit(options)) {
413
+ throw new ArtifactReadLimitError(requirePlainReadLimit(options));
414
+ }
415
+ return true;
416
+ }
417
+ catch (error) {
418
+ if (isStorageError(error) && error.code === StorageErrorCode.NOT_FOUND)
419
+ return false;
420
+ throw error;
421
+ }
422
+ }
423
+ function enforcePlainReadLimit(plain, options) {
424
+ if (options?.maxPlainBytes === undefined)
425
+ return;
426
+ const limit = requirePlainReadLimit(options);
427
+ if (plain.length > limit)
428
+ throw new ArtifactReadLimitError(limit);
429
+ }
430
+ const MIME = {
431
+ '.html': 'text/html; charset=utf-8',
432
+ '.js': 'text/javascript; charset=utf-8',
433
+ '.mjs': 'text/javascript; charset=utf-8',
434
+ '.css': 'text/css; charset=utf-8',
435
+ '.json': 'application/json; charset=utf-8',
436
+ '.svg': 'image/svg+xml',
437
+ '.png': 'image/png',
438
+ '.jpg': 'image/jpeg',
439
+ '.jpeg': 'image/jpeg',
440
+ '.gif': 'image/gif',
441
+ '.webp': 'image/webp',
442
+ '.ico': 'image/x-icon',
443
+ '.woff': 'font/woff',
444
+ '.woff2': 'font/woff2',
445
+ '.map': 'application/json; charset=utf-8',
446
+ '.wasm': 'application/wasm',
447
+ };
448
+ /** content-type for an artifact file path, falling back to octet-stream. */
449
+ export function contentTypeFor(path) {
450
+ return MIME[extname(path).toLowerCase()] ?? 'application/octet-stream';
451
+ }
452
+ //# sourceMappingURL=artifact-storage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"artifact-storage.js","sourceRoot":"","sources":["../../src/artifacts/artifact-storage.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n StorageErrorCode,\n isStorageError,\n type StorageClient,\n} from '../core/index.js';\nimport AdmZip from 'adm-zip';\nimport { extname } from 'node:path';\n\nimport {\n ENVELOPE_MAX_OVERHEAD_BYTES,\n open,\n seal,\n SCOPE_FROM_HEADER,\n type EnvelopeContext,\n type StorageCrypto,\n} from './crypto/index.js';\nimport {\n createArtifactStorageClient,\n DEFAULT_OBJECT_NAMESPACES,\n resolveObjectNamespaces,\n type ArtifactStorageConfig,\n} from './storage-driver.js';\n\nexport {\n assertStorageProvider,\n defaultFsRoot,\n type ArtifactStorageConfig,\n} from './storage-driver.js';\n\n/**\n * Storage for uploaded artifact files, keyed `<artifactId>/<path>` (e.g. `<id>/index.html`,\n * `<id>/assets/app.js`). The api writes; the sandbox reads. The `fs` provider keeps both pointed\n * at one directory (fine for a single host / shared volume); any object-store provider — S3, GCS,\n * Azure, R2, and the rest — lets the two services share storage without a shared volume, which is\n * what running them on separate hosts (as on ECS) requires.\n *\n * Every object is stored as a CAE1 encryption envelope (ADR-0001, SEC-03): callers must say who\n * owns what they write (`ScopeRef`) and what they expect to read (`ReadRef`) — the codec binds\n * ciphertext to `{scope, artifactId, version, path}` so a swapped or replayed object fails closed.\n */\nexport interface ArtifactStorage {\n /** Single self-contained HTML artifact → `<id>/index.html`. */\n writeHtml(artifactId: string, buffer: Buffer, ref: ScopeRef): Promise<void>;\n /**\n * Pre-built SPA bundle (a zip whose root contains index.html). Encrypted per extracted entry.\n * With `opts.versionId` (VER-03), entries land under `v/<versionId>/…` and are sealed with\n * `ctx.version = versionId` (the caller's `ref.version` is overridden to match — the path⇔version\n * biconditional is derived, never trusted). Reserved-namespace screening applies to the zip\n * ENTRY name in both modes: a bundle shipping `v/…` or `__…` files has those skipped, not nested.\n */\n writeBundle(\n artifactId: string,\n zipBuffer: Buffer,\n ref: ScopeRef,\n opts?: WriteBundleOptions,\n ): Promise<void>;\n /**\n * Write an arbitrary file within the artifact (e.g. `__vendor/<hash>.js`, `__meta.json`).\n * Rejects any `relPath` escaping the artifact directory. `contentType` rides the envelope\n * header (authenticated) on both backends.\n */\n writeFile(\n artifactId: string,\n relPath: string,\n buffer: Buffer,\n ref: ScopeRef,\n contentType?: string,\n ): Promise<void>;\n /** Server-rendered PNG preview → reserved key `<id>/__thumb.png`. */\n writeThumbnail(\n artifactId: string,\n pngBuffer: Buffer,\n ref: ScopeRef,\n ): Promise<void>;\n /** Delete every file under the artifact (includes the thumbnail). */\n remove(artifactId: string): Promise<void>;\n /**\n * Read a file within the artifact; `relPath === \"\"` resolves to index.html. `null` if absent.\n * Throws `EnvelopeError` on any decrypt failure — a corrupt or swapped object must never be\n * mistaken for a missing one (no SPA fallback over tampering).\n */\n read(\n artifactId: string,\n relPath: string,\n ref: ReadRef,\n options?: ArtifactReadOptions,\n ): Promise<Buffer | null>;\n /**\n * Like `read`, but also returns the envelope's AUTHENTICATED content type (VER-03). This is the\n * read primitive for publish/CoW copy loops: a decrypt→re-encrypt copy must re-seal with the\n * same `ct` it opened (the type is AAD-bound and steers serving), and `read()` discards it.\n * `contentType` is undefined only for legacy plaintext pass-throughs (nothing was verified).\n */\n readWithInfo(\n artifactId: string,\n relPath: string,\n ref: ReadRef,\n options?: ArtifactReadOptions,\n ): Promise<StoredObject | null>;\n /**\n * Delete specific files under the artifact (VER-03: publish prunes root paths removed by the\n * newly-materialized version). Paths escaping the artifact directory and missing paths are\n * ignored (idempotent — publish repair re-runs the same prune).\n */\n deleteFiles(artifactId: string, relPaths: string[]): Promise<void>;\n /**\n * List stored objects under the artifact (optionally under `subPrefix`, e.g. `v/<versionId>/`).\n * Returns artifact-relative forward-slash paths + last-modified stamps, sorted by path. Reads\n * no object bodies and needs no crypto — it serves the sweepers/invariants (version-prefix\n * existence, orphan-prefix age, canonical bundle enumeration), never the serving plane.\n */\n listFiles(artifactId: string, subPrefix?: string): Promise<StoredFileInfo[]>;\n}\n\n/** One listed storage object (metadata only — the body stays sealed). */\nexport interface StoredFileInfo {\n /** Artifact-relative path, forward slashes (e.g. `index.html`, `v/<vid>/index.html`). */\n path: string;\n lastModified: Date;\n}\n\n/** A decrypted object plus the authenticated content type it was sealed with (see readWithInfo). */\nexport interface StoredObject {\n plain: Buffer;\n /** From the envelope header (`ct`, AAD-bound); undefined for legacy plaintext pass-throughs. */\n contentType?: string;\n}\n\n/** Bounds decrypted data while also bounding the opaque envelope downloaded before decryption. */\nexport interface ArtifactReadOptions {\n readonly maxPlainBytes?: number;\n}\n\n/** Permanent, address-independent read rejection safe for callers to map to a bounded 4xx. */\nexport class ArtifactReadLimitError extends Error {\n constructor(readonly maxPlainBytes: number) {\n super(`artifact plaintext exceeds the ${maxPlainBytes} byte read limit`);\n this.name = 'ArtifactReadLimitError';\n }\n}\n\n/** Permanent rejection for a ZIP whose declared expansion exceeds the public protocol limits. */\nexport class ArtifactBundleLimitError extends Error {\n constructor(readonly reason: 'entries' | 'entry-bytes' | 'total-bytes') {\n super(`artifact bundle exceeds the ${reason} limit`);\n this.name = 'ArtifactBundleLimitError';\n }\n}\n\nexport const ARTIFACT_BUNDLE_MAX_ENTRIES = 1_000;\nexport const ARTIFACT_BUNDLE_MAX_ENTRY_BYTES = 64 * 1024 * 1024;\nexport const ARTIFACT_BUNDLE_MAX_TOTAL_BYTES = 256 * 1024 * 1024;\n\n/** Reserved storage path for the generated PNG preview, served by the sandbox as `<id>/__thumb.png`. */\nexport const THUMBNAIL_KEY = '__thumb.png';\n\n/** Options for writeBundle (VER-03 versioned bundle writes). */\nexport interface WriteBundleOptions {\n /** Write entries under `v/<versionId>/…` with `ctx.version = versionId` instead of the root. */\n versionId?: string;\n /** Optional stricter limits; values may lower, but never raise, the protocol hard limits. */\n limits?: {\n maxEntries?: number;\n maxEntryBytes?: number;\n maxTotalBytes?: number;\n };\n}\n\n/** Who owns an object being written; `version` stays null until VER-02 introduces versions. */\nexport interface ScopeRef {\n /** `org:<organizationId>` | `user:<ownerUserId>` | `upload:<uploadId>` — see artifactScope(). */\n scope: string;\n version?: string | null;\n}\n\n/**\n * What the reader independently knows. The api asserts the scope it fetched from the DB; the\n * sandbox — which only knows the artifactId — passes SCOPE_FROM_HEADER and relies on the wrap\n * layer to enforce scope cryptographically (see crypto/context.ts).\n */\nexport interface ReadRef {\n // `string` absorbs the literal for the checker, but the sentinel is domain vocabulary\n // here — collapsing the union would erase what @from-header means to a reader.\n // eslint-disable-next-line typescript/no-redundant-type-constituents\n scope: string | typeof SCOPE_FROM_HEADER;\n version?: string | null;\n}\n\nexport async function createArtifactStorage(\n cfg: ArtifactStorageConfig,\n crypto: StorageCrypto,\n): Promise<ArtifactStorage> {\n return createArtifactStorageWithClient(\n cfg,\n crypto,\n await createArtifactStorageClient(cfg),\n );\n}\n\n/** Framework-neutral domain adapter used by both direct consumers and the Nest composition entry. */\nexport function createArtifactStorageWithClient(\n cfg: ArtifactStorageConfig,\n crypto: StorageCrypto,\n client: StorageClient,\n): ArtifactStorage {\n return new ClientArtifactStorage(\n crypto,\n client,\n resolveObjectNamespaces(cfg.objectNamespaces ?? DEFAULT_OBJECT_NAMESPACES),\n cfg.provider === 'fs',\n );\n}\n\n/**\n * Storage path prefix for a version's objects (AP-002): `\"\"` for a backfilled `canonical_v1`\n * version (its bytes ARE the canonical root objects) and `v/<versionId>/` for a `versioned` one.\n */\nexport function versionPrefix(\n layout: 'canonical_v1' | 'versioned',\n versionId: string,\n): string {\n assertVersionId(versionId);\n return layout === 'canonical_v1' ? '' : `v/${versionId}/`;\n}\n\n/**\n * Envelope `ctx.version` for a version's objects (AP-002): `null` for `canonical_v1` (root AAD —\n * the serving plane is version-free forever) and the version UUID, byte-identical to the path\n * segment, for `versioned`.\n */\nexport function versionAad(\n layout: 'canonical_v1' | 'versioned',\n versionId: string,\n): string | null {\n assertVersionId(versionId);\n return layout === 'canonical_v1' ? null : versionId;\n}\n\n/** Artifact ids are one storage segment; `_objects` is the filesystem object-store namespace. */\nexport function assertArtifactId(artifactId: string): void {\n assertStorageSegment(artifactId, 'artifact id');\n if (artifactId.toLowerCase() === '_objects') {\n throw new Error('storage: artifact id \"_objects\" is reserved');\n }\n}\n\n/** Version ids are one storage segment because they are interpolated under `v/<id>/`. */\nexport function assertVersionId(versionId: string): void {\n assertStorageSegment(versionId, 'version id');\n}\n\nfunction assertStorageSegment(value: string, label: string): void {\n if (\n value === '' ||\n value === '.' ||\n value === '..' ||\n value.includes('/') ||\n value.includes('\\\\') ||\n value.includes('\\0') ||\n Buffer.byteLength(value, 'utf8') > 255\n ) {\n throw new Error(`storage: invalid ${label} ${JSON.stringify(value)}`);\n }\n}\n\nfunction normalizeRelativePath(path: string): string {\n return path.replace(/\\\\/g, '/');\n}\n\nfunction isUnsafeRelativePath(\n path: string,\n allowEmpty = false,\n allowTrailingSlash = false,\n): boolean {\n if (path === '') return !allowEmpty;\n if (path.startsWith('/') || path.includes('\\0')) {\n return true;\n }\n const segments = path.split('/');\n if (allowTrailingSlash && segments.at(-1) === '') segments.pop();\n return segments.some(\n (segment) => segment === '' || segment === '.' || segment === '..',\n );\n}\n\n/**\n * Is this artifact-relative path inside the namespaces VER-02 reserves for the platform — the\n * `v/` version prefix and the `__`-internal families (meta/thumb/source/snapshot/overflow) —\n * with the one serving exemption, top-level single-segment `__vendor/<name>` pool assets?\n * User-supplied bundle entries matching this are SKIPPED at write time (writeBundle), exactly\n * like zip-slip entries: the sandbox deny-guard will never serve them, and letting them through\n * to the seal chokepoint would turn a hostile/quirky zip into a mid-write throw.\n */\nexport function isReservedArtifactPath(relPath: string): boolean {\n const segments = relPath.split('/');\n if (segments[0]?.toLowerCase() === 'v') return true;\n for (let i = 0; i < segments.length; i++) {\n const seg = segments[i];\n if (seg === undefined || !seg.startsWith('__')) continue;\n if (\n i === 0 &&\n seg === '__vendor' &&\n segments.length === 2 &&\n segments[1] !== ''\n )\n continue;\n return true;\n }\n return false;\n}\n\n/**\n * AP-002 storage rule 2: `path.startsWith(\"v/\" + version + \"/\") ⇔ version !== null`. Root objects\n * never live under the reserved `v/` namespace and versioned objects never live anywhere else —\n * asserted at the single ctx chokepoint (covers reads and writes, both backends), so a\n * mis-addressed artifact object is impossible rather than unlikely. Throws (fail-closed): this is\n * a programming error, never user input — every USER-named path (zip entries) is screened by\n * `isReservedArtifactPath` before it can reach this assertion.\n */\nexport function assertVersionPath(path: string, version: string | null): void {\n if (isUnsafeRelativePath(path)) {\n throw new Error(`storage: invalid artifact path ${JSON.stringify(path)}`);\n }\n if (version !== null) {\n assertVersionId(version);\n if (!path.startsWith(`v/${version}/`)) {\n throw new Error(\n `storage: versioned object path \"${path}\" must live under \"v/${version}/\" (AP-002 rule 2)`,\n );\n }\n } else if (path.split('/', 1)[0]?.toLowerCase() === 'v') {\n throw new Error(\n `storage: root object path \"${path}\" is inside the reserved v/ namespace (AP-002 rule 2)`,\n );\n }\n}\n\ntype RawRead =\n | { kind: 'hit'; raw: Buffer; rel: string }\n | { kind: 'miss' }\n | { kind: 'error'; error: unknown };\n\ninterface PreparedBundleEntry {\n name: string;\n body: Buffer;\n}\n\nfunction bundleLimit(\n requested: number | undefined,\n hardLimit: number,\n name: string,\n): number {\n const value = requested ?? hardLimit;\n if (!Number.isSafeInteger(value) || value < 1 || value > hardLimit) {\n throw new RangeError(\n `${name} must be an integer between 1 and ${hardLimit}`,\n );\n }\n return value;\n}\n\nfunction prepareBundle(\n zipBuffer: Buffer,\n limits: WriteBundleOptions['limits'],\n): PreparedBundleEntry[] {\n const maxEntries = bundleLimit(\n limits?.maxEntries,\n ARTIFACT_BUNDLE_MAX_ENTRIES,\n 'maxEntries',\n );\n const maxEntryBytes = bundleLimit(\n limits?.maxEntryBytes,\n ARTIFACT_BUNDLE_MAX_ENTRY_BYTES,\n 'maxEntryBytes',\n );\n const maxTotalBytes = bundleLimit(\n limits?.maxTotalBytes,\n ARTIFACT_BUNDLE_MAX_TOTAL_BYTES,\n 'maxTotalBytes',\n );\n const entries = new AdmZip(zipBuffer).getEntries();\n if (entries.length > maxEntries) {\n throw new ArtifactBundleLimitError('entries');\n }\n\n const accepted: Array<{ entry: (typeof entries)[number]; name: string }> = [];\n const names = new Set<string>();\n let declaredTotal = 0;\n for (const entry of entries) {\n if (entry.isDirectory) continue;\n const name = normalizeRelativePath(entry.entryName).replace(/^\\/+/, '');\n if (isUnsafeRelativePath(name) || isReservedArtifactPath(name)) {\n continue;\n }\n const portableName = name.toLowerCase();\n if (names.has(portableName)) {\n throw new Error(`artifact bundle contains duplicate entry \"${name}\"`);\n }\n names.add(portableName);\n const declaredSize = entry.header.size;\n if (\n !Number.isSafeInteger(declaredSize) ||\n declaredSize < 0 ||\n declaredSize > maxEntryBytes\n ) {\n throw new ArtifactBundleLimitError('entry-bytes');\n }\n declaredTotal += declaredSize;\n if (!Number.isSafeInteger(declaredTotal) || declaredTotal > maxTotalBytes) {\n throw new ArtifactBundleLimitError('total-bytes');\n }\n accepted.push({ entry, name });\n }\n\n // Inflate and validate every accepted entry before the first storage write. This bounds peak\n // memory by maxTotalBytes and prevents a late malformed/oversized entry from leaving a partial\n // bundle behind.\n const prepared: PreparedBundleEntry[] = [];\n let actualTotal = 0;\n for (const { entry, name } of accepted) {\n const body = entry.getData();\n if (body.length > maxEntryBytes) {\n throw new ArtifactBundleLimitError('entry-bytes');\n }\n actualTotal += body.length;\n if (!Number.isSafeInteger(actualTotal) || actualTotal > maxTotalBytes) {\n throw new ArtifactBundleLimitError('total-bytes');\n }\n prepared.push({ name, body });\n }\n return prepared;\n}\n\n/** Envelope context for an artifact-relative path. */\nfunction artifactCtx(\n artifactId: string,\n path: string,\n ref: ScopeRef | ReadRef,\n): EnvelopeContext {\n assertArtifactId(artifactId);\n const version = ref.version ?? null;\n assertVersionPath(path, version);\n return { scope: ref.scope as string, artifactId, version, path };\n}\n\n/** Structured, greppable trace of a legacy plaintext pass-through (SEC-04 migration window). */\nexport function warnLegacyRead(where: string, key: string): void {\n // `console` is deliberate: this package is consumed by apps/sandbox as well as the Nest API,\n // so a framework logger is not available here. It is the only portable sink.\n // eslint-disable-next-line no-console\n console.warn(\n `[storage] legacy plaintext read (${where}): ${key} — pending SEC-04 backfill`,\n );\n}\n\n// ── storage-client backend ───────────────────────────────────────────────────\n/**\n * The one implementation, for every provider. It reaches storage only through `StorageClient`,\n * so filesystem, S3, GCS, Azure, R2 and the rest differ by driver, not by code path here. The\n * filesystem once had a parallel implementation that resolved real paths and probed with\n * `existsSync`; it is gone, and with it the chance of the two drifting on reserved namespaces,\n * version prefixes, or traversal guards.\n */\nclass ClientArtifactStorage implements ArtifactStorage {\n constructor(\n private readonly crypto: StorageCrypto,\n private readonly client: StorageClient,\n private readonly objectNamespaces: ReadonlySet<string>,\n private readonly filesystemLayout: boolean,\n ) {}\n\n private assertArtifactId(artifactId: string): void {\n assertArtifactId(artifactId);\n if (this.objectNamespaces.has(artifactId.toLowerCase())) {\n throw new Error(\n `storage: artifact id ${JSON.stringify(artifactId)} is a reserved object namespace`,\n );\n }\n }\n\n async writeHtml(\n artifactId: string,\n buffer: Buffer,\n ref: ScopeRef,\n ): Promise<void> {\n await this.put(\n artifactId,\n 'index.html',\n buffer,\n ref,\n 'text/html; charset=utf-8',\n );\n }\n\n async writeBundle(\n artifactId: string,\n zipBuffer: Buffer,\n ref: ScopeRef,\n opts?: WriteBundleOptions,\n ): Promise<void> {\n this.assertArtifactId(artifactId);\n // Versioned mode (VER-03): mirror the fs backend — prefix + derived ctx.version, entry-name\n // screening unchanged.\n const versionId = opts?.versionId;\n if (versionId !== undefined) assertVersionId(versionId);\n const prefix = versionId === undefined ? '' : `v/${versionId}/`;\n const effRef: ScopeRef =\n versionId === undefined ? ref : { ...ref, version: versionId };\n const entries = prepareBundle(zipBuffer, opts?.limits);\n for (const { name, body } of entries) {\n // `__vendor/<name>` stays ROOT-scoped in versioned mode — see the fs backend.\n if (prefix !== '' && /^__vendor\\/[^/]+$/.test(name)) {\n await this.put(\n artifactId,\n name,\n body,\n { scope: ref.scope },\n contentTypeFor(name),\n );\n continue;\n }\n await this.put(\n artifactId,\n prefix + name,\n body,\n effRef,\n contentTypeFor(name),\n );\n }\n }\n\n async writeFile(\n artifactId: string,\n relPath: string,\n buffer: Buffer,\n ref: ScopeRef,\n contentType?: string,\n ): Promise<void> {\n const name = normalizeRelativePath(relPath);\n // containment guard at the key level: reject absolute paths, empty, or any traversal segment.\n if (isUnsafeRelativePath(name)) {\n throw new Error(`writeFile: path escapes artifact directory: ${relPath}`);\n }\n await this.put(\n artifactId,\n name,\n buffer,\n ref,\n contentType ?? contentTypeFor(name),\n );\n }\n\n async writeThumbnail(\n artifactId: string,\n pngBuffer: Buffer,\n ref: ScopeRef,\n ): Promise<void> {\n await this.put(artifactId, THUMBNAIL_KEY, pngBuffer, ref, 'image/png');\n }\n\n async remove(artifactId: string): Promise<void> {\n this.assertArtifactId(artifactId);\n for await (const object of this.client.listAll({\n prefix: `${artifactId}/`,\n })) {\n await this.client.delete(object.key);\n }\n }\n\n async listFiles(\n artifactId: string,\n subPrefix = '',\n ): Promise<StoredFileInfo[]> {\n this.assertArtifactId(artifactId);\n const normalizedPrefix = normalizeRelativePath(subPrefix);\n if (isUnsafeRelativePath(normalizedPrefix, true, true)) {\n throw new Error(\n `storage: invalid artifact list prefix ${JSON.stringify(subPrefix)}`,\n );\n }\n const base = `${artifactId}/`;\n const out: StoredFileInfo[] = [];\n for await (const object of this.client.listAll({\n prefix: `${base}${normalizedPrefix}`,\n })) {\n if (!object.key.startsWith(base)) continue;\n out.push({\n path: object.key.slice(base.length),\n lastModified: object.lastModified ?? new Date(0),\n });\n }\n return out.sort((a, b) => (a.path < b.path ? -1 : 1));\n }\n\n async read(\n artifactId: string,\n relPath: string,\n ref: ReadRef,\n options?: ArtifactReadOptions,\n ): Promise<Buffer | null> {\n const out = await this.readWithInfo(artifactId, relPath, ref, options);\n return out === null ? null : out.plain;\n }\n\n /**\n * Raw bytes for one artifact-relative path. Reports a plain miss and a provider failure\n * separately so the caller can retry a directory-shaped key without ever turning a genuine\n * backend failure into a 404.\n */\n private async fetchRaw(\n artifactId: string,\n rel: string,\n options: ArtifactReadOptions | undefined,\n preflight = true,\n ): Promise<RawRead> {\n const key = `${artifactId}/${rel}`;\n if (preflight && !(await preflightRawRead(this.client, key, options)))\n return { kind: 'miss' };\n try {\n const bytes = await this.client.downloadBytes(key, {\n maxBytes: rawReadLimit(options),\n });\n return { kind: 'hit', raw: Buffer.from(bytes), rel };\n } catch (error) {\n if (isStorageError(error) && error.code === StorageErrorCode.NOT_FOUND) {\n return { kind: 'miss' };\n }\n // A read limit is the caller's own bound, not a backend condition: it must surface as\n // itself rather than as a miss the index.html rewrite would paper over.\n if (\n isStorageError(error) &&\n error.code === StorageErrorCode.LIMIT_EXCEEDED\n ) {\n throw new ArtifactReadLimitError(requirePlainReadLimit(options));\n }\n return { kind: 'error', error };\n }\n }\n\n async readWithInfo(\n artifactId: string,\n relPath: string,\n ref: ReadRef,\n options?: ArtifactReadOptions,\n ): Promise<StoredObject | null> {\n this.assertArtifactId(artifactId);\n const normalized = normalizeRelativePath(relPath)\n .replace(/\\/{2,}/g, '/')\n .replace(/^\\/+|\\/+$/g, '');\n const rel = normalized === '' ? 'index.html' : normalized;\n if (isUnsafeRelativePath(rel)) return null;\n\n // A directory-shaped request (\"\" or \"docs\") serves that directory's index.html. Providers\n // disagree on how a directory key fails — an object store has no such key at all, while the\n // filesystem reports reading a directory as its own error — so the rewrite is driven by the\n // miss, not by probing the backend. The AAD binds to the path actually read, never to the\n // one requested (ADR D6.1).\n // A filesystem HEAD cannot distinguish a file from a directory candidate. Skip that one\n // preflight so a directory inode size cannot trigger the caller's plaintext limit before we\n // try its bounded `index.html`; downloadBytes still enforces the raw-byte ceiling.\n const direct = await this.fetchRaw(\n artifactId,\n rel,\n options,\n !this.filesystemLayout,\n );\n if (direct.kind === 'error' && !this.filesystemLayout) {\n throw direct.error;\n }\n let resolved = direct;\n if (resolved.kind !== 'hit') {\n const fallback = await this.fetchRaw(\n artifactId,\n `${rel}/index.html`,\n options,\n );\n if (fallback.kind === 'error') throw fallback.error;\n if (fallback.kind === 'miss') {\n if (direct.kind === 'error') throw direct.error;\n return null;\n }\n resolved = fallback;\n }\n\n const out = await open(\n resolved.raw,\n artifactCtx(artifactId, resolved.rel, ref),\n this.crypto.keyProvider,\n this.crypto.allowLegacyPlaintext === undefined\n ? {}\n : { allowLegacyPlaintext: this.crypto.allowLegacyPlaintext },\n );\n enforcePlainReadLimit(out.plain, options);\n if (out.legacy) warnLegacyRead('storage', `${artifactId}/${resolved.rel}`);\n return {\n plain: out.plain,\n ...(out.contentType === undefined\n ? {}\n : { contentType: out.contentType }),\n };\n }\n\n async deleteFiles(artifactId: string, relPaths: string[]): Promise<void> {\n this.assertArtifactId(artifactId);\n for (const relPath of relPaths) {\n const name = normalizeRelativePath(relPath);\n // containment at the key level: reject absolute/empty paths and traversal segments.\n if (isUnsafeRelativePath(name)) {\n continue;\n }\n await this.client.delete(`${artifactId}/${name}`);\n }\n }\n\n /** The single s3 write primitive; enveloped objects are opaque octet streams to the bucket. */\n private async put(\n artifactId: string,\n relPath: string,\n body: Buffer,\n ref: ScopeRef,\n contentType: string,\n ): Promise<void> {\n this.assertArtifactId(artifactId);\n const sealed = await seal(\n body,\n artifactCtx(artifactId, relPath, ref),\n this.crypto.keyProvider,\n contentType,\n );\n await this.client.upload(`${artifactId}/${relPath}`, sealed, {\n // The real content type is authenticated inside CAE1 and must not leak through S3 metadata.\n contentType: 'application/octet-stream',\n });\n }\n}\n\nfunction requirePlainReadLimit(\n options: ArtifactReadOptions | undefined,\n): number {\n const limit = options?.maxPlainBytes;\n if (limit === undefined || !Number.isSafeInteger(limit) || limit < 0) {\n throw new Error('maxPlainBytes must be a non-negative safe integer');\n }\n return limit;\n}\n\nfunction rawReadLimit(options: ArtifactReadOptions | undefined): number {\n if (options?.maxPlainBytes === undefined) return Infinity;\n return requirePlainReadLimit(options) + ENVELOPE_MAX_OVERHEAD_BYTES;\n}\n\nasync function preflightRawRead(\n client: StorageClient,\n key: string,\n options: ArtifactReadOptions | undefined,\n): Promise<boolean> {\n if (options?.maxPlainBytes === undefined) return true;\n try {\n const metadata = await client.head(key);\n if (metadata.size > rawReadLimit(options)) {\n throw new ArtifactReadLimitError(requirePlainReadLimit(options));\n }\n return true;\n } catch (error) {\n if (isStorageError(error) && error.code === StorageErrorCode.NOT_FOUND)\n return false;\n throw error;\n }\n}\n\nfunction enforcePlainReadLimit(\n plain: Buffer,\n options: ArtifactReadOptions | undefined,\n): void {\n if (options?.maxPlainBytes === undefined) return;\n const limit = requirePlainReadLimit(options);\n if (plain.length > limit) throw new ArtifactReadLimitError(limit);\n}\n\nconst MIME: Record<string, string> = {\n '.html': 'text/html; charset=utf-8',\n '.js': 'text/javascript; charset=utf-8',\n '.mjs': 'text/javascript; charset=utf-8',\n '.css': 'text/css; charset=utf-8',\n '.json': 'application/json; charset=utf-8',\n '.svg': 'image/svg+xml',\n '.png': 'image/png',\n '.jpg': 'image/jpeg',\n '.jpeg': 'image/jpeg',\n '.gif': 'image/gif',\n '.webp': 'image/webp',\n '.ico': 'image/x-icon',\n '.woff': 'font/woff',\n '.woff2': 'font/woff2',\n '.map': 'application/json; charset=utf-8',\n '.wasm': 'application/wasm',\n};\n\n/** content-type for an artifact file path, falling back to octet-stream. */\nexport function contentTypeFor(path: string): string {\n return MIME[extname(path).toLowerCase()] ?? 'application/octet-stream';\n}\n"]}
@@ -0,0 +1,44 @@
1
+ import { type EnvelopeContext, type ExpectedContext } from './context.js';
2
+ import type { KeyProvider } from './key-provider.js';
3
+ /** Maximum bytes added around plaintext by a valid CAE1 envelope. Useful for bounded raw reads. */
4
+ export declare const ENVELOPE_MAX_OVERHEAD_BYTES: number;
5
+ export declare const ENVELOPE_WRITE_VERSION = 1;
6
+ export declare const ENVELOPE_SUPPORTED_VERSIONS: readonly number[];
7
+ export type EnvelopeFailureReason = 'bad-magic' | 'truncated' | 'malformed-header' | 'unsupported-version' | 'unsupported-alg' | 'context-mismatch' | 'key-unavailable' | 'auth-failed';
8
+ /**
9
+ * Structured decrypt failure. Carries only address metadata, never key material or plaintext —
10
+ * safe to log as-is (the failure-reason counter in the runbook keys off `reason`).
11
+ */
12
+ export declare class EnvelopeError extends Error {
13
+ readonly reason: EnvelopeFailureReason;
14
+ readonly artifactId: string | null;
15
+ readonly path: string;
16
+ constructor(reason: EnvelopeFailureReason, artifactId: string | null, path: string, detail?: string);
17
+ }
18
+ export interface OpenResult {
19
+ plain: Buffer;
20
+ ctx: EnvelopeContext;
21
+ contentType?: string;
22
+ /** True only when legacy plaintext passed through under the explicit migration flag. */
23
+ legacy: boolean;
24
+ }
25
+ export interface OpenOptions {
26
+ /**
27
+ * SEC-04's migration-window escape hatch, default off. Consulted ONLY when the magic bytes
28
+ * are absent (a pre-envelope plaintext object) — never for a valid-magic object that fails
29
+ * validation, so the flag cannot downgrade a tampered envelope to a plaintext read.
30
+ */
31
+ allowLegacyPlaintext?: boolean;
32
+ }
33
+ /** Encrypt one object. Mints a fresh DEK per write; the DEK never outlives the call. */
34
+ export declare function seal(plain: Buffer, ctx: EnvelopeContext, provider: KeyProvider, contentType?: string): Promise<Buffer>;
35
+ /** True when the bytes carry the envelope magic (cheap pre-check; open() revalidates). */
36
+ export declare function isEnvelope(bytes: Buffer): boolean;
37
+ /**
38
+ * Decrypt one object, fail-closed (ADR-0001 D2/D6). Order matters and is normative:
39
+ * frame → header shape → CONTEXT ASSERTION → key unwrap → tag check. The context assertion runs
40
+ * before any key operation so a swapped object costs no KMS call and leaks no oracle; `scope`
41
+ * alone may come from the header (SCOPE_FROM_HEADER) because the wrap layer binds it.
42
+ */
43
+ export declare function open(bytes: Buffer, expect: ExpectedContext, provider: KeyProvider, opts?: OpenOptions): Promise<OpenResult>;
44
+ //# sourceMappingURL=codec.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"codec.d.ts","sourceRoot":"","sources":["../../../src/artifacts/crypto/codec.ts"],"names":[],"mappings":"AAEA,OAAO,EAGL,KAAK,eAAe,EACpB,KAAK,eAAe,EACrB,MAAM,cAAc,CAAC;AACtB,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAkBrD,mGAAmG;AACnG,eAAO,MAAM,2BAA2B,QAA+B,CAAC;AAExE,eAAO,MAAM,sBAAsB,IAAI,CAAC;AACxC,eAAO,MAAM,2BAA2B,EAAE,SAAS,MAAM,EAExD,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAC7B,WAAW,GACX,WAAW,GACX,kBAAkB,GAClB,qBAAqB,GACrB,iBAAiB,GACjB,kBAAkB,GAClB,iBAAiB,GACjB,aAAa,CAAC;AAElB;;;GAGG;AACH,qBAAa,aAAc,SAAQ,KAAK;IAEpC,QAAQ,CAAC,MAAM,EAAE,qBAAqB;IACtC,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IAClC,QAAQ,CAAC,IAAI,EAAE,MAAM;IAHvB,YACW,MAAM,EAAE,qBAAqB,EAC7B,UAAU,EAAE,MAAM,GAAG,IAAI,EACzB,IAAI,EAAE,MAAM,EACrB,MAAM,CAAC,EAAE,MAAM,EAMhB;CACF;AAYD,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,eAAe,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,wFAAwF;IACxF,MAAM,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,WAAW;IAC1B;;;;OAIG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAC;CAChC;AAoBD,wFAAwF;AACxF,wBAAsB,IAAI,CACxB,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,eAAe,EACpB,QAAQ,EAAE,WAAW,EACrB,WAAW,CAAC,EAAE,MAAM,GACnB,OAAO,CAAC,MAAM,CAAC,CA0DjB;AAED,0FAA0F;AAC1F,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAKjD;AAED;;;;;GAKG;AACH,wBAAsB,IAAI,CACxB,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,eAAe,EACvB,QAAQ,EAAE,WAAW,EACrB,IAAI,GAAE,WAAgB,GACrB,OAAO,CAAC,UAAU,CAAC,CAwFrB"}