@nominalso/vibe-auth 0.2.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/AGENTS.md +101 -0
- package/LICENSE +10 -0
- package/README.md +103 -0
- package/dist/index.cjs +725 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +338 -0
- package/dist/index.d.ts +338 -0
- package/dist/index.js +695 -0
- package/dist/index.js.map +1 -0
- package/llms.txt +18 -0
- package/package.json +56 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,695 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
// src/config.ts
|
|
4
|
+
var DEFAULT_TIMEOUTS = {
|
|
5
|
+
silentIframeMs: 1e4,
|
|
6
|
+
silentFlowCapMs: 15e3,
|
|
7
|
+
oauthCompletionMs: 15e3,
|
|
8
|
+
popupMs: 12e4,
|
|
9
|
+
previewPrimingMs: 8e3,
|
|
10
|
+
previewPrimingPollMs: 250
|
|
11
|
+
};
|
|
12
|
+
function resolveConfig(config) {
|
|
13
|
+
const callbackPath = config.callbackPath ?? "/silent-callback";
|
|
14
|
+
const prefix = config.storageKeyPrefix ?? "nominal";
|
|
15
|
+
const silentFlowCapMs = config.timeouts?.silentFlowCapMs ?? DEFAULT_TIMEOUTS.silentFlowCapMs;
|
|
16
|
+
const lockWaitMs = config.timeouts?.lockWaitMs ?? 2 * silentFlowCapMs + 5e3;
|
|
17
|
+
return {
|
|
18
|
+
supabase: config.supabase,
|
|
19
|
+
provider: config.provider,
|
|
20
|
+
callbackPath,
|
|
21
|
+
timeouts: {
|
|
22
|
+
silentIframeMs: config.timeouts?.silentIframeMs ?? DEFAULT_TIMEOUTS.silentIframeMs,
|
|
23
|
+
silentFlowCapMs,
|
|
24
|
+
lockWaitMs,
|
|
25
|
+
oauthCompletionMs: config.timeouts?.oauthCompletionMs ?? DEFAULT_TIMEOUTS.oauthCompletionMs,
|
|
26
|
+
popupMs: config.timeouts?.popupMs ?? DEFAULT_TIMEOUTS.popupMs,
|
|
27
|
+
previewPrimingMs: config.timeouts?.previewPrimingMs ?? DEFAULT_TIMEOUTS.previewPrimingMs,
|
|
28
|
+
previewPrimingPollMs: config.timeouts?.previewPrimingPollMs ?? DEFAULT_TIMEOUTS.previewPrimingPollMs
|
|
29
|
+
},
|
|
30
|
+
lockName: `${prefix}-silent-sso`,
|
|
31
|
+
boundUserKey: `${prefix}-sso-bound-user`
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function assertPkce(supabase) {
|
|
35
|
+
try {
|
|
36
|
+
const flowType = supabase.auth.flowType;
|
|
37
|
+
if (flowType !== "pkce") {
|
|
38
|
+
console.error(
|
|
39
|
+
`[vibe-auth] Supabase client is not configured with { auth: { flowType: 'pkce' } } (got: ${String(flowType)}). This is a security control, not a preference \u2014 with implicit flow the OAuth redirect carries a live access_token/refresh_token in the URL, so an attacker-controlled redirect target yields a full session. Fix the app's createClient(...) call before shipping.`
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
} catch {
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// src/authMessage.ts
|
|
47
|
+
var RESULT_MESSAGE_TYPE = "silent-auth-result";
|
|
48
|
+
|
|
49
|
+
// src/silentAuth.ts
|
|
50
|
+
var AuthResultKind = /* @__PURE__ */ ((AuthResultKind2) => {
|
|
51
|
+
AuthResultKind2["Code"] = "code";
|
|
52
|
+
return AuthResultKind2;
|
|
53
|
+
})(AuthResultKind || {});
|
|
54
|
+
function parseCallbackMessage(data) {
|
|
55
|
+
if (!data || data.type !== RESULT_MESSAGE_TYPE) return null;
|
|
56
|
+
if (data.code) return { kind: "code" /* Code */, code: data.code };
|
|
57
|
+
if (data.accessToken || data.refreshToken) {
|
|
58
|
+
console.warn("[vibe-auth] ignoring implicit-flow token payload; PKCE expects ?code=");
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
function createSilentAuth(config, getHostPrincipal) {
|
|
63
|
+
const { supabase, provider, callbackPath, timeouts, lockName, boundUserKey } = config;
|
|
64
|
+
async function getSessionUserId() {
|
|
65
|
+
const { data } = await supabase.auth.getSession();
|
|
66
|
+
return data.session?.user?.id ?? null;
|
|
67
|
+
}
|
|
68
|
+
async function hasValidSession() {
|
|
69
|
+
return await getSessionUserId() !== null;
|
|
70
|
+
}
|
|
71
|
+
function readBoundMarker() {
|
|
72
|
+
if (typeof window === "undefined") return null;
|
|
73
|
+
try {
|
|
74
|
+
return JSON.parse(localStorage.getItem(boundUserKey) ?? "null");
|
|
75
|
+
} catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function isBoundTo(principal) {
|
|
80
|
+
const m = readBoundMarker();
|
|
81
|
+
return m?.userId === principal.userId && m?.tenant === principal.tenant;
|
|
82
|
+
}
|
|
83
|
+
function writeBoundMarker({ userId, tenant }) {
|
|
84
|
+
if (typeof window === "undefined") return;
|
|
85
|
+
localStorage.setItem(boundUserKey, JSON.stringify({ userId, tenant, at: Date.now() }));
|
|
86
|
+
}
|
|
87
|
+
async function completeWithResult(result, priorUserId = null) {
|
|
88
|
+
const { error } = await supabase.auth.exchangeCodeForSession(result.code);
|
|
89
|
+
if (error) {
|
|
90
|
+
const sessionUserId = await getSessionUserId();
|
|
91
|
+
if (sessionUserId === null) return false;
|
|
92
|
+
if (priorUserId !== null && sessionUserId === priorUserId) return false;
|
|
93
|
+
}
|
|
94
|
+
const host = getHostPrincipal?.();
|
|
95
|
+
if (host) writeBoundMarker(host);
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
function runHiddenAuthFrame(url) {
|
|
99
|
+
return new Promise((resolve) => {
|
|
100
|
+
const expectedOrigin = window.location.origin;
|
|
101
|
+
let settled = false;
|
|
102
|
+
const iframe = document.createElement("iframe");
|
|
103
|
+
iframe.style.display = "none";
|
|
104
|
+
iframe.setAttribute("aria-hidden", "true");
|
|
105
|
+
const cleanup = () => {
|
|
106
|
+
window.removeEventListener("message", onMessage);
|
|
107
|
+
window.clearTimeout(timer);
|
|
108
|
+
if (iframe.parentNode) iframe.parentNode.removeChild(iframe);
|
|
109
|
+
};
|
|
110
|
+
const finish = (result) => {
|
|
111
|
+
if (settled) return;
|
|
112
|
+
settled = true;
|
|
113
|
+
cleanup();
|
|
114
|
+
resolve(result);
|
|
115
|
+
};
|
|
116
|
+
const onMessage = (event) => {
|
|
117
|
+
if (event.origin !== expectedOrigin) return;
|
|
118
|
+
if (event.source !== iframe.contentWindow) return;
|
|
119
|
+
const data = event.data;
|
|
120
|
+
if (!data || data.type !== RESULT_MESSAGE_TYPE) return;
|
|
121
|
+
finish(parseCallbackMessage(data));
|
|
122
|
+
};
|
|
123
|
+
const timer = window.setTimeout(() => finish(null), timeouts.silentIframeMs);
|
|
124
|
+
window.addEventListener("message", onMessage);
|
|
125
|
+
iframe.src = url;
|
|
126
|
+
document.body.appendChild(iframe);
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
let inFlightSilentSignIn = null;
|
|
130
|
+
function trySilentSignIn() {
|
|
131
|
+
if (inFlightSilentSignIn) return inFlightSilentSignIn;
|
|
132
|
+
let expired = false;
|
|
133
|
+
inFlightSilentSignIn = Promise.race([
|
|
134
|
+
runTrySilentSignIn(() => expired),
|
|
135
|
+
new Promise(
|
|
136
|
+
(resolve) => setTimeout(() => {
|
|
137
|
+
expired = true;
|
|
138
|
+
resolve(false);
|
|
139
|
+
}, timeouts.silentFlowCapMs)
|
|
140
|
+
)
|
|
141
|
+
]).finally(() => {
|
|
142
|
+
inFlightSilentSignIn = null;
|
|
143
|
+
});
|
|
144
|
+
return inFlightSilentSignIn;
|
|
145
|
+
}
|
|
146
|
+
async function runTrySilentSignIn(isExpired) {
|
|
147
|
+
if (typeof window === "undefined") return false;
|
|
148
|
+
const priorUserId = await getSessionUserId();
|
|
149
|
+
const redirectTo = `${window.location.origin}${callbackPath}`;
|
|
150
|
+
const { data, error } = await supabase.auth.signInWithOAuth({
|
|
151
|
+
provider,
|
|
152
|
+
options: {
|
|
153
|
+
redirectTo,
|
|
154
|
+
scopes: "openid profile email",
|
|
155
|
+
skipBrowserRedirect: true,
|
|
156
|
+
// gives us the URL instead of redirecting
|
|
157
|
+
queryParams: { prompt: "none" }
|
|
158
|
+
// provider returns a result or error, no UI
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
if (error || !data?.url) return false;
|
|
162
|
+
const result = await runHiddenAuthFrame(data.url);
|
|
163
|
+
if (!result) return false;
|
|
164
|
+
if (isExpired()) return false;
|
|
165
|
+
const ok = await completeWithResult(result, priorUserId);
|
|
166
|
+
if (ok && isExpired()) {
|
|
167
|
+
await supabase.auth.signOut();
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
return ok;
|
|
171
|
+
}
|
|
172
|
+
async function withSsoLock(fn, fallback) {
|
|
173
|
+
const locks = navigator.locks;
|
|
174
|
+
if (!locks?.request) {
|
|
175
|
+
if (await fn()) return true;
|
|
176
|
+
return fallback();
|
|
177
|
+
}
|
|
178
|
+
const controller = new AbortController();
|
|
179
|
+
const timer = window.setTimeout(() => controller.abort(), timeouts.lockWaitMs);
|
|
180
|
+
try {
|
|
181
|
+
return await locks.request(lockName, { signal: controller.signal }, fn);
|
|
182
|
+
} catch {
|
|
183
|
+
return fallback();
|
|
184
|
+
} finally {
|
|
185
|
+
window.clearTimeout(timer);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
async function ensureSession() {
|
|
189
|
+
if (typeof window === "undefined") return false;
|
|
190
|
+
if (await hasValidSession()) return true;
|
|
191
|
+
return withSsoLock(
|
|
192
|
+
async () => {
|
|
193
|
+
if (await hasValidSession()) return true;
|
|
194
|
+
if (await trySilentSignIn()) return true;
|
|
195
|
+
return hasValidSession();
|
|
196
|
+
},
|
|
197
|
+
// Lock unavailable/aborted: a sibling may still have succeeded meanwhile.
|
|
198
|
+
hasValidSession
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
async function rebindSession(principal) {
|
|
202
|
+
if (typeof window === "undefined") return false;
|
|
203
|
+
const siblingRebound = async () => {
|
|
204
|
+
const m = readBoundMarker();
|
|
205
|
+
return m?.userId === principal.userId && m?.tenant === principal.tenant && Date.now() - (m.at ?? 0) < timeouts.lockWaitMs && await hasValidSession();
|
|
206
|
+
};
|
|
207
|
+
return withSsoLock(
|
|
208
|
+
async () => {
|
|
209
|
+
if (await siblingRebound()) return true;
|
|
210
|
+
const ok = await trySilentSignIn();
|
|
211
|
+
if (ok) writeBoundMarker(principal);
|
|
212
|
+
return ok;
|
|
213
|
+
},
|
|
214
|
+
// Reached on lock-wait abort — i.e. we gave up while a sibling may
|
|
215
|
+
// STILL be mid-flow, about to succeed and write the marker. Returning
|
|
216
|
+
// false here makes the caller signOut(), which would destroy that
|
|
217
|
+
// near-complete session, so grant one grace period before concluding
|
|
218
|
+
// failure.
|
|
219
|
+
async () => {
|
|
220
|
+
if (await siblingRebound()) return true;
|
|
221
|
+
await new Promise((r) => setTimeout(r, timeouts.silentFlowCapMs));
|
|
222
|
+
return siblingRebound();
|
|
223
|
+
}
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
function clearBoundUser() {
|
|
227
|
+
if (typeof window === "undefined") return;
|
|
228
|
+
localStorage.removeItem(boundUserKey);
|
|
229
|
+
}
|
|
230
|
+
return {
|
|
231
|
+
hasValidSession,
|
|
232
|
+
trySilentSignIn,
|
|
233
|
+
ensureSession,
|
|
234
|
+
rebindSession,
|
|
235
|
+
isBoundTo,
|
|
236
|
+
clearBoundUser,
|
|
237
|
+
completeWithResult
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// src/previewToken.ts
|
|
242
|
+
var PREVIEW_HOST_SUFFIX = ".lovable.app";
|
|
243
|
+
var SANDBOX_HOST_SUFFIX = ".lovableproject.com";
|
|
244
|
+
var capturedToken = null;
|
|
245
|
+
function capturePreviewToken() {
|
|
246
|
+
if (typeof window === "undefined") return;
|
|
247
|
+
if (!window.location.hostname.endsWith(PREVIEW_HOST_SUFFIX)) return;
|
|
248
|
+
if (capturedToken) return;
|
|
249
|
+
try {
|
|
250
|
+
const token = new URLSearchParams(window.location.search).get("__lovable_token");
|
|
251
|
+
if (token) capturedToken = token;
|
|
252
|
+
} catch {
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
function isLovableHostedHost() {
|
|
256
|
+
if (typeof window === "undefined") return false;
|
|
257
|
+
const h = window.location.hostname;
|
|
258
|
+
return h.endsWith(PREVIEW_HOST_SUFFIX) || h.endsWith(SANDBOX_HOST_SUFFIX);
|
|
259
|
+
}
|
|
260
|
+
function getPreviewPrimingUrl() {
|
|
261
|
+
if (!isLovableHostedHost()) return null;
|
|
262
|
+
if (window.location.hostname.endsWith(PREVIEW_HOST_SUFFIX) && capturedToken) {
|
|
263
|
+
return `${window.location.origin}/?__lovable_token=${encodeURIComponent(capturedToken)}`;
|
|
264
|
+
}
|
|
265
|
+
return `${window.location.origin}/`;
|
|
266
|
+
}
|
|
267
|
+
capturePreviewToken();
|
|
268
|
+
|
|
269
|
+
// src/signInResult.ts
|
|
270
|
+
var SignInKind = /* @__PURE__ */ ((SignInKind2) => {
|
|
271
|
+
SignInKind2["Authenticated"] = "authenticated";
|
|
272
|
+
SignInKind2["Redirecting"] = "redirecting";
|
|
273
|
+
SignInKind2["Failed"] = "failed";
|
|
274
|
+
return SignInKind2;
|
|
275
|
+
})(SignInKind || {});
|
|
276
|
+
var SignInFailureReason = /* @__PURE__ */ ((SignInFailureReason2) => {
|
|
277
|
+
SignInFailureReason2["PopupBlocked"] = "popup-blocked";
|
|
278
|
+
SignInFailureReason2["OauthError"] = "oauth-error";
|
|
279
|
+
SignInFailureReason2["PopupClosed"] = "popup-closed";
|
|
280
|
+
SignInFailureReason2["ExchangeFailed"] = "exchange-failed";
|
|
281
|
+
SignInFailureReason2["Unsupported"] = "unsupported";
|
|
282
|
+
SignInFailureReason2["Unexpected"] = "unexpected";
|
|
283
|
+
return SignInFailureReason2;
|
|
284
|
+
})(SignInFailureReason || {});
|
|
285
|
+
|
|
286
|
+
// src/interactive.ts
|
|
287
|
+
function createInteractiveAuth(config, silentAuth) {
|
|
288
|
+
const { supabase, provider, callbackPath, timeouts } = config;
|
|
289
|
+
function waitForPopupResult(popup) {
|
|
290
|
+
return new Promise((resolve) => {
|
|
291
|
+
const expectedOrigin = window.location.origin;
|
|
292
|
+
let settled = false;
|
|
293
|
+
const finish = (result) => {
|
|
294
|
+
if (settled) return;
|
|
295
|
+
settled = true;
|
|
296
|
+
window.removeEventListener("message", onMessage);
|
|
297
|
+
window.clearInterval(closedPoll);
|
|
298
|
+
resolve(result);
|
|
299
|
+
};
|
|
300
|
+
const onMessage = (event) => {
|
|
301
|
+
if (event.origin !== expectedOrigin) return;
|
|
302
|
+
if (event.source !== popup) return;
|
|
303
|
+
const data = event.data;
|
|
304
|
+
if (!data || data.type !== RESULT_MESSAGE_TYPE) return;
|
|
305
|
+
finish(parseCallbackMessage(data));
|
|
306
|
+
};
|
|
307
|
+
window.addEventListener("message", onMessage);
|
|
308
|
+
const closedPoll = window.setInterval(() => {
|
|
309
|
+
if (popup.closed) finish(null);
|
|
310
|
+
}, 500);
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
function primePopupForPreview(popup) {
|
|
314
|
+
const url = getPreviewPrimingUrl();
|
|
315
|
+
if (!url) return Promise.resolve();
|
|
316
|
+
return new Promise((resolve) => {
|
|
317
|
+
let settled = false;
|
|
318
|
+
const expectedOrigin = window.location.origin;
|
|
319
|
+
const finish = () => {
|
|
320
|
+
if (settled) return;
|
|
321
|
+
settled = true;
|
|
322
|
+
window.clearInterval(poll);
|
|
323
|
+
window.clearTimeout(timer);
|
|
324
|
+
resolve();
|
|
325
|
+
};
|
|
326
|
+
const timer = window.setTimeout(finish, timeouts.previewPrimingMs);
|
|
327
|
+
const poll = window.setInterval(() => {
|
|
328
|
+
if (popup.closed) return finish();
|
|
329
|
+
try {
|
|
330
|
+
if (popup.location.origin !== expectedOrigin) return;
|
|
331
|
+
if (popup.document.readyState !== "complete") return;
|
|
332
|
+
finish();
|
|
333
|
+
} catch {
|
|
334
|
+
}
|
|
335
|
+
}, timeouts.previewPrimingPollMs);
|
|
336
|
+
try {
|
|
337
|
+
popup.location.href = url;
|
|
338
|
+
} catch {
|
|
339
|
+
finish();
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
async function signInInteractive() {
|
|
344
|
+
const popupRef = { current: null };
|
|
345
|
+
try {
|
|
346
|
+
return await attemptSignIn((p) => popupRef.current = p);
|
|
347
|
+
} catch (err) {
|
|
348
|
+
console.error("[vibe-auth] signInInteractive failed unexpectedly:", err);
|
|
349
|
+
popupRef.current?.close();
|
|
350
|
+
return { kind: "failed" /* Failed */, reason: "unexpected" /* Unexpected */ };
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
async function attemptSignIn(trackPopup) {
|
|
354
|
+
if (typeof window === "undefined")
|
|
355
|
+
return { kind: "failed" /* Failed */, reason: "unsupported" /* Unsupported */ };
|
|
356
|
+
if (window.self === window.top) {
|
|
357
|
+
const { error } = await supabase.auth.signInWithOAuth({
|
|
358
|
+
provider,
|
|
359
|
+
options: {
|
|
360
|
+
redirectTo: `${window.location.origin}${callbackPath}`,
|
|
361
|
+
scopes: "openid profile email"
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
if (error) return { kind: "failed" /* Failed */, reason: "oauth-error" /* OauthError */ };
|
|
365
|
+
return { kind: "redirecting" /* Redirecting */ };
|
|
366
|
+
}
|
|
367
|
+
const popup = window.open("about:blank", "nominal-signin", "width=520,height=680");
|
|
368
|
+
if (!popup) return { kind: "failed" /* Failed */, reason: "popup-blocked" /* PopupBlocked */ };
|
|
369
|
+
trackPopup(popup);
|
|
370
|
+
let expired = false;
|
|
371
|
+
let flowTimer;
|
|
372
|
+
const timeout = new Promise((resolve) => {
|
|
373
|
+
flowTimer = window.setTimeout(() => {
|
|
374
|
+
expired = true;
|
|
375
|
+
popup.close();
|
|
376
|
+
resolve({ kind: "failed" /* Failed */, reason: "popup-closed" /* PopupClosed */ });
|
|
377
|
+
}, timeouts.popupMs);
|
|
378
|
+
});
|
|
379
|
+
const flow = (async () => {
|
|
380
|
+
await primePopupForPreview(popup).catch(() => void 0);
|
|
381
|
+
if (expired || popup.closed)
|
|
382
|
+
return { kind: "failed" /* Failed */, reason: "popup-closed" /* PopupClosed */ };
|
|
383
|
+
const { data, error } = await supabase.auth.signInWithOAuth({
|
|
384
|
+
provider,
|
|
385
|
+
options: {
|
|
386
|
+
redirectTo: `${window.location.origin}${callbackPath}`,
|
|
387
|
+
scopes: "openid profile email",
|
|
388
|
+
skipBrowserRedirect: true
|
|
389
|
+
// we navigate the popup ourselves
|
|
390
|
+
}
|
|
391
|
+
});
|
|
392
|
+
if (expired || popup.closed)
|
|
393
|
+
return { kind: "failed" /* Failed */, reason: "popup-closed" /* PopupClosed */ };
|
|
394
|
+
if (error || !data?.url) {
|
|
395
|
+
popup.close();
|
|
396
|
+
return { kind: "failed" /* Failed */, reason: "oauth-error" /* OauthError */ };
|
|
397
|
+
}
|
|
398
|
+
popup.location.href = data.url;
|
|
399
|
+
const result = await waitForPopupResult(popup);
|
|
400
|
+
if (expired || !result)
|
|
401
|
+
return { kind: "failed" /* Failed */, reason: "popup-closed" /* PopupClosed */ };
|
|
402
|
+
const ok = await silentAuth.completeWithResult(result);
|
|
403
|
+
if (expired) {
|
|
404
|
+
void supabase.auth.signOut().catch(() => {
|
|
405
|
+
});
|
|
406
|
+
return { kind: "failed" /* Failed */, reason: "popup-closed" /* PopupClosed */ };
|
|
407
|
+
}
|
|
408
|
+
return ok ? { kind: "authenticated" /* Authenticated */ } : { kind: "failed" /* Failed */, reason: "exchange-failed" /* ExchangeFailed */ };
|
|
409
|
+
})();
|
|
410
|
+
try {
|
|
411
|
+
return await Promise.race([flow, timeout]);
|
|
412
|
+
} finally {
|
|
413
|
+
if (flowTimer !== void 0) window.clearTimeout(flowTimer);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
return { signInInteractive };
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// src/SilentCallback.tsx
|
|
420
|
+
import { useEffect } from "react";
|
|
421
|
+
function createCallbackHandler() {
|
|
422
|
+
const INITIAL_SEARCH = typeof window !== "undefined" ? window.location.search : "";
|
|
423
|
+
const INITIAL_HASH = typeof window !== "undefined" ? window.location.hash : "";
|
|
424
|
+
let posted = false;
|
|
425
|
+
function SilentCallback() {
|
|
426
|
+
useEffect(() => {
|
|
427
|
+
if (posted) return;
|
|
428
|
+
const target = window.opener ?? (window.parent !== window ? window.parent : null);
|
|
429
|
+
if (!target) {
|
|
430
|
+
posted = true;
|
|
431
|
+
window.location.replace(`/${INITIAL_SEARCH}${INITIAL_HASH}`);
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
posted = true;
|
|
435
|
+
const params = new URLSearchParams(INITIAL_SEARCH);
|
|
436
|
+
const hash = new URLSearchParams(INITIAL_HASH.replace(/^#/, ""));
|
|
437
|
+
const code = params.get("code");
|
|
438
|
+
const error = params.get("error") ?? params.get("error_description") ?? hash.get("error") ?? hash.get("error_description");
|
|
439
|
+
target.postMessage(
|
|
440
|
+
{ type: RESULT_MESSAGE_TYPE, code, error: code ? null : error },
|
|
441
|
+
window.location.origin
|
|
442
|
+
// never use '*'
|
|
443
|
+
);
|
|
444
|
+
if (window.opener) window.close();
|
|
445
|
+
}, []);
|
|
446
|
+
return null;
|
|
447
|
+
}
|
|
448
|
+
return { SilentCallback };
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// src/AuthGate.tsx
|
|
452
|
+
import { useEffect as useEffect2, useState } from "react";
|
|
453
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
454
|
+
function createAuthGate(config, silentAuth, interactiveAuth, hostAuth) {
|
|
455
|
+
const { supabase, callbackPath, timeouts } = config;
|
|
456
|
+
const RETURNING_FROM_OAUTH = typeof window !== "undefined" && (new URLSearchParams(window.location.search).has("code") || window.location.hash.includes("access_token"));
|
|
457
|
+
const IS_CALLBACK_PATH = typeof window !== "undefined" && window.location.pathname === callbackPath;
|
|
458
|
+
function DefaultFullScreenLoader() {
|
|
459
|
+
return /* @__PURE__ */ jsx(
|
|
460
|
+
"div",
|
|
461
|
+
{
|
|
462
|
+
style: { display: "grid", placeItems: "center", height: "100vh", fontFamily: "sans-serif" },
|
|
463
|
+
children: "Signing you in\u2026"
|
|
464
|
+
}
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
function DefaultSignInScreen() {
|
|
468
|
+
const [pending, setPending] = useState(false);
|
|
469
|
+
const [failure, setFailure] = useState(null);
|
|
470
|
+
return /* @__PURE__ */ jsx(
|
|
471
|
+
"div",
|
|
472
|
+
{
|
|
473
|
+
style: { display: "grid", placeItems: "center", height: "100vh", fontFamily: "sans-serif" },
|
|
474
|
+
children: /* @__PURE__ */ jsxs("div", { style: { textAlign: "center" }, children: [
|
|
475
|
+
/* @__PURE__ */ jsx(
|
|
476
|
+
"button",
|
|
477
|
+
{
|
|
478
|
+
type: "button",
|
|
479
|
+
disabled: pending,
|
|
480
|
+
onClick: () => {
|
|
481
|
+
setPending(true);
|
|
482
|
+
setFailure(null);
|
|
483
|
+
void interactiveAuth.signInInteractive().then((result) => {
|
|
484
|
+
setFailure(result.kind === "failed" /* Failed */ ? result.reason : null);
|
|
485
|
+
if (result.kind !== "redirecting" /* Redirecting */) setPending(false);
|
|
486
|
+
});
|
|
487
|
+
},
|
|
488
|
+
children: pending ? "Opening sign-in\u2026" : "Sign in"
|
|
489
|
+
}
|
|
490
|
+
),
|
|
491
|
+
failure ? /* @__PURE__ */ jsx("p", { style: { fontSize: 12, marginTop: 8 }, children: failure === "popup-blocked" /* PopupBlocked */ ? "Your browser blocked the sign-in popup. Allow popups for this site and try again." : "Sign-in didn't complete. Please try again." }) : null
|
|
492
|
+
] })
|
|
493
|
+
}
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
function AuthGate({ children, signInScreen, loader }) {
|
|
497
|
+
const [isCallback, setIsCallback] = useState(false);
|
|
498
|
+
const [status, setStatus] = useState("checking" /* Checking */);
|
|
499
|
+
useEffect2(() => {
|
|
500
|
+
if (IS_CALLBACK_PATH) setIsCallback(true);
|
|
501
|
+
}, []);
|
|
502
|
+
useEffect2(() => {
|
|
503
|
+
if (IS_CALLBACK_PATH) return;
|
|
504
|
+
let cancelled = false;
|
|
505
|
+
let oauthTimer;
|
|
506
|
+
let hostPrincipal;
|
|
507
|
+
let applyGen = 0;
|
|
508
|
+
let acceptPrincipalUpdates = !hostAuth?.isHostWired();
|
|
509
|
+
const applyPrincipal = async (principal) => {
|
|
510
|
+
hostPrincipal = principal;
|
|
511
|
+
const gen = ++applyGen;
|
|
512
|
+
setStatus("checking" /* Checking */);
|
|
513
|
+
const sessionOk = await silentAuth.hasValidSession().catch(() => false);
|
|
514
|
+
if (cancelled || gen !== applyGen) return;
|
|
515
|
+
if (sessionOk && silentAuth.isBoundTo(principal)) {
|
|
516
|
+
setStatus("authenticated" /* Authenticated */);
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
const ok = await silentAuth.rebindSession(principal).catch(() => false);
|
|
520
|
+
if (cancelled || gen !== applyGen) return;
|
|
521
|
+
if (!ok) void supabase.auth.signOut().catch(() => {
|
|
522
|
+
});
|
|
523
|
+
setStatus(ok ? "authenticated" /* Authenticated */ : "unauthenticated" /* Unauthenticated */);
|
|
524
|
+
};
|
|
525
|
+
const openIfBound = async () => {
|
|
526
|
+
if (cancelled) return;
|
|
527
|
+
if (hostAuth?.isHostWired() && !hostPrincipal) return;
|
|
528
|
+
const gen = applyGen;
|
|
529
|
+
const principal = hostPrincipal;
|
|
530
|
+
if (principal) {
|
|
531
|
+
const sessionOk = await silentAuth.hasValidSession().catch(() => false);
|
|
532
|
+
if (cancelled || gen !== applyGen) return;
|
|
533
|
+
if (!(sessionOk && silentAuth.isBoundTo(principal))) return;
|
|
534
|
+
}
|
|
535
|
+
setStatus("authenticated" /* Authenticated */);
|
|
536
|
+
};
|
|
537
|
+
async function resolve() {
|
|
538
|
+
if (RETURNING_FROM_OAUTH) {
|
|
539
|
+
oauthTimer = window.setTimeout(() => {
|
|
540
|
+
if (!cancelled)
|
|
541
|
+
setStatus((s) => s === "checking" /* Checking */ ? "unauthenticated" /* Unauthenticated */ : s);
|
|
542
|
+
}, timeouts.oauthCompletionMs);
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
if (hostAuth?.isHostWired()) {
|
|
546
|
+
acceptPrincipalUpdates = true;
|
|
547
|
+
const next = await hostAuth.waitForSeededPrincipal(timeouts.oauthCompletionMs);
|
|
548
|
+
if (cancelled) return;
|
|
549
|
+
if (next && applyGen === 0) await applyPrincipal(next);
|
|
550
|
+
if (applyGen === 0) setStatus("unauthenticated" /* Unauthenticated */);
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
if (await silentAuth.ensureSession().catch(() => false)) {
|
|
554
|
+
if (!cancelled && !hostPrincipal) setStatus("authenticated" /* Authenticated */);
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
if (!cancelled && !hostPrincipal) setStatus("unauthenticated" /* Unauthenticated */);
|
|
558
|
+
}
|
|
559
|
+
void resolve();
|
|
560
|
+
const unsubPrincipal = hostAuth?.onSeededPrincipal((next) => {
|
|
561
|
+
if (cancelled || !acceptPrincipalUpdates) return;
|
|
562
|
+
void applyPrincipal(next);
|
|
563
|
+
});
|
|
564
|
+
const onStorage = (e) => {
|
|
565
|
+
if (cancelled) return;
|
|
566
|
+
if (!e.key?.endsWith("-auth-token") || !e.newValue) return;
|
|
567
|
+
void openIfBound();
|
|
568
|
+
};
|
|
569
|
+
window.addEventListener("storage", onStorage);
|
|
570
|
+
const { data: sub } = supabase.auth.onAuthStateChange((event, session) => {
|
|
571
|
+
if (cancelled || event === "INITIAL_SESSION") return;
|
|
572
|
+
if (session) void openIfBound();
|
|
573
|
+
else setStatus((s) => s === "authenticated" /* Authenticated */ ? "unauthenticated" /* Unauthenticated */ : s);
|
|
574
|
+
});
|
|
575
|
+
return () => {
|
|
576
|
+
cancelled = true;
|
|
577
|
+
unsubPrincipal?.();
|
|
578
|
+
if (oauthTimer !== void 0) window.clearTimeout(oauthTimer);
|
|
579
|
+
sub.subscription.unsubscribe();
|
|
580
|
+
window.removeEventListener("storage", onStorage);
|
|
581
|
+
};
|
|
582
|
+
}, []);
|
|
583
|
+
if (isCallback) return /* @__PURE__ */ jsx(Fragment, { children });
|
|
584
|
+
if (status === "checking" /* Checking */) return loader ?? /* @__PURE__ */ jsx(DefaultFullScreenLoader, {});
|
|
585
|
+
if (status === "unauthenticated" /* Unauthenticated */) return signInScreen ?? /* @__PURE__ */ jsx(DefaultSignInScreen, {});
|
|
586
|
+
return /* @__PURE__ */ jsx(Fragment, { children });
|
|
587
|
+
}
|
|
588
|
+
return { AuthGate, DefaultSignInScreen };
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// src/hostAuth.ts
|
|
592
|
+
function createHostAuth(config, silentAuth) {
|
|
593
|
+
const { supabase } = config;
|
|
594
|
+
let principal;
|
|
595
|
+
let hostWired = false;
|
|
596
|
+
const seedListeners = /* @__PURE__ */ new Set();
|
|
597
|
+
function isHostWired() {
|
|
598
|
+
return hostWired;
|
|
599
|
+
}
|
|
600
|
+
function onSeededPrincipal(cb) {
|
|
601
|
+
seedListeners.add(cb);
|
|
602
|
+
return () => {
|
|
603
|
+
seedListeners.delete(cb);
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
function waitForSeededPrincipal(timeoutMs) {
|
|
607
|
+
if (principal) return Promise.resolve(principal);
|
|
608
|
+
return new Promise((resolve) => {
|
|
609
|
+
let settled = false;
|
|
610
|
+
const finish = (value) => {
|
|
611
|
+
if (settled) return;
|
|
612
|
+
settled = true;
|
|
613
|
+
window.clearTimeout(timer);
|
|
614
|
+
off();
|
|
615
|
+
resolve(value);
|
|
616
|
+
};
|
|
617
|
+
const timer = window.setTimeout(() => finish(null), timeoutMs);
|
|
618
|
+
const off = onSeededPrincipal((next) => finish(next));
|
|
619
|
+
if (principal) finish(principal);
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
function seedLastUserId(userId, tenant) {
|
|
623
|
+
principal = { userId, tenant };
|
|
624
|
+
for (const listener of [...seedListeners]) listener(principal);
|
|
625
|
+
}
|
|
626
|
+
function wireHostAuth(bridge) {
|
|
627
|
+
hostWired = true;
|
|
628
|
+
return bridge.onAuthChange((auth) => {
|
|
629
|
+
if (!auth.authenticated) {
|
|
630
|
+
principal = void 0;
|
|
631
|
+
silentAuth.clearBoundUser();
|
|
632
|
+
void supabase.auth.signOut().catch(() => {
|
|
633
|
+
});
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
if (!auth.userId) return;
|
|
637
|
+
const tenant = auth.tenant ?? principal?.tenant;
|
|
638
|
+
if (!tenant) {
|
|
639
|
+
console.warn("[vibe-auth] AUTH_CHANGED authenticated without tenant; ignoring");
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
if (principal?.userId === auth.userId && principal.tenant === tenant) return;
|
|
643
|
+
seedLastUserId(auth.userId, tenant);
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
return {
|
|
647
|
+
wireHostAuth,
|
|
648
|
+
seedLastUserId,
|
|
649
|
+
getPrincipal: () => principal,
|
|
650
|
+
isHostWired,
|
|
651
|
+
waitForSeededPrincipal,
|
|
652
|
+
onSeededPrincipal
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// src/createVibeAuth.ts
|
|
657
|
+
function createVibeAuth(userConfig) {
|
|
658
|
+
const config = resolveConfig(userConfig);
|
|
659
|
+
assertPkce(config.supabase);
|
|
660
|
+
let hostAuth;
|
|
661
|
+
const silentAuth = createSilentAuth(config, () => hostAuth.getPrincipal());
|
|
662
|
+
const interactiveAuth = createInteractiveAuth(config, silentAuth);
|
|
663
|
+
const { SilentCallback } = createCallbackHandler();
|
|
664
|
+
hostAuth = createHostAuth(config, silentAuth);
|
|
665
|
+
const { AuthGate, DefaultSignInScreen } = createAuthGate(
|
|
666
|
+
config,
|
|
667
|
+
silentAuth,
|
|
668
|
+
interactiveAuth,
|
|
669
|
+
hostAuth
|
|
670
|
+
);
|
|
671
|
+
const { wireHostAuth, seedLastUserId } = hostAuth;
|
|
672
|
+
return {
|
|
673
|
+
hasValidSession: silentAuth.hasValidSession,
|
|
674
|
+
rebindSession: silentAuth.rebindSession,
|
|
675
|
+
trySilentSignIn: silentAuth.trySilentSignIn,
|
|
676
|
+
ensureSession: silentAuth.ensureSession,
|
|
677
|
+
signInInteractive: interactiveAuth.signInInteractive,
|
|
678
|
+
clearBoundUser: silentAuth.clearBoundUser,
|
|
679
|
+
AuthGate,
|
|
680
|
+
DefaultSignInScreen,
|
|
681
|
+
SilentCallback,
|
|
682
|
+
wireHostAuth,
|
|
683
|
+
seedLastUserId,
|
|
684
|
+
RESULT_MESSAGE_TYPE,
|
|
685
|
+
callbackPath: config.callbackPath
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
export {
|
|
689
|
+
AuthResultKind,
|
|
690
|
+
RESULT_MESSAGE_TYPE,
|
|
691
|
+
SignInFailureReason,
|
|
692
|
+
SignInKind,
|
|
693
|
+
createVibeAuth
|
|
694
|
+
};
|
|
695
|
+
//# sourceMappingURL=index.js.map
|