@oxy.so/contracts 1.0.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 (147) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +16 -0
  3. package/dist/cjs/.tsbuildinfo +1 -0
  4. package/dist/cjs/accountGraph.js +489 -0
  5. package/dist/cjs/agency.js +439 -0
  6. package/dist/cjs/browserHub.js +215 -0
  7. package/dist/cjs/civic.js +163 -0
  8. package/dist/cjs/commonsSignIn.js +59 -0
  9. package/dist/cjs/deviceBoot.js +50 -0
  10. package/dist/cjs/deviceDirectory.js +189 -0
  11. package/dist/cjs/devicePairing.js +138 -0
  12. package/dist/cjs/deviceSession.js +164 -0
  13. package/dist/cjs/emailAgentContext.js +32 -0
  14. package/dist/cjs/followGraph.js +28 -0
  15. package/dist/cjs/identity.js +258 -0
  16. package/dist/cjs/inboxPush.js +24 -0
  17. package/dist/cjs/index.js +618 -0
  18. package/dist/cjs/inference/accountBilling.js +334 -0
  19. package/dist/cjs/inference/aliaModelRelease.js +262 -0
  20. package/dist/cjs/inference/attribution.js +106 -0
  21. package/dist/cjs/inference/catalogue.js +487 -0
  22. package/dist/cjs/inference/entitlement.js +217 -0
  23. package/dist/cjs/inference/errors.js +309 -0
  24. package/dist/cjs/inference/identifiers.js +224 -0
  25. package/dist/cjs/inference/inbox.js +105 -0
  26. package/dist/cjs/inference/modelDocumentation.js +433 -0
  27. package/dist/cjs/inference/money.js +188 -0
  28. package/dist/cjs/inference/priceVersion.js +110 -0
  29. package/dist/cjs/inference/providerConnection.js +455 -0
  30. package/dist/cjs/inference/request.js +477 -0
  31. package/dist/cjs/inference/routingPolicy.js +318 -0
  32. package/dist/cjs/inference/streamEvents.js +258 -0
  33. package/dist/cjs/inference/usage.js +329 -0
  34. package/dist/cjs/inference/version.js +105 -0
  35. package/dist/cjs/keyRecovery.js +91 -0
  36. package/dist/cjs/keyRotation.js +75 -0
  37. package/dist/cjs/links.js +68 -0
  38. package/dist/cjs/moderationReputation.js +298 -0
  39. package/dist/cjs/oauth.js +66 -0
  40. package/dist/cjs/oxyRecordTypes.js +71 -0
  41. package/dist/cjs/protocol.js +53 -0
  42. package/dist/cjs/recommendations.js +168 -0
  43. package/dist/cjs/reputation.js +297 -0
  44. package/dist/cjs/sessionStatus.js +121 -0
  45. package/dist/cjs/transparency.js +89 -0
  46. package/dist/cjs/updates.js +252 -0
  47. package/dist/cjs/userInvalidation.js +89 -0
  48. package/dist/cjs/userResponse.js +245 -0
  49. package/dist/cjs/username.js +290 -0
  50. package/dist/cjs/webauthn.js +71 -0
  51. package/dist/esm/.tsbuildinfo +1 -0
  52. package/dist/esm/accountGraph.js +480 -0
  53. package/dist/esm/agency.js +436 -0
  54. package/dist/esm/browserHub.js +212 -0
  55. package/dist/esm/civic.js +160 -0
  56. package/dist/esm/commonsSignIn.js +56 -0
  57. package/dist/esm/deviceBoot.js +47 -0
  58. package/dist/esm/deviceDirectory.js +186 -0
  59. package/dist/esm/devicePairing.js +135 -0
  60. package/dist/esm/deviceSession.js +161 -0
  61. package/dist/esm/emailAgentContext.js +29 -0
  62. package/dist/esm/followGraph.js +27 -0
  63. package/dist/esm/identity.js +255 -0
  64. package/dist/esm/inboxPush.js +21 -0
  65. package/dist/esm/index.js +172 -0
  66. package/dist/esm/inference/accountBilling.js +331 -0
  67. package/dist/esm/inference/aliaModelRelease.js +259 -0
  68. package/dist/esm/inference/attribution.js +103 -0
  69. package/dist/esm/inference/catalogue.js +484 -0
  70. package/dist/esm/inference/entitlement.js +214 -0
  71. package/dist/esm/inference/errors.js +306 -0
  72. package/dist/esm/inference/identifiers.js +221 -0
  73. package/dist/esm/inference/inbox.js +102 -0
  74. package/dist/esm/inference/modelDocumentation.js +430 -0
  75. package/dist/esm/inference/money.js +185 -0
  76. package/dist/esm/inference/priceVersion.js +107 -0
  77. package/dist/esm/inference/providerConnection.js +452 -0
  78. package/dist/esm/inference/request.js +474 -0
  79. package/dist/esm/inference/routingPolicy.js +315 -0
  80. package/dist/esm/inference/streamEvents.js +255 -0
  81. package/dist/esm/inference/usage.js +326 -0
  82. package/dist/esm/inference/version.js +102 -0
  83. package/dist/esm/keyRecovery.js +88 -0
  84. package/dist/esm/keyRotation.js +72 -0
  85. package/dist/esm/links.js +65 -0
  86. package/dist/esm/moderationReputation.js +295 -0
  87. package/dist/esm/oauth.js +63 -0
  88. package/dist/esm/oxyRecordTypes.js +68 -0
  89. package/dist/esm/protocol.js +50 -0
  90. package/dist/esm/recommendations.js +165 -0
  91. package/dist/esm/reputation.js +293 -0
  92. package/dist/esm/sessionStatus.js +118 -0
  93. package/dist/esm/transparency.js +86 -0
  94. package/dist/esm/updates.js +249 -0
  95. package/dist/esm/userInvalidation.js +85 -0
  96. package/dist/esm/userResponse.js +240 -0
  97. package/dist/esm/username.js +283 -0
  98. package/dist/esm/webauthn.js +68 -0
  99. package/dist/types/.tsbuildinfo +1 -0
  100. package/dist/types/accountGraph.d.ts +378 -0
  101. package/dist/types/agency.d.ts +2162 -0
  102. package/dist/types/browserHub.d.ts +856 -0
  103. package/dist/types/civic.d.ts +338 -0
  104. package/dist/types/commonsSignIn.d.ts +58 -0
  105. package/dist/types/deviceBoot.d.ts +74 -0
  106. package/dist/types/deviceDirectory.d.ts +1317 -0
  107. package/dist/types/devicePairing.d.ts +130 -0
  108. package/dist/types/deviceSession.d.ts +411 -0
  109. package/dist/types/emailAgentContext.d.ts +248 -0
  110. package/dist/types/followGraph.d.ts +150 -0
  111. package/dist/types/identity.d.ts +402 -0
  112. package/dist/types/inboxPush.d.ts +30 -0
  113. package/dist/types/index.d.ts +100 -0
  114. package/dist/types/inference/accountBilling.d.ts +738 -0
  115. package/dist/types/inference/aliaModelRelease.d.ts +609 -0
  116. package/dist/types/inference/attribution.d.ts +176 -0
  117. package/dist/types/inference/catalogue.d.ts +1618 -0
  118. package/dist/types/inference/entitlement.d.ts +519 -0
  119. package/dist/types/inference/errors.d.ts +242 -0
  120. package/dist/types/inference/identifiers.d.ts +182 -0
  121. package/dist/types/inference/inbox.d.ts +374 -0
  122. package/dist/types/inference/modelDocumentation.d.ts +1603 -0
  123. package/dist/types/inference/money.d.ts +185 -0
  124. package/dist/types/inference/priceVersion.d.ts +182 -0
  125. package/dist/types/inference/providerConnection.d.ts +968 -0
  126. package/dist/types/inference/request.d.ts +2800 -0
  127. package/dist/types/inference/routingPolicy.d.ts +616 -0
  128. package/dist/types/inference/streamEvents.d.ts +950 -0
  129. package/dist/types/inference/usage.d.ts +1164 -0
  130. package/dist/types/inference/version.d.ts +102 -0
  131. package/dist/types/keyRecovery.d.ts +138 -0
  132. package/dist/types/keyRotation.d.ts +103 -0
  133. package/dist/types/links.d.ts +96 -0
  134. package/dist/types/moderationReputation.d.ts +487 -0
  135. package/dist/types/oauth.d.ts +86 -0
  136. package/dist/types/oxyRecordTypes.d.ts +62 -0
  137. package/dist/types/protocol.d.ts +86 -0
  138. package/dist/types/recommendations.d.ts +542 -0
  139. package/dist/types/reputation.d.ts +457 -0
  140. package/dist/types/sessionStatus.d.ts +231 -0
  141. package/dist/types/transparency.d.ts +392 -0
  142. package/dist/types/updates.d.ts +545 -0
  143. package/dist/types/userInvalidation.d.ts +94 -0
  144. package/dist/types/userResponse.d.ts +1706 -0
  145. package/dist/types/username.d.ts +265 -0
  146. package/dist/types/webauthn.d.ts +77 -0
  147. package/package.json +87 -0
