@hsuite/smart-engines-cli 1.5.1 → 1.6.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.
Files changed (55) hide show
  1. package/README.md +5 -3
  2. package/dist/_lib/baas.d.ts +27 -4
  3. package/dist/_lib/baas.d.ts.map +1 -1
  4. package/dist/_lib/baas.js +76 -42
  5. package/dist/_lib/baas.js.map +1 -1
  6. package/dist/_lib/cluster-convergence.d.ts +214 -0
  7. package/dist/_lib/cluster-convergence.d.ts.map +1 -0
  8. package/dist/_lib/cluster-convergence.js +381 -0
  9. package/dist/_lib/cluster-convergence.js.map +1 -0
  10. package/dist/_lib/deploy-manifest.d.ts +32 -0
  11. package/dist/_lib/deploy-manifest.d.ts.map +1 -1
  12. package/dist/_lib/deploy-manifest.js +71 -0
  13. package/dist/_lib/deploy-manifest.js.map +1 -1
  14. package/dist/_lib/docker.d.ts +35 -23
  15. package/dist/_lib/docker.d.ts.map +1 -1
  16. package/dist/_lib/docker.js +58 -54
  17. package/dist/_lib/docker.js.map +1 -1
  18. package/dist/_lib/frontend-guard.d.ts +59 -0
  19. package/dist/_lib/frontend-guard.d.ts.map +1 -1
  20. package/dist/_lib/frontend-guard.js +148 -0
  21. package/dist/_lib/frontend-guard.js.map +1 -1
  22. package/dist/_lib/registry-fanout.d.ts +299 -9
  23. package/dist/_lib/registry-fanout.d.ts.map +1 -1
  24. package/dist/_lib/registry-fanout.js +557 -27
  25. package/dist/_lib/registry-fanout.js.map +1 -1
  26. package/dist/_lib/subscription-client.d.ts +14 -0
  27. package/dist/_lib/subscription-client.d.ts.map +1 -1
  28. package/dist/_lib/subscription-client.js +13 -0
  29. package/dist/_lib/subscription-client.js.map +1 -1
  30. package/dist/commands/deploy.d.ts +28 -4
  31. package/dist/commands/deploy.d.ts.map +1 -1
  32. package/dist/commands/deploy.js +163 -20
  33. package/dist/commands/deploy.js.map +1 -1
  34. package/dist/commands/redeploy.d.ts +10 -0
  35. package/dist/commands/redeploy.d.ts.map +1 -1
  36. package/dist/commands/redeploy.js +146 -15
  37. package/dist/commands/redeploy.js.map +1 -1
  38. package/dist/commands/status.d.ts +4 -3
  39. package/dist/commands/status.d.ts.map +1 -1
  40. package/dist/commands/status.js +15 -3
  41. package/dist/commands/status.js.map +1 -1
  42. package/dist/commands/topup.d.ts +55 -0
  43. package/dist/commands/topup.d.ts.map +1 -0
  44. package/dist/commands/topup.js +180 -0
  45. package/dist/commands/topup.js.map +1 -0
  46. package/dist/commands/update.d.ts +33 -0
  47. package/dist/commands/update.d.ts.map +1 -1
  48. package/dist/commands/update.js +38 -2
  49. package/dist/commands/update.js.map +1 -1
  50. package/dist/commands/verify.d.ts.map +1 -1
  51. package/dist/commands/verify.js +24 -1
  52. package/dist/commands/verify.js.map +1 -1
  53. package/dist/index.js +2 -0
  54. package/dist/index.js.map +1 -1
  55. package/package.json +1 -1
