@oxyhq/core 20.1.0 → 21.0.1

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 (109) hide show
  1. package/dist/cjs/.tsbuildinfo +1 -1
  2. package/dist/cjs/HttpService.js +47 -8
  3. package/dist/cjs/boot/sessionColdBoot.js +107 -8
  4. package/dist/cjs/i18n/locales/en-US.json +26 -4
  5. package/dist/cjs/i18n/locales/es-ES.json +26 -4
  6. package/dist/cjs/i18n/locales/locales/en-US.json +26 -4
  7. package/dist/cjs/i18n/locales/locales/es-ES.json +26 -4
  8. package/dist/cjs/index.js +57 -16
  9. package/dist/cjs/inference/OxyInferenceClient.js +330 -0
  10. package/dist/cjs/mixins/OxyServices.accounts.js +5 -72
  11. package/dist/cjs/mixins/OxyServices.auth.js +27 -3
  12. package/dist/cjs/mixins/OxyServices.inference.js +59 -0
  13. package/dist/cjs/mixins/OxyServices.utility.js +18 -6
  14. package/dist/cjs/mixins/index.js +6 -0
  15. package/dist/cjs/server/auth.js +76 -0
  16. package/dist/cjs/server/index.js +5 -1
  17. package/dist/cjs/session/SessionClient.js +361 -1
  18. package/dist/cjs/session/accountDialogController.js +121 -147
  19. package/dist/cjs/session/accountSwitchTargets.js +75 -0
  20. package/dist/cjs/session/deviceDirectory.js +143 -0
  21. package/dist/cjs/session/deviceSwitcherRows.js +76 -0
  22. package/dist/cjs/session/projectSessionState.js +8 -1
  23. package/dist/cjs/session/sharedDeviceCredential.js +247 -0
  24. package/dist/esm/.tsbuildinfo +1 -1
  25. package/dist/esm/HttpService.js +47 -8
  26. package/dist/esm/boot/sessionColdBoot.js +107 -8
  27. package/dist/esm/i18n/locales/en-US.json +26 -4
  28. package/dist/esm/i18n/locales/es-ES.json +26 -4
  29. package/dist/esm/i18n/locales/locales/en-US.json +26 -4
  30. package/dist/esm/i18n/locales/locales/es-ES.json +26 -4
  31. package/dist/esm/index.js +36 -10
  32. package/dist/esm/inference/OxyInferenceClient.js +325 -0
  33. package/dist/esm/mixins/OxyServices.accounts.js +5 -72
  34. package/dist/esm/mixins/OxyServices.auth.js +27 -3
  35. package/dist/esm/mixins/OxyServices.inference.js +56 -0
  36. package/dist/esm/mixins/OxyServices.utility.js +18 -6
  37. package/dist/esm/mixins/index.js +6 -0
  38. package/dist/esm/server/auth.js +72 -0
  39. package/dist/esm/server/index.js +1 -1
  40. package/dist/esm/session/SessionClient.js +362 -2
  41. package/dist/esm/session/accountDialogController.js +121 -147
  42. package/dist/esm/session/accountSwitchTargets.js +71 -0
  43. package/dist/esm/session/deviceDirectory.js +135 -0
  44. package/dist/esm/session/deviceSwitcherRows.js +72 -0
  45. package/dist/esm/session/projectSessionState.js +8 -2
  46. package/dist/esm/session/sharedDeviceCredential.js +239 -0
  47. package/dist/types/.tsbuildinfo +1 -1
  48. package/dist/types/HttpService.d.ts +39 -1
  49. package/dist/types/boot/sessionColdBoot.d.ts +24 -4
  50. package/dist/types/index.d.ts +11 -4
  51. package/dist/types/inference/OxyInferenceClient.d.ts +324 -0
  52. package/dist/types/mixins/OxyServices.accounts.d.ts +73 -95
  53. package/dist/types/mixins/OxyServices.auth.d.ts +75 -3
  54. package/dist/types/mixins/OxyServices.inference.d.ts +95 -0
  55. package/dist/types/mixins/OxyServices.utility.d.ts +44 -13
  56. package/dist/types/mixins/index.d.ts +2 -1
  57. package/dist/types/models/session.d.ts +11 -0
  58. package/dist/types/server/auth.d.ts +80 -0
  59. package/dist/types/server/index.d.ts +2 -2
  60. package/dist/types/session/SessionClient.d.ts +202 -1
  61. package/dist/types/session/accountDialogController.d.ts +76 -64
  62. package/dist/types/session/accountSwitchTargets.d.ts +64 -0
  63. package/dist/types/session/deviceDirectory.d.ts +182 -0
  64. package/dist/types/session/deviceSwitcherRows.d.ts +92 -0
  65. package/dist/types/session/projectSessionState.d.ts +29 -0
  66. package/dist/types/session/sharedDeviceCredential.d.ts +202 -0
  67. package/package.json +3 -3
  68. package/src/HttpService.ts +50 -10
  69. package/src/__tests__/httpServiceUnwrapEnvelope.test.ts +115 -0
  70. package/src/boot/__tests__/sessionColdBoot.sharedDevice.test.ts +325 -0
  71. package/src/boot/sessionColdBoot.ts +133 -9
  72. package/src/i18n/locales/en-US.json +26 -4
  73. package/src/i18n/locales/es-ES.json +26 -4
  74. package/src/index.ts +94 -25
  75. package/src/inference/OxyInferenceClient.ts +590 -0
  76. package/src/inference/__tests__/OxyInferenceClient.test.ts +383 -0
  77. package/src/mixins/OxyServices.accounts.ts +75 -176
  78. package/src/mixins/OxyServices.auth.ts +67 -5
  79. package/src/mixins/OxyServices.inference.ts +57 -0
  80. package/src/mixins/OxyServices.utility.ts +58 -14
  81. package/src/mixins/__tests__/accounts.test.ts +57 -102
  82. package/src/mixins/__tests__/inferenceFactory.test.ts +58 -0
  83. package/src/mixins/__tests__/preSessionSkipAuth.test.ts +54 -1
  84. package/src/mixins/__tests__/serviceAuth.test.ts +2 -0
  85. package/src/mixins/index.ts +8 -0
  86. package/src/models/session.ts +11 -0
  87. package/src/server/__tests__/serviceTokenAttribution.test.ts +396 -0
  88. package/src/server/auth.ts +118 -0
  89. package/src/server/index.ts +6 -0
  90. package/src/session/SessionClient.ts +386 -1
  91. package/src/session/__tests__/SessionClient.directory.test.ts +688 -0
  92. package/src/session/__tests__/accountDialogController.test.ts +411 -278
  93. package/src/session/__tests__/accountDialogShape.test.ts +118 -0
  94. package/src/session/__tests__/accountSwitchTargets.test.ts +132 -0
  95. package/src/session/__tests__/deviceDirectory.test.ts +422 -0
  96. package/src/session/__tests__/deviceSwitcherRows.test.ts +223 -0
  97. package/src/session/__tests__/projectSessionState.test.ts +17 -0
  98. package/src/session/__tests__/sharedDeviceCredential.test.ts +300 -0
  99. package/src/session/accountDialogController.ts +141 -179
  100. package/src/session/accountSwitchTargets.ts +87 -0
  101. package/src/session/deviceDirectory.ts +269 -0
  102. package/src/session/deviceSwitcherRows.ts +145 -0
  103. package/src/session/projectSessionState.ts +9 -3
  104. package/src/session/sharedDeviceCredential.ts +349 -0
  105. package/dist/cjs/session/accountProjection.js +0 -213
  106. package/dist/esm/session/accountProjection.js +0 -207
  107. package/dist/types/session/accountProjection.d.ts +0 -198
  108. package/src/session/__tests__/accountProjection.test.ts +0 -447
  109. package/src/session/accountProjection.ts +0 -354
