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

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 (119) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +158 -92
  3. package/SECURITY.md +40 -0
  4. package/dist/ai-sdk/ai-sdk-workspace-tools.d.ts +63 -0
  5. package/dist/ai-sdk/ai-sdk-workspace-tools.d.ts.map +1 -0
  6. package/dist/ai-sdk/ai-sdk-workspace-tools.js +383 -0
  7. package/dist/ai-sdk/ai-sdk-workspace-tools.js.map +1 -0
  8. package/dist/ai-sdk/index.d.ts +2 -0
  9. package/dist/ai-sdk/index.d.ts.map +1 -0
  10. package/dist/ai-sdk/index.js +2 -0
  11. package/dist/ai-sdk/index.js.map +1 -0
  12. package/dist/files-sdk/files-sdk.driver.d.ts +16 -2
  13. package/dist/files-sdk/files-sdk.driver.d.ts.map +1 -1
  14. package/dist/files-sdk/files-sdk.driver.js +159 -0
  15. package/dist/files-sdk/files-sdk.driver.js.map +1 -1
  16. package/dist/files-sdk/fs/index.d.ts +21 -2
  17. package/dist/files-sdk/fs/index.d.ts.map +1 -1
  18. package/dist/files-sdk/fs/index.js +600 -1
  19. package/dist/files-sdk/fs/index.js.map +1 -1
  20. package/dist/files-sdk/index.d.ts +1 -1
  21. package/dist/files-sdk/index.d.ts.map +1 -1
  22. package/dist/files-sdk/index.js.map +1 -1
  23. package/dist/files-sdk/provider/index.d.ts.map +1 -1
  24. package/dist/files-sdk/provider/index.js +7 -0
  25. package/dist/files-sdk/provider/index.js.map +1 -1
  26. package/dist/files-sdk/s3/index.d.ts +12 -3
  27. package/dist/files-sdk/s3/index.d.ts.map +1 -1
  28. package/dist/files-sdk/s3/index.js +203 -37
  29. package/dist/files-sdk/s3/index.js.map +1 -1
  30. package/dist/storage.client.d.ts +5 -1
  31. package/dist/storage.client.d.ts.map +1 -1
  32. package/dist/storage.client.js +49 -0
  33. package/dist/storage.client.js.map +1 -1
  34. package/dist/storage.driver.d.ts +3 -1
  35. package/dist/storage.driver.d.ts.map +1 -1
  36. package/dist/storage.driver.js.map +1 -1
  37. package/dist/storage.service.d.ts.map +1 -1
  38. package/dist/storage.service.js +28 -3
  39. package/dist/storage.service.js.map +1 -1
  40. package/dist/storage.types.d.ts +36 -0
  41. package/dist/storage.types.d.ts.map +1 -1
  42. package/dist/storage.types.js.map +1 -1
  43. package/dist/workspace/index.d.ts +4 -0
  44. package/dist/workspace/index.d.ts.map +1 -0
  45. package/dist/workspace/index.js +4 -0
  46. package/dist/workspace/index.js.map +1 -0
  47. package/dist/workspace/storage-workspace.cursor.d.ts +6 -0
  48. package/dist/workspace/storage-workspace.cursor.d.ts.map +1 -0
  49. package/dist/workspace/storage-workspace.cursor.js +45 -0
  50. package/dist/workspace/storage-workspace.cursor.js.map +1 -0
  51. package/dist/workspace/storage-workspace.d.ts +6 -0
  52. package/dist/workspace/storage-workspace.d.ts.map +1 -0
  53. package/dist/workspace/storage-workspace.error.d.ts +28 -0
  54. package/dist/workspace/storage-workspace.error.d.ts.map +1 -0
  55. package/dist/workspace/storage-workspace.error.js +67 -0
  56. package/dist/workspace/storage-workspace.error.js.map +1 -0
  57. package/dist/workspace/storage-workspace.js +747 -0
  58. package/dist/workspace/storage-workspace.js.map +1 -0
  59. package/dist/workspace/storage-workspace.path.d.ts +9 -0
  60. package/dist/workspace/storage-workspace.path.d.ts.map +1 -0
  61. package/dist/workspace/storage-workspace.path.js +68 -0
  62. package/dist/workspace/storage-workspace.path.js.map +1 -0
  63. package/dist/workspace/storage-workspace.types.d.ts +97 -0
  64. package/dist/workspace/storage-workspace.types.d.ts.map +1 -0
  65. package/dist/workspace/storage-workspace.types.js +20 -0
  66. package/dist/workspace/storage-workspace.types.js.map +1 -0
  67. package/package.json +23 -20
  68. package/dist/artifacts/artifact-storage.d.ts +0 -161
  69. package/dist/artifacts/artifact-storage.d.ts.map +0 -1
  70. package/dist/artifacts/artifact-storage.js +0 -452
  71. package/dist/artifacts/artifact-storage.js.map +0 -1
  72. package/dist/artifacts/crypto/codec.d.ts +0 -44
  73. package/dist/artifacts/crypto/codec.d.ts.map +0 -1
  74. package/dist/artifacts/crypto/codec.js +0 -273
  75. package/dist/artifacts/crypto/codec.js.map +0 -1
  76. package/dist/artifacts/crypto/context.d.ts +0 -48
  77. package/dist/artifacts/crypto/context.d.ts.map +0 -1
  78. package/dist/artifacts/crypto/context.js +0 -37
  79. package/dist/artifacts/crypto/context.js.map +0 -1
  80. package/dist/artifacts/crypto/dek-cache.d.ts +0 -38
  81. package/dist/artifacts/crypto/dek-cache.d.ts.map +0 -1
  82. package/dist/artifacts/crypto/dek-cache.js +0 -137
  83. package/dist/artifacts/crypto/dek-cache.js.map +0 -1
  84. package/dist/artifacts/crypto/index.d.ts +0 -25
  85. package/dist/artifacts/crypto/index.d.ts.map +0 -1
  86. package/dist/artifacts/crypto/index.js +0 -29
  87. package/dist/artifacts/crypto/index.js.map +0 -1
  88. package/dist/artifacts/crypto/key-provider.d.ts +0 -44
  89. package/dist/artifacts/crypto/key-provider.d.ts.map +0 -1
  90. package/dist/artifacts/crypto/key-provider.js +0 -36
  91. package/dist/artifacts/crypto/key-provider.js.map +0 -1
  92. package/dist/artifacts/crypto/kms-provider.d.ts +0 -16
  93. package/dist/artifacts/crypto/kms-provider.d.ts.map +0 -1
  94. package/dist/artifacts/crypto/kms-provider.js +0 -90
  95. package/dist/artifacts/crypto/kms-provider.js.map +0 -1
  96. package/dist/artifacts/crypto/local-provider.d.ts +0 -17
  97. package/dist/artifacts/crypto/local-provider.d.ts.map +0 -1
  98. package/dist/artifacts/crypto/local-provider.js +0 -58
  99. package/dist/artifacts/crypto/local-provider.js.map +0 -1
  100. package/dist/artifacts/env-config.d.ts +0 -15
  101. package/dist/artifacts/env-config.d.ts.map +0 -1
  102. package/dist/artifacts/env-config.js +0 -95
  103. package/dist/artifacts/env-config.js.map +0 -1
  104. package/dist/artifacts/index.d.ts +0 -6
  105. package/dist/artifacts/index.d.ts.map +0 -1
  106. package/dist/artifacts/index.js +0 -6
  107. package/dist/artifacts/index.js.map +0 -1
  108. package/dist/artifacts/nest/index.d.ts +0 -29
  109. package/dist/artifacts/nest/index.d.ts.map +0 -1
  110. package/dist/artifacts/nest/index.js +0 -129
  111. package/dist/artifacts/nest/index.js.map +0 -1
  112. package/dist/artifacts/object-store.d.ts +0 -27
  113. package/dist/artifacts/object-store.d.ts.map +0 -1
  114. package/dist/artifacts/object-store.js +0 -198
  115. package/dist/artifacts/object-store.js.map +0 -1
  116. package/dist/artifacts/storage-driver.d.ts +0 -48
  117. package/dist/artifacts/storage-driver.d.ts.map +0 -1
  118. package/dist/artifacts/storage-driver.js +0 -89
  119. package/dist/artifacts/storage-driver.js.map +0 -1