@@ -0,0 +1,381 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.probeClusterRuntimes = probeClusterRuntimes;
4
+ exports.assertClustersConverged = assertClustersConverged;
5
+ exports.waitForClusterConvergence = waitForClusterConvergence;
6
+ /**
7
+ * Per-cluster post-deploy convergence gate (#2787).
8
+ *
9
+ * ## Why this exists
10
+ *
11
+ * A deploy reaches exactly ONE cluster. `gateway.<domain>` is public-DNS
12
+ * round-robin over the three cluster ingresses, each fronting an INDEPENDENT
13
+ * smart-host + Mongo, and the runtime record is replicated to the peers
14
+ * asynchronously (the validator's `runtime.attached` fan-out). When that
15
+ * replication misses a cluster, nothing notices: the CLI polls the same
16
+ * round-robin name, gets a `RUNNING` from whichever cluster answers, and prints
17
+ * `✓ redeployed`.
18
+ *
19
+ * That is not hypothetical. On testnet 2026-08-22 (#2787) repeated
20
+ * `hsuite redeploy --tag v12` runs converged sn2 and sn3 and never sn1: sn1 kept
21
+ * a build-attestation pinning the PREVIOUS digest, its deployer refused the new
22
+ * image on every reconcile, and it served nothing for that app while the load
23
+ * balancer kept sending it a third of the traffic. Every run reported success.
24
+ *
25
+ * ## How it checks
26
+ *
27
+ * Round-robin polling cannot attribute an answer to a cluster, so we address the
28
+ * clusters directly: resolve the gateway's A-records and dial each ingress IP
29
+ * with TLS SNI + the HTTP `Host` header pinned to the gateway hostname — exactly
30
+ * the transport the registry fan-out already uses for the three Harbors
31
+ * ({@link defaultTransport}). Each cluster then:
32
+ * 1. names itself on the `@Public()` `GET /api/v3/cluster/health` route, and
33
+ * 2. issues its OWN session bearer (the XRPL web3 challenge/verify handshake —
34
+ * a session minted on one cluster is not valid on its peers), which is then
35
+ * used to read that cluster's own `…/deployment/apps/:appId/status`.
36
+ *
37
+ * The result is one observation per cluster, which {@link assertClustersConverged}
38
+ * turns into a hard error naming the cluster and both digests.
39
+ *
40
+ * ## What a PASS here does and does not mean (#2887)
41
+ *
42
+ * A pass means: every cluster the gateway advertises answered, and each one's
43
+ * runtime RECORD names the image ref this run deployed. It does NOT mean the
44
+ * running container's digest was verified — `runtime.image` is the desired-state
45
+ * ref the host attached, and no field on the status read path carries the
46
+ * `@sha256:` the deployer pins at apply time. Anything short of one answer per
47
+ * advertised cluster is an evidence gap and fails loudly rather than passing.
48
+ *
49
+ * Read-only: the probe issues auth handshakes and GETs, never a deploy.
50
+ */
51
+ const registry_fanout_1 = require("./registry-fanout");
52
+ /** The host API is mounted behind this prefix on the gateway (matches `connectAndAuth`). */
53
+ const HOST_PATH_PREFIX = '/host';
54
+ /**
55
+ * Ask EVERY cluster behind the gateway what it has deployed for `appId`.
56
+ *
57
+ * One observation per resolved ingress IP, in endpoint order. A cluster that
58
+ * cannot be reached or authenticated yields `reachable: false` with `error` set
59
+ * — never a throw at the per-cluster level, so {@link assertClustersConverged}
60
+ * can weigh WHICH clusters gave evidence against how many were expected.
61
+ *
62
+ * #2887: a DNS failure is NOT one of those soft outcomes. It used to log
63
+ * `skipping the check` and return `[]`, and an empty observation list then fell
64
+ * straight through the old `observations.length <= 1` short-circuit in
65
+ * `assertClustersConverged` — so "I could not find the clusters" was reported
66
+ * as "the clusters agree". Not being able to enumerate the mesh is a total
67
+ * evidence gap; it throws, and {@link waitForClusterConvergence} gives it the
68
+ * same retry budget as any other transient fault before failing loudly.
69
+ */
70
+ async function probeClusterRuntimes(opts) {
71
+ const host = (0, registry_fanout_1.registryHostOf)(opts.gateway);
72
+ const transport = opts.transport ?? (0, registry_fanout_1.defaultTransport)(host);
73
+ const timeoutMs = opts.timeoutMs ?? registry_fanout_1.DEFAULT_PROBE_TIMEOUT_MS;
74
+ const endpoints = opts.endpoints ?? (await (0, registry_fanout_1.resolveClusterEndpoints)(host, opts.resolve4));
75
+ return Promise.all(endpoints.map((ip) => probeOne(ip, { ...opts, transport, timeoutMs })));
76
+ }
77
+ async function probeOne(ip, opts) {
78
+ const send = (req) => (0, registry_fanout_1.withRequestTimeout)(opts.transport(req), opts.timeoutMs, `${req.method} ${req.path} on ${ip}`);
79
+ // 1. Who is this? Best-effort — a cluster that won't name itself is still
80
+ // worth probing, it just gets reported by IP.
81
+ let clusterId = null;
82
+ try {
83
+ const health = await send({ ip, method: 'GET', path: '/api/v3/cluster/health' });
84
+ if (health.status === 200) {
85
+ const body = parseJson(health);
86
+ const id = body?.clusterId;
87
+ if (typeof id === 'string' && id)
88
+ clusterId = id;
89
+ }
90
+ }
91
+ catch {
92
+ // fall through — the status read below decides reachability
93
+ }
94
+ // 2. This cluster's OWN session. Sessions are per-cluster: a bearer minted on
95
+ // a peer 401s here, which is exactly why the round-robin client cannot be
96
+ // reused for a per-cluster read.
97
+ // Cached across the retry polls of `waitForClusterConvergence`.
98
+ let token = opts.tokenCache?.get(ip);
99
+ // Only a REUSED bearer can have outlived its session; one minted a moment ago
100
+ // 401ing means something else is wrong and re-minting would just repeat it.
101
+ const tokenWasReused = token !== undefined;
102
+ if (!token) {
103
+ try {
104
+ token = await authenticateAgainst(ip, send, opts);
105
+ opts.tokenCache?.set(ip, token);
106
+ }
107
+ catch (err) {
108
+ return { ip, clusterId, reachable: false, error: `auth: ${err.message}` };
109
+ }
110
+ }
111
+ // 3. What does THIS cluster have deployed?
112
+ const statusPath = `${HOST_PATH_PREFIX}/api/v3/baas/deployment/apps/${encodeURIComponent(opts.appId)}/status`;
113
+ const readStatus = (bearer) => send({
114
+ ip,
115
+ method: 'GET',
116
+ path: statusPath,
117
+ headers: { Authorization: `Bearer ${bearer}`, Accept: 'application/json' },
118
+ });
119
+ try {
120
+ let res = await readStatus(token);
121
+ // A cached bearer can outlive its session. Re-mint once rather than let the
122
+ // 401 degrade into "unreachable" — an unreachable cluster is EXCLUDED from
123
+ // the divergence check, so a silently-expired token would weaken the gate.
124
+ if (res.status === 401 && tokenWasReused && opts.tokenCache) {
125
+ opts.tokenCache.delete(ip);
126
+ token = await authenticateAgainst(ip, send, opts);
127
+ opts.tokenCache.set(ip, token);
128
+ res = await readStatus(token);
129
+ }
130
+ if (res.status === 404) {
131
+ return { ip, clusterId, reachable: true, error: 'no runtime record on this cluster' };
132
+ }
133
+ if (res.status !== 200) {
134
+ return { ip, clusterId, reachable: false, error: `status read HTTP ${res.status}` };
135
+ }
136
+ const body = parseJson(res);
137
+ if (!body)
138
+ return { ip, clusterId, reachable: false, error: 'status read: malformed JSON' };
139
+ const obs = { ip, clusterId, reachable: true };
140
+ if (body.state !== undefined)
141
+ obs.state = body.state;
142
+ if (body.runtime?.image !== undefined)
143
+ obs.image = body.runtime.image;
144
+ if (body.runtime?.runtimeState !== undefined)
145
+ obs.runtimeState = body.runtime.runtimeState;
146
+ if (body.runtime?.lastError)
147
+ obs.lastError = body.runtime.lastError;
148
+ return obs;
149
+ }
150
+ catch (err) {
151
+ return { ip, clusterId, reachable: false, error: err.message };
152
+ }
153
+ }
154
+ /** Run the web3 challenge/verify handshake against ONE cluster; returns its bearer. */
155
+ async function authenticateAgainst(ip, send, opts) {
156
+ const challengeRes = await send({
157
+ ip,
158
+ method: 'POST',
159
+ path: `${HOST_PATH_PREFIX}/api/v3/baas/auth/challenge`,
160
+ headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
161
+ body: Buffer.from(JSON.stringify({
162
+ chain: opts.signer.chain,
163
+ walletAddress: opts.signer.walletAddress,
164
+ appId: opts.authAppId,
165
+ })),
166
+ });
167
+ const challenge = parseJson(challengeRes);
168
+ if (challengeRes.status >= 400 || !challenge?.challengeId || !challenge.message) {
169
+ throw new Error(`challenge HTTP ${challengeRes.status}`);
170
+ }
171
+ const signature = await opts.signer.signFn(challenge.message);
172
+ const verifyRes = await send({
173
+ ip,
174
+ method: 'POST',
175
+ path: `${HOST_PATH_PREFIX}/api/v3/baas/auth/verify`,
176
+ headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
177
+ body: Buffer.from(JSON.stringify({
178
+ challengeId: challenge.challengeId,
179
+ signature,
180
+ publicKey: opts.signer.publicKey,
181
+ })),
182
+ });
183
+ const verified = parseJson(verifyRes);
184
+ if (verifyRes.status >= 400 || !verified?.token) {
185
+ throw new Error(`verify HTTP ${verifyRes.status}`);
186
+ }
187
+ return verified.token;
188
+ }
189
+ /**
190
+ * Throw unless EVERY cluster the gateway advertises answered, and every answer
191
+ * is the image this run deployed.
192
+ *
193
+ * Divergence is: a cluster pinned to a different image, a cluster whose runtime
194
+ * is `FAILED`, or a cluster with no runtime record at all. The error names each
195
+ * diverged cluster, what it is stuck on, and the digest this run attested — so
196
+ * the operator sees both sides of the mismatch instead of `✓ deployed`.
197
+ *
198
+ * ## Full evidence is required (#2887)
199
+ *
200
+ * This gate previously excluded unreachable clusters from the comparison and
201
+ * passed on whatever was left, short-circuiting entirely at
202
+ * `observations.length <= 1`. Both rules turned MISSING evidence into a pass:
203
+ * one matching answer out of three clusters read as "converged", and an empty
204
+ * probe (the gateway failing to resolve) read as "converged" too. Measured on
205
+ * testnet 2026-08-27: a deploy printed `✓ deployed` while `kubectl` showed sn1
206
+ * on `app:v33` and sn2 + sn3 still on `app:v32`.
207
+ *
208
+ * "I could not reach 2 of 3 clusters" and "all 3 clusters agree" are different
209
+ * sentences and only one of them is a pass. Transient faults are absorbed by
210
+ * the retry budget in {@link waitForClusterConvergence}, not by lowering the
211
+ * bar here; a mesh that is knowingly unreachable from the developer's network
212
+ * has `--skip-cluster-check`, which at least says so out loud.
213
+ *
214
+ * The one genuine no-op left: `expectedClusters <= 1`. A single-cluster / dev
215
+ * gateway has nothing to diverge from — the same rule the image fan-out
216
+ * applies at `endpoints.length <= 1`.
217
+ *
218
+ * ## What this can and cannot see
219
+ *
220
+ * `observation.image` is the per-cluster runtime RECORD (`runtime.image`,
221
+ * written by the host's `attachRuntime` call and read back through
222
+ * `GET …/deployment/apps/:appId/status`). It is a desired-state tag ref, not
223
+ * the digest of the container actually running: nothing on that read path
224
+ * surfaces the `@sha256:` the deployer pins at apply time. So this gate catches
225
+ * a cluster on the WRONG TAG or a `FAILED` reconcile, and cannot catch a
226
+ * cluster serving a different artifact under the same tag. Closing that needs
227
+ * the deployer's resolved digest surfaced in the runtime projection
228
+ * (smart-deployer → validator `runtime/status` → smart-host → SDK) — tracked
229
+ * separately; do not paper over it here with a comparison that has no digest
230
+ * to compare.
231
+ */
232
+ function assertClustersConverged(args) {
233
+ if (args.expectedClusters <= 1)
234
+ return;
235
+ const reachable = args.observations.filter((o) => o.reachable);
236
+ // MISSING evidence is not evidence of convergence. Anything short of one
237
+ // answer per advertised cluster leaves clusters we know nothing about, and
238
+ // passing on the remainder is exactly the silent success this gate exists to
239
+ // remove.
240
+ if (reachable.length < args.expectedClusters) {
241
+ const silent = args.observations.filter((o) => !o.reachable);
242
+ const missing = args.expectedClusters - args.observations.length;
243
+ // `detail` is never empty here: entering this branch means
244
+ // `reachable.length < expectedClusters`, so either some observation is
245
+ // unreachable (→ a `silent` entry) or there are fewer observations than
246
+ // expected (→ `missing > 0`). No `|| 'no detail'` fallback, which would be
247
+ // a branch that cannot fire — the exact shape this PR removes elsewhere.
248
+ const detail = [
249
+ ...silent.map((o) => `${safe(o.clusterId) ?? o.ip}=${safe(o.error) ?? 'no answer'}`),
250
+ ...(missing > 0 ? [`${missing} endpoint(s) never probed`] : []),
251
+ ];
252
+ throw new Error(`cluster convergence UNVERIFIED for ${args.appId}: only ${reachable.length} of ` +
253
+ `${args.expectedClusters} clusters answered the per-cluster status probe (` +
254
+ detail.join(', ') +
255
+ `). A cluster that gives no answer is one this deploy cannot vouch for — it may be ` +
256
+ `serving an older release while the load balancer keeps routing to it. Refusing to ` +
257
+ `report success without full evidence — re-run, or pass --skip-cluster-check if the ` +
258
+ `mesh is knowingly unreachable from here (#2787/#2887).`);
259
+ }
260
+ const diverged = reachable.filter((o) => o.image !== args.expectedImage || o.runtimeState === 'FAILED');
261
+ if (diverged.length === 0)
262
+ return;
263
+ const detail = diverged
264
+ .map((o) => {
265
+ const who = o.clusterId ? `${safe(o.clusterId)} (${o.ip})` : o.ip;
266
+ const bits = [
267
+ `image=${safe(o.image) ?? '<none>'}`,
268
+ `runtimeState=${safe(o.runtimeState) ?? '<none>'}`,
269
+ ];
270
+ if (o.error)
271
+ bits.push(`probe=${safe(o.error)}`);
272
+ if (o.lastError)
273
+ bits.push(`lastError=${safe(o.lastError)}`);
274
+ return ` - ${who}: ${bits.join(' ')}`;
275
+ })
276
+ .join('\n');
277
+ throw new Error(`cluster divergence after deploying ${args.appId}: ${diverged.length} of ` +
278
+ `${reachable.length} reachable clusters did not converge on ${args.expectedImage} ` +
279
+ `(attested digest ${args.attestedDigest}).\n${detail}\n` +
280
+ ' A cluster that keeps an older runtime record serves the OLD release (or nothing) ' +
281
+ 'while the load balancer keeps routing to it, and it does not self-heal: the clusters ' +
282
+ 'hold independent records and a deploy call reaches only the one the gateway picked. ' +
283
+ 'Re-run the redeploy until this check passes — each run is another draw, and a run ' +
284
+ 'that lands on the stuck cluster overwrites its record (a `build-attestation digest ' +
285
+ 'MISMATCH` above is a STALE attestation on that cluster, not a bad image) (#2787).');
286
+ }
287
+ /**
288
+ * Probe every cluster until they all run the deployed image, or the budget is
289
+ * exhausted — then throw ({@link assertClustersConverged}).
290
+ *
291
+ * The retry budget exists because peer replication is asynchronous: a cluster
292
+ * that simply hasn't caught up yet must not fail a healthy deploy. A cluster
293
+ * that is genuinely STUCK (a stale build-attestation, a crash-looping image)
294
+ * does not converge within any budget, which is the case this gate is for.
295
+ *
296
+ * #2887: the budget now covers the EVIDENCE-gathering failures too — a gateway
297
+ * that won't resolve, or clusters that won't answer. Those used to be reported
298
+ * as a pass; they are retried like any other transient fault and then fail
299
+ * loudly, because a check with no evidence has no verdict to give.
300
+ *
301
+ * The gateway is resolved ONCE and the answer threaded through every poll, so
302
+ * the expected cluster count is fixed for the whole wait. Re-resolving each
303
+ * poll would let a short round-robin answer shrink the expectation to match the
304
+ * clusters that happened to reply — the exact vacuity being removed.
305
+ */
306
+ async function waitForClusterConvergence(opts) {
307
+ const sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
308
+ const interval = opts.intervalMs ?? 5000;
309
+ const maxPolls = opts.maxPolls ?? 6;
310
+ const log = opts.log ?? (() => undefined);
311
+ const host = (0, registry_fanout_1.registryHostOf)(opts.gateway);
312
+ const tokenCache = opts.tokenCache ?? new Map();
313
+ let endpoints = opts.endpoints ?? null;
314
+ for (let poll = 0;; poll++) {
315
+ const last = poll >= maxPolls - 1;
316
+ try {
317
+ if (!endpoints) {
318
+ const resolved = await (0, registry_fanout_1.resolveClusterEndpoints)(host, opts.resolve4);
319
+ // An EMPTY answer is a total evidence gap, not a single-cluster mesh.
320
+ // `expectedClusters: 0` would slip past the `<= 1` no-op below and turn
321
+ // the whole gate off — the one input that must not do that. Real
322
+ // `dns.resolve4` throws ENODATA rather than returning `[]`, but the
323
+ // resolver is an injectable seam and this is the gate's own off-switch.
324
+ if (resolved.length === 0) {
325
+ throw new Error(`cluster convergence UNVERIFIED for ${opts.appId}: ${host} returned no cluster ` +
326
+ 'endpoints to probe, so there is no evidence any cluster runs ' +
327
+ `${opts.expectedImage}. Refusing to report success — re-run, or pass ` +
328
+ '--skip-cluster-check if the mesh is knowingly unreachable from here (#2887).');
329
+ }
330
+ endpoints = resolved;
331
+ }
332
+ const observations = await probeClusterRuntimes({ ...opts, endpoints, tokenCache });
333
+ assertClustersConverged({
334
+ appId: opts.appId,
335
+ expectedImage: opts.expectedImage,
336
+ attestedDigest: opts.attestedDigest,
337
+ expectedClusters: endpoints.length,
338
+ observations,
339
+ });
340
+ return observations;
341
+ }
342
+ catch (err) {
343
+ if (last)
344
+ throw err;
345
+ // Name the actual reason: "not converged yet" and "could not gather the
346
+ // evidence" are different problems and the retry log used to claim the
347
+ // first for both. Through `safe()` — the reason can carry a cluster's own
348
+ // `error` text, and this line prints to the operator's terminal on every
349
+ // poll (same display-safety rule as the assertion messages below).
350
+ const reason = safe(err?.message?.split('\n')[0] ?? String(err));
351
+ log(` converge: ${opts.expectedImage} not confirmed on every cluster yet ` +
352
+ `(attempt ${poll + 1}/${maxPolls}: ${reason ?? 'unknown'}); re-checking in ${interval}ms`);
353
+ await sleep(interval);
354
+ }
355
+ }
356
+ }
357
+ /**
358
+ * Make a server-controlled string safe to print. `clusterId`, `image`,
359
+ * `runtimeState` and `lastError` all come verbatim from a cluster's JSON, and
360
+ * these error messages go straight to the operator's terminal via
361
+ * `console.error(chalk.red(...))` — so a compromised cluster would otherwise
362
+ * inject ANSI/control sequences into the console on every run against it.
363
+ * Strips C0/C1 control characters and bounds the length.
364
+ */
365
+ function safe(v) {
366
+ if (v === undefined || v === null)
367
+ return undefined;
368
+ // eslint-disable-next-line no-control-regex
369
+ const stripped = v.replace(/[\u0000-\u001f\u007f-\u009f]/g, ' ').trim();
370
+ return stripped.length > 400 ? `${stripped.slice(0, 400)}…` : stripped;
371
+ }
372
+ /** Parse a JSON response body; `null` when it isn't JSON. */
373
+ function parseJson(res) {
374
+ try {
375
+ return JSON.parse(res.body.toString('utf8'));
376
+ }
377
+ catch {
378
+ return null;
379
+ }
380
+ }
381
+ //# sourceMappingURL=cluster-convergence.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cluster-convergence.js","sourceRoot":"","sources":["../../src/_lib/cluster-convergence.ts"],"names":[],"mappings":";;AAuIA,oDAYC;AA2LD,0DA0DC;AAkCD,8DAuDC;AAjeD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AACH,uDAS2B;AAE3B,4FAA4F;AAC5F,MAAM,gBAAgB,GAAG,OAAO,CAAC;AA8DjC;;;;;;;;;;;;;;;GAeG;AACI,KAAK,UAAU,oBAAoB,CACxC,IAA8B;IAE9B,MAAM,IAAI,GAAG,IAAA,gCAAc,EAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAC1C,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,IAAI,IAAA,kCAAgB,EAAC,IAAI,CAAC,CAAC;IAC3D,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,IAAI,0CAAwB,CAAC;IAE7D,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,IAAI,CAAC,MAAM,IAAA,yCAAuB,EAAC,IAAI,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC;IAEzF,OAAO,OAAO,CAAC,GAAG,CAChB,SAAS,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,QAAQ,CAAC,EAAE,EAAE,EAAE,GAAG,IAAI,EAAE,SAAS,EAAE,SAAS,EAAE,CAAC,CAAC,CACvE,CAAC;AACJ,CAAC;AAED,KAAK,UAAU,QAAQ,CACrB,EAAU,EACV,IAAoF;IAEpF,MAAM,IAAI,GAAG,CAAC,GAAoB,EAA6B,EAAE,CAC/D,IAAA,oCAAkB,EAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,IAAI,CAAC,SAAS,EAAE,GAAG,GAAG,CAAC,MAAM,IAAI,GAAG,CAAC,IAAI,OAAO,EAAE,EAAE,CAAC,CAAC;IAEhG,0EAA0E;IAC1E,iDAAiD;IACjD,IAAI,SAAS,GAAkB,IAAI,CAAC;IACpC,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,wBAAwB,EAAE,CAAC,CAAC;QACjF,IAAI,MAAM,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;YAC1B,MAAM,IAAI,GAAG,SAAS,CAAC,MAAM,CAAC,CAAC;YAC/B,MAAM,EAAE,GAAI,IAAuC,EAAE,SAAS,CAAC;YAC/D,IAAI,OAAO,EAAE,KAAK,QAAQ,IAAI,EAAE;gBAAE,SAAS,GAAG,EAAE,CAAC;QACnD,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,4DAA4D;IAC9D,CAAC;IAED,8EAA8E;IAC9E,6EAA6E;IAC7E,oCAAoC;IACpC,mEAAmE;IACnE,IAAI,KAAK,GAAG,IAAI,CAAC,UAAU,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC;IACrC,8EAA8E;IAC9E,4EAA4E;IAC5E,MAAM,cAAc,GAAG,KAAK,KAAK,SAAS,CAAC;IAC3C,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,IAAI,CAAC;YACH,KAAK,GAAG,MAAM,mBAAmB,CAAC,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;YAClD,IAAI,CAAC,UAAU,EAAE,GAAG,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC;QAClC,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO,EAAE,EAAE,EAAE,SAAS,EAAE,SAAS,EAAE,KAAK,EAAE,KAAK,EAAE,SAAU,GAAa,CAAC,OAAO,EAAE,EAAE,CAAC;QACvF,CAAC;IACH,CAAC;IAED,2CAA2C;IAC3C,MAAM,UAAU,GAAG,GAAG,gBAAgB,gCAAgC,kBAAkB,CAAC,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC;IAC9G,MAAM,UAAU,GAAG,CAAC,MAAc,EAA6B,EAAE,CAC/D,IAAI,CAAC;QACH,EAAE;QACF,MAAM,EAAE,KAAK;QACb,IAAI,EAAE,UAAU;QAChB,OAAO,EAAE,EAAE,aAAa,EAAE,UAAU,MAAM,EAAE,EAAE,MAAM,EAAE,kBAAkB,EAAE;KAC3E,CAAC,CAAC;IACL,IAAI,CAAC;QACH,IAAI,GAAG,GAAG,MAAM,UAAU,CAAC,KAAK,CAAC,CAAC;QAClC,4EAA4E;QAC5E,2EAA2E;QAC3E,2EAA2E;QAC3E,IAAI,GAAG,CAAC,MAAM,KAAK,GAAG,IAAI,cAAc,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;YAC5D,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;YAC3B,KAAK,GAAG,MAAM,mBAAmB,CAAC,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;YAClD,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC;YAC/B,GAAG,GAAG,MAAM,UAAU,CAAC,KAAK,CAAC,CAAC;QAChC,CAAC;QACD,IAAI,GAAG,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;YACvB,OAAO,EAAE,EAAE,EAAE,SAAS,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,mCAAmC,EAAE,CAAC;QACxF,CAAC;QACD,IAAI,GAAG,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;YACvB,OAAO,EAAE,EAAE,EAAE,SAAS,EAAE,SAAS,EAAE,KAAK,EAAE,KAAK,EAAE,oBAAoB,GAAG,CAAC,MAAM,EAAE,EAAE,CAAC;QACtF,CAAC;QACD,MAAM,IAAI,GAAG,SAAS,CAAC,GAAG,CAGlB,CAAC;QACT,IAAI,CAAC,IAAI;YAAE,OAAO,EAAE,EAAE,EAAE,SAAS,EAAE,SAAS,EAAE,KAAK,EAAE,KAAK,EAAE,6BAA6B,EAAE,CAAC;QAC5F,MAAM,GAAG,GAA8B,EAAE,EAAE,EAAE,SAAS,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC;QAC1E,IAAI,IAAI,CAAC,KAAK,KAAK,SAAS;YAAE,GAAG,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC;QACrD,IAAI,IAAI,CAAC,OAAO,EAAE,KAAK,KAAK,SAAS;YAAE,GAAG,CAAC,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC;QACtE,IAAI,IAAI,CAAC,OAAO,EAAE,YAAY,KAAK,SAAS;YAAE,GAAG,CAAC,YAAY,GAAG,IAAI,CAAC,OAAO,CAAC,YAAY,CAAC;QAC3F,IAAI,IAAI,CAAC,OAAO,EAAE,SAAS;YAAE,GAAG,CAAC,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC;QACpE,OAAO,GAAG,CAAC;IACb,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,EAAE,EAAE,EAAE,SAAS,EAAE,SAAS,EAAE,KAAK,EAAE,KAAK,EAAG,GAAa,CAAC,OAAO,EAAE,CAAC;IAC5E,CAAC;AACH,CAAC;AAED,uFAAuF;AACvF,KAAK,UAAU,mBAAmB,CAChC,EAAU,EACV,IAAyD,EACzD,IAA8B;IAE9B,MAAM,YAAY,GAAG,MAAM,IAAI,CAAC;QAC9B,EAAE;QACF,MAAM,EAAE,MAAM;QACd,IAAI,EAAE,GAAG,gBAAgB,6BAA6B;QACtD,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,EAAE,kBAAkB,EAAE;QAC3E,IAAI,EAAE,MAAM,CAAC,IAAI,CACf,IAAI,CAAC,SAAS,CAAC;YACb,KAAK,EAAE,IAAI,CAAC,MAAM,CAAC,KAAK;YACxB,aAAa,EAAE,IAAI,CAAC,MAAM,CAAC,aAAa;YACxC,KAAK,EAAE,IAAI,CAAC,SAAS;SACtB,CAAC,CACH;KACF,CAAC,CAAC;IACH,MAAM,SAAS,GAAG,SAAS,CAAC,YAAY,CAAsD,CAAC;IAC/F,IAAI,YAAY,CAAC,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,EAAE,WAAW,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,CAAC;QAChF,MAAM,IAAI,KAAK,CAAC,kBAAkB,YAAY,CAAC,MAAM,EAAE,CAAC,CAAC;IAC3D,CAAC;IACD,MAAM,SAAS,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;IAC9D,MAAM,SAAS,GAAG,MAAM,IAAI,CAAC;QAC3B,EAAE;QACF,MAAM,EAAE,MAAM;QACd,IAAI,EAAE,GAAG,gBAAgB,0BAA0B;QACnD,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,EAAE,kBAAkB,EAAE;QAC3E,IAAI,EAAE,MAAM,CAAC,IAAI,CACf,IAAI,CAAC,SAAS,CAAC;YACb,WAAW,EAAE,SAAS,CAAC,WAAW;YAClC,SAAS;YACT,SAAS,EAAE,IAAI,CAAC,MAAM,CAAC,SAAS;SACjC,CAAC,CACH;KACF,CAAC,CAAC;IACH,MAAM,QAAQ,GAAG,SAAS,CAAC,SAAS,CAA8B,CAAC;IACnE,IAAI,SAAS,CAAC,MAAM,IAAI,GAAG,IAAI,CAAC,QAAQ,EAAE,KAAK,EAAE,CAAC;QAChD,MAAM,IAAI,KAAK,CAAC,eAAe,SAAS,CAAC,MAAM,EAAE,CAAC,CAAC;IACrD,CAAC;IACD,OAAO,QAAQ,CAAC,KAAK,CAAC;AACxB,CAAC;AAoBD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AACH,SAAgB,uBAAuB,CAAC,IAAyB;IAC/D,IAAI,IAAI,CAAC,gBAAgB,IAAI,CAAC;QAAE,OAAO;IACvC,MAAM,SAAS,GAAG,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;IAC/D,yEAAyE;IACzE,2EAA2E;IAC3E,6EAA6E;IAC7E,UAAU;IACV,IAAI,SAAS,CAAC,MAAM,GAAG,IAAI,CAAC,gBAAgB,EAAE,CAAC;QAC7C,MAAM,MAAM,GAAG,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;QAC7D,MAAM,OAAO,GAAG,IAAI,CAAC,gBAAgB,GAAG,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC;QACjE,2DAA2D;QAC3D,uEAAuE;QACvE,wEAAwE;QACxE,2EAA2E;QAC3E,yEAAyE;QACzE,MAAM,MAAM,GAAG;YACb,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,EAAE,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,WAAW,EAAE,CAAC;YACpF,GAAG,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,OAAO,2BAA2B,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;SAChE,CAAC;QACF,MAAM,IAAI,KAAK,CACb,sCAAsC,IAAI,CAAC,KAAK,UAAU,SAAS,CAAC,MAAM,MAAM;YAC9E,GAAG,IAAI,CAAC,gBAAgB,mDAAmD;YAC3E,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC;YACjB,oFAAoF;YACpF,oFAAoF;YACpF,qFAAqF;YACrF,wDAAwD,CAC3D,CAAC;IACJ,CAAC;IACD,MAAM,QAAQ,GAAG,SAAS,CAAC,MAAM,CAC/B,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,IAAI,CAAC,aAAa,IAAI,CAAC,CAAC,YAAY,KAAK,QAAQ,CACrE,CAAC;IACF,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO;IAElC,MAAM,MAAM,GAAG,QAAQ;SACpB,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;QACT,MAAM,GAAG,GAAG,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAClE,MAAM,IAAI,GAAG;YACX,SAAS,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,QAAQ,EAAE;YACpC,gBAAgB,IAAI,CAAC,CAAC,CAAC,YAAY,CAAC,IAAI,QAAQ,EAAE;SACnD,CAAC;QACF,IAAI,CAAC,CAAC,KAAK;YAAE,IAAI,CAAC,IAAI,CAAC,SAAS,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QACjD,IAAI,CAAC,CAAC,SAAS;YAAE,IAAI,CAAC,IAAI,CAAC,aAAa,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC;QAC7D,OAAO,SAAS,GAAG,KAAK,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;IAC3C,CAAC,CAAC;SACD,IAAI,CAAC,IAAI,CAAC,CAAC;IAEd,MAAM,IAAI,KAAK,CACb,sCAAsC,IAAI,CAAC,KAAK,KAAK,QAAQ,CAAC,MAAM,MAAM;QACxE,GAAG,SAAS,CAAC,MAAM,2CAA2C,IAAI,CAAC,aAAa,GAAG;QACnF,oBAAoB,IAAI,CAAC,cAAc,OAAO,MAAM,IAAI;QACxD,qFAAqF;QACrF,uFAAuF;QACvF,sFAAsF;QACtF,oFAAoF;QACpF,qFAAqF;QACrF,mFAAmF,CACtF,CAAC;AACJ,CAAC;AAeD;;;;;;;;;;;;;;;;;;GAkBG;AACI,KAAK,UAAU,yBAAyB,CAC7C,IAAmC;IAEnC,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,CAAC,CAAC,EAAU,EAAE,EAAE,CAAC,IAAI,OAAO,CAAO,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC;IAC1F,MAAM,QAAQ,GAAG,IAAI,CAAC,UAAU,IAAI,IAAI,CAAC;IACzC,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,IAAI,CAAC,CAAC;IACpC,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;IAC1C,MAAM,IAAI,GAAG,IAAA,gCAAc,EAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAE1C,MAAM,UAAU,GAAG,IAAI,CAAC,UAAU,IAAI,IAAI,GAAG,EAAkB,CAAC;IAChE,IAAI,SAAS,GAAoB,IAAI,CAAC,SAAS,IAAI,IAAI,CAAC;IACxD,KAAK,IAAI,IAAI,GAAG,CAAC,GAAI,IAAI,EAAE,EAAE,CAAC;QAC5B,MAAM,IAAI,GAAG,IAAI,IAAI,QAAQ,GAAG,CAAC,CAAC;QAClC,IAAI,CAAC;YACH,IAAI,CAAC,SAAS,EAAE,CAAC;gBACf,MAAM,QAAQ,GAAG,MAAM,IAAA,yCAAuB,EAAC,IAAI,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC;gBACpE,sEAAsE;gBACtE,wEAAwE;gBACxE,iEAAiE;gBACjE,oEAAoE;gBACpE,wEAAwE;gBACxE,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;oBAC1B,MAAM,IAAI,KAAK,CACb,sCAAsC,IAAI,CAAC,KAAK,KAAK,IAAI,uBAAuB;wBAC9E,+DAA+D;wBAC/D,GAAG,IAAI,CAAC,aAAa,iDAAiD;wBACtE,8EAA8E,CACjF,CAAC;gBACJ,CAAC;gBACD,SAAS,GAAG,QAAQ,CAAC;YACvB,CAAC;YACD,MAAM,YAAY,GAAG,MAAM,oBAAoB,CAAC,EAAE,GAAG,IAAI,EAAE,SAAS,EAAE,UAAU,EAAE,CAAC,CAAC;YACpF,uBAAuB,CAAC;gBACtB,KAAK,EAAE,IAAI,CAAC,KAAK;gBACjB,aAAa,EAAE,IAAI,CAAC,aAAa;gBACjC,cAAc,EAAE,IAAI,CAAC,cAAc;gBACnC,gBAAgB,EAAE,SAAS,CAAC,MAAM;gBAClC,YAAY;aACb,CAAC,CAAC;YACH,OAAO,YAAY,CAAC;QACtB,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,IAAI;gBAAE,MAAM,GAAG,CAAC;YACpB,wEAAwE;YACxE,uEAAuE;YACvE,0EAA0E;YAC1E,yEAAyE;YACzE,mEAAmE;YACnE,MAAM,MAAM,GAAG,IAAI,CAAE,GAAa,EAAE,OAAO,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;YAC5E,GAAG,CACD,eAAe,IAAI,CAAC,aAAa,sCAAsC;gBACrE,YAAY,IAAI,GAAG,CAAC,IAAI,QAAQ,KAAK,MAAM,IAAI,SAAS,qBAAqB,QAAQ,IAAI,CAC5F,CAAC;YACF,MAAM,KAAK,CAAC,QAAQ,CAAC,CAAC;QACxB,CAAC;IACH,CAAC;AACH,CAAC;AAED;;;;;;;GAOG;AACH,SAAS,IAAI,CAAC,CAA4B;IACxC,IAAI,CAAC,KAAK,SAAS,IAAI,CAAC,KAAK,IAAI;QAAE,OAAO,SAAS,CAAC;IACpD,4CAA4C;IAC5C,MAAM,QAAQ,GAAG,CAAC,CAAC,OAAO,CAAC,+BAA+B,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;IACxE,OAAO,QAAQ,CAAC,MAAM,GAAG,GAAG,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC;AACzE,CAAC;AAED,6DAA6D;AAC7D,SAAS,SAAS,CAAC,GAAqB;IACtC,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC;IAC/C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC"}
@@ -1,4 +1,19 @@
1
1
  type BaasService = 'auth' | 'database' | 'storage' | 'functions' | 'messaging';
