@githat/nextjs 0.2.5 → 0.2.7
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.mts +177 -1
- package/dist/index.d.ts +177 -1
- package/dist/index.js +581 -53
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +524 -1
- package/dist/index.mjs.map +1 -1
- package/dist/server.d.mts +41 -1
- package/dist/server.d.ts +41 -1
- package/dist/server.js +18 -0
- package/dist/server.js.map +1 -1
- package/dist/server.mjs +17 -0
- package/dist/server.mjs.map +1 -1
- package/package.json +1 -1
package/dist/server.d.ts
CHANGED
|
@@ -160,5 +160,45 @@ interface WithAuthOptions {
|
|
|
160
160
|
* ```
|
|
161
161
|
*/
|
|
162
162
|
declare function withAuth(handler: AuthenticatedHandler, options?: WithAuthOptions): (request: Request) => Promise<Response>;
|
|
163
|
+
interface ServerSendEmailOptions {
|
|
164
|
+
/** Recipient email address(es). */
|
|
165
|
+
to: string | string[];
|
|
166
|
+
/** Email subject line. */
|
|
167
|
+
subject: string;
|
|
168
|
+
/** HTML body. */
|
|
169
|
+
html?: string;
|
|
170
|
+
/** Plain text body. */
|
|
171
|
+
text?: string;
|
|
172
|
+
/** Reply-to email address. */
|
|
173
|
+
replyTo?: string;
|
|
174
|
+
}
|
|
175
|
+
interface ServerSendEmailResult {
|
|
176
|
+
messageId: string;
|
|
177
|
+
to: string[];
|
|
178
|
+
subject: string;
|
|
179
|
+
sent: boolean;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Send a transactional email from the server.
|
|
183
|
+
* Use this in Next.js API routes, server actions, or any server-side code.
|
|
184
|
+
*
|
|
185
|
+
* @example
|
|
186
|
+
* ```ts
|
|
187
|
+
* import { sendEmail } from '@githat/nextjs/server';
|
|
188
|
+
*
|
|
189
|
+
* const result = await sendEmail({
|
|
190
|
+
* to: 'user@example.com',
|
|
191
|
+
* subject: 'Welcome!',
|
|
192
|
+
* html: '<h1>Welcome aboard</h1>',
|
|
193
|
+
* replyTo: 'support@myapp.com'
|
|
194
|
+
* }, {
|
|
195
|
+
* token: accessToken
|
|
196
|
+
* });
|
|
197
|
+
* ```
|
|
198
|
+
*/
|
|
199
|
+
declare function sendEmail(options: ServerSendEmailOptions, config: {
|
|
200
|
+
token: string;
|
|
201
|
+
apiUrl?: string;
|
|
202
|
+
}): Promise<ServerSendEmailResult>;
|
|
163
203
|
|
|
164
|
-
export { type AuthPayload, type AuthenticatedHandler, COOKIE_NAMES, type OrgMetadata, type VerifyOptions, type WithAuthOptions, getAuth, getOrgMetadata, updateOrgMetadata, verifyToken, withAuth };
|
|
204
|
+
export { type AuthPayload, type AuthenticatedHandler, COOKIE_NAMES, type OrgMetadata, type ServerSendEmailOptions, type ServerSendEmailResult, type VerifyOptions, type WithAuthOptions, getAuth, getOrgMetadata, sendEmail, updateOrgMetadata, verifyToken, withAuth };
|
package/dist/server.js
CHANGED
|
@@ -33,6 +33,7 @@ __export(server_exports, {
|
|
|
33
33
|
COOKIE_NAMES: () => COOKIE_NAMES,
|
|
34
34
|
getAuth: () => getAuth,
|
|
35
35
|
getOrgMetadata: () => getOrgMetadata,
|
|
36
|
+
sendEmail: () => sendEmail,
|
|
36
37
|
updateOrgMetadata: () => updateOrgMetadata,
|
|
37
38
|
verifyToken: () => verifyToken,
|
|
38
39
|
withAuth: () => withAuth
|
|
@@ -191,11 +192,28 @@ function withAuth(handler, options = {}) {
|
|
|
191
192
|
return handler(request, auth);
|
|
192
193
|
};
|
|
193
194
|
}
|
|
195
|
+
async function sendEmail(options, config) {
|
|
196
|
+
const { token, apiUrl = DEFAULT_API_URL } = config;
|
|
197
|
+
const response = await fetch(`${apiUrl}/email/send`, {
|
|
198
|
+
method: "POST",
|
|
199
|
+
headers: {
|
|
200
|
+
Authorization: `Bearer ${token}`,
|
|
201
|
+
"Content-Type": "application/json"
|
|
202
|
+
},
|
|
203
|
+
body: JSON.stringify(options)
|
|
204
|
+
});
|
|
205
|
+
if (!response.ok) {
|
|
206
|
+
const data = await response.json().catch(() => ({}));
|
|
207
|
+
throw new Error(data.error || "Failed to send email");
|
|
208
|
+
}
|
|
209
|
+
return response.json();
|
|
210
|
+
}
|
|
194
211
|
// Annotate the CommonJS export names for ESM import in node:
|
|
195
212
|
0 && (module.exports = {
|
|
196
213
|
COOKIE_NAMES,
|
|
197
214
|
getAuth,
|
|
198
215
|
getOrgMetadata,
|
|
216
|
+
sendEmail,
|
|
199
217
|
updateOrgMetadata,
|
|
200
218
|
verifyToken,
|
|
201
219
|
withAuth
|
package/dist/server.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/server.ts","../src/config.ts"],"sourcesContent":["/**\n * @githat/nextjs/server\n *\n * Server-side utilities for token verification in Next.js API routes and middleware.\n * This module runs on the server only — do not import in client components.\n */\n\nimport * as jose from 'jose';\nimport { DEFAULT_API_URL } from './config';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface AuthPayload {\n userId: string;\n email: string;\n orgId: string | null;\n orgSlug: string | null;\n role: 'owner' | 'admin' | 'member' | null;\n tier: 'free' | 'basic' | 'pro' | 'enterprise' | null;\n}\n\nexport interface VerifyOptions {\n /**\n * Secret key for local JWT verification. If provided, tokens are verified\n * locally without making an API call (~1ms vs ~50-100ms).\n * Must match the JWT_SECRET used by the GitHat backend.\n */\n secretKey?: string;\n\n /**\n * API URL for remote token verification. Defaults to https://api.githat.io\n */\n apiUrl?: string;\n}\n\nexport interface OrgMetadata {\n [key: string]: unknown;\n}\n\n// ============================================================================\n// Cookie Names\n// ============================================================================\n\nexport const COOKIE_NAMES = {\n accessToken: 'githat_access',\n refreshToken: 'githat_refresh',\n} as const;\n\n// ============================================================================\n// Token Verification\n// ============================================================================\n\n/**\n * Verify a JWT token and return the decoded auth payload.\n *\n * Supports two verification modes:\n * 1. Local verification (recommended for performance): Pass `secretKey` option\n * 2. API-based verification: No secret key needed, calls api.githat.io/auth/verify\n *\n * @example Local verification (fast, ~1ms)\n * ```ts\n * const auth = await verifyToken(token, {\n * secretKey: process.env.GITHAT_SECRET_KEY\n * });\n * ```\n *\n * @example API-based verification (simpler setup)\n * ```ts\n * const auth = await verifyToken(token);\n * ```\n */\nexport async function verifyToken(\n token: string,\n options: VerifyOptions = {}\n): Promise<AuthPayload> {\n const { secretKey, apiUrl = DEFAULT_API_URL } = options;\n\n if (secretKey) {\n // Local verification using jose\n return verifyTokenLocally(token, secretKey);\n }\n\n // API-based verification\n return verifyTokenViaAPI(token, apiUrl);\n}\n\n/**\n * Verify token locally using the secret key (no network call)\n */\nasync function verifyTokenLocally(\n token: string,\n secretKey: string\n): Promise<AuthPayload> {\n try {\n const secret = new TextEncoder().encode(secretKey);\n const { payload } = await jose.jwtVerify(token, secret, {\n algorithms: ['HS256'],\n });\n\n // Validate this is a user access token\n if (payload.type && payload.type !== 'user') {\n throw new Error('Invalid token type');\n }\n if (payload.tokenType !== 'access') {\n throw new Error('Invalid token type');\n }\n\n return {\n userId: payload.userId as string,\n email: payload.email as string,\n orgId: (payload.orgId as string) || null,\n orgSlug: (payload.orgSlug as string) || null,\n role: (payload.orgRole as AuthPayload['role']) || null,\n tier: null, // Tier requires DB lookup, not available in local verification\n };\n } catch (err) {\n if (err instanceof jose.errors.JWTExpired) {\n throw new Error('Token expired');\n }\n if (err instanceof jose.errors.JWTClaimValidationFailed) {\n throw new Error('Invalid token claims');\n }\n if (err instanceof jose.errors.JWTInvalid) {\n throw new Error('Invalid token');\n }\n // JWSSignatureVerificationFailed is used for invalid signatures\n if (err instanceof jose.errors.JWSSignatureVerificationFailed) {\n throw new Error('Invalid token signature');\n }\n throw err;\n }\n}\n\n/**\n * Verify token via API call to api.githat.io/auth/verify\n */\nasync function verifyTokenViaAPI(\n token: string,\n apiUrl: string\n): Promise<AuthPayload> {\n const response = await fetch(`${apiUrl}/auth/verify`, {\n method: 'GET',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n });\n\n if (!response.ok) {\n const data = await response.json().catch(() => ({}));\n throw new Error(data.error || 'Token verification failed');\n }\n\n const data = await response.json();\n return {\n userId: data.userId,\n email: data.email,\n orgId: data.orgId,\n orgSlug: data.orgSlug,\n role: data.role,\n tier: data.tier,\n };\n}\n\n// ============================================================================\n// Request Helpers\n// ============================================================================\n\n/**\n * Extract and verify the auth token from a Next.js request.\n * Checks cookies first (for httpOnly cookie mode), then Authorization header.\n *\n * Returns null if no token is found (unauthenticated request).\n * Throws if a token is found but is invalid/expired.\n *\n * @example In a Next.js API route\n * ```ts\n * import { getAuth } from '@githat/nextjs/server';\n *\n * export async function GET(request: Request) {\n * const auth = await getAuth(request, {\n * secretKey: process.env.GITHAT_SECRET_KEY\n * });\n *\n * if (!auth) {\n * return Response.json({ error: 'Unauthorized' }, { status: 401 });\n * }\n *\n * return Response.json({ userId: auth.userId });\n * }\n * ```\n */\nexport async function getAuth(\n request: Request,\n options: VerifyOptions = {}\n): Promise<AuthPayload | null> {\n // Try to get token from cookie first (httpOnly cookie mode)\n const cookieHeader = request.headers.get('cookie');\n let token: string | null = null;\n\n if (cookieHeader) {\n const cookies = parseCookies(cookieHeader);\n token = cookies[COOKIE_NAMES.accessToken] || null;\n }\n\n // Fallback to Authorization header\n if (!token) {\n const authHeader = request.headers.get('authorization');\n if (authHeader?.startsWith('Bearer ')) {\n token = authHeader.slice(7);\n }\n }\n\n // No token found — return null (unauthenticated)\n if (!token) {\n return null;\n }\n\n // Verify the token\n return verifyToken(token, options);\n}\n\n/**\n * Parse cookies from a cookie header string\n */\nfunction parseCookies(cookieHeader: string): Record<string, string> {\n const cookies: Record<string, string> = {};\n const pairs = cookieHeader.split(';');\n\n for (const pair of pairs) {\n const [name, ...rest] = pair.trim().split('=');\n if (name) {\n cookies[name] = rest.join('=');\n }\n }\n\n return cookies;\n}\n\n// ============================================================================\n// Org Metadata (Server-side)\n// ============================================================================\n\n/**\n * Get organization metadata from the server.\n *\n * @example\n * ```ts\n * import { getOrgMetadata } from '@githat/nextjs/server';\n *\n * const meta = await getOrgMetadata(orgId, {\n * token: accessToken,\n * apiUrl: 'https://api.githat.io'\n * });\n * console.log(meta.stripeAccountId);\n * ```\n */\nexport async function getOrgMetadata(\n orgId: string,\n options: { token: string; apiUrl?: string }\n): Promise<OrgMetadata> {\n const { token, apiUrl = DEFAULT_API_URL } = options;\n\n const response = await fetch(`${apiUrl}/orgs/${orgId}/metadata`, {\n method: 'GET',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n });\n\n if (!response.ok) {\n const data = await response.json().catch(() => ({}));\n throw new Error(data.error || 'Failed to get org metadata');\n }\n\n const data = await response.json();\n return data.metadata || {};\n}\n\n/**\n * Update organization metadata from the server.\n *\n * @example\n * ```ts\n * import { updateOrgMetadata } from '@githat/nextjs/server';\n *\n * await updateOrgMetadata(orgId, { stripeAccountId: 'acct_xxx' }, {\n * token: accessToken,\n * apiUrl: 'https://api.githat.io'\n * });\n * ```\n */\nexport async function updateOrgMetadata(\n orgId: string,\n metadata: OrgMetadata,\n options: { token: string; apiUrl?: string }\n): Promise<OrgMetadata> {\n const { token, apiUrl = DEFAULT_API_URL } = options;\n\n const response = await fetch(`${apiUrl}/orgs/${orgId}/metadata`, {\n method: 'PATCH',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(metadata),\n });\n\n if (!response.ok) {\n const data = await response.json().catch(() => ({}));\n throw new Error(data.error || 'Failed to update org metadata');\n }\n\n const data = await response.json();\n return data.metadata || {};\n}\n\n// ============================================================================\n// API Route Helpers\n// ============================================================================\n\n/**\n * Handler function that receives the request and verified auth payload.\n */\nexport type AuthenticatedHandler = (\n request: Request,\n auth: AuthPayload\n) => Promise<Response> | Response;\n\n/**\n * Options for the withAuth wrapper.\n */\nexport interface WithAuthOptions {\n /**\n * Secret key for local JWT verification.\n * Defaults to process.env.GITHAT_SECRET_KEY if not provided.\n */\n secretKey?: string;\n\n /**\n * Custom response to return when authentication fails.\n * Defaults to JSON { error: 'Unauthorized' } with status 401.\n */\n onUnauthorized?: () => Response;\n}\n\n/**\n * Wrap an API route handler with authentication.\n * The handler will only be called if the request has a valid auth token.\n *\n * @example\n * ```ts\n * // app/api/orders/route.ts\n * import { withAuth } from '@githat/nextjs/server';\n *\n * export const GET = withAuth(async (request, auth) => {\n * // auth.userId, auth.orgId, auth.role available\n * const orders = await db.orders.findMany({ where: { orgId: auth.orgId } });\n * return Response.json({ orders });\n * }, { secretKey: process.env.GITHAT_SECRET_KEY });\n * ```\n *\n * @example With custom unauthorized response\n * ```ts\n * export const GET = withAuth(\n * async (request, auth) => {\n * return Response.json({ userId: auth.userId });\n * },\n * {\n * secretKey: process.env.GITHAT_SECRET_KEY,\n * onUnauthorized: () => Response.redirect('/sign-in'),\n * }\n * );\n * ```\n */\nexport function withAuth(\n handler: AuthenticatedHandler,\n options: WithAuthOptions = {}\n): (request: Request) => Promise<Response> {\n return async (request: Request): Promise<Response> => {\n const auth = await getAuth(request, { secretKey: options.secretKey });\n\n if (!auth) {\n if (options.onUnauthorized) {\n return options.onUnauthorized();\n }\n return new Response(JSON.stringify({ error: 'Unauthorized' }), {\n status: 401,\n headers: { 'Content-Type': 'application/json' },\n });\n }\n\n return handler(request, auth);\n };\n}\n","import type { GitHatConfig } from './types';\n\nexport const DEFAULT_API_URL = 'https://api.githat.io';\n\nexport const TOKEN_KEYS = {\n accessToken: 'githat_access_token',\n refreshToken: 'githat_refresh_token',\n user: 'githat_user',\n org: 'githat_org',\n} as const;\n\nexport const COOKIE_NAMES = {\n accessToken: 'githat_access',\n refreshToken: 'githat_refresh',\n} as const;\n\nexport function resolveConfig(config: GitHatConfig): Required<GitHatConfig> {\n return {\n publishableKey: config.publishableKey,\n apiUrl: config.apiUrl || DEFAULT_API_URL,\n signInUrl: config.signInUrl || '/sign-in',\n signUpUrl: config.signUpUrl || '/sign-up',\n afterSignInUrl: config.afterSignInUrl || '/dashboard',\n afterSignOutUrl: config.afterSignOutUrl || '/',\n tokenStorage: config.tokenStorage || 'localStorage',\n };\n}\n"],"mappings":"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","names":["data"]}
|
|
1
|
+
{"version":3,"sources":["../src/server.ts","../src/config.ts"],"sourcesContent":["/**\n * @githat/nextjs/server\n *\n * Server-side utilities for token verification in Next.js API routes and middleware.\n * This module runs on the server only — do not import in client components.\n */\n\nimport * as jose from 'jose';\nimport { DEFAULT_API_URL } from './config';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface AuthPayload {\n userId: string;\n email: string;\n orgId: string | null;\n orgSlug: string | null;\n role: 'owner' | 'admin' | 'member' | null;\n tier: 'free' | 'basic' | 'pro' | 'enterprise' | null;\n}\n\nexport interface VerifyOptions {\n /**\n * Secret key for local JWT verification. If provided, tokens are verified\n * locally without making an API call (~1ms vs ~50-100ms).\n * Must match the JWT_SECRET used by the GitHat backend.\n */\n secretKey?: string;\n\n /**\n * API URL for remote token verification. Defaults to https://api.githat.io\n */\n apiUrl?: string;\n}\n\nexport interface OrgMetadata {\n [key: string]: unknown;\n}\n\n// ============================================================================\n// Cookie Names\n// ============================================================================\n\nexport const COOKIE_NAMES = {\n accessToken: 'githat_access',\n refreshToken: 'githat_refresh',\n} as const;\n\n// ============================================================================\n// Token Verification\n// ============================================================================\n\n/**\n * Verify a JWT token and return the decoded auth payload.\n *\n * Supports two verification modes:\n * 1. Local verification (recommended for performance): Pass `secretKey` option\n * 2. API-based verification: No secret key needed, calls api.githat.io/auth/verify\n *\n * @example Local verification (fast, ~1ms)\n * ```ts\n * const auth = await verifyToken(token, {\n * secretKey: process.env.GITHAT_SECRET_KEY\n * });\n * ```\n *\n * @example API-based verification (simpler setup)\n * ```ts\n * const auth = await verifyToken(token);\n * ```\n */\nexport async function verifyToken(\n token: string,\n options: VerifyOptions = {}\n): Promise<AuthPayload> {\n const { secretKey, apiUrl = DEFAULT_API_URL } = options;\n\n if (secretKey) {\n // Local verification using jose\n return verifyTokenLocally(token, secretKey);\n }\n\n // API-based verification\n return verifyTokenViaAPI(token, apiUrl);\n}\n\n/**\n * Verify token locally using the secret key (no network call)\n */\nasync function verifyTokenLocally(\n token: string,\n secretKey: string\n): Promise<AuthPayload> {\n try {\n const secret = new TextEncoder().encode(secretKey);\n const { payload } = await jose.jwtVerify(token, secret, {\n algorithms: ['HS256'],\n });\n\n // Validate this is a user access token\n if (payload.type && payload.type !== 'user') {\n throw new Error('Invalid token type');\n }\n if (payload.tokenType !== 'access') {\n throw new Error('Invalid token type');\n }\n\n return {\n userId: payload.userId as string,\n email: payload.email as string,\n orgId: (payload.orgId as string) || null,\n orgSlug: (payload.orgSlug as string) || null,\n role: (payload.orgRole as AuthPayload['role']) || null,\n tier: null, // Tier requires DB lookup, not available in local verification\n };\n } catch (err) {\n if (err instanceof jose.errors.JWTExpired) {\n throw new Error('Token expired');\n }\n if (err instanceof jose.errors.JWTClaimValidationFailed) {\n throw new Error('Invalid token claims');\n }\n if (err instanceof jose.errors.JWTInvalid) {\n throw new Error('Invalid token');\n }\n // JWSSignatureVerificationFailed is used for invalid signatures\n if (err instanceof jose.errors.JWSSignatureVerificationFailed) {\n throw new Error('Invalid token signature');\n }\n throw err;\n }\n}\n\n/**\n * Verify token via API call to api.githat.io/auth/verify\n */\nasync function verifyTokenViaAPI(\n token: string,\n apiUrl: string\n): Promise<AuthPayload> {\n const response = await fetch(`${apiUrl}/auth/verify`, {\n method: 'GET',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n });\n\n if (!response.ok) {\n const data = await response.json().catch(() => ({}));\n throw new Error(data.error || 'Token verification failed');\n }\n\n const data = await response.json();\n return {\n userId: data.userId,\n email: data.email,\n orgId: data.orgId,\n orgSlug: data.orgSlug,\n role: data.role,\n tier: data.tier,\n };\n}\n\n// ============================================================================\n// Request Helpers\n// ============================================================================\n\n/**\n * Extract and verify the auth token from a Next.js request.\n * Checks cookies first (for httpOnly cookie mode), then Authorization header.\n *\n * Returns null if no token is found (unauthenticated request).\n * Throws if a token is found but is invalid/expired.\n *\n * @example In a Next.js API route\n * ```ts\n * import { getAuth } from '@githat/nextjs/server';\n *\n * export async function GET(request: Request) {\n * const auth = await getAuth(request, {\n * secretKey: process.env.GITHAT_SECRET_KEY\n * });\n *\n * if (!auth) {\n * return Response.json({ error: 'Unauthorized' }, { status: 401 });\n * }\n *\n * return Response.json({ userId: auth.userId });\n * }\n * ```\n */\nexport async function getAuth(\n request: Request,\n options: VerifyOptions = {}\n): Promise<AuthPayload | null> {\n // Try to get token from cookie first (httpOnly cookie mode)\n const cookieHeader = request.headers.get('cookie');\n let token: string | null = null;\n\n if (cookieHeader) {\n const cookies = parseCookies(cookieHeader);\n token = cookies[COOKIE_NAMES.accessToken] || null;\n }\n\n // Fallback to Authorization header\n if (!token) {\n const authHeader = request.headers.get('authorization');\n if (authHeader?.startsWith('Bearer ')) {\n token = authHeader.slice(7);\n }\n }\n\n // No token found — return null (unauthenticated)\n if (!token) {\n return null;\n }\n\n // Verify the token\n return verifyToken(token, options);\n}\n\n/**\n * Parse cookies from a cookie header string\n */\nfunction parseCookies(cookieHeader: string): Record<string, string> {\n const cookies: Record<string, string> = {};\n const pairs = cookieHeader.split(';');\n\n for (const pair of pairs) {\n const [name, ...rest] = pair.trim().split('=');\n if (name) {\n cookies[name] = rest.join('=');\n }\n }\n\n return cookies;\n}\n\n// ============================================================================\n// Org Metadata (Server-side)\n// ============================================================================\n\n/**\n * Get organization metadata from the server.\n *\n * @example\n * ```ts\n * import { getOrgMetadata } from '@githat/nextjs/server';\n *\n * const meta = await getOrgMetadata(orgId, {\n * token: accessToken,\n * apiUrl: 'https://api.githat.io'\n * });\n * console.log(meta.stripeAccountId);\n * ```\n */\nexport async function getOrgMetadata(\n orgId: string,\n options: { token: string; apiUrl?: string }\n): Promise<OrgMetadata> {\n const { token, apiUrl = DEFAULT_API_URL } = options;\n\n const response = await fetch(`${apiUrl}/orgs/${orgId}/metadata`, {\n method: 'GET',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n });\n\n if (!response.ok) {\n const data = await response.json().catch(() => ({}));\n throw new Error(data.error || 'Failed to get org metadata');\n }\n\n const data = await response.json();\n return data.metadata || {};\n}\n\n/**\n * Update organization metadata from the server.\n *\n * @example\n * ```ts\n * import { updateOrgMetadata } from '@githat/nextjs/server';\n *\n * await updateOrgMetadata(orgId, { stripeAccountId: 'acct_xxx' }, {\n * token: accessToken,\n * apiUrl: 'https://api.githat.io'\n * });\n * ```\n */\nexport async function updateOrgMetadata(\n orgId: string,\n metadata: OrgMetadata,\n options: { token: string; apiUrl?: string }\n): Promise<OrgMetadata> {\n const { token, apiUrl = DEFAULT_API_URL } = options;\n\n const response = await fetch(`${apiUrl}/orgs/${orgId}/metadata`, {\n method: 'PATCH',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(metadata),\n });\n\n if (!response.ok) {\n const data = await response.json().catch(() => ({}));\n throw new Error(data.error || 'Failed to update org metadata');\n }\n\n const data = await response.json();\n return data.metadata || {};\n}\n\n// ============================================================================\n// API Route Helpers\n// ============================================================================\n\n/**\n * Handler function that receives the request and verified auth payload.\n */\nexport type AuthenticatedHandler = (\n request: Request,\n auth: AuthPayload\n) => Promise<Response> | Response;\n\n/**\n * Options for the withAuth wrapper.\n */\nexport interface WithAuthOptions {\n /**\n * Secret key for local JWT verification.\n * Defaults to process.env.GITHAT_SECRET_KEY if not provided.\n */\n secretKey?: string;\n\n /**\n * Custom response to return when authentication fails.\n * Defaults to JSON { error: 'Unauthorized' } with status 401.\n */\n onUnauthorized?: () => Response;\n}\n\n/**\n * Wrap an API route handler with authentication.\n * The handler will only be called if the request has a valid auth token.\n *\n * @example\n * ```ts\n * // app/api/orders/route.ts\n * import { withAuth } from '@githat/nextjs/server';\n *\n * export const GET = withAuth(async (request, auth) => {\n * // auth.userId, auth.orgId, auth.role available\n * const orders = await db.orders.findMany({ where: { orgId: auth.orgId } });\n * return Response.json({ orders });\n * }, { secretKey: process.env.GITHAT_SECRET_KEY });\n * ```\n *\n * @example With custom unauthorized response\n * ```ts\n * export const GET = withAuth(\n * async (request, auth) => {\n * return Response.json({ userId: auth.userId });\n * },\n * {\n * secretKey: process.env.GITHAT_SECRET_KEY,\n * onUnauthorized: () => Response.redirect('/sign-in'),\n * }\n * );\n * ```\n */\nexport function withAuth(\n handler: AuthenticatedHandler,\n options: WithAuthOptions = {}\n): (request: Request) => Promise<Response> {\n return async (request: Request): Promise<Response> => {\n const auth = await getAuth(request, { secretKey: options.secretKey });\n\n if (!auth) {\n if (options.onUnauthorized) {\n return options.onUnauthorized();\n }\n return new Response(JSON.stringify({ error: 'Unauthorized' }), {\n status: 401,\n headers: { 'Content-Type': 'application/json' },\n });\n }\n\n return handler(request, auth);\n };\n}\n\n// ============================================================================\n// Email Sending (Server-side)\n// ============================================================================\n\nexport interface ServerSendEmailOptions {\n /** Recipient email address(es). */\n to: string | string[];\n /** Email subject line. */\n subject: string;\n /** HTML body. */\n html?: string;\n /** Plain text body. */\n text?: string;\n /** Reply-to email address. */\n replyTo?: string;\n}\n\nexport interface ServerSendEmailResult {\n messageId: string;\n to: string[];\n subject: string;\n sent: boolean;\n}\n\n/**\n * Send a transactional email from the server.\n * Use this in Next.js API routes, server actions, or any server-side code.\n *\n * @example\n * ```ts\n * import { sendEmail } from '@githat/nextjs/server';\n *\n * const result = await sendEmail({\n * to: 'user@example.com',\n * subject: 'Welcome!',\n * html: '<h1>Welcome aboard</h1>',\n * replyTo: 'support@myapp.com'\n * }, {\n * token: accessToken\n * });\n * ```\n */\nexport async function sendEmail(\n options: ServerSendEmailOptions,\n config: { token: string; apiUrl?: string }\n): Promise<ServerSendEmailResult> {\n const { token, apiUrl = DEFAULT_API_URL } = config;\n\n const response = await fetch(`${apiUrl}/email/send`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(options),\n });\n\n if (!response.ok) {\n const data = await response.json().catch(() => ({}));\n throw new Error(data.error || 'Failed to send email');\n }\n\n return response.json();\n}\n","import type { GitHatConfig } from './types';\n\nexport const DEFAULT_API_URL = 'https://api.githat.io';\n\nexport const TOKEN_KEYS = {\n accessToken: 'githat_access_token',\n refreshToken: 'githat_refresh_token',\n user: 'githat_user',\n org: 'githat_org',\n} as const;\n\nexport const COOKIE_NAMES = {\n accessToken: 'githat_access',\n refreshToken: 'githat_refresh',\n} as const;\n\nexport function resolveConfig(config: GitHatConfig): Required<GitHatConfig> {\n return {\n publishableKey: config.publishableKey,\n apiUrl: config.apiUrl || DEFAULT_API_URL,\n signInUrl: config.signInUrl || '/sign-in',\n signUpUrl: config.signUpUrl || '/sign-up',\n afterSignInUrl: config.afterSignInUrl || '/dashboard',\n afterSignOutUrl: config.afterSignOutUrl || '/',\n tokenStorage: config.tokenStorage || 'localStorage',\n };\n}\n"],"mappings":"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","names":["data"]}
|
package/dist/server.mjs
CHANGED
|
@@ -152,10 +152,27 @@ function withAuth(handler, options = {}) {
|
|
|
152
152
|
return handler(request, auth);
|
|
153
153
|
};
|
|
154
154
|
}
|
|
155
|
+
async function sendEmail(options, config) {
|
|
156
|
+
const { token, apiUrl = DEFAULT_API_URL } = config;
|
|
157
|
+
const response = await fetch(`${apiUrl}/email/send`, {
|
|
158
|
+
method: "POST",
|
|
159
|
+
headers: {
|
|
160
|
+
Authorization: `Bearer ${token}`,
|
|
161
|
+
"Content-Type": "application/json"
|
|
162
|
+
},
|
|
163
|
+
body: JSON.stringify(options)
|
|
164
|
+
});
|
|
165
|
+
if (!response.ok) {
|
|
166
|
+
const data = await response.json().catch(() => ({}));
|
|
167
|
+
throw new Error(data.error || "Failed to send email");
|
|
168
|
+
}
|
|
169
|
+
return response.json();
|
|
170
|
+
}
|
|
155
171
|
export {
|
|
156
172
|
COOKIE_NAMES,
|
|
157
173
|
getAuth,
|
|
158
174
|
getOrgMetadata,
|
|
175
|
+
sendEmail,
|
|
159
176
|
updateOrgMetadata,
|
|
160
177
|
verifyToken,
|
|
161
178
|
withAuth
|
package/dist/server.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/server.ts","../src/config.ts"],"sourcesContent":["/**\n * @githat/nextjs/server\n *\n * Server-side utilities for token verification in Next.js API routes and middleware.\n * This module runs on the server only — do not import in client components.\n */\n\nimport * as jose from 'jose';\nimport { DEFAULT_API_URL } from './config';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface AuthPayload {\n userId: string;\n email: string;\n orgId: string | null;\n orgSlug: string | null;\n role: 'owner' | 'admin' | 'member' | null;\n tier: 'free' | 'basic' | 'pro' | 'enterprise' | null;\n}\n\nexport interface VerifyOptions {\n /**\n * Secret key for local JWT verification. If provided, tokens are verified\n * locally without making an API call (~1ms vs ~50-100ms).\n * Must match the JWT_SECRET used by the GitHat backend.\n */\n secretKey?: string;\n\n /**\n * API URL for remote token verification. Defaults to https://api.githat.io\n */\n apiUrl?: string;\n}\n\nexport interface OrgMetadata {\n [key: string]: unknown;\n}\n\n// ============================================================================\n// Cookie Names\n// ============================================================================\n\nexport const COOKIE_NAMES = {\n accessToken: 'githat_access',\n refreshToken: 'githat_refresh',\n} as const;\n\n// ============================================================================\n// Token Verification\n// ============================================================================\n\n/**\n * Verify a JWT token and return the decoded auth payload.\n *\n * Supports two verification modes:\n * 1. Local verification (recommended for performance): Pass `secretKey` option\n * 2. API-based verification: No secret key needed, calls api.githat.io/auth/verify\n *\n * @example Local verification (fast, ~1ms)\n * ```ts\n * const auth = await verifyToken(token, {\n * secretKey: process.env.GITHAT_SECRET_KEY\n * });\n * ```\n *\n * @example API-based verification (simpler setup)\n * ```ts\n * const auth = await verifyToken(token);\n * ```\n */\nexport async function verifyToken(\n token: string,\n options: VerifyOptions = {}\n): Promise<AuthPayload> {\n const { secretKey, apiUrl = DEFAULT_API_URL } = options;\n\n if (secretKey) {\n // Local verification using jose\n return verifyTokenLocally(token, secretKey);\n }\n\n // API-based verification\n return verifyTokenViaAPI(token, apiUrl);\n}\n\n/**\n * Verify token locally using the secret key (no network call)\n */\nasync function verifyTokenLocally(\n token: string,\n secretKey: string\n): Promise<AuthPayload> {\n try {\n const secret = new TextEncoder().encode(secretKey);\n const { payload } = await jose.jwtVerify(token, secret, {\n algorithms: ['HS256'],\n });\n\n // Validate this is a user access token\n if (payload.type && payload.type !== 'user') {\n throw new Error('Invalid token type');\n }\n if (payload.tokenType !== 'access') {\n throw new Error('Invalid token type');\n }\n\n return {\n userId: payload.userId as string,\n email: payload.email as string,\n orgId: (payload.orgId as string) || null,\n orgSlug: (payload.orgSlug as string) || null,\n role: (payload.orgRole as AuthPayload['role']) || null,\n tier: null, // Tier requires DB lookup, not available in local verification\n };\n } catch (err) {\n if (err instanceof jose.errors.JWTExpired) {\n throw new Error('Token expired');\n }\n if (err instanceof jose.errors.JWTClaimValidationFailed) {\n throw new Error('Invalid token claims');\n }\n if (err instanceof jose.errors.JWTInvalid) {\n throw new Error('Invalid token');\n }\n // JWSSignatureVerificationFailed is used for invalid signatures\n if (err instanceof jose.errors.JWSSignatureVerificationFailed) {\n throw new Error('Invalid token signature');\n }\n throw err;\n }\n}\n\n/**\n * Verify token via API call to api.githat.io/auth/verify\n */\nasync function verifyTokenViaAPI(\n token: string,\n apiUrl: string\n): Promise<AuthPayload> {\n const response = await fetch(`${apiUrl}/auth/verify`, {\n method: 'GET',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n });\n\n if (!response.ok) {\n const data = await response.json().catch(() => ({}));\n throw new Error(data.error || 'Token verification failed');\n }\n\n const data = await response.json();\n return {\n userId: data.userId,\n email: data.email,\n orgId: data.orgId,\n orgSlug: data.orgSlug,\n role: data.role,\n tier: data.tier,\n };\n}\n\n// ============================================================================\n// Request Helpers\n// ============================================================================\n\n/**\n * Extract and verify the auth token from a Next.js request.\n * Checks cookies first (for httpOnly cookie mode), then Authorization header.\n *\n * Returns null if no token is found (unauthenticated request).\n * Throws if a token is found but is invalid/expired.\n *\n * @example In a Next.js API route\n * ```ts\n * import { getAuth } from '@githat/nextjs/server';\n *\n * export async function GET(request: Request) {\n * const auth = await getAuth(request, {\n * secretKey: process.env.GITHAT_SECRET_KEY\n * });\n *\n * if (!auth) {\n * return Response.json({ error: 'Unauthorized' }, { status: 401 });\n * }\n *\n * return Response.json({ userId: auth.userId });\n * }\n * ```\n */\nexport async function getAuth(\n request: Request,\n options: VerifyOptions = {}\n): Promise<AuthPayload | null> {\n // Try to get token from cookie first (httpOnly cookie mode)\n const cookieHeader = request.headers.get('cookie');\n let token: string | null = null;\n\n if (cookieHeader) {\n const cookies = parseCookies(cookieHeader);\n token = cookies[COOKIE_NAMES.accessToken] || null;\n }\n\n // Fallback to Authorization header\n if (!token) {\n const authHeader = request.headers.get('authorization');\n if (authHeader?.startsWith('Bearer ')) {\n token = authHeader.slice(7);\n }\n }\n\n // No token found — return null (unauthenticated)\n if (!token) {\n return null;\n }\n\n // Verify the token\n return verifyToken(token, options);\n}\n\n/**\n * Parse cookies from a cookie header string\n */\nfunction parseCookies(cookieHeader: string): Record<string, string> {\n const cookies: Record<string, string> = {};\n const pairs = cookieHeader.split(';');\n\n for (const pair of pairs) {\n const [name, ...rest] = pair.trim().split('=');\n if (name) {\n cookies[name] = rest.join('=');\n }\n }\n\n return cookies;\n}\n\n// ============================================================================\n// Org Metadata (Server-side)\n// ============================================================================\n\n/**\n * Get organization metadata from the server.\n *\n * @example\n * ```ts\n * import { getOrgMetadata } from '@githat/nextjs/server';\n *\n * const meta = await getOrgMetadata(orgId, {\n * token: accessToken,\n * apiUrl: 'https://api.githat.io'\n * });\n * console.log(meta.stripeAccountId);\n * ```\n */\nexport async function getOrgMetadata(\n orgId: string,\n options: { token: string; apiUrl?: string }\n): Promise<OrgMetadata> {\n const { token, apiUrl = DEFAULT_API_URL } = options;\n\n const response = await fetch(`${apiUrl}/orgs/${orgId}/metadata`, {\n method: 'GET',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n });\n\n if (!response.ok) {\n const data = await response.json().catch(() => ({}));\n throw new Error(data.error || 'Failed to get org metadata');\n }\n\n const data = await response.json();\n return data.metadata || {};\n}\n\n/**\n * Update organization metadata from the server.\n *\n * @example\n * ```ts\n * import { updateOrgMetadata } from '@githat/nextjs/server';\n *\n * await updateOrgMetadata(orgId, { stripeAccountId: 'acct_xxx' }, {\n * token: accessToken,\n * apiUrl: 'https://api.githat.io'\n * });\n * ```\n */\nexport async function updateOrgMetadata(\n orgId: string,\n metadata: OrgMetadata,\n options: { token: string; apiUrl?: string }\n): Promise<OrgMetadata> {\n const { token, apiUrl = DEFAULT_API_URL } = options;\n\n const response = await fetch(`${apiUrl}/orgs/${orgId}/metadata`, {\n method: 'PATCH',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(metadata),\n });\n\n if (!response.ok) {\n const data = await response.json().catch(() => ({}));\n throw new Error(data.error || 'Failed to update org metadata');\n }\n\n const data = await response.json();\n return data.metadata || {};\n}\n\n// ============================================================================\n// API Route Helpers\n// ============================================================================\n\n/**\n * Handler function that receives the request and verified auth payload.\n */\nexport type AuthenticatedHandler = (\n request: Request,\n auth: AuthPayload\n) => Promise<Response> | Response;\n\n/**\n * Options for the withAuth wrapper.\n */\nexport interface WithAuthOptions {\n /**\n * Secret key for local JWT verification.\n * Defaults to process.env.GITHAT_SECRET_KEY if not provided.\n */\n secretKey?: string;\n\n /**\n * Custom response to return when authentication fails.\n * Defaults to JSON { error: 'Unauthorized' } with status 401.\n */\n onUnauthorized?: () => Response;\n}\n\n/**\n * Wrap an API route handler with authentication.\n * The handler will only be called if the request has a valid auth token.\n *\n * @example\n * ```ts\n * // app/api/orders/route.ts\n * import { withAuth } from '@githat/nextjs/server';\n *\n * export const GET = withAuth(async (request, auth) => {\n * // auth.userId, auth.orgId, auth.role available\n * const orders = await db.orders.findMany({ where: { orgId: auth.orgId } });\n * return Response.json({ orders });\n * }, { secretKey: process.env.GITHAT_SECRET_KEY });\n * ```\n *\n * @example With custom unauthorized response\n * ```ts\n * export const GET = withAuth(\n * async (request, auth) => {\n * return Response.json({ userId: auth.userId });\n * },\n * {\n * secretKey: process.env.GITHAT_SECRET_KEY,\n * onUnauthorized: () => Response.redirect('/sign-in'),\n * }\n * );\n * ```\n */\nexport function withAuth(\n handler: AuthenticatedHandler,\n options: WithAuthOptions = {}\n): (request: Request) => Promise<Response> {\n return async (request: Request): Promise<Response> => {\n const auth = await getAuth(request, { secretKey: options.secretKey });\n\n if (!auth) {\n if (options.onUnauthorized) {\n return options.onUnauthorized();\n }\n return new Response(JSON.stringify({ error: 'Unauthorized' }), {\n status: 401,\n headers: { 'Content-Type': 'application/json' },\n });\n }\n\n return handler(request, auth);\n };\n}\n","import type { GitHatConfig } from './types';\n\nexport const DEFAULT_API_URL = 'https://api.githat.io';\n\nexport const TOKEN_KEYS = {\n accessToken: 'githat_access_token',\n refreshToken: 'githat_refresh_token',\n user: 'githat_user',\n org: 'githat_org',\n} as const;\n\nexport const COOKIE_NAMES = {\n accessToken: 'githat_access',\n refreshToken: 'githat_refresh',\n} as const;\n\nexport function resolveConfig(config: GitHatConfig): Required<GitHatConfig> {\n return {\n publishableKey: config.publishableKey,\n apiUrl: config.apiUrl || DEFAULT_API_URL,\n signInUrl: config.signInUrl || '/sign-in',\n signUpUrl: config.signUpUrl || '/sign-up',\n afterSignInUrl: config.afterSignInUrl || '/dashboard',\n afterSignOutUrl: config.afterSignOutUrl || '/',\n tokenStorage: config.tokenStorage || 'localStorage',\n };\n}\n"],"mappings":";AAOA,YAAY,UAAU;;;ACLf,IAAM,kBAAkB;;;AD2CxB,IAAM,eAAe;AAAA,EAC1B,aAAa;AAAA,EACb,cAAc;AAChB;AAyBA,eAAsB,YACpB,OACA,UAAyB,CAAC,GACJ;AACtB,QAAM,EAAE,WAAW,SAAS,gBAAgB,IAAI;AAEhD,MAAI,WAAW;AAEb,WAAO,mBAAmB,OAAO,SAAS;AAAA,EAC5C;AAGA,SAAO,kBAAkB,OAAO,MAAM;AACxC;AAKA,eAAe,mBACb,OACA,WACsB;AACtB,MAAI;AACF,UAAM,SAAS,IAAI,YAAY,EAAE,OAAO,SAAS;AACjD,UAAM,EAAE,QAAQ,IAAI,MAAW,eAAU,OAAO,QAAQ;AAAA,MACtD,YAAY,CAAC,OAAO;AAAA,IACtB,CAAC;AAGD,QAAI,QAAQ,QAAQ,QAAQ,SAAS,QAAQ;AAC3C,YAAM,IAAI,MAAM,oBAAoB;AAAA,IACtC;AACA,QAAI,QAAQ,cAAc,UAAU;AAClC,YAAM,IAAI,MAAM,oBAAoB;AAAA,IACtC;AAEA,WAAO;AAAA,MACL,QAAQ,QAAQ;AAAA,MAChB,OAAO,QAAQ;AAAA,MACf,OAAQ,QAAQ,SAAoB;AAAA,MACpC,SAAU,QAAQ,WAAsB;AAAA,MACxC,MAAO,QAAQ,WAAmC;AAAA,MAClD,MAAM;AAAA;AAAA,IACR;AAAA,EACF,SAAS,KAAK;AACZ,QAAI,eAAoB,YAAO,YAAY;AACzC,YAAM,IAAI,MAAM,eAAe;AAAA,IACjC;AACA,QAAI,eAAoB,YAAO,0BAA0B;AACvD,YAAM,IAAI,MAAM,sBAAsB;AAAA,IACxC;AACA,QAAI,eAAoB,YAAO,YAAY;AACzC,YAAM,IAAI,MAAM,eAAe;AAAA,IACjC;AAEA,QAAI,eAAoB,YAAO,gCAAgC;AAC7D,YAAM,IAAI,MAAM,yBAAyB;AAAA,IAC3C;AACA,UAAM;AAAA,EACR;AACF;AAKA,eAAe,kBACb,OACA,QACsB;AACtB,QAAM,WAAW,MAAM,MAAM,GAAG,MAAM,gBAAgB;AAAA,IACpD,QAAQ;AAAA,IACR,SAAS;AAAA,MACP,eAAe,UAAU,KAAK;AAAA,MAC9B,gBAAgB;AAAA,IAClB;AAAA,EACF,CAAC;AAED,MAAI,CAAC,SAAS,IAAI;AAChB,UAAMA,QAAO,MAAM,SAAS,KAAK,EAAE,MAAM,OAAO,CAAC,EAAE;AACnD,UAAM,IAAI,MAAMA,MAAK,SAAS,2BAA2B;AAAA,EAC3D;AAEA,QAAM,OAAO,MAAM,SAAS,KAAK;AACjC,SAAO;AAAA,IACL,QAAQ,KAAK;AAAA,IACb,OAAO,KAAK;AAAA,IACZ,OAAO,KAAK;AAAA,IACZ,SAAS,KAAK;AAAA,IACd,MAAM,KAAK;AAAA,IACX,MAAM,KAAK;AAAA,EACb;AACF;AA8BA,eAAsB,QACpB,SACA,UAAyB,CAAC,GACG;AAE7B,QAAM,eAAe,QAAQ,QAAQ,IAAI,QAAQ;AACjD,MAAI,QAAuB;AAE3B,MAAI,cAAc;AAChB,UAAM,UAAU,aAAa,YAAY;AACzC,YAAQ,QAAQ,aAAa,WAAW,KAAK;AAAA,EAC/C;AAGA,MAAI,CAAC,OAAO;AACV,UAAM,aAAa,QAAQ,QAAQ,IAAI,eAAe;AACtD,QAAI,YAAY,WAAW,SAAS,GAAG;AACrC,cAAQ,WAAW,MAAM,CAAC;AAAA,IAC5B;AAAA,EACF;AAGA,MAAI,CAAC,OAAO;AACV,WAAO;AAAA,EACT;AAGA,SAAO,YAAY,OAAO,OAAO;AACnC;AAKA,SAAS,aAAa,cAA8C;AAClE,QAAM,UAAkC,CAAC;AACzC,QAAM,QAAQ,aAAa,MAAM,GAAG;AAEpC,aAAW,QAAQ,OAAO;AACxB,UAAM,CAAC,MAAM,GAAG,IAAI,IAAI,KAAK,KAAK,EAAE,MAAM,GAAG;AAC7C,QAAI,MAAM;AACR,cAAQ,IAAI,IAAI,KAAK,KAAK,GAAG;AAAA,IAC/B;AAAA,EACF;AAEA,SAAO;AACT;AAoBA,eAAsB,eACpB,OACA,SACsB;AACtB,QAAM,EAAE,OAAO,SAAS,gBAAgB,IAAI;AAE5C,QAAM,WAAW,MAAM,MAAM,GAAG,MAAM,SAAS,KAAK,aAAa;AAAA,IAC/D,QAAQ;AAAA,IACR,SAAS;AAAA,MACP,eAAe,UAAU,KAAK;AAAA,MAC9B,gBAAgB;AAAA,IAClB;AAAA,EACF,CAAC;AAED,MAAI,CAAC,SAAS,IAAI;AAChB,UAAMA,QAAO,MAAM,SAAS,KAAK,EAAE,MAAM,OAAO,CAAC,EAAE;AACnD,UAAM,IAAI,MAAMA,MAAK,SAAS,4BAA4B;AAAA,EAC5D;AAEA,QAAM,OAAO,MAAM,SAAS,KAAK;AACjC,SAAO,KAAK,YAAY,CAAC;AAC3B;AAeA,eAAsB,kBACpB,OACA,UACA,SACsB;AACtB,QAAM,EAAE,OAAO,SAAS,gBAAgB,IAAI;AAE5C,QAAM,WAAW,MAAM,MAAM,GAAG,MAAM,SAAS,KAAK,aAAa;AAAA,IAC/D,QAAQ;AAAA,IACR,SAAS;AAAA,MACP,eAAe,UAAU,KAAK;AAAA,MAC9B,gBAAgB;AAAA,IAClB;AAAA,IACA,MAAM,KAAK,UAAU,QAAQ;AAAA,EAC/B,CAAC;AAED,MAAI,CAAC,SAAS,IAAI;AAChB,UAAMA,QAAO,MAAM,SAAS,KAAK,EAAE,MAAM,OAAO,CAAC,EAAE;AACnD,UAAM,IAAI,MAAMA,MAAK,SAAS,+BAA+B;AAAA,EAC/D;AAEA,QAAM,OAAO,MAAM,SAAS,KAAK;AACjC,SAAO,KAAK,YAAY,CAAC;AAC3B;AA4DO,SAAS,SACd,SACA,UAA2B,CAAC,GACa;AACzC,SAAO,OAAO,YAAwC;AACpD,UAAM,OAAO,MAAM,QAAQ,SAAS,EAAE,WAAW,QAAQ,UAAU,CAAC;AAEpE,QAAI,CAAC,MAAM;AACT,UAAI,QAAQ,gBAAgB;AAC1B,eAAO,QAAQ,eAAe;AAAA,MAChC;AACA,aAAO,IAAI,SAAS,KAAK,UAAU,EAAE,OAAO,eAAe,CAAC,GAAG;AAAA,QAC7D,QAAQ;AAAA,QACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,MAChD,CAAC;AAAA,IACH;AAEA,WAAO,QAAQ,SAAS,IAAI;AAAA,EAC9B;AACF;","names":["data"]}
|
|
1
|
+
{"version":3,"sources":["../src/server.ts","../src/config.ts"],"sourcesContent":["/**\n * @githat/nextjs/server\n *\n * Server-side utilities for token verification in Next.js API routes and middleware.\n * This module runs on the server only — do not import in client components.\n */\n\nimport * as jose from 'jose';\nimport { DEFAULT_API_URL } from './config';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface AuthPayload {\n userId: string;\n email: string;\n orgId: string | null;\n orgSlug: string | null;\n role: 'owner' | 'admin' | 'member' | null;\n tier: 'free' | 'basic' | 'pro' | 'enterprise' | null;\n}\n\nexport interface VerifyOptions {\n /**\n * Secret key for local JWT verification. If provided, tokens are verified\n * locally without making an API call (~1ms vs ~50-100ms).\n * Must match the JWT_SECRET used by the GitHat backend.\n */\n secretKey?: string;\n\n /**\n * API URL for remote token verification. Defaults to https://api.githat.io\n */\n apiUrl?: string;\n}\n\nexport interface OrgMetadata {\n [key: string]: unknown;\n}\n\n// ============================================================================\n// Cookie Names\n// ============================================================================\n\nexport const COOKIE_NAMES = {\n accessToken: 'githat_access',\n refreshToken: 'githat_refresh',\n} as const;\n\n// ============================================================================\n// Token Verification\n// ============================================================================\n\n/**\n * Verify a JWT token and return the decoded auth payload.\n *\n * Supports two verification modes:\n * 1. Local verification (recommended for performance): Pass `secretKey` option\n * 2. API-based verification: No secret key needed, calls api.githat.io/auth/verify\n *\n * @example Local verification (fast, ~1ms)\n * ```ts\n * const auth = await verifyToken(token, {\n * secretKey: process.env.GITHAT_SECRET_KEY\n * });\n * ```\n *\n * @example API-based verification (simpler setup)\n * ```ts\n * const auth = await verifyToken(token);\n * ```\n */\nexport async function verifyToken(\n token: string,\n options: VerifyOptions = {}\n): Promise<AuthPayload> {\n const { secretKey, apiUrl = DEFAULT_API_URL } = options;\n\n if (secretKey) {\n // Local verification using jose\n return verifyTokenLocally(token, secretKey);\n }\n\n // API-based verification\n return verifyTokenViaAPI(token, apiUrl);\n}\n\n/**\n * Verify token locally using the secret key (no network call)\n */\nasync function verifyTokenLocally(\n token: string,\n secretKey: string\n): Promise<AuthPayload> {\n try {\n const secret = new TextEncoder().encode(secretKey);\n const { payload } = await jose.jwtVerify(token, secret, {\n algorithms: ['HS256'],\n });\n\n // Validate this is a user access token\n if (payload.type && payload.type !== 'user') {\n throw new Error('Invalid token type');\n }\n if (payload.tokenType !== 'access') {\n throw new Error('Invalid token type');\n }\n\n return {\n userId: payload.userId as string,\n email: payload.email as string,\n orgId: (payload.orgId as string) || null,\n orgSlug: (payload.orgSlug as string) || null,\n role: (payload.orgRole as AuthPayload['role']) || null,\n tier: null, // Tier requires DB lookup, not available in local verification\n };\n } catch (err) {\n if (err instanceof jose.errors.JWTExpired) {\n throw new Error('Token expired');\n }\n if (err instanceof jose.errors.JWTClaimValidationFailed) {\n throw new Error('Invalid token claims');\n }\n if (err instanceof jose.errors.JWTInvalid) {\n throw new Error('Invalid token');\n }\n // JWSSignatureVerificationFailed is used for invalid signatures\n if (err instanceof jose.errors.JWSSignatureVerificationFailed) {\n throw new Error('Invalid token signature');\n }\n throw err;\n }\n}\n\n/**\n * Verify token via API call to api.githat.io/auth/verify\n */\nasync function verifyTokenViaAPI(\n token: string,\n apiUrl: string\n): Promise<AuthPayload> {\n const response = await fetch(`${apiUrl}/auth/verify`, {\n method: 'GET',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n });\n\n if (!response.ok) {\n const data = await response.json().catch(() => ({}));\n throw new Error(data.error || 'Token verification failed');\n }\n\n const data = await response.json();\n return {\n userId: data.userId,\n email: data.email,\n orgId: data.orgId,\n orgSlug: data.orgSlug,\n role: data.role,\n tier: data.tier,\n };\n}\n\n// ============================================================================\n// Request Helpers\n// ============================================================================\n\n/**\n * Extract and verify the auth token from a Next.js request.\n * Checks cookies first (for httpOnly cookie mode), then Authorization header.\n *\n * Returns null if no token is found (unauthenticated request).\n * Throws if a token is found but is invalid/expired.\n *\n * @example In a Next.js API route\n * ```ts\n * import { getAuth } from '@githat/nextjs/server';\n *\n * export async function GET(request: Request) {\n * const auth = await getAuth(request, {\n * secretKey: process.env.GITHAT_SECRET_KEY\n * });\n *\n * if (!auth) {\n * return Response.json({ error: 'Unauthorized' }, { status: 401 });\n * }\n *\n * return Response.json({ userId: auth.userId });\n * }\n * ```\n */\nexport async function getAuth(\n request: Request,\n options: VerifyOptions = {}\n): Promise<AuthPayload | null> {\n // Try to get token from cookie first (httpOnly cookie mode)\n const cookieHeader = request.headers.get('cookie');\n let token: string | null = null;\n\n if (cookieHeader) {\n const cookies = parseCookies(cookieHeader);\n token = cookies[COOKIE_NAMES.accessToken] || null;\n }\n\n // Fallback to Authorization header\n if (!token) {\n const authHeader = request.headers.get('authorization');\n if (authHeader?.startsWith('Bearer ')) {\n token = authHeader.slice(7);\n }\n }\n\n // No token found — return null (unauthenticated)\n if (!token) {\n return null;\n }\n\n // Verify the token\n return verifyToken(token, options);\n}\n\n/**\n * Parse cookies from a cookie header string\n */\nfunction parseCookies(cookieHeader: string): Record<string, string> {\n const cookies: Record<string, string> = {};\n const pairs = cookieHeader.split(';');\n\n for (const pair of pairs) {\n const [name, ...rest] = pair.trim().split('=');\n if (name) {\n cookies[name] = rest.join('=');\n }\n }\n\n return cookies;\n}\n\n// ============================================================================\n// Org Metadata (Server-side)\n// ============================================================================\n\n/**\n * Get organization metadata from the server.\n *\n * @example\n * ```ts\n * import { getOrgMetadata } from '@githat/nextjs/server';\n *\n * const meta = await getOrgMetadata(orgId, {\n * token: accessToken,\n * apiUrl: 'https://api.githat.io'\n * });\n * console.log(meta.stripeAccountId);\n * ```\n */\nexport async function getOrgMetadata(\n orgId: string,\n options: { token: string; apiUrl?: string }\n): Promise<OrgMetadata> {\n const { token, apiUrl = DEFAULT_API_URL } = options;\n\n const response = await fetch(`${apiUrl}/orgs/${orgId}/metadata`, {\n method: 'GET',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n });\n\n if (!response.ok) {\n const data = await response.json().catch(() => ({}));\n throw new Error(data.error || 'Failed to get org metadata');\n }\n\n const data = await response.json();\n return data.metadata || {};\n}\n\n/**\n * Update organization metadata from the server.\n *\n * @example\n * ```ts\n * import { updateOrgMetadata } from '@githat/nextjs/server';\n *\n * await updateOrgMetadata(orgId, { stripeAccountId: 'acct_xxx' }, {\n * token: accessToken,\n * apiUrl: 'https://api.githat.io'\n * });\n * ```\n */\nexport async function updateOrgMetadata(\n orgId: string,\n metadata: OrgMetadata,\n options: { token: string; apiUrl?: string }\n): Promise<OrgMetadata> {\n const { token, apiUrl = DEFAULT_API_URL } = options;\n\n const response = await fetch(`${apiUrl}/orgs/${orgId}/metadata`, {\n method: 'PATCH',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(metadata),\n });\n\n if (!response.ok) {\n const data = await response.json().catch(() => ({}));\n throw new Error(data.error || 'Failed to update org metadata');\n }\n\n const data = await response.json();\n return data.metadata || {};\n}\n\n// ============================================================================\n// API Route Helpers\n// ============================================================================\n\n/**\n * Handler function that receives the request and verified auth payload.\n */\nexport type AuthenticatedHandler = (\n request: Request,\n auth: AuthPayload\n) => Promise<Response> | Response;\n\n/**\n * Options for the withAuth wrapper.\n */\nexport interface WithAuthOptions {\n /**\n * Secret key for local JWT verification.\n * Defaults to process.env.GITHAT_SECRET_KEY if not provided.\n */\n secretKey?: string;\n\n /**\n * Custom response to return when authentication fails.\n * Defaults to JSON { error: 'Unauthorized' } with status 401.\n */\n onUnauthorized?: () => Response;\n}\n\n/**\n * Wrap an API route handler with authentication.\n * The handler will only be called if the request has a valid auth token.\n *\n * @example\n * ```ts\n * // app/api/orders/route.ts\n * import { withAuth } from '@githat/nextjs/server';\n *\n * export const GET = withAuth(async (request, auth) => {\n * // auth.userId, auth.orgId, auth.role available\n * const orders = await db.orders.findMany({ where: { orgId: auth.orgId } });\n * return Response.json({ orders });\n * }, { secretKey: process.env.GITHAT_SECRET_KEY });\n * ```\n *\n * @example With custom unauthorized response\n * ```ts\n * export const GET = withAuth(\n * async (request, auth) => {\n * return Response.json({ userId: auth.userId });\n * },\n * {\n * secretKey: process.env.GITHAT_SECRET_KEY,\n * onUnauthorized: () => Response.redirect('/sign-in'),\n * }\n * );\n * ```\n */\nexport function withAuth(\n handler: AuthenticatedHandler,\n options: WithAuthOptions = {}\n): (request: Request) => Promise<Response> {\n return async (request: Request): Promise<Response> => {\n const auth = await getAuth(request, { secretKey: options.secretKey });\n\n if (!auth) {\n if (options.onUnauthorized) {\n return options.onUnauthorized();\n }\n return new Response(JSON.stringify({ error: 'Unauthorized' }), {\n status: 401,\n headers: { 'Content-Type': 'application/json' },\n });\n }\n\n return handler(request, auth);\n };\n}\n\n// ============================================================================\n// Email Sending (Server-side)\n// ============================================================================\n\nexport interface ServerSendEmailOptions {\n /** Recipient email address(es). */\n to: string | string[];\n /** Email subject line. */\n subject: string;\n /** HTML body. */\n html?: string;\n /** Plain text body. */\n text?: string;\n /** Reply-to email address. */\n replyTo?: string;\n}\n\nexport interface ServerSendEmailResult {\n messageId: string;\n to: string[];\n subject: string;\n sent: boolean;\n}\n\n/**\n * Send a transactional email from the server.\n * Use this in Next.js API routes, server actions, or any server-side code.\n *\n * @example\n * ```ts\n * import { sendEmail } from '@githat/nextjs/server';\n *\n * const result = await sendEmail({\n * to: 'user@example.com',\n * subject: 'Welcome!',\n * html: '<h1>Welcome aboard</h1>',\n * replyTo: 'support@myapp.com'\n * }, {\n * token: accessToken\n * });\n * ```\n */\nexport async function sendEmail(\n options: ServerSendEmailOptions,\n config: { token: string; apiUrl?: string }\n): Promise<ServerSendEmailResult> {\n const { token, apiUrl = DEFAULT_API_URL } = config;\n\n const response = await fetch(`${apiUrl}/email/send`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(options),\n });\n\n if (!response.ok) {\n const data = await response.json().catch(() => ({}));\n throw new Error(data.error || 'Failed to send email');\n }\n\n return response.json();\n}\n","import type { GitHatConfig } from './types';\n\nexport const DEFAULT_API_URL = 'https://api.githat.io';\n\nexport const TOKEN_KEYS = {\n accessToken: 'githat_access_token',\n refreshToken: 'githat_refresh_token',\n user: 'githat_user',\n org: 'githat_org',\n} as const;\n\nexport const COOKIE_NAMES = {\n accessToken: 'githat_access',\n refreshToken: 'githat_refresh',\n} as const;\n\nexport function resolveConfig(config: GitHatConfig): Required<GitHatConfig> {\n return {\n publishableKey: config.publishableKey,\n apiUrl: config.apiUrl || DEFAULT_API_URL,\n signInUrl: config.signInUrl || '/sign-in',\n signUpUrl: config.signUpUrl || '/sign-up',\n afterSignInUrl: config.afterSignInUrl || '/dashboard',\n afterSignOutUrl: config.afterSignOutUrl || '/',\n tokenStorage: config.tokenStorage || 'localStorage',\n };\n}\n"],"mappings":"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","names":["data"]}
|