@nestm/storage 0.1.0-alpha.4 → 0.1.0-alpha.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/README.md +114 -0
- package/dist/artifacts/artifact-storage.d.ts +161 -0
- package/dist/artifacts/artifact-storage.d.ts.map +1 -0
- package/dist/artifacts/artifact-storage.js +452 -0
- package/dist/artifacts/artifact-storage.js.map +1 -0
- package/dist/artifacts/crypto/codec.d.ts +44 -0
- package/dist/artifacts/crypto/codec.d.ts.map +1 -0
- package/dist/artifacts/crypto/codec.js +273 -0
- package/dist/artifacts/crypto/codec.js.map +1 -0
- package/dist/artifacts/crypto/context.d.ts +48 -0
- package/dist/artifacts/crypto/context.d.ts.map +1 -0
- package/dist/artifacts/crypto/context.js +37 -0
- package/dist/artifacts/crypto/context.js.map +1 -0
- package/dist/artifacts/crypto/dek-cache.d.ts +38 -0
- package/dist/artifacts/crypto/dek-cache.d.ts.map +1 -0
- package/dist/artifacts/crypto/dek-cache.js +137 -0
- package/dist/artifacts/crypto/dek-cache.js.map +1 -0
- package/dist/artifacts/crypto/index.d.ts +25 -0
- package/dist/artifacts/crypto/index.d.ts.map +1 -0
- package/dist/artifacts/crypto/index.js +29 -0
- package/dist/artifacts/crypto/index.js.map +1 -0
- package/dist/artifacts/crypto/key-provider.d.ts +44 -0
- package/dist/artifacts/crypto/key-provider.d.ts.map +1 -0
- package/dist/artifacts/crypto/key-provider.js +36 -0
- package/dist/artifacts/crypto/key-provider.js.map +1 -0
- package/dist/artifacts/crypto/kms-provider.d.ts +16 -0
- package/dist/artifacts/crypto/kms-provider.d.ts.map +1 -0
- package/dist/artifacts/crypto/kms-provider.js +90 -0
- package/dist/artifacts/crypto/kms-provider.js.map +1 -0
- package/dist/artifacts/crypto/local-provider.d.ts +17 -0
- package/dist/artifacts/crypto/local-provider.d.ts.map +1 -0
- package/dist/artifacts/crypto/local-provider.js +58 -0
- package/dist/artifacts/crypto/local-provider.js.map +1 -0
- package/dist/artifacts/env-config.d.ts +15 -0
- package/dist/artifacts/env-config.d.ts.map +1 -0
- package/dist/artifacts/env-config.js +95 -0
- package/dist/artifacts/env-config.js.map +1 -0
- package/dist/artifacts/index.d.ts +6 -0
- package/dist/artifacts/index.d.ts.map +1 -0
- package/dist/artifacts/index.js +6 -0
- package/dist/artifacts/index.js.map +1 -0
- package/dist/artifacts/nest/index.d.ts +29 -0
- package/dist/artifacts/nest/index.d.ts.map +1 -0
- package/dist/artifacts/nest/index.js +129 -0
- package/dist/artifacts/nest/index.js.map +1 -0
- package/dist/artifacts/object-store.d.ts +27 -0
- package/dist/artifacts/object-store.d.ts.map +1 -0
- package/dist/artifacts/object-store.js +198 -0
- package/dist/artifacts/object-store.js.map +1 -0
- package/dist/artifacts/storage-driver.d.ts +48 -0
- package/dist/artifacts/storage-driver.d.ts.map +1 -0
- package/dist/artifacts/storage-driver.js +89 -0
- package/dist/artifacts/storage-driver.js.map +1 -0
- package/package.json +17 -1
|
@@ -0,0 +1,25 @@
|
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
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"}
|
|
@@ -0,0 +1,29 @@
|
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
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"]}
|
|
@@ -0,0 +1,44 @@
|
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
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"}
|
|
@@ -0,0 +1,36 @@
|
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
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"]}
|
|
@@ -0,0 +1,16 @@
|
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
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"}
|
|
@@ -0,0 +1,90 @@
|
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
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"]}
|
|
@@ -0,0 +1,17 @@
|
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
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"}
|
|
@@ -0,0 +1,58 @@
|
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"local-provider.js","sourceRoot":"","sources":["../../../src/artifacts/crypto/local-provider.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE5E,OAAO,EAAE,aAAa,EAAwB,MAAM,cAAc,CAAC;AAGnE,kEAAkE;AAClE,MAAM,MAAM,GAAG,EAAE,CAAC;AAClB,MAAM,OAAO,GAAG,EAAE,CAAC;AACnB,MAAM,OAAO,GAAG,EAAE,CAAC;AAEnB;;;;;GAKG;AACH,MAAM,OAAO,gBAAgB;IAKR,GAAG;IAJL,GAAG,CAAS;IAE7B,YACE,IAAY,EACK,GAAW;mBAAX,GAAG;QAEpB,IAAI,GAAG,CAAC,MAAM,KAAK,EAAE;YACnB,MAAM,IAAI,KAAK,CAAC,oCAAoC,CAAC,CAAC;QACxD,IAAI,CAAC,GAAG,GAAG,SAAS,IAAI,EAAE,CAAC;IAC7B,CAAC;IAED,eAAe,CAAC,GAAoB;QAClC,MAAM,GAAG,GAAG,WAAW,CAAC,OAAO,CAAC,CAAC;QACjC,MAAM,EAAE,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC;QAC/B,MAAM,MAAM,GAAG,cAAc,CAAC,aAAa,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;QAC3D,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;QAC/C,MAAM,OAAO,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;QACpE,MAAM,GAAG,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,EAAE,MAAM,CAAC,UAAU,EAAE,EAAE,OAAO,CAAC,CAAC,CAAC;QAC9D,OAAO,OAAO,CAAC,OAAO,CAAC,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;IACtD,CAAC;IAED,aAAa,CACX,GAAW,EACX,GAAW,EACX,GAAoB;QAEpB,4EAA4E;QAC5E,IAAI,GAAG,KAAK,IAAI,CAAC,GAAG,EAAE,CAAC;YACrB,OAAO,OAAO,CAAC,MAAM,CACnB,IAAI,KAAK,CAAC,mBAAmB,GAAG,mBAAmB,IAAI,CAAC,GAAG,IAAI,CAAC,CACjE,CAAC;QACJ,CAAC;QACD,IAAI,GAAG,CAAC,MAAM,KAAK,MAAM,GAAG,OAAO,GAAG,OAAO,EAAE,CAAC;YAC9C,OAAO,OAAO,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,4BAA4B,CAAC,CAAC,CAAC;QACjE,CAAC;QACD,MAAM,EAAE,GAAG,GAAG,CAAC,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;QACnC,MAAM,GAAG,GAAG,GAAG,CAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC;QACnD,MAAM,OAAO,GAAG,GAAG,CAAC,QAAQ,CAAC,MAAM,GAAG,OAAO,CAAC,CAAC;QAC/C,MAAM,QAAQ,GAAG,gBAAgB,CAAC,aAAa,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;QAC/D,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;QACjD,QAAQ,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;QACzB,IAAI,CAAC;YACH,OAAO,OAAO,CAAC,OAAO,CACpB,MAAM,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAC,CAC5D,CAAC;QACJ,CAAC;QAAC,MAAM,CAAC;YACP,kFAAkF;YAClF,4DAA4D;YAC5D,OAAO,OAAO,CAAC,MAAM,CACnB,IAAI,KAAK,CAAC,oDAAoD,CAAC,CAChE,CAAC;QACJ,CAAC;IACH,CAAC;IAED,KAAK;QACH,uFAAuF;IACzF,CAAC;CACF","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"]}
|
|
@@ -0,0 +1,15 @@
|
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
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"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { assertStorageProvider, defaultFsRoot, } 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 function artifactStorageConfigFromEnv(env) {
|
|
15
|
+
const provider = assertStorageProvider(env.STORAGE_PROVIDER || legacyProvider(env) || 'fs');
|
|
16
|
+
const prefix = env.STORAGE_PREFIX || env.S3_PREFIX || undefined;
|
|
17
|
+
const config = flatConfig(env);
|
|
18
|
+
// Both-or-neither: both set → static credentials (an IAM user, or any S3-compatible store);
|
|
19
|
+
// both unset → the provider SDK's own chain (the ECS task role, Application Default
|
|
20
|
+
// Credentials, a shared profile) so no long-lived keys sit in env.
|
|
21
|
+
if (!config.accessKeyId !== !config.secretAccessKey) {
|
|
22
|
+
throw new Error("STORAGE_ACCESS_KEY_ID and STORAGE_SECRET_ACCESS_KEY must be set together (omit both to use the provider's default credential chain)");
|
|
23
|
+
}
|
|
24
|
+
if (provider === 'fs') {
|
|
25
|
+
return {
|
|
26
|
+
provider,
|
|
27
|
+
...(prefix ? { prefix } : {}),
|
|
28
|
+
config: { root: config.root ?? defaultFsRoot() },
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
// `root` is filesystem-only, and the api's schema always defaults one. Carrying it onto an
|
|
32
|
+
// object-store provider would both litter the config and make the guard below think a bucketless
|
|
33
|
+
// deployment was configured.
|
|
34
|
+
const { root: _fsOnly, ...objectStoreConfig } = config;
|
|
35
|
+
// Generic "you configured nothing" guard. Which field a provider actually needs is the
|
|
36
|
+
// adapter's business — it fails closed on its own — but an empty config is always a
|
|
37
|
+
// deployment mistake, and catching it here names the variable to set.
|
|
38
|
+
if (!identifiesAStore(objectStoreConfig)) {
|
|
39
|
+
throw new Error(`STORAGE_PROVIDER=${provider} needs somewhere to write: set STORAGE_BUCKET (or the container/store field that provider uses)`);
|
|
40
|
+
}
|
|
41
|
+
return { provider, ...(prefix ? { prefix } : {}), config: objectStoreConfig };
|
|
42
|
+
}
|
|
43
|
+
/** The retired `ARTIFACT_STORAGE=fs|s3` switch, mapped onto a provider slug. */
|
|
44
|
+
function legacyProvider(env) {
|
|
45
|
+
return env.ARTIFACT_STORAGE || undefined;
|
|
46
|
+
}
|
|
47
|
+
function identifiesAStore(config) {
|
|
48
|
+
return [
|
|
49
|
+
config.bucket,
|
|
50
|
+
config.container,
|
|
51
|
+
config.connectionString,
|
|
52
|
+
config.storeName,
|
|
53
|
+
config.token,
|
|
54
|
+
config.url,
|
|
55
|
+
].some((value) => typeof value === 'string' && value.length > 0);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* One flat bag of provider settings, assembled from `STORAGE_*` with the retired `S3_*` and
|
|
59
|
+
* `ARTIFACTS_DIR` names as fallbacks. Absent values are omitted rather than set to `undefined`,
|
|
60
|
+
* so a provider's own credential chain stays reachable.
|
|
61
|
+
*/
|
|
62
|
+
function flatConfig(env) {
|
|
63
|
+
const entries = [
|
|
64
|
+
['accessKeyId', env.STORAGE_ACCESS_KEY_ID || env.S3_ACCESS_KEY_ID],
|
|
65
|
+
['accountId', env.STORAGE_ACCOUNT_ID],
|
|
66
|
+
['accountKey', env.STORAGE_ACCOUNT_KEY],
|
|
67
|
+
['accountName', env.STORAGE_ACCOUNT_NAME],
|
|
68
|
+
['bucket', env.STORAGE_BUCKET || env.S3_BUCKET],
|
|
69
|
+
['connectionString', env.STORAGE_CONNECTION_STRING],
|
|
70
|
+
['container', env.STORAGE_CONTAINER],
|
|
71
|
+
['endpoint', env.STORAGE_ENDPOINT || env.S3_ENDPOINT],
|
|
72
|
+
['keyFilename', env.STORAGE_KEY_FILENAME],
|
|
73
|
+
['projectId', env.STORAGE_PROJECT_ID],
|
|
74
|
+
['publicBaseUrl', env.STORAGE_PUBLIC_BASE_URL],
|
|
75
|
+
['region', env.STORAGE_REGION || env.S3_REGION],
|
|
76
|
+
['root', env.STORAGE_ROOT || env.ARTIFACTS_DIR],
|
|
77
|
+
[
|
|
78
|
+
'secretAccessKey',
|
|
79
|
+
env.STORAGE_SECRET_ACCESS_KEY || env.S3_SECRET_ACCESS_KEY,
|
|
80
|
+
],
|
|
81
|
+
['serviceRoleKey', env.STORAGE_SERVICE_ROLE_KEY],
|
|
82
|
+
['sessionToken', env.STORAGE_SESSION_TOKEN],
|
|
83
|
+
['siteId', env.STORAGE_SITE_ID],
|
|
84
|
+
['storeName', env.STORAGE_STORE_NAME],
|
|
85
|
+
['token', env.STORAGE_TOKEN],
|
|
86
|
+
['url', env.STORAGE_URL],
|
|
87
|
+
];
|
|
88
|
+
const forcePathStyle = env.STORAGE_FORCE_PATH_STYLE || env.S3_FORCE_PATH_STYLE;
|
|
89
|
+
if (forcePathStyle)
|
|
90
|
+
entries.push(['forcePathStyle', forcePathStyle === 'true']);
|
|
91
|
+
// One dynamic bag by design: the fields are provider-specific and the union is wider than any
|
|
92
|
+
// one deployment uses, so it is assembled by name rather than spelled out per provider.
|
|
93
|
+
return Object.fromEntries(entries.flatMap(([key, value]) => value === undefined || value === '' ? [] : [[key, value]]));
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=env-config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"env-config.js","sourceRoot":"","sources":["../../src/artifacts/env-config.ts"],"names":[],"mappings":"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","sourcesContent":["import type { StorageProviderConfig } from '../files-sdk/provider/index.js';\n\nimport {\n assertStorageProvider,\n defaultFsRoot,\n type ArtifactStorageConfig,\n} from './storage-driver.js';\n\n/**\n * Shared env→storage-config validation for every service that constructs storage (the api's\n * zod-validated env delegates here with normalized values; the sandbox passes process.env\n * directly). One source of truth for the rules — the two services once drifted by\n * re-implementing them independently.\n *\n * `STORAGE_PROVIDER` names any provider `@nestm/storage` can build (`fs`, `s3`, `gcs`, `azure`,\n * `r2`, `minio`, `supabase`, …); the `STORAGE_*` values below are one flat bag that each provider\n * reads what it needs from. The platform is not S3-only: it used to be, because storage shipped\n * exactly two hand-written drivers, and the old `ARTIFACT_STORAGE=fs|s3` switch chose between\n * them. Those names are still accepted so a deployment can migrate without a coordinated restart.\n */\nexport function artifactStorageConfigFromEnv(\n env: NodeJS.ProcessEnv,\n): ArtifactStorageConfig {\n const provider = assertStorageProvider(\n env.STORAGE_PROVIDER || legacyProvider(env) || 'fs',\n );\n const prefix = env.STORAGE_PREFIX || env.S3_PREFIX || undefined;\n const config = flatConfig(env);\n\n // Both-or-neither: both set → static credentials (an IAM user, or any S3-compatible store);\n // both unset → the provider SDK's own chain (the ECS task role, Application Default\n // Credentials, a shared profile) so no long-lived keys sit in env.\n if (!config.accessKeyId !== !config.secretAccessKey) {\n throw new Error(\n \"STORAGE_ACCESS_KEY_ID and STORAGE_SECRET_ACCESS_KEY must be set together (omit both to use the provider's default credential chain)\",\n );\n }\n\n if (provider === 'fs') {\n return {\n provider,\n ...(prefix ? { prefix } : {}),\n config: { root: config.root ?? defaultFsRoot() },\n };\n }\n\n // `root` is filesystem-only, and the api's schema always defaults one. Carrying it onto an\n // object-store provider would both litter the config and make the guard below think a bucketless\n // deployment was configured.\n const { root: _fsOnly, ...objectStoreConfig } = config;\n\n // Generic \"you configured nothing\" guard. Which field a provider actually needs is the\n // adapter's business — it fails closed on its own — but an empty config is always a\n // deployment mistake, and catching it here names the variable to set.\n if (!identifiesAStore(objectStoreConfig)) {\n throw new Error(\n `STORAGE_PROVIDER=${provider} needs somewhere to write: set STORAGE_BUCKET (or the container/store field that provider uses)`,\n );\n }\n\n return { provider, ...(prefix ? { prefix } : {}), config: objectStoreConfig };\n}\n\n/** The retired `ARTIFACT_STORAGE=fs|s3` switch, mapped onto a provider slug. */\nfunction legacyProvider(env: NodeJS.ProcessEnv): string | undefined {\n return env.ARTIFACT_STORAGE || undefined;\n}\n\nfunction identifiesAStore(config: StorageProviderConfig): boolean {\n return [\n config.bucket,\n config.container,\n config.connectionString,\n config.storeName,\n config.token,\n config.url,\n ].some((value) => typeof value === 'string' && value.length > 0);\n}\n\n/**\n * One flat bag of provider settings, assembled from `STORAGE_*` with the retired `S3_*` and\n * `ARTIFACTS_DIR` names as fallbacks. Absent values are omitted rather than set to `undefined`,\n * so a provider's own credential chain stays reachable.\n */\nfunction flatConfig(env: NodeJS.ProcessEnv): StorageProviderConfig {\n const entries: Array<[string, string | boolean | undefined]> = [\n ['accessKeyId', env.STORAGE_ACCESS_KEY_ID || env.S3_ACCESS_KEY_ID],\n ['accountId', env.STORAGE_ACCOUNT_ID],\n ['accountKey', env.STORAGE_ACCOUNT_KEY],\n ['accountName', env.STORAGE_ACCOUNT_NAME],\n ['bucket', env.STORAGE_BUCKET || env.S3_BUCKET],\n ['connectionString', env.STORAGE_CONNECTION_STRING],\n ['container', env.STORAGE_CONTAINER],\n ['endpoint', env.STORAGE_ENDPOINT || env.S3_ENDPOINT],\n ['keyFilename', env.STORAGE_KEY_FILENAME],\n ['projectId', env.STORAGE_PROJECT_ID],\n ['publicBaseUrl', env.STORAGE_PUBLIC_BASE_URL],\n ['region', env.STORAGE_REGION || env.S3_REGION],\n ['root', env.STORAGE_ROOT || env.ARTIFACTS_DIR],\n [\n 'secretAccessKey',\n env.STORAGE_SECRET_ACCESS_KEY || env.S3_SECRET_ACCESS_KEY,\n ],\n ['serviceRoleKey', env.STORAGE_SERVICE_ROLE_KEY],\n ['sessionToken', env.STORAGE_SESSION_TOKEN],\n ['siteId', env.STORAGE_SITE_ID],\n ['storeName', env.STORAGE_STORE_NAME],\n ['token', env.STORAGE_TOKEN],\n ['url', env.STORAGE_URL],\n ];\n\n const forcePathStyle =\n env.STORAGE_FORCE_PATH_STYLE || env.S3_FORCE_PATH_STYLE;\n if (forcePathStyle)\n entries.push(['forcePathStyle', forcePathStyle === 'true']);\n\n // One dynamic bag by design: the fields are provider-specific and the union is wider than any\n // one deployment uses, so it is assembled by name rather than spelled out per provider.\n return Object.fromEntries(\n entries.flatMap(([key, value]) =>\n value === undefined || value === '' ? [] : [[key, value]],\n ),\n ) as StorageProviderConfig;\n}\n"]}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export * from './artifact-storage.js';
|
|
2
|
+
export * from './crypto/index.js';
|
|
3
|
+
export * from './env-config.js';
|
|
4
|
+
export * from './object-store.js';
|
|
5
|
+
export { ARTIFACT_STORAGE_CLIENT_NAME, DEFAULT_OBJECT_NAMESPACES, OBJECT_STORE_PREFIX, OBJECT_STORAGE_CLIENT_NAME, createArtifactStorageClient, createArtifactStorageDriver, createObjectStorageClient, createObjectStorageDriver, resolveObjectNamespaces, } from './storage-driver.js';
|
|
6
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/artifacts/index.ts"],"names":[],"mappings":"AAAA,cAAc,uBAAuB,CAAC;AACtC,cAAc,mBAAmB,CAAC;AAClC,cAAc,iBAAiB,CAAC;AAChC,cAAc,mBAAmB,CAAC;AAClC,OAAO,EACL,4BAA4B,EAC5B,yBAAyB,EACzB,mBAAmB,EACnB,0BAA0B,EAC1B,2BAA2B,EAC3B,2BAA2B,EAC3B,yBAAyB,EACzB,yBAAyB,EACzB,uBAAuB,GACxB,MAAM,qBAAqB,CAAC"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export * from './artifact-storage.js';
|
|
2
|
+
export * from './crypto/index.js';
|
|
3
|
+
export * from './env-config.js';
|
|
4
|
+
export * from './object-store.js';
|
|
5
|
+
export { ARTIFACT_STORAGE_CLIENT_NAME, DEFAULT_OBJECT_NAMESPACES, OBJECT_STORE_PREFIX, OBJECT_STORAGE_CLIENT_NAME, createArtifactStorageClient, createArtifactStorageDriver, createObjectStorageClient, createObjectStorageDriver, resolveObjectNamespaces, } from './storage-driver.js';
|
|
6
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/artifacts/index.ts"],"names":[],"mappings":"AAAA,cAAc,uBAAuB,CAAC;AACtC,cAAc,mBAAmB,CAAC;AAClC,cAAc,iBAAiB,CAAC;AAChC,cAAc,mBAAmB,CAAC;AAClC,OAAO,EACL,4BAA4B,EAC5B,yBAAyB,EACzB,mBAAmB,EACnB,0BAA0B,EAC1B,2BAA2B,EAC3B,2BAA2B,EAC3B,yBAAyB,EACzB,yBAAyB,EACzB,uBAAuB,GACxB,MAAM,qBAAqB,CAAC","sourcesContent":["export * from './artifact-storage.js';\nexport * from './crypto/index.js';\nexport * from './env-config.js';\nexport * from './object-store.js';\nexport {\n ARTIFACT_STORAGE_CLIENT_NAME,\n DEFAULT_OBJECT_NAMESPACES,\n OBJECT_STORE_PREFIX,\n OBJECT_STORAGE_CLIENT_NAME,\n createArtifactStorageClient,\n createArtifactStorageDriver,\n createObjectStorageClient,\n createObjectStorageDriver,\n resolveObjectNamespaces,\n} from './storage-driver.js';\n"]}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type DynamicModule, type FactoryProvider, type ModuleMetadata } from '@nestjs/common';
|
|
2
|
+
import type { StorageCrypto } from '../crypto/index.js';
|
|
3
|
+
import { type ArtifactStorageConfig } from '../storage-driver.js';
|
|
4
|
+
export declare const ARTIFACT_STORAGE: unique symbol;
|
|
5
|
+
export declare const OBJECT_STORE: unique symbol;
|
|
6
|
+
export declare const ARTIFACT_STORAGE_MODULE_OPTIONS: unique symbol;
|
|
7
|
+
export interface ArtifactStorageModuleOptions {
|
|
8
|
+
config: ArtifactStorageConfig;
|
|
9
|
+
crypto: StorageCrypto;
|
|
10
|
+
/** Makes the product adapters and @nestm/storage clients globally injectable. */
|
|
11
|
+
isGlobal?: boolean;
|
|
12
|
+
}
|
|
13
|
+
interface ResolvedArtifactStorageModuleOptions {
|
|
14
|
+
config: ArtifactStorageConfig;
|
|
15
|
+
crypto: StorageCrypto;
|
|
16
|
+
}
|
|
17
|
+
export interface ArtifactStorageModuleAsyncOptions extends Pick<ModuleMetadata, 'imports'> {
|
|
18
|
+
inject?: FactoryProvider['inject'];
|
|
19
|
+
isGlobal?: boolean;
|
|
20
|
+
useFactory: (...dependencies: never[]) => ResolvedArtifactStorageModuleOptions | Promise<ResolvedArtifactStorageModuleOptions>;
|
|
21
|
+
}
|
|
22
|
+
export declare function InjectArtifactStorage(): ParameterDecorator & PropertyDecorator;
|
|
23
|
+
export declare function InjectObjectStore(): ParameterDecorator & PropertyDecorator;
|
|
24
|
+
export declare class ArtifactStorageModule {
|
|
25
|
+
static forRoot(options: ArtifactStorageModuleOptions): DynamicModule;
|
|
26
|
+
static forRootAsync(options: ArtifactStorageModuleAsyncOptions): DynamicModule;
|
|
27
|
+
}
|
|
28
|
+
export {};
|
|
29
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/artifacts/nest/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,aAAa,EAClB,KAAK,eAAe,EACpB,KAAK,cAAc,EAGpB,MAAM,gBAAgB,CAAC;AASxB,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAKxD,OAAO,EAKL,KAAK,qBAAqB,EAC3B,MAAM,sBAAsB,CAAC;AAE9B,eAAO,MAAM,gBAAgB,eAE5B,CAAC;AACF,eAAO,MAAM,YAAY,eAAsD,CAAC;AAChF,eAAO,MAAM,+BAA+B,eAE3C,CAAC;AAEF,MAAM,WAAW,4BAA4B;IAC3C,MAAM,EAAE,qBAAqB,CAAC;IAC9B,MAAM,EAAE,aAAa,CAAC;IACtB,iFAAiF;IACjF,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,UAAU,oCAAoC;IAC5C,MAAM,EAAE,qBAAqB,CAAC;IAC9B,MAAM,EAAE,aAAa,CAAC;CACvB;AAED,MAAM,WAAW,iCAAkC,SAAQ,IAAI,CAC7D,cAAc,EACd,SAAS,CACV;IACC,MAAM,CAAC,EAAE,eAAe,CAAC,QAAQ,CAAC,CAAC;IACnC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,UAAU,EAAE,CACV,GAAG,YAAY,EAAE,KAAK,EAAE,KAEtB,oCAAoC,GACpC,OAAO,CAAC,oCAAoC,CAAC,CAAC;CACnD;AAED,wBAAgB,qBAAqB,IAAI,kBAAkB,GACzD,iBAAiB,CAElB;AAED,wBAAgB,iBAAiB,IAAI,kBAAkB,GAAG,iBAAiB,CAE1E;AA4GD,qBACa,qBAAqB;IAChC,MAAM,CAAC,OAAO,CAAC,OAAO,EAAE,4BAA4B,GAAG,aAAa,CAMnE;IAED,MAAM,CAAC,YAAY,CACjB,OAAO,EAAE,iCAAiC,GACzC,aAAa,CAEf;CACF"}
|