@stigmer/temporal-codecs 3.29.0 → 3.31.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/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/index.d.ts +11 -7
- package/index.d.ts.map +1 -1
- package/index.js +10 -7
- package/index.js.map +1 -1
- package/package.json +4 -3
- package/src/__tests__/connection-config.test.ts +155 -0
- package/src/connection/config.ts +217 -0
- package/src/index.ts +21 -7
|
@@ -0,0 +1,100 @@
|
|
|
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 type { SecretReader } from "../encryption/config.js";
|
|
45
|
+
/**
|
|
46
|
+
* TLS settings in the Temporal SDK's own shape (`TLSConfig` in
|
|
47
|
+
* `@temporalio/common`), declared structurally so this library imports
|
|
48
|
+
* nothing internal. An empty object means TLS with the system's roots.
|
|
49
|
+
*/
|
|
50
|
+
export interface TemporalTls {
|
|
51
|
+
readonly serverNameOverride?: string;
|
|
52
|
+
readonly serverRootCACertificate?: Uint8Array;
|
|
53
|
+
readonly clientCertPair?: {
|
|
54
|
+
readonly crt: Uint8Array;
|
|
55
|
+
readonly key: Uint8Array;
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* What a connection needs beyond its address. Both fields are absent for a
|
|
60
|
+
* plaintext connection; spread it into `Connection.connect` or
|
|
61
|
+
* `NativeConnection.connect` as is.
|
|
62
|
+
*/
|
|
63
|
+
export interface TemporalConnectionConfig {
|
|
64
|
+
readonly tls?: TemporalTls;
|
|
65
|
+
readonly apiKey?: string;
|
|
66
|
+
}
|
|
67
|
+
export declare const TEMPORAL_API_KEY_ENV = "STIGMER_TEMPORAL_API_KEY";
|
|
68
|
+
export declare const TEMPORAL_TLS_ENV = "STIGMER_TEMPORAL_TLS";
|
|
69
|
+
export declare const TEMPORAL_TLS_SERVER_NAME_ENV = "STIGMER_TEMPORAL_TLS_SERVER_NAME";
|
|
70
|
+
/**
|
|
71
|
+
* The `_DATA` name of the client key: a credential, which the runner takes
|
|
72
|
+
* into custody beside the API key (its `_PATH` twin names a file, not a
|
|
73
|
+
* secret).
|
|
74
|
+
*/
|
|
75
|
+
export declare const TEMPORAL_TLS_CLIENT_KEY_DATA_ENV: string;
|
|
76
|
+
/**
|
|
77
|
+
* Every setting name this module reads, exactly. A process that hands its
|
|
78
|
+
* children their own rendered settings ({@link temporalConnectionEnv})
|
|
79
|
+
* removes these from what the child inherits; other `STIGMER_TEMPORAL_*`
|
|
80
|
+
* names (the CLI's own) are not connection settings.
|
|
81
|
+
*/
|
|
82
|
+
export declare const TEMPORAL_CONNECTION_ENV_NAMES: readonly string[];
|
|
83
|
+
/**
|
|
84
|
+
* Loads the connection settings.
|
|
85
|
+
*
|
|
86
|
+
* @throws when a setting is contradictory or unreadable: a client
|
|
87
|
+
* certificate without its key (or the reverse), both the `_PATH` and the
|
|
88
|
+
* `_DATA` form of one item, a path that cannot be read, or a
|
|
89
|
+
* `STIGMER_TEMPORAL_TLS` that is neither true nor false.
|
|
90
|
+
*/
|
|
91
|
+
export declare function loadTemporalConnectionConfig(read: SecretReader): TemporalConnectionConfig;
|
|
92
|
+
/**
|
|
93
|
+
* Renders loaded settings back as the environment a child runner reads,
|
|
94
|
+
* every PEM item in its `_DATA` form, so that no file has to exist where
|
|
95
|
+
* the child runs. The server's sandbox drivers pass this to the runners
|
|
96
|
+
* they start. Round-trips: loading the rendered environment yields the
|
|
97
|
+
* same settings.
|
|
98
|
+
*/
|
|
99
|
+
export declare function temporalConnectionEnv(config: TemporalConnectionConfig): Record<string, string>;
|
|
100
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/connection/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AAIH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAE5D;;;;GAIG;AACH,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IACrC,QAAQ,CAAC,uBAAuB,CAAC,EAAE,UAAU,CAAC;IAC9C,QAAQ,CAAC,cAAc,CAAC,EAAE;QACxB,QAAQ,CAAC,GAAG,EAAE,UAAU,CAAC;QACzB,QAAQ,CAAC,GAAG,EAAE,UAAU,CAAC;KAC1B,CAAC;CACH;AAED;;;;GAIG;AACH,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,GAAG,CAAC,EAAE,WAAW,CAAC;IAC3B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,eAAO,MAAM,oBAAoB,6BAA6B,CAAC;AAC/D,eAAO,MAAM,gBAAgB,yBAAyB,CAAC;AACvD,eAAO,MAAM,4BAA4B,qCAAqC,CAAC;AAS/E;;;;GAIG;AACH,eAAO,MAAM,gCAAgC,QAAgC,CAAC;AAE9E;;;;;GAKG;AACH,eAAO,MAAM,6BAA6B,EAAE,SAAS,MAAM,EAK1D,CAAC;AAEF;;;;;;;GAOG;AACH,wBAAgB,4BAA4B,CAC1C,IAAI,EAAE,YAAY,GACjB,wBAAwB,CA2C1B;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,wBAAwB,GAC/B,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAgBxB"}
|
|
@@ -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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"}
|
package/index.d.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @stigmer/temporal-codecs — Temporal
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* (
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
2
|
+
* @stigmer/temporal-codecs — the Temporal plumbing Stigmer's TypeScript
|
|
3
|
+
* Temporal processes (the server and the runner) share: payload codecs,
|
|
4
|
+
* and the connection-security settings every Temporal connection reads
|
|
5
|
+
* (`connection/config.ts`). The encryption envelope is a cross-language
|
|
6
|
+
* wire contract (the Java decode-only codec in stigmer-cloud's
|
|
7
|
+
* temporal-starter must match it byte-for-byte, pinned by the fixture in
|
|
8
|
+
* src/__tests__/fixtures/), and the connection settings must mean the same
|
|
9
|
+
* on every connection, which is why both live in one library instead of
|
|
10
|
+
* per-consumer copies that could fork.
|
|
9
11
|
*
|
|
10
12
|
* This is the package's ONLY public boundary. Codec order at the consumer
|
|
11
13
|
* is a correctness property: install [encryption, claimcheck] so encode
|
|
@@ -21,6 +23,8 @@
|
|
|
21
23
|
export { EncryptionPayloadCodec } from "./encryption/payload-codec.js";
|
|
22
24
|
export { loadPayloadEncryptionConfig } from "./encryption/config.js";
|
|
23
25
|
export type { BootstrapKeyMaterial, EncryptionKey, PayloadEncryptionConfig, PayloadKeyResolver, SecretReader, } from "./encryption/config.js";
|
|
26
|
+
export { loadTemporalConnectionConfig, temporalConnectionEnv, TEMPORAL_API_KEY_ENV, TEMPORAL_CONNECTION_ENV_NAMES, TEMPORAL_TLS_CLIENT_KEY_DATA_ENV, } from "./connection/config.js";
|
|
27
|
+
export type { TemporalConnectionConfig, TemporalTls, } from "./connection/config.js";
|
|
24
28
|
export { ClaimcheckPayloadCodec } from "./claimcheck/payload-codec.js";
|
|
25
29
|
export { loadClaimcheckConfig } from "./claimcheck/config.js";
|
|
26
30
|
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
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;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,EACL,4BAA4B,EAC5B,qBAAqB,EACrB,oBAAoB,EACpB,6BAA6B,EAC7B,gCAAgC,GACjC,MAAM,wBAAwB,CAAC;AAChC,YAAY,EACV,wBAAwB,EACxB,WAAW,GACZ,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
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @stigmer/temporal-codecs — Temporal
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* (
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
2
|
+
* @stigmer/temporal-codecs — the Temporal plumbing Stigmer's TypeScript
|
|
3
|
+
* Temporal processes (the server and the runner) share: payload codecs,
|
|
4
|
+
* and the connection-security settings every Temporal connection reads
|
|
5
|
+
* (`connection/config.ts`). The encryption envelope is a cross-language
|
|
6
|
+
* wire contract (the Java decode-only codec in stigmer-cloud's
|
|
7
|
+
* temporal-starter must match it byte-for-byte, pinned by the fixture in
|
|
8
|
+
* src/__tests__/fixtures/), and the connection settings must mean the same
|
|
9
|
+
* on every connection, which is why both live in one library instead of
|
|
10
|
+
* per-consumer copies that could fork.
|
|
9
11
|
*
|
|
10
12
|
* This is the package's ONLY public boundary. Codec order at the consumer
|
|
11
13
|
* is a correctness property: install [encryption, claimcheck] so encode
|
|
@@ -20,6 +22,7 @@
|
|
|
20
22
|
*/
|
|
21
23
|
export { EncryptionPayloadCodec } from "./encryption/payload-codec.js";
|
|
22
24
|
export { loadPayloadEncryptionConfig } from "./encryption/config.js";
|
|
25
|
+
export { loadTemporalConnectionConfig, temporalConnectionEnv, TEMPORAL_API_KEY_ENV, TEMPORAL_CONNECTION_ENV_NAMES, TEMPORAL_TLS_CLIENT_KEY_DATA_ENV, } from "./connection/config.js";
|
|
23
26
|
export { ClaimcheckPayloadCodec } from "./claimcheck/payload-codec.js";
|
|
24
27
|
export { loadClaimcheckConfig } from "./claimcheck/config.js";
|
|
25
28
|
//# sourceMappingURL=index.js.map
|
package/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,EAAE,sBAAsB,EAAE,MAAM,+BAA+B,CAAC;AACvE,OAAO,EAAE,2BAA2B,EAAE,MAAM,wBAAwB,CAAC;AASrE,OAAO,EACL,4BAA4B,EAC5B,qBAAqB,EACrB,oBAAoB,EACpB,6BAA6B,EAC7B,gCAAgC,GACjC,MAAM,wBAAwB,CAAC;AAMhC,OAAO,EAAE,sBAAsB,EAAE,MAAM,+BAA+B,CAAC;AACvE,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stigmer/temporal-codecs",
|
|
3
|
-
"version": "3.
|
|
4
|
-
"description": "Temporal
|
|
3
|
+
"version": "3.31.0",
|
|
4
|
+
"description": "The Temporal plumbing Stigmer's TypeScript Temporal processes share — AES-256-GCM payload encryption and claim-check offloading (the one home for the cross-language envelope contract), and the connection-security settings every Temporal connection reads",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"sideEffects": false,
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
"temporal",
|
|
19
19
|
"payload-codec",
|
|
20
20
|
"encryption",
|
|
21
|
-
"claim-check"
|
|
21
|
+
"claim-check",
|
|
22
|
+
"tls"
|
|
22
23
|
],
|
|
23
24
|
"main": "./index.js",
|
|
24
25
|
"types": "./index.d.ts",
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Temporal connection settings (`connection/config.ts`): what each
|
|
3
|
+
* setting turns on, the misconfigurations that stop the boot, the rule that
|
|
4
|
+
* an empty value is unset, the refusal to read Temporal's own names (they
|
|
5
|
+
* are the user's), and the round trip through the environment a child
|
|
6
|
+
* runner receives.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, it, expect, afterAll } from "vitest";
|
|
10
|
+
import { mkdtempSync, writeFileSync, rmSync } from "node:fs";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { tmpdir } from "node:os";
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
loadTemporalConnectionConfig,
|
|
16
|
+
temporalConnectionEnv,
|
|
17
|
+
TEMPORAL_CONNECTION_ENV_NAMES,
|
|
18
|
+
} from "../connection/config.js";
|
|
19
|
+
|
|
20
|
+
const CA = "-----BEGIN CERTIFICATE-----\nca\n-----END CERTIFICATE-----\n";
|
|
21
|
+
const CRT = "-----BEGIN CERTIFICATE-----\nclient\n-----END CERTIFICATE-----\n";
|
|
22
|
+
const KEY = "-----BEGIN PRIVATE KEY-----\nkey\n-----END PRIVATE KEY-----\n";
|
|
23
|
+
|
|
24
|
+
const dir = mkdtempSync(join(tmpdir(), "temporal-connection-config-"));
|
|
25
|
+
afterAll(() => rmSync(dir, { recursive: true, force: true }));
|
|
26
|
+
|
|
27
|
+
function load(env: Record<string, string>) {
|
|
28
|
+
return loadTemporalConnectionConfig((name) => env[name]);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function text(bytes: Uint8Array | undefined): string | undefined {
|
|
32
|
+
return bytes === undefined ? undefined : Buffer.from(bytes).toString("utf8");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe("loadTemporalConnectionConfig", () => {
|
|
36
|
+
it("is a plaintext connection when nothing is set", () => {
|
|
37
|
+
expect(load({})).toEqual({});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("treats every empty or blank value as unset", () => {
|
|
41
|
+
expect(
|
|
42
|
+
load({
|
|
43
|
+
STIGMER_TEMPORAL_API_KEY: "",
|
|
44
|
+
STIGMER_TEMPORAL_TLS: " ",
|
|
45
|
+
STIGMER_TEMPORAL_TLS_SERVER_NAME: "",
|
|
46
|
+
STIGMER_TEMPORAL_TLS_SERVER_CA_CERT_DATA: "",
|
|
47
|
+
STIGMER_TEMPORAL_TLS_CLIENT_CERT_PATH: "",
|
|
48
|
+
}),
|
|
49
|
+
).toEqual({});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("never reads Temporal's own TEMPORAL_* names, which are the user's", () => {
|
|
53
|
+
expect(
|
|
54
|
+
load({
|
|
55
|
+
TEMPORAL_API_KEY: "the-users-own-key",
|
|
56
|
+
TEMPORAL_TLS: "true",
|
|
57
|
+
TEMPORAL_TLS_SERVER_CA_CERT_DATA: CA,
|
|
58
|
+
}),
|
|
59
|
+
).toEqual({});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("turns TLS on with the system's roots for STIGMER_TEMPORAL_TLS=true or 1", () => {
|
|
63
|
+
expect(load({ STIGMER_TEMPORAL_TLS: "true" })).toEqual({ tls: {} });
|
|
64
|
+
expect(load({ STIGMER_TEMPORAL_TLS: "1" })).toEqual({ tls: {} });
|
|
65
|
+
expect(load({ STIGMER_TEMPORAL_TLS: "false" })).toEqual({});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("an API key implies TLS", () => {
|
|
69
|
+
expect(load({ STIGMER_TEMPORAL_API_KEY: " k-1 " })).toEqual({ tls: {}, apiKey: "k-1" });
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("a CA or a server name implies TLS and is carried in the SDK's shape", () => {
|
|
73
|
+
const config = load({
|
|
74
|
+
STIGMER_TEMPORAL_TLS_SERVER_CA_CERT_DATA: CA,
|
|
75
|
+
STIGMER_TEMPORAL_TLS_SERVER_NAME: "temporal.internal",
|
|
76
|
+
});
|
|
77
|
+
expect(config.tls?.serverNameOverride).toBe("temporal.internal");
|
|
78
|
+
expect(text(config.tls?.serverRootCACertificate)).toBe(CA);
|
|
79
|
+
expect(config.apiKey).toBeUndefined();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("reads a client pair for mutual TLS, from data or from files", () => {
|
|
83
|
+
const fromData = load({
|
|
84
|
+
STIGMER_TEMPORAL_TLS_CLIENT_CERT_DATA: CRT,
|
|
85
|
+
STIGMER_TEMPORAL_TLS_CLIENT_KEY_DATA: KEY,
|
|
86
|
+
});
|
|
87
|
+
expect(text(fromData.tls?.clientCertPair?.crt)).toBe(CRT);
|
|
88
|
+
expect(text(fromData.tls?.clientCertPair?.key)).toBe(KEY);
|
|
89
|
+
|
|
90
|
+
writeFileSync(join(dir, "client.crt"), CRT);
|
|
91
|
+
writeFileSync(join(dir, "client.key"), KEY);
|
|
92
|
+
const fromFiles = load({
|
|
93
|
+
STIGMER_TEMPORAL_TLS_CLIENT_CERT_PATH: join(dir, "client.crt"),
|
|
94
|
+
STIGMER_TEMPORAL_TLS_CLIENT_KEY_PATH: join(dir, "client.key"),
|
|
95
|
+
});
|
|
96
|
+
expect(text(fromFiles.tls?.clientCertPair?.crt)).toBe(CRT);
|
|
97
|
+
expect(text(fromFiles.tls?.clientCertPair?.key)).toBe(KEY);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it.each([
|
|
101
|
+
[{ STIGMER_TEMPORAL_TLS_CLIENT_CERT_DATA: CRT }, /mutual TLS needs both/],
|
|
102
|
+
[{ STIGMER_TEMPORAL_TLS_CLIENT_KEY_DATA: KEY }, /mutual TLS needs both/],
|
|
103
|
+
[
|
|
104
|
+
{
|
|
105
|
+
STIGMER_TEMPORAL_TLS_SERVER_CA_CERT_DATA: CA,
|
|
106
|
+
STIGMER_TEMPORAL_TLS_SERVER_CA_CERT_PATH: "/etc/ca.pem",
|
|
107
|
+
},
|
|
108
|
+
/set STIGMER_TEMPORAL_TLS_SERVER_CA_CERT_PATH or STIGMER_TEMPORAL_TLS_SERVER_CA_CERT_DATA, not both/,
|
|
109
|
+
],
|
|
110
|
+
[
|
|
111
|
+
{ STIGMER_TEMPORAL_TLS_SERVER_CA_CERT_PATH: "/nonexistent/ca.pem" },
|
|
112
|
+
/STIGMER_TEMPORAL_TLS_SERVER_CA_CERT_PATH "\/nonexistent\/ca.pem" cannot be read/,
|
|
113
|
+
],
|
|
114
|
+
[{ STIGMER_TEMPORAL_TLS: "yes" }, /must be true, 1, false or 0/],
|
|
115
|
+
[
|
|
116
|
+
{ STIGMER_TEMPORAL_TLS: "false", STIGMER_TEMPORAL_API_KEY: "k" },
|
|
117
|
+
/STIGMER_TEMPORAL_TLS is false while/,
|
|
118
|
+
],
|
|
119
|
+
])("stops the boot on a contradictory or unreadable setting (%o)", (env, message) => {
|
|
120
|
+
expect(() => load(env)).toThrow(message);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("reads every value through the injected reader, and only it", () => {
|
|
124
|
+
const asked: string[] = [];
|
|
125
|
+
loadTemporalConnectionConfig((name) => {
|
|
126
|
+
asked.push(name);
|
|
127
|
+
return undefined;
|
|
128
|
+
});
|
|
129
|
+
// The exported list is exactly what the reader reads, so a process
|
|
130
|
+
// that strips inherited settings strips all of them and nothing else.
|
|
131
|
+
expect([...new Set(asked)].sort()).toEqual([...TEMPORAL_CONNECTION_ENV_NAMES].sort());
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe("temporalConnectionEnv", () => {
|
|
136
|
+
it("renders nothing for a plaintext connection", () => {
|
|
137
|
+
expect(temporalConnectionEnv({})).toEqual({});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("round-trips every setting through the _DATA forms a child runner reads", () => {
|
|
141
|
+
writeFileSync(join(dir, "ca.pem"), CA);
|
|
142
|
+
const loaded = load({
|
|
143
|
+
STIGMER_TEMPORAL_API_KEY: "k-2",
|
|
144
|
+
STIGMER_TEMPORAL_TLS_SERVER_NAME: "temporal.internal",
|
|
145
|
+
STIGMER_TEMPORAL_TLS_SERVER_CA_CERT_PATH: join(dir, "ca.pem"),
|
|
146
|
+
STIGMER_TEMPORAL_TLS_CLIENT_CERT_DATA: CRT,
|
|
147
|
+
STIGMER_TEMPORAL_TLS_CLIENT_KEY_DATA: KEY,
|
|
148
|
+
});
|
|
149
|
+
const env = temporalConnectionEnv(loaded);
|
|
150
|
+
|
|
151
|
+
expect(Object.keys(env).some((name) => name.endsWith("_PATH"))).toBe(false);
|
|
152
|
+
expect(env["STIGMER_TEMPORAL_TLS_SERVER_CA_CERT_DATA"]).toBe(CA);
|
|
153
|
+
expect(load(env)).toEqual(loaded);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
@@ -0,0 +1,217 @@
|
|
|
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
|
+
|
|
45
|
+
import { readFileSync } from "node:fs";
|
|
46
|
+
|
|
47
|
+
import type { SecretReader } from "../encryption/config.js";
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* TLS settings in the Temporal SDK's own shape (`TLSConfig` in
|
|
51
|
+
* `@temporalio/common`), declared structurally so this library imports
|
|
52
|
+
* nothing internal. An empty object means TLS with the system's roots.
|
|
53
|
+
*/
|
|
54
|
+
export interface TemporalTls {
|
|
55
|
+
readonly serverNameOverride?: string;
|
|
56
|
+
readonly serverRootCACertificate?: Uint8Array;
|
|
57
|
+
readonly clientCertPair?: {
|
|
58
|
+
readonly crt: Uint8Array;
|
|
59
|
+
readonly key: Uint8Array;
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* What a connection needs beyond its address. Both fields are absent for a
|
|
65
|
+
* plaintext connection; spread it into `Connection.connect` or
|
|
66
|
+
* `NativeConnection.connect` as is.
|
|
67
|
+
*/
|
|
68
|
+
export interface TemporalConnectionConfig {
|
|
69
|
+
readonly tls?: TemporalTls;
|
|
70
|
+
readonly apiKey?: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export const TEMPORAL_API_KEY_ENV = "STIGMER_TEMPORAL_API_KEY";
|
|
74
|
+
export const TEMPORAL_TLS_ENV = "STIGMER_TEMPORAL_TLS";
|
|
75
|
+
export const TEMPORAL_TLS_SERVER_NAME_ENV = "STIGMER_TEMPORAL_TLS_SERVER_NAME";
|
|
76
|
+
|
|
77
|
+
/** The PEM items, each settable as a file path or as its text. */
|
|
78
|
+
const PEM_ITEMS = {
|
|
79
|
+
serverCa: "STIGMER_TEMPORAL_TLS_SERVER_CA_CERT",
|
|
80
|
+
clientCert: "STIGMER_TEMPORAL_TLS_CLIENT_CERT",
|
|
81
|
+
clientKey: "STIGMER_TEMPORAL_TLS_CLIENT_KEY",
|
|
82
|
+
} as const;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The `_DATA` name of the client key: a credential, which the runner takes
|
|
86
|
+
* into custody beside the API key (its `_PATH` twin names a file, not a
|
|
87
|
+
* secret).
|
|
88
|
+
*/
|
|
89
|
+
export const TEMPORAL_TLS_CLIENT_KEY_DATA_ENV = `${PEM_ITEMS.clientKey}_DATA`;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Every setting name this module reads, exactly. A process that hands its
|
|
93
|
+
* children their own rendered settings ({@link temporalConnectionEnv})
|
|
94
|
+
* removes these from what the child inherits; other `STIGMER_TEMPORAL_*`
|
|
95
|
+
* names (the CLI's own) are not connection settings.
|
|
96
|
+
*/
|
|
97
|
+
export const TEMPORAL_CONNECTION_ENV_NAMES: readonly string[] = [
|
|
98
|
+
TEMPORAL_API_KEY_ENV,
|
|
99
|
+
TEMPORAL_TLS_ENV,
|
|
100
|
+
TEMPORAL_TLS_SERVER_NAME_ENV,
|
|
101
|
+
...Object.values(PEM_ITEMS).flatMap((item) => [`${item}_PATH`, `${item}_DATA`]),
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Loads the connection settings.
|
|
106
|
+
*
|
|
107
|
+
* @throws when a setting is contradictory or unreadable: a client
|
|
108
|
+
* certificate without its key (or the reverse), both the `_PATH` and the
|
|
109
|
+
* `_DATA` form of one item, a path that cannot be read, or a
|
|
110
|
+
* `STIGMER_TEMPORAL_TLS` that is neither true nor false.
|
|
111
|
+
*/
|
|
112
|
+
export function loadTemporalConnectionConfig(
|
|
113
|
+
read: SecretReader,
|
|
114
|
+
): TemporalConnectionConfig {
|
|
115
|
+
const value = (name: string): string | undefined => {
|
|
116
|
+
const raw = read(name);
|
|
117
|
+
return raw === undefined || raw.trim() === "" ? undefined : raw;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const apiKey = value(TEMPORAL_API_KEY_ENV)?.trim();
|
|
121
|
+
const tlsFlag = parseFlag(value(TEMPORAL_TLS_ENV));
|
|
122
|
+
const serverName = value(TEMPORAL_TLS_SERVER_NAME_ENV)?.trim();
|
|
123
|
+
const serverCa = readPem(PEM_ITEMS.serverCa, value);
|
|
124
|
+
const clientCert = readPem(PEM_ITEMS.clientCert, value);
|
|
125
|
+
const clientKey = readPem(PEM_ITEMS.clientKey, value);
|
|
126
|
+
|
|
127
|
+
if ((clientCert === undefined) !== (clientKey === undefined)) {
|
|
128
|
+
throw new Error(
|
|
129
|
+
`Temporal connection misconfigured: mutual TLS needs both ` +
|
|
130
|
+
`${PEM_ITEMS.clientCert}_* and ${PEM_ITEMS.clientKey}_* — set both or neither`,
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const tlsRequested =
|
|
135
|
+
tlsFlag === true ||
|
|
136
|
+
apiKey !== undefined ||
|
|
137
|
+
serverName !== undefined ||
|
|
138
|
+
serverCa !== undefined ||
|
|
139
|
+
clientCert !== undefined;
|
|
140
|
+
if (tlsFlag === false && tlsRequested) {
|
|
141
|
+
throw new Error(
|
|
142
|
+
`Temporal connection misconfigured: ${TEMPORAL_TLS_ENV} is false while ` +
|
|
143
|
+
`an API key, a server name, a CA or a client certificate is set — ` +
|
|
144
|
+
`each of those needs TLS`,
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
if (!tlsRequested) return {};
|
|
148
|
+
|
|
149
|
+
const tls: TemporalTls = {
|
|
150
|
+
...(serverName !== undefined ? { serverNameOverride: serverName } : {}),
|
|
151
|
+
...(serverCa !== undefined ? { serverRootCACertificate: serverCa } : {}),
|
|
152
|
+
...(clientCert !== undefined && clientKey !== undefined
|
|
153
|
+
? { clientCertPair: { crt: clientCert, key: clientKey } }
|
|
154
|
+
: {}),
|
|
155
|
+
};
|
|
156
|
+
return { tls, ...(apiKey !== undefined ? { apiKey } : {}) };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Renders loaded settings back as the environment a child runner reads,
|
|
161
|
+
* every PEM item in its `_DATA` form, so that no file has to exist where
|
|
162
|
+
* the child runs. The server's sandbox drivers pass this to the runners
|
|
163
|
+
* they start. Round-trips: loading the rendered environment yields the
|
|
164
|
+
* same settings.
|
|
165
|
+
*/
|
|
166
|
+
export function temporalConnectionEnv(
|
|
167
|
+
config: TemporalConnectionConfig,
|
|
168
|
+
): Record<string, string> {
|
|
169
|
+
const env: Record<string, string> = {};
|
|
170
|
+
if (config.tls === undefined) return env;
|
|
171
|
+
env[TEMPORAL_TLS_ENV] = "true";
|
|
172
|
+
const text = (bytes: Uint8Array): string => Buffer.from(bytes).toString("utf8");
|
|
173
|
+
const { serverNameOverride, serverRootCACertificate, clientCertPair } = config.tls;
|
|
174
|
+
if (serverNameOverride !== undefined) env[TEMPORAL_TLS_SERVER_NAME_ENV] = serverNameOverride;
|
|
175
|
+
if (serverRootCACertificate !== undefined) {
|
|
176
|
+
env[`${PEM_ITEMS.serverCa}_DATA`] = text(serverRootCACertificate);
|
|
177
|
+
}
|
|
178
|
+
if (clientCertPair !== undefined) {
|
|
179
|
+
env[`${PEM_ITEMS.clientCert}_DATA`] = text(clientCertPair.crt);
|
|
180
|
+
env[TEMPORAL_TLS_CLIENT_KEY_DATA_ENV] = text(clientCertPair.key);
|
|
181
|
+
}
|
|
182
|
+
if (config.apiKey !== undefined) env[TEMPORAL_API_KEY_ENV] = config.apiKey;
|
|
183
|
+
return env;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function parseFlag(raw: string | undefined): boolean | undefined {
|
|
187
|
+
if (raw === undefined) return undefined;
|
|
188
|
+
const flag = raw.trim().toLowerCase();
|
|
189
|
+
if (flag === "true" || flag === "1") return true;
|
|
190
|
+
if (flag === "false" || flag === "0") return false;
|
|
191
|
+
throw new Error(
|
|
192
|
+
`Temporal connection misconfigured: ${TEMPORAL_TLS_ENV} must be true, 1, false or 0, got "${raw}"`,
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function readPem(
|
|
197
|
+
item: string,
|
|
198
|
+
value: (name: string) => string | undefined,
|
|
199
|
+
): Uint8Array | undefined {
|
|
200
|
+
const path = value(`${item}_PATH`);
|
|
201
|
+
const data = value(`${item}_DATA`);
|
|
202
|
+
if (path !== undefined && data !== undefined) {
|
|
203
|
+
throw new Error(
|
|
204
|
+
`Temporal connection misconfigured: set ${item}_PATH or ${item}_DATA, not both`,
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
if (data !== undefined) return Buffer.from(data, "utf8");
|
|
208
|
+
if (path === undefined) return undefined;
|
|
209
|
+
try {
|
|
210
|
+
return readFileSync(path.trim());
|
|
211
|
+
} catch (err) {
|
|
212
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
213
|
+
throw new Error(
|
|
214
|
+
`Temporal connection misconfigured: ${item}_PATH "${path.trim()}" cannot be read (${reason})`,
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @stigmer/temporal-codecs — Temporal
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* (
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
2
|
+
* @stigmer/temporal-codecs — the Temporal plumbing Stigmer's TypeScript
|
|
3
|
+
* Temporal processes (the server and the runner) share: payload codecs,
|
|
4
|
+
* and the connection-security settings every Temporal connection reads
|
|
5
|
+
* (`connection/config.ts`). The encryption envelope is a cross-language
|
|
6
|
+
* wire contract (the Java decode-only codec in stigmer-cloud's
|
|
7
|
+
* temporal-starter must match it byte-for-byte, pinned by the fixture in
|
|
8
|
+
* src/__tests__/fixtures/), and the connection settings must mean the same
|
|
9
|
+
* on every connection, which is why both live in one library instead of
|
|
10
|
+
* per-consumer copies that could fork.
|
|
9
11
|
*
|
|
10
12
|
* This is the package's ONLY public boundary. Codec order at the consumer
|
|
11
13
|
* is a correctness property: install [encryption, claimcheck] so encode
|
|
@@ -29,6 +31,18 @@ export type {
|
|
|
29
31
|
SecretReader,
|
|
30
32
|
} from "./encryption/config.js";
|
|
31
33
|
|
|
34
|
+
export {
|
|
35
|
+
loadTemporalConnectionConfig,
|
|
36
|
+
temporalConnectionEnv,
|
|
37
|
+
TEMPORAL_API_KEY_ENV,
|
|
38
|
+
TEMPORAL_CONNECTION_ENV_NAMES,
|
|
39
|
+
TEMPORAL_TLS_CLIENT_KEY_DATA_ENV,
|
|
40
|
+
} from "./connection/config.js";
|
|
41
|
+
export type {
|
|
42
|
+
TemporalConnectionConfig,
|
|
43
|
+
TemporalTls,
|
|
44
|
+
} from "./connection/config.js";
|
|
45
|
+
|
|
32
46
|
export { ClaimcheckPayloadCodec } from "./claimcheck/payload-codec.js";
|
|
33
47
|
export { loadClaimcheckConfig } from "./claimcheck/config.js";
|
|
34
48
|
export type { ClaimcheckConfig } from "./claimcheck/config.js";
|