@hsuite/smart-engines-cli 1.5.1 → 1.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/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/docker.d.ts +35 -23
  11. package/dist/_lib/docker.d.ts.map +1 -1
  12. package/dist/_lib/docker.js +58 -54
  13. package/dist/_lib/docker.js.map +1 -1
  14. package/dist/_lib/frontend-guard.d.ts +59 -0
  15. package/dist/_lib/frontend-guard.d.ts.map +1 -1
  16. package/dist/_lib/frontend-guard.js +148 -0
  17. package/dist/_lib/frontend-guard.js.map +1 -1
  18. package/dist/_lib/registry-fanout.d.ts +299 -9
  19. package/dist/_lib/registry-fanout.d.ts.map +1 -1
  20. package/dist/_lib/registry-fanout.js +557 -27
  21. package/dist/_lib/registry-fanout.js.map +1 -1
  22. package/dist/_lib/subscription-client.d.ts +14 -0
  23. package/dist/_lib/subscription-client.d.ts.map +1 -1
  24. package/dist/_lib/subscription-client.js +13 -0
  25. package/dist/_lib/subscription-client.js.map +1 -1
  26. package/dist/commands/deploy.d.ts +28 -4
  27. package/dist/commands/deploy.d.ts.map +1 -1
  28. package/dist/commands/deploy.js +157 -20
  29. package/dist/commands/deploy.js.map +1 -1
  30. package/dist/commands/redeploy.d.ts +10 -0
  31. package/dist/commands/redeploy.d.ts.map +1 -1
  32. package/dist/commands/redeploy.js +140 -15
  33. package/dist/commands/redeploy.js.map +1 -1
  34. package/dist/commands/topup.d.ts +55 -0
  35. package/dist/commands/topup.d.ts.map +1 -0
  36. package/dist/commands/topup.js +180 -0
  37. package/dist/commands/topup.js.map +1 -0
  38. package/dist/commands/verify.d.ts.map +1 -1
  39. package/dist/commands/verify.js +24 -1
  40. package/dist/commands/verify.js.map +1 -1
  41. package/dist/index.js +2 -0
  42. package/dist/index.js.map +1 -1
  43. package/package.json +1 -1
package/README.md CHANGED
@@ -278,9 +278,11 @@ authenticates as that id and fetches push creds for it (`pushCredentials`,
278
278
  idempotent).
279
279
 
280
280
  Flow: resolve `SUBSCRIPTION_APP_ID` → `connectAndAuth` → `pushCredentials` →
