@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
@@ -17,9 +17,6 @@ import type { User } from '../../models/interfaces';
17
17
  import type {
18
18
  AccountNode,
19
19
  AccountMember,
20
- AccountCredential,
21
- AccountCredentialWithSecret,
22
- RotateAccountCredentialResult,
23
20
  Application,
24
21
  ApplicationCredential,
25
22
  ApplicationCredentialWithSecret,
@@ -62,20 +59,6 @@ const accountNodeFixture: AccountNode = {
62
59
  childCount: 2,
63
60
  };
64
61
 
65
- const credentialFixture: AccountCredential = {
66
- _id: 'cred1',
67
- accountId: 'acc1',
68
- name: 'bot-prod',
69
- publicKey: 'oxy_dk_abc',
70
- type: 'service',
71
- environment: 'production',
72
- scopes: ['user:read'],
73
- status: 'active',
74
- createdByUserId: 'u1',
75
- createdAt: '2026-06-29T00:00:00.000Z',
76
- updatedAt: '2026-06-29T00:00:00.000Z',
77
- };
78
-
79
62
  const appFixture: Application = {
80
63
  _id: 'app1',
81
64
  name: 'Mention',
@@ -476,91 +459,6 @@ describe('OxyServices.accounts', () => {
476
459
  });
477
460
  });
478
461
 
