@sylphx/sdk 0.10.4 → 0.10.5
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/dist/health/index.d.ts +681 -0
- package/dist/index.d.ts +17 -17
- package/dist/index.mjs +6 -6
- package/dist/index.mjs.map +1 -1
- package/dist/nextjs/index.d.ts +3 -0
- package/dist/nextjs/index.mjs +166 -3
- package/dist/nextjs/index.mjs.map +1 -1
- package/dist/react/index.mjs.map +1 -1
- package/dist/server/index.mjs.map +1 -1
- package/dist/web-analytics.mjs.map +1 -1
- package/package.json +1 -1
package/dist/nextjs/index.d.ts
CHANGED
|
@@ -63,6 +63,9 @@ interface SylphxMiddlewareConfig {
|
|
|
63
63
|
afterSignInUrl?: string;
|
|
64
64
|
/**
|
|
65
65
|
* Auth routes prefix. Routes are mounted at:
|
|
66
|
+
* - {prefix}/login — credentials login handler
|
|
67
|
+
* - {prefix}/oauth-providers — enabled social login providers
|
|
68
|
+
* - {prefix}/oauth/authorize — social login start handler
|
|
66
69
|
* - {prefix}/callback — OAuth callback handler
|
|
67
70
|
* - {prefix}/signout — Sign out handler
|
|
68
71
|
*
|
package/dist/nextjs/index.mjs
CHANGED
|
@@ -453,6 +453,8 @@ function resolveNextjsSecretKey(options) {
|
|
|
453
453
|
}
|
|
454
454
|
|
|
455
455
|
// src/nextjs/middleware.ts
|
|
456
|
+
var OAUTH_PKCE_TTL_SECONDS = 10 * 60;
|
|
457
|
+
var OAUTH_PKCE_TTL_MS = OAUTH_PKCE_TTL_SECONDS * 1e3;
|
|
456
458
|
function isTokenResponse(data) {
|
|
457
459
|
return typeof data === "object" && data !== null && "accessToken" in data && "refreshToken" in data && "user" in data && typeof data.accessToken === "string" && typeof data.refreshToken === "string";
|
|
458
460
|
}
|
|
@@ -501,6 +503,151 @@ function resolveSafeRelativeRedirectPath(value, fallback) {
|
|
|
501
503
|
if (isSafeRelativeRedirectPath(fallback)) return fallback;
|
|
502
504
|
return "/";
|
|
503
505
|
}
|
|
506
|
+
function bytesToBase64Url(bytes) {
|
|
507
|
+
let binary = "";
|
|
508
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
509
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/u, "");
|
|
510
|
+
}
|
|
511
|
+
function randomBase64Url(byteLength) {
|
|
512
|
+
const bytes = new Uint8Array(byteLength);
|
|
513
|
+
crypto.getRandomValues(bytes);
|
|
514
|
+
return bytesToBase64Url(bytes);
|
|
515
|
+
}
|
|
516
|
+
async function sha256Base64Url(value) {
|
|
517
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value));
|
|
518
|
+
return bytesToBase64Url(new Uint8Array(digest));
|
|
519
|
+
}
|
|
520
|
+
async function parseJsonObject(request) {
|
|
521
|
+
try {
|
|
522
|
+
const body = await request.json();
|
|
523
|
+
if (typeof body === "object" && body !== null && !Array.isArray(body)) return body;
|
|
524
|
+
return null;
|
|
525
|
+
} catch {
|
|
526
|
+
return null;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
function getOAuthPkceCookieName(ctx) {
|
|
530
|
+
return `__${ctx.namespace}_oauth_pkce`;
|
|
531
|
+
}
|
|
532
|
+
function encodeOAuthPkceCookie(value) {
|
|
533
|
+
return encodeURIComponent(JSON.stringify(value));
|
|
534
|
+
}
|
|
535
|
+
function readOAuthPkceVerifier(request, ctx) {
|
|
536
|
+
const raw = request.cookies.get(getOAuthPkceCookieName(ctx))?.value;
|
|
537
|
+
if (!raw) return null;
|
|
538
|
+
try {
|
|
539
|
+
const parsed = JSON.parse(decodeURIComponent(raw));
|
|
540
|
+
if (typeof parsed.verifier !== "string" || parsed.verifier.length < 43) return null;
|
|
541
|
+
if (typeof parsed.createdAt !== "number") return null;
|
|
542
|
+
if (Date.now() - parsed.createdAt > OAUTH_PKCE_TTL_MS) return null;
|
|
543
|
+
return parsed.verifier;
|
|
544
|
+
} catch {
|
|
545
|
+
return null;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
function setOAuthPkceCookie(response, ctx, value) {
|
|
549
|
+
response.cookies.set(getOAuthPkceCookieName(ctx), encodeOAuthPkceCookie(value), {
|
|
550
|
+
...SECURE_COOKIE_OPTIONS,
|
|
551
|
+
maxAge: OAUTH_PKCE_TTL_SECONDS,
|
|
552
|
+
path: ctx.config.authPrefix
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
function clearOAuthPkceCookie(response, ctx) {
|
|
556
|
+
response.cookies.set(getOAuthPkceCookieName(ctx), "", {
|
|
557
|
+
...SECURE_COOKIE_OPTIONS,
|
|
558
|
+
maxAge: 0,
|
|
559
|
+
path: ctx.config.authPrefix
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
function isTwoFactorLoginResponse(data) {
|
|
563
|
+
return typeof data === "object" && data !== null && data.requiresTwoFactor === true && typeof data.userId === "string";
|
|
564
|
+
}
|
|
565
|
+
function isOAuthAuthorizeResponse(data) {
|
|
566
|
+
return typeof data === "object" && data !== null && typeof data.authorization_url === "string";
|
|
567
|
+
}
|
|
568
|
+
function headersForPlatformAuth(ctx) {
|
|
569
|
+
return {
|
|
570
|
+
"Content-Type": "application/json",
|
|
571
|
+
"x-app-secret": ctx.secretKey
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
async function handleLogin(request, ctx) {
|
|
575
|
+
if (request.method !== "POST") {
|
|
576
|
+
return NextResponse.json({ error: "Method not allowed" }, { status: 405 });
|
|
577
|
+
}
|
|
578
|
+
const body = await parseJsonObject(request);
|
|
579
|
+
const email = typeof body?.email === "string" ? body.email : null;
|
|
580
|
+
const password = typeof body?.password === "string" ? body.password : null;
|
|
581
|
+
if (!email || !password) {
|
|
582
|
+
return NextResponse.json({ error: "email and password are required" }, { status: 400 });
|
|
583
|
+
}
|
|
584
|
+
const res = await fetch(`${ctx.platformUrl}/v1/auth/login`, {
|
|
585
|
+
method: "POST",
|
|
586
|
+
headers: headersForPlatformAuth(ctx),
|
|
587
|
+
body: JSON.stringify({ email, password })
|
|
588
|
+
});
|
|
589
|
+
const data = await res.json().catch(() => ({}));
|
|
590
|
+
if (!res.ok) {
|
|
591
|
+
return NextResponse.json(data, { status: res.status });
|
|
592
|
+
}
|
|
593
|
+
if (isTwoFactorLoginResponse(data)) {
|
|
594
|
+
return NextResponse.json(data);
|
|
595
|
+
}
|
|
596
|
+
if (!isTokenResponse(data)) {
|
|
597
|
+
return NextResponse.json({ error: "invalid_response" }, { status: 502 });
|
|
598
|
+
}
|
|
599
|
+
const response = NextResponse.json({ success: true, user: data.user });
|
|
600
|
+
setAuthCookiesMiddleware(response, ctx.namespace, data);
|
|
601
|
+
return response;
|
|
602
|
+
}
|
|
603
|
+
async function handleOAuthProviders(ctx) {
|
|
604
|
+
const res = await fetch(`${ctx.platformUrl}/v1/auth/oauth-providers`, {
|
|
605
|
+
method: "GET",
|
|
606
|
+
headers: headersForPlatformAuth(ctx)
|
|
607
|
+
});
|
|
608
|
+
const data = await res.json().catch(() => ({}));
|
|
609
|
+
return NextResponse.json(data, { status: res.status });
|
|
610
|
+
}
|
|
611
|
+
async function handleOAuthAuthorize(request, ctx) {
|
|
612
|
+
if (request.method !== "GET" && request.method !== "POST") {
|
|
613
|
+
return NextResponse.json({ error: "Method not allowed" }, { status: 405 });
|
|
614
|
+
}
|
|
615
|
+
const body = request.method === "POST" ? await parseJsonObject(request) : null;
|
|
616
|
+
const provider = typeof body?.provider === "string" ? body.provider : request.nextUrl.searchParams.get("provider");
|
|
617
|
+
const rawRedirectTo = typeof body?.redirectTo === "string" ? body.redirectTo : request.nextUrl.searchParams.get("redirect_to") ?? request.nextUrl.searchParams.get("callbackUrl");
|
|
618
|
+
if (!provider) {
|
|
619
|
+
return NextResponse.json({ error: "provider is required" }, { status: 400 });
|
|
620
|
+
}
|
|
621
|
+
const redirectTo = resolveSafeRelativeRedirectPath(rawRedirectTo, ctx.config.afterSignInUrl);
|
|
622
|
+
const redirectUri = new URL(`${ctx.config.authPrefix}/callback`, request.url);
|
|
623
|
+
redirectUri.searchParams.set("redirect_to", redirectTo);
|
|
624
|
+
const verifier = randomBase64Url(32);
|
|
625
|
+
const challenge = await sha256Base64Url(verifier);
|
|
626
|
+
const scopes = Array.isArray(body?.scopes) ? body.scopes.filter((scope) => typeof scope === "string") : void 0;
|
|
627
|
+
const res = await fetch(`${ctx.platformUrl}/v1/oauth/authorize`, {
|
|
628
|
+
method: "POST",
|
|
629
|
+
headers: headersForPlatformAuth(ctx),
|
|
630
|
+
body: JSON.stringify({
|
|
631
|
+
provider,
|
|
632
|
+
redirect_uri: redirectUri.toString(),
|
|
633
|
+
code_challenge: challenge,
|
|
634
|
+
code_challenge_method: "S256",
|
|
635
|
+
...scopes && scopes.length > 0 ? { scopes } : {}
|
|
636
|
+
})
|
|
637
|
+
});
|
|
638
|
+
const data = await res.json().catch(() => ({}));
|
|
639
|
+
if (!res.ok || !isOAuthAuthorizeResponse(data)) {
|
|
640
|
+
return NextResponse.json(res.ok ? { error: "invalid_response" } : data, {
|
|
641
|
+
status: res.ok ? 502 : res.status
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
const response = request.method === "GET" ? NextResponse.redirect(data.authorization_url) : NextResponse.json({
|
|
645
|
+
authorization_url: data.authorization_url,
|
|
646
|
+
authorizationUrl: data.authorization_url
|
|
647
|
+
});
|
|
648
|
+
setOAuthPkceCookie(response, ctx, { verifier, createdAt: Date.now() });
|
|
649
|
+
return response;
|
|
650
|
+
}
|
|
504
651
|
async function handleCallback(request, ctx) {
|
|
505
652
|
const { searchParams } = request.nextUrl;
|
|
506
653
|
const code = searchParams.get("code");
|
|
@@ -520,30 +667,37 @@ async function handleCallback(request, ctx) {
|
|
|
520
667
|
return NextResponse.redirect(url);
|
|
521
668
|
}
|
|
522
669
|
try {
|
|
670
|
+
const codeVerifier = readOAuthPkceVerifier(request, ctx);
|
|
523
671
|
const res = await fetch(`${ctx.platformUrl}/v1/auth/token`, {
|
|
524
672
|
method: "POST",
|
|
525
673
|
headers: { "Content-Type": "application/json" },
|
|
526
674
|
body: JSON.stringify({
|
|
527
675
|
grant_type: "authorization_code",
|
|
528
676
|
code,
|
|
529
|
-
client_secret: ctx.secretKey
|
|
677
|
+
client_secret: ctx.secretKey,
|
|
678
|
+
...codeVerifier ? { code_verifier: codeVerifier } : {}
|
|
530
679
|
})
|
|
531
680
|
});
|
|
532
681
|
if (!res.ok) {
|
|
533
682
|
const data2 = await res.json().catch(() => ({}));
|
|
534
683
|
const url = new URL(ctx.config.signInUrl, request.url);
|
|
535
684
|
url.searchParams.set("error", data2.error || "token_exchange_failed");
|
|
536
|
-
|
|
685
|
+
const response2 = NextResponse.redirect(url);
|
|
686
|
+
clearOAuthPkceCookie(response2, ctx);
|
|
687
|
+
return response2;
|
|
537
688
|
}
|
|
538
689
|
const data = await res.json();
|
|
539
690
|
if (!isTokenResponse(data)) {
|
|
540
691
|
const url = new URL(ctx.config.signInUrl, request.url);
|
|
541
692
|
url.searchParams.set("error", "invalid_response");
|
|
542
|
-
|
|
693
|
+
const response2 = NextResponse.redirect(url);
|
|
694
|
+
clearOAuthPkceCookie(response2, ctx);
|
|
695
|
+
return response2;
|
|
543
696
|
}
|
|
544
697
|
const successUrl = new URL(redirectTo, request.url);
|
|
545
698
|
const response = NextResponse.redirect(successUrl);
|
|
546
699
|
setAuthCookiesMiddleware(response, ctx.namespace, data);
|
|
700
|
+
clearOAuthPkceCookie(response, ctx);
|
|
547
701
|
ctx.log("Callback success", { redirectTo });
|
|
548
702
|
return response;
|
|
549
703
|
} catch (err) {
|
|
@@ -947,6 +1101,15 @@ function createSylphxMiddleware(userConfig = {}) {
|
|
|
947
1101
|
if (pathname === `${config.authPrefix}/callback`) {
|
|
948
1102
|
return handleCallback(request, ctx);
|
|
949
1103
|
}
|
|
1104
|
+
if (pathname === `${config.authPrefix}/login`) {
|
|
1105
|
+
return handleLogin(request, ctx);
|
|
1106
|
+
}
|
|
1107
|
+
if (pathname === `${config.authPrefix}/oauth-providers`) {
|
|
1108
|
+
return handleOAuthProviders(ctx);
|
|
1109
|
+
}
|
|
1110
|
+
if (pathname === `${config.authPrefix}/oauth/authorize`) {
|
|
1111
|
+
return handleOAuthAuthorize(request, ctx);
|
|
1112
|
+
}
|
|
950
1113
|
if (pathname === `${config.authPrefix}/signout`) {
|
|
951
1114
|
return handleSignOut(request, ctx);
|
|
952
1115
|
}
|