@cat-factory/contracts 0.336.0 → 0.337.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 (39) hide show
  1. package/dist/environment-reachability.d.ts +246 -0
  2. package/dist/environment-reachability.d.ts.map +1 -0
  3. package/dist/environment-reachability.js +248 -0
  4. package/dist/environment-reachability.js.map +1 -0
  5. package/dist/environments.d.ts +276 -1
  6. package/dist/environments.d.ts.map +1 -1
  7. package/dist/environments.js +35 -0
  8. package/dist/environments.js.map +1 -1
  9. package/dist/execution.d.ts +1 -0
  10. package/dist/execution.d.ts.map +1 -1
  11. package/dist/frontend.d.ts +52 -1
  12. package/dist/frontend.d.ts.map +1 -1
  13. package/dist/frontend.js +69 -9
  14. package/dist/frontend.js.map +1 -1
  15. package/dist/index.d.ts +1 -0
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +1 -0
  18. package/dist/index.js.map +1 -1
  19. package/dist/routes/agent-runs.d.ts +2 -0
  20. package/dist/routes/agent-runs.d.ts.map +1 -1
  21. package/dist/routes/bug-hunt.d.ts +2 -0
  22. package/dist/routes/bug-hunt.d.ts.map +1 -1
  23. package/dist/routes/environmentUserHandlers.d.ts +6 -0
  24. package/dist/routes/environmentUserHandlers.d.ts.map +1 -1
  25. package/dist/routes/environments.d.ts +105 -0
  26. package/dist/routes/environments.d.ts.map +1 -1
  27. package/dist/routes/execution.d.ts +10 -0
  28. package/dist/routes/execution.d.ts.map +1 -1
  29. package/dist/routes/human-review.d.ts +1 -0
  30. package/dist/routes/human-review.d.ts.map +1 -1
  31. package/dist/routes/human-test.d.ts +5 -0
  32. package/dist/routes/human-test.d.ts.map +1 -1
  33. package/dist/routes/visual-confirm.d.ts +3 -0
  34. package/dist/routes/visual-confirm.d.ts.map +1 -1
  35. package/dist/routes/workspaces.d.ts +2 -0
  36. package/dist/routes/workspaces.d.ts.map +1 -1
  37. package/dist/snapshot.d.ts +1 -0
  38. package/dist/snapshot.d.ts.map +1 -1
  39. package/package.json +1 -1