479
- describe('listAccountCredentials', () => {
480
- it('unwraps the `credentials` array and caches the read', async () => {
481
- makeRequestSpy.mockResolvedValue({ credentials: [credentialFixture] });
482
-
483
- const result = await oxy.listAccountCredentials('acc1');
484
-
485
- expect(result).toEqual([credentialFixture]);
486
- expect(makeRequestSpy).toHaveBeenCalledWith(
487
- 'GET',
488
- '/accounts/acc1/credentials',
489
- undefined,
490
- expect.objectContaining({ cache: true }),
491
- );
492
- });
493
-
494
- it('returns an empty array when `credentials` is absent', async () => {
495
- makeRequestSpy.mockResolvedValue({});
496
- expect(await oxy.listAccountCredentials('acc1')).toEqual([]);
497
- });
498
- });
499
-
500
- describe('createAccountCredential', () => {
501
- it('posts the config, returns the secret result, and busts the credentials cache', async () => {
502
- const created: AccountCredentialWithSecret = {
503
- credential: credentialFixture,
504
- secret: 'sk_once',
505
- };
506
- makeRequestSpy.mockResolvedValue(created);
507
-
508
- const result = await oxy.createAccountCredential('acc1', {
509
- name: 'bot-prod',
510
- environment: 'production',
511
- });
512
-
513
- expect(result).toEqual(created);
514
- expect(makeRequestSpy).toHaveBeenCalledWith(
515
- 'POST',
516
- '/accounts/acc1/credentials',
517
- { name: 'bot-prod', environment: 'production' },
518
- expect.objectContaining({ cache: false }),
519
- );
520
- expect(clearEntrySpy).toHaveBeenCalledWith('GET:/accounts/acc1/credentials');
521
- });
522
- });
523
-
524
- describe('rotateAccountCredential', () => {
525
- it('posts to /rotate, encodes ids, returns the audit result, and busts credentials', async () => {
526
- const rotated: RotateAccountCredentialResult = {
527
- credential: { ...credentialFixture, _id: 'cred2' },
528
- secret: 'sk_new',
529
- rotatedFrom: 'cred1',
530
- graceExpiresAt: '2026-07-06T00:00:00.000Z',
531
- };
532
- makeRequestSpy.mockResolvedValue(rotated);
533
-
534
- const result = await oxy.rotateAccountCredential('acc1', 'cred 1');
535
-
536
- expect(result).toEqual(rotated);
537
- expect(makeRequestSpy).toHaveBeenCalledWith(
538
- 'POST',
539
- '/accounts/acc1/credentials/cred%201/rotate',
540
- undefined,
541
- expect.objectContaining({ cache: false }),
542
- );
543
- expect(clearEntrySpy).toHaveBeenCalledWith('GET:/accounts/acc1/credentials');
544
- });
545
- });
546
-
547
- describe('revokeAccountCredential', () => {
548
- it('deletes the credential and busts the credentials cache', async () => {
549
- makeRequestSpy.mockResolvedValue({ success: true });
550
-
551
- const result = await oxy.revokeAccountCredential('acc1', 'cred1');
552
-
553
- expect(result).toEqual({ success: true });
554
- expect(makeRequestSpy).toHaveBeenCalledWith(
555
- 'DELETE',
556
- '/accounts/acc1/credentials/cred1',
557
- undefined,
558
- expect.objectContaining({ cache: false }),
559
- );
560
- expect(clearEntrySpy).toHaveBeenCalledWith('GET:/accounts/acc1/credentials');
561
- });
562
- });
563
-
564
462
  describe('createApp', () => {
565
463
  it('posts the payload, unwraps `application`, and busts every app list', async () => {
566
464
  makeRequestSpy.mockResolvedValue({ application: appFixture });
@@ -678,6 +576,39 @@ describe('OxyServices.accounts', () => {
678
576
  );
679
577
  expect(clearEntrySpy).toHaveBeenCalledWith('GET:/applications/app1/credentials');
680
578
  });
579
+
580
+ it("forwards a machine credential's expiresInSeconds and its one-time token", async () => {
581
+ // A `machine` credential's material arrives in `token`, never `secret` —
582
+ // a surface rendering "the secret" must not silently print an API key.
583
+ const created: ApplicationCredentialWithSecret = {
584
+ credential: { ...appCredentialFixture, type: 'machine', tokenPrefix: 'oxy_sk_0123456789abcdef' },
585
+ secret: null,
586
+ token: 'oxy_sk_0123456789abcdef_' + 'a'.repeat(64),
587
+ };
588
+ makeRequestSpy.mockResolvedValue(created);
589
+
590
+ const result = await oxy.createAppCredential('app1', {
591
+ name: 'ci-runner',
592
+ type: 'machine',
593
+ environment: 'production',
594
+ scopes: ['inference:invoke'],
595
+ expiresInSeconds: 86_400,
596
+ });
597
+
598
+ expect(result.token).toBe(created.token);
599
+ expect(makeRequestSpy).toHaveBeenCalledWith(
600
+ 'POST',
601
+ '/applications/app1/credentials',
602
+ {
603
+ name: 'ci-runner',
604
+ type: 'machine',
605
+ environment: 'production',
606
+ scopes: ['inference:invoke'],
607
+ expiresInSeconds: 86_400,
608
+ },
609
+ expect.objectContaining({ cache: false }),
610
+ );
611
+ });
681
612
  });
682
613
 
683
614
  describe('rotateAppCredential', () => {
@@ -701,6 +632,30 @@ describe('OxyServices.accounts', () => {
701
632
  );
702
633
  expect(clearEntrySpy).toHaveBeenCalledWith('GET:/applications/app1/credentials');
703
634
  });
635
+
636
+ it('forwards an opt-in rotation grace window', async () => {
637
+ // Omitting it (the case above) sends `undefined`, and the server then
638
+ // revokes the superseded machine token immediately. Sending it must reach
639
+ // the body, or a caller asking for zero-downtime rotation would silently
640
+ // get the instant-revoke behaviour instead.
641
+ const rotated: RotateApplicationCredentialResult = {
642
+ credential: { ...appCredentialFixture, _id: 'appcred2', type: 'machine' },
643
+ secret: null,
644
+ token: 'oxy_sk_fedcba9876543210_' + 'b'.repeat(64),
645
+ rotatedFrom: 'appcred1',
646
+ graceExpiresAt: '2026-08-17T00:00:00.000Z',
647
+ };
648
+ makeRequestSpy.mockResolvedValue(rotated);
649
+
650
+ await oxy.rotateAppCredential('app1', 'appcred1', { graceSeconds: 3600 });
651
+
652
+ expect(makeRequestSpy).toHaveBeenCalledWith(
653
+ 'POST',
654
+ '/applications/app1/credentials/appcred1/rotate',
655
+ { graceSeconds: 3600 },
656
+ expect.objectContaining({ cache: false }),
657
+ );
658
+ });
704
659
  });
705
660
 
