@stigmer/temporal-codecs 0.1.0-dev.20261017020314.g644422724c27
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/LICENSE +194 -0
- package/README.md +48 -0
- package/claimcheck/compressor.d.ts +12 -0
- package/claimcheck/compressor.d.ts.map +1 -0
- package/claimcheck/compressor.js +17 -0
- package/claimcheck/compressor.js.map +1 -0
- package/claimcheck/config.d.ts +17 -0
- package/claimcheck/config.d.ts.map +1 -0
- package/claimcheck/config.js +19 -0
- package/claimcheck/config.js.map +1 -0
- package/claimcheck/payload-codec.d.ts +29 -0
- package/claimcheck/payload-codec.d.ts.map +1 -0
- package/claimcheck/payload-codec.js +110 -0
- package/claimcheck/payload-codec.js.map +1 -0
- package/claimcheck/storage.d.ts +20 -0
- package/claimcheck/storage.d.ts.map +1 -0
- package/claimcheck/storage.js +2 -0
- package/claimcheck/storage.js.map +1 -0
- package/connection/config.d.ts +100 -0
- package/connection/config.d.ts.map +1 -0
- package/connection/config.js +171 -0
- package/connection/config.js.map +1 -0
- package/encryption/config.d.ts +100 -0
- package/encryption/config.d.ts.map +1 -0
- package/encryption/config.js +119 -0
- package/encryption/config.js.map +1 -0
- package/encryption/payload-codec.d.ts +60 -0
- package/encryption/payload-codec.d.ts.map +1 -0
- package/encryption/payload-codec.js +163 -0
- package/encryption/payload-codec.js.map +1 -0
- package/index.d.ts +32 -0
- package/index.d.ts.map +1 -0
- package/index.js +28 -0
- package/index.js.map +1 -0
- package/package.json +42 -0
- package/src/claimcheck/compressor.ts +19 -0
- package/src/claimcheck/config.ts +30 -0
- package/src/claimcheck/payload-codec.ts +144 -0
- package/src/claimcheck/storage.ts +19 -0
- package/src/connection/config.ts +217 -0
- package/src/encryption/config.ts +204 -0
- package/src/encryption/payload-codec.ts +186 -0
- package/src/index.ts +49 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Temporal connection security: how a Stigmer process authenticates to the
|
|
3
|
+
* Temporal frontend it dials. The server's client and worker connections
|
|
4
|
+
* and the runner's worker connection all read it, so it lives here, in the
|
|
5
|
+
* one library both processes share. Unset means today's plaintext
|
|
6
|
+
* connection, unchanged.
|
|
7
|
+
*
|
|
8
|
+
* The settings (each `_PATH` item has a `_DATA` twin holding the PEM text):
|
|
9
|
+
*
|
|
10
|
+
* STIGMER_TEMPORAL_API_KEY bearer key; implies TLS
|
|
11
|
+
* STIGMER_TEMPORAL_TLS "true" or "1": TLS with the
|
|
12
|
+
* system's trusted roots
|
|
13
|
+
* STIGMER_TEMPORAL_TLS_SERVER_NAME SNI / host-name override
|
|
14
|
+
* STIGMER_TEMPORAL_TLS_SERVER_CA_CERT_PATH the frontend's CA (implies TLS)
|
|
15
|
+
* STIGMER_TEMPORAL_TLS_CLIENT_CERT_PATH mutual TLS: the client's
|
|
16
|
+
* STIGMER_TEMPORAL_TLS_CLIENT_KEY_PATH certificate and key, together
|
|
17
|
+
*
|
|
18
|
+
* WHY Stigmer's own names, not Temporal's `TEMPORAL_API_KEY` and
|
|
19
|
+
* `TEMPORAL_TLS_*`: those belong to the user's own Temporal work. A local
|
|
20
|
+
* Stigmer (`stigmer up`) inherits the user's shell, and agents that build
|
|
21
|
+
* Temporal applications export `TEMPORAL_API_KEY` for their own namespaces.
|
|
22
|
+
* Reading it would present the user's key to Stigmer's own Temporal and,
|
|
23
|
+
* through the runner's secret custody, take it away from the agents that
|
|
24
|
+
* need it. The shape follows Temporal's so an operator recognises it.
|
|
25
|
+
*
|
|
26
|
+
* WHY not Temporal's reader (`@temporalio/envconfig`): at the SDK version
|
|
27
|
+
* every Stigmer package pins (1.16.2), it reads `TEMPORAL_TLS=true` as TLS
|
|
28
|
+
* disabled. Releases before 1.22.0 carry that inversion. It also ignores
|
|
29
|
+
* host-verification settings, and it reads the user's own Temporal profile
|
|
30
|
+
* file by default.
|
|
31
|
+
*
|
|
32
|
+
* Misconfiguration fails the boot, the posture of the payload-encryption
|
|
33
|
+
* config beside this module: an operator who set half a client pair, or
|
|
34
|
+
* the same item twice, believes the connection is authenticated, and a
|
|
35
|
+
* silently weaker connection would be a security failure. An empty value
|
|
36
|
+
* means unset, so a blank line in a deployment's environment turns nothing
|
|
37
|
+
* on.
|
|
38
|
+
*
|
|
39
|
+
* Values are read through an injected {@link SecretReader}, as the
|
|
40
|
+
* encryption keys are: secret custody is the consumer's policy. The runner
|
|
41
|
+
* passes its credential store's reader, so the API key and the client key
|
|
42
|
+
* never live in an environment its agent tools can read.
|
|
43
|
+
*/
|
|
44
|
+
import { readFileSync } from "node:fs";
|
|
45
|
+
export const TEMPORAL_API_KEY_ENV = "STIGMER_TEMPORAL_API_KEY";
|
|
46
|
+
export const TEMPORAL_TLS_ENV = "STIGMER_TEMPORAL_TLS";
|
|
47
|
+
export const TEMPORAL_TLS_SERVER_NAME_ENV = "STIGMER_TEMPORAL_TLS_SERVER_NAME";
|
|
48
|
+
/** The PEM items, each settable as a file path or as its text. */
|
|
49
|
+
const PEM_ITEMS = {
|
|
50
|
+
serverCa: "STIGMER_TEMPORAL_TLS_SERVER_CA_CERT",
|
|
51
|
+
clientCert: "STIGMER_TEMPORAL_TLS_CLIENT_CERT",
|
|
52
|
+
clientKey: "STIGMER_TEMPORAL_TLS_CLIENT_KEY",
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* The `_DATA` name of the client key: a credential, which the runner takes
|
|
56
|
+
* into custody beside the API key (its `_PATH` twin names a file, not a
|
|
57
|
+
* secret).
|
|
58
|
+
*/
|
|
59
|
+
export const TEMPORAL_TLS_CLIENT_KEY_DATA_ENV = `${PEM_ITEMS.clientKey}_DATA`;
|
|
60
|
+
/**
|
|
61
|
+
* Every setting name this module reads, exactly. A process that hands its
|
|
62
|
+
* children their own rendered settings ({@link temporalConnectionEnv})
|
|
63
|
+
* removes these from what the child inherits; other `STIGMER_TEMPORAL_*`
|
|
64
|
+
* names (the CLI's own) are not connection settings.
|
|
65
|
+
*/
|
|
66
|
+
export const TEMPORAL_CONNECTION_ENV_NAMES = [
|
|
67
|
+
TEMPORAL_API_KEY_ENV,
|
|
68
|
+
TEMPORAL_TLS_ENV,
|
|
69
|
+
TEMPORAL_TLS_SERVER_NAME_ENV,
|
|
70
|
+
...Object.values(PEM_ITEMS).flatMap((item) => [`${item}_PATH`, `${item}_DATA`]),
|
|
71
|
+
];
|
|
72
|
+
/**
|
|
73
|
+
* Loads the connection settings.
|
|
74
|
+
*
|
|
75
|
+
* @throws when a setting is contradictory or unreadable: a client
|
|
76
|
+
* certificate without its key (or the reverse), both the `_PATH` and the
|
|
77
|
+
* `_DATA` form of one item, a path that cannot be read, or a
|
|
78
|
+
* `STIGMER_TEMPORAL_TLS` that is neither true nor false.
|
|
79
|
+
*/
|
|
80
|
+
export function loadTemporalConnectionConfig(read) {
|
|
81
|
+
const value = (name) => {
|
|
82
|
+
const raw = read(name);
|
|
83
|
+
return raw === undefined || raw.trim() === "" ? undefined : raw;
|
|
84
|
+
};
|
|
85
|
+
const apiKey = value(TEMPORAL_API_KEY_ENV)?.trim();
|
|
86
|
+
const tlsFlag = parseFlag(value(TEMPORAL_TLS_ENV));
|
|
87
|
+
const serverName = value(TEMPORAL_TLS_SERVER_NAME_ENV)?.trim();
|
|
88
|
+
const serverCa = readPem(PEM_ITEMS.serverCa, value);
|
|
89
|
+
const clientCert = readPem(PEM_ITEMS.clientCert, value);
|
|
90
|
+
const clientKey = readPem(PEM_ITEMS.clientKey, value);
|
|
91
|
+
if ((clientCert === undefined) !== (clientKey === undefined)) {
|
|
92
|
+
throw new Error(`Temporal connection misconfigured: mutual TLS needs both ` +
|
|
93
|
+
`${PEM_ITEMS.clientCert}_* and ${PEM_ITEMS.clientKey}_* — set both or neither`);
|
|
94
|
+
}
|
|
95
|
+
const tlsRequested = tlsFlag === true ||
|
|
96
|
+
apiKey !== undefined ||
|
|
97
|
+
serverName !== undefined ||
|
|
98
|
+
serverCa !== undefined ||
|
|
99
|
+
clientCert !== undefined;
|
|
100
|
+
if (tlsFlag === false && tlsRequested) {
|
|
101
|
+
throw new Error(`Temporal connection misconfigured: ${TEMPORAL_TLS_ENV} is false while ` +
|
|
102
|
+
`an API key, a server name, a CA or a client certificate is set — ` +
|
|
103
|
+
`each of those needs TLS`);
|
|
104
|
+
}
|
|
105
|
+
if (!tlsRequested)
|
|
106
|
+
return {};
|
|
107
|
+
const tls = {
|
|
108
|
+
...(serverName !== undefined ? { serverNameOverride: serverName } : {}),
|
|
109
|
+
...(serverCa !== undefined ? { serverRootCACertificate: serverCa } : {}),
|
|
110
|
+
...(clientCert !== undefined && clientKey !== undefined
|
|
111
|
+
? { clientCertPair: { crt: clientCert, key: clientKey } }
|
|
112
|
+
: {}),
|
|
113
|
+
};
|
|
114
|
+
return { tls, ...(apiKey !== undefined ? { apiKey } : {}) };
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Renders loaded settings back as the environment a child runner reads,
|
|
118
|
+
* every PEM item in its `_DATA` form, so that no file has to exist where
|
|
119
|
+
* the child runs. The server's sandbox drivers pass this to the runners
|
|
120
|
+
* they start. Round-trips: loading the rendered environment yields the
|
|
121
|
+
* same settings.
|
|
122
|
+
*/
|
|
123
|
+
export function temporalConnectionEnv(config) {
|
|
124
|
+
const env = {};
|
|
125
|
+
if (config.tls === undefined)
|
|
126
|
+
return env;
|
|
127
|
+
env[TEMPORAL_TLS_ENV] = "true";
|
|
128
|
+
const text = (bytes) => Buffer.from(bytes).toString("utf8");
|
|
129
|
+
const { serverNameOverride, serverRootCACertificate, clientCertPair } = config.tls;
|
|
130
|
+
if (serverNameOverride !== undefined)
|
|
131
|
+
env[TEMPORAL_TLS_SERVER_NAME_ENV] = serverNameOverride;
|
|
132
|
+
if (serverRootCACertificate !== undefined) {
|
|
133
|
+
env[`${PEM_ITEMS.serverCa}_DATA`] = text(serverRootCACertificate);
|
|
134
|
+
}
|
|
135
|
+
if (clientCertPair !== undefined) {
|
|
136
|
+
env[`${PEM_ITEMS.clientCert}_DATA`] = text(clientCertPair.crt);
|
|
137
|
+
env[TEMPORAL_TLS_CLIENT_KEY_DATA_ENV] = text(clientCertPair.key);
|
|
138
|
+
}
|
|
139
|
+
if (config.apiKey !== undefined)
|
|
140
|
+
env[TEMPORAL_API_KEY_ENV] = config.apiKey;
|
|
141
|
+
return env;
|
|
142
|
+
}
|
|
143
|
+
function parseFlag(raw) {
|
|
144
|
+
if (raw === undefined)
|
|
145
|
+
return undefined;
|
|
146
|
+
const flag = raw.trim().toLowerCase();
|
|
147
|
+
if (flag === "true" || flag === "1")
|
|
148
|
+
return true;
|
|
149
|
+
if (flag === "false" || flag === "0")
|
|
150
|
+
return false;
|
|
151
|
+
throw new Error(`Temporal connection misconfigured: ${TEMPORAL_TLS_ENV} must be true, 1, false or 0, got "${raw}"`);
|
|
152
|
+
}
|
|
153
|
+
function readPem(item, value) {
|
|
154
|
+
const path = value(`${item}_PATH`);
|
|
155
|
+
const data = value(`${item}_DATA`);
|
|
156
|
+
if (path !== undefined && data !== undefined) {
|
|
157
|
+
throw new Error(`Temporal connection misconfigured: set ${item}_PATH or ${item}_DATA, not both`);
|
|
158
|
+
}
|
|
159
|
+
if (data !== undefined)
|
|
160
|
+
return Buffer.from(data, "utf8");
|
|
161
|
+
if (path === undefined)
|
|
162
|
+
return undefined;
|
|
163
|
+
try {
|
|
164
|
+
return readFileSync(path.trim());
|
|
165
|
+
}
|
|
166
|
+
catch (err) {
|
|
167
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
168
|
+
throw new Error(`Temporal connection misconfigured: ${item}_PATH "${path.trim()}" cannot be read (${reason})`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/connection/config.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Payload-encryption configuration (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
|
+
export interface EncryptionKey {
|
|
39
|
+
readonly keyId: string;
|
|
40
|
+
/** 32-byte AES-256 key. */
|
|
41
|
+
readonly key: Buffer;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Resolves decrypt-key material for a key id absent from the static
|
|
45
|
+
* config (a hosting server may decode desktop-runner histories written
|
|
46
|
+
* under server-managed per-identity `rpk_` keys, whose material lives in
|
|
47
|
+
* 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>;
|
|
55
|
+
export interface PayloadEncryptionConfig {
|
|
56
|
+
/** Key used to encrypt outgoing payloads (and decrypt its own). */
|
|
57
|
+
readonly primary: EncryptionKey;
|
|
58
|
+
/** Decrypt-only key accepted during rotation windows. */
|
|
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;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Reads one secret VALUE by its env-var name. Consumers inject their own
|
|
69
|
+
* custody policy (see the module doc); the *_KEY_ID companions are
|
|
70
|
+
* rotation bookkeeping, not secrets, and stay plain `process.env` reads.
|
|
71
|
+
*/
|
|
72
|
+
export type SecretReader = (name: string) => string | undefined;
|
|
73
|
+
/**
|
|
74
|
+
* Server-managed key material delivered by the runner's
|
|
75
|
+
* getRunnerBootstrapConfig. Structurally mirrors
|
|
76
|
+
* {@link BootstrapPayloadEncryptionKeys} in the runner's stigmer-client.ts
|
|
77
|
+
* — declared here so the library stays free of client imports.
|
|
78
|
+
*/
|
|
79
|
+
export interface BootstrapKeyMaterial {
|
|
80
|
+
readonly key: string;
|
|
81
|
+
readonly keyId?: string;
|
|
82
|
+
readonly secondaryKey?: string;
|
|
83
|
+
readonly secondaryKeyId?: string;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Returns the encryption config, or undefined when encryption is not
|
|
87
|
+
* configured (the codec is then simply not installed).
|
|
88
|
+
*
|
|
89
|
+
* Source precedence: the env key wins outright; bootstrap-delivered
|
|
90
|
+
* material applies only when no env key is set (see the module doc).
|
|
91
|
+
*
|
|
92
|
+
* @throws when a key is present but malformed, or a key id is missing —
|
|
93
|
+
* key misconfiguration must stop the boot, not degrade to plaintext.
|
|
94
|
+
* This applies equally to bootstrap material: a server that hands out
|
|
95
|
+
* a bad key or omits its id has broken the protocol contract, and
|
|
96
|
+
* running plaintext against a server that manages keys would silently
|
|
97
|
+
* defeat the feature.
|
|
98
|
+
*/
|
|
99
|
+
export declare function loadPayloadEncryptionConfig(readSecret: SecretReader, bootstrap?: BootstrapKeyMaterial): PayloadEncryptionConfig | undefined;
|
|
100
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +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;;;;;;;;;;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"}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Payload-encryption configuration (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":"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"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Temporal PayloadCodec that encrypts payloads at rest in workflow
|
|
3
|
+
* history.
|
|
4
|
+
*
|
|
5
|
+
* Why: Temporal history is durable, and an agent run puts customer data
|
|
6
|
+
* there at every crossing — the run credential on each runner activity's
|
|
7
|
+
* input, and each activity result carrying the run's status (its errors,
|
|
8
|
+
* its pending approvals, what the run said and did). A run's vault values
|
|
9
|
+
* never ride history (the runner fetches them when the work starts), but
|
|
10
|
+
* the rest is still a customer's. Encrypting at the payload codec layer
|
|
11
|
+
* covers every crossing with one mechanism instead of chasing each one.
|
|
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
|
+
* Key ids outside the static primary/secondary pair may be resolved
|
|
32
|
+
* through the config's optional resolveKey seam (a hosting server may
|
|
33
|
+
* decode 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
|
+
*
|
|
39
|
+
* Moved from backend/services/runner/src/encryption/payload-codec.ts when
|
|
40
|
+
* the codecs became @stigmer/temporal-codecs (one home for the
|
|
41
|
+
* cross-language envelope contract; the TS server is the second consumer).
|
|
42
|
+
*/
|
|
43
|
+
import type { Payload, PayloadCodec } from "@temporalio/common";
|
|
44
|
+
import type { PayloadEncryptionConfig } from "./config.js";
|
|
45
|
+
export declare class EncryptionPayloadCodec implements PayloadCodec {
|
|
46
|
+
private readonly config;
|
|
47
|
+
private readonly decryptKeysById;
|
|
48
|
+
constructor(config: PayloadEncryptionConfig);
|
|
49
|
+
encode(payloads: Payload[]): Promise<Payload[]>;
|
|
50
|
+
decode(payloads: Payload[]): Promise<Payload[]>;
|
|
51
|
+
private encodePayload;
|
|
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;
|
|
59
|
+
}
|
|
60
|
+
//# 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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;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"}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Temporal PayloadCodec that encrypts payloads at rest in workflow
|
|
3
|
+
* history.
|
|
4
|
+
*
|
|
5
|
+
* Why: Temporal history is durable, and an agent run puts customer data
|
|
6
|
+
* there at every crossing — the run credential on each runner activity's
|
|
7
|
+
* input, and each activity result carrying the run's status (its errors,
|
|
8
|
+
* its pending approvals, what the run said and did). A run's vault values
|
|
9
|
+
* never ride history (the runner fetches them when the work starts), but
|
|
10
|
+
* the rest is still a customer's. Encrypting at the payload codec layer
|
|
11
|
+
* covers every crossing with one mechanism instead of chasing each one.
|
|
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
|
+
* Key ids outside the static primary/secondary pair may be resolved
|
|
32
|
+
* through the config's optional resolveKey seam (a hosting server may
|
|
33
|
+
* decode 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
|
+
*
|
|
39
|
+
* Moved from backend/services/runner/src/encryption/payload-codec.ts when
|
|
40
|
+
* the codecs became @stigmer/temporal-codecs (one home for the
|
|
41
|
+
* cross-language envelope contract; the TS server is the second consumer).
|
|
42
|
+
*/
|
|
43
|
+
import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
|
|
44
|
+
// Default-import + destructure, NOT `import { temporal } from …`:
|
|
45
|
+
// @temporalio/proto is CommonJS and its `temporal` export defeats Node's
|
|
46
|
+
// cjs-module-lexer named-export detection, so the named form loads under
|
|
47
|
+
// tsx/vitest (their interop is looser) but crashes plain `node dist/main.js`
|
|
48
|
+
// at startup with "Named export 'temporal' not found" (stigmer/stigmer#399
|
|
49
|
+
// boot regression). Pinned by scripts/verify-dist-boot.mjs in CI.
|
|
50
|
+
import proto from "@temporalio/proto";
|
|
51
|
+
const { temporal } = proto;
|
|
52
|
+
const ENCODING_METADATA_KEY = "encoding";
|
|
53
|
+
const ENCRYPTED_ENCODING_VALUE = "binary/encrypted";
|
|
54
|
+
const KEY_ID_METADATA_KEY = "encryption-key-id";
|
|
55
|
+
/** AES-GCM parameters shared with the Java implementation. */
|
|
56
|
+
const IV_BYTES = 12;
|
|
57
|
+
const AUTH_TAG_BYTES = 16;
|
|
58
|
+
export class EncryptionPayloadCodec {
|
|
59
|
+
config;
|
|
60
|
+
decryptKeysById;
|
|
61
|
+
constructor(config) {
|
|
62
|
+
this.config = config;
|
|
63
|
+
this.decryptKeysById = new Map([
|
|
64
|
+
[config.primary.keyId, config.primary.key],
|
|
65
|
+
]);
|
|
66
|
+
if (config.secondary) {
|
|
67
|
+
this.decryptKeysById.set(config.secondary.keyId, config.secondary.key);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
async encode(payloads) {
|
|
71
|
+
return payloads.map((p) => this.encodePayload(p));
|
|
72
|
+
}
|
|
73
|
+
async decode(payloads) {
|
|
74
|
+
return Promise.all(payloads.map((p) => this.decodePayload(p)));
|
|
75
|
+
}
|
|
76
|
+
encodePayload(payload) {
|
|
77
|
+
// Data-less payloads (binary/null from void results) stay as-is:
|
|
78
|
+
// there is nothing to protect, and the cross-language parents that
|
|
79
|
+
// await our workflows as void must be able to read them without a key.
|
|
80
|
+
if (!payload.data || payload.data.length === 0) {
|
|
81
|
+
return payload;
|
|
82
|
+
}
|
|
83
|
+
const plaintext = temporal.api.common.v1.Payload.encode(payload).finish();
|
|
84
|
+
return {
|
|
85
|
+
metadata: {
|
|
86
|
+
[ENCODING_METADATA_KEY]: Buffer.from(ENCRYPTED_ENCODING_VALUE),
|
|
87
|
+
[KEY_ID_METADATA_KEY]: Buffer.from(this.config.primary.keyId),
|
|
88
|
+
},
|
|
89
|
+
data: encrypt(plaintext, this.config.primary),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
async decodePayload(payload) {
|
|
93
|
+
if (!isEncryptedPayload(payload)) {
|
|
94
|
+
return payload;
|
|
95
|
+
}
|
|
96
|
+
const keyIdBytes = payload.metadata?.[KEY_ID_METADATA_KEY];
|
|
97
|
+
if (!keyIdBytes) {
|
|
98
|
+
throw new Error("Encrypted payload is missing its encryption-key-id metadata — refusing to decode");
|
|
99
|
+
}
|
|
100
|
+
const keyId = Buffer.from(keyIdBytes).toString("utf-8");
|
|
101
|
+
const key = await this.decryptKeyFor(keyId);
|
|
102
|
+
if (!key) {
|
|
103
|
+
throw new Error(`Encrypted payload uses unknown key id '${keyId}' — configure it as the ` +
|
|
104
|
+
`primary or secondary payload encryption key (rotation window?)`);
|
|
105
|
+
}
|
|
106
|
+
const plaintext = decrypt(payload.data, key, keyId);
|
|
107
|
+
return temporal.api.common.v1.Payload.decode(plaintext);
|
|
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
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function isEncryptedPayload(payload) {
|
|
130
|
+
const encoding = payload.metadata?.[ENCODING_METADATA_KEY];
|
|
131
|
+
if (!encoding)
|
|
132
|
+
return false;
|
|
133
|
+
return Buffer.from(encoding).toString("utf-8") === ENCRYPTED_ENCODING_VALUE;
|
|
134
|
+
}
|
|
135
|
+
function encrypt(plaintext, key) {
|
|
136
|
+
const iv = randomBytes(IV_BYTES);
|
|
137
|
+
const cipher = createCipheriv("aes-256-gcm", key.key, iv);
|
|
138
|
+
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
139
|
+
// Layout iv ‖ ciphertext ‖ tag matches Java's AES/GCM/NoPadding, whose
|
|
140
|
+
// doFinal() output is ciphertext ‖ tag.
|
|
141
|
+
return Buffer.concat([iv, ciphertext, cipher.getAuthTag()]);
|
|
142
|
+
}
|
|
143
|
+
function decrypt(data, key, keyId) {
|
|
144
|
+
if (data.length < IV_BYTES + AUTH_TAG_BYTES) {
|
|
145
|
+
throw new Error(`Encrypted payload under key id '${keyId}' is truncated (${data.length} bytes)`);
|
|
146
|
+
}
|
|
147
|
+
const buf = Buffer.from(data);
|
|
148
|
+
const iv = buf.subarray(0, IV_BYTES);
|
|
149
|
+
const ciphertext = buf.subarray(IV_BYTES, buf.length - AUTH_TAG_BYTES);
|
|
150
|
+
const tag = buf.subarray(buf.length - AUTH_TAG_BYTES);
|
|
151
|
+
const decipher = createDecipheriv("aes-256-gcm", key, iv);
|
|
152
|
+
decipher.setAuthTag(tag);
|
|
153
|
+
try {
|
|
154
|
+
return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
// GCM auth failure: tampered ciphertext or a key that does not match
|
|
158
|
+
// its advertised id. Never surface partially decrypted bytes.
|
|
159
|
+
throw new Error(`Failed to decrypt payload under key id '${keyId}' — ciphertext is ` +
|
|
160
|
+
`corrupt or the configured key does not match`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
//# 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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"}
|