281
- docker build+push (backend) **fan the image out to every cluster Harbor**
282
- (verify + abort on any miss) → tar + `uploadFrontend` (frontend) sign a build
283
- attestation`deploy()`. Writes `DEPLOYED_APP_ID` to the env file.
281
+ tar + `uploadFrontend` (frontend, when present — **before** the backend image
282
+ build, since the backend `Dockerfile` `COPY`s the frontend hook's gitignored
283
+ output, #2788) docker build+push (backend) **fan the image out to every
284
+ cluster Harbor** (verify + abort on any miss) → sign a build attestation →
285
+ `deploy()`. Writes `DEPLOYED_APP_ID` to the env file.
284
286
 
285
287
  ```bash
286
288
  hsuite deploy --manifest ./my-app.json --env .env.local
@@ -90,6 +90,26 @@ export type BuildAndPushOpts = {
90
90
  runFn?: RunFn;
91
91
  /** Push retry attempts (round-robin robot-auth can need a couple). Default 6. */
92
92
  pushAttempts?: number;
93
+ /**
94
+ * SINGLE-ORIGIN registry host to aim the `tag` → `login` → `push` →
95
+ * `imagetools inspect` sequence at, instead of `registry.server` (#1679).
96
+ *
97
+ * `registry.server` is the round-robin name over three INDEPENDENT Harbors; a
98
+ * chunked blob upload that hops clusters mid-session dies (`blob upload
99
+ * invalid`), because the uploaded bytes live on the originating cluster's own
100
+ * RWO PVC. Resolve this with `resolvePushOrigin` (`_lib/registry-fanout.ts`),
101
+ * which asks the mesh for the name it publishes for itself.
102
+ *
103
+ * LOAD-BEARING: this pins the DOCKER SEQUENCE ONLY. `registry.server` is
104
+ * never rewritten — `resolveAttestationDigest` / `fanOutPushedImage` derive
105
+ * their per-cluster target list from it, and a collapsed single-origin value
106
+ * makes the fan-out resolve one endpoint and SILENTLY skip
107
+ * (`endpoints.length <= 1`), leaving two clusters unable to pull the image.
108
+ *
109
+ * Ignored when `skipPush` is set: nothing is uploaded, so the bytes are
110
+ * wherever a prior push left them and any cluster may answer the digest read.
111
+ */
112
+ pushHost?: string;
93
113
  };
94
114
  /**
95
115
  * The fully-qualified target image reference: `<server>/<repository>:<tag>`.
@@ -114,10 +134,13 @@ export declare function imageTarget(registry: RegistryCreds, tag: string): strin
114
134
  * ref. The build attestation's `imageDigest` must carry this digest: the host
115
135
  * binds it to the digest Harbor serves for the tag with strict equality, so a
116
136
  * tag-form ref is rejected once `requireSignature` is enforced. The digest is
117
- * read from the `docker push` summary ({@link parsePushDigest}); if the summary
118
- * lacked one (older docker / buildx drivers / `--skip-push`), it falls back to a
119
- * `docker inspect ... .RepoDigests` parse ({@link parseRepoDigests}). Throws if
120
- * no digest can be recovered a deploy must never proceed with an unverifiable
137
+ * resolved from the REGISTRY via `docker buildx imagetools inspect`
138
+ * ({@link inspectRegistryDigestOrThrow}) the authoritative value Harbor
139
+ * serves NOT the local daemon cache. The `docker push` summary digest
140
+ * ({@link parsePushDigest}), when present, is cross-checked against it and a
141
+ * mismatch is a hard error (#1679: a stale local digest / cross-cluster push
142
+ * split). Throws if no registry digest can be resolved or if it disagrees with
143
+ * the push summary — a deploy must never proceed with an unverifiable or stale
121
144
  * artifact reference.
122
145
  *
123
146
  * NOTE: for a single-arch (linux/amd64, the platform standard) push the push
@@ -1 +1 @@
1
- {"version":3,"file":"baas.d.ts","sourceRoot":"","sources":["../../src/_lib/baas.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"baas.d.ts","sourceRoot":"","sources":["../../src/_lib/baas.ts"],"names":[],"mappings":"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"}
package/dist/_lib/baas.js CHANGED
@@ -40,13 +40,16 @@ exports.tarFrontend = tarFrontend;
40
40
  * (`--password-stdin`), never on argv. Both accept an injectable `runFn`
41
41
  * (default `execFileSync`) so unit tests never invoke docker for real.
42
42
  *
43
- * {@link buildAndPush} additionally CAPTURES the `docker push` stdout (rather
44
- * than inheriting it) so it can recover the pushed image's `sha256:<64hex>`
45
- * registry manifest digest — via {@link parsePushDigest}, falling back to a
46
- * `docker inspect ... .RepoDigests` parse ({@link parseRepoDigests}). That
47
- * digest is the value the build attestation's `imageDigest` must carry: the
48
- * host binds it to the digest Harbor serves for the tag with STRICT equality,
49
- * so a tag-form ref would be rejected once `requireSignature` is enforced.
43
+ * {@link buildAndPush} records the pushed image's `sha256:<64hex>` registry
44
+ * manifest digest by resolving it from the REGISTRY via `docker buildx
45
+ * imagetools inspect` ({@link parseImagetoolsDigest}) NOT from the local
46
+ * daemon cache, which a same-tag re-push or a DNS-round-robin retry can leave
47
+ * STALE (#1679). The `digest:` line the push summary carries (captured stdout,
48
+ * {@link parsePushDigest}) is cross-checked against the registry digest; a
49
+ * mismatch is a hard error. That digest is the value the build attestation's
50
+ * `imageDigest` must carry: the host binds it to the digest Harbor serves for
51
+ * the tag with STRICT equality, so a stale/tag-form ref would be rejected once
52
+ * `requireSignature` is enforced.
50
53
  */
51
54
  const child_process_1 = require("child_process");
52
55
  const smart_engines_sdk_1 = require("@hsuite/smart-engines-sdk");
@@ -110,10 +113,13 @@ function imageTarget(registry, tag) {
110
113
  * ref. The build attestation's `imageDigest` must carry this digest: the host
111
114
  * binds it to the digest Harbor serves for the tag with strict equality, so a
112
115
  * tag-form ref is rejected once `requireSignature` is enforced. The digest is
113
- * read from the `docker push` summary ({@link parsePushDigest}); if the summary
114
- * lacked one (older docker / buildx drivers / `--skip-push`), it falls back to a
115
- * `docker inspect ... .RepoDigests` parse ({@link parseRepoDigests}). Throws if
116
- * no digest can be recovered a deploy must never proceed with an unverifiable
116
+ * resolved from the REGISTRY via `docker buildx imagetools inspect`
117
+ * ({@link inspectRegistryDigestOrThrow}) the authoritative value Harbor
118
+ * serves NOT the local daemon cache. The `docker push` summary digest
119
+ * ({@link parsePushDigest}), when present, is cross-checked against it and a
120
+ * mismatch is a hard error (#1679: a stale local digest / cross-cluster push
121
+ * split). Throws if no registry digest can be resolved or if it disagrees with
122
+ * the push summary — a deploy must never proceed with an unverifiable or stale
117
123
  * artifact reference.
118
124
  *
119
125
  * NOTE: for a single-arch (linux/amd64, the platform standard) push the push
@@ -181,41 +187,53 @@ function buildAndPush(opts) {
181
187
  env: dockerEnv,
182
188
  });
183
189
  }
184
- const target = imageTarget(opts.registry, opts.tag);
190
+ // #1679: aim the whole docker sequence at ONE origin when the caller resolved
191
+ // a pin. `opts.registry` itself is left alone — see `pushHost`'s docblock for
192
+ // why collapsing it breaks the downstream fan-out.
193
+ const pinnedHost = !opts.skipPush && opts.pushHost ? opts.pushHost : null;
194
+ const target = pinnedHost
195
+ ? `${pinnedHost}/${opts.registry.repository}:${opts.tag}`
196
+ : imageTarget(opts.registry, opts.tag);
197
+ // docker keys stored credentials by registry HOST, so the login must name the
198
+ // same host the push dials or the pinned push goes out unauthenticated.
199
+ const loginHost = pinnedHost ?? opts.registry.server;
185
200
  run('docker', ['tag', localTag, target], { stdio: 'inherit', env: dockerEnv });
186
201
  if (opts.skipPush) {
187
202
  // Nothing was pushed this run, but the image is asserted present at the
188
- // registry — recover its digest from `RepoDigests` so the attestation is
189
- // still bound to the real artifact.
190
- return inspectDigestOrThrow(run, target, dockerEnv, opts.registry, opts.tag);
203
+ // registry — resolve its digest from the registry (`buildx imagetools
204
+ // inspect`) so the attestation is bound to what Harbor actually serves.
205
+ return inspectRegistryDigestOrThrow(run, target, dockerEnv, null);
191
206
  }
