@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.
- package/CHANGELOG.md +26 -0
- package/README.md +158 -92
- package/SECURITY.md +40 -0
- package/dist/ai-sdk/ai-sdk-workspace-tools.d.ts +63 -0
- package/dist/ai-sdk/ai-sdk-workspace-tools.d.ts.map +1 -0
- package/dist/ai-sdk/ai-sdk-workspace-tools.js +383 -0
- package/dist/ai-sdk/ai-sdk-workspace-tools.js.map +1 -0
- package/dist/ai-sdk/index.d.ts +2 -0
- package/dist/ai-sdk/index.d.ts.map +1 -0
- package/dist/ai-sdk/index.js +2 -0
- package/dist/ai-sdk/index.js.map +1 -0
- package/dist/files-sdk/files-sdk.driver.d.ts +16 -2
- package/dist/files-sdk/files-sdk.driver.d.ts.map +1 -1
- package/dist/files-sdk/files-sdk.driver.js +159 -0
- package/dist/files-sdk/files-sdk.driver.js.map +1 -1
- package/dist/files-sdk/fs/index.d.ts +21 -2
- package/dist/files-sdk/fs/index.d.ts.map +1 -1
- package/dist/files-sdk/fs/index.js +600 -1
- package/dist/files-sdk/fs/index.js.map +1 -1
- package/dist/files-sdk/index.d.ts +1 -1
- package/dist/files-sdk/index.d.ts.map +1 -1
- package/dist/files-sdk/index.js.map +1 -1
- package/dist/files-sdk/provider/index.d.ts.map +1 -1
- package/dist/files-sdk/provider/index.js +7 -0
- package/dist/files-sdk/provider/index.js.map +1 -1
- package/dist/files-sdk/s3/index.d.ts +12 -3
- package/dist/files-sdk/s3/index.d.ts.map +1 -1
- package/dist/files-sdk/s3/index.js +203 -37
- package/dist/files-sdk/s3/index.js.map +1 -1
- package/dist/storage.client.d.ts +5 -1
- package/dist/storage.client.d.ts.map +1 -1
- package/dist/storage.client.js +49 -0
- package/dist/storage.client.js.map +1 -1
- package/dist/storage.driver.d.ts +3 -1
- package/dist/storage.driver.d.ts.map +1 -1
- package/dist/storage.driver.js.map +1 -1
- package/dist/storage.service.d.ts.map +1 -1
- package/dist/storage.service.js +28 -3
- package/dist/storage.service.js.map +1 -1
- package/dist/storage.types.d.ts +36 -0
- package/dist/storage.types.d.ts.map +1 -1
- package/dist/storage.types.js.map +1 -1
- package/dist/workspace/index.d.ts +4 -0
- package/dist/workspace/index.d.ts.map +1 -0
- package/dist/workspace/index.js +4 -0
- package/dist/workspace/index.js.map +1 -0
- package/dist/workspace/storage-workspace.cursor.d.ts +6 -0
- package/dist/workspace/storage-workspace.cursor.d.ts.map +1 -0
- package/dist/workspace/storage-workspace.cursor.js +45 -0
- package/dist/workspace/storage-workspace.cursor.js.map +1 -0
- package/dist/workspace/storage-workspace.d.ts +6 -0
- package/dist/workspace/storage-workspace.d.ts.map +1 -0
- package/dist/workspace/storage-workspace.error.d.ts +28 -0
- package/dist/workspace/storage-workspace.error.d.ts.map +1 -0
- package/dist/workspace/storage-workspace.error.js +67 -0
- package/dist/workspace/storage-workspace.error.js.map +1 -0
- package/dist/workspace/storage-workspace.js +747 -0
- package/dist/workspace/storage-workspace.js.map +1 -0
- package/dist/workspace/storage-workspace.path.d.ts +9 -0
- package/dist/workspace/storage-workspace.path.d.ts.map +1 -0
- package/dist/workspace/storage-workspace.path.js +68 -0
- package/dist/workspace/storage-workspace.path.js.map +1 -0
- package/dist/workspace/storage-workspace.types.d.ts +97 -0
- package/dist/workspace/storage-workspace.types.d.ts.map +1 -0
- package/dist/workspace/storage-workspace.types.js +20 -0
- package/dist/workspace/storage-workspace.types.js.map +1 -0
- package/package.json +23 -20
- package/dist/artifacts/artifact-storage.d.ts +0 -161
- package/dist/artifacts/artifact-storage.d.ts.map +0 -1
- package/dist/artifacts/artifact-storage.js +0 -452
- package/dist/artifacts/artifact-storage.js.map +0 -1
- package/dist/artifacts/crypto/codec.d.ts +0 -44
- package/dist/artifacts/crypto/codec.d.ts.map +0 -1
- package/dist/artifacts/crypto/codec.js +0 -273
- package/dist/artifacts/crypto/codec.js.map +0 -1
- package/dist/artifacts/crypto/context.d.ts +0 -48
- package/dist/artifacts/crypto/context.d.ts.map +0 -1
- package/dist/artifacts/crypto/context.js +0 -37
- package/dist/artifacts/crypto/context.js.map +0 -1
- package/dist/artifacts/crypto/dek-cache.d.ts +0 -38
- package/dist/artifacts/crypto/dek-cache.d.ts.map +0 -1
- package/dist/artifacts/crypto/dek-cache.js +0 -137
- package/dist/artifacts/crypto/dek-cache.js.map +0 -1
- package/dist/artifacts/crypto/index.d.ts +0 -25
- package/dist/artifacts/crypto/index.d.ts.map +0 -1
- package/dist/artifacts/crypto/index.js +0 -29
- package/dist/artifacts/crypto/index.js.map +0 -1
- package/dist/artifacts/crypto/key-provider.d.ts +0 -44
- package/dist/artifacts/crypto/key-provider.d.ts.map +0 -1
- package/dist/artifacts/crypto/key-provider.js +0 -36
- package/dist/artifacts/crypto/key-provider.js.map +0 -1
- package/dist/artifacts/crypto/kms-provider.d.ts +0 -16
- package/dist/artifacts/crypto/kms-provider.d.ts.map +0 -1
- package/dist/artifacts/crypto/kms-provider.js +0 -90
- package/dist/artifacts/crypto/kms-provider.js.map +0 -1
- package/dist/artifacts/crypto/local-provider.d.ts +0 -17
- package/dist/artifacts/crypto/local-provider.d.ts.map +0 -1
- package/dist/artifacts/crypto/local-provider.js +0 -58
- package/dist/artifacts/crypto/local-provider.js.map +0 -1
- package/dist/artifacts/env-config.d.ts +0 -15
- package/dist/artifacts/env-config.d.ts.map +0 -1
- package/dist/artifacts/env-config.js +0 -95
- package/dist/artifacts/env-config.js.map +0 -1
- package/dist/artifacts/index.d.ts +0 -6
- package/dist/artifacts/index.d.ts.map +0 -1
- package/dist/artifacts/index.js +0 -6
- package/dist/artifacts/index.js.map +0 -1
- package/dist/artifacts/nest/index.d.ts +0 -29
- package/dist/artifacts/nest/index.d.ts.map +0 -1
- package/dist/artifacts/nest/index.js +0 -129
- package/dist/artifacts/nest/index.js.map +0 -1
- package/dist/artifacts/object-store.d.ts +0 -27
- package/dist/artifacts/object-store.d.ts.map +0 -1
- package/dist/artifacts/object-store.js +0 -198
- package/dist/artifacts/object-store.js.map +0 -1
- package/dist/artifacts/storage-driver.d.ts +0 -48
- package/dist/artifacts/storage-driver.d.ts.map +0 -1
- package/dist/artifacts/storage-driver.js +0 -89
- package/dist/artifacts/storage-driver.js.map +0 -1
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto';
|
|
2
|
-
import { canonicalJson } from './context.js';
|
|
3
|
-
/** Hard policy bounds for cached plaintext data-encryption keys. */
|
|
4
|
-
export const MAX_DEK_CACHE_ENTRIES = 65_536;
|
|
5
|
-
export const MAX_DEK_CACHE_TTL_MS = 5 * 60_000;
|
|
6
|
-
/**
|
|
7
|
-
* Bounded in-memory LRU of unwrapped DEKs so the sandbox's per-request reads don't turn into a
|
|
8
|
-
* KMS Decrypt per object (ADR-0001 D6.2). Never persisted; cleared on shutdown. Deliberate
|
|
9
|
-
* T4-shaped trade: a compromised runtime could read this cache, but it can already call unwrap.
|
|
10
|
-
*
|
|
11
|
-
* Deviation from the ADR's literal wording, on purpose: the key is a length-prefixed hash of
|
|
12
|
-
* (kid, wdk, ctx), not sha256(wdk) — keying on wdk alone would let a cache hit bypass the very
|
|
13
|
-
* context check (KMS EncryptionContext / local wrap-AAD) that makes header-sourced scope
|
|
14
|
-
* trustworthy. Length prefixes are required because kid may contain NUL and wdk is arbitrary
|
|
15
|
-
* binary; delimiter concatenation would let distinct tuples collide before hashing.
|
|
16
|
-
*/
|
|
17
|
-
export class CachingKeyProvider {
|
|
18
|
-
inner;
|
|
19
|
-
cache = new Map();
|
|
20
|
-
inFlight = new Map();
|
|
21
|
-
max;
|
|
22
|
-
ttlMs;
|
|
23
|
-
generation = 0;
|
|
24
|
-
nextToken = 0;
|
|
25
|
-
constructor(inner, opts = {}) {
|
|
26
|
-
this.inner = inner;
|
|
27
|
-
this.max = opts.max ?? 1024;
|
|
28
|
-
this.ttlMs = opts.ttlMs ?? MAX_DEK_CACHE_TTL_MS;
|
|
29
|
-
if (!Number.isSafeInteger(this.max) ||
|
|
30
|
-
this.max < 1 ||
|
|
31
|
-
this.max > MAX_DEK_CACHE_ENTRIES) {
|
|
32
|
-
throw new RangeError(`DEK cache max must be an integer between 1 and ${MAX_DEK_CACHE_ENTRIES}`);
|
|
33
|
-
}
|
|
34
|
-
if (!Number.isFinite(this.ttlMs) ||
|
|
35
|
-
this.ttlMs <= 0 ||
|
|
36
|
-
this.ttlMs > MAX_DEK_CACHE_TTL_MS) {
|
|
37
|
-
throw new RangeError(`DEK cache ttlMs must be greater than 0 and at most ${MAX_DEK_CACHE_TTL_MS}`);
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
generateDataKey(ctx) {
|
|
41
|
-
// Writes always mint a fresh DEK (one per object write) — nothing to cache.
|
|
42
|
-
return this.inner.generateDataKey(ctx);
|
|
43
|
-
}
|
|
44
|
-
async unwrapDataKey(wdk, kid, ctx) {
|
|
45
|
-
const hash = createHash('sha256');
|
|
46
|
-
for (const part of [
|
|
47
|
-
Buffer.from(kid, 'utf8'),
|
|
48
|
-
wdk,
|
|
49
|
-
Buffer.from(canonicalJson(ctx), 'utf8'),
|
|
50
|
-
]) {
|
|
51
|
-
const length = Buffer.allocUnsafe(8);
|
|
52
|
-
length.writeBigUInt64BE(BigInt(part.byteLength));
|
|
53
|
-
hash.update(length).update(part);
|
|
54
|
-
}
|
|
55
|
-
const key = hash.digest('hex');
|
|
56
|
-
const hit = this.cache.get(key);
|
|
57
|
-
if (hit && hit.expiresAt > Date.now()) {
|
|
58
|
-
// Refresh recency (Map preserves insertion order — oldest entry is the first key).
|
|
59
|
-
this.cache.delete(key);
|
|
60
|
-
this.cache.set(key, hit);
|
|
61
|
-
return Buffer.from(hit.dek);
|
|
62
|
-
}
|
|
63
|
-
if (hit)
|
|
64
|
-
this.evict(key, hit);
|
|
65
|
-
const existingFlight = this.inFlight.get(key);
|
|
66
|
-
if (existingFlight !== undefined &&
|
|
67
|
-
existingFlight.generation === this.generation) {
|
|
68
|
-
const dek = await existingFlight.promise;
|
|
69
|
-
if (existingFlight.generation !== this.generation) {
|
|
70
|
-
throw new Error('DEK cache was cleared during unwrap');
|
|
71
|
-
}
|
|
72
|
-
return Buffer.from(dek);
|
|
73
|
-
}
|
|
74
|
-
const generation = this.generation;
|
|
75
|
-
const flight = {
|
|
76
|
-
generation,
|
|
77
|
-
promise: this.loadAndCache(key, wdk, kid, ctx, generation),
|
|
78
|
-
};
|
|
79
|
-
this.inFlight.set(key, flight);
|
|
80
|
-
try {
|
|
81
|
-
const dek = await flight.promise;
|
|
82
|
-
if (generation !== this.generation) {
|
|
83
|
-
throw new Error('DEK cache was cleared during unwrap');
|
|
84
|
-
}
|
|
85
|
-
return Buffer.from(dek);
|
|
86
|
-
}
|
|
87
|
-
finally {
|
|
88
|
-
if (this.inFlight.get(key) === flight)
|
|
89
|
-
this.inFlight.delete(key);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
async loadAndCache(key, wdk, kid, ctx, generation) {
|
|
93
|
-
const dek = await this.inner.unwrapDataKey(wdk, kid, ctx);
|
|
94
|
-
if (!Buffer.isBuffer(dek) || dek.length !== 32) {
|
|
95
|
-
if (Buffer.isBuffer(dek))
|
|
96
|
-
dek.fill(0);
|
|
97
|
-
throw new Error('inner key provider returned an invalid 32-byte DEK');
|
|
98
|
-
}
|
|
99
|
-
if (generation !== this.generation) {
|
|
100
|
-
dek.fill(0);
|
|
101
|
-
throw new Error('DEK cache was cleared during unwrap');
|
|
102
|
-
}
|
|
103
|
-
if (this.cache.size >= this.max) {
|
|
104
|
-
const oldest = this.cache.keys().next().value;
|
|
105
|
-
if (oldest !== undefined)
|
|
106
|
-
this.evict(oldest, this.cache.get(oldest));
|
|
107
|
-
}
|
|
108
|
-
const token = ++this.nextToken;
|
|
109
|
-
const timer = setTimeout(() => {
|
|
110
|
-
const entry = this.cache.get(key);
|
|
111
|
-
if (entry?.token === token)
|
|
112
|
-
this.evict(key, entry);
|
|
113
|
-
}, this.ttlMs);
|
|
114
|
-
timer.unref?.();
|
|
115
|
-
this.cache.set(key, {
|
|
116
|
-
dek,
|
|
117
|
-
expiresAt: Date.now() + this.ttlMs,
|
|
118
|
-
timer,
|
|
119
|
-
token,
|
|
120
|
-
});
|
|
121
|
-
return dek;
|
|
122
|
-
}
|
|
123
|
-
clear() {
|
|
124
|
-
for (const [key, entry] of this.cache)
|
|
125
|
-
this.evict(key, entry);
|
|
126
|
-
this.generation++;
|
|
127
|
-
this.inFlight.clear();
|
|
128
|
-
this.inner.clear();
|
|
129
|
-
}
|
|
130
|
-
evict(key, entry) {
|
|
131
|
-
if (entry !== undefined)
|
|
132
|
-
clearTimeout(entry.timer);
|
|
133
|
-
entry?.dek.fill(0);
|
|
134
|
-
this.cache.delete(key);
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
//# sourceMappingURL=dek-cache.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"dek-cache.js","sourceRoot":"","sources":["../../../src/artifacts/crypto/dek-cache.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash } from 'node:crypto';\n\nimport { canonicalJson, type EnvelopeContext } from './context.js';\nimport type { DataKey, KeyProvider } from './key-provider.js';\n\nexport interface DekCacheOptions {\n /** Max cached unwrapped DEKs (default 1024). */\n max?: number;\n /** Entry lifetime; ADR-0001 D6.2 caps this at 5 minutes (the default). */\n ttlMs?: number;\n}\n\n/** Hard policy bounds for cached plaintext data-encryption keys. */\nexport const MAX_DEK_CACHE_ENTRIES = 65_536;\nexport const MAX_DEK_CACHE_TTL_MS = 5 * 60_000;\n\n/**\n * Bounded in-memory LRU of unwrapped DEKs so the sandbox's per-request reads don't turn into a\n * KMS Decrypt per object (ADR-0001 D6.2). Never persisted; cleared on shutdown. Deliberate\n * T4-shaped trade: a compromised runtime could read this cache, but it can already call unwrap.\n *\n * Deviation from the ADR's literal wording, on purpose: the key is a length-prefixed hash of\n * (kid, wdk, ctx), not sha256(wdk) — keying on wdk alone would let a cache hit bypass the very\n * context check (KMS EncryptionContext / local wrap-AAD) that makes header-sourced scope\n * trustworthy. Length prefixes are required because kid may contain NUL and wdk is arbitrary\n * binary; delimiter concatenation would let distinct tuples collide before hashing.\n */\nexport class CachingKeyProvider implements KeyProvider {\n private readonly cache = new Map<\n string,\n {\n dek: Buffer;\n expiresAt: number;\n timer: ReturnType<typeof setTimeout>;\n token: number;\n }\n >();\n private readonly inFlight = new Map<\n string,\n { generation: number; promise: Promise<Buffer> }\n >();\n private readonly max: number;\n private readonly ttlMs: number;\n private generation = 0;\n private nextToken = 0;\n\n constructor(\n private readonly inner: KeyProvider,\n opts: DekCacheOptions = {},\n ) {\n this.max = opts.max ?? 1024;\n this.ttlMs = opts.ttlMs ?? MAX_DEK_CACHE_TTL_MS;\n if (\n !Number.isSafeInteger(this.max) ||\n this.max < 1 ||\n this.max > MAX_DEK_CACHE_ENTRIES\n ) {\n throw new RangeError(\n `DEK cache max must be an integer between 1 and ${MAX_DEK_CACHE_ENTRIES}`,\n );\n }\n if (\n !Number.isFinite(this.ttlMs) ||\n this.ttlMs <= 0 ||\n this.ttlMs > MAX_DEK_CACHE_TTL_MS\n ) {\n throw new RangeError(\n `DEK cache ttlMs must be greater than 0 and at most ${MAX_DEK_CACHE_TTL_MS}`,\n );\n }\n }\n\n generateDataKey(ctx: EnvelopeContext): Promise<DataKey> {\n // Writes always mint a fresh DEK (one per object write) — nothing to cache.\n return this.inner.generateDataKey(ctx);\n }\n\n async unwrapDataKey(\n wdk: Buffer,\n kid: string,\n ctx: EnvelopeContext,\n ): Promise<Buffer> {\n const hash = createHash('sha256');\n for (const part of [\n Buffer.from(kid, 'utf8'),\n wdk,\n Buffer.from(canonicalJson(ctx), 'utf8'),\n ]) {\n const length = Buffer.allocUnsafe(8);\n length.writeBigUInt64BE(BigInt(part.byteLength));\n hash.update(length).update(part);\n }\n const key = hash.digest('hex');\n const hit = this.cache.get(key);\n if (hit && hit.expiresAt > Date.now()) {\n // Refresh recency (Map preserves insertion order — oldest entry is the first key).\n this.cache.delete(key);\n this.cache.set(key, hit);\n return Buffer.from(hit.dek);\n }\n if (hit) this.evict(key, hit);\n\n const existingFlight = this.inFlight.get(key);\n if (\n existingFlight !== undefined &&\n existingFlight.generation === this.generation\n ) {\n const dek = await existingFlight.promise;\n if (existingFlight.generation !== this.generation) {\n throw new Error('DEK cache was cleared during unwrap');\n }\n return Buffer.from(dek);\n }\n\n const generation = this.generation;\n const flight = {\n generation,\n promise: this.loadAndCache(key, wdk, kid, ctx, generation),\n };\n this.inFlight.set(key, flight);\n try {\n const dek = await flight.promise;\n if (generation !== this.generation) {\n throw new Error('DEK cache was cleared during unwrap');\n }\n return Buffer.from(dek);\n } finally {\n if (this.inFlight.get(key) === flight) this.inFlight.delete(key);\n }\n }\n\n private async loadAndCache(\n key: string,\n wdk: Buffer,\n kid: string,\n ctx: EnvelopeContext,\n generation: number,\n ): Promise<Buffer> {\n const dek = await this.inner.unwrapDataKey(wdk, kid, ctx);\n if (!Buffer.isBuffer(dek) || dek.length !== 32) {\n if (Buffer.isBuffer(dek)) dek.fill(0);\n throw new Error('inner key provider returned an invalid 32-byte DEK');\n }\n if (generation !== this.generation) {\n dek.fill(0);\n throw new Error('DEK cache was cleared during unwrap');\n }\n if (this.cache.size >= this.max) {\n const oldest = this.cache.keys().next().value;\n if (oldest !== undefined) this.evict(oldest, this.cache.get(oldest));\n }\n const token = ++this.nextToken;\n const timer = setTimeout(() => {\n const entry = this.cache.get(key);\n if (entry?.token === token) this.evict(key, entry);\n }, this.ttlMs);\n timer.unref?.();\n this.cache.set(key, {\n dek,\n expiresAt: Date.now() + this.ttlMs,\n timer,\n token,\n });\n return dek;\n }\n\n clear(): void {\n for (const [key, entry] of this.cache) this.evict(key, entry);\n this.generation++;\n this.inFlight.clear();\n this.inner.clear();\n }\n\n private evict(\n key: string,\n entry?: { dek: Buffer; timer: ReturnType<typeof setTimeout> },\n ): void {\n if (entry !== undefined) clearTimeout(entry.timer);\n entry?.dek.fill(0);\n this.cache.delete(key);\n }\n}\n"]}
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { type DekCacheOptions } from './dek-cache.js';
|
|
2
|
-
import { type KeyProvider, type KeyProviderConfig } from './key-provider.js';
|
|
3
|
-
export { artifactScope, canonicalJson, orgScope, SCOPE_FROM_HEADER, uploadScope, type EnvelopeContext, type ExpectedContext, } from './context.js';
|
|
4
|
-
export { ENVELOPE_SUPPORTED_VERSIONS, ENVELOPE_WRITE_VERSION, ENVELOPE_MAX_OVERHEAD_BYTES, EnvelopeError, isEnvelope, open, seal, type EnvelopeFailureReason, type OpenOptions, type OpenResult, } from './codec.js';
|
|
5
|
-
export { CachingKeyProvider, type DekCacheOptions } from './dek-cache.js';
|
|
6
|
-
export { KEK_GENERATE_HINT, keyProviderConfigFromEnv, type DataKey, type KeyProvider, type KeyProviderConfig, } from './key-provider.js';
|
|
7
|
-
export { KmsKeyProvider } from './kms-provider.js';
|
|
8
|
-
export { LocalKeyProvider } from './local-provider.js';
|
|
9
|
-
/** Everything a storage backend needs to encrypt/decrypt. Required — there is no plaintext mode. */
|
|
10
|
-
export interface StorageCrypto {
|
|
11
|
-
keyProvider: KeyProvider;
|
|
12
|
-
/** SEC-04's migration-window flag; see OpenOptions.allowLegacyPlaintext. Default off. */
|
|
13
|
-
allowLegacyPlaintext?: boolean;
|
|
14
|
-
}
|
|
15
|
-
/** Build the configured provider wrapped in the process-wide DEK cache (ADR-0001 D6.2). */
|
|
16
|
-
export declare function createKeyProvider(cfg: KeyProviderConfig, opts?: {
|
|
17
|
-
cache?: DekCacheOptions;
|
|
18
|
-
}): KeyProvider;
|
|
19
|
-
/**
|
|
20
|
-
* One-call env→crypto resolution shared by the api and the sandbox (each constructs storage from
|
|
21
|
-
* env separately; this keeps their validation identical). Throws on misconfiguration — callers
|
|
22
|
-
* fail boot, following the SEC-00 gating pattern.
|
|
23
|
-
*/
|
|
24
|
-
export declare function storageCryptoFromEnv(env: NodeJS.ProcessEnv): StorageCrypto;
|
|
25
|
-
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/artifacts/crypto/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAsB,KAAK,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAC1E,OAAO,EAEL,KAAK,WAAW,EAChB,KAAK,iBAAiB,EACvB,MAAM,mBAAmB,CAAC;AAI3B,OAAO,EACL,aAAa,EACb,aAAa,EACb,QAAQ,EACR,iBAAiB,EACjB,WAAW,EACX,KAAK,eAAe,EACpB,KAAK,eAAe,GACrB,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,2BAA2B,EAC3B,sBAAsB,EACtB,2BAA2B,EAC3B,aAAa,EACb,UAAU,EACV,IAAI,EACJ,IAAI,EACJ,KAAK,qBAAqB,EAC1B,KAAK,WAAW,EAChB,KAAK,UAAU,GAChB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,kBAAkB,EAAE,KAAK,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAC1E,OAAO,EACL,iBAAiB,EACjB,wBAAwB,EACxB,KAAK,OAAO,EACZ,KAAK,WAAW,EAChB,KAAK,iBAAiB,GACvB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACnD,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAEvD,oGAAoG;AACpG,MAAM,WAAW,aAAa;IAC5B,WAAW,EAAE,WAAW,CAAC;IACzB,yFAAyF;IACzF,oBAAoB,CAAC,EAAE,OAAO,CAAC;CAChC;AAED,2FAA2F;AAC3F,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE,iBAAiB,EACtB,IAAI,CAAC,EAAE;IAAE,KAAK,CAAC,EAAE,eAAe,CAAA;CAAE,GACjC,WAAW,CAMb;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,CAAC,UAAU,GAAG,aAAa,CAK1E"}
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { CachingKeyProvider } from './dek-cache.js';
|
|
2
|
-
import { keyProviderConfigFromEnv, } from './key-provider.js';
|
|
3
|
-
import { KmsKeyProvider } from './kms-provider.js';
|
|
4
|
-
import { LocalKeyProvider } from './local-provider.js';
|
|
5
|
-
export { artifactScope, canonicalJson, orgScope, SCOPE_FROM_HEADER, uploadScope, } from './context.js';
|
|
6
|
-
export { ENVELOPE_SUPPORTED_VERSIONS, ENVELOPE_WRITE_VERSION, ENVELOPE_MAX_OVERHEAD_BYTES, EnvelopeError, isEnvelope, open, seal, } from './codec.js';
|
|
7
|
-
export { CachingKeyProvider } from './dek-cache.js';
|
|
8
|
-
export { KEK_GENERATE_HINT, keyProviderConfigFromEnv, } from './key-provider.js';
|
|
9
|
-
export { KmsKeyProvider } from './kms-provider.js';
|
|
10
|
-
export { LocalKeyProvider } from './local-provider.js';
|
|
11
|
-
/** Build the configured provider wrapped in the process-wide DEK cache (ADR-0001 D6.2). */
|
|
12
|
-
export function createKeyProvider(cfg, opts) {
|
|
13
|
-
const inner = cfg.kind === 'kms'
|
|
14
|
-
? new KmsKeyProvider(cfg.keyId, cfg.region)
|
|
15
|
-
: new LocalKeyProvider(cfg.name, cfg.kek);
|
|
16
|
-
return new CachingKeyProvider(inner, opts?.cache);
|
|
17
|
-
}
|
|
18
|
-
/**
|
|
19
|
-
* One-call env→crypto resolution shared by the api and the sandbox (each constructs storage from
|
|
20
|
-
* env separately; this keeps their validation identical). Throws on misconfiguration — callers
|
|
21
|
-
* fail boot, following the SEC-00 gating pattern.
|
|
22
|
-
*/
|
|
23
|
-
export function storageCryptoFromEnv(env) {
|
|
24
|
-
return {
|
|
25
|
-
keyProvider: createKeyProvider(keyProviderConfigFromEnv(env)),
|
|
26
|
-
allowLegacyPlaintext: env.ARTIFACT_ENCRYPTION_READ_LEGACY === 'true',
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
//# sourceMappingURL=index.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/artifacts/crypto/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAwB,MAAM,gBAAgB,CAAC;AAC1E,OAAO,EACL,wBAAwB,GAGzB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACnD,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAEvD,OAAO,EACL,aAAa,EACb,aAAa,EACb,QAAQ,EACR,iBAAiB,EACjB,WAAW,GAGZ,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,2BAA2B,EAC3B,sBAAsB,EACtB,2BAA2B,EAC3B,aAAa,EACb,UAAU,EACV,IAAI,EACJ,IAAI,GAIL,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,kBAAkB,EAAwB,MAAM,gBAAgB,CAAC;AAC1E,OAAO,EACL,iBAAiB,EACjB,wBAAwB,GAIzB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACnD,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AASvD,2FAA2F;AAC3F,MAAM,UAAU,iBAAiB,CAC/B,GAAsB,EACtB,IAAkC;IAElC,MAAM,KAAK,GACT,GAAG,CAAC,IAAI,KAAK,KAAK;QAChB,CAAC,CAAC,IAAI,cAAc,CAAC,GAAG,CAAC,KAAK,EAAE,GAAG,CAAC,MAAM,CAAC;QAC3C,CAAC,CAAC,IAAI,gBAAgB,CAAC,GAAG,CAAC,IAAI,EAAE,GAAG,CAAC,GAAG,CAAC,CAAC;IAC9C,OAAO,IAAI,kBAAkB,CAAC,KAAK,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC;AACpD,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,oBAAoB,CAAC,GAAsB;IACzD,OAAO;QACL,WAAW,EAAE,iBAAiB,CAAC,wBAAwB,CAAC,GAAG,CAAC,CAAC;QAC7D,oBAAoB,EAAE,GAAG,CAAC,+BAA+B,KAAK,MAAM;KACrE,CAAC;AACJ,CAAC","sourcesContent":["import { CachingKeyProvider, type DekCacheOptions } from './dek-cache.js';\nimport {\n keyProviderConfigFromEnv,\n type KeyProvider,\n type KeyProviderConfig,\n} from './key-provider.js';\nimport { KmsKeyProvider } from './kms-provider.js';\nimport { LocalKeyProvider } from './local-provider.js';\n\nexport {\n artifactScope,\n canonicalJson,\n orgScope,\n SCOPE_FROM_HEADER,\n uploadScope,\n type EnvelopeContext,\n type ExpectedContext,\n} from './context.js';\nexport {\n ENVELOPE_SUPPORTED_VERSIONS,\n ENVELOPE_WRITE_VERSION,\n ENVELOPE_MAX_OVERHEAD_BYTES,\n EnvelopeError,\n isEnvelope,\n open,\n seal,\n type EnvelopeFailureReason,\n type OpenOptions,\n type OpenResult,\n} from './codec.js';\nexport { CachingKeyProvider, type DekCacheOptions } from './dek-cache.js';\nexport {\n KEK_GENERATE_HINT,\n keyProviderConfigFromEnv,\n type DataKey,\n type KeyProvider,\n type KeyProviderConfig,\n} from './key-provider.js';\nexport { KmsKeyProvider } from './kms-provider.js';\nexport { LocalKeyProvider } from './local-provider.js';\n\n/** Everything a storage backend needs to encrypt/decrypt. Required — there is no plaintext mode. */\nexport interface StorageCrypto {\n keyProvider: KeyProvider;\n /** SEC-04's migration-window flag; see OpenOptions.allowLegacyPlaintext. Default off. */\n allowLegacyPlaintext?: boolean;\n}\n\n/** Build the configured provider wrapped in the process-wide DEK cache (ADR-0001 D6.2). */\nexport function createKeyProvider(\n cfg: KeyProviderConfig,\n opts?: { cache?: DekCacheOptions },\n): KeyProvider {\n const inner =\n cfg.kind === 'kms'\n ? new KmsKeyProvider(cfg.keyId, cfg.region)\n : new LocalKeyProvider(cfg.name, cfg.kek);\n return new CachingKeyProvider(inner, opts?.cache);\n}\n\n/**\n * One-call env→crypto resolution shared by the api and the sandbox (each constructs storage from\n * env separately; this keeps their validation identical). Throws on misconfiguration — callers\n * fail boot, following the SEC-00 gating pattern.\n */\nexport function storageCryptoFromEnv(env: NodeJS.ProcessEnv): StorageCrypto {\n return {\n keyProvider: createKeyProvider(keyProviderConfigFromEnv(env)),\n allowLegacyPlaintext: env.ARTIFACT_ENCRYPTION_READ_LEGACY === 'true',\n };\n}\n"]}
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import type { EnvelopeContext } from './context.js';
|
|
2
|
-
/** A fresh per-object data key: the plaintext DEK plus its wrapped form and the wrapping key id. */
|
|
3
|
-
export interface DataKey {
|
|
4
|
-
/** 32-byte AES-256 key. Callers own this buffer and must zero it after use. */
|
|
5
|
-
dek: Buffer;
|
|
6
|
-
/** The DEK wrapped by the provider's KEK/CMK; safe to persist in the envelope header. */
|
|
7
|
-
wdk: Buffer;
|
|
8
|
-
/** Wrapping key identifier (`local:<name>` or the KMS key ARN). */
|
|
9
|
-
kid: string;
|
|
10
|
-
}
|
|
11
|
-
/**
|
|
12
|
-
* Pluggable wrap/unwrap of per-object DEKs (ADR-0001 D4). Both operations take the full
|
|
13
|
-
* EnvelopeContext and MUST bind it to the wrap (KMS EncryptionContext / local wrap-AAD): that
|
|
14
|
-
* binding is what makes the sandbox's header-sourced `scope` trustworthy — see SCOPE_FROM_HEADER.
|
|
15
|
-
* There is deliberately no `none` implementation; unenveloped writes must not exist.
|
|
16
|
-
*/
|
|
17
|
-
export interface KeyProvider {
|
|
18
|
-
generateDataKey(ctx: EnvelopeContext): Promise<DataKey>;
|
|
19
|
-
/**
|
|
20
|
-
* Unwrap a stored DEK. Must reject a `kid` that does not belong to this provider's configured
|
|
21
|
-
* key — a hostile header can never select the decrypting key. Returns a caller-owned copy.
|
|
22
|
-
*/
|
|
23
|
-
unwrapDataKey(wdk: Buffer, kid: string, ctx: EnvelopeContext): Promise<Buffer>;
|
|
24
|
-
/** Drop and zero any cached key material (shutdown hook). */
|
|
25
|
-
clear(): void;
|
|
26
|
-
}
|
|
27
|
-
export type KeyProviderConfig = {
|
|
28
|
-
kind: 'local';
|
|
29
|
-
name: string;
|
|
30
|
-
kek: Buffer;
|
|
31
|
-
} | {
|
|
32
|
-
kind: 'kms';
|
|
33
|
-
keyId: string;
|
|
34
|
-
region?: string;
|
|
35
|
-
};
|
|
36
|
-
/** One-liner shown wherever a missing/invalid KEK aborts boot. */
|
|
37
|
-
export declare const KEK_GENERATE_HINT = "generate one with: node -e \"console.log(require('node:crypto').randomBytes(32).toString('base64'))\"";
|
|
38
|
-
/**
|
|
39
|
-
* Shared env→config validation for the api and the sandbox (both construct storage from env
|
|
40
|
-
* separately; this is the single source of truth, same role as the S3 both-or-neither rule).
|
|
41
|
-
* Throws on any misconfiguration — boot must fail, never degrade to plaintext (ADR D4).
|
|
42
|
-
*/
|
|
43
|
-
export declare function keyProviderConfigFromEnv(env: NodeJS.ProcessEnv): KeyProviderConfig;
|
|
44
|
-
//# sourceMappingURL=key-provider.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"key-provider.d.ts","sourceRoot":"","sources":["../../../src/artifacts/crypto/key-provider.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAEpD,oGAAoG;AACpG,MAAM,WAAW,OAAO;IACtB,+EAA+E;IAC/E,GAAG,EAAE,MAAM,CAAC;IACZ,yFAAyF;IACzF,GAAG,EAAE,MAAM,CAAC;IACZ,mEAAmE;IACnE,GAAG,EAAE,MAAM,CAAC;CACb;AAED;;;;;GAKG;AACH,MAAM,WAAW,WAAW;IAC1B,eAAe,CAAC,GAAG,EAAE,eAAe,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACxD;;;OAGG;IACH,aAAa,CACX,GAAG,EAAE,MAAM,EACX,GAAG,EAAE,MAAM,EACX,GAAG,EAAE,eAAe,GACnB,OAAO,CAAC,MAAM,CAAC,CAAC;IACnB,6DAA6D;IAC7D,KAAK,IAAI,IAAI,CAAC;CACf;AAED,MAAM,MAAM,iBAAiB,GACzB;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAC5C;IAAE,IAAI,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAEpD,kEAAkE;AAClE,eAAO,MAAM,iBAAiB,0GAC2E,CAAC;AAE1G;;;;GAIG;AACH,wBAAgB,wBAAwB,CACtC,GAAG,EAAE,MAAM,CAAC,UAAU,GACrB,iBAAiB,CAqCnB"}
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
/** One-liner shown wherever a missing/invalid KEK aborts boot. */
|
|
2
|
-
export const KEK_GENERATE_HINT = "generate one with: node -e \"console.log(require('node:crypto').randomBytes(32).toString('base64'))\"";
|
|
3
|
-
/**
|
|
4
|
-
* Shared env→config validation for the api and the sandbox (both construct storage from env
|
|
5
|
-
* separately; this is the single source of truth, same role as the S3 both-or-neither rule).
|
|
6
|
-
* Throws on any misconfiguration — boot must fail, never degrade to plaintext (ADR D4).
|
|
7
|
-
*/
|
|
8
|
-
export function keyProviderConfigFromEnv(env) {
|
|
9
|
-
const provider = env.ARTIFACT_KEY_PROVIDER;
|
|
10
|
-
if (provider === 'local') {
|
|
11
|
-
if (!env.ARTIFACT_KEK) {
|
|
12
|
-
throw new Error(`ARTIFACT_KEY_PROVIDER=local requires ARTIFACT_KEK (base64, 32 bytes); ${KEK_GENERATE_HINT}`);
|
|
13
|
-
}
|
|
14
|
-
const kek = Buffer.from(env.ARTIFACT_KEK, 'base64');
|
|
15
|
-
// Round-trip the decode: Buffer.from(_, "base64") silently tolerates garbage.
|
|
16
|
-
if (kek.length !== 32 ||
|
|
17
|
-
kek.toString('base64').replace(/=+$/, '') !==
|
|
18
|
-
env.ARTIFACT_KEK.replace(/=+$/, '')) {
|
|
19
|
-
throw new Error(`ARTIFACT_KEK must be exactly 32 random bytes, base64-encoded; ${KEK_GENERATE_HINT}`);
|
|
20
|
-
}
|
|
21
|
-
return { kind: 'local', name: env.ARTIFACT_KEK_NAME || 'default', kek };
|
|
22
|
-
}
|
|
23
|
-
if (provider === 'kms') {
|
|
24
|
-
if (!env.ARTIFACT_KMS_KEY_ID) {
|
|
25
|
-
throw new Error('ARTIFACT_KEY_PROVIDER=kms requires ARTIFACT_KMS_KEY_ID (KMS key id, ARN, or alias)');
|
|
26
|
-
}
|
|
27
|
-
const region = env.ARTIFACT_KMS_REGION || env.S3_REGION;
|
|
28
|
-
return {
|
|
29
|
-
kind: 'kms',
|
|
30
|
-
keyId: env.ARTIFACT_KMS_KEY_ID,
|
|
31
|
-
...(region === undefined ? {} : { region }),
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
throw new Error('ARTIFACT_KEY_PROVIDER must be "local" or "kms" — artifact storage is always encrypted and has no plaintext mode (ADR-0001)');
|
|
35
|
-
}
|
|
36
|
-
//# sourceMappingURL=key-provider.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"key-provider.js","sourceRoot":"","sources":["../../../src/artifacts/crypto/key-provider.ts"],"names":[],"mappings":"AAqCA,kEAAkE;AAClE,MAAM,CAAC,MAAM,iBAAiB,GAC5B,uGAAuG,CAAC;AAE1G;;;;GAIG;AACH,MAAM,UAAU,wBAAwB,CACtC,GAAsB;IAEtB,MAAM,QAAQ,GAAG,GAAG,CAAC,qBAAqB,CAAC;IAC3C,IAAI,QAAQ,KAAK,OAAO,EAAE,CAAC;QACzB,IAAI,CAAC,GAAG,CAAC,YAAY,EAAE,CAAC;YACtB,MAAM,IAAI,KAAK,CACb,yEAAyE,iBAAiB,EAAE,CAC7F,CAAC;QACJ,CAAC;QACD,MAAM,GAAG,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,YAAY,EAAE,QAAQ,CAAC,CAAC;QACpD,8EAA8E;QAC9E,IACE,GAAG,CAAC,MAAM,KAAK,EAAE;YACjB,GAAG,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC;gBACvC,GAAG,CAAC,YAAY,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,EACrC,CAAC;YACD,MAAM,IAAI,KAAK,CACb,iEAAiE,iBAAiB,EAAE,CACrF,CAAC;QACJ,CAAC;QACD,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,GAAG,CAAC,iBAAiB,IAAI,SAAS,EAAE,GAAG,EAAE,CAAC;IAC1E,CAAC;IACD,IAAI,QAAQ,KAAK,KAAK,EAAE,CAAC;QACvB,IAAI,CAAC,GAAG,CAAC,mBAAmB,EAAE,CAAC;YAC7B,MAAM,IAAI,KAAK,CACb,oFAAoF,CACrF,CAAC;QACJ,CAAC;QACD,MAAM,MAAM,GAAG,GAAG,CAAC,mBAAmB,IAAI,GAAG,CAAC,SAAS,CAAC;QACxD,OAAO;YACL,IAAI,EAAE,KAAK;YACX,KAAK,EAAE,GAAG,CAAC,mBAAmB;YAC9B,GAAG,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC;SAC5C,CAAC;IACJ,CAAC;IACD,MAAM,IAAI,KAAK,CACb,4HAA4H,CAC7H,CAAC;AACJ,CAAC","sourcesContent":["import type { EnvelopeContext } from './context.js';\n\n/** A fresh per-object data key: the plaintext DEK plus its wrapped form and the wrapping key id. */\nexport interface DataKey {\n /** 32-byte AES-256 key. Callers own this buffer and must zero it after use. */\n dek: Buffer;\n /** The DEK wrapped by the provider's KEK/CMK; safe to persist in the envelope header. */\n wdk: Buffer;\n /** Wrapping key identifier (`local:<name>` or the KMS key ARN). */\n kid: string;\n}\n\n/**\n * Pluggable wrap/unwrap of per-object DEKs (ADR-0001 D4). Both operations take the full\n * EnvelopeContext and MUST bind it to the wrap (KMS EncryptionContext / local wrap-AAD): that\n * binding is what makes the sandbox's header-sourced `scope` trustworthy — see SCOPE_FROM_HEADER.\n * There is deliberately no `none` implementation; unenveloped writes must not exist.\n */\nexport interface KeyProvider {\n generateDataKey(ctx: EnvelopeContext): Promise<DataKey>;\n /**\n * Unwrap a stored DEK. Must reject a `kid` that does not belong to this provider's configured\n * key — a hostile header can never select the decrypting key. Returns a caller-owned copy.\n */\n unwrapDataKey(\n wdk: Buffer,\n kid: string,\n ctx: EnvelopeContext,\n ): Promise<Buffer>;\n /** Drop and zero any cached key material (shutdown hook). */\n clear(): void;\n}\n\nexport type KeyProviderConfig =\n | { kind: 'local'; name: string; kek: Buffer }\n | { kind: 'kms'; keyId: string; region?: string };\n\n/** One-liner shown wherever a missing/invalid KEK aborts boot. */\nexport const KEK_GENERATE_HINT =\n \"generate one with: node -e \\\"console.log(require('node:crypto').randomBytes(32).toString('base64'))\\\"\";\n\n/**\n * Shared env→config validation for the api and the sandbox (both construct storage from env\n * separately; this is the single source of truth, same role as the S3 both-or-neither rule).\n * Throws on any misconfiguration — boot must fail, never degrade to plaintext (ADR D4).\n */\nexport function keyProviderConfigFromEnv(\n env: NodeJS.ProcessEnv,\n): KeyProviderConfig {\n const provider = env.ARTIFACT_KEY_PROVIDER;\n if (provider === 'local') {\n if (!env.ARTIFACT_KEK) {\n throw new Error(\n `ARTIFACT_KEY_PROVIDER=local requires ARTIFACT_KEK (base64, 32 bytes); ${KEK_GENERATE_HINT}`,\n );\n }\n const kek = Buffer.from(env.ARTIFACT_KEK, 'base64');\n // Round-trip the decode: Buffer.from(_, \"base64\") silently tolerates garbage.\n if (\n kek.length !== 32 ||\n kek.toString('base64').replace(/=+$/, '') !==\n env.ARTIFACT_KEK.replace(/=+$/, '')\n ) {\n throw new Error(\n `ARTIFACT_KEK must be exactly 32 random bytes, base64-encoded; ${KEK_GENERATE_HINT}`,\n );\n }\n return { kind: 'local', name: env.ARTIFACT_KEK_NAME || 'default', kek };\n }\n if (provider === 'kms') {\n if (!env.ARTIFACT_KMS_KEY_ID) {\n throw new Error(\n 'ARTIFACT_KEY_PROVIDER=kms requires ARTIFACT_KMS_KEY_ID (KMS key id, ARN, or alias)',\n );\n }\n const region = env.ARTIFACT_KMS_REGION || env.S3_REGION;\n return {\n kind: 'kms',\n keyId: env.ARTIFACT_KMS_KEY_ID,\n ...(region === undefined ? {} : { region }),\n };\n }\n throw new Error(\n 'ARTIFACT_KEY_PROVIDER must be \"local\" or \"kms\" — artifact storage is always encrypted and has no plaintext mode (ADR-0001)',\n );\n}\n"]}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import type { EnvelopeContext } from './context.js';
|
|
2
|
-
import type { DataKey, KeyProvider } from './key-provider.js';
|
|
3
|
-
/**
|
|
4
|
-
* AWS KMS provider for deployed environments (ADR-0001 D4). Uses the SDK default credential
|
|
5
|
-
* chain (ECS task role), like the S3 backend. Requires SEC-02's CMK and task-role grants:
|
|
6
|
-
* api — GenerateDataKey/Decrypt/ReEncrypt*; sandbox — Decrypt only.
|
|
7
|
-
*/
|
|
8
|
-
export declare class KmsKeyProvider implements KeyProvider {
|
|
9
|
-
private readonly keyId;
|
|
10
|
-
private readonly client;
|
|
11
|
-
constructor(keyId: string, region?: string);
|
|
12
|
-
generateDataKey(ctx: EnvelopeContext): Promise<DataKey>;
|
|
13
|
-
unwrapDataKey(wdk: Buffer, kid: string, ctx: EnvelopeContext): Promise<Buffer>;
|
|
14
|
-
clear(): void;
|
|
15
|
-
}
|
|
16
|
-
//# sourceMappingURL=kms-provider.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"kms-provider.d.ts","sourceRoot":"","sources":["../../../src/artifacts/crypto/kms-provider.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AACpD,OAAO,KAAK,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAiC9D;;;;GAIG;AACH,qBAAa,cAAe,YAAW,WAAW;IAI9C,OAAO,CAAC,QAAQ,CAAC,KAAK;IAHxB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAY;IAEnC,YACmB,KAAK,EAAE,MAAM,EAC9B,MAAM,CAAC,EAAE,MAAM,EAGhB;IAEK,eAAe,CAAC,GAAG,EAAE,eAAe,GAAG,OAAO,CAAC,OAAO,CAAC,CAuB5D;IAEK,aAAa,CACjB,GAAG,EAAE,MAAM,EACX,GAAG,EAAE,MAAM,EACX,GAAG,EAAE,eAAe,GACnB,OAAO,CAAC,MAAM,CAAC,CAyBjB;IAED,KAAK,IAAI,IAAI,CAEZ;CACF"}
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
import { DecryptCommand, GenerateDataKeyCommand, KMSClient, } from '@aws-sdk/client-kms';
|
|
2
|
-
/**
|
|
3
|
-
* KMS EncryptionContext for the wrap (ADR-0001 D3): `{scope, artifactId}` — so CloudTrail
|
|
4
|
-
* records which tenant's which artifact every decrypt serves, and a wrapped DEK cannot be
|
|
5
|
-
* unwrapped under a different claimed context. KMS context values must be strings, so a null
|
|
6
|
-
* artifactId (ObjectStore objects) is omitted rather than stringified.
|
|
7
|
-
*/
|
|
8
|
-
function encryptionContext(ctx) {
|
|
9
|
-
return ctx.artifactId === null
|
|
10
|
-
? { scope: ctx.scope }
|
|
11
|
-
: { scope: ctx.scope, artifactId: ctx.artifactId };
|
|
12
|
-
}
|
|
13
|
-
function equivalentKmsKeyId(headerKid, resolvedKeyId) {
|
|
14
|
-
if (headerKid === resolvedKeyId)
|
|
15
|
-
return true;
|
|
16
|
-
// Multi-Region replicas share the same `mrk-…` resource id but have region-specific ARNs.
|
|
17
|
-
// Treat those replicas as one logical CMK only when partition and account also match.
|
|
18
|
-
const parseMrk = (value) => {
|
|
19
|
-
const match = /^arn:([^:]+):kms:[^:]+:([^:]+):key\/(mrk-[A-Za-z0-9-]+)$/.exec(value);
|
|
20
|
-
return match === null ? null : [match[1], match[2], match[3]];
|
|
21
|
-
};
|
|
22
|
-
const header = parseMrk(headerKid);
|
|
23
|
-
const resolved = parseMrk(resolvedKeyId);
|
|
24
|
-
return (header !== null &&
|
|
25
|
-
resolved !== null &&
|
|
26
|
-
header.every((part, index) => part === resolved[index]));
|
|
27
|
-
}
|
|
28
|
-
/**
|
|
29
|
-
* AWS KMS provider for deployed environments (ADR-0001 D4). Uses the SDK default credential
|
|
30
|
-
* chain (ECS task role), like the S3 backend. Requires SEC-02's CMK and task-role grants:
|
|
31
|
-
* api — GenerateDataKey/Decrypt/ReEncrypt*; sandbox — Decrypt only.
|
|
32
|
-
*/
|
|
33
|
-
export class KmsKeyProvider {
|
|
34
|
-
keyId;
|
|
35
|
-
client;
|
|
36
|
-
constructor(keyId, region) {
|
|
37
|
-
this.keyId = keyId;
|
|
38
|
-
this.client = new KMSClient(region ? { region } : {});
|
|
39
|
-
}
|
|
40
|
-
async generateDataKey(ctx) {
|
|
41
|
-
const out = await this.client.send(new GenerateDataKeyCommand({
|
|
42
|
-
KeyId: this.keyId,
|
|
43
|
-
KeySpec: 'AES_256',
|
|
44
|
-
EncryptionContext: encryptionContext(ctx),
|
|
45
|
-
}));
|
|
46
|
-
if (!out.Plaintext || !out.CiphertextBlob || !out.KeyId) {
|
|
47
|
-
out.Plaintext?.fill(0);
|
|
48
|
-
throw new Error('KMS GenerateDataKey returned an incomplete response');
|
|
49
|
-
}
|
|
50
|
-
// kid is the resolved key ARN (not the configured alias) so envelopes are self-describing
|
|
51
|
-
// for SEC-04's rewrap tooling.
|
|
52
|
-
try {
|
|
53
|
-
return {
|
|
54
|
-
dek: Buffer.from(out.Plaintext),
|
|
55
|
-
wdk: Buffer.from(out.CiphertextBlob),
|
|
56
|
-
kid: out.KeyId,
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
finally {
|
|
60
|
-
out.Plaintext.fill(0);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
async unwrapDataKey(wdk, kid, ctx) {
|
|
64
|
-
// The header kid is attacker-influenced and is deliberately NOT passed to KMS: Decrypt is
|
|
65
|
-
// pinned to the configured key, so KMS itself rejects ciphertext wrapped under any other key
|
|
66
|
-
// (a stricter guarantee than string-comparing kid, which may be an alias vs the ARN).
|
|
67
|
-
const out = await this.client.send(new DecryptCommand({
|
|
68
|
-
KeyId: this.keyId,
|
|
69
|
-
CiphertextBlob: wdk,
|
|
70
|
-
EncryptionContext: encryptionContext(ctx),
|
|
71
|
-
}));
|
|
72
|
-
if (!out.Plaintext || !out.KeyId) {
|
|
73
|
-
out.Plaintext?.fill(0);
|
|
74
|
-
throw new Error('KMS Decrypt returned an incomplete response');
|
|
75
|
-
}
|
|
76
|
-
try {
|
|
77
|
-
if (!equivalentKmsKeyId(kid, out.KeyId)) {
|
|
78
|
-
throw new Error('KMS Decrypt resolved a key that does not match the envelope');
|
|
79
|
-
}
|
|
80
|
-
return Buffer.from(out.Plaintext);
|
|
81
|
-
}
|
|
82
|
-
finally {
|
|
83
|
-
out.Plaintext.fill(0);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
clear() {
|
|
87
|
-
this.client.destroy();
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
//# sourceMappingURL=kms-provider.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"kms-provider.js","sourceRoot":"","sources":["../../../src/artifacts/crypto/kms-provider.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n DecryptCommand,\n GenerateDataKeyCommand,\n KMSClient,\n} from '@aws-sdk/client-kms';\n\nimport type { EnvelopeContext } from './context.js';\nimport type { DataKey, KeyProvider } from './key-provider.js';\n\n/**\n * KMS EncryptionContext for the wrap (ADR-0001 D3): `{scope, artifactId}` — so CloudTrail\n * records which tenant's which artifact every decrypt serves, and a wrapped DEK cannot be\n * unwrapped under a different claimed context. KMS context values must be strings, so a null\n * artifactId (ObjectStore objects) is omitted rather than stringified.\n */\nfunction encryptionContext(ctx: EnvelopeContext): Record<string, string> {\n return ctx.artifactId === null\n ? { scope: ctx.scope }\n : { scope: ctx.scope, artifactId: ctx.artifactId };\n}\n\nfunction equivalentKmsKeyId(headerKid: string, resolvedKeyId: string): boolean {\n if (headerKid === resolvedKeyId) return true;\n\n // Multi-Region replicas share the same `mrk-…` resource id but have region-specific ARNs.\n // Treat those replicas as one logical CMK only when partition and account also match.\n const parseMrk = (value: string): [string, string, string] | null => {\n const match =\n /^arn:([^:]+):kms:[^:]+:([^:]+):key\\/(mrk-[A-Za-z0-9-]+)$/.exec(value);\n return match === null ? null : [match[1]!, match[2]!, match[3]!];\n };\n const header = parseMrk(headerKid);\n const resolved = parseMrk(resolvedKeyId);\n return (\n header !== null &&\n resolved !== null &&\n header.every((part, index) => part === resolved[index])\n );\n}\n\n/**\n * AWS KMS provider for deployed environments (ADR-0001 D4). Uses the SDK default credential\n * chain (ECS task role), like the S3 backend. Requires SEC-02's CMK and task-role grants:\n * api — GenerateDataKey/Decrypt/ReEncrypt*; sandbox — Decrypt only.\n */\nexport class KmsKeyProvider implements KeyProvider {\n private readonly client: KMSClient;\n\n constructor(\n private readonly keyId: string,\n region?: string,\n ) {\n this.client = new KMSClient(region ? { region } : {});\n }\n\n async generateDataKey(ctx: EnvelopeContext): Promise<DataKey> {\n const out = await this.client.send(\n new GenerateDataKeyCommand({\n KeyId: this.keyId,\n KeySpec: 'AES_256',\n EncryptionContext: encryptionContext(ctx),\n }),\n );\n if (!out.Plaintext || !out.CiphertextBlob || !out.KeyId) {\n out.Plaintext?.fill(0);\n throw new Error('KMS GenerateDataKey returned an incomplete response');\n }\n // kid is the resolved key ARN (not the configured alias) so envelopes are self-describing\n // for SEC-04's rewrap tooling.\n try {\n return {\n dek: Buffer.from(out.Plaintext),\n wdk: Buffer.from(out.CiphertextBlob),\n kid: out.KeyId,\n };\n } finally {\n out.Plaintext.fill(0);\n }\n }\n\n async unwrapDataKey(\n wdk: Buffer,\n kid: string,\n ctx: EnvelopeContext,\n ): Promise<Buffer> {\n // The header kid is attacker-influenced and is deliberately NOT passed to KMS: Decrypt is\n // pinned to the configured key, so KMS itself rejects ciphertext wrapped under any other key\n // (a stricter guarantee than string-comparing kid, which may be an alias vs the ARN).\n const out = await this.client.send(\n new DecryptCommand({\n KeyId: this.keyId,\n CiphertextBlob: wdk,\n EncryptionContext: encryptionContext(ctx),\n }),\n );\n if (!out.Plaintext || !out.KeyId) {\n out.Plaintext?.fill(0);\n throw new Error('KMS Decrypt returned an incomplete response');\n }\n try {\n if (!equivalentKmsKeyId(kid, out.KeyId)) {\n throw new Error(\n 'KMS Decrypt resolved a key that does not match the envelope',\n );\n }\n return Buffer.from(out.Plaintext);\n } finally {\n out.Plaintext.fill(0);\n }\n }\n\n clear(): void {\n this.client.destroy();\n }\n}\n"]}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { type EnvelopeContext } from './context.js';
|
|
2
|
-
import type { DataKey, KeyProvider } from './key-provider.js';
|
|
3
|
-
/**
|
|
4
|
-
* Static-KEK provider for dev, CI, and self-hosted fs deployments (ADR-0001 D4). The wrap is
|
|
5
|
-
* AES-256-GCM with the canonical context as AAD, so a wdk replayed under a different
|
|
6
|
-
* scope/artifact/path fails to unwrap — the same property KMS EncryptionContext gives the `kms`
|
|
7
|
-
* provider, and the property the sandbox's header-sourced scope relies on.
|
|
8
|
-
*/
|
|
9
|
-
export declare class LocalKeyProvider implements KeyProvider {
|
|
10
|
-
private readonly kek;
|
|
11
|
-
private readonly kid;
|
|
12
|
-
constructor(name: string, kek: Buffer);
|
|
13
|
-
generateDataKey(ctx: EnvelopeContext): Promise<DataKey>;
|
|
14
|
-
unwrapDataKey(wdk: Buffer, kid: string, ctx: EnvelopeContext): Promise<Buffer>;
|
|
15
|
-
clear(): void;
|
|
16
|
-
}
|
|
17
|
-
//# sourceMappingURL=local-provider.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"local-provider.d.ts","sourceRoot":"","sources":["../../../src/artifacts/crypto/local-provider.ts"],"names":[],"mappings":"AAEA,OAAO,EAAiB,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;AACnE,OAAO,KAAK,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAO9D;;;;;GAKG;AACH,qBAAa,gBAAiB,YAAW,WAAW;IAKhD,OAAO,CAAC,QAAQ,CAAC,GAAG;IAJtB,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAS;IAE7B,YACE,IAAI,EAAE,MAAM,EACK,GAAG,EAAE,MAAM,EAK7B;IAED,eAAe,CAAC,GAAG,EAAE,eAAe,GAAG,OAAO,CAAC,OAAO,CAAC,CAQtD;IAED,aAAa,CACX,GAAG,EAAE,MAAM,EACX,GAAG,EAAE,MAAM,EACX,GAAG,EAAE,eAAe,GACnB,OAAO,CAAC,MAAM,CAAC,CA2BjB;IAED,KAAK,IAAI,IAAI,CAEZ;CACF"}
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto';
|
|
2
|
-
import { canonicalJson } from './context.js';
|
|
3
|
-
// wdk layout: 12-byte IV ‖ 16-byte GCM tag ‖ 32-byte wrapped DEK.
|
|
4
|
-
const IV_LEN = 12;
|
|
5
|
-
const TAG_LEN = 16;
|
|
6
|
-
const DEK_LEN = 32;
|
|
7
|
-
/**
|
|
8
|
-
* Static-KEK provider for dev, CI, and self-hosted fs deployments (ADR-0001 D4). The wrap is
|
|
9
|
-
* AES-256-GCM with the canonical context as AAD, so a wdk replayed under a different
|
|
10
|
-
* scope/artifact/path fails to unwrap — the same property KMS EncryptionContext gives the `kms`
|
|
11
|
-
* provider, and the property the sandbox's header-sourced scope relies on.
|
|
12
|
-
*/
|
|
13
|
-
export class LocalKeyProvider {
|
|
14
|
-
kek;
|
|
15
|
-
kid;
|
|
16
|
-
constructor(name, kek) {
|
|
17
|
-
this.kek = kek;
|
|
18
|
-
if (kek.length !== 32)
|
|
19
|
-
throw new Error('local KEK must be exactly 32 bytes');
|
|
20
|
-
this.kid = `local:${name}`;
|
|
21
|
-
}
|
|
22
|
-
generateDataKey(ctx) {
|
|
23
|
-
const dek = randomBytes(DEK_LEN);
|
|
24
|
-
const iv = randomBytes(IV_LEN);
|
|
25
|
-
const cipher = createCipheriv('aes-256-gcm', this.kek, iv);
|
|
26
|
-
cipher.setAAD(Buffer.from(canonicalJson(ctx)));
|
|
27
|
-
const wrapped = Buffer.concat([cipher.update(dek), cipher.final()]);
|
|
28
|
-
const wdk = Buffer.concat([iv, cipher.getAuthTag(), wrapped]);
|
|
29
|
-
return Promise.resolve({ dek, wdk, kid: this.kid });
|
|
30
|
-
}
|
|
31
|
-
unwrapDataKey(wdk, kid, ctx) {
|
|
32
|
-
// A hostile header must never select the key: only our own kid is accepted.
|
|
33
|
-
if (kid !== this.kid) {
|
|
34
|
-
return Promise.reject(new Error(`unknown key id "${kid}" (configured: "${this.kid}")`));
|
|
35
|
-
}
|
|
36
|
-
if (wdk.length !== IV_LEN + TAG_LEN + DEK_LEN) {
|
|
37
|
-
return Promise.reject(new Error('malformed wrapped data key'));
|
|
38
|
-
}
|
|
39
|
-
const iv = wdk.subarray(0, IV_LEN);
|
|
40
|
-
const tag = wdk.subarray(IV_LEN, IV_LEN + TAG_LEN);
|
|
41
|
-
const wrapped = wdk.subarray(IV_LEN + TAG_LEN);
|
|
42
|
-
const decipher = createDecipheriv('aes-256-gcm', this.kek, iv);
|
|
43
|
-
decipher.setAAD(Buffer.from(canonicalJson(ctx)));
|
|
44
|
-
decipher.setAuthTag(tag);
|
|
45
|
-
try {
|
|
46
|
-
return Promise.resolve(Buffer.concat([decipher.update(wrapped), decipher.final()]));
|
|
47
|
-
}
|
|
48
|
-
catch {
|
|
49
|
-
// GCM cannot distinguish wrong-context from corruption; both are "this key is not
|
|
50
|
-
// available for this context" — fail closed without detail.
|
|
51
|
-
return Promise.reject(new Error('wrapped data key failed to unwrap for this context'));
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
clear() {
|
|
55
|
-
// Stateless: the KEK itself must outlive clear() (it is the provider's configuration).
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
//# sourceMappingURL=local-provider.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"local-provider.js","sourceRoot":"","sources":["../../../src/artifacts/crypto/local-provider.ts"],"names":[],"mappings":"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","sourcesContent":["import { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto';\n\nimport { canonicalJson, type EnvelopeContext } from './context.js';\nimport type { DataKey, KeyProvider } from './key-provider.js';\n\n// wdk layout: 12-byte IV ‖ 16-byte GCM tag ‖ 32-byte wrapped DEK.\nconst IV_LEN = 12;\nconst TAG_LEN = 16;\nconst DEK_LEN = 32;\n\n/**\n * Static-KEK provider for dev, CI, and self-hosted fs deployments (ADR-0001 D4). The wrap is\n * AES-256-GCM with the canonical context as AAD, so a wdk replayed under a different\n * scope/artifact/path fails to unwrap — the same property KMS EncryptionContext gives the `kms`\n * provider, and the property the sandbox's header-sourced scope relies on.\n */\nexport class LocalKeyProvider implements KeyProvider {\n private readonly kid: string;\n\n constructor(\n name: string,\n private readonly kek: Buffer,\n ) {\n if (kek.length !== 32)\n throw new Error('local KEK must be exactly 32 bytes');\n this.kid = `local:${name}`;\n }\n\n generateDataKey(ctx: EnvelopeContext): Promise<DataKey> {\n const dek = randomBytes(DEK_LEN);\n const iv = randomBytes(IV_LEN);\n const cipher = createCipheriv('aes-256-gcm', this.kek, iv);\n cipher.setAAD(Buffer.from(canonicalJson(ctx)));\n const wrapped = Buffer.concat([cipher.update(dek), cipher.final()]);\n const wdk = Buffer.concat([iv, cipher.getAuthTag(), wrapped]);\n return Promise.resolve({ dek, wdk, kid: this.kid });\n }\n\n unwrapDataKey(\n wdk: Buffer,\n kid: string,\n ctx: EnvelopeContext,\n ): Promise<Buffer> {\n // A hostile header must never select the key: only our own kid is accepted.\n if (kid !== this.kid) {\n return Promise.reject(\n new Error(`unknown key id \"${kid}\" (configured: \"${this.kid}\")`),\n );\n }\n if (wdk.length !== IV_LEN + TAG_LEN + DEK_LEN) {\n return Promise.reject(new Error('malformed wrapped data key'));\n }\n const iv = wdk.subarray(0, IV_LEN);\n const tag = wdk.subarray(IV_LEN, IV_LEN + TAG_LEN);\n const wrapped = wdk.subarray(IV_LEN + TAG_LEN);\n const decipher = createDecipheriv('aes-256-gcm', this.kek, iv);\n decipher.setAAD(Buffer.from(canonicalJson(ctx)));\n decipher.setAuthTag(tag);\n try {\n return Promise.resolve(\n Buffer.concat([decipher.update(wrapped), decipher.final()]),\n );\n } catch {\n // GCM cannot distinguish wrong-context from corruption; both are \"this key is not\n // available for this context\" — fail closed without detail.\n return Promise.reject(\n new Error('wrapped data key failed to unwrap for this context'),\n );\n }\n }\n\n clear(): void {\n // Stateless: the KEK itself must outlive clear() (it is the provider's configuration).\n }\n}\n"]}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { type ArtifactStorageConfig } from './storage-driver.js';
|
|
2
|
-
/**
|
|
3
|
-
* Shared env→storage-config validation for every service that constructs storage (the api's
|
|
4
|
-
* zod-validated env delegates here with normalized values; the sandbox passes process.env
|
|
5
|
-
* directly). One source of truth for the rules — the two services once drifted by
|
|
6
|
-
* re-implementing them independently.
|
|
7
|
-
*
|
|
8
|
-
* `STORAGE_PROVIDER` names any provider `@nestm/storage` can build (`fs`, `s3`, `gcs`, `azure`,
|
|
9
|
-
* `r2`, `minio`, `supabase`, …); the `STORAGE_*` values below are one flat bag that each provider
|
|
10
|
-
* reads what it needs from. The platform is not S3-only: it used to be, because storage shipped
|
|
11
|
-
* exactly two hand-written drivers, and the old `ARTIFACT_STORAGE=fs|s3` switch chose between
|
|
12
|
-
* them. Those names are still accepted so a deployment can migrate without a coordinated restart.
|
|
13
|
-
*/
|
|
14
|
-
export declare function artifactStorageConfigFromEnv(env: NodeJS.ProcessEnv): ArtifactStorageConfig;
|
|
15
|
-
//# sourceMappingURL=env-config.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"env-config.d.ts","sourceRoot":"","sources":["../../src/artifacts/env-config.ts"],"names":[],"mappings":"AAEA,OAAO,EAGL,KAAK,qBAAqB,EAC3B,MAAM,qBAAqB,CAAC;AAE7B;;;;;;;;;;;GAWG;AACH,wBAAgB,4BAA4B,CAC1C,GAAG,EAAE,MAAM,CAAC,UAAU,GACrB,qBAAqB,CAuCvB"}
|