706
661
  describe('revokeAppCredential', () => {
@@ -0,0 +1,58 @@
1
+ /**
2
+ * `oxyServices.inference()` — the binding, which is the only thing this mixin
3
+ * does.
4
+ *
5
+ * Two properties matter and neither is visible from the client's own suite: the
6
+ * client must inherit THIS instance's base URL, and it must read the bearer
7
+ * LATE, so a token planted after construction is the one that gets sent. A
8
+ * factory that captured `getAccessToken()` eagerly would pass every test that
9
+ * signs in first and fail on the cold-boot ordering every app actually has.
10
+ *
11
+ * Driven through the real global `fetch`, spied — the factory takes no
12
+ * transport, so a test that built its own client would be asserting against a
13
+ * re-implementation of the thing under test rather than against it.
14
+ */
15
+
16
+ import { OxyServices } from '../../OxyServices';
17
+ import { OxyInferenceClient } from '../../inference/OxyInferenceClient';
18
+
19
+ describe('OxyServices.inference()', () => {
20
+ let fetchSpy: jest.SpyInstance;
21
+
22
+ beforeEach(() => {
23
+ fetchSpy = jest.spyOn(globalThis, 'fetch').mockResolvedValue(
24
+ new Response(JSON.stringify({ data: [], count: 0 }), {
25
+ status: 200,
26
+ headers: { 'Content-Type': 'application/json' },
27
+ }),
28
+ );
29
+ });
30
+
31
+ afterEach(() => {
32
+ fetchSpy.mockRestore();
33
+ });
34
+
35
+ it('returns one memoized client, so a consumer can hold it', () => {
36
+ const oxy = new OxyServices({ baseURL: 'http://test.invalid' });
37
+
38
+ const first = oxy.inference();
39
+ expect(first).toBeInstanceOf(OxyInferenceClient);
40
+ expect(oxy.inference()).toBe(first);
41
+ });
42
+
43
+ it('binds this instance base URL and reads the bearer at request time', async () => {
44
+ const oxy = new OxyServices({ baseURL: 'http://test.invalid' });
45
+ const client = oxy.inference();
46
+
47
+ // The token is planted AFTER the client exists — the cold-boot order.
48
+ oxy.setTokens('planted-later');
49
+ await client.listModels();
50
+
51
+ expect(fetchSpy).toHaveBeenCalledTimes(1);
52
+ const [url, init] = fetchSpy.mock.calls[0] as [string, RequestInit];
53
+ expect(String(url)).toBe('http://test.invalid/v1/models');
54
+ expect((init.headers as Record<string, string>)['Authorization']).toBe(
55
+ 'Bearer planted-later',
56
+ );
57
+ });
58
+ });
@@ -158,7 +158,40 @@ describe('pre-session public endpoints use skipAuth', () => {
158
158
  });
159
159
  });
160
160
 