@@ -0,0 +1,217 @@
1
+ "use strict";
2
+ /**
3
+ * Product entitlements — the interface a first-party product (Alia) queries to
4
+ * learn what an account is entitled to, WITHOUT learning anything it could
5
+ * mistake for a balance.
6
+ *
7
+ * ## The separation this file is
8
+ *
9
+ * #972 states the failure mode outright: confusing a product subscription with
10
+ * pay-as-you-go inference usage. So the two live in disjoint shapes here, and
11
+ * the separation is structural rather than documented:
12
+ *
13
+ * - {@link productEntitlementSchema} carries PLAN and ALLOWANCES. Allowances
14
+ * are whole integer counts of a product entitlement (API credits, included
15
+ * requests) — `z.number().int()`, never `exactDecimalSchema`, so an allowance
16
+ * is not even the same TYPE as money and cannot be added to one.
17
+ * - `accountBalanceSchema` (in `accountBilling.ts`) carries MONEY, as exact
18
+ * decimal strings.
19
+ *
20
+ * There is no field anywhere that is both, and no schema that sums them. A
21
+ * consumer wanting "what can this account do right now" reads both sections and
22
+ * presents both; a consumer that only wanted one is not handed the other in a
23
+ * form it can accidentally arithmetic.
24
+ *
25
+ * ## Allowances do not change what a request COSTS
26
+ *
27
+ * An Alia plan may include an allowance of inference. Oxy still records the
28
+ * exact underlying cost of every request against the account's ledger — the
29
+ * allowance is a PRODUCT-side entitlement that decides what Alia charges its
30
+ * user, not a discount applied to the receipt. That is why
31
+ * {@link productEntitlementSchema} names no price and no currency: a plan that
32
+ * could restate the cost of a request would be a second pricing authority, and
33
+ * a receipt has exactly one.
34
+ *
35
+ * ## Cost centres
36
+ *
37
+ * A first-party cost centre IS an Oxy project account — `users.kind` already has
38
+ * `project`, and an application's `ownerAccountId` already points at one. So a
39
+ * cost centre adds a LABEL and a slug to an account rather than a parallel
40
+ * hierarchy, which is the epic's "do not add a second organization model" rule
41
+ * applied to internal accounting.
42
+ *
43
+ * Decided in: docs/adr/0014-account-billing-and-entitlements.md.
44
+ */
45
+ Object.defineProperty(exports, "__esModule", { value: true });
46
+ exports.productEntitlementSchema = exports.costCenterSpendSchema = exports.costCenterSchema = exports.costCenterStatusSchema = exports.COST_CENTER_STATUSES = exports.payAsYouGoEntitlementSchema = exports.productPlanSchema = exports.planAllowanceSchema = exports.LIVE_PRODUCT_PLAN_STATUSES = exports.productPlanStatusSchema = exports.PRODUCT_PLAN_STATUSES = void 0;
47
+ const zod_1 = require("zod");
48
+ const identifiers_1 = require("./identifiers");
49
+ const accountBilling_1 = require("./accountBilling");
50
+ const money_1 = require("./money");
51
+ /* -------------------------------------------------------------------------- */
52
+ /* Plans and allowances */
53
+ /* -------------------------------------------------------------------------- */
54
+ /**
55
+ * The statuses a subscription may be mirrored in.
56
+ *
57
+ * All of the processor's, not just the ones this platform sells: a mirror that
58
+ * cannot represent what it mirrors freezes at its previous value, and a
59
+ * subscription the processor moved to `paused` would keep granting a plan
60
+ * nobody is paying for.
61
+ */
62
+ exports.PRODUCT_PLAN_STATUSES = [
63
+ 'active',
64
+ 'canceled',
65
+ 'incomplete',
66
+ 'incomplete_expired',
67
+ 'past_due',
68
+ 'paused',
69
+ 'trialing',
70
+ 'unpaid',
71
+ ];
72
+ exports.productPlanStatusSchema = zod_1.z.enum(exports.PRODUCT_PLAN_STATUSES);
73
+ /**
74
+ * The statuses that mean the plan is LIVE.
75
+ *
76
+ * Exported because "is this entitlement in force" must have one answer across
77
+ * Oxy and every product consuming it — a consumer deriving its own list is how
78
+ * `past_due` comes to be honoured in one place and refused in another.
79
+ */
80
+ exports.LIVE_PRODUCT_PLAN_STATUSES = ['active', 'trialing'];
81
+ /**
82
+ * One allowance included in a plan.
83
+ *
84
+ * `remaining` is optional and absent means "not metered against this allowance
85
+ * here" — NOT zero. A consumer that read an absent allowance as exhausted would
86
+ * refuse a user who has spent nothing.
87
+ */
88
+ exports.planAllowanceSchema = zod_1.z
89
+ .object({
90
+ /** A stable machine name, e.g. `api_credits_per_month`. */
91
+ key: zod_1.z.string().regex(/^[a-z][a-z0-9_]{0,62}$/),
92
+ /** Whole units included per period. Never money. */
93
+ included: zod_1.z.number().int().nonnegative().safe(),
94
+ remaining: zod_1.z.number().int().nonnegative().safe().optional(),
95
+ })
96
+ .strict();
97
+ /**
98
+ * The plan an account is on, if any.
99
+ *
100
+ * `price` is deliberately absent. What a customer pays for their plan is the
101
+ * processor's record and this platform's `billing_transactions`; restating it
102
+ * here would put a second price authority in the one interface whose whole job
103
+ * is to keep product pricing and inference cost apart.
104
+ */
105
+ exports.productPlanSchema = zod_1.z
106
+ .object({
107
+ id: zod_1.z.string().min(1).max(64),
108
+ name: zod_1.z.string().min(1).max(120),
109
+ status: exports.productPlanStatusSchema,
110
+ live: zod_1.z.boolean(),
111
+ currentPeriodStart: zod_1.z.string().datetime(),
112
+ currentPeriodEnd: zod_1.z.string().datetime(),
113
+ cancelAtPeriodEnd: zod_1.z.boolean(),
114
+ allowances: zod_1.z.array(exports.planAllowanceSchema),
115
+ })
116
+ .strict();
117
+ /* -------------------------------------------------------------------------- */
118
+ /* Pay-as-you-go position */
119
+ /* -------------------------------------------------------------------------- */
120
+ /**
121
+ * The account's inference-spend position, summarised for a product consumer.
122
+ *
123
+ * A REDUCTION of `accountBillingStateSchema`, not a copy: a product asking "may
124
+ * this account run another request" needs to know whether spending is possible
125
+ * and roughly how much room is left, and does not need the bucket breakdown.
126
+ * `promotionalBalance` and `purchasedBalance` are still separate — the rule that
127
+ * a grant and a purchase are never one number does not relax because the
128
+ * consumer is first-party.
129
+ */
130
+ exports.payAsYouGoEntitlementSchema = zod_1.z
131
+ .object({
132
+ /** The account that actually pays — the nearest ancestor with a profile. */
133
+ billingAccountId: identifiers_1.oxyAccountIdSchema,
134
+ currency: money_1.currencyCodeSchema,
135
+ billingMode: accountBilling_1.billingModeSchema,
136
+ purchasedBalance: money_1.exactDecimalSchema,
137
+ promotionalBalance: money_1.exactDecimalSchema,
138
+ availableToSpend: money_1.exactDecimalSchema,
139
+ /** False when the profile is suspended, closed, or out of room. */
140
+ canSpend: zod_1.z.boolean(),
141
+ })
142
+ .strict();
143
+ /* -------------------------------------------------------------------------- */
144
+ /* Cost centres */
145
+ /* -------------------------------------------------------------------------- */
146
+ exports.COST_CENTER_STATUSES = ['active', 'retired'];
147
+ exports.costCenterStatusSchema = zod_1.z.enum(exports.COST_CENTER_STATUSES);
148
+ /**
149
+ * An internal cost centre — an Oxy account that first-party spend is attributed
150
+ * to, with a stable slug so a report can name it without an id.
151
+ *
152
+ * The account IS the cost centre; this shape only labels it. There is no
153
+ * `parentId` here for the same reason: the account graph already has one, and a
154
+ * second parent link would be a second hierarchy that can disagree with it.
155
+ */
156
+ exports.costCenterSchema = zod_1.z
157
+ .object({
158
+ /** See `version.ts`: this shape is served to Console and to Alia. */
159
+ schemaVersion: zod_1.z.literal(1),
160
+ accountId: identifiers_1.oxyAccountIdSchema,
161
+ slug: zod_1.z.string().regex(/^[a-z0-9][a-z0-9-]{0,62}$/),
162
+ label: zod_1.z.string().min(1).max(120),
163
+ status: exports.costCenterStatusSchema,
164
+ createdAt: zod_1.z.string().datetime(),
165
+ updatedAt: zod_1.z.string().datetime(),
166
+ })
167
+ .strict();
168
+ /**
169
+ * What one cost centre spent over a window.
170
+ *
171
+ * `billedAmount` comes from settled receipts — the FINANCIAL ledger — never from
172
+ * telemetry sums, per #972 workstream 8. `requestCount` is a count of receipts,
173
+ * so the two are always about the same set of rows.
174
+ */
175
+ exports.costCenterSpendSchema = zod_1.z
176
+ .object({
177
+ /** See `version.ts`: this shape is served to Console and to Alia. */
178
+ schemaVersion: zod_1.z.literal(1),
179
+ costCenter: exports.costCenterSchema,
180
+ currency: money_1.currencyCodeSchema,
181
+ periodStart: zod_1.z.string().datetime(),
182
+ periodEnd: zod_1.z.string().datetime(),
183
+ billedAmount: money_1.exactDecimalSchema,
184
+ requestCount: zod_1.z.number().int().nonnegative().safe(),
185
+ })
186
+ .strict();
187
+ /* -------------------------------------------------------------------------- */
188
+ /* The interface Alia queries */
189
+ /* -------------------------------------------------------------------------- */
190
+ /**
191
+ * Everything a product needs to decide what an account may do, in one read.
192
+ *
193
+ * The three sections never merge:
194
+ *
195
+ * - `plan` + `allowances` — the product subscription.
196
+ * - `payAsYouGo` — inference money. `null` when the account has no billing
197
+ * profile anywhere up its ancestry, which is a REAL and distinct state from a
198
+ * zero balance: nobody has decided who pays for this account yet.
199
+ * - `costCenter` — where first-party spend is booked, `null` for a customer.
200
+ */
201
+ exports.productEntitlementSchema = zod_1.z
202
+ .object({
203
+ /** See `version.ts`: this shape is served to Console and to Alia. */
204
+ schemaVersion: zod_1.z.literal(1),
205
+ accountId: identifiers_1.oxyAccountIdSchema,
206
+ plan: exports.productPlanSchema.nullable(),
207
+ /**
208
+ * Allowances in force right now, whether they came from a plan or from the
209
+ * platform's own free tier. Whole counts; never money.
210
+ */
211
+ allowances: zod_1.z.array(exports.planAllowanceSchema),
212
+ payAsYouGo: exports.payAsYouGoEntitlementSchema.nullable(),
213
+ costCenter: exports.costCenterSchema.nullable(),
214
+ /** When this view was computed. Entitlements are eventually consistent. */
215
+ resolvedAt: zod_1.z.string().datetime(),
216
+ })
217
+ .strict();
@@ -0,0 +1,309 @@
1
+ "use strict";
2
+ /**
3
+ * Inference errors and retryability.
4
+ *
5
+ * One closed set of codes, shared by the Oxy public edge, the data plane and
6
+ * every SDK.
7
+ * Closed because the alternative — a free-form `code` string — makes a client's
8
+ * error handling a guess about the producer's spelling, and makes "is this
9
+ * worth retrying" a decision every consumer re-derives from prose.
10
+ *
11
+ * Retryability is carried explicitly and is CONSTRAINED by the code: a code
12
+ * that can never succeed on a bare retry (an invalid request, a denied
13
+ * permission, an insufficient balance) cannot claim `retryable: true`. Without
14
+ * that constraint the field is advisory, and one producer setting it optimistically
15
+ * turns every client into a retry storm against a request that will never pass.
16
+ *
17
+ * The provider passthrough exists so a customer can see what the upstream said
18
+ * without Oxy having to interpret every provider's error vocabulary — but it is
19
+ * the single most likely place for an upstream credential to escape, because
20
+ * provider errors routinely echo the request that caused them. It is therefore
21
+ * a `.strict()` object of four fields with no room for headers or a request
22
+ * body, and its free text is refused if it looks like it contains a credential.
23
+ *
24
+ * Decided in: docs/adr/0010-public-api-compatibility.md.
25
+ */
26
+ Object.defineProperty(exports, "__esModule", { value: true });
27
+ exports.inferenceErrorSchema = exports.providerErrorPassthroughSchema = exports.upstreamErrorCategorySchema = exports.safeErrorTextSchema = exports.NON_RETRYABLE_INFERENCE_ERROR_CODES = exports.inferenceErrorCodeSchema = exports.INFERENCE_ERROR_CODES = void 0;
28
+ const zod_1 = require("zod");
29
+ const identifiers_1 = require("./identifiers");
30
+ /**
31
+ * The closed set of inference error codes.
32
+ *
33
+ * Grouped by who must act: the caller (`invalid_request` … `idempotency_conflict`),
34
+ * the account owner (`insufficient_balance`, `spending_limit_exceeded`,
35
+ * `quota_exceeded`, `byok_credential_invalid`), routing/permission policy
36
+ * (`policy_violation`, `commercial_permission_denied`, `no_route_available`),
37
+ * and the platform or its upstreams (everything from `deployment_unavailable`).
38
+ *
39
+ * The platform group is NOT uniformly retryable, and that is the point of
40
+ * `provider_credential_invalid` sitting in it: an upstream that refuses the
41
+ * PLATFORM's own credential fails every identical retry until an operator
42
+ * rotates a key, so classifying it as `provider_error` would send every client
43
+ * into a retry loop against a request that cannot succeed.
44
+ *
45
+ * `provider_billing_refused` is in that group for the same reason and was found
46
+ * the same way — an upstream declining to bill OXY (Anthropic answers 402) has
47
+ * to be distinguishable from the customer's own balance running out, or the
48
+ * error tells them to go and top up an account that is not the one at fault.
49
+ */
50
+ exports.INFERENCE_ERROR_CODES = [
51
+ 'invalid_request',
52
+ 'authentication_failed',
53
+ 'permission_denied',
54
+ 'insufficient_scope',
55
+ 'model_not_found',
56
+ 'unsupported_modality',
57
+ 'context_length_exceeded',
58
+ 'request_too_large',
59
+ 'output_limit_exceeded',
60
+ 'idempotency_conflict',
61
+ 'insufficient_balance',
62
+ 'spending_limit_exceeded',
63
+ 'quota_exceeded',
64
+ 'byok_credential_invalid',
65
+ 'policy_violation',
66
+ 'commercial_permission_denied',
67
+ 'no_route_available',
68
+ 'upstream_content_filtered',
69
+ 'cancelled',
70
+ 'rate_limited',
71
+ 'deployment_unavailable',
72
+ 'provider_error',
73
+ 'provider_timeout',
74
+ 'provider_overloaded',
75
+ 'provider_credential_invalid',
76
+ 'provider_billing_refused',
77
+ 'service_unavailable',
78
+ 'internal_error',
79
+ ];
80
+ exports.inferenceErrorCodeSchema = zod_1.z.enum(exports.INFERENCE_ERROR_CODES);
81
+ /**
82
+ * Codes for which an identical retried request cannot succeed.
83
+ *
84
+ * `rate_limited` and `quota_exceeded` sit on opposite sides of this line
85
+ * deliberately: a rate limit clears on its own within the window the response
86
+ * names, while a quota is an account-level ceiling that only a human raises.
87
+ * `cancelled` is here because the caller already withdrew the request; a client
88
+ * that retries it is contradicting its own cancellation.
89
+ *
90
+ * `byok_credential_invalid` and `provider_credential_invalid` are the same
91
+ * failure seen from the two sides of the BYOK boundary — the customer's own
92
+ * upstream credential and the platform's — and they are two codes rather than
93
+ * one because only the first names an action the customer can take. Both are
94
+ * non-retryable for the same reason: a credential an upstream has refused keeps
95
+ * being refused until somebody replaces it.
96
+ *
97
+ * `quota_exceeded` and `provider_billing_refused` divide along the same line:
98
+ * both are money, but one is the CUSTOMER's ceiling and the other is Oxy's
99
+ * account with an upstream. Reporting the second as the first is retryability-
100
+ * correct and diagnostically wrong, which is the worst combination — it reads
101
+ * as actionable and the action does nothing.
102
+ */
103
+ exports.NON_RETRYABLE_INFERENCE_ERROR_CODES = [
104
+ 'invalid_request',
105
+ 'authentication_failed',
106
+ 'permission_denied',
107
+ 'insufficient_scope',
108
+ 'model_not_found',
109
+ 'unsupported_modality',
110
+ 'context_length_exceeded',
111
+ 'request_too_large',
112
+ 'output_limit_exceeded',
113
+ 'idempotency_conflict',
114
+ 'insufficient_balance',
115
+ 'spending_limit_exceeded',
116
+ 'quota_exceeded',
117
+ 'byok_credential_invalid',
118
+ 'policy_violation',
119
+ 'commercial_permission_denied',
120
+ 'no_route_available',
121
+ 'upstream_content_filtered',
122
+ 'cancelled',
123
+ 'provider_credential_invalid',
124
+ 'provider_billing_refused',
125
+ ];
126
+ const NON_RETRYABLE_CODE_SET = new Set(exports.NON_RETRYABLE_INFERENCE_ERROR_CODES);
127
+ /* -------------------------------------------------------------------------- */
128
+ /* Credential-shaped text */
129
+ /* -------------------------------------------------------------------------- */
130
+ /**
131
+ * A run of characters long enough and opaque enough to BE a credential.
132
+ *
133
+ * The alphabet every bearer token, API key and base64/base64url secret is
134
+ * written in. The LENGTH floors below are what keep this from being an entropy
135
+ * heuristic: nothing here fires on a short word, so `authorization: none` and
136
+ * `api_key=***` read as what they are.
137
+ */
138
+ const OPAQUE_ALPHABET = '[A-Za-z0-9][A-Za-z0-9._~+/=-]';
139
+ /**
140
+ * Words a producer substitutes FOR a credential.
141
+ *
142
+ * Excluded at the value position so a message whose secret has already been
143
+ * replaced is accepted. That acceptance is deliberate and is half the fix for
144
+ * issue #1027: the previous pattern refused `Authorization: [redacted]` — a
145
+ * correctly redacted string — which is precisely what pushed a producer into
146
+ * redacting the MARKER instead, and a marker-redacted string carries the secret
147
+ * and passes.
148
+ */
149
+ const PLACEHOLDER_WORDS = 'redacted|removed|hidden|masked|scrubbed|elided|omitted|filtered|sanitized|sanitised|none|null|undefined|empty';
150
+ /** A value position whose contents are a placeholder rather than a secret. */
151
+ const NOT_A_PLACEHOLDER = `(?!(?:${PLACEHOLDER_WORDS})\\b)`;
152
+ /**
153
+ * Header and parameter names that carry a credential, as any provider spells
154
+ * them.
155
+ *
156
+ * The prefix group is the whole point of the rewrite: `authorization` and
157
+ * `api_key` were matched literally, so `x-api-key`, `anthropic-api-key`,
158
+ * `x-goog-api-key` and `proxy-authorization` — the spellings an upstream
159
+ * actually echoes — went unrecognised.
160
+ */
161
+ const CREDENTIAL_NAME = '(?:[a-z0-9]{1,20}[-_]){0,3}(?:api[-_]?(?:key|token|secret)|authorization|auth[-_]?(?:token|key)?|access[-_]?token|id[-_]?token|refresh[-_]?token|bearer[-_]?token|secret[-_]?key|private[-_]?key|client[-_]?secret|session[-_]?(?:id|key|token)|passwords?|passwd|cookie|credentials?|tokens?|secrets?)';
162
+ /** An auth scheme sitting between the marker and the value. */
163
+ const AUTH_SCHEME = '(?:(?:bearer|basic|token|apikey|digest)\\s+)?';
164
+ /**
165
+ * The four ways a credential is recognisable in free text.
166
+ *
167
+ * Each is checked independently, so removing one signal does not clear the
168
+ * string — which is the failure #1027 reported. All four are load-bearing:
169
+ * `inference.errors.test.ts` has a case that only one of them catches, and
170
+ * deleting any one entry turns a test red.
171
+ */
172
+ const CREDENTIAL_PATTERNS = [
173
+ // 1. A credential-bearing name ASSIGNED a value that is long enough to be a
174
+ // credential. The value is anchored to the separator so a placeholder at
175
+ // that position ends the match rather than being skipped over.
176
+ new RegExp(`(?:^|[^a-z0-9])${CREDENTIAL_NAME}["']?\\s*[:=]\\s*["']?${AUTH_SCHEME}${NOT_A_PLACEHOLDER}${OPAQUE_ALPHABET}{7,}`, 'i'),
177
+ // 2. A bearer token with no marker in front of it, which is how an upstream
178
+ // quotes the header value alone.
179
+ new RegExp(`\\bbearer\\s+${NOT_A_PLACEHOLDER}${OPAQUE_ALPHABET}{7,}`, 'i'),
180
+ // 3. Token grammars that ARE credentials wherever they appear, marker or not.
181
+ // This is the layer that survives a producer stripping the marker, and it
182
+ // is a closed list of issued shapes rather than an entropy score, so a
183
+ // request id or a base64 image fragment is unaffected.
184
+ //
185
+ // Case-SENSITIVE on purpose: `AKIA`, `AIza` and `gh[pousr]_` are issued in
186
+ // exactly that case, and matching them case-insensitively would start
187
+ // firing on ordinary words.
188
+ /\b(?:sk-[A-Za-z0-9_-]{8,}|[sprk]k_(?:live|test)_[A-Za-z0-9]{8,}|AKIA[0-9A-Z]{12,}|ASIA[0-9A-Z]{12,}|AIza[0-9A-Za-z_-]{20,}|gh[pousr]_[A-Za-z0-9]{16,}|github_pat_[A-Za-z0-9_]{20,}|xox[abeprs]-[A-Za-z0-9-]{10,}|glpat-[A-Za-z0-9_-]{16,}|npm_[A-Za-z0-9]{20,}|eyJ[A-Za-z0-9_-]{6,}\.[A-Za-z0-9_-]{6,}\.[A-Za-z0-9_-]{4,})/,
189
+ // 4. A redaction placeholder standing NEXT TO a surviving opaque value — the
190
+ // exact residue of the span redaction in #1027 (`{x-[redacted] <key>}`).
191
+ // A correct redaction puts the placeholder WHERE the value was, so the two
192
+ // never appear side by side; a marker-span redaction leaves them adjacent.
193
+ // Both signals are required, which is what keeps an ordinary redacted
194
+ // message from being refused.
195
+ new RegExp(`(?:[[<({]\\s*(?:${PLACEHOLDER_WORDS})[^\\])}>]{0,16}[\\])}>]|\\*{3,})[^A-Za-z0-9]{0,4}${OPAQUE_ALPHABET}{11,}`, 'i'),
196
+ ];
197
+ /**
198
+ * Free text that is safe to hand a customer: bounded, and refused if it still
199
+ * looks like it carries a credential. Applied to BOTH the Oxy message and the
200
+ * upstream one — a leak is no less a leak for having been written by a provider.
201
+ *
202
+ * ## This is a last-resort REFUSAL, not protection
203
+ *
204
+ * A pattern over the OUTPUT cannot be the control that keeps a credential out of
205
+ * an error, and a producer that treats it as one has the hole #1027 reported.
206
+ * The only reliable control is redacting the KNOWN SECRET VALUE at the point
207
+ * where the producer still holds the bytes it sent — which is an adapter's job
208
+ * and is available to nobody else. This refinement exists to catch what that
209
+ * control missed, and nothing here is a licence to skip it.
210
+ *
211
+ * Two rules follow, and they are the whole reason this text is longer than the
212
+ * pattern it describes:
213
+ *
214
+ * - **Never redact by replacing the span this pattern matched.** The span is
215
+ * the MARKER; the secret is what follows it. OxyHQ/Kaana#3 measured the
216
+ * result: `{x-api-key: <key>}` is refused, `{x-[redacted] <key>}` was
217
+ * accepted, and both carry the key. Redaction made the leak worse by
218
+ * converting "this string is dangerous" into "this string is fine".
219
+ * - **This package deliberately ships no redaction helper.** One keyed on these
220
+ * patterns would rebuild the same defect one layer up, and one that took the
221
+ * secret as an argument would only restate what the producer already has.
222
+ *
223
+ * What it still cannot see, stated so nobody relies on it: a credential with no
224
+ * marker, no issued-token prefix and no placeholder beside it is bytes that look
225
+ * like a request id, and refusing those means refusing request ids.
226
+ */
227
+ exports.safeErrorTextSchema = zod_1.z
228
+ .string()
229
+ .min(1)
230
+ .max(2000)
231
+ .refine((value) => !CREDENTIAL_PATTERNS.some((pattern) => pattern.test(value)), 'error text must not contain credential-shaped material');
232
+ /**
233
+ * A coarse classification of an upstream failure (ADR 0010's `upstreamCategory`).
234
+ *
235
+ * Distinct from {@link providerErrorPassthroughSchema}, which carries the
236
+ * upstream's OWN code and text: this is Oxy's reading of what kind of failure it
237
+ * was, in a vocabulary that is the same across every provider, so a client can
238
+ * branch on it without knowing who served the request.
239
+ */
240
+ exports.upstreamErrorCategorySchema = zod_1.z.enum([
241
+ 'rate_limit',
242
+ 'quota',
243
+ 'timeout',
244
+ 'overloaded',
245
+ 'server_error',
246
+ 'content_filter',
247
+ 'invalid_request',
248
+ 'authentication',
249
+ 'unknown',
250
+ ]);
251
+ /**
252
+ * What the upstream provider said, reduced to the four fields a customer can
253
+ * act on.
254
+ *
255
+ * `.strict()` is the security control here, not a tidiness preference: it means
256
+ * a producer cannot widen this by attaching `requestHeaders`, `curl`, `body` or
257
+ * `raw` and have it silently pass. Adding a field is a contract change with a
258
+ * version bump and a review, which is the point.
259
+ */
260
+ exports.providerErrorPassthroughSchema = zod_1.z
261
+ .object({
262
+ provider: identifiers_1.inferenceProviderSlugSchema,
263
+ /** The upstream HTTP status, when the upstream spoke HTTP. */
264
+ status: zod_1.z.number().int().min(100).max(599).optional(),
265
+ /** The upstream's own error code, verbatim and uninterpreted. */
266
+ code: zod_1.z.string().max(128).optional(),
267
+ /** The upstream's message, subject to the same credential refusal. */
268
+ message: exports.safeErrorTextSchema.optional(),
269
+ })
270
+ .strict();
271
+ /**
272
+ * The error body every inference surface returns and every stream error event
273
+ * carries.
274
+ *
275
+ * `requestId` is always present — an error a customer cannot correlate with a
276
+ * log line is an error they have to reproduce to report.
277
+ */
278
+ exports.inferenceErrorSchema = zod_1.z
279
+ .object({
280
+ /** See `version.ts`: this shape appears alone on the wire, so it is versioned. */
281
+ schemaVersion: zod_1.z.literal(1),
282
+ code: exports.inferenceErrorCodeSchema,
283
+ message: exports.safeErrorTextSchema,
284
+ retryable: zod_1.z.boolean(),
285
+ requestId: identifiers_1.requestIdSchema,
286
+ /** How long to wait before retrying. Only meaningful when `retryable`. */
287
+ retryAfterMs: zod_1.z.number().int().nonnegative().safe().optional(),
288
+ /** The request field at fault, for `invalid_request`. */
289
+ param: zod_1.z.string().max(128).optional(),
290
+ /** Present only when an upstream provider was reached and failed. */
291
+ upstreamCategory: exports.upstreamErrorCategorySchema.optional(),
292
+ providerError: exports.providerErrorPassthroughSchema.optional(),
293
+ })
294
+ .superRefine((error, ctx) => {
295
+ if (error.retryable && NON_RETRYABLE_CODE_SET.has(error.code)) {
296
+ ctx.addIssue({
297
+ code: zod_1.z.ZodIssueCode.custom,
298
+ path: ['retryable'],
299
+ message: `${error.code} can never succeed on an identical retry, so it cannot be retryable`,
300
+ });
301
+ }
302
+ if (error.retryAfterMs !== undefined && !error.retryable) {
303
+ ctx.addIssue({
304
+ code: zod_1.z.ZodIssueCode.custom,
305
+ path: ['retryAfterMs'],
306
+ message: 'retryAfterMs tells a client when to retry, so it requires retryable: true',
307
+ });
308
+ }
309
+ });