@@ -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.
@@ -4,7 +4,7 @@
4
4
  * Supports password-based login (email/username) and public key challenge-response.
5
5
  */
6
6
  import type { User } from '../models/interfaces';
7
- import type { LoginResult, LoginSessionResult, CommonsDenyReason } from '@oxyhq/contracts';
7
+ import type { LoginResult, CommonsDenyReason } from '@oxyhq/contracts';
8
8
  import type { SessionLoginResponse } from '../models/session';
9
9
  import type { OxyServicesBase } from '../OxyServices.base';
10
10
  import type { PublicApplication } from './OxyServices.connectedApps';
@@ -39,6 +39,41 @@ export interface OAuthUserInfoResponse {
39
39
  name?: string;
40
40
  picture?: string;
41
41
  }
42
+ /**
43
+ * The session an OAuth authorization-code exchange yields.
44
+ *
45
+ * Deliberately NOT `LoginSessionResult`. That type mirrors the API's
46
+ * `buildSessionAuthResponse`, which every FIRST-PARTY sign-in lane emits, and it
47
+ * requires `deviceId` because those lanes always join the origin's DeviceSession.
48
+ * `POST /auth/oauth/token` is the RFC 6749 token endpoint and serves third
49
+ * parties, whose grant is deliberately ISOLATED: an untrusted application must be
50
+ * able to receive a session carrying NO DeviceSession credential at all.
51
+ *
52
+ * Both device fields are therefore optional here, and a response omitting them is
53
+ * a well-formed device-less grant rather than a malformed payload. What that
54
+ * costs the session is spelled out on `exchangeOAuthCode` below.
55
+ */
56
+ export interface OAuthTokenExchangeResult {
57
+ sessionId: string;
58
+ /** ISO-8601 expiry of {@link accessToken}, derived from RFC 6749 `expires_in`. */
59
+ expiresAt: string;
60
+ accessToken?: string;
61
+ /**
62
+ * The DeviceSession this grant joined, when the server issued one. ABSENT for
63
+ * an isolated third-party grant — never assume a string.
64
+ */
65
+ deviceId?: string;
66
+ /**
67
+ * The zero-cookie mint credential for {@link deviceId}. Present only alongside
68
+ * it; absent for an isolated third-party grant.
69
+ */
70
+ deviceSecret?: string;
71
+ user: {
72
+ id: string;
73
+ username?: string;
74
+ avatar?: string;
75
+ };
76
+ }
42
77
  /**
43
78
  * How a "Sign in with Oxy" request finalizes once the approver authorizes it.
44
79
  *
@@ -793,13 +828,30 @@ export declare function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(B
793
828
  * response this method used before were an Oxy invention no OAuth library
794
829
  * could interoperate with; the endpoint no longer accepts them. The method's
795
830
  * OWN signature is unchanged, so callers are unaffected.
831
+ *
832
+ * `deviceId` + `deviceSecret` are OPTIONAL and their absence is a valid
833
+ * outcome, not an error. A third-party grant is meant to be isolated from the
834
+ * browser's shared DeviceSession, so the token endpoint must be free to return
835
+ * no device credential at all — the guard that used to require the pair made
836
+ * that omission unshippable, since it turned every third-party sign-in through
837
+ * the SDK into a silent `exchange-failed`.
838
+ *
839
+ * The cost is real and deliberate: a DEVICE-LESS session cannot use the
840
+ * zero-cookie mint lane (`POST /session/device/token`), because that lane's
841
+ * whole proof is possession of a `deviceSecret`. Its lifetime is therefore the
842
+ * access token itself — nothing persists a restore credential, the cold boot's
843
+ * `device-secret-mint` step reports `no-secret` and skips, and the refresh
844
+ * scheduler has nothing to re-mint from. When the token expires the session
845
+ * ends LOUDLY: the 401 lane clears the tokens and the provider resolves signed
846
+ * out, so the app can run the OAuth flow again. It never degrades into a
847
+ * session that looks alive and cannot refresh.
796
848
  */