2
+ /**
3
+ * One external destination the smart-app declares it needs to reach (#2912).
4
+ *
5
+ * Tenant egress is default-deny: a pod that declares nothing can reach DNS and
6
+ * the platform BaaS endpoint, and nothing else. Declaring a destination is a
7
+ * REQUEST, not a grant — the cluster operator's destination allow-list decides
8
+ * whether it is honored, and a destination outside that list is dropped at
9
+ * reconcile time (visible in the tenant NetworkPolicy's annotations).
10
+ */
11
+ export type ManifestEgress = {
12
+ /** Bare lowercase DNS hostname — no scheme, port, path or wildcard. */
13
+ host: string;
14
+ /** TCP port on `host`. Defaults to 443. */
15
+ port?: number;
16
+ };
2
17
  /** Backend build + push + deploy inputs. REQUIRED — every smart-app has one. */
3
18
  export type ManifestBackend = {
4
19
  /** Backend listen port inside the customer container (1..65535, never 8080). */
@@ -26,6 +41,13 @@ export type ManifestBackend = {
26
41
  cpu?: string;
27
42
  memory?: string;
28
43
  };
44
+ /**
45
+ * External hosts this app needs to reach (#2912). Optional; omitting it
46
+ * leaves whatever the app previously declared in place, so a redeploy from
47
+ * a manifest that predates this field never revokes working egress. To
48
+ * revoke, declare an explicit empty array.
49
+ */
50
+ egress?: ManifestEgress[];
29
51
  };
