@helix-id/voice-auth 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
@@ -43,10 +43,19 @@ window.location.href = url;
43
43
 
44
44
  ```typescript
45
45
  import { verifyCallback, InvalidSignatureError, NonceMismatchError, ExpiredTimestampError } from "@helix-id/voice-auth";
46
+ import type { CallbackQuery } from "@helix-id/voice-auth";
46
47
 
47
48
  app.get("/auth/callback", (req, res) => {
48
49
  try {
49
- const result = verifyCallback(req.query, {
50
+ // verifyCallback expects every field to be a plain string.
51
+ // Frameworks like Express can parse duplicate query params as arrays
52
+ // (e.g. ?status=verified&status=failed → { status: ["verified","failed"] }).
53
+ // Normalize req.query before passing it in:
54
+ const query: CallbackQuery = Object.fromEntries(
55
+ Object.entries(req.query).map(([k, v]) => [k, Array.isArray(v) ? v[0] : String(v)]),
56
+ ) as CallbackQuery;
57
+
58
+ const result = verifyCallback(query, {
50
59
  nonce: req.session.helixNonce,
51
60
  secret: process.env.HELIX_SECRET,
52
61
  });
@@ -101,6 +110,8 @@ Returns `{ status, statusCode, logId, state }`.
101
110
 
102
111
  Throws: `InvalidSignatureError`, `NonceMismatchError`, `ExpiredTimestampError`, `MalformedCallbackError`.
103
112
 
113
+ > **Important:** `query` must be a flat `Record<string, string>`. Some frameworks (Express, Koa) can parse duplicate query parameters as arrays. Normalize the query object before passing it to `verifyCallback` — see the callback example above.
114
+
104
115
  ## Status Codes
105
116
 
106
117
  | status_code | status | Description |
@@ -116,7 +127,8 @@ Throws: `InvalidSignatureError`, `NonceMismatchError`, `ExpiredTimestampError`,
116
127
  Use the test credentials for local development:
117
128
 
118
129
  ```typescript
119
- import { createAuthUrl, HELIX_TEST_SPACE_ID, HELIX_TEST_SECRET } from "@helix-id/voice-auth";
130
+ import { createAuthUrl } from "@helix-id/voice-auth";
131
+ import { HELIX_TEST_SPACE_ID, HELIX_TEST_SECRET } from "@helix-id/voice-auth/testing";
120
132
 