@@ -1,161 +0,0 @@
1
- import { type StorageClient } from '../core/index.js';
2
- import { SCOPE_FROM_HEADER, type StorageCrypto } from './crypto/index.js';
3
- import { type ArtifactStorageConfig } from './storage-driver.js';
4
- export { assertStorageProvider, defaultFsRoot, type ArtifactStorageConfig, } from './storage-driver.js';
5
- /**
6
- * Storage for uploaded artifact files, keyed `<artifactId>/<path>` (e.g. `<id>/index.html`,
7
- * `<id>/assets/app.js`). The api writes; the sandbox reads. The `fs` provider keeps both pointed
8
- * at one directory (fine for a single host / shared volume); any object-store provider — S3, GCS,
9
- * Azure, R2, and the rest — lets the two services share storage without a shared volume, which is
10
- * what running them on separate hosts (as on ECS) requires.
11
- *
12
- * Every object is stored as a CAE1 encryption envelope (ADR-0001, SEC-03): callers must say who
13
- * owns what they write (`ScopeRef`) and what they expect to read (`ReadRef`) — the codec binds
14
- * ciphertext to `{scope, artifactId, version, path}` so a swapped or replayed object fails closed.
15
- */
16
- export interface ArtifactStorage {
17
- /** Single self-contained HTML artifact → `<id>/index.html`. */
18
- writeHtml(artifactId: string, buffer: Buffer, ref: ScopeRef): Promise<void>;
19
- /**
20
- * Pre-built SPA bundle (a zip whose root contains index.html). Encrypted per extracted entry.
21
- * With `opts.versionId` (VER-03), entries land under `v/<versionId>/…` and are sealed with
22
- * `ctx.version = versionId` (the caller's `ref.version` is overridden to match — the path⇔version
23
- * biconditional is derived, never trusted). Reserved-namespace screening applies to the zip
24
- * ENTRY name in both modes: a bundle shipping `v/…` or `__…` files has those skipped, not nested.
25
- */
26
- writeBundle(artifactId: string, zipBuffer: Buffer, ref: ScopeRef, opts?: WriteBundleOptions): Promise<void>;
27
- /**
28
- * Write an arbitrary file within the artifact (e.g. `__vendor/<hash>.js`, `__meta.json`).
29
- * Rejects any `relPath` escaping the artifact directory. `contentType` rides the envelope
30
- * header (authenticated) on both backends.
31
- */
32
- writeFile(artifactId: string, relPath: string, buffer: Buffer, ref: ScopeRef, contentType?: string): Promise<void>;
33
- /** Server-rendered PNG preview → reserved key `<id>/__thumb.png`. */
34
- writeThumbnail(artifactId: string, pngBuffer: Buffer, ref: ScopeRef): Promise<void>;
35
- /** Delete every file under the artifact (includes the thumbnail). */
36
- remove(artifactId: string): Promise<void>;
37
- /**
38
- * Read a file within the artifact; `relPath === ""` resolves to index.html. `null` if absent.
39
- * Throws `EnvelopeError` on any decrypt failure — a corrupt or swapped object must never be
40
- * mistaken for a missing one (no SPA fallback over tampering).
41
- */
42
- read(artifactId: string, relPath: string, ref: ReadRef, options?: ArtifactReadOptions): Promise<Buffer | null>;
43
- /**
44
- * Like `read`, but also returns the envelope's AUTHENTICATED content type (VER-03). This is the
45
- * read primitive for publish/CoW copy loops: a decrypt→re-encrypt copy must re-seal with the
46
- * same `ct` it opened (the type is AAD-bound and steers serving), and `read()` discards it.
47
- * `contentType` is undefined only for legacy plaintext pass-throughs (nothing was verified).
48
- */
49
- readWithInfo(artifactId: string, relPath: string, ref: ReadRef, options?: ArtifactReadOptions): Promise<StoredObject | null>;
50
- /**
51
- * Delete specific files under the artifact (VER-03: publish prunes root paths removed by the
52
- * newly-materialized version). Paths escaping the artifact directory and missing paths are
53
- * ignored (idempotent — publish repair re-runs the same prune).
54
- */
55
- deleteFiles(artifactId: string, relPaths: string[]): Promise<void>;
56
- /**
57
- * List stored objects under the artifact (optionally under `subPrefix`, e.g. `v/<versionId>/`).
58
- * Returns artifact-relative forward-slash paths + last-modified stamps, sorted by path. Reads
59
- * no object bodies and needs no crypto — it serves the sweepers/invariants (version-prefix
60
- * existence, orphan-prefix age, canonical bundle enumeration), never the serving plane.
61
- */
62
- listFiles(artifactId: string, subPrefix?: string): Promise<StoredFileInfo[]>;
63
- }
64
- /** One listed storage object (metadata only — the body stays sealed). */
65
- export interface StoredFileInfo {
66
- /** Artifact-relative path, forward slashes (e.g. `index.html`, `v/<vid>/index.html`). */
67
- path: string;
68
- lastModified: Date;
69
- }
70
- /** A decrypted object plus the authenticated content type it was sealed with (see readWithInfo). */
71
- export interface StoredObject {
72
- plain: Buffer;
73
- /** From the envelope header (`ct`, AAD-bound); undefined for legacy plaintext pass-throughs. */
74
- contentType?: string;
75
- }
76
- /** Bounds decrypted data while also bounding the opaque envelope downloaded before decryption. */
77
- export interface ArtifactReadOptions {
78
- readonly maxPlainBytes?: number;
79
- }
80
- /** Permanent, address-independent read rejection safe for callers to map to a bounded 4xx. */
81
- export declare class ArtifactReadLimitError extends Error {
82
- readonly maxPlainBytes: number;
83
- constructor(maxPlainBytes: number);
84
- }
85
- /** Permanent rejection for a ZIP whose declared expansion exceeds the public protocol limits. */
86
- export declare class ArtifactBundleLimitError extends Error {
87
- readonly reason: 'entries' | 'entry-bytes' | 'total-bytes';
88
- constructor(reason: 'entries' | 'entry-bytes' | 'total-bytes');
89
- }
90
- export declare const ARTIFACT_BUNDLE_MAX_ENTRIES = 1000;
91
- export declare const ARTIFACT_BUNDLE_MAX_ENTRY_BYTES: number;
92
- export declare const ARTIFACT_BUNDLE_MAX_TOTAL_BYTES: number;
93
- /** Reserved storage path for the generated PNG preview, served by the sandbox as `<id>/__thumb.png`. */
94
- export declare const THUMBNAIL_KEY = "__thumb.png";
95
- /** Options for writeBundle (VER-03 versioned bundle writes). */
96
- export interface WriteBundleOptions {
97
- /** Write entries under `v/<versionId>/…` with `ctx.version = versionId` instead of the root. */
98
- versionId?: string;
99
- /** Optional stricter limits; values may lower, but never raise, the protocol hard limits. */
100
- limits?: {
101
- maxEntries?: number;
102
- maxEntryBytes?: number;
103
- maxTotalBytes?: number;
104
- };
105
- }
106
- /** Who owns an object being written; `version` stays null until VER-02 introduces versions. */
107
- export interface ScopeRef {
108
- /** `org:<organizationId>` | `user:<ownerUserId>` | `upload:<uploadId>` — see artifactScope(). */
109
- scope: string;
110
- version?: string | null;
111
- }
112
- /**
113
- * What the reader independently knows. The api asserts the scope it fetched from the DB; the
114
- * sandbox — which only knows the artifactId — passes SCOPE_FROM_HEADER and relies on the wrap
115
- * layer to enforce scope cryptographically (see crypto/context.ts).
116
- */
117
- export interface ReadRef {
118
- scope: string | typeof SCOPE_FROM_HEADER;
119
- version?: string | null;
120
- }
121
- export declare function createArtifactStorage(cfg: ArtifactStorageConfig, crypto: StorageCrypto): Promise<ArtifactStorage>;
122
- /** Framework-neutral domain adapter used by both direct consumers and the Nest composition entry. */
123
- export declare function createArtifactStorageWithClient(cfg: ArtifactStorageConfig, crypto: StorageCrypto, client: StorageClient): ArtifactStorage;
124
- /**
125
- * Storage path prefix for a version's objects (AP-002): `""` for a backfilled `canonical_v1`
126
- * version (its bytes ARE the canonical root objects) and `v/<versionId>/` for a `versioned` one.
127
- */
128
- export declare function versionPrefix(layout: 'canonical_v1' | 'versioned', versionId: string): string;
129
- /**
130
- * Envelope `ctx.version` for a version's objects (AP-002): `null` for `canonical_v1` (root AAD —
131
- * the serving plane is version-free forever) and the version UUID, byte-identical to the path
132
- * segment, for `versioned`.
133
- */
134
- export declare function versionAad(layout: 'canonical_v1' | 'versioned', versionId: string): string | null;
135
- /** Artifact ids are one storage segment; `_objects` is the filesystem object-store namespace. */
136
- export declare function assertArtifactId(artifactId: string): void;
137
- /** Version ids are one storage segment because they are interpolated under `v/<id>/`. */
138
- export declare function assertVersionId(versionId: string): void;
139
- /**
140
- * Is this artifact-relative path inside the namespaces VER-02 reserves for the platform — the
141
- * `v/` version prefix and the `__`-internal families (meta/thumb/source/snapshot/overflow) —
142
- * with the one serving exemption, top-level single-segment `__vendor/<name>` pool assets?
143
- * User-supplied bundle entries matching this are SKIPPED at write time (writeBundle), exactly
144
- * like zip-slip entries: the sandbox deny-guard will never serve them, and letting them through
145
- * to the seal chokepoint would turn a hostile/quirky zip into a mid-write throw.
146
- */
147
- export declare function isReservedArtifactPath(relPath: string): boolean;
148
- /**
149
- * AP-002 storage rule 2: `path.startsWith("v/" + version + "/") ⇔ version !== null`. Root objects
150
- * never live under the reserved `v/` namespace and versioned objects never live anywhere else —
151
- * asserted at the single ctx chokepoint (covers reads and writes, both backends), so a
152
- * mis-addressed artifact object is impossible rather than unlikely. Throws (fail-closed): this is
153
- * a programming error, never user input — every USER-named path (zip entries) is screened by
154
- * `isReservedArtifactPath` before it can reach this assertion.
155
- */
156
- export declare function assertVersionPath(path: string, version: string | null): void;
157
- /** Structured, greppable trace of a legacy plaintext pass-through (SEC-04 migration window). */
158
- export declare function warnLegacyRead(where: string, key: string): void;
159
- /** content-type for an artifact file path, falling back to octet-stream. */
160
- export declare function contentTypeFor(path: string): string;
161
- //# sourceMappingURL=artifact-storage.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"artifact-storage.d.ts","sourceRoot":"","sources":["../../src/artifacts/artifact-storage.ts"],"names":[],"mappings":"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"}
@@ -1,452 +0,0 @@
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