30
52
  /**
31
53
  * Frontend tar + upload inputs. OPTIONAL — present iff the smart-app ships a
@@ -65,5 +87,15 @@ export type Manifest = {
65
87
  export declare function parseManifest(path: string): Manifest;
66
88
  /** Validate an already-parsed manifest object. Shared by file + programmatic callers. */
67
89
  export declare function validateManifest(raw: unknown): Manifest;
90
+ /**
91
+ * Resolve `manifest.backend.egress[]` into the wire shape the deploy API takes
92
+ * — every entry's port made explicit. Returns `undefined` when the manifest
93
+ * declares nothing, which the server reads as "leave the stored declaration
94
+ * alone" (as opposed to `[]`, which clears it).
95
+ */
96
+ export declare function resolveEgress(manifest: Manifest): Array<{
97
+ host: string;
98
+ port: number;
99
+ }> | undefined;
68
100
  export {};
69
101
  //# sourceMappingURL=deploy-manifest.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"deploy-manifest.d.ts","sourceRoot":"","sources":["../../src/_lib/deploy-manifest.ts"],"names":[],"mappings":"AAmBA,KAAK,WAAW,GAAG,MAAM,GAAG,UAAU,GAAG,SAAS,GAAG,WAAW,GAAG,WAAW,CAAC;AAK/E,gFAAgF;AAChF,MAAM,MAAM,eAAe,GAAG;IAC5B,gFAAgF;IAChF,IAAI,EAAE,MAAM,CAAC;IACb,8DAA8D;IAC9D,GAAG,EAAE,MAAM,CAAC;IACZ,kDAAkD;IAClD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,mDAAmD;IACnD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,qEAAqE;IACrE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,yEAAyE;IACzE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,0EAA0E;IAC1E,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B,8EAA8E;IAC9E,MAAM,CAAC,EAAE;QAAE,GAAG,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CAC5C,CAAC;AAEF;;;;;GAKG;AACH,MAAM,MAAM,gBAAgB,GAAG;IAC7B,6EAA6E;IAC7E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,6EAA6E;IAC7E,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,oEAAoE;IACpE,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B,CAAC;AAEF;;;;;GAKG;AACH,MAAM,MAAM,QAAQ,GAAG;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,iFAAiF;IACjF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,eAAe,CAAC;IACzB,QAAQ,CAAC,EAAE,gBAAgB,CAAC;CAC7B,CAAC;AAEF;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,CAYpD;AAED,yFAAyF;AACzF,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,OAAO,GAAG,QAAQ,CA4CvD"}
