Skip to content

Commit 11fc486

Browse files
authored
Merge pull request #220 from thedanchez/crypto
feat: remove cross-sha256 dependency from js sdk
2 parents cb759a3 + d5c46f2 commit 11fc486

File tree

22 files changed

+510
-84
lines changed

22 files changed

+510
-84
lines changed

examples/todo-frontend-react-sdk/tsconfig.json

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,6 @@
44
"compilerOptions": {
55
"baseUrl": ".",
66
"paths": {
7-
"cross-sha256": [
8-
"../../node_modules/.pnpm/[email protected]/node_modules/cross-sha256/index.d.ts"
9-
],
107
"featurehub-javascript-client-sdk": ["../../packages/js/src"],
118
"featurehub-react-sdk": ["../../packages/react/src"]
129
}

examples/todo-frontend-react-typescript-catch-and-release/tsconfig.json

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,6 @@
55
"baseUrl": ".",
66
"outDir": "dist",
77
"paths": {
8-
"cross-sha256": [
9-
"../../node_modules/.pnpm/[email protected]/node_modules/cross-sha256/index.d.ts"
10-
],
118
"featurehub-javascript-client-sdk": ["../../packages/js/src"]
129
}
1310
},

examples/todo-frontend-react-typescript-feature-override/tsconfig.json

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,6 @@
55
"baseUrl": ".",
66
"outDir": "dist",
77
"paths": {
8-
"cross-sha256": [
9-
"../../node_modules/.pnpm/[email protected]/node_modules/cross-sha256/index.d.ts"
10-
],
118
"featurehub-javascript-client-sdk": ["../../packages/js/src"]
129
}
1310
},

examples/todo-frontend-react-typescript/tsconfig.json

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,6 @@
55
"baseUrl": ".",
66
"outDir": "dist",
77
"paths": {
8-
"cross-sha256": [
9-
"../../node_modules/.pnpm/[email protected]/node_modules/cross-sha256/index.d.ts"
10-
],
118
"featurehub-javascript-client-sdk": ["../../packages/js/src"]
129
}
1310
},

examples/todo-frontend-solid-sdk/tsconfig.json

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,6 @@
66
"outDir": "dist",
77
"types": ["vite/client"],
88
"paths": {
9-
"cross-sha256": [
10-
"../../node_modules/.pnpm/[email protected]/node_modules/cross-sha256/index.d.ts"
11-
],
129
"featurehub-javascript-client-sdk": ["../../packages/js/src"],
1310
"featurehub-solid-sdk": ["../../packages/solid/src"]
1411
}

examples/todo-server-tests/tsconfig.json

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,7 @@
1212
"types": ["node"],
1313
"paths": {
1414
"featurehub-javascript-client-sdk": ["../../packages/js/src"],
15-
"featurehub-javascript-node-sdk": ["../../packages/node/src"],
16-
/*
17-
This is a workaround to bypass Typescript checking of the cross-sha256 package.
18-
The author of the package improperly exported the source .ts file alongside the js file
19-
*/
20-
"cross-sha256": [
21-
"../../node_modules/.pnpm/[email protected]/node_modules/cross-sha256/index.d.ts"
22-
]
15+
"featurehub-javascript-node-sdk": ["../../packages/node/src"]
2316
}
2417
},
2518
"include": ["**/*.ts", "**/*.tsx"],