192
207
  // CRITICAL: username is a discrete argv element (no shell, no `$` expansion);
193
208
  // password via stdin. See the module header.
194
- run('docker', ['login', opts.registry.server, '-u', opts.registry.username, '--password-stdin'], { input: opts.registry.password, stdio: loginStdio, env: dockerEnv });
209
+ run('docker', ['login', loginHost, '-u', opts.registry.username, '--password-stdin'], { input: opts.registry.password, stdio: loginStdio, env: dockerEnv });
210
+ // Push with a retry budget (Harbor round-robin robot-auth occasionally 401s
211
+ // the first hit). Digest RESOLUTION happens AFTER a push succeeds — outside
212
+ // the retry loop — so a registry-verify failure or a digest mismatch surfaces
213
+ // as its own hard error rather than being masked as a push retry.
195
214
  const attempts = opts.pushAttempts ?? 6;
196
215
  let lastErr;
216
+ let pushOut = '';
217
+ let pushed = false;
197
218
  for (let i = 1; i <= attempts; i++) {
198
219
  try {
199
220
  // Capture stdout (encoding 'utf8' → a string) so `parsePushDigest` can
200
- // read the `… : digest: sha256:<64hex> size: …` summary line.
201
- const pushOut = run('docker', ['push', target], {
202
- stdio: captureStdio,
203
- encoding: 'utf8',
204
- env: dockerEnv,
205
- });
206
- const fromPush = (0, docker_1.parsePushDigest)(asStdout(pushOut));
207
- if (fromPush)
208
- return fromPush;
209
- // Push succeeded but the summary carried no digest (older docker / buildx
210
- // drivers). Recover it from `RepoDigests`.
211
- return inspectDigestOrThrow(run, target, dockerEnv, opts.registry, opts.tag);
221
+ // read the `… : digest: sha256:<64hex> size: …` summary line for the
222
+ // registry cross-check.
223
+ pushOut = asStdout(run('docker', ['push', target], { stdio: captureStdio, encoding: 'utf8', env: dockerEnv }));
224
+ pushed = true;
225
+ break;
212
226
  }
213
227
  catch (err) {
214
228
  lastErr = err;
215
- // Harbor round-robin robot-auth occasionally 401s the first hit; retry.
216
229
  }
217
230
  }
