@stigmer/temporal-codecs 3.12.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +48 -0
  3. package/claimcheck/compressor.d.ts +12 -0
  4. package/claimcheck/compressor.d.ts.map +1 -0
  5. package/claimcheck/compressor.js +17 -0
  6. package/claimcheck/compressor.js.map +1 -0
  7. package/claimcheck/config.d.ts +17 -0
  8. package/claimcheck/config.d.ts.map +1 -0
  9. package/claimcheck/config.js +19 -0
  10. package/claimcheck/config.js.map +1 -0
  11. package/claimcheck/payload-codec.d.ts +29 -0
  12. package/claimcheck/payload-codec.d.ts.map +1 -0
  13. package/claimcheck/payload-codec.js +110 -0
  14. package/claimcheck/payload-codec.js.map +1 -0
  15. package/claimcheck/storage.d.ts +20 -0
  16. package/claimcheck/storage.d.ts.map +1 -0
  17. package/claimcheck/storage.js +2 -0
  18. package/claimcheck/storage.js.map +1 -0
  19. package/encryption/config.d.ts +82 -0
  20. package/encryption/config.d.ts.map +1 -0
  21. package/encryption/config.js +119 -0
  22. package/encryption/config.js.map +1 -0
  23. package/encryption/payload-codec.d.ts +46 -0
  24. package/encryption/payload-codec.d.ts.map +1 -0
  25. package/encryption/payload-codec.js +134 -0
  26. package/encryption/payload-codec.js.map +1 -0
  27. package/index.d.ts +28 -0
  28. package/index.d.ts.map +1 -0
  29. package/index.js +25 -0
  30. package/index.js.map +1 -0
  31. package/package.json +36 -0
  32. package/src/__test-utils__/fake-claimcheck-storage.ts +54 -0
  33. package/src/__tests__/claimcheck-codec.test.ts +256 -0
  34. package/src/__tests__/encryption-codec.test.ts +292 -0
  35. package/src/__tests__/fixtures/encrypted-payload-fixture.json +15 -0
  36. package/src/claimcheck/compressor.ts +19 -0
  37. package/src/claimcheck/config.ts +30 -0
  38. package/src/claimcheck/payload-codec.ts +144 -0
  39. package/src/claimcheck/storage.ts +19 -0
  40. package/src/encryption/config.ts +174 -0
  41. package/src/encryption/payload-codec.ts +156 -0
  42. package/src/index.ts +34 -0
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Payload-encryption configuration (stigmer-cloud#227, stigmer#398).
3
+ *
4
+ * Two key sources, strict precedence:
5
+ *
6
+ * 1. Environment (STIGMER_PAYLOAD_ENCRYPTION_KEY(_ID)) — the operator's
7
+ * explicit choice: self-hosted deployments sharing one key with their
8
+ * server, and cloud sandboxes injected with the platform key. When the
9
+ * env key is set, bootstrap-delivered material is ignored entirely.
10
+ * 2. Bootstrap-delivered — server-managed per-identity keys handed to
11
+ * desktop-class runners by getRunnerBootstrapConfig. Held in memory
12
+ * only; persistence lives server-side, which is what makes Temporal
13
+ * replay work across runner restarts (every boot re-fetches the SAME
14
+ * key).
15
+ *
16
+ * Encryption is enabled iff a key is present from either source — the same
17
+ * enabled-iff-configured pattern as the claim-check codec. A malformed key
18
+ * fails the boot rather than silently running plaintext: an operator who set
19
+ * the key (or a server that minted one) intended history to be encrypted.
20
+ *
21
+ * Key rotation: payloads carry the id of the key that encrypted them.
22
+ * During a rotation window the previous key stays readable via the
23
+ * secondary pair while new payloads are written under the primary key.
24
+ * Workers capture keys at construction, so a rotated bootstrap key lands
25
+ * on the next runner boot — there is no live re-key.
26
+ *
27
+ * Moved from backend/services/runner/src/encryption/config.ts when the
28
+ * codecs became @stigmer/temporal-codecs (one home for the cross-language
29
+ * envelope contract; the TS server is the second consumer). Key VALUES are
30
+ * read through an injected {@link SecretReader} because secret custody is
31
+ * consumer policy, not codec policy: the runner routes reads through its
32
+ * boot-capture credential store (stigmer#508 — secrets must not live in
33
+ * process.env where agent shells could read them), while other consumers
34
+ * read their own stores. No process.env default is provided for the
35
+ * VALUES: a forgotten injection must fail to compile, not silently bypass
36
+ * a consumer's secret custody.
37
+ */
38
+ const KEY_ENV = "STIGMER_PAYLOAD_ENCRYPTION_KEY";
39
+ const KEY_ID_ENV = "STIGMER_PAYLOAD_ENCRYPTION_KEY_ID";
40
+ const SECONDARY_KEY_ENV = "STIGMER_PAYLOAD_ENCRYPTION_SECONDARY_KEY";
41
+ const SECONDARY_KEY_ID_ENV = "STIGMER_PAYLOAD_ENCRYPTION_SECONDARY_KEY_ID";
42
+ const AES_256_KEY_BYTES = 32;
43
+ /**
44
+ * Returns the encryption config, or undefined when encryption is not
45
+ * configured (the codec is then simply not installed).
46
+ *
47
+ * Source precedence: the env key wins outright; bootstrap-delivered
48
+ * material applies only when no env key is set (see the module doc).
49
+ *
50
+ * @throws when a key is present but malformed, or a key id is missing —
51
+ * key misconfiguration must stop the boot, not degrade to plaintext.
52
+ * This applies equally to bootstrap material: a server that hands out
53
+ * a bad key or omits its id has broken the protocol contract, and
54
+ * running plaintext against a server that manages keys would silently
55
+ * defeat the feature.
56
+ */
57
+ export function loadPayloadEncryptionConfig(readSecret, bootstrap) {
58
+ const rawKey = readSecret(KEY_ENV);
59
+ if (rawKey) {
60
+ const primary = {
61
+ keyId: requireKeyId(KEY_ID_ENV),
62
+ key: parseKey(rawKey, KEY_ENV),
63
+ };
64
+ const rawSecondary = readSecret(SECONDARY_KEY_ENV);
65
+ const secondary = rawSecondary
66
+ ? {
67
+ keyId: requireKeyId(SECONDARY_KEY_ID_ENV),
68
+ key: parseKey(rawSecondary, SECONDARY_KEY_ENV),
69
+ }
70
+ : undefined;
71
+ return { primary, secondary };
72
+ }
73
+ if (bootstrap?.key) {
74
+ const primary = {
75
+ keyId: requireBootstrapKeyId(bootstrap.keyId, "payload_encryption_key_id"),
76
+ key: parseKey(bootstrap.key, "bootstrap payload_encryption_key"),
77
+ };
78
+ const secondary = bootstrap.secondaryKey
79
+ ? {
80
+ keyId: requireBootstrapKeyId(bootstrap.secondaryKeyId, "payload_encryption_secondary_key_id"),
81
+ key: parseKey(bootstrap.secondaryKey, "bootstrap payload_encryption_secondary_key"),
82
+ }
83
+ : undefined;
84
+ return { primary, secondary };
85
+ }
86
+ return undefined;
87
+ }
88
+ function requireKeyId(envName) {
89
+ const keyId = process.env[envName];
90
+ // An explicit id is required (no default): during rotation two keys
91
+ // coexist, and payloads must name which one encrypted them.
92
+ if (!keyId) {
93
+ throw new Error(`Payload encryption misconfigured: ${envName} is required when the ` +
94
+ `corresponding key is set`);
95
+ }
96
+ return keyId;
97
+ }
98
+ function requireBootstrapKeyId(keyId, fieldName) {
99
+ if (!keyId) {
100
+ throw new Error(`Runner bootstrap returned a payload encryption key without its ${fieldName} — ` +
101
+ `refusing to encrypt under an unidentified key (server contract violation)`);
102
+ }
103
+ return keyId;
104
+ }
105
+ function parseKey(rawBase64, envName) {
106
+ let key;
107
+ try {
108
+ key = Buffer.from(rawBase64, "base64");
109
+ }
110
+ catch {
111
+ throw new Error(`Payload encryption misconfigured: ${envName} is not valid base64`);
112
+ }
113
+ if (key.length !== AES_256_KEY_BYTES) {
114
+ throw new Error(`Payload encryption misconfigured: ${envName} must decode to ` +
115
+ `${AES_256_KEY_BYTES} bytes (AES-256), got ${key.length}`);
116
+ }
117
+ return key;
118
+ }
119
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/encryption/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAmCH,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,CAAC,SAAS,CAAC,KAAK,EAAE,2BAA2B,CAAC;YAC1E,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,CAAC,SAAS,CAAC,YAAY,EAAE,4CAA4C,CAAC;aACpF;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,CAAC,KAAyB,EAAE,SAAiB;IACzE,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,CAAC,qCAAqC,OAAO,sBAAsB,CAAC,CAAC;IACtF,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"}
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Temporal PayloadCodec that encrypts payloads at rest in workflow
3
+ * history (stigmer-cloud#227).
4
+ *
5
+ * Why: the workflow engine runs inside the Temporal deterministic
6
+ * sandbox, so decrypted execution-context values cross the history
7
+ * boundary in many places — the hydrate activity result, the runtime
8
+ * env passed as an input to every per-task activity, and expression
9
+ * results recorded as local-activity markers. Encrypting at the payload
10
+ * codec layer closes the entire class with one mechanism instead of
11
+ * chasing each crossing.
12
+ *
13
+ * Envelope (cross-SDK contract — the Java decode-only codec in
14
+ * stigmer-cloud's temporal-starter must match it byte-for-byte, pinned
15
+ * by the conformance fixture in __tests__/fixtures/):
16
+ *
17
+ * metadata: encoding = "binary/encrypted"
18
+ * encryption-key-id = <key id that encrypted this payload>
19
+ * data: iv (12 bytes) ‖ AES-256-GCM(ciphertext ‖ tag (16 bytes))
20
+ *
21
+ * The plaintext is the serialized ORIGINAL Payload proto (metadata AND
22
+ * data), so decode restores the payload exactly — including its
23
+ * original encoding — with no side channel.
24
+ *
25
+ * Decode passes through payloads it did not encode. This is what keeps
26
+ * plaintext signals from the Java/Go orchestrators and pre-rollout
27
+ * in-flight histories working with zero migration. Everything else
28
+ * fails closed: unknown key id, missing key id, and ciphertext tampering
29
+ * all throw rather than surfacing bogus payloads.
30
+ *
31
+ * Moved from backend/services/runner/src/encryption/payload-codec.ts when
32
+ * the codecs became @stigmer/temporal-codecs (one home for the
33
+ * cross-language envelope contract; the TS server is the second consumer).
34
+ */
35
+ import type { Payload, PayloadCodec } from "@temporalio/common";
36
+ import type { PayloadEncryptionConfig } from "./config.js";
37
+ export declare class EncryptionPayloadCodec implements PayloadCodec {
38
+ private readonly config;
39
+ private readonly decryptKeysById;
40
+ constructor(config: PayloadEncryptionConfig);
41
+ encode(payloads: Payload[]): Promise<Payload[]>;
42
+ decode(payloads: Payload[]): Promise<Payload[]>;
43
+ private encodePayload;
44
+ private decodePayload;
45
+ }
46
+ //# sourceMappingURL=payload-codec.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"payload-codec.d.ts","sourceRoot":"","sources":["../../src/encryption/payload-codec.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;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;IAOtD,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;IAkBrB,OAAO,CAAC,aAAa;CAuBtB"}
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Temporal PayloadCodec that encrypts payloads at rest in workflow
3
+ * history (stigmer-cloud#227).
4
+ *
5
+ * Why: the workflow engine runs inside the Temporal deterministic
6
+ * sandbox, so decrypted execution-context values cross the history
7
+ * boundary in many places — the hydrate activity result, the runtime
8
+ * env passed as an input to every per-task activity, and expression
9
+ * results recorded as local-activity markers. Encrypting at the payload
10
+ * codec layer closes the entire class with one mechanism instead of
11
+ * chasing each crossing.
12
+ *
13
+ * Envelope (cross-SDK contract — the Java decode-only codec in
14
+ * stigmer-cloud's temporal-starter must match it byte-for-byte, pinned
15
+ * by the conformance fixture in __tests__/fixtures/):
16
+ *
17
+ * metadata: encoding = "binary/encrypted"
18
+ * encryption-key-id = <key id that encrypted this payload>
19
+ * data: iv (12 bytes) ‖ AES-256-GCM(ciphertext ‖ tag (16 bytes))
20
+ *
21
+ * The plaintext is the serialized ORIGINAL Payload proto (metadata AND
22
+ * data), so decode restores the payload exactly — including its
23
+ * original encoding — with no side channel.
24
+ *
25
+ * Decode passes through payloads it did not encode. This is what keeps
26
+ * plaintext signals from the Java/Go orchestrators and pre-rollout
27
+ * in-flight histories working with zero migration. Everything else
28
+ * fails closed: unknown key id, missing key id, and ciphertext tampering
29
+ * all throw rather than surfacing bogus payloads.
30
+ *
31
+ * Moved from backend/services/runner/src/encryption/payload-codec.ts when
32
+ * the codecs became @stigmer/temporal-codecs (one home for the
33
+ * cross-language envelope contract; the TS server is the second consumer).
34
+ */
35
+ import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
36
+ // Default-import + destructure, NOT `import { temporal } from …`:
37
+ // @temporalio/proto is CommonJS and its `temporal` export defeats Node's
38
+ // cjs-module-lexer named-export detection, so the named form loads under
39
+ // tsx/vitest (their interop is looser) but crashes plain `node dist/main.js`
40
+ // at startup with "Named export 'temporal' not found" (stigmer/stigmer#399
41
+ // boot regression). Pinned by scripts/verify-dist-boot.mjs in CI.
42
+ import proto from "@temporalio/proto";
43
+ const { temporal } = proto;
44
+ const ENCODING_METADATA_KEY = "encoding";
45
+ const ENCRYPTED_ENCODING_VALUE = "binary/encrypted";
46
+ const KEY_ID_METADATA_KEY = "encryption-key-id";
47
+ /** AES-GCM parameters shared with the Java implementation. */
48
+ const IV_BYTES = 12;
49
+ const AUTH_TAG_BYTES = 16;
50
+ export class EncryptionPayloadCodec {
51
+ config;
52
+ decryptKeysById;
53
+ constructor(config) {
54
+ this.config = config;
55
+ this.decryptKeysById = new Map([[config.primary.keyId, config.primary.key]]);
56
+ if (config.secondary) {
57
+ this.decryptKeysById.set(config.secondary.keyId, config.secondary.key);
58
+ }
59
+ }
60
+ async encode(payloads) {
61
+ return payloads.map((p) => this.encodePayload(p));
62
+ }
63
+ async decode(payloads) {
64
+ return payloads.map((p) => this.decodePayload(p));
65
+ }
66
+ encodePayload(payload) {
67
+ // Data-less payloads (binary/null from void results) stay as-is:
68
+ // there is nothing to protect, and the cross-language parents that
69
+ // await our workflows as void must be able to read them without a key.
70
+ if (!payload.data || payload.data.length === 0) {
71
+ return payload;
72
+ }
73
+ const plaintext = temporal.api.common.v1.Payload.encode(payload).finish();
74
+ return {
75
+ metadata: {
76
+ [ENCODING_METADATA_KEY]: Buffer.from(ENCRYPTED_ENCODING_VALUE),
77
+ [KEY_ID_METADATA_KEY]: Buffer.from(this.config.primary.keyId),
78
+ },
79
+ data: encrypt(plaintext, this.config.primary),
80
+ };
81
+ }
82
+ decodePayload(payload) {
83
+ if (!isEncryptedPayload(payload)) {
84
+ return payload;
85
+ }
86
+ const keyIdBytes = payload.metadata?.[KEY_ID_METADATA_KEY];
87
+ if (!keyIdBytes) {
88
+ throw new Error("Encrypted payload is missing its encryption-key-id metadata — refusing to decode");
89
+ }
90
+ const keyId = Buffer.from(keyIdBytes).toString("utf-8");
91
+ const key = this.decryptKeysById.get(keyId);
92
+ if (!key) {
93
+ throw new Error(`Encrypted payload uses unknown key id '${keyId}' — configure it as the ` +
94
+ `primary or secondary payload encryption key (rotation window?)`);
95
+ }
96
+ const plaintext = decrypt(payload.data, key, keyId);
97
+ return temporal.api.common.v1.Payload.decode(plaintext);
98
+ }
99
+ }
100
+ function isEncryptedPayload(payload) {
101
+ const encoding = payload.metadata?.[ENCODING_METADATA_KEY];
102
+ if (!encoding)
103
+ return false;
104
+ return Buffer.from(encoding).toString("utf-8") === ENCRYPTED_ENCODING_VALUE;
105
+ }
106
+ function encrypt(plaintext, key) {
107
+ const iv = randomBytes(IV_BYTES);
108
+ const cipher = createCipheriv("aes-256-gcm", key.key, iv);
109
+ const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
110
+ // Layout iv ‖ ciphertext ‖ tag matches Java's AES/GCM/NoPadding, whose
111
+ // doFinal() output is ciphertext ‖ tag.
112
+ return Buffer.concat([iv, ciphertext, cipher.getAuthTag()]);
113
+ }
114
+ function decrypt(data, key, keyId) {
115
+ if (data.length < IV_BYTES + AUTH_TAG_BYTES) {
116
+ throw new Error(`Encrypted payload under key id '${keyId}' is truncated (${data.length} bytes)`);
117
+ }
118
+ const buf = Buffer.from(data);
119
+ const iv = buf.subarray(0, IV_BYTES);
120
+ const ciphertext = buf.subarray(IV_BYTES, buf.length - AUTH_TAG_BYTES);
121
+ const tag = buf.subarray(buf.length - AUTH_TAG_BYTES);
122
+ const decipher = createDecipheriv("aes-256-gcm", key, iv);
123
+ decipher.setAuthTag(tag);
124
+ try {
125
+ return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
126
+ }
127
+ catch {
128
+ // GCM auth failure: tampered ciphertext or a key that does not match
129
+ // its advertised id. Never surface partially decrypted bytes.
130
+ throw new Error(`Failed to decrypt payload under key id '${keyId}' — ciphertext is ` +
131
+ `corrupt or the configured key does not match`);
132
+ }
133
+ }
134
+ //# sourceMappingURL=payload-codec.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"payload-codec.js","sourceRoot":"","sources":["../../src/encryption/payload-codec.ts"],"names":[],"mappings":"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"}
package/index.d.ts ADDED
@@ -0,0 +1,28 @@
1
+ /**
2
+ * @stigmer/temporal-codecs — Temporal payload codecs shared by Stigmer's
3
+ * TypeScript Temporal processes (the runner today; the TS server at its
4
+ * cutover). The encryption envelope is a cross-language wire contract
5
+ * (the Java decode-only codec in stigmer-cloud's temporal-starter must
6
+ * match it byte-for-byte, pinned by the fixture in
7
+ * src/__tests__/fixtures/), which is why the codecs live in one library
8
+ * instead of per-consumer copies that could fork.
9
+ *
10
+ * This is the package's ONLY public boundary. Codec order at the consumer
11
+ * is a correctness property: install [encryption, claimcheck] so encode
12
+ * encrypts before relocating (object storage only ever sees ciphertext)
13
+ * and decode restores the blob before decrypting.
14
+ *
15
+ * Dependency policy: @temporalio/common and @temporalio/proto are pinned
16
+ * exact and identical to the runner's pins, bumped in lockstep with it —
17
+ * the runner's consumer-install gate (stigmer#786) fails any version mix
18
+ * because a split @temporalio/proto tree registers the core-sdk protobuf
19
+ * namespace twice and crashes worker init.
20
+ */
21
+ export { EncryptionPayloadCodec } from "./encryption/payload-codec.js";
22
+ export { loadPayloadEncryptionConfig } from "./encryption/config.js";
23
+ export type { BootstrapKeyMaterial, EncryptionKey, PayloadEncryptionConfig, SecretReader, } from "./encryption/config.js";
24
+ export { ClaimcheckPayloadCodec } from "./claimcheck/payload-codec.js";
25
+ export { loadClaimcheckConfig } from "./claimcheck/config.js";
26
+ export type { ClaimcheckConfig } from "./claimcheck/config.js";
27
+ export type { ClaimcheckStorage } from "./claimcheck/storage.js";
28
+ //# sourceMappingURL=index.d.ts.map
package/index.d.ts.map ADDED
@@ -0,0 +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"}
package/index.js ADDED
@@ -0,0 +1,25 @@
1
+ /**
2
+ * @stigmer/temporal-codecs — Temporal payload codecs shared by Stigmer's
3
+ * TypeScript Temporal processes (the runner today; the TS server at its
4
+ * cutover). The encryption envelope is a cross-language wire contract
5
+ * (the Java decode-only codec in stigmer-cloud's temporal-starter must
6
+ * match it byte-for-byte, pinned by the fixture in
7
+ * src/__tests__/fixtures/), which is why the codecs live in one library
8
+ * instead of per-consumer copies that could fork.
9
+ *
10
+ * This is the package's ONLY public boundary. Codec order at the consumer
11
+ * is a correctness property: install [encryption, claimcheck] so encode
12
+ * encrypts before relocating (object storage only ever sees ciphertext)
13
+ * and decode restores the blob before decrypting.
14
+ *
15
+ * Dependency policy: @temporalio/common and @temporalio/proto are pinned
16
+ * exact and identical to the runner's pins, bumped in lockstep with it —
17
+ * the runner's consumer-install gate (stigmer#786) fails any version mix
18
+ * because a split @temporalio/proto tree registers the core-sdk protobuf
19
+ * namespace twice and crashes worker init.
20
+ */
21
+ export { EncryptionPayloadCodec } from "./encryption/payload-codec.js";
22
+ export { loadPayloadEncryptionConfig } from "./encryption/config.js";
23
+ export { ClaimcheckPayloadCodec } from "./claimcheck/payload-codec.js";
24
+ export { loadClaimcheckConfig } from "./claimcheck/config.js";
25
+ //# sourceMappingURL=index.js.map
package/index.js.map ADDED
@@ -0,0 +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;AAQrE,OAAO,EAAE,sBAAsB,EAAE,MAAM,+BAA+B,CAAC;AACvE,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC"}
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@stigmer/temporal-codecs",
3
+ "version": "3.12.9",
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
+ "license": "Apache-2.0",
6
+ "type": "module",
7
+ "sideEffects": false,
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/stigmer/stigmer.git",
11
+ "directory": "backend/libs/ts/temporal-codecs"
12
+ },
13
+ "engines": {
14
+ "node": "^22.13.0 || >=23.4.0"
15
+ },
16
+ "keywords": [
17
+ "stigmer",
18
+ "temporal",
19
+ "payload-codec",
20
+ "encryption",
21
+ "claim-check"
22
+ ],
23
+ "main": "./index.js",
24
+ "types": "./index.d.ts",
25
+ "exports": {
26
+ ".": {
27
+ "types": "./index.d.ts",
28
+ "import": "./index.js",
29
+ "default": "./index.js"
30
+ }
31
+ },
32
+ "dependencies": {
33
+ "@temporalio/common": "1.16.2",
34
+ "@temporalio/proto": "1.16.2"
35
+ }
36
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * The canonical in-memory {@link ClaimcheckStorage} test double — the
3
+ * lib-scoped sibling of the runner's fake-artifact-storage.ts (which
4
+ * models the runner's richer ArtifactStorage port and stays there).
5
+ *
6
+ * Design, mirrored from that double:
7
+ * - `upload` / `download` are backed by one shared `Map`, so a key that
8
+ * was uploaded reads back byte-exact — the double behaves like a real
9
+ * content store, not a pair of disconnected stubs.
10
+ * - Both methods are `vi.fn`s, so call assertions work and either can be
11
+ * overridden per-test to force a failure
12
+ * (`storage.download.mockRejectedValueOnce(new Error("… HTTP 404 …"))`).
13
+ */
14
+
15
+ import { vi } from "vitest";
16
+ import type { ClaimcheckStorage } from "../claimcheck/storage.js";
17
+
18
+ /** A {@link ClaimcheckStorage} whose methods are `vi.fn` spies over a shared Map. */
19
+ export type InMemoryClaimcheckStorage = {
20
+ [K in keyof ClaimcheckStorage]: ReturnType<typeof vi.fn>;
21
+ } & ClaimcheckStorage;
22
+
23
+ export interface InMemoryClaimcheckStorageHandle {
24
+ /** The storage double to inject; both methods are spy-able `vi.fn`s. */
25
+ readonly storage: InMemoryClaimcheckStorage;
26
+ /** The backing store — inspect or seed it directly in a test. */
27
+ readonly blobs: Map<string, Buffer>;
28
+ }
29
+
30
+ /**
31
+ * Build an in-memory {@link ClaimcheckStorage} backed by a shared `Map`.
32
+ *
33
+ * @example
34
+ * const { storage, blobs } = makeInMemoryClaimcheckStorage();
35
+ * await storage.upload("k", Buffer.from("hi"));
36
+ * expect((await storage.download("k")).toString()).toBe("hi");
37
+ */
38
+ export function makeInMemoryClaimcheckStorage(): InMemoryClaimcheckStorageHandle {
39
+ const blobs = new Map<string, Buffer>();
40
+
41
+ const storage = {
42
+ upload: vi.fn(async (key: string, content: Buffer, _contentType?: string) => {
43
+ blobs.set(key, Buffer.from(content));
44
+ return key;
45
+ }),
46
+ download: vi.fn(async (key: string) => {
47
+ const b = blobs.get(key);
48
+ if (!b) throw new Error(`Artifact not found for key '${key}'`);
49
+ return Buffer.from(b);
50
+ }),
51
+ } as InMemoryClaimcheckStorage;
52
+
53
+ return { storage, blobs };
54
+ }