packages/js/package.json

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
"description": "FeatureHub client/browser SDK",
55
"author": "[email protected]",
66
"type": "module",
7-
"sideEffects": false,
87
"main": "dist/index.js",
98
"types": "dist/index.d.ts",
109
"module": "lib/index.js",
@@ -53,12 +52,14 @@
5352
},
5453
"devDependencies": {
5554
"@fluffy-spoon/substitute": "^1.208.0",
55+
"@types/jsdom": "^27.0.0",
5656
"@types/netmask": "^2.0.5",
5757
"@types/node": "catalog:node",
5858
"@types/semver-compare": "^1.0.3",
5959
"@types/sinon": "^10.0.13",
6060
"@vitest/coverage-istanbul": "catalog:vitest",
6161
"featurehub-tsconfig": "workspace:*",
62+
"jsdom": "^27.1.0",
6263
"rimraf": "catalog:rimraf",
6364
"sinon": "^15.0.1",
6465
"tsup": "catalog:tsup",
@@ -67,8 +68,6 @@
6768
"vitest": "catalog:vitest"
6869
},
6970
"dependencies": {
70-
"@juanelas/base64": "^1.1.5",
71-
"cross-sha256": "^1.2.0",
7271
"murmurhash": "^2.0.1",
7372
"netmask": "^2.0.2",
7473
"semver-compare": "^1.0.0"

packages/js/src/__tests__/polling_sdk.test.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,4 +197,91 @@ describe("basic polling sdk works as expected", () => {
197197
expect(counter).toBe(2);
198198
});
199199
});
200+
201+
describe("attributeHeader hashing", () => {
202+
const TEST_HASH = "n4bQgYhMfWWaL-qgxVrQFaO_TxsrC4Is0V1sFbDwCgg";
203+
const USER_ID_HASH = "F7827sASf1GewZfMvDzBlYpLnaFcKM0xchzNEElvh94";
204+
const SESSION_HASH = "ZJ2mGsLI7CqpUgmQqueaBudcjnxW6SZ9uVuwMWASoe4";
205+
206+
it("should produce consistent hash results for context headers", async () => {
207+
class TestPoller extends PollingBase {
208+
constructor() {
209+
super("", 0, () => {});
210+
}
211+
212+
poll(): Promise<void> {
213+
return Promise.resolve();
214+
}
215+
216+
// Expose the protected _shaHeader for testing
217+
get shaHeader(): string {
218+
return this._shaHeader;
219+
}
220+
}
221+
222+
const testPoller = new TestPoller();
223+
224+
// Test known hash values to ensure consistency
225+
await testPoller.attributeHeader("");
226+
expect(testPoller.shaHeader).toBe("0");
227+
228+
await testPoller.attributeHeader("test");
229+
expect(testPoller.shaHeader).toBe(TEST_HASH);
230+
231+
await testPoller.attributeHeader("user-id:12345");
232+
expect(testPoller.shaHeader).toBe(USER_ID_HASH);
233+
234+
await testPoller.attributeHeader("user-id:12345,session:abcdef");
235+
expect(testPoller.shaHeader).toBe(SESSION_HASH);
236+
});
237+
238+
it("produces same hash results in browser environment as node environment", async () => {
239+
// Create JSDOM environment to simulate browser
240+
const { JSDOM } = await import("jsdom");
241+
const dom = new JSDOM("", {
242+
url: "https://localhost",
243+
pretendToBeVisual: true,
244+
resources: "usable",
245+
});
246+
247+
// Store original window
248+
const originalWindow = (globalThis as any).window;
249+
250+
try {
251+
// Set up browser-like environment
252+
(globalThis as any).window = dom.window;
253+
254+
Object.defineProperty(globalThis, "window", {
255+
value: dom.window,
256+
writable: true,
257+
configurable: true,
258+
});
259+
260+
const { webcrypto } = await import("crypto");
261+
Object.defineProperty(globalThis.window, "crypto", {
262+
value: {
263+
...webcrypto,
264+
subtle: webcrypto.subtle,
265+
},
266+
writable: true,
267+
configurable: true,
268+
});
269+
270+
const { createBase64UrlSafeHash } = await import("../crypto/crypto-browser");
271+
272+
const testHash = await createBase64UrlSafeHash("sha256", "test");
273+
expect(testHash).toBe(TEST_HASH);
274+
275+
const userIdHash = await createBase64UrlSafeHash("sha256", "user-id:12345");
276+
expect(userIdHash).toBe(USER_ID_HASH);
277+
278+
const sessionHash = await createBase64UrlSafeHash("sha256", "user-id:12345,session:abcdef");
279+
expect(sessionHash).toBe(SESSION_HASH);
280+
} finally {
281+
// Restore original window
282+
(globalThis as any).window = originalWindow;
283+
dom.window.close();
284+
}
285+
});
286+
});
200287
});
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/**
2+
* Browser crypto utilities using Web Crypto API
3+
* This module should only be imported in browser environments
4+
*/
5+
6+
import type { HashAlgorithm } from "./types";
7+
8+
const algorithmMap: Record<HashAlgorithm, string> = {
9+
sha1: "SHA-1",
10+
sha256: "SHA-256",
11+
sha384: "SHA-384",
12+
sha512: "SHA-512",
13+
};
14+
15+
export const createBase64UrlSafeHash = async (algorithm: string, data: string): Promise<string> => {
16+
const encoder = new TextEncoder();
17+
const dataBuffer = encoder.encode(data);
18+
19+
const cryptoAlgorithm = algorithmMap[algorithm as HashAlgorithm];
20+
if (!cryptoAlgorithm) {
21+
throw new Error(`Unsupported hash algorithm: ${algorithm}`);
22+
}
23+
24+
const hashBuffer = await window.crypto.subtle.digest(cryptoAlgorithm, dataBuffer);
25+
const hashArray = new Uint8Array(hashBuffer);
26+
27+
// Convert to base64
28+
let binary = "";
29+
for (let i = 0; i < hashArray.byteLength; i++) {
30+
binary += String.fromCharCode(hashArray[i]!);
31+
}
32+
33+
const base64 = btoa(binary);
34+
35+
// Convert to base64 url encoding (no padding)
36+
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
37+
};
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/**
2+
* Node.js/Bun/Deno crypto utilities
3+
* This module should only be imported in server environments
4+
*/
5+
6+
import type { HashAlgorithm } from "./types";
7+
8+
export const createBase64UrlSafeHash = async (
9+
algorithm: HashAlgorithm,
10+
data: string,
11+
): Promise<string> => {
12+
const crypto = await import("crypto");
13+
const hash = crypto.createHash(algorithm).update(data).digest("base64");
14+
15+
// Convert to base64 url encoding (no padding)
16+
return hash.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
17+
};

0 commit comments

Comments
 (0)