121
133
  const { url, nonce } = createAuthUrl({
122
134
  spaceId: HELIX_TEST_SPACE_ID,
@@ -0,0 +1,13 @@
1
+ //#region src/constants.d.ts
2
+ /** Default Helix base URL for production. */
3
+ declare const HELIX_DEFAULT_BASE_URL = "https://capsule.helix.id";
4
+ /** Test Space ID for local development and integration testing. */
5
+ declare const HELIX_TEST_SPACE_ID = "a1b2c3d4-e5f6-7890-abcd-ef1234567890";
6
+ /** Test shared secret for local development and integration testing. */
7
+ declare const HELIX_TEST_SECRET = "tk_test_hx_4a7f2c9d8e1b3f6a5c2d9e8f7b4a1c3d";
8
+ /** Test callback URL for local development. */
9
+ declare const HELIX_TEST_CALLBACK_URL = "http://localhost:8080/helix/callback";
10
+ /** Callback timestamp validity window in seconds (5 minutes). */
11
+ declare const TIMESTAMP_WINDOW_SECONDS = 300;
12
+ //#endregion
13
+ export { TIMESTAMP_WINDOW_SECONDS as a, HELIX_TEST_SPACE_ID as i, HELIX_TEST_CALLBACK_URL as n, HELIX_TEST_SECRET as r, HELIX_DEFAULT_BASE_URL as t };
@@ -0,0 +1,14 @@
1
+ //#region src/constants.ts
2
+ /** Default Helix base URL for production. */
3
+ const HELIX_DEFAULT_BASE_URL = "https://capsule.helix.id";
4
+ /** Test Space ID for local development and integration testing. */
5
+ const HELIX_TEST_SPACE_ID = "a1b2c3d4-e5f6-7890-abcd-ef1234567890";
6
+ /** Test shared secret for local development and integration testing. */
7
+ const HELIX_TEST_SECRET = "tk_test_hx_4a7f2c9d8e1b3f6a5c2d9e8f7b4a1c3d";
8
+ /** Test callback URL for local development. */
9
+ const HELIX_TEST_CALLBACK_URL = "http://localhost:8080/helix/callback";
10
+ /** Callback timestamp validity window in seconds (5 minutes). */
11
+ const TIMESTAMP_WINDOW_SECONDS = 300;
12
+
13
+ //#endregion
14
+ export { TIMESTAMP_WINDOW_SECONDS as a, HELIX_TEST_SPACE_ID as i, HELIX_TEST_CALLBACK_URL as n, HELIX_TEST_SECRET as r, HELIX_DEFAULT_BASE_URL as t };
package/dist/index.d.mts CHANGED
@@ -1,3 +1,5 @@
1
+ import { a as TIMESTAMP_WINDOW_SECONDS, t as HELIX_DEFAULT_BASE_URL } from "./constants-Bq_R93HE.mjs";
2
+
1
3
  //#region src/types.d.ts
2
4
  /** Human-readable status codes returned in the callback `status_code` param. */
3
5
  type StatusCode = "voice_verified" | "voice_enrolled" | "challenge_mismatch" | "synthetic_voice_detected" | "voice_mismatch";
@@ -79,7 +81,7 @@ declare class InvalidSignatureError extends VoiceAuthError {
79
81
  }
80
82
  /** Callback nonce does not match the session nonce — possible CSRF. */
81
83
  declare class NonceMismatchError extends VoiceAuthError {
82
- constructor(expected: string, actual: string);
84
+ constructor();
83
85
  }
84
86
  /** Callback timestamp is outside the 5-minute validity window — possible replay. */
85
87
  declare class ExpiredTimestampError extends VoiceAuthError {
@@ -90,18 +92,4 @@ declare class MalformedCallbackError extends VoiceAuthError {
90
92
  constructor(field: string);
91
93
  }
92
94
  //#endregion
93
- //#region src/constants.d.ts
94
- /** Default Helix base URL for production. */
95
- declare const HELIX_DEFAULT_BASE_URL = "https://capsule.helix.id";
96
- /** Test Space ID for local development and integration testing. */
97
- declare const HELIX_TEST_SPACE_ID = "a1b2c3d4-e5f6-7890-abcd-ef1234567890";
98
- /** Test shared secret for local development and integration testing. */
99
- declare const HELIX_TEST_SECRET = "tk_test_hx_4a7f2c9d8e1b3f6a5c2d9e8f7b4a1c3d";
100
- /** Test callback URL for local development. */
101
- declare const HELIX_TEST_CALLBACK_URL = "http://localhost:8080/helix/callback";
102
- /** Callback timestamp validity window in seconds (5 minutes). */
103
- declare const TIMESTAMP_WINDOW_SECONDS = 300;
104
- /** Mapping of status codes to their HTTP-like numeric codes (for reference). */
105
- declare const STATUS_CODE_NUMBERS: Record<string, string>;
106
- //#endregion
107
- export { type CallbackQuery, type CallbackStatus, type CreateAuthUrlOptions, type CreateAuthUrlResult, ExpiredTimestampError, HELIX_DEFAULT_BASE_URL, HELIX_TEST_CALLBACK_URL, HELIX_TEST_SECRET, HELIX_TEST_SPACE_ID, InvalidSignatureError, MalformedCallbackError, NonceMismatchError, STATUS_CODE_NUMBERS, type StatusCode, TIMESTAMP_WINDOW_SECONDS, type VerifyCallbackOptions, type VerifyCallbackResult, VoiceAuthError, createAuthUrl, verifyCallback };
95
+ export { type CallbackQuery, type CallbackStatus, type CreateAuthUrlOptions, type CreateAuthUrlResult, ExpiredTimestampError, HELIX_DEFAULT_BASE_URL, InvalidSignatureError, MalformedCallbackError, NonceMismatchError, type StatusCode, TIMESTAMP_WINDOW_SECONDS, type VerifyCallbackOptions, type VerifyCallbackResult, VoiceAuthError, createAuthUrl, verifyCallback };
package/dist/index.mjs CHANGED
@@ -1,26 +1,6 @@
1
+ import { a as TIMESTAMP_WINDOW_SECONDS, t as HELIX_DEFAULT_BASE_URL } from "./constants-CVQVsNWb.mjs";
1
2
  import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
2
3
 
3
- //#region src/constants.ts
4
- /** Default Helix base URL for production. */
5
- const HELIX_DEFAULT_BASE_URL = "https://capsule.helix.id";
6
- /** Test Space ID for local development and integration testing. */
7
- const HELIX_TEST_SPACE_ID = "a1b2c3d4-e5f6-7890-abcd-ef1234567890";
8
- /** Test shared secret for local development and integration testing. */
9
- const HELIX_TEST_SECRET = "tk_test_hx_4a7f2c9d8e1b3f6a5c2d9e8f7b4a1c3d";
10
- /** Test callback URL for local development. */
11
- const HELIX_TEST_CALLBACK_URL = "http://localhost:8080/helix/callback";
12
- /** Callback timestamp validity window in seconds (5 minutes). */
13
- const TIMESTAMP_WINDOW_SECONDS = 300;
14
- /** Mapping of status codes to their HTTP-like numeric codes (for reference). */
15
- const STATUS_CODE_NUMBERS = {
16
- voice_verified: "200",
17
- voice_enrolled: "201",
18
- voice_mismatch: "401",
19
- synthetic_voice_detected: "403",
20
- challenge_mismatch: "422"
21
- };
22
-
23
- //#endregion
24
4
  //#region src/auth-url.ts
25
5
  /**
26
6
  * Generate a redirect URL and cryptographic nonce for initiating a Helix voice auth session.
@@ -60,8 +40,8 @@ var InvalidSignatureError = class extends VoiceAuthError {
60
40
  };
61
41
  /** Callback nonce does not match the session nonce — possible CSRF. */
62
42
  var NonceMismatchError = class extends VoiceAuthError {
63
- constructor(expected, actual) {
64
- super(`Nonce mismatch: expected "${expected}", got "${actual}"`, "NONCE_MISMATCH");
43
+ constructor() {
44
+ super("Nonce mismatch", "NONCE_MISMATCH");
65
45
  this.name = "NonceMismatchError";
66
46
  }
67
47
  };
@@ -99,15 +79,17 @@ const REQUIRED_FIELDS = [
99
79
  * or `MalformedCallbackError` if verification fails.
100
80
  */
101
81
  function verifyCallback(query, options) {
102
- for (const field of REQUIRED_FIELDS) if (!query[field]) throw new MalformedCallbackError(field);
103
- if (query.nonce !== options.nonce) throw new NonceMismatchError(options.nonce, query.nonce);
104
- const now = Math.floor(Date.now() / 1e3);
105
- const ts = Number(query.ts);
106
- if (Math.abs(now - ts) > TIMESTAMP_WINDOW_SECONDS) throw new ExpiredTimestampError(query.ts);
82
+ for (const field of REQUIRED_FIELDS) if (query[field] == null || query[field] === "") throw new MalformedCallbackError(field);
107
83
  const { sig, ...rest } = query;
108
84
  const canonical = Object.keys(rest).sort().map((k) => `${k}=${rest[k]}`).join("&");
109
85
  const expected = createHmac("sha256", options.secret).update(canonical).digest("hex");
110
- if (sig.length !== expected.length || !timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) throw new InvalidSignatureError();
86
+ const expectedBuf = Buffer.from(expected, "hex");
87
+ const sigBuf = Buffer.from(sig, "hex");
88
+ if (sigBuf.length !== expectedBuf.length || !timingSafeEqual(sigBuf, expectedBuf)) throw new InvalidSignatureError();
89
+ if (query.nonce !== options.nonce) throw new NonceMismatchError();
90
+ const now = Math.floor(Date.now() / 1e3);
91
+ const ts = Number(query.ts);
92
+ if (Math.abs(now - ts) > TIMESTAMP_WINDOW_SECONDS) throw new ExpiredTimestampError(query.ts);
111
93
  return {
112
94
  status: query.status,
113
95
  statusCode: query.status_code,
@@ -117,4 +99,4 @@ function verifyCallback(query, options) {
117
99
  }
118
100
 
119
101
  //#endregion
120
- export { ExpiredTimestampError, HELIX_DEFAULT_BASE_URL, HELIX_TEST_CALLBACK_URL, HELIX_TEST_SECRET, HELIX_TEST_SPACE_ID, InvalidSignatureError, MalformedCallbackError, NonceMismatchError, STATUS_CODE_NUMBERS, TIMESTAMP_WINDOW_SECONDS, VoiceAuthError, createAuthUrl, verifyCallback };
102
+ export { ExpiredTimestampError, HELIX_DEFAULT_BASE_URL, InvalidSignatureError, MalformedCallbackError, NonceMismatchError, TIMESTAMP_WINDOW_SECONDS, VoiceAuthError, createAuthUrl, verifyCallback };
@@ -0,0 +1,2 @@
1
+ import { i as HELIX_TEST_SPACE_ID, n as HELIX_TEST_CALLBACK_URL, r as HELIX_TEST_SECRET } from "./constants-Bq_R93HE.mjs";
2
+ export { HELIX_TEST_CALLBACK_URL, HELIX_TEST_SECRET, HELIX_TEST_SPACE_ID };
@@ -0,0 +1,3 @@
1
+ import { i as HELIX_TEST_SPACE_ID, n as HELIX_TEST_CALLBACK_URL, r as HELIX_TEST_SECRET } from "./constants-CVQVsNWb.mjs";
2
+
3
+ export { HELIX_TEST_CALLBACK_URL, HELIX_TEST_SECRET, HELIX_TEST_SPACE_ID };
package/package.json CHANGED
@@ -1,26 +1,22 @@
1
1
  {
2
2
  "name": "@helix-id/voice-auth",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Backend SDK for Helix Voice Authentication redirect flow",
5
5
  "type": "module",
6
6
  "exports": {
7
7
  ".": {
8
8
  "import": "./dist/index.mjs",
9
9
  "types": "./dist/index.d.mts"
10
+ },
11
+ "./testing": {
12
+ "import": "./dist/testing.mjs",
13
+ "types": "./dist/testing.d.mts"
10
14
  }
11
15
  },
12
16
  "files": [
13
17
  "dist",
14
18
  "README.md"
15
19
  ],
16
- "scripts": {
17
- "build": "tsdown",
18
- "type-check": "tsc --noEmit",
19
- "test": "vitest run",
20
- "test:watch": "vitest watch",
21
- "test:coverage": "vitest run --coverage",
22
- "lint": "eslint"
23
- },
24
20
  "license": "MIT",
25
21
  "publishConfig": {
26
22
  "access": "public",
@@ -30,11 +26,19 @@
30
26
  "node": ">=18"
31
27
  },
32
28
  "devDependencies": {
33
- "@helix/typescript-config": "workspace:*",
34
- "@helix/eslint-config": "workspace:*",
35
29
  "vitest": "4.0.18",
36
30
  "tsdown": "0.20.1",
37
31
  "typescript": "5.9.3",
38
- "eslint": "9.39.1"
32
+ "eslint": "9.39.1",
33
+ "@helix/typescript-config": "0.0.1",
34
+ "@helix/eslint-config": "0.0.1"
35
+ },
36
+ "scripts": {
37
+ "build": "tsdown",
38
+ "type-check": "tsc --noEmit",
39
+ "test": "vitest run",
40
+ "test:watch": "vitest watch",
41
+ "test:coverage": "vitest run --coverage",
42
+ "lint": "eslint"
39
43
  }
40
- }
44
+ }