797
849
  exchangeOAuthCode(params: {
798
850
  code: string;
799
851
  clientId: string;
800
852
  redirectUri: string;
801
853
  codeVerifier: string;
802
- }): Promise<LoginSessionResult>;
854
+ }): Promise<OAuthTokenExchangeResult>;
803
855
  /**
804
856
  * Fetch OpenID Connect userinfo for the current bearer (`GET /auth/oauth/userinfo`).
805
857
  * The response is a flat JSON document — no `{ data }` wrapper.
@@ -850,7 +902,27 @@ export declare function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(B
850
902
  handleError(error: unknown): Error;
851
903
  healthCheck(): Promise<{
852
904
  status: string;
853
- users?: number;
905
+ users
906
+ /**
907
+ * @internal Narrow an untrusted delivery-progress timestamp from the status
908
+ * response.
909
+ *
910
+ * Returns the ISO-8601 string unchanged when it is a real, parseable instant,
911
+ * and `null` for everything else — absent (an older API that has no delivery
912
+ * progress at all), empty, non-string, or unparseable. Progress is advisory, so
913
+ * degrading to "no progress yet" is always safe; surfacing a garbage timestamp
914
+ * to the waiting UI is not.
915
+ */
916
+ ? /**
917
+ * @internal Narrow an untrusted delivery-progress timestamp from the status
918
+ * response.
919
+ *
920
+ * Returns the ISO-8601 string unchanged when it is a real, parseable instant,
921
+ * and `null` for everything else — absent (an older API that has no delivery
922
+ * progress at all), empty, non-string, or unparseable. Progress is advisory, so
923
+ * degrading to "no progress yet" is always safe; surfacing a garbage timestamp
924
+ * to the waiting UI is not.
925
+ */: number;
854
926
  timestamp?: string;
