@oxyhq/core 12.11.0 → 13.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 +66 -6
- package/dist/cjs/index.js +2 -12
- package/dist/cjs/mixins/OxyServices.deviceBoot.js +0 -31
- package/dist/cjs/mixins/OxyServices.privacy.js +6 -0
- package/dist/cjs/mixins/OxyServices.user.js +1 -0
- package/dist/cjs/server/index.js +1 -6
- package/dist/cjs/server/rateLimit.js +3 -0
- package/dist/cjs/session/accountDialogController.js +6 -8
- 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/HttpService.js +66 -6
- package/dist/esm/index.js +2 -3
- package/dist/esm/mixins/OxyServices.deviceBoot.js +1 -32
- package/dist/esm/mixins/OxyServices.privacy.js +6 -0
- package/dist/esm/mixins/OxyServices.user.js +1 -0
- package/dist/esm/server/index.js +1 -1
- package/dist/esm/server/rateLimit.js +3 -0
- package/dist/esm/session/accountDialogController.js +6 -8
- 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/HttpService.d.ts +18 -1
- package/dist/types/index.d.ts +3 -5
- 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 +2 -0
- package/dist/types/server/index.d.ts +1 -1
- package/dist/types/session/accountDialogController.d.ts +9 -15
- package/dist/types/utils/oauthPkce.d.ts +11 -7
- package/dist/types/utils/officialOrigins.d.ts +3 -13
- package/package.json +4 -4
- package/src/HttpService.ts +67 -6
- package/src/__tests__/inSessionRefresh.test.ts +67 -0
- package/src/index.ts +6 -14
- package/src/mixins/OxyServices.auth.ts +6 -13
- package/src/mixins/OxyServices.deviceBoot.ts +0 -47
- package/src/mixins/OxyServices.privacy.ts +6 -0
- package/src/mixins/OxyServices.user.ts +3 -0
- package/src/mixins/__tests__/commonsSignIn.test.ts +9 -2
- package/src/mixins/__tests__/privacyCacheInvalidation.test.ts +2 -0
- package/src/server/index.ts +1 -8
- package/src/server/rateLimit.ts +3 -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/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
package/dist/esm/HttpService.js
CHANGED
|
@@ -53,11 +53,30 @@ const CSRF_FETCH_MAX_ATTEMPTS = 2;
|
|
|
53
53
|
const CSRF_FETCH_RETRY_DELAY_MS = 500;
|
|
54
54
|
/**
|
|
55
55
|
* Cooldown (ms) applied after a failed access-token refresh before another
|
|
56
|
-
* refresh is attempted
|
|
56
|
+
* refresh is attempted while the CURRENT token is still valid (a proactive,
|
|
57
|
+
* near-expiry refresh). Prevents a refresh storm (and server hammering) when
|
|
57
58
|
* the auth refresh handler is failing — every in-flight request that
|
|
58
|
-
* hits a 401 would otherwise trigger its own refresh.
|
|
59
|
+
* hits a 401 would otherwise trigger its own refresh. A still-valid token can
|
|
60
|
+
* afford to wait this out; the request keeps carrying it in the meantime.
|
|
59
61
|
*/
|
|
60
62
|
const TOKEN_REFRESH_COOLDOWN_MS = 15000;
|
|
63
|
+
/**
|
|
64
|
+
* Cooldown (ms) applied after a failed refresh when the CURRENT access token is
|
|
65
|
+
* already past its `exp`. Much shorter than {@link TOKEN_REFRESH_COOLDOWN_MS}:
|
|
66
|
+
* an expired token is UNUSABLE, so the client must re-mint as soon as the mint
|
|
67
|
+
* endpoint is reachable again (e.g. a few seconds after an ECS rolling-deploy
|
|
68
|
+
* blip drains/restarts a task) instead of waiting out the full proactive
|
|
69
|
+
* cooldown while every request forwards or omits a stale bearer → server 401.
|
|
70
|
+
*
|
|
71
|
+
* Still NON-ZERO on purpose: it bounds the request-driven retry rate to at most
|
|
72
|
+
* one attempt per this interval so a PROLONGED outage cannot become a tight
|
|
73
|
+
* network storm. Combined with the process-wide single-flight below (concurrent
|
|
74
|
+
* requests coalesce to one in-flight mint) and the refresh handler's own
|
|
75
|
+
* terminal-state handling (a genuinely revoked session clears its device
|
|
76
|
+
* credential and stops issuing network mints), this recovers a transient blip
|
|
77
|
+
* ~15× faster without weakening the storm guard.
|
|
78
|
+
*/
|
|
79
|
+
const EXPIRED_TOKEN_REFRESH_COOLDOWN_MS = 1000;
|
|
61
80
|
/**
|
|
62
81
|
* Lead time (seconds) before access-token expiry at which a preflight refresh
|
|
63
82
|
* is triggered. A token within this window of `exp` is treated as effectively
|
|
@@ -130,7 +149,15 @@ class TokenStore {
|
|
|
130
149
|
export class HttpService {
|
|
131
150
|
constructor(config) {
|
|
132
151
|
this.tokenRefreshPromise = null;
|
|
133
|
-
|
|
152
|
+
/**
|
|
153
|
+
* Epoch ms of the last FAILED refresh (0 = none since the last success). The
|
|
154
|
+
* post-failure cooldown is measured from here; its length depends on whether
|
|
155
|
+
* the current token is still valid ({@link TOKEN_REFRESH_COOLDOWN_MS}) or
|
|
156
|
+
* already expired ({@link EXPIRED_TOKEN_REFRESH_COOLDOWN_MS}), so an expired
|
|
157
|
+
* token recovers promptly the instant it crosses `exp` — without storing a
|
|
158
|
+
* fixed deadline that could not shrink once the token expired mid-cooldown.
|
|
159
|
+
*/
|
|
160
|
+
this.lastRefreshFailureAt = 0;
|
|
134
161
|
this.authRefreshHandler = null;
|
|
135
162
|
this.accessTokenProvider = null;
|
|
136
163
|
this.deviceSecretMintInFlight = null;
|
|
@@ -820,26 +847,38 @@ export class HttpService {
|
|
|
820
847
|
if (!this.authRefreshHandler) {
|
|
821
848
|
return null;
|
|
822
849
|
}
|
|
823
|
-
|
|
850
|
+
// Post-failure cooldown. A genuinely EXPIRED current token uses a much
|
|
851
|
+
// shorter cooldown than a still-valid (proactive, near-expiry) one: an
|
|
852
|
+
// expired token is unusable, so re-mint as soon as the endpoint is reachable
|
|
853
|
+
// again rather than waiting out the full window while requests carry a stale
|
|
854
|
+
// bearer. Both cooldowns are measured from the last failure, so the moment a
|
|
855
|
+
// still-valid token crosses `exp` mid-cooldown the shorter window applies.
|
|
856
|
+
const cooldownMs = this.isAccessTokenExpired()
|
|
857
|
+
? EXPIRED_TOKEN_REFRESH_COOLDOWN_MS
|
|
858
|
+
: TOKEN_REFRESH_COOLDOWN_MS;
|
|
859
|
+
if (Date.now() - this.lastRefreshFailureAt < cooldownMs) {
|
|
824
860
|
return null;
|
|
825
861
|
}
|
|
826
862
|
if (!this.tokenRefreshPromise) {
|
|
827
863
|
this.tokenRefreshPromise = this.authRefreshHandler(reason)
|
|
828
864
|
.then((newToken) => {
|
|
829
865
|
if (!newToken) {
|
|
830
|
-
this.
|
|
866
|
+
this.lastRefreshFailureAt = Date.now();
|
|
831
867
|
return null;
|
|
832
868
|
}
|
|
833
869
|
if (this.tokenStore.getAccessToken() !== newToken) {
|
|
834
870
|
this.tokenStore.setTokens(newToken);
|
|
835
871
|
this.notifyTokenChange();
|
|
836
872
|
}
|
|
873
|
+
// A success clears the failure timestamp so the next refresh is never
|
|
874
|
+
// throttled by a stale cooldown.
|
|
875
|
+
this.lastRefreshFailureAt = 0;
|
|
837
876
|
this.logger.debug('Token refreshed via the auth refresh handler');
|
|
838
877
|
return newToken;
|
|
839
878
|
})
|
|
840
879
|
.catch((error) => {
|
|
841
880
|
this.logger.warn('Token refresh failed:', error);
|
|
842
|
-
this.
|
|
881
|
+
this.lastRefreshFailureAt = Date.now();
|
|
843
882
|
return null;
|
|
844
883
|
})
|
|
845
884
|
.finally(() => {
|
|
@@ -848,6 +887,27 @@ export class HttpService {
|
|
|
848
887
|
}
|
|
849
888
|
return this.tokenRefreshPromise;
|
|
850
889
|
}
|
|
890
|
+
/**
|
|
891
|
+
* Whether the CURRENT stored access token is already past its `exp`. Drives
|
|
892
|
+
* the shorter post-failure refresh cooldown ({@link EXPIRED_TOKEN_REFRESH_COOLDOWN_MS}):
|
|
893
|
+
* a still-valid (near-expiry) token can wait out the full cooldown, but an
|
|
894
|
+
* expired one must re-mint promptly. Returns `false` for an absent or
|
|
895
|
+
* opaque/no-`exp` token — no proof it is expired, so keep the conservative
|
|
896
|
+
* (longer) cooldown and avoid an unnecessary retry loop.
|
|
897
|
+
*/
|
|
898
|
+
isAccessTokenExpired() {
|
|
899
|
+
const token = this.tokenStore.getAccessToken();
|
|
900
|
+
if (!token) {
|
|
901
|
+
return false;
|
|
902
|
+
}
|
|
903
|
+
try {
|
|
904
|
+
const decoded = jwtDecode(token);
|
|
905
|
+
return typeof decoded.exp === 'number' && decoded.exp <= Math.floor(Date.now() / 1000);
|
|
906
|
+
}
|
|
907
|
+
catch {
|
|
908
|
+
return false;
|
|
909
|
+
}
|
|
910
|
+
}
|
|
851
911
|
/**
|
|
852
912
|
* PROCESS-WIDE single-flight for the rotating device-secret mint
|
|
853
913
|
* (`POST /session/device/token`).
|
package/dist/esm/index.js
CHANGED
|
@@ -148,9 +148,8 @@ export { runColdBoot } from './utils/coldBoot.js';
|
|
|
148
148
|
// OAuth 2.0 Authorization Code + PKCE helpers ("Sign in with Oxy" third party).
|
|
149
149
|
// Standard OAuth against auth.oxy.so/authorize — no FedCM/cookies/SSO bounce.
|
|
150
150
|
// ---------------------------------------------------------------------------
|
|
151
|
-
export { buildOAuthAuthorizeUrl, computeCodeChallenge, generateOAuthState, generatePkcePair, DEFAULT_OAUTH_SCOPE, OXY_AUTHORIZE_URL, OXY_OAUTH_STATE_STORAGE_KEY, OXY_OAUTH_CODE_VERIFIER_STORAGE_KEY, OXY_OAUTH_REDIRECT_URI_STORAGE_KEY,
|
|
152
|
-
export {
|
|
153
|
-
export { syncHubAfterSignIn, redeemHubTicketOnHub, } from './session/hubSync.js';
|
|
151
|
+
export { buildOAuthAuthorizeUrl, computeCodeChallenge, generateOAuthState, generatePkcePair, DEFAULT_OAUTH_SCOPE, OXY_AUTHORIZE_URL, OXY_OAUTH_STATE_STORAGE_KEY, OXY_OAUTH_CODE_VERIFIER_STORAGE_KEY, OXY_OAUTH_REDIRECT_URI_STORAGE_KEY, OXY_OAUTH_RETURN_PATH_STORAGE_KEY, normalizeOAuthRedirectUri, canonicalizeOAuthRedirectUri, persistOAuthHandshake, readOAuthHandshake, clearOAuthHandshake, persistOAuthReturnPath, consumeOAuthReturnPath, } from './utils/oauthPkce.js';
|
|
152
|
+
export { isLoopbackOrigin, isOfficialWebOrigin, isAllowedDeviceJoinOrigin, } from './utils/officialOrigins.js';
|
|
154
153
|
// ---------------------------------------------------------------------------
|
|
155
154
|
// Session sync (device-scoped multi-account session client)
|
|
156
155
|
// ---------------------------------------------------------------------------
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* the cold boot / re-mint handler own persistence and `setTokens`, so the same
|
|
14
14
|
* primitive can be reused from either without double-planting.
|
|
15
15
|
*/
|
|
16
|
-
import { deviceTokenMintResponseSchema,
|
|
16
|
+
import { deviceTokenMintResponseSchema, safeParseContract, } from '@oxyhq/contracts';
|
|
17
17
|
/**
|
|
18
18
|
* The server's `401 account_not_on_device` for a PINNED mint: the requested
|
|
19
19
|
* `accountId` is not (or is no longer) a live account of this device session.
|
|
@@ -105,36 +105,5 @@ export function OxyServicesDeviceBootMixin(Base) {
|
|
|
105
105
|
throw normalized;
|
|
106
106
|
}
|
|
107
107
|
}
|
|
108
|
-
/** Mint a one-time hub sync ticket (bearer required). */
|
|
109
|
-
async issueHubTicket(returnOrigin) {
|
|
110
|
-
try {
|
|
111
|
-
const res = await this.makeRequest('POST', '/session/device/hub-ticket', { returnOrigin }, { cache: false });
|
|
112
|
-
const parsed = safeParseContract(deviceHubTicketIssueResponseSchema, res);
|
|
113
|
-
if (!parsed) {
|
|
114
|
-
throw new Error('session/device/hub-ticket returned an unexpected response shape');
|
|
115
|
-
}
|
|
116
|
-
return parsed;
|
|
117
|
-
}
|
|
118
|
-
catch (error) {
|
|
119
|
-
throw this.handleError(error);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
/** Redeem a hub sync ticket for a fresh device secret (public). */
|
|
123
|
-
async redeemHubTicket(ticket, returnOrigin) {
|
|
124
|
-
try {
|
|
125
|
-
const res = await this.makeRequest('POST', '/session/device/redeem-ticket', { ticket, returnOrigin },
|
|
126
|
-
// Public device-hub sync mint (bearer-less). Same control-plane class as
|
|
127
|
-
// the device-secret mint — bypassQueue so it never waits for a slot.
|
|
128
|
-
{ cache: false, skipAuth: true, bypassQueue: true });
|
|
129
|
-
const parsed = safeParseContract(deviceHubTicketRedeemResponseSchema, res);
|
|
130
|
-
if (!parsed) {
|
|
131
|
-
throw new Error('session/device/redeem-ticket returned an unexpected response shape');
|
|
132
|
-
}
|
|
133
|
-
return parsed;
|
|
134
|
-
}
|
|
135
|
-
catch (error) {
|
|
136
|
-
throw this.handleError(error);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
108
|
};
|
|
140
109
|
}
|
|
@@ -149,6 +149,9 @@ export function OxyServicesPrivacyMixin(Base) {
|
|
|
149
149
|
cache: false,
|
|
150
150
|
});
|
|
151
151
|
this.clearCacheEntry('GET:/privacy/restricted');
|
|
152
|
+
// The restriction changed the viewer's graph (`restrictedIds`) — bust the
|
|
153
|
+
// cached consolidated `GET /users/me/graph` so the next read reflects it.
|
|
154
|
+
this.clearCacheEntry('GET:/users/me/graph');
|
|
152
155
|
return result;
|
|
153
156
|
}
|
|
154
157
|
catch (error) {
|
|
@@ -172,6 +175,9 @@ export function OxyServicesPrivacyMixin(Base) {
|
|
|
172
175
|
cache: false,
|
|
173
176
|
});
|
|
174
177
|
this.clearCacheEntry('GET:/privacy/restricted');
|
|
178
|
+
// Symmetric to restrictUser: the unrestrict changed the viewer's
|
|
179
|
+
// `restrictedIds`, so bust the consolidated `GET /users/me/graph` cache.
|
|
180
|
+
this.clearCacheEntry('GET:/users/me/graph');
|
|
175
181
|
return result;
|
|
176
182
|
}
|
|
177
183
|
catch (error) {
|
package/dist/esm/server/index.js
CHANGED
|
@@ -27,4 +27,4 @@ export { verifySecret } from './verifySecret.js';
|
|
|
27
27
|
// Pure host handling (no browser deps), so it is safe on the server subpath and
|
|
28
28
|
// lets `@oxyhq/api` derive `auth.<apex>` without duplicating PSL logic.
|
|
29
29
|
export { registrableApex } from '../utils/registrableApex.js';
|
|
30
|
-
export {
|
|
30
|
+
export { isOfficialWebOrigin } from '../utils/officialOrigins.js';
|
|
@@ -168,6 +168,9 @@ export function createOxyRateLimit(oxy, options = {}) {
|
|
|
168
168
|
standardHeaders: true,
|
|
169
169
|
legacyHeaders: false,
|
|
170
170
|
skip,
|
|
171
|
+
// hashAnonymousIp already buckets IPv6 to /56 before HMAC; disable the v8
|
|
172
|
+
// static source scan that false-positives on req.ip (ERR_ERL_KEY_GEN_IPV6).
|
|
173
|
+
validate: { keyGeneratorIpFallback: false },
|
|
171
174
|
});
|
|
172
175
|
return (req, res, next) => {
|
|
173
176
|
// Skipped paths bypass BOTH session resolution and limiting — cheap and
|
|
@@ -477,9 +477,9 @@ export class AccountDialogController {
|
|
|
477
477
|
user: result.user,
|
|
478
478
|
accessToken: result.accessToken,
|
|
479
479
|
}, result.user,
|
|
480
|
-
// A switch is IN-PLACE:
|
|
481
|
-
//
|
|
482
|
-
//
|
|
480
|
+
// A switch is IN-PLACE: use the switch commit funnel (not sign-in).
|
|
481
|
+
// Cross-tab/app propagation rides the server's `session_state` socket
|
|
482
|
+
// broadcast, not a navigation.
|
|
483
483
|
{ fromSwitch: true });
|
|
484
484
|
}
|
|
485
485
|
// Re-project + refetch immediately; the subscription also fires.
|
|
@@ -982,11 +982,9 @@ export class AccountDialogController {
|
|
|
982
982
|
* consumer's commit funnel (durable persist + hydration); falls back to
|
|
983
983
|
* `SessionClient.registerAndActivate` (registration + activation only).
|
|
984
984
|
*
|
|
985
|
-
* A SWITCH (`opts.fromSwitch`) uses the IN-PLACE `commitSwitchedSession` funnel
|
|
986
|
-
*
|
|
987
|
-
*
|
|
988
|
-
* switch funnel is not wired it falls back to the sign-in funnel, then to
|
|
989
|
-
* `registerAndActivate`.
|
|
985
|
+
* A SWITCH (`opts.fromSwitch`) uses the IN-PLACE `commitSwitchedSession` funnel;
|
|
986
|
+
* a SIGN-IN uses `commitSession`. When the switch funnel is not wired it falls
|
|
987
|
+
* back to the sign-in funnel, then to `registerAndActivate`.
|
|
990
988
|
*/
|
|
991
989
|
async commitAuthorizedSession(session, user, opts) {
|
|
992
990
|
const commit = opts?.fromSwitch
|
|
@@ -145,10 +145,6 @@ export const OXY_OAUTH_STATE_STORAGE_KEY = 'oxy_oauth_state';
|
|
|
145
145
|
export const OXY_OAUTH_CODE_VERIFIER_STORAGE_KEY = 'oxy_oauth_code_verifier';
|
|
146
146
|
/** `sessionStorage` key — the exact `redirect_uri` sent on the authorize request. */
|
|
147
147
|
export const OXY_OAUTH_REDIRECT_URI_STORAGE_KEY = 'oxy.oauth_redirect_uri';
|
|
148
|
-
/** `sessionStorage` key — at most one silent OAuth attempt per navigation. */
|
|
149
|
-
export const OXY_SILENT_OAUTH_ATTEMPTED_KEY = 'oxy.silent_oauth_attempted';
|
|
150
|
-
/** `sessionStorage` key — blocks further cross-origin auto-restore in this tab. */
|
|
151
|
-
export const OXY_CROSS_ORIGIN_RESTORE_ATTEMPTED_KEY = 'oxy.cross_origin_restore_attempted';
|
|
152
148
|
/**
|
|
153
149
|
* `sessionStorage` key for the in-app path to return to after an authorize
|
|
154
150
|
* 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
|
import { CENTRAL_IDP_APEX } from './authWebUrl.js';
|
|
6
6
|
import { registrableApex } from './registrableApex.js';
|
|
@@ -17,30 +17,9 @@ const OFFICIAL_APEXES = new Set([
|
|
|
17
17
|
'moovo.now',
|
|
18
18
|
'mercaria.co',
|
|
19
19
|
]);
|
|
20
|
-
export function buildIdpHubOrigin() {
|
|
21
|
-
return `https://auth.${CENTRAL_IDP_APEX}`;
|
|
22
|
-
}
|
|
23
|
-
/** Whether the current web origin is the central IdP hub (`auth.oxy.so`). */
|
|
24
|
-
export function isIdpHubOrigin() {
|
|
25
|
-
if (typeof globalThis === 'undefined') {
|
|
26
|
-
return false;
|
|
27
|
-
}
|
|
28
|
-
const location = globalThis.location;
|
|
29
|
-
if (!location) {
|
|
30
|
-
return false;
|
|
31
|
-
}
|
|
32
|
-
try {
|
|
33
|
-
const { hostname } = new URL(location.href);
|
|
34
|
-
return hostname === `auth.${CENTRAL_IDP_APEX}`;
|
|
35
|
-
}
|
|
36
|
-
catch {
|
|
37
|
-
return false;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
20
|
/**
|
|
41
21
|
* Whether an origin is a loopback / local-dev origin (`localhost`, `127.0.0.1`,
|
|
42
|
-
* or `[::1]` on any port, http or https).
|
|
43
|
-
* hosted IdP for cross-origin session restore.
|
|
22
|
+
* or `[::1]` on any port, http or https).
|
|
44
23
|
*/
|
|
45
24
|
export function isLoopbackOrigin(origin) {
|
|
46
25
|
try {
|
|
@@ -76,49 +55,5 @@ export function isOfficialWebOrigin(origin) {
|
|
|
76
55
|
return false;
|
|
77
56
|
}
|
|
78
57
|
}
|
|
79
|
-
/** Normalize and validate a return URL against official origins. Returns origin only. */
|
|
80
|
-
export function normalizeOfficialReturnOrigin(raw) {
|
|
81
|
-
try {
|
|
82
|
-
const parsed = new URL(raw);
|
|
83
|
-
if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
|
|
84
|
-
return null;
|
|
85
|
-
}
|
|
86
|
-
if (!isOfficialWebOrigin(parsed.origin)) {
|
|
87
|
-
return null;
|
|
88
|
-
}
|
|
89
|
-
return parsed.origin;
|
|
90
|
-
}
|
|
91
|
-
catch {
|
|
92
|
-
return null;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
/** Validate a hub-sync return URL; returns the full normalized URL string. */
|
|
96
|
-
export function parseHubSyncReturnUrl(raw) {
|
|
97
|
-
if (!raw) {
|
|
98
|
-
return null;
|
|
99
|
-
}
|
|
100
|
-
try {
|
|
101
|
-
const parsed = new URL(raw);
|
|
102
|
-
if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
|
|
103
|
-
return null;
|
|
104
|
-
}
|
|
105
|
-
if (!isOfficialWebOrigin(parsed.origin)) {
|
|
106
|
-
return null;
|
|
107
|
-
}
|
|
108
|
-
return parsed.toString();
|
|
109
|
-
}
|
|
110
|
-
catch {
|
|
111
|
-
return null;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
/** Build auth.oxy.so/sync redirect URL with a one-time hub ticket. */
|
|
115
|
-
export function buildHubSyncUrl(ticket, returnUrl) {
|
|
116
|
-
const url = new URL('/sync', buildIdpHubOrigin());
|
|
117
|
-
url.searchParams.set('ticket', ticket);
|
|
118
|
-
if (returnUrl) {
|
|
119
|
-
url.searchParams.set('return', returnUrl);
|
|
120
|
-
}
|
|
121
|
-
return url.toString();
|
|
122
|
-
}
|
|
123
58
|
/** @deprecated Use {@link isOfficialWebOrigin}. */
|
|
124
59
|
export const isAllowedDeviceJoinOrigin = isOfficialWebOrigin;
|