@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
@@ -965,19 +965,46 @@ class HttpService {
965
965
  return this.deviceSecretMintInFlight;
966
966
  }
967
967
  /**
968
- * Unwrap standardized API response format
968
+ * Unwrap the standardized API response envelope — EXCEPT when the envelope is
969
+ * a page, in which case it travels whole.
970
+ *
971
+ * `{ data: <payload> }` is the house success envelope (`sendSuccess`), and
972
+ * reducing it to `<payload>` is what every call site in the SDK expects. But
973
+ * the reduction DISCARDS every sibling key, silently, and a page's siblings
974
+ * are the only thing that says where the next page starts. That is how
975
+ * `GET /accounts/:id/audit` lost its `nextCursor`: the caller received a bare
976
+ * array, `getNextPageParam` read `undefined`, and pagination was dead past the
977
+ * first page with nothing to show that it was.
978
+ *
979
+ * ## Why the rule is narrow, and not "any sibling key survives"
980
+ *
981
+ * "An object carrying `data` plus anything else is not an envelope" is the
982
+ * tempting general rule, and it is wrong here: this API already answers
983
+ * `{ data, count }` on ~15 routes, plus `{ data, source }`, `{ data, reason }`
984
+ * and `{ data, secretDestroyed }`, and a dozen measured Console call sites
985
+ * type those as the bare payload (`Array<ProviderConnection>`,
986
+ * `AccountBillingState | null`, …). Preserving those envelopes would hand every
987
+ * one of them an object where it expects its payload — at runtime only, since
988
+ * the response type is a call-site assertion. So the rule names PAGINATION
989
+ * specifically: `data` beside {@link PAGE_ENVELOPE_KEYS} is a page.
990
+ *
991
+ * A route whose sibling key genuinely matters to its caller belongs in that
992
+ * list, or should not be a sibling of `data` at all — the cursor-paginated
993
+ * surfaces already in the SDK (`{ follows, nextCursor }`,
994
+ * `{ records, nextCursor }`) sidestep this by never using `data`.
969
995
  */
