@sylphx/sdk 0.10.6 → 0.11.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.
@@ -64,14 +64,18 @@ interface SylphxMiddlewareConfig {
64
64
  afterSignInUrl?: string;
65
65
  /**
66
66
  * Auth routes prefix. Routes are mounted at:
67
+ * - {prefix}/register — email/password registration handler
67
68
  * - {prefix}/login — credentials login handler
69
+ * - {prefix}/verify-email — email verification handler
68
70
  * - {prefix}/oauth-providers — enabled social login providers
69
71
  * - {prefix}/oauth/authorize — social login start handler
70
72
  * - {prefix}/callback — OAuth callback handler
71
73
  * - {prefix}/passkey/options — passkey login challenge handler
72
74
  * - {prefix}/passkey/authenticate — passkey login verification handler
75
+ * - {prefix}/verify-2fa — TOTP/backup-code verification handler
73
76
  * - {prefix}/forgot-password — password reset email handler
74
77
  * - {prefix}/reset-password — password reset verification handler
78
+ * - {prefix}/session — safe session metadata handler
75
79
  * - {prefix}/signout — Sign out handler
76
80
  *
77
81
  * @default '/auth'
@@ -112,6 +116,25 @@ interface SylphxMiddlewareConfig {
112
116
  * @default process.env.SYLPHX_SECRET_URL
113
117
  */
114
118
  secretUrl?: string;
119
+ /**
120
+ * Publishable key for browser-safe auth routes.
121
+ *
122
+ * Public auth handlers (`/auth/login`, `/auth/oauth-providers`,
123
+ * `/auth/oauth/authorize`, password reset, passkeys) call runtime routes
124
+ * guarded by projectAuth, which accepts pk_* only.
125
+ *
126
+ * @default credential parsed from process.env.SYLPHX_URL or
127
+ * process.env.NEXT_PUBLIC_SYLPHX_URL
128
+ */
129
+ publishableKey?: string;
130
+ /**
131
+ * Public connection URL for browser-safe auth routes.
132
+ *
133
+ * Format: sylphx://pk_<env>_<hex>@<tenant>.api.sylphx.com
134
+ *
135
+ * @default process.env.SYLPHX_URL or process.env.NEXT_PUBLIC_SYLPHX_URL
136
+ */
137
+ publicUrl?: string;
115
138
  /**
116
139
  * Platform URL for API calls.
117
140
  * Override for self-hosted or same-origin deployments.
@@ -3,6 +3,7 @@ import { NextResponse } from "next/server";
3
3
 
4
4
  // src/constants.ts
5
5
  var ENV_URL = "SYLPHX_URL";
6
+ var ENV_PUBLIC_URL = "NEXT_PUBLIC_SYLPHX_URL";
6
7
  var ENV_SECRET_URL = "SYLPHX_SECRET_URL";
7
8
  var DEFAULT_SDK_API_HOST = "api.sylphx.com";
8
9
  var SDK_PLATFORM = typeof window !== "undefined" ? "browser" : typeof process !== "undefined" && process.versions?.node ? "node" : "unknown";
@@ -127,6 +128,12 @@ function validateKeyForType(key, keyType, pattern, envVarName) {
127
128
  function validatePublicKey(key) {
128
129
  return validateKeyForType(key, "publicKey", PUBLIC_KEY_PATTERN, "publishable credential");
129
130
  }
131
+ function validateAndSanitizePublicKey(key) {
132
+ const result = validatePublicKey(key);
133
+ if (!result.valid) throw new Error(result.error);
134
+ if (result.warning) console.warn(result.warning);
135
+ return result.sanitizedKey;
136
+ }
130
137
  function validateAppId(key) {
131
138
  return validateKeyForType(key, "appId", APP_ID_PATTERN, "NEXT_PUBLIC_SYLPHX_APP_ID");
132
139
  }
@@ -427,6 +434,11 @@ function secretKeyFromConnectionUrl(value) {
427
434
  if (!parsed || parsed.credentialType !== "sk") return null;
428
435
  return parsed.credential;
429
436
  }
437
+ function publishableKeyFromConnectionUrl(value) {
438
+ const parsed = parseConnection(value);
439
+ if (!parsed || parsed.credentialType !== "pk") return null;
440
+ return parsed.credential;
441
+ }
430
442
  function resolveNextjsPlatformUrl(options) {
431
443
  if (options.explicitPlatformUrl) return normalizePlatformUrl(options.explicitPlatformUrl);
432
444
  const fromExplicitSecretUrl = platformUrlFromConnectionUrl(options.secretUrl);
@@ -451,6 +463,16 @@ function resolveNextjsSecretKey(options) {
451
463
  if (fromGenericUrl) return fromGenericUrl;
452
464
  return null;
453
465
  }
466
+ function resolveNextjsPublishableKey(options) {
467
+ if (options.explicitPublishableKey?.trim()) return options.explicitPublishableKey.trim();
468
+ const fromExplicitPublicUrl = publishableKeyFromConnectionUrl(options.explicitPublicUrl);
469
+ if (fromExplicitPublicUrl) return fromExplicitPublicUrl;
470
+ const fromPublicUrl = publishableKeyFromConnectionUrl(process.env[ENV_URL]);
471
+ if (fromPublicUrl) return fromPublicUrl;
472
+ const fromNextPublicUrl = publishableKeyFromConnectionUrl(process.env[ENV_PUBLIC_URL]);
473
+ if (fromNextPublicUrl) return fromNextPublicUrl;
474
+ return null;
475
+ }
454
476
 
455
477
  // src/nextjs/middleware.ts
456
478
  var OAUTH_PKCE_TTL_SECONDS = 10 * 60;
@@ -634,12 +656,30 @@ function isTwoFactorLoginResponse(data) {
634
656
  function isOAuthAuthorizeResponse(data) {
635
657
  return typeof data === "object" && data !== null && typeof data.authorization_url === "string";
636
658
  }
637
- function headersForPlatformAuth(ctx) {
659
+ function authCookieJsonResponse(ctx, data) {
660
+ const response = NextResponse.json({ success: true, user: data.user });
661
+ setAuthCookiesMiddleware(response, ctx.namespace, data);
662
+ return response;
663
+ }
664
+ function headersForProjectAuth(ctx) {
638
665
  return {
639
666
  "Content-Type": "application/json",
640
- "x-app-secret": ctx.secretKey
667
+ "x-app-secret": ctx.publishableKey
641
668
  };
642
669
  }
670
+ async function handleRegister(request, ctx) {
671
+ if (request.method !== "POST") {
672
+ return NextResponse.json({ error: "Method not allowed" }, { status: 405 });
673
+ }
674
+ const body = await parseJsonObject(request);
675
+ const res = await fetch(`${ctx.platformUrl}/v1/auth/register`, {
676
+ method: "POST",
677
+ headers: headersForProjectAuth(ctx),
678
+ body: JSON.stringify(body ?? {})
679
+ });
680
+ const data = await res.json().catch(() => ({}));
681
+ return NextResponse.json(data, { status: res.status });
682
+ }
643
683
  async function handleLogin(request, ctx) {
644
684
  if (request.method !== "POST") {
645
685
  return NextResponse.json({ error: "Method not allowed" }, { status: 405 });
@@ -652,7 +692,7 @@ async function handleLogin(request, ctx) {
652
692
  }
653
693
  const res = await fetch(`${ctx.platformUrl}/v1/auth/login`, {
654
694
  method: "POST",
655
- headers: headersForPlatformAuth(ctx),
695
+ headers: headersForProjectAuth(ctx),
656
696
  body: JSON.stringify({ email, password })
657
697
  });
658
698
  const data = await res.json().catch(() => ({}));
@@ -665,14 +705,83 @@ async function handleLogin(request, ctx) {
665
705
  if (!isTokenResponse(data)) {
666
706
  return NextResponse.json({ error: "invalid_response" }, { status: 502 });
667
707
  }
668
- const response = NextResponse.json({ success: true, user: data.user });
669
- setAuthCookiesMiddleware(response, ctx.namespace, data);
670
- return response;
708
+ return authCookieJsonResponse(ctx, data);
709
+ }
710
+ async function handleVerifyEmail(request, ctx) {
711
+ if (request.method !== "POST") {
712
+ return NextResponse.json({ error: "Method not allowed" }, { status: 405 });
713
+ }
714
+ const body = await parseJsonObject(request);
715
+ const token = typeof body?.token === "string" ? body.token : null;
716
+ if (!token) {
717
+ return NextResponse.json({ error: "token is required" }, { status: 400 });
718
+ }
719
+ const res = await fetch(`${ctx.platformUrl}/v1/auth/verify-email`, {
720
+ method: "POST",
721
+ headers: headersForProjectAuth(ctx),
722
+ body: JSON.stringify({ token })
723
+ });
724
+ const data = await res.json().catch(() => ({}));
725
+ return NextResponse.json(data, { status: res.status });
726
+ }
727
+ async function handleVerifyTwoFactor(request, ctx) {
728
+ if (request.method !== "POST") {
729
+ return NextResponse.json({ error: "Method not allowed" }, { status: 405 });
730
+ }
731
+ const body = await parseJsonObject(request);
732
+ const userId = typeof body?.userId === "string" ? body.userId : null;
733
+ const code = typeof body?.code === "string" ? body.code : null;
734
+ if (!userId || !code) {
735
+ return NextResponse.json({ error: "userId and code are required" }, { status: 400 });
736
+ }
737
+ const res = await fetch(`${ctx.platformUrl}/v1/auth/verify-2fa`, {
738
+ method: "POST",
739
+ headers: headersForProjectAuth(ctx),
740
+ body: JSON.stringify({ userId, code })
741
+ });
742
+ const data = await res.json().catch(() => ({}));
743
+ if (!res.ok) {
744
+ return NextResponse.json(data, { status: res.status });
745
+ }
746
+ if (!isTokenResponse(data)) {
747
+ return NextResponse.json({ error: "invalid_response" }, { status: 502 });
748
+ }
749
+ return authCookieJsonResponse(ctx, data);
750
+ }
751
+ function handleSession(request, ctx) {
752
+ if (request.method !== "GET") {
753
+ return NextResponse.json({ error: "Method not allowed" }, { status: 405 });
754
+ }
755
+ const sessionToken = request.cookies.get(ctx.cookieNames.SESSION)?.value;
756
+ const userCookieValue = request.cookies.get(ctx.cookieNames.USER)?.value;
757
+ let userCookie = userCookieValue ? parseUserCookie2(userCookieValue) : null;
758
+ if (!userCookie && userCookieValue) {
759
+ try {
760
+ userCookie = parseUserCookie2(decodeURIComponent(userCookieValue));
761
+ } catch {
762
+ userCookie = null;
763
+ }
764
+ }
765
+ if (!sessionToken || isTokenExpired(sessionToken) || !userCookie?.user) {
766
+ return NextResponse.json({ success: true, session: null, user: null });
767
+ }
768
+ const payload = decodeJwtPayload(sessionToken);
769
+ const userId = payload?.sub ?? userCookie.user.id;
770
+ const expiresAt = typeof payload?.exp === "number" ? new Date(payload.exp * 1e3).toISOString() : new Date(userCookie.expiresAt).toISOString();
771
+ return NextResponse.json({
772
+ success: true,
773
+ session: {
774
+ id: `platform:${userId}`,
775
+ userId,
776
+ expiresAt
777
+ },
778
+ user: userCookie.user
779
+ });
671
780
  }
672
781
  async function handleOAuthProviders(ctx) {
673
782
  const res = await fetch(`${ctx.platformUrl}/v1/auth/oauth-providers`, {
674
783
  method: "GET",
675
- headers: headersForPlatformAuth(ctx)
784
+ headers: headersForProjectAuth(ctx)
676
785
  });
677
786
  const data = await res.json().catch(() => ({}));
678
787
  return NextResponse.json(data, { status: res.status });
@@ -695,7 +804,7 @@ async function handleOAuthAuthorize(request, ctx) {
695
804
  const scopes = Array.isArray(body?.scopes) ? body.scopes.filter((scope) => typeof scope === "string") : void 0;
696
805
  const res = await fetch(`${ctx.platformUrl}/v1/oauth/authorize`, {
697
806
  method: "POST",
698
- headers: headersForPlatformAuth(ctx),
807
+ headers: headersForProjectAuth(ctx),
699
808
  body: JSON.stringify({
700
809
  provider,
701
810
  redirect_uri: redirectUri.toString(),
@@ -724,7 +833,7 @@ async function handlePasskeyOptions(request, ctx) {
724
833
  const body = await parseJsonObject(request);
725
834
  const res = await fetch(`${ctx.platformUrl}/v1/auth/passkey/options`, {
726
835
  method: "POST",
727
- headers: headersForPlatformAuth(ctx),
836
+ headers: headersForProjectAuth(ctx),
728
837
  body: JSON.stringify(body ?? {})
729
838
  });
730
839
  const data = await res.json().catch(() => ({}));
@@ -737,7 +846,7 @@ async function handlePasskeyAuthenticate(request, ctx) {
737
846
  const body = await parseJsonObject(request);
738
847
  const res = await fetch(`${ctx.platformUrl}/v1/auth/passkey/authenticate`, {
739
848
  method: "POST",
740
- headers: headersForPlatformAuth(ctx),
849
+ headers: headersForProjectAuth(ctx),
741
850
  body: JSON.stringify(body ?? {})
742
851
  });
743
852
  const data = await res.json().catch(() => ({}));
@@ -747,9 +856,7 @@ async function handlePasskeyAuthenticate(request, ctx) {
747
856
  if (!isTokenResponse(data)) {
748
857
  return NextResponse.json({ error: "invalid_response" }, { status: 502 });
749
858
  }
750
- const response = NextResponse.json({ success: true, user: data.user });
751
- setAuthCookiesMiddleware(response, ctx.namespace, data);
752
- return response;
859
+ return authCookieJsonResponse(ctx, data);
753
860
  }
754
861
  async function handleForgotPassword(request, ctx) {
755
862
  if (request.method !== "POST") {
@@ -767,7 +874,7 @@ async function handleForgotPassword(request, ctx) {
767
874
  }
768
875
  const res = await fetch(`${ctx.platformUrl}/v1/auth/forgot-password`, {
769
876
  method: "POST",
770
- headers: headersForPlatformAuth(ctx),
877
+ headers: headersForProjectAuth(ctx),
771
878
  body: JSON.stringify({ email, redirectUrl })
772
879
  });
773
880
  const data = await res.json().catch(() => ({}));
@@ -785,7 +892,7 @@ async function handleResetPassword(request, ctx) {
785
892
  }
786
893
  const res = await fetch(`${ctx.platformUrl}/v1/auth/reset-password`, {
787
894
  method: "POST",
788
- headers: headersForPlatformAuth(ctx),
895
+ headers: headersForProjectAuth(ctx),
789
896
  body: JSON.stringify({ token, password })
790
897
  });
791
898
  const data = await res.json().catch(() => ({}));
@@ -1091,12 +1198,15 @@ async function handleSwitchOrg(request, ctx) {
1091
1198
  }
1092
1199
  const expiresIn = resolveOrgScopedTokenExpiresIn(data);
1093
1200
  const expiresAt = Date.now() + expiresIn * 1e3;
1201
+ const payload = decodeJwtPayload(accessToken);
1202
+ const activeOrganization = {
1203
+ id: payload?.org_id ?? orgId,
1204
+ slug: payload?.org_slug ?? requestedOrgSlug
1205
+ };
1094
1206
  const response = NextResponse.json({
1095
- accessToken,
1096
- token: accessToken,
1097
- expiresIn,
1098
- tokenType: resolveOrgScopedTokenType(data),
1099
- user: data.user ?? null
1207
+ success: true,
1208
+ user: data.user ?? null,
1209
+ organization: activeOrganization
1100
1210
  });
1101
1211
  response.cookies.set(ctx.cookieNames.SESSION, accessToken, {
1102
1212
  ...SECURE_COOKIE_OPTIONS,
@@ -1117,11 +1227,7 @@ async function handleSwitchOrg(request, ctx) {
1117
1227
  }
1118
1228
  );
1119
1229
  }
1120
- const payload = decodeJwtPayload(accessToken);
1121
- setActiveOrganizationCookies(response, ctx, {
1122
- id: payload?.org_id ?? orgId,
1123
- slug: payload?.org_slug ?? requestedOrgSlug
1124
- });
1230
+ setActiveOrganizationCookies(response, ctx, activeOrganization);
1125
1231
  ctx.log("Switch org success", { orgId });
1126
1232
  return response;
1127
1233
  } catch (err) {
@@ -1197,6 +1303,21 @@ function createSylphxMiddleware(userConfig = {}) {
1197
1303
  secretKey = validateAndSanitizeSecretKey(rawSecretKey);
1198
1304
  return secretKey;
1199
1305
  }
1306
+ let publishableKey = null;
1307
+ function resolvePublishableKey() {
1308
+ if (publishableKey) return publishableKey;
1309
+ const rawPublishableKey = resolveNextjsPublishableKey({
1310
+ explicitPublishableKey: userConfig.publishableKey,
1311
+ explicitPublicUrl: userConfig.publicUrl
1312
+ });
1313
+ if (!rawPublishableKey) {
1314
+ throw new Error(
1315
+ "[Sylphx] Publishable connection URL is required for public auth routes.\nEither pass publicUrl/publishableKey in config or set SYLPHX_URL/NEXT_PUBLIC_SYLPHX_URL."
1316
+ );
1317
+ }
1318
+ publishableKey = validateAndSanitizePublicKey(rawPublishableKey);
1319
+ return publishableKey;
1320
+ }
1200
1321
  let platformUrl;
1201
1322
  let namespace;
1202
1323
  let cookieNames;
@@ -1244,6 +1365,9 @@ function createSylphxMiddleware(userConfig = {}) {
1244
1365
  get secretKey() {
1245
1366
  return secretKey;
1246
1367
  },
1368
+ get publishableKey() {
1369
+ return resolvePublishableKey();
1370
+ },
1247
1371
  get platformUrl() {
1248
1372
  return platformUrl;
1249
1373
  },
@@ -1267,12 +1391,18 @@ function createSylphxMiddleware(userConfig = {}) {
1267
1391
  if (pathname.includes(".") || pathname.startsWith("/_next")) {
1268
1392
  return NextResponse.next();
1269
1393
  }
1394
+ if (pathname === `${config.authPrefix}/register`) {
1395
+ return handleRegister(request, ctx);
1396
+ }
1270
1397
  if (pathname === `${config.authPrefix}/callback`) {
1271
1398
  return handleCallback(request, ctx);
1272
1399
  }
1273
1400
  if (pathname === `${config.authPrefix}/login`) {
1274
1401
  return handleLogin(request, ctx);
1275
1402
  }
1403
+ if (pathname === `${config.authPrefix}/verify-email`) {
1404
+ return handleVerifyEmail(request, ctx);
1405
+ }
1276
1406
  if (pathname === `${config.authPrefix}/oauth-providers`) {
1277
1407
  return handleOAuthProviders(ctx);
1278
1408
  }
@@ -1285,12 +1415,18 @@ function createSylphxMiddleware(userConfig = {}) {
1285
1415
  if (pathname === `${config.authPrefix}/passkey/authenticate`) {
1286
1416
  return handlePasskeyAuthenticate(request, ctx);
1287
1417
  }
1418
+ if (pathname === `${config.authPrefix}/verify-2fa`) {
1419
+ return handleVerifyTwoFactor(request, ctx);
1420
+ }
1288
1421
  if (pathname === `${config.authPrefix}/forgot-password`) {
1289
1422
  return handleForgotPassword(request, ctx);
1290
1423
  }
1291
1424
  if (pathname === `${config.authPrefix}/reset-password`) {
1292
1425
  return handleResetPassword(request, ctx);
1293
1426
  }
1427
+ if (pathname === `${config.authPrefix}/session`) {
1428
+ return handleSession(request, ctx);
1429
+ }
1294
1430
  if (pathname === `${config.authPrefix}/signout`) {
1295
1431
  return handleSignOut(request, ctx);
1296
1432
  }