@socialproof/mysocial-auth 0.1.0 → 0.3.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 +12 -0
- package/README.md +26 -2
- package/dist/auth.d.mts.map +1 -1
- package/dist/auth.mjs +1 -6
- package/dist/auth.mjs.map +1 -1
- package/dist/pkce.mjs +1 -18
- package/dist/pkce.mjs.map +1 -1
- package/dist/popup.mjs +11 -14
- package/dist/popup.mjs.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
package/README.md
CHANGED
|
@@ -95,6 +95,12 @@ await auth.signOut();
|
|
|
95
95
|
|
|
96
96
|
## Hosted UI Contract (auth.mysocial.network)
|
|
97
97
|
|
|
98
|
+
The SDK passes `return_origin` and `code_challenge_method: S256` in the login URL params. The auth
|
|
99
|
+
frontend generates PKCE (code_challenge/code_verifier) server-side for Google/Apple; the package
|
|
100
|
+
does not send them. `return_origin` must match the dApp's origin
|
|
101
|
+
(`window.location.origin`) so the auth server can post the auth result to the correct target.
|
|
102
|
+
The auth server uses `return_origin` as the `postMessage` targetOrigin when sending the result.
|
|
103
|
+
|
|
98
104
|
The popup page must call:
|
|
99
105
|
|
|
100
106
|
```javascript
|
|
@@ -121,7 +127,6 @@ window.opener.postMessage(payload, validatedTargetOrigin); // targetOrigin, NOT
|
|
|
121
127
|
"type": "MYSOCIAL_AUTH_ERROR",
|
|
122
128
|
"error": "error_code_or_message",
|
|
123
129
|
"state": "...",
|
|
124
|
-
"nonce": "...",
|
|
125
130
|
"clientId": "...",
|
|
126
131
|
"requestId": "..."
|
|
127
132
|
}
|
|
@@ -130,17 +135,36 @@ window.opener.postMessage(payload, validatedTargetOrigin); // targetOrigin, NOT
|
|
|
130
135
|
`return_origin` must never be trusted from the query param. The backend must validate it against the
|
|
131
136
|
allowlist for `client_id`.
|
|
132
137
|
|
|
138
|
+
**Important:** The package calls your app's backend (`apiBaseUrl`), never the auth frontend. The auth
|
|
139
|
+
frontend's `/api/auth/callback` is internal and is not used by this SDK.
|
|
140
|
+
|
|
133
141
|
## Backend Contract
|
|
134
142
|
|
|
135
143
|
- `POST ${apiBaseUrl}/auth/request` (optional): `{ client_id, redirect_uri, return_origin }` →
|
|
136
144
|
`{ request_id }`
|
|
137
145
|
- `POST ${apiBaseUrl}/auth/exchange`:
|
|
138
|
-
`{ code,
|
|
146
|
+
`{ code, redirect_uri, state?, nonce?, request_id? }` (your app's backend; code_verifier not sent—auth frontend handles OAuth/salt internally) →
|
|
139
147
|
`{ access_token, refresh_token?, expires_in, user }`
|
|
140
148
|
- `POST ${apiBaseUrl}/auth/refresh`: `{ refresh_token }` →
|
|
141
149
|
`{ access_token, refresh_token?, expires_in, user }`
|
|
142
150
|
- `POST ${apiBaseUrl}/auth/logout`
|
|
143
151
|
|
|
152
|
+
## Troubleshooting
|
|
153
|
+
|
|
154
|
+
### Popup stays open after login
|
|
155
|
+
|
|
156
|
+
If the popup does not close after successful authentication:
|
|
157
|
+
|
|
158
|
+
1. **`return_origin` mismatch** – The SDK sends `return_origin` (derived from `window.location.origin`)
|
|
159
|
+
in the login URL. The auth server posts with this as the `postMessage` target. Ensure your dApp
|
|
160
|
+
runs on the same origin you expect (e.g. `https://dripdrop.social` vs `https://www.dripdrop.social`).
|
|
161
|
+
|
|
162
|
+
2. **`authOrigin` mismatch** – The SDK's `authOrigin` must match the auth server's origin exactly
|
|
163
|
+
(e.g. `https://auth.mysocial.network`, or `http://localhost:3000` for local development).
|
|
164
|
+
|
|
165
|
+
3. **`redirectUri` origin** – The `redirectUri` must be allowlisted per `clientId`. Its origin
|
|
166
|
+
should match your dApp's origin for consistency.
|
|
167
|
+
|
|
144
168
|
## Security Notes
|
|
145
169
|
|
|
146
170
|
- **Tokens in browser storage** (session/localStorage) are XSS-risky. Prefer `storage: 'memory'`
|
package/dist/auth.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.d.mts","names":[],"sources":["../src/auth.ts"],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"auth.d.mts","names":[],"sources":["../src/auth.ts"],"mappings":";;;UAiBiB,YAAA;EAChB,MAAA,CAAO,OAAA,GAAU,aAAA,GAAgB,OAAA,CAAQ,OAAA;EACzC,OAAA,IAAW,OAAA;EACX,UAAA,IAAc,OAAA,CAAQ,OAAA;EACtB,OAAA,IAAW,OAAA,CAAQ,OAAA;EACnB,sBAAA,CAAuB,GAAA,YAAe,OAAA,CAAQ,OAAA;EAC9C,iBAAA,CAAkB,QAAA,EAAU,uBAAA;AAAA"}
|
package/dist/auth.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { REDIRECT_STATE_PREFIX, SESSION_KEY, createStorage, redirectStorage } from "./storage.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { generateNonce, generateState } from "./pkce.mjs";
|
|
3
3
|
import { exchangeCode, fetchRequestId, logout, refreshTokens } from "./exchange.mjs";
|
|
4
4
|
import { openAuthPopup } from "./popup.mjs";
|
|
5
5
|
import { mitt } from "@socialproof/utils";
|
|
@@ -67,8 +67,6 @@ function createAuth(config) {
|
|
|
67
67
|
}
|
|
68
68
|
const state = generateState();
|
|
69
69
|
const nonce = generateNonce();
|
|
70
|
-
const codeVerifier = await generateCodeVerifier();
|
|
71
|
-
const codeChallenge = await generateCodeChallenge(codeVerifier);
|
|
72
70
|
const returnOrigin = getReturnOrigin(config.redirectUri);
|
|
73
71
|
let requestId;
|
|
74
72
|
if (config.useRequestId) requestId = await fetchRequestId(config.apiBaseUrl, {
|
|
@@ -79,7 +77,6 @@ function createAuth(config) {
|
|
|
79
77
|
const redirectState = {
|
|
80
78
|
state,
|
|
81
79
|
nonce,
|
|
82
|
-
codeVerifier,
|
|
83
80
|
requestId
|
|
84
81
|
};
|
|
85
82
|
const key = `${REDIRECT_STATE_PREFIX}${state}`;
|
|
@@ -92,7 +89,6 @@ function createAuth(config) {
|
|
|
92
89
|
return_origin: returnOrigin,
|
|
93
90
|
mode: "redirect",
|
|
94
91
|
provider: provider ?? "",
|
|
95
|
-
code_challenge: codeChallenge,
|
|
96
92
|
code_challenge_method: "S256"
|
|
97
93
|
});
|
|
98
94
|
if (requestId) params.set("request_id", requestId);
|
|
@@ -142,7 +138,6 @@ function createAuth(config) {
|
|
|
142
138
|
redirectStorage.remove(key);
|
|
143
139
|
const session = await exchangeCode(config.apiBaseUrl, {
|
|
144
140
|
code,
|
|
145
|
-
code_verifier: redirectState.codeVerifier,
|
|
146
141
|
redirect_uri: config.redirectUri,
|
|
147
142
|
state,
|
|
148
143
|
nonce,
|
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 {\n\tgenerateCodeVerifier,\n\tgenerateCodeChallenge,\n\tgenerateState,\n\tgenerateNonce,\n} 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 codeVerifier = await generateCodeVerifier();\n\t\t\tconst codeChallenge = await generateCodeChallenge(codeVerifier);\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\tcodeVerifier,\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: codeChallenge,\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\tcodeVerifier: 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\tcode_verifier: redirectState.codeVerifier,\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 { 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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"}
|
package/dist/pkce.mjs
CHANGED
|
@@ -1,22 +1,5 @@
|
|
|
1
1
|
//#region src/pkce.ts
|
|
2
2
|
/**
|
|
3
|
-
* Generate a cryptographically random code verifier (32 bytes = 256 bits, base64url).
|
|
4
|
-
* Used for PKCE in OAuth flows.
|
|
5
|
-
*/
|
|
6
|
-
async function generateCodeVerifier() {
|
|
7
|
-
const array = new Uint8Array(32);
|
|
8
|
-
crypto.getRandomValues(array);
|
|
9
|
-
return base64UrlEncode(array);
|
|
10
|
-
}
|
|
11
|
-
/**
|
|
12
|
-
* Generate code challenge from verifier using SHA-256 (S256 method).
|
|
13
|
-
*/
|
|
14
|
-
async function generateCodeChallenge(verifier) {
|
|
15
|
-
const data = new TextEncoder().encode(verifier);
|
|
16
|
-
const hash = await crypto.subtle.digest("SHA-256", data);
|
|
17
|
-
return base64UrlEncode(new Uint8Array(hash));
|
|
18
|
-
}
|
|
19
|
-
/**
|
|
20
3
|
* Generate a random state (128 bits+ entropy) for CSRF protection.
|
|
21
4
|
*/
|
|
22
5
|
function generateState() {
|
|
@@ -37,5 +20,5 @@ function base64UrlEncode(array) {
|
|
|
37
20
|
}
|
|
38
21
|
|
|
39
22
|
//#endregion
|
|
40
|
-
export {
|
|
23
|
+
export { generateNonce, generateState };
|
|
41
24
|
//# sourceMappingURL=pkce.mjs.map
|
package/dist/pkce.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pkce.mjs","names":[],"sources":["../src/pkce.ts"],"sourcesContent":["// Copyright (c) The Social Proof Foundation, LLC.\n// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Generate a cryptographically random code verifier (32 bytes = 256 bits, base64url).\n * Used for PKCE in OAuth flows.\n */\nexport async function generateCodeVerifier(): Promise<string> {\n\tconst array = new Uint8Array(32);\n\tcrypto.getRandomValues(array);\n\treturn base64UrlEncode(array);\n}\n\n/**\n * Generate code challenge from verifier using SHA-256 (S256 method).\n */\nexport async function generateCodeChallenge(verifier: string): Promise<string> {\n\tconst encoder = new TextEncoder();\n\tconst data = encoder.encode(verifier);\n\tconst hash = await crypto.subtle.digest('SHA-256', data);\n\treturn base64UrlEncode(new Uint8Array(hash));\n}\n\n/**\n * Generate a random state (128 bits+ entropy) for CSRF protection.\n */\nexport function generateState(): string {\n\tconst array = new Uint8Array(16);\n\tcrypto.getRandomValues(array);\n\treturn base64UrlEncode(array);\n}\n\n/**\n * Generate a random nonce (128 bits+ entropy) for replay protection.\n */\nexport function generateNonce(): string {\n\tconst array = new Uint8Array(16);\n\tcrypto.getRandomValues(array);\n\treturn base64UrlEncode(array);\n}\n\nfunction base64UrlEncode(array: Uint8Array): string {\n\tconst base64 = btoa(String.fromCharCode(...array));\n\treturn base64.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"pkce.mjs","names":[],"sources":["../src/pkce.ts"],"sourcesContent":["// Copyright (c) The Social Proof Foundation, LLC.\n// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Generate a cryptographically random code verifier (32 bytes = 256 bits, base64url).\n * Used for PKCE in OAuth flows.\n */\nexport async function generateCodeVerifier(): Promise<string> {\n\tconst array = new Uint8Array(32);\n\tcrypto.getRandomValues(array);\n\treturn base64UrlEncode(array);\n}\n\n/**\n * Generate code challenge from verifier using SHA-256 (S256 method).\n */\nexport async function generateCodeChallenge(verifier: string): Promise<string> {\n\tconst encoder = new TextEncoder();\n\tconst data = encoder.encode(verifier);\n\tconst hash = await crypto.subtle.digest('SHA-256', data);\n\treturn base64UrlEncode(new Uint8Array(hash));\n}\n\n/**\n * Generate a random state (128 bits+ entropy) for CSRF protection.\n */\nexport function generateState(): string {\n\tconst array = new Uint8Array(16);\n\tcrypto.getRandomValues(array);\n\treturn base64UrlEncode(array);\n}\n\n/**\n * Generate a random nonce (128 bits+ entropy) for replay protection.\n */\nexport function generateNonce(): string {\n\tconst array = new Uint8Array(16);\n\tcrypto.getRandomValues(array);\n\treturn base64UrlEncode(array);\n}\n\nfunction base64UrlEncode(array: Uint8Array): string {\n\tconst base64 = btoa(String.fromCharCode(...array));\n\treturn base64.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');\n}\n"],"mappings":";;;;AA0BA,SAAgB,gBAAwB;CACvC,MAAM,QAAQ,IAAI,WAAW,GAAG;AAChC,QAAO,gBAAgB,MAAM;AAC7B,QAAO,gBAAgB,MAAM;;;;;AAM9B,SAAgB,gBAAwB;CACvC,MAAM,QAAQ,IAAI,WAAW,GAAG;AAChC,QAAO,gBAAgB,MAAM;AAC7B,QAAO,gBAAgB,MAAM;;AAG9B,SAAS,gBAAgB,OAA2B;AAEnD,QADe,KAAK,OAAO,aAAa,GAAG,MAAM,CAAC,CACpC,QAAQ,OAAO,IAAI,CAAC,QAAQ,OAAO,IAAI,CAAC,QAAQ,OAAO,GAAG"}
|
package/dist/popup.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { AuthTimeoutError, InvalidStateError, PopupBlockedError, PopupClosedError } from "./errors.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { generateNonce, generateState } from "./pkce.mjs";
|
|
3
3
|
import { getPopupFeatures } from "./popup-utils.mjs";
|
|
4
4
|
import { exchangeCode, fetchRequestId } from "./exchange.mjs";
|
|
5
5
|
|
|
@@ -21,20 +21,13 @@ async function openAuthPopup(options) {
|
|
|
21
21
|
const { apiBaseUrl, authOrigin, clientId, redirectUri, provider, timeout = 12e4, useRequestId = false } = options;
|
|
22
22
|
const state = generateState();
|
|
23
23
|
const nonce = generateNonce();
|
|
24
|
-
const returnOrigin = getReturnOrigin(redirectUri);
|
|
24
|
+
const returnOrigin = typeof window !== "undefined" ? window.location.origin : getReturnOrigin(redirectUri);
|
|
25
25
|
let requestId;
|
|
26
26
|
if (useRequestId) requestId = await fetchRequestId(apiBaseUrl, {
|
|
27
27
|
client_id: clientId,
|
|
28
28
|
redirect_uri: redirectUri,
|
|
29
29
|
return_origin: returnOrigin
|
|
30
30
|
});
|
|
31
|
-
const { codeVerifier, codeChallenge } = await (async () => {
|
|
32
|
-
const verifier = await generateCodeVerifier();
|
|
33
|
-
return {
|
|
34
|
-
codeVerifier: verifier,
|
|
35
|
-
codeChallenge: await generateCodeChallenge(verifier)
|
|
36
|
-
};
|
|
37
|
-
})();
|
|
38
31
|
const features = getPopupFeatures(420, 720);
|
|
39
32
|
const popup = window.open("about:blank", "_blank", features);
|
|
40
33
|
if (!popup || popup.closed) throw new PopupBlockedError();
|
|
@@ -46,7 +39,6 @@ async function openAuthPopup(options) {
|
|
|
46
39
|
return_origin: returnOrigin,
|
|
47
40
|
mode: "popup",
|
|
48
41
|
provider: provider ?? "",
|
|
49
|
-
code_challenge: codeChallenge,
|
|
50
42
|
code_challenge_method: "S256"
|
|
51
43
|
});
|
|
52
44
|
if (requestId) params.set("request_id", requestId);
|
|
@@ -83,10 +75,11 @@ async function openAuthPopup(options) {
|
|
|
83
75
|
return;
|
|
84
76
|
}
|
|
85
77
|
cleanup();
|
|
86
|
-
|
|
78
|
+
try {
|
|
79
|
+
popup.close();
|
|
80
|
+
} catch {}
|
|
87
81
|
exchangeCode(apiBaseUrl, {
|
|
88
82
|
code: msg.code,
|
|
89
|
-
code_verifier: codeVerifier,
|
|
90
83
|
redirect_uri: redirectUri,
|
|
91
84
|
state,
|
|
92
85
|
nonce,
|
|
@@ -95,14 +88,18 @@ async function openAuthPopup(options) {
|
|
|
95
88
|
} else if (data.type === "MYSOCIAL_AUTH_ERROR") {
|
|
96
89
|
const msg = data;
|
|
97
90
|
cleanup();
|
|
98
|
-
|
|
91
|
+
try {
|
|
92
|
+
popup.close();
|
|
93
|
+
} catch {}
|
|
99
94
|
reject(new Error(msg.error ?? "Authentication failed"));
|
|
100
95
|
}
|
|
101
96
|
};
|
|
102
97
|
window.addEventListener("message", handler);
|
|
103
98
|
timeoutId = setTimeout(() => {
|
|
104
99
|
cleanup();
|
|
105
|
-
|
|
100
|
+
try {
|
|
101
|
+
popup.close();
|
|
102
|
+
} catch {}
|
|
106
103
|
reject(new AuthTimeoutError());
|
|
107
104
|
}, timeout);
|
|
108
105
|
closedCheckId = setInterval(() => {
|
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 {
|
|
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 { exchangeCode, 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 ?? '',\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\texchangeCode(apiBaseUrl, {\n\t\t\t\t\tcode: msg.code,\n\t\t\t\t\tredirect_uri: redirectUri,\n\t\t\t\t\tstate,\n\t\t\t\t\tnonce,\n\t\t\t\t\trequest_id: requestId,\n\t\t\t\t})\n\t\t\t\t\t.then(resolve)\n\t\t\t\t\t.catch(reject);\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;AAIR,iBAAa,YAAY;KACxB,MAAM,IAAI;KACV,cAAc;KACd;KACA;KACA,YAAY;KACZ,CAAC,CACA,KAAK,QAAQ,CACb,MAAM,OAAO;cACL,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"}
|