@@ -0,0 +1,246 @@
1
+ import * as v from 'valibot';
2
+ /**
3
+ * One address a provider states will carry traffic for its environment's URL host.
4
+ *
5
+ * The motivating shape is an org running per-PR preview environments whose per-environment DNS
6
+ * record lives in an internal view while the load balancers fronting it are ordinary names and
7
+ * the ingress routes on the `Host` header. The address exists and a container's egress reaches
8
+ * it. The only missing thing is a name-to-address mapping, which is exactly what a hosts-file
9
+ * entry (or a Kubernetes `hostAliases` entry) is.
10
+ *
11
+ * `label` is for the human reading a diagnostic ("internal ALB", "public ALB"), never for
12
+ * matching: the platform picks by PROBING, never by name.
13
+ */
14
+ export declare const environmentAddressSchema: v.ObjectSchema<{
15
+ /** An IP literal. Never a name: a name would just be the lookup that already failed. */
16
+ readonly address: v.StringSchema<undefined>;
17
+ /** What this address IS, for the diagnostic. Never load-bearing. */
18
+ readonly label: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
19
+ }, undefined>;
20
+ export type EnvironmentAddress = v.InferOutput<typeof environmentAddressSchema>;
21
+ /**
22
+ * Why a `ready` environment could not be reached, at the layer the platform can observe.
23
+ *
24
+ * Separate members rather than one "unreachable" because they need different reactions and name
25
+ * different owners: `name_unresolved` with candidates that also failed is an environment nobody
26
+ * can reach, `no_candidate` is a PROVIDER that never told us where the thing lives, and
27
+ * `connection_refused` is a route that carries to a box with nothing listening, which is the
28
+ * deployed workload rather than the network.
29
+ *
30
+ * - `no_candidate` the environment carries no URL, or one with no host to probe. There
31
+ * was nothing to try, which is not the same as trying and failing.
32
+ * - `name_unresolved` the URL's host resolved nowhere, and no stated address carried either.
33
+ * - `no_route` something resolved and the connect never completed (timeout,
34
+ * host/network unreachable). The expensive failure: a lookup that
35
+ * worked followed by a connect that hangs.
36
+ * - `connection_refused` the route carries and nothing is listening on the port.
37
+ * - `address_refused` the provider stated an address the platform will not dial: loopback,
38
+ * link-local/vendor metadata, or a non-canonical literal. Recorded as an
39
+ * attempt rather than dropped, because a refused input is an omission the
40
+ * operator has to be able to see.
41
+ * - `probe_failed` the probe itself errored in a way it could not classify. Kept apart
42
+ * from the three above so "we could not tell" never renders as a
43
+ * verdict about the environment.
44
+ */
45
+ export declare const environmentUnreachableReasonSchema: v.PicklistSchema<["no_candidate", "name_unresolved", "no_route", "connection_refused", "address_refused", "probe_failed"], undefined>;
46
+ export type EnvironmentUnreachableReason = v.InferOutput<typeof environmentUnreachableReasonSchema>;
47
+ /** One target the proof tried, in the order it was tried, and what came back. */
48
+ export declare const environmentRouteAttemptSchema: v.ObjectSchema<{
49
+ /** `host:port` for the name itself, `host@address:port` for a stated address. */
50
+ readonly target: v.StringSchema<undefined>;
51
+ /** `carried`, or the {@link EnvironmentUnreachableReason} that target produced. */
52
+ readonly outcome: v.StringSchema<undefined>;
53
+ /**
54
+ * What the probe said when it could not classify its own failure, capped for a rendered
55
+ * surface. The ONLY field carrying WHY a `probe_failed` attempt failed.
56
+ *
57
+ * Kept because `probe_failed` names no layer by design, so without this an operator reading a
58
+ * proof cannot tell a TLS or resolver fault from a runtime restriction from a bug in the probe:
59
+ * the three need different fixes and the reason renders identically for all of them. Absent for
60
+ * every other outcome, which is already self-describing.
61
+ */
62
+ readonly detail: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
63
+ }, undefined>;
64
+ export type EnvironmentRouteAttempt = v.InferOutput<typeof environmentRouteAttemptSchema>;
65
+ /**
66
+ * What the platform PROVED about reaching an environment, once, at the moment it went `ready`.
67
+ *
68
+ * `state` splits along TWO axes and both are load-bearing. `reached` and `not_reached` are
69
+ * verdicts about the ENVIRONMENT, and only `not_reached` fails a deployer frame. `inconclusive`
70
+ * and `unproved` are verdicts about the PLATFORM, and neither may ever fail anything: collapsing
71
+ * either into `not_reached` turns a diagnostic into a second way for a healthy deploy to die,
72
+ * which is the one failure mode this whole module must not introduce, and collapsing either into
73
+ * `reached` hands a tester the unbacked claim it exists to retire.
74
+ *
75
+ * - `inconclusive` the platform LOOKED and established nothing either way: a probe that
76
+ * errored in a way it could not classify, or an environment with no address to
77
+ * dial. Narrated, because "we could not tell" is exactly the fact that stops an
78
+ * agent concluding the environment is dead.
79
+ * - `unproved` nothing was wired to open a socket, so nothing was tried. SILENT (see
80
+ * {@link reachabilityNote}): it is the standing state of every deployment with
81
+ * no prober, and a line on every prompt is a line nobody reads on the one
82
+ * prompt where it matters.
83
+ */
84
+ export declare const environmentRouteProofSchema: v.ObjectSchema<{
85
+ readonly state: v.PicklistSchema<["reached", "not_reached", "inconclusive", "unproved"], undefined>;
86
+ /**
87
+ * The stated address that CARRIED, or null when the URL's own name carried (the ordinary case)
88
+ * and when nothing carried at all. Read `state` to tell those two apart.
89
+ *
90
+ * This is the field a container bridge is built from, which is why the proof publishes the
91
+ * candidate that carried rather than the first that resolved: a bridge built from an unproved
92
+ * address is recorded as successfully applied while the tester still fails, and the evidence
93
+ * then points further from the cause than no bridge at all did.
94
+ */
95
+ readonly via: v.NullableSchema<v.StringSchema<undefined>, undefined>;
96
+ /**
97
+ * The {@link EnvironmentUnreachableReason} when `state` is `not_reached` or `inconclusive`,
98
+ * else null. An open string on the wire so a stored proof written by an older build never fails
99
+ * to parse; readers that branch on it treat an unknown value as "not one of the cases I handle".
100
+ */
101
+ readonly reason: v.NullableSchema<v.StringSchema<undefined>, undefined>;
102
+ /** Every target tried, in order. Recorded whether or not one carried. */
103
+ readonly attempts: v.ArraySchema<v.ObjectSchema<{
104
+ /** `host:port` for the name itself, `host@address:port` for a stated address. */
105
+ readonly target: v.StringSchema<undefined>;
106
+ /** `carried`, or the {@link EnvironmentUnreachableReason} that target produced. */
107
+ readonly outcome: v.StringSchema<undefined>;
108
+ /**
109
+ * What the probe said when it could not classify its own failure, capped for a rendered
110
+ * surface. The ONLY field carrying WHY a `probe_failed` attempt failed.
111
+ *
112
+ * Kept because `probe_failed` names no layer by design, so without this an operator reading a
113
+ * proof cannot tell a TLS or resolver fault from a runtime restriction from a bug in the probe:
114
+ * the three need different fixes and the reason renders identically for all of them. Absent for
115
+ * every other outcome, which is already self-describing.
116
+ */
117
+ readonly detail: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
118
+ }, undefined>, undefined>;
119
+ /** When the proof ran (epoch ms). */
120
+ readonly checkedAt: v.NumberSchema<undefined>;
121
+ }, undefined>;
122
+ export type EnvironmentRouteProof = v.InferOutput<typeof environmentRouteProofSchema>;
123
+ /**
124
+ * Everything the platform knows about ADDRESSING an environment, beside the one string it knows
125
+ * about naming it.
126
+ *
127
+ * Two halves because they come from two places and one of them is a claim: `candidates` is what
128
+ * the PROVIDER said, `proof` is what the platform TRIED. Keeping the claim after the proof runs
129
+ * is deliberate: an operator debugging a dead environment wants to see which addresses were
130
+ * offered as well as which were reached, and a re-probe on a later poll re-reads the same claim.
131
+ */
132
+ export declare const environmentReachabilitySchema: v.ObjectSchema<{
133
+ /** Addresses the provider states carry traffic for the URL's host, in ITS preference order. */
134
+ readonly candidates: v.ArraySchema<v.ObjectSchema<{
135
+ /** An IP literal. Never a name: a name would just be the lookup that already failed. */
136
+ readonly address: v.StringSchema<undefined>;
137
+ /** What this address IS, for the diagnostic. Never load-bearing. */
138
+ readonly label: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
139
+ }, undefined>, undefined>;
140
+ /** What proving found, or null when nothing has probed this environment yet. */
141
+ readonly proof: v.NullableSchema<v.ObjectSchema<{
142
+ readonly state: v.PicklistSchema<["reached", "not_reached", "inconclusive", "unproved"], undefined>;
143
+ /**
144
+ * The stated address that CARRIED, or null when the URL's own name carried (the ordinary case)
145
+ * and when nothing carried at all. Read `state` to tell those two apart.
146
+ *
147
+ * This is the field a container bridge is built from, which is why the proof publishes the
148
+ * candidate that carried rather than the first that resolved: a bridge built from an unproved
149
+ * address is recorded as successfully applied while the tester still fails, and the evidence
150
+ * then points further from the cause than no bridge at all did.
151
+ */
152
+ readonly via: v.NullableSchema<v.StringSchema<undefined>, undefined>;
153
+ /**
154
+ * The {@link EnvironmentUnreachableReason} when `state` is `not_reached` or `inconclusive`,
155
+ * else null. An open string on the wire so a stored proof written by an older build never fails
156
+ * to parse; readers that branch on it treat an unknown value as "not one of the cases I handle".
157
+ */
158
+ readonly reason: v.NullableSchema<v.StringSchema<undefined>, undefined>;
159
+ /** Every target tried, in order. Recorded whether or not one carried. */
160
+ readonly attempts: v.ArraySchema<v.ObjectSchema<{
161
+ /** `host:port` for the name itself, `host@address:port` for a stated address. */
162
+ readonly target: v.StringSchema<undefined>;
163
+ /** `carried`, or the {@link EnvironmentUnreachableReason} that target produced. */
164
+ readonly outcome: v.StringSchema<undefined>;
165
+ /**
166
+ * What the probe said when it could not classify its own failure, capped for a rendered
167
+ * surface. The ONLY field carrying WHY a `probe_failed` attempt failed.
168
+ *
169
+ * Kept because `probe_failed` names no layer by design, so without this an operator reading a
170
+ * proof cannot tell a TLS or resolver fault from a runtime restriction from a bug in the probe:
171
+ * the three need different fixes and the reason renders identically for all of them. Absent for
172
+ * every other outcome, which is already self-describing.
173
+ */
174
+ readonly detail: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
175
+ }, undefined>, undefined>;
176
+ /** When the proof ran (epoch ms). */
177
+ readonly checkedAt: v.NumberSchema<undefined>;
178
+ }, undefined>, undefined>;
179
+ }, undefined>;
180
+ export type EnvironmentReachability = v.InferOutput<typeof environmentReachabilitySchema>;
181
+ /**
182
+ * The reachability facts an agent (or a container dispatch) is handed for ONE environment it is
183
+ * being pointed at.
184
+ *
185
+ * A flattened projection rather than the stored shape, because the reader's question is narrower
186
+ * than the operator's: it needs the address it may dial and the layer that failed, not the
187
+ * provider's full candidate list. `state: 'reached'` with no `address` means the name itself
188
+ * carried, which is the case that needs no narration at all.
189
+ */
190
+ export interface EnvironmentReachabilityNote {
191
+ /**
192
+ * Deliberately NOT the proof's full state union: an `unproved` note is unrepresentable, because
193
+ * the projection withholds it (see {@link reachabilityNote}). A reader that branched on
194
+ * `'unproved'` here would be writing a case its input can never hold.
195
+ */
196
+ state: Exclude<EnvironmentRouteProof['state'], 'unproved'>;
197
+ /** The address that carried, when the name did not. */
198
+ address?: string;
199
+ reason?: string;
200
+ /** What a `probe_failed` attempt said, when one did. See the attempt's own `detail`. */
201
+ detail?: string;
202
+ }
203
+ /**
204
+ * Project a stored {@link EnvironmentReachability} onto the note an agent or a dispatch reads, or
205
+ * undefined when there is nothing to say.
206
+ *
207
+ * Undefined in two cases, and they are the same fact from two directions: no proof has been
208
+ * written, and a proof recording that nothing was WIRED to probe. "Nothing has looked" is the
209
+ * ordinary state of an environment mid-provision AND the permanent state of a deployment with no
210
+ * prober, so narrating it would put an unverified-reachability warning on every prompt of such a
211
+ * deployment and train a reader to skip the section that matters. The row still records the
212
+ * `unproved` proof, because when the probe ran is a fact an operator reads off the environment.
213
+ */
214
+ export declare function reachabilityNote(reachability: EnvironmentReachability | null | undefined): EnvironmentReachabilityNote | undefined;
215
+ /** Where an environment URL is dialled: its host, the port, and the scheme it names. */
216
+ export interface EnvironmentCoordinates {
217
+ host: string;
218
+ /** Explicit from the URL, else the scheme default (443/80), else null: nothing to dial. */
219
+ port: number | null;
220
+ /** URL scheme without the trailing colon (e.g. `https`). */
221
+ scheme: string;
222
+ }
223
+ /**
224
+ * Derive the coordinates of an environment URL, or null when there is no URL or it does not parse.
225
+ *
226
+ * ONE deriver, here rather than beside either of its two readers, because they have to agree about
227
+ * the same string and the divergence is not cosmetic: the route proof DIALS what this returns and
228
+ * the Tester prompt states it to the agent as the environment's Host / Port / Scheme. Two parsers
229
+ * mean an agent told to dial coordinates the platform never probed, and the copies this replaced
230
+ * had already diverged on the one case that mattered (an unknown scheme became port `0` in one and
231
+ * `null` in the other, and `0` is what routed a non-http URL into a failed deploy).
232
+ *
233
+ * The parse is hand-rolled because contracts compiles against `lib: ["ES2022"]` with no DOM and no
234
+ * Node types, so `URL` is unavailable here exactly as it is in kernel, and this is the one package
235
+ * both readers can see. The three things a naive split gets wrong are handled: userinfo (whose
236
+ * password may itself contain `@`, so the LAST one separates it from the host), a bracketed IPv6
237
+ * literal (kept bracketed, which is what `URL.hostname` also returns), and an explicit port, where
238
+ * a MALFORMED one answers null rather than being dropped so a garbled URL cannot be silently
239
+ * dialled on the scheme default. Deliberately not attempted: IDNA/punycode and percent-decoding,
240
+ * neither of which an environment URL from a provider needs.
241
+ *
242
+ * An unknown scheme yields `port: null` (there is no default to invent), which every caller reads
243
+ * as "nothing to dial".
244
+ */
245
+ export declare function deriveEnvironmentCoordinates(url: string | null | undefined): EnvironmentCoordinates | null;
246
+ //# sourceMappingURL=environment-reachability.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"environment-reachability.d.ts","sourceRoot":"","sources":["../src/environment-reachability.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,CAAC,MAAM,SAAS,CAAA;AAE5B;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,wBAAwB;IACnC,wFAAwF;;IAExF,oEAAoE;;aAEpE,CAAA;AACF,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,WAAW,CAAC,OAAO,wBAAwB,CAAC,CAAA;AAE/E;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,eAAO,MAAM,kCAAkC,uIAO7C,CAAA;AACF,MAAM,MAAM,4BAA4B,GAAG,CAAC,CAAC,WAAW,CAAC,OAAO,kCAAkC,CAAC,CAAA;AAEnG,iFAAiF;AACjF,eAAO,MAAM,6BAA6B;IACxC,iFAAiF;;IAEjF,mFAAmF;;IAEnF;;;;;;;;OAQG;;aAEH,CAAA;AACF,MAAM,MAAM,uBAAuB,GAAG,CAAC,CAAC,WAAW,CAAC,OAAO,6BAA6B,CAAC,CAAA;AAEzF;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,2BAA2B;;IAEtC;;;;;;;;OAQG;;IAEH;;;;OAIG;;IAEH,yEAAyE;;QAtDzE,iFAAiF;;QAEjF,mFAAmF;;QAEnF;;;;;;;;WAQG;;;IA4CH,qCAAqC;;aAErC,CAAA;AACF,MAAM,MAAM,qBAAqB,GAAG,CAAC,CAAC,WAAW,CAAC,OAAO,2BAA2B,CAAC,CAAA;AAErF;;;;;;;;GAQG;AACH,eAAO,MAAM,6BAA6B;IACxC,+FAA+F;;QAlH/F,wFAAwF;;QAExF,oEAAoE;;;IAkHpE,gFAAgF;;;QAnChF;;;;;;;;WAQG;;QAEH;;;;WAIG;;QAEH,yEAAyE;;YAtDzE,iFAAiF;;YAEjF,mFAAmF;;YAEnF;;;;;;;;eAQG;;;QA4CH,qCAAqC;;;aAmBrC,CAAA;AACF,MAAM,MAAM,uBAAuB,GAAG,CAAC,CAAC,WAAW,CAAC,OAAO,6BAA6B,CAAC,CAAA;AAEzF;;;;;;;;GAQG;AACH,MAAM,WAAW,2BAA2B;IAC1C;;;;OAIG;IACH,KAAK,EAAE,OAAO,CAAC,qBAAqB,CAAC,OAAO,CAAC,EAAE,UAAU,CAAC,CAAA;IAC1D,uDAAuD;IACvD,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,wFAAwF;IACxF,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAC9B,YAAY,EAAE,uBAAuB,GAAG,IAAI,GAAG,SAAS,GACvD,2BAA2B,GAAG,SAAS,CAUzC;AAED,wFAAwF;AACxF,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,MAAM,CAAA;IACZ,2FAA2F;IAC3F,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IACnB,4DAA4D;IAC5D,MAAM,EAAE,MAAM,CAAA;CACf;AAKD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,4BAA4B,CAC1C,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAC7B,sBAAsB,GAAG,IAAI,CAW/B"}
@@ -0,0 +1,248 @@
1
+ // Whether a `ready` environment can actually be REACHED, as opposed to whether it was
2
+ // provisioned.
3
+ //
4
+ // These are two questions, and the platform only ever asked the first one. An environment
5
+ // carries one nullable `url`, so "reachable" has meant "a URL exists"; the tester is then handed
6
+ // a name, gets `curl` exit code 000, and reports the one hypothesis its own task makes salient,
7
+ // that the ENVIRONMENT is down. That reading is wrong often enough to be expensive: a name that
8
+ // does not resolve, a route that does not carry, and an application that answers 503 are three
9
+ // different faults with three different owners, and a connection failure renders all three
10
+ // identically.
11
+ //
12
+ // The vocabulary here is the platform's answer to the LOWER two layers. It is deliberately a
13
+ // SIBLING of `EnvironmentFailureReason` rather than an extension of it: that one is
14
+ // provisioning-scoped in every member and in its docstring (`cluster_unreachable` means the
15
+ // PROVIDER could not be reached), and a reaching failure against an environment the provider
16
+ // calls `ready` is a different question for a different audience. Only the one-word verdict the
17
+ // DEPLOYER settles on lives over there (`environment_unreachable`), because the deployer settles
18
+ // in that vocabulary.
19
+ import * as v from 'valibot';
20
+ /**
21
+ * One address a provider states will carry traffic for its environment's URL host.
22
+ *
23
+ * The motivating shape is an org running per-PR preview environments whose per-environment DNS
24
+ * record lives in an internal view while the load balancers fronting it are ordinary names and
25
+ * the ingress routes on the `Host` header. The address exists and a container's egress reaches
26
+ * it. The only missing thing is a name-to-address mapping, which is exactly what a hosts-file
27
+ * entry (or a Kubernetes `hostAliases` entry) is.
28
+ *
29
+ * `label` is for the human reading a diagnostic ("internal ALB", "public ALB"), never for
30
+ * matching: the platform picks by PROBING, never by name.
31
+ */
32
+ export const environmentAddressSchema = v.object({
33
+ /** An IP literal. Never a name: a name would just be the lookup that already failed. */
34
+ address: v.string(),
35
+ /** What this address IS, for the diagnostic. Never load-bearing. */
36
+ label: v.optional(v.string()),
37
+ });
38
+ /**
39
+ * Why a `ready` environment could not be reached, at the layer the platform can observe.
40
+ *
41
+ * Separate members rather than one "unreachable" because they need different reactions and name
42
+ * different owners: `name_unresolved` with candidates that also failed is an environment nobody
43
+ * can reach, `no_candidate` is a PROVIDER that never told us where the thing lives, and
44
+ * `connection_refused` is a route that carries to a box with nothing listening, which is the
45
+ * deployed workload rather than the network.
46
+ *
47
+ * - `no_candidate` the environment carries no URL, or one with no host to probe. There
48
+ * was nothing to try, which is not the same as trying and failing.
49
+ * - `name_unresolved` the URL's host resolved nowhere, and no stated address carried either.
50
+ * - `no_route` something resolved and the connect never completed (timeout,
51
+ * host/network unreachable). The expensive failure: a lookup that
52
+ * worked followed by a connect that hangs.
53
+ * - `connection_refused` the route carries and nothing is listening on the port.
54
+ * - `address_refused` the provider stated an address the platform will not dial: loopback,
55
+ * link-local/vendor metadata, or a non-canonical literal. Recorded as an
56
+ * attempt rather than dropped, because a refused input is an omission the
57
+ * operator has to be able to see.
58
+ * - `probe_failed` the probe itself errored in a way it could not classify. Kept apart
59
+ * from the three above so "we could not tell" never renders as a
60
+ * verdict about the environment.
61
+ */
62
+ export const environmentUnreachableReasonSchema = v.picklist([
63
+ 'no_candidate',
64
+ 'name_unresolved',
65
+ 'no_route',
66
+ 'connection_refused',
67
+ 'address_refused',
68
+ 'probe_failed',
69
+ ]);
70
+ /** One target the proof tried, in the order it was tried, and what came back. */
71
+ export const environmentRouteAttemptSchema = v.object({
72
+ /** `host:port` for the name itself, `host@address:port` for a stated address. */
73
+ target: v.string(),
74
+ /** `carried`, or the {@link EnvironmentUnreachableReason} that target produced. */
75
+ outcome: v.string(),
76
+ /**
77
+ * What the probe said when it could not classify its own failure, capped for a rendered
78
+ * surface. The ONLY field carrying WHY a `probe_failed` attempt failed.
79
+ *
80
+ * Kept because `probe_failed` names no layer by design, so without this an operator reading a
81
+ * proof cannot tell a TLS or resolver fault from a runtime restriction from a bug in the probe:
82
+ * the three need different fixes and the reason renders identically for all of them. Absent for
83
+ * every other outcome, which is already self-describing.
84
+ */
85
+ detail: v.optional(v.string()),
86
+ });
87
+ /**
88
+ * What the platform PROVED about reaching an environment, once, at the moment it went `ready`.
89
+ *
90
+ * `state` splits along TWO axes and both are load-bearing. `reached` and `not_reached` are
91
+ * verdicts about the ENVIRONMENT, and only `not_reached` fails a deployer frame. `inconclusive`
92
+ * and `unproved` are verdicts about the PLATFORM, and neither may ever fail anything: collapsing
93
+ * either into `not_reached` turns a diagnostic into a second way for a healthy deploy to die,
94
+ * which is the one failure mode this whole module must not introduce, and collapsing either into
95
+ * `reached` hands a tester the unbacked claim it exists to retire.
96
+ *
97
+ * - `inconclusive` the platform LOOKED and established nothing either way: a probe that
98
+ * errored in a way it could not classify, or an environment with no address to
99
+ * dial. Narrated, because "we could not tell" is exactly the fact that stops an
100
+ * agent concluding the environment is dead.
101
+ * - `unproved` nothing was wired to open a socket, so nothing was tried. SILENT (see
102
+ * {@link reachabilityNote}): it is the standing state of every deployment with
103
+ * no prober, and a line on every prompt is a line nobody reads on the one
104
+ * prompt where it matters.
105
+ */
106
+ export const environmentRouteProofSchema = v.object({
107
+ state: v.picklist(['reached', 'not_reached', 'inconclusive', 'unproved']),
108
+ /**
109
+ * The stated address that CARRIED, or null when the URL's own name carried (the ordinary case)
110
+ * and when nothing carried at all. Read `state` to tell those two apart.
111
+ *
112
+ * This is the field a container bridge is built from, which is why the proof publishes the
113
+ * candidate that carried rather than the first that resolved: a bridge built from an unproved
114
+ * address is recorded as successfully applied while the tester still fails, and the evidence
115
+ * then points further from the cause than no bridge at all did.
116
+ */
117
+ via: v.nullable(v.string()),
118
+ /**
119
+ * The {@link EnvironmentUnreachableReason} when `state` is `not_reached` or `inconclusive`,
120
+ * else null. An open string on the wire so a stored proof written by an older build never fails
121
+ * to parse; readers that branch on it treat an unknown value as "not one of the cases I handle".
122
+ */
123
+ reason: v.nullable(v.string()),
124
+ /** Every target tried, in order. Recorded whether or not one carried. */
125
+ attempts: v.array(environmentRouteAttemptSchema),
126
+ /** When the proof ran (epoch ms). */
127
+ checkedAt: v.number(),
128
+ });
129
+ /**
130
+ * Everything the platform knows about ADDRESSING an environment, beside the one string it knows
131
+ * about naming it.
132
+ *
133
+ * Two halves because they come from two places and one of them is a claim: `candidates` is what
134
+ * the PROVIDER said, `proof` is what the platform TRIED. Keeping the claim after the proof runs
135
+ * is deliberate: an operator debugging a dead environment wants to see which addresses were
136
+ * offered as well as which were reached, and a re-probe on a later poll re-reads the same claim.
137
+ */
138
+ export const environmentReachabilitySchema = v.object({
139
+ /** Addresses the provider states carry traffic for the URL's host, in ITS preference order. */
140
+ candidates: v.array(environmentAddressSchema),
141
+ /** What proving found, or null when nothing has probed this environment yet. */
142
+ proof: v.nullable(environmentRouteProofSchema),
143
+ });
144
+ /**
145
+ * Project a stored {@link EnvironmentReachability} onto the note an agent or a dispatch reads, or
146
+ * undefined when there is nothing to say.
147
+ *
148
+ * Undefined in two cases, and they are the same fact from two directions: no proof has been
149
+ * written, and a proof recording that nothing was WIRED to probe. "Nothing has looked" is the
150
+ * ordinary state of an environment mid-provision AND the permanent state of a deployment with no
151
+ * prober, so narrating it would put an unverified-reachability warning on every prompt of such a
152
+ * deployment and train a reader to skip the section that matters. The row still records the
153
+ * `unproved` proof, because when the probe ran is a fact an operator reads off the environment.
154
+ */
155
+ export function reachabilityNote(reachability) {
156
+ const proof = reachability?.proof;
157
+ if (!proof || proof.state === 'unproved')
158
+ return undefined;
159
+ const detail = proof.attempts.find((attempt) => attempt.detail)?.detail;
160
+ return {
161
+ state: proof.state,
162
+ ...(proof.via ? { address: proof.via } : {}),
163
+ ...(proof.reason ? { reason: proof.reason } : {}),
164
+ ...(detail ? { detail } : {}),
165
+ };
166
+ }
167
+ /** `scheme://` plus everything up to the path, query or fragment: the authority. */
168
+ const URL_AUTHORITY = /^([a-z][a-z0-9+.-]*):\/\/([^/?#]*)/i;
169
+ /**
170
+ * Derive the coordinates of an environment URL, or null when there is no URL or it does not parse.
171
+ *
172
+ * ONE deriver, here rather than beside either of its two readers, because they have to agree about
173
+ * the same string and the divergence is not cosmetic: the route proof DIALS what this returns and
174
+ * the Tester prompt states it to the agent as the environment's Host / Port / Scheme. Two parsers
175
+ * mean an agent told to dial coordinates the platform never probed, and the copies this replaced
176
+ * had already diverged on the one case that mattered (an unknown scheme became port `0` in one and
177
+ * `null` in the other, and `0` is what routed a non-http URL into a failed deploy).
178
+ *
179
+ * The parse is hand-rolled because contracts compiles against `lib: ["ES2022"]` with no DOM and no
180
+ * Node types, so `URL` is unavailable here exactly as it is in kernel, and this is the one package
181
+ * both readers can see. The three things a naive split gets wrong are handled: userinfo (whose
182
+ * password may itself contain `@`, so the LAST one separates it from the host), a bracketed IPv6
183
+ * literal (kept bracketed, which is what `URL.hostname` also returns), and an explicit port, where
184
+ * a MALFORMED one answers null rather than being dropped so a garbled URL cannot be silently
185
+ * dialled on the scheme default. Deliberately not attempted: IDNA/punycode and percent-decoding,
186
+ * neither of which an environment URL from a provider needs.
187
+ *
188
+ * An unknown scheme yields `port: null` (there is no default to invent), which every caller reads
189
+ * as "nothing to dial".
190
+ */
191
+ export function deriveEnvironmentCoordinates(url) {
192
+ if (!url)
193
+ return null;
194
+ const match = URL_AUTHORITY.exec(url.trim());
195
+ if (!match)
196
+ return null;
197
+ const scheme = (match[1] ?? '').toLowerCase();
198
+ const authority = match[2] ?? '';
199
+ const hostPort = authority.slice(authority.lastIndexOf('@') + 1);
200
+ const split = splitHostPort(hostPort);
201
+ if (!split)
202
+ return null;
203
+ const port = split.port ?? defaultPortForScheme(scheme);
204
+ return { host: split.host, port, scheme };
205
+ }
206
+ /** The default port a scheme implies, or null when it implies none the platform knows. */
207
+ function defaultPortForScheme(scheme) {
208
+ return scheme === 'https' ? 443 : scheme === 'http' ? 80 : null;
209
+ }
210
+ /**
211
+ * Split an authority's `host[:port]` half, or null when it is not one.
212
+ *
213
+ * Null rather than a best guess for every malformed shape: an unclosed bracket, an unbracketed
214
+ * literal carrying several colons (an IPv6 address that a URL may not spell that way), and a port
215
+ * that is not a number in range. Each of those is a URL `new URL` would throw on, and answering
216
+ * with a host anyway is how a garbled URL gets dialled.
217
+ */
218
+ function splitHostPort(hostPort) {
219
+ if (hostPort.startsWith('[')) {
220
+ const close = hostPort.indexOf(']');
221
+ if (close < 1)
222
+ return null;
223
+ const host = hostPort.slice(0, close + 1).toLowerCase();
224
+ const rest = hostPort.slice(close + 1);
225
+ if (rest === '')
226
+ return { host, port: null };
227
+ if (!rest.startsWith(':'))
228
+ return null;
229
+ const port = parsePort(rest.slice(1));
230
+ return port === null ? null : { host, port };
231
+ }
232
+ const colon = hostPort.indexOf(':');
233
+ if (colon < 0)
234
+ return hostPort ? { host: hostPort.toLowerCase(), port: null } : null;
235
+ if (hostPort.indexOf(':', colon + 1) >= 0)
236
+ return null;
237
+ const host = hostPort.slice(0, colon).toLowerCase();
238
+ const port = parsePort(hostPort.slice(colon + 1));
239
+ return host && port !== null ? { host, port } : null;
240
+ }
241
+ /** A port as written in a URL: digits, 1-65535. Null for anything else. */
242
+ function parsePort(raw) {
243
+ if (!/^\d{1,5}$/.test(raw))
244
+ return null;
245
+ const port = Number(raw);
246
+ return port >= 1 && port <= 65535 ? port : null;
247
+ }
248
+ //# sourceMappingURL=environment-reachability.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"environment-reachability.js","sourceRoot":"","sources":["../src/environment-reachability.ts"],"names":[],"mappings":"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"}