1
+ {"version":3,"file":"deploy-manifest.d.ts","sourceRoot":"","sources":["../../src/_lib/deploy-manifest.ts"],"names":[],"mappings":"AAmBA,KAAK,WAAW,GAAG,MAAM,GAAG,UAAU,GAAG,SAAS,GAAG,WAAW,GAAG,WAAW,CAAC;AAyB/E;;;;;;;;GAQG;AACH,MAAM,MAAM,cAAc,GAAG;IAC3B,uEAAuE;IACvE,IAAI,EAAE,MAAM,CAAC;IACb,2CAA2C;IAC3C,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,gFAAgF;AAChF,MAAM,MAAM,eAAe,GAAG;IAC5B,gFAAgF;IAChF,IAAI,EAAE,MAAM,CAAC;IACb,8DAA8D;IAC9D,GAAG,EAAE,MAAM,CAAC;IACZ,kDAAkD;IAClD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,mDAAmD;IACnD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,qEAAqE;IACrE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,yEAAyE;IACzE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,0EAA0E;IAC1E,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B,8EAA8E;IAC9E,MAAM,CAAC,EAAE;QAAE,GAAG,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC3C;;;;;OAKG;IACH,MAAM,CAAC,EAAE,cAAc,EAAE,CAAC;CAC3B,CAAC;AAEF;;;;;GAKG;AACH,MAAM,MAAM,gBAAgB,GAAG;IAC7B,6EAA6E;IAC7E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,6EAA6E;IAC7E,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,oEAAoE;IACpE,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B,CAAC;AAEF;;;;;GAKG;AACH,MAAM,MAAM,QAAQ,GAAG;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,iFAAiF;IACjF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,eAAe,CAAC;IACzB,QAAQ,CAAC,EAAE,gBAAgB,CAAC;CAC7B,CAAC;AAEF;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,CAYpD;AAED,yFAAyF;AACzF,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,OAAO,GAAG,QAAQ,CA8CvD;AA+CD;;;;;GAKG;AACH,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,QAAQ,GACjB,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,GAAG,SAAS,CAInD"}
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.parseManifest = parseManifest;
4
4
  exports.validateManifest = validateManifest;
