@sylphx/sdk 0.10.4 → 0.10.6
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/index.d.ts +27 -25
- package/dist/index.mjs +12 -9
- package/dist/index.mjs.map +1 -1
- package/dist/nextjs/index.d.ts +21 -0
- package/dist/nextjs/index.mjs +351 -4
- package/dist/nextjs/index.mjs.map +1 -1
- package/dist/react/index.d.ts +5 -1
- package/dist/react/index.mjs +34 -17
- 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
|
@@ -6,6 +6,7 @@ import { AuthTokensResponse } from '@sylphx/contract';
|
|
|
6
6
|
*
|
|
7
7
|
* ONE middleware function handles EVERYTHING:
|
|
8
8
|
* - Auth routes (mounted automatically, zero manual API routes)
|
|
9
|
+
* - BaaS routes (same-origin proxy, no browser bearer-token exposure)
|
|
9
10
|
* - Token refresh (automatic, every request)
|
|
10
11
|
* - Route protection
|
|
11
12
|
* - Cookie management
|
|
@@ -63,12 +64,32 @@ interface SylphxMiddlewareConfig {
|
|
|
63
64
|
afterSignInUrl?: string;
|
|
64
65
|
/**
|
|
65
66
|
* Auth routes prefix. Routes are mounted at:
|
|
67
|
+
* - {prefix}/login — credentials login handler
|
|
68
|
+
* - {prefix}/oauth-providers — enabled social login providers
|
|
69
|
+
* - {prefix}/oauth/authorize — social login start handler
|
|
66
70
|
* - {prefix}/callback — OAuth callback handler
|
|
71
|
+
* - {prefix}/passkey/options — passkey login challenge handler
|
|
72
|
+
* - {prefix}/passkey/authenticate — passkey login verification handler
|
|
73
|
+
* - {prefix}/forgot-password — password reset email handler
|
|
74
|
+
* - {prefix}/reset-password — password reset verification handler
|
|
67
75
|
* - {prefix}/signout — Sign out handler
|
|
68
76
|
*
|
|
69
77
|
* @default '/auth'
|
|
70
78
|
*/
|
|
71
79
|
authPrefix?: string;
|
|
80
|
+
/**
|
|
81
|
+
* Same-origin BaaS proxy prefix.
|
|
82
|
+
*
|
|
83
|
+
* Mounted routes are allowlisted SDK-owned BaaS surfaces such as
|
|
84
|
+
* `{prefix}/security/*` and `{prefix}/challenge/*`. The middleware converts
|
|
85
|
+
* the HttpOnly session cookie into an upstream bearer token, so browser code
|
|
86
|
+
* can use BaaS features without ever reading Platform access tokens.
|
|
87
|
+
*
|
|
88
|
+
* Set to `false` to disable the proxy.
|
|
89
|
+
*
|
|
90
|
+
* @default '/sylphx'
|
|
91
|
+
*/
|
|
92
|
+
baasPrefix?: string | false;
|
|
72
93
|
/**
|
|
73
94
|
* Enable debug logging.
|
|
74
95
|
* @default false
|
package/dist/nextjs/index.mjs
CHANGED
|
@@ -453,6 +453,21 @@ 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;
|
|
458
|
+
var DEFAULT_BAAS_PREFIX = "/sylphx";
|
|
459
|
+
var BAAS_PROXY_AUTH_REQUIRED_PATHS = ["/challenge/", "/security/"];
|
|
460
|
+
var BAAS_PROXY_PUBLIC_PATHS = ["/app"];
|
|
461
|
+
var BODYLESS_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD"]);
|
|
462
|
+
var RESPONSE_HEADER_ALLOWLIST = [
|
|
463
|
+
"cache-control",
|
|
464
|
+
"content-language",
|
|
465
|
+
"content-type",
|
|
466
|
+
"etag",
|
|
467
|
+
"expires",
|
|
468
|
+
"last-modified",
|
|
469
|
+
"retry-after"
|
|
470
|
+
];
|
|
456
471
|
function isTokenResponse(data) {
|
|
457
472
|
return typeof data === "object" && data !== null && "accessToken" in data && "refreshToken" in data && "user" in data && typeof data.accessToken === "string" && typeof data.refreshToken === "string";
|
|
458
473
|
}
|
|
@@ -489,6 +504,46 @@ function matchesPattern(pathname, pattern) {
|
|
|
489
504
|
function matchesAny(pathname, patterns) {
|
|
490
505
|
return patterns.some((p) => matchesPattern(pathname, p));
|
|
491
506
|
}
|
|
507
|
+
function normalizeRoutePrefix(prefix) {
|
|
508
|
+
const normalized = prefix.trim().replace(/\/+$/u, "");
|
|
509
|
+
return normalized.startsWith("/") ? normalized : `/${normalized}`;
|
|
510
|
+
}
|
|
511
|
+
function stripRoutePrefix(pathname, prefix) {
|
|
512
|
+
if (pathname === prefix) return "/";
|
|
513
|
+
if (!pathname.startsWith(`${prefix}/`)) return null;
|
|
514
|
+
return pathname.slice(prefix.length);
|
|
515
|
+
}
|
|
516
|
+
function isPublicBaasProxyPath(pathname) {
|
|
517
|
+
return BAAS_PROXY_PUBLIC_PATHS.some((path) => pathname === path);
|
|
518
|
+
}
|
|
519
|
+
function isAuthenticatedBaasProxyPath(pathname) {
|
|
520
|
+
return BAAS_PROXY_AUTH_REQUIRED_PATHS.some((prefix) => pathname.startsWith(prefix));
|
|
521
|
+
}
|
|
522
|
+
function isAllowedBaasProxyPath(pathname) {
|
|
523
|
+
return isPublicBaasProxyPath(pathname) || isAuthenticatedBaasProxyPath(pathname);
|
|
524
|
+
}
|
|
525
|
+
function copyRequestHeader(source, target, name) {
|
|
526
|
+
const value = source.get(name);
|
|
527
|
+
if (value) target.set(name, value);
|
|
528
|
+
}
|
|
529
|
+
function buildUpstreamProxyHeaders(request, ctx, sessionToken) {
|
|
530
|
+
const headers = new Headers();
|
|
531
|
+
copyRequestHeader(request.headers, headers, "accept");
|
|
532
|
+
copyRequestHeader(request.headers, headers, "content-type");
|
|
533
|
+
copyRequestHeader(request.headers, headers, "user-agent");
|
|
534
|
+
copyRequestHeader(request.headers, headers, "x-correlation-id");
|
|
535
|
+
headers.set("x-app-secret", ctx.secretKey);
|
|
536
|
+
if (sessionToken) headers.set("authorization", `Bearer ${sessionToken}`);
|
|
537
|
+
return headers;
|
|
538
|
+
}
|
|
539
|
+
function copyResponseHeaders(source) {
|
|
540
|
+
const headers = new Headers();
|
|
541
|
+
for (const name of RESPONSE_HEADER_ALLOWLIST) {
|
|
542
|
+
const value = source.get(name);
|
|
543
|
+
if (value) headers.set(name, value);
|
|
544
|
+
}
|
|
545
|
+
return headers;
|
|
546
|
+
}
|
|
492
547
|
function requestPathWithSearch(request) {
|
|
493
548
|
const { pathname, search } = request.nextUrl;
|
|
494
549
|
return `${pathname}${search}`;
|
|
@@ -501,6 +556,241 @@ function resolveSafeRelativeRedirectPath(value, fallback) {
|
|
|
501
556
|
if (isSafeRelativeRedirectPath(fallback)) return fallback;
|
|
502
557
|
return "/";
|
|
503
558
|
}
|
|
559
|
+
function resolveSameOriginUrl(request, value, fallbackPath) {
|
|
560
|
+
if (!value) return new URL(fallbackPath, request.url).toString();
|
|
561
|
+
if (isSafeRelativeRedirectPath(value)) {
|
|
562
|
+
const url = new URL(value, request.url);
|
|
563
|
+
if (url.hash) return null;
|
|
564
|
+
return url.toString();
|
|
565
|
+
}
|
|
566
|
+
try {
|
|
567
|
+
const url = new URL(value);
|
|
568
|
+
if (url.origin !== new URL(request.url).origin) return null;
|
|
569
|
+
if (url.hash) return null;
|
|
570
|
+
return url.toString();
|
|
571
|
+
} catch {
|
|
572
|
+
return null;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
function bytesToBase64Url(bytes) {
|
|
576
|
+
let binary = "";
|
|
577
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
578
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/u, "");
|
|
579
|
+
}
|
|
580
|
+
function randomBase64Url(byteLength) {
|
|
581
|
+
const bytes = new Uint8Array(byteLength);
|
|
582
|
+
crypto.getRandomValues(bytes);
|
|
583
|
+
return bytesToBase64Url(bytes);
|
|
584
|
+
}
|
|
585
|
+
async function sha256Base64Url(value) {
|
|
586
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value));
|
|
587
|
+
return bytesToBase64Url(new Uint8Array(digest));
|
|
588
|
+
}
|
|
589
|
+
async function parseJsonObject(request) {
|
|
590
|
+
try {
|
|
591
|
+
const body = await request.json();
|
|
592
|
+
if (typeof body === "object" && body !== null && !Array.isArray(body)) return body;
|
|
593
|
+
return null;
|
|
594
|
+
} catch {
|
|
595
|
+
return null;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
function getOAuthPkceCookieName(ctx) {
|
|
599
|
+
return `__${ctx.namespace}_oauth_pkce`;
|
|
600
|
+
}
|
|
601
|
+
function encodeOAuthPkceCookie(value) {
|
|
602
|
+
return encodeURIComponent(JSON.stringify(value));
|
|
603
|
+
}
|
|
604
|
+
function readOAuthPkceVerifier(request, ctx) {
|
|
605
|
+
const raw = request.cookies.get(getOAuthPkceCookieName(ctx))?.value;
|
|
606
|
+
if (!raw) return null;
|
|
607
|
+
try {
|
|
608
|
+
const parsed = JSON.parse(decodeURIComponent(raw));
|
|
609
|
+
if (typeof parsed.verifier !== "string" || parsed.verifier.length < 43) return null;
|
|
610
|
+
if (typeof parsed.createdAt !== "number") return null;
|
|
611
|
+
if (Date.now() - parsed.createdAt > OAUTH_PKCE_TTL_MS) return null;
|
|
612
|
+
return parsed.verifier;
|
|
613
|
+
} catch {
|
|
614
|
+
return null;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
function setOAuthPkceCookie(response, ctx, value) {
|
|
618
|
+
response.cookies.set(getOAuthPkceCookieName(ctx), encodeOAuthPkceCookie(value), {
|
|
619
|
+
...SECURE_COOKIE_OPTIONS,
|
|
620
|
+
maxAge: OAUTH_PKCE_TTL_SECONDS,
|
|
621
|
+
path: ctx.config.authPrefix
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
function clearOAuthPkceCookie(response, ctx) {
|
|
625
|
+
response.cookies.set(getOAuthPkceCookieName(ctx), "", {
|
|
626
|
+
...SECURE_COOKIE_OPTIONS,
|
|
627
|
+
maxAge: 0,
|
|
628
|
+
path: ctx.config.authPrefix
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
function isTwoFactorLoginResponse(data) {
|
|
632
|
+
return typeof data === "object" && data !== null && data.requiresTwoFactor === true && typeof data.userId === "string";
|
|
633
|
+
}
|
|
634
|
+
function isOAuthAuthorizeResponse(data) {
|
|
635
|
+
return typeof data === "object" && data !== null && typeof data.authorization_url === "string";
|
|
636
|
+
}
|
|
637
|
+
function headersForPlatformAuth(ctx) {
|
|
638
|
+
return {
|
|
639
|
+
"Content-Type": "application/json",
|
|
640
|
+
"x-app-secret": ctx.secretKey
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
async function handleLogin(request, ctx) {
|
|
644
|
+
if (request.method !== "POST") {
|
|
645
|
+
return NextResponse.json({ error: "Method not allowed" }, { status: 405 });
|
|
646
|
+
}
|
|
647
|
+
const body = await parseJsonObject(request);
|
|
648
|
+
const email = typeof body?.email === "string" ? body.email : null;
|
|
649
|
+
const password = typeof body?.password === "string" ? body.password : null;
|
|
650
|
+
if (!email || !password) {
|
|
651
|
+
return NextResponse.json({ error: "email and password are required" }, { status: 400 });
|
|
652
|
+
}
|
|
653
|
+
const res = await fetch(`${ctx.platformUrl}/v1/auth/login`, {
|
|
654
|
+
method: "POST",
|
|
655
|
+
headers: headersForPlatformAuth(ctx),
|
|
656
|
+
body: JSON.stringify({ email, password })
|
|
657
|
+
});
|
|
658
|
+
const data = await res.json().catch(() => ({}));
|
|
659
|
+
if (!res.ok) {
|
|
660
|
+
return NextResponse.json(data, { status: res.status });
|
|
661
|
+
}
|
|
662
|
+
if (isTwoFactorLoginResponse(data)) {
|
|
663
|
+
return NextResponse.json(data);
|
|
664
|
+
}
|
|
665
|
+
if (!isTokenResponse(data)) {
|
|
666
|
+
return NextResponse.json({ error: "invalid_response" }, { status: 502 });
|
|
667
|
+
}
|
|
668
|
+
const response = NextResponse.json({ success: true, user: data.user });
|
|
669
|
+
setAuthCookiesMiddleware(response, ctx.namespace, data);
|
|
670
|
+
return response;
|
|
671
|
+
}
|
|
672
|
+
async function handleOAuthProviders(ctx) {
|
|
673
|
+
const res = await fetch(`${ctx.platformUrl}/v1/auth/oauth-providers`, {
|
|
674
|
+
method: "GET",
|
|
675
|
+
headers: headersForPlatformAuth(ctx)
|
|
676
|
+
});
|
|
677
|
+
const data = await res.json().catch(() => ({}));
|
|
678
|
+
return NextResponse.json(data, { status: res.status });
|
|
679
|
+
}
|
|
680
|
+
async function handleOAuthAuthorize(request, ctx) {
|
|
681
|
+
if (request.method !== "GET" && request.method !== "POST") {
|
|
682
|
+
return NextResponse.json({ error: "Method not allowed" }, { status: 405 });
|
|
683
|
+
}
|
|
684
|
+
const body = request.method === "POST" ? await parseJsonObject(request) : null;
|
|
685
|
+
const provider = typeof body?.provider === "string" ? body.provider : request.nextUrl.searchParams.get("provider");
|
|
686
|
+
const rawRedirectTo = typeof body?.redirectTo === "string" ? body.redirectTo : request.nextUrl.searchParams.get("redirect_to") ?? request.nextUrl.searchParams.get("callbackUrl");
|
|
687
|
+
if (!provider) {
|
|
688
|
+
return NextResponse.json({ error: "provider is required" }, { status: 400 });
|
|
689
|
+
}
|
|
690
|
+
const redirectTo = resolveSafeRelativeRedirectPath(rawRedirectTo, ctx.config.afterSignInUrl);
|
|
691
|
+
const redirectUri = new URL(`${ctx.config.authPrefix}/callback`, request.url);
|
|
692
|
+
redirectUri.searchParams.set("redirect_to", redirectTo);
|
|
693
|
+
const verifier = randomBase64Url(32);
|
|
694
|
+
const challenge = await sha256Base64Url(verifier);
|
|
695
|
+
const scopes = Array.isArray(body?.scopes) ? body.scopes.filter((scope) => typeof scope === "string") : void 0;
|
|
696
|
+
const res = await fetch(`${ctx.platformUrl}/v1/oauth/authorize`, {
|
|
697
|
+
method: "POST",
|
|
698
|
+
headers: headersForPlatformAuth(ctx),
|
|
699
|
+
body: JSON.stringify({
|
|
700
|
+
provider,
|
|
701
|
+
redirect_uri: redirectUri.toString(),
|
|
702
|
+
code_challenge: challenge,
|
|
703
|
+
code_challenge_method: "S256",
|
|
704
|
+
...scopes && scopes.length > 0 ? { scopes } : {}
|
|
705
|
+
})
|
|
706
|
+
});
|
|
707
|
+
const data = await res.json().catch(() => ({}));
|
|
708
|
+
if (!res.ok || !isOAuthAuthorizeResponse(data)) {
|
|
709
|
+
return NextResponse.json(res.ok ? { error: "invalid_response" } : data, {
|
|
710
|
+
status: res.ok ? 502 : res.status
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
const response = request.method === "GET" ? NextResponse.redirect(data.authorization_url) : NextResponse.json({
|
|
714
|
+
authorization_url: data.authorization_url,
|
|
715
|
+
authorizationUrl: data.authorization_url
|
|
716
|
+
});
|
|
717
|
+
setOAuthPkceCookie(response, ctx, { verifier, createdAt: Date.now() });
|
|
718
|
+
return response;
|
|
719
|
+
}
|
|
720
|
+
async function handlePasskeyOptions(request, ctx) {
|
|
721
|
+
if (request.method !== "POST") {
|
|
722
|
+
return NextResponse.json({ error: "Method not allowed" }, { status: 405 });
|
|
723
|
+
}
|
|
724
|
+
const body = await parseJsonObject(request);
|
|
725
|
+
const res = await fetch(`${ctx.platformUrl}/v1/auth/passkey/options`, {
|
|
726
|
+
method: "POST",
|
|
727
|
+
headers: headersForPlatformAuth(ctx),
|
|
728
|
+
body: JSON.stringify(body ?? {})
|
|
729
|
+
});
|
|
730
|
+
const data = await res.json().catch(() => ({}));
|
|
731
|
+
return NextResponse.json(data, { status: res.status });
|
|
732
|
+
}
|
|
733
|
+
async function handlePasskeyAuthenticate(request, ctx) {
|
|
734
|
+
if (request.method !== "POST") {
|
|
735
|
+
return NextResponse.json({ error: "Method not allowed" }, { status: 405 });
|
|
736
|
+
}
|
|
737
|
+
const body = await parseJsonObject(request);
|
|
738
|
+
const res = await fetch(`${ctx.platformUrl}/v1/auth/passkey/authenticate`, {
|
|
739
|
+
method: "POST",
|
|
740
|
+
headers: headersForPlatformAuth(ctx),
|
|
741
|
+
body: JSON.stringify(body ?? {})
|
|
742
|
+
});
|
|
743
|
+
const data = await res.json().catch(() => ({}));
|
|
744
|
+
if (!res.ok) {
|
|
745
|
+
return NextResponse.json(data, { status: res.status });
|
|
746
|
+
}
|
|
747
|
+
if (!isTokenResponse(data)) {
|
|
748
|
+
return NextResponse.json({ error: "invalid_response" }, { status: 502 });
|
|
749
|
+
}
|
|
750
|
+
const response = NextResponse.json({ success: true, user: data.user });
|
|
751
|
+
setAuthCookiesMiddleware(response, ctx.namespace, data);
|
|
752
|
+
return response;
|
|
753
|
+
}
|
|
754
|
+
async function handleForgotPassword(request, ctx) {
|
|
755
|
+
if (request.method !== "POST") {
|
|
756
|
+
return NextResponse.json({ error: "Method not allowed" }, { status: 405 });
|
|
757
|
+
}
|
|
758
|
+
const body = await parseJsonObject(request);
|
|
759
|
+
const email = typeof body?.email === "string" ? body.email : null;
|
|
760
|
+
const rawRedirect = typeof body?.redirectUrl === "string" ? body.redirectUrl : typeof body?.redirectTo === "string" ? body.redirectTo : null;
|
|
761
|
+
const redirectUrl = resolveSameOriginUrl(request, rawRedirect, "/reset-password");
|
|
762
|
+
if (!email) {
|
|
763
|
+
return NextResponse.json({ error: "email is required" }, { status: 400 });
|
|
764
|
+
}
|
|
765
|
+
if (!redirectUrl) {
|
|
766
|
+
return NextResponse.json({ error: "invalid_redirect_url" }, { status: 400 });
|
|
767
|
+
}
|
|
768
|
+
const res = await fetch(`${ctx.platformUrl}/v1/auth/forgot-password`, {
|
|
769
|
+
method: "POST",
|
|
770
|
+
headers: headersForPlatformAuth(ctx),
|
|
771
|
+
body: JSON.stringify({ email, redirectUrl })
|
|
772
|
+
});
|
|
773
|
+
const data = await res.json().catch(() => ({}));
|
|
774
|
+
return NextResponse.json(data, { status: res.status });
|
|
775
|
+
}
|
|
776
|
+
async function handleResetPassword(request, ctx) {
|
|
777
|
+
if (request.method !== "POST") {
|
|
778
|
+
return NextResponse.json({ error: "Method not allowed" }, { status: 405 });
|
|
779
|
+
}
|
|
780
|
+
const body = await parseJsonObject(request);
|
|
781
|
+
const token = typeof body?.token === "string" ? body.token : null;
|
|
782
|
+
const password = typeof body?.password === "string" ? body.password : typeof body?.newPassword === "string" ? body.newPassword : null;
|
|
783
|
+
if (!token || !password) {
|
|
784
|
+
return NextResponse.json({ error: "token and password are required" }, { status: 400 });
|
|
785
|
+
}
|
|
786
|
+
const res = await fetch(`${ctx.platformUrl}/v1/auth/reset-password`, {
|
|
787
|
+
method: "POST",
|
|
788
|
+
headers: headersForPlatformAuth(ctx),
|
|
789
|
+
body: JSON.stringify({ token, password })
|
|
790
|
+
});
|
|
791
|
+
const data = await res.json().catch(() => ({}));
|
|
792
|
+
return NextResponse.json(data, { status: res.status });
|
|
793
|
+
}
|
|
504
794
|
async function handleCallback(request, ctx) {
|
|
505
795
|
const { searchParams } = request.nextUrl;
|
|
506
796
|
const code = searchParams.get("code");
|
|
@@ -520,30 +810,37 @@ async function handleCallback(request, ctx) {
|
|
|
520
810
|
return NextResponse.redirect(url);
|
|
521
811
|
}
|
|
522
812
|
try {
|
|
813
|
+
const codeVerifier = readOAuthPkceVerifier(request, ctx);
|
|
523
814
|
const res = await fetch(`${ctx.platformUrl}/v1/auth/token`, {
|
|
524
815
|
method: "POST",
|
|
525
816
|
headers: { "Content-Type": "application/json" },
|
|
526
817
|
body: JSON.stringify({
|
|
527
818
|
grant_type: "authorization_code",
|
|
528
819
|
code,
|
|
529
|
-
client_secret: ctx.secretKey
|
|
820
|
+
client_secret: ctx.secretKey,
|
|
821
|
+
...codeVerifier ? { code_verifier: codeVerifier } : {}
|
|
530
822
|
})
|
|
531
823
|
});
|
|
532
824
|
if (!res.ok) {
|
|
533
825
|
const data2 = await res.json().catch(() => ({}));
|
|
534
826
|
const url = new URL(ctx.config.signInUrl, request.url);
|
|
535
827
|
url.searchParams.set("error", data2.error || "token_exchange_failed");
|
|
536
|
-
|
|
828
|
+
const response2 = NextResponse.redirect(url);
|
|
829
|
+
clearOAuthPkceCookie(response2, ctx);
|
|
830
|
+
return response2;
|
|
537
831
|
}
|
|
538
832
|
const data = await res.json();
|
|
539
833
|
if (!isTokenResponse(data)) {
|
|
540
834
|
const url = new URL(ctx.config.signInUrl, request.url);
|
|
541
835
|
url.searchParams.set("error", "invalid_response");
|
|
542
|
-
|
|
836
|
+
const response2 = NextResponse.redirect(url);
|
|
837
|
+
clearOAuthPkceCookie(response2, ctx);
|
|
838
|
+
return response2;
|
|
543
839
|
}
|
|
544
840
|
const successUrl = new URL(redirectTo, request.url);
|
|
545
841
|
const response = NextResponse.redirect(successUrl);
|
|
546
842
|
setAuthCookiesMiddleware(response, ctx.namespace, data);
|
|
843
|
+
clearOAuthPkceCookie(response, ctx);
|
|
547
844
|
ctx.log("Callback success", { redirectTo });
|
|
548
845
|
return response;
|
|
549
846
|
} catch (err) {
|
|
@@ -832,6 +1129,30 @@ async function handleSwitchOrg(request, ctx) {
|
|
|
832
1129
|
return NextResponse.json({ error: "internal_error" }, { status: 500 });
|
|
833
1130
|
}
|
|
834
1131
|
}
|
|
1132
|
+
async function handleBaasProxy(request, ctx, sessionToken) {
|
|
1133
|
+
const prefix = ctx.config.baasPrefix;
|
|
1134
|
+
if (!prefix) return NextResponse.json({ error: "Not found" }, { status: 404 });
|
|
1135
|
+
const upstreamPath = stripRoutePrefix(request.nextUrl.pathname, prefix);
|
|
1136
|
+
if (!upstreamPath || !isAllowedBaasProxyPath(upstreamPath)) {
|
|
1137
|
+
return NextResponse.json({ error: "Not found" }, { status: 404 });
|
|
1138
|
+
}
|
|
1139
|
+
if (isAuthenticatedBaasProxyPath(upstreamPath) && !sessionToken) {
|
|
1140
|
+
return NextResponse.json({ error: "Not authenticated" }, { status: 401 });
|
|
1141
|
+
}
|
|
1142
|
+
const upstreamUrl = new URL(`${ctx.platformUrl}/v1${upstreamPath}`);
|
|
1143
|
+
upstreamUrl.search = request.nextUrl.search;
|
|
1144
|
+
const method = request.method.toUpperCase();
|
|
1145
|
+
const body = BODYLESS_METHODS.has(method) ? void 0 : await request.arrayBuffer();
|
|
1146
|
+
const res = await fetch(upstreamUrl, {
|
|
1147
|
+
method,
|
|
1148
|
+
headers: buildUpstreamProxyHeaders(request, ctx, sessionToken),
|
|
1149
|
+
body
|
|
1150
|
+
});
|
|
1151
|
+
return new NextResponse(res.body, {
|
|
1152
|
+
status: res.status,
|
|
1153
|
+
headers: copyResponseHeaders(res.headers)
|
|
1154
|
+
});
|
|
1155
|
+
}
|
|
835
1156
|
async function refreshTokens(refreshToken, ctx) {
|
|
836
1157
|
ctx.log("Refreshing tokens");
|
|
837
1158
|
try {
|
|
@@ -896,6 +1217,7 @@ function createSylphxMiddleware(userConfig = {}) {
|
|
|
896
1217
|
afterSignOutUrl: userConfig.afterSignOutUrl ?? "/",
|
|
897
1218
|
afterSignInUrl: userConfig.afterSignInUrl ?? "/dashboard",
|
|
898
1219
|
authPrefix: userConfig.authPrefix ?? "/auth",
|
|
1220
|
+
baasPrefix: userConfig.baasPrefix === false ? null : normalizeRoutePrefix(userConfig.baasPrefix ?? DEFAULT_BAAS_PREFIX),
|
|
899
1221
|
debug: userConfig.debug ?? false,
|
|
900
1222
|
orgRequired: userConfig.orgRequired ?? false,
|
|
901
1223
|
orgSelectionUrl: userConfig.orgSelectionUrl ?? "/select-organization",
|
|
@@ -911,7 +1233,8 @@ function createSylphxMiddleware(userConfig = {}) {
|
|
|
911
1233
|
...config.publicRoutes,
|
|
912
1234
|
config.signInUrl,
|
|
913
1235
|
"/signup",
|
|
914
|
-
`${config.authPrefix}
|
|
1236
|
+
`${config.authPrefix}/*`,
|
|
1237
|
+
...config.baasPrefix ? [`${config.baasPrefix}/*`] : []
|
|
915
1238
|
];
|
|
916
1239
|
const log = (msg, data) => {
|
|
917
1240
|
if (config.debug) {
|
|
@@ -947,6 +1270,27 @@ function createSylphxMiddleware(userConfig = {}) {
|
|
|
947
1270
|
if (pathname === `${config.authPrefix}/callback`) {
|
|
948
1271
|
return handleCallback(request, ctx);
|
|
949
1272
|
}
|
|
1273
|
+
if (pathname === `${config.authPrefix}/login`) {
|
|
1274
|
+
return handleLogin(request, ctx);
|
|
1275
|
+
}
|
|
1276
|
+
if (pathname === `${config.authPrefix}/oauth-providers`) {
|
|
1277
|
+
return handleOAuthProviders(ctx);
|
|
1278
|
+
}
|
|
1279
|
+
if (pathname === `${config.authPrefix}/oauth/authorize`) {
|
|
1280
|
+
return handleOAuthAuthorize(request, ctx);
|
|
1281
|
+
}
|
|
1282
|
+
if (pathname === `${config.authPrefix}/passkey/options`) {
|
|
1283
|
+
return handlePasskeyOptions(request, ctx);
|
|
1284
|
+
}
|
|
1285
|
+
if (pathname === `${config.authPrefix}/passkey/authenticate`) {
|
|
1286
|
+
return handlePasskeyAuthenticate(request, ctx);
|
|
1287
|
+
}
|
|
1288
|
+
if (pathname === `${config.authPrefix}/forgot-password`) {
|
|
1289
|
+
return handleForgotPassword(request, ctx);
|
|
1290
|
+
}
|
|
1291
|
+
if (pathname === `${config.authPrefix}/reset-password`) {
|
|
1292
|
+
return handleResetPassword(request, ctx);
|
|
1293
|
+
}
|
|
950
1294
|
if (pathname === `${config.authPrefix}/signout`) {
|
|
951
1295
|
return handleSignOut(request, ctx);
|
|
952
1296
|
}
|
|
@@ -984,6 +1328,9 @@ function createSylphxMiddleware(userConfig = {}) {
|
|
|
984
1328
|
activeSessionToken = null;
|
|
985
1329
|
}
|
|
986
1330
|
}
|
|
1331
|
+
if (config.baasPrefix && stripRoutePrefix(pathname, config.baasPrefix)) {
|
|
1332
|
+
return handleBaasProxy(request, ctx, activeSessionToken);
|
|
1333
|
+
}
|
|
987
1334
|
const isPublic = matchesAny(pathname, publicRoutes);
|
|
988
1335
|
if (!isPublic && !isAuthenticated) {
|
|
989
1336
|
log("Redirecting to sign-in");
|