@oxyhq/core 12.11.1 → 13.2.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/README.md +36 -2
- package/dist/cjs/.tsbuildinfo +1 -1
- package/dist/cjs/index.js +4 -13
- package/dist/cjs/mixins/OxyServices.deviceBoot.js +0 -31
- package/dist/cjs/mixins/OxyServices.user.js +50 -44
- package/dist/cjs/server/index.js +9 -6
- package/dist/cjs/server/rateLimit.js +3 -0
- package/dist/cjs/server/securityHeaders.js +234 -0
- package/dist/cjs/session/accountDialogController.js +6 -8
- package/dist/cjs/utils/apiUtils.js +40 -10
- package/dist/cjs/utils/oauthPkce.js +1 -5
- package/dist/cjs/utils/officialOrigins.js +3 -73
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/index.js +3 -4
- package/dist/esm/mixins/OxyServices.deviceBoot.js +1 -32
- package/dist/esm/mixins/OxyServices.user.js +51 -45
- package/dist/esm/server/index.js +4 -1
- package/dist/esm/server/rateLimit.js +3 -0
- package/dist/esm/server/securityHeaders.js +224 -0
- package/dist/esm/session/accountDialogController.js +6 -8
- package/dist/esm/utils/apiUtils.js +39 -10
- package/dist/esm/utils/oauthPkce.js +0 -4
- package/dist/esm/utils/officialOrigins.js +3 -68
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/index.d.ts +5 -7
- package/dist/types/mixins/OxyServices.auth.d.ts +1 -12
- package/dist/types/mixins/OxyServices.deviceBoot.d.ts +1 -5
- package/dist/types/mixins/OxyServices.user.d.ts +27 -6
- package/dist/types/server/index.d.ts +3 -1
- package/dist/types/server/securityHeaders.d.ts +154 -0
- package/dist/types/session/accountDialogController.d.ts +9 -15
- package/dist/types/utils/apiUtils.d.ts +48 -6
- package/dist/types/utils/oauthPkce.d.ts +11 -7
- package/dist/types/utils/officialOrigins.d.ts +3 -13
- package/package.json +10 -5
- package/src/index.ts +9 -14
- package/src/mixins/OxyServices.auth.ts +6 -13
- package/src/mixins/OxyServices.deviceBoot.ts +0 -47
- package/src/mixins/OxyServices.user.ts +60 -49
- package/src/mixins/__tests__/commonsSignIn.test.ts +9 -2
- package/src/mixins/__tests__/followGraphPagination.test.ts +250 -0
- package/src/server/__tests__/securityHeaders.test.ts +244 -0
- package/src/server/index.ts +17 -8
- package/src/server/rateLimit.ts +3 -0
- package/src/server/securityHeaders.ts +304 -0
- package/src/session/__tests__/accountDialogController.test.ts +3 -5
- package/src/session/accountDialogController.ts +12 -18
- package/src/utils/__tests__/officialOrigins.test.ts +0 -57
- package/src/utils/apiUtils.ts +64 -15
- package/src/utils/oauthPkce.ts +11 -9
- package/src/utils/officialOrigins.ts +3 -70
- package/dist/cjs/session/hubSync.js +0 -55
- package/dist/esm/session/hubSync.js +0 -51
- package/dist/types/session/hubSync.d.ts +0 -20
- package/src/session/__tests__/hubSync.test.ts +0 -51
- package/src/session/hubSync.ts +0 -79
|
@@ -1,19 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
|
-
buildHubSyncUrl,
|
|
3
|
-
buildIdpHubOrigin,
|
|
4
2
|
isAllowedDeviceJoinOrigin,
|
|
5
|
-
isIdpHubOrigin,
|
|
6
3
|
isLoopbackOrigin,
|
|
7
4
|
isOfficialWebOrigin,
|
|
8
|
-
normalizeOfficialReturnOrigin,
|
|
9
|
-
parseHubSyncReturnUrl,
|
|
10
5
|
} from '../officialOrigins';
|
|
11
6
|
|
|
12
7
|
describe('officialOrigins', () => {
|
|
13
|
-
it('builds the IdP hub origin', () => {
|
|
14
|
-
expect(buildIdpHubOrigin()).toBe('https://auth.oxy.so');
|
|
15
|
-
});
|
|
16
|
-
|
|
17
8
|
it('allows official first-party origins', () => {
|
|
18
9
|
expect(isOfficialWebOrigin('https://inbox.oxy.so')).toBe(true);
|
|
19
10
|
expect(isOfficialWebOrigin('https://mention.earth')).toBe(true);
|
|
@@ -37,52 +28,4 @@ describe('officialOrigins', () => {
|
|
|
37
28
|
expect(isAllowedDeviceJoinOrigin('https://accounts.oxy.so')).toBe(true);
|
|
38
29
|
expect(isAllowedDeviceJoinOrigin('https://evil.example')).toBe(false);
|
|
39
30
|
});
|
|
40
|
-
|
|
41
|
-
it('normalizes return origins to origin only', () => {
|
|
42
|
-
expect(normalizeOfficialReturnOrigin('https://accounts.oxy.so/settings')).toBe(
|
|
43
|
-
'https://accounts.oxy.so',
|
|
44
|
-
);
|
|
45
|
-
expect(normalizeOfficialReturnOrigin('https://evil.example/')).toBeNull();
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it('parses hub-sync return URLs', () => {
|
|
49
|
-
expect(parseHubSyncReturnUrl('https://inbox.oxy.so/messages')).toBe(
|
|
50
|
-
'https://inbox.oxy.so/messages',
|
|
51
|
-
);
|
|
52
|
-
expect(parseHubSyncReturnUrl('https://evil.example/')).toBeNull();
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it('builds hub sync URLs with ticket and optional return', () => {
|
|
56
|
-
const url = new URL(buildHubSyncUrl('tk-abc', 'https://accounts.oxy.so/'));
|
|
57
|
-
expect(url.pathname).toBe('/sync');
|
|
58
|
-
expect(url.searchParams.get('ticket')).toBe('tk-abc');
|
|
59
|
-
expect(url.searchParams.get('return')).toBe('https://accounts.oxy.so/');
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
describe('isIdpHubOrigin', () => {
|
|
63
|
-
const originalLocation = globalThis.location;
|
|
64
|
-
|
|
65
|
-
afterEach(() => {
|
|
66
|
-
Object.defineProperty(globalThis, 'location', {
|
|
67
|
-
configurable: true,
|
|
68
|
-
value: originalLocation,
|
|
69
|
-
});
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it('returns true on auth.oxy.so', () => {
|
|
73
|
-
Object.defineProperty(globalThis, 'location', {
|
|
74
|
-
configurable: true,
|
|
75
|
-
value: { href: 'https://auth.oxy.so/sync' },
|
|
76
|
-
});
|
|
77
|
-
expect(isIdpHubOrigin()).toBe(true);
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it('returns false on satellite origins', () => {
|
|
81
|
-
Object.defineProperty(globalThis, 'location', {
|
|
82
|
-
configurable: true,
|
|
83
|
-
value: { href: 'https://inbox.oxy.so/' },
|
|
84
|
-
});
|
|
85
|
-
expect(isIdpHubOrigin()).toBe(false);
|
|
86
|
-
});
|
|
87
|
-
});
|
|
88
31
|
});
|
package/src/utils/apiUtils.ts
CHANGED
|
@@ -3,20 +3,46 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* Build
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* Build a plain query-parameter record from an object, stringifying values and
|
|
7
|
+
* dropping `undefined`/`null` entries.
|
|
8
|
+
*
|
|
9
|
+
* This is the shape `OxyServices.makeRequest` expects for a GET's `params`:
|
|
10
|
+
* `HttpService` inspects it with `Object.keys(...)` (both to decide whether to
|
|
11
|
+
* append a query string and to build the request's cache key), and
|
|
12
|
+
* `Object.keys(new URLSearchParams({ limit: '20' }))` is `[]` — a
|
|
13
|
+
* `URLSearchParams` exposes its entries through iterator methods, never as own
|
|
14
|
+
* enumerable properties. Passing one to `makeRequest` therefore silently drops
|
|
15
|
+
* the whole query string. Always hand `makeRequest` a plain record.
|
|
16
|
+
*
|
|
17
|
+
* Generic over the input object rather than taking `Record<string, unknown>`,
|
|
18
|
+
* because a TypeScript `interface` (`PaginationParams`, `FollowGraphParams`, …)
|
|
19
|
+
* has no implicit index signature and so is not assignable to that type.
|
|
9
20
|
*/
|
|
10
|
-
export function
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
21
|
+
export function buildQueryParams<T extends object>(params: T): Record<string, string> {
|
|
22
|
+
const query: Record<string, string> = {};
|
|
23
|
+
|
|
24
|
+
// Widening the value to `unknown` is always sound; the default overload of
|
|
25
|
+
// `Object.entries` would otherwise infer `any` here.
|
|
26
|
+
for (const [key, value] of Object.entries(params) as [string, unknown][]) {
|
|
14
27
|
if (value !== undefined && value !== null) {
|
|
15
|
-
|
|
28
|
+
query[key] = String(value);
|
|
16
29
|
}
|
|
17
30
|
}
|
|
18
|
-
|
|
19
|
-
return
|
|
31
|
+
|
|
32
|
+
return query;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Build URL search parameters from an object.
|
|
37
|
+
*
|
|
38
|
+
* For building a URL string only — see {@link buildQueryParams} for the shape
|
|
39
|
+
* `makeRequest` needs.
|
|
40
|
+
*
|
|
41
|
+
* @param params Object with parameter key-value pairs
|
|
42
|
+
* @returns URLSearchParams instance
|
|
43
|
+
*/
|
|
44
|
+
export function buildSearchParams<T extends object>(params: T): URLSearchParams {
|
|
45
|
+
return new URLSearchParams(buildQueryParams(params));
|
|
20
46
|
}
|
|
21
47
|
|
|
22
48
|
/**
|
|
@@ -25,7 +51,7 @@ export function buildSearchParams(params: Record<string, any>): URLSearchParams
|
|
|
25
51
|
* @param params Object with parameter key-value pairs
|
|
26
52
|
* @returns Complete URL with search parameters
|
|
27
53
|
*/
|
|
28
|
-
export function buildUrl(baseUrl: string, params?:
|
|
54
|
+
export function buildUrl<T extends object>(baseUrl: string, params?: T): string {
|
|
29
55
|
if (!params) return baseUrl;
|
|
30
56
|
|
|
31
57
|
const searchParams = buildSearchParams(params);
|
|
@@ -43,12 +69,35 @@ export interface PaginationParams {
|
|
|
43
69
|
}
|
|
44
70
|
|
|
45
71
|
/**
|
|
46
|
-
*
|
|
72
|
+
* Ordering for the follow-graph list endpoints (`/users/:id/followers`,
|
|
73
|
+
* `/users/:id/following`, `/users/:id/mutuals`).
|
|
74
|
+
*
|
|
75
|
+
* - `recent` — newest follow edge first (the server default).
|
|
76
|
+
* - `oldest` — oldest follow edge first.
|
|
77
|
+
*/
|
|
78
|
+
export type FollowGraphSort = 'recent' | 'oldest';
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Pagination plus the follow-graph ordering.
|
|
82
|
+
*
|
|
83
|
+
* Kept separate from {@link PaginationParams}, which is shared by endpoints
|
|
84
|
+
* that have no `sort` at all.
|
|
85
|
+
*/
|
|
86
|
+
export interface FollowGraphParams extends PaginationParams {
|
|
87
|
+
sort?: FollowGraphSort;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Build pagination query parameters.
|
|
92
|
+
*
|
|
93
|
+
* Returns a plain record — NOT a `URLSearchParams` — because that is the only
|
|
94
|
+
* shape `makeRequest`/`HttpService` can read. See {@link buildQueryParams}.
|
|
95
|
+
*
|
|
47
96
|
* @param params Pagination parameters
|
|
48
|
-
* @returns
|
|
97
|
+
* @returns Query record with pagination
|
|
49
98
|
*/
|
|
50
|
-
export function buildPaginationParams(params: PaginationParams):
|
|
51
|
-
return
|
|
99
|
+
export function buildPaginationParams(params: PaginationParams): Record<string, string> {
|
|
100
|
+
return buildQueryParams(params);
|
|
52
101
|
}
|
|
53
102
|
|
|
54
103
|
/**
|
package/src/utils/oauthPkce.ts
CHANGED
|
@@ -62,10 +62,18 @@ export interface BuildOAuthAuthorizeUrlParams {
|
|
|
62
62
|
/** The PKCE `codeChallenge` from {@link generatePkcePair}. */
|
|
63
63
|
codeChallenge: string;
|
|
64
64
|
/**
|
|
65
|
-
* OAuth `prompt` parameter
|
|
66
|
-
*
|
|
65
|
+
* OAuth `prompt` parameter — `login` forces a fresh authentication even when
|
|
66
|
+
* the IdP already has a session, `consent` forces the consent screen even for
|
|
67
|
+
* an already-granted scope. Both are ordinary OAuth/OIDC and are here for
|
|
68
|
+
* third-party relying parties building their own authorize link.
|
|
69
|
+
*
|
|
70
|
+
* `'none'` is deliberately NOT in this union. It is the silent-SSO value, and
|
|
71
|
+
* it was the only value this SDK ever sent — from the cold-boot cross-origin
|
|
72
|
+
* restore deleted in #691 phase 7b. Accepting it again would hand consumers a
|
|
73
|
+
* one-line rebuild of the automatic, gesture-less full-page bounce to the IdP
|
|
74
|
+
* that the popup transport exists to eliminate.
|
|
67
75
|
*/
|
|
68
|
-
prompt?: '
|
|
76
|
+
prompt?: 'login' | 'consent';
|
|
69
77
|
/**
|
|
70
78
|
* How the IdP should deliver the authorization response. Omitted (the
|
|
71
79
|
* default) means the ordinary top-level redirect back to `redirectUri`.
|
|
@@ -220,12 +228,6 @@ export const OXY_OAUTH_CODE_VERIFIER_STORAGE_KEY = 'oxy_oauth_code_verifier';
|
|
|
220
228
|
/** `sessionStorage` key — the exact `redirect_uri` sent on the authorize request. */
|
|
221
229
|
export const OXY_OAUTH_REDIRECT_URI_STORAGE_KEY = 'oxy.oauth_redirect_uri';
|
|
222
230
|
|
|
223
|
-
/** `sessionStorage` key — at most one silent OAuth attempt per navigation. */
|
|
224
|
-
export const OXY_SILENT_OAUTH_ATTEMPTED_KEY = 'oxy.silent_oauth_attempted';
|
|
225
|
-
|
|
226
|
-
/** `sessionStorage` key — blocks further cross-origin auto-restore in this tab. */
|
|
227
|
-
export const OXY_CROSS_ORIGIN_RESTORE_ATTEMPTED_KEY = 'oxy.cross_origin_restore_attempted';
|
|
228
|
-
|
|
229
231
|
/**
|
|
230
232
|
* `sessionStorage` key for the in-app path to return to after an authorize
|
|
231
233
|
* round trip. See {@link persistOAuthReturnPath}.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Official first-party web origin allowlist — shared by
|
|
3
|
-
*
|
|
2
|
+
* Official first-party web origin allowlist — shared by OAuth redirect
|
|
3
|
+
* validation and the server-side trusted-origin checks.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { CENTRAL_IDP_APEX } from './authWebUrl';
|
|
@@ -20,31 +20,9 @@ const OFFICIAL_APEXES = new Set([
|
|
|
20
20
|
'mercaria.co',
|
|
21
21
|
]);
|
|
22
22
|
|
|
23
|
-
export function buildIdpHubOrigin(): string {
|
|
24
|
-
return `https://auth.${CENTRAL_IDP_APEX}`;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/** Whether the current web origin is the central IdP hub (`auth.oxy.so`). */
|
|
28
|
-
export function isIdpHubOrigin(): boolean {
|
|
29
|
-
if (typeof globalThis === 'undefined') {
|
|
30
|
-
return false;
|
|
31
|
-
}
|
|
32
|
-
const location = (globalThis as { location?: Location }).location;
|
|
33
|
-
if (!location) {
|
|
34
|
-
return false;
|
|
35
|
-
}
|
|
36
|
-
try {
|
|
37
|
-
const { hostname } = new URL(location.href);
|
|
38
|
-
return hostname === `auth.${CENTRAL_IDP_APEX}`;
|
|
39
|
-
} catch {
|
|
40
|
-
return false;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
23
|
/**
|
|
45
24
|
* Whether an origin is a loopback / local-dev origin (`localhost`, `127.0.0.1`,
|
|
46
|
-
* or `[::1]` on any port, http or https).
|
|
47
|
-
* hosted IdP for cross-origin session restore.
|
|
25
|
+
* or `[::1]` on any port, http or https).
|
|
48
26
|
*/
|
|
49
27
|
export function isLoopbackOrigin(origin: string): boolean {
|
|
50
28
|
try {
|
|
@@ -80,50 +58,5 @@ export function isOfficialWebOrigin(origin: string): boolean {
|
|
|
80
58
|
}
|
|
81
59
|
}
|
|
82
60
|
|
|
83
|
-
/** Normalize and validate a return URL against official origins. Returns origin only. */
|
|
84
|
-
export function normalizeOfficialReturnOrigin(raw: string): string | null {
|
|
85
|
-
try {
|
|
86
|
-
const parsed = new URL(raw);
|
|
87
|
-
if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
|
|
88
|
-
return null;
|
|
89
|
-
}
|
|
90
|
-
if (!isOfficialWebOrigin(parsed.origin)) {
|
|
91
|
-
return null;
|
|
92
|
-
}
|
|
93
|
-
return parsed.origin;
|
|
94
|
-
} catch {
|
|
95
|
-
return null;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/** Validate a hub-sync return URL; returns the full normalized URL string. */
|
|
100
|
-
export function parseHubSyncReturnUrl(raw: string | null): string | null {
|
|
101
|
-
if (!raw) {
|
|
102
|
-
return null;
|
|
103
|
-
}
|
|
104
|
-
try {
|
|
105
|
-
const parsed = new URL(raw);
|
|
106
|
-
if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
|
|
107
|
-
return null;
|
|
108
|
-
}
|
|
109
|
-
if (!isOfficialWebOrigin(parsed.origin)) {
|
|
110
|
-
return null;
|
|
111
|
-
}
|
|
112
|
-
return parsed.toString();
|
|
113
|
-
} catch {
|
|
114
|
-
return null;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/** Build auth.oxy.so/sync redirect URL with a one-time hub ticket. */
|
|
119
|
-
export function buildHubSyncUrl(ticket: string, returnUrl?: string): string {
|
|
120
|
-
const url = new URL('/sync', buildIdpHubOrigin());
|
|
121
|
-
url.searchParams.set('ticket', ticket);
|
|
122
|
-
if (returnUrl) {
|
|
123
|
-
url.searchParams.set('return', returnUrl);
|
|
124
|
-
}
|
|
125
|
-
return url.toString();
|
|
126
|
-
}
|
|
127
|
-
|
|
128
61
|
/** @deprecated Use {@link isOfficialWebOrigin}. */
|
|
129
62
|
export const isAllowedDeviceJoinOrigin = isOfficialWebOrigin;
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* Post-sign-in hub sync — plant device credentials on auth.oxy.so via a
|
|
4
|
-
* one-time server ticket (no secrets in URL fragments).
|
|
5
|
-
*/
|
|
6
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
-
exports.syncHubAfterSignIn = syncHubAfterSignIn;
|
|
8
|
-
exports.redeemHubTicketOnHub = redeemHubTicketOnHub;
|
|
9
|
-
const officialOrigins_1 = require("../utils/officialOrigins");
|
|
10
|
-
/**
|
|
11
|
-
* After a successful sign-in on an official web app, mint a hub ticket and
|
|
12
|
-
* redirect to auth.oxy.so/sync so the IdP hub can redeem it and persist the
|
|
13
|
-
* shared device credential for silent OAuth restore on other origins.
|
|
14
|
-
*
|
|
15
|
-
* No-op on native, non-official origins, and when already on the IdP hub.
|
|
16
|
-
*/
|
|
17
|
-
async function syncHubAfterSignIn(oxy, opts) {
|
|
18
|
-
if (opts?.enabled === false) {
|
|
19
|
-
return false;
|
|
20
|
-
}
|
|
21
|
-
if (typeof globalThis === 'undefined') {
|
|
22
|
-
return false;
|
|
23
|
-
}
|
|
24
|
-
const location = globalThis.location;
|
|
25
|
-
if (!location) {
|
|
26
|
-
return false;
|
|
27
|
-
}
|
|
28
|
-
if ((0, officialOrigins_1.isIdpHubOrigin)()) {
|
|
29
|
-
return false;
|
|
30
|
-
}
|
|
31
|
-
if (!(0, officialOrigins_1.isOfficialWebOrigin)(location.origin)) {
|
|
32
|
-
return false;
|
|
33
|
-
}
|
|
34
|
-
const hubOrigin = (0, officialOrigins_1.buildIdpHubOrigin)();
|
|
35
|
-
const issued = await oxy.issueHubTicket(hubOrigin);
|
|
36
|
-
const returnUrl = `${location.origin}${location.pathname}${location.search}${location.hash}`;
|
|
37
|
-
const syncUrl = (0, officialOrigins_1.buildHubSyncUrl)(issued.ticket, returnUrl);
|
|
38
|
-
window.location.assign(syncUrl);
|
|
39
|
-
return true;
|
|
40
|
-
}
|
|
41
|
-
/** Redeem a hub ticket on auth.oxy.so and persist credentials locally. */
|
|
42
|
-
async function redeemHubTicketOnHub(oxy, store, ticket) {
|
|
43
|
-
const hubOrigin = (0, officialOrigins_1.buildIdpHubOrigin)();
|
|
44
|
-
const creds = await oxy.redeemHubTicket(ticket, hubOrigin);
|
|
45
|
-
const prior = await store.load();
|
|
46
|
-
await store.save({
|
|
47
|
-
sessionId: prior?.sessionId ?? '',
|
|
48
|
-
userId: prior?.userId ?? '',
|
|
49
|
-
deviceId: creds.deviceId,
|
|
50
|
-
deviceSecret: creds.deviceSecret,
|
|
51
|
-
...(prior?.accessToken ? { accessToken: prior.accessToken } : {}),
|
|
52
|
-
...(prior?.expiresAt ? { expiresAt: prior.expiresAt } : {}),
|
|
53
|
-
});
|
|
54
|
-
return true;
|
|
55
|
-
}
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Post-sign-in hub sync — plant device credentials on auth.oxy.so via a
|
|
3
|
-
* one-time server ticket (no secrets in URL fragments).
|
|
4
|
-
*/
|
|
5
|
-
import { buildHubSyncUrl, buildIdpHubOrigin, isIdpHubOrigin, isOfficialWebOrigin, } from '../utils/officialOrigins.js';
|
|
6
|
-
/**
|
|
7
|
-
* After a successful sign-in on an official web app, mint a hub ticket and
|
|
8
|
-
* redirect to auth.oxy.so/sync so the IdP hub can redeem it and persist the
|
|
9
|
-
* shared device credential for silent OAuth restore on other origins.
|
|
10
|
-
*
|
|
11
|
-
* No-op on native, non-official origins, and when already on the IdP hub.
|
|
12
|
-
*/
|
|
13
|
-
export async function syncHubAfterSignIn(oxy, opts) {
|
|
14
|
-
if (opts?.enabled === false) {
|
|
15
|
-
return false;
|
|
16
|
-
}
|
|
17
|
-
if (typeof globalThis === 'undefined') {
|
|
18
|
-
return false;
|
|
19
|
-
}
|
|
20
|
-
const location = globalThis.location;
|
|
21
|
-
if (!location) {
|
|
22
|
-
return false;
|
|
23
|
-
}
|
|
24
|
-
if (isIdpHubOrigin()) {
|
|
25
|
-
return false;
|
|
26
|
-
}
|
|
27
|
-
if (!isOfficialWebOrigin(location.origin)) {
|
|
28
|
-
return false;
|
|
29
|
-
}
|
|
30
|
-
const hubOrigin = buildIdpHubOrigin();
|
|
31
|
-
const issued = await oxy.issueHubTicket(hubOrigin);
|
|
32
|
-
const returnUrl = `${location.origin}${location.pathname}${location.search}${location.hash}`;
|
|
33
|
-
const syncUrl = buildHubSyncUrl(issued.ticket, returnUrl);
|
|
34
|
-
window.location.assign(syncUrl);
|
|
35
|
-
return true;
|
|
36
|
-
}
|
|
37
|
-
/** Redeem a hub ticket on auth.oxy.so and persist credentials locally. */
|
|
38
|
-
export async function redeemHubTicketOnHub(oxy, store, ticket) {
|
|
39
|
-
const hubOrigin = buildIdpHubOrigin();
|
|
40
|
-
const creds = await oxy.redeemHubTicket(ticket, hubOrigin);
|
|
41
|
-
const prior = await store.load();
|
|
42
|
-
await store.save({
|
|
43
|
-
sessionId: prior?.sessionId ?? '',
|
|
44
|
-
userId: prior?.userId ?? '',
|
|
45
|
-
deviceId: creds.deviceId,
|
|
46
|
-
deviceSecret: creds.deviceSecret,
|
|
47
|
-
...(prior?.accessToken ? { accessToken: prior.accessToken } : {}),
|
|
48
|
-
...(prior?.expiresAt ? { expiresAt: prior.expiresAt } : {}),
|
|
49
|
-
});
|
|
50
|
-
return true;
|
|
51
|
-
}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Post-sign-in hub sync — plant device credentials on auth.oxy.so via a
|
|
3
|
-
* one-time server ticket (no secrets in URL fragments).
|
|
4
|
-
*/
|
|
5
|
-
import type { OxyServices } from '../OxyServices';
|
|
6
|
-
import type { AuthStateStore } from './authStateStore';
|
|
7
|
-
export interface SyncHubAfterSignInOptions {
|
|
8
|
-
/** Skip sync when false (OxyProvider hubSync prop). @default true */
|
|
9
|
-
enabled?: boolean;
|
|
10
|
-
}
|
|
11
|
-
/**
|
|
12
|
-
* After a successful sign-in on an official web app, mint a hub ticket and
|
|
13
|
-
* redirect to auth.oxy.so/sync so the IdP hub can redeem it and persist the
|
|
14
|
-
* shared device credential for silent OAuth restore on other origins.
|
|
15
|
-
*
|
|
16
|
-
* No-op on native, non-official origins, and when already on the IdP hub.
|
|
17
|
-
*/
|
|
18
|
-
export declare function syncHubAfterSignIn(oxy: Pick<OxyServices, 'issueHubTicket'>, opts?: SyncHubAfterSignInOptions): Promise<boolean>;
|
|
19
|
-
/** Redeem a hub ticket on auth.oxy.so and persist credentials locally. */
|
|
20
|
-
export declare function redeemHubTicketOnHub(oxy: Pick<OxyServices, 'redeemHubTicket'>, store: AuthStateStore, ticket: string): Promise<boolean>;
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import { syncHubAfterSignIn } from '../hubSync';
|
|
2
|
-
|
|
3
|
-
jest.mock('../../utils/officialOrigins', () => {
|
|
4
|
-
const actual = jest.requireActual('../../utils/officialOrigins');
|
|
5
|
-
return {
|
|
6
|
-
...actual,
|
|
7
|
-
isIdpHubOrigin: jest.fn(() => false),
|
|
8
|
-
isOfficialWebOrigin: jest.fn(() => true),
|
|
9
|
-
buildIdpHubOrigin: jest.fn(() => 'https://auth.oxy.so'),
|
|
10
|
-
};
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
describe('syncHubAfterSignIn', () => {
|
|
14
|
-
const originalLocation = globalThis.location;
|
|
15
|
-
const originalAssign = (globalThis as { location?: Location }).location?.assign;
|
|
16
|
-
|
|
17
|
-
afterEach(() => {
|
|
18
|
-
Object.defineProperty(globalThis, 'location', {
|
|
19
|
-
configurable: true,
|
|
20
|
-
value: originalLocation,
|
|
21
|
-
});
|
|
22
|
-
jest.restoreAllMocks();
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
it('includes the hash fragment in the hub-sync return URL', async () => {
|
|
26
|
-
const assign = jest.fn();
|
|
27
|
-
Object.defineProperty(globalThis, 'location', {
|
|
28
|
-
configurable: true,
|
|
29
|
-
value: {
|
|
30
|
-
origin: 'https://oxy.so',
|
|
31
|
-
pathname: '/pricing',
|
|
32
|
-
search: '?plan=pro',
|
|
33
|
-
hash: '#compare',
|
|
34
|
-
assign,
|
|
35
|
-
},
|
|
36
|
-
});
|
|
37
|
-
Object.defineProperty(globalThis, 'window', {
|
|
38
|
-
configurable: true,
|
|
39
|
-
value: { location: globalThis.location },
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
const issueHubTicket = jest.fn().mockResolvedValue({ ticket: 'tk-1' });
|
|
43
|
-
|
|
44
|
-
const redirected = await syncHubAfterSignIn({ issueHubTicket });
|
|
45
|
-
|
|
46
|
-
expect(redirected).toBe(true);
|
|
47
|
-
expect(issueHubTicket).toHaveBeenCalledWith('https://auth.oxy.so');
|
|
48
|
-
const syncUrl = new URL(String(assign.mock.calls[0]?.[0] ?? ''));
|
|
49
|
-
expect(syncUrl.searchParams.get('return')).toBe('https://oxy.so/pricing?plan=pro#compare');
|
|
50
|
-
});
|
|
51
|
-
});
|
package/src/session/hubSync.ts
DELETED
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Post-sign-in hub sync — plant device credentials on auth.oxy.so via a
|
|
3
|
-
* one-time server ticket (no secrets in URL fragments).
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type { OxyServices } from '../OxyServices';
|
|
7
|
-
import type { AuthStateStore } from './authStateStore';
|
|
8
|
-
import {
|
|
9
|
-
buildHubSyncUrl,
|
|
10
|
-
buildIdpHubOrigin,
|
|
11
|
-
isIdpHubOrigin,
|
|
12
|
-
isOfficialWebOrigin,
|
|
13
|
-
} from '../utils/officialOrigins';
|
|
14
|
-
|
|
15
|
-
export interface SyncHubAfterSignInOptions {
|
|
16
|
-
/** Skip sync when false (OxyProvider hubSync prop). @default true */
|
|
17
|
-
enabled?: boolean;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* After a successful sign-in on an official web app, mint a hub ticket and
|
|
22
|
-
* redirect to auth.oxy.so/sync so the IdP hub can redeem it and persist the
|
|
23
|
-
* shared device credential for silent OAuth restore on other origins.
|
|
24
|
-
*
|
|
25
|
-
* No-op on native, non-official origins, and when already on the IdP hub.
|
|
26
|
-
*/
|
|
27
|
-
export async function syncHubAfterSignIn(
|
|
28
|
-
oxy: Pick<OxyServices, 'issueHubTicket'>,
|
|
29
|
-
opts?: SyncHubAfterSignInOptions,
|
|
30
|
-
): Promise<boolean> {
|
|
31
|
-
if (opts?.enabled === false) {
|
|
32
|
-
return false;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
if (typeof globalThis === 'undefined') {
|
|
36
|
-
return false;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const location = (globalThis as { location?: Location }).location;
|
|
40
|
-
if (!location) {
|
|
41
|
-
return false;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
if (isIdpHubOrigin()) {
|
|
45
|
-
return false;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
if (!isOfficialWebOrigin(location.origin)) {
|
|
49
|
-
return false;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const hubOrigin = buildIdpHubOrigin();
|
|
53
|
-
const issued = await oxy.issueHubTicket(hubOrigin);
|
|
54
|
-
const returnUrl = `${location.origin}${location.pathname}${location.search}${location.hash}`;
|
|
55
|
-
const syncUrl = buildHubSyncUrl(issued.ticket, returnUrl);
|
|
56
|
-
|
|
57
|
-
window.location.assign(syncUrl);
|
|
58
|
-
return true;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/** Redeem a hub ticket on auth.oxy.so and persist credentials locally. */
|
|
62
|
-
export async function redeemHubTicketOnHub(
|
|
63
|
-
oxy: Pick<OxyServices, 'redeemHubTicket'>,
|
|
64
|
-
store: AuthStateStore,
|
|
65
|
-
ticket: string,
|
|
66
|
-
): Promise<boolean> {
|
|
67
|
-
const hubOrigin = buildIdpHubOrigin();
|
|
68
|
-
const creds = await oxy.redeemHubTicket(ticket, hubOrigin);
|
|
69
|
-
const prior = await store.load();
|
|
70
|
-
await store.save({
|
|
71
|
-
sessionId: prior?.sessionId ?? '',
|
|
72
|
-
userId: prior?.userId ?? '',
|
|
73
|
-
deviceId: creds.deviceId,
|
|
74
|
-
deviceSecret: creds.deviceSecret,
|
|
75
|
-
...(prior?.accessToken ? { accessToken: prior.accessToken } : {}),
|
|
76
|
-
...(prior?.expiresAt ? { expiresAt: prior.expiresAt } : {}),
|
|
77
|
-
});
|
|
78
|
-
return true;
|
|
79
|
-
}
|