5
+ exports.resolveEgress = resolveEgress;
5
6
  /**
6
7
  * Shared smart-app deploy manifest schema + loader.
7
8
  *
@@ -22,6 +23,22 @@ const fs_1 = require("fs");
22
23
  const path_1 = require("path");
23
24
  /** Lowest decentralization degree the host accepts (genesis cluster floor). */
24
25
  const MIN_DEGREE = 3;
26
+ /**
27
+ * Default TCP port for a declared egress destination when the manifest entry
28
+ * omits one. HTTPS is what a smart-app dials in practice; anything else has to
29
+ * be stated.
30
+ */
31
+ const DEFAULT_EGRESS_PORT = 443;
32
+ /**
33
+ * Mirror of `DeployAppDto`'s hostname rule (smart-host,
34
+ * `apps/smart-host/src/deployment/dto/deploy-app.dto.ts`) — SSoT lives there;
35
+ * this copy exists so a typo fails at `hsuite deploy` with a message naming
36
+ * the manifest field, not as an opaque server 400 halfway through a build.
37
+ * Keep the two in step, exactly as `MIN_DEGREE` mirrors the DTO's degree floor.
38
+ */
39
+ const EGRESS_HOST_REGEX = /^(?=.{1,253}$)(?!\d+\.\d+\.\d+\.\d+$)[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)+$/;
40
+ /** Largest tier's destination cap — the DTO's schema-level ceiling. */
41
+ const MAX_EGRESS_ENTRIES = 50;
25
42
  /**
26
43
  * Parse + validate a deploy manifest from a JSON file.
27
44
  *
@@ -82,6 +99,60 @@ function validateManifest(raw) {
82
99
  if (frontend && !frontend.dir && !frontend.bundlePath) {
83
100
  throw new Error('manifest.frontend requires `dir` or `bundlePath`');
84
101
  }
102
+ validateEgress(backend.egress);
85
103
  return raw;
86
104
  }
105
+ /**
106
+ * Validate `manifest.backend.egress[]` (#2912).
107
+ *
108
+ * Fails on the shapes a developer actually types — a URL, a `host:port`
109
+ * string, a wildcard, an IP literal — with a message that names the entry, so
110
+ * the mistake surfaces before the docker build rather than as a server 400
111
+ * after it. `undefined` is valid and means "don't touch the declaration".
112
+ */
113
+ function validateEgress(value) {
114
+ if (value === undefined)
115
+ return;
116
+ if (!Array.isArray(value)) {
117
+ throw new Error('manifest.backend.egress must be an array when present');
118
+ }
119
+ if (value.length > MAX_EGRESS_ENTRIES) {
120
+ throw new Error(`manifest.backend.egress has ${value.length} entries; at most ${MAX_EGRESS_ENTRIES} ` +
121
+ `are accepted (your subscription tier may allow fewer)`);
122
+ }
123
+ const seen = new Set();
124
+ for (const [i, entry] of value.entries()) {
125
+ const at = `manifest.backend.egress[${i}]`;
126
+ if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) {
127
+ throw new Error(`${at} must be an object like { "host": "api.example.com", "port": 443 }`);
128
+ }
129
+ const { host, port } = entry;
130
+ if (typeof host !== 'string' || !EGRESS_HOST_REGEX.test(host)) {
131
+ throw new Error(`${at}.host must be a bare lowercase DNS hostname (e.g. "api.example.com") — ` +
132
+ `no scheme, port, path or wildcard; got ${JSON.stringify(host)}`);
133
+ }
134
+ if (port !== undefined) {
135
+ if (typeof port !== 'number' || !Number.isInteger(port) || port < 1 || port > 65535) {
136
+ throw new Error(`${at}.port must be an integer 1-65535; got ${JSON.stringify(port)}`);
137
+ }
138
+ }
139
+ const key = `${host}:${port ?? DEFAULT_EGRESS_PORT}`;
140
+ if (seen.has(key)) {
141
+ throw new Error(`${at} duplicates an earlier destination (${key})`);
142
+ }
143
+ seen.add(key);
144
+ }
145
+ }
146
+ /**
147
+ * Resolve `manifest.backend.egress[]` into the wire shape the deploy API takes
148
+ * — every entry's port made explicit. Returns `undefined` when the manifest
149
+ * declares nothing, which the server reads as "leave the stored declaration
150
+ * alone" (as opposed to `[]`, which clears it).
151
+ */
152
+ function resolveEgress(manifest) {
153
+ const declared = manifest.backend.egress;
154
+ if (declared === undefined)
155
+ return undefined;
156
+ return declared.map((e) => ({ host: e.host, port: e.port ?? DEFAULT_EGRESS_PORT }));
157
+ }
87
158
  //# sourceMappingURL=deploy-manifest.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"deploy-manifest.js","sourceRoot":"","sources":["../../src/_lib/deploy-manifest.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"deploy-manifest.js","sourceRoot":"","sources":["../../src/_lib/deploy-manifest.ts"],"names":[],"mappings":"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"}
