@helix-id/voice-auth 0.1.0 → 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 +14 -2
- package/dist/constants-Bq_R93HE.d.mts +13 -0
- package/dist/constants-CVQVsNWb.mjs +14 -0
- package/dist/index.d.mts +4 -16
- package/dist/index.mjs +12 -30
- package/dist/testing.d.mts +2 -0
- package/dist/testing.mjs +3 -0
- package/package.json +19 -14
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
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
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(
|
|
64
|
-
super(
|
|
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 (
|
|
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
|
-
|
|
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,
|
|
102
|
+
export { ExpiredTimestampError, HELIX_DEFAULT_BASE_URL, InvalidSignatureError, MalformedCallbackError, NonceMismatchError, TIMESTAMP_WINDOW_SECONDS, VoiceAuthError, createAuthUrl, verifyCallback };
|
package/dist/testing.mjs
ADDED
package/package.json
CHANGED
|
@@ -1,25 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@helix-id/voice-auth",
|
|
3
|
-
"version": "0.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
|
-
"dist"
|
|
17
|
+
"dist",
|
|
18
|
+
"README.md"
|
|
14
19
|
],
|
|
15
|
-
"scripts": {
|
|
16
|
-
"build": "tsdown",
|
|
17
|
-
"type-check": "tsc --noEmit",
|
|
18
|
-
"test": "vitest run",
|
|
19
|
-
"test:watch": "vitest watch",
|
|
20
|
-
"test:coverage": "vitest run --coverage",
|
|
21
|
-
"lint": "eslint"
|
|
22
|
-
},
|
|
23
20
|
"license": "MIT",
|
|
24
21
|
"publishConfig": {
|
|
25
22
|
"access": "public",
|
|
@@ -29,11 +26,19 @@
|
|
|
29
26
|
"node": ">=18"
|
|
30
27
|
},
|
|
31
28
|
"devDependencies": {
|
|
32
|
-
"@helix/typescript-config": "workspace:*",
|
|
33
|
-
"@helix/eslint-config": "workspace:*",
|
|
34
29
|
"vitest": "4.0.18",
|
|
35
30
|
"tsdown": "0.20.1",
|
|
36
31
|
"typescript": "5.9.3",
|
|
37
|
-
"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"
|
|
38
43
|
}
|
|
39
|
-
}
|
|
44
|
+
}
|