@oxyhq/contracts 0.26.0 → 0.28.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 (52) hide show
  1. package/dist/cjs/.tsbuildinfo +1 -1
  2. package/dist/cjs/accountGraph.js +4 -3
  3. package/dist/cjs/index.js +186 -1
  4. package/dist/cjs/inference/accountBilling.js +334 -0
  5. package/dist/cjs/inference/attribution.js +106 -0
  6. package/dist/cjs/inference/catalogue.js +482 -0
  7. package/dist/cjs/inference/entitlement.js +217 -0
  8. package/dist/cjs/inference/errors.js +210 -0
  9. package/dist/cjs/inference/identifiers.js +197 -0
  10. package/dist/cjs/inference/money.js +188 -0
  11. package/dist/cjs/inference/priceVersion.js +110 -0
  12. package/dist/cjs/inference/providerConnection.js +142 -0
  13. package/dist/cjs/inference/request.js +288 -0
  14. package/dist/cjs/inference/routingPolicy.js +213 -0
  15. package/dist/cjs/inference/streamEvents.js +219 -0
  16. package/dist/cjs/inference/usage.js +297 -0
  17. package/dist/cjs/inference/version.js +85 -0
  18. package/dist/esm/.tsbuildinfo +1 -1
  19. package/dist/esm/accountGraph.js +4 -3
  20. package/dist/esm/index.js +54 -0
  21. package/dist/esm/inference/accountBilling.js +331 -0
  22. package/dist/esm/inference/attribution.js +103 -0
  23. package/dist/esm/inference/catalogue.js +479 -0
  24. package/dist/esm/inference/entitlement.js +214 -0
  25. package/dist/esm/inference/errors.js +207 -0
  26. package/dist/esm/inference/identifiers.js +194 -0
  27. package/dist/esm/inference/money.js +185 -0
  28. package/dist/esm/inference/priceVersion.js +107 -0
  29. package/dist/esm/inference/providerConnection.js +139 -0
  30. package/dist/esm/inference/request.js +285 -0
  31. package/dist/esm/inference/routingPolicy.js +210 -0
  32. package/dist/esm/inference/streamEvents.js +216 -0
  33. package/dist/esm/inference/usage.js +294 -0
  34. package/dist/esm/inference/version.js +82 -0
  35. package/dist/types/.tsbuildinfo +1 -1
  36. package/dist/types/accountGraph.d.ts +6 -5
  37. package/dist/types/index.d.ts +27 -0
  38. package/dist/types/inference/accountBilling.d.ts +738 -0
  39. package/dist/types/inference/attribution.d.ts +176 -0
  40. package/dist/types/inference/catalogue.d.ts +1612 -0
  41. package/dist/types/inference/entitlement.d.ts +519 -0
  42. package/dist/types/inference/errors.d.ts +206 -0
  43. package/dist/types/inference/identifiers.d.ts +157 -0
  44. package/dist/types/inference/money.d.ts +185 -0
  45. package/dist/types/inference/priceVersion.d.ts +182 -0
  46. package/dist/types/inference/providerConnection.d.ts +297 -0
  47. package/dist/types/inference/request.d.ts +2364 -0
  48. package/dist/types/inference/routingPolicy.d.ts +426 -0
  49. package/dist/types/inference/streamEvents.d.ts +906 -0
  50. package/dist/types/inference/usage.d.ts +1139 -0
  51. package/dist/types/inference/version.d.ts +82 -0
  52. package/package.json +1 -1
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Money and usage units for the inference contracts.
3
+ *
4
+ * The non-negotiable invariant this file exists to make structural: **customer
5
+ * charges never use floating-point values as the financial source of truth.**
6
+ * A JS `number` cannot represent `0.1 + 0.2` exactly, and an inference ledger
7
+ * adds millions of small amounts, so a float total is wrong by construction
8
+ * rather than by accident.
9
+ *
10
+ * Every amount and every price is one representation: {@link exactDecimalSchema},
11
+ * an exact decimal STRING at the scale ADR 0009 declares. Amounts are not
12
+ * integer minor units, and that is the ADR's decision rather than an oversight:
13
+ * one token costs several orders of magnitude less than one cent, so rounding
14
+ * per request would make a customer's bill depend on how their client chunked
15
+ * its work. Rounding happens ONCE, at the invoice boundary, and is itself a
16
+ * ledger entry.
17
+ *
18
+ * A string is also what the driver hands back — `postgres.js` decodes `NUMERIC`
19
+ * as a string — so keeping it a string on the wire means accidental JS
20
+ * arithmetic fails loudly instead of silently losing precision. Money
21
+ * arithmetic happens in SQL or in a decimal type, never in a JS `number`.
22
+ *
23
+ * Units are carried separately from money in every shape: a receipt says both
24
+ * "204 output tokens" and "0.003060000000 USD", and neither is derived from the
25
+ * other at read time. That separation is what lets a price version change
26
+ * without rewriting settled history.
27
+ *
28
+ * Decided in: docs/adr/0009-usage-reservation-and-settlement.md.
29
+ */
30
+ import { z } from 'zod';
31
+ /* -------------------------------------------------------------------------- */
32
+ /* Money */
33
+ /* -------------------------------------------------------------------------- */
34
+ /** ISO 4217 alpha-3 currency code, e.g. `USD`. */
35
+ export const currencyCodeSchema = z
36
+ .string()
37
+ .regex(/^[A-Z]{3}$/, 'currency must be an ISO 4217 alpha-3 code');
38
+ /**
39
+ * The declared fractional scale of every amount and price in this contract, and
40
+ * of the `NUMERIC` columns the ledger stores them in.
41
+ *
42
+ * Twelve digits is sub-minor-unit precision by a wide margin: at $3 per million
43
+ * input tokens, one token costs `0.000003000000`, which this scale represents
44
+ * exactly. Amounts are compared and summed NUMERICALLY, never as text — `3.0`
45
+ * and `3.000000000000` are one amount written two ways.
46
+ */
47
+ export const INFERENCE_MONEY_SCALE = 12;
48
+ /**
49
+ * An exact non-negative decimal, carried as a STRING so no parse step can turn
50
+ * it into a float on the way past. Up to 18 integer digits and
51
+ * {@link INFERENCE_MONEY_SCALE} fractional digits.
52
+ *
53
+ * Non-negative: direction is carried by the SHAPE — a receipt debits, a refund
54
+ * credits — so a stray sign can never silently invert an entry.
55
+ *
56
+ * No exponent form: `1e-6` and `0.000001` are the same number, but only one of
57
+ * them survives a naive string comparison, a cache key or a log grep intact.
58
+ *
59
+ * Branded, so a bare `string` is not assignable and an amount cannot arrive
60
+ * from string concatenation that was never checked. Producers construct one
61
+ * with `exactDecimalSchema.parse(value)`.
62
+ */
63
+ export const exactDecimalSchema = z
64
+ .string()
65
+ .regex(/^(?:0|[1-9][0-9]{0,17})(?:\.[0-9]{1,12})?$/, 'must be an exact non-negative decimal string without an exponent')
66
+ .brand();
67
+ /**
68
+ * An amount of money: the exact decimal plus the currency it is in.
69
+ *
70
+ * `.strict()` so a payload carrying a convenience float beside the exact value
71
+ * (`{ amount: '18.06', amountFloat: 18.06 }`) is REJECTED rather than stripped.
72
+ * A stripped float is the more dangerous outcome: it disappears silently here
73
+ * and survives in the producer, where it is the value somebody eventually
74
+ * displays.
75
+ */
76
+ export const moneySchema = z
77
+ .object({
78
+ amount: exactDecimalSchema,
79
+ currency: currencyCodeSchema,
80
+ })
81
+ .strict();
82
+ /* -------------------------------------------------------------------------- */
83
+ /* Usage units */
84
+ /* -------------------------------------------------------------------------- */
85
+ /**
86
+ * The closed set of units inference is metered in.
87
+ *
88
+ * **The units PARTITION a request: every unit counts material no other unit
89
+ * counts.** `cached_input_tokens` is not part of `input_tokens`, and
90
+ * `reasoning_tokens` is not part of `output_tokens` — they are siblings, not
91
+ * subsets. A request whose 10 000-token prompt was served 9 000 tokens from
92
+ * cache is reported as `input_tokens: 1000` beside `cached_input_tokens: 9000`,
93
+ * never as `input_tokens: 10000` beside it.
94
+ *
95
+ * That belongs to the definition rather than to a convention somewhere else,
96
+ * because settlement applies a price to EVERY reported unit and sums them
97
+ * (`inferenceLedger.service.ts`'s `computeCharge`). Under the partition rule
98
+ * that sum IS the request's cost, and a cached token can carry its own — lower
99
+ * — price. Under the nested reading the same sum charges the cached and
100
+ * reasoning tokens twice: once inside their parent and once on their own line.
101
+ * It fails silently, because every total still looks plausible and the receipt
102
+ * is still internally consistent, and on a reasoning model the reasoning tokens
103
+ * can dominate the completion, so the error is not marginal.
104
+ *
105
+ * **Every OpenAI-compatible provider reports the other way round**:
106
+ * `prompt_tokens` INCLUDES `prompt_tokens_details.cached_tokens`, and
107
+ * `completion_tokens` INCLUDES `completion_tokens_details.reasoning_tokens`.
108
+ * Normalising is the data plane's job and it is subtraction:
109
+ *
110
+ * ```text
111
+ * input_tokens = prompt_tokens - prompt_tokens_details.cached_tokens
112
+ * output_tokens = completion_tokens - completion_tokens_details.reasoning_tokens
113
+ * ```
114
+ *
115
+ * No refinement in this package can enforce it, and saying so is part of the
116
+ * rule: a nested report and a disjoint one are the same four non-negative
117
+ * integers, so no predicate over a single report can tell them apart. The two
118
+ * structural guards that DO exist — refining `cached <= input` and
119
+ * `reasoning <= output`, or deriving the parents instead of reporting them —
120
+ * both encode the nested reading, which is the one this rule rejects. What IS
121
+ * enforceable is the arithmetic that depends on the rule, and that is where the
122
+ * enforcement lives — `inferenceLedger.service.test.ts` prices a report in
123
+ * which cached and reasoning tokens are both non-zero and asserts the exact
124
+ * total, which the nested reading cannot produce.
125
+ *
126
+ * Where the public surface has to speak a nested dialect, the sum is put back
127
+ * at the boundary rather than the internal reading being bent to it
128
+ * (`routes/inferenceEdge.ts` renders `prompt_tokens` as
129
+ * `input_tokens + cached_input_tokens`).
130
+ *
131
+ * Time is carried in integer MILLISECONDS rather than seconds so that no unit
132
+ * quantity is ever fractional: a 12.5-second transcription is `12500`, exactly,
133
+ * and the "units are integers" rule holds for every modality instead of holding
134
+ * for tokens and being quietly broken by audio.
135
+ */
136
+ export const USAGE_UNITS = [
137
+ 'input_tokens',
138
+ 'cached_input_tokens',
139
+ 'output_tokens',
140
+ 'reasoning_tokens',
141
+ 'requests',
142
+ 'images',
143
+ 'audio_input_milliseconds',
144
+ 'audio_output_milliseconds',
145
+ 'video_milliseconds',
146
+ 'characters',
147
+ 'embeddings',
148
+ ];
149
+ export const usageUnitSchema = z.enum(USAGE_UNITS);
150
+ /**
151
+ * A metered quantity of ONE unit. Never money — a quantity carries no price and
152
+ * no currency, so a consumer cannot mistake a token count for an amount owed.
153
+ */
154
+ export const usageQuantitySchema = z
155
+ .object({
156
+ unit: usageUnitSchema,
157
+ quantity: z.number().int().nonnegative().safe(),
158
+ })
159
+ .strict();
160
+ /**
161
+ * Where a metered quantity came from.
162
+ *
163
+ * Kept explicit because the three are not interchangeable when a charge is
164
+ * disputed: `provider_reported` is the upstream's own count, `oxy_measured` is
165
+ * counted by the platform (streamed bytes, wall-clock milliseconds), and
166
+ * `estimated` is a reconstruction used when a provider returned no usage at
167
+ * all. An estimate that is indistinguishable from a reported number is an
168
+ * estimate nobody can later reconcile or refund against.
169
+ */
170
+ export const USAGE_SOURCES = ['provider_reported', 'oxy_measured', 'estimated'];
171
+ export const usageSourceSchema = z.enum(USAGE_SOURCES);
172
+ /**
173
+ * A price for one unit, as `amount` per `per` units — `per` because a price
174
+ * quoted per single token would need more fractional digits than it is worth
175
+ * ("$3.00 per 1000000 input_tokens" is how every provider quotes it, and how
176
+ * every customer reads it).
177
+ */
178
+ export const unitPriceSchema = z
179
+ .object({
180
+ unit: usageUnitSchema,
181
+ amount: exactDecimalSchema,
182
+ per: z.number().int().positive().safe(),
183
+ currency: currencyCodeSchema,
184
+ })
185
+ .strict();
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Price versions — the immutable snapshots customer pricing is quoted and
3
+ * settled against.
4
+ *
5
+ * A price is never edited in place. A change publishes a NEW version that
6
+ * supersedes the old one, and every settled receipt keeps the id of the version
7
+ * it was priced with. That is what makes an invoice reproducible a year later:
8
+ * the receipt does not say "3.00 per million tokens", it says "priced under
9
+ * `pv_2026_08`", and that version still exists, unchanged, with its own
10
+ * effective window.
11
+ *
12
+ * Prices are exact decimal strings (see `money.ts`), never floats, and they are
13
+ * quoted per unit. The amount a customer owes is computed from them and is
14
+ * carried in the same exact form, so no step of the calculation passes through
15
+ * a representation that cannot hold the value.
16
+ *
17
+ * Decided in: docs/adr/0009-usage-reservation-and-settlement.md.
18
+ */
19
+ import { z } from 'zod';
20
+ import { inferenceTimestampSchema, modelReferenceSchema, inferenceProviderSlugSchema, } from './identifiers.js';
21
+ import { currencyCodeSchema, unitPriceSchema } from './money.js';
22
+ /**
23
+ * Lifecycle of a price version.
24
+ *
25
+ * `draft` is quotable in Console previews but may never price a receipt;
26
+ * `active` is what live requests are priced with; `superseded` priced receipts
27
+ * in the past and still resolves for them forever.
28
+ */
29
+ export const priceVersionStatusSchema = z.enum(['draft', 'active', 'superseded']);
30
+ /**
31
+ * A published set of customer prices for one model reference on one provider.
32
+ *
33
+ * Scoped to a `(modelReference, provider)` pair rather than to a model alone
34
+ * because the same model costs different amounts on different providers, and a
35
+ * receipt has to be reproducible against the route that actually served it.
36
+ */
37
+ export const priceVersionSchema = z
38
+ .object({
39
+ /** See `version.ts`: served on its own by the catalogue, so it is versioned. */
40
+ schemaVersion: z.literal(1),
41
+ priceVersionId: z.string().min(1).max(128),
42
+ status: priceVersionStatusSchema,
43
+ modelReference: modelReferenceSchema,
44
+ provider: inferenceProviderSlugSchema,
45
+ currency: currencyCodeSchema,
46
+ unitPrices: z.array(unitPriceSchema).min(1),
47
+ effectiveFrom: inferenceTimestampSchema,
48
+ /** Absent while this version is the current one. */
49
+ effectiveUntil: inferenceTimestampSchema.optional(),
50
+ /** The version this one replaced, absent for the first version of a route. */
51
+ supersedesPriceVersionId: z.string().min(1).max(128).optional(),
52
+ createdAt: inferenceTimestampSchema,
53
+ })
54
+ .superRefine((priceVersion, ctx) => {
55
+ const units = priceVersion.unitPrices.map((price) => price.unit);
56
+ if (new Set(units).size !== units.length) {
57
+ ctx.addIssue({
58
+ code: z.ZodIssueCode.custom,
59
+ path: ['unitPrices'],
60
+ message: 'a unit may be priced only once per price version',
61
+ });
62
+ }
63
+ for (const [index, price] of priceVersion.unitPrices.entries()) {
64
+ if (price.currency !== priceVersion.currency) {
65
+ ctx.addIssue({
66
+ code: z.ZodIssueCode.custom,
67
+ path: ['unitPrices', index, 'currency'],
68
+ message: 'every unit price must be quoted in the price version currency',
69
+ });
70
+ }
71
+ }
72
+ // Compared as instants, not as strings: `…T00:00:00Z` and `…T00:00:00.000Z`
73
+ // are the same moment and sort differently as text.
74
+ if (priceVersion.effectiveUntil !== undefined &&
75
+ Date.parse(priceVersion.effectiveUntil) <= Date.parse(priceVersion.effectiveFrom)) {
76
+ ctx.addIssue({
77
+ code: z.ZodIssueCode.custom,
78
+ path: ['effectiveUntil'],
79
+ message: 'a price version must stop applying after it started applying',
80
+ });
81
+ }
82
+ // A superseded version priced requests during a window that has closed. Left
83
+ // open, it is indistinguishable from the current one when a receipt is
84
+ // re-priced years later — which is the one job this record exists to do.
85
+ if (priceVersion.status === 'superseded' && priceVersion.effectiveUntil === undefined) {
86
+ ctx.addIssue({
87
+ code: z.ZodIssueCode.custom,
88
+ path: ['effectiveUntil'],
89
+ message: 'a superseded price version must record when it stopped applying',
90
+ });
91
+ }
92
+ });
93
+ /**
94
+ * The price snapshot a settled receipt keeps.
95
+ *
96
+ * The unit prices are COPIED onto the receipt, not just referenced, so a receipt
97
+ * remains readable even if the price version record is later archived, and so
98
+ * that a mistake in the copy is visible as a disagreement with the version it
99
+ * names rather than silently invisible.
100
+ */
101
+ export const priceSnapshotSchema = z
102
+ .object({
103
+ priceVersionId: z.string().min(1).max(128),
104
+ currency: currencyCodeSchema,
105
+ unitPrices: z.array(unitPriceSchema).min(1),
106
+ })
107
+ .strict();
@@ -0,0 +1,139 @@
1
+ /**
2
+ * BYOK provider connections — the metadata Oxy holds about a customer's own
3
+ * upstream provider credential.
4
+ *
5
+ * The credential itself is NOT here and cannot be put here. This shape carries
6
+ * a locator (`secretRef`) into Vault/KMS/managed secret storage, a prefix short
7
+ * enough to be useless, a fingerprint, and validation state. Two mechanisms
8
+ * make that structural rather than a convention somebody must remember:
9
+ *
10
+ * - The object is `.strict()`. A producer that attaches `apiKey`, `secret`,
11
+ * `token`, `privateKey` or `headers` fails the parse. Nothing is silently
12
+ * stripped, because a stripped field is one that still exists upstream of
13
+ * the parse, in a log line or an error report.
14
+ * - `keyPrefix` is capped at 12 characters — shorter than any provider's
15
+ * usable credential — so the one field designed to show part of a key cannot
16
+ * be widened into showing all of it without changing the contract.
17
+ *
18
+ * BYOK does not move the billing relationship: the upstream provider bills the
19
+ * customer's own account directly, and Oxy charges only its platform fee. The
20
+ * record says so explicitly so a receipt against a BYOK route can be read
21
+ * correctly without consulting anything else.
22
+ *
23
+ * Decided in: issue #972 workstream 10.
24
+ */
25
+ import { z } from 'zod';
26
+ import { inferenceEnvironmentSchema, inferenceProviderSlugSchema, inferenceTimestampSchema, oxyAccountIdSchema, oxyApplicationIdSchema, } from './identifiers.js';
27
+ /**
28
+ * How widely a connection applies.
29
+ *
30
+ * In the unified account graph a project IS an account, so `account` and
31
+ * `project` differ by INHERITANCE, not by id space: an `account` connection is
32
+ * inherited by every descendant project and application, a `project` one
33
+ * applies to that project account alone, and an `application` one to a single
34
+ * application. Recording which the customer chose is what makes a later
35
+ * "why did this app use that key" answerable.
36
+ */
37
+ export const providerConnectionScopeSchema = z.discriminatedUnion('kind', [
38
+ z.object({ kind: z.literal('account'), accountId: oxyAccountIdSchema }).strict(),
39
+ z.object({ kind: z.literal('project'), accountId: oxyAccountIdSchema }).strict(),
40
+ z
41
+ .object({
42
+ kind: z.literal('application'),
43
+ accountId: oxyAccountIdSchema,
44
+ applicationId: oxyApplicationIdSchema,
45
+ })
46
+ .strict(),
47
+ ]);
48
+ /**
49
+ * A locator for the credential in managed secret storage — never the credential.
50
+ *
51
+ * The scheme prefix is constrained to the stores Oxy actually uses, so a
52
+ * producer cannot pass a raw key through this field and have it look like a
53
+ * reference; whitespace is excluded for the same reason.
54
+ */
55
+ export const providerSecretReferenceSchema = z
56
+ .string()
57
+ .max(512)
58
+ .regex(/^(?:vault|kms|ssm|secretsmanager):[A-Za-z0-9/_.:@-]{1,480}$/, 'a secret reference is a <store>:<locator> pointer, never credential material');
59
+ /** Why a credential check failed, as a closed set the Console can render. */
60
+ export const providerConnectionValidationSchema = z
61
+ .object({
62
+ state: z.enum(['unvalidated', 'valid', 'invalid', 'expired']),
63
+ lastValidatedAt: inferenceTimestampSchema.optional(),
64
+ /** Required when `invalid`: a failure nobody can act on is not a result. */
65
+ failureCode: z
66
+ .enum(['unauthorized', 'forbidden', 'not_found', 'rate_limited', 'network', 'unknown'])
67
+ .optional(),
68
+ })
69
+ .strict();
70
+ /** Lifecycle of a connection. `revoked` is terminal; `disabled` is reversible. */
71
+ export const providerConnectionStatusSchema = z.enum([
72
+ 'pending_validation',
73
+ 'active',
74
+ 'disabled',
75
+ 'revoked',
76
+ ]);
77
+ /**
78
+ * A customer's provider connection, without secrets.
79
+ *
80
+ * This is the whole of what Oxy stores, and the whole of what the data plane
81
+ * is given.
82
+ * Resolving `secretRef` to credential material happens in the secret store, at
83
+ * use time, in the data plane — never in a database row, an API response, a
84
+ * Console screen or a log line.
85
+ */
86
+ export const providerConnectionSchema = z
87
+ .object({
88
+ /** See `version.ts`: exchanged with the data plane and rendered by Console. */
89
+ schemaVersion: z.literal(1),
90
+ connectionId: z.string().min(1).max(128),
91
+ provider: inferenceProviderSlugSchema,
92
+ /** The Oxy account that owns the connection and answers for its use. */
93
+ ownerAccountId: oxyAccountIdSchema,
94
+ scope: providerConnectionScopeSchema,
95
+ environment: inferenceEnvironmentSchema,
96
+ status: providerConnectionStatusSchema,
97
+ secretRef: providerSecretReferenceSchema,
98
+ /**
99
+ * The leading characters of the credential, for recognition only. Capped at
100
+ * 12 — long enough to tell two keys apart, far too short to be one.
101
+ */
102
+ keyPrefix: z.string().min(1).max(12),
103
+ /** SHA-256 of the credential, so rotation is verifiable without the key. */
104
+ fingerprint: z
105
+ .string()
106
+ .regex(/^[a-f0-9]{64}$/, 'fingerprint must be 64 lowercase hex characters'),
107
+ validation: providerConnectionValidationSchema,
108
+ /**
109
+ * Always `true` for a BYOK connection: the provider bills the customer's own
110
+ * upstream account, and Oxy charges only its platform fee. Stated as data so
111
+ * a receipt against this route is readable without a second lookup.
112
+ */
113
+ upstreamBillsCustomerDirectly: z.literal(true),
114
+ /** Set when the provider's terms require a per-customer acknowledgement. */
115
+ termsAcknowledgedAt: inferenceTimestampSchema.optional(),
116
+ createdAt: inferenceTimestampSchema,
117
+ rotatedAt: inferenceTimestampSchema.optional(),
118
+ })
119
+ .strict()
120
+ .superRefine((connection, ctx) => {
121
+ if (connection.validation.state === 'invalid' &&
122
+ connection.validation.failureCode === undefined) {
123
+ ctx.addIssue({
124
+ code: z.ZodIssueCode.custom,
125
+ path: ['validation', 'failureCode'],
126
+ message: 'an invalid credential must record why the check failed',
127
+ });
128
+ }
129
+ // A credential the provider has rejected cannot be the one live requests are
130
+ // routed through: leaving it active turns every request on this route into a
131
+ // customer-visible upstream failure.
132
+ if (connection.status === 'active' && connection.validation.state === 'invalid') {
133
+ ctx.addIssue({
134
+ code: z.ZodIssueCode.custom,
135
+ path: ['status'],
136
+ message: 'a connection whose credential failed validation cannot be active',
137
+ });
138
+ }
139
+ });