@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.
Files changed (43) hide show
  1. package/LICENSE +194 -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/connection/config.d.ts +100 -0
  20. package/connection/config.d.ts.map +1 -0
  21. package/connection/config.js +171 -0
  22. package/connection/config.js.map +1 -0
  23. package/encryption/config.d.ts +100 -0
  24. package/encryption/config.d.ts.map +1 -0
  25. package/encryption/config.js +119 -0
  26. package/encryption/config.js.map +1 -0
  27. package/encryption/payload-codec.d.ts +60 -0
  28. package/encryption/payload-codec.d.ts.map +1 -0
  29. package/encryption/payload-codec.js +163 -0
  30. package/encryption/payload-codec.js.map +1 -0
  31. package/index.d.ts +32 -0
  32. package/index.d.ts.map +1 -0
  33. package/index.js +28 -0
  34. package/index.js.map +1 -0
  35. package/package.json +42 -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/connection/config.ts +217 -0
  41. package/src/encryption/config.ts +204 -0
  42. package/src/encryption/payload-codec.ts +186 -0
  43. package/src/index.ts +49 -0
package/index.d.ts ADDED
@@ -0,0 +1,32 @@
1
+ /**
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.
11
+ *
12
+ * This is the package's ONLY public boundary. Codec order at the consumer
13
+ * is a correctness property: install [encryption, claimcheck] so encode
14
+ * encrypts before relocating (object storage only ever sees ciphertext)
15
+ * and decode restores the blob before decrypting.
16
+ *
17
+ * Dependency policy: @temporalio/common and @temporalio/proto are pinned
18
+ * exact and identical to the runner's pins, bumped in lockstep with it —
19
+ * the runner's consumer-install gate (stigmer#786) fails any version mix
20
+ * because a split @temporalio/proto tree registers the core-sdk protobuf
21
+ * namespace twice and crashes worker init.
22
+ */
23
+ export { EncryptionPayloadCodec } from "./encryption/payload-codec.js";
24
+ export { loadPayloadEncryptionConfig } from "./encryption/config.js";
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";
28
+ export { ClaimcheckPayloadCodec } from "./claimcheck/payload-codec.js";
29
+ export { loadClaimcheckConfig } from "./claimcheck/config.js";
30
+ export type { ClaimcheckConfig } from "./claimcheck/config.js";
31
+ export type { ClaimcheckStorage } from "./claimcheck/storage.js";
32
+ //# 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;;;;;;;;;;;;;;;;;;;;;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 ADDED
@@ -0,0 +1,28 @@
1
+ /**
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.
11
+ *
12
+ * This is the package's ONLY public boundary. Codec order at the consumer
13
+ * is a correctness property: install [encryption, claimcheck] so encode
14
+ * encrypts before relocating (object storage only ever sees ciphertext)
15
+ * and decode restores the blob before decrypting.
16
+ *
17
+ * Dependency policy: @temporalio/common and @temporalio/proto are pinned
18
+ * exact and identical to the runner's pins, bumped in lockstep with it —
19
+ * the runner's consumer-install gate (stigmer#786) fails any version mix
20
+ * because a split @temporalio/proto tree registers the core-sdk protobuf
21
+ * namespace twice and crashes worker init.
22
+ */
23
+ export { EncryptionPayloadCodec } from "./encryption/payload-codec.js";
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";
26
+ export { ClaimcheckPayloadCodec } from "./claimcheck/payload-codec.js";
27
+ export { loadClaimcheckConfig } from "./claimcheck/config.js";
28
+ //# 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;;;;;;;;;;;;;;;;;;;;;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 ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@stigmer/temporal-codecs",
3
+ "version": "0.1.0-dev.20261017020314.g644422724c27",
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
+ "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
+ "tls"
23
+ ],
24
+ "main": "./index.js",
25
+ "types": "./index.d.ts",
26
+ "exports": {
27
+ ".": {
28
+ "types": "./index.d.ts",
29
+ "import": "./index.js",
30
+ "default": "./index.js"
31
+ },
32
+ "./connection": {
33
+ "types": "./connection/config.d.ts",
34
+ "import": "./connection/config.js",
35
+ "default": "./connection/config.js"
36
+ }
37
+ },
38
+ "dependencies": {
39
+ "@temporalio/common": "1.16.2",
40
+ "@temporalio/proto": "1.16.2"
41
+ }
42
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Gzip helpers for claim-checked blobs. Synchronous by design: the codec
3
+ * runs on the Temporal worker's payload path where the blobs are bounded
4
+ * by the claim-check threshold, and gzip's format is part of the marker
5
+ * contract (`compressed: true` blobs must gunzip on any consumer).
6
+ *
7
+ * Moved from backend/services/runner/src/claimcheck/compressor.ts when the
8
+ * codecs became @stigmer/temporal-codecs.
9
+ */
10
+
11
+ import { gzipSync, gunzipSync } from "node:zlib";
12
+
13
+ export function compress(data: Buffer): Buffer<ArrayBuffer> {
14
+ return gzipSync(data) as Buffer<ArrayBuffer>;
15
+ }
16
+
17
+ export function decompress(data: Buffer): Buffer<ArrayBuffer> {
18
+ return gunzipSync(data) as Buffer<ArrayBuffer>;
19
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Claim-check configuration, env-driven with an enabled-iff-configured
3
+ * gate (CLAIMCHECK_ENABLED) — unlike encryption, the offload threshold and
4
+ * key prefix are operational tuning, not secrets, so plain env reads are
5
+ * the whole policy.
6
+ *
7
+ * Moved from backend/services/runner/src/claimcheck/config.ts when the
8
+ * codecs became @stigmer/temporal-codecs.
9
+ */
10
+
11
+ export interface ClaimcheckConfig {
12
+ readonly enabled: boolean;
13
+ readonly thresholdBytes: number;
14
+ readonly compressionEnabled: boolean;
15
+ readonly keyPrefix: string;
16
+ }
17
+
18
+ const DEFAULT_THRESHOLD_BYTES = 128 * 1024; // 128KB
19
+
20
+ export function loadClaimcheckConfig(): ClaimcheckConfig {
21
+ return {
22
+ enabled: process.env.CLAIMCHECK_ENABLED === "true",
23
+ thresholdBytes: parseInt(
24
+ process.env.CLAIMCHECK_THRESHOLD_BYTES ?? String(DEFAULT_THRESHOLD_BYTES),
25
+ 10,
26
+ ),
27
+ compressionEnabled: process.env.CLAIMCHECK_COMPRESSION_ENABLED !== "false",
28
+ keyPrefix: process.env.CLAIMCHECK_KEY_PREFIX ?? "claimcheck/",
29
+ };
30
+ }
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Temporal PayloadCodec that transparently offloads large payloads to
3
+ * external storage ({@link ClaimcheckStorage}). Payloads below the
4
+ * threshold pass through unchanged. Payloads at or above the threshold
5
+ * are compressed (optional), uploaded, and replaced with a small
6
+ * reference marker.
7
+ *
8
+ * On decode, markers are detected, the original payload is downloaded
9
+ * and decompressed, and the original bytes are restored — transparent
10
+ * to workflow/activity code.
11
+ *
12
+ * Moved from backend/services/runner/src/claimcheck/payload-codec.ts when
13
+ * the codecs became @stigmer/temporal-codecs (one home for the
14
+ * cross-language envelope contract; the TS server is the second consumer).
15
+ */
16
+
17
+ import { randomUUID } from "node:crypto";
18
+ import type { Payload, PayloadCodec } from "@temporalio/common";
19
+ import type { ClaimcheckStorage } from "./storage.js";
20
+ import type { ClaimcheckConfig } from "./config.js";
21
+ import { compress, decompress } from "./compressor.js";
22
+
23
+ const MARKER_METADATA_KEY = "encoding";
24
+ const MARKER_ENCODING_VALUE = "binary/claimcheck";
25
+
26
+ interface ClaimcheckMarker {
27
+ key: string;
28
+ size: number;
29
+ compressed: boolean;
30
+ /**
31
+ * The relocated payload's original metadata, base64-encoded per value.
32
+ * Without it the restored payload would carry the marker's own
33
+ * "binary/claimcheck" encoding and no payload converter could interpret
34
+ * it. Absent on markers written before this field existed; those decode
35
+ * with the legacy (metadata-less) behavior.
36
+ */
37
+ metadata?: Record<string, string>;
38
+ }
39
+
40
+ export class ClaimcheckPayloadCodec implements PayloadCodec {
41
+ constructor(
42
+ private readonly storage: ClaimcheckStorage,
43
+ private readonly config: ClaimcheckConfig,
44
+ ) {}
45
+
46
+ async encode(payloads: Payload[]): Promise<Payload[]> {
47
+ return Promise.all(payloads.map((p) => this.encodePayload(p)));
48
+ }
49
+
50
+ async decode(payloads: Payload[]): Promise<Payload[]> {
51
+ return Promise.all(payloads.map((p) => this.decodePayload(p)));
52
+ }
53
+
54
+ private async encodePayload(payload: Payload): Promise<Payload> {
55
+ const data = payload.data;
56
+ if (!data || data.length < this.config.thresholdBytes) {
57
+ return payload;
58
+ }
59
+
60
+ const originalBuf = Buffer.from(data);
61
+ let uploadBuf = originalBuf;
62
+ let compressed = false;
63
+
64
+ if (this.config.compressionEnabled) {
65
+ const compressedBuf = compress(originalBuf);
66
+ if (compressedBuf.length < originalBuf.length) {
67
+ uploadBuf = compressedBuf;
68
+ compressed = true;
69
+ }
70
+ }
71
+
72
+ const key = `${this.config.keyPrefix}${randomUUID()}`;
73
+ await this.storage.upload(key, uploadBuf, "application/octet-stream");
74
+
75
+ const marker: ClaimcheckMarker = {
76
+ key,
77
+ size: data.length,
78
+ compressed,
79
+ metadata: serializeMetadata(payload.metadata),
80
+ };
81
+
82
+ return {
83
+ metadata: {
84
+ [MARKER_METADATA_KEY]: Buffer.from(MARKER_ENCODING_VALUE),
85
+ },
86
+ data: Buffer.from(JSON.stringify(marker)),
87
+ };
88
+ }
89
+
90
+ private async decodePayload(payload: Payload): Promise<Payload> {
91
+ if (!this.isClaimcheckPayload(payload)) {
92
+ return payload;
93
+ }
94
+
95
+ const marker: ClaimcheckMarker = JSON.parse(
96
+ Buffer.from(payload.data!).toString("utf-8"),
97
+ );
98
+
99
+ let rawBuf: Buffer;
100
+ try {
101
+ rawBuf = await this.storage.download(marker.key);
102
+ } catch (err) {
103
+ // Preserve the claimcheck-scoped error contract; the shared download error
104
+ // already carries the HTTP status (proxy) or the miss (local) as the cause.
105
+ const cause = err instanceof Error ? err.message : String(err);
106
+ throw new Error(`Claimcheck retrieve failed for key ${marker.key}: ${cause}`);
107
+ }
108
+ const dataBuf = marker.compressed ? decompress(rawBuf) : rawBuf;
109
+
110
+ return {
111
+ metadata: marker.metadata
112
+ ? deserializeMetadata(marker.metadata)
113
+ : payload.metadata,
114
+ data: dataBuf,
115
+ };
116
+ }
117
+
118
+ private isClaimcheckPayload(payload: Payload): boolean {
119
+ const encoding = payload.metadata?.[MARKER_METADATA_KEY];
120
+ if (!encoding) return false;
121
+ return Buffer.from(encoding).toString("utf-8") === MARKER_ENCODING_VALUE;
122
+ }
123
+ }
124
+
125
+ function serializeMetadata(
126
+ metadata: Payload["metadata"],
127
+ ): Record<string, string> | undefined {
128
+ if (!metadata) return undefined;
129
+ const out: Record<string, string> = {};
130
+ for (const [key, value] of Object.entries(metadata)) {
131
+ if (value) out[key] = Buffer.from(value).toString("base64");
132
+ }
133
+ return out;
134
+ }
135
+
136
+ function deserializeMetadata(
137
+ metadata: Record<string, string>,
138
+ ): Record<string, Uint8Array> {
139
+ const out: Record<string, Uint8Array> = {};
140
+ for (const [key, value] of Object.entries(metadata)) {
141
+ out[key] = Buffer.from(value, "base64");
142
+ }
143
+ return out;
144
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The blob-store contract the claim-check codec needs — nothing more.
3
+ *
4
+ * Deliberately minimal (upload + download by key) so any consumer's
5
+ * storage client satisfies it structurally: the runner's ArtifactStorage
6
+ * (a richer interface with presigned-URL minting) matches as-is, and the
7
+ * TS server brings its own. Widening this port widens the wire contract's
8
+ * dependency surface for every consumer — do not add methods the codec
9
+ * itself does not call.
10
+ */
11
+ export interface ClaimcheckStorage {
12
+ /** Store `content` under `key`; resolves once the blob is durable. */
13
+ upload(key: string, content: Buffer, contentType?: string): Promise<string>;
14
+ /**
15
+ * Return the exact bytes stored under `key`, or throw a descriptive,
16
+ * key-scoped `Error` when the object is missing or the transport fails.
17
+ */
18
+ download(key: string): Promise<Buffer>;
19
+ }
@@ -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
+ }