@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,329 @@
1
+ "use strict";
2
+ /**
3
+ * The reserve → settle → refund protocol.
4
+ *
5
+ * Four records, in the order they happen:
6
+ *
7
+ * 1. **Reservation** — before a request enters the data plane, Oxy holds the
8
+ * MAXIMUM the request could cost (input units + maximum output + the
9
+ * allowed route's price ceiling). A request whose account cannot cover that
10
+ * hold is rejected before anything is spent upstream.
11
+ * 2. **Usage report** — the data plane's technical account of what was
12
+ * consumed. Units and route, never money: the data plane measures, the
13
+ * control plane prices.
14
+ * 3. **Receipt** — the immutable settlement. Carries the exact units, a COPY
15
+ * of the prices they were charged at, and the amount booked. It is never
16
+ * edited afterwards.
17
+ * 4. **Refund/reversal** — an equally immutable entry that releases an unused
18
+ * hold or reverses a settled charge. Settled history is compensated, never
19
+ * rewritten, because an invoice a customer already received must remain
20
+ * reconstructible.
21
+ *
22
+ * Every one of them is keyed by an idempotency key, so a retried call, a
23
+ * redelivered event or a duplicated webhook produces the same record rather than
24
+ * a second charge.
25
+ *
26
+ * Money is an exact decimal string throughout (ADR 0009 — never integer minor
27
+ * units, never a float), and unit counts are carried separately from it (see
28
+ * `money.ts`).
29
+ *
30
+ * Decided in: docs/adr/0009-usage-reservation-and-settlement.md.
31
+ */
32
+ Object.defineProperty(exports, "__esModule", { value: true });
33
+ exports.usageRefundSchema = exports.usageRefundReasonSchema = exports.usageRefundSubjectSchema = exports.usageReceiptSchema = exports.normalizedUsageReportSchema = exports.inferenceRequestOutcomeSchema = exports.usageReservationSchema = exports.usageReservationStatusSchema = exports.usageReservationRequestSchema = void 0;
34
+ const zod_1 = require("zod");
35
+ const attribution_1 = require("./attribution");
36
+ const identifiers_1 = require("./identifiers");
37
+ const money_1 = require("./money");
38
+ const priceVersion_1 = require("./priceVersion");
39
+ /* -------------------------------------------------------------------------- */
40
+ /* 1. Reservation */
41
+ /* -------------------------------------------------------------------------- */
42
+ /**
43
+ * Ask the ledger to hold the maximum a request could cost.
44
+ *
45
+ * `maxAmount` is the number the balance check is made against, and it is
46
+ * computed from the three inputs beside it rather than guessed: the units
47
+ * already known (the prompt), the ceiling on units still to come
48
+ * (`maxOutputTokens`), and the most expensive route the policy allows. Sizing a
49
+ * hold from a typical response rather than the worst allowed one is how a
50
+ * balance goes negative on a long generation.
51
+ */
52
+ exports.usageReservationRequestSchema = zod_1.z.object({
53
+ /** See `version.ts`: exchanged between the edge and the ledger on its own. */
54
+ schemaVersion: zod_1.z.literal(1),
55
+ idempotencyKey: identifiers_1.idempotencyKeySchema,
56
+ attribution: attribution_1.inferenceAttributionSchema,
57
+ /** Units already determined by the request itself, e.g. input tokens. */
58
+ knownUnits: zod_1.z.array(money_1.usageQuantitySchema).default([]),
59
+ /** The ceiling on generated output, when the request set one. */
60
+ maxOutputTokens: zod_1.z.number().int().positive().safe().optional(),
61
+ /** The price version of the most expensive route the policy permits. */
62
+ ceilingPriceVersionId: zod_1.z.string().min(1).max(128),
63
+ maxAmount: money_1.exactDecimalSchema,
64
+ currency: money_1.currencyCodeSchema,
65
+ expiresInSeconds: zod_1.z.number().int().positive().max(86400),
66
+ });
67
+ /** Lifecycle of a hold. Terminal states are `settled`, `released`, `expired`. */
68
+ exports.usageReservationStatusSchema = zod_1.z.enum([
69
+ 'held',
70
+ 'settled',
71
+ 'released',
72
+ 'expired',
73
+ ]);
74
+ /**
75
+ * A hold placed against an account's balance.
76
+ *
77
+ * Reserved amounts are shown to customers distinctly from settled charges: a
78
+ * reservation is not money spent, and presenting the two as one number makes a
79
+ * balance appear to drop and then recover for every request.
80
+ */
81
+ exports.usageReservationSchema = zod_1.z
82
+ .object({
83
+ /** See `version.ts`: read back by the edge and the Console on its own. */
84
+ schemaVersion: zod_1.z.literal(1),
85
+ reservationId: zod_1.z.string().min(1).max(128),
86
+ idempotencyKey: identifiers_1.idempotencyKeySchema,
87
+ attribution: attribution_1.inferenceAttributionSchema,
88
+ status: exports.usageReservationStatusSchema,
89
+ reservedAmount: money_1.exactDecimalSchema,
90
+ currency: money_1.currencyCodeSchema,
91
+ ceilingPriceVersionId: zod_1.z.string().min(1).max(128),
92
+ createdAt: identifiers_1.inferenceTimestampSchema,
93
+ expiresAt: identifiers_1.inferenceTimestampSchema,
94
+ /** The receipt that consumed this hold. Present exactly when `settled`. */
95
+ settledReceiptId: zod_1.z.string().min(1).max(128).optional(),
96
+ })
97
+ .superRefine((reservation, ctx) => {
98
+ if (reservation.status === 'settled' && reservation.settledReceiptId === undefined) {
99
+ ctx.addIssue({
100
+ code: zod_1.z.ZodIssueCode.custom,
101
+ path: ['settledReceiptId'],
102
+ message: 'a settled reservation must name the receipt that settled it',
103
+ });
104
+ }
105
+ if (reservation.status !== 'settled' && reservation.settledReceiptId !== undefined) {
106
+ ctx.addIssue({
107
+ code: zod_1.z.ZodIssueCode.custom,
108
+ path: ['settledReceiptId'],
109
+ message: 'only a settled reservation has a settling receipt',
110
+ });
111
+ }
112
+ });
113
+ /* -------------------------------------------------------------------------- */
114
+ /* 2. Usage report (data plane → Oxy) */
115
+ /* -------------------------------------------------------------------------- */
116
+ /** How a request ended, from the data plane's point of view. */
117
+ exports.inferenceRequestOutcomeSchema = zod_1.z.enum([
118
+ 'completed',
119
+ 'partial',
120
+ 'cancelled',
121
+ 'failed',
122
+ ]);
123
+ /**
124
+ * The data plane's technical account of one request, and the ONLY shape a
125
+ * settlement is written from.
126
+ *
127
+ * `inferenceStreamUsageEventSchema` is not a narrower version of this one that
128
+ * could be widened into it — see that event's own comment. Its units are exact
129
+ * and settleable; the record around them is not inferable, so an edge settling
130
+ * from a stream event supplies the outcome itself and never promotes the event.
131
+ *
132
+ * No money and no price: the data plane measures units and names the route it
133
+ * used, and the control plane decides what that costs. Keeping the two apart is
134
+ * what allows a price to be corrected after the fact without re-running or
135
+ * re-measuring anything.
136
+ *
137
+ * `usageSource` is load-bearing when a provider returns no usage at all: the
138
+ * report still arrives, marked `estimated`, so settlement can apply the
139
+ * estimation policy knowingly instead of treating a reconstruction as fact.
140
+ *
141
+ * `units` is a PARTITION of what the request consumed, not a set of totals with
142
+ * details hanging off them — see `USAGE_UNITS` in `money.ts`. Reporting a provider's
143
+ * nested `prompt_tokens`/`completion_tokens` verbatim charges the cached and
144
+ * reasoning tokens twice, so subtracting the children out is part of what
145
+ * "normalized" means in this shape's name.
146
+ *
147
+ * **A `completed` report carries at least one unit, and the other outcomes need
148
+ * not.** `completed` is the one outcome that asserts the customer received the
149
+ * whole answer, so "delivered in full, consumed nothing measurable" is a
150
+ * contradiction — and it is one that BILLS NOTHING: settlement prices every
151
+ * reported unit and sums, so an empty list is a free request produced by a
152
+ * provider that simply omitted its usage block. The refinement makes that shape
153
+ * unparseable, and the policy behind it is refuse-and-release: the report is
154
+ * rejected and the hold is released, never estimated and charged.
155
+ *
156
+ * The three other outcomes legitimately carry none, which is why the rule is
157
+ * conditional rather than a `.min(1)` on the field. In the reference data plane
158
+ * `failed` is DERIVED from having no units — `outcomeFor` in
159
+ * Kaana's `internal/kaana/executor.go` returns `partial` when units exist and `failed`
160
+ * when they do not — and `cancelled` is reported for a client that stopped
161
+ * before anything was measured. An unconditional minimum would refuse those
162
+ * reports, and a refused report is a request that ran, cost money upstream and
163
+ * can never be settled or refunded.
164
+ */
165
+ exports.normalizedUsageReportSchema = zod_1.z
166
+ .object({
167
+ /** See `version.ts`: emitted by the data plane as a whole message. */
168
+ schemaVersion: zod_1.z.literal(2),
169
+ requestId: identifiers_1.requestIdSchema,
170
+ generationId: identifiers_1.generationIdSchema.optional(),
171
+ attribution: attribution_1.inferenceAttributionSchema,
172
+ outcome: exports.inferenceRequestOutcomeSchema,
173
+ units: zod_1.z.array(money_1.usageQuantitySchema),
174
+ usageSource: money_1.usageSourceSchema,
175
+ resolvedModelReference: identifiers_1.modelReferenceSchema,
176
+ servingProvider: identifiers_1.inferenceProviderSlugSchema,
177
+ /** Exact Kaana deployment that attempted the request; never inferred from names. */
178
+ deploymentId: identifiers_1.deploymentIdSchema,
179
+ /** How many allowed route switches occurred while serving this request. */
180
+ routeSwitches: zod_1.z.number().int().nonnegative().max(100),
181
+ startedAt: identifiers_1.inferenceTimestampSchema,
182
+ completedAt: identifiers_1.inferenceTimestampSchema,
183
+ timeToFirstTokenMs: zod_1.z.number().int().nonnegative().safe().optional(),
184
+ })
185
+ .superRefine((report, ctx) => {
186
+ // Compared as instants: two spellings of one moment sort differently as text.
187
+ if (Date.parse(report.completedAt) < Date.parse(report.startedAt)) {
188
+ ctx.addIssue({
189
+ code: zod_1.z.ZodIssueCode.custom,
190
+ path: ['completedAt'],
191
+ message: 'a request cannot complete before it started',
192
+ });
193
+ }
194
+ const units = report.units.map((quantity) => quantity.unit);
195
+ if (new Set(units).size !== units.length) {
196
+ ctx.addIssue({
197
+ code: zod_1.z.ZodIssueCode.custom,
198
+ path: ['units'],
199
+ message: 'each unit is reported once, as a total',
200
+ });
201
+ }
202
+ if (report.outcome === 'completed' && report.units.length === 0) {
203
+ ctx.addIssue({
204
+ code: zod_1.z.ZodIssueCode.custom,
205
+ path: ['units'],
206
+ message: 'a completed request consumed something; report at least one unit',
207
+ });
208
+ }
209
+ });
210
+ /* -------------------------------------------------------------------------- */
211
+ /* 3. Receipt (settlement) */
212
+ /* -------------------------------------------------------------------------- */
213
+ /**
214
+ * The immutable record of what a request was actually charged.
215
+ *
216
+ * `priceSnapshot` is a COPY of the unit prices applied, not just the id of the
217
+ * price version they came from, so the arithmetic on this receipt can be
218
+ * checked years later without depending on any other record still existing.
219
+ *
220
+ * `platformFeeOnly` marks a BYOK request: the upstream provider billed the
221
+ * customer's own account directly, and `billedAmount` is Oxy's service fee
222
+ * rather than the cost of the tokens. Without the flag, the two look identical
223
+ * and a BYOK customer appears to have been charged twice for one request.
224
+ */
225
+ exports.usageReceiptSchema = zod_1.z
226
+ .object({
227
+ /** See `version.ts`: returned to customers by `GET /v1/generations/:id`. */
228
+ schemaVersion: zod_1.z.literal(1),
229
+ receiptId: zod_1.z.string().min(1).max(128),
230
+ /** The hold this settled against. Absent for a charge with no reservation. */
231
+ reservationId: zod_1.z.string().min(1).max(128).optional(),
232
+ idempotencyKey: identifiers_1.idempotencyKeySchema,
233
+ attribution: attribution_1.inferenceAttributionSchema,
234
+ outcome: exports.inferenceRequestOutcomeSchema,
235
+ units: zod_1.z.array(money_1.usageQuantitySchema).min(1),
236
+ usageSource: money_1.usageSourceSchema,
237
+ priceSnapshot: priceVersion_1.priceSnapshotSchema,
238
+ billedAmount: money_1.exactDecimalSchema,
239
+ currency: money_1.currencyCodeSchema,
240
+ platformFeeOnly: zod_1.z.boolean(),
241
+ resolvedModelReference: identifiers_1.modelReferenceSchema,
242
+ servingProvider: identifiers_1.inferenceProviderSlugSchema,
243
+ settledAt: identifiers_1.inferenceTimestampSchema,
244
+ })
245
+ .superRefine((receipt, ctx) => {
246
+ if (receipt.currency !== receipt.priceSnapshot.currency) {
247
+ ctx.addIssue({
248
+ code: zod_1.z.ZodIssueCode.custom,
249
+ path: ['priceSnapshot', 'currency'],
250
+ message: 'a receipt must be settled in the currency it was priced in',
251
+ });
252
+ }
253
+ const units = receipt.units.map((quantity) => quantity.unit);
254
+ if (new Set(units).size !== units.length) {
255
+ ctx.addIssue({
256
+ code: zod_1.z.ZodIssueCode.custom,
257
+ path: ['units'],
258
+ message: 'each unit is settled once, as a total',
259
+ });
260
+ }
261
+ });
262
+ /* -------------------------------------------------------------------------- */
263
+ /* 4. Refund / reversal */
264
+ /* -------------------------------------------------------------------------- */
265
+ /**
266
+ * What a reversal acts on: an unused hold, or a settled charge.
267
+ *
268
+ * A discriminated union, so "release the rest of the hold" and "give back money
269
+ * already booked" can never be confused for one another — they hit different
270
+ * ledger accounts and only the second one is visible on an invoice.
271
+ */
272
+ exports.usageRefundSubjectSchema = zod_1.z.discriminatedUnion('kind', [
273
+ zod_1.z
274
+ .object({ kind: zod_1.z.literal('reservation'), reservationId: zod_1.z.string().min(1).max(128) })
275
+ .strict(),
276
+ zod_1.z.object({ kind: zod_1.z.literal('receipt'), receiptId: zod_1.z.string().min(1).max(128) }).strict(),
277
+ ]);
278
+ /** Why money was given back or a hold was released. */
279
+ exports.usageRefundReasonSchema = zod_1.z.enum([
280
+ 'unused_reservation',
281
+ 'client_cancelled',
282
+ 'upstream_failure',
283
+ 'partial_stream',
284
+ 'usage_unavailable',
285
+ 'billing_correction',
286
+ 'duplicate_charge',
287
+ ]);
288
+ /** Reasons that can only ever act on a settled charge. */
289
+ const RECEIPT_ONLY_REFUND_REASONS = new Set([
290
+ 'billing_correction',
291
+ 'duplicate_charge',
292
+ ]);
293
+ /**
294
+ * An immutable reversal entry.
295
+ *
296
+ * `amount` is non-negative like every other amount in this contract: the
297
+ * direction is carried by the record being a refund, not by a sign that a
298
+ * consumer could read the wrong way round. Idempotent by key, so a retried
299
+ * refund releases the same money once.
300
+ */
301
+ exports.usageRefundSchema = zod_1.z
302
+ .object({
303
+ /** See `version.ts`: a ledger record read back on its own. */
304
+ schemaVersion: zod_1.z.literal(1),
305
+ refundId: zod_1.z.string().min(1).max(128),
306
+ idempotencyKey: identifiers_1.idempotencyKeySchema,
307
+ attribution: attribution_1.inferenceAttributionSchema,
308
+ subject: exports.usageRefundSubjectSchema,
309
+ reason: exports.usageRefundReasonSchema,
310
+ amount: money_1.exactDecimalSchema,
311
+ currency: money_1.currencyCodeSchema,
312
+ createdAt: identifiers_1.inferenceTimestampSchema,
313
+ })
314
+ .superRefine((refund, ctx) => {
315
+ if (refund.reason === 'unused_reservation' && refund.subject.kind !== 'reservation') {
316
+ ctx.addIssue({
317
+ code: zod_1.z.ZodIssueCode.custom,
318
+ path: ['reason'],
319
+ message: 'an unused reservation is released against the reservation, not a receipt',
320
+ });
321
+ }
322
+ if (RECEIPT_ONLY_REFUND_REASONS.has(refund.reason) && refund.subject.kind !== 'receipt') {
323
+ ctx.addIssue({
324
+ code: zod_1.z.ZodIssueCode.custom,
325
+ path: ['reason'],
326
+ message: `${refund.reason} reverses a settled charge, so it acts on a receipt`,
327
+ });
328
+ }
329
+ });
@@ -0,0 +1,105 @@
1
+ "use strict";
2
+ /**
3
+ * Version rule for the Oxy↔data-plane inference contracts.
4
+ *
5
+ * Oxy is the control plane; the inference data plane is a separate service.
6
+ * They are deployed independently, in different repositories, possibly in
7
+ * different languages.
8
+ * Every shape they exchange therefore carries its version IN THE PARSED DATA,
9
+ * never as a comment or an out-of-band assumption, so a producer running ahead
10
+ * of a consumer fails loudly at the parse instead of being silently reinterpreted.
11
+ *
12
+ * The rule, enforced by `src/__tests__/inference.compatibility.test.ts`:
13
+ *
14
+ * - A schema carries `schemaVersion: z.literal(<n>)` **if and only if** it can
15
+ * appear on the wire as a whole message — a request envelope, a stream event,
16
+ * a catalogue descriptor, a ledger record, an error body.
17
+ * - A schema that only ever appears EMBEDDED inside such a message (the
18
+ * attribution block, one message part, one usage quantity, a data-retention
19
+ * policy) carries no version of its own: it inherits the version of the
20
+ * envelope it rides in. Versioning it separately would create two versions
21
+ * that can disagree about one byte stream.
22
+ * - A shape that is BOTH — `inferenceErrorSchema` is returned as an HTTP body
23
+ * and also rides inside the stream's error event — keeps its own version.
24
+ * The envelope's version then governs the envelope and the payload's governs
25
+ * the payload, which is two versions of two things rather than two versions
26
+ * of one.
27
+ * - Every exported object schema in `src/inference/` must fall into exactly one
28
+ * of those groups. The compatibility test holds both lists as exact
29
+ * equalities, so a new shape that is in neither fails the build rather than
30
+ * quietly shipping unversioned.
31
+ *
32
+ * A shape's own version is bumped when its meaning changes in a way a consumer
33
+ * pinned to the previous version would misread — a field removed, a field's
34
+ * units changed, a closed enum's member given a new meaning. Adding an OPTIONAL
35
+ * field is additive and does not bump it, because a consumer on the previous
36
+ * version parses the message correctly and simply does not read the new field.
37
+ *
38
+ * ## Which shapes reject an unknown field
39
+ *
40
+ * That last rule is why the shapes EXCHANGED WITH THE DATA PLANE are not
41
+ * `.strict()` at their top level — the request envelope, the four usage records,
42
+ * the stream events, the error body, the catalogue descriptors, the price
43
+ * version. The split is a decision rather than an omission: `.strict()` and
44
+ * "adding an optional field is additive" cannot both hold on one shape, because
45
+ * a producer one minor version ahead would have its whole message REFUSED
46
+ * rather than its new field ignored. For a usage report that means a request
47
+ * already served upstream can never be settled and Oxy absorbs its cost, which
48
+ * is a worse failure than the one strictness would have caught.
49
+ *
50
+ * Their LEAVES are strict, and that is where the protection lives: a stripped
51
+ * field is the worse outcome exactly where it would be a leak or a second
52
+ * source of truth, because it disappears at this parse and survives in the
53
+ * producer, which is where somebody eventually reads it. So
54
+ * `clientRequestMetadataSchema` (no IP, ever), `moneySchema` (no convenience
55
+ * float beside the exact decimal), `providerErrorPassthroughSchema` (no
56
+ * upstream request or headers beside the message), `usageQuantitySchema` and
57
+ * `unitPriceSchema` all refuse an unknown field, while the envelope carrying
58
+ * them tolerates an additive one.
59
+ *
60
+ * A shape Oxy does NOT exchange with the data plane is strict at its top level
61
+ * too, since nothing there can run ahead of this package:
62
+ * `providerConnectionSchema`, where an unknown field is how a BYOK credential
63
+ * escapes, and the billing and entitlement records, where one is a second
64
+ * number beside an exact amount.
65
+ *
66
+ * A SIGNED document is strict at its top level for a third reason, and there it
67
+ * is forced rather than chosen. `aliaModelReleaseManifestSchema` carries
68
+ * signatures over its own canonical bytes, so a field stripped at this parse is a
69
+ * field missing from the bytes a verifier re-canonicalizes: a tolerant parse
70
+ * would report an invalid SIGNATURE where the truth is that this build does not
71
+ * understand the DOCUMENT. Its producer can run ahead of this package — Alia's
72
+ * release tooling is deployed independently — so the usual argument applies here
73
+ * and is outweighed, because the refusal costs an operator one retry after Oxy
74
+ * takes the newer contract, while the tolerant parse costs a misdiagnosis of a
75
+ * cryptographic failure.
76
+ *
77
+ * Decided in: docs/adr/0006-oxy-kaana-boundary.md,
78
+ * docs/adr/0010-public-api-compatibility.md,
79
+ * docs/adr/0017-authorized-routes-in-the-envelope.md.
80
+ */
81
+ Object.defineProperty(exports, "__esModule", { value: true });
82
+ exports.INFERENCE_CONTRACT_VERSION = void 0;
83
+ /**
84
+ * Version of the contract SET as a whole — the value the control plane and the
85
+ * data plane exchange in a startup/health handshake to establish that they were built against
86
+ * compatible definitions before a single inference request is served.
87
+ *
88
+ * MAJOR is bumped when any individual shape's `schemaVersion` increments (at
89
+ * least one message is now read differently by the two sides); MINOR when a
90
+ * shape or an optional field is added, when a CLOSED ENUM gains a member, or
91
+ * when a refinement changes which bytes parse; PATCH for documentation-only
92
+ * changes that leave every parsed byte identical.
93
+ *
94
+ * The last two are MINOR rather than PATCH because both produce the same
95
+ * failure: a producer on the newer set emits something the older set refuses,
96
+ * with no `schemaVersion` difference to explain it. A new enum member and a
97
+ * loosened refinement are exactly what the handshake exists to surface — a
98
+ * skew the per-message version cannot express.
99
+ *
100
+ * This constant is deliberately NOT embedded in the request envelope. Pinning a
101
+ * request to the version of the whole set would make an unrelated additive
102
+ * change to, say, the catalogue reject every in-flight inference request; the
103
+ * per-shape `schemaVersion` is what a message is validated against.
104
+ */
105
+ exports.INFERENCE_CONTRACT_VERSION = '2.0.0';
@@ -0,0 +1,91 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.backupStatusResponseSchema = exports.backupUploadRequestSchema = exports.encryptedBackupEnvelopeSchema = exports.backupLookupIdSchema = void 0;
4
+ /**
5
+ * Encrypted off-device identity backup contract (b3 Feature 1).
6
+ *
7
+ * SINGLE SOURCE OF TRUTH for the "encrypted identity backup" flow, where a
8
+ * client stores an encrypted copy of its self-custody identity key off-device so
9
+ * a lost/wiped device can be recovered from the recovery phrase ALONE — without
10
+ * the platform ever seeing the phrase, the derived encryption key, or the
11
+ * plaintext private key.
12
+ *
13
+ * Zero-knowledge design (mirrors the zero-cookie `DeviceSession.secretHash`
14
+ * pattern): the client derives, from the FULL 64-byte BIP-39 seed, both an
15
+ * encryption key (`backupKey`) and a locator (`lookupId`) via HKDF with
16
+ * domain-separated `info` labels. It uploads ONLY the XChaCha20-Poly1305
17
+ * ciphertext plus the raw `lookupId`; the server stores the ciphertext and
18
+ * `sha256(lookupId)` (never the raw `lookupId`). Restoration re-derives both
19
+ * from the phrase, fetches the envelope by `lookupId`, and decrypts locally.
20
+ *
21
+ * The server can neither locate a backup (it lacks the seed to compute the
22
+ * lookup id) nor decrypt one (it lacks the seed to compute the backup key) — a
23
+ * DB dump yields only opaque ciphertext keyed by an un-invertible hash.
24
+ *
25
+ * The producer (`@oxy.so/api`) validates its request/response against these
26
+ * schemas; the consumer (`@oxy.so/core` identity-backup mixin) validates its
27
+ * input against the same definitions, so the wire shape cannot drift.
28
+ *
29
+ * All shapes here are FLAT (no nested objects), so `z.infer<>` is safe under a
30
+ * consumer's node10 `moduleResolution`. Platform-agnostic — zod only, ESM-safe
31
+ * (no `require()`).
32
+ */
33
+ const zod_1 = require("zod");
34
+ /** 256-bit backup locator (32 bytes), lowercase/uppercase hex. */
35
+ exports.backupLookupIdSchema = zod_1.z
36
+ .string()
37
+ .trim()
38
+ .regex(/^[0-9a-fA-F]{64}$/, 'lookupId must be 64 hex characters');
39
+ /**
40
+ * The stored, self-describing encrypted backup as it lives at rest and travels
41
+ * on the public restore endpoint. Contains NO secret and NO locator: the
42
+ * `lookupId` is uploaded separately (see {@link backupUploadRequestSchema}) and
43
+ * only its hash is ever persisted.
44
+ *
45
+ * - `version` — envelope/KDF version, so a future scheme migration is
46
+ * distinguishable at rest.
47
+ * - `algorithm` — the AEAD used. Pinned literal so a mismatched decryptor
48
+ * fails loudly rather than silently.
49
+ * - `kdfInfo` — the HKDF `info` label used to derive the encryption key
50
+ * (domain-separation tag; documents exactly which context produced the key).
51
+ * - `nonce` — the 24-byte XChaCha20-Poly1305 nonce, hex.
52
+ * - `ciphertext` — the encrypted `{privateKey, publicKey, createdAt}` payload
53
+ * with the appended Poly1305 tag, hex.
54
+ * - `publicKeyHint` — a short, non-sensitive prefix of the backed-up identity's
55
+ * public key, so the owner can recognise WHICH identity a backup belongs to
56
+ * without exposing the full key. Bound into the AEAD associated data.
57
+ * - `createdAt` — ISO-8601 creation timestamp.
58
+ */
59
+ exports.encryptedBackupEnvelopeSchema = zod_1.z.object({
60
+ version: zod_1.z.number().int().positive(),
61
+ algorithm: zod_1.z.literal('xchacha20poly1305'),
62
+ kdfInfo: zod_1.z.string().min(1),
63
+ nonce: zod_1.z.string().trim().min(1),
64
+ ciphertext: zod_1.z.string().trim().min(1),
65
+ publicKeyHint: zod_1.z.string().trim().min(1),
66
+ createdAt: zod_1.z.string().trim().min(1),
67
+ });
68
+ /**
69
+ * Request body of `POST /identity/backup` — the envelope PLUS the raw
70
+ * `lookupId`. The server sha256-hashes `lookupId` before storing it (it never
71
+ * persists the raw value), and upserts by the authenticated user id so a
72
+ * re-upload REPLACES the prior backup rather than accumulating duplicates.
73
+ */
74
+ exports.backupUploadRequestSchema = exports.encryptedBackupEnvelopeSchema.extend({
75
+ /**
76
+ * The raw 256-bit backup locator (hex), derived client-side from the seed
77
+ * with a domain-separated HKDF `info`. The server stores ONLY its sha256; a
78
+ * DB dump therefore cannot recompute a locator to enumerate backups.
79
+ */
80
+ lookupId: exports.backupLookupIdSchema,
81
+ });
82
+ /**
83
+ * Response of `GET /identity/backup/status` (and the write/delete acks): whether
84
+ * the authenticated user has a stored backup, plus the non-sensitive hint +
85
+ * timestamp when one exists. Carries no ciphertext and no locator.
86
+ */
87
+ exports.backupStatusResponseSchema = zod_1.z.object({
88
+ exists: zod_1.z.boolean(),
89
+ publicKeyHint: zod_1.z.string().optional(),
90
+ createdAt: zod_1.z.string().optional(),
91
+ });
@@ -0,0 +1,75 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.rotateKeyCompleteResponseSchema = exports.rotateKeyCompleteRequestSchema = exports.rotateKeyChallengeResponseSchema = void 0;
4
+ /**
5
+ * Key-rotation contract (b3 Feature 3 — key rotation + last-credential replacement).
6
+ *
7
+ * SINGLE SOURCE OF TRUTH for the atomic key-rotation flow:
8
+ * - `POST /auth/rotate/challenge` — mint a single-use `rotate_key` challenge.
9
+ * - `POST /auth/rotate/complete` — prove control of the CURRENT (old) key and
10
+ * atomically swap in the new one.
11
+ *
12
+ * Rotation is an atomic REPLACE of the single identity key, never a
13
+ * remove-then-add, so it never passes through a zero-auth-method state and is
14
+ * independent of the unlink guards. Because the client proves possession of the
15
+ * current key (from SecureStore OR a recovery-phrase re-derivation), the LAST
16
+ * remaining credential can be replaced — the server only cares that the
17
+ * signature validates against the current `publicKey`.
18
+ *
19
+ * The API validates its output against these schemas; `@oxy.so/core`'s identity
20
+ * mixin validates its input against the same definitions, so producer and
21
+ * consumer cannot drift.
22
+ *
23
+ * All shapes here are FLAT (no nested objects), so `z.infer<>` is safe under a
24
+ * consumer's node10 `moduleResolution` — no interface-pinning needed.
25
+ *
26
+ * Platform-agnostic — zod only, ESM-safe (no `require()`).
27
+ */
28
+ const zod_1 = require("zod");
29
+ /**
30
+ * Response of `POST /auth/rotate/challenge`: the single-use `rotate_key`
31
+ * challenge the client must sign with its CURRENT key, plus its expiry.
32
+ */
33
+ exports.rotateKeyChallengeResponseSchema = zod_1.z.object({
34
+ challenge: zod_1.z.string(),
35
+ /** ISO-8601 expiry timestamp. */
36
+ expiresAt: zod_1.z.string(),
37
+ });
38
+ /**
39
+ * Request body of `POST /auth/rotate/complete`.
40
+ *
41
+ * Two proofs are required:
42
+ * - `signature` — the CURRENT (old) key signs
43
+ * `JSON.stringify({ action: 'rotate_key', userId, oldPublicKey, newPublicKey,
44
+ * challenge, timestamp })` (proves control of the key being replaced).
45
+ * - `newKeyProof` — the NEW key signs
46
+ * `JSON.stringify({ action: 'rotate_key_new', userId, newPublicKey, challenge,
47
+ * timestamp })` (proof-of-possession of the key being rotated IN; prevents an
48
+ * attacker rotating their account to a re-encoding of someone else's key they
49
+ * do not control).
50
+ *
51
+ * The request carries ONLY `newPublicKey` — `oldPublicKey` and `userId` are
52
+ * derived server-side from the authenticated user document (never
53
+ * client-supplied), so a caller cannot prove control of key X while rotating
54
+ * key Y.
55
+ */
56
+ exports.rotateKeyCompleteRequestSchema = zod_1.z.object({
57
+ newPublicKey: zod_1.z.string().trim().min(1),
58
+ challenge: zod_1.z.string().trim().min(1),
59
+ signature: zod_1.z.string().trim().min(1),
60
+ /** Proof-of-possession: the NEW key signs the rotate_key_new payload. */
61
+ newKeyProof: zod_1.z.string().trim().min(1),
62
+ timestamp: zod_1.z.number(),
63
+ /**
64
+ * When true, all OTHER active sessions for the account are revoked after a
65
+ * successful rotation (the rotating device stays signed in). Use it when the
66
+ * old key is presumed compromised.
67
+ */
68
+ signOutEverywhere: zod_1.z.boolean().optional(),
69
+ });
70
+ /** Response of `POST /auth/rotate/complete`: the account's new (rotated) public key. */
71
+ exports.rotateKeyCompleteResponseSchema = zod_1.z.object({
72
+ success: zod_1.z.boolean(),
73
+ publicKey: zod_1.z.string(),
74
+ message: zod_1.z.string(),
75
+ });
@@ -0,0 +1,68 @@
1
+ "use strict";
2
+ /**
3
+ * Link-preview / unfurl API contracts.
4
+ *
5
+ * SINGLE SOURCE OF TRUTH for the wire shape of Oxy's link-preview ("unfurl")
6
+ * resolution surface: the single `GET` lookup and the `POST` batch lookup that
7
+ * every app calls through the SDK so apps stop duplicating their own
8
+ * link-metadata fetching. The API validates its OUTPUT against these schemas;
9
+ * every consumer (`@oxy.so/core`'s link mixin and the apps that call it)
10
+ * validates its INPUT against the same definitions, so producer and consumers
11
+ * cannot drift.
12
+ *
13
+ * Design anchors:
14
+ * - Oxy owns resolution. The `image` (and `favicon`) URLs a preview carries are
15
+ * re-hosted on Oxy media (`cloud.oxy.so/<fileId>`), never raw remote URLs —
16
+ * apps render them directly with no per-app proxy.
17
+ * - Resolution is best-effort and asynchronous. A preview is `'resolved'` once
18
+ * metadata is materialised, `'pending'` while a first-seen URL is being
19
+ * fetched in the background, or `'empty'` when the target yielded no usable
20
+ * metadata. `resolvedAt` (ISO datetime) is present only once `'resolved'`.
21
+ * - The batch response is keyed by the REQUESTED url (the exact string the
22
+ * caller sent), not the canonical/final URL, so a caller can always look its
23
+ * own input back up; the canonical URL lives on `LinkPreview.url`.
24
+ *
25
+ * The `LinkPreview` / `LinkPreviewBatchResponse` exports are declared as explicit
26
+ * `interface`s (with their runtime schemas annotated `z.ZodType<Interface>`),
27
+ * following the same rationale as `UserNameResponse` in `./userResponse`: a
28
+ * `z.infer<>` of a nested-object schema can degrade to `{}` under a consumer's
29
+ * `moduleResolution: "node"` (node10) resolution. A literal interface emits the
30
+ * field types verbatim in the `.d.ts` and survives BOTH `node` and `bundler`
31
+ * resolution. The flat batch-request schema (no nested-object hazard) is inferred
32
+ * via `z.infer<>`.
33
+ *
34
+ * Platform-agnostic — zod only, no react/react-native/expo. ESM-safe (no
35
+ * `require()`).
36
+ */
37
+ Object.defineProperty(exports, "__esModule", { value: true });
38
+ exports.linkPreviewResponseSchema = exports.linkPreviewBatchResponseSchema = exports.linkPreviewBatchRequestSchema = exports.linkPreviewSchema = void 0;
39
+ const zod_1 = require("zod");
40
+ exports.linkPreviewSchema = zod_1.z.object({
41
+ url: zod_1.z.string(),
42
+ status: zod_1.z.enum(['resolved', 'pending', 'empty']),
43
+ title: zod_1.z.string().optional(),
44
+ description: zod_1.z.string().optional(),
45
+ image: zod_1.z.string().optional(),
46
+ siteName: zod_1.z.string().optional(),
47
+ favicon: zod_1.z.string().optional(),
48
+ resolvedAt: zod_1.z.string().optional(),
49
+ });
50
+ /* -------------------------------------------------------------------------- */
51
+ /* Batch request / response */
52
+ /* -------------------------------------------------------------------------- */
53
+ /**
54
+ * Request body for the batch unfurl endpoint. Between 1 and 50 URLs per call;
55
+ * the server resolves each (returning a `'pending'` placeholder for any URL it
56
+ * has not seen before and is fetching in the background).
57
+ */
58
+ exports.linkPreviewBatchRequestSchema = zod_1.z.object({
59
+ urls: zod_1.z.array(zod_1.z.string().max(2048)).min(1).max(50),
60
+ });
61
+ exports.linkPreviewBatchResponseSchema = zod_1.z.object({
62
+ data: zod_1.z.record(zod_1.z.string(), exports.linkPreviewSchema),
63
+ });
64
+ /**
65
+ * Wire shape of the single-URL unfurl lookup (`GET`) — a bare
66
+ * {@link LinkPreview}.
67
+ */
68
+ exports.linkPreviewResponseSchema = exports.linkPreviewSchema;