@guidekit/server 0.1.0-beta.1

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/README.md ADDED
@@ -0,0 +1,68 @@
1
+ # @guidekit/server
2
+
3
+ Server-side utilities for the GuideKit SDK. Provides secure token generation and validation so API keys never reach the browser.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @guidekit/server
9
+ ```
10
+
11
+ ## Token Generation
12
+
13
+ Create a token endpoint that the client SDK calls to obtain a short-lived session token.
14
+
15
+ ```typescript
16
+ import { createSessionToken } from '@guidekit/server';
17
+
18
+ // Next.js App Router example
19
+ export async function POST() {
20
+ const token = await createSessionToken({
21
+ signingSecret: process.env.GUIDEKIT_SECRET!,
22
+ geminiKey: process.env.GEMINI_KEY!,
23
+ deepgramKey: process.env.DEEPGRAM_KEY,
24
+ elevenlabsKey: process.env.ELEVENLABS_KEY,
25
+ expiresIn: '15m',
26
+ });
27
+
28
+ return Response.json(token);
29
+ }
30
+ ```
31
+
32
+ ## Token Validation
33
+
34
+ Validate an incoming token (useful for custom middleware):
35
+
36
+ ```typescript
37
+ import { validateSessionToken } from '@guidekit/server';
38
+
39
+ const payload = await validateSessionToken({
40
+ token: bearerToken,
41
+ signingSecret: process.env.GUIDEKIT_SECRET!,
42
+ });
43
+ ```
44
+
45
+ ## Secret Rotation
46
+
47
+ To rotate your signing secret with zero downtime, temporarily accept both old and new secrets:
48
+
49
+ ```typescript
50
+ const token = await createSessionToken({
51
+ signingSecret: process.env.GUIDEKIT_SECRET_NEW!,
52
+ // ...
53
+ });
54
+ ```
55
+
56
+ Generate a new secret via the CLI:
57
+
58
+ ```bash
59
+ npx guidekit generate-secret
60
+ ```
61
+
62
+ ## Documentation
63
+
64
+ Full documentation: [guidekit.dev/docs/server](https://guidekit.dev/docs/server)
65
+
66
+ ## License
67
+
68
+ [MIT](../../LICENSE)
package/dist/index.cjs ADDED
@@ -0,0 +1,183 @@
1
+ 'use strict';
2
+
3
+ var jose = require('jose');
4
+
5
+ // src/index.ts
6
+ var sessionKeyStore = /* @__PURE__ */ new Map();
7
+ function getSessionKeys(sessionId) {
8
+ return sessionKeyStore.get(sessionId);
9
+ }
10
+ function clearSessionKeys(sessionId) {
11
+ return sessionKeyStore.delete(sessionId);
12
+ }
13
+ function parseDuration(duration) {
14
+ if (typeof duration !== "string" || duration.trim().length === 0) {
15
+ throw new Error('Duration must be a non-empty string (e.g. "15m", "1h", "30s").');
16
+ }
17
+ const cleaned = duration.trim().toLowerCase();
18
+ const regex = /(\d+)\s*(d|h|m|s)/g;
19
+ let match;
20
+ let totalSeconds = 0;
21
+ let matched = false;
22
+ while ((match = regex.exec(cleaned)) !== null) {
23
+ matched = true;
24
+ const value = parseInt(match[1], 10);
25
+ const unit = match[2];
26
+ switch (unit) {
27
+ case "s":
28
+ totalSeconds += value;
29
+ break;
30
+ case "m":
31
+ totalSeconds += value * 60;
32
+ break;
33
+ case "h":
34
+ totalSeconds += value * 3600;
35
+ break;
36
+ case "d":
37
+ totalSeconds += value * 86400;
38
+ break;
39
+ }
40
+ }
41
+ if (!matched) {
42
+ throw new Error(
43
+ `Invalid duration format: "${duration}". Expected a string like "15m", "1h", "30s", "7d", or compound "2h30m".`
44
+ );
45
+ }
46
+ if (totalSeconds <= 0) {
47
+ throw new Error("Duration must resolve to a positive number of seconds.");
48
+ }
49
+ return totalSeconds;
50
+ }
51
+ function encodeSecret(secret) {
52
+ return new TextEncoder().encode(secret);
53
+ }
54
+ function resolveSigningSecret(signingSecret) {
55
+ if (Array.isArray(signingSecret)) {
56
+ if (signingSecret.length === 0) {
57
+ throw new Error("signingSecret array must contain at least one secret.");
58
+ }
59
+ return signingSecret[0];
60
+ }
61
+ if (typeof signingSecret !== "string" || signingSecret.length === 0) {
62
+ throw new Error("signingSecret must be a non-empty string or a non-empty array of strings.");
63
+ }
64
+ return signingSecret;
65
+ }
66
+ function resolveAllSecrets(signingSecret) {
67
+ if (Array.isArray(signingSecret)) {
68
+ if (signingSecret.length === 0) {
69
+ throw new Error("signingSecret array must contain at least one secret.");
70
+ }
71
+ return signingSecret;
72
+ }
73
+ if (typeof signingSecret !== "string" || signingSecret.length === 0) {
74
+ throw new Error("signingSecret must be a non-empty string or a non-empty array of strings.");
75
+ }
76
+ return [signingSecret];
77
+ }
78
+ async function createSessionToken(options) {
79
+ const {
80
+ signingSecret,
81
+ deepgramKey,
82
+ elevenlabsKey,
83
+ geminiKey,
84
+ expiresIn = "15m",
85
+ allowedOrigins,
86
+ permissions = ["stt", "tts", "llm"],
87
+ userId,
88
+ sessionId = crypto.randomUUID(),
89
+ metadata
90
+ } = options;
91
+ const secret = resolveSigningSecret(signingSecret);
92
+ const lifetimeSeconds = parseDuration(expiresIn);
93
+ const now = Math.floor(Date.now() / 1e3);
94
+ const expiresAt = now + lifetimeSeconds;
95
+ const jwtPayload = {
96
+ sessionId,
97
+ permissions
98
+ };
99
+ if (userId !== void 0) {
100
+ jwtPayload.userId = userId;
101
+ }
102
+ if (metadata !== void 0 && Object.keys(metadata).length > 0) {
103
+ jwtPayload.metadata = metadata;
104
+ }
105
+ if (allowedOrigins !== void 0 && allowedOrigins.length > 0) {
106
+ jwtPayload.aud = allowedOrigins;
107
+ }
108
+ const token = await new jose.SignJWT(jwtPayload).setProtectedHeader({ alg: "HS256" }).setIssuedAt(now).setExpirationTime(expiresAt).sign(encodeSecret(secret));
109
+ const providerKeys = {};
110
+ if (deepgramKey) providerKeys.deepgramKey = deepgramKey;
111
+ if (elevenlabsKey) providerKeys.elevenlabsKey = elevenlabsKey;
112
+ if (geminiKey) providerKeys.geminiKey = geminiKey;
113
+ if (Object.keys(providerKeys).length > 0) {
114
+ sessionKeyStore.set(sessionId, providerKeys);
115
+ }
116
+ return {
117
+ token,
118
+ expiresIn: lifetimeSeconds,
119
+ expiresAt
120
+ };
121
+ }
122
+ async function validateSessionToken(token, signingSecret, options) {
123
+ if (typeof token !== "string" || token.trim().length === 0) {
124
+ return { valid: false, error: "Token must be a non-empty string." };
125
+ }
126
+ const secrets = resolveAllSecrets(signingSecret);
127
+ let lastError;
128
+ for (const secret of secrets) {
129
+ try {
130
+ const verifyOptions = {
131
+ algorithms: ["HS256"]
132
+ };
133
+ if (options?.audience) {
134
+ verifyOptions.audience = options.audience;
135
+ }
136
+ const { payload } = await jose.jwtVerify(token, encodeSecret(secret), verifyOptions);
137
+ const tokenPayload = {
138
+ sessionId: payload.sessionId,
139
+ expiresAt: payload.exp,
140
+ audience: normalizeAudience(payload.aud),
141
+ permissions: payload.permissions ?? [],
142
+ iat: payload.iat
143
+ };
144
+ if (payload.userId !== void 0) {
145
+ tokenPayload.userId = payload.userId;
146
+ }
147
+ if (payload.metadata !== void 0) {
148
+ tokenPayload.metadata = payload.metadata;
149
+ }
150
+ return { valid: true, payload: tokenPayload };
151
+ } catch (err) {
152
+ lastError = err;
153
+ }
154
+ }
155
+ const message = lastError instanceof Error ? lastError.message : "Token validation failed.";
156
+ return { valid: false, error: message };
157
+ }
158
+ function normalizeAudience(aud) {
159
+ if (aud === void 0 || aud === null) return [];
160
+ if (typeof aud === "string") return [aud];
161
+ if (Array.isArray(aud)) return aud.filter((a) => typeof a === "string");
162
+ return [];
163
+ }
164
+ function generateSecret() {
165
+ const bytes = new Uint8Array(32);
166
+ crypto.getRandomValues(bytes);
167
+ return base64UrlEncode(bytes);
168
+ }
169
+ function base64UrlEncode(bytes) {
170
+ let binary = "";
171
+ for (let i = 0; i < bytes.length; i++) {
172
+ binary += String.fromCharCode(bytes[i]);
173
+ }
174
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
175
+ }
176
+
177
+ exports.clearSessionKeys = clearSessionKeys;
178
+ exports.createSessionToken = createSessionToken;
179
+ exports.generateSecret = generateSecret;
180
+ exports.getSessionKeys = getSessionKeys;
181
+ exports.validateSessionToken = validateSessionToken;
182
+ //# sourceMappingURL=index.cjs.map
183
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"names":["SignJWT","jwtVerify"],"mappings":"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","file":"index.cjs","sourcesContent":["import { SignJWT, jwtVerify, type JWTPayload } from 'jose';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface TokenPayload {\n sessionId: string;\n expiresAt: number;\n audience: string[];\n permissions: string[];\n userId?: string;\n metadata?: Record<string, unknown>;\n iat: number;\n}\n\nexport interface CreateSessionTokenOptions {\n signingSecret: string | string[];\n deepgramKey?: string;\n elevenlabsKey?: string;\n geminiKey?: string;\n expiresIn?: string;\n allowedOrigins?: string[];\n permissions?: string[];\n userId?: string;\n sessionId?: string;\n metadata?: Record<string, unknown>;\n}\n\nexport interface CreateSessionTokenResult {\n token: string;\n expiresIn: number;\n expiresAt: number;\n}\n\nexport interface ValidateSessionTokenResult {\n valid: boolean;\n payload?: TokenPayload;\n error?: string;\n}\n\nexport interface ValidateSessionTokenOptions {\n audience?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Server-side provider key store\n// ---------------------------------------------------------------------------\n\ninterface ProviderKeys {\n deepgramKey?: string;\n elevenlabsKey?: string;\n geminiKey?: string;\n}\n\n/**\n * In-memory store mapping sessionId -> provider API keys.\n * Provider keys are NEVER placed in the JWT; they stay server-side only.\n */\nconst sessionKeyStore = new Map<string, ProviderKeys>();\n\n/**\n * Retrieve provider API keys associated with a session.\n * Returns `undefined` if the session is not found.\n */\nexport function getSessionKeys(sessionId: string): ProviderKeys | undefined {\n return sessionKeyStore.get(sessionId);\n}\n\n/**\n * Remove provider API keys for a session (e.g. on expiry or logout).\n */\nexport function clearSessionKeys(sessionId: string): boolean {\n return sessionKeyStore.delete(sessionId);\n}\n\n// ---------------------------------------------------------------------------\n// Duration parsing\n// ---------------------------------------------------------------------------\n\n/**\n * Parse a human-readable duration string into seconds.\n *\n * Supported units:\n * - `s` seconds\n * - `m` minutes\n * - `h` hours\n * - `d` days\n *\n * Compound durations are supported (e.g. `2h30m`, `1d12h`).\n *\n * @throws {Error} If the duration string is empty or contains no valid segments.\n */\nfunction parseDuration(duration: string): number {\n if (typeof duration !== 'string' || duration.trim().length === 0) {\n throw new Error('Duration must be a non-empty string (e.g. \"15m\", \"1h\", \"30s\").');\n }\n\n const cleaned = duration.trim().toLowerCase();\n const regex = /(\\d+)\\s*(d|h|m|s)/g;\n let match: RegExpExecArray | null;\n let totalSeconds = 0;\n let matched = false;\n\n while ((match = regex.exec(cleaned)) !== null) {\n matched = true;\n const value = parseInt(match[1]!, 10);\n const unit = match[2]!;\n\n switch (unit) {\n case 's':\n totalSeconds += value;\n break;\n case 'm':\n totalSeconds += value * 60;\n break;\n case 'h':\n totalSeconds += value * 3600;\n break;\n case 'd':\n totalSeconds += value * 86400;\n break;\n }\n }\n\n if (!matched) {\n throw new Error(\n `Invalid duration format: \"${duration}\". ` +\n 'Expected a string like \"15m\", \"1h\", \"30s\", \"7d\", or compound \"2h30m\".',\n );\n }\n\n if (totalSeconds <= 0) {\n throw new Error('Duration must resolve to a positive number of seconds.');\n }\n\n return totalSeconds;\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Encode a signing secret string into a Uint8Array suitable for HMAC operations.\n */\nfunction encodeSecret(secret: string): Uint8Array {\n return new TextEncoder().encode(secret);\n}\n\n/**\n * Resolve the signing secret to use.\n *\n * - If a single string is provided, it is used directly.\n * - If an array is provided, the **first** element (newest) is used for signing.\n *\n * @throws {Error} If no signing secret is provided or the array is empty.\n */\nfunction resolveSigningSecret(signingSecret: string | string[]): string {\n if (Array.isArray(signingSecret)) {\n if (signingSecret.length === 0) {\n throw new Error('signingSecret array must contain at least one secret.');\n }\n return signingSecret[0]!;\n }\n\n if (typeof signingSecret !== 'string' || signingSecret.length === 0) {\n throw new Error('signingSecret must be a non-empty string or a non-empty array of strings.');\n }\n\n return signingSecret;\n}\n\n/**\n * Flatten signingSecret into an array for rotation-aware verification.\n */\nfunction resolveAllSecrets(signingSecret: string | string[]): string[] {\n if (Array.isArray(signingSecret)) {\n if (signingSecret.length === 0) {\n throw new Error('signingSecret array must contain at least one secret.');\n }\n return signingSecret;\n }\n\n if (typeof signingSecret !== 'string' || signingSecret.length === 0) {\n throw new Error('signingSecret must be a non-empty string or a non-empty array of strings.');\n }\n\n return [signingSecret];\n}\n\n// ---------------------------------------------------------------------------\n// createSessionToken\n// ---------------------------------------------------------------------------\n\n/**\n * Create a signed session token (JWT) for use with GuideKit client SDKs.\n *\n * Provider API keys (`deepgramKey`, `elevenlabsKey`, `geminiKey`) are\n * **never** embedded in the JWT. They are stored in a server-side in-memory\n * map keyed by `sessionId` and can be retrieved via {@link getSessionKeys}.\n */\nexport async function createSessionToken(\n options: CreateSessionTokenOptions,\n): Promise<CreateSessionTokenResult> {\n const {\n signingSecret,\n deepgramKey,\n elevenlabsKey,\n geminiKey,\n expiresIn = '15m',\n allowedOrigins,\n permissions = ['stt', 'tts', 'llm'],\n userId,\n sessionId = crypto.randomUUID(),\n metadata,\n } = options;\n\n // Resolve the secret to sign with (first / newest).\n const secret = resolveSigningSecret(signingSecret);\n\n // Parse the requested lifetime.\n const lifetimeSeconds = parseDuration(expiresIn);\n\n // Build the JWT payload — provider keys are intentionally excluded.\n const now = Math.floor(Date.now() / 1000);\n const expiresAt = now + lifetimeSeconds;\n\n const jwtPayload: JWTPayload & {\n sessionId: string;\n permissions: string[];\n userId?: string;\n metadata?: Record<string, unknown>;\n } = {\n sessionId,\n permissions,\n };\n\n if (userId !== undefined) {\n jwtPayload.userId = userId;\n }\n\n if (metadata !== undefined && Object.keys(metadata).length > 0) {\n jwtPayload.metadata = metadata;\n }\n\n if (allowedOrigins !== undefined && allowedOrigins.length > 0) {\n jwtPayload.aud = allowedOrigins;\n }\n\n const token = await new SignJWT(jwtPayload)\n .setProtectedHeader({ alg: 'HS256' })\n .setIssuedAt(now)\n .setExpirationTime(expiresAt)\n .sign(encodeSecret(secret));\n\n // Store provider keys server-side, keyed by sessionId.\n const providerKeys: ProviderKeys = {};\n if (deepgramKey) providerKeys.deepgramKey = deepgramKey;\n if (elevenlabsKey) providerKeys.elevenlabsKey = elevenlabsKey;\n if (geminiKey) providerKeys.geminiKey = geminiKey;\n\n if (Object.keys(providerKeys).length > 0) {\n sessionKeyStore.set(sessionId, providerKeys);\n }\n\n return {\n token,\n expiresIn: lifetimeSeconds,\n expiresAt,\n };\n}\n\n// ---------------------------------------------------------------------------\n// validateSessionToken\n// ---------------------------------------------------------------------------\n\n/**\n * Validate and decode a GuideKit session token.\n *\n * When `signingSecret` is an array, the function tries each secret in order\n * to support zero-downtime key rotation. Verification succeeds as soon as\n * any secret produces a valid result.\n */\nexport async function validateSessionToken(\n token: string,\n signingSecret: string | string[],\n options?: ValidateSessionTokenOptions,\n): Promise<ValidateSessionTokenResult> {\n if (typeof token !== 'string' || token.trim().length === 0) {\n return { valid: false, error: 'Token must be a non-empty string.' };\n }\n\n const secrets = resolveAllSecrets(signingSecret);\n let lastError: unknown;\n\n for (const secret of secrets) {\n try {\n const verifyOptions: Parameters<typeof jwtVerify>[2] = {\n algorithms: ['HS256'],\n };\n\n if (options?.audience) {\n verifyOptions.audience = options.audience;\n }\n\n const { payload } = await jwtVerify(token, encodeSecret(secret), verifyOptions);\n\n const tokenPayload: TokenPayload = {\n sessionId: payload.sessionId as string,\n expiresAt: payload.exp as number,\n audience: normalizeAudience(payload.aud),\n permissions: (payload.permissions as string[]) ?? [],\n iat: payload.iat as number,\n };\n\n if (payload.userId !== undefined) {\n tokenPayload.userId = payload.userId as string;\n }\n\n if (payload.metadata !== undefined) {\n tokenPayload.metadata = payload.metadata as Record<string, unknown>;\n }\n\n return { valid: true, payload: tokenPayload };\n } catch (err) {\n lastError = err;\n // Continue to next secret — it may have been rotated.\n }\n }\n\n // All secrets failed.\n const message =\n lastError instanceof Error ? lastError.message : 'Token validation failed.';\n return { valid: false, error: message };\n}\n\n/**\n * Normalize the JWT `aud` claim into a string array.\n */\nfunction normalizeAudience(aud: unknown): string[] {\n if (aud === undefined || aud === null) return [];\n if (typeof aud === 'string') return [aud];\n if (Array.isArray(aud)) return aud.filter((a): a is string => typeof a === 'string');\n return [];\n}\n\n// ---------------------------------------------------------------------------\n// generateSecret\n// ---------------------------------------------------------------------------\n\n/**\n * Generate a cryptographically random 256-bit (32-byte) base64url-encoded\n * secret suitable for use as a GuideKit signing secret.\n *\n * Usage: `npx guidekit generate-secret`\n */\nexport function generateSecret(): string {\n const bytes = new Uint8Array(32);\n crypto.getRandomValues(bytes);\n return base64UrlEncode(bytes);\n}\n\n/**\n * Base64url-encode a Uint8Array without padding, per RFC 4648 section 5.\n */\nfunction base64UrlEncode(bytes: Uint8Array): string {\n let binary = '';\n for (let i = 0; i < bytes.length; i++) {\n binary += String.fromCharCode(bytes[i]!);\n }\n return btoa(binary).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');\n}\n"]}
@@ -0,0 +1,73 @@
1
+ interface TokenPayload {
2
+ sessionId: string;
3
+ expiresAt: number;
4
+ audience: string[];
5
+ permissions: string[];
6
+ userId?: string;
7
+ metadata?: Record<string, unknown>;
8
+ iat: number;
9
+ }
10
+ interface CreateSessionTokenOptions {
11
+ signingSecret: string | string[];
12
+ deepgramKey?: string;
13
+ elevenlabsKey?: string;
14
+ geminiKey?: string;
15
+ expiresIn?: string;
16
+ allowedOrigins?: string[];
17
+ permissions?: string[];
18
+ userId?: string;
19
+ sessionId?: string;
20
+ metadata?: Record<string, unknown>;
21
+ }
22
+ interface CreateSessionTokenResult {
23
+ token: string;
24
+ expiresIn: number;
25
+ expiresAt: number;
26
+ }
27
+ interface ValidateSessionTokenResult {
28
+ valid: boolean;
29
+ payload?: TokenPayload;
30
+ error?: string;
31
+ }
32
+ interface ValidateSessionTokenOptions {
33
+ audience?: string;
34
+ }
35
+ interface ProviderKeys {
36
+ deepgramKey?: string;
37
+ elevenlabsKey?: string;
38
+ geminiKey?: string;
39
+ }
40
+ /**
41
+ * Retrieve provider API keys associated with a session.
42
+ * Returns `undefined` if the session is not found.
43
+ */
44
+ declare function getSessionKeys(sessionId: string): ProviderKeys | undefined;
45
+ /**
46
+ * Remove provider API keys for a session (e.g. on expiry or logout).
47
+ */
48
+ declare function clearSessionKeys(sessionId: string): boolean;
49
+ /**
50
+ * Create a signed session token (JWT) for use with GuideKit client SDKs.
51
+ *
52
+ * Provider API keys (`deepgramKey`, `elevenlabsKey`, `geminiKey`) are
53
+ * **never** embedded in the JWT. They are stored in a server-side in-memory
54
+ * map keyed by `sessionId` and can be retrieved via {@link getSessionKeys}.
55
+ */
56
+ declare function createSessionToken(options: CreateSessionTokenOptions): Promise<CreateSessionTokenResult>;
57
+ /**
58
+ * Validate and decode a GuideKit session token.
59
+ *
60
+ * When `signingSecret` is an array, the function tries each secret in order
61
+ * to support zero-downtime key rotation. Verification succeeds as soon as
62
+ * any secret produces a valid result.
63
+ */
64
+ declare function validateSessionToken(token: string, signingSecret: string | string[], options?: ValidateSessionTokenOptions): Promise<ValidateSessionTokenResult>;
65
+ /**
66
+ * Generate a cryptographically random 256-bit (32-byte) base64url-encoded
67
+ * secret suitable for use as a GuideKit signing secret.
68
+ *
69
+ * Usage: `npx guidekit generate-secret`
70
+ */
71
+ declare function generateSecret(): string;
72
+
73
+ export { type CreateSessionTokenOptions, type CreateSessionTokenResult, type TokenPayload, type ValidateSessionTokenOptions, type ValidateSessionTokenResult, clearSessionKeys, createSessionToken, generateSecret, getSessionKeys, validateSessionToken };
@@ -0,0 +1,73 @@
1
+ interface TokenPayload {
2
+ sessionId: string;
3
+ expiresAt: number;
4
+ audience: string[];
5
+ permissions: string[];
6
+ userId?: string;
7
+ metadata?: Record<string, unknown>;
8
+ iat: number;
9
+ }
10
+ interface CreateSessionTokenOptions {
11
+ signingSecret: string | string[];
12
+ deepgramKey?: string;
13
+ elevenlabsKey?: string;
14
+ geminiKey?: string;
15
+ expiresIn?: string;
16
+ allowedOrigins?: string[];
17
+ permissions?: string[];
18
+ userId?: string;
19
+ sessionId?: string;
20
+ metadata?: Record<string, unknown>;
21
+ }
22
+ interface CreateSessionTokenResult {
23
+ token: string;
24
+ expiresIn: number;
25
+ expiresAt: number;
26
+ }
27
+ interface ValidateSessionTokenResult {
28
+ valid: boolean;
29
+ payload?: TokenPayload;
30
+ error?: string;
31
+ }
32
+ interface ValidateSessionTokenOptions {
33
+ audience?: string;
34
+ }
35
+ interface ProviderKeys {
36
+ deepgramKey?: string;
37
+ elevenlabsKey?: string;
38
+ geminiKey?: string;
39
+ }
40
+ /**
41
+ * Retrieve provider API keys associated with a session.
42
+ * Returns `undefined` if the session is not found.
43
+ */
44
+ declare function getSessionKeys(sessionId: string): ProviderKeys | undefined;
45
+ /**
46
+ * Remove provider API keys for a session (e.g. on expiry or logout).
47
+ */
48
+ declare function clearSessionKeys(sessionId: string): boolean;
49
+ /**
50
+ * Create a signed session token (JWT) for use with GuideKit client SDKs.
51
+ *
52
+ * Provider API keys (`deepgramKey`, `elevenlabsKey`, `geminiKey`) are
53
+ * **never** embedded in the JWT. They are stored in a server-side in-memory
54
+ * map keyed by `sessionId` and can be retrieved via {@link getSessionKeys}.
55
+ */
56
+ declare function createSessionToken(options: CreateSessionTokenOptions): Promise<CreateSessionTokenResult>;
57
+ /**
58
+ * Validate and decode a GuideKit session token.
59
+ *
60
+ * When `signingSecret` is an array, the function tries each secret in order
61
+ * to support zero-downtime key rotation. Verification succeeds as soon as
62
+ * any secret produces a valid result.
63
+ */
64
+ declare function validateSessionToken(token: string, signingSecret: string | string[], options?: ValidateSessionTokenOptions): Promise<ValidateSessionTokenResult>;
65
+ /**
66
+ * Generate a cryptographically random 256-bit (32-byte) base64url-encoded
67
+ * secret suitable for use as a GuideKit signing secret.
68
+ *
69
+ * Usage: `npx guidekit generate-secret`
70
+ */
71
+ declare function generateSecret(): string;
72
+
73
+ export { type CreateSessionTokenOptions, type CreateSessionTokenResult, type TokenPayload, type ValidateSessionTokenOptions, type ValidateSessionTokenResult, clearSessionKeys, createSessionToken, generateSecret, getSessionKeys, validateSessionToken };
package/dist/index.js ADDED
@@ -0,0 +1,177 @@
1
+ import { SignJWT, jwtVerify } from 'jose';
2
+
3
+ // src/index.ts
4
+ var sessionKeyStore = /* @__PURE__ */ new Map();
5
+ function getSessionKeys(sessionId) {
6
+ return sessionKeyStore.get(sessionId);
7
+ }
8
+ function clearSessionKeys(sessionId) {
9
+ return sessionKeyStore.delete(sessionId);
10
+ }
11
+ function parseDuration(duration) {
12
+ if (typeof duration !== "string" || duration.trim().length === 0) {
13
+ throw new Error('Duration must be a non-empty string (e.g. "15m", "1h", "30s").');
14
+ }
15
+ const cleaned = duration.trim().toLowerCase();
16
+ const regex = /(\d+)\s*(d|h|m|s)/g;
17
+ let match;
18
+ let totalSeconds = 0;
19
+ let matched = false;
20
+ while ((match = regex.exec(cleaned)) !== null) {
21
+ matched = true;
22
+ const value = parseInt(match[1], 10);
23
+ const unit = match[2];
24
+ switch (unit) {
25
+ case "s":
26
+ totalSeconds += value;
27
+ break;
28
+ case "m":
29
+ totalSeconds += value * 60;
30
+ break;
31
+ case "h":
32
+ totalSeconds += value * 3600;
33
+ break;
34
+ case "d":
35
+ totalSeconds += value * 86400;
36
+ break;
37
+ }
38
+ }
39
+ if (!matched) {
40
+ throw new Error(
41
+ `Invalid duration format: "${duration}". Expected a string like "15m", "1h", "30s", "7d", or compound "2h30m".`
42
+ );
43
+ }
44
+ if (totalSeconds <= 0) {
45
+ throw new Error("Duration must resolve to a positive number of seconds.");
46
+ }
47
+ return totalSeconds;
48
+ }
49
+ function encodeSecret(secret) {
50
+ return new TextEncoder().encode(secret);
51
+ }
52
+ function resolveSigningSecret(signingSecret) {
53
+ if (Array.isArray(signingSecret)) {
54
+ if (signingSecret.length === 0) {
55
+ throw new Error("signingSecret array must contain at least one secret.");
56
+ }
57
+ return signingSecret[0];
58
+ }
59
+ if (typeof signingSecret !== "string" || signingSecret.length === 0) {
60
+ throw new Error("signingSecret must be a non-empty string or a non-empty array of strings.");
61
+ }
62
+ return signingSecret;
63
+ }
64
+ function resolveAllSecrets(signingSecret) {
65
+ if (Array.isArray(signingSecret)) {
66
+ if (signingSecret.length === 0) {
67
+ throw new Error("signingSecret array must contain at least one secret.");
68
+ }
69
+ return signingSecret;
70
+ }
71
+ if (typeof signingSecret !== "string" || signingSecret.length === 0) {
72
+ throw new Error("signingSecret must be a non-empty string or a non-empty array of strings.");
73
+ }
74
+ return [signingSecret];
75
+ }
76
+ async function createSessionToken(options) {
77
+ const {
78
+ signingSecret,
79
+ deepgramKey,
80
+ elevenlabsKey,
81
+ geminiKey,
82
+ expiresIn = "15m",
83
+ allowedOrigins,
84
+ permissions = ["stt", "tts", "llm"],
85
+ userId,
86
+ sessionId = crypto.randomUUID(),
87
+ metadata
88
+ } = options;
89
+ const secret = resolveSigningSecret(signingSecret);
90
+ const lifetimeSeconds = parseDuration(expiresIn);
91
+ const now = Math.floor(Date.now() / 1e3);
92
+ const expiresAt = now + lifetimeSeconds;
93
+ const jwtPayload = {
94
+ sessionId,
95
+ permissions
96
+ };
97
+ if (userId !== void 0) {
98
+ jwtPayload.userId = userId;
99
+ }
100
+ if (metadata !== void 0 && Object.keys(metadata).length > 0) {
101
+ jwtPayload.metadata = metadata;
102
+ }
103
+ if (allowedOrigins !== void 0 && allowedOrigins.length > 0) {
104
+ jwtPayload.aud = allowedOrigins;
105
+ }
106
+ const token = await new SignJWT(jwtPayload).setProtectedHeader({ alg: "HS256" }).setIssuedAt(now).setExpirationTime(expiresAt).sign(encodeSecret(secret));
107
+ const providerKeys = {};
108
+ if (deepgramKey) providerKeys.deepgramKey = deepgramKey;
109
+ if (elevenlabsKey) providerKeys.elevenlabsKey = elevenlabsKey;
110
+ if (geminiKey) providerKeys.geminiKey = geminiKey;
111
+ if (Object.keys(providerKeys).length > 0) {
112
+ sessionKeyStore.set(sessionId, providerKeys);
113
+ }
114
+ return {
115
+ token,
116
+ expiresIn: lifetimeSeconds,
117
+ expiresAt
118
+ };
119
+ }
120
+ async function validateSessionToken(token, signingSecret, options) {
121
+ if (typeof token !== "string" || token.trim().length === 0) {
122
+ return { valid: false, error: "Token must be a non-empty string." };
123
+ }
124
+ const secrets = resolveAllSecrets(signingSecret);
125
+ let lastError;
126
+ for (const secret of secrets) {
127
+ try {
128
+ const verifyOptions = {
129
+ algorithms: ["HS256"]
130
+ };
131
+ if (options?.audience) {
132
+ verifyOptions.audience = options.audience;
133
+ }
134
+ const { payload } = await jwtVerify(token, encodeSecret(secret), verifyOptions);
135
+ const tokenPayload = {
136
+ sessionId: payload.sessionId,
137
+ expiresAt: payload.exp,
138
+ audience: normalizeAudience(payload.aud),
139
+ permissions: payload.permissions ?? [],
140
+ iat: payload.iat
141
+ };
142
+ if (payload.userId !== void 0) {
143
+ tokenPayload.userId = payload.userId;
144
+ }
145
+ if (payload.metadata !== void 0) {
146
+ tokenPayload.metadata = payload.metadata;
147
+ }
148
+ return { valid: true, payload: tokenPayload };
149
+ } catch (err) {
150
+ lastError = err;
151
+ }
152
+ }
153
+ const message = lastError instanceof Error ? lastError.message : "Token validation failed.";
154
+ return { valid: false, error: message };
155
+ }
156
+ function normalizeAudience(aud) {
157
+ if (aud === void 0 || aud === null) return [];
158
+ if (typeof aud === "string") return [aud];
159
+ if (Array.isArray(aud)) return aud.filter((a) => typeof a === "string");
160
+ return [];
161
+ }
162
+ function generateSecret() {
163
+ const bytes = new Uint8Array(32);
164
+ crypto.getRandomValues(bytes);
165
+ return base64UrlEncode(bytes);
166
+ }
167
+ function base64UrlEncode(bytes) {
168
+ let binary = "";
169
+ for (let i = 0; i < bytes.length; i++) {
170
+ binary += String.fromCharCode(bytes[i]);
171
+ }
172
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
173
+ }
174
+
175
+ export { clearSessionKeys, createSessionToken, generateSecret, getSessionKeys, validateSessionToken };
176
+ //# sourceMappingURL=index.js.map
177
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import { SignJWT, jwtVerify, type JWTPayload } from 'jose';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface TokenPayload {\n sessionId: string;\n expiresAt: number;\n audience: string[];\n permissions: string[];\n userId?: string;\n metadata?: Record<string, unknown>;\n iat: number;\n}\n\nexport interface CreateSessionTokenOptions {\n signingSecret: string | string[];\n deepgramKey?: string;\n elevenlabsKey?: string;\n geminiKey?: string;\n expiresIn?: string;\n allowedOrigins?: string[];\n permissions?: string[];\n userId?: string;\n sessionId?: string;\n metadata?: Record<string, unknown>;\n}\n\nexport interface CreateSessionTokenResult {\n token: string;\n expiresIn: number;\n expiresAt: number;\n}\n\nexport interface ValidateSessionTokenResult {\n valid: boolean;\n payload?: TokenPayload;\n error?: string;\n}\n\nexport interface ValidateSessionTokenOptions {\n audience?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Server-side provider key store\n// ---------------------------------------------------------------------------\n\ninterface ProviderKeys {\n deepgramKey?: string;\n elevenlabsKey?: string;\n geminiKey?: string;\n}\n\n/**\n * In-memory store mapping sessionId -> provider API keys.\n * Provider keys are NEVER placed in the JWT; they stay server-side only.\n */\nconst sessionKeyStore = new Map<string, ProviderKeys>();\n\n/**\n * Retrieve provider API keys associated with a session.\n * Returns `undefined` if the session is not found.\n */\nexport function getSessionKeys(sessionId: string): ProviderKeys | undefined {\n return sessionKeyStore.get(sessionId);\n}\n\n/**\n * Remove provider API keys for a session (e.g. on expiry or logout).\n */\nexport function clearSessionKeys(sessionId: string): boolean {\n return sessionKeyStore.delete(sessionId);\n}\n\n// ---------------------------------------------------------------------------\n// Duration parsing\n// ---------------------------------------------------------------------------\n\n/**\n * Parse a human-readable duration string into seconds.\n *\n * Supported units:\n * - `s` seconds\n * - `m` minutes\n * - `h` hours\n * - `d` days\n *\n * Compound durations are supported (e.g. `2h30m`, `1d12h`).\n *\n * @throws {Error} If the duration string is empty or contains no valid segments.\n */\nfunction parseDuration(duration: string): number {\n if (typeof duration !== 'string' || duration.trim().length === 0) {\n throw new Error('Duration must be a non-empty string (e.g. \"15m\", \"1h\", \"30s\").');\n }\n\n const cleaned = duration.trim().toLowerCase();\n const regex = /(\\d+)\\s*(d|h|m|s)/g;\n let match: RegExpExecArray | null;\n let totalSeconds = 0;\n let matched = false;\n\n while ((match = regex.exec(cleaned)) !== null) {\n matched = true;\n const value = parseInt(match[1]!, 10);\n const unit = match[2]!;\n\n switch (unit) {\n case 's':\n totalSeconds += value;\n break;\n case 'm':\n totalSeconds += value * 60;\n break;\n case 'h':\n totalSeconds += value * 3600;\n break;\n case 'd':\n totalSeconds += value * 86400;\n break;\n }\n }\n\n if (!matched) {\n throw new Error(\n `Invalid duration format: \"${duration}\". ` +\n 'Expected a string like \"15m\", \"1h\", \"30s\", \"7d\", or compound \"2h30m\".',\n );\n }\n\n if (totalSeconds <= 0) {\n throw new Error('Duration must resolve to a positive number of seconds.');\n }\n\n return totalSeconds;\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Encode a signing secret string into a Uint8Array suitable for HMAC operations.\n */\nfunction encodeSecret(secret: string): Uint8Array {\n return new TextEncoder().encode(secret);\n}\n\n/**\n * Resolve the signing secret to use.\n *\n * - If a single string is provided, it is used directly.\n * - If an array is provided, the **first** element (newest) is used for signing.\n *\n * @throws {Error} If no signing secret is provided or the array is empty.\n */\nfunction resolveSigningSecret(signingSecret: string | string[]): string {\n if (Array.isArray(signingSecret)) {\n if (signingSecret.length === 0) {\n throw new Error('signingSecret array must contain at least one secret.');\n }\n return signingSecret[0]!;\n }\n\n if (typeof signingSecret !== 'string' || signingSecret.length === 0) {\n throw new Error('signingSecret must be a non-empty string or a non-empty array of strings.');\n }\n\n return signingSecret;\n}\n\n/**\n * Flatten signingSecret into an array for rotation-aware verification.\n */\nfunction resolveAllSecrets(signingSecret: string | string[]): string[] {\n if (Array.isArray(signingSecret)) {\n if (signingSecret.length === 0) {\n throw new Error('signingSecret array must contain at least one secret.');\n }\n return signingSecret;\n }\n\n if (typeof signingSecret !== 'string' || signingSecret.length === 0) {\n throw new Error('signingSecret must be a non-empty string or a non-empty array of strings.');\n }\n\n return [signingSecret];\n}\n\n// ---------------------------------------------------------------------------\n// createSessionToken\n// ---------------------------------------------------------------------------\n\n/**\n * Create a signed session token (JWT) for use with GuideKit client SDKs.\n *\n * Provider API keys (`deepgramKey`, `elevenlabsKey`, `geminiKey`) are\n * **never** embedded in the JWT. They are stored in a server-side in-memory\n * map keyed by `sessionId` and can be retrieved via {@link getSessionKeys}.\n */\nexport async function createSessionToken(\n options: CreateSessionTokenOptions,\n): Promise<CreateSessionTokenResult> {\n const {\n signingSecret,\n deepgramKey,\n elevenlabsKey,\n geminiKey,\n expiresIn = '15m',\n allowedOrigins,\n permissions = ['stt', 'tts', 'llm'],\n userId,\n sessionId = crypto.randomUUID(),\n metadata,\n } = options;\n\n // Resolve the secret to sign with (first / newest).\n const secret = resolveSigningSecret(signingSecret);\n\n // Parse the requested lifetime.\n const lifetimeSeconds = parseDuration(expiresIn);\n\n // Build the JWT payload — provider keys are intentionally excluded.\n const now = Math.floor(Date.now() / 1000);\n const expiresAt = now + lifetimeSeconds;\n\n const jwtPayload: JWTPayload & {\n sessionId: string;\n permissions: string[];\n userId?: string;\n metadata?: Record<string, unknown>;\n } = {\n sessionId,\n permissions,\n };\n\n if (userId !== undefined) {\n jwtPayload.userId = userId;\n }\n\n if (metadata !== undefined && Object.keys(metadata).length > 0) {\n jwtPayload.metadata = metadata;\n }\n\n if (allowedOrigins !== undefined && allowedOrigins.length > 0) {\n jwtPayload.aud = allowedOrigins;\n }\n\n const token = await new SignJWT(jwtPayload)\n .setProtectedHeader({ alg: 'HS256' })\n .setIssuedAt(now)\n .setExpirationTime(expiresAt)\n .sign(encodeSecret(secret));\n\n // Store provider keys server-side, keyed by sessionId.\n const providerKeys: ProviderKeys = {};\n if (deepgramKey) providerKeys.deepgramKey = deepgramKey;\n if (elevenlabsKey) providerKeys.elevenlabsKey = elevenlabsKey;\n if (geminiKey) providerKeys.geminiKey = geminiKey;\n\n if (Object.keys(providerKeys).length > 0) {\n sessionKeyStore.set(sessionId, providerKeys);\n }\n\n return {\n token,\n expiresIn: lifetimeSeconds,\n expiresAt,\n };\n}\n\n// ---------------------------------------------------------------------------\n// validateSessionToken\n// ---------------------------------------------------------------------------\n\n/**\n * Validate and decode a GuideKit session token.\n *\n * When `signingSecret` is an array, the function tries each secret in order\n * to support zero-downtime key rotation. Verification succeeds as soon as\n * any secret produces a valid result.\n */\nexport async function validateSessionToken(\n token: string,\n signingSecret: string | string[],\n options?: ValidateSessionTokenOptions,\n): Promise<ValidateSessionTokenResult> {\n if (typeof token !== 'string' || token.trim().length === 0) {\n return { valid: false, error: 'Token must be a non-empty string.' };\n }\n\n const secrets = resolveAllSecrets(signingSecret);\n let lastError: unknown;\n\n for (const secret of secrets) {\n try {\n const verifyOptions: Parameters<typeof jwtVerify>[2] = {\n algorithms: ['HS256'],\n };\n\n if (options?.audience) {\n verifyOptions.audience = options.audience;\n }\n\n const { payload } = await jwtVerify(token, encodeSecret(secret), verifyOptions);\n\n const tokenPayload: TokenPayload = {\n sessionId: payload.sessionId as string,\n expiresAt: payload.exp as number,\n audience: normalizeAudience(payload.aud),\n permissions: (payload.permissions as string[]) ?? [],\n iat: payload.iat as number,\n };\n\n if (payload.userId !== undefined) {\n tokenPayload.userId = payload.userId as string;\n }\n\n if (payload.metadata !== undefined) {\n tokenPayload.metadata = payload.metadata as Record<string, unknown>;\n }\n\n return { valid: true, payload: tokenPayload };\n } catch (err) {\n lastError = err;\n // Continue to next secret — it may have been rotated.\n }\n }\n\n // All secrets failed.\n const message =\n lastError instanceof Error ? lastError.message : 'Token validation failed.';\n return { valid: false, error: message };\n}\n\n/**\n * Normalize the JWT `aud` claim into a string array.\n */\nfunction normalizeAudience(aud: unknown): string[] {\n if (aud === undefined || aud === null) return [];\n if (typeof aud === 'string') return [aud];\n if (Array.isArray(aud)) return aud.filter((a): a is string => typeof a === 'string');\n return [];\n}\n\n// ---------------------------------------------------------------------------\n// generateSecret\n// ---------------------------------------------------------------------------\n\n/**\n * Generate a cryptographically random 256-bit (32-byte) base64url-encoded\n * secret suitable for use as a GuideKit signing secret.\n *\n * Usage: `npx guidekit generate-secret`\n */\nexport function generateSecret(): string {\n const bytes = new Uint8Array(32);\n crypto.getRandomValues(bytes);\n return base64UrlEncode(bytes);\n}\n\n/**\n * Base64url-encode a Uint8Array without padding, per RFC 4648 section 5.\n */\nfunction base64UrlEncode(bytes: Uint8Array): string {\n let binary = '';\n for (let i = 0; i < bytes.length; i++) {\n binary += String.fromCharCode(bytes[i]!);\n }\n return btoa(binary).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@guidekit/server",
3
+ "version": "0.1.0-beta.1",
4
+ "description": "Server-side utilities for GuideKit SDK — token generation, auth middleware",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "require": "./dist/index.cjs"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "../../LICENSE",
19
+ "README.md"
20
+ ],
21
+ "scripts": {
22
+ "build": "tsup",
23
+ "dev": "tsup --watch",
24
+ "test": "vitest run --config ../../vitest.config.ts",
25
+ "test:unit": "vitest run --config ../../vitest.config.ts",
26
+ "test:watch": "vitest --config ../../vitest.config.ts",
27
+ "typecheck": "tsc --noEmit",
28
+ "clean": "rm -rf dist"
29
+ },
30
+ "dependencies": {
31
+ "jose": "^6.0.0"
32
+ },
33
+ "devDependencies": {
34
+ "typescript": "^5.7.0",
35
+ "tsup": "^8.4.0",
36
+ "vitest": "^3.0.0",
37
+ "@types/node": "^22.0.0"
38
+ },
39
+ "sideEffects": false,
40
+ "license": "MIT"
41
+ }