@socialproof/mysocial-auth 0.3.0 → 0.4.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/CHANGELOG.md +6 -0
- package/README.md +18 -13
- package/dist/auth.mjs +18 -9
- package/dist/auth.mjs.map +1 -1
- package/dist/exchange.mjs +1 -22
- package/dist/exchange.mjs.map +1 -1
- package/dist/popup.mjs +9 -9
- package/dist/popup.mjs.map +1 -1
- package/dist/types.d.mts +4 -2
- package/dist/types.d.mts.map +1 -1
- package/package.json +5 -5
package/CHANGELOG.md
CHANGED
package/README.md
CHANGED
|
@@ -68,7 +68,7 @@ await auth.signOut();
|
|
|
68
68
|
|
|
69
69
|
### auth.signIn(options?)
|
|
70
70
|
|
|
71
|
-
- `provider?`: 'google' | 'apple' | 'facebook' | 'twitch'
|
|
71
|
+
- `provider?`: 'google' | 'apple' | 'facebook' | 'twitch' | 'none' (default 'none' = home screen)
|
|
72
72
|
- `mode?`: 'popup' | 'redirect' (default: 'popup')
|
|
73
73
|
- Returns: `Promise<Session>` (popup) or never (redirect; page navigates)
|
|
74
74
|
|
|
@@ -86,8 +86,8 @@ await auth.signOut();
|
|
|
86
86
|
|
|
87
87
|
### auth.handleRedirectCallback(url?)
|
|
88
88
|
|
|
89
|
-
- For redirect mode. Parses URL for code/state/nonce,
|
|
90
|
-
`window.location.href`.
|
|
89
|
+
- For redirect mode. Parses URL for code/state/nonce, builds session from payload (no exchange).
|
|
90
|
+
Omit `url` to use `window.location.href`.
|
|
91
91
|
|
|
92
92
|
### auth.onAuthStateChange(callback)
|
|
93
93
|
|
|
@@ -95,9 +95,10 @@ await auth.signOut();
|
|
|
95
95
|
|
|
96
96
|
## Hosted UI Contract (auth.mysocial.network)
|
|
97
97
|
|
|
98
|
-
The SDK passes `return_origin
|
|
99
|
-
|
|
100
|
-
|
|
98
|
+
The SDK passes `return_origin`, `code_challenge_method: S256`, and `provider` in the login URL
|
|
99
|
+
params. `provider` is never empty: `'google'`, `'apple'`, `'facebook'`, `'twitch'`, or `'none'`
|
|
100
|
+
(home screen). The auth frontend generates PKCE server-side; the package does not send
|
|
101
|
+
code_challenge. `return_origin` must match the dApp's origin
|
|
101
102
|
(`window.location.origin`) so the auth server can post the auth result to the correct target.
|
|
102
103
|
The auth server uses `return_origin` as the `postMessage` targetOrigin when sending the result.
|
|
103
104
|
|
|
@@ -107,7 +108,7 @@ The popup page must call:
|
|
|
107
108
|
window.opener.postMessage(payload, validatedTargetOrigin); // targetOrigin, NOT "*"
|
|
108
109
|
```
|
|
109
110
|
|
|
110
|
-
**Success payload:**
|
|
111
|
+
**Success payload (MYSOCIAL_AUTH_RESULT is the final result; no exchange needed):**
|
|
111
112
|
|
|
112
113
|
```json
|
|
113
114
|
{
|
|
@@ -116,10 +117,17 @@ window.opener.postMessage(payload, validatedTargetOrigin); // targetOrigin, NOT
|
|
|
116
117
|
"state": "...",
|
|
117
118
|
"nonce": "...",
|
|
118
119
|
"clientId": "...",
|
|
119
|
-
"requestId": "..."
|
|
120
|
+
"requestId": "...",
|
|
121
|
+
"salt": "...",
|
|
122
|
+
"user": {},
|
|
123
|
+
"access_token": "...",
|
|
124
|
+
"refresh_token": "...",
|
|
125
|
+
"expires_at": 0
|
|
120
126
|
}
|
|
121
127
|
```
|
|
122
128
|
|
|
129
|
+
`code` is the token (id_token or access_token). Optional: `salt`, `user`, `access_token`, `refresh_token`, `expires_at`.
|
|
130
|
+
|
|
123
131
|
**Error payload:**
|
|
124
132
|
|
|
125
133
|
```json
|
|
@@ -135,16 +143,13 @@ window.opener.postMessage(payload, validatedTargetOrigin); // targetOrigin, NOT
|
|
|
135
143
|
`return_origin` must never be trusted from the query param. The backend must validate it against the
|
|
136
144
|
allowlist for `client_id`.
|
|
137
145
|
|
|
138
|
-
**Important:** The
|
|
139
|
-
|
|
146
|
+
**Important:** The auth frontend and salt service handle the OAuth exchange internally. The package
|
|
147
|
+
does not call `/auth/exchange`. `MYSOCIAL_AUTH_RESULT` is the final result: `code` is the token.
|
|
140
148
|
|
|
141
149
|
## Backend Contract
|
|
142
150
|
|
|
143
151
|
- `POST ${apiBaseUrl}/auth/request` (optional): `{ client_id, redirect_uri, return_origin }` →
|
|
144
152
|
`{ request_id }`
|
|
145
|
-
- `POST ${apiBaseUrl}/auth/exchange`:
|
|
146
|
-
`{ code, redirect_uri, state?, nonce?, request_id? }` (your app's backend; code_verifier not sent—auth frontend handles OAuth/salt internally) →
|
|
147
|
-
`{ access_token, refresh_token?, expires_in, user }`
|
|
148
153
|
- `POST ${apiBaseUrl}/auth/refresh`: `{ refresh_token }` →
|
|
149
154
|
`{ access_token, refresh_token?, expires_in, user }`
|
|
150
155
|
- `POST ${apiBaseUrl}/auth/logout`
|
package/dist/auth.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { REDIRECT_STATE_PREFIX, SESSION_KEY, createStorage, redirectStorage } from "./storage.mjs";
|
|
2
2
|
import { generateNonce, generateState } from "./pkce.mjs";
|
|
3
|
-
import {
|
|
3
|
+
import { fetchRequestId, logout, refreshTokens } from "./exchange.mjs";
|
|
4
4
|
import { openAuthPopup } from "./popup.mjs";
|
|
5
5
|
import { mitt } from "@socialproof/utils";
|
|
6
6
|
|
|
@@ -88,7 +88,7 @@ function createAuth(config) {
|
|
|
88
88
|
nonce,
|
|
89
89
|
return_origin: returnOrigin,
|
|
90
90
|
mode: "redirect",
|
|
91
|
-
provider: provider ?? "",
|
|
91
|
+
provider: provider ?? "none",
|
|
92
92
|
code_challenge_method: "S256"
|
|
93
93
|
});
|
|
94
94
|
if (requestId) params.set("request_id", requestId);
|
|
@@ -122,6 +122,11 @@ function createAuth(config) {
|
|
|
122
122
|
const state = parsed.searchParams.get("state");
|
|
123
123
|
const nonce = parsed.searchParams.get("nonce");
|
|
124
124
|
const requestId = parsed.searchParams.get("request_id") ?? void 0;
|
|
125
|
+
const salt = parsed.searchParams.get("salt") ?? void 0;
|
|
126
|
+
const accessToken = parsed.searchParams.get("access_token") ?? void 0;
|
|
127
|
+
const refreshToken = parsed.searchParams.get("refresh_token") ?? void 0;
|
|
128
|
+
const expiresAtParam = parsed.searchParams.get("expires_at");
|
|
129
|
+
const userParam = parsed.searchParams.get("user");
|
|
125
130
|
if (!code || !state || !nonce) throw new Error("Missing code, state, or nonce in callback URL");
|
|
126
131
|
const key = `${REDIRECT_STATE_PREFIX}${state}`;
|
|
127
132
|
const raw = redirectStorage.get(key);
|
|
@@ -136,13 +141,17 @@ function createAuth(config) {
|
|
|
136
141
|
throw new Error("Request ID mismatch");
|
|
137
142
|
}
|
|
138
143
|
redirectStorage.remove(key);
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
144
|
+
let user = {};
|
|
145
|
+
if (userParam) try {
|
|
146
|
+
user = JSON.parse(decodeURIComponent(userParam));
|
|
147
|
+
} catch {}
|
|
148
|
+
const session = {
|
|
149
|
+
access_token: accessToken ?? code,
|
|
150
|
+
refresh_token: refreshToken,
|
|
151
|
+
user,
|
|
152
|
+
expires_at: expiresAtParam ? Number(expiresAtParam) : Date.now() + 36e5,
|
|
153
|
+
...salt && { salt }
|
|
154
|
+
};
|
|
146
155
|
await saveSession(session);
|
|
147
156
|
emitter.emit("change", session);
|
|
148
157
|
return session;
|
package/dist/auth.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.mjs","names":[],"sources":["../src/auth.ts"],"sourcesContent":["// Copyright (c) The Social Proof Foundation, LLC.\n// SPDX-License-Identifier: Apache-2.0\n\nimport { mitt, type Emitter } from '@socialproof/utils';\nimport type {\n\tMySocialAuthConfig,\n\tSignInOptions,\n\tSession,\n\tAuthStateChangeCallback,\n} from './types.js';\nimport { createStorage, redirectStorage, SESSION_KEY, REDIRECT_STATE_PREFIX } from './storage.js';\nimport { openAuthPopup } from './popup.js';\nimport { exchangeCode, refreshTokens, logout, fetchRequestId } from './exchange.js';\nimport { generateState, generateNonce } from './pkce.js';\n\ntype AuthEvents = { change: Session | null };\n\nexport interface MySocialAuth {\n\tsignIn(options?: SignInOptions): Promise<Session>;\n\tsignOut(): Promise<void>;\n\tgetSession(): Promise<Session | null>;\n\trefresh(): Promise<Session | null>;\n\thandleRedirectCallback(url?: string): Promise<Session>;\n\tonAuthStateChange(callback: AuthStateChangeCallback): () => void;\n}\n\nfunction getReturnOrigin(redirectUri: string): string {\n\ttry {\n\t\tconst u = new URL(redirectUri);\n\t\treturn `${u.protocol}//${u.host}`;\n\t} catch {\n\t\treturn '';\n\t}\n}\n\nexport function createAuth(config: MySocialAuthConfig): MySocialAuth {\n\tconst storage = createStorage(config.storage);\n\tconst emitter: Emitter<AuthEvents> = mitt<AuthEvents>();\n\n\tlet cachedSession: Session | null = null;\n\n\tasync function loadSession(): Promise<Session | null> {\n\t\tconst raw = storage.get(SESSION_KEY);\n\t\tif (!raw) return null;\n\t\ttry {\n\t\t\tconst session = JSON.parse(raw) as Session;\n\t\t\tif (session.expires_at && session.expires_at > Date.now()) {\n\t\t\t\treturn session;\n\t\t\t}\n\t\t\tif (session.refresh_token) {\n\t\t\t\tconst refreshed = await refreshTokens(config.apiBaseUrl, session.refresh_token);\n\t\t\t\tawait saveSession(refreshed);\n\t\t\t\temitter.emit('change', refreshed);\n\t\t\t\treturn refreshed;\n\t\t\t}\n\t\t\tstorage.remove(SESSION_KEY);\n\t\t\tcachedSession = null;\n\t\t\temitter.emit('change', null);\n\t\t\treturn null;\n\t\t} catch {\n\t\t\tstorage.remove(SESSION_KEY);\n\t\t\tcachedSession = null;\n\t\t\treturn null;\n\t\t}\n\t}\n\n\tasync function saveSession(session: Session): Promise<void> {\n\t\tcachedSession = session;\n\t\tstorage.set(SESSION_KEY, JSON.stringify(session));\n\t}\n\n\tasync function clearSession(): Promise<void> {\n\t\tcachedSession = null;\n\t\tstorage.remove(SESSION_KEY);\n\t}\n\n\treturn {\n\t\tasync signIn(options: SignInOptions = {}) {\n\t\t\tconst mode = options.mode ?? 'popup';\n\t\t\tconst provider = options.provider;\n\n\t\t\tif (mode === 'popup') {\n\t\t\t\tconst session = await openAuthPopup({\n\t\t\t\t\tapiBaseUrl: config.apiBaseUrl,\n\t\t\t\t\tauthOrigin: config.authOrigin,\n\t\t\t\t\tclientId: config.clientId,\n\t\t\t\t\tredirectUri: config.redirectUri,\n\t\t\t\t\tprovider,\n\t\t\t\t\ttimeout: config.popupTimeout ?? 120_000,\n\t\t\t\t\tuseRequestId: config.useRequestId ?? false,\n\t\t\t\t});\n\t\t\t\tawait saveSession(session);\n\t\t\t\temitter.emit('change', session);\n\t\t\t\treturn session;\n\t\t\t}\n\n\t\t\t// Redirect mode\n\t\t\tconst state = generateState();\n\t\t\tconst nonce = generateNonce();\n\t\t\tconst returnOrigin = getReturnOrigin(config.redirectUri);\n\n\t\t\tlet requestId: string | undefined;\n\t\t\tif (config.useRequestId) {\n\t\t\t\trequestId = await fetchRequestId(config.apiBaseUrl, {\n\t\t\t\t\tclient_id: config.clientId,\n\t\t\t\t\tredirect_uri: config.redirectUri,\n\t\t\t\t\treturn_origin: returnOrigin,\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tconst redirectState = {\n\t\t\t\tstate,\n\t\t\t\tnonce,\n\t\t\t\trequestId,\n\t\t\t};\n\t\t\tconst key = `${REDIRECT_STATE_PREFIX}${state}`;\n\t\t\tredirectStorage.set(key, JSON.stringify(redirectState));\n\n\t\t\tconst params = new URLSearchParams({\n\t\t\t\tclient_id: config.clientId,\n\t\t\t\tredirect_uri: config.redirectUri,\n\t\t\t\tstate,\n\t\t\t\tnonce,\n\t\t\t\treturn_origin: returnOrigin,\n\t\t\t\tmode: 'redirect',\n\t\t\t\tprovider: provider ?? '',\n\t\t\t\tcode_challenge_method: 'S256',\n\t\t\t});\n\t\t\tif (requestId) params.set('request_id', requestId);\n\n\t\t\tconst loginUrl = `${config.authOrigin.replace(/\\/$/, '')}/login?${params.toString()}`;\n\t\t\twindow.location.href = loginUrl;\n\n\t\t\treturn new Promise<Session>(() => {}); // Never resolves; page navigates away\n\t\t},\n\n\t\tasync signOut() {\n\t\t\ttry {\n\t\t\t\tawait logout(config.apiBaseUrl);\n\t\t\t} catch {\n\t\t\t\t// Best-effort; clear local state regardless\n\t\t\t}\n\t\t\tawait clearSession();\n\t\t\temitter.emit('change', null);\n\t\t},\n\n\t\tasync getSession() {\n\t\t\tif (cachedSession && cachedSession.expires_at > Date.now()) {\n\t\t\t\treturn cachedSession;\n\t\t\t}\n\t\t\treturn loadSession();\n\t\t},\n\n\t\tasync refresh() {\n\t\t\tconst session = await loadSession();\n\t\t\tif (!session?.refresh_token) return null;\n\t\t\tconst refreshed = await refreshTokens(config.apiBaseUrl, session.refresh_token);\n\t\t\tawait saveSession(refreshed);\n\t\t\temitter.emit('change', refreshed);\n\t\t\treturn refreshed;\n\t\t},\n\n\t\tasync handleRedirectCallback(url?: string) {\n\t\t\tconst targetUrl = url ?? (typeof window !== 'undefined' ? window.location.href : '');\n\t\t\tconst parsed = new URL(targetUrl);\n\t\t\tconst code = parsed.searchParams.get('code');\n\t\t\tconst state = parsed.searchParams.get('state');\n\t\t\tconst nonce = parsed.searchParams.get('nonce');\n\t\t\tconst requestId = parsed.searchParams.get('request_id') ?? undefined;\n\n\t\t\tif (!code || !state || !nonce) {\n\t\t\t\tthrow new Error('Missing code, state, or nonce in callback URL');\n\t\t\t}\n\n\t\t\tconst key = `${REDIRECT_STATE_PREFIX}${state}`;\n\t\t\tconst raw = redirectStorage.get(key);\n\t\t\tif (!raw) {\n\t\t\t\tthrow new Error('No matching redirect state found. Session may have expired.');\n\t\t\t}\n\n\t\t\tconst redirectState = JSON.parse(raw) as {\n\t\t\t\tstate: string;\n\t\t\t\tnonce: string;\n\t\t\t\trequestId?: string;\n\t\t\t};\n\n\t\t\tif (redirectState.state !== state || redirectState.nonce !== nonce) {\n\t\t\t\tredirectStorage.remove(key);\n\t\t\t\tthrow new Error('State or nonce mismatch');\n\t\t\t}\n\t\t\tif (requestId && redirectState.requestId !== requestId) {\n\t\t\t\tredirectStorage.remove(key);\n\t\t\t\tthrow new Error('Request ID mismatch');\n\t\t\t}\n\n\t\t\tredirectStorage.remove(key);\n\n\t\t\tconst session = await exchangeCode(config.apiBaseUrl, {\n\t\t\t\tcode,\n\t\t\t\tredirect_uri: config.redirectUri,\n\t\t\t\tstate,\n\t\t\t\tnonce,\n\t\t\t\trequest_id: requestId ?? redirectState.requestId,\n\t\t\t});\n\n\t\t\tawait saveSession(session);\n\t\t\temitter.emit('change', session);\n\t\t\treturn session;\n\t\t},\n\n\t\tonAuthStateChange(callback: AuthStateChangeCallback) {\n\t\t\temitter.on('change', callback);\n\t\t\treturn () => emitter.off('change', callback);\n\t\t},\n\t};\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"auth.mjs","names":[],"sources":["../src/auth.ts"],"sourcesContent":["// Copyright (c) The Social Proof Foundation, LLC.\n// SPDX-License-Identifier: Apache-2.0\n\nimport { mitt, type Emitter } from '@socialproof/utils';\nimport type {\n\tMySocialAuthConfig,\n\tSignInOptions,\n\tSession,\n\tAuthStateChangeCallback,\n} from './types.js';\nimport { createStorage, redirectStorage, SESSION_KEY, REDIRECT_STATE_PREFIX } from './storage.js';\nimport { openAuthPopup } from './popup.js';\nimport { refreshTokens, logout, fetchRequestId } from './exchange.js';\nimport { generateState, generateNonce } from './pkce.js';\n\ntype AuthEvents = { change: Session | null };\n\nexport interface MySocialAuth {\n\tsignIn(options?: SignInOptions): Promise<Session>;\n\tsignOut(): Promise<void>;\n\tgetSession(): Promise<Session | null>;\n\trefresh(): Promise<Session | null>;\n\thandleRedirectCallback(url?: string): Promise<Session>;\n\tonAuthStateChange(callback: AuthStateChangeCallback): () => void;\n}\n\nfunction getReturnOrigin(redirectUri: string): string {\n\ttry {\n\t\tconst u = new URL(redirectUri);\n\t\treturn `${u.protocol}//${u.host}`;\n\t} catch {\n\t\treturn '';\n\t}\n}\n\nexport function createAuth(config: MySocialAuthConfig): MySocialAuth {\n\tconst storage = createStorage(config.storage);\n\tconst emitter: Emitter<AuthEvents> = mitt<AuthEvents>();\n\n\tlet cachedSession: Session | null = null;\n\n\tasync function loadSession(): Promise<Session | null> {\n\t\tconst raw = storage.get(SESSION_KEY);\n\t\tif (!raw) return null;\n\t\ttry {\n\t\t\tconst session = JSON.parse(raw) as Session;\n\t\t\tif (session.expires_at && session.expires_at > Date.now()) {\n\t\t\t\treturn session;\n\t\t\t}\n\t\t\tif (session.refresh_token) {\n\t\t\t\tconst refreshed = await refreshTokens(config.apiBaseUrl, session.refresh_token);\n\t\t\t\tawait saveSession(refreshed);\n\t\t\t\temitter.emit('change', refreshed);\n\t\t\t\treturn refreshed;\n\t\t\t}\n\t\t\tstorage.remove(SESSION_KEY);\n\t\t\tcachedSession = null;\n\t\t\temitter.emit('change', null);\n\t\t\treturn null;\n\t\t} catch {\n\t\t\tstorage.remove(SESSION_KEY);\n\t\t\tcachedSession = null;\n\t\t\treturn null;\n\t\t}\n\t}\n\n\tasync function saveSession(session: Session): Promise<void> {\n\t\tcachedSession = session;\n\t\tstorage.set(SESSION_KEY, JSON.stringify(session));\n\t}\n\n\tasync function clearSession(): Promise<void> {\n\t\tcachedSession = null;\n\t\tstorage.remove(SESSION_KEY);\n\t}\n\n\treturn {\n\t\tasync signIn(options: SignInOptions = {}) {\n\t\t\tconst mode = options.mode ?? 'popup';\n\t\t\tconst provider = options.provider;\n\n\t\t\tif (mode === 'popup') {\n\t\t\t\tconst session = await openAuthPopup({\n\t\t\t\t\tapiBaseUrl: config.apiBaseUrl,\n\t\t\t\t\tauthOrigin: config.authOrigin,\n\t\t\t\t\tclientId: config.clientId,\n\t\t\t\t\tredirectUri: config.redirectUri,\n\t\t\t\t\tprovider,\n\t\t\t\t\ttimeout: config.popupTimeout ?? 120_000,\n\t\t\t\t\tuseRequestId: config.useRequestId ?? false,\n\t\t\t\t});\n\t\t\t\tawait saveSession(session);\n\t\t\t\temitter.emit('change', session);\n\t\t\t\treturn session;\n\t\t\t}\n\n\t\t\t// Redirect mode\n\t\t\tconst state = generateState();\n\t\t\tconst nonce = generateNonce();\n\t\t\tconst returnOrigin = getReturnOrigin(config.redirectUri);\n\n\t\t\tlet requestId: string | undefined;\n\t\t\tif (config.useRequestId) {\n\t\t\t\trequestId = await fetchRequestId(config.apiBaseUrl, {\n\t\t\t\t\tclient_id: config.clientId,\n\t\t\t\t\tredirect_uri: config.redirectUri,\n\t\t\t\t\treturn_origin: returnOrigin,\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tconst redirectState = {\n\t\t\t\tstate,\n\t\t\t\tnonce,\n\t\t\t\trequestId,\n\t\t\t};\n\t\t\tconst key = `${REDIRECT_STATE_PREFIX}${state}`;\n\t\t\tredirectStorage.set(key, JSON.stringify(redirectState));\n\n\t\t\tconst params = new URLSearchParams({\n\t\t\t\tclient_id: config.clientId,\n\t\t\t\tredirect_uri: config.redirectUri,\n\t\t\t\tstate,\n\t\t\t\tnonce,\n\t\t\t\treturn_origin: returnOrigin,\n\t\t\t\tmode: 'redirect',\n\t\t\t\tprovider: provider ?? 'none',\n\t\t\t\tcode_challenge_method: 'S256',\n\t\t\t});\n\t\t\tif (requestId) params.set('request_id', requestId);\n\n\t\t\tconst loginUrl = `${config.authOrigin.replace(/\\/$/, '')}/login?${params.toString()}`;\n\t\t\twindow.location.href = loginUrl;\n\n\t\t\treturn new Promise<Session>(() => {}); // Never resolves; page navigates away\n\t\t},\n\n\t\tasync signOut() {\n\t\t\ttry {\n\t\t\t\tawait logout(config.apiBaseUrl);\n\t\t\t} catch {\n\t\t\t\t// Best-effort; clear local state regardless\n\t\t\t}\n\t\t\tawait clearSession();\n\t\t\temitter.emit('change', null);\n\t\t},\n\n\t\tasync getSession() {\n\t\t\tif (cachedSession && cachedSession.expires_at > Date.now()) {\n\t\t\t\treturn cachedSession;\n\t\t\t}\n\t\t\treturn loadSession();\n\t\t},\n\n\t\tasync refresh() {\n\t\t\tconst session = await loadSession();\n\t\t\tif (!session?.refresh_token) return null;\n\t\t\tconst refreshed = await refreshTokens(config.apiBaseUrl, session.refresh_token);\n\t\t\tawait saveSession(refreshed);\n\t\t\temitter.emit('change', refreshed);\n\t\t\treturn refreshed;\n\t\t},\n\n\t\tasync handleRedirectCallback(url?: string) {\n\t\t\tconst targetUrl = url ?? (typeof window !== 'undefined' ? window.location.href : '');\n\t\t\tconst parsed = new URL(targetUrl);\n\t\t\tconst code = parsed.searchParams.get('code');\n\t\t\tconst state = parsed.searchParams.get('state');\n\t\t\tconst nonce = parsed.searchParams.get('nonce');\n\t\t\tconst requestId = parsed.searchParams.get('request_id') ?? undefined;\n\t\t\tconst salt = parsed.searchParams.get('salt') ?? undefined;\n\t\t\tconst accessToken = parsed.searchParams.get('access_token') ?? undefined;\n\t\t\tconst refreshToken = parsed.searchParams.get('refresh_token') ?? undefined;\n\t\t\tconst expiresAtParam = parsed.searchParams.get('expires_at');\n\t\t\tconst userParam = parsed.searchParams.get('user');\n\n\t\t\tif (!code || !state || !nonce) {\n\t\t\t\tthrow new Error('Missing code, state, or nonce in callback URL');\n\t\t\t}\n\n\t\t\tconst key = `${REDIRECT_STATE_PREFIX}${state}`;\n\t\t\tconst raw = redirectStorage.get(key);\n\t\t\tif (!raw) {\n\t\t\t\tthrow new Error('No matching redirect state found. Session may have expired.');\n\t\t\t}\n\n\t\t\tconst redirectState = JSON.parse(raw) as {\n\t\t\t\tstate: string;\n\t\t\t\tnonce: string;\n\t\t\t\trequestId?: string;\n\t\t\t};\n\n\t\t\tif (redirectState.state !== state || redirectState.nonce !== nonce) {\n\t\t\t\tredirectStorage.remove(key);\n\t\t\t\tthrow new Error('State or nonce mismatch');\n\t\t\t}\n\t\t\tif (requestId && redirectState.requestId !== requestId) {\n\t\t\t\tredirectStorage.remove(key);\n\t\t\t\tthrow new Error('Request ID mismatch');\n\t\t\t}\n\n\t\t\tredirectStorage.remove(key);\n\n\t\t\tlet user: Session['user'] = {};\n\t\t\tif (userParam) {\n\t\t\t\ttry {\n\t\t\t\t\tuser = JSON.parse(decodeURIComponent(userParam)) as Session['user'];\n\t\t\t\t} catch {\n\t\t\t\t\t// Ignore invalid user param\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst session: Session = {\n\t\t\t\taccess_token: accessToken ?? code,\n\t\t\t\trefresh_token: refreshToken,\n\t\t\t\tuser,\n\t\t\t\texpires_at: expiresAtParam ? Number(expiresAtParam) : Date.now() + 3600_000,\n\t\t\t\t...(salt && { salt }),\n\t\t\t};\n\n\t\t\tawait saveSession(session);\n\t\t\temitter.emit('change', session);\n\t\t\treturn session;\n\t\t},\n\n\t\tonAuthStateChange(callback: AuthStateChangeCallback) {\n\t\t\temitter.on('change', callback);\n\t\t\treturn () => emitter.off('change', callback);\n\t\t},\n\t};\n}\n"],"mappings":"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"}
|
package/dist/exchange.mjs
CHANGED
|
@@ -13,27 +13,6 @@ async function fetchRequestId(apiBaseUrl, body) {
|
|
|
13
13
|
}
|
|
14
14
|
return (await res.json()).request_id;
|
|
15
15
|
}
|
|
16
|
-
/** Exchange auth code for tokens */
|
|
17
|
-
async function exchangeCode(apiBaseUrl, body) {
|
|
18
|
-
const url = `${apiBaseUrl.replace(/\/$/, "")}/auth/exchange`;
|
|
19
|
-
const res = await fetch(url, {
|
|
20
|
-
method: "POST",
|
|
21
|
-
headers: { "Content-Type": "application/json" },
|
|
22
|
-
body: JSON.stringify(body)
|
|
23
|
-
});
|
|
24
|
-
if (!res.ok) {
|
|
25
|
-
const text = await res.text();
|
|
26
|
-
throw new Error(`Token exchange failed: ${res.status} ${text}`);
|
|
27
|
-
}
|
|
28
|
-
const data = await res.json();
|
|
29
|
-
const expires_at = Date.now() + data.expires_in * 1e3;
|
|
30
|
-
return {
|
|
31
|
-
access_token: data.access_token,
|
|
32
|
-
refresh_token: data.refresh_token,
|
|
33
|
-
expires_at,
|
|
34
|
-
user: data.user
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
16
|
/** Refresh access token using refresh_token */
|
|
38
17
|
async function refreshTokens(apiBaseUrl, refreshToken) {
|
|
39
18
|
const url = `${apiBaseUrl.replace(/\/$/, "")}/auth/refresh`;
|
|
@@ -69,5 +48,5 @@ async function logout(apiBaseUrl) {
|
|
|
69
48
|
}
|
|
70
49
|
|
|
71
50
|
//#endregion
|
|
72
|
-
export {
|
|
51
|
+
export { fetchRequestId, logout, refreshTokens };
|
|
73
52
|
//# sourceMappingURL=exchange.mjs.map
|
package/dist/exchange.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"exchange.mjs","names":[],"sources":["../src/exchange.ts"],"sourcesContent":["// Copyright (c) The Social Proof Foundation, LLC.\n// SPDX-License-Identifier: Apache-2.0\n\nimport type {\n\tAuthRequestRequest,\n\tAuthRequestResponse,\n\tExchangeRequest,\n\tExchangeResponse,\n\tSession,\n} from './types.js';\n\n/** Fetch request_id from backend (optional flow). Backend validates return_origin against allowlist. */\nexport async function fetchRequestId(\n\tapiBaseUrl: string,\n\tbody: AuthRequestRequest,\n): Promise<string> {\n\tconst url = `${apiBaseUrl.replace(/\\/$/, '')}/auth/request`;\n\tconst res = await fetch(url, {\n\t\tmethod: 'POST',\n\t\theaders: { 'Content-Type': 'application/json' },\n\t\tbody: JSON.stringify(body),\n\t});\n\tif (!res.ok) {\n\t\tconst text = await res.text();\n\t\tthrow new Error(`Auth request failed: ${res.status} ${text}`);\n\t}\n\tconst data = (await res.json()) as AuthRequestResponse;\n\treturn data.request_id;\n}\n\n/** Exchange auth code for tokens */\nexport async function exchangeCode(apiBaseUrl: string, body: ExchangeRequest): Promise<Session> {\n\tconst url = `${apiBaseUrl.replace(/\\/$/, '')}/auth/exchange`;\n\tconst res = await fetch(url, {\n\t\tmethod: 'POST',\n\t\theaders: { 'Content-Type': 'application/json' },\n\t\tbody: JSON.stringify(body),\n\t});\n\tif (!res.ok) {\n\t\tconst text = await res.text();\n\t\tthrow new Error(`Token exchange failed: ${res.status} ${text}`);\n\t}\n\tconst data = (await res.json()) as ExchangeResponse;\n\tconst expires_at = Date.now() + data.expires_in * 1000;\n\treturn {\n\t\taccess_token: data.access_token,\n\t\trefresh_token: data.refresh_token,\n\t\texpires_at,\n\t\tuser: data.user,\n\t};\n}\n\n/** Refresh access token using refresh_token */\nexport async function refreshTokens(apiBaseUrl: string, refreshToken: string): Promise<Session> {\n\tconst url = `${apiBaseUrl.replace(/\\/$/, '')}/auth/refresh`;\n\tconst res = await fetch(url, {\n\t\tmethod: 'POST',\n\t\theaders: { 'Content-Type': 'application/json' },\n\t\tbody: JSON.stringify({ refresh_token: refreshToken }),\n\t});\n\tif (!res.ok) {\n\t\tconst text = await res.text();\n\t\tthrow new Error(`Token refresh failed: ${res.status} ${text}`);\n\t}\n\tconst data = (await res.json()) as ExchangeResponse;\n\tconst expires_at = Date.now() + data.expires_in * 1000;\n\treturn {\n\t\taccess_token: data.access_token,\n\t\trefresh_token: data.refresh_token ?? refreshToken,\n\t\texpires_at,\n\t\tuser: data.user,\n\t};\n}\n\n/** Logout / invalidate session on backend */\nexport async function logout(apiBaseUrl: string): Promise<void> {\n\tconst url = `${apiBaseUrl.replace(/\\/$/, '')}/auth/logout`;\n\tconst res = await fetch(url, {\n\t\tmethod: 'POST',\n\t\theaders: { 'Content-Type': 'application/json' },\n\t});\n\tif (!res.ok) {\n\t\tconst text = await res.text();\n\t\tthrow new Error(`Logout failed: ${res.status} ${text}`);\n\t}\n}\n"],"mappings":";;AAYA,eAAsB,eACrB,YACA,MACkB;CAClB,MAAM,MAAM,GAAG,WAAW,QAAQ,OAAO,GAAG,CAAC;CAC7C,MAAM,MAAM,MAAM,MAAM,KAAK;EAC5B,QAAQ;EACR,SAAS,EAAE,gBAAgB,oBAAoB;EAC/C,MAAM,KAAK,UAAU,KAAK;EAC1B,CAAC;AACF,KAAI,CAAC,IAAI,IAAI;EACZ,MAAM,OAAO,MAAM,IAAI,MAAM;AAC7B,QAAM,IAAI,MAAM,wBAAwB,IAAI,OAAO,GAAG,OAAO;;AAG9D,SADc,MAAM,IAAI,MAAM,EAClB;;;
|
|
1
|
+
{"version":3,"file":"exchange.mjs","names":[],"sources":["../src/exchange.ts"],"sourcesContent":["// Copyright (c) The Social Proof Foundation, LLC.\n// SPDX-License-Identifier: Apache-2.0\n\nimport type {\n\tAuthRequestRequest,\n\tAuthRequestResponse,\n\tExchangeRequest,\n\tExchangeResponse,\n\tSession,\n} from './types.js';\n\n/** Fetch request_id from backend (optional flow). Backend validates return_origin against allowlist. */\nexport async function fetchRequestId(\n\tapiBaseUrl: string,\n\tbody: AuthRequestRequest,\n): Promise<string> {\n\tconst url = `${apiBaseUrl.replace(/\\/$/, '')}/auth/request`;\n\tconst res = await fetch(url, {\n\t\tmethod: 'POST',\n\t\theaders: { 'Content-Type': 'application/json' },\n\t\tbody: JSON.stringify(body),\n\t});\n\tif (!res.ok) {\n\t\tconst text = await res.text();\n\t\tthrow new Error(`Auth request failed: ${res.status} ${text}`);\n\t}\n\tconst data = (await res.json()) as AuthRequestResponse;\n\treturn data.request_id;\n}\n\n/** Exchange auth code for tokens */\nexport async function exchangeCode(apiBaseUrl: string, body: ExchangeRequest): Promise<Session> {\n\tconst url = `${apiBaseUrl.replace(/\\/$/, '')}/auth/exchange`;\n\tconst res = await fetch(url, {\n\t\tmethod: 'POST',\n\t\theaders: { 'Content-Type': 'application/json' },\n\t\tbody: JSON.stringify(body),\n\t});\n\tif (!res.ok) {\n\t\tconst text = await res.text();\n\t\tthrow new Error(`Token exchange failed: ${res.status} ${text}`);\n\t}\n\tconst data = (await res.json()) as ExchangeResponse;\n\tconst expires_at = Date.now() + data.expires_in * 1000;\n\treturn {\n\t\taccess_token: data.access_token,\n\t\trefresh_token: data.refresh_token,\n\t\texpires_at,\n\t\tuser: data.user,\n\t};\n}\n\n/** Refresh access token using refresh_token */\nexport async function refreshTokens(apiBaseUrl: string, refreshToken: string): Promise<Session> {\n\tconst url = `${apiBaseUrl.replace(/\\/$/, '')}/auth/refresh`;\n\tconst res = await fetch(url, {\n\t\tmethod: 'POST',\n\t\theaders: { 'Content-Type': 'application/json' },\n\t\tbody: JSON.stringify({ refresh_token: refreshToken }),\n\t});\n\tif (!res.ok) {\n\t\tconst text = await res.text();\n\t\tthrow new Error(`Token refresh failed: ${res.status} ${text}`);\n\t}\n\tconst data = (await res.json()) as ExchangeResponse;\n\tconst expires_at = Date.now() + data.expires_in * 1000;\n\treturn {\n\t\taccess_token: data.access_token,\n\t\trefresh_token: data.refresh_token ?? refreshToken,\n\t\texpires_at,\n\t\tuser: data.user,\n\t};\n}\n\n/** Logout / invalidate session on backend */\nexport async function logout(apiBaseUrl: string): Promise<void> {\n\tconst url = `${apiBaseUrl.replace(/\\/$/, '')}/auth/logout`;\n\tconst res = await fetch(url, {\n\t\tmethod: 'POST',\n\t\theaders: { 'Content-Type': 'application/json' },\n\t});\n\tif (!res.ok) {\n\t\tconst text = await res.text();\n\t\tthrow new Error(`Logout failed: ${res.status} ${text}`);\n\t}\n}\n"],"mappings":";;AAYA,eAAsB,eACrB,YACA,MACkB;CAClB,MAAM,MAAM,GAAG,WAAW,QAAQ,OAAO,GAAG,CAAC;CAC7C,MAAM,MAAM,MAAM,MAAM,KAAK;EAC5B,QAAQ;EACR,SAAS,EAAE,gBAAgB,oBAAoB;EAC/C,MAAM,KAAK,UAAU,KAAK;EAC1B,CAAC;AACF,KAAI,CAAC,IAAI,IAAI;EACZ,MAAM,OAAO,MAAM,IAAI,MAAM;AAC7B,QAAM,IAAI,MAAM,wBAAwB,IAAI,OAAO,GAAG,OAAO;;AAG9D,SADc,MAAM,IAAI,MAAM,EAClB;;;AA0Bb,eAAsB,cAAc,YAAoB,cAAwC;CAC/F,MAAM,MAAM,GAAG,WAAW,QAAQ,OAAO,GAAG,CAAC;CAC7C,MAAM,MAAM,MAAM,MAAM,KAAK;EAC5B,QAAQ;EACR,SAAS,EAAE,gBAAgB,oBAAoB;EAC/C,MAAM,KAAK,UAAU,EAAE,eAAe,cAAc,CAAC;EACrD,CAAC;AACF,KAAI,CAAC,IAAI,IAAI;EACZ,MAAM,OAAO,MAAM,IAAI,MAAM;AAC7B,QAAM,IAAI,MAAM,yBAAyB,IAAI,OAAO,GAAG,OAAO;;CAE/D,MAAM,OAAQ,MAAM,IAAI,MAAM;CAC9B,MAAM,aAAa,KAAK,KAAK,GAAG,KAAK,aAAa;AAClD,QAAO;EACN,cAAc,KAAK;EACnB,eAAe,KAAK,iBAAiB;EACrC;EACA,MAAM,KAAK;EACX;;;AAIF,eAAsB,OAAO,YAAmC;CAC/D,MAAM,MAAM,GAAG,WAAW,QAAQ,OAAO,GAAG,CAAC;CAC7C,MAAM,MAAM,MAAM,MAAM,KAAK;EAC5B,QAAQ;EACR,SAAS,EAAE,gBAAgB,oBAAoB;EAC/C,CAAC;AACF,KAAI,CAAC,IAAI,IAAI;EACZ,MAAM,OAAO,MAAM,IAAI,MAAM;AAC7B,QAAM,IAAI,MAAM,kBAAkB,IAAI,OAAO,GAAG,OAAO"}
|
package/dist/popup.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { AuthTimeoutError, InvalidStateError, PopupBlockedError, PopupClosedError } from "./errors.mjs";
|
|
2
2
|
import { generateNonce, generateState } from "./pkce.mjs";
|
|
3
3
|
import { getPopupFeatures } from "./popup-utils.mjs";
|
|
4
|
-
import {
|
|
4
|
+
import { fetchRequestId } from "./exchange.mjs";
|
|
5
5
|
|
|
6
6
|
//#region src/popup.ts
|
|
7
7
|
/** Get return_origin from redirectUri (e.g. https://dripdrop.social/auth/callback -> https://dripdrop.social) */
|
|
@@ -38,7 +38,7 @@ async function openAuthPopup(options) {
|
|
|
38
38
|
nonce,
|
|
39
39
|
return_origin: returnOrigin,
|
|
40
40
|
mode: "popup",
|
|
41
|
-
provider: provider ?? "",
|
|
41
|
+
provider: provider ?? "none",
|
|
42
42
|
code_challenge_method: "S256"
|
|
43
43
|
});
|
|
44
44
|
if (requestId) params.set("request_id", requestId);
|
|
@@ -78,13 +78,13 @@ async function openAuthPopup(options) {
|
|
|
78
78
|
try {
|
|
79
79
|
popup.close();
|
|
80
80
|
} catch {}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
})
|
|
81
|
+
resolve({
|
|
82
|
+
access_token: msg.access_token ?? msg.code,
|
|
83
|
+
refresh_token: msg.refresh_token,
|
|
84
|
+
user: msg.user ?? {},
|
|
85
|
+
expires_at: msg.expires_at ?? Date.now() + 36e5,
|
|
86
|
+
...msg.salt && { salt: msg.salt }
|
|
87
|
+
});
|
|
88
88
|
} else if (data.type === "MYSOCIAL_AUTH_ERROR") {
|
|
89
89
|
const msg = data;
|
|
90
90
|
cleanup();
|
package/dist/popup.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"popup.mjs","names":[],"sources":["../src/popup.ts"],"sourcesContent":["// Copyright (c) The Social Proof Foundation, LLC.\n// SPDX-License-Identifier: Apache-2.0\n\nimport type { AuthProvider, AuthResultMessage, AuthErrorMessage } from './types.js';\nimport {\n\tAuthTimeoutError,\n\tInvalidStateError,\n\tPopupBlockedError,\n\tPopupClosedError,\n} from './errors.js';\nimport { generateNonce, generateState } from './pkce.js';\nimport { getPopupFeatures } from './popup-utils.js';\nimport {
|
|
1
|
+
{"version":3,"file":"popup.mjs","names":[],"sources":["../src/popup.ts"],"sourcesContent":["// Copyright (c) The Social Proof Foundation, LLC.\n// SPDX-License-Identifier: Apache-2.0\n\nimport type { AuthProvider, AuthResultMessage, AuthErrorMessage } from './types.js';\nimport {\n\tAuthTimeoutError,\n\tInvalidStateError,\n\tPopupBlockedError,\n\tPopupClosedError,\n} from './errors.js';\nimport { generateNonce, generateState } from './pkce.js';\nimport { getPopupFeatures } from './popup-utils.js';\nimport { fetchRequestId } from './exchange.js';\nimport type { Session } from './types.js';\n\n/** Get return_origin from redirectUri (e.g. https://dripdrop.social/auth/callback -> https://dripdrop.social) */\nfunction getReturnOrigin(redirectUri: string): string {\n\ttry {\n\t\tconst u = new URL(redirectUri);\n\t\treturn `${u.protocol}//${u.host}`;\n\t} catch {\n\t\treturn '';\n\t}\n}\n\nexport interface OpenPopupOptions {\n\tapiBaseUrl: string;\n\tauthOrigin: string;\n\tclientId: string;\n\tredirectUri: string;\n\tprovider?: AuthProvider;\n\ttimeout?: number;\n\tuseRequestId?: boolean;\n}\n\n/**\n * Open auth popup and wait for postMessage. Safari: open with about:blank first, then set location.\n * Returns session on success. Rejects on error, timeout, or user close.\n */\nexport async function openAuthPopup(options: OpenPopupOptions): Promise<Session> {\n\tconst {\n\t\tapiBaseUrl,\n\t\tauthOrigin,\n\t\tclientId,\n\t\tredirectUri,\n\t\tprovider,\n\t\ttimeout = 120_000,\n\t\tuseRequestId = false,\n\t} = options;\n\n\tconst state = generateState();\n\tconst nonce = generateNonce();\n\tconst returnOrigin =\n\t\ttypeof window !== 'undefined' ? window.location.origin : getReturnOrigin(redirectUri);\n\n\tlet requestId: string | undefined;\n\tif (useRequestId) {\n\t\trequestId = await fetchRequestId(apiBaseUrl, {\n\t\t\tclient_id: clientId,\n\t\t\tredirect_uri: redirectUri,\n\t\t\treturn_origin: returnOrigin,\n\t\t});\n\t}\n\n\t// Open popup immediately (Safari requires user gesture). Use about:blank, then set location.\n\tconst features = getPopupFeatures(420, 720);\n\tconst popup = window.open('about:blank', '_blank', features);\n\n\tif (!popup || popup.closed) {\n\t\tthrow new PopupBlockedError();\n\t}\n\n\tconst params = new URLSearchParams({\n\t\tclient_id: clientId,\n\t\tredirect_uri: redirectUri,\n\t\tstate,\n\t\tnonce,\n\t\treturn_origin: returnOrigin,\n\t\tmode: 'popup',\n\t\tprovider: provider ?? 'none',\n\t\tcode_challenge_method: 'S256',\n\t});\n\tif (requestId) params.set('request_id', requestId);\n\n\tconst loginUrl = `${authOrigin.replace(/\\/$/, '')}/login?${params.toString()}`;\n\n\t// Set location after popup is open (Safari-safe)\n\tpopup.location.href = loginUrl;\n\n\treturn new Promise<Session>((resolve, reject) => {\n\t\tlet timeoutId: ReturnType<typeof setTimeout> | null = null;\n\t\tlet closedCheckId: ReturnType<typeof setInterval> | null = null;\n\n\t\tconst cleanup = () => {\n\t\t\tif (timeoutId) clearTimeout(timeoutId);\n\t\t\tif (closedCheckId) clearInterval(closedCheckId);\n\t\t\twindow.removeEventListener('message', handler);\n\t\t};\n\n\t\tconst handler = (event: MessageEvent) => {\n\t\t\tif (event.origin !== authOrigin) return;\n\t\t\tif (event.source !== popup) return;\n\n\t\t\tconst data = event.data;\n\t\t\tif (!data || typeof data !== 'object' || !data.type) return;\n\n\t\t\tif (data.type === 'MYSOCIAL_AUTH_RESULT') {\n\t\t\t\tconst msg = data as AuthResultMessage;\n\t\t\t\tif (msg.state !== state || msg.nonce !== nonce) {\n\t\t\t\t\tcleanup();\n\t\t\t\t\treject(new InvalidStateError());\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (msg.clientId !== clientId) {\n\t\t\t\t\tcleanup();\n\t\t\t\t\treject(new InvalidStateError());\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (requestId && msg.requestId !== requestId) {\n\t\t\t\t\tcleanup();\n\t\t\t\t\treject(new InvalidStateError());\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tcleanup();\n\t\t\t\ttry {\n\t\t\t\t\tpopup.close();\n\t\t\t\t} catch {\n\t\t\t\t\t// Popup may already be closed by auth server\n\t\t\t\t}\n\n\t\t\t\tconst session: Session = {\n\t\t\t\t\taccess_token: msg.access_token ?? msg.code,\n\t\t\t\t\trefresh_token: msg.refresh_token,\n\t\t\t\t\tuser: msg.user ?? {},\n\t\t\t\t\texpires_at: msg.expires_at ?? Date.now() + 3600_000,\n\t\t\t\t\t...(msg.salt && { salt: msg.salt }),\n\t\t\t\t};\n\t\t\t\tresolve(session);\n\t\t\t} else if (data.type === 'MYSOCIAL_AUTH_ERROR') {\n\t\t\t\tconst msg = data as AuthErrorMessage;\n\t\t\t\tcleanup();\n\t\t\t\ttry {\n\t\t\t\t\tpopup.close();\n\t\t\t\t} catch {\n\t\t\t\t\t// Popup may already be closed\n\t\t\t\t}\n\t\t\t\treject(new Error(msg.error ?? 'Authentication failed'));\n\t\t\t}\n\t\t};\n\n\t\twindow.addEventListener('message', handler);\n\n\t\ttimeoutId = setTimeout(() => {\n\t\t\tcleanup();\n\t\t\ttry {\n\t\t\t\tpopup.close();\n\t\t\t} catch {\n\t\t\t\t// Popup may already be closed\n\t\t\t}\n\t\t\treject(new AuthTimeoutError());\n\t\t}, timeout);\n\n\t\tclosedCheckId = setInterval(() => {\n\t\t\tif (popup.closed) {\n\t\t\t\tcleanup();\n\t\t\t\treject(new PopupClosedError());\n\t\t\t}\n\t\t}, 200);\n\t});\n}\n"],"mappings":";;;;;;;AAgBA,SAAS,gBAAgB,aAA6B;AACrD,KAAI;EACH,MAAM,IAAI,IAAI,IAAI,YAAY;AAC9B,SAAO,GAAG,EAAE,SAAS,IAAI,EAAE;SACpB;AACP,SAAO;;;;;;;AAkBT,eAAsB,cAAc,SAA6C;CAChF,MAAM,EACL,YACA,YACA,UACA,aACA,UACA,UAAU,MACV,eAAe,UACZ;CAEJ,MAAM,QAAQ,eAAe;CAC7B,MAAM,QAAQ,eAAe;CAC7B,MAAM,eACL,OAAO,WAAW,cAAc,OAAO,SAAS,SAAS,gBAAgB,YAAY;CAEtF,IAAI;AACJ,KAAI,aACH,aAAY,MAAM,eAAe,YAAY;EAC5C,WAAW;EACX,cAAc;EACd,eAAe;EACf,CAAC;CAIH,MAAM,WAAW,iBAAiB,KAAK,IAAI;CAC3C,MAAM,QAAQ,OAAO,KAAK,eAAe,UAAU,SAAS;AAE5D,KAAI,CAAC,SAAS,MAAM,OACnB,OAAM,IAAI,mBAAmB;CAG9B,MAAM,SAAS,IAAI,gBAAgB;EAClC,WAAW;EACX,cAAc;EACd;EACA;EACA,eAAe;EACf,MAAM;EACN,UAAU,YAAY;EACtB,uBAAuB;EACvB,CAAC;AACF,KAAI,UAAW,QAAO,IAAI,cAAc,UAAU;CAElD,MAAM,WAAW,GAAG,WAAW,QAAQ,OAAO,GAAG,CAAC,SAAS,OAAO,UAAU;AAG5E,OAAM,SAAS,OAAO;AAEtB,QAAO,IAAI,SAAkB,SAAS,WAAW;EAChD,IAAI,YAAkD;EACtD,IAAI,gBAAuD;EAE3D,MAAM,gBAAgB;AACrB,OAAI,UAAW,cAAa,UAAU;AACtC,OAAI,cAAe,eAAc,cAAc;AAC/C,UAAO,oBAAoB,WAAW,QAAQ;;EAG/C,MAAM,WAAW,UAAwB;AACxC,OAAI,MAAM,WAAW,WAAY;AACjC,OAAI,MAAM,WAAW,MAAO;GAE5B,MAAM,OAAO,MAAM;AACnB,OAAI,CAAC,QAAQ,OAAO,SAAS,YAAY,CAAC,KAAK,KAAM;AAErD,OAAI,KAAK,SAAS,wBAAwB;IACzC,MAAM,MAAM;AACZ,QAAI,IAAI,UAAU,SAAS,IAAI,UAAU,OAAO;AAC/C,cAAS;AACT,YAAO,IAAI,mBAAmB,CAAC;AAC/B;;AAED,QAAI,IAAI,aAAa,UAAU;AAC9B,cAAS;AACT,YAAO,IAAI,mBAAmB,CAAC;AAC/B;;AAED,QAAI,aAAa,IAAI,cAAc,WAAW;AAC7C,cAAS;AACT,YAAO,IAAI,mBAAmB,CAAC;AAC/B;;AAGD,aAAS;AACT,QAAI;AACH,WAAM,OAAO;YACN;AAWR,YAPyB;KACxB,cAAc,IAAI,gBAAgB,IAAI;KACtC,eAAe,IAAI;KACnB,MAAM,IAAI,QAAQ,EAAE;KACpB,YAAY,IAAI,cAAc,KAAK,KAAK,GAAG;KAC3C,GAAI,IAAI,QAAQ,EAAE,MAAM,IAAI,MAAM;KAClC,CACe;cACN,KAAK,SAAS,uBAAuB;IAC/C,MAAM,MAAM;AACZ,aAAS;AACT,QAAI;AACH,WAAM,OAAO;YACN;AAGR,WAAO,IAAI,MAAM,IAAI,SAAS,wBAAwB,CAAC;;;AAIzD,SAAO,iBAAiB,WAAW,QAAQ;AAE3C,cAAY,iBAAiB;AAC5B,YAAS;AACT,OAAI;AACH,UAAM,OAAO;WACN;AAGR,UAAO,IAAI,kBAAkB,CAAC;KAC5B,QAAQ;AAEX,kBAAgB,kBAAkB;AACjC,OAAI,MAAM,QAAQ;AACjB,aAAS;AACT,WAAO,IAAI,kBAAkB,CAAC;;KAE7B,IAAI;GACN"}
|
package/dist/types.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
//#region src/types.d.ts
|
|
2
|
-
/** OAuth provider options for Login with MySocial */
|
|
3
|
-
type AuthProvider = 'google' | 'apple' | 'facebook' | 'twitch';
|
|
2
|
+
/** OAuth provider options for Login with MySocial. Use 'none' for home screen (user picks provider). */
|
|
3
|
+
type AuthProvider = 'google' | 'apple' | 'facebook' | 'twitch' | 'none';
|
|
4
4
|
/** Auth flow mode: popup (opens window) or redirect (navigates away) */
|
|
5
5
|
type AuthMode = 'popup' | 'redirect';
|
|
6
6
|
/** User object returned from exchange/refresh */
|
|
@@ -16,6 +16,7 @@ interface Session {
|
|
|
16
16
|
refresh_token?: string;
|
|
17
17
|
expires_at: number;
|
|
18
18
|
user: AuthUser;
|
|
19
|
+
salt?: string;
|
|
19
20
|
}
|
|
20
21
|
/** Storage adapter for persisting session (memory, sessionStorage, or custom) */
|
|
21
22
|
interface StorageAdapter {
|
|
@@ -39,6 +40,7 @@ interface MySocialAuthConfig {
|
|
|
39
40
|
}
|
|
40
41
|
/** Sign-in options */
|
|
41
42
|
interface SignInOptions {
|
|
43
|
+
/** Provider to use. Default 'none' shows auth home screen. */
|
|
42
44
|
provider?: AuthProvider;
|
|
43
45
|
mode?: AuthMode;
|
|
44
46
|
}
|
package/dist/types.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.mts","names":[],"sources":["../src/types.ts"],"mappings":";;KAIY,YAAA;;KAGA,QAAA;;UAGK,QAAA;EAChB,EAAA;EACA,KAAA;EACA,IAAA;EAAA,CACC,GAAA;AAAA;AAJF;AAAA,UAQiB,OAAA;EAChB,YAAA;EACA,aAAA;EACA,UAAA;EACA,IAAA,EAAM,QAAA;AAAA;;
|
|
1
|
+
{"version":3,"file":"types.d.mts","names":[],"sources":["../src/types.ts"],"mappings":";;KAIY,YAAA;;KAGA,QAAA;;UAGK,QAAA;EAChB,EAAA;EACA,KAAA;EACA,IAAA;EAAA,CACC,GAAA;AAAA;AAJF;AAAA,UAQiB,OAAA;EAChB,YAAA;EACA,aAAA;EACA,UAAA;EACA,IAAA,EAAM,QAAA;EACN,IAAA;AAAA;;UAIgB,cAAA;EAChB,GAAA,CAAI,GAAA;EACJ,GAAA,CAAI,GAAA,UAAa,KAAA;EACjB,MAAA,CAAO,GAAA;AAAA;;KAII,aAAA,0BAAuC,cAAA;;UAGlC,kBAAA;EAChB,UAAA;EACA,UAAA;EACA,QAAA;EACA,WAAA;EACA,OAAA,GAAU,aAAA;EAfoB;EAiB9B,YAAA;EAjB8B;EAmB9B,YAAA;AAAA;;UAIgB,aAAA;EArBC;EAuBjB,QAAA,GAAW,YAAA;EACX,IAAA,GAAO,QAAA;AAAA;;KAII,uBAAA,IAA2B,OAAA,EAAS,OAAA"}
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@socialproof/mysocial-auth",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "MySocial Auth SDK for Login with MySocial via popup or redirect",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
|
-
"author": "
|
|
7
|
+
"author": "The Social Proof Foundation, LLC. <build@socialprooflabs.com>",
|
|
8
8
|
"type": "module",
|
|
9
9
|
"main": "./dist/index.mjs",
|
|
10
10
|
"types": "./dist/index.d.mts",
|
|
@@ -22,12 +22,12 @@
|
|
|
22
22
|
],
|
|
23
23
|
"repository": {
|
|
24
24
|
"type": "git",
|
|
25
|
-
"url": "git+https://github.com/
|
|
25
|
+
"url": "git+https://github.com/the-social-proof-foundation/myso-ts-sdks.git"
|
|
26
26
|
},
|
|
27
27
|
"bugs": {
|
|
28
|
-
"url": "https://github.com/
|
|
28
|
+
"url": "https://github.com/the-social-proof-foundation/myso-ts-sdks/issues"
|
|
29
29
|
},
|
|
30
|
-
"homepage": "https://github.com/
|
|
30
|
+
"homepage": "https://github.com/the-social-proof-foundation/myso-ts-sdks/tree/main/packages/mysocial-auth#readme",
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"@types/node": "^25.0.8",
|
|
33
33
|
"happy-dom": "^20.1.0",
|