@@ -46,37 +46,49 @@ export declare function dockerLogin(registry: RegistryCreds, spawnFn?: typeof sp
46
46
  */
47
47
  export declare function parsePushDigest(output: string): string | null;
48
48
  /**
49
- * Pick the `sha256:<64hex>` digest for `<server>/<repository>` out of a parsed
50
- * `docker inspect ... .RepoDigests` array. Pure shared by both the
51
- * `spawn`-based {@link inspectImageDigest} and the `execFileSync`-based
52
- * `buildAndPush` inspect-fallback in `_lib/baas.ts`, so the matching rule lives
53
- * in exactly one place.
49
+ * Parse the top-level manifest digest from `docker buildx imagetools inspect
50
+ * <ref>` output. `imagetools inspect` queries the REGISTRY (not the local
51
+ * daemon cache), so this is the authoritative `sha256:<64hex>` the registry
52
+ * serves for the tag the value the attestation must bind.
54
53
  *
55
- * `stdout` is the raw `{{json .RepoDigests}}` output (a JSON string array).
56
- * Returns the lower-cased `sha256:<64hex>` of the first entry whose ref matches
57
- * `<server>/<repository>@`, or `null` if the JSON is unparseable / no entry
58
- * matches.
54
+ * The default (non-`--raw`) report prints a top-level block:
55
+ * `Name: <ref>`
56
+ * `MediaType: application/vnd.docker.distribution.manifest.v2+json`
57
+ * `Digest: sha256:<64hex>`
58
+ * We match the FIRST `Digest:` line — the top-level manifest digest. (Per-arch
59
+ * child digests appear only under a later `Manifests:` block for a multi-arch
60
+ * index, which the single-arch amd64 deploy flow never produces.)
61
+ *
62
+ * @returns the lower-cased `sha256:<64hex>` digest, or `null` if absent.
59
63
  */
60
- export declare function parseRepoDigests(stdout: string, registry: RegistryCreds): string | null;
64
+ export declare function parseImagetoolsDigest(output: string): string | null;
61
65
  /**
62
- * `docker inspect` fallback to recover a pushed image's registry digest from
63
- * its `RepoDigests` when the push output didn't surface one (older docker,
64
- * buildx drivers, or an externally-run push we're inspecting after the fact).
66
+ * Resolve the digest the REGISTRY currently serves for `<server>/<repository>:
67
+ * <tag>` via `docker buildx imagetools inspect`. Authoritative: it hits the
68
+ * registry, so unlike a local `docker inspect ... .RepoDigests` read — a
69
+ * prior push of the same tag or a round-robin retry can never leave a STALE
70
+ * value here (#1679).
65
71
  *
66
- * Returns the `sha256:<64hex>` portion of the matching `RepoDigests` entry, or
67
- * `null` if none matches `<server>/<repository>`.
72
+ * @returns the `sha256:<64hex>` registry digest, or `null` if it could not be
73
+ * resolved (imagetools unavailable / registry unreachable / no digest line).
68
74
  */
69
- export declare function inspectImageDigest(registry: RegistryCreds, tag: string, spawnFn?: typeof spawn): Promise<string | null>;
75
+ export declare function inspectRegistryDigest(registry: RegistryCreds, tag: string, spawnFn?: typeof spawn): Promise<string | null>;
70
76
  /**
71
- * Push the backend image and RECORD its registry manifest digest.
77
+ * Push the backend image and RECORD the digest the registry serves for it.
72
78
  *
73
- * Runs `docker push <server>/<repository>:<tag>`, parses the digest from the
74
- * push summary, and falls back to {@link inspectImageDigest} if the summary
75
- * didn't carry one. Throws if the push fails OR if no digest can be recovered
76
- * (a deploy must never proceed with an unverifiable artifact reference).
79
+ * Runs `docker push <server>/<repository>:<tag>`, then resolves the pushed
80
+ * tag's digest from the REGISTRY via {@link inspectRegistryDigest} (NOT the
81
+ * local daemon cache). The `digest: sha256:...` line from the push summary,
82
+ * when present, is cross-checked against the registry digest and a MISMATCH is
83
+ * a hard error — that is the #1679 failure mode (a DNS-round-robin split /
84
+ * same-tag re-push surfacing a stale local digest that disagrees with what
85
+ * Harbor actually stored). Throws if the push fails, if the registry digest
86
+ * can't be resolved, or on a push/registry mismatch — a deploy must never
87
+ * proceed with an unverifiable or stale artifact reference.
77
88
  *
78
- * @returns the `sha256:<64hex>` manifest digest of the pushed tag.
79
- * @throws if push exits non-zero, or if no manifest digest can be determined.
89
+ * @returns the `sha256:<64hex>` registry manifest digest of the pushed tag.
90
+ * @throws if push exits non-zero, the registry digest is unresolvable, or the
91
+ * push-summary digest disagrees with the registry.
80
92
  */
81
93
  export declare function dockerPush(registry: RegistryCreds, tag: string, spawnFn?: typeof spawn): Promise<string>;
82
94
  //# sourceMappingURL=docker.d.ts.map