reflet-sdk 0.1.1 → 0.1.2
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 +6 -4
- package/dist/server.d.ts +5 -5
- package/dist/server.js +39 -13
- package/dist/server.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -112,7 +112,7 @@ For production apps, sign user tokens on your server to prevent spoofing:
|
|
|
112
112
|
import { signUser } from 'reflet-sdk/server';
|
|
113
113
|
|
|
114
114
|
export async function getUserToken(user: User) {
|
|
115
|
-
const { token, expiresAt } = signUser(
|
|
115
|
+
const { token, expiresAt } = await signUser(
|
|
116
116
|
{
|
|
117
117
|
id: user.id,
|
|
118
118
|
email: user.email,
|
|
@@ -185,14 +185,16 @@ const reflet = new Reflet(config: RefletConfig);
|
|
|
185
185
|
|
|
186
186
|
### Server Utilities
|
|
187
187
|
|
|
188
|
+
Works in Node.js, Edge runtimes, Cloudflare Workers, and Convex.
|
|
189
|
+
|
|
188
190
|
```typescript
|
|
189
191
|
import { signUser, verifyUser } from 'reflet-sdk/server';
|
|
190
192
|
|
|
191
|
-
// Sign a user token
|
|
192
|
-
const { token, expiresAt } = signUser(user, secretKey, expiresInSeconds?);
|
|
193
|
+
// Sign a user token (async - uses Web Crypto API)
|
|
194
|
+
const { token, expiresAt } = await signUser(user, secretKey, expiresInSeconds?);
|
|
193
195
|
|
|
194
196
|
// Verify a token (optional - server-side validation)
|
|
195
|
-
const user = verifyUser(token, secretKey);
|
|
197
|
+
const user = await verifyUser(token, secretKey);
|
|
196
198
|
```
|
|
197
199
|
|
|
198
200
|
## Types
|
package/dist/server.d.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Server-side utilities for Reflet SDK
|
|
3
|
-
*
|
|
3
|
+
* Works in Node.js, Edge runtimes, Cloudflare Workers, and Convex
|
|
4
4
|
*
|
|
5
5
|
* @example
|
|
6
6
|
* ```ts
|
|
7
7
|
* import { signUser } from 'reflet-sdk/server';
|
|
8
8
|
*
|
|
9
|
-
* const { token } = signUser(
|
|
9
|
+
* const { token } = await signUser(
|
|
10
10
|
* { id: user.id, email: user.email, name: user.name },
|
|
11
11
|
* process.env.REFLET_SECRET_KEY!
|
|
12
12
|
* );
|
|
@@ -32,13 +32,13 @@ interface SignedUserToken {
|
|
|
32
32
|
}
|
|
33
33
|
/**
|
|
34
34
|
* Sign a user token for secure client-side identification
|
|
35
|
-
* Uses HMAC-SHA256
|
|
35
|
+
* Uses HMAC-SHA256 via Web Crypto API (works in all runtimes)
|
|
36
36
|
*/
|
|
37
|
-
declare function signUser(user: SignUserOptions, secretKey: string, expiresInSeconds?: number): SignedUserToken
|
|
37
|
+
declare function signUser(user: SignUserOptions, secretKey: string, expiresInSeconds?: number): Promise<SignedUserToken>;
|
|
38
38
|
/**
|
|
39
39
|
* Verify a user token
|
|
40
40
|
* Returns the user data if valid, null otherwise
|
|
41
41
|
*/
|
|
42
|
-
declare function verifyUser(token: string, secretKey: string): SignUserOptions | null
|
|
42
|
+
declare function verifyUser(token: string, secretKey: string): Promise<SignUserOptions | null>;
|
|
43
43
|
|
|
44
44
|
export { type SignUserOptions as RefletServerUser, type SignUserOptions, type SignedUserToken, signUser, verifyUser };
|
package/dist/server.js
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
import { createHmac } from 'crypto';
|
|
2
|
-
|
|
3
1
|
// src/server.ts
|
|
4
2
|
var PLUS_REGEX = /\+/g;
|
|
5
3
|
var SLASH_REGEX = /\//g;
|
|
6
4
|
var PADDING_REGEX = /=+$/;
|
|
7
5
|
var DASH_REGEX = /-/g;
|
|
8
6
|
var UNDERSCORE_REGEX = /_/g;
|
|
9
|
-
function signUser(user, secretKey, expiresInSeconds = 86400) {
|
|
7
|
+
async function signUser(user, secretKey, expiresInSeconds = 86400) {
|
|
10
8
|
if (!secretKey) {
|
|
11
9
|
throw new Error("signUser: secretKey is required");
|
|
12
10
|
}
|
|
@@ -28,7 +26,7 @@ function signUser(user, secretKey, expiresInSeconds = 86400) {
|
|
|
28
26
|
};
|
|
29
27
|
const headerB64 = base64UrlEncode(JSON.stringify(header));
|
|
30
28
|
const payloadB64 = base64UrlEncode(JSON.stringify(payload));
|
|
31
|
-
const signature = createHmacSha256Signature(
|
|
29
|
+
const signature = await createHmacSha256Signature(
|
|
32
30
|
`${headerB64}.${payloadB64}`,
|
|
33
31
|
secretKey
|
|
34
32
|
);
|
|
@@ -37,7 +35,7 @@ function signUser(user, secretKey, expiresInSeconds = 86400) {
|
|
|
37
35
|
expiresAt: exp * 1e3
|
|
38
36
|
};
|
|
39
37
|
}
|
|
40
|
-
function verifyUser(token, secretKey) {
|
|
38
|
+
async function verifyUser(token, secretKey) {
|
|
41
39
|
if (!(token && secretKey)) {
|
|
42
40
|
return null;
|
|
43
41
|
}
|
|
@@ -46,7 +44,7 @@ function verifyUser(token, secretKey) {
|
|
|
46
44
|
return null;
|
|
47
45
|
}
|
|
48
46
|
const [headerB64, payloadB64, signature] = parts;
|
|
49
|
-
const expectedSignature = createHmacSha256Signature(
|
|
47
|
+
const expectedSignature = await createHmacSha256Signature(
|
|
50
48
|
`${headerB64}.${payloadB64}`,
|
|
51
49
|
secretKey
|
|
52
50
|
);
|
|
@@ -74,20 +72,48 @@ function verifyUser(token, secretKey) {
|
|
|
74
72
|
}
|
|
75
73
|
}
|
|
76
74
|
function base64UrlEncode(str) {
|
|
77
|
-
const
|
|
75
|
+
const bytes = new TextEncoder().encode(str);
|
|
76
|
+
let binary = "";
|
|
77
|
+
for (const byte of bytes) {
|
|
78
|
+
binary += String.fromCharCode(byte);
|
|
79
|
+
}
|
|
80
|
+
const base64 = btoa(binary);
|
|
78
81
|
return base64.replace(PLUS_REGEX, "-").replace(SLASH_REGEX, "_").replace(PADDING_REGEX, "");
|
|
79
82
|
}
|
|
80
83
|
function base64UrlDecode(str) {
|
|
81
84
|
const padding = 4 - str.length % 4;
|
|
82
85
|
const paddedStr = padding < 4 ? str + "=".repeat(padding) : str;
|
|
83
86
|
const base64 = paddedStr.replace(DASH_REGEX, "+").replace(UNDERSCORE_REGEX, "/");
|
|
84
|
-
|
|
87
|
+
const binary = atob(base64);
|
|
88
|
+
const bytes = new Uint8Array(binary.length);
|
|
89
|
+
for (let i = 0; i < binary.length; i++) {
|
|
90
|
+
bytes[i] = binary.charCodeAt(i);
|
|
91
|
+
}
|
|
92
|
+
return new TextDecoder().decode(bytes);
|
|
85
93
|
}
|
|
86
|
-
function createHmacSha256Signature(data, secret) {
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
const
|
|
90
|
-
|
|
94
|
+
async function createHmacSha256Signature(data, secret) {
|
|
95
|
+
const encoder = new TextEncoder();
|
|
96
|
+
const keyData = encoder.encode(secret);
|
|
97
|
+
const messageData = encoder.encode(data);
|
|
98
|
+
const cryptoKey = await crypto.subtle.importKey(
|
|
99
|
+
"raw",
|
|
100
|
+
keyData,
|
|
101
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
102
|
+
false,
|
|
103
|
+
["sign"]
|
|
104
|
+
);
|
|
105
|
+
const signatureBuffer = await crypto.subtle.sign(
|
|
106
|
+
"HMAC",
|
|
107
|
+
cryptoKey,
|
|
108
|
+
messageData
|
|
109
|
+
);
|
|
110
|
+
const signatureArray = new Uint8Array(signatureBuffer);
|
|
111
|
+
let binary = "";
|
|
112
|
+
for (const byte of signatureArray) {
|
|
113
|
+
binary += String.fromCharCode(byte);
|
|
114
|
+
}
|
|
115
|
+
const base64 = btoa(binary);
|
|
116
|
+
return base64.replace(PLUS_REGEX, "-").replace(SLASH_REGEX, "_").replace(PADDING_REGEX, "");
|
|
91
117
|
}
|
|
92
118
|
function constantTimeEqual(a, b) {
|
|
93
119
|
if (a.length !== b.length) {
|
package/dist/server.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/server.ts"],"names":[],"mappings":"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","file":"server.js","sourcesContent":["/**\n * Server-side utilities for Reflet SDK\n * Use these functions on your backend to securely sign user tokens\n *\n * @example\n * ```ts\n * import { signUser } from 'reflet-sdk/server';\n *\n * const { token } = signUser(\n * { id: user.id, email: user.email, name: user.name },\n * process.env.REFLET_SECRET_KEY!\n * );\n * ```\n */\n\nimport { createHmac } from \"node:crypto\";\n\n// Top-level regex patterns for base64url encoding\nconst PLUS_REGEX = /\\+/g;\nconst SLASH_REGEX = /\\//g;\nconst PADDING_REGEX = /=+$/;\nconst DASH_REGEX = /-/g;\nconst UNDERSCORE_REGEX = /_/g;\n\nexport interface SignUserOptions {\n /** User's unique ID in your system */\n id: string;\n /** User's email address */\n email?: string;\n /** User's display name */\n name?: string;\n /** URL to user's avatar image */\n avatar?: string;\n /** Additional metadata */\n metadata?: Record<string, unknown>;\n}\n\nexport interface SignedUserToken {\n /** The signed JWT token */\n token: string;\n /** Token expiration timestamp (ms) */\n expiresAt: number;\n}\n\n/**\n * Sign a user token for secure client-side identification\n * Uses HMAC-SHA256 for cryptographically secure signatures\n */\nexport function signUser(\n user: SignUserOptions,\n secretKey: string,\n expiresInSeconds = 86_400 // 24 hours default\n): SignedUserToken {\n if (!secretKey) {\n throw new Error(\"signUser: secretKey is required\");\n }\n\n if (!user.id) {\n throw new Error(\"signUser: user.id is required\");\n }\n\n const now = Math.floor(Date.now() / 1000);\n const exp = now + expiresInSeconds;\n\n const header = { alg: \"HS256\", typ: \"JWT\" };\n const payload = {\n sub: user.id,\n id: user.id,\n email: user.email,\n name: user.name,\n avatar: user.avatar,\n metadata: user.metadata,\n iat: now,\n exp,\n };\n\n const headerB64 = base64UrlEncode(JSON.stringify(header));\n const payloadB64 = base64UrlEncode(JSON.stringify(payload));\n\n // Create signature using HMAC-SHA256\n const signature = createHmacSha256Signature(\n `${headerB64}.${payloadB64}`,\n secretKey\n );\n\n return {\n token: `${headerB64}.${payloadB64}.${signature}`,\n expiresAt: exp * 1000,\n };\n}\n\n/**\n * Verify a user token\n * Returns the user data if valid, null otherwise\n */\nexport function verifyUser(\n token: string,\n secretKey: string\n): SignUserOptions | null {\n if (!(token && secretKey)) {\n return null;\n }\n\n const parts = token.split(\".\");\n if (parts.length !== 3) {\n return null;\n }\n\n const [headerB64, payloadB64, signature] = parts;\n\n // Verify signature using constant-time comparison\n const expectedSignature = createHmacSha256Signature(\n `${headerB64}.${payloadB64}`,\n secretKey\n );\n\n if (!constantTimeEqual(signature, expectedSignature)) {\n return null;\n }\n\n try {\n const payload = JSON.parse(base64UrlDecode(payloadB64)) as {\n id?: string;\n sub?: string;\n email?: string;\n name?: string;\n avatar?: string;\n metadata?: Record<string, unknown>;\n exp?: number;\n };\n\n // Check expiration\n if (payload.exp && Date.now() > payload.exp * 1000) {\n return null;\n }\n\n const userId = payload.id ?? payload.sub;\n if (!userId) {\n return null;\n }\n\n return {\n id: userId,\n email: payload.email,\n name: payload.name,\n avatar: payload.avatar,\n metadata: payload.metadata,\n };\n } catch {\n return null;\n }\n}\n\n// ============================================\n// Utility Functions\n// ============================================\n\nfunction base64UrlEncode(str: string): string {\n const base64 = Buffer.from(str).toString(\"base64\");\n\n return base64\n .replace(PLUS_REGEX, \"-\")\n .replace(SLASH_REGEX, \"_\")\n .replace(PADDING_REGEX, \"\");\n}\n\nfunction base64UrlDecode(str: string): string {\n // Restore base64 padding\n const padding = 4 - (str.length % 4);\n const paddedStr = padding < 4 ? str + \"=\".repeat(padding) : str;\n\n // Replace URL-safe characters\n const base64 = paddedStr\n .replace(DASH_REGEX, \"+\")\n .replace(UNDERSCORE_REGEX, \"/\");\n\n return Buffer.from(base64, \"base64\").toString();\n}\n\n/**\n * Create HMAC-SHA256 signature (cryptographically secure)\n */\nfunction createHmacSha256Signature(data: string, secret: string): string {\n const hmac = createHmac(\"sha256\", secret);\n hmac.update(data);\n const signature = hmac.digest(\"base64\");\n\n // Convert to base64url\n return signature\n .replace(PLUS_REGEX, \"-\")\n .replace(SLASH_REGEX, \"_\")\n .replace(PADDING_REGEX, \"\");\n}\n\n/**\n * Constant-time string comparison to prevent timing attacks\n */\nfunction constantTimeEqual(a: string, b: string): boolean {\n if (a.length !== b.length) {\n return false;\n }\n\n let result = 0;\n for (let i = 0; i < a.length; i++) {\n // biome-ignore lint/suspicious/noBitwiseOperators: intentional for constant-time comparison\n result |= a.charCodeAt(i) ^ b.charCodeAt(i);\n }\n\n return result === 0;\n}\n\n// Export types\nexport type { SignUserOptions as RefletServerUser };\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/server.ts"],"names":[],"mappings":"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","file":"server.js","sourcesContent":["/**\n * Server-side utilities for Reflet SDK\n * Works in Node.js, Edge runtimes, Cloudflare Workers, and Convex\n *\n * @example\n * ```ts\n * import { signUser } from 'reflet-sdk/server';\n *\n * const { token } = await signUser(\n * { id: user.id, email: user.email, name: user.name },\n * process.env.REFLET_SECRET_KEY!\n * );\n * ```\n */\n\n// Top-level regex patterns for base64url encoding\nconst PLUS_REGEX = /\\+/g;\nconst SLASH_REGEX = /\\//g;\nconst PADDING_REGEX = /=+$/;\nconst DASH_REGEX = /-/g;\nconst UNDERSCORE_REGEX = /_/g;\n\nexport interface SignUserOptions {\n /** User's unique ID in your system */\n id: string;\n /** User's email address */\n email?: string;\n /** User's display name */\n name?: string;\n /** URL to user's avatar image */\n avatar?: string;\n /** Additional metadata */\n metadata?: Record<string, unknown>;\n}\n\nexport interface SignedUserToken {\n /** The signed JWT token */\n token: string;\n /** Token expiration timestamp (ms) */\n expiresAt: number;\n}\n\n/**\n * Sign a user token for secure client-side identification\n * Uses HMAC-SHA256 via Web Crypto API (works in all runtimes)\n */\nexport async function signUser(\n user: SignUserOptions,\n secretKey: string,\n expiresInSeconds = 86_400 // 24 hours default\n): Promise<SignedUserToken> {\n if (!secretKey) {\n throw new Error(\"signUser: secretKey is required\");\n }\n\n if (!user.id) {\n throw new Error(\"signUser: user.id is required\");\n }\n\n const now = Math.floor(Date.now() / 1000);\n const exp = now + expiresInSeconds;\n\n const header = { alg: \"HS256\", typ: \"JWT\" };\n const payload = {\n sub: user.id,\n id: user.id,\n email: user.email,\n name: user.name,\n avatar: user.avatar,\n metadata: user.metadata,\n iat: now,\n exp,\n };\n\n const headerB64 = base64UrlEncode(JSON.stringify(header));\n const payloadB64 = base64UrlEncode(JSON.stringify(payload));\n\n // Create signature using HMAC-SHA256 via Web Crypto API\n const signature = await createHmacSha256Signature(\n `${headerB64}.${payloadB64}`,\n secretKey\n );\n\n return {\n token: `${headerB64}.${payloadB64}.${signature}`,\n expiresAt: exp * 1000,\n };\n}\n\n/**\n * Verify a user token\n * Returns the user data if valid, null otherwise\n */\nexport async function verifyUser(\n token: string,\n secretKey: string\n): Promise<SignUserOptions | null> {\n if (!(token && secretKey)) {\n return null;\n }\n\n const parts = token.split(\".\");\n if (parts.length !== 3) {\n return null;\n }\n\n const [headerB64, payloadB64, signature] = parts;\n\n // Verify signature using constant-time comparison\n const expectedSignature = await createHmacSha256Signature(\n `${headerB64}.${payloadB64}`,\n secretKey\n );\n\n if (!constantTimeEqual(signature, expectedSignature)) {\n return null;\n }\n\n try {\n const payload = JSON.parse(base64UrlDecode(payloadB64)) as {\n id?: string;\n sub?: string;\n email?: string;\n name?: string;\n avatar?: string;\n metadata?: Record<string, unknown>;\n exp?: number;\n };\n\n // Check expiration\n if (payload.exp && Date.now() > payload.exp * 1000) {\n return null;\n }\n\n const userId = payload.id ?? payload.sub;\n if (!userId) {\n return null;\n }\n\n return {\n id: userId,\n email: payload.email,\n name: payload.name,\n avatar: payload.avatar,\n metadata: payload.metadata,\n };\n } catch {\n return null;\n }\n}\n\n// ============================================\n// Utility Functions (Web Crypto API - Universal)\n// ============================================\n\nfunction base64UrlEncode(str: string): string {\n // Use TextEncoder for universal compatibility\n const bytes = new TextEncoder().encode(str);\n let binary = \"\";\n for (const byte of bytes) {\n binary += String.fromCharCode(byte);\n }\n const base64 = btoa(binary);\n\n return base64\n .replace(PLUS_REGEX, \"-\")\n .replace(SLASH_REGEX, \"_\")\n .replace(PADDING_REGEX, \"\");\n}\n\nfunction base64UrlDecode(str: string): string {\n // Restore base64 padding\n const padding = 4 - (str.length % 4);\n const paddedStr = padding < 4 ? str + \"=\".repeat(padding) : str;\n\n // Replace URL-safe characters\n const base64 = paddedStr\n .replace(DASH_REGEX, \"+\")\n .replace(UNDERSCORE_REGEX, \"/\");\n\n const binary = atob(base64);\n const bytes = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) {\n bytes[i] = binary.charCodeAt(i);\n }\n return new TextDecoder().decode(bytes);\n}\n\n/**\n * Create HMAC-SHA256 signature using Web Crypto API\n * Works in Node.js 18+, Edge runtimes, Cloudflare Workers, Convex, etc.\n */\nasync function createHmacSha256Signature(\n data: string,\n secret: string\n): Promise<string> {\n const encoder = new TextEncoder();\n const keyData = encoder.encode(secret);\n const messageData = encoder.encode(data);\n\n // Import the secret key\n const cryptoKey = await crypto.subtle.importKey(\n \"raw\",\n keyData,\n { name: \"HMAC\", hash: \"SHA-256\" },\n false,\n [\"sign\"]\n );\n\n // Sign the data\n const signatureBuffer = await crypto.subtle.sign(\n \"HMAC\",\n cryptoKey,\n messageData\n );\n\n // Convert to base64url\n const signatureArray = new Uint8Array(signatureBuffer);\n let binary = \"\";\n for (const byte of signatureArray) {\n binary += String.fromCharCode(byte);\n }\n const base64 = btoa(binary);\n\n return base64\n .replace(PLUS_REGEX, \"-\")\n .replace(SLASH_REGEX, \"_\")\n .replace(PADDING_REGEX, \"\");\n}\n\n/**\n * Constant-time string comparison to prevent timing attacks\n */\nfunction constantTimeEqual(a: string, b: string): boolean {\n if (a.length !== b.length) {\n return false;\n }\n\n let result = 0;\n for (let i = 0; i < a.length; i++) {\n // biome-ignore lint/suspicious/noBitwiseOperators: intentional for constant-time comparison\n result |= a.charCodeAt(i) ^ b.charCodeAt(i);\n }\n\n return result === 0;\n}\n\n// Export types\nexport type { SignUserOptions as RefletServerUser };\n"]}
|
package/package.json
CHANGED