@helix-id/voice-auth 0.1.0
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 +135 -0
- package/dist/index.d.mts +107 -0
- package/dist/index.mjs +120 -0
- package/package.json +39 -0
package/README.md
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# @helix-id/voice-auth
|
|
2
|
+
|
|
3
|
+
Backend SDK for [Helix Voice Authentication](https://helix.id) — a redirect-based voice identity verification flow.
|
|
4
|
+
|
|
5
|
+
Your app redirects users to Helix, where they speak a short numeric challenge. Helix verifies their voice and redirects back with an HMAC-signed result. This package handles nonce generation, URL building, and callback verification.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @helix-id/voice-auth
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
### 1. Generate the Redirect URL (backend)
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
import { createAuthUrl } from "@helix-id/voice-auth";
|
|
19
|
+
|
|
20
|
+
app.get("/auth/start", (req, res) => {
|
|
21
|
+
const state = Buffer.from(JSON.stringify({ action: "approve_tx", txId: "abc123", returnTo: "/dashboard" })).toString(
|
|
22
|
+
"base64",
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
const { url, nonce } = createAuthUrl({
|
|
26
|
+
spaceId: process.env.HELIX_SPACE_ID,
|
|
27
|
+
state,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
req.session.helixNonce = nonce;
|
|
31
|
+
res.json({ url });
|
|
32
|
+
});
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### 2. Redirect the User (frontend)
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
const { url } = await fetch("/auth/start").then((r) => r.json());
|
|
39
|
+
window.location.href = url;
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### 3. Verify the Callback (backend)
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
import { verifyCallback, InvalidSignatureError, NonceMismatchError, ExpiredTimestampError } from "@helix-id/voice-auth";
|
|
46
|
+
|
|
47
|
+
app.get("/auth/callback", (req, res) => {
|
|
48
|
+
try {
|
|
49
|
+
const result = verifyCallback(req.query, {
|
|
50
|
+
nonce: req.session.helixNonce,
|
|
51
|
+
secret: process.env.HELIX_SECRET,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
req.session.helixNonce = null;
|
|
55
|
+
|
|
56
|
+
const state = JSON.parse(Buffer.from(result.state, "base64").toString());
|
|
57
|
+
|
|
58
|
+
if (result.status === "verified") {
|
|
59
|
+
res.redirect(state.returnTo);
|
|
60
|
+
} else {
|
|
61
|
+
res.redirect(`${state.returnTo}?error=${result.statusCode}`);
|
|
62
|
+
}
|
|
63
|
+
} catch (error) {
|
|
64
|
+
if (error instanceof InvalidSignatureError) {
|
|
65
|
+
res.status(401).json({ error: "Invalid signature" });
|
|
66
|
+
} else if (error instanceof NonceMismatchError) {
|
|
67
|
+
res.status(401).json({ error: "Nonce mismatch" });
|
|
68
|
+
} else if (error instanceof ExpiredTimestampError) {
|
|
69
|
+
res.status(401).json({ error: "Callback expired" });
|
|
70
|
+
} else {
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## API
|
|
78
|
+
|
|
79
|
+
### `createAuthUrl(options)`
|
|
80
|
+
|
|
81
|
+
Generates a redirect URL and cryptographic nonce.
|
|
82
|
+
|
|
83
|
+
| Option | Type | Required | Description |
|
|
84
|
+
| --------- | -------- | -------- | -------------------------------------------------------- |
|
|
85
|
+
| `spaceId` | `string` | Yes | Your Space ID (UUID) |
|
|
86
|
+
| `state` | `string` | Yes | Opaque state echoed back in callback |
|
|
87
|
+
| `baseUrl` | `string` | No | Override Helix URL (default: `https://capsule.helix.id`) |
|
|
88
|
+
|
|
89
|
+
Returns `{ url: string, nonce: string }`. Store `nonce` in the user's session.
|
|
90
|
+
|
|
91
|
+
### `verifyCallback(query, options)`
|
|
92
|
+
|
|
93
|
+
Verifies the HMAC-signed callback and parses the result.
|
|
94
|
+
|
|
95
|
+
| Option | Type | Required | Description |
|
|
96
|
+
| -------- | -------- | -------- | --------------------------------------------- |
|
|
97
|
+
| `nonce` | `string` | Yes | Nonce from `createAuthUrl`, stored in session |
|
|
98
|
+
| `secret` | `string` | Yes | Shared secret from Helix admin panel |
|
|
99
|
+
|
|
100
|
+
Returns `{ status, statusCode, logId, state }`.
|
|
101
|
+
|
|
102
|
+
Throws: `InvalidSignatureError`, `NonceMismatchError`, `ExpiredTimestampError`, `MalformedCallbackError`.
|
|
103
|
+
|
|
104
|
+
## Status Codes
|
|
105
|
+
|
|
106
|
+
| status_code | status | Description |
|
|
107
|
+
| -------------------------- | ---------- | ------------------------------------------ |
|
|
108
|
+
| `voice_verified` | `verified` | Voice matched successfully |
|
|
109
|
+
| `voice_enrolled` | `verified` | First-time user — voice profile created |
|
|
110
|
+
| `challenge_mismatch` | `failed` | Spoken words didn't match displayed digits |
|
|
111
|
+
| `synthetic_voice_detected` | `failed` | Audio flagged as AI-generated or spoofed |
|
|
112
|
+
| `voice_mismatch` | `failed` | Voice didn't match enrolled profile |
|
|
113
|
+
|
|
114
|
+
## Testing
|
|
115
|
+
|
|
116
|
+
Use the test credentials for local development:
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
import { createAuthUrl, HELIX_TEST_SPACE_ID, HELIX_TEST_SECRET } from "@helix-id/voice-auth";
|
|
120
|
+
|
|
121
|
+
const { url, nonce } = createAuthUrl({
|
|
122
|
+
spaceId: HELIX_TEST_SPACE_ID,
|
|
123
|
+
state: "test",
|
|
124
|
+
});
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
| Credential | Value |
|
|
128
|
+
| ------------ | --------------------------------------------- |
|
|
129
|
+
| Space ID | `a1b2c3d4-e5f6-7890-abcd-ef1234567890` |
|
|
130
|
+
| Secret | `tk_test_hx_4a7f2c9d8e1b3f6a5c2d9e8f7b4a1c3d` |
|
|
131
|
+
| Callback URL | `http://localhost:8080/helix/callback` |
|
|
132
|
+
|
|
133
|
+
## License
|
|
134
|
+
|
|
135
|
+
MIT
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
//#region src/types.d.ts
|
|
2
|
+
/** Human-readable status codes returned in the callback `status_code` param. */
|
|
3
|
+
type StatusCode = "voice_verified" | "voice_enrolled" | "challenge_mismatch" | "synthetic_voice_detected" | "voice_mismatch";
|
|
4
|
+
/** Generic status from callback `status` param. */
|
|
5
|
+
type CallbackStatus = "verified" | "failed";
|
|
6
|
+
/** Raw query parameters Helix appends to the callback URL. */
|
|
7
|
+
interface CallbackQuery {
|
|
8
|
+
status: string;
|
|
9
|
+
status_code: string;
|
|
10
|
+
log_id: string;
|
|
11
|
+
space_id: string;
|
|
12
|
+
nonce: string;
|
|
13
|
+
state: string;
|
|
14
|
+
ts: string;
|
|
15
|
+
sig: string;
|
|
16
|
+
}
|
|
17
|
+
/** Options for `createAuthUrl`. */
|
|
18
|
+
interface CreateAuthUrlOptions {
|
|
19
|
+
/** Your Space ID (UUID from Helix admin panel). */
|
|
20
|
+
spaceId: string;
|
|
21
|
+
/** Opaque state string echoed back in the callback. Base64-encode JSON before passing. */
|
|
22
|
+
state: string;
|
|
23
|
+
/** Override the Helix base URL (defaults to `https://capsule.helix.id`). */
|
|
24
|
+
baseUrl?: string;
|
|
25
|
+
}
|
|
26
|
+
/** Result from `createAuthUrl`. */
|
|
27
|
+
interface CreateAuthUrlResult {
|
|
28
|
+
/** Full URL to redirect the user to. */
|
|
29
|
+
url: string;
|
|
30
|
+
/** Cryptographically random 32-char hex nonce. Store this in the user's session. */
|
|
31
|
+
nonce: string;
|
|
32
|
+
}
|
|
33
|
+
/** Options for `verifyCallback`. */
|
|
34
|
+
interface VerifyCallbackOptions {
|
|
35
|
+
/** The nonce stored in the user's session (from `createAuthUrl`). */
|
|
36
|
+
nonce: string;
|
|
37
|
+
/** Your shared secret from the Helix admin panel. */
|
|
38
|
+
secret: string;
|
|
39
|
+
}
|
|
40
|
+
/** Parsed and verified callback result. */
|
|
41
|
+
interface VerifyCallbackResult {
|
|
42
|
+
/** `"verified"` or `"failed"`. */
|
|
43
|
+
status: CallbackStatus;
|
|
44
|
+
/** Detailed status code (e.g. `"voice_verified"`, `"voice_mismatch"`). */
|
|
45
|
+
statusCode: StatusCode;
|
|
46
|
+
/** UUID of the verification log entry. */
|
|
47
|
+
logId: string;
|
|
48
|
+
/** The state string you passed to `createAuthUrl`, echoed back unchanged. */
|
|
49
|
+
state: string;
|
|
50
|
+
}
|
|
51
|
+
//#endregion
|
|
52
|
+
//#region src/auth-url.d.ts
|
|
53
|
+
/**
|
|
54
|
+
* Generate a redirect URL and cryptographic nonce for initiating a Helix voice auth session.
|
|
55
|
+
*
|
|
56
|
+
* The nonce must be stored in the user's server-side session and passed to `verifyCallback`
|
|
57
|
+
* when the callback arrives.
|
|
58
|
+
*/
|
|
59
|
+
declare function createAuthUrl(options: CreateAuthUrlOptions): CreateAuthUrlResult;
|
|
60
|
+
//#endregion
|
|
61
|
+
//#region src/verify.d.ts
|
|
62
|
+
/**
|
|
63
|
+
* Verify an HMAC-SHA256 signed callback from Helix and parse the result.
|
|
64
|
+
*
|
|
65
|
+
* Throws `NonceMismatchError`, `ExpiredTimestampError`, `InvalidSignatureError`,
|
|
66
|
+
* or `MalformedCallbackError` if verification fails.
|
|
67
|
+
*/
|
|
68
|
+
declare function verifyCallback(query: CallbackQuery, options: VerifyCallbackOptions): VerifyCallbackResult;
|
|
69
|
+
//#endregion
|
|
70
|
+
//#region src/errors.d.ts
|
|
71
|
+
/** Base error class for all @helix-id/voice-auth errors. */
|
|
72
|
+
declare class VoiceAuthError extends Error {
|
|
73
|
+
readonly code: string;
|
|
74
|
+
constructor(message: string, code: string);
|
|
75
|
+
}
|
|
76
|
+
/** HMAC signature verification failed — payload may have been tampered with. */
|
|
77
|
+
declare class InvalidSignatureError extends VoiceAuthError {
|
|
78
|
+
constructor();
|
|
79
|
+
}
|
|
80
|
+
/** Callback nonce does not match the session nonce — possible CSRF. */
|
|
81
|
+
declare class NonceMismatchError extends VoiceAuthError {
|
|
82
|
+
constructor(expected: string, actual: string);
|
|
83
|
+
}
|
|
84
|
+
/** Callback timestamp is outside the 5-minute validity window — possible replay. */
|
|
85
|
+
declare class ExpiredTimestampError extends VoiceAuthError {
|
|
86
|
+
constructor(ts: string);
|
|
87
|
+
}
|
|
88
|
+
/** A required query parameter is missing or malformed. */
|
|
89
|
+
declare class MalformedCallbackError extends VoiceAuthError {
|
|
90
|
+
constructor(field: string);
|
|
91
|
+
}
|
|
92
|
+
//#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 };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
2
|
+
|
|
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
|
+
//#region src/auth-url.ts
|
|
25
|
+
/**
|
|
26
|
+
* Generate a redirect URL and cryptographic nonce for initiating a Helix voice auth session.
|
|
27
|
+
*
|
|
28
|
+
* The nonce must be stored in the user's server-side session and passed to `verifyCallback`
|
|
29
|
+
* when the callback arrives.
|
|
30
|
+
*/
|
|
31
|
+
function createAuthUrl(options) {
|
|
32
|
+
const { spaceId, state, baseUrl = HELIX_DEFAULT_BASE_URL } = options;
|
|
33
|
+
const nonce = randomBytes(16).toString("hex");
|
|
34
|
+
const url = new URL("/voice-auth", baseUrl);
|
|
35
|
+
url.searchParams.set("space_id", spaceId);
|
|
36
|
+
url.searchParams.set("nonce", nonce);
|
|
37
|
+
url.searchParams.set("state", state);
|
|
38
|
+
return {
|
|
39
|
+
url: url.toString(),
|
|
40
|
+
nonce
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
//#endregion
|
|
45
|
+
//#region src/errors.ts
|
|
46
|
+
/** Base error class for all @helix-id/voice-auth errors. */
|
|
47
|
+
var VoiceAuthError = class extends Error {
|
|
48
|
+
constructor(message, code) {
|
|
49
|
+
super(message);
|
|
50
|
+
this.name = "VoiceAuthError";
|
|
51
|
+
this.code = code;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
/** HMAC signature verification failed — payload may have been tampered with. */
|
|
55
|
+
var InvalidSignatureError = class extends VoiceAuthError {
|
|
56
|
+
constructor() {
|
|
57
|
+
super("Invalid callback signature", "INVALID_SIGNATURE");
|
|
58
|
+
this.name = "InvalidSignatureError";
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
/** Callback nonce does not match the session nonce — possible CSRF. */
|
|
62
|
+
var NonceMismatchError = class extends VoiceAuthError {
|
|
63
|
+
constructor(expected, actual) {
|
|
64
|
+
super(`Nonce mismatch: expected "${expected}", got "${actual}"`, "NONCE_MISMATCH");
|
|
65
|
+
this.name = "NonceMismatchError";
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
/** Callback timestamp is outside the 5-minute validity window — possible replay. */
|
|
69
|
+
var ExpiredTimestampError = class extends VoiceAuthError {
|
|
70
|
+
constructor(ts) {
|
|
71
|
+
super(`Callback timestamp expired: ${ts}`, "EXPIRED_TIMESTAMP");
|
|
72
|
+
this.name = "ExpiredTimestampError";
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
/** A required query parameter is missing or malformed. */
|
|
76
|
+
var MalformedCallbackError = class extends VoiceAuthError {
|
|
77
|
+
constructor(field) {
|
|
78
|
+
super(`Missing or malformed callback parameter: ${field}`, "MALFORMED_CALLBACK");
|
|
79
|
+
this.name = "MalformedCallbackError";
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
//#endregion
|
|
84
|
+
//#region src/verify.ts
|
|
85
|
+
const REQUIRED_FIELDS = [
|
|
86
|
+
"status",
|
|
87
|
+
"status_code",
|
|
88
|
+
"log_id",
|
|
89
|
+
"space_id",
|
|
90
|
+
"nonce",
|
|
91
|
+
"state",
|
|
92
|
+
"ts",
|
|
93
|
+
"sig"
|
|
94
|
+
];
|
|
95
|
+
/**
|
|
96
|
+
* Verify an HMAC-SHA256 signed callback from Helix and parse the result.
|
|
97
|
+
*
|
|
98
|
+
* Throws `NonceMismatchError`, `ExpiredTimestampError`, `InvalidSignatureError`,
|
|
99
|
+
* or `MalformedCallbackError` if verification fails.
|
|
100
|
+
*/
|
|
101
|
+
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);
|
|
107
|
+
const { sig, ...rest } = query;
|
|
108
|
+
const canonical = Object.keys(rest).sort().map((k) => `${k}=${rest[k]}`).join("&");
|
|
109
|
+
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();
|
|
111
|
+
return {
|
|
112
|
+
status: query.status,
|
|
113
|
+
statusCode: query.status_code,
|
|
114
|
+
logId: query.log_id,
|
|
115
|
+
state: query.state
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
//#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 };
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@helix-id/voice-auth",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Backend SDK for Helix Voice Authentication redirect flow",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"import": "./dist/index.mjs",
|
|
9
|
+
"types": "./dist/index.d.mts"
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"dist"
|
|
14
|
+
],
|
|
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
|
+
"license": "MIT",
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public",
|
|
26
|
+
"registry": "https://registry.npmjs.org/"
|
|
27
|
+
},
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=18"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@helix/typescript-config": "workspace:*",
|
|
33
|
+
"@helix/eslint-config": "workspace:*",
|
|
34
|
+
"vitest": "4.0.18",
|
|
35
|
+
"tsdown": "0.20.1",
|
|
36
|
+
"typescript": "5.9.3",
|
|
37
|
+
"eslint": "9.39.1"
|
|
38
|
+
}
|
|
39
|
+
}
|