@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.
- package/dist/cjs/.tsbuildinfo +1 -1
- package/dist/cjs/HttpService.js +47 -8
- package/dist/cjs/boot/sessionColdBoot.js +107 -8
- package/dist/cjs/i18n/locales/en-US.json +26 -4
- package/dist/cjs/i18n/locales/es-ES.json +26 -4
- package/dist/cjs/i18n/locales/locales/en-US.json +26 -4
- package/dist/cjs/i18n/locales/locales/es-ES.json +26 -4
- package/dist/cjs/index.js +57 -16
- package/dist/cjs/inference/OxyInferenceClient.js +330 -0
- package/dist/cjs/mixins/OxyServices.accounts.js +5 -72
- package/dist/cjs/mixins/OxyServices.auth.js +27 -3
- package/dist/cjs/mixins/OxyServices.inference.js +59 -0
- package/dist/cjs/mixins/OxyServices.utility.js +18 -6
- package/dist/cjs/mixins/index.js +6 -0
- package/dist/cjs/server/auth.js +76 -0
- package/dist/cjs/server/index.js +5 -1
- package/dist/cjs/session/SessionClient.js +361 -1
- package/dist/cjs/session/accountDialogController.js +121 -147
- package/dist/cjs/session/accountSwitchTargets.js +75 -0
- package/dist/cjs/session/deviceDirectory.js +143 -0
- package/dist/cjs/session/deviceSwitcherRows.js +76 -0
- package/dist/cjs/session/projectSessionState.js +8 -1
- package/dist/cjs/session/sharedDeviceCredential.js +247 -0
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/HttpService.js +47 -8
- package/dist/esm/boot/sessionColdBoot.js +107 -8
- package/dist/esm/i18n/locales/en-US.json +26 -4
- package/dist/esm/i18n/locales/es-ES.json +26 -4
- package/dist/esm/i18n/locales/locales/en-US.json +26 -4
- package/dist/esm/i18n/locales/locales/es-ES.json +26 -4
- package/dist/esm/index.js +36 -10
- package/dist/esm/inference/OxyInferenceClient.js +325 -0
- package/dist/esm/mixins/OxyServices.accounts.js +5 -72
- package/dist/esm/mixins/OxyServices.auth.js +27 -3
- package/dist/esm/mixins/OxyServices.inference.js +56 -0
- package/dist/esm/mixins/OxyServices.utility.js +18 -6
- package/dist/esm/mixins/index.js +6 -0
- package/dist/esm/server/auth.js +72 -0
- package/dist/esm/server/index.js +1 -1
- package/dist/esm/session/SessionClient.js +362 -2
- package/dist/esm/session/accountDialogController.js +121 -147
- package/dist/esm/session/accountSwitchTargets.js +71 -0
- package/dist/esm/session/deviceDirectory.js +135 -0
- package/dist/esm/session/deviceSwitcherRows.js +72 -0
- package/dist/esm/session/projectSessionState.js +8 -2
- package/dist/esm/session/sharedDeviceCredential.js +239 -0
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/HttpService.d.ts +39 -1
- package/dist/types/boot/sessionColdBoot.d.ts +24 -4
- package/dist/types/index.d.ts +11 -4
- package/dist/types/inference/OxyInferenceClient.d.ts +324 -0
- package/dist/types/mixins/OxyServices.accounts.d.ts +73 -95
- package/dist/types/mixins/OxyServices.auth.d.ts +75 -3
- package/dist/types/mixins/OxyServices.inference.d.ts +95 -0
- package/dist/types/mixins/OxyServices.utility.d.ts +44 -13
- package/dist/types/mixins/index.d.ts +2 -1
- package/dist/types/models/session.d.ts +11 -0
- package/dist/types/server/auth.d.ts +80 -0
- package/dist/types/server/index.d.ts +2 -2
- package/dist/types/session/SessionClient.d.ts +202 -1
- package/dist/types/session/accountDialogController.d.ts +76 -64
- package/dist/types/session/accountSwitchTargets.d.ts +64 -0
- package/dist/types/session/deviceDirectory.d.ts +182 -0
- package/dist/types/session/deviceSwitcherRows.d.ts +92 -0
- package/dist/types/session/projectSessionState.d.ts +29 -0
- package/dist/types/session/sharedDeviceCredential.d.ts +202 -0
- package/package.json +3 -3
- package/src/HttpService.ts +50 -10
- package/src/__tests__/httpServiceUnwrapEnvelope.test.ts +115 -0
- package/src/boot/__tests__/sessionColdBoot.sharedDevice.test.ts +325 -0
- package/src/boot/sessionColdBoot.ts +133 -9
- package/src/i18n/locales/en-US.json +26 -4
- package/src/i18n/locales/es-ES.json +26 -4
- package/src/index.ts +94 -25
- package/src/inference/OxyInferenceClient.ts +590 -0
- package/src/inference/__tests__/OxyInferenceClient.test.ts +383 -0
- package/src/mixins/OxyServices.accounts.ts +75 -176
- package/src/mixins/OxyServices.auth.ts +67 -5
- package/src/mixins/OxyServices.inference.ts +57 -0
- package/src/mixins/OxyServices.utility.ts +58 -14
- package/src/mixins/__tests__/accounts.test.ts +57 -102
- package/src/mixins/__tests__/inferenceFactory.test.ts +58 -0
- package/src/mixins/__tests__/preSessionSkipAuth.test.ts +54 -1
- package/src/mixins/__tests__/serviceAuth.test.ts +2 -0
- package/src/mixins/index.ts +8 -0
- package/src/models/session.ts +11 -0
- package/src/server/__tests__/serviceTokenAttribution.test.ts +396 -0
- package/src/server/auth.ts +118 -0
- package/src/server/index.ts +6 -0
- package/src/session/SessionClient.ts +386 -1
- package/src/session/__tests__/SessionClient.directory.test.ts +688 -0
- package/src/session/__tests__/accountDialogController.test.ts +411 -278
- package/src/session/__tests__/accountDialogShape.test.ts +118 -0
- package/src/session/__tests__/accountSwitchTargets.test.ts +132 -0
- package/src/session/__tests__/deviceDirectory.test.ts +422 -0
- package/src/session/__tests__/deviceSwitcherRows.test.ts +223 -0
- package/src/session/__tests__/projectSessionState.test.ts +17 -0
- package/src/session/__tests__/sharedDeviceCredential.test.ts +300 -0
- package/src/session/accountDialogController.ts +141 -179
- package/src/session/accountSwitchTargets.ts +87 -0
- package/src/session/deviceDirectory.ts +269 -0
- package/src/session/deviceSwitcherRows.ts +145 -0
- package/src/session/projectSessionState.ts +9 -3
- package/src/session/sharedDeviceCredential.ts +349 -0
- package/dist/cjs/session/accountProjection.js +0 -213
- package/dist/esm/session/accountProjection.js +0 -207
- package/dist/types/session/accountProjection.d.ts +0 -198
- package/src/session/__tests__/accountProjection.test.ts +0 -447
- 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
|
-
|
|
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
|
});
|
package/src/mixins/index.ts
CHANGED
|
@@ -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,
|
package/src/models/session.ts
CHANGED
|
@@ -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 {
|