@oxyhq/core 3.8.1 → 3.9.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.
Files changed (55) hide show
  1. package/README.md +10 -0
  2. package/dist/cjs/.tsbuildinfo +1 -1
  3. package/dist/cjs/HttpService.js +18 -4
  4. package/dist/cjs/OxyServices.base.js +15 -1
  5. package/dist/cjs/mixins/OxyServices.applications.js +69 -6
  6. package/dist/cjs/mixins/OxyServices.assets.js +16 -3
  7. package/dist/cjs/mixins/OxyServices.features.js +47 -10
  8. package/dist/cjs/mixins/OxyServices.managedAccounts.js +29 -3
  9. package/dist/cjs/mixins/OxyServices.privacy.js +34 -8
  10. package/dist/cjs/mixins/OxyServices.topics.js +5 -1
  11. package/dist/cjs/mixins/OxyServices.user.js +11 -2
  12. package/dist/cjs/mixins/OxyServices.workspaces.js +38 -3
  13. package/dist/cjs/utils/cache.js +9 -2
  14. package/dist/esm/.tsbuildinfo +1 -1
  15. package/dist/esm/HttpService.js +18 -4
  16. package/dist/esm/OxyServices.base.js +15 -1
  17. package/dist/esm/mixins/OxyServices.applications.js +69 -6
  18. package/dist/esm/mixins/OxyServices.assets.js +16 -3
  19. package/dist/esm/mixins/OxyServices.features.js +47 -10
  20. package/dist/esm/mixins/OxyServices.managedAccounts.js +29 -3
  21. package/dist/esm/mixins/OxyServices.privacy.js +34 -8
  22. package/dist/esm/mixins/OxyServices.topics.js +5 -1
  23. package/dist/esm/mixins/OxyServices.user.js +11 -2
  24. package/dist/esm/mixins/OxyServices.workspaces.js +38 -3
  25. package/dist/esm/utils/cache.js +9 -2
  26. package/dist/types/.tsbuildinfo +1 -1
  27. package/dist/types/HttpService.d.ts +9 -0
  28. package/dist/types/OxyServices.base.d.ts +12 -0
  29. package/dist/types/mixins/OxyServices.applications.d.ts +26 -0
  30. package/dist/types/mixins/OxyServices.features.d.ts +27 -6
  31. package/dist/types/mixins/OxyServices.managedAccounts.d.ts +16 -1
  32. package/dist/types/mixins/OxyServices.privacy.d.ts +22 -4
  33. package/dist/types/mixins/OxyServices.user.d.ts +8 -1
  34. package/dist/types/mixins/OxyServices.workspaces.d.ts +12 -0
  35. package/dist/types/models/interfaces.d.ts +12 -0
  36. package/dist/types/utils/cache.d.ts +4 -1
  37. package/package.json +1 -4
  38. package/src/HttpService.ts +28 -4
  39. package/src/OxyServices.base.ts +15 -1
  40. package/src/__tests__/httpServiceCache.test.ts +68 -0
  41. package/src/__tests__/linkedClient.test.ts +61 -0
  42. package/src/mixins/OxyServices.applications.ts +71 -6
  43. package/src/mixins/OxyServices.assets.ts +16 -3
  44. package/src/mixins/OxyServices.features.ts +47 -10
  45. package/src/mixins/OxyServices.managedAccounts.ts +29 -3
  46. package/src/mixins/OxyServices.privacy.ts +34 -8
  47. package/src/mixins/OxyServices.topics.ts +5 -1
  48. package/src/mixins/OxyServices.user.ts +11 -2
  49. package/src/mixins/OxyServices.workspaces.ts +39 -3
  50. package/src/mixins/__tests__/privacyCacheInvalidation.test.ts +147 -0
  51. package/src/models/interfaces.ts +13 -1
  52. package/src/utils/cache.ts +9 -2
  53. package/dist/cjs/mixins/OxyServices.popup.js +0 -263
  54. package/dist/esm/mixins/OxyServices.popup.js +0 -261
  55. package/dist/types/mixins/OxyServices.popup.d.ts +0 -170
