@vaultix.ai/nextjs 0.2.0 → 0.4.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.
@@ -35,31 +35,51 @@ var HEADER_ORG_ID = "x-vaultix-org-id";
35
35
  var HEADER_ORG_ROLE = "x-vaultix-org-role";
36
36
  var HEADER_SESSION_ID = "x-vaultix-session-id";
37
37
  var HEADER_RISK_LEVEL = "x-vaultix-risk-level";
38
- var cachedKey = null;
39
- var cachedPem = null;
40
- async function getPublicKey() {
41
- const pem = process.env.VAULTIX_JWT_PUBLIC_KEY;
42
- if (!pem) return null;
43
- if (cachedKey && cachedPem === pem) return cachedKey;
38
+ function decodeApiUrlFromKey(pk) {
44
39
  try {
45
- cachedKey = await (0, import_jose.importSPKI)(pem.replace(/\\n/g, "\n"), "RS256");
46
- cachedPem = pem;
47
- return cachedKey;
40
+ const parts = pk.split("_");
41
+ if (parts.length >= 4 && parts[0] === "vaultix" && parts[1] === "pk") {
42
+ return atob(parts.slice(3).join("_")).replace(/\/$/, "");
43
+ }
48
44
  } catch {
49
- return null;
50
45
  }
46
+ return "";
47
+ }
48
+ function resolveApiUrl(options) {
49
+ if (options.apiUrl) return options.apiUrl.replace(/\/$/, "");
50
+ if (process.env.VAULTIX_API_URL) return process.env.VAULTIX_API_URL.replace(/\/$/, "");
51
+ const pk = process.env.NEXT_PUBLIC_VAULTIX_PUBLISHABLE_KEY ?? "";
52
+ return decodeApiUrlFromKey(pk);
53
+ }
54
+ var remoteJwks = null;
55
+ var remoteJwksUrl = null;
56
+ var staticKey = null;
57
+ var staticPem = null;
58
+ async function getVerifyKey(apiUrl) {
59
+ const pem = process.env.VAULTIX_JWT_PUBLIC_KEY;
60
+ if (pem) {
61
+ const normalized = pem.replace(/\\n/g, "\n");
62
+ if (staticKey && staticPem === normalized) return { key: staticKey, mode: "static" };
63
+ staticKey = await (0, import_jose.importSPKI)(normalized, "RS256");
64
+ staticPem = normalized;
65
+ return { key: staticKey, mode: "static" };
66
+ }
67
+ if (!apiUrl) return null;
68
+ const jwksUrl = `${apiUrl}/api/v1/.well-known/jwks.json`;
69
+ if (!remoteJwks || remoteJwksUrl !== jwksUrl) {
70
+ remoteJwks = (0, import_jose.createRemoteJWKSet)(new URL(jwksUrl));
71
+ remoteJwksUrl = jwksUrl;
72
+ }
73
+ return { key: remoteJwks, mode: "remote" };
51
74
  }
52
75
  function isPublic(pathname, rules) {
53
- return rules.some((rule) => {
54
- if (typeof rule === "string") {
55
- return pathname === rule || pathname.startsWith(rule);
56
- }
57
- return rule.test(pathname);
58
- });
76
+ return rules.some(
77
+ (rule) => typeof rule === "string" ? pathname === rule || pathname.startsWith(rule) : rule.test(pathname)
78
+ );
59
79
  }
60
80
  async function handleHandshake(req, handshakeToken, apiUrl) {
61
81
  try {
62
- const res = await fetch(`${apiUrl}/v1/tokens/exchange`, {
82
+ const res = await fetch(`${apiUrl}/api/v1/tokens/exchange`, {
63
83
  method: "POST",
64
84
  headers: { "Content-Type": "application/json" },
65
85
  body: JSON.stringify({ handshake_token: handshakeToken })
@@ -75,7 +95,6 @@ async function handleHandshake(req, handshakeToken, apiUrl) {
75
95
  sameSite: "lax",
76
96
  path: "/",
77
97
  maxAge: 30 * 24 * 60 * 60
78
- // 30 days
79
98
  });
80
99
  return response;
81
100
  } catch {
@@ -83,21 +102,16 @@ async function handleHandshake(req, handshakeToken, apiUrl) {
83
102
  }
84
103
  }
85
104
  function authMiddleware(options = {}) {
86
- const {
87
- publicRoutes = [],
88
- signInUrl = "/sign-in",
89
- afterAuth
90
- } = options;
105
+ const { publicRoutes = [], afterAuth } = options;
91
106
  return async function middleware(req) {
92
107
  const { pathname } = req.nextUrl;
93
108
  const publicRoute = isPublic(pathname, publicRoutes);
109
+ const apiUrl = resolveApiUrl(options);
110
+ const signInUrl = options.signInUrl ?? (apiUrl ? `${apiUrl}/auth/sign-in` : "/sign-in");
94
111
  const handshakeToken = req.nextUrl.searchParams.get("__vaultix_handshake");
95
- if (handshakeToken) {
96
- const apiUrl = options.apiUrl ?? process.env.VAULTIX_API_URL ?? "";
97
- if (apiUrl) {
98
- const response = await handleHandshake(req, handshakeToken, apiUrl);
99
- if (response) return response;
100
- }
112
+ if (handshakeToken && apiUrl) {
113
+ const response = await handleHandshake(req, handshakeToken, apiUrl);
114
+ if (response) return response;
101
115
  }
102
116
  let result = {
103
117
  userId: null,
@@ -108,12 +122,12 @@ function authMiddleware(options = {}) {
108
122
  isSignedIn: false,
109
123
  isPublicRoute: publicRoute
110
124
  };
111
- const sessionCookie = req.cookies.get("vaultix-session")?.value ?? extractBearer(req.headers.get("authorization") ?? "");
112
- if (sessionCookie) {
113
- const key = await getPublicKey();
114
- if (key) {
125
+ const token = req.cookies.get("vaultix-session")?.value ?? extractBearer(req.headers.get("authorization") ?? "");
126
+ if (token) {
127
+ const verifyKey = await getVerifyKey(apiUrl);
128
+ if (verifyKey) {
115
129
  try {
116
- const { payload } = await (0, import_jose.jwtVerify)(sessionCookie, key, {
130
+ const { payload } = await (0, import_jose.jwtVerify)(token, verifyKey.key, {
117
131
  algorithms: ["RS256"]
118
132
  });
119
133
  result = {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/middleware.ts"],"sourcesContent":["// Edge-runtime compatible. Uses jose (not Node crypto) for JWT verification.\nimport { importSPKI, jwtVerify, type KeyLike } from \"jose\";\nimport { NextRequest, NextResponse } from \"next/server\";\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\nexport interface AuthResult {\n /** Authenticated user ID (`sub` claim), or null if unauthenticated. */\n userId: string | null;\n orgId: string | null;\n orgRole: string | null;\n sessionId: string | null;\n riskLevel: string | null;\n isSignedIn: boolean;\n isPublicRoute: boolean;\n}\n\nexport interface AuthMiddlewareOptions {\n /**\n * Routes that do not require authentication.\n * Strings are matched by exact path or prefix (e.g. \"/sign-in\" or \"/api/public/\").\n * RegExps are tested against `pathname`.\n */\n publicRoutes?: Array<string | RegExp>;\n\n /** Where to redirect unauthenticated users. Default: \"/sign-in\". */\n signInUrl?: string;\n\n /**\n * Vaultix API origin — required to exchange handshake tokens.\n * Falls back to VAULTIX_API_URL env var.\n * Example: \"https://vaultix.smritix-ai.com\"\n */\n apiUrl?: string;\n\n /**\n * Custom logic after auth state is determined.\n * Return a `NextResponse` to short-circuit the default redirect behaviour;\n * return `undefined` to continue with defaults.\n */\n afterAuth?: (\n auth: AuthResult,\n req: NextRequest,\n ) => NextResponse | Response | undefined | void;\n}\n\n// ─── Header names injected into RSC requests ──────────────────────────────────\n\nexport const HEADER_USER_ID = \"x-vaultix-user-id\";\nexport const HEADER_ORG_ID = \"x-vaultix-org-id\";\nexport const HEADER_ORG_ROLE = \"x-vaultix-org-role\";\nexport const HEADER_SESSION_ID = \"x-vaultix-session-id\";\nexport const HEADER_RISK_LEVEL = \"x-vaultix-risk-level\";\n\n// ─── Public-key cache ─────────────────────────────────────────────────────────\n\nlet cachedKey: KeyLike | null = null;\nlet cachedPem: string | null = null;\n\nasync function getPublicKey(): Promise<KeyLike | null> {\n const pem = process.env.VAULTIX_JWT_PUBLIC_KEY;\n if (!pem) return null;\n if (cachedKey && cachedPem === pem) return cachedKey;\n\n try {\n cachedKey = await importSPKI(pem.replace(/\\\\n/g, \"\\n\"), \"RS256\");\n cachedPem = pem;\n return cachedKey;\n } catch {\n return null;\n }\n}\n\n// ─── Route matching ───────────────────────────────────────────────────────────\n\nfunction isPublic(pathname: string, rules: Array<string | RegExp>): boolean {\n return rules.some((rule) => {\n if (typeof rule === \"string\") {\n return pathname === rule || pathname.startsWith(rule);\n }\n return rule.test(pathname);\n });\n}\n\n// ─── Handshake token exchange ─────────────────────────────────────────────────\n// Called when `__vaultix_handshake` appears in the URL.\n// Exchanges the short-lived token for a full session JWT, sets a same-domain\n// cookie, and redirects to the clean URL — mirroring Clerk's __clerk_handshake.\n\nasync function handleHandshake(\n req: NextRequest,\n handshakeToken: string,\n apiUrl: string,\n): Promise<NextResponse | null> {\n try {\n const res = await fetch(`${apiUrl}/v1/tokens/exchange`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ handshake_token: handshakeToken }),\n });\n\n if (!res.ok) return null;\n\n const { session_jwt } = (await res.json()) as { session_jwt: string };\n\n const cleanUrl = req.nextUrl.clone();\n cleanUrl.searchParams.delete(\"__vaultix_handshake\");\n\n const response = NextResponse.redirect(cleanUrl);\n response.cookies.set(\"vaultix-session\", session_jwt, {\n httpOnly: true,\n secure: true,\n sameSite: \"lax\",\n path: \"/\",\n maxAge: 30 * 24 * 60 * 60, // 30 days\n });\n return response;\n } catch {\n return null;\n }\n}\n\n// ─── authMiddleware factory ───────────────────────────────────────────────────\n\n/**\n * authMiddleware wraps your Next.js middleware.ts export.\n *\n * @example\n * // middleware.ts\n * import { authMiddleware } from \"@vaultix.ai/nextjs/middleware\";\n * export default authMiddleware({\n * apiUrl: \"https://vaultix.smritix-ai.com\",\n * publicRoutes: [\"/sign-in\", \"/sign-up\"],\n * });\n * export const config = { matcher: [\"/((?!_next|.*\\\\..*).*)\"] };\n */\nexport function authMiddleware(options: AuthMiddlewareOptions = {}) {\n const {\n publicRoutes = [],\n signInUrl = \"/sign-in\",\n afterAuth,\n } = options;\n\n return async function middleware(req: NextRequest): Promise<NextResponse> {\n const { pathname } = req.nextUrl;\n const publicRoute = isPublic(pathname, publicRoutes);\n\n // ── Handshake exchange ─────────────────────────────────────────────────\n const handshakeToken = req.nextUrl.searchParams.get(\"__vaultix_handshake\");\n if (handshakeToken) {\n const apiUrl =\n options.apiUrl ??\n process.env.VAULTIX_API_URL ??\n \"\";\n if (apiUrl) {\n const response = await handleHandshake(req, handshakeToken, apiUrl);\n if (response) return response;\n // If exchange failed, fall through to normal auth check\n }\n }\n\n // ── JWT verification ───────────────────────────────────────────────────\n let result: AuthResult = {\n userId: null,\n orgId: null,\n orgRole: null,\n sessionId: null,\n riskLevel: null,\n isSignedIn: false,\n isPublicRoute: publicRoute,\n };\n\n const sessionCookie =\n req.cookies.get(\"vaultix-session\")?.value ??\n extractBearer(req.headers.get(\"authorization\") ?? \"\");\n\n if (sessionCookie) {\n const key = await getPublicKey();\n if (key) {\n try {\n const { payload } = await jwtVerify(sessionCookie, key, {\n algorithms: [\"RS256\"],\n });\n\n result = {\n userId: (payload[\"uid\"] as string) ?? null,\n orgId: (payload[\"org\"] as string) ?? null,\n orgRole: (payload[\"rol\"] as string) ?? null,\n sessionId: (payload[\"sid\"] as string) ?? null,\n riskLevel: (payload[\"risk\"] as string) ?? \"low\",\n isSignedIn: true,\n isPublicRoute: publicRoute,\n };\n } catch {\n // Expired or tampered token → treat as signed-out.\n }\n }\n }\n\n // Let callers override the default redirect logic.\n if (afterAuth) {\n const override = afterAuth(result, req);\n if (override) return override as NextResponse;\n }\n\n // Default: redirect unauthenticated requests to the sign-in page.\n if (!result.isSignedIn && !publicRoute) {\n const dest = new URL(signInUrl, req.url);\n dest.searchParams.set(\"redirect_url\", req.url);\n return NextResponse.redirect(dest);\n }\n\n // Propagate auth claims to Server Components via request headers.\n const next = new Headers(req.headers);\n if (result.userId) {\n next.set(HEADER_USER_ID, result.userId);\n next.set(HEADER_ORG_ID, result.orgId ?? \"\");\n next.set(HEADER_ORG_ROLE, result.orgRole ?? \"\");\n next.set(HEADER_SESSION_ID, result.sessionId ?? \"\");\n next.set(HEADER_RISK_LEVEL, result.riskLevel ?? \"low\");\n } else {\n [HEADER_USER_ID, HEADER_ORG_ID, HEADER_ORG_ROLE, HEADER_SESSION_ID, HEADER_RISK_LEVEL]\n .forEach((h) => next.delete(h));\n }\n\n return NextResponse.next({ request: { headers: next } });\n };\n}\n\nfunction extractBearer(header: string): string {\n return header.startsWith(\"Bearer \") ? header.slice(7) : \"\";\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/middleware.ts"],"sourcesContent":["// Edge-runtime compatible. Uses jose for JWT verification.\nimport { createRemoteJWKSet, importSPKI, jwtVerify, type KeyLike } from \"jose\";\nimport { NextRequest, NextResponse } from \"next/server\";\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\nexport interface AuthResult {\n userId: string | null;\n orgId: string | null;\n orgRole: string | null;\n sessionId: string | null;\n riskLevel: string | null;\n isSignedIn: boolean;\n isPublicRoute: boolean;\n}\n\nexport interface AuthMiddlewareOptions {\n /**\n * Routes that do not require authentication.\n * Strings: exact match or prefix. RegExps: tested against pathname.\n */\n publicRoutes?: Array<string | RegExp>;\n\n /**\n * Where to redirect unauthenticated users.\n * Defaults to the Vaultix hosted sign-in page (decoded from publishable key).\n * Override with \"/sign-in\" to use your own page.\n */\n signInUrl?: string;\n\n /**\n * Vaultix API origin. Auto-decoded from NEXT_PUBLIC_VAULTIX_PUBLISHABLE_KEY.\n * Only needed if you're not using the standard publishable key format.\n */\n apiUrl?: string;\n\n /**\n * Custom logic after auth state is resolved.\n * Return a NextResponse to override default behaviour.\n */\n afterAuth?: (auth: AuthResult, req: NextRequest) => NextResponse | Response | undefined | void;\n}\n\n// ─── Header names ─────────────────────────────────────────────────────────────\n\nexport const HEADER_USER_ID = \"x-vaultix-user-id\";\nexport const HEADER_ORG_ID = \"x-vaultix-org-id\";\nexport const HEADER_ORG_ROLE = \"x-vaultix-org-role\";\nexport const HEADER_SESSION_ID = \"x-vaultix-session-id\";\nexport const HEADER_RISK_LEVEL = \"x-vaultix-risk-level\";\n\n// ─── Publishable key → API URL ────────────────────────────────────────────────\n\nfunction decodeApiUrlFromKey(pk: string): string {\n try {\n const parts = pk.split(\"_\");\n if (parts.length >= 4 && parts[0] === \"vaultix\" && parts[1] === \"pk\") {\n return atob(parts.slice(3).join(\"_\")).replace(/\\/$/, \"\");\n }\n } catch {}\n return \"\";\n}\n\nfunction resolveApiUrl(options: AuthMiddlewareOptions): string {\n if (options.apiUrl) return options.apiUrl.replace(/\\/$/, \"\");\n if (process.env.VAULTIX_API_URL) return process.env.VAULTIX_API_URL.replace(/\\/$/, \"\");\n const pk = process.env.NEXT_PUBLIC_VAULTIX_PUBLISHABLE_KEY ?? \"\";\n return decodeApiUrlFromKey(pk);\n}\n\n// ─── JWKS cache ───────────────────────────────────────────────────────────────\n// createRemoteJWKSet fetches and caches the key set, re-fetches on rotation.\n// Falls back to a static PEM key if VAULTIX_JWT_PUBLIC_KEY is set (backward compat).\n\nlet remoteJwks: ReturnType<typeof createRemoteJWKSet> | null = null;\nlet remoteJwksUrl: string | null = null;\n\nlet staticKey: KeyLike | null = null;\nlet staticPem: string | null = null;\n\nasync function getVerifyKey(apiUrl: string) {\n // Prefer static PEM (set by env var) for zero-network-call verification\n const pem = process.env.VAULTIX_JWT_PUBLIC_KEY;\n if (pem) {\n const normalized = pem.replace(/\\\\n/g, \"\\n\");\n if (staticKey && staticPem === normalized) return { key: staticKey, mode: \"static\" as const };\n staticKey = await importSPKI(normalized, \"RS256\");\n staticPem = normalized;\n return { key: staticKey, mode: \"static\" as const };\n }\n\n // Auto-fetch JWKS from the API — no env var needed\n if (!apiUrl) return null;\n const jwksUrl = `${apiUrl}/api/v1/.well-known/jwks.json`;\n if (!remoteJwks || remoteJwksUrl !== jwksUrl) {\n remoteJwks = createRemoteJWKSet(new URL(jwksUrl));\n remoteJwksUrl = jwksUrl;\n }\n return { key: remoteJwks, mode: \"remote\" as const };\n}\n\n// ─── Route matching ───────────────────────────────────────────────────────────\n\nfunction isPublic(pathname: string, rules: Array<string | RegExp>): boolean {\n return rules.some((rule) =>\n typeof rule === \"string\"\n ? pathname === rule || pathname.startsWith(rule)\n : rule.test(pathname),\n );\n}\n\n// ─── Handshake exchange ───────────────────────────────────────────────────────\n\nasync function handleHandshake(\n req: NextRequest,\n handshakeToken: string,\n apiUrl: string,\n): Promise<NextResponse | null> {\n try {\n const res = await fetch(`${apiUrl}/api/v1/tokens/exchange`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ handshake_token: handshakeToken }),\n });\n if (!res.ok) return null;\n\n const { session_jwt } = (await res.json()) as { session_jwt: string };\n const cleanUrl = req.nextUrl.clone();\n cleanUrl.searchParams.delete(\"__vaultix_handshake\");\n\n const response = NextResponse.redirect(cleanUrl);\n response.cookies.set(\"vaultix-session\", session_jwt, {\n httpOnly: true,\n secure: true,\n sameSite: \"lax\",\n path: \"/\",\n maxAge: 30 * 24 * 60 * 60,\n });\n return response;\n } catch {\n return null;\n }\n}\n\n// ─── authMiddleware ───────────────────────────────────────────────────────────\n\n/**\n * Drop-in auth middleware — works with zero config when\n * NEXT_PUBLIC_VAULTIX_PUBLISHABLE_KEY is set.\n *\n * @example\n * // middleware.ts\n * import { authMiddleware } from \"@vaultix.ai/nextjs/middleware\";\n * export default authMiddleware({ publicRoutes: [\"/\", \"/about\"] });\n * export const config = { matcher: [\"/((?!_next|.*\\\\..*).*)\"] };\n */\nexport function authMiddleware(options: AuthMiddlewareOptions = {}) {\n const { publicRoutes = [], afterAuth } = options;\n\n return async function middleware(req: NextRequest): Promise<NextResponse> {\n const { pathname } = req.nextUrl;\n const publicRoute = isPublic(pathname, publicRoutes);\n\n const apiUrl = resolveApiUrl(options);\n\n // Default sign-in URL: hosted Vaultix page decoded from publishable key\n const signInUrl =\n options.signInUrl ?? (apiUrl ? `${apiUrl}/auth/sign-in` : \"/sign-in\");\n\n // ── Handshake exchange ─────────────────────────────────────────────────\n const handshakeToken = req.nextUrl.searchParams.get(\"__vaultix_handshake\");\n if (handshakeToken && apiUrl) {\n const response = await handleHandshake(req, handshakeToken, apiUrl);\n if (response) return response;\n }\n\n // ── JWT verification ───────────────────────────────────────────────────\n let result: AuthResult = {\n userId: null, orgId: null, orgRole: null,\n sessionId: null, riskLevel: null,\n isSignedIn: false, isPublicRoute: publicRoute,\n };\n\n const token =\n req.cookies.get(\"vaultix-session\")?.value ??\n extractBearer(req.headers.get(\"authorization\") ?? \"\");\n\n if (token) {\n const verifyKey = await getVerifyKey(apiUrl);\n if (verifyKey) {\n try {\n const { payload } = await jwtVerify(token, verifyKey.key as Parameters<typeof jwtVerify>[1], {\n algorithms: [\"RS256\"],\n });\n result = {\n userId: (payload[\"uid\"] as string) ?? null,\n orgId: (payload[\"org\"] as string) ?? null,\n orgRole: (payload[\"rol\"] as string) ?? null,\n sessionId: (payload[\"sid\"] as string) ?? null,\n riskLevel: (payload[\"risk\"] as string) ?? \"low\",\n isSignedIn: true,\n isPublicRoute: publicRoute,\n };\n } catch {\n // expired / tampered\n }\n }\n }\n\n // ── Custom afterAuth hook ──────────────────────────────────────────────\n if (afterAuth) {\n const override = afterAuth(result, req);\n if (override) return override as NextResponse;\n }\n\n // ── Default: redirect unauthenticated to sign-in ───────────────────────\n if (!result.isSignedIn && !publicRoute) {\n const dest = new URL(signInUrl, req.url);\n dest.searchParams.set(\"redirect_url\", req.url);\n return NextResponse.redirect(dest);\n }\n\n // ── Inject auth headers for Server Components ──────────────────────────\n const next = new Headers(req.headers);\n if (result.userId) {\n next.set(HEADER_USER_ID, result.userId);\n next.set(HEADER_ORG_ID, result.orgId ?? \"\");\n next.set(HEADER_ORG_ROLE, result.orgRole ?? \"\");\n next.set(HEADER_SESSION_ID, result.sessionId ?? \"\");\n next.set(HEADER_RISK_LEVEL, result.riskLevel ?? \"low\");\n } else {\n [HEADER_USER_ID, HEADER_ORG_ID, HEADER_ORG_ROLE, HEADER_SESSION_ID, HEADER_RISK_LEVEL]\n .forEach((h) => next.delete(h));\n }\n\n return NextResponse.next({ request: { headers: next } });\n };\n}\n\nfunction extractBearer(header: string): string {\n return header.startsWith(\"Bearer \") ? header.slice(7) : \"\";\n}\n"],"mappings":"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","names":[]}
@@ -1,36 +1,56 @@
1
1
  // src/middleware.ts
2
- import { importSPKI, jwtVerify } from "jose";
2
+ import { createRemoteJWKSet, importSPKI, jwtVerify } from "jose";
3
3
  import { NextResponse } from "next/server";
4
4
  var HEADER_USER_ID = "x-vaultix-user-id";
5
5
  var HEADER_ORG_ID = "x-vaultix-org-id";
6
6
  var HEADER_ORG_ROLE = "x-vaultix-org-role";
7
7
  var HEADER_SESSION_ID = "x-vaultix-session-id";
8
8
  var HEADER_RISK_LEVEL = "x-vaultix-risk-level";
9
- var cachedKey = null;
10
- var cachedPem = null;
11
- async function getPublicKey() {
12
- const pem = process.env.VAULTIX_JWT_PUBLIC_KEY;
13
- if (!pem) return null;
14
- if (cachedKey && cachedPem === pem) return cachedKey;
9
+ function decodeApiUrlFromKey(pk) {
15
10
  try {
16
- cachedKey = await importSPKI(pem.replace(/\\n/g, "\n"), "RS256");
17
- cachedPem = pem;
18
- return cachedKey;
11
+ const parts = pk.split("_");
12
+ if (parts.length >= 4 && parts[0] === "vaultix" && parts[1] === "pk") {
13
+ return atob(parts.slice(3).join("_")).replace(/\/$/, "");
14
+ }
19
15
  } catch {
20
- return null;
21
16
  }
17
+ return "";
18
+ }
19
+ function resolveApiUrl(options) {
20
+ if (options.apiUrl) return options.apiUrl.replace(/\/$/, "");
21
+ if (process.env.VAULTIX_API_URL) return process.env.VAULTIX_API_URL.replace(/\/$/, "");
22
+ const pk = process.env.NEXT_PUBLIC_VAULTIX_PUBLISHABLE_KEY ?? "";
23
+ return decodeApiUrlFromKey(pk);
24
+ }
25
+ var remoteJwks = null;
26
+ var remoteJwksUrl = null;
27
+ var staticKey = null;
28
+ var staticPem = null;
29
+ async function getVerifyKey(apiUrl) {
30
+ const pem = process.env.VAULTIX_JWT_PUBLIC_KEY;
31
+ if (pem) {
32
+ const normalized = pem.replace(/\\n/g, "\n");
33
+ if (staticKey && staticPem === normalized) return { key: staticKey, mode: "static" };
34
+ staticKey = await importSPKI(normalized, "RS256");
35
+ staticPem = normalized;
36
+ return { key: staticKey, mode: "static" };
37
+ }
38
+ if (!apiUrl) return null;
39
+ const jwksUrl = `${apiUrl}/api/v1/.well-known/jwks.json`;
40
+ if (!remoteJwks || remoteJwksUrl !== jwksUrl) {
41
+ remoteJwks = createRemoteJWKSet(new URL(jwksUrl));
42
+ remoteJwksUrl = jwksUrl;
43
+ }
44
+ return { key: remoteJwks, mode: "remote" };
22
45
  }
23
46
  function isPublic(pathname, rules) {
24
- return rules.some((rule) => {
25
- if (typeof rule === "string") {
26
- return pathname === rule || pathname.startsWith(rule);
27
- }
28
- return rule.test(pathname);
29
- });
47
+ return rules.some(
48
+ (rule) => typeof rule === "string" ? pathname === rule || pathname.startsWith(rule) : rule.test(pathname)
49
+ );
30
50
  }
31
51
  async function handleHandshake(req, handshakeToken, apiUrl) {
32
52
  try {
33
- const res = await fetch(`${apiUrl}/v1/tokens/exchange`, {
53
+ const res = await fetch(`${apiUrl}/api/v1/tokens/exchange`, {
34
54
  method: "POST",
35
55
  headers: { "Content-Type": "application/json" },
36
56
  body: JSON.stringify({ handshake_token: handshakeToken })
@@ -46,7 +66,6 @@ async function handleHandshake(req, handshakeToken, apiUrl) {
46
66
  sameSite: "lax",
47
67
  path: "/",
48
68
  maxAge: 30 * 24 * 60 * 60
49
- // 30 days
50
69
  });
51
70
  return response;
52
71
  } catch {
@@ -54,21 +73,16 @@ async function handleHandshake(req, handshakeToken, apiUrl) {
54
73
  }
55
74
  }
56
75
  function authMiddleware(options = {}) {
57
- const {
58
- publicRoutes = [],
59
- signInUrl = "/sign-in",
60
- afterAuth
61
- } = options;
76
+ const { publicRoutes = [], afterAuth } = options;
62
77
  return async function middleware(req) {
63
78
  const { pathname } = req.nextUrl;
64
79
  const publicRoute = isPublic(pathname, publicRoutes);
80
+ const apiUrl = resolveApiUrl(options);
81
+ const signInUrl = options.signInUrl ?? (apiUrl ? `${apiUrl}/auth/sign-in` : "/sign-in");
65
82
  const handshakeToken = req.nextUrl.searchParams.get("__vaultix_handshake");
66
- if (handshakeToken) {
67
- const apiUrl = options.apiUrl ?? process.env.VAULTIX_API_URL ?? "";
68
- if (apiUrl) {
69
- const response = await handleHandshake(req, handshakeToken, apiUrl);
70
- if (response) return response;
71
- }
83
+ if (handshakeToken && apiUrl) {
84
+ const response = await handleHandshake(req, handshakeToken, apiUrl);
85
+ if (response) return response;
72
86
  }
73
87
  let result = {
74
88
  userId: null,
@@ -79,12 +93,12 @@ function authMiddleware(options = {}) {
79
93
  isSignedIn: false,
80
94
  isPublicRoute: publicRoute
81
95
  };
82
- const sessionCookie = req.cookies.get("vaultix-session")?.value ?? extractBearer(req.headers.get("authorization") ?? "");
83
- if (sessionCookie) {
84
- const key = await getPublicKey();
85
- if (key) {
96
+ const token = req.cookies.get("vaultix-session")?.value ?? extractBearer(req.headers.get("authorization") ?? "");
97
+ if (token) {
98
+ const verifyKey = await getVerifyKey(apiUrl);
99
+ if (verifyKey) {
86
100
  try {
87
- const { payload } = await jwtVerify(sessionCookie, key, {
101
+ const { payload } = await jwtVerify(token, verifyKey.key, {
88
102
  algorithms: ["RS256"]
89
103
  });
90
104
  result = {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/middleware.ts"],"sourcesContent":["// Edge-runtime compatible. Uses jose (not Node crypto) for JWT verification.\nimport { importSPKI, jwtVerify, type KeyLike } from \"jose\";\nimport { NextRequest, NextResponse } from \"next/server\";\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\nexport interface AuthResult {\n /** Authenticated user ID (`sub` claim), or null if unauthenticated. */\n userId: string | null;\n orgId: string | null;\n orgRole: string | null;\n sessionId: string | null;\n riskLevel: string | null;\n isSignedIn: boolean;\n isPublicRoute: boolean;\n}\n\nexport interface AuthMiddlewareOptions {\n /**\n * Routes that do not require authentication.\n * Strings are matched by exact path or prefix (e.g. \"/sign-in\" or \"/api/public/\").\n * RegExps are tested against `pathname`.\n */\n publicRoutes?: Array<string | RegExp>;\n\n /** Where to redirect unauthenticated users. Default: \"/sign-in\". */\n signInUrl?: string;\n\n /**\n * Vaultix API origin — required to exchange handshake tokens.\n * Falls back to VAULTIX_API_URL env var.\n * Example: \"https://vaultix.smritix-ai.com\"\n */\n apiUrl?: string;\n\n /**\n * Custom logic after auth state is determined.\n * Return a `NextResponse` to short-circuit the default redirect behaviour;\n * return `undefined` to continue with defaults.\n */\n afterAuth?: (\n auth: AuthResult,\n req: NextRequest,\n ) => NextResponse | Response | undefined | void;\n}\n\n// ─── Header names injected into RSC requests ──────────────────────────────────\n\nexport const HEADER_USER_ID = \"x-vaultix-user-id\";\nexport const HEADER_ORG_ID = \"x-vaultix-org-id\";\nexport const HEADER_ORG_ROLE = \"x-vaultix-org-role\";\nexport const HEADER_SESSION_ID = \"x-vaultix-session-id\";\nexport const HEADER_RISK_LEVEL = \"x-vaultix-risk-level\";\n\n// ─── Public-key cache ─────────────────────────────────────────────────────────\n\nlet cachedKey: KeyLike | null = null;\nlet cachedPem: string | null = null;\n\nasync function getPublicKey(): Promise<KeyLike | null> {\n const pem = process.env.VAULTIX_JWT_PUBLIC_KEY;\n if (!pem) return null;\n if (cachedKey && cachedPem === pem) return cachedKey;\n\n try {\n cachedKey = await importSPKI(pem.replace(/\\\\n/g, \"\\n\"), \"RS256\");\n cachedPem = pem;\n return cachedKey;\n } catch {\n return null;\n }\n}\n\n// ─── Route matching ───────────────────────────────────────────────────────────\n\nfunction isPublic(pathname: string, rules: Array<string | RegExp>): boolean {\n return rules.some((rule) => {\n if (typeof rule === \"string\") {\n return pathname === rule || pathname.startsWith(rule);\n }\n return rule.test(pathname);\n });\n}\n\n// ─── Handshake token exchange ─────────────────────────────────────────────────\n// Called when `__vaultix_handshake` appears in the URL.\n// Exchanges the short-lived token for a full session JWT, sets a same-domain\n// cookie, and redirects to the clean URL — mirroring Clerk's __clerk_handshake.\n\nasync function handleHandshake(\n req: NextRequest,\n handshakeToken: string,\n apiUrl: string,\n): Promise<NextResponse | null> {\n try {\n const res = await fetch(`${apiUrl}/v1/tokens/exchange`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ handshake_token: handshakeToken }),\n });\n\n if (!res.ok) return null;\n\n const { session_jwt } = (await res.json()) as { session_jwt: string };\n\n const cleanUrl = req.nextUrl.clone();\n cleanUrl.searchParams.delete(\"__vaultix_handshake\");\n\n const response = NextResponse.redirect(cleanUrl);\n response.cookies.set(\"vaultix-session\", session_jwt, {\n httpOnly: true,\n secure: true,\n sameSite: \"lax\",\n path: \"/\",\n maxAge: 30 * 24 * 60 * 60, // 30 days\n });\n return response;\n } catch {\n return null;\n }\n}\n\n// ─── authMiddleware factory ───────────────────────────────────────────────────\n\n/**\n * authMiddleware wraps your Next.js middleware.ts export.\n *\n * @example\n * // middleware.ts\n * import { authMiddleware } from \"@vaultix.ai/nextjs/middleware\";\n * export default authMiddleware({\n * apiUrl: \"https://vaultix.smritix-ai.com\",\n * publicRoutes: [\"/sign-in\", \"/sign-up\"],\n * });\n * export const config = { matcher: [\"/((?!_next|.*\\\\..*).*)\"] };\n */\nexport function authMiddleware(options: AuthMiddlewareOptions = {}) {\n const {\n publicRoutes = [],\n signInUrl = \"/sign-in\",\n afterAuth,\n } = options;\n\n return async function middleware(req: NextRequest): Promise<NextResponse> {\n const { pathname } = req.nextUrl;\n const publicRoute = isPublic(pathname, publicRoutes);\n\n // ── Handshake exchange ─────────────────────────────────────────────────\n const handshakeToken = req.nextUrl.searchParams.get(\"__vaultix_handshake\");\n if (handshakeToken) {\n const apiUrl =\n options.apiUrl ??\n process.env.VAULTIX_API_URL ??\n \"\";\n if (apiUrl) {\n const response = await handleHandshake(req, handshakeToken, apiUrl);\n if (response) return response;\n // If exchange failed, fall through to normal auth check\n }\n }\n\n // ── JWT verification ───────────────────────────────────────────────────\n let result: AuthResult = {\n userId: null,\n orgId: null,\n orgRole: null,\n sessionId: null,\n riskLevel: null,\n isSignedIn: false,\n isPublicRoute: publicRoute,\n };\n\n const sessionCookie =\n req.cookies.get(\"vaultix-session\")?.value ??\n extractBearer(req.headers.get(\"authorization\") ?? \"\");\n\n if (sessionCookie) {\n const key = await getPublicKey();\n if (key) {\n try {\n const { payload } = await jwtVerify(sessionCookie, key, {\n algorithms: [\"RS256\"],\n });\n\n result = {\n userId: (payload[\"uid\"] as string) ?? null,\n orgId: (payload[\"org\"] as string) ?? null,\n orgRole: (payload[\"rol\"] as string) ?? null,\n sessionId: (payload[\"sid\"] as string) ?? null,\n riskLevel: (payload[\"risk\"] as string) ?? \"low\",\n isSignedIn: true,\n isPublicRoute: publicRoute,\n };\n } catch {\n // Expired or tampered token → treat as signed-out.\n }\n }\n }\n\n // Let callers override the default redirect logic.\n if (afterAuth) {\n const override = afterAuth(result, req);\n if (override) return override as NextResponse;\n }\n\n // Default: redirect unauthenticated requests to the sign-in page.\n if (!result.isSignedIn && !publicRoute) {\n const dest = new URL(signInUrl, req.url);\n dest.searchParams.set(\"redirect_url\", req.url);\n return NextResponse.redirect(dest);\n }\n\n // Propagate auth claims to Server Components via request headers.\n const next = new Headers(req.headers);\n if (result.userId) {\n next.set(HEADER_USER_ID, result.userId);\n next.set(HEADER_ORG_ID, result.orgId ?? \"\");\n next.set(HEADER_ORG_ROLE, result.orgRole ?? \"\");\n next.set(HEADER_SESSION_ID, result.sessionId ?? \"\");\n next.set(HEADER_RISK_LEVEL, result.riskLevel ?? \"low\");\n } else {\n [HEADER_USER_ID, HEADER_ORG_ID, HEADER_ORG_ROLE, HEADER_SESSION_ID, HEADER_RISK_LEVEL]\n .forEach((h) => next.delete(h));\n }\n\n return NextResponse.next({ request: { headers: next } });\n };\n}\n\nfunction extractBearer(header: string): string {\n return header.startsWith(\"Bearer \") ? header.slice(7) : \"\";\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/middleware.ts"],"sourcesContent":["// Edge-runtime compatible. Uses jose for JWT verification.\nimport { createRemoteJWKSet, importSPKI, jwtVerify, type KeyLike } from \"jose\";\nimport { NextRequest, NextResponse } from \"next/server\";\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\nexport interface AuthResult {\n userId: string | null;\n orgId: string | null;\n orgRole: string | null;\n sessionId: string | null;\n riskLevel: string | null;\n isSignedIn: boolean;\n isPublicRoute: boolean;\n}\n\nexport interface AuthMiddlewareOptions {\n /**\n * Routes that do not require authentication.\n * Strings: exact match or prefix. RegExps: tested against pathname.\n */\n publicRoutes?: Array<string | RegExp>;\n\n /**\n * Where to redirect unauthenticated users.\n * Defaults to the Vaultix hosted sign-in page (decoded from publishable key).\n * Override with \"/sign-in\" to use your own page.\n */\n signInUrl?: string;\n\n /**\n * Vaultix API origin. Auto-decoded from NEXT_PUBLIC_VAULTIX_PUBLISHABLE_KEY.\n * Only needed if you're not using the standard publishable key format.\n */\n apiUrl?: string;\n\n /**\n * Custom logic after auth state is resolved.\n * Return a NextResponse to override default behaviour.\n */\n afterAuth?: (auth: AuthResult, req: NextRequest) => NextResponse | Response | undefined | void;\n}\n\n// ─── Header names ─────────────────────────────────────────────────────────────\n\nexport const HEADER_USER_ID = \"x-vaultix-user-id\";\nexport const HEADER_ORG_ID = \"x-vaultix-org-id\";\nexport const HEADER_ORG_ROLE = \"x-vaultix-org-role\";\nexport const HEADER_SESSION_ID = \"x-vaultix-session-id\";\nexport const HEADER_RISK_LEVEL = \"x-vaultix-risk-level\";\n\n// ─── Publishable key → API URL ────────────────────────────────────────────────\n\nfunction decodeApiUrlFromKey(pk: string): string {\n try {\n const parts = pk.split(\"_\");\n if (parts.length >= 4 && parts[0] === \"vaultix\" && parts[1] === \"pk\") {\n return atob(parts.slice(3).join(\"_\")).replace(/\\/$/, \"\");\n }\n } catch {}\n return \"\";\n}\n\nfunction resolveApiUrl(options: AuthMiddlewareOptions): string {\n if (options.apiUrl) return options.apiUrl.replace(/\\/$/, \"\");\n if (process.env.VAULTIX_API_URL) return process.env.VAULTIX_API_URL.replace(/\\/$/, \"\");\n const pk = process.env.NEXT_PUBLIC_VAULTIX_PUBLISHABLE_KEY ?? \"\";\n return decodeApiUrlFromKey(pk);\n}\n\n// ─── JWKS cache ───────────────────────────────────────────────────────────────\n// createRemoteJWKSet fetches and caches the key set, re-fetches on rotation.\n// Falls back to a static PEM key if VAULTIX_JWT_PUBLIC_KEY is set (backward compat).\n\nlet remoteJwks: ReturnType<typeof createRemoteJWKSet> | null = null;\nlet remoteJwksUrl: string | null = null;\n\nlet staticKey: KeyLike | null = null;\nlet staticPem: string | null = null;\n\nasync function getVerifyKey(apiUrl: string) {\n // Prefer static PEM (set by env var) for zero-network-call verification\n const pem = process.env.VAULTIX_JWT_PUBLIC_KEY;\n if (pem) {\n const normalized = pem.replace(/\\\\n/g, \"\\n\");\n if (staticKey && staticPem === normalized) return { key: staticKey, mode: \"static\" as const };\n staticKey = await importSPKI(normalized, \"RS256\");\n staticPem = normalized;\n return { key: staticKey, mode: \"static\" as const };\n }\n\n // Auto-fetch JWKS from the API — no env var needed\n if (!apiUrl) return null;\n const jwksUrl = `${apiUrl}/api/v1/.well-known/jwks.json`;\n if (!remoteJwks || remoteJwksUrl !== jwksUrl) {\n remoteJwks = createRemoteJWKSet(new URL(jwksUrl));\n remoteJwksUrl = jwksUrl;\n }\n return { key: remoteJwks, mode: \"remote\" as const };\n}\n\n// ─── Route matching ───────────────────────────────────────────────────────────\n\nfunction isPublic(pathname: string, rules: Array<string | RegExp>): boolean {\n return rules.some((rule) =>\n typeof rule === \"string\"\n ? pathname === rule || pathname.startsWith(rule)\n : rule.test(pathname),\n );\n}\n\n// ─── Handshake exchange ───────────────────────────────────────────────────────\n\nasync function handleHandshake(\n req: NextRequest,\n handshakeToken: string,\n apiUrl: string,\n): Promise<NextResponse | null> {\n try {\n const res = await fetch(`${apiUrl}/api/v1/tokens/exchange`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ handshake_token: handshakeToken }),\n });\n if (!res.ok) return null;\n\n const { session_jwt } = (await res.json()) as { session_jwt: string };\n const cleanUrl = req.nextUrl.clone();\n cleanUrl.searchParams.delete(\"__vaultix_handshake\");\n\n const response = NextResponse.redirect(cleanUrl);\n response.cookies.set(\"vaultix-session\", session_jwt, {\n httpOnly: true,\n secure: true,\n sameSite: \"lax\",\n path: \"/\",\n maxAge: 30 * 24 * 60 * 60,\n });\n return response;\n } catch {\n return null;\n }\n}\n\n// ─── authMiddleware ───────────────────────────────────────────────────────────\n\n/**\n * Drop-in auth middleware — works with zero config when\n * NEXT_PUBLIC_VAULTIX_PUBLISHABLE_KEY is set.\n *\n * @example\n * // middleware.ts\n * import { authMiddleware } from \"@vaultix.ai/nextjs/middleware\";\n * export default authMiddleware({ publicRoutes: [\"/\", \"/about\"] });\n * export const config = { matcher: [\"/((?!_next|.*\\\\..*).*)\"] };\n */\nexport function authMiddleware(options: AuthMiddlewareOptions = {}) {\n const { publicRoutes = [], afterAuth } = options;\n\n return async function middleware(req: NextRequest): Promise<NextResponse> {\n const { pathname } = req.nextUrl;\n const publicRoute = isPublic(pathname, publicRoutes);\n\n const apiUrl = resolveApiUrl(options);\n\n // Default sign-in URL: hosted Vaultix page decoded from publishable key\n const signInUrl =\n options.signInUrl ?? (apiUrl ? `${apiUrl}/auth/sign-in` : \"/sign-in\");\n\n // ── Handshake exchange ─────────────────────────────────────────────────\n const handshakeToken = req.nextUrl.searchParams.get(\"__vaultix_handshake\");\n if (handshakeToken && apiUrl) {\n const response = await handleHandshake(req, handshakeToken, apiUrl);\n if (response) return response;\n }\n\n // ── JWT verification ───────────────────────────────────────────────────\n let result: AuthResult = {\n userId: null, orgId: null, orgRole: null,\n sessionId: null, riskLevel: null,\n isSignedIn: false, isPublicRoute: publicRoute,\n };\n\n const token =\n req.cookies.get(\"vaultix-session\")?.value ??\n extractBearer(req.headers.get(\"authorization\") ?? \"\");\n\n if (token) {\n const verifyKey = await getVerifyKey(apiUrl);\n if (verifyKey) {\n try {\n const { payload } = await jwtVerify(token, verifyKey.key as Parameters<typeof jwtVerify>[1], {\n algorithms: [\"RS256\"],\n });\n result = {\n userId: (payload[\"uid\"] as string) ?? null,\n orgId: (payload[\"org\"] as string) ?? null,\n orgRole: (payload[\"rol\"] as string) ?? null,\n sessionId: (payload[\"sid\"] as string) ?? null,\n riskLevel: (payload[\"risk\"] as string) ?? \"low\",\n isSignedIn: true,\n isPublicRoute: publicRoute,\n };\n } catch {\n // expired / tampered\n }\n }\n }\n\n // ── Custom afterAuth hook ──────────────────────────────────────────────\n if (afterAuth) {\n const override = afterAuth(result, req);\n if (override) return override as NextResponse;\n }\n\n // ── Default: redirect unauthenticated to sign-in ───────────────────────\n if (!result.isSignedIn && !publicRoute) {\n const dest = new URL(signInUrl, req.url);\n dest.searchParams.set(\"redirect_url\", req.url);\n return NextResponse.redirect(dest);\n }\n\n // ── Inject auth headers for Server Components ──────────────────────────\n const next = new Headers(req.headers);\n if (result.userId) {\n next.set(HEADER_USER_ID, result.userId);\n next.set(HEADER_ORG_ID, result.orgId ?? \"\");\n next.set(HEADER_ORG_ROLE, result.orgRole ?? \"\");\n next.set(HEADER_SESSION_ID, result.sessionId ?? \"\");\n next.set(HEADER_RISK_LEVEL, result.riskLevel ?? \"low\");\n } else {\n [HEADER_USER_ID, HEADER_ORG_ID, HEADER_ORG_ROLE, HEADER_SESSION_ID, HEADER_RISK_LEVEL]\n .forEach((h) => next.delete(h));\n }\n\n return NextResponse.next({ request: { headers: next } });\n };\n}\n\nfunction extractBearer(header: string): string {\n return header.startsWith(\"Bearer \") ? header.slice(7) : \"\";\n}\n"],"mappings":"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","names":[]}
package/dist/server.d.mts CHANGED
@@ -7,45 +7,58 @@ interface AuthObject {
7
7
  sessionId: string | null;
8
8
  riskLevel: "low" | "medium" | "high" | "critical" | null;
9
9
  isSignedIn: boolean;
10
+ /**
11
+ * Throws a redirect if the user is not authenticated.
12
+ * Usage: const { userId } = await auth(); — or call auth().then(a => a.protect())
13
+ */
14
+ protect: (redirectTo?: string) => void;
10
15
  }
11
16
  /**
12
- * Returns the current auth state by reading the headers injected by
13
- * `authMiddleware`. Call this in Server Components, Route Handlers, and
14
- * Server Actions never in Client Components.
17
+ * Returns the current auth state. Works in Server Components, Route Handlers,
18
+ * and Server Actions. Falls back to verifying the session cookie directly
19
+ * if middleware headers are not present.
15
20
  *
16
21
  * @example
17
- * import { auth } from "@smritix.ai/nextjs/server";
22
+ * import { auth } from "@vaultix.ai/nextjs/server";
23
+ *
18
24
  * export default async function Page() {
19
- * const { userId, orgId } = await auth();
20
- *
25
+ * const { userId, protect } = await auth();
26
+ * protect(); // redirects to sign-in if not authenticated
27
+ * return <div>Hello {userId}</div>;
21
28
  * }
22
29
  */
23
30
  declare function auth(): Promise<AuthObject>;
24
31
  /**
25
- * Asserts that the current user is authenticated. Redirects to `/sign-in`
26
- * if not. Returns the auth object for convenience.
32
+ * Returns the full user record for the currently signed-in user.
33
+ * Calls GET /api/v1/me using the session JWT from the cookie as a Bearer token.
34
+ * No extra env vars required.
27
35
  *
28
36
  * @example
29
- * export default async function ProtectedPage() {
30
- * const { userId } = await protect();
31
- *
37
+ * import { currentUser } from "@vaultix.ai/nextjs/server";
38
+ *
39
+ * export default async function Page() {
40
+ * const user = await currentUser();
41
+ * if (!user) redirect("/sign-in");
42
+ * return <div>Hello {user.email}</div>;
32
43
  * }
33
44
  */
34
- declare function protect(redirectTo?: string): Promise<AuthObject>;
45
+ declare function currentUser(): Promise<VaultixUser | null>;
35
46
  /**
36
- * Fetches the full `VaultixUser` record from the auth engine using server-to-server
37
- * credentials (`VAULTIX_API_URL` + `VAULTIX_SECRET_KEY` env vars).
38
- * Returns null when unauthenticated or when env vars are missing.
39
- *
40
- * Responses are never cached (`cache: "no-store"`) — user data must be fresh.
47
+ * Returns the active organization for the current user.
48
+ * Requires VAULTIX_SECRET_KEY env var.
41
49
  */
42
- declare function currentUser(): Promise<VaultixUser | null>;
50
+ declare function currentOrg(): Promise<VaultixOrganization | null>;
43
51
  /**
44
- * Fetches the active `VaultixOrganization` from the auth engine.
45
- * Returns null when the user has no active org or env vars are missing.
52
+ * Asserts the current user is authenticated. Redirects to sign-in if not.
53
+ * Prefer calling `protect()` from the auth object returned by `auth()`.
46
54
  *
47
- * Responses are cached for 30 seconds — org data changes infrequently.
55
+ * @example
56
+ * import { protect } from "@vaultix.ai/nextjs/server";
57
+ * export default async function Page() {
58
+ * const { userId } = await protect();
59
+ * return <div>{userId}</div>;
60
+ * }
48
61
  */
49
- declare function currentOrg(): Promise<VaultixOrganization | null>;
62
+ declare function protect(redirectTo?: string): Promise<AuthObject>;
50
63
 
51
64
  export { type AuthObject, auth, currentOrg, currentUser, protect };
package/dist/server.d.ts CHANGED
@@ -7,45 +7,58 @@ interface AuthObject {
7
7
  sessionId: string | null;
8
8
  riskLevel: "low" | "medium" | "high" | "critical" | null;
9
9
  isSignedIn: boolean;
10
+ /**
11
+ * Throws a redirect if the user is not authenticated.
12
+ * Usage: const { userId } = await auth(); — or call auth().then(a => a.protect())
13
+ */
14
+ protect: (redirectTo?: string) => void;
10
15
  }
11
16
  /**
12
- * Returns the current auth state by reading the headers injected by
13
- * `authMiddleware`. Call this in Server Components, Route Handlers, and
14
- * Server Actions never in Client Components.
17
+ * Returns the current auth state. Works in Server Components, Route Handlers,
18
+ * and Server Actions. Falls back to verifying the session cookie directly
19
+ * if middleware headers are not present.
15
20
  *
16
21
  * @example
17
- * import { auth } from "@smritix.ai/nextjs/server";
22
+ * import { auth } from "@vaultix.ai/nextjs/server";
23
+ *
18
24
  * export default async function Page() {
19
- * const { userId, orgId } = await auth();
20
- *
25
+ * const { userId, protect } = await auth();
26
+ * protect(); // redirects to sign-in if not authenticated
27
+ * return <div>Hello {userId}</div>;
21
28
  * }
22
29
  */
23
30
  declare function auth(): Promise<AuthObject>;
24
31
  /**
25
- * Asserts that the current user is authenticated. Redirects to `/sign-in`
26
- * if not. Returns the auth object for convenience.
32
+ * Returns the full user record for the currently signed-in user.
33
+ * Calls GET /api/v1/me using the session JWT from the cookie as a Bearer token.
34
+ * No extra env vars required.
27
35
  *
28
36
  * @example
29
- * export default async function ProtectedPage() {
30
- * const { userId } = await protect();
31
- *
37
+ * import { currentUser } from "@vaultix.ai/nextjs/server";
38
+ *
39
+ * export default async function Page() {
40
+ * const user = await currentUser();
41
+ * if (!user) redirect("/sign-in");
42
+ * return <div>Hello {user.email}</div>;
32
43
  * }
33
44
  */
34
- declare function protect(redirectTo?: string): Promise<AuthObject>;
45
+ declare function currentUser(): Promise<VaultixUser | null>;
35
46
  /**
36
- * Fetches the full `VaultixUser` record from the auth engine using server-to-server
37
- * credentials (`VAULTIX_API_URL` + `VAULTIX_SECRET_KEY` env vars).
38
- * Returns null when unauthenticated or when env vars are missing.
39
- *
40
- * Responses are never cached (`cache: "no-store"`) — user data must be fresh.
47
+ * Returns the active organization for the current user.
48
+ * Requires VAULTIX_SECRET_KEY env var.
41
49
  */
42
- declare function currentUser(): Promise<VaultixUser | null>;
50
+ declare function currentOrg(): Promise<VaultixOrganization | null>;
43
51
  /**
44
- * Fetches the active `VaultixOrganization` from the auth engine.
45
- * Returns null when the user has no active org or env vars are missing.
52
+ * Asserts the current user is authenticated. Redirects to sign-in if not.
53
+ * Prefer calling `protect()` from the auth object returned by `auth()`.
46
54
  *
47
- * Responses are cached for 30 seconds — org data changes infrequently.
55
+ * @example
56
+ * import { protect } from "@vaultix.ai/nextjs/server";
57
+ * export default async function Page() {
58
+ * const { userId } = await protect();
59
+ * return <div>{userId}</div>;
60
+ * }
48
61
  */
49
- declare function currentOrg(): Promise<VaultixOrganization | null>;
62
+ declare function protect(redirectTo?: string): Promise<AuthObject>;
50
63
 
51
64
  export { type AuthObject, auth, currentOrg, currentUser, protect };