@mesh-tech/mesh-cli 0.16.0 → 0.17.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/dist/bin/mesh.js +1381 -897
- package/dist/bin/mesh.js.map +4 -4
- package/dist/build-info.json +2 -2
- package/dist/src/commands/agent-api-client.d.ts +17 -0
- package/dist/src/commands/agent-api-client.d.ts.map +1 -1
- package/dist/src/commands/agent-api-client.js +22 -0
- package/dist/src/commands/agent-api-client.js.map +1 -1
- package/dist/src/commands/dev-doctor.d.ts.map +1 -1
- package/dist/src/commands/dev-doctor.js +7 -2
- package/dist/src/commands/dev-doctor.js.map +1 -1
- package/dist/src/commands/dev.d.ts +22 -0
- package/dist/src/commands/dev.d.ts.map +1 -1
- package/dist/src/commands/dev.js +124 -9
- package/dist/src/commands/dev.js.map +1 -1
- package/dist/src/commands/local/docker-runner.d.ts.map +1 -1
- package/dist/src/commands/local/docker-runner.js +2 -4
- package/dist/src/commands/local/docker-runner.js.map +1 -1
- package/dist/src/commands/local/hub-local.d.ts.map +1 -1
- package/dist/src/commands/local/hub-local.js +2 -3
- package/dist/src/commands/local/hub-local.js.map +1 -1
- package/dist/src/commands/local/seed-zitadel.d.ts.map +1 -1
- package/dist/src/commands/local/seed-zitadel.js +108 -109
- package/dist/src/commands/local/seed-zitadel.js.map +1 -1
- package/dist/src/commands/local/stack.d.ts.map +1 -1
- package/dist/src/commands/local/stack.js +4 -9
- package/dist/src/commands/local/stack.js.map +1 -1
- package/dist/src/commands/site.d.ts +3 -0
- package/dist/src/commands/site.d.ts.map +1 -0
- package/dist/src/commands/site.js +277 -0
- package/dist/src/commands/site.js.map +1 -0
- package/dist/src/commands/temporal.d.ts +25 -0
- package/dist/src/commands/temporal.d.ts.map +1 -1
- package/dist/src/commands/temporal.js +2459 -732
- package/dist/src/commands/temporal.js.map +7 -1
- package/dist/src/commands/vcs/clone.d.ts.map +1 -1
- package/dist/src/commands/vcs/clone.js +4 -4
- package/dist/src/commands/vcs/clone.js.map +1 -1
- package/dist/src/commands/vcs/common.d.ts +34 -0
- package/dist/src/commands/vcs/common.d.ts.map +1 -1
- package/dist/src/commands/vcs/common.js +52 -0
- package/dist/src/commands/vcs/common.js.map +1 -1
- package/dist/src/commands/vcs/index.d.ts.map +1 -1
- package/dist/src/commands/vcs/index.js +7 -1
- package/dist/src/commands/vcs/index.js.map +1 -1
- package/dist/src/commands/vcs/propose.d.ts +22 -0
- package/dist/src/commands/vcs/propose.d.ts.map +1 -1
- package/dist/src/commands/vcs/propose.js +25 -5
- package/dist/src/commands/vcs/propose.js.map +1 -1
- package/dist/src/commands/vcs/rm.d.ts +18 -0
- package/dist/src/commands/vcs/rm.d.ts.map +1 -0
- package/dist/src/commands/vcs/rm.js +88 -0
- package/dist/src/commands/vcs/rm.js.map +1 -0
- package/dist/src/program.d.ts.map +1 -1
- package/dist/src/program.js +2 -0
- package/dist/src/program.js.map +1 -1
- package/dist/src/utils/aws-auth.d.ts +10 -0
- package/dist/src/utils/aws-auth.d.ts.map +1 -1
- package/dist/src/utils/aws-auth.js +35 -0
- package/dist/src/utils/aws-auth.js.map +1 -1
- package/dist/src/utils/cache-home.d.ts +25 -0
- package/dist/src/utils/cache-home.d.ts.map +1 -0
- package/dist/src/utils/cache-home.js +36 -0
- package/dist/src/utils/cache-home.js.map +1 -0
- package/dist/src/utils/temporal-codec.js +207 -294
- package/dist/src/utils/temporal-codec.js.map +7 -1
- package/fragments/base/index.ts.hbs +1 -1
- package/package.json +12 -3
- package/skills/core/SKILL.md +13 -3
- package/templates/api-auth/README.md.hbs +6 -0
- package/templates/api-auth/index.ts.hbs +1 -1
- package/templates/api-auth/package.json.hbs +1 -0
- package/templates/api-role-gating/index.ts.hbs +1 -1
- package/templates/api-role-gating/package.json.hbs +1 -0
- package/templates/external-service/api/package.json.hbs +1 -0
- package/templates/external-service/index.ts.hbs +1 -1
- package/templates/external-service/package.json.hbs +1 -0
- package/templates/workflow/index.ts.hbs +1 -1
- package/templates/workflow/package.json.hbs +1 -0
|
@@ -1,319 +1,232 @@
|
|
|
1
|
-
|
|
2
|
-
* Temporal payload decryption helpers for mesh CLI.
|
|
3
|
-
*
|
|
4
|
-
* Mirrors the AES-256-GCM codec in `@mesh-tech/app-kit/temporal-codec`. The CLI
|
|
5
|
-
* can't depend on app-kit directly because app-kit depends on mesh-cli — so
|
|
6
|
-
* the codec logic is intentionally duplicated here, kept small, and bounded
|
|
7
|
-
* to read-only decryption (the CLI never encrypts).
|
|
8
|
-
*
|
|
9
|
-
* Wire format (matches app-kit EncryptionCodec):
|
|
10
|
-
* metadata["encoding"] = "binary/encrypted"
|
|
11
|
-
* metadata["encryption-key-id"] = first 16 hex chars of SHA-256(encodingKey)
|
|
12
|
-
* data = IV (12 bytes) || ciphertext || GCM auth tag (16 bytes)
|
|
13
|
-
*
|
|
14
|
-
* Decryption of `data` yields a serialized Temporal Payload protobuf, whose
|
|
15
|
-
* inner `encoding` metadata tells you how to interpret the inner bytes
|
|
16
|
-
* (`json/plain`, `binary/plain`, etc.). We hand-decode the Payload message
|
|
17
|
-
* (a trivial 2-field protobuf) instead of depending on `@temporalio/proto`,
|
|
18
|
-
* so the CLI stays dependency-clean and doesn't need a Temporal SDK peer.
|
|
19
|
-
*/
|
|
1
|
+
// libs/mesh-cli/src/utils/temporal-codec.ts
|
|
20
2
|
import { execFileSync } from "child_process";
|
|
21
3
|
import { webcrypto as crypto } from "node:crypto";
|
|
22
|
-
|
|
23
|
-
//
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
4
|
+
|
|
5
|
+
// libs/mesh-cli/src/utils/log.ts
|
|
6
|
+
import chalk from "chalk";
|
|
7
|
+
function logPrefix(opts) {
|
|
8
|
+
const enabled = opts.envFlag === "1" || opts.envFlag !== "0" && !opts.isTTY;
|
|
9
|
+
return enabled ? `[${opts.now.toISOString().slice(11, 19)}] ` : "";
|
|
10
|
+
}
|
|
11
|
+
function prefix() {
|
|
12
|
+
return chalk.dim(
|
|
13
|
+
logPrefix({
|
|
14
|
+
isTTY: !!process.stderr.isTTY,
|
|
15
|
+
envFlag: process.env.MESH_LOG_TIMESTAMPS,
|
|
16
|
+
now: /* @__PURE__ */ new Date()
|
|
17
|
+
})
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
function logSuccess(message) {
|
|
21
|
+
console.error(prefix() + chalk.green("\u2713"), message);
|
|
22
|
+
}
|
|
23
|
+
function logWarn(message) {
|
|
24
|
+
console.error(prefix() + chalk.yellow("\u26A0"), message);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// libs/mesh-cli/src/utils/temporal-codec.ts
|
|
28
|
+
function resolveTemporalEncodingKeyFromK8s(namespace) {
|
|
29
|
+
const secretName = `${namespace}-temporal-encoding-key`;
|
|
30
|
+
try {
|
|
31
|
+
const b64 = execFileSync(
|
|
32
|
+
"kubectl",
|
|
33
|
+
[
|
|
34
|
+
"get",
|
|
35
|
+
"secret",
|
|
36
|
+
secretName,
|
|
37
|
+
"-n",
|
|
38
|
+
namespace,
|
|
39
|
+
"-o",
|
|
40
|
+
"jsonpath={.data.TEMPORAL_ENCODING_KEY}"
|
|
41
|
+
],
|
|
42
|
+
{ encoding: "utf-8", timeout: 1e4, stdio: ["pipe", "pipe", "pipe"] }
|
|
43
|
+
).trim();
|
|
44
|
+
if (!b64) return void 0;
|
|
45
|
+
return Buffer.from(b64, "base64").toString("utf-8");
|
|
46
|
+
} catch {
|
|
47
|
+
return void 0;
|
|
48
|
+
}
|
|
56
49
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const IV_LENGTH = 12;
|
|
50
|
+
var ENCRYPTED_ENCODING = "binary/encrypted";
|
|
51
|
+
var IV_LENGTH = 12;
|
|
60
52
|
async function deriveKey(encodingKey) {
|
|
61
|
-
|
|
62
|
-
|
|
53
|
+
const keyData = await crypto.subtle.digest(
|
|
54
|
+
"SHA-256",
|
|
55
|
+
new TextEncoder().encode(encodingKey)
|
|
56
|
+
);
|
|
57
|
+
return crypto.subtle.importKey(
|
|
58
|
+
"raw",
|
|
59
|
+
keyData,
|
|
60
|
+
{ name: "AES-GCM" },
|
|
61
|
+
false,
|
|
62
|
+
["decrypt"]
|
|
63
|
+
);
|
|
63
64
|
}
|
|
64
65
|
async function aesGcmDecrypt(data, key) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
66
|
+
const iv = data.slice(0, IV_LENGTH);
|
|
67
|
+
const ciphertextWithTag = data.slice(IV_LENGTH);
|
|
68
|
+
const decrypted = await crypto.subtle.decrypt(
|
|
69
|
+
{ name: "AES-GCM", iv },
|
|
70
|
+
key,
|
|
71
|
+
ciphertextWithTag
|
|
72
|
+
);
|
|
73
|
+
return new Uint8Array(decrypted);
|
|
69
74
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
inner = decodePayloadProtobuf(decrypted);
|
|
90
|
-
}
|
|
91
|
-
catch (err) {
|
|
92
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
93
|
-
return `[undecryptable: ${msg}]`;
|
|
94
|
-
}
|
|
95
|
-
return decodeInnerPayload(inner);
|
|
96
|
-
};
|
|
75
|
+
async function createPayloadDecrypter(encodingKey) {
|
|
76
|
+
const key = await deriveKey(encodingKey);
|
|
77
|
+
return async (payload) => {
|
|
78
|
+
if (!payload?.metadata) return null;
|
|
79
|
+
const encodingBytes = payload.metadata["encoding"];
|
|
80
|
+
if (!encodingBytes) return null;
|
|
81
|
+
const encoding = new TextDecoder().decode(encodingBytes);
|
|
82
|
+
if (encoding !== ENCRYPTED_ENCODING) return null;
|
|
83
|
+
if (!payload.data) return "[encrypted payload missing data]";
|
|
84
|
+
let inner;
|
|
85
|
+
try {
|
|
86
|
+
const decrypted = await aesGcmDecrypt(payload.data, key);
|
|
87
|
+
inner = decodePayloadProtobuf(decrypted);
|
|
88
|
+
} catch (err) {
|
|
89
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
90
|
+
return `[undecryptable: ${msg}]`;
|
|
91
|
+
}
|
|
92
|
+
return decodeInnerPayload(inner);
|
|
93
|
+
};
|
|
97
94
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
if (!encodingBytes)
|
|
113
|
-
return null;
|
|
114
|
-
const encoding = new TextDecoder().decode(encodingBytes);
|
|
115
|
-
if (encoding !== ENCRYPTED_ENCODING)
|
|
116
|
-
return null;
|
|
117
|
-
if (!payload.data) {
|
|
118
|
-
throw new Error("encrypted payload has no data to decrypt");
|
|
119
|
-
}
|
|
120
|
-
const decrypted = await aesGcmDecrypt(payload.data, key);
|
|
121
|
-
return decodePayloadProtobuf(decrypted);
|
|
122
|
-
};
|
|
95
|
+
async function createRawPayloadDecrypter(encodingKey) {
|
|
96
|
+
const key = await deriveKey(encodingKey);
|
|
97
|
+
return async (payload) => {
|
|
98
|
+
if (!payload?.metadata) return null;
|
|
99
|
+
const encodingBytes = payload.metadata["encoding"];
|
|
100
|
+
if (!encodingBytes) return null;
|
|
101
|
+
const encoding = new TextDecoder().decode(encodingBytes);
|
|
102
|
+
if (encoding !== ENCRYPTED_ENCODING) return null;
|
|
103
|
+
if (!payload.data) {
|
|
104
|
+
throw new Error("encrypted payload has no data to decrypt");
|
|
105
|
+
}
|
|
106
|
+
const decrypted = await aesGcmDecrypt(payload.data, key);
|
|
107
|
+
return decodePayloadProtobuf(decrypted);
|
|
108
|
+
};
|
|
123
109
|
}
|
|
124
|
-
// ── Hand-rolled Payload protobuf decoder ──────────────────────────────────
|
|
125
|
-
//
|
|
126
|
-
// temporal.api.common.v1.Payload has exactly two fields:
|
|
127
|
-
// message Payload {
|
|
128
|
-
// map<string, bytes> metadata = 1; // field 1, length-delimited
|
|
129
|
-
// bytes data = 2; // field 2, length-delimited
|
|
130
|
-
// }
|
|
131
|
-
// A protobuf `map<K, V>` is syntactic sugar for:
|
|
132
|
-
// repeated MapEntry { K key = 1; V value = 2; }
|
|
133
|
-
// so each metadata entry is itself a length-delimited submessage with two
|
|
134
|
-
// inner length-delimited fields.
|
|
135
|
-
//
|
|
136
|
-
// Decoding this by hand avoids pulling in `@temporalio/proto` as a peer dep.
|
|
137
110
|
function readVarint(buf, pos) {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
}
|
|
151
|
-
throw new Error("truncated varint");
|
|
111
|
+
let value = 0;
|
|
112
|
+
let shift = 0;
|
|
113
|
+
let next = pos;
|
|
114
|
+
while (next < buf.length) {
|
|
115
|
+
const byte = buf[next];
|
|
116
|
+
next += 1;
|
|
117
|
+
value |= (byte & 127) << shift;
|
|
118
|
+
if ((byte & 128) === 0) return { value, next };
|
|
119
|
+
shift += 7;
|
|
120
|
+
if (shift >= 32) throw new Error("varint too large");
|
|
121
|
+
}
|
|
122
|
+
throw new Error("truncated varint");
|
|
152
123
|
}
|
|
153
124
|
function readLengthDelimited(buf, pos) {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
return { bytes: buf.slice(afterLen, end), next: end };
|
|
125
|
+
const { value: length, next: afterLen } = readVarint(buf, pos);
|
|
126
|
+
const end = afterLen + length;
|
|
127
|
+
if (end > buf.length) throw new Error("truncated length-delimited field");
|
|
128
|
+
return { bytes: buf.slice(afterLen, end), next: end };
|
|
159
129
|
}
|
|
160
|
-
/**
|
|
161
|
-
* Decode a serialized `temporal.api.common.v1.Payload`.
|
|
162
|
-
*
|
|
163
|
-
* Unknown fields and wire types are skipped rather than throwing; we only
|
|
164
|
-
* need the two documented fields and Temporal's wire format is stable.
|
|
165
|
-
*/
|
|
166
130
|
function decodePayloadProtobuf(buf) {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
else if (wireType === 1) {
|
|
193
|
-
pos += 8; // fixed64
|
|
194
|
-
}
|
|
195
|
-
else if (wireType === 5) {
|
|
196
|
-
pos += 4; // fixed32
|
|
197
|
-
}
|
|
198
|
-
else {
|
|
199
|
-
throw new Error(`unsupported wire type ${wireType} at pos ${pos}`);
|
|
200
|
-
}
|
|
131
|
+
const metadata = {};
|
|
132
|
+
let data;
|
|
133
|
+
let pos = 0;
|
|
134
|
+
while (pos < buf.length) {
|
|
135
|
+
const { value: tag, next: afterTag } = readVarint(buf, pos);
|
|
136
|
+
const fieldNumber = tag >>> 3;
|
|
137
|
+
const wireType = tag & 7;
|
|
138
|
+
pos = afterTag;
|
|
139
|
+
if (wireType === 2) {
|
|
140
|
+
const { bytes, next } = readLengthDelimited(buf, pos);
|
|
141
|
+
pos = next;
|
|
142
|
+
if (fieldNumber === 1) {
|
|
143
|
+
const entry = decodeMetadataEntry(bytes);
|
|
144
|
+
if (entry) metadata[entry.key] = entry.value;
|
|
145
|
+
} else if (fieldNumber === 2) {
|
|
146
|
+
data = bytes;
|
|
147
|
+
}
|
|
148
|
+
} else if (wireType === 0) {
|
|
149
|
+
pos = readVarint(buf, pos).next;
|
|
150
|
+
} else if (wireType === 1) {
|
|
151
|
+
pos += 8;
|
|
152
|
+
} else if (wireType === 5) {
|
|
153
|
+
pos += 4;
|
|
154
|
+
} else {
|
|
155
|
+
throw new Error(`unsupported wire type ${wireType} at pos ${pos}`);
|
|
201
156
|
}
|
|
202
|
-
|
|
157
|
+
}
|
|
158
|
+
return { metadata, data: data ?? new Uint8Array(0) };
|
|
203
159
|
}
|
|
204
160
|
function decodeMetadataEntry(buf) {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
else if (wireType === 5)
|
|
220
|
-
pos += 4;
|
|
221
|
-
else
|
|
222
|
-
throw new Error(`unsupported metadata wire type ${wireType}`);
|
|
223
|
-
continue;
|
|
224
|
-
}
|
|
225
|
-
const { bytes, next } = readLengthDelimited(buf, pos);
|
|
226
|
-
pos = next;
|
|
227
|
-
if (fieldNumber === 1)
|
|
228
|
-
key = new TextDecoder().decode(bytes);
|
|
229
|
-
else if (fieldNumber === 2)
|
|
230
|
-
value = bytes;
|
|
161
|
+
let key;
|
|
162
|
+
let value;
|
|
163
|
+
let pos = 0;
|
|
164
|
+
while (pos < buf.length) {
|
|
165
|
+
const { value: tag, next: afterTag } = readVarint(buf, pos);
|
|
166
|
+
const fieldNumber = tag >>> 3;
|
|
167
|
+
const wireType = tag & 7;
|
|
168
|
+
pos = afterTag;
|
|
169
|
+
if (wireType !== 2) {
|
|
170
|
+
if (wireType === 0) pos = readVarint(buf, pos).next;
|
|
171
|
+
else if (wireType === 1) pos += 8;
|
|
172
|
+
else if (wireType === 5) pos += 4;
|
|
173
|
+
else throw new Error(`unsupported metadata wire type ${wireType}`);
|
|
174
|
+
continue;
|
|
231
175
|
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
176
|
+
const { bytes, next } = readLengthDelimited(buf, pos);
|
|
177
|
+
pos = next;
|
|
178
|
+
if (fieldNumber === 1) key = new TextDecoder().decode(bytes);
|
|
179
|
+
else if (fieldNumber === 2) value = bytes;
|
|
180
|
+
}
|
|
181
|
+
if (key === void 0 || value === void 0) return null;
|
|
182
|
+
return { key, value };
|
|
235
183
|
}
|
|
236
|
-
/**
|
|
237
|
-
* Render a decrypted inner Payload as a human-readable string.
|
|
238
|
-
*
|
|
239
|
-
* Handles the common encodings Mesh workers emit:
|
|
240
|
-
* - `json/plain` → pretty-printed JSON
|
|
241
|
-
* - `binary/null` → literal `null`
|
|
242
|
-
* - `binary/plain` → `[binary N bytes]`
|
|
243
|
-
* - anything else → `[encoding=X, N bytes]`
|
|
244
|
-
*/
|
|
245
184
|
function decodeInnerPayload(payload) {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
return JSON.stringify(JSON.parse(text), null, 2);
|
|
258
|
-
}
|
|
259
|
-
catch {
|
|
260
|
-
// Not valid JSON despite the encoding claim — surface the raw text so
|
|
261
|
-
// the debugger sees whatever garbage the worker actually wrote.
|
|
262
|
-
return text;
|
|
263
|
-
}
|
|
185
|
+
const meta = payload.metadata ?? {};
|
|
186
|
+
const encodingBytes = meta["encoding"];
|
|
187
|
+
const encoding = encodingBytes ? new TextDecoder().decode(encodingBytes) : "unknown";
|
|
188
|
+
const data = payload.data ?? new Uint8Array(0);
|
|
189
|
+
if (encoding === "binary/null") return "null";
|
|
190
|
+
if (encoding === "json/plain") {
|
|
191
|
+
const text = new TextDecoder().decode(data);
|
|
192
|
+
try {
|
|
193
|
+
return JSON.stringify(JSON.parse(text), null, 2);
|
|
194
|
+
} catch {
|
|
195
|
+
return text;
|
|
264
196
|
}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
197
|
+
}
|
|
198
|
+
if (encoding === "binary/plain") return `[binary, ${data.length} bytes]`;
|
|
199
|
+
return `[encoding=${encoding}, ${data.length} bytes]`;
|
|
268
200
|
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
const fromK8s = resolveTemporalEncodingKeyFromK8s(namespace);
|
|
283
|
-
const key = fromK8s ?? process.env.TEMPORAL_ENCODING_KEY;
|
|
284
|
-
if (!key) {
|
|
285
|
-
logWarn(`Could not read TEMPORAL_ENCODING_KEY from secret ` +
|
|
286
|
-
`${namespace}/${namespace}-temporal-encoding-key, and TEMPORAL_ENCODING_KEY ` +
|
|
287
|
-
"is not set. Configure kubectl (namespace read " +
|
|
288
|
-
"access), or export TEMPORAL_ENCODING_KEY (e.g. from a `mesh dev` worker env).");
|
|
289
|
-
return undefined;
|
|
290
|
-
}
|
|
291
|
-
logSuccess(fromK8s
|
|
292
|
-
? `Resolved encoding key from ${namespace}-temporal-encoding-key`
|
|
293
|
-
: "Resolved encoding key from TEMPORAL_ENCODING_KEY env");
|
|
294
|
-
return key;
|
|
201
|
+
function resolveEncodingKey(namespace) {
|
|
202
|
+
const fromK8s = resolveTemporalEncodingKeyFromK8s(namespace);
|
|
203
|
+
const key = fromK8s ?? process.env.TEMPORAL_ENCODING_KEY;
|
|
204
|
+
if (!key) {
|
|
205
|
+
logWarn(
|
|
206
|
+
`Could not read TEMPORAL_ENCODING_KEY from secret ${namespace}/${namespace}-temporal-encoding-key, and TEMPORAL_ENCODING_KEY is not set. Configure kubectl (namespace read access), or export TEMPORAL_ENCODING_KEY (e.g. from a \`mesh dev\` worker env).`
|
|
207
|
+
);
|
|
208
|
+
return void 0;
|
|
209
|
+
}
|
|
210
|
+
logSuccess(
|
|
211
|
+
fromK8s ? `Resolved encoding key from ${namespace}-temporal-encoding-key` : "Resolved encoding key from TEMPORAL_ENCODING_KEY env"
|
|
212
|
+
);
|
|
213
|
+
return key;
|
|
295
214
|
}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
*/
|
|
301
|
-
export async function buildDecrypterForNamespace(namespace) {
|
|
302
|
-
const key = resolveEncodingKey(namespace);
|
|
303
|
-
if (!key)
|
|
304
|
-
return undefined;
|
|
305
|
-
return createPayloadDecrypter(key);
|
|
215
|
+
async function buildDecrypterForNamespace(namespace) {
|
|
216
|
+
const key = resolveEncodingKey(namespace);
|
|
217
|
+
if (!key) return void 0;
|
|
218
|
+
return createPayloadDecrypter(key);
|
|
306
219
|
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
* callers decide whether that's fatal.
|
|
312
|
-
*/
|
|
313
|
-
export async function buildRawDecrypterForNamespace(namespace) {
|
|
314
|
-
const key = resolveEncodingKey(namespace);
|
|
315
|
-
if (!key)
|
|
316
|
-
return undefined;
|
|
317
|
-
return createRawPayloadDecrypter(key);
|
|
220
|
+
async function buildRawDecrypterForNamespace(namespace) {
|
|
221
|
+
const key = resolveEncodingKey(namespace);
|
|
222
|
+
if (!key) return void 0;
|
|
223
|
+
return createRawPayloadDecrypter(key);
|
|
318
224
|
}
|
|
319
|
-
|
|
225
|
+
export {
|
|
226
|
+
buildDecrypterForNamespace,
|
|
227
|
+
buildRawDecrypterForNamespace,
|
|
228
|
+
createPayloadDecrypter,
|
|
229
|
+
createRawPayloadDecrypter,
|
|
230
|
+
resolveEncodingKey,
|
|
231
|
+
resolveTemporalEncodingKeyFromK8s
|
|
232
|
+
};
|
|
@@ -1 +1,7 @@
|
|
|
1
|
-
{
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/utils/temporal-codec.ts", "../../../src/utils/log.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Temporal payload decryption helpers for mesh CLI.\n *\n * Mirrors the AES-256-GCM codec in `@mesh-tech/app-kit/temporal-codec`. The CLI\n * can't depend on app-kit directly because app-kit depends on mesh-cli \u2014 so\n * the codec logic is intentionally duplicated here, kept small, and bounded\n * to read-only decryption (the CLI never encrypts).\n *\n * Wire format (matches app-kit EncryptionCodec):\n * metadata[\"encoding\"] = \"binary/encrypted\"\n * metadata[\"encryption-key-id\"] = first 16 hex chars of SHA-256(encodingKey)\n * data = IV (12 bytes) || ciphertext || GCM auth tag (16 bytes)\n *\n * Decryption of `data` yields a serialized Temporal Payload protobuf, whose\n * inner `encoding` metadata tells you how to interpret the inner bytes\n * (`json/plain`, `binary/plain`, etc.). We hand-decode the Payload message\n * (a trivial 2-field protobuf) instead of depending on `@temporalio/proto`,\n * so the CLI stays dependency-clean and doesn't need a Temporal SDK peer.\n */\n\nimport { execFileSync } from \"child_process\";\nimport { webcrypto as crypto } from \"node:crypto\";\nimport { logSuccess, logWarn } from \"./log.js\";\n\n// \u2500\u2500 Encoding key resolution \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Read the Temporal encoding key from a Kubernetes secret in the given\n * namespace. Returns `undefined` when kubectl is unavailable, the secret\n * doesn't exist, or the caller isn't authenticated to the cluster.\n *\n * The K8s secret is preferred over Pulumi stack output because personal dev\n * stacks ship a placeholder encoding key that cannot decrypt deployed\n * payloads; the K8s secret is always authoritative for a given namespace.\n *\n * @param namespace - Kubernetes namespace matching the Temporal namespace\n * (mesh apps use the same name for both).\n */\nexport function resolveTemporalEncodingKeyFromK8s(\n namespace: string,\n): string | undefined {\n const secretName = `${namespace}-temporal-encoding-key`;\n try {\n const b64 = execFileSync(\n \"kubectl\",\n [\n \"get\",\n \"secret\",\n secretName,\n \"-n\",\n namespace,\n \"-o\",\n \"jsonpath={.data.TEMPORAL_ENCODING_KEY}\",\n ],\n { encoding: \"utf-8\", timeout: 10_000, stdio: [\"pipe\", \"pipe\", \"pipe\"] },\n ).trim();\n\n if (!b64) return undefined;\n return Buffer.from(b64, \"base64\").toString(\"utf-8\");\n } catch {\n // kubectl missing, secret absent, or no cluster access \u2014 caller decides.\n return undefined;\n }\n}\n\n// \u2500\u2500 AES-256-GCM decryption \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst ENCRYPTED_ENCODING = \"binary/encrypted\";\nconst IV_LENGTH = 12;\n\nasync function deriveKey(encodingKey: string): Promise<crypto.CryptoKey> {\n const keyData = await crypto.subtle.digest(\n \"SHA-256\",\n new TextEncoder().encode(encodingKey),\n );\n return crypto.subtle.importKey(\n \"raw\",\n keyData,\n { name: \"AES-GCM\" },\n false,\n [\"decrypt\"],\n );\n}\n\nasync function aesGcmDecrypt(\n data: Uint8Array,\n key: crypto.CryptoKey,\n): Promise<Uint8Array> {\n const iv = data.slice(0, IV_LENGTH);\n const ciphertextWithTag = data.slice(IV_LENGTH);\n const decrypted = await crypto.subtle.decrypt(\n { name: \"AES-GCM\", iv },\n key,\n ciphertextWithTag,\n );\n return new Uint8Array(decrypted);\n}\n\n// \u2500\u2500 Payload decoder \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Minimal shape of a Temporal Payload we care about.\n * Matches `temporal.api.common.v1.Payload` from @temporalio/proto but avoids\n * importing the type at module load time (so the CLI still loads without the\n * Temporal SDK installed).\n */\nexport interface RawPayload {\n metadata?: Record<string, Uint8Array | null | undefined> | null;\n data?: Uint8Array | null;\n}\n\n/**\n * A decrypter closure bound to a specific encoding key. Returns a decoded\n * human-readable string for a single encrypted Payload, or `null` when the\n * payload wasn't encrypted and the caller should fall back to raw decoding.\n *\n * Error paths (`[undecryptable]`, `[decode error: ...]`) are returned as\n * strings rather than thrown \u2014 debugging a history is a best-effort read-only\n * operation and one bad payload shouldn't abort the whole dump.\n */\nexport type PayloadDecrypter = (\n payload: RawPayload | null | undefined,\n) => Promise<string | null>;\n\n/**\n * Build a decrypter closure for the given encoding key.\n */\nexport async function createPayloadDecrypter(\n encodingKey: string,\n): Promise<PayloadDecrypter> {\n const key = await deriveKey(encodingKey);\n\n return async (payload) => {\n if (!payload?.metadata) return null;\n\n const encodingBytes = payload.metadata[\"encoding\"];\n if (!encodingBytes) return null;\n const encoding = new TextDecoder().decode(encodingBytes);\n if (encoding !== ENCRYPTED_ENCODING) return null;\n\n if (!payload.data) return \"[encrypted payload missing data]\";\n\n let inner: RawPayload;\n try {\n const decrypted = await aesGcmDecrypt(payload.data, key);\n inner = decodePayloadProtobuf(decrypted);\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n return `[undecryptable: ${msg}]`;\n }\n\n return decodeInnerPayload(inner);\n };\n}\n\n/**\n * A closure that decrypts a single encrypted Temporal Payload to its INNER\n * (plaintext) Payload \u2014 the `{ metadata, data }` the worker's codec wrapped \u2014\n * rather than the human-readable string {@link PayloadDecrypter} produces.\n *\n * Returns `null` when the payload isn't encrypted (the caller leaves it as-is).\n * Unlike the display decrypter, decryption errors THROW here: a fixture with a\n * silently-mangled payload would replay wrong, so the caller must surface it.\n */\nexport type RawPayloadDecrypter = (\n payload: RawPayload | null | undefined,\n) => Promise<RawPayload | null>;\n\n/**\n * Build a raw (Payload-in, Payload-out) decrypter for the given encoding key.\n *\n * This is the fixture-capture counterpart of {@link createPayloadDecrypter}:\n * where that one renders a payload for human display (lossy \u2014 binary payloads\n * become placeholders), this one returns the exact decrypted inner Payload so\n * the bytes can be re-serialized into a replayable proto3-JSON history.\n */\nexport async function createRawPayloadDecrypter(\n encodingKey: string,\n): Promise<RawPayloadDecrypter> {\n const key = await deriveKey(encodingKey);\n\n return async (payload) => {\n if (!payload?.metadata) return null;\n\n const encodingBytes = payload.metadata[\"encoding\"];\n if (!encodingBytes) return null;\n const encoding = new TextDecoder().decode(encodingBytes);\n if (encoding !== ENCRYPTED_ENCODING) return null;\n\n if (!payload.data) {\n throw new Error(\"encrypted payload has no data to decrypt\");\n }\n\n const decrypted = await aesGcmDecrypt(payload.data, key);\n return decodePayloadProtobuf(decrypted);\n };\n}\n\n// \u2500\u2500 Hand-rolled Payload protobuf decoder \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n//\n// temporal.api.common.v1.Payload has exactly two fields:\n// message Payload {\n// map<string, bytes> metadata = 1; // field 1, length-delimited\n// bytes data = 2; // field 2, length-delimited\n// }\n// A protobuf `map<K, V>` is syntactic sugar for:\n// repeated MapEntry { K key = 1; V value = 2; }\n// so each metadata entry is itself a length-delimited submessage with two\n// inner length-delimited fields.\n//\n// Decoding this by hand avoids pulling in `@temporalio/proto` as a peer dep.\n\nfunction readVarint(buf: Uint8Array, pos: number): { value: number; next: number } {\n let value = 0;\n let shift = 0;\n let next = pos;\n while (next < buf.length) {\n const byte = buf[next]!;\n next += 1;\n value |= (byte & 0x7f) << shift;\n if ((byte & 0x80) === 0) return { value, next };\n shift += 7;\n if (shift >= 32) throw new Error(\"varint too large\");\n }\n throw new Error(\"truncated varint\");\n}\n\nfunction readLengthDelimited(\n buf: Uint8Array,\n pos: number,\n): { bytes: Uint8Array; next: number } {\n const { value: length, next: afterLen } = readVarint(buf, pos);\n const end = afterLen + length;\n if (end > buf.length) throw new Error(\"truncated length-delimited field\");\n return { bytes: buf.slice(afterLen, end), next: end };\n}\n\n/**\n * Decode a serialized `temporal.api.common.v1.Payload`.\n *\n * Unknown fields and wire types are skipped rather than throwing; we only\n * need the two documented fields and Temporal's wire format is stable.\n */\nfunction decodePayloadProtobuf(buf: Uint8Array): RawPayload {\n const metadata: Record<string, Uint8Array> = {};\n let data: Uint8Array | undefined;\n let pos = 0;\n\n while (pos < buf.length) {\n const { value: tag, next: afterTag } = readVarint(buf, pos);\n const fieldNumber = tag >>> 3;\n const wireType = tag & 0x7;\n pos = afterTag;\n\n if (wireType === 2) {\n const { bytes, next } = readLengthDelimited(buf, pos);\n pos = next;\n if (fieldNumber === 1) {\n const entry = decodeMetadataEntry(bytes);\n if (entry) metadata[entry.key] = entry.value;\n } else if (fieldNumber === 2) {\n data = bytes;\n }\n // Other fields: ignore.\n } else if (wireType === 0) {\n // varint \u2014 skip\n pos = readVarint(buf, pos).next;\n } else if (wireType === 1) {\n pos += 8; // fixed64\n } else if (wireType === 5) {\n pos += 4; // fixed32\n } else {\n throw new Error(`unsupported wire type ${wireType} at pos ${pos}`);\n }\n }\n\n return { metadata, data: data ?? new Uint8Array(0) };\n}\n\nfunction decodeMetadataEntry(\n buf: Uint8Array,\n): { key: string; value: Uint8Array } | null {\n let key: string | undefined;\n let value: Uint8Array | undefined;\n let pos = 0;\n\n while (pos < buf.length) {\n const { value: tag, next: afterTag } = readVarint(buf, pos);\n const fieldNumber = tag >>> 3;\n const wireType = tag & 0x7;\n pos = afterTag;\n if (wireType !== 2) {\n // Skip unsupported fields defensively.\n if (wireType === 0) pos = readVarint(buf, pos).next;\n else if (wireType === 1) pos += 8;\n else if (wireType === 5) pos += 4;\n else throw new Error(`unsupported metadata wire type ${wireType}`);\n continue;\n }\n const { bytes, next } = readLengthDelimited(buf, pos);\n pos = next;\n if (fieldNumber === 1) key = new TextDecoder().decode(bytes);\n else if (fieldNumber === 2) value = bytes;\n }\n\n if (key === undefined || value === undefined) return null;\n return { key, value };\n}\n\n/**\n * Render a decrypted inner Payload as a human-readable string.\n *\n * Handles the common encodings Mesh workers emit:\n * - `json/plain` \u2192 pretty-printed JSON\n * - `binary/null` \u2192 literal `null`\n * - `binary/plain` \u2192 `[binary N bytes]`\n * - anything else \u2192 `[encoding=X, N bytes]`\n */\nfunction decodeInnerPayload(payload: RawPayload): string {\n const meta = payload.metadata ?? {};\n const encodingBytes = meta[\"encoding\"];\n const encoding = encodingBytes\n ? new TextDecoder().decode(encodingBytes)\n : \"unknown\";\n const data = payload.data ?? new Uint8Array(0);\n\n if (encoding === \"binary/null\") return \"null\";\n if (encoding === \"json/plain\") {\n const text = new TextDecoder().decode(data);\n try {\n return JSON.stringify(JSON.parse(text), null, 2);\n } catch {\n // Not valid JSON despite the encoding claim \u2014 surface the raw text so\n // the debugger sees whatever garbage the worker actually wrote.\n return text;\n }\n }\n if (encoding === \"binary/plain\") return `[binary, ${data.length} bytes]`;\n return `[encoding=${encoding}, ${data.length} bytes]`;\n}\n\n// \u2500\u2500 High-level helper \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Resolve the Temporal encoding key for a namespace, logging where it came\n * from (or why it couldn't be found). Prefer the K8s secret (authoritative for\n * the namespace); fall back to an explicit TEMPORAL_ENCODING_KEY env var. The\n * env fallback lets you decrypt a workflow's history OFFLINE \u2014 e.g. to\n * reconstruct a conversation, or capture a replay fixture, whose worker can no\n * longer replay it \u2014 using the key from a running `mesh dev` worker env when\n * kubectl/cluster access isn't set up.\n *\n * Returns `undefined` when neither source yields a key.\n */\nexport function resolveEncodingKey(namespace: string): string | undefined {\n const fromK8s = resolveTemporalEncodingKeyFromK8s(namespace);\n const key = fromK8s ?? process.env.TEMPORAL_ENCODING_KEY;\n if (!key) {\n logWarn(\n `Could not read TEMPORAL_ENCODING_KEY from secret ` +\n `${namespace}/${namespace}-temporal-encoding-key, and TEMPORAL_ENCODING_KEY ` +\n \"is not set. Configure kubectl (namespace read \" +\n \"access), or export TEMPORAL_ENCODING_KEY (e.g. from a `mesh dev` worker env).\",\n );\n return undefined;\n }\n logSuccess(\n fromK8s\n ? `Resolved encoding key from ${namespace}-temporal-encoding-key`\n : \"Resolved encoding key from TEMPORAL_ENCODING_KEY env\",\n );\n return key;\n}\n\n/**\n * Resolve the encoding key and build a decrypter for the given namespace.\n * Logs a warning and returns `undefined` when the key can't be read; callers\n * should fall back to printing events without payloads.\n */\nexport async function buildDecrypterForNamespace(\n namespace: string,\n): Promise<PayloadDecrypter | undefined> {\n const key = resolveEncodingKey(namespace);\n if (!key) return undefined;\n return createPayloadDecrypter(key);\n}\n\n/**\n * Resolve the encoding key and build a RAW (Payload-in/Payload-out) decrypter\n * for the given namespace \u2014 used by `capture-history` to decrypt payloads back\n * into replayable proto bytes. Returns `undefined` when the key can't be read;\n * callers decide whether that's fatal.\n */\nexport async function buildRawDecrypterForNamespace(\n namespace: string,\n): Promise<RawPayloadDecrypter | undefined> {\n const key = resolveEncodingKey(namespace);\n if (!key) return undefined;\n return createRawPayloadDecrypter(key);\n}\n", "/**\n * Logging utilities with colored output.\n *\n * Non-TTY streams (CI, agent runs, piped output) get an [HH:MM:SS] prefix on\n * every line so long quiet stretches are attributable after the fact\n * (MESH-2479); interactive terminals stay clean. Override with\n * MESH_LOG_TIMESTAMPS=1 (force on) / =0 (force off).\n */\n\nimport chalk from \"chalk\";\n\n/**\n * Pure prefix decision, exported for tests: timestamps when the env flag says\n * so, else whenever stderr is not a TTY.\n */\nexport function logPrefix(opts: {\n isTTY: boolean;\n envFlag: string | undefined;\n now: Date;\n}): string {\n const enabled = opts.envFlag === \"1\" || (opts.envFlag !== \"0\" && !opts.isTTY);\n return enabled ? `[${opts.now.toISOString().slice(11, 19)}] ` : \"\";\n}\n\nfunction prefix(): string {\n return chalk.dim(\n logPrefix({\n isTTY: !!process.stderr.isTTY,\n envFlag: process.env.MESH_LOG_TIMESTAMPS,\n now: new Date(),\n }),\n );\n}\n\nexport function logInfo(message: string): void {\n console.error(prefix() + chalk.blue(\"\u2139\"), message);\n}\n\nexport function logSuccess(message: string): void {\n console.error(prefix() + chalk.green(\"\u2713\"), message);\n}\n\nexport function logWarn(message: string): void {\n console.error(prefix() + chalk.yellow(\"\u26A0\"), message);\n}\n\nexport function logError(message: string): void {\n console.error(prefix() + chalk.red(\"\u2717\"), message);\n}\n\n/** Render elapsed milliseconds compactly: \"45s\", \"2m 30s\", \"1h 5m\". */\nexport function formatElapsed(ms: number): string {\n const totalSec = Math.max(0, Math.round(ms / 1000));\n const h = Math.floor(totalSec / 3600);\n const m = Math.floor((totalSec % 3600) / 60);\n const s = totalSec % 60;\n if (h > 0) return `${h}h ${m}m`;\n if (m > 0) return `${m}m ${s}s`;\n return `${s}s`;\n}\n\n/**\n * Periodic \"still working\" line during a long silent operation (registry\n * fetches, image builds) so a non-TTY first run never sits at 0 bytes of\n * output for minutes (MESH-2479). `touch()` marks recent real output, which\n * suppresses the next beat \u2014 the heartbeat only fills silence.\n */\nexport function startHeartbeat(\n label: string,\n intervalMs = 15_000,\n): { stop: () => void; touch: () => void } {\n const startedAt = Date.now();\n let lastOutputAt = startedAt;\n const timer = setInterval(() => {\n if (Date.now() - lastOutputAt < intervalMs) return;\n logInfo(`\u2026 still working: ${label} (${formatElapsed(Date.now() - startedAt)} elapsed)`);\n }, intervalMs);\n timer.unref?.();\n return {\n stop: () => clearInterval(timer),\n touch: () => {\n lastOutputAt = Date.now();\n },\n };\n}\n"],
|
|
5
|
+
"mappings": ";AAoBA,SAAS,oBAAoB;AAC7B,SAAS,aAAa,cAAc;;;ACZpC,OAAO,WAAW;AAMX,SAAS,UAAU,MAIf;AACT,QAAM,UAAU,KAAK,YAAY,OAAQ,KAAK,YAAY,OAAO,CAAC,KAAK;AACvE,SAAO,UAAU,IAAI,KAAK,IAAI,YAAY,EAAE,MAAM,IAAI,EAAE,CAAC,OAAO;AAClE;AAEA,SAAS,SAAiB;AACxB,SAAO,MAAM;AAAA,IACX,UAAU;AAAA,MACR,OAAO,CAAC,CAAC,QAAQ,OAAO;AAAA,MACxB,SAAS,QAAQ,IAAI;AAAA,MACrB,KAAK,oBAAI,KAAK;AAAA,IAChB,CAAC;AAAA,EACH;AACF;AAMO,SAAS,WAAW,SAAuB;AAChD,UAAQ,MAAM,OAAO,IAAI,MAAM,MAAM,QAAG,GAAG,OAAO;AACpD;AAEO,SAAS,QAAQ,SAAuB;AAC7C,UAAQ,MAAM,OAAO,IAAI,MAAM,OAAO,QAAG,GAAG,OAAO;AACrD;;;ADNO,SAAS,kCACd,WACoB;AACpB,QAAM,aAAa,GAAG,SAAS;AAC/B,MAAI;AACF,UAAM,MAAM;AAAA,MACV;AAAA,MACA;AAAA,QACE;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,MACA,EAAE,UAAU,SAAS,SAAS,KAAQ,OAAO,CAAC,QAAQ,QAAQ,MAAM,EAAE;AAAA,IACxE,EAAE,KAAK;AAEP,QAAI,CAAC,IAAK,QAAO;AACjB,WAAO,OAAO,KAAK,KAAK,QAAQ,EAAE,SAAS,OAAO;AAAA,EACpD,QAAQ;AAEN,WAAO;AAAA,EACT;AACF;AAIA,IAAM,qBAAqB;AAC3B,IAAM,YAAY;AAElB,eAAe,UAAU,aAAgD;AACvE,QAAM,UAAU,MAAM,OAAO,OAAO;AAAA,IAClC;AAAA,IACA,IAAI,YAAY,EAAE,OAAO,WAAW;AAAA,EACtC;AACA,SAAO,OAAO,OAAO;AAAA,IACnB;AAAA,IACA;AAAA,IACA,EAAE,MAAM,UAAU;AAAA,IAClB;AAAA,IACA,CAAC,SAAS;AAAA,EACZ;AACF;AAEA,eAAe,cACb,MACA,KACqB;AACrB,QAAM,KAAK,KAAK,MAAM,GAAG,SAAS;AAClC,QAAM,oBAAoB,KAAK,MAAM,SAAS;AAC9C,QAAM,YAAY,MAAM,OAAO,OAAO;AAAA,IACpC,EAAE,MAAM,WAAW,GAAG;AAAA,IACtB;AAAA,IACA;AAAA,EACF;AACA,SAAO,IAAI,WAAW,SAAS;AACjC;AA+BA,eAAsB,uBACpB,aAC2B;AAC3B,QAAM,MAAM,MAAM,UAAU,WAAW;AAEvC,SAAO,OAAO,YAAY;AACxB,QAAI,CAAC,SAAS,SAAU,QAAO;AAE/B,UAAM,gBAAgB,QAAQ,SAAS,UAAU;AACjD,QAAI,CAAC,cAAe,QAAO;AAC3B,UAAM,WAAW,IAAI,YAAY,EAAE,OAAO,aAAa;AACvD,QAAI,aAAa,mBAAoB,QAAO;AAE5C,QAAI,CAAC,QAAQ,KAAM,QAAO;AAE1B,QAAI;AACJ,QAAI;AACF,YAAM,YAAY,MAAM,cAAc,QAAQ,MAAM,GAAG;AACvD,cAAQ,sBAAsB,SAAS;AAAA,IACzC,SAAS,KAAK;AACZ,YAAM,MAAM,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAC3D,aAAO,mBAAmB,GAAG;AAAA,IAC/B;AAEA,WAAO,mBAAmB,KAAK;AAAA,EACjC;AACF;AAuBA,eAAsB,0BACpB,aAC8B;AAC9B,QAAM,MAAM,MAAM,UAAU,WAAW;AAEvC,SAAO,OAAO,YAAY;AACxB,QAAI,CAAC,SAAS,SAAU,QAAO;AAE/B,UAAM,gBAAgB,QAAQ,SAAS,UAAU;AACjD,QAAI,CAAC,cAAe,QAAO;AAC3B,UAAM,WAAW,IAAI,YAAY,EAAE,OAAO,aAAa;AACvD,QAAI,aAAa,mBAAoB,QAAO;AAE5C,QAAI,CAAC,QAAQ,MAAM;AACjB,YAAM,IAAI,MAAM,0CAA0C;AAAA,IAC5D;AAEA,UAAM,YAAY,MAAM,cAAc,QAAQ,MAAM,GAAG;AACvD,WAAO,sBAAsB,SAAS;AAAA,EACxC;AACF;AAgBA,SAAS,WAAW,KAAiB,KAA8C;AACjF,MAAI,QAAQ;AACZ,MAAI,QAAQ;AACZ,MAAI,OAAO;AACX,SAAO,OAAO,IAAI,QAAQ;AACxB,UAAM,OAAO,IAAI,IAAI;AACrB,YAAQ;AACR,cAAU,OAAO,QAAS;AAC1B,SAAK,OAAO,SAAU,EAAG,QAAO,EAAE,OAAO,KAAK;AAC9C,aAAS;AACT,QAAI,SAAS,GAAI,OAAM,IAAI,MAAM,kBAAkB;AAAA,EACrD;AACA,QAAM,IAAI,MAAM,kBAAkB;AACpC;AAEA,SAAS,oBACP,KACA,KACqC;AACrC,QAAM,EAAE,OAAO,QAAQ,MAAM,SAAS,IAAI,WAAW,KAAK,GAAG;AAC7D,QAAM,MAAM,WAAW;AACvB,MAAI,MAAM,IAAI,OAAQ,OAAM,IAAI,MAAM,kCAAkC;AACxE,SAAO,EAAE,OAAO,IAAI,MAAM,UAAU,GAAG,GAAG,MAAM,IAAI;AACtD;AAQA,SAAS,sBAAsB,KAA6B;AAC1D,QAAM,WAAuC,CAAC;AAC9C,MAAI;AACJ,MAAI,MAAM;AAEV,SAAO,MAAM,IAAI,QAAQ;AACvB,UAAM,EAAE,OAAO,KAAK,MAAM,SAAS,IAAI,WAAW,KAAK,GAAG;AAC1D,UAAM,cAAc,QAAQ;AAC5B,UAAM,WAAW,MAAM;AACvB,UAAM;AAEN,QAAI,aAAa,GAAG;AAClB,YAAM,EAAE,OAAO,KAAK,IAAI,oBAAoB,KAAK,GAAG;AACpD,YAAM;AACN,UAAI,gBAAgB,GAAG;AACrB,cAAM,QAAQ,oBAAoB,KAAK;AACvC,YAAI,MAAO,UAAS,MAAM,GAAG,IAAI,MAAM;AAAA,MACzC,WAAW,gBAAgB,GAAG;AAC5B,eAAO;AAAA,MACT;AAAA,IAEF,WAAW,aAAa,GAAG;AAEzB,YAAM,WAAW,KAAK,GAAG,EAAE;AAAA,IAC7B,WAAW,aAAa,GAAG;AACzB,aAAO;AAAA,IACT,WAAW,aAAa,GAAG;AACzB,aAAO;AAAA,IACT,OAAO;AACL,YAAM,IAAI,MAAM,yBAAyB,QAAQ,WAAW,GAAG,EAAE;AAAA,IACnE;AAAA,EACF;AAEA,SAAO,EAAE,UAAU,MAAM,QAAQ,IAAI,WAAW,CAAC,EAAE;AACrD;AAEA,SAAS,oBACP,KAC2C;AAC3C,MAAI;AACJ,MAAI;AACJ,MAAI,MAAM;AAEV,SAAO,MAAM,IAAI,QAAQ;AACvB,UAAM,EAAE,OAAO,KAAK,MAAM,SAAS,IAAI,WAAW,KAAK,GAAG;AAC1D,UAAM,cAAc,QAAQ;AAC5B,UAAM,WAAW,MAAM;AACvB,UAAM;AACN,QAAI,aAAa,GAAG;AAElB,UAAI,aAAa,EAAG,OAAM,WAAW,KAAK,GAAG,EAAE;AAAA,eACtC,aAAa,EAAG,QAAO;AAAA,eACvB,aAAa,EAAG,QAAO;AAAA,UAC3B,OAAM,IAAI,MAAM,kCAAkC,QAAQ,EAAE;AACjE;AAAA,IACF;AACA,UAAM,EAAE,OAAO,KAAK,IAAI,oBAAoB,KAAK,GAAG;AACpD,UAAM;AACN,QAAI,gBAAgB,EAAG,OAAM,IAAI,YAAY,EAAE,OAAO,KAAK;AAAA,aAClD,gBAAgB,EAAG,SAAQ;AAAA,EACtC;AAEA,MAAI,QAAQ,UAAa,UAAU,OAAW,QAAO;AACrD,SAAO,EAAE,KAAK,MAAM;AACtB;AAWA,SAAS,mBAAmB,SAA6B;AACvD,QAAM,OAAO,QAAQ,YAAY,CAAC;AAClC,QAAM,gBAAgB,KAAK,UAAU;AACrC,QAAM,WAAW,gBACb,IAAI,YAAY,EAAE,OAAO,aAAa,IACtC;AACJ,QAAM,OAAO,QAAQ,QAAQ,IAAI,WAAW,CAAC;AAE7C,MAAI,aAAa,cAAe,QAAO;AACvC,MAAI,aAAa,cAAc;AAC7B,UAAM,OAAO,IAAI,YAAY,EAAE,OAAO,IAAI;AAC1C,QAAI;AACF,aAAO,KAAK,UAAU,KAAK,MAAM,IAAI,GAAG,MAAM,CAAC;AAAA,IACjD,QAAQ;AAGN,aAAO;AAAA,IACT;AAAA,EACF;AACA,MAAI,aAAa,eAAgB,QAAO,YAAY,KAAK,MAAM;AAC/D,SAAO,aAAa,QAAQ,KAAK,KAAK,MAAM;AAC9C;AAeO,SAAS,mBAAmB,WAAuC;AACxE,QAAM,UAAU,kCAAkC,SAAS;AAC3D,QAAM,MAAM,WAAW,QAAQ,IAAI;AACnC,MAAI,CAAC,KAAK;AACR;AAAA,MACE,oDACK,SAAS,IAAI,SAAS;AAAA,IAG7B;AACA,WAAO;AAAA,EACT;AACA;AAAA,IACE,UACI,8BAA8B,SAAS,2BACvC;AAAA,EACN;AACA,SAAO;AACT;AAOA,eAAsB,2BACpB,WACuC;AACvC,QAAM,MAAM,mBAAmB,SAAS;AACxC,MAAI,CAAC,IAAK,QAAO;AACjB,SAAO,uBAAuB,GAAG;AACnC;AAQA,eAAsB,8BACpB,WAC0C;AAC1C,QAAM,MAAM,mBAAmB,SAAS;AACxC,MAAI,CAAC,IAAK,QAAO;AACjB,SAAO,0BAA0B,GAAG;AACtC;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import * as pulumi from "@pulumi/pulumi";
|
|
8
8
|
import * as path from "node:path";
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
10
|
-
import { mesh } from "@mesh-tech/app-kit";
|
|
10
|
+
import { mesh } from "@mesh-tech/app-kit/infra";
|
|
11
11
|
|
|
12
12
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
13
13
|
const { tenant, platform, stack, deploy } = mesh.apps.getConfig(pulumi);
|