970
996
  unwrapResponse(responseData) {
971
- // Handle paginated responses: { data: [...], pagination: {...} }
972
- if (responseData && typeof responseData === 'object' && 'data' in responseData && 'pagination' in responseData) {
997
+ if (!responseData || typeof responseData !== 'object' || !('data' in responseData)) {
998
+ // Not the success envelope (or not an object at all) as-is.
973
999
  return responseData;
974
1000
  }
975
- // Handle regular success responses: { data: ... }
976
- if (responseData && typeof responseData === 'object' && 'data' in responseData && !Array.isArray(responseData)) {
977
- return responseData.data;
1001
+ // A page travels whole: its cursor/pagination sibling is unrecoverable
1002
+ // information, not decoration.
1003
+ if (HttpService.PAGE_ENVELOPE_KEYS.some((key) => key in responseData)) {
1004
+ return responseData;
978
1005
  }
979
- // Return as-is for responses that don't use sendSuccess wrapper
980
- return responseData;
1006
+ // Regular success envelope: `{ data: ... }` -> the payload.
1007
+ return Array.isArray(responseData) ? responseData : responseData.data;
981
1008
  }
982
1009
  /**
983
1010
  * Update request metrics
@@ -1155,3 +1182,15 @@ exports.HttpService = HttpService;
1155
1182
  * ambiguous with a serialized request body.
1156
1183
  */
1157
1184
  HttpService.CACHE_IDENTITY_DELIM = ' id=';
1185
+ /**
1186
+ * The keys whose presence beside `data` makes a body a PAGE rather than a
1187
+ * payload — see {@link unwrapResponse} for why this list is narrow.
1188
+ *
1189
+ * - `pagination` — the offset-paginated house envelope (`sendPaginated`).
1190
+ * - `nextCursor` — the keyset-paginated one (the account audit trails).
1191
+ *
1192
+ * Membership is decided by key PRESENCE, never by value: the last page sends
1193
+ * `nextCursor: null`, and an envelope that collapsed into a bare payload
1194
+ * exactly when the stream ended would be a worse bug than the one this fixes.
1195
+ */
1196
+ HttpService.PAGE_ENVELOPE_KEYS = ['pagination', 'nextCursor'];
@@ -2136,8 +2136,13 @@
2136
2136
  "filesWrite": "Upload and modify your files",
2137
2137
  "filesDelete": "Delete your files",
2138
2138
  "webhooksReceive": "Receive webhooks",
2139
- "chatCompletions": "Use AI chat on your behalf",
2140
- "modelsRead": "List available AI models",
2139
+ "inferenceInvoke": "Run AI requests on your behalf",
2140
+ "inferenceModelsRead": "List available AI models",
2141
+ "inferenceUsageRead": "Read its AI usage and costs",
2142
+ "inferenceRoutingRead": "Read how AI requests are routed",
2143
+ "inferenceRoutingWrite": "Change how AI requests are routed",
2144
+ "inferenceProvidersRead": "Read its connected AI providers",
2145
+ "inferenceProvidersWrite": "Manage its connected AI providers",
2141
2146
  "federationWrite": "Act across federated services"
2142
2147
  },
2143
2148
  "account": {
@@ -2136,8 +2136,13 @@
2136
2136
  "filesWrite": "Subir y modificar tus archivos",
2137
2137
  "filesDelete": "Eliminar tus archivos",
2138
2138
  "webhooksReceive": "Recibir webhooks",
2139
- "chatCompletions": "Usar el chat con IA en tu nombre",
2140
- "modelsRead": "Ver los modelos de IA disponibles",
2139
+ "inferenceInvoke": "Ejecutar peticiones de IA en tu nombre",
2140
+ "inferenceModelsRead": "Ver los modelos de IA disponibles",
2141
+ "inferenceUsageRead": "Ver su consumo y costes de IA",
2142
+ "inferenceRoutingRead": "Ver cómo se enrutan las peticiones de IA",
2143
+ "inferenceRoutingWrite": "Cambiar cómo se enrutan las peticiones de IA",
2144
+ "inferenceProvidersRead": "Ver sus proveedores de IA conectados",
2145
+ "inferenceProvidersWrite": "Gestionar sus proveedores de IA conectados",
2141
2146
  "federationWrite": "Actuar en servicios federados"
2142
2147
  },
2143
2148
  "account": {
@@ -2136,8 +2136,13 @@
2136
2136
  "filesWrite": "Upload and modify your files",
2137
2137
  "filesDelete": "Delete your files",
2138
2138
  "webhooksReceive": "Receive webhooks",
2139
- "chatCompletions": "Use AI chat on your behalf",
2140
- "modelsRead": "List available AI models",
2139
+ "inferenceInvoke": "Run AI requests on your behalf",
2140
+ "inferenceModelsRead": "List available AI models",
2141
+ "inferenceUsageRead": "Read its AI usage and costs",
2142
+ "inferenceRoutingRead": "Read how AI requests are routed",
2143
+ "inferenceRoutingWrite": "Change how AI requests are routed",
2144
+ "inferenceProvidersRead": "Read its connected AI providers",
2145
+ "inferenceProvidersWrite": "Manage its connected AI providers",
2141
2146
  "federationWrite": "Act across federated services"
2142
2147
  },
2143
2148
  "account": {
@@ -2136,8 +2136,13 @@
2136
2136
  "filesWrite": "Subir y modificar tus archivos",
2137
2137
  "filesDelete": "Eliminar tus archivos",
2138
2138
  "webhooksReceive": "Recibir webhooks",
2139
- "chatCompletions": "Usar el chat con IA en tu nombre",
2140
- "modelsRead": "Ver los modelos de IA disponibles",
2139
+ "inferenceInvoke": "Ejecutar peticiones de IA en tu nombre",
2140
+ "inferenceModelsRead": "Ver los modelos de IA disponibles",
2141
+ "inferenceUsageRead": "Ver su consumo y costes de IA",
2142
+ "inferenceRoutingRead": "Ver cómo se enrutan las peticiones de IA",
2143
+ "inferenceRoutingWrite": "Cambiar cómo se enrutan las peticiones de IA",
2144
+ "inferenceProvidersRead": "Ver sus proveedores de IA conectados",
2145
+ "inferenceProvidersWrite": "Gestionar sus proveedores de IA conectados",
2141
2146
  "federationWrite": "Actuar en servicios federados"
2142
2147
  },
2143
2148
  "account": {
package/dist/cjs/index.js CHANGED
@@ -22,7 +22,7 @@ exports.AEAD_KEY_LENGTH = exports.decryptAead = exports.encryptAead = exports.hk
22
22
  exports.calculateBackoffInterval = exports.createCircuitBreakerState = exports.DEFAULT_CIRCUIT_BREAKER_CONFIG = exports.isRetryableError = exports.isNetworkError = exports.isServerError = exports.isRateLimitError = exports.isNotFoundError = exports.isForbiddenError = exports.isUnauthorizedError = exports.isAlreadyRegisteredError = exports.getErrorMessage = exports.getErrorStatus = exports.HttpStatus = exports.getSystemColorScheme = exports.systemPrefersDarkMode = exports.getOppositeTheme = exports.normalizeColorScheme = exports.normalizeTheme = exports.getContrastTextColor = exports.isLightColor = exports.withOpacity = exports.rgbToHex = exports.hexToRgb = exports.lightenColor = exports.darkenColor = exports.isWebBrowser = exports.isAndroid = exports.isIOS = exports.isNative = exports.isWeb = exports.setPlatformOS = exports.getPlatformOS = exports.getPrimaryLanguage = exports.getUserLanguages = exports.isRTLLocale = exports.getNativeLanguageName = exports.getLanguageName = exports.getLanguageMetadata = exports.isSupportedLocale = exports.normalizeLocale = exports.getBaseLanguage = exports.FALLBACK_LOCALE = exports.SUPPORTED_LANGUAGES = exports.TopicSource = exports.TopicType = exports.SECURITY_EVENT_SEVERITY_MAP = exports.DeviceManager = exports.deriveSharedSecret = exports.AEAD_NONCE_LENGTH = void 0;
23
23
  exports.normalizeInlineText = exports.validateAndSanitizeUserInput = exports.isValidObjectId = exports.sanitizeHTML = exports.sanitizeString = exports.isValidFileType = exports.isValidFileSize = exports.isValidDate = exports.isValidURL = exports.isValidUUID = exports.isValidObject = exports.isValidArray = exports.isRequiredBoolean = exports.isRequiredNumber = exports.isRequiredString = exports.DISPLAY_NAME_UNFLANKED_SEPARATOR_SOURCE = exports.DISPLAY_NAME_ORPHANED_MARK_SOURCE = exports.DISPLAY_NAME_DISALLOWED_SOURCE = exports.DISPLAY_NAME_ALLOWED_SCRIPTS = exports.isValidDisplayName = exports.isValidPassword = exports.isValidUsername = exports.isValidEmail = exports.DISPLAY_NAME_INVALID_MESSAGE = exports.MAX_DISPLAY_NAME_LENGTH = exports.PASSWORD_REGEX = exports.USERNAME_REGEX = exports.EMAIL_REGEX = exports.retryAsync = exports.validateRequiredFields = exports.parseHttpErrorBody = exports.isHttpRequestError = exports.handleHttpError = exports.createApiError = exports.ErrorCodes = exports.safeJsonParse = exports.buildPaginationParams = exports.buildUrl = exports.buildSearchParams = exports.buildQueryParams = exports.trustTierLabel = exports.reputationCategoryLabel = exports.accountRoleLabel = exports.accountCategoryLabel = exports.translate = exports.withRetry = exports.delay = exports.shouldAllowRequest = exports.recordSuccess = exports.recordFailure = void 0;
24
24
  exports.projectDevicePrincipals = exports.directoryHandle = exports.directoryDisplayName = exports.canActivateContext = exports.accountIdsOf = exports.activeUserOf = exports.activeSessionIdOf = exports.deviceStateToClientSessions = exports.createSessionClient = exports.createSessionClientHost = exports.SessionClient = exports.isAllowedDeviceJoinOrigin = exports.isOfficialWebOrigin = exports.isLoopbackOrigin = exports.consumeOAuthReturnPath = exports.persistOAuthReturnPath = exports.clearOAuthHandshake = exports.readOAuthHandshake = exports.persistOAuthHandshake = exports.canonicalizeOAuthRedirectUri = exports.normalizeOAuthRedirectUri = exports.OXY_OAUTH_RETURN_PATH_STORAGE_KEY = exports.OXY_OAUTH_REDIRECT_URI_STORAGE_KEY = exports.OXY_OAUTH_CODE_VERIFIER_STORAGE_KEY = exports.OXY_OAUTH_STATE_STORAGE_KEY = exports.OXY_AUTHORIZE_URL = exports.DEFAULT_OAUTH_SCOPE = exports.generatePkcePair = exports.generateOAuthState = exports.computeCodeChallenge = exports.buildOAuthAuthorizeUrl = exports.runColdBoot = exports.isOxyRpOrigin = exports.CENTRAL_IDP_APEX = exports.registrableApex = exports.getAccountColor = exports.formatPublicKeyHandle = exports.getAccountFallbackHandle = exports.getAccountDisplayName = exports.createQuickAccount = exports.buildAccountsArray = exports.updateAvatarVisibility = exports.isDev = exports.consoleSink = exports.resetLoggerConfig = exports.getLoggerConfig = exports.configureLogger = exports.createLogger = exports.logger = exports.normalizeMultilineText = void 0;
25
- exports.packageInfo = exports.runSessionColdBoot = exports.TOKEN_REFRESH_LEAD_MS = exports.startTokenRefreshScheduler = exports.installAuthRefreshHandler = exports.createAuthRefreshHandler = exports.refreshDeviceSecretArm = exports.refreshPersistedSession = exports.AccountNotOnDeviceError = exports.establishIdentitySession = exports.resolveIdentityPin = exports.IDENTITY_PIN_STORAGE_KEY = exports.identityPinMatches = exports.createMemoryIdentityPinStore = exports.createNativeIdentityPinStore = exports.createWebIdentityPinStore = exports.readLocalDeviceCredential = exports.publishProvenDeviceCredential = exports.normalizeSharedDeviceSessionRead = exports.decideSharedDevicePublish = exports.decideSharedDeviceJoin = exports.createSharedMirroringAuthStateStore = exports.AUTH_STATE_STORAGE_KEY = exports.createMemoryAuthStateStore = exports.createNativeAuthStateStore = exports.createWebAuthStateStore = exports.createAccountDialogController = exports.AccountDialogController = exports.canSwitchIntoAccount = exports.isSwitchTargetAccount = exports.showsPrincipalHeaders = exports.buildSwitcherRows = exports.resolveDeviceContext = exports.resolveActiveContext = void 0;
25
+ exports.packageInfo = exports.runSessionColdBoot = exports.OXY_INFERENCE_BASE_URL = exports.OxyInferenceError = exports.OxyInferenceClient = exports.TOKEN_REFRESH_LEAD_MS = exports.startTokenRefreshScheduler = exports.installAuthRefreshHandler = exports.createAuthRefreshHandler = exports.refreshDeviceSecretArm = exports.refreshPersistedSession = exports.AccountNotOnDeviceError = exports.establishIdentitySession = exports.resolveIdentityPin = exports.IDENTITY_PIN_STORAGE_KEY = exports.identityPinMatches = exports.createMemoryIdentityPinStore = exports.createNativeIdentityPinStore = exports.createWebIdentityPinStore = exports.readLocalDeviceCredential = exports.publishProvenDeviceCredential = exports.normalizeSharedDeviceSessionRead = exports.decideSharedDevicePublish = exports.decideSharedDeviceJoin = exports.createSharedMirroringAuthStateStore = exports.AUTH_STATE_STORAGE_KEY = exports.createMemoryAuthStateStore = exports.createNativeAuthStateStore = exports.createWebAuthStateStore = exports.createAccountDialogController = exports.AccountDialogController = exports.canSwitchIntoAccount = exports.isSwitchTargetAccount = exports.showsPrincipalHeaders = exports.buildSwitcherRows = exports.resolveDeviceContext = exports.resolveActiveContext = void 0;
26
26
  // Ensure crypto polyfills are loaded before anything else
27
27
  require("./crypto/polyfill");
28
28
  // ---------------------------------------------------------------------------
@@ -470,6 +470,13 @@ Object.defineProperty(exports, "createAuthRefreshHandler", { enumerable: true, g
470
470
  Object.defineProperty(exports, "installAuthRefreshHandler", { enumerable: true, get: function () { return refresh_1.installAuthRefreshHandler; } });
471
471
  Object.defineProperty(exports, "startTokenRefreshScheduler", { enumerable: true, get: function () { return refresh_1.startTokenRefreshScheduler; } });
472
472
  Object.defineProperty(exports, "TOKEN_REFRESH_LEAD_MS", { enumerable: true, get: function () { return refresh_1.TOKEN_REFRESH_LEAD_MS; } });
473
+ // The inference API. `oxyServices.inference()` binds the session bearer into
474
+ // the same client an external developer constructs with an `oxy_sk_…` machine
475
+ // key — one surface, two credential lanes. See `docs/inference/sdk.md`.
476
+ var OxyInferenceClient_1 = require("./inference/OxyInferenceClient");
477
+ Object.defineProperty(exports, "OxyInferenceClient", { enumerable: true, get: function () { return OxyInferenceClient_1.OxyInferenceClient; } });
478
+ Object.defineProperty(exports, "OxyInferenceError", { enumerable: true, get: function () { return OxyInferenceClient_1.OxyInferenceError; } });
479
+ Object.defineProperty(exports, "OXY_INFERENCE_BASE_URL", { enumerable: true, get: function () { return OxyInferenceClient_1.OXY_INFERENCE_BASE_URL; } });
473
480
  var sessionColdBoot_1 = require("./boot/sessionColdBoot");
474
481
  Object.defineProperty(exports, "runSessionColdBoot", { enumerable: true, get: function () { return sessionColdBoot_1.runSessionColdBoot; } });
475
482
  // API response contracts (request/response Zod schemas + inferred types) live in
@@ -0,0 +1,330 @@
1
+ "use strict";
2
+ /**
3
+ * The Oxy inference client — one surface, two credential lanes (issue #972,
4
+ * workstream 15).
5
+ *
6
+ * ```typescript
7
+ * // An OpenAI-style machine key: one bearer string, no session, no exchange.
8
+ * const oxy = new OxyInferenceClient({ credential: process.env.OXY_API_KEY });
9
+ *
10
+ * // Oxy auth: whatever bearer the session or the service-token mint holds.
11
+ * const oxy = oxyServices.inference();
12
+ * ```
13
+ *
14
+ * Both lanes reach the SAME endpoints and are told apart only by how the bearer
15
+ * is produced: a machine key is a constant string, and an Oxy bearer rotates, so
16
+ * it is a function this client calls on every request rather than a value it
17
+ * captures once. There is no third lane, and no method behaves differently
18
+ * depending on which one you used.
19
+ *
20
+ * ## What you will observe today
21
+ *
22
+ * **Every invoke refuses.** `respond()` reaches the public edge, which
23
+ * authenticates the credential, resolves attribution, authorizes scopes, pins a
24
+ * routing policy and reserves spend — and then has no data plane to forward to,
25
+ * so it releases the hold and answers `service_unavailable`. That surfaces here
26
+ * as an {@link OxyInferenceError} with `code: 'service_unavailable'`,
27
+ * `retryable: false` and a `requestId`. It is the correct answer, not a
28
+ * misconfiguration of yours, and no balance is spent.
29
+ *
30
+ * **The catalogue is empty**, so {@link OxyInferenceClient.listModels} answers
31
+ * `[]` and {@link OxyInferenceClient.getModel} throws for every id. `[]` is a
32
+ * normal answer to render, not an error to retry.
33
+ *
34
+ * `docs/inference/README.md` is the status board; `docs/inference/sdk.md` is
35
+ * this client's page.
36
+ *
37
+ * ## Why this is a client and not more methods on `OxyServices`
38
+ *
39
+ * Two reasons, both structural. A machine-key holder has no Oxy session at all,
40
+ * so a surface reached only through the session client would be unreachable for
41
+ * exactly the developer this workstream exists to serve. And the `/v1` error
42
+ * body is the contract's `InferenceError` at the top level rather than the
43
+ * platform's `{ error, message }` envelope — it carries `requestId`, `retryable`
44
+ * and `retryAfterMs`, all of which `OxyServices.handleError` would flatten into a
45
+ * message string. `oxyServices.inference()` binds the session bearer into this
46
+ * client so a session-holding app writes no plumbing of its own.
47
+ *
48
+ * ## Streaming is absent on purpose
49
+ *
50
+ * There is no `stream()` method and no `stream` field on a request. The stream
51
+ * event union exists in `@oxyhq/contracts` and no endpoint emits one — the edge
52
+ * refuses `stream: true` with `invalid_request`. A method that always failed
53
+ * would be a worse artefact than an absent one. See
54
+ * `docs/inference/streaming.md`.
55
+ *
56
+ * ## Field names, and the one place they could drift
57
+ *
58
+ * Every VALUE type here comes from `@oxyhq/contracts` — messages, tools, tool
59
+ * choice, response format, usage quantities, unit prices, error codes. The
60
+ * request FIELD NAMES cannot: they belong to `responsesRequestSchema`, which
61
+ * lives in the API because it is a public dialect rather than an Oxy↔data-plane
62
+ * contract. `packages/api/src/schemas/__tests__/sdkRequestCompatibility.test.ts`
63
+ * is the gate — it parses a value of this module's request type against that
64
+ * schema, so a rename on either side fails a build rather than a customer's
65
+ * request.
66
+ */
67
+ var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
68
+ if (kind === "m") throw new TypeError("Private method is not writable");
69
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
70
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
71
+ return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
72
+ };
73
+ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
74
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
75
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
76
+ return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
77
+ };
78
+ var _OxyInferenceClient_instances, _OxyInferenceClient_baseURL, _OxyInferenceClient_credential, _OxyInferenceClient_fetch, _OxyInferenceClient_bearer, _OxyInferenceClient_request;
79
+ Object.defineProperty(exports, "__esModule", { value: true });
80
+ exports.OxyInferenceClient = exports.OxyInferenceError = exports.OXY_INFERENCE_BASE_URL = void 0;
81
+ const contracts_1 = require("@oxyhq/contracts");
82
+ /** The base URL of the Oxy API, when a caller names none. */
83
+ exports.OXY_INFERENCE_BASE_URL = 'https://api.oxy.so';
84
+ /**
85
+ * Anything the inference API refused.
86
+ *
87
+ * `retryable` is asserted by the server and looked up from a total map over the
88
+ * closed code set — never inferred here from the status. A client that decides
89
+ * retryability from an HTTP status is exactly what the contract's retryability
90
+ * rule exists to prevent, so this class carries the server's answer and does not
91
+ * compute one.
92
+ */
93
+ class OxyInferenceError extends Error {
94
+ constructor(input) {
95
+ super(input.message);
96
+ this.name = 'OxyInferenceError';
97
+ this.code = input.code;
98
+ this.retryable = input.retryable;
99
+ this.requestId = input.requestId;
100
+ this.status = input.status;
101
+ if (input.retryAfterMs !== undefined)
102
+ this.retryAfterMs = input.retryAfterMs;
103
+ if (input.param !== undefined)
104
+ this.param = input.param;
105
+ }
106
+ }
107
+ exports.OxyInferenceError = OxyInferenceError;
108
+ /**
109
+ * The Oxy inference API.
110
+ *
111
+ * Stateless: it holds a base URL, a way to get a bearer and a `fetch`. Nothing
112
+ * is cached, because the two things worth caching here are a catalogue that is
113
+ * audience-scoped and a receipt that is immutable but rarely re-read.
114
+ *
115
+ * Successful responses are TYPED, not re-parsed. The server validates every one
116
+ * against its own schema before serving it, and a second client-side parse of a
117
+ * non-strict shape would silently DROP fields a newer API added — turning
118
+ * forward compatibility into data loss. Refusals are read defensively, because
119
+ * two routers answer under `/v1` and an unreadable failure must still reach the
120
+ * caller as one.
121
+ */
122
+ class OxyInferenceClient {
123
+ constructor(options) {
124
+ _OxyInferenceClient_instances.add(this);
125
+ _OxyInferenceClient_baseURL.set(this, void 0);
126
+ _OxyInferenceClient_credential.set(this, void 0);
127
+ _OxyInferenceClient_fetch.set(this, void 0);
128
+ const baseURL = options.baseURL ?? exports.OXY_INFERENCE_BASE_URL;
129
+ __classPrivateFieldSet(this, _OxyInferenceClient_baseURL, baseURL.endsWith('/') ? baseURL.slice(0, -1) : baseURL, "f");
130
+ __classPrivateFieldSet(this, _OxyInferenceClient_credential, options.credential, "f");
131
+ const fetchImpl = options.fetch ?? globalThis.fetch;
132
+ if (fetchImpl === undefined) {
133
+ throw new Error('OxyInferenceClient needs a fetch implementation: this runtime has no global fetch, so pass one as `fetch`.');
134
+ }
135
+ __classPrivateFieldSet(this, _OxyInferenceClient_fetch, fetchImpl, "f");
136
+ }
137
+ /**
138
+ * The models this caller may use — `GET /v1/models`.
139
+ *
140
+ * Audience-scoped server-side. A machine credential and an anonymous caller
141
+ * both see the PUBLIC catalogue; only an internal/system application's
142
+ * service token sees internal-only routes.
143
+ *
144
+ * **`[]` is a normal answer**, and is the only answer today: the catalogue
145
+ * is populated by operators, and a route is not publicly exposed until
146
+ * somebody has reviewed the right to resell it.
147
+ */
148
+ async listModels(options = {}) {
149
+ const body = await __classPrivateFieldGet(this, _OxyInferenceClient_instances, "m", _OxyInferenceClient_request).call(this, 'GET', '/v1/models', { ...(options.signal === undefined ? {} : { signal: options.signal }) });
150
+ return body.data;
151
+ }
152
+ /**
153
+ * One catalogue entry by its canonical id — `GET /v1/models/:publisher/:model`.
154
+ *
155
+ * The id is TWO path segments, because a canonical model id contains a slash
156
+ * and a single encoded segment would never match the route.
157
+ *
158
+ * A model you may not see answers 404 identically to one that does not
159
+ * exist, deliberately: the catalogue is never an existence oracle for what
160
+ * Oxy runs internally.
161
+ *
162
+ * @param modelId - `<publisher>/<model>`. A revision pin
163
+ * (`<publisher>/<model>@<revision>`) names a model REFERENCE rather than a
164
+ * model and is rejected here rather than sent, because the catalogue is
165
+ * keyed on models and a pinned reference would 404 indistinguishably from
166
+ * "no such model".
167
+ */
168
+ async getModel(modelId, options = {}) {
169
+ const parsed = contracts_1.modelIdSchema.safeParse(modelId);
170
+ if (!parsed.success) {
171
+ throw new Error(`Not a canonical model id: ${modelId}. Expected <publisher>/<model>, e.g. acme/some-model.`);
172
+ }
173
+ const [publisher, model] = parsed.data.split('/');
174
+ const body = await __classPrivateFieldGet(this, _OxyInferenceClient_instances, "m", _OxyInferenceClient_request).call(this, 'GET', `/v1/models/${encodeURIComponent(publisher)}/${encodeURIComponent(model)}`, { ...(options.signal === undefined ? {} : { signal: options.signal }) });
175
+ return body.data;
176
+ }
177
+ /**
178
+ * The routing profiles this caller may select — `GET /v1/models/routing-profiles`.
179
+ *
180
+ * A profile is a named strategy for CHOOSING among routes, not a model: no
181
+ * publisher, no revision, no licence, no weights. Like the model list, `[]`
182
+ * is a normal answer.
183
+ */
184
+ async listRoutingProfiles(options = {}) {
185
+ const body = await __classPrivateFieldGet(this, _OxyInferenceClient_instances, "m", _OxyInferenceClient_request).call(this, 'GET', '/v1/models/routing-profiles', { ...(options.signal === undefined ? {} : { signal: options.signal }) });
186
+ return body.data;
187
+ }
188
+ /**
189
+ * Send one non-streaming inference request — `POST /v1/responses`.
190
+ *
191
+ * **This refuses in every deployment today** with `service_unavailable`,
192
+ * because there is no data plane behind the edge. The spend held for the
193
+ * request is released before the refusal returns, so nothing is charged.
194
+ *
195
+ * @throws {OxyInferenceError} for every refusal, carrying the server's own
196
+ * `code`, `retryable` and `requestId`.
197
+ */
198
+ async respond(request, options = {}) {
199
+ return __classPrivateFieldGet(this, _OxyInferenceClient_instances, "m", _OxyInferenceClient_request).call(this, 'POST', '/v1/responses', {
200
+ body: request,
201
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
202
+ ...(options.idempotencyKey === undefined
203
+ ? {}
204
+ : { idempotencyKey: options.idempotencyKey }),
205
+ ...(options.delegatedUserId === undefined
206
+ ? {}
207
+ : { delegatedUserId: options.delegatedUserId }),
208
+ });
209
+ }
210
+ /**
211
+ * Read back the settled receipt for one request —
212
+ * `GET /v1/generations/:id`.
213
+ *
214
+ * `id` is the `requestId` you already hold (it is on every response and
215
+ * every error) or the `generationId`. Requires the `inference:usage:read`
216
+ * scope; a caller without it, or one whose application did not make the
217
+ * request, is told the receipt does not exist rather than that it belongs to
218
+ * somebody else.
219
+ */
220
+ async getGeneration(id, options = {}) {
221
+ const body = await __classPrivateFieldGet(this, _OxyInferenceClient_instances, "m", _OxyInferenceClient_request).call(this, 'GET', `/v1/generations/${encodeURIComponent(id)}`, { ...(options.signal === undefined ? {} : { signal: options.signal }) });
222
+ return body.data;
223
+ }
224
+ }
225
+ exports.OxyInferenceClient = OxyInferenceClient;
226
+ _OxyInferenceClient_baseURL = new WeakMap(), _OxyInferenceClient_credential = new WeakMap(), _OxyInferenceClient_fetch = new WeakMap(), _OxyInferenceClient_instances = new WeakSet(), _OxyInferenceClient_bearer =
227
+ /** The bearer for this request, from whichever lane was configured. */
228
+ async function _OxyInferenceClient_bearer() {
229
+ const value = typeof __classPrivateFieldGet(this, _OxyInferenceClient_credential, "f") === 'string'
230
+ ? __classPrivateFieldGet(this, _OxyInferenceClient_credential, "f")
231
+ : await __classPrivateFieldGet(this, _OxyInferenceClient_credential, "f").call(this);
232
+ if (value === null || value === undefined || value.length === 0) {
233
+ throw new Error('OxyInferenceClient has no bearer: the configured credential resolved to nothing. On the Oxy auth lane this usually means the session is not restored yet.');
234
+ }
235
+ return value;
236
+ }, _OxyInferenceClient_request =
237
+ /**
238
+ * One request, and the one place a refusal becomes an
239
+ * {@link OxyInferenceError}.
240
+ *
241
+ * Two error shapes arrive here, because two routers serve `/v1`. The edge
242
+ * returns the contract error at the top level; the catalogue returns the
243
+ * platform's `{ error, message }` envelope. Both are read, and a body that
244
+ * is neither still produces an `OxyInferenceError` — with the code the
245
+ * status maps to — rather than a bare `Error`, so a caller's `catch` never
246
+ * has to branch on which router answered.
247
+ */
248
+ async function _OxyInferenceClient_request(method, path, options) {
249
+ const headers = {
250
+ Authorization: `Bearer ${await __classPrivateFieldGet(this, _OxyInferenceClient_instances, "m", _OxyInferenceClient_bearer).call(this)}`,
251
+ Accept: 'application/json',
252
+ };
253
+ if (options.body !== undefined)
254
+ headers['Content-Type'] = 'application/json';
255
+ if (options.idempotencyKey !== undefined) {
256
+ headers['Idempotency-Key'] = options.idempotencyKey;
257
+ }
258
+ if (options.delegatedUserId !== undefined) {
259
+ headers['X-Oxy-User-Id'] = options.delegatedUserId;
260
+ }
261
+ const response = await __classPrivateFieldGet(this, _OxyInferenceClient_fetch, "f").call(this, `${__classPrivateFieldGet(this, _OxyInferenceClient_baseURL, "f")}${path}`, {
262
+ method,
263
+ headers,
264
+ ...(options.body === undefined ? {} : { body: JSON.stringify(options.body) }),
265
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
266
+ });
267
+ const payload = await response.json().catch(() => undefined);
268
+ if (!response.ok) {
269
+ throw toInferenceError(payload, response.status, response.headers.get('X-Oxy-Request-Id'));
270
+ }
271
+ return payload;
272
+ };
273
+ /**
274
+ * Which code a status means when the body did not name one.
275
+ *
276
+ * Deliberately partial: only the statuses whose meaning is unambiguous without a
277
+ * body. Everything else becomes `internal_error`, which is non-retryable — the
278
+ * safe direction, since inventing a retryable code for an unreadable failure is
279
+ * how one outage becomes a retry storm.
280
+ */
281
+ const STATUS_FALLBACK_CODE = {
282
+ 400: 'invalid_request',
283
+ 401: 'authentication_failed',
284
+ 403: 'permission_denied',
285
+ 404: 'model_not_found',
286
+ 409: 'idempotency_conflict',
287
+ 413: 'request_too_large',
288
+ 429: 'rate_limited',
289
+ 502: 'provider_error',
290
+ 503: 'service_unavailable',
291
+ 504: 'provider_timeout',
292
+ };
293
+ /**
294
+ * The closed set the contract defines, as a lookup.
295
+ *
296
+ * A `code` outside it is a contract violation rather than a code this client
297
+ * has not caught up with — `INFERENCE_ERROR_CODES` and the version header move
298
+ * together — so an unrecognised one falls back to the status map instead of
299
+ * being asserted into the type.
300
+ */
301
+ const INFERENCE_ERROR_CODE_SET = new Set(contracts_1.INFERENCE_ERROR_CODES);
302
+ /** Read whichever error shape arrived into the one this client throws. */
303
+ function toInferenceError(payload, status, requestIdHeader) {
304
+ const body = (payload ?? {});
305
+ // The edge's own shape is the contract error at the top level; the
306
+ // catalogue's is the platform envelope, whose `error` is a string.
307
+ const code = typeof body.code === 'string' && INFERENCE_ERROR_CODE_SET.has(body.code)
308
+ ? body.code
309
+ : (STATUS_FALLBACK_CODE[status] ?? 'internal_error');
310
+ const message = typeof body.message === 'string' && body.message.length > 0
311
+ ? body.message
312
+ : typeof body.error === 'string' && body.error.length > 0
313
+ ? body.error
314
+ : `The inference API answered ${status}.`;
315
+ return new OxyInferenceError({
316
+ code,
317
+ message,
318
+ // A body that did not assert retryability is not retryable: the server
319
+ // is the only thing that may say a retry could succeed.
320
+ retryable: body.retryable === true,
321
+ requestId: typeof body.requestId === 'string' && body.requestId.length > 0
322
+ ? body.requestId
323
+ : (requestIdHeader ?? ''),
324
+ status,
325
+ ...(body.retryable === true && typeof body.retryAfterMs === 'number'
326
+ ? { retryAfterMs: body.retryAfterMs }
327
+ : {}),
328
+ ...(typeof body.param === 'string' ? { param: body.param } : {}),
329
+ });
330
+ }
@@ -341,76 +341,6 @@ function OxyServicesAccountsMixin(Base) {
341
341
  }
342
342
  }
343
343
  // =========================================================================
344
- // Bot (account) service credentials — /accounts/:id/credentials
345
- // =========================================================================
346
- /**
347
- * List a bot account's service credentials. The response NEVER includes
348
- * secrets.
349
- * @param accountId - The account's Mongo `_id`.
350
- */
351
- async listAccountCredentials(accountId) {
352
- try {
353
- const res = await this.makeRequest('GET', `/accounts/${encodeURIComponent(accountId)}/credentials`, undefined, { cache: true, cacheTTL: mixinHelpers_1.CACHE_TIMES.MEDIUM });
354
- return res.credentials ?? [];
355
- }
356
- catch (error) {
357
- throw this.handleError(error);
358
- }
359
- }
360
- /**
361
- * Create a service credential for a bot account. The plaintext `secret` is
362
- * returned exactly ONCE; the server stores only a hash and will never return
363
- * it again.
364
- * @param accountId - The account's Mongo `_id`.
365
- * @param data - Credential configuration (`type` is always `service`).
366
- */
367
- async createAccountCredential(accountId, data) {
368
- try {
369
- const result = await this.makeRequest('POST', `/accounts/${encodeURIComponent(accountId)}/credentials`, data, { cache: false });
370
- this.clearCacheEntry(`GET:/accounts/${encodeURIComponent(accountId)}/credentials`);
371
- return result;
372
- }
373
- catch (error) {
374
- throw this.handleError(error);
375
- }
376
- }
377
- /**
378
- * Rotate a bot credential's secret. The new plaintext `secret` is returned
379
- * exactly ONCE, along with audit fields: `rotatedFrom` (the previous
380
- * credentialId) and `graceExpiresAt` (ISO string for the grace window during
381
- * which the old credential is still honoured).
382
- * @param accountId - The account's Mongo `_id`.
383
- * @param credentialId - The credential's Mongo `_id`.
384
- */
385
- async rotateAccountCredential(accountId, credentialId) {
386
- try {
387
- const result = await this.makeRequest('POST', `/accounts/${encodeURIComponent(accountId)}/credentials/${encodeURIComponent(credentialId)}/rotate`, undefined, { cache: false });
388
- // Rotation changes credential status/audit fields surfaced by the list.
389
- this.clearCacheEntry(`GET:/accounts/${encodeURIComponent(accountId)}/credentials`);
390
- return result;
391
- }
392
- catch (error) {
393
- throw this.handleError(error);
394
- }
395
- }
396
- /**
397
- * Revoke a bot credential (`status='revoked'`). Revoked credentials can no
398
- * longer authenticate.
399
- * @param accountId - The account's Mongo `_id`.
400
- * @param credentialId - The credential's Mongo `_id`.
401
- */
402
- async revokeAccountCredential(accountId, credentialId) {
403
- try {
404
- const result = await this.makeRequest('DELETE', `/accounts/${encodeURIComponent(accountId)}/credentials/${encodeURIComponent(credentialId)}`, undefined, { cache: false });
405
- // Revocation flips the credential's status in the cached list.
406
- this.clearCacheEntry(`GET:/accounts/${encodeURIComponent(accountId)}/credentials`);
407
- return result;
408
- }
409
- catch (error) {
410
- throw this.handleError(error);
411
- }
412
- }
413
- // =========================================================================
414
344
  // Applications owned by an account — /applications
415
345
  // =========================================================================
416
346
  /**
@@ -531,10 +461,13 @@ function OxyServicesAccountsMixin(Base) {
531
461
  * which the old credential is still honoured).
532
462
  * @param applicationId - The application's Mongo `_id`.
533
463
  * @param credentialId - The credential's Mongo `_id`.
464
+ * @param options - `graceSeconds` keeps a superseded `machine` token working
465
+ * for that long. Omitted, the previous token dies the moment the
466
+ * replacement is minted.
534
467
  */
535
- async rotateAppCredential(applicationId, credentialId) {
468
+ async rotateAppCredential(applicationId, credentialId, options) {
536
469
  try {
537
- const result = await this.makeRequest('POST', `/applications/${encodeURIComponent(applicationId)}/credentials/${encodeURIComponent(credentialId)}/rotate`, undefined, { cache: false });
470
+ const result = await this.makeRequest('POST', `/applications/${encodeURIComponent(applicationId)}/credentials/${encodeURIComponent(credentialId)}/rotate`, options, { cache: false });
538
471
  // Rotation changes credential status/audit fields surfaced by the list.
539
472
  this.clearCacheEntry(`GET:/applications/${encodeURIComponent(applicationId)}/credentials`);
540
473
  return result;