855
927
  [key: string]: any;
856
928
  }>;
@@ -0,0 +1,95 @@
1
+ /**
2
+ * The inference API, reached with whatever bearer this session already holds
3
+ * (issue #972, workstream 15).
4
+ *
5
+ * ```typescript
6
+ * const models = await oxyServices.inference().listModels();
7
+ * ```
8
+ *
9
+ * One method, and it is a FACTORY rather than a set of inference methods on
10
+ * `OxyServices`. The calls themselves live once, in
11
+ * {@link OxyInferenceClient} — which an external developer holding only an
12
+ * `oxy_sk_…` machine key constructs directly, with no Oxy session anywhere in
13
+ * the picture. Declaring the same calls a second time here would give the
14
+ * ecosystem two spellings of one request, and only one of them would stay
15
+ * correct.
16
+ *
17
+ * This is the reasoning `createLinkedClient` is already built on: the plumbing
18
+ * that binds an Oxy bearer to a client belongs in core, once, rather than in
19
+ * each app.
20
+ *
21
+ * The credential is a FUNCTION, not the current token: a session bearer rotates
22
+ * on refresh and on account switch, and a client that captured one at
23
+ * construction would start answering 401 an hour into the process's life.
24
+ */
25
+ import { OxyInferenceClient } from '../inference/OxyInferenceClient';
26
+ import type { OxyServicesBase } from '../OxyServices.base';
27
+ export declare function OxyServicesInferenceMixin<T extends typeof OxyServicesBase>(Base: T): {
28
+ new (...args: any[]): {
29
+ /** @internal Memoized so repeated calls return one object identity. */
30
+ _inferenceClient: OxyInferenceClient | null;
31
+ /**
32
+ * The inference client for this session.
33
+ *
34
+ * Bound to this instance's base URL and to `getAccessToken()`, so it
35
+ * follows every refresh, sign-in and account switch without being
36
+ * rebuilt.
37
+ *
38
+ * A service-authenticated process wants a different credential and
39
+ * builds {@link OxyInferenceClient} directly:
40
+ * `new OxyInferenceClient({ credential: () => oxy.getServiceToken() })`.
41
+ * The mint is asynchronous and cached, which is exactly what a
42
+ * credential function is for.
43
+ */
44
+ inference(): OxyInferenceClient;
45
+ httpService: import("../HttpService").HttpService;
46
+ cloudURL: string;
47
+ config: import("../OxyServices.base").OxyConfig;
48
+ __resetTokensForTests(): void;
49
+ makeRequest<T_1>(method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE", url: string, data?: any, options?: import("../HttpService").RequestOptions): Promise<T_1>;
50
+ getBaseURL(): string;
51
+ getClient(): import("../HttpService").HttpService;
52
+ createLinkedClient(config: import("../OxyServices.base").OxyConfig): import("..").LinkedHttpClient;
53
+ getMetrics(): {
54
+ totalRequests: number;
55
+ successfulRequests: number;
56
+ failedRequests: number;
57
+ cacheHits: number;
58
+ cacheMisses: number;
59
+ averageResponseTime: number;
60
+ };
61
+ clearCache(): void;
62
+ clearCacheEntry(key: string): void;
63
+ clearCacheByPrefix(prefix: string): number;
64
+ getCacheStats(): {
65
+ size: number;
66
+ hits: number;
67
+ misses: number;
68
+ hitRate: number;
69
+ };
70
+ getCloudURL(): string;
71
+ setTokens(accessToken: string): void;
72
+ clearTokens(): void;
73
+ onTokensChanged(listener: (accessToken: string | null) => void): () => void;
74
+ _cachedUserId: string | null | undefined;
75
+ _cachedAccessToken: string | null;
76
+ getCurrentUserId(): string | null;
77
+ hasValidToken(): boolean;
78
+ getAccessToken(): string | null;
79
+ getAccessTokenExpiry(): number | null;
80
+ waitForAuth(timeoutMs?: number): Promise<boolean>;
81
+ withAuthRetry<T_1>(operation: () => Promise<T_1>, operationName: string, options?: {
82
+ maxRetries?: number;
83
+ retryDelay?: number;
84
+ authTimeoutMs?: number;
85
+ }): Promise<T_1>;
86
+ validate(): Promise<boolean>;
87
+ handleError(error: unknown): Error;
88
+ healthCheck(): Promise<{
89
+ status: string;
90
+ users?: number;
91
+ timestamp?: string;
92
+ [key: string]: any;
93
+ }>;
94
+ };
95
+ } & T;
@@ -19,9 +19,21 @@ export interface ServiceActingAsVerification {
19
19
  }
20
20
  /**
21
21
  * Service app metadata attached to requests authenticated with service tokens.
22
- * `scopes` reflects the scopes granted to the app at signup time (from the
23
- * `Application.scopes` field); route-level checks can require additional
24
- * scope-narrowing via `requireScope()`.
22
+ *
23
+ * Every field comes from the token's SIGNED payload and is populated only after
24
+ * the signature, `iss`/`aud`/`type` binding and expiry all pass — so a verifier
25
+ * holding this object can name the responsible principals without a lookup of
26
+ * its own. Together with `credentialId` and `ownerAccountId` it is the canonical
27
+ * attribution tuple of ADR 0007 minus the delegated user.
28
+ *
29
+ * `scopes` are the EFFECTIVE scopes: the credential's own scopes intersected
30
+ * with the owning application's grant at mint time (the API's `intersectScopes`
31
+ * is the single authority for that intersection — nothing re-intersects here).
32
+ * Route-level checks narrow further via `requireScope()`.
33
+ *
34
+ * A delegated end user is NOT a field of this type, and must never become one.
35
+ * It lives in `req.serviceActingAs` / `req.userId`, is authorised per request,
36
+ * and is attribution only.
25
37
  */
26
38
  export interface ServiceApp {
27
39
  appId: string;
@@ -29,6 +41,12 @@ export interface ServiceApp {
29
41
  scopes: string[];
30
42
  /** The credentialId of the specific service credential that minted this token. */
31
43
  credentialId: string;
44
+ /**
45
+ * The Oxy account that owns `appId` and is financially responsible for it.
46
+ * The BILLING principal — never a user id, and never the delegated
47
+ * `X-Oxy-User-Id` (ADR 0007).
48
+ */
49
+ ownerAccountId: string;
32
50
  /** Test/live isolation (F2.0): which `ApplicationCredential.environment` minted this token. */
33
51
  environment: OxyServiceEnvironment;
34
52
  }
@@ -49,11 +67,24 @@ interface AuthMiddlewareOptions {
49
67
  * When provided, service tokens will be cryptographically verified.
50
68
  * When omitted, service tokens will be rejected (secure default).
51
69
  *
52
- * **Migration note (>=1.11.14):** the Oxy API now signs service tokens
53
- * with a dedicated `SERVICE_TOKEN_SECRET` distinct from `ACCESS_TOKEN_SECRET`.
54
- * Pass that value here. If you keep passing the access-token secret you will
55
- * still verify ALL signed-by-Oxy tokens (which is the whole class of bug
56
- * H4 was supposed to prevent DO NOT do that in production).
70
+ * **The only value that works is `ACCESS_TOKEN_SECRET`, and you should not
71
+ * want to hold it see issue #987 and ADR 0012.** The Oxy API signs service
72
+ * tokens with `ACCESS_TOKEN_SECRET` (`packages/api/src/routes/auth.ts`),
73
+ * which is also the key that signs every user access token. There is no
74
+ * separate service-token secret: earlier revisions of this comment named a
75
+ * `SERVICE_TOKEN_SECRET` that has never existed in the API, in any workflow or
76
+ * in any task definition, and passing one would fail every verification.
77
+ *
78
+ * The consequence to hold onto: the scheme is symmetric, so a host that can
79
+ * VERIFY a service token can also MINT one — including a user access token.
80
+ * **Local verification is therefore appropriate only inside the Oxy API's own
81
+ * trust boundary, and no service outside it holds this key today.** Do not be
82
+ * the first: if you need to verify Oxy service tokens from another service,
83
+ * follow #987 rather than copying the secret.
84
+ *
85
+ * `docs/adr/0012-service-token-signing-key-model.md` records the decision to
86
+ * retire this option in favour of asymmetric signing against a published
87
+ * JWKS, at which point it is removed rather than deprecated.
57
88
  */
58
89
  jwtSecret?: string;
59
90
  /**
@@ -125,7 +156,7 @@ export declare function OxyServicesUtilityMixin<T extends typeof OxyServicesBase
125
156
  * additionally checked for `aud`, `iss`, and `type` claims to prevent
126
157
  * cross-token-type confusion attacks.
127
158
  * - The backend's own `authMiddleware` uses `jwt.verify()` because it has
128
- * direct access to `SERVICE_TOKEN_SECRET` / `ACCESS_TOKEN_SECRET`.
159
+ * direct access to `ACCESS_TOKEN_SECRET`.
129
160
  *
130
161
  * **Why session-less user tokens are refused rather than trusted:**
131
162
  * every user access token the Oxy API issues carries a `sessionId` (see
@@ -157,7 +188,7 @@ export declare function OxyServicesUtilityMixin<T extends typeof OxyServicesBase
157
188
  * const oxy = new OxyServices({ baseURL: 'https://api.oxy.so' });
158
189
  *
159
190
  * // Protect all routes under /protected
160
- * app.use('/protected', oxy.auth({ jwtSecret: process.env.SERVICE_TOKEN_SECRET }));
191
+ * app.use('/protected', oxy.auth({ jwtSecret: process.env.ACCESS_TOKEN_SECRET }));
161
192
  *
162
193
  * // Access user in route handler
163
194
  * app.get('/protected/me', (req, res) => {
@@ -171,7 +202,7 @@ export declare function OxyServicesUtilityMixin<T extends typeof OxyServicesBase
171
202
  * app.use('/public', oxy.auth({ optional: true }));
172
203
  *
173
204
  * // Require a specific scope on a service-token-protected route
174
- * app.use('/internal/files', oxy.serviceAuth({ jwtSecret: process.env.SERVICE_TOKEN_SECRET }), oxy.requireScope('files:write'));
205
+ * app.use('/internal/files', oxy.serviceAuth({ jwtSecret: process.env.ACCESS_TOKEN_SECRET }), oxy.requireScope('files:write'));
175
206
  * ```
176
207
  *
177
208
  * @param options Optional configuration
@@ -213,7 +244,7 @@ export declare function OxyServicesUtilityMixin<T extends typeof OxyServicesBase
213
244
  * @example
214
245
  * ```typescript
215
246
  * // Protect internal endpoints
216
- * app.use('/internal', oxy.serviceAuth({ jwtSecret: process.env.SERVICE_TOKEN_SECRET }));
247
+ * app.use('/internal', oxy.serviceAuth({ jwtSecret: process.env.ACCESS_TOKEN_SECRET }));
217
248
  *
218
249
  * app.post('/internal/trigger', (req, res) => {
219
250
  * console.log('Service app:', req.serviceApp);
@@ -242,7 +273,7 @@ export declare function OxyServicesUtilityMixin<T extends typeof OxyServicesBase
242
273
  * ```typescript
243
274
  * app.use(
244
275
  * '/internal/files',
245
- * oxy.serviceAuth({ jwtSecret: process.env.SERVICE_TOKEN_SECRET }),
276
+ * oxy.serviceAuth({ jwtSecret: process.env.ACCESS_TOKEN_SECRET }),
246
277
  * oxy.requireScope('files:write'),
247
278
  * );
248
279
  * ```
@@ -32,6 +32,7 @@ import { OxyServicesChainsMixin } from './OxyServices.chains';
32
32
  import { OxyServicesNodesMixin } from './OxyServices.nodes';
33
33
  import { OxyServicesLinksMixin } from './OxyServices.links';
34
34
  import { OxyServicesFollowGraphMixin } from './OxyServices.followGraph';
35
+ import { OxyServicesInferenceMixin } from './OxyServices.inference';
35
36
  import { OxyServicesDeviceBootMixin } from './OxyServices.deviceBoot';
36
37
  import { OxyServicesDeviceTransferMixin } from './OxyServices.deviceTransfer';
37
38
  /**
@@ -43,7 +44,7 @@ import { OxyServicesDeviceTransferMixin } from './OxyServices.deviceTransfer';
43
44
  * If you add a new mixin to `MIXIN_PIPELINE`, add it here too so its methods
44
45
  * are visible without a cast.
45
46
  */
46
- type AllMixinInstances = InstanceType<ReturnType<typeof OxyServicesAuthMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesUserMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesIdentityMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesIdentityBackupMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesPrivacyMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesLanguageMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesPaymentMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesReputationMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesAssetsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesAccountsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesConnectedAppsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesStoreMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesLocationMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesAnalyticsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesDevicesMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesSecurityMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesFeaturesMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesTopicsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesContactsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesNotificationsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesAppDataMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesCivicMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesChainsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesNodesMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesLinksMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesFollowGraphMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesDeviceBootMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesDeviceTransferMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesUtilityMixin<typeof OxyServicesBase>>>;
47
+ type AllMixinInstances = InstanceType<ReturnType<typeof OxyServicesAuthMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesUserMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesIdentityMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesIdentityBackupMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesPrivacyMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesLanguageMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesPaymentMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesReputationMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesAssetsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesAccountsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesConnectedAppsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesStoreMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesLocationMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesAnalyticsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesDevicesMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesSecurityMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesFeaturesMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesTopicsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesContactsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesNotificationsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesAppDataMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesCivicMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesChainsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesNodesMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesLinksMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesFollowGraphMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesInferenceMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesDeviceBootMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesDeviceTransferMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesUtilityMixin<typeof OxyServicesBase>>>;
47
48
  /**
48
49
  * Constructor type for the fully composed mixin pipeline. Each mixin returns
49
50
  * a new constructor that augments its input; reducing across the pipeline
@@ -13,6 +13,17 @@ export interface ClientSession {
13
13
  * account-chooser ordering, not for any token-refresh mechanism.
14
14
  */
15
15
  authuser?: number;
16
+ /**
17
+ * The HUMAN operating this account, when it is a delegated session — the
18
+ * audit actor behind "The Oxy Collective". Absent when the session belongs to
19
+ * the account itself.
20
+ *
21
+ * The flat wire shape has carried it since the multi-account model shipped and
22
+ * nothing read it, so an operated org rendered exactly like a directly
23
+ * signed-in one. `SessionClient.getActiveContext()` is the richer answer
24
+ * (ADR 0002); this is the same fact on the compatibility lane.
25
+ */
26
+ operatedByUserId?: string;
16
27
  }
17
28
  export interface StorageKeys {
18
29
  sessions: string;