161
- it('exchangeOAuthCode rejects a response without deviceSecret', async () => {
161
+ // A third-party grant is meant to be ISOLATED from the browser's shared
162
+ // DeviceSession, so the token endpoint must be free to return no device
163
+ // credential. Core used to require the pair, which made that omission
164
+ // unshippable: every third-party sign-in through the SDK would have collapsed
165
+ // into a silent `exchange-failed` (issue #954).
166
+ it('exchangeOAuthCode accepts a device-less grant and still plants the token', async () => {
167
+ makeRequest.mockResolvedValueOnce({
168
+ access_token: 'tok',
169
+ token_type: 'Bearer',
170
+ expires_in: 900,
171
+ session_id: 's1',
172
+ user: { id: 'u1', username: 'alice' },
173
+ });
174
+
175
+ const result = await oxy.exchangeOAuthCode({
176
+ code: 'code-1',
177
+ clientId: 'oxy_dk_test',
178
+ redirectUri: 'https://app.example/callback',
179
+ codeVerifier: 'verifier',
180
+ });
181
+
182
+ expect(result).toMatchObject({
183
+ sessionId: 's1',
184
+ accessToken: 'tok',
185
+ user: { id: 'u1', username: 'alice' },
186
+ });
187
+ // Absent, not `undefined`-valued: a device-less grant carries no device keys
188
+ // at all, so nothing downstream can read one and persist an empty credential.
189
+ expect(result).not.toHaveProperty('deviceId');
190
+ expect(result).not.toHaveProperty('deviceSecret');
191
+ expect(oxy.getAccessToken()).toBe('tok');
192
+ });
193
+
194
+ it('exchangeOAuthCode accepts a deviceId with no deviceSecret', async () => {
162
195
  makeRequest.mockResolvedValueOnce({
163
196
  access_token: 'tok',
164
197
  token_type: 'Bearer',
@@ -167,6 +200,26 @@ describe('pre-session public endpoints use skipAuth', () => {
167
200
  deviceId: 'd1',
168
201
  user: { id: 'u1' },
169
202
  });
203
+
204
+ const result = await oxy.exchangeOAuthCode({
205
+ code: 'code-1',
206
+ clientId: 'oxy_dk_test',
207
+ redirectUri: 'https://app.example/callback',
208
+ codeVerifier: 'verifier',
209
+ });
210
+
211
+ expect(result).toMatchObject({ sessionId: 's1', deviceId: 'd1' });
212
+ expect(result).not.toHaveProperty('deviceSecret');
213
+ });
214
+
215
+ // The device pair left the guard; what identifies the session did NOT. Without
216
+ // these two the whole guard could be deleted and every test above would stay
217
+ // green — `sessionId` would silently become `undefined` on the returned session.
218
+ it.each([
219
+ ['session_id', { access_token: 'tok', expires_in: 900, user: { id: 'u1' } }],
220
+ ['user', { access_token: 'tok', expires_in: 900, session_id: 's1' }],
221
+ ])('exchangeOAuthCode still rejects a response missing %s', async (_field, response) => {
222
+ makeRequest.mockResolvedValueOnce(response);
170
223
  await expect(
171
224
  oxy.exchangeOAuthCode({
172
225
  code: 'code-1',
@@ -50,6 +50,7 @@ const signServiceToken = (claims: ServiceTokenClaims, secret: string): string =>
50
50
  aud: 'oxy-api',
51
51
  iss: 'oxy-auth',
52
52
  credentialId: 'cred-1',
53
+ ownerAccountId: 'owner-account-1',
53
54
  environment: 'production',
54
55
  ...claims,
55
56
  };
@@ -184,6 +185,7 @@ describe('C3: service-token acting-as enforcement', () => {
184
185
  appId: 'app-1',
185
186
  appName: 'trusted-service',
186
187
  credentialId: 'cred-1',
188
+ ownerAccountId: 'owner-account-1',
187
189
  scopes: ['user:read'],
188
190
  environment: 'production',
189
191
  });
@@ -33,6 +33,7 @@ import { OxyServicesChainsMixin } from './OxyServices.chains';
33
33
  import { OxyServicesNodesMixin } from './OxyServices.nodes';
34
34
  import { OxyServicesLinksMixin } from './OxyServices.links';
35
35
  import { OxyServicesFollowGraphMixin } from './OxyServices.followGraph';
36
+ import { OxyServicesInferenceMixin } from './OxyServices.inference';
36
37
  import { OxyServicesDeviceBootMixin } from './OxyServices.deviceBoot';
37
38
  import { OxyServicesDeviceTransferMixin } from './OxyServices.deviceTransfer';
38
39
 
@@ -72,6 +73,7 @@ type AllMixinInstances =
72
73
  & InstanceType<ReturnType<typeof OxyServicesNodesMixin<typeof OxyServicesBase>>>
73
74
  & InstanceType<ReturnType<typeof OxyServicesLinksMixin<typeof OxyServicesBase>>>
74
75
  & InstanceType<ReturnType<typeof OxyServicesFollowGraphMixin<typeof OxyServicesBase>>>
76
+ & InstanceType<ReturnType<typeof OxyServicesInferenceMixin<typeof OxyServicesBase>>>
75
77
  & InstanceType<ReturnType<typeof OxyServicesDeviceBootMixin<typeof OxyServicesBase>>>
76
78
  & InstanceType<ReturnType<typeof OxyServicesDeviceTransferMixin<typeof OxyServicesBase>>>
77
79
  & InstanceType<ReturnType<typeof OxyServicesUtilityMixin<typeof OxyServicesBase>>>;
@@ -156,6 +158,12 @@ const MIXIN_PIPELINE: MixinFunction[] = [
156
158
  // shared across applications, with per-application context on top.
157
159
  OxyServicesFollowGraphMixin,
158
160
 
161
+ // The inference model catalogue (#972). Reads only, and deliberately no
162
+ // request/stream/receipt methods — the public inference edge those would
163
+ // call is workstream 4 and does not exist yet. See
164
+ // `docs/inference/README.md` for what is and is not built.
165
+ OxyServicesInferenceMixin,
166
+
159
167
  // Device-first token mint: the client half of the zero-cookie transport
160
168
  // (`mintFromDeviceSecret` → `POST /session/device/token`).
161
169
  OxyServicesDeviceBootMixin,
@@ -14,6 +14,17 @@ export interface ClientSession {
14
14
  * account-chooser ordering, not for any token-refresh mechanism.
15
15
  */
16
16
  authuser?: number;
17
+ /**
18
+ * The HUMAN operating this account, when it is a delegated session — the
19
+ * audit actor behind "The Oxy Collective". Absent when the session belongs to
20
+ * the account itself.
21
+ *
22
+ * The flat wire shape has carried it since the multi-account model shipped and
23
+ * nothing read it, so an operated org rendered exactly like a directly
24
+ * signed-in one. `SessionClient.getActiveContext()` is the richer answer
25
+ * (ADR 0002); this is the same fact on the compatibility lane.
26
+ */
27
+ operatedByUserId?: string;
17
28
  }
18
29
 
19
30
  export interface StorageKeys {