@@ -1,261 +0,0 @@
1
- import { OxyAuthenticationError } from "../OxyServices.errors.js";
2
- import { createDebugLogger } from "../shared/utils/debugUtils.js";
3
- const debug = createDebugLogger("PopupAuth");
4
- /**
5
- * Cross-domain browser auth helpers.
6
- *
7
- * Popup sign-in is intentionally fail-closed in the clean session model because
8
- * the historical implementation required bearer-token callback URLs. FedCM,
9
- * redirect SSO, and silent iframe SSO are the supported browser paths.
10
- */
11
- export function OxyServicesPopupAuthMixin(Base) {
12
- var _a;
13
- return _a = class extends Base {
14
- constructor(...args) {
15
- super(...args);
16
- }
17
- /** Resolve auth URL from config or static default (method, not getter — getters break in TS mixins) */
18
- resolveAuthUrl() {
19
- return (this.config.authWebUrl || this.constructor.DEFAULT_AUTH_URL);
20
- }
21
- /**
22
- * Removed popup sign-in. Closes a caller-supplied popup handle and throws.
23
- */
24
- async signInWithPopup(options = {}) {
25
- if (typeof window === "undefined") {
26
- throw new OxyAuthenticationError("Popup authentication requires browser environment");
27
- }
28
- if (options.popup && !options.popup.closed) {
29
- options.popup.close();
30
- }
31
- throw new OxyAuthenticationError("Popup authentication has been removed because it required access-token callback URLs. Use FedCM or redirect authentication.");
32
- }
33
- /**
34
- * Removed popup signup. Closes a caller-supplied popup handle and throws.
35
- */
36
- async signUpWithPopup(options = {}) {
37
- return this.signInWithPopup({ ...options, mode: "signup" });
38
- }
39
- /**
40
- * Silent sign-in using hidden iframe
41
- *
42
- * Attempts to automatically re-authenticate the user without any UI.
43
- * This is what enables seamless SSO across all Oxy domains.
44
- *
45
- * How it works:
46
- * 1. Creates hidden iframe pointing to auth.oxy.so/silent-auth
47
- * 2. If user has valid session at auth.oxy.so, it exchanges an opaque SSO code
48
- * 3. If not, iframe responds with null (no error thrown)
49
- *
50
- * This should be called on app startup to check for existing sessions.
51
- *
52
- * @param options - Silent auth options
53
- * @returns Session if user is signed in, null otherwise
54
- *
55
- * @example
56
- * ```typescript
57
- * useEffect(() => {
58
- * const checkAuth = async () => {
59
- * const session = await oxyServices.silentSignIn();
60
- * if (session) {
61
- * setUser(session.user);
62
- * }
63
- * };
64
- * checkAuth();
65
- * }, []);
66
- * ```
67
- */
68
- async silentSignIn(options = {}) {
69
- if (typeof window === "undefined") {
70
- return null;
71
- }
72
- const timeout = options.timeout || this.constructor.SILENT_TIMEOUT;
73
- const nonce = this.generateNonce();
74
- const clientId = window.location.origin;
75
- // Resolve the IdP origin for the iframe. An explicit per-apex override (the
76
- // durable cross-domain reload path — see `SilentAuthOptions.authWebUrlOverride`)
77
- // wins over the instance's configured central auth URL. The SAME origin is
78
- // handed to `waitForIframeAuth` so the postMessage origin check matches the
79
- // exact host the iframe was loaded from.
80
- const authOrigin = options.authWebUrlOverride && options.authWebUrlOverride.length > 0
81
- ? options.authWebUrlOverride
82
- : this.resolveAuthUrl();
83
- const iframe = document.createElement("iframe");
84
- iframe.style.display = "none";
85
- iframe.style.position = "absolute";
86
- iframe.style.width = "0";
87
- iframe.style.height = "0";
88
- iframe.style.border = "none";
89
- const silentUrl = `${authOrigin}/auth/silent?` +
90
- `client_id=${encodeURIComponent(clientId)}&` +
91
- `nonce=${nonce}`;
92
- iframe.src = silentUrl;
93
- document.body.appendChild(iframe);
94
- try {
95
- const session = await this.waitForIframeAuth(iframe, timeout, authOrigin);
96
- // Bail early on incomplete responses. The iframe contract requires
97
- // both an access token and a session id; anything less is unusable.
98
- // Returning `null` here (without installing the token) prevents a
99
- // stale credential from being committed to HttpService when the
100
- // user is actually signed out — that pattern caused a `getCurrentUser`
101
- // -> 401 -> token-clear loop in consumer apps because callers gated
102
- // on `session?.user` and never installed the user via
103
- // `handleAuthSuccess`, while HttpService quietly held the token.
104
- const accessToken = session
105
- ? session.accessToken
106
- : undefined;
107
- if (!session || !accessToken || !session.sessionId) {
108
- return null;
109
- }
110
- // Snapshot the previous token so we can roll back if the user
111
- // lookup below fails — this avoids leaving a half-committed session
112
- // (token installed, user missing) which would let the next
113
- // authenticated request 401 with no way to recover.
114
- const previousAccessToken = this.httpService.getAccessToken();
115
- this.httpService.setTokens(accessToken);
116
- // The iframe typically returns `{ sessionId, accessToken }` without
117
- // user data. Fetch the user explicitly so callers receive a
118
- // fully-formed session and never need a second `/users/me` round
119
- // trip. If this fails the session is unusable — revert the token
120
- // and return null so the caller treats this exactly like a
121
- // missing-session response.
122
- if (!session.user) {
123
- try {
124
- const userData = await this.makeRequest("GET", `/session/user/${session.sessionId}`, undefined, { cache: false, retry: false });
125
- if (!userData) {
126
- throw new Error("Empty user response");
127
- }
128
- session.user = userData;
129
- }
130
- catch (userError) {
131
- debug.warn("silentSignIn: failed to fetch user data, rolling back token", userError);
132
- if (previousAccessToken) {
133
- this.httpService.setTokens(previousAccessToken);
134
- }
135
- else {
136
- this.httpService.clearTokens();
137
- }
138
- return null;
139
- }
140
- }
141
- return session;
142
- }
143
- catch (error) {
144
- return null;
145
- }
146
- finally {
147
- document.body.removeChild(iframe);
148
- }
149
- }
150
- /**
151
- * Open a blank, centered popup window SYNCHRONOUSLY.
152
- *
153
- * Kept only so legacy callers can pass a handle to the removed popup method,
154
- * which closes it before throwing. New auth code should use FedCM or redirect.
155
- */
156
- openBlankPopup(width, height) {
157
- if (typeof window === "undefined") {
158
- return null;
159
- }
160
- const ctor = this.constructor;
161
- const w = width ?? ctor.POPUP_WIDTH;
162
- const h = height ?? ctor.POPUP_HEIGHT;
163
- return this.openCenteredPopup("about:blank", "Oxy Sign In", w, h);
164
- }
165
- /**
166
- * Open a centered popup window
167
- *
168
- * @private
169
- */
170
- openCenteredPopup(url, title, width, height) {
171
- const left = window.screenX + (window.outerWidth - width) / 2;
172
- const top = window.screenY + (window.outerHeight - height) / 2;
173
- const features = [
174
- `width=${width}`,
175
- `height=${height}`,
176
- `left=${left}`,
177
- `top=${top}`,
178
- "toolbar=no",
179
- "menubar=no",
180
- "scrollbars=yes",
181
- "resizable=yes",
182
- "status=no",
183
- "location=no",
184
- ].join(",");
185
- return window.open(url, title, features);
186
- }
187
- /**
188
- * Wait for authentication response from iframe
189
- *
190
- * @private
191
- */
192
- async waitForIframeAuth(iframe, timeout, expectedOrigin) {
193
- return new Promise((resolve) => {
194
- const timeoutId = setTimeout(() => {
195
- cleanup();
196
- resolve(null); // Silent failure - don't throw
197
- }, timeout);
198
- const messageHandler = (event) => {
199
- // Verify origin against the EXACT host the iframe was loaded from
200
- // (`expectedOrigin`). For the per-apex durable-restore path this is
201
- // `auth.<rp-apex>`, not the instance's central `resolveAuthUrl()` — so
202
- // we must honour the caller-supplied origin, never re-derive it here.
203
- if (event.origin !== expectedOrigin) {
204
- return;
205
- }
206
- const { type, session } = event.data;
207
- if (type !== "oxy_silent_auth") {
208
- return;
209
- }
210
- cleanup();
211
- resolve(session || null);
212
- };
213
- // Fail-fast on a load failure. When the per-apex `/auth/silent` host is
214
- // unreachable, blocked by CSP `frame-ancestors`/`X-Frame-Options`, or the
215
- // network drops, the iframe never posts a message — without this handler
216
- // the silent restore would block for the FULL `timeout` (dead latency in
217
- // the cold-boot critical path). `onerror`/`onabort` fire on a failed load,
218
- // so resolve `null` immediately and let the next cold-boot step run. The
219
- // success path posts a message and is handled above; these only catch the
220
- // no-message failure modes.
221
- const failFast = () => {
222
- cleanup();
223
- resolve(null);
224
- };
225
- iframe.onerror = failFast;
226
- iframe.onabort = failFast;
227
- const cleanup = () => {
228
- clearTimeout(timeoutId);
229
- iframe.onerror = null;
230
- iframe.onabort = null;
231
- window.removeEventListener("message", messageHandler);
232
- };
233
- window.addEventListener("message", messageHandler);
234
- });
235
- }
236
- /**
237
- * Generate nonce for replay attack prevention
238
- *
239
- * @private
240
- */
241
- generateNonce() {
242
- if (typeof crypto !== "undefined" && crypto.randomUUID) {
243
- return crypto.randomUUID();
244
- }
245
- if (typeof crypto !== "undefined" && crypto.getRandomValues) {
246
- const bytes = new Uint8Array(16);
247
- crypto.getRandomValues(bytes);
248
- return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
249
- }
250
- throw new Error("No secure random source available for nonce generation");
251
- }
252
- },
253
- _a.DEFAULT_AUTH_URL = "https://auth.oxy.so",
254
- _a.POPUP_WIDTH = 500,
255
- _a.POPUP_HEIGHT = 700,
256
- _a.SILENT_TIMEOUT = 5000 // 5 seconds
257
- ,
258
- _a;
259
- }
260
- // Export the mixin function as both named and default
261
- export { OxyServicesPopupAuthMixin as PopupAuthMixin };
@@ -1,170 +0,0 @@
1
- import type { OxyServicesBase } from "../OxyServices.base";
2
- import type { SessionLoginResponse } from "../models/session";
3
- export interface PopupAuthOptions {
4
- /** Legacy option. Popup auth is removed; dimensions are ignored. */
5
- width?: number;
6
- /** Legacy option. Popup auth is removed; dimensions are ignored. */
7
- height?: number;
8
- /** Legacy option. Popup auth is removed; timeout is ignored. */
9
- timeout?: number;
10
- /** Legacy option. Signup also fails closed. */
11
- mode?: "login" | "signup";
12
- /**
13
- * A legacy popup window handle. `signInWithPopup` closes it and throws
14
- * because popup auth has been removed.
15
- */
16
- popup?: Window | null;
17
- }
18
- export interface SilentAuthOptions {
19
- timeout?: number;
20
- /**
21
- * Override the auth-web (IdP) origin used for the silent iframe, instead of
22
- * the instance's configured `resolveAuthUrl()`.
23
- *
24
- * Why this exists: an instance configured with the CENTRAL IdP
25
- * (`authWebUrl=https://auth.oxy.so`, for the opaque-code `/sso` bounce and
26
- * FedCM) cannot read the DURABLE per-apex `fedcm_session` cookie via the
27
- * central host — that cookie is first-party only on `auth.<rp-apex>` (e.g.
28
- * `auth.mention.earth`). The cross-domain reload-restore path must point the
29
- * `/auth/silent` iframe at the PER-APEX host so the cookie is same-site to
30
- * the RP page (first-party under Safari ITP / Firefox TCP) and the restore
31
- * is NOT a top-level navigation (no flash, works in a backgrounded tab).
32
- *
33
- * When provided this value is used BOTH for the iframe `src` AND for the
34
- * `postMessage` origin validation in {@link waitForIframeAuth}, so the
35
- * security check still matches the exact origin the iframe was loaded from.
36
- * Must be an absolute origin (`https://auth.<apex>`); ignored if empty.
37
- */
38
- authWebUrlOverride?: string;
39
- }
40
- /**
41
- * Cross-domain browser auth helpers.
42
- *
43
- * Popup sign-in is intentionally fail-closed in the clean session model because
44
- * the historical implementation required bearer-token callback URLs. FedCM,
45
- * redirect SSO, and silent iframe SSO are the supported browser paths.
46
- */
47
- export declare function OxyServicesPopupAuthMixin<T extends typeof OxyServicesBase>(Base: T): {
48
- new (...args: any[]): {
49
- /** Resolve auth URL from config or static default (method, not getter — getters break in TS mixins) */
50
- resolveAuthUrl(): string;
51
- /**
52
- * Removed popup sign-in. Closes a caller-supplied popup handle and throws.
53
- */
54
- signInWithPopup(options?: PopupAuthOptions): Promise<SessionLoginResponse>;
55
- /**
56
- * Removed popup signup. Closes a caller-supplied popup handle and throws.
57
- */
58
- signUpWithPopup(options?: PopupAuthOptions): Promise<SessionLoginResponse>;
59
- /**
60
- * Silent sign-in using hidden iframe
61
- *
62
- * Attempts to automatically re-authenticate the user without any UI.
63
- * This is what enables seamless SSO across all Oxy domains.
64
- *
65
- * How it works:
66
- * 1. Creates hidden iframe pointing to auth.oxy.so/silent-auth
67
- * 2. If user has valid session at auth.oxy.so, it exchanges an opaque SSO code
68
- * 3. If not, iframe responds with null (no error thrown)
69
- *
70
- * This should be called on app startup to check for existing sessions.
71
- *
72
- * @param options - Silent auth options
73
- * @returns Session if user is signed in, null otherwise
74
- *
75
- * @example
76
- * ```typescript
77
- * useEffect(() => {
78
- * const checkAuth = async () => {
79
- * const session = await oxyServices.silentSignIn();
80
- * if (session) {
81
- * setUser(session.user);
82
- * }
83
- * };
84
- * checkAuth();
85
- * }, []);
86
- * ```
87
- */
88
- silentSignIn(options?: SilentAuthOptions): Promise<SessionLoginResponse | null>;
89
- /**
90
- * Open a blank, centered popup window SYNCHRONOUSLY.
91
- *
92
- * Kept only so legacy callers can pass a handle to the removed popup method,
93
- * which closes it before throwing. New auth code should use FedCM or redirect.
94
- */
95
- openBlankPopup(width?: number, height?: number): Window | null;
96
- /**
97
- * Open a centered popup window
98
- *
99
- * @private
100
- */
101
- openCenteredPopup(url: string, title: string, width: number, height: number): Window | null;
102
- /**
103
- * Wait for authentication response from iframe
104
- *
105
- * @private
106
- */
107
- waitForIframeAuth(iframe: HTMLIFrameElement, timeout: number, expectedOrigin: string): Promise<SessionLoginResponse | null>;
108
- /**
109
- * Generate nonce for replay attack prevention
110
- *
111
- * @private
112
- */
113
- generateNonce(): string;
114
- httpService: import("../HttpService").HttpService;
115
- cloudURL: string;
116
- config: import("../OxyServices.base").OxyConfig;
117
- __resetTokensForTests(): void;
118
- makeRequest<T_1>(method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE", url: string, data?: any, options?: import("../HttpService").RequestOptions): Promise<T_1>;
119
- getBaseURL(): string;
120
- getSessionBaseUrl(): string;
121
- getClient(): import("../HttpService").HttpService;
122
- getMetrics(): {
123
- totalRequests: number;
124
- successfulRequests: number;
125
- failedRequests: number;
126
- cacheHits: number;
127
- cacheMisses: number;
128
- averageResponseTime: number;
129
- };
130
- clearCache(): void;
131
- clearCacheEntry(key: string): void;
132
- clearCacheByPrefix(prefix: string): number;
133
- getCacheStats(): {
134
- size: number;
135
- hits: number;
136
- misses: number;
137
- hitRate: number;
138
- };
139
- getCloudURL(): string;
140
- setTokens(accessToken: string, refreshToken?: string): void;
141
- clearTokens(): void;
142
- onTokensChanged(listener: (accessToken: string | null) => void): () => void;
143
- _cachedUserId: string | null | undefined;
144
- _cachedAccessToken: string | null;
145
- getCurrentUserId(): string | null;
146
- hasValidToken(): boolean;
147
- getAccessToken(): string | null;
148
- setActingAs(userId: string | null): void;
149
- getActingAs(): string | null;
150
- waitForAuth(timeoutMs?: number): Promise<boolean>;
151
- withAuthRetry<T_1>(operation: () => Promise<T_1>, operationName: string, options?: {
152
- maxRetries?: number;
153
- retryDelay?: number;
154
- authTimeoutMs?: number;
155
- }): Promise<T_1>;
156
- validate(): Promise<boolean>;
157
- handleError(error: unknown): Error;
158
- healthCheck(): Promise<{
159
- status: string;
160
- users?: number;
161
- timestamp?: string;
162
- [key: string]: any;
163
- }>;
164
- };
165
- readonly DEFAULT_AUTH_URL: "https://auth.oxy.so";
166
- readonly POPUP_WIDTH: 500;
167
- readonly POPUP_HEIGHT: 700;
168
- readonly SILENT_TIMEOUT: 5000;
169
- } & T;
170
- export { OxyServicesPopupAuthMixin as PopupAuthMixin };