@oxyhq/core 21.0.0 → 21.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/dist/cjs/.tsbuildinfo +1 -1
  2. package/dist/cjs/HttpService.js +47 -8
  3. package/dist/cjs/i18n/locales/en-US.json +7 -2
  4. package/dist/cjs/i18n/locales/es-ES.json +7 -2
  5. package/dist/cjs/i18n/locales/locales/en-US.json +7 -2
  6. package/dist/cjs/i18n/locales/locales/es-ES.json +7 -2
  7. package/dist/cjs/index.js +8 -1
  8. package/dist/cjs/inference/OxyInferenceClient.js +330 -0
  9. package/dist/cjs/mixins/OxyServices.accounts.js +5 -72
  10. package/dist/cjs/mixins/OxyServices.inference.js +59 -0
  11. package/dist/cjs/mixins/OxyServices.utility.js +18 -6
  12. package/dist/cjs/mixins/index.js +6 -0
  13. package/dist/cjs/server/auth.js +76 -0
  14. package/dist/cjs/server/cors.js +84 -15
  15. package/dist/cjs/server/index.js +5 -1
  16. package/dist/esm/.tsbuildinfo +1 -1
  17. package/dist/esm/HttpService.js +47 -8
  18. package/dist/esm/i18n/locales/en-US.json +7 -2
  19. package/dist/esm/i18n/locales/es-ES.json +7 -2
  20. package/dist/esm/i18n/locales/locales/en-US.json +7 -2
  21. package/dist/esm/i18n/locales/locales/es-ES.json +7 -2
  22. package/dist/esm/index.js +4 -0
  23. package/dist/esm/inference/OxyInferenceClient.js +325 -0
  24. package/dist/esm/mixins/OxyServices.accounts.js +5 -72
  25. package/dist/esm/mixins/OxyServices.inference.js +56 -0
  26. package/dist/esm/mixins/OxyServices.utility.js +18 -6
  27. package/dist/esm/mixins/index.js +6 -0
  28. package/dist/esm/server/auth.js +72 -0
  29. package/dist/esm/server/cors.js +82 -15
  30. package/dist/esm/server/index.js +1 -1
  31. package/dist/types/.tsbuildinfo +1 -1
  32. package/dist/types/HttpService.d.ts +39 -1
  33. package/dist/types/index.d.ts +3 -1
  34. package/dist/types/inference/OxyInferenceClient.d.ts +324 -0
  35. package/dist/types/mixins/OxyServices.accounts.d.ts +73 -95
  36. package/dist/types/mixins/OxyServices.inference.d.ts +95 -0
  37. package/dist/types/mixins/OxyServices.utility.d.ts +44 -13
  38. package/dist/types/mixins/index.d.ts +2 -1
  39. package/dist/types/server/auth.d.ts +80 -0
  40. package/dist/types/server/cors.d.ts +41 -0
  41. package/dist/types/server/index.d.ts +2 -2
  42. package/package.json +2 -2
  43. package/src/HttpService.ts +50 -10
  44. package/src/__tests__/httpServiceUnwrapEnvelope.test.ts +115 -0
  45. package/src/i18n/locales/en-US.json +7 -2
  46. package/src/i18n/locales/es-ES.json +7 -2
  47. package/src/index.ts +19 -7
  48. package/src/inference/OxyInferenceClient.ts +590 -0
  49. package/src/inference/__tests__/OxyInferenceClient.test.ts +383 -0
  50. package/src/mixins/OxyServices.accounts.ts +75 -176
  51. package/src/mixins/OxyServices.inference.ts +57 -0
  52. package/src/mixins/OxyServices.utility.ts +58 -14
  53. package/src/mixins/__tests__/accounts.test.ts +57 -102
  54. package/src/mixins/__tests__/inferenceFactory.test.ts +58 -0
  55. package/src/mixins/__tests__/serviceAuth.test.ts +2 -0
  56. package/src/mixins/index.ts +8 -0
  57. package/src/server/__tests__/cors.socket.test.ts +225 -0
  58. package/src/server/__tests__/serviceTokenAttribution.test.ts +396 -0
  59. package/src/server/auth.ts +118 -0
  60. package/src/server/cors.ts +87 -12
  61. package/src/server/index.ts +6 -0
  62. package/src/session/__tests__/accountDialogShape.test.ts +118 -0
@@ -194,6 +194,18 @@ export declare class HttpService {
194
194
  * ambiguous with a serialized request body.
195
195
  */
196
196
  private static readonly CACHE_IDENTITY_DELIM;
197
+ /**
198
+ * The keys whose presence beside `data` makes a body a PAGE rather than a
199
+ * payload — see {@link unwrapResponse} for why this list is narrow.
200
+ *
201
+ * - `pagination` — the offset-paginated house envelope (`sendPaginated`).
202
+ * - `nextCursor` — the keyset-paginated one (the account audit trails).
203
+ *
204
+ * Membership is decided by key PRESENCE, never by value: the last page sends
205
+ * `nextCursor: null`, and an envelope that collapsed into a bare payload
206
+ * exactly when the stream ended would be a worse bug than the one this fixes.
207
+ */
208
+ private static readonly PAGE_ENVELOPE_KEYS;
197
209
  /**
198
210
  * Derive a stable, non-sensitive identity discriminator for cache scoping.
199
211
  *
@@ -270,7 +282,33 @@ export declare class HttpService {
270
282
  */
271
283
  runSingleFlightDeviceSecretMint(mint: () => Promise<DeviceSecretMintOutcome>): Promise<DeviceSecretMintOutcome>;
272
284
  /**
273
- * Unwrap standardized API response format
285
+ * Unwrap the standardized API response envelope — EXCEPT when the envelope is
286
+ * a page, in which case it travels whole.
287
+ *
288
+ * `{ data: <payload> }` is the house success envelope (`sendSuccess`), and
289
+ * reducing it to `<payload>` is what every call site in the SDK expects. But
290
+ * the reduction DISCARDS every sibling key, silently, and a page's siblings
291
+ * are the only thing that says where the next page starts. That is how
292
+ * `GET /accounts/:id/audit` lost its `nextCursor`: the caller received a bare
293
+ * array, `getNextPageParam` read `undefined`, and pagination was dead past the
294
+ * first page with nothing to show that it was.
295
+ *
296
+ * ## Why the rule is narrow, and not "any sibling key survives"
297
+ *
298
+ * "An object carrying `data` plus anything else is not an envelope" is the
299
+ * tempting general rule, and it is wrong here: this API already answers
300
+ * `{ data, count }` on ~15 routes, plus `{ data, source }`, `{ data, reason }`
301
+ * and `{ data, secretDestroyed }`, and a dozen measured Console call sites
302
+ * type those as the bare payload (`Array<ProviderConnection>`,
303
+ * `AccountBillingState | null`, …). Preserving those envelopes would hand every
304
+ * one of them an object where it expects its payload — at runtime only, since
305
+ * the response type is a call-site assertion. So the rule names PAGINATION
306
+ * specifically: `data` beside {@link PAGE_ENVELOPE_KEYS} is a page.
307
+ *
308
+ * A route whose sibling key genuinely matters to its caller belongs in that
309
+ * list, or should not be a sibling of `data` at all — the cursor-paginated
310
+ * surfaces already in the SDK (`{ follows, nextCursor }`,
311
+ * `{ records, nextCursor }`) sidestep this by never using `data`.
274
312
  */
275
313
  private unwrapResponse;
276
314
  /**
@@ -40,7 +40,7 @@ export { normalizeProfileLinks } from './utils/profileLinks';
40
40
  export type { ProfileLink, ProfileLinkMetadata } from './utils/profileLinks';
41
41
  export type { PublicApplication, ConnectedApp, } from './mixins/OxyServices.connectedApps';
42
42
  export type { StoreCategory, StoreRating, StoreListingSummary, StoreListingDetail, StoreScreenshot, StoreScreenshotPlatform, StoreReview, StoreOwnReview, WriteStoreReviewInput, StoreListingStatus, PublisherListing, WriteListingInput, AddScreenshotInput, UpdateScreenshotInput, StorePage, StorePageOptions, StoreReviewsOptions, } from './mixins/OxyServices.store';
43
- export type { AccountKind, AccountCategoryId, AccountRelationship, AccountRole, AccountMemberStatus, AccountMemberSource, AccountMember, AccountNode, AccountCredentialType, AccountCredentialEnvironment, AccountCredentialStatus, AccountCredential, AccountCredentialWithSecret, RotateAccountCredentialResult, ListAccountsOptions, CreateAccountInput, UpdateAccountInput, ProvisionChannelInput, ProvisionChannelMemberInput, ProvisionChannelResult, InviteAccountMemberInput, UpdateAccountMemberInput, TransferAccountOwnershipInput, CreateAccountCredentialInput, AccountSuccessResult, SwitchAccountResult, Application, ApplicationType, ApplicationStatus, ApplicationCredential, ApplicationCredentialType, ApplicationCredentialStatus, ApplicationEnvironment, CreateApplicationInput, UpdateApplicationInput, CreateApplicationCredentialInput, ApplicationCredentialWithSecret, RotateApplicationCredentialResult, ApplicationUsagePeriod, ApplicationUsageSummary, ApplicationUsageByDay, ApplicationUsageByEndpoint, ApplicationUsageStats, } from './mixins/OxyServices.accounts';
43
+ export type { AccountKind, AccountCategoryId, AccountRelationship, AccountRole, AccountMemberStatus, AccountMemberSource, AccountMember, AccountNode, ListAccountsOptions, CreateAccountInput, UpdateAccountInput, ProvisionChannelInput, ProvisionChannelMemberInput, ProvisionChannelResult, InviteAccountMemberInput, UpdateAccountMemberInput, TransferAccountOwnershipInput, AccountSuccessResult, SwitchAccountResult, Application, ApplicationType, ApplicationStatus, ApplicationCredential, ApplicationCredentialType, ApplicationCredentialStatus, ApplicationEnvironment, CreateApplicationInput, UpdateApplicationInput, CreateApplicationCredentialInput, RotateApplicationCredentialInput, ApplicationCredentialWithSecret, RotateApplicationCredentialResult, ApplicationUsagePeriod, ApplicationUsageSummary, ApplicationUsageByDay, ApplicationUsageByEndpoint, ApplicationUsageStats, } from './mixins/OxyServices.accounts';
44
44
  export { ACCOUNT_CATEGORY_IDS, MAX_ACCOUNT_CATEGORIES, SELECTABLE_ACCOUNT_CATEGORY_IDS, isSelectableAccountCategoryId, kindAcceptsAccountCategories, } from './mixins/OxyServices.accounts';
45
45
  export { buildUserDid } from './mixins/OxyServices.identity';
46
46
  export type { IdentityRecordType, UnlinkableAuthMethodType, LinkAuthMethodResult, PublishRecordResult, VerifyRecordResult, VerifyDomainResult, RemoveDomainResult, RotateKeyProof, RotateKeyOptions, RotateKeyResult, } from './mixins/OxyServices.identity';
@@ -134,6 +134,8 @@ export type { IdentityBinding, IdentityRequestOptions, EstablishedIdentitySessio
134
134
  export { AccountNotOnDeviceError } from './mixins/OxyServices.deviceBoot';
135
135
  export { refreshPersistedSession, refreshDeviceSecretArm, createAuthRefreshHandler, installAuthRefreshHandler, startTokenRefreshScheduler, TOKEN_REFRESH_LEAD_MS, } from './session/refresh';
136
136
  export type { RefreshDeps, TokenRefreshSchedulerHandle, DeviceSecretMintOutcome } from './session/refresh';
137
+ export { OxyInferenceClient, OxyInferenceError, OXY_INFERENCE_BASE_URL, } from './inference/OxyInferenceClient';
138
+ export type { OxyInferenceClientOptions, OxyInferenceCredential, OxyInferenceFetch, OxyInferenceRequestOptions, OxyInferenceResponse, OxyGenerationReceipt, OxyResponsesRequest, } from './inference/OxyInferenceClient';
137
139
  export { runSessionColdBoot } from './boot/sessionColdBoot';
138
140
  export type { RunSessionColdBootOptions, SessionMode, SignedOutReason, DeviceBootSession, } from './boot/sessionColdBoot';
139
141
  export { packageInfo } from './constants/version';
@@ -0,0 +1,324 @@
1
+ /**
2
+ * The Oxy inference client — one surface, two credential lanes (issue #972,
3
+ * workstream 15).
4
+ *
5
+ * ```typescript
6
+ * // An OpenAI-style machine key: one bearer string, no session, no exchange.
7
+ * const oxy = new OxyInferenceClient({ credential: process.env.OXY_API_KEY });
8
+ *
9
+ * // Oxy auth: whatever bearer the session or the service-token mint holds.
10
+ * const oxy = oxyServices.inference();
11
+ * ```
12
+ *
13
+ * Both lanes reach the SAME endpoints and are told apart only by how the bearer
14
+ * is produced: a machine key is a constant string, and an Oxy bearer rotates, so
15
+ * it is a function this client calls on every request rather than a value it
16
+ * captures once. There is no third lane, and no method behaves differently
17
+ * depending on which one you used.
18
+ *
19
+ * ## What you will observe today
20
+ *
21
+ * **Every invoke refuses.** `respond()` reaches the public edge, which
22
+ * authenticates the credential, resolves attribution, authorizes scopes, pins a
23
+ * routing policy and reserves spend — and then has no data plane to forward to,
24
+ * so it releases the hold and answers `service_unavailable`. That surfaces here
25
+ * as an {@link OxyInferenceError} with `code: 'service_unavailable'`,
26
+ * `retryable: false` and a `requestId`. It is the correct answer, not a
27
+ * misconfiguration of yours, and no balance is spent.
28
+ *
29
+ * **The catalogue is empty**, so {@link OxyInferenceClient.listModels} answers
30
+ * `[]` and {@link OxyInferenceClient.getModel} throws for every id. `[]` is a
31
+ * normal answer to render, not an error to retry.
32
+ *
33
+ * `docs/inference/README.md` is the status board; `docs/inference/sdk.md` is
34
+ * this client's page.
35
+ *
36
+ * ## Why this is a client and not more methods on `OxyServices`
37
+ *
38
+ * Two reasons, both structural. A machine-key holder has no Oxy session at all,
39
+ * so a surface reached only through the session client would be unreachable for
40
+ * exactly the developer this workstream exists to serve. And the `/v1` error
41
+ * body is the contract's `InferenceError` at the top level rather than the
42
+ * platform's `{ error, message }` envelope — it carries `requestId`, `retryable`
43
+ * and `retryAfterMs`, all of which `OxyServices.handleError` would flatten into a
44
+ * message string. `oxyServices.inference()` binds the session bearer into this
45
+ * client so a session-holding app writes no plumbing of its own.
46
+ *
47
+ * ## Streaming is absent on purpose
48
+ *
49
+ * There is no `stream()` method and no `stream` field on a request. The stream
50
+ * event union exists in `@oxyhq/contracts` and no endpoint emits one — the edge
51
+ * refuses `stream: true` with `invalid_request`. A method that always failed
52
+ * would be a worse artefact than an absent one. See
53
+ * `docs/inference/streaming.md`.
54
+ *
55
+ * ## Field names, and the one place they could drift
56
+ *
57
+ * Every VALUE type here comes from `@oxyhq/contracts` — messages, tools, tool
58
+ * choice, response format, usage quantities, unit prices, error codes. The
59
+ * request FIELD NAMES cannot: they belong to `responsesRequestSchema`, which
60
+ * lives in the API because it is a public dialect rather than an Oxy↔data-plane
61
+ * contract. `packages/api/src/schemas/__tests__/sdkRequestCompatibility.test.ts`
62
+ * is the gate — it parses a value of this module's request type against that
63
+ * schema, so a rename on either side fails a build rather than a customer's
64
+ * request.
65
+ */
66
+ import type { CurrencyCode, ExactDecimal, InferenceEnvironment, InferenceErrorCode, InferenceFinishReason, InferenceMessage, InferenceRequestOutcome, ModelCatalogueEntry, ResponseFormat, RoutingPolicyReference, RoutingProfile, ToolChoice, ToolDefinition, UnitPrice, UsageQuantity, UsageSource } from '@oxyhq/contracts';
67
+ /** The base URL of the Oxy API, when a caller names none. */
68
+ export declare const OXY_INFERENCE_BASE_URL = "https://api.oxy.so";
69
+ /**
70
+ * How this client gets its bearer.
71
+ *
72
+ * A `string` is a static machine credential (`oxy_sk_…`) — presented verbatim,
73
+ * exactly as a stock OpenAI SDK would present it. A function is the Oxy auth
74
+ * lane and is called on EVERY request, because a session bearer and a service
75
+ * token both rotate and a captured one goes stale inside the hour.
76
+ */
77
+ export type OxyInferenceCredential = string | (() => string | null | Promise<string | null>);
78
+ /**
79
+ * The `fetch` this client calls.
80
+ *
81
+ * The global signature rather than a narrowed one, so any drop-in
82
+ * implementation — a test double, an instrumented wrapper, a Node agent — is
83
+ * assignable without a cast at either end.
84
+ */
85
+ export type OxyInferenceFetch = typeof fetch;
86
+ export interface OxyInferenceClientOptions {
87
+ readonly credential: OxyInferenceCredential;
88
+ /** Defaults to {@link OXY_INFERENCE_BASE_URL}. A trailing slash is trimmed. */
89
+ readonly baseURL?: string;
90
+ /** Defaults to the global `fetch`. */
91
+ readonly fetch?: OxyInferenceFetch;
92
+ }
93
+ /**
94
+ * A request to `POST /v1/responses`.
95
+ *
96
+ * `model` and `routingProfile` are mutually exclusive and BOTH are optional: an
97
+ * application whose routing policy carries a `defaultTarget` may name neither,
98
+ * which is what "per-application default model or routing profile" means. Naming
99
+ * both is refused by the edge with `invalid_request`.
100
+ */
101
+ export interface OxyResponsesRequest {
102
+ /** `<publisher>/<model>` or `<publisher>/<model>@<revision>`. */
103
+ readonly model?: string;
104
+ /** A routing profile slug. Never contains a slash, so it is never a model id. */
105
+ readonly routingProfile?: string;
106
+ /** A prompt, or the message list it is shorthand for. */
107
+ readonly input: string | readonly InferenceMessage[];
108
+ readonly maxOutputTokens?: number;
109
+ readonly temperature?: number;
110
+ readonly topP?: number;
111
+ readonly topK?: number;
112
+ readonly frequencyPenalty?: number;
113
+ readonly presencePenalty?: number;
114
+ readonly seed?: number;
115
+ readonly stopSequences?: readonly string[];
116
+ readonly tools?: readonly ToolDefinition[];
117
+ readonly toolChoice?: ToolChoice;
118
+ readonly responseFormat?: ResponseFormat;
119
+ /** Cost-attribution tags, echoed back on the receipt. At most 16. */
120
+ readonly labels?: Readonly<Record<string, string>>;
121
+ /** Your own correlation id, echoed on the response. */
122
+ readonly clientRequestId?: string;
123
+ }
124
+ export interface OxyInferenceRequestOptions {
125
+ /**
126
+ * Abort the request. The edge treats a client disconnect as a cancellation:
127
+ * it settles what was produced and refunds the rest, so a cancelled request
128
+ * is a normal terminal state rather than an error to clean up after.
129
+ */
130
+ readonly signal?: AbortSignal;
131
+ /**
132
+ * `Idempotency-Key`. A key already bound to a reservation is REFUSED with
133
+ * `idempotency_conflict` rather than replayed — responses are not retained,
134
+ * so there is nothing to replay, and refusing is what makes "a retry never
135
+ * produces a second charge" structural. At most 128 characters.
136
+ */
137
+ readonly idempotencyKey?: string;
138
+ /**
139
+ * `X-Oxy-User-Id` — the end user this request is made on behalf of.
140
+ * ATTRIBUTION ONLY: it never changes which account is charged.
141
+ */
142
+ readonly delegatedUserId?: string;
143
+ }
144
+ /** The body of a successful `POST /v1/responses`. */
145
+ export interface OxyInferenceResponse {
146
+ readonly schemaVersion: 1;
147
+ /** Also on `X-Oxy-Request-Id`, on success and on every refusal. */
148
+ readonly requestId: string;
149
+ readonly generationId?: string;
150
+ /** Always revision-pinned, even when you named only the model line. */
151
+ readonly model: string;
152
+ readonly servingProvider: string;
153
+ readonly finishReason: InferenceFinishReason;
154
+ readonly output: readonly InferenceMessage[];
155
+ /** Metered quantities. Never money — the charge is on the receipt. */
156
+ readonly usage: readonly UsageQuantity[];
157
+ /** The exact policy version this request was admitted under. */
158
+ readonly routingPolicy: RoutingPolicyReference;
159
+ /**
160
+ * How long Oxy took over this request, in whole milliseconds — also on
161
+ * `X-Oxy-Latency-Ms`.
162
+ *
163
+ * Measured from the moment the edge received the request through
164
+ * authentication, admission, routing, the reservation, the call to the
165
+ * inference data plane and the settlement of the hold. Most of it is the
166
+ * upstream generating tokens; it does not separate the two.
167
+ *
168
+ * It is NOT the round trip you can measure yourself, which additionally
169
+ * covers DNS, TLS, both network legs and your own parse. Report them side by
170
+ * side rather than picking one — this figure has no network in it and yours
171
+ * cannot be attributed to the model.
172
+ *
173
+ * Optional because it is additive: an Oxy deployment older than the field
174
+ * omits it, and a streamed request never carries it (the head is written
175
+ * before the first frame arrives, so the number does not exist yet).
176
+ */
177
+ readonly latencyMs?: number;
178
+ }
179
+ /**
180
+ * A settled receipt, as `GET /v1/generations/:id` returns it.
181
+ *
182
+ * Carries the price SNAPSHOT rather than a reference to a price version, so the
183
+ * arithmetic stays checkable after that version has been superseded.
184
+ */
185
+ export interface OxyGenerationReceipt {
186
+ readonly schemaVersion: 1;
187
+ readonly receiptId: string;
188
+ readonly requestId: string;
189
+ readonly generationId?: string;
190
+ readonly applicationId: string;
191
+ readonly credentialId: string;
192
+ /** Attribution only. Never the billing identity. */
193
+ readonly delegatedUserId?: string;
194
+ readonly environment: InferenceEnvironment;
195
+ readonly outcome: InferenceRequestOutcome;
196
+ readonly usageSource: UsageSource;
197
+ /** EVERY unit, including the zeros — see `usageSource` for what a zero means. */
198
+ readonly units: readonly UsageQuantity[];
199
+ readonly resolvedModelReference: string;
200
+ readonly servingProvider: string;
201
+ readonly priceSnapshot: {
202
+ readonly priceVersionId: string;
203
+ readonly currency: CurrencyCode;
204
+ readonly unitPrices: readonly UnitPrice[];
205
+ };
206
+ readonly billedAmount: ExactDecimal;
207
+ readonly currency: CurrencyCode;
208
+ /** A BYOK route: `billedAmount` is Oxy's fee, not the cost of the tokens. */
209
+ readonly platformFeeOnly: boolean;
210
+ readonly settledAt: string;
211
+ }
212
+ /**
213
+ * Anything the inference API refused.
214
+ *
215
+ * `retryable` is asserted by the server and looked up from a total map over the
216
+ * closed code set — never inferred here from the status. A client that decides
217
+ * retryability from an HTTP status is exactly what the contract's retryability
218
+ * rule exists to prevent, so this class carries the server's answer and does not
219
+ * compute one.
220
+ */
221
+ export declare class OxyInferenceError extends Error {
222
+ readonly code: InferenceErrorCode;
223
+ readonly retryable: boolean;
224
+ readonly requestId: string;
225
+ readonly status: number;
226
+ /** How long to wait. Only ever present when `retryable`. */
227
+ readonly retryAfterMs?: number;
228
+ /** The request field at fault, for `invalid_request`. */
229
+ readonly param?: string;
230
+ constructor(input: {
231
+ code: InferenceErrorCode;
232
+ message: string;
233
+ retryable: boolean;
234
+ requestId: string;
235
+ status: number;
236
+ retryAfterMs?: number;
237
+ param?: string;
238
+ });
239
+ }
240
+ /**
241
+ * The Oxy inference API.
242
+ *
243
+ * Stateless: it holds a base URL, a way to get a bearer and a `fetch`. Nothing
244
+ * is cached, because the two things worth caching here are a catalogue that is
245
+ * audience-scoped and a receipt that is immutable but rarely re-read.
246
+ *
247
+ * Successful responses are TYPED, not re-parsed. The server validates every one
248
+ * against its own schema before serving it, and a second client-side parse of a
249
+ * non-strict shape would silently DROP fields a newer API added — turning
250
+ * forward compatibility into data loss. Refusals are read defensively, because
251
+ * two routers answer under `/v1` and an unreadable failure must still reach the
252
+ * caller as one.
253
+ */
254
+ export declare class OxyInferenceClient {
255
+ #private;
256
+ constructor(options: OxyInferenceClientOptions);
257
+ /**
258
+ * The models this caller may use — `GET /v1/models`.
259
+ *
260
+ * Audience-scoped server-side. A machine credential and an anonymous caller
261
+ * both see the PUBLIC catalogue; only an internal/system application's
262
+ * service token sees internal-only routes.
263
+ *
264
+ * **`[]` is a normal answer**, and is the only answer today: the catalogue
265
+ * is populated by operators, and a route is not publicly exposed until
266
+ * somebody has reviewed the right to resell it.
267
+ */
268
+ listModels(options?: {
269
+ signal?: AbortSignal;
270
+ }): Promise<ModelCatalogueEntry[]>;
271
+ /**
272
+ * One catalogue entry by its canonical id — `GET /v1/models/:publisher/:model`.
273
+ *
274
+ * The id is TWO path segments, because a canonical model id contains a slash
275
+ * and a single encoded segment would never match the route.
276
+ *
277
+ * A model you may not see answers 404 identically to one that does not
278
+ * exist, deliberately: the catalogue is never an existence oracle for what
279
+ * Oxy runs internally.
280
+ *
281
+ * @param modelId - `<publisher>/<model>`. A revision pin
282
+ * (`<publisher>/<model>@<revision>`) names a model REFERENCE rather than a
283
+ * model and is rejected here rather than sent, because the catalogue is
284
+ * keyed on models and a pinned reference would 404 indistinguishably from
285
+ * "no such model".
286
+ */
287
+ getModel(modelId: string, options?: {
288
+ signal?: AbortSignal;
289
+ }): Promise<ModelCatalogueEntry>;
290
+ /**
291
+ * The routing profiles this caller may select — `GET /v1/models/routing-profiles`.
292
+ *
293
+ * A profile is a named strategy for CHOOSING among routes, not a model: no
294
+ * publisher, no revision, no licence, no weights. Like the model list, `[]`
295
+ * is a normal answer.
296
+ */
297
+ listRoutingProfiles(options?: {
298
+ signal?: AbortSignal;
299
+ }): Promise<RoutingProfile[]>;
300
+ /**
301
+ * Send one non-streaming inference request — `POST /v1/responses`.
302
+ *
303
+ * **This refuses in every deployment today** with `service_unavailable`,
304
+ * because there is no data plane behind the edge. The spend held for the
305
+ * request is released before the refusal returns, so nothing is charged.
306
+ *
307
+ * @throws {OxyInferenceError} for every refusal, carrying the server's own
308
+ * `code`, `retryable` and `requestId`.
309
+ */
310
+ respond(request: OxyResponsesRequest, options?: OxyInferenceRequestOptions): Promise<OxyInferenceResponse>;
311
+ /**
312
+ * Read back the settled receipt for one request —
313
+ * `GET /v1/generations/:id`.
314
+ *
315
+ * `id` is the `requestId` you already hold (it is on every response and
316
+ * every error) or the `generationId`. Requires the `inference:usage:read`
317
+ * scope; a caller without it, or one whose application did not make the
318
+ * request, is told the receipt does not exist rather than that it belongs to
319
+ * somebody else.
320
+ */
321
+ getGeneration(id: string, options?: {
322
+ signal?: AbortSignal;
323
+ }): Promise<OxyGenerationReceipt>;
324
+ }
@@ -267,61 +267,6 @@ export interface UpdateAccountMemberInput {
267
267
  export interface TransferAccountOwnershipInput {
268
268
  userId: string;
269
269
  }
270
- /** Credential kind. Account (bot) credentials are always `service` tokens. */
271
- export type AccountCredentialType = 'service';
272
- /** Deployment environment a bot credential is scoped to. */
273
- export type AccountCredentialEnvironment = 'development' | 'staging' | 'production';
274
- /** Bot credential lifecycle status. */
275
- export type AccountCredentialStatus = 'active' | 'deprecated' | 'revoked';
276
- /** Input accepted by `createAccountCredential`. Credential `type` is always `service`. */
277
- export interface CreateAccountCredentialInput {
278
- name: string;
279
- environment: AccountCredentialEnvironment;
280
- scopes?: string[];
281
- }
282
- /**
283
- * Client-facing AccountCredential shape (a bot account's service token). The raw
284
- * secret is NEVER part of this shape — it is returned exactly once, separately,
285
- * at creation/rotation.
286
- */
287
- export interface AccountCredential {
288
- _id: string;
289
- /** The bot account this credential authenticates as (account `_id`). */
290
- accountId: string;
291
- name: string;
292
- publicKey: string;
293
- type: AccountCredentialType;
294
- environment: AccountCredentialEnvironment;
295
- scopes: string[];
296
- status: AccountCredentialStatus;
297
- lastUsedAt?: string;
298
- expiresAt?: string;
299
- /**
300
- * Audit link to the credential this one was rotated FROM. Populated on
301
- * credentials created via rotation; absent on original credentials.
302
- */
303
- rotatedFromCredentialId?: string;
304
- createdByUserId: string;
305
- createdAt: string;
306
- updatedAt: string;
307
- }
308
- /** Result of creating a bot credential — `secret` is returned ONCE. */
309
- export interface AccountCredentialWithSecret {
310
- credential: AccountCredential;
311
- secret: string;
312
- }
313
- /**
314
- * Result of rotating a bot credential. Extends the create result with audit
315
- * fields: the new plaintext `secret` is returned ONCE, plus `rotatedFrom` (the
316
- * previous credential's `credentialId`) and `graceExpiresAt` (ISO string marking
317
- * when the old credential stops being honoured during the rotation grace window).
318
- */
319
- export interface RotateAccountCredentialResult extends AccountCredentialWithSecret {
320
- /** The previous credential's `credentialId` that this rotation supersedes. */
321
- rotatedFrom: string;
322
- /** ISO timestamp at which the rotated-from credential's grace window ends. */
323
- graceExpiresAt: string;
324
- }
325
270
  /**
326
271
  * Application classification. Set only by Oxy platform staff — never editable
327
272
  * through the normal member-facing update path.
@@ -329,8 +274,18 @@ export interface RotateAccountCredentialResult extends AccountCredentialWithSecr
329
274
  export type ApplicationType = 'first_party' | 'third_party' | 'internal' | 'system';
330
275
  /** Lifecycle status of an application. */
331
276
  export type ApplicationStatus = 'active' | 'suspended' | 'deleted' | 'pending_review';
332
- /** OAuth credential kind. `service` credentials mint service tokens. */
333
- export type ApplicationCredentialType = 'public' | 'confidential' | 'service';
277
+ /**
278
+ * Credential kind.
279
+ *
280
+ * The first three are OAuth clients: the `oxy_dk_…` `publicKey` is the
281
+ * `client_id`, and any secret is presented BESIDE it. `service` credentials
282
+ * additionally mint service tokens.
283
+ *
284
+ * `machine` is the OpenAI-SDK-compatible API key (issue #972 §2.3): its
285
+ * credential material is ONE `oxy_sk_…` bearer string returned in `token`
286
+ * exactly once on create/rotate, never in `secret`.
287
+ */
288
+ export type ApplicationCredentialType = 'public' | 'confidential' | 'service' | 'machine';
334
289
  /** Deployment environment an application credential is scoped to. */
335
290
  export type ApplicationEnvironment = 'development' | 'staging' | 'production';
336
291
  /** Application credential lifecycle status. */
@@ -386,6 +341,12 @@ export interface ApplicationCredential {
386
341
  applicationId: string;
387
342
  name: string;
388
343
  publicKey: string;
344
+ /**
345
+ * `oxy_sk_<id>` — the PUBLIC lookup half of a `machine` credential's bearer
346
+ * token, present only on that type. Safe to render: the secret half is 256
347
+ * bits that were shown exactly once and are never returned again.
348
+ */
349
+ tokenPrefix?: string;
389
350
  type: ApplicationCredentialType;
390
351
  environment: ApplicationEnvironment;
391
352
  scopes: string[];
@@ -441,23 +402,67 @@ export interface CreateApplicationCredentialInput {
441
402
  type: ApplicationCredentialType;
442
403
  environment: ApplicationEnvironment;
443
404
  scopes?: string[];
405
+ /**
406
+ * Lifetime of a `machine` credential, in seconds — 60 to 730 days. Omit for a
407
+ * key that does not expire on its own.
408
+ *
409
+ * **`machine` only.** On every other credential type `expires_at` means the
410
+ * rotation grace deadline, so a caller setting it at creation would make a
411
+ * brand-new credential indistinguishable from a rotated one. The server
412
+ * REJECTS it for those types rather than ignoring it, so sending it with the
413
+ * wrong `type` is a 400, not a silently dropped field.
414
+ */
415
+ expiresInSeconds?: number;
416
+ }
417
+ /** Input accepted by `rotateAppCredential`. */
418
+ export interface RotateApplicationCredentialInput {
419
+ /**
420
+ * How long the superseded `machine` token keeps working, in seconds — 1 to 30
421
+ * days. Omitting it revokes the previous token the instant the replacement is
422
+ * minted, which is the safe default for a leaked key.
423
+ *
424
+ * **`machine` only, and opt-in.** `confidential`/`service` credentials always
425
+ * retire on the platform's fixed seven-day grace and the server REJECTS this
426
+ * field for them, so their contract is unchanged.
427
+ */
428
+ graceSeconds?: number;
444
429
  }
445
- /** Result of creating an application credential — `secret` is returned ONCE. */
430
+ /**
431
+ * Result of creating an application credential — credential material is returned
432
+ * ONCE and can never be read back.
433
+ *
434
+ * Exactly one of the two fields carries it, decided by
435
+ * {@link ApplicationCredentialType}: `secret` for a `confidential`/`service`
436
+ * client, `token` for a `machine` API key, and NEITHER for a `public` client
437
+ * (`secret` is `null`). They are separate fields rather than one, so a surface
438
+ * that renders "the secret" cannot silently render an API key's bearer token
439
+ * under the wrong label, or a `null` where a token should be.
440
+ */
446
441
  export interface ApplicationCredentialWithSecret {
447
442
  credential: ApplicationCredential;
448
- secret: string;
443
+ /** The OAuth client secret. `null` for `public` and `machine` credentials. */
444
+ secret: string | null;
445
+ /** The full `oxy_sk_…` bearer token. Present ONLY for a `machine` credential. */
446
+ token?: string;
449
447
  }
450
448
  /**
451
449
  * Result of rotating an application credential. Extends the create result with
452
- * audit fields: the new plaintext `secret` is returned ONCE, plus `rotatedFrom`
453
- * (the previous credential's `credentialId`) and `graceExpiresAt` (ISO string
454
- * marking when the old credential stops being honoured during the grace window).
450
+ * audit fields: the new credential material is returned ONCE, plus `rotatedFrom`
451
+ * (the previous credential's `credentialId`) and `graceExpiresAt`.
455
452
  */
456
453
  export interface RotateApplicationCredentialResult extends ApplicationCredentialWithSecret {
457
454
  /** The previous credential's `credentialId` that this rotation supersedes. */
458
455
  rotatedFrom: string;
459
- /** ISO timestamp at which the rotated-from credential's grace window ends. */
460
- graceExpiresAt: string;
456
+ /**
457
+ * ISO timestamp at which the rotated-from credential stops being honoured, or
458
+ * `null` when no grace window was configured and it was revoked outright.
459
+ *
460
+ * Nullable because a `machine` credential's grace is OPT-IN (issue #972 §2.3):
461
+ * rotating an API key without asking for a window kills the old token
462
+ * immediately, and there is then no deadline to report. The OAuth/service
463
+ * types always carry their fixed seven-day deadline.
464
+ */
465
+ graceExpiresAt: string | null;
461
466
  }
462
467
  /** Time window for application usage statistics. */
463
468
  export type ApplicationUsagePeriod = '24h' | '7d' | '30d' | '90d';
@@ -675,36 +680,6 @@ export declare function OxyServicesAccountsMixin<T extends typeof OxyServicesBas
675
680
  * @param data - Target user id.
676
681
  */
677
682
  transferAccountOwnership(accountId: string, data: TransferAccountOwnershipInput): Promise<AccountSuccessResult>;
678
- /**
679
- * List a bot account's service credentials. The response NEVER includes
680
- * secrets.
681
- * @param accountId - The account's Mongo `_id`.
682
- */
683
- listAccountCredentials(accountId: string): Promise<AccountCredential[]>;
684
- /**
685
- * Create a service credential for a bot account. The plaintext `secret` is
686
- * returned exactly ONCE; the server stores only a hash and will never return
687
- * it again.
688
- * @param accountId - The account's Mongo `_id`.
689
- * @param data - Credential configuration (`type` is always `service`).
690
- */
691
- createAccountCredential(accountId: string, data: CreateAccountCredentialInput): Promise<AccountCredentialWithSecret>;
692
- /**
693
- * Rotate a bot credential's secret. The new plaintext `secret` is returned
694
- * exactly ONCE, along with audit fields: `rotatedFrom` (the previous
695
- * credentialId) and `graceExpiresAt` (ISO string for the grace window during
696
- * which the old credential is still honoured).
697
- * @param accountId - The account's Mongo `_id`.
698
- * @param credentialId - The credential's Mongo `_id`.
699
- */
700
- rotateAccountCredential(accountId: string, credentialId: string): Promise<RotateAccountCredentialResult>;
701
- /**
702
- * Revoke a bot credential (`status='revoked'`). Revoked credentials can no
703
- * longer authenticate.
704
- * @param accountId - The account's Mongo `_id`.
705
- * @param credentialId - The credential's Mongo `_id`.
706
- */
707
- revokeAccountCredential(accountId: string, credentialId: string): Promise<AccountSuccessResult>;
708
683
  /**
709
684
  * List the applications owned by an account. Backed by
710
685
  * `GET /applications?ownerAccountId=<id>`.
@@ -753,8 +728,11 @@ export declare function OxyServicesAccountsMixin<T extends typeof OxyServicesBas
753
728
  * which the old credential is still honoured).
754
729
  * @param applicationId - The application's Mongo `_id`.
755
730
  * @param credentialId - The credential's Mongo `_id`.
731
+ * @param options - `graceSeconds` keeps a superseded `machine` token working
732
+ * for that long. Omitted, the previous token dies the moment the
733
+ * replacement is minted.
756
734
  */
757
- rotateAppCredential(applicationId: string, credentialId: string): Promise<RotateApplicationCredentialResult>;
735
+ rotateAppCredential(applicationId: string, credentialId: string, options?: RotateApplicationCredentialInput): Promise<RotateApplicationCredentialResult>;
758
736
  /**
759
737
  * Revoke an application credential (`status='revoked'`). Revoked credentials
760
738
  * can no longer authenticate.