@sylphx/sdk 0.10.5 → 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 +10 -8
- package/dist/index.mjs +6 -3
- package/dist/index.mjs.map +1 -1
- package/dist/nextjs/index.d.ts +18 -0
- package/dist/nextjs/index.mjs +185 -1
- 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/package.json +1 -1
- package/dist/health/index.d.ts +0 -681
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
|
|
@@ -67,11 +68,28 @@ interface SylphxMiddlewareConfig {
|
|
|
67
68
|
* - {prefix}/oauth-providers — enabled social login providers
|
|
68
69
|
* - {prefix}/oauth/authorize — social login start handler
|
|
69
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
|
|
70
75
|
* - {prefix}/signout — Sign out handler
|
|
71
76
|
*
|
|
72
77
|
* @default '/auth'
|
|
73
78
|
*/
|
|
74
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;
|
|
75
93
|
/**
|
|
76
94
|
* Enable debug logging.
|
|
77
95
|
* @default false
|
package/dist/nextjs/index.mjs
CHANGED
|
@@ -455,6 +455,19 @@ function resolveNextjsSecretKey(options) {
|
|
|
455
455
|
// src/nextjs/middleware.ts
|
|
456
456
|
var OAUTH_PKCE_TTL_SECONDS = 10 * 60;
|
|
457
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
|
+
];
|
|
458
471
|
function isTokenResponse(data) {
|
|
459
472
|
return typeof data === "object" && data !== null && "accessToken" in data && "refreshToken" in data && "user" in data && typeof data.accessToken === "string" && typeof data.refreshToken === "string";
|
|
460
473
|
}
|
|
@@ -491,6 +504,46 @@ function matchesPattern(pathname, pattern) {
|
|
|
491
504
|
function matchesAny(pathname, patterns) {
|
|
492
505
|
return patterns.some((p) => matchesPattern(pathname, p));
|
|
493
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
|
+
}
|
|
494
547
|
function requestPathWithSearch(request) {
|
|
495
548
|
const { pathname, search } = request.nextUrl;
|
|
496
549
|
return `${pathname}${search}`;
|
|
@@ -503,6 +556,22 @@ function resolveSafeRelativeRedirectPath(value, fallback) {
|
|
|
503
556
|
if (isSafeRelativeRedirectPath(fallback)) return fallback;
|
|
504
557
|
return "/";
|
|
505
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
|
+
}
|
|
506
575
|
function bytesToBase64Url(bytes) {
|
|
507
576
|
let binary = "";
|
|
508
577
|
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
@@ -648,6 +717,80 @@ async function handleOAuthAuthorize(request, ctx) {
|
|
|
648
717
|
setOAuthPkceCookie(response, ctx, { verifier, createdAt: Date.now() });
|
|
649
718
|
return response;
|
|
650
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
|
+
}
|
|
651
794
|
async function handleCallback(request, ctx) {
|
|
652
795
|
const { searchParams } = request.nextUrl;
|
|
653
796
|
const code = searchParams.get("code");
|
|
@@ -986,6 +1129,30 @@ async function handleSwitchOrg(request, ctx) {
|
|
|
986
1129
|
return NextResponse.json({ error: "internal_error" }, { status: 500 });
|
|
987
1130
|
}
|
|
988
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
|
+
}
|
|
989
1156
|
async function refreshTokens(refreshToken, ctx) {
|
|
990
1157
|
ctx.log("Refreshing tokens");
|
|
991
1158
|
try {
|
|
@@ -1050,6 +1217,7 @@ function createSylphxMiddleware(userConfig = {}) {
|
|
|
1050
1217
|
afterSignOutUrl: userConfig.afterSignOutUrl ?? "/",
|
|
1051
1218
|
afterSignInUrl: userConfig.afterSignInUrl ?? "/dashboard",
|
|
1052
1219
|
authPrefix: userConfig.authPrefix ?? "/auth",
|
|
1220
|
+
baasPrefix: userConfig.baasPrefix === false ? null : normalizeRoutePrefix(userConfig.baasPrefix ?? DEFAULT_BAAS_PREFIX),
|
|
1053
1221
|
debug: userConfig.debug ?? false,
|
|
1054
1222
|
orgRequired: userConfig.orgRequired ?? false,
|
|
1055
1223
|
orgSelectionUrl: userConfig.orgSelectionUrl ?? "/select-organization",
|
|
@@ -1065,7 +1233,8 @@ function createSylphxMiddleware(userConfig = {}) {
|
|
|
1065
1233
|
...config.publicRoutes,
|
|
1066
1234
|
config.signInUrl,
|
|
1067
1235
|
"/signup",
|
|
1068
|
-
`${config.authPrefix}
|
|
1236
|
+
`${config.authPrefix}/*`,
|
|
1237
|
+
...config.baasPrefix ? [`${config.baasPrefix}/*`] : []
|
|
1069
1238
|
];
|
|
1070
1239
|
const log = (msg, data) => {
|
|
1071
1240
|
if (config.debug) {
|
|
@@ -1110,6 +1279,18 @@ function createSylphxMiddleware(userConfig = {}) {
|
|
|
1110
1279
|
if (pathname === `${config.authPrefix}/oauth/authorize`) {
|
|
1111
1280
|
return handleOAuthAuthorize(request, ctx);
|
|
1112
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
|
+
}
|
|
1113
1294
|
if (pathname === `${config.authPrefix}/signout`) {
|
|
1114
1295
|
return handleSignOut(request, ctx);
|
|
1115
1296
|
}
|
|
@@ -1147,6 +1328,9 @@ function createSylphxMiddleware(userConfig = {}) {
|
|
|
1147
1328
|
activeSessionToken = null;
|
|
1148
1329
|
}
|
|
1149
1330
|
}
|
|
1331
|
+
if (config.baasPrefix && stripRoutePrefix(pathname, config.baasPrefix)) {
|
|
1332
|
+
return handleBaasProxy(request, ctx, activeSessionToken);
|
|
1333
|
+
}
|
|
1150
1334
|
const isPublic = matchesAny(pathname, publicRoutes);
|
|
1151
1335
|
if (!isPublic && !isAuthenticated) {
|
|
1152
1336
|
log("Redirecting to sign-in");
|