@stigmer/temporal-codecs 3.13.0 → 3.14.0
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/encryption/config.d.ts +18 -0
- package/encryption/config.d.ts.map +1 -1
- package/encryption/config.js.map +1 -1
- package/encryption/payload-codec.d.ts +14 -0
- package/encryption/payload-codec.d.ts.map +1 -1
- package/encryption/payload-codec.js +33 -4
- package/encryption/payload-codec.js.map +1 -1
- package/index.d.ts +1 -1
- package/index.d.ts.map +1 -1
- package/index.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/encryption-codec.test.ts +112 -13
- package/src/encryption/config.ts +34 -4
- package/src/encryption/payload-codec.ts +34 -4
- package/src/index.ts +1 -0
package/encryption/config.d.ts
CHANGED
|
@@ -40,11 +40,29 @@ export interface EncryptionKey {
|
|
|
40
40
|
/** 32-byte AES-256 key. */
|
|
41
41
|
readonly key: Buffer;
|
|
42
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* Resolves decrypt-key material for a key id absent from the static
|
|
45
|
+
* config (C4, stigmer-cloud 20260827.09: the cloud server decodes
|
|
46
|
+
* desktop-runner histories written under server-managed per-identity
|
|
47
|
+
* `rpk_` keys, whose material lives in a database, not the environment).
|
|
48
|
+
* Returns undefined when the id is unknown — the codec then fails closed
|
|
49
|
+
* with its pinned unknown-key-id error. Implementations own their lookup
|
|
50
|
+
* and unsealing; the codec caches every RESOLVED key for the process
|
|
51
|
+
* lifetime (key material per id is immutable — rotation mints a new id),
|
|
52
|
+
* and never caches misses (the key may be minted moments later).
|
|
53
|
+
*/
|
|
54
|
+
export type PayloadKeyResolver = (keyId: string) => Promise<Buffer | undefined>;
|
|
43
55
|
export interface PayloadEncryptionConfig {
|
|
44
56
|
/** Key used to encrypt outgoing payloads (and decrypt its own). */
|
|
45
57
|
readonly primary: EncryptionKey;
|
|
46
58
|
/** Decrypt-only key accepted during rotation windows. */
|
|
47
59
|
readonly secondary?: EncryptionKey;
|
|
60
|
+
/**
|
|
61
|
+
* Decrypt-only fallback consulted when a payload names a key id the
|
|
62
|
+
* static pair does not cover ({@link PayloadKeyResolver}). Encode never
|
|
63
|
+
* consults it — outgoing payloads are always written under `primary`.
|
|
64
|
+
*/
|
|
65
|
+
readonly resolveKey?: PayloadKeyResolver;
|
|
48
66
|
}
|
|
49
67
|
/**
|
|
50
68
|
* Reads one secret VALUE by its env-var name. Consumers inject their own
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/encryption/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAEH,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,2BAA2B;IAC3B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,uBAAuB;IACtC,mEAAmE;IACnE,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC;IAChC,yDAAyD;IACzD,QAAQ,CAAC,SAAS,CAAC,EAAE,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/encryption/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAEH,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,2BAA2B;IAC3B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;;;;GAUG;AACH,MAAM,MAAM,kBAAkB,GAAG,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;AAEhF,MAAM,WAAW,uBAAuB;IACtC,mEAAmE;IACnE,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC;IAChC,yDAAyD;IACzD,QAAQ,CAAC,SAAS,CAAC,EAAE,aAAa,CAAC;IACnC;;;;OAIG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,kBAAkB,CAAC;CAC1C;AAED;;;;GAIG;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAC;AAEhE;;;;;GAKG;AACH,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;CAClC;AASD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,2BAA2B,CACzC,UAAU,EAAE,YAAY,EACxB,SAAS,CAAC,EAAE,oBAAoB,GAC/B,uBAAuB,GAAG,SAAS,CA6CrC"}
|
package/encryption/config.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/encryption/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/encryption/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAsDH,MAAM,OAAO,GAAG,gCAAgC,CAAC;AACjD,MAAM,UAAU,GAAG,mCAAmC,CAAC;AACvD,MAAM,iBAAiB,GAAG,0CAA0C,CAAC;AACrE,MAAM,oBAAoB,GAAG,6CAA6C,CAAC;AAE3E,MAAM,iBAAiB,GAAG,EAAE,CAAC;AAE7B;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,2BAA2B,CACzC,UAAwB,EACxB,SAAgC;IAEhC,MAAM,MAAM,GAAG,UAAU,CAAC,OAAO,CAAC,CAAC;IACnC,IAAI,MAAM,EAAE,CAAC;QACX,MAAM,OAAO,GAAkB;YAC7B,KAAK,EAAE,YAAY,CAAC,UAAU,CAAC;YAC/B,GAAG,EAAE,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;SAC/B,CAAC;QAEF,MAAM,YAAY,GAAG,UAAU,CAAC,iBAAiB,CAAC,CAAC;QACnD,MAAM,SAAS,GAA8B,YAAY;YACvD,CAAC,CAAC;gBACE,KAAK,EAAE,YAAY,CAAC,oBAAoB,CAAC;gBACzC,GAAG,EAAE,QAAQ,CAAC,YAAY,EAAE,iBAAiB,CAAC;aAC/C;YACH,CAAC,CAAC,SAAS,CAAC;QAEd,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC;IAChC,CAAC;IAED,IAAI,SAAS,EAAE,GAAG,EAAE,CAAC;QACnB,MAAM,OAAO,GAAkB;YAC7B,KAAK,EAAE,qBAAqB,CAC1B,SAAS,CAAC,KAAK,EACf,2BAA2B,CAC5B;YACD,GAAG,EAAE,QAAQ,CAAC,SAAS,CAAC,GAAG,EAAE,kCAAkC,CAAC;SACjE,CAAC;QAEF,MAAM,SAAS,GAA8B,SAAS,CAAC,YAAY;YACjE,CAAC,CAAC;gBACE,KAAK,EAAE,qBAAqB,CAC1B,SAAS,CAAC,cAAc,EACxB,qCAAqC,CACtC;gBACD,GAAG,EAAE,QAAQ,CACX,SAAS,CAAC,YAAY,EACtB,4CAA4C,CAC7C;aACF;YACH,CAAC,CAAC,SAAS,CAAC;QAEd,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC;IAChC,CAAC;IAED,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,SAAS,YAAY,CAAC,OAAe;IACnC,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IACnC,oEAAoE;IACpE,4DAA4D;IAC5D,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,MAAM,IAAI,KAAK,CACb,qCAAqC,OAAO,wBAAwB;YAClE,0BAA0B,CAC7B,CAAC;IACJ,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,qBAAqB,CAC5B,KAAyB,EACzB,SAAiB;IAEjB,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,MAAM,IAAI,KAAK,CACb,kEAAkE,SAAS,KAAK;YAC9E,2EAA2E,CAC9E,CAAC;IACJ,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,QAAQ,CAAC,SAAiB,EAAE,OAAe;IAClD,IAAI,GAAW,CAAC;IAChB,IAAI,CAAC;QACH,GAAG,GAAG,MAAM,CAAC,IAAI,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;IACzC,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,KAAK,CACb,qCAAqC,OAAO,sBAAsB,CACnE,CAAC;IACJ,CAAC;IACD,IAAI,GAAG,CAAC,MAAM,KAAK,iBAAiB,EAAE,CAAC;QACrC,MAAM,IAAI,KAAK,CACb,qCAAqC,OAAO,kBAAkB;YAC5D,GAAG,iBAAiB,yBAAyB,GAAG,CAAC,MAAM,EAAE,CAC5D,CAAC;IACJ,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC"}
|
|
@@ -28,6 +28,14 @@
|
|
|
28
28
|
* fails closed: unknown key id, missing key id, and ciphertext tampering
|
|
29
29
|
* all throw rather than surfacing bogus payloads.
|
|
30
30
|
*
|
|
31
|
+
* Key ids outside the static primary/secondary pair may be resolved
|
|
32
|
+
* through the config's optional resolveKey seam (C4: the cloud server
|
|
33
|
+
* decodes desktop-runner histories under database-resident `rpk_` keys).
|
|
34
|
+
* Resolution is decode-only, resolved keys are cached for the process
|
|
35
|
+
* lifetime (an id's material is immutable — rotation mints a new id),
|
|
36
|
+
* misses are never cached, and an unresolvable id keeps the pinned
|
|
37
|
+
* fail-closed throw.
|
|
38
|
+
*
|
|
31
39
|
* Moved from backend/services/runner/src/encryption/payload-codec.ts when
|
|
32
40
|
* the codecs became @stigmer/temporal-codecs (one home for the
|
|
33
41
|
* cross-language envelope contract; the TS server is the second consumer).
|
|
@@ -42,5 +50,11 @@ export declare class EncryptionPayloadCodec implements PayloadCodec {
|
|
|
42
50
|
decode(payloads: Payload[]): Promise<Payload[]>;
|
|
43
51
|
private encodePayload;
|
|
44
52
|
private decodePayload;
|
|
53
|
+
/**
|
|
54
|
+
* The static map first; on a miss, the config's resolver (when present),
|
|
55
|
+
* caching the RESOLVED key only — a miss stays unresolved so a key
|
|
56
|
+
* minted after this process booted is found on the next decode.
|
|
57
|
+
*/
|
|
58
|
+
private decryptKeyFor;
|
|
45
59
|
}
|
|
46
60
|
//# sourceMappingURL=payload-codec.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"payload-codec.d.ts","sourceRoot":"","sources":["../../src/encryption/payload-codec.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"payload-codec.d.ts","sourceRoot":"","sources":["../../src/encryption/payload-codec.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AAGH,OAAO,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAQhE,OAAO,KAAK,EAAiB,uBAAuB,EAAE,MAAM,aAAa,CAAC;AAY1E,qBAAa,sBAAuB,YAAW,YAAY;IAG7C,OAAO,CAAC,QAAQ,CAAC,MAAM;IAFnC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAsB;gBAEzB,MAAM,EAAE,uBAAuB;IAStD,MAAM,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC;IAI/C,MAAM,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC;IAIrD,OAAO,CAAC,aAAa;YAkBP,aAAa;IAwB3B;;;;OAIG;YACW,aAAa;CAc5B"}
|
|
@@ -28,6 +28,14 @@
|
|
|
28
28
|
* fails closed: unknown key id, missing key id, and ciphertext tampering
|
|
29
29
|
* all throw rather than surfacing bogus payloads.
|
|
30
30
|
*
|
|
31
|
+
* Key ids outside the static primary/secondary pair may be resolved
|
|
32
|
+
* through the config's optional resolveKey seam (C4: the cloud server
|
|
33
|
+
* decodes desktop-runner histories under database-resident `rpk_` keys).
|
|
34
|
+
* Resolution is decode-only, resolved keys are cached for the process
|
|
35
|
+
* lifetime (an id's material is immutable — rotation mints a new id),
|
|
36
|
+
* misses are never cached, and an unresolvable id keeps the pinned
|
|
37
|
+
* fail-closed throw.
|
|
38
|
+
*
|
|
31
39
|
* Moved from backend/services/runner/src/encryption/payload-codec.ts when
|
|
32
40
|
* the codecs became @stigmer/temporal-codecs (one home for the
|
|
33
41
|
* cross-language envelope contract; the TS server is the second consumer).
|
|
@@ -52,7 +60,9 @@ export class EncryptionPayloadCodec {
|
|
|
52
60
|
decryptKeysById;
|
|
53
61
|
constructor(config) {
|
|
54
62
|
this.config = config;
|
|
55
|
-
this.decryptKeysById = new Map([
|
|
63
|
+
this.decryptKeysById = new Map([
|
|
64
|
+
[config.primary.keyId, config.primary.key],
|
|
65
|
+
]);
|
|
56
66
|
if (config.secondary) {
|
|
57
67
|
this.decryptKeysById.set(config.secondary.keyId, config.secondary.key);
|
|
58
68
|
}
|
|
@@ -61,7 +71,7 @@ export class EncryptionPayloadCodec {
|
|
|
61
71
|
return payloads.map((p) => this.encodePayload(p));
|
|
62
72
|
}
|
|
63
73
|
async decode(payloads) {
|
|
64
|
-
return payloads.map((p) => this.decodePayload(p));
|
|
74
|
+
return Promise.all(payloads.map((p) => this.decodePayload(p)));
|
|
65
75
|
}
|
|
66
76
|
encodePayload(payload) {
|
|
67
77
|
// Data-less payloads (binary/null from void results) stay as-is:
|
|
@@ -79,7 +89,7 @@ export class EncryptionPayloadCodec {
|
|
|
79
89
|
data: encrypt(plaintext, this.config.primary),
|
|
80
90
|
};
|
|
81
91
|
}
|
|
82
|
-
decodePayload(payload) {
|
|
92
|
+
async decodePayload(payload) {
|
|
83
93
|
if (!isEncryptedPayload(payload)) {
|
|
84
94
|
return payload;
|
|
85
95
|
}
|
|
@@ -88,7 +98,7 @@ export class EncryptionPayloadCodec {
|
|
|
88
98
|
throw new Error("Encrypted payload is missing its encryption-key-id metadata — refusing to decode");
|
|
89
99
|
}
|
|
90
100
|
const keyId = Buffer.from(keyIdBytes).toString("utf-8");
|
|
91
|
-
const key = this.
|
|
101
|
+
const key = await this.decryptKeyFor(keyId);
|
|
92
102
|
if (!key) {
|
|
93
103
|
throw new Error(`Encrypted payload uses unknown key id '${keyId}' — configure it as the ` +
|
|
94
104
|
`primary or secondary payload encryption key (rotation window?)`);
|
|
@@ -96,6 +106,25 @@ export class EncryptionPayloadCodec {
|
|
|
96
106
|
const plaintext = decrypt(payload.data, key, keyId);
|
|
97
107
|
return temporal.api.common.v1.Payload.decode(plaintext);
|
|
98
108
|
}
|
|
109
|
+
/**
|
|
110
|
+
* The static map first; on a miss, the config's resolver (when present),
|
|
111
|
+
* caching the RESOLVED key only — a miss stays unresolved so a key
|
|
112
|
+
* minted after this process booted is found on the next decode.
|
|
113
|
+
*/
|
|
114
|
+
async decryptKeyFor(keyId) {
|
|
115
|
+
const known = this.decryptKeysById.get(keyId);
|
|
116
|
+
if (known !== undefined) {
|
|
117
|
+
return known;
|
|
118
|
+
}
|
|
119
|
+
if (this.config.resolveKey === undefined) {
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
const resolved = await this.config.resolveKey(keyId);
|
|
123
|
+
if (resolved !== undefined) {
|
|
124
|
+
this.decryptKeysById.set(keyId, resolved);
|
|
125
|
+
}
|
|
126
|
+
return resolved;
|
|
127
|
+
}
|
|
99
128
|
}
|
|
100
129
|
function isEncryptedPayload(payload) {
|
|
101
130
|
const encoding = payload.metadata?.[ENCODING_METADATA_KEY];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"payload-codec.js","sourceRoot":"","sources":["../../src/encryption/payload-codec.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"payload-codec.js","sourceRoot":"","sources":["../../src/encryption/payload-codec.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AAEH,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE5E,kEAAkE;AAClE,yEAAyE;AACzE,yEAAyE;AACzE,6EAA6E;AAC7E,2EAA2E;AAC3E,kEAAkE;AAClE,OAAO,KAAK,MAAM,mBAAmB,CAAC;AAGtC,MAAM,EAAE,QAAQ,EAAE,GAAG,KAAK,CAAC;AAE3B,MAAM,qBAAqB,GAAG,UAAU,CAAC;AACzC,MAAM,wBAAwB,GAAG,kBAAkB,CAAC;AACpD,MAAM,mBAAmB,GAAG,mBAAmB,CAAC;AAEhD,8DAA8D;AAC9D,MAAM,QAAQ,GAAG,EAAE,CAAC;AACpB,MAAM,cAAc,GAAG,EAAE,CAAC;AAE1B,MAAM,OAAO,sBAAsB;IAGJ;IAFZ,eAAe,CAAsB;IAEtD,YAA6B,MAA+B;QAA/B,WAAM,GAAN,MAAM,CAAyB;QAC1D,IAAI,CAAC,eAAe,GAAG,IAAI,GAAG,CAAC;YAC7B,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,EAAE,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC;SAC3C,CAAC,CAAC;QACH,IAAI,MAAM,CAAC,SAAS,EAAE,CAAC;YACrB,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,EAAE,MAAM,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;QACzE,CAAC;IACH,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,QAAmB;QAC9B,OAAO,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC;IACpD,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,QAAmB;QAC9B,OAAO,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACjE,CAAC;IAEO,aAAa,CAAC,OAAgB;QACpC,iEAAiE;QACjE,mEAAmE;QACnE,uEAAuE;QACvE,IAAI,CAAC,OAAO,CAAC,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC/C,OAAO,OAAO,CAAC;QACjB,CAAC;QAED,MAAM,SAAS,GAAG,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,MAAM,EAAE,CAAC;QAC1E,OAAO;YACL,QAAQ,EAAE;gBACR,CAAC,qBAAqB,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,wBAAwB,CAAC;gBAC9D,CAAC,mBAAmB,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC;aAC9D;YACD,IAAI,EAAE,OAAO,CAAC,SAAS,EAAE,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC;SAC9C,CAAC;IACJ,CAAC;IAEO,KAAK,CAAC,aAAa,CAAC,OAAgB;QAC1C,IAAI,CAAC,kBAAkB,CAAC,OAAO,CAAC,EAAE,CAAC;YACjC,OAAO,OAAO,CAAC;QACjB,CAAC;QAED,MAAM,UAAU,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC,mBAAmB,CAAC,CAAC;QAC3D,IAAI,CAAC,UAAU,EAAE,CAAC;YAChB,MAAM,IAAI,KAAK,CACb,kFAAkF,CACnF,CAAC;QACJ,CAAC;QACD,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;QACxD,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;QAC5C,IAAI,CAAC,GAAG,EAAE,CAAC;YACT,MAAM,IAAI,KAAK,CACb,0CAA0C,KAAK,0BAA0B;gBACvE,gEAAgE,CACnE,CAAC;QACJ,CAAC;QAED,MAAM,SAAS,GAAG,OAAO,CAAC,OAAO,CAAC,IAAK,EAAE,GAAG,EAAE,KAAK,CAAC,CAAC;QACrD,OAAO,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;IAC1D,CAAC;IAED;;;;OAIG;IACK,KAAK,CAAC,aAAa,CAAC,KAAa;QACvC,MAAM,KAAK,GAAG,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QAC9C,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;YACxB,OAAO,KAAK,CAAC;QACf,CAAC;QACD,IAAI,IAAI,CAAC,MAAM,CAAC,UAAU,KAAK,SAAS,EAAE,CAAC;YACzC,OAAO,SAAS,CAAC;QACnB,CAAC;QACD,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;QACrD,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;YAC3B,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC;QAC5C,CAAC;QACD,OAAO,QAAQ,CAAC;IAClB,CAAC;CACF;AAED,SAAS,kBAAkB,CAAC,OAAgB;IAC1C,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC,qBAAqB,CAAC,CAAC;IAC3D,IAAI,CAAC,QAAQ;QAAE,OAAO,KAAK,CAAC;IAC5B,OAAO,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,KAAK,wBAAwB,CAAC;AAC9E,CAAC;AAED,SAAS,OAAO,CAAC,SAAqB,EAAE,GAAkB;IACxD,MAAM,EAAE,GAAG,WAAW,CAAC,QAAQ,CAAC,CAAC;IACjC,MAAM,MAAM,GAAG,cAAc,CAAC,aAAa,EAAE,GAAG,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;IAC1D,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;IAC7E,uEAAuE;IACvE,wCAAwC;IACxC,OAAO,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,EAAE,UAAU,EAAE,MAAM,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC;AAC9D,CAAC;AAED,SAAS,OAAO,CAAC,IAAgB,EAAE,GAAW,EAAE,KAAa;IAC3D,IAAI,IAAI,CAAC,MAAM,GAAG,QAAQ,GAAG,cAAc,EAAE,CAAC;QAC5C,MAAM,IAAI,KAAK,CACb,mCAAmC,KAAK,mBAAmB,IAAI,CAAC,MAAM,SAAS,CAChF,CAAC;IACJ,CAAC;IACD,MAAM,GAAG,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC9B,MAAM,EAAE,GAAG,GAAG,CAAC,QAAQ,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC;IACrC,MAAM,UAAU,GAAG,GAAG,CAAC,QAAQ,CAAC,QAAQ,EAAE,GAAG,CAAC,MAAM,GAAG,cAAc,CAAC,CAAC;IACvE,MAAM,GAAG,GAAG,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,MAAM,GAAG,cAAc,CAAC,CAAC;IAEtD,MAAM,QAAQ,GAAG,gBAAgB,CAAC,aAAa,EAAE,GAAG,EAAE,EAAE,CAAC,CAAC;IAC1D,QAAQ,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;IACzB,IAAI,CAAC;QACH,OAAO,MAAM,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,UAAU,CAAC,EAAE,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;IACxE,CAAC;IAAC,MAAM,CAAC;QACP,qEAAqE;QACrE,8DAA8D;QAC9D,MAAM,IAAI,KAAK,CACb,2CAA2C,KAAK,oBAAoB;YAClE,8CAA8C,CACjD,CAAC;IACJ,CAAC;AACH,CAAC"}
|
package/index.d.ts
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
*/
|
|
21
21
|
export { EncryptionPayloadCodec } from "./encryption/payload-codec.js";
|
|
22
22
|
export { loadPayloadEncryptionConfig } from "./encryption/config.js";
|
|
23
|
-
export type { BootstrapKeyMaterial, EncryptionKey, PayloadEncryptionConfig, SecretReader, } from "./encryption/config.js";
|
|
23
|
+
export type { BootstrapKeyMaterial, EncryptionKey, PayloadEncryptionConfig, PayloadKeyResolver, SecretReader, } from "./encryption/config.js";
|
|
24
24
|
export { ClaimcheckPayloadCodec } from "./claimcheck/payload-codec.js";
|
|
25
25
|
export { loadClaimcheckConfig } from "./claimcheck/config.js";
|
|
26
26
|
export type { ClaimcheckConfig } from "./claimcheck/config.js";
|
package/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EAAE,sBAAsB,EAAE,MAAM,+BAA+B,CAAC;AACvE,OAAO,EAAE,2BAA2B,EAAE,MAAM,wBAAwB,CAAC;AACrE,YAAY,EACV,oBAAoB,EACpB,aAAa,EACb,uBAAuB,EACvB,YAAY,GACb,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EAAE,sBAAsB,EAAE,MAAM,+BAA+B,CAAC;AACvE,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAC9D,YAAY,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC/D,YAAY,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EAAE,sBAAsB,EAAE,MAAM,+BAA+B,CAAC;AACvE,OAAO,EAAE,2BAA2B,EAAE,MAAM,wBAAwB,CAAC;AACrE,YAAY,EACV,oBAAoB,EACpB,aAAa,EACb,uBAAuB,EACvB,kBAAkB,EAClB,YAAY,GACb,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EAAE,sBAAsB,EAAE,MAAM,+BAA+B,CAAC;AACvE,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAC9D,YAAY,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC/D,YAAY,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC"}
|
package/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EAAE,sBAAsB,EAAE,MAAM,+BAA+B,CAAC;AACvE,OAAO,EAAE,2BAA2B,EAAE,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EAAE,sBAAsB,EAAE,MAAM,+BAA+B,CAAC;AACvE,OAAO,EAAE,2BAA2B,EAAE,MAAM,wBAAwB,CAAC;AASrE,OAAO,EAAE,sBAAsB,EAAE,MAAM,+BAA+B,CAAC;AACvE,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stigmer/temporal-codecs",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.14.0",
|
|
4
4
|
"description": "Temporal payload codecs shared by Stigmer's TypeScript Temporal processes — AES-256-GCM payload encryption and claim-check offloading, the one home for the cross-language envelope contract",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -23,7 +23,9 @@ import { makeInMemoryClaimcheckStorage } from "../__test-utils__/fake-claimcheck
|
|
|
23
23
|
|
|
24
24
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
25
25
|
|
|
26
|
-
function makeKeyConfig(
|
|
26
|
+
function makeKeyConfig(
|
|
27
|
+
overrides: Partial<PayloadEncryptionConfig> = {},
|
|
28
|
+
): PayloadEncryptionConfig {
|
|
27
29
|
return {
|
|
28
30
|
primary: { keyId: "test-key-1", key: randomBytes(32) },
|
|
29
31
|
...overrides,
|
|
@@ -121,7 +123,92 @@ describe("EncryptionPayloadCodec", () => {
|
|
|
121
123
|
secondary: oldKey,
|
|
122
124
|
});
|
|
123
125
|
const [decoded] = await rotatedReader.decode([encoded]);
|
|
124
|
-
expect(JSON.parse(Buffer.from(decoded.data!).toString())).toEqual({
|
|
126
|
+
expect(JSON.parse(Buffer.from(decoded.data!).toString())).toEqual({
|
|
127
|
+
rotated: true,
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// The resolveKey seam (C4 Stage 2): decrypt-only fallback for key ids
|
|
132
|
+
// outside the static pair — the cloud server's database-resident rpk_
|
|
133
|
+
// keys. Pinned properties: resolved keys decode, resolved material is
|
|
134
|
+
// cached (one lookup per id per process), misses are NOT cached, an
|
|
135
|
+
// unresolvable id keeps the fail-closed unknown-key-id throw, and
|
|
136
|
+
// encode never consults the resolver.
|
|
137
|
+
describe("resolveKey fallback", () => {
|
|
138
|
+
it("decodes a payload under a resolver-supplied key and caches the material", async () => {
|
|
139
|
+
const runnerKey = { keyId: "rpk_abc123", key: randomBytes(32) };
|
|
140
|
+
const writer = new EncryptionPayloadCodec({ primary: runnerKey });
|
|
141
|
+
const [encoded] = await writer.encode([
|
|
142
|
+
makeJsonPayload({ desktop: true }),
|
|
143
|
+
]);
|
|
144
|
+
|
|
145
|
+
const lookups: string[] = [];
|
|
146
|
+
const reader = new EncryptionPayloadCodec({
|
|
147
|
+
...makeKeyConfig(),
|
|
148
|
+
resolveKey: async (keyId) => {
|
|
149
|
+
lookups.push(keyId);
|
|
150
|
+
return keyId === runnerKey.keyId ? runnerKey.key : undefined;
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const [first] = await reader.decode([encoded]);
|
|
155
|
+
expect(JSON.parse(Buffer.from(first.data!).toString())).toEqual({
|
|
156
|
+
desktop: true,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
const [second] = await reader.decode([encoded]);
|
|
160
|
+
expect(JSON.parse(Buffer.from(second.data!).toString())).toEqual({
|
|
161
|
+
desktop: true,
|
|
162
|
+
});
|
|
163
|
+
expect(lookups).toEqual(["rpk_abc123"]);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("fails closed when the resolver does not know the key id, and retries on the next decode", async () => {
|
|
167
|
+
const runnerKey = { keyId: "rpk_late", key: randomBytes(32) };
|
|
168
|
+
const writer = new EncryptionPayloadCodec({ primary: runnerKey });
|
|
169
|
+
const [encoded] = await writer.encode([makeJsonPayload({ a: 1 })]);
|
|
170
|
+
|
|
171
|
+
let known = false;
|
|
172
|
+
const lookups: string[] = [];
|
|
173
|
+
const reader = new EncryptionPayloadCodec({
|
|
174
|
+
...makeKeyConfig(),
|
|
175
|
+
resolveKey: async (keyId) => {
|
|
176
|
+
lookups.push(keyId);
|
|
177
|
+
return known && keyId === runnerKey.keyId ? runnerKey.key : undefined;
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
await expect(reader.decode([encoded])).rejects.toThrow(
|
|
182
|
+
/unknown key id 'rpk_late'/,
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
// The miss was not cached: once the key exists (minted after this
|
|
186
|
+
// process booted), the next decode finds it.
|
|
187
|
+
known = true;
|
|
188
|
+
const [decoded] = await reader.decode([encoded]);
|
|
189
|
+
expect(JSON.parse(Buffer.from(decoded.data!).toString())).toEqual({
|
|
190
|
+
a: 1,
|
|
191
|
+
});
|
|
192
|
+
expect(lookups).toEqual(["rpk_late", "rpk_late"]);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("never consults the resolver for the static keys or on encode", async () => {
|
|
196
|
+
const lookups: string[] = [];
|
|
197
|
+
const codec = new EncryptionPayloadCodec({
|
|
198
|
+
...makeKeyConfig(),
|
|
199
|
+
resolveKey: async (keyId) => {
|
|
200
|
+
lookups.push(keyId);
|
|
201
|
+
return undefined;
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
const [encoded] = await codec.encode([makeJsonPayload({ b: 2 })]);
|
|
206
|
+
const [decoded] = await codec.decode([encoded]);
|
|
207
|
+
expect(JSON.parse(Buffer.from(decoded.data!).toString())).toEqual({
|
|
208
|
+
b: 2,
|
|
209
|
+
});
|
|
210
|
+
expect(lookups).toEqual([]);
|
|
211
|
+
});
|
|
125
212
|
});
|
|
126
213
|
});
|
|
127
214
|
|
|
@@ -177,9 +264,11 @@ describe("loadPayloadEncryptionConfig", () => {
|
|
|
177
264
|
});
|
|
178
265
|
|
|
179
266
|
it("loads primary and secondary keys", () => {
|
|
180
|
-
process.env.STIGMER_PAYLOAD_ENCRYPTION_KEY =
|
|
267
|
+
process.env.STIGMER_PAYLOAD_ENCRYPTION_KEY =
|
|
268
|
+
randomBytes(32).toString("base64");
|
|
181
269
|
process.env.STIGMER_PAYLOAD_ENCRYPTION_KEY_ID = "k2";
|
|
182
|
-
process.env.STIGMER_PAYLOAD_ENCRYPTION_SECONDARY_KEY =
|
|
270
|
+
process.env.STIGMER_PAYLOAD_ENCRYPTION_SECONDARY_KEY =
|
|
271
|
+
randomBytes(32).toString("base64");
|
|
183
272
|
process.env.STIGMER_PAYLOAD_ENCRYPTION_SECONDARY_KEY_ID = "k1";
|
|
184
273
|
|
|
185
274
|
const config = loadPayloadEncryptionConfig(readEnv);
|
|
@@ -189,16 +278,20 @@ describe("loadPayloadEncryptionConfig", () => {
|
|
|
189
278
|
});
|
|
190
279
|
|
|
191
280
|
it("fails the boot when a key is set without an id", () => {
|
|
192
|
-
process.env.STIGMER_PAYLOAD_ENCRYPTION_KEY =
|
|
281
|
+
process.env.STIGMER_PAYLOAD_ENCRYPTION_KEY =
|
|
282
|
+
randomBytes(32).toString("base64");
|
|
193
283
|
expect(() => loadPayloadEncryptionConfig(readEnv)).toThrow(
|
|
194
284
|
/STIGMER_PAYLOAD_ENCRYPTION_KEY_ID is required/,
|
|
195
285
|
);
|
|
196
286
|
});
|
|
197
287
|
|
|
198
288
|
it("fails the boot on a key of the wrong length", () => {
|
|
199
|
-
process.env.STIGMER_PAYLOAD_ENCRYPTION_KEY =
|
|
289
|
+
process.env.STIGMER_PAYLOAD_ENCRYPTION_KEY =
|
|
290
|
+
randomBytes(16).toString("base64");
|
|
200
291
|
process.env.STIGMER_PAYLOAD_ENCRYPTION_KEY_ID = "k1";
|
|
201
|
-
expect(() => loadPayloadEncryptionConfig(readEnv)).toThrow(
|
|
292
|
+
expect(() => loadPayloadEncryptionConfig(readEnv)).toThrow(
|
|
293
|
+
/must decode to 32 bytes/,
|
|
294
|
+
);
|
|
202
295
|
});
|
|
203
296
|
|
|
204
297
|
// Server-managed key material from getRunnerBootstrapConfig (stigmer#398):
|
|
@@ -220,7 +313,8 @@ describe("loadPayloadEncryptionConfig", () => {
|
|
|
220
313
|
});
|
|
221
314
|
|
|
222
315
|
it("env-configured key wins over bootstrap material", () => {
|
|
223
|
-
process.env.STIGMER_PAYLOAD_ENCRYPTION_KEY =
|
|
316
|
+
process.env.STIGMER_PAYLOAD_ENCRYPTION_KEY =
|
|
317
|
+
randomBytes(32).toString("base64");
|
|
224
318
|
process.env.STIGMER_PAYLOAD_ENCRYPTION_KEY_ID = "env-key";
|
|
225
319
|
|
|
226
320
|
const config = loadPayloadEncryptionConfig(readEnv, bootstrapKeys());
|
|
@@ -230,7 +324,9 @@ describe("loadPayloadEncryptionConfig", () => {
|
|
|
230
324
|
|
|
231
325
|
it("fails the boot on a bootstrap key without its id (server contract violation)", () => {
|
|
232
326
|
expect(() =>
|
|
233
|
-
loadPayloadEncryptionConfig(readEnv, {
|
|
327
|
+
loadPayloadEncryptionConfig(readEnv, {
|
|
328
|
+
key: randomBytes(32).toString("base64"),
|
|
329
|
+
}),
|
|
234
330
|
).toThrow(/payload_encryption_key_id/);
|
|
235
331
|
});
|
|
236
332
|
|
|
@@ -264,7 +360,10 @@ describe("cross-language conformance fixture", () => {
|
|
|
264
360
|
// lockstep.
|
|
265
361
|
it("decrypts the committed fixture to the expected payload", async () => {
|
|
266
362
|
const fixture = JSON.parse(
|
|
267
|
-
readFileSync(
|
|
363
|
+
readFileSync(
|
|
364
|
+
join(__dirname, "fixtures", "encrypted-payload-fixture.json"),
|
|
365
|
+
"utf-8",
|
|
366
|
+
),
|
|
268
367
|
);
|
|
269
368
|
|
|
270
369
|
const codec = new EncryptionPayloadCodec({
|
|
@@ -276,9 +375,9 @@ describe("cross-language conformance fixture", () => {
|
|
|
276
375
|
|
|
277
376
|
const encrypted: Payload = {
|
|
278
377
|
metadata: Object.fromEntries(
|
|
279
|
-
Object.entries(
|
|
280
|
-
|
|
281
|
-
),
|
|
378
|
+
Object.entries(
|
|
379
|
+
fixture.encrypted.metadataBase64 as Record<string, string>,
|
|
380
|
+
).map(([k, v]) => [k, Buffer.from(v, "base64")]),
|
|
282
381
|
),
|
|
283
382
|
data: Buffer.from(fixture.encrypted.dataBase64, "base64"),
|
|
284
383
|
};
|
package/src/encryption/config.ts
CHANGED
|
@@ -42,11 +42,30 @@ export interface EncryptionKey {
|
|
|
42
42
|
readonly key: Buffer;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Resolves decrypt-key material for a key id absent from the static
|
|
47
|
+
* config (C4, stigmer-cloud 20260827.09: the cloud server decodes
|
|
48
|
+
* desktop-runner histories written under server-managed per-identity
|
|
49
|
+
* `rpk_` keys, whose material lives in a database, not the environment).
|
|
50
|
+
* Returns undefined when the id is unknown — the codec then fails closed
|
|
51
|
+
* with its pinned unknown-key-id error. Implementations own their lookup
|
|
52
|
+
* and unsealing; the codec caches every RESOLVED key for the process
|
|
53
|
+
* lifetime (key material per id is immutable — rotation mints a new id),
|
|
54
|
+
* and never caches misses (the key may be minted moments later).
|
|
55
|
+
*/
|
|
56
|
+
export type PayloadKeyResolver = (keyId: string) => Promise<Buffer | undefined>;
|
|
57
|
+
|
|
45
58
|
export interface PayloadEncryptionConfig {
|
|
46
59
|
/** Key used to encrypt outgoing payloads (and decrypt its own). */
|
|
47
60
|
readonly primary: EncryptionKey;
|
|
48
61
|
/** Decrypt-only key accepted during rotation windows. */
|
|
49
62
|
readonly secondary?: EncryptionKey;
|
|
63
|
+
/**
|
|
64
|
+
* Decrypt-only fallback consulted when a payload names a key id the
|
|
65
|
+
* static pair does not cover ({@link PayloadKeyResolver}). Encode never
|
|
66
|
+
* consults it — outgoing payloads are always written under `primary`.
|
|
67
|
+
*/
|
|
68
|
+
readonly resolveKey?: PayloadKeyResolver;
|
|
50
69
|
}
|
|
51
70
|
|
|
52
71
|
/**
|
|
@@ -114,7 +133,10 @@ export function loadPayloadEncryptionConfig(
|
|
|
114
133
|
|
|
115
134
|
if (bootstrap?.key) {
|
|
116
135
|
const primary: EncryptionKey = {
|
|
117
|
-
keyId: requireBootstrapKeyId(
|
|
136
|
+
keyId: requireBootstrapKeyId(
|
|
137
|
+
bootstrap.keyId,
|
|
138
|
+
"payload_encryption_key_id",
|
|
139
|
+
),
|
|
118
140
|
key: parseKey(bootstrap.key, "bootstrap payload_encryption_key"),
|
|
119
141
|
};
|
|
120
142
|
|
|
@@ -124,7 +146,10 @@ export function loadPayloadEncryptionConfig(
|
|
|
124
146
|
bootstrap.secondaryKeyId,
|
|
125
147
|
"payload_encryption_secondary_key_id",
|
|
126
148
|
),
|
|
127
|
-
key: parseKey(
|
|
149
|
+
key: parseKey(
|
|
150
|
+
bootstrap.secondaryKey,
|
|
151
|
+
"bootstrap payload_encryption_secondary_key",
|
|
152
|
+
),
|
|
128
153
|
}
|
|
129
154
|
: undefined;
|
|
130
155
|
|
|
@@ -147,7 +172,10 @@ function requireKeyId(envName: string): string {
|
|
|
147
172
|
return keyId;
|
|
148
173
|
}
|
|
149
174
|
|
|
150
|
-
function requireBootstrapKeyId(
|
|
175
|
+
function requireBootstrapKeyId(
|
|
176
|
+
keyId: string | undefined,
|
|
177
|
+
fieldName: string,
|
|
178
|
+
): string {
|
|
151
179
|
if (!keyId) {
|
|
152
180
|
throw new Error(
|
|
153
181
|
`Runner bootstrap returned a payload encryption key without its ${fieldName} — ` +
|
|
@@ -162,7 +190,9 @@ function parseKey(rawBase64: string, envName: string): Buffer {
|
|
|
162
190
|
try {
|
|
163
191
|
key = Buffer.from(rawBase64, "base64");
|
|
164
192
|
} catch {
|
|
165
|
-
throw new Error(
|
|
193
|
+
throw new Error(
|
|
194
|
+
`Payload encryption misconfigured: ${envName} is not valid base64`,
|
|
195
|
+
);
|
|
166
196
|
}
|
|
167
197
|
if (key.length !== AES_256_KEY_BYTES) {
|
|
168
198
|
throw new Error(
|
|
@@ -28,6 +28,14 @@
|
|
|
28
28
|
* fails closed: unknown key id, missing key id, and ciphertext tampering
|
|
29
29
|
* all throw rather than surfacing bogus payloads.
|
|
30
30
|
*
|
|
31
|
+
* Key ids outside the static primary/secondary pair may be resolved
|
|
32
|
+
* through the config's optional resolveKey seam (C4: the cloud server
|
|
33
|
+
* decodes desktop-runner histories under database-resident `rpk_` keys).
|
|
34
|
+
* Resolution is decode-only, resolved keys are cached for the process
|
|
35
|
+
* lifetime (an id's material is immutable — rotation mints a new id),
|
|
36
|
+
* misses are never cached, and an unresolvable id keeps the pinned
|
|
37
|
+
* fail-closed throw.
|
|
38
|
+
*
|
|
31
39
|
* Moved from backend/services/runner/src/encryption/payload-codec.ts when
|
|
32
40
|
* the codecs became @stigmer/temporal-codecs (one home for the
|
|
33
41
|
* cross-language envelope contract; the TS server is the second consumer).
|
|
@@ -58,7 +66,9 @@ export class EncryptionPayloadCodec implements PayloadCodec {
|
|
|
58
66
|
private readonly decryptKeysById: Map<string, Buffer>;
|
|
59
67
|
|
|
60
68
|
constructor(private readonly config: PayloadEncryptionConfig) {
|
|
61
|
-
this.decryptKeysById = new Map([
|
|
69
|
+
this.decryptKeysById = new Map([
|
|
70
|
+
[config.primary.keyId, config.primary.key],
|
|
71
|
+
]);
|
|
62
72
|
if (config.secondary) {
|
|
63
73
|
this.decryptKeysById.set(config.secondary.keyId, config.secondary.key);
|
|
64
74
|
}
|
|
@@ -69,7 +79,7 @@ export class EncryptionPayloadCodec implements PayloadCodec {
|
|
|
69
79
|
}
|
|
70
80
|
|
|
71
81
|
async decode(payloads: Payload[]): Promise<Payload[]> {
|
|
72
|
-
return payloads.map((p) => this.decodePayload(p));
|
|
82
|
+
return Promise.all(payloads.map((p) => this.decodePayload(p)));
|
|
73
83
|
}
|
|
74
84
|
|
|
75
85
|
private encodePayload(payload: Payload): Payload {
|
|
@@ -90,7 +100,7 @@ export class EncryptionPayloadCodec implements PayloadCodec {
|
|
|
90
100
|
};
|
|
91
101
|
}
|
|
92
102
|
|
|
93
|
-
private decodePayload(payload: Payload): Payload {
|
|
103
|
+
private async decodePayload(payload: Payload): Promise<Payload> {
|
|
94
104
|
if (!isEncryptedPayload(payload)) {
|
|
95
105
|
return payload;
|
|
96
106
|
}
|
|
@@ -102,7 +112,7 @@ export class EncryptionPayloadCodec implements PayloadCodec {
|
|
|
102
112
|
);
|
|
103
113
|
}
|
|
104
114
|
const keyId = Buffer.from(keyIdBytes).toString("utf-8");
|
|
105
|
-
const key = this.
|
|
115
|
+
const key = await this.decryptKeyFor(keyId);
|
|
106
116
|
if (!key) {
|
|
107
117
|
throw new Error(
|
|
108
118
|
`Encrypted payload uses unknown key id '${keyId}' — configure it as the ` +
|
|
@@ -113,6 +123,26 @@ export class EncryptionPayloadCodec implements PayloadCodec {
|
|
|
113
123
|
const plaintext = decrypt(payload.data!, key, keyId);
|
|
114
124
|
return temporal.api.common.v1.Payload.decode(plaintext);
|
|
115
125
|
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* The static map first; on a miss, the config's resolver (when present),
|
|
129
|
+
* caching the RESOLVED key only — a miss stays unresolved so a key
|
|
130
|
+
* minted after this process booted is found on the next decode.
|
|
131
|
+
*/
|
|
132
|
+
private async decryptKeyFor(keyId: string): Promise<Buffer | undefined> {
|
|
133
|
+
const known = this.decryptKeysById.get(keyId);
|
|
134
|
+
if (known !== undefined) {
|
|
135
|
+
return known;
|
|
136
|
+
}
|
|
137
|
+
if (this.config.resolveKey === undefined) {
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
140
|
+
const resolved = await this.config.resolveKey(keyId);
|
|
141
|
+
if (resolved !== undefined) {
|
|
142
|
+
this.decryptKeysById.set(keyId, resolved);
|
|
143
|
+
}
|
|
144
|
+
return resolved;
|
|
145
|
+
}
|
|
116
146
|
}
|
|
117
147
|
|
|
118
148
|
function isEncryptedPayload(payload: Payload): boolean {
|