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 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
- * Use these functions on your backend to securely sign user tokens
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 for cryptographically secure signatures
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 base64 = Buffer.from(str).toString("base64");
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
- return Buffer.from(base64, "base64").toString();
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 hmac = createHmac("sha256", secret);
88
- hmac.update(data);
89
- const signature = hmac.digest("base64");
90
- return signature.replace(PLUS_REGEX, "-").replace(SLASH_REGEX, "_").replace(PADDING_REGEX, "");
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) {
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reflet-sdk",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Official Reflet SDK - Collect user feedback, feature requests, and bug reports from your app",
5
5
  "license": "MIT",
6
6
  "author": "Reflet <hello@reflet.app>",