218
- throw new Error(`docker push failed after ${attempts} attempts: ${lastErr?.message ?? 'unknown'}`);
231
+ if (!pushed) {
232
+ throw new Error(`docker push failed after ${attempts} attempts: ${lastErr?.message ?? 'unknown'}`);
233
+ }
234
+ // AUTHORITATIVE: bind the attestation to the digest the registry serves for
235
+ // the tag, cross-checked against the push summary (mismatch ⇒ throw, #1679).
236
+ return inspectRegistryDigestOrThrow(run, target, dockerEnv, (0, docker_1.parsePushDigest)(pushOut));
219
237
  }
220
238
  /** Coerce a `runFn` stdout return (string under `encoding:'utf8'`, else Buffer) to a string. */
221
239
  function asStdout(out) {
@@ -226,23 +244,39 @@ function asStdout(out) {
226
244
  return '';
227
245
  }
228
246
  /**
229
- * `docker inspect --format '{{json .RepoDigests}}' <target>` via the shared
230
- * `runFn` seam, returning the `sha256:<64hex>` digest for `<server>/<repo>` (the
231
- * inspect-fallback path of {@link buildAndPush}). Throws if no matching digest
232
- * is present a deploy must never proceed with an unverifiable artifact ref.
247
+ * Resolve the digest the REGISTRY serves for `<target>` via `docker buildx
248
+ * imagetools inspect` (the shared `runFn` seam), and when a `pushDigest` from
249
+ * the push summary is supplied assert they agree. `imagetools inspect` queries
250
+ * the registry, so unlike a local `docker inspect ... .RepoDigests` read it can
251
+ * never return a STALE same-tag digest (#1679).
252
+ *
253
+ * Throws if the registry digest can't be resolved, or if it disagrees with the
254
+ * push-summary digest — a deploy must never proceed with an unverifiable or
255
+ * stale artifact reference.
233
256
  */
234
- function inspectDigestOrThrow(run, target, env, registry, tag) {
235
- const inspectOut = asStdout(run('docker', ['inspect', '--format', '{{json .RepoDigests}}', target], {
257
+ function inspectRegistryDigestOrThrow(run, target, env, pushDigest) {
258
+ // Name the ref we ACTUALLY inspected. Under a #1679 push pin that is the
259
+ // single-origin target, NOT `registry.server`/<repo>:<tag> — a failure naming
260
+ // the round-robin host while the pinned host is what failed sends the reader
261
+ // to the wrong cluster.
262
+ const ref = target;
263
+ const inspectOut = asStdout(run('docker', ['buildx', 'imagetools', 'inspect', target], {
236
264
  stdio: ['ignore', 'pipe', 'ignore'],
237
265
  encoding: 'utf8',
238
266
  env,
239
267
  }));
240
- const digest = (0, docker_1.parseRepoDigests)(inspectOut, registry);
241
- if (digest)
242
- return digest;
243
- throw new Error(`pushed ${registry.server}/${registry.repository}:${tag} but could not determine its ` +
244
- 'registry manifest digest (no `digest: sha256:...` in push output and no matching ' +
245
- 'RepoDigests from `docker inspect`).');
268
+ const registryDigest = (0, docker_1.parseImagetoolsDigest)(inspectOut);
269
+ if (!registryDigest) {
270
+ throw new Error(`pushed ${ref} but could not resolve its registry manifest digest via ` +
271
+ '`docker buildx imagetools inspect` (registry unreachable or buildx unavailable). ' +
272
+ 'Refusing to attest an unverifiable artifact reference.');
273
+ }
274
+ if (pushDigest && pushDigest !== registryDigest) {
275
+ throw new Error(`digest mismatch for ${ref}: push summary reported ${pushDigest} but the registry ` +
276
+ `serves ${registryDigest} (stale local digest / cross-cluster push split, #1679). ` +
277
+ 'Refusing to attest a digest the registry does not serve.');
278
+ }
279
+ return registryDigest;
246
280
  }
247
281
  /**
248
282
  * Tar a frontend `dir` into a plain `.tar` at `out` (the smart-host accepts a
@@ -1 +1 @@
1
- {"version":3,"file":"baas.js","sourceRoot":"","sources":["../../src/_lib/baas.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"baas.js","sourceRoot":"","sources":["../../src/_lib/baas.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,214 @@
1
+ /**
2
+ * Per-cluster post-deploy convergence gate (#2787).
3
+ *
4
+ * ## Why this exists
5
+ *
6
+ * A deploy reaches exactly ONE cluster. `gateway.<domain>` is public-DNS
7
+ * round-robin over the three cluster ingresses, each fronting an INDEPENDENT
8
+ * smart-host + Mongo, and the runtime record is replicated to the peers
9
+ * asynchronously (the validator's `runtime.attached` fan-out). When that
10
+ * replication misses a cluster, nothing notices: the CLI polls the same
11
+ * round-robin name, gets a `RUNNING` from whichever cluster answers, and prints
12
+ * `✓ redeployed`.
13
+ *
14
+ * That is not hypothetical. On testnet 2026-08-22 (#2787) repeated
15
+ * `hsuite redeploy --tag v12` runs converged sn2 and sn3 and never sn1: sn1 kept
16
+ * a build-attestation pinning the PREVIOUS digest, its deployer refused the new
17
+ * image on every reconcile, and it served nothing for that app while the load
18
+ * balancer kept sending it a third of the traffic. Every run reported success.
19
+ *
20
+ * ## How it checks
21
+ *
22
+ * Round-robin polling cannot attribute an answer to a cluster, so we address the
23
+ * clusters directly: resolve the gateway's A-records and dial each ingress IP
24
+ * with TLS SNI + the HTTP `Host` header pinned to the gateway hostname — exactly
25
+ * the transport the registry fan-out already uses for the three Harbors
26
+ * ({@link defaultTransport}). Each cluster then:
27
+ * 1. names itself on the `@Public()` `GET /api/v3/cluster/health` route, and
28
+ * 2. issues its OWN session bearer (the XRPL web3 challenge/verify handshake —
29
+ * a session minted on one cluster is not valid on its peers), which is then
30
+ * used to read that cluster's own `…/deployment/apps/:appId/status`.
31
+ *
32
+ * The result is one observation per cluster, which {@link assertClustersConverged}
33
+ * turns into a hard error naming the cluster and both digests.
34
+ *
35
+ * ## What a PASS here does and does not mean (#2887)
36
+ *
37
+ * A pass means: every cluster the gateway advertises answered, and each one's
38
+ * runtime RECORD names the image ref this run deployed. It does NOT mean the
39
+ * running container's digest was verified — `runtime.image` is the desired-state
40
+ * ref the host attached, and no field on the status read path carries the
41
+ * `@sha256:` the deployer pins at apply time. Anything short of one answer per
42
+ * advertised cluster is an evidence gap and fails loudly rather than passing.
43
+ *
44
+ * Read-only: the probe issues auth handshakes and GETs, never a deploy.
45
+ */
46
+ import { type RegistryTransport } from './registry-fanout';
47
+ /** What ONE cluster reports about the app. */
48
+ export type ClusterRuntimeObservation = {
49
+ /** Cluster-ingress address dialled. */
50
+ ip: string;
51
+ /** Self-reported `CLUSTER_ID` (`sn1`…), or `null` when the cluster didn't name itself. */
52
+ clusterId: string | null;
53
+ /** Did this cluster answer the authenticated status read at all? */
54
+ reachable: boolean;
55
+ /** App lifecycle state (`ACTIVE` / `SUSPENDED` / …). */
56
+ state?: string;
57
+ /** Runtime state as THIS cluster's deployer sees it. */
58
+ runtimeState?: string;
59
+ /** Image ref THIS cluster has pinned — the field that exposes the divergence. */
60
+ image?: string;
61
+ /** This cluster's own reconcile error (carries the digest MISMATCH text). */
62
+ lastError?: string;
63
+ /** Probe-level failure (unreachable / auth failed / malformed answer). */
64
+ error?: string;
65
+ };
66
+ /** XRPL (or other chain) signer — same shape `buildSignerFromEnv` returns. */
67
+ export type ConvergenceSigner = {
68
+ chain: string;
69
+ walletAddress: string;
70
+ publicKey: string;
71
+ signFn: (message: string) => string | Promise<string>;
72
+ };
73
+ export type ProbeClusterRuntimesOpts = {
74
+ /** Public gateway URL (`https://gateway.<domain>`) — the round-robin name. */
75
+ gateway: string;
76
+ /** Management TARGET — the deployed app to read on every cluster. */
77
+ appId: string;
78
+ /** Auth IDENTITY — the developer's subscription appId (challenge context). */
79
+ authAppId: string;
80
+ /** Developer signer used for the per-cluster web3 handshake. */
81
+ signer: ConvergenceSigner;
82
+ /** Per-request timeout (ms). Default 10s. */
83
+ timeoutMs?: number;
84
+ /** Test seam — the pinned HTTPS transport. Defaults to {@link defaultTransport}. */
85
+ transport?: RegistryTransport;
86
+ /** Test seam — DNS A-record resolver (defaults to `dns.promises.resolve4`). */
87
+ resolve4?: (h: string) => Promise<string[]>;
88
+ /**
89
+ * Already-resolved cluster ingress IPs. {@link waitForClusterConvergence}
90
+ * resolves the gateway ONCE and threads the answer through every poll, so the
91
+ * expected cluster count cannot drift mid-wait (round-robin DNS can return a
92
+ * short answer, and a shrinking expectation would quietly re-open the
93
+ * partial-evidence hole this gate exists to close).
94
+ */
95
+ endpoints?: string[];
96
+ /** Optional progress log sink (defaults to no-op). */
97
+ log?: (msg: string) => void;
98
+ /**
99
+ * Bearer cache keyed by cluster IP, shared across the retry polls of
100
+ * {@link waitForClusterConvergence} so a 6-poll wait mints 3 sessions, not 18.
101
+ */
102
+ tokenCache?: Map<string, string>;
103
+ };
104
+ /**
105
+ * Ask EVERY cluster behind the gateway what it has deployed for `appId`.
106
+ *
107
+ * One observation per resolved ingress IP, in endpoint order. A cluster that
108
+ * cannot be reached or authenticated yields `reachable: false` with `error` set
109
+ * — never a throw at the per-cluster level, so {@link assertClustersConverged}
110
+ * can weigh WHICH clusters gave evidence against how many were expected.
111
+ *
112
+ * #2887: a DNS failure is NOT one of those soft outcomes. It used to log
113
+ * `skipping the check` and return `[]`, and an empty observation list then fell
114
+ * straight through the old `observations.length <= 1` short-circuit in
115
+ * `assertClustersConverged` — so "I could not find the clusters" was reported
116
+ * as "the clusters agree". Not being able to enumerate the mesh is a total
117
+ * evidence gap; it throws, and {@link waitForClusterConvergence} gives it the
118
+ * same retry budget as any other transient fault before failing loudly.
119
+ */
120
+ export declare function probeClusterRuntimes(opts: ProbeClusterRuntimesOpts): Promise<ClusterRuntimeObservation[]>;
121
+ export type AssertConvergedArgs = {
122
+ appId: string;
123
+ /** The image ref this run deployed (`<server>/<repo>:<tag>`). */
124
+ expectedImage: string;
125
+ /** The digest this run attested — half of the pair an error message must carry. */
126
+ attestedDigest: string;
127
+ /**
128
+ * How many INDEPENDENT clusters the gateway advertises — the number of
129
+ * answers a converged verdict requires. Sourced from the gateway's A-records
130
+ * by {@link waitForClusterConvergence}, NOT from `observations.length`:
131
+ * deriving it from the answers we happened to get is what made the old gate
132
+ * vacuous (#2887). REQUIRED, deliberately — an optional field with a
133
+ * permissive default is how this hole would grow back.
134
+ */
135
+ expectedClusters: number;
136
+ observations: ClusterRuntimeObservation[];
137
+ };
138
+ /**
139
+ * Throw unless EVERY cluster the gateway advertises answered, and every answer
140
+ * is the image this run deployed.
141
+ *
142
+ * Divergence is: a cluster pinned to a different image, a cluster whose runtime
143
+ * is `FAILED`, or a cluster with no runtime record at all. The error names each
144
+ * diverged cluster, what it is stuck on, and the digest this run attested — so
145
+ * the operator sees both sides of the mismatch instead of `✓ deployed`.
146
+ *
147
+ * ## Full evidence is required (#2887)
148
+ *
149
+ * This gate previously excluded unreachable clusters from the comparison and
150
+ * passed on whatever was left, short-circuiting entirely at
151
+ * `observations.length <= 1`. Both rules turned MISSING evidence into a pass:
152
+ * one matching answer out of three clusters read as "converged", and an empty
153
+ * probe (the gateway failing to resolve) read as "converged" too. Measured on
154
+ * testnet 2026-08-27: a deploy printed `✓ deployed` while `kubectl` showed sn1
155
+ * on `app:v33` and sn2 + sn3 still on `app:v32`.
156
+ *
157
+ * "I could not reach 2 of 3 clusters" and "all 3 clusters agree" are different
158
+ * sentences and only one of them is a pass. Transient faults are absorbed by
159
+ * the retry budget in {@link waitForClusterConvergence}, not by lowering the
160
+ * bar here; a mesh that is knowingly unreachable from the developer's network
161
+ * has `--skip-cluster-check`, which at least says so out loud.
162
+ *
163
+ * The one genuine no-op left: `expectedClusters <= 1`. A single-cluster / dev
164
+ * gateway has nothing to diverge from — the same rule the image fan-out
165
+ * applies at `endpoints.length <= 1`.
166
+ *
167
+ * ## What this can and cannot see
168
+ *
169
+ * `observation.image` is the per-cluster runtime RECORD (`runtime.image`,
170
+ * written by the host's `attachRuntime` call and read back through
171
+ * `GET …/deployment/apps/:appId/status`). It is a desired-state tag ref, not
172
+ * the digest of the container actually running: nothing on that read path
173
+ * surfaces the `@sha256:` the deployer pins at apply time. So this gate catches
174
+ * a cluster on the WRONG TAG or a `FAILED` reconcile, and cannot catch a
175
+ * cluster serving a different artifact under the same tag. Closing that needs
176
+ * the deployer's resolved digest surfaced in the runtime projection
177
+ * (smart-deployer → validator `runtime/status` → smart-host → SDK) — tracked
178
+ * separately; do not paper over it here with a comparison that has no digest
179
+ * to compare.
180
+ */
181
+ export declare function assertClustersConverged(args: AssertConvergedArgs): void;
182
+ export type WaitForClusterConvergenceOpts = ProbeClusterRuntimesOpts & {
183
+ /** The image ref this run deployed (`<server>/<repo>:<tag>`). */
184
+ expectedImage: string;
185
+ /** The digest this run attested. */
186
+ attestedDigest: string;
187
+ /** Delay between convergence polls (ms). Default 5s. */
188
+ intervalMs?: number;
189
+ /** Poll budget. Default 6 (~30s) — replication lag, not a wait-for-RUNNING. */
190
+ maxPolls?: number;
191
+ /** Injectable sleep for tests. */
192
+ sleep?: (ms: number) => Promise<void>;
193
+ };
194
+ /**
195
+ * Probe every cluster until they all run the deployed image, or the budget is
196
+ * exhausted — then throw ({@link assertClustersConverged}).
197
+ *
198
+ * The retry budget exists because peer replication is asynchronous: a cluster
199
+ * that simply hasn't caught up yet must not fail a healthy deploy. A cluster
200
+ * that is genuinely STUCK (a stale build-attestation, a crash-looping image)
201
+ * does not converge within any budget, which is the case this gate is for.
202
+ *
203
+ * #2887: the budget now covers the EVIDENCE-gathering failures too — a gateway
204
+ * that won't resolve, or clusters that won't answer. Those used to be reported
205
+ * as a pass; they are retried like any other transient fault and then fail
206
+ * loudly, because a check with no evidence has no verdict to give.
207
+ *
208
+ * The gateway is resolved ONCE and the answer threaded through every poll, so
209
+ * the expected cluster count is fixed for the whole wait. Re-resolving each
210
+ * poll would let a short round-robin answer shrink the expectation to match the
211
+ * clusters that happened to reply — the exact vacuity being removed.
212
+ */
213
+ export declare function waitForClusterConvergence(opts: WaitForClusterConvergenceOpts): Promise<ClusterRuntimeObservation[]>;
214
+ //# sourceMappingURL=cluster-convergence.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cluster-convergence.d.ts","sourceRoot":"","sources":["../../src/_lib/cluster-convergence.ts"],"names":[],"mappings":"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"}