@lanonasis/oauth-client 1.2.8 → 2.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/README.md +19 -0
- package/dist/constants-BOZ6jJU4.d.cts +110 -0
- package/dist/constants-BOZ6jJU4.d.ts +110 -0
- package/dist/index.cjs +18 -2
- package/dist/index.d.cts +13 -1
- package/dist/index.d.ts +13 -1
- package/dist/index.mjs +18 -2
- package/dist/react/index.cjs +261 -0
- package/dist/react/index.d.cts +95 -0
- package/dist/react/index.d.ts +95 -0
- package/dist/react/index.mjs +238 -0
- package/dist/server/index.cjs +169 -0
- package/dist/server/index.d.cts +184 -0
- package/dist/server/index.d.ts +184 -0
- package/dist/server/index.mjs +146 -0
- package/package.json +19 -3
package/README.md
CHANGED
|
@@ -126,5 +126,24 @@ const hashed = await apiKeys.getApiKey(); // returns sha256 hex digest
|
|
|
126
126
|
- `README.md`
|
|
127
127
|
- `LICENSE`
|
|
128
128
|
|
|
129
|
+
## Auth-Gateway Compatibility
|
|
130
|
+
|
|
131
|
+
This SDK implements client-side equivalents of the auth-gateway's authentication methods:
|
|
132
|
+
|
|
133
|
+
| Auth-Gateway Endpoint | SDK Class | Use Case |
|
|
134
|
+
|-----------------------|-----------|----------|
|
|
135
|
+
| `POST /oauth/device` | `TerminalOAuthFlow` | CLI/terminal apps |
|
|
136
|
+
| `GET /oauth/authorize` | `DesktopOAuthFlow` | Desktop apps (Electron) |
|
|
137
|
+
| `POST /v1/auth/otp/*` | `MagicLinkFlow` | Mobile/passwordless |
|
|
138
|
+
| `X-API-Key` header | `APIKeyFlow` | Server-to-server |
|
|
139
|
+
| `MCPClient` | Combined | Auto-detects best method |
|
|
140
|
+
|
|
141
|
+
### Methods NOT Exposed (Security)
|
|
142
|
+
- Email/Password: Server-rendered form only
|
|
143
|
+
- Admin Bypass: Internal emergency access
|
|
144
|
+
- Supabase OAuth 2.1: Internal Supabase integration
|
|
145
|
+
|
|
146
|
+
See [auth-gateway/AUTHENTICATION-METHODS.md](../../apps/onasis-core/services/auth-gateway/AUTHENTICATION-METHODS.md) for complete server-side documentation.
|
|
147
|
+
|
|
129
148
|
## License
|
|
130
149
|
MIT © Lan Onasis
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React-specific types for SSO authentication
|
|
3
|
+
* @module @lanonasis/oauth-client/react
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* SSO User information from the lanonasis_user cookie
|
|
7
|
+
*/
|
|
8
|
+
interface SSOUser {
|
|
9
|
+
id: string;
|
|
10
|
+
email: string;
|
|
11
|
+
role: string;
|
|
12
|
+
name?: string;
|
|
13
|
+
avatar_url?: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* SSO authentication state
|
|
17
|
+
*/
|
|
18
|
+
interface SSOState {
|
|
19
|
+
/** Whether the user is authenticated */
|
|
20
|
+
isAuthenticated: boolean;
|
|
21
|
+
/** Whether the auth state is being determined */
|
|
22
|
+
isLoading: boolean;
|
|
23
|
+
/** User information if authenticated */
|
|
24
|
+
user: SSOUser | null;
|
|
25
|
+
/** Error message if auth check failed */
|
|
26
|
+
error: string | null;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Configuration for the SSO hook
|
|
30
|
+
*/
|
|
31
|
+
interface SSOConfig {
|
|
32
|
+
/** Auth gateway URL (default: https://auth.lanonasis.com) */
|
|
33
|
+
authGatewayUrl?: string;
|
|
34
|
+
/** Cookie domain (default: .lanonasis.com) */
|
|
35
|
+
cookieDomain?: string;
|
|
36
|
+
/** Callback when auth state changes */
|
|
37
|
+
onAuthChange?: (state: SSOState) => void;
|
|
38
|
+
/** Polling interval in ms for cookie changes (default: 30000) */
|
|
39
|
+
pollInterval?: number;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Configuration for Supabase-to-cookie sync
|
|
43
|
+
*/
|
|
44
|
+
interface SSOSyncConfig {
|
|
45
|
+
/** Auth gateway URL (default: https://auth.lanonasis.com) */
|
|
46
|
+
authGatewayUrl?: string;
|
|
47
|
+
/** Project scope for multi-tenant auth */
|
|
48
|
+
projectScope?: string;
|
|
49
|
+
/** Callback when sync completes */
|
|
50
|
+
onSyncComplete?: (success: boolean) => void;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Supabase session interface (minimal for what we need)
|
|
54
|
+
*/
|
|
55
|
+
interface SupabaseSession {
|
|
56
|
+
access_token: string;
|
|
57
|
+
refresh_token?: string;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Return type for useSSO hook
|
|
61
|
+
*/
|
|
62
|
+
interface UseSSOReturn extends SSOState {
|
|
63
|
+
/** Manually refresh auth state */
|
|
64
|
+
refresh: () => void;
|
|
65
|
+
/** Logout and redirect to auth gateway */
|
|
66
|
+
logout: (returnTo?: string) => void;
|
|
67
|
+
/** Get login URL with optional return URL */
|
|
68
|
+
getLoginUrl: (returnTo?: string) => string;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Return type for useSSOSync hook
|
|
72
|
+
*/
|
|
73
|
+
interface UseSSOSyncReturn {
|
|
74
|
+
/** Manually trigger sync */
|
|
75
|
+
sync: () => Promise<boolean>;
|
|
76
|
+
/** Sync with gateway directly */
|
|
77
|
+
syncWithGateway: (session: SupabaseSession) => Promise<boolean>;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Cookie constants and utilities shared between browser and server
|
|
82
|
+
* @module @lanonasis/oauth-client/cookies
|
|
83
|
+
*/
|
|
84
|
+
/**
|
|
85
|
+
* Cookie names used by Lan Onasis auth system
|
|
86
|
+
*/
|
|
87
|
+
declare const COOKIE_NAMES: {
|
|
88
|
+
/** HttpOnly JWT session token */
|
|
89
|
+
readonly SESSION: "lanonasis_session";
|
|
90
|
+
/** Readable user metadata (JSON) */
|
|
91
|
+
readonly USER: "lanonasis_user";
|
|
92
|
+
};
|
|
93
|
+
/**
|
|
94
|
+
* Default cookie domain for cross-subdomain SSO
|
|
95
|
+
*/
|
|
96
|
+
declare const DEFAULT_COOKIE_DOMAIN = ".lanonasis.com";
|
|
97
|
+
/**
|
|
98
|
+
* Default auth gateway URL
|
|
99
|
+
*/
|
|
100
|
+
declare const DEFAULT_AUTH_GATEWAY = "https://auth.lanonasis.com";
|
|
101
|
+
/**
|
|
102
|
+
* Default polling interval for cookie changes (30 seconds)
|
|
103
|
+
*/
|
|
104
|
+
declare const DEFAULT_POLL_INTERVAL = 30000;
|
|
105
|
+
/**
|
|
106
|
+
* Default project scope for multi-tenant auth
|
|
107
|
+
*/
|
|
108
|
+
declare const DEFAULT_PROJECT_SCOPE = "lanonasis-maas";
|
|
109
|
+
|
|
110
|
+
export { COOKIE_NAMES as C, DEFAULT_AUTH_GATEWAY as D, type SSOConfig as S, type UseSSOReturn as U, type SupabaseSession as a, type SSOSyncConfig as b, type UseSSOSyncReturn as c, type SSOUser as d, type SSOState as e, DEFAULT_COOKIE_DOMAIN as f, DEFAULT_POLL_INTERVAL as g, DEFAULT_PROJECT_SCOPE as h };
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React-specific types for SSO authentication
|
|
3
|
+
* @module @lanonasis/oauth-client/react
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* SSO User information from the lanonasis_user cookie
|
|
7
|
+
*/
|
|
8
|
+
interface SSOUser {
|
|
9
|
+
id: string;
|
|
10
|
+
email: string;
|
|
11
|
+
role: string;
|
|
12
|
+
name?: string;
|
|
13
|
+
avatar_url?: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* SSO authentication state
|
|
17
|
+
*/
|
|
18
|
+
interface SSOState {
|
|
19
|
+
/** Whether the user is authenticated */
|
|
20
|
+
isAuthenticated: boolean;
|
|
21
|
+
/** Whether the auth state is being determined */
|
|
22
|
+
isLoading: boolean;
|
|
23
|
+
/** User information if authenticated */
|
|
24
|
+
user: SSOUser | null;
|
|
25
|
+
/** Error message if auth check failed */
|
|
26
|
+
error: string | null;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Configuration for the SSO hook
|
|
30
|
+
*/
|
|
31
|
+
interface SSOConfig {
|
|
32
|
+
/** Auth gateway URL (default: https://auth.lanonasis.com) */
|
|
33
|
+
authGatewayUrl?: string;
|
|
34
|
+
/** Cookie domain (default: .lanonasis.com) */
|
|
35
|
+
cookieDomain?: string;
|
|
36
|
+
/** Callback when auth state changes */
|
|
37
|
+
onAuthChange?: (state: SSOState) => void;
|
|
38
|
+
/** Polling interval in ms for cookie changes (default: 30000) */
|
|
39
|
+
pollInterval?: number;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Configuration for Supabase-to-cookie sync
|
|
43
|
+
*/
|
|
44
|
+
interface SSOSyncConfig {
|
|
45
|
+
/** Auth gateway URL (default: https://auth.lanonasis.com) */
|
|
46
|
+
authGatewayUrl?: string;
|
|
47
|
+
/** Project scope for multi-tenant auth */
|
|
48
|
+
projectScope?: string;
|
|
49
|
+
/** Callback when sync completes */
|
|
50
|
+
onSyncComplete?: (success: boolean) => void;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Supabase session interface (minimal for what we need)
|
|
54
|
+
*/
|
|
55
|
+
interface SupabaseSession {
|
|
56
|
+
access_token: string;
|
|
57
|
+
refresh_token?: string;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Return type for useSSO hook
|
|
61
|
+
*/
|
|
62
|
+
interface UseSSOReturn extends SSOState {
|
|
63
|
+
/** Manually refresh auth state */
|
|
64
|
+
refresh: () => void;
|
|
65
|
+
/** Logout and redirect to auth gateway */
|
|
66
|
+
logout: (returnTo?: string) => void;
|
|
67
|
+
/** Get login URL with optional return URL */
|
|
68
|
+
getLoginUrl: (returnTo?: string) => string;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Return type for useSSOSync hook
|
|
72
|
+
*/
|
|
73
|
+
interface UseSSOSyncReturn {
|
|
74
|
+
/** Manually trigger sync */
|
|
75
|
+
sync: () => Promise<boolean>;
|
|
76
|
+
/** Sync with gateway directly */
|
|
77
|
+
syncWithGateway: (session: SupabaseSession) => Promise<boolean>;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Cookie constants and utilities shared between browser and server
|
|
82
|
+
* @module @lanonasis/oauth-client/cookies
|
|
83
|
+
*/
|
|
84
|
+
/**
|
|
85
|
+
* Cookie names used by Lan Onasis auth system
|
|
86
|
+
*/
|
|
87
|
+
declare const COOKIE_NAMES: {
|
|
88
|
+
/** HttpOnly JWT session token */
|
|
89
|
+
readonly SESSION: "lanonasis_session";
|
|
90
|
+
/** Readable user metadata (JSON) */
|
|
91
|
+
readonly USER: "lanonasis_user";
|
|
92
|
+
};
|
|
93
|
+
/**
|
|
94
|
+
* Default cookie domain for cross-subdomain SSO
|
|
95
|
+
*/
|
|
96
|
+
declare const DEFAULT_COOKIE_DOMAIN = ".lanonasis.com";
|
|
97
|
+
/**
|
|
98
|
+
* Default auth gateway URL
|
|
99
|
+
*/
|
|
100
|
+
declare const DEFAULT_AUTH_GATEWAY = "https://auth.lanonasis.com";
|
|
101
|
+
/**
|
|
102
|
+
* Default polling interval for cookie changes (30 seconds)
|
|
103
|
+
*/
|
|
104
|
+
declare const DEFAULT_POLL_INTERVAL = 30000;
|
|
105
|
+
/**
|
|
106
|
+
* Default project scope for multi-tenant auth
|
|
107
|
+
*/
|
|
108
|
+
declare const DEFAULT_PROJECT_SCOPE = "lanonasis-maas";
|
|
109
|
+
|
|
110
|
+
export { COOKIE_NAMES as C, DEFAULT_AUTH_GATEWAY as D, type SSOConfig as S, type UseSSOReturn as U, type SupabaseSession as a, type SSOSyncConfig as b, type UseSSOSyncReturn as c, type SSOUser as d, type SSOState as e, DEFAULT_COOKIE_DOMAIN as f, DEFAULT_POLL_INTERVAL as g, DEFAULT_PROJECT_SCOPE as h };
|
package/dist/index.cjs
CHANGED
|
@@ -398,11 +398,15 @@ var MagicLinkFlow = class extends BaseOAuthFlow {
|
|
|
398
398
|
* @returns Response with success status and expiration time
|
|
399
399
|
*/
|
|
400
400
|
async requestOTP(email) {
|
|
401
|
+
if (typeof email !== "string" || !email.trim()) {
|
|
402
|
+
throw new Error("Invalid email: a non-empty email address is required to request an OTP.");
|
|
403
|
+
}
|
|
404
|
+
const normalizedEmail = email.trim().toLowerCase();
|
|
401
405
|
const response = await (0, import_cross_fetch3.default)(`${this.authBaseUrl}/v1/auth/otp/send`, {
|
|
402
406
|
method: "POST",
|
|
403
407
|
headers: { "Content-Type": "application/json" },
|
|
404
408
|
body: JSON.stringify({
|
|
405
|
-
email:
|
|
409
|
+
email: normalizedEmail,
|
|
406
410
|
type: "email",
|
|
407
411
|
// Explicitly request 6-digit code, not magic link
|
|
408
412
|
platform: this.platform,
|
|
@@ -556,7 +560,19 @@ var MagicLinkFlow = class extends BaseOAuthFlow {
|
|
|
556
560
|
return emailRegex.test(email.trim());
|
|
557
561
|
}
|
|
558
562
|
/**
|
|
559
|
-
* Check if an OTP code is valid format (6 digits)
|
|
563
|
+
* Check if an OTP code is valid format (6 digits).
|
|
564
|
+
*
|
|
565
|
+
* This method:
|
|
566
|
+
* - Trims leading and trailing whitespace from the input before validating.
|
|
567
|
+
* - Requires exactly 6 numeric digits (0–9); any non-numeric characters or
|
|
568
|
+
* incorrect length will cause it to return `false`.
|
|
569
|
+
*
|
|
570
|
+
* Note: This method expects a string value. Passing `null`, `undefined`, or
|
|
571
|
+
* other non-string values will result in a runtime error when `.trim()` is
|
|
572
|
+
* called, rather than returning `false`.
|
|
573
|
+
*
|
|
574
|
+
* @param code - The OTP code to validate.
|
|
575
|
+
* @returns `true` if the trimmed code consists of exactly 6 digits, otherwise `false`.
|
|
560
576
|
*/
|
|
561
577
|
static isValidOTPCode(code) {
|
|
562
578
|
return /^\d{6}$/.test(code.trim());
|
package/dist/index.d.cts
CHANGED
|
@@ -131,7 +131,19 @@ declare class MagicLinkFlow extends BaseOAuthFlow {
|
|
|
131
131
|
*/
|
|
132
132
|
static isValidEmail(email: string): boolean;
|
|
133
133
|
/**
|
|
134
|
-
* Check if an OTP code is valid format (6 digits)
|
|
134
|
+
* Check if an OTP code is valid format (6 digits).
|
|
135
|
+
*
|
|
136
|
+
* This method:
|
|
137
|
+
* - Trims leading and trailing whitespace from the input before validating.
|
|
138
|
+
* - Requires exactly 6 numeric digits (0–9); any non-numeric characters or
|
|
139
|
+
* incorrect length will cause it to return `false`.
|
|
140
|
+
*
|
|
141
|
+
* Note: This method expects a string value. Passing `null`, `undefined`, or
|
|
142
|
+
* other non-string values will result in a runtime error when `.trim()` is
|
|
143
|
+
* called, rather than returning `false`.
|
|
144
|
+
*
|
|
145
|
+
* @param code - The OTP code to validate.
|
|
146
|
+
* @returns `true` if the trimmed code consists of exactly 6 digits, otherwise `false`.
|
|
135
147
|
*/
|
|
136
148
|
static isValidOTPCode(code: string): boolean;
|
|
137
149
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -131,7 +131,19 @@ declare class MagicLinkFlow extends BaseOAuthFlow {
|
|
|
131
131
|
*/
|
|
132
132
|
static isValidEmail(email: string): boolean;
|
|
133
133
|
/**
|
|
134
|
-
* Check if an OTP code is valid format (6 digits)
|
|
134
|
+
* Check if an OTP code is valid format (6 digits).
|
|
135
|
+
*
|
|
136
|
+
* This method:
|
|
137
|
+
* - Trims leading and trailing whitespace from the input before validating.
|
|
138
|
+
* - Requires exactly 6 numeric digits (0–9); any non-numeric characters or
|
|
139
|
+
* incorrect length will cause it to return `false`.
|
|
140
|
+
*
|
|
141
|
+
* Note: This method expects a string value. Passing `null`, `undefined`, or
|
|
142
|
+
* other non-string values will result in a runtime error when `.trim()` is
|
|
143
|
+
* called, rather than returning `false`.
|
|
144
|
+
*
|
|
145
|
+
* @param code - The OTP code to validate.
|
|
146
|
+
* @returns `true` if the trimmed code consists of exactly 6 digits, otherwise `false`.
|
|
135
147
|
*/
|
|
136
148
|
static isValidOTPCode(code: string): boolean;
|
|
137
149
|
}
|
package/dist/index.mjs
CHANGED
|
@@ -359,11 +359,15 @@ var MagicLinkFlow = class extends BaseOAuthFlow {
|
|
|
359
359
|
* @returns Response with success status and expiration time
|
|
360
360
|
*/
|
|
361
361
|
async requestOTP(email) {
|
|
362
|
+
if (typeof email !== "string" || !email.trim()) {
|
|
363
|
+
throw new Error("Invalid email: a non-empty email address is required to request an OTP.");
|
|
364
|
+
}
|
|
365
|
+
const normalizedEmail = email.trim().toLowerCase();
|
|
362
366
|
const response = await fetch3(`${this.authBaseUrl}/v1/auth/otp/send`, {
|
|
363
367
|
method: "POST",
|
|
364
368
|
headers: { "Content-Type": "application/json" },
|
|
365
369
|
body: JSON.stringify({
|
|
366
|
-
email:
|
|
370
|
+
email: normalizedEmail,
|
|
367
371
|
type: "email",
|
|
368
372
|
// Explicitly request 6-digit code, not magic link
|
|
369
373
|
platform: this.platform,
|
|
@@ -517,7 +521,19 @@ var MagicLinkFlow = class extends BaseOAuthFlow {
|
|
|
517
521
|
return emailRegex.test(email.trim());
|
|
518
522
|
}
|
|
519
523
|
/**
|
|
520
|
-
* Check if an OTP code is valid format (6 digits)
|
|
524
|
+
* Check if an OTP code is valid format (6 digits).
|
|
525
|
+
*
|
|
526
|
+
* This method:
|
|
527
|
+
* - Trims leading and trailing whitespace from the input before validating.
|
|
528
|
+
* - Requires exactly 6 numeric digits (0–9); any non-numeric characters or
|
|
529
|
+
* incorrect length will cause it to return `false`.
|
|
530
|
+
*
|
|
531
|
+
* Note: This method expects a string value. Passing `null`, `undefined`, or
|
|
532
|
+
* other non-string values will result in a runtime error when `.trim()` is
|
|
533
|
+
* called, rather than returning `false`.
|
|
534
|
+
*
|
|
535
|
+
* @param code - The OTP code to validate.
|
|
536
|
+
* @returns `true` if the trimmed code consists of exactly 6 digits, otherwise `false`.
|
|
521
537
|
*/
|
|
522
538
|
static isValidOTPCode(code) {
|
|
523
539
|
return /^\d{6}$/.test(code.trim());
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/react/index.ts
|
|
21
|
+
var react_exports = {};
|
|
22
|
+
__export(react_exports, {
|
|
23
|
+
COOKIE_NAMES: () => COOKIE_NAMES,
|
|
24
|
+
DEFAULT_AUTH_GATEWAY: () => DEFAULT_AUTH_GATEWAY,
|
|
25
|
+
DEFAULT_COOKIE_DOMAIN: () => DEFAULT_COOKIE_DOMAIN,
|
|
26
|
+
DEFAULT_POLL_INTERVAL: () => DEFAULT_POLL_INTERVAL,
|
|
27
|
+
DEFAULT_PROJECT_SCOPE: () => DEFAULT_PROJECT_SCOPE,
|
|
28
|
+
clearUserCookie: () => clearUserCookie,
|
|
29
|
+
hasAuthCookies: () => hasAuthCookies,
|
|
30
|
+
hasSessionCookie: () => hasSessionCookie,
|
|
31
|
+
isBrowser: () => isBrowser,
|
|
32
|
+
parseUserCookie: () => parseUserCookie,
|
|
33
|
+
useSSO: () => useSSO,
|
|
34
|
+
useSSOSync: () => useSSOSync
|
|
35
|
+
});
|
|
36
|
+
module.exports = __toCommonJS(react_exports);
|
|
37
|
+
|
|
38
|
+
// src/react/useSSO.ts
|
|
39
|
+
var import_react = require("react");
|
|
40
|
+
|
|
41
|
+
// src/cookies/constants.ts
|
|
42
|
+
var COOKIE_NAMES = {
|
|
43
|
+
/** HttpOnly JWT session token */
|
|
44
|
+
SESSION: "lanonasis_session",
|
|
45
|
+
/** Readable user metadata (JSON) */
|
|
46
|
+
USER: "lanonasis_user"
|
|
47
|
+
};
|
|
48
|
+
var DEFAULT_COOKIE_DOMAIN = ".lanonasis.com";
|
|
49
|
+
var DEFAULT_AUTH_GATEWAY = "https://auth.lanonasis.com";
|
|
50
|
+
var DEFAULT_POLL_INTERVAL = 3e4;
|
|
51
|
+
var DEFAULT_PROJECT_SCOPE = "lanonasis-maas";
|
|
52
|
+
|
|
53
|
+
// src/react/cookie-utils-browser.ts
|
|
54
|
+
function isBrowser() {
|
|
55
|
+
return typeof window !== "undefined" && typeof document !== "undefined";
|
|
56
|
+
}
|
|
57
|
+
function parseUserCookie() {
|
|
58
|
+
if (!isBrowser()) return null;
|
|
59
|
+
try {
|
|
60
|
+
const cookies = document.cookie.split(";");
|
|
61
|
+
const userCookie = cookies.find(
|
|
62
|
+
(c) => c.trim().startsWith(`${COOKIE_NAMES.USER}=`)
|
|
63
|
+
);
|
|
64
|
+
if (!userCookie) return null;
|
|
65
|
+
const value = userCookie.split("=").slice(1).join("=").trim();
|
|
66
|
+
const decoded = decodeURIComponent(value);
|
|
67
|
+
const parsed = JSON.parse(decoded);
|
|
68
|
+
if (!parsed.id || !parsed.email || !parsed.role) {
|
|
69
|
+
console.warn("[oauth-client] Invalid user cookie: missing required fields");
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
return parsed;
|
|
73
|
+
} catch (error) {
|
|
74
|
+
console.warn("[oauth-client] Failed to parse user cookie:", error);
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function hasSessionCookie() {
|
|
79
|
+
if (!isBrowser()) return false;
|
|
80
|
+
return document.cookie.includes(`${COOKIE_NAMES.SESSION}=`);
|
|
81
|
+
}
|
|
82
|
+
function hasAuthCookies() {
|
|
83
|
+
return hasSessionCookie() && parseUserCookie() !== null;
|
|
84
|
+
}
|
|
85
|
+
function clearUserCookie(domain = ".lanonasis.com") {
|
|
86
|
+
if (!isBrowser()) return;
|
|
87
|
+
document.cookie = `${COOKIE_NAMES.USER}=; domain=${domain}; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// src/react/useSSO.ts
|
|
91
|
+
function useSSO(config = {}) {
|
|
92
|
+
const {
|
|
93
|
+
authGatewayUrl = DEFAULT_AUTH_GATEWAY,
|
|
94
|
+
onAuthChange,
|
|
95
|
+
pollInterval = DEFAULT_POLL_INTERVAL
|
|
96
|
+
} = config;
|
|
97
|
+
const [state, setState] = (0, import_react.useState)({
|
|
98
|
+
isAuthenticated: false,
|
|
99
|
+
isLoading: true,
|
|
100
|
+
user: null,
|
|
101
|
+
error: null
|
|
102
|
+
});
|
|
103
|
+
const prevStateRef = (0, import_react.useRef)(null);
|
|
104
|
+
const checkAuthState = (0, import_react.useCallback)(() => {
|
|
105
|
+
if (!isBrowser()) {
|
|
106
|
+
setState({
|
|
107
|
+
isAuthenticated: false,
|
|
108
|
+
isLoading: false,
|
|
109
|
+
user: null,
|
|
110
|
+
error: null
|
|
111
|
+
});
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
try {
|
|
115
|
+
const hasSession = hasSessionCookie();
|
|
116
|
+
const user = parseUserCookie();
|
|
117
|
+
const newState = {
|
|
118
|
+
isAuthenticated: hasSession && user !== null,
|
|
119
|
+
isLoading: false,
|
|
120
|
+
user: hasSession ? user : null,
|
|
121
|
+
error: null
|
|
122
|
+
};
|
|
123
|
+
setState(newState);
|
|
124
|
+
if (onAuthChange && prevStateRef.current && prevStateRef.current.isAuthenticated !== newState.isAuthenticated) {
|
|
125
|
+
onAuthChange(newState);
|
|
126
|
+
}
|
|
127
|
+
prevStateRef.current = newState;
|
|
128
|
+
} catch (error) {
|
|
129
|
+
const errorState = {
|
|
130
|
+
isAuthenticated: false,
|
|
131
|
+
isLoading: false,
|
|
132
|
+
user: null,
|
|
133
|
+
error: error instanceof Error ? error.message : "Failed to check auth state"
|
|
134
|
+
};
|
|
135
|
+
setState(errorState);
|
|
136
|
+
prevStateRef.current = errorState;
|
|
137
|
+
}
|
|
138
|
+
}, [onAuthChange]);
|
|
139
|
+
const refresh = (0, import_react.useCallback)(() => {
|
|
140
|
+
setState((prev) => ({ ...prev, isLoading: true }));
|
|
141
|
+
checkAuthState();
|
|
142
|
+
}, [checkAuthState]);
|
|
143
|
+
const logout = (0, import_react.useCallback)((returnTo) => {
|
|
144
|
+
if (!isBrowser()) return;
|
|
145
|
+
const logoutUrl = new URL("/web/logout", authGatewayUrl);
|
|
146
|
+
if (returnTo) {
|
|
147
|
+
logoutUrl.searchParams.set("return_to", returnTo);
|
|
148
|
+
}
|
|
149
|
+
window.location.href = logoutUrl.toString();
|
|
150
|
+
}, [authGatewayUrl]);
|
|
151
|
+
const getLoginUrl = (0, import_react.useCallback)((returnTo) => {
|
|
152
|
+
const loginUrl = new URL("/web/login", authGatewayUrl);
|
|
153
|
+
if (returnTo) {
|
|
154
|
+
loginUrl.searchParams.set("return_to", returnTo);
|
|
155
|
+
}
|
|
156
|
+
return loginUrl.toString();
|
|
157
|
+
}, [authGatewayUrl]);
|
|
158
|
+
(0, import_react.useEffect)(() => {
|
|
159
|
+
const timer = setTimeout(checkAuthState, 0);
|
|
160
|
+
return () => clearTimeout(timer);
|
|
161
|
+
}, [checkAuthState]);
|
|
162
|
+
(0, import_react.useEffect)(() => {
|
|
163
|
+
if (!isBrowser() || pollInterval <= 0) return;
|
|
164
|
+
const interval = setInterval(checkAuthState, pollInterval);
|
|
165
|
+
return () => clearInterval(interval);
|
|
166
|
+
}, [checkAuthState, pollInterval]);
|
|
167
|
+
(0, import_react.useEffect)(() => {
|
|
168
|
+
if (!isBrowser()) return;
|
|
169
|
+
const handleStorageChange = (event) => {
|
|
170
|
+
if (event.key === null || event.key?.includes("auth")) {
|
|
171
|
+
checkAuthState();
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
window.addEventListener("storage", handleStorageChange);
|
|
175
|
+
return () => window.removeEventListener("storage", handleStorageChange);
|
|
176
|
+
}, [checkAuthState]);
|
|
177
|
+
(0, import_react.useEffect)(() => {
|
|
178
|
+
if (!isBrowser()) return;
|
|
179
|
+
const handleFocus = () => {
|
|
180
|
+
checkAuthState();
|
|
181
|
+
};
|
|
182
|
+
window.addEventListener("focus", handleFocus);
|
|
183
|
+
return () => window.removeEventListener("focus", handleFocus);
|
|
184
|
+
}, [checkAuthState]);
|
|
185
|
+
return {
|
|
186
|
+
...state,
|
|
187
|
+
refresh,
|
|
188
|
+
logout,
|
|
189
|
+
getLoginUrl
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// src/react/useSSOSync.ts
|
|
194
|
+
var import_react2 = require("react");
|
|
195
|
+
function useSSOSync(supabaseSession, config = {}) {
|
|
196
|
+
const {
|
|
197
|
+
authGatewayUrl = DEFAULT_AUTH_GATEWAY,
|
|
198
|
+
projectScope = DEFAULT_PROJECT_SCOPE,
|
|
199
|
+
onSyncComplete
|
|
200
|
+
} = config;
|
|
201
|
+
const lastSyncedTokenRef = (0, import_react2.useRef)(null);
|
|
202
|
+
const syncWithGateway = (0, import_react2.useCallback)(
|
|
203
|
+
async (session) => {
|
|
204
|
+
if (!isBrowser()) return false;
|
|
205
|
+
try {
|
|
206
|
+
const response = await fetch(
|
|
207
|
+
`${authGatewayUrl}/v1/auth/token/exchange`,
|
|
208
|
+
{
|
|
209
|
+
method: "POST",
|
|
210
|
+
headers: {
|
|
211
|
+
Authorization: `Bearer ${session.access_token}`,
|
|
212
|
+
"Content-Type": "application/json",
|
|
213
|
+
"X-Project-Scope": projectScope
|
|
214
|
+
},
|
|
215
|
+
credentials: "include",
|
|
216
|
+
// Important: include cookies
|
|
217
|
+
body: JSON.stringify({
|
|
218
|
+
project_scope: projectScope,
|
|
219
|
+
platform: "web"
|
|
220
|
+
})
|
|
221
|
+
}
|
|
222
|
+
);
|
|
223
|
+
if (!response.ok) {
|
|
224
|
+
console.warn("[oauth-client] SSO sync failed:", response.status);
|
|
225
|
+
onSyncComplete?.(false);
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
const data = await response.json();
|
|
229
|
+
if (data.cookies_set) {
|
|
230
|
+
console.log("[oauth-client] SSO cookies set successfully");
|
|
231
|
+
}
|
|
232
|
+
onSyncComplete?.(true);
|
|
233
|
+
return true;
|
|
234
|
+
} catch (error) {
|
|
235
|
+
console.error("[oauth-client] SSO sync error:", error);
|
|
236
|
+
onSyncComplete?.(false);
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
},
|
|
240
|
+
[authGatewayUrl, projectScope, onSyncComplete]
|
|
241
|
+
);
|
|
242
|
+
const sync = (0, import_react2.useCallback)(async () => {
|
|
243
|
+
if (!supabaseSession?.access_token) {
|
|
244
|
+
console.warn("[oauth-client] Cannot sync: no Supabase session");
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
return syncWithGateway(supabaseSession);
|
|
248
|
+
}, [supabaseSession, syncWithGateway]);
|
|
249
|
+
(0, import_react2.useEffect)(() => {
|
|
250
|
+
const token = supabaseSession?.access_token;
|
|
251
|
+
if (!token || token === lastSyncedTokenRef.current) {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
lastSyncedTokenRef.current = token;
|
|
255
|
+
syncWithGateway(supabaseSession);
|
|
256
|
+
}, [supabaseSession?.access_token, syncWithGateway, supabaseSession]);
|
|
257
|
+
return {
|
|
258
|
+
sync,
|
|
259
|
+
syncWithGateway
|
|
260
|
+
};
|
|
261
|
+
}
|