@oxyhq/core 5.5.0 → 7.0.0
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 +7 -4
- package/dist/cjs/OxyServices.base.js +7 -102
- package/dist/cjs/boot/coldBootV2.js +350 -0
- package/dist/cjs/boot/deviceBootReturn.js +152 -0
- package/dist/cjs/crypto/keyManager.js +95 -0
- package/dist/cjs/i18n/locales/en-US.json +13 -1
- package/dist/cjs/i18n/locales/es-ES.json +13 -1
- package/dist/cjs/i18n/locales/locales/en-US.json +13 -1
- package/dist/cjs/i18n/locales/locales/es-ES.json +13 -1
- package/dist/cjs/index.js +54 -45
- package/dist/cjs/mixins/OxyServices.accounts.js +13 -26
- package/dist/cjs/mixins/OxyServices.auth.js +66 -201
- package/dist/cjs/mixins/OxyServices.deviceBoot.js +119 -0
- package/dist/cjs/mixins/index.js +8 -17
- package/dist/cjs/server/index.js +2 -2
- package/dist/cjs/session/SessionClient.js +40 -1
- package/dist/cjs/session/authStateStore.js +284 -0
- package/dist/cjs/session/refresh.js +264 -0
- package/dist/cjs/shared/utils/debugUtils.js +3 -3
- package/dist/cjs/utils/accountUtils.js +1 -55
- package/dist/cjs/utils/authWebUrl.js +10 -39
- package/dist/cjs/utils/coldBoot.js +10 -8
- package/dist/cjs/utils/platform.js +19 -0
- package/dist/cjs/utils/registrableApex.js +49 -0
- package/dist/cjs/utils/ssoBounce.js +15 -362
- package/dist/cjs/utils/validationUtils.js +57 -0
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/HttpService.js +7 -4
- package/dist/esm/OxyServices.base.js +7 -102
- package/dist/esm/boot/coldBootV2.js +344 -0
- package/dist/esm/boot/deviceBootReturn.js +146 -0
- package/dist/esm/crypto/keyManager.js +95 -0
- package/dist/esm/i18n/locales/en-US.json +13 -1
- package/dist/esm/i18n/locales/es-ES.json +13 -1
- package/dist/esm/i18n/locales/locales/en-US.json +13 -1
- package/dist/esm/i18n/locales/locales/es-ES.json +13 -1
- package/dist/esm/index.js +34 -17
- package/dist/esm/mixins/OxyServices.accounts.js +13 -26
- package/dist/esm/mixins/OxyServices.auth.js +66 -201
- package/dist/esm/mixins/OxyServices.deviceBoot.js +116 -0
- package/dist/esm/mixins/index.js +8 -17
- package/dist/esm/server/index.js +1 -1
- package/dist/esm/session/SessionClient.js +40 -1
- package/dist/esm/session/authStateStore.js +278 -0
- package/dist/esm/session/refresh.js +257 -0
- package/dist/esm/shared/utils/debugUtils.js +3 -3
- package/dist/esm/utils/accountUtils.js +0 -53
- package/dist/esm/utils/authWebUrl.js +9 -37
- package/dist/esm/utils/coldBoot.js +10 -8
- package/dist/esm/utils/platform.js +18 -0
- package/dist/esm/utils/registrableApex.js +46 -0
- package/dist/esm/utils/ssoBounce.js +14 -345
- package/dist/esm/utils/validationUtils.js +56 -0
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/HttpService.d.ts +14 -1
- package/dist/types/OxyServices.base.d.ts +0 -52
- package/dist/types/OxyServices.d.ts +0 -25
- package/dist/types/boot/coldBootV2.d.ts +76 -0
- package/dist/types/boot/deviceBootReturn.d.ts +83 -0
- package/dist/types/crypto/keyManager.d.ts +21 -0
- package/dist/types/index.d.ts +14 -21
- package/dist/types/mixins/OxyServices.accounts.d.ts +24 -29
- package/dist/types/mixins/OxyServices.analytics.d.ts +0 -2
- package/dist/types/mixins/OxyServices.appData.d.ts +0 -2
- package/dist/types/mixins/OxyServices.assets.d.ts +0 -2
- package/dist/types/mixins/OxyServices.auth.d.ts +35 -77
- package/dist/types/mixins/OxyServices.civic.d.ts +0 -2
- package/dist/types/mixins/OxyServices.connectedApps.d.ts +0 -2
- package/dist/types/mixins/OxyServices.contacts.d.ts +0 -2
- package/dist/types/mixins/OxyServices.deviceBoot.d.ts +110 -0
- package/dist/types/mixins/OxyServices.devices.d.ts +0 -2
- package/dist/types/mixins/OxyServices.features.d.ts +0 -2
- package/dist/types/mixins/OxyServices.identity.d.ts +0 -2
- package/dist/types/mixins/OxyServices.language.d.ts +0 -2
- package/dist/types/mixins/OxyServices.links.d.ts +0 -2
- package/dist/types/mixins/OxyServices.location.d.ts +0 -2
- package/dist/types/mixins/OxyServices.nodes.d.ts +0 -2
- package/dist/types/mixins/OxyServices.payment.d.ts +0 -2
- package/dist/types/mixins/OxyServices.privacy.d.ts +0 -2
- package/dist/types/mixins/OxyServices.reputation.d.ts +0 -2
- package/dist/types/mixins/OxyServices.security.d.ts +0 -2
- package/dist/types/mixins/OxyServices.topics.d.ts +0 -2
- package/dist/types/mixins/OxyServices.user.d.ts +0 -2
- package/dist/types/mixins/OxyServices.utility.d.ts +0 -2
- package/dist/types/mixins/index.d.ts +5 -9
- package/dist/types/models/interfaces.d.ts +0 -67
- package/dist/types/models/session.d.ts +4 -5
- package/dist/types/server/index.d.ts +1 -1
- package/dist/types/session/SessionClient.d.ts +26 -1
- package/dist/types/session/authStateStore.d.ts +119 -0
- package/dist/types/session/refresh.d.ts +93 -0
- package/dist/types/shared/utils/debugUtils.d.ts +3 -3
- package/dist/types/utils/accountUtils.d.ts +2 -17
- package/dist/types/utils/authWebUrl.d.ts +9 -35
- package/dist/types/utils/coldBoot.d.ts +17 -14
- package/dist/types/utils/platform.d.ts +14 -0
- package/dist/types/utils/registrableApex.d.ts +31 -0
- package/dist/types/utils/ssoBounce.d.ts +14 -280
- package/dist/types/utils/validationUtils.d.ts +15 -0
- package/package.json +2 -2
- package/src/HttpService.ts +20 -4
- package/src/OxyServices.base.ts +7 -112
- package/src/OxyServices.ts +0 -38
- package/src/boot/__tests__/coldBootV2.test.ts +317 -0
- package/src/boot/__tests__/deviceBootReturn.test.ts +158 -0
- package/src/boot/coldBootV2.ts +426 -0
- package/src/boot/deviceBootReturn.ts +195 -0
- package/src/crypto/__tests__/sharedDeviceToken.test.ts +24 -0
- package/src/crypto/keyManager.ts +101 -0
- package/src/i18n/locales/en-US.json +13 -1
- package/src/i18n/locales/es-ES.json +13 -1
- package/src/index.ts +75 -65
- package/src/mixins/OxyServices.accounts.ts +27 -39
- package/src/mixins/OxyServices.auth.ts +78 -253
- package/src/mixins/OxyServices.deviceBoot.ts +146 -0
- package/src/mixins/__tests__/OxyServices.deviceBoot.test.ts +107 -0
- package/src/mixins/__tests__/accounts.test.ts +17 -44
- package/src/mixins/__tests__/passwordSignIn.test.ts +91 -0
- package/src/mixins/index.ts +10 -23
- package/src/models/interfaces.ts +0 -79
- package/src/models/session.ts +4 -5
- package/src/server/index.ts +1 -1
- package/src/session/SessionClient.ts +53 -2
- package/src/session/__tests__/SessionClient.additive.test.ts +92 -0
- package/src/session/__tests__/SessionClient.rest.test.ts +25 -0
- package/src/session/__tests__/SessionClient.state.test.ts +18 -5
- package/src/session/__tests__/authStateStore.test.ts +209 -0
- package/src/session/__tests__/refresh.test.ts +256 -0
- package/src/session/authStateStore.ts +335 -0
- package/src/session/refresh.ts +334 -0
- package/src/shared/utils/debugUtils.ts +3 -3
- package/src/utils/__tests__/authWebUrl.test.ts +5 -40
- package/src/utils/__tests__/registrableApex.test.ts +62 -0
- package/src/utils/__tests__/validationUtils.test.ts +30 -0
- package/src/utils/accountUtils.ts +2 -65
- package/src/utils/authWebUrl.ts +9 -39
- package/src/utils/coldBoot.ts +17 -14
- package/src/utils/platform.ts +21 -0
- package/src/utils/registrableApex.ts +45 -0
- package/src/utils/ssoBounce.ts +14 -393
- package/src/utils/validationUtils.ts +62 -0
- package/dist/cjs/AuthManager.js +0 -1110
- package/dist/cjs/AuthManagerTypes.js +0 -13
- package/dist/cjs/CrossDomainAuth.js +0 -206
- package/dist/cjs/mixins/OxyServices.fedcm.js +0 -823
- package/dist/cjs/mixins/OxyServices.redirect.js +0 -95
- package/dist/cjs/mixins/OxyServices.silent.js +0 -204
- package/dist/cjs/mixins/OxyServices.sso.js +0 -208
- package/dist/cjs/utils/fapiAutoDetect.js +0 -99
- package/dist/cjs/utils/ssoEstablish.js +0 -110
- package/dist/cjs/utils/ssoReturn.js +0 -275
- package/dist/esm/AuthManager.js +0 -1105
- package/dist/esm/AuthManagerTypes.js +0 -12
- package/dist/esm/CrossDomainAuth.js +0 -201
- package/dist/esm/mixins/OxyServices.fedcm.js +0 -821
- package/dist/esm/mixins/OxyServices.redirect.js +0 -92
- package/dist/esm/mixins/OxyServices.silent.js +0 -202
- package/dist/esm/mixins/OxyServices.sso.js +0 -204
- package/dist/esm/utils/fapiAutoDetect.js +0 -95
- package/dist/esm/utils/ssoEstablish.js +0 -107
- package/dist/esm/utils/ssoReturn.js +0 -271
- package/dist/types/AuthManager.d.ts +0 -380
- package/dist/types/AuthManagerTypes.d.ts +0 -81
- package/dist/types/CrossDomainAuth.d.ts +0 -164
- package/dist/types/mixins/OxyServices.fedcm.d.ts +0 -331
- package/dist/types/mixins/OxyServices.redirect.d.ts +0 -92
- package/dist/types/mixins/OxyServices.silent.d.ts +0 -132
- package/dist/types/mixins/OxyServices.sso.d.ts +0 -138
- package/dist/types/utils/fapiAutoDetect.d.ts +0 -56
- package/dist/types/utils/ssoEstablish.d.ts +0 -85
- package/dist/types/utils/ssoReturn.d.ts +0 -156
- package/src/AuthManager.ts +0 -1269
- package/src/AuthManagerTypes.ts +0 -86
- package/src/CrossDomainAuth.ts +0 -243
- package/src/__tests__/authManager.cookiePath.test.ts +0 -390
- package/src/__tests__/authManager.security.test.ts +0 -377
- package/src/__tests__/crossDomainAuth.test.ts +0 -116
- package/src/__tests__/establishDeviceRefreshSlot.test.ts +0 -221
- package/src/mixins/OxyServices.fedcm.ts +0 -1026
- package/src/mixins/OxyServices.redirect.ts +0 -122
- package/src/mixins/OxyServices.silent.ts +0 -272
- package/src/mixins/OxyServices.sso.ts +0 -261
- package/src/mixins/__tests__/constructorAuthWebUrl.test.ts +0 -85
- package/src/mixins/__tests__/fedcm.test.ts +0 -667
- package/src/mixins/__tests__/sessionBaseUrl.test.ts +0 -61
- package/src/mixins/__tests__/silent.test.ts +0 -102
- package/src/mixins/__tests__/sso.test.ts +0 -228
- package/src/utils/__tests__/consumeSsoReturn.test.ts +0 -816
- package/src/utils/__tests__/fapiAutoDetect.test.ts +0 -183
- package/src/utils/__tests__/ssoBounce.test.ts +0 -219
- package/src/utils/__tests__/ssoEstablish.test.ts +0 -204
- package/src/utils/__tests__/ssoReturn.test.ts +0 -276
- package/src/utils/fapiAutoDetect.ts +0 -91
- package/src/utils/ssoEstablish.ts +0 -174
- package/src/utils/ssoReturn.ts +0 -389
|
@@ -1,276 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `parseSsoReturnFragment` — SSO return fragment parsing.
|
|
3
|
-
*
|
|
4
|
-
* The central IdP returns the RP via a top-level redirect with the bounce
|
|
5
|
-
* result in the URL fragment. The parser must be pure, total (never throws),
|
|
6
|
-
* and report `kind` strictly as one of `'ok' | 'none' | 'error'`, returning
|
|
7
|
-
* `null` for anything that is not an oxy_sso fragment.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import type { SessionLoginResponse } from '../../models/session';
|
|
11
|
-
import { consumeSsoReturn, parseSsoReturnFragment } from '../ssoReturn';
|
|
12
|
-
import {
|
|
13
|
-
getSsoCallbackBootstrapScript,
|
|
14
|
-
ssoAttemptedKey,
|
|
15
|
-
ssoCallbackBootstrapKey,
|
|
16
|
-
ssoDestKey,
|
|
17
|
-
ssoNoSessionKey,
|
|
18
|
-
ssoStateKey,
|
|
19
|
-
} from '../ssoBounce';
|
|
20
|
-
|
|
21
|
-
class MemorySsoStorage implements Pick<Storage, 'getItem' | 'setItem' | 'removeItem'> {
|
|
22
|
-
private readonly values = new Map<string, string>();
|
|
23
|
-
|
|
24
|
-
getItem(key: string): string | null {
|
|
25
|
-
return this.values.get(key) ?? null;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
setItem(key: string, value: string): void {
|
|
29
|
-
this.values.set(key, value);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
removeItem(key: string): void {
|
|
33
|
-
this.values.delete(key);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const ORIGIN = 'https://app.mention.earth';
|
|
38
|
-
|
|
39
|
-
const exchangedSession: SessionLoginResponse = {
|
|
40
|
-
sessionId: 'sess_sso',
|
|
41
|
-
deviceId: 'device_sso',
|
|
42
|
-
accessToken: 'access_sso',
|
|
43
|
-
expiresAt: '2030-01-01T00:00:00.000Z',
|
|
44
|
-
user: { id: 'user_sso', username: 'sso-user' },
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
describe('parseSsoReturnFragment', () => {
|
|
48
|
-
describe('ok', () => {
|
|
49
|
-
it('parses a success fragment with code and state', () => {
|
|
50
|
-
const result = parseSsoReturnFragment('#oxy_sso=ok&code=abc123&state=xyz');
|
|
51
|
-
|
|
52
|
-
expect(result).toEqual({ kind: 'ok', code: 'abc123', state: 'xyz' });
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it('parses a success fragment without a leading #', () => {
|
|
56
|
-
const result = parseSsoReturnFragment('oxy_sso=ok&code=abc123&state=xyz');
|
|
57
|
-
|
|
58
|
-
expect(result).toEqual({ kind: 'ok', code: 'abc123', state: 'xyz' });
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it('omits code when ok carries no code', () => {
|
|
62
|
-
const result = parseSsoReturnFragment('#oxy_sso=ok&state=xyz');
|
|
63
|
-
|
|
64
|
-
expect(result).toEqual({ kind: 'ok', state: 'xyz' });
|
|
65
|
-
expect(result?.code).toBeUndefined();
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it('URL-decodes percent-encoded values', () => {
|
|
69
|
-
const result = parseSsoReturnFragment('#oxy_sso=ok&code=a%2Bb%2Fc&state=s%20t');
|
|
70
|
-
|
|
71
|
-
expect(result).toEqual({ kind: 'ok', code: 'a+b/c', state: 's t' });
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it('ignores a stray reason on an ok outcome', () => {
|
|
75
|
-
const result = parseSsoReturnFragment('#oxy_sso=ok&code=abc123&state=xyz&reason=no_cookie');
|
|
76
|
-
|
|
77
|
-
expect(result).toEqual({ kind: 'ok', code: 'abc123', state: 'xyz' });
|
|
78
|
-
expect(result?.reason).toBeUndefined();
|
|
79
|
-
});
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
describe('none', () => {
|
|
83
|
-
it('parses a none fragment and carries state but never a code', () => {
|
|
84
|
-
const result = parseSsoReturnFragment('#oxy_sso=none&state=xyz');
|
|
85
|
-
|
|
86
|
-
expect(result).toEqual({ kind: 'none', state: 'xyz' });
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it('ignores a stray code on a none outcome', () => {
|
|
90
|
-
const result = parseSsoReturnFragment('#oxy_sso=none&code=leaked&state=xyz');
|
|
91
|
-
|
|
92
|
-
expect(result).toEqual({ kind: 'none', state: 'xyz' });
|
|
93
|
-
expect(result?.code).toBeUndefined();
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it('carries a machine-readable reason on a none outcome', () => {
|
|
97
|
-
const result = parseSsoReturnFragment('#oxy_sso=none&reason=no_cookie&state=xyz');
|
|
98
|
-
|
|
99
|
-
expect(result).toEqual({ kind: 'none', state: 'xyz', reason: 'no_cookie' });
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it('omits reason when the none outcome carries none', () => {
|
|
103
|
-
const result = parseSsoReturnFragment('#oxy_sso=none&state=xyz');
|
|
104
|
-
|
|
105
|
-
expect(result?.reason).toBeUndefined();
|
|
106
|
-
});
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
describe('error', () => {
|
|
110
|
-
it('parses an error fragment', () => {
|
|
111
|
-
const result = parseSsoReturnFragment('#oxy_sso=error&state=xyz');
|
|
112
|
-
|
|
113
|
-
expect(result).toEqual({ kind: 'error', state: 'xyz' });
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it('ignores a stray code on an error outcome', () => {
|
|
117
|
-
const result = parseSsoReturnFragment('#oxy_sso=error&code=leaked');
|
|
118
|
-
|
|
119
|
-
expect(result).toEqual({ kind: 'error' });
|
|
120
|
-
expect(result?.code).toBeUndefined();
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
it('carries a machine-readable reason on an error outcome', () => {
|
|
124
|
-
const result = parseSsoReturnFragment('#oxy_sso=error&reason=no_grant_establish');
|
|
125
|
-
|
|
126
|
-
expect(result).toEqual({ kind: 'error', reason: 'no_grant_establish' });
|
|
127
|
-
});
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
describe('null (not an oxy_sso fragment)', () => {
|
|
131
|
-
it('returns null for an empty string', () => {
|
|
132
|
-
expect(parseSsoReturnFragment('')).toBeNull();
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
it('returns null for a bare #', () => {
|
|
136
|
-
expect(parseSsoReturnFragment('#')).toBeNull();
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
it('returns null for undefined', () => {
|
|
140
|
-
expect(parseSsoReturnFragment(undefined)).toBeNull();
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
it('returns null for null', () => {
|
|
144
|
-
expect(parseSsoReturnFragment(null)).toBeNull();
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
it('returns null for a fragment without oxy_sso', () => {
|
|
148
|
-
expect(parseSsoReturnFragment('#access_token=foo&state=bar')).toBeNull();
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
it('returns null for an unrecognised oxy_sso value', () => {
|
|
152
|
-
expect(parseSsoReturnFragment('#oxy_sso=bogus&code=x')).toBeNull();
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
it('returns null for an empty oxy_sso value', () => {
|
|
156
|
-
expect(parseSsoReturnFragment('#oxy_sso=&code=x')).toBeNull();
|
|
157
|
-
});
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
describe('malformed / defensive', () => {
|
|
161
|
-
it('never throws and returns a valid kind for junk after the marker', () => {
|
|
162
|
-
const result = parseSsoReturnFragment('#oxy_sso=ok&=&&&code=c&&');
|
|
163
|
-
|
|
164
|
-
expect(result?.kind).toBe('ok');
|
|
165
|
-
expect(result?.code).toBe('c');
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
it('always reports a kind in the strict union', () => {
|
|
169
|
-
for (const input of [
|
|
170
|
-
'#oxy_sso=ok',
|
|
171
|
-
'#oxy_sso=none',
|
|
172
|
-
'#oxy_sso=error',
|
|
173
|
-
]) {
|
|
174
|
-
const result = parseSsoReturnFragment(input);
|
|
175
|
-
expect(result).not.toBeNull();
|
|
176
|
-
expect(['ok', 'none', 'error']).toContain(result?.kind);
|
|
177
|
-
}
|
|
178
|
-
});
|
|
179
|
-
});
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
describe('consumeSsoReturn pre-hydration callback bootstrap', () => {
|
|
183
|
-
it('continues an ok callback after the HTML bootstrap moved the URL to a hydratable route', async () => {
|
|
184
|
-
const storage = new MemorySsoStorage();
|
|
185
|
-
const replaceStateCalls: string[] = [];
|
|
186
|
-
const dispatchPopState = jest.fn();
|
|
187
|
-
const hardRedirect = jest.fn();
|
|
188
|
-
const exchangeSsoCode = jest.fn(async (): Promise<SessionLoginResponse> => exchangedSession);
|
|
189
|
-
|
|
190
|
-
storage.setItem(ssoStateKey(ORIGIN), 'state-ok');
|
|
191
|
-
storage.setItem(ssoDestKey(ORIGIN), `${ORIGIN}/explore?tab=home#top`);
|
|
192
|
-
storage.setItem(ssoCallbackBootstrapKey(ORIGIN), '1');
|
|
193
|
-
|
|
194
|
-
const session = await consumeSsoReturn(
|
|
195
|
-
{ exchangeSsoCode },
|
|
196
|
-
{
|
|
197
|
-
isWeb: () => true,
|
|
198
|
-
storage,
|
|
199
|
-
location: {
|
|
200
|
-
hash: '#oxy_sso=ok&code=opaque-code&state=state-ok',
|
|
201
|
-
origin: ORIGIN,
|
|
202
|
-
pathname: '/',
|
|
203
|
-
search: '',
|
|
204
|
-
},
|
|
205
|
-
history: {
|
|
206
|
-
replaceState: (_data: unknown, _unused: string, url?: string | URL | null): void => {
|
|
207
|
-
replaceStateCalls.push(String(url ?? ''));
|
|
208
|
-
},
|
|
209
|
-
},
|
|
210
|
-
dispatchPopState,
|
|
211
|
-
hardRedirect,
|
|
212
|
-
},
|
|
213
|
-
);
|
|
214
|
-
|
|
215
|
-
expect(session).toBe(exchangedSession);
|
|
216
|
-
expect(exchangeSsoCode).toHaveBeenCalledWith('opaque-code', 'state-ok');
|
|
217
|
-
expect(replaceStateCalls).toEqual(['/', '/explore?tab=home#top']);
|
|
218
|
-
expect(dispatchPopState).toHaveBeenCalledTimes(1);
|
|
219
|
-
expect(hardRedirect).not.toHaveBeenCalled();
|
|
220
|
-
expect(storage.getItem(ssoCallbackBootstrapKey(ORIGIN))).toBeNull();
|
|
221
|
-
expect(storage.getItem(ssoDestKey(ORIGIN))).toBeNull();
|
|
222
|
-
expect(storage.getItem(ssoNoSessionKey(ORIGIN))).toBeNull();
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
it('leaves a bootstrapped none callback with loop breakers set and no exchange', async () => {
|
|
226
|
-
const storage = new MemorySsoStorage();
|
|
227
|
-
const replaceStateCalls: string[] = [];
|
|
228
|
-
const dispatchPopState = jest.fn();
|
|
229
|
-
const hardRedirect = jest.fn();
|
|
230
|
-
const exchangeSsoCode = jest.fn(async (): Promise<SessionLoginResponse> => exchangedSession);
|
|
231
|
-
|
|
232
|
-
storage.setItem(ssoStateKey(ORIGIN), 'state-none');
|
|
233
|
-
storage.setItem(ssoDestKey(ORIGIN), `${ORIGIN}/library`);
|
|
234
|
-
storage.setItem(ssoCallbackBootstrapKey(ORIGIN), '1');
|
|
235
|
-
|
|
236
|
-
const session = await consumeSsoReturn(
|
|
237
|
-
{ exchangeSsoCode },
|
|
238
|
-
{
|
|
239
|
-
isWeb: () => true,
|
|
240
|
-
storage,
|
|
241
|
-
location: {
|
|
242
|
-
hash: '#oxy_sso=none&state=state-none',
|
|
243
|
-
origin: ORIGIN,
|
|
244
|
-
pathname: '/',
|
|
245
|
-
search: '',
|
|
246
|
-
},
|
|
247
|
-
history: {
|
|
248
|
-
replaceState: (_data: unknown, _unused: string, url?: string | URL | null): void => {
|
|
249
|
-
replaceStateCalls.push(String(url ?? ''));
|
|
250
|
-
},
|
|
251
|
-
},
|
|
252
|
-
dispatchPopState,
|
|
253
|
-
hardRedirect,
|
|
254
|
-
},
|
|
255
|
-
);
|
|
256
|
-
|
|
257
|
-
expect(session).toBeNull();
|
|
258
|
-
expect(exchangeSsoCode).not.toHaveBeenCalled();
|
|
259
|
-
expect(replaceStateCalls).toEqual(['/']);
|
|
260
|
-
expect(dispatchPopState).not.toHaveBeenCalled();
|
|
261
|
-
expect(hardRedirect).toHaveBeenCalledWith(`${ORIGIN}/library`);
|
|
262
|
-
expect(storage.getItem(ssoCallbackBootstrapKey(ORIGIN))).toBeNull();
|
|
263
|
-
expect(storage.getItem(ssoDestKey(ORIGIN))).toBeNull();
|
|
264
|
-
expect(storage.getItem(ssoNoSessionKey(ORIGIN))).toBe('1');
|
|
265
|
-
expect(storage.getItem(ssoAttemptedKey(ORIGIN))).toBe('1');
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
it('exposes a pre-hydration script that preserves the SSO fragment', () => {
|
|
269
|
-
const script = getSsoCallbackBootstrapScript();
|
|
270
|
-
|
|
271
|
-
expect(script).toContain('/__oxy/sso-callback');
|
|
272
|
-
expect(script).toContain('oxy_sso=');
|
|
273
|
-
expect(script).toContain('window.history.replaceState');
|
|
274
|
-
expect(script).toContain('window.location.hash');
|
|
275
|
-
});
|
|
276
|
-
});
|
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Auto-detect the FAPI (IdP) URL from the current browser hostname.
|
|
3
|
-
*
|
|
4
|
-
* This is the canonical cross-domain IdP-resolution primitive for the Oxy
|
|
5
|
-
* ecosystem. Both candidate cross-domain SSO designs derive `auth.<rp-apex>`
|
|
6
|
-
* through this helper; do not fork it.
|
|
7
|
-
*
|
|
8
|
-
* Clerk-style multi-domain SSO depends on the IdP being reachable on a
|
|
9
|
-
* subdomain of the RP's own apex (e.g. `auth.mention.earth` CNAMEd to the
|
|
10
|
-
* central Oxy IdP). That way every FedCM endpoint, the session cookie,
|
|
11
|
-
* and any redirect target are same-site with the RP — the only way
|
|
12
|
-
* to get first-party cookies in Safari ITP and Firefox Total Cookie
|
|
13
|
-
* Protection.
|
|
14
|
-
*
|
|
15
|
-
* This helper computes `https://auth.<rp-apex>` from
|
|
16
|
-
* `window.location.hostname` so a consuming app doesn't have to pass
|
|
17
|
-
* `authWebUrl` explicitly. Returns `undefined` for environments where
|
|
18
|
-
* auto-detection would be wrong:
|
|
19
|
-
*
|
|
20
|
-
* - SSR / non-browser (no `window`).
|
|
21
|
-
* - `localhost`, `127.0.0.1`, IPv4/IPv6 literals.
|
|
22
|
-
* - Hostnames with fewer than two labels.
|
|
23
|
-
* - Hostnames where a registrable domain cannot be determined from the
|
|
24
|
-
* Public Suffix List, including private hosted suffixes such as
|
|
25
|
-
* `github.io`, `pages.dev`, and `netlify.app`.
|
|
26
|
-
*
|
|
27
|
-
* When the page is already loaded ON the IdP itself (`auth.<anything>`),
|
|
28
|
-
* the helper returns the current origin so the SDK keeps everything
|
|
29
|
-
* same-origin instead of hopping to a different IdP host.
|
|
30
|
-
*
|
|
31
|
-
* The IdP backend independently derives `iss`, `provider_urls`, and the
|
|
32
|
-
* `fedcm.json` icon URLs from the request host
|
|
33
|
-
* (`packages/auth/server/index.ts`), so an honest CNAME pair is all that
|
|
34
|
-
* is required for end-to-end FedCM correctness — no per-RP config.
|
|
35
|
-
*/
|
|
36
|
-
|
|
37
|
-
import { getDomain } from 'tldts';
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Compute the bare registrable apex (eTLD+1) of a hostname using the Public
|
|
41
|
-
* Suffix List, including private hosted suffixes.
|
|
42
|
-
*
|
|
43
|
-
* This is the pure host-handling kernel shared by {@link autoDetectAuthWebUrl}
|
|
44
|
-
* and the IdP worker — it performs NO protocol handling, NO `auth.` prefixing,
|
|
45
|
-
* and builds NO URL. It only answers "what is the registrable domain of this
|
|
46
|
-
* host, or is that undefinable?".
|
|
47
|
-
*
|
|
48
|
-
* Returns `null` (apex undefinable) for:
|
|
49
|
-
* - empty input;
|
|
50
|
-
* - IPv4 literals (`192.168.1.10`);
|
|
51
|
-
* - IPv6 literals or any host carrying a port (`[::1]`, anything with `:`);
|
|
52
|
-
* - single-label hosts (`intranet`, `localhost`);
|
|
53
|
-
* - public suffixes without a registrable label (e.g. `co.uk`, `github.io`).
|
|
54
|
-
*
|
|
55
|
-
* @param hostname - A bare hostname (no scheme), e.g. `www.mention.earth`.
|
|
56
|
-
* @returns The eTLD+1 (`mention.earth`), or `null` when undefinable.
|
|
57
|
-
*/
|
|
58
|
-
export function registrableApex(hostname: string): string | null {
|
|
59
|
-
if (!hostname) return null;
|
|
60
|
-
const host = hostname.toLowerCase();
|
|
61
|
-
if (/^\d+\.\d+\.\d+\.\d+$/.test(host)) return null;
|
|
62
|
-
// IPv6 literals are bracketed; any remaining ':' implies a port — neither
|
|
63
|
-
// yields a registrable apex.
|
|
64
|
-
if (host.startsWith('[') || host.includes(':')) return null;
|
|
65
|
-
// The Public Suffix List (private suffixes included) is the source of truth
|
|
66
|
-
// for what an attacker can register. A multi-part public suffix like `co.uk`
|
|
67
|
-
// or a hosted suffix like `github.io` returns no registrable domain, so
|
|
68
|
-
// deriving `auth.<apex>` against it is impossible — `getDomain` returns null.
|
|
69
|
-
return getDomain(host, { allowPrivateDomains: true });
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export function autoDetectAuthWebUrl(
|
|
73
|
-
location: Pick<Location, 'hostname' | 'protocol'> | undefined =
|
|
74
|
-
typeof window !== 'undefined' ? window.location : undefined
|
|
75
|
-
): string | undefined {
|
|
76
|
-
if (!location) return undefined;
|
|
77
|
-
const { hostname, protocol } = location;
|
|
78
|
-
if (!hostname) return undefined;
|
|
79
|
-
if (protocol !== 'https:' && protocol !== 'http:') return undefined;
|
|
80
|
-
if (hostname === 'localhost' || hostname === '127.0.0.1') return undefined;
|
|
81
|
-
if (/^\d+\.\d+\.\d+\.\d+$/.test(hostname)) return undefined;
|
|
82
|
-
if (hostname.startsWith('[')) return undefined;
|
|
83
|
-
// Already ON the IdP — keep everything same-origin instead of hopping to a
|
|
84
|
-
// sibling host.
|
|
85
|
-
if (hostname.startsWith('auth.')) {
|
|
86
|
-
return `${protocol}//${hostname}`;
|
|
87
|
-
}
|
|
88
|
-
const apex = registrableApex(hostname);
|
|
89
|
-
if (apex === null) return undefined;
|
|
90
|
-
return `${protocol}//auth.${apex}`;
|
|
91
|
-
}
|
|
@@ -1,174 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Post-claim durable-session establish hop (web device-flow / "Sign in with
|
|
3
|
-
* Oxy" QR).
|
|
4
|
-
*
|
|
5
|
-
* A WEB device-flow claim (`claimSessionByToken`) plants only IN-MEMORY tokens:
|
|
6
|
-
* unlike a redirect/FedCM/silent sign-in, it never causes the IdP to plant a
|
|
7
|
-
* `fedcm_session` cookie. So a reload has nothing to restore from — the
|
|
8
|
-
* silent-iframe and `/sso` paths find no IdP session and the session is lost.
|
|
9
|
-
*
|
|
10
|
-
* This primitive closes that gap. AFTER the claim has committed and the session
|
|
11
|
-
* has been durably persisted, it performs ONE top-level establish hop through
|
|
12
|
-
* the RP's own per-apex IdP host (`auth.<rp-apex>`), reusing the EXISTING
|
|
13
|
-
* `/sso/establish` endpoint: the server mints a short-lived, host+audience-bound
|
|
14
|
-
* establish-token and returns a fully-formed establish URL; navigating to it
|
|
15
|
-
* plants the durable first-party `fedcm_session` cookie and bounces back to the
|
|
16
|
-
* RP callback with an opaque code the standard `sso-return` cold-boot step
|
|
17
|
-
* exchanges.
|
|
18
|
-
*
|
|
19
|
-
* It reuses the SAME per-origin `sessionStorage` bounce contract
|
|
20
|
-
* (`ssoStateKey` / `ssoGuardKey` / `ssoDestKey`) that {@link buildSsoBounceUrl}
|
|
21
|
-
* primes for the terminal `/sso` bounce, so the post-bounce `sso-return` step
|
|
22
|
-
* (`consumeSsoReturn`) validates the CSRF `state`, exchanges the code, and
|
|
23
|
-
* restores the user's real destination with no extra wiring.
|
|
24
|
-
*
|
|
25
|
-
* Contract:
|
|
26
|
-
* - WEB only — off-web / native it is a no-op returning `false`.
|
|
27
|
-
* - NEVER fires while sitting on the central IdP origin (that would loop the
|
|
28
|
-
* IdP against itself).
|
|
29
|
-
* - Bounce state is persisted ONLY after the establish-URL request succeeds, so
|
|
30
|
-
* a failed request leaves no stale state behind.
|
|
31
|
-
* - SINGLE attempt: the caller invokes this exactly once per successful claim.
|
|
32
|
-
* On ANY failure it does NOT navigate and returns `false`, leaving the
|
|
33
|
-
* committed in-memory session exactly as-is (the user is no worse off than
|
|
34
|
-
* before this hop existed).
|
|
35
|
-
* - Total: never throws. Failures are reported via {@link deps.onError} only.
|
|
36
|
-
*/
|
|
37
|
-
|
|
38
|
-
import {
|
|
39
|
-
ssoStateKey,
|
|
40
|
-
ssoGuardKey,
|
|
41
|
-
ssoDestKey,
|
|
42
|
-
ssoNavigate,
|
|
43
|
-
isCentralIdPOrigin,
|
|
44
|
-
} from './ssoBounce';
|
|
45
|
-
import { generateSsoState } from '../mixins/OxyServices.sso';
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* The minimal SDK surface this hop needs: mint a server-formed establish URL
|
|
49
|
-
* bound to the caller's own session for an approved RP origin. Structural so the
|
|
50
|
-
* primitive is unit-testable with a stub and never imports the full client.
|
|
51
|
-
*/
|
|
52
|
-
export interface SsoEstablishClient {
|
|
53
|
-
requestSsoEstablishUrl(
|
|
54
|
-
origin: string,
|
|
55
|
-
state: string,
|
|
56
|
-
): Promise<{ establishUrl: string }>;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Injectable web seams for {@link establishIdpSessionAfterClaim}. Every seam is
|
|
61
|
-
* overridable so the primitive is fully unit-testable with fakes and so native
|
|
62
|
-
* callers rely on the defaults (which resolve to `window.*` only when a browser
|
|
63
|
-
* is present). Defaults are evaluated lazily inside the function so importing
|
|
64
|
-
* this module never touches `window`.
|
|
65
|
-
*/
|
|
66
|
-
export interface EstablishAfterClaimDeps {
|
|
67
|
-
/** Per-tab SSO bounce store. Default: `window.sessionStorage`. */
|
|
68
|
-
storage?: Pick<Storage, 'getItem' | 'setItem'>;
|
|
69
|
-
/** The current location. Default: `window.location`. */
|
|
70
|
-
location?: Pick<Location, 'origin' | 'href'>;
|
|
71
|
-
/** Top-level navigation seam. Default: {@link ssoNavigate} (`location.assign`). */
|
|
72
|
-
navigate?: (url: string) => void;
|
|
73
|
-
/**
|
|
74
|
-
* Whether the current environment is a web browser with usable
|
|
75
|
-
* `sessionStorage`. Default: `typeof window !== 'undefined' && typeof
|
|
76
|
-
* window.sessionStorage !== 'undefined'`.
|
|
77
|
-
*/
|
|
78
|
-
isWeb?: () => boolean;
|
|
79
|
-
/** CSRF state generator. Default: {@link generateSsoState}. */
|
|
80
|
-
generateState?: () => string;
|
|
81
|
-
/** Epoch-ms clock for the bounce guard. Default: `Date.now`. */
|
|
82
|
-
now?: () => number;
|
|
83
|
-
/**
|
|
84
|
-
* Optional debug hook invoked with the thrown error when the establish
|
|
85
|
-
* request (or state persistence) fails. NEVER rethrown. Default: no-op.
|
|
86
|
-
*/
|
|
87
|
-
onError?: (error: unknown) => void;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Perform the post-claim establish hop. Returns `true` when a navigation to the
|
|
92
|
-
* establish URL was initiated (the document is being torn down and replaced by
|
|
93
|
-
* the IdP), `false` on every no-op / failure path.
|
|
94
|
-
*
|
|
95
|
-
* @param client - The exchange surface (`oxyServices.requestSsoEstablishUrl`).
|
|
96
|
-
* @param deps - Injectable web seams; see {@link EstablishAfterClaimDeps}.
|
|
97
|
-
*/
|
|
98
|
-
export async function establishIdpSessionAfterClaim(
|
|
99
|
-
client: SsoEstablishClient,
|
|
100
|
-
deps: EstablishAfterClaimDeps = {},
|
|
101
|
-
): Promise<boolean> {
|
|
102
|
-
const isWeb =
|
|
103
|
-
deps.isWeb ??
|
|
104
|
-
(() =>
|
|
105
|
-
typeof window !== 'undefined' &&
|
|
106
|
-
typeof window.sessionStorage !== 'undefined');
|
|
107
|
-
|
|
108
|
-
if (!isWeb()) {
|
|
109
|
-
return false;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
const storage = deps.storage ?? window.sessionStorage;
|
|
113
|
-
const location = deps.location ?? window.location;
|
|
114
|
-
const navigate = deps.navigate ?? ssoNavigate;
|
|
115
|
-
const generateState = deps.generateState ?? generateSsoState;
|
|
116
|
-
const now = deps.now ?? (() => Date.now());
|
|
117
|
-
|
|
118
|
-
const origin = location.origin;
|
|
119
|
-
|
|
120
|
-
// Never establish while sitting on the central IdP itself — that would bounce
|
|
121
|
-
// the IdP against itself. (The device-flow claim can only happen on an RP.)
|
|
122
|
-
if (isCentralIdPOrigin(origin)) {
|
|
123
|
-
return false;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const state = generateState();
|
|
127
|
-
|
|
128
|
-
let establishUrl: string;
|
|
129
|
-
try {
|
|
130
|
-
const result = await client.requestSsoEstablishUrl(origin, state);
|
|
131
|
-
if (
|
|
132
|
-
!result ||
|
|
133
|
-
typeof result.establishUrl !== 'string' ||
|
|
134
|
-
result.establishUrl.length === 0
|
|
135
|
-
) {
|
|
136
|
-
return false;
|
|
137
|
-
}
|
|
138
|
-
establishUrl = result.establishUrl;
|
|
139
|
-
|
|
140
|
-
// Defense-in-depth before a TOP-LEVEL navigation: the establish URL is
|
|
141
|
-
// server-formed, but never follow anything that is not a well-formed https
|
|
142
|
-
// `/sso/establish` on an `auth.` host. A malformed or unexpected URL aborts
|
|
143
|
-
// silently (no navigation, no state persisted) — same total/no-throw
|
|
144
|
-
// contract — rather than navigating the document somewhere unintended.
|
|
145
|
-
let parsedEstablishUrl: URL;
|
|
146
|
-
try {
|
|
147
|
-
parsedEstablishUrl = new URL(establishUrl);
|
|
148
|
-
} catch {
|
|
149
|
-
return false;
|
|
150
|
-
}
|
|
151
|
-
if (
|
|
152
|
-
parsedEstablishUrl.protocol !== 'https:' ||
|
|
153
|
-
parsedEstablishUrl.pathname !== '/sso/establish' ||
|
|
154
|
-
!parsedEstablishUrl.hostname.toLowerCase().startsWith('auth.')
|
|
155
|
-
) {
|
|
156
|
-
return false;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// Persist the bounce state ONLY now that the request has succeeded and the
|
|
160
|
-
// URL is validated — so a failed/rejected request never leaves stale state
|
|
161
|
-
// behind. This is the exact contract the terminal `/sso` bounce primes via
|
|
162
|
-
// `buildSsoBounceUrl`, so the post-bounce `sso-return` step (`consumeSsoReturn`)
|
|
163
|
-
// validates `state`, exchanges the returned code, and restores the dest.
|
|
164
|
-
storage.setItem(ssoStateKey(origin), state);
|
|
165
|
-
storage.setItem(ssoGuardKey(origin), String(now()));
|
|
166
|
-
storage.setItem(ssoDestKey(origin), location.href);
|
|
167
|
-
} catch (error) {
|
|
168
|
-
deps.onError?.(error);
|
|
169
|
-
return false;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
navigate(establishUrl);
|
|
173
|
-
return true;
|
|
174
|
-
}
|