@vex-chat/spire 4.1.0 → 5.0.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 +8 -11
- package/dist/Database.js +2 -6
- package/dist/Database.js.map +1 -1
- package/dist/Spire.d.ts +2 -10
- package/dist/Spire.js +24 -60
- package/dist/Spire.js.map +1 -1
- package/dist/run.js +7 -12
- package/dist/run.js.map +1 -1
- package/dist/server/cliPasskeyPage.js +491 -100
- package/dist/server/cliPasskeyPage.js.map +1 -1
- package/dist/spireListenPort.d.ts +1 -2
- package/dist/spireListenPort.js +1 -2
- package/dist/spireListenPort.js.map +1 -1
- package/dist/utils/loadEnv.d.ts +0 -1
- package/dist/utils/loadEnv.js +4 -14
- package/dist/utils/loadEnv.js.map +1 -1
- package/dist/utils/spireXSignOpenAsync.d.ts +1 -3
- package/dist/utils/spireXSignOpenAsync.js +4 -9
- package/dist/utils/spireXSignOpenAsync.js.map +1 -1
- package/package.json +3 -4
- package/src/Database.ts +2 -11
- package/src/Spire.ts +31 -80
- package/src/__tests__/cliPasskeyPage.spec.ts +11 -1
- package/src/__tests__/loadEnv.spec.ts +2 -26
- package/src/__tests__/preKeySignature.spec.ts +1 -6
- package/src/__tests__/spireListenPort.spec.ts +1 -1
- package/src/run.ts +6 -13
- package/src/server/cliPasskeyPage.ts +491 -100
- package/src/spireListenPort.ts +1 -2
- package/src/utils/loadEnv.ts +4 -16
- package/src/utils/spireXSignOpenAsync.ts +4 -9
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cliPasskeyPage.js","sourceRoot":"","sources":["../../src/server/cliPasskeyPage.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,OAAO,MAAM,SAAS,CAAC;AAE9B,MAAM,GAAG,GAAG;IACR,oBAAoB;IACpB,iBAAiB;IACjB,0DAA0D;IAC1D,oBAAoB;IACpB,wBAAwB;IACxB,gBAAgB;IAChB,4BAA4B;IAC5B,2BAA2B;CAC9B,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAEb,MAAM,gBAAgB,GAAG
|
|
1
|
+
{"version":3,"file":"cliPasskeyPage.js","sourceRoot":"","sources":["../../src/server/cliPasskeyPage.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,OAAO,MAAM,SAAS,CAAC;AAE9B,MAAM,GAAG,GAAG;IACR,oBAAoB;IACpB,iBAAiB;IACjB,0DAA0D;IAC1D,oBAAoB;IACpB,wBAAwB;IACxB,gBAAgB;IAChB,4BAA4B;IAC5B,2BAA2B;CAC9B,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAEb,MAAM,gBAAgB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QAg8BjB,CAAC;AAET,MAAM,CAAC,MAAM,uBAAuB,GAAG,GAAmB,EAAE;IACxD,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAEhC,MAAM,CAAC,GAAG,CAAC,cAAc,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,EAAE;QACrC,GAAG,CAAC,GAAG,CAAC;YACJ,eAAe,EAAE,UAAU;YAC3B,yBAAyB,EAAE,GAAG;YAC9B,cAAc,EAAE,0BAA0B;YAC1C,oBAAoB,EAChB,uEAAuE;YAC3E,iBAAiB,EAAE,aAAa;YAChC,wBAAwB,EAAE,SAAS;SACtC,CAAC,CAAC;QACH,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC;IAC3C,CAAC,CAAC,CAAC;IAEH,OAAO,MAAM,CAAC;AAClB,CAAC,CAAC"}
|
|
@@ -4,8 +4,7 @@
|
|
|
4
4
|
* Commercial licenses available at vex.wtf
|
|
5
5
|
*/
|
|
6
6
|
/**
|
|
7
|
-
* Default HTTP/WS port for the Spire API
|
|
8
|
-
* `API_PORT` / `apiPort` is set. Clients can tell profiles apart via `GET /status`.
|
|
7
|
+
* Default HTTP/WS port for the Spire API unless `API_PORT` / `apiPort` is set.
|
|
9
8
|
*/
|
|
10
9
|
export declare const DEFAULT_SPIRE_API_PORT = 16777;
|
|
11
10
|
/**
|
package/dist/spireListenPort.js
CHANGED
|
@@ -4,8 +4,7 @@
|
|
|
4
4
|
* Commercial licenses available at vex.wtf
|
|
5
5
|
*/
|
|
6
6
|
/**
|
|
7
|
-
* Default HTTP/WS port for the Spire API
|
|
8
|
-
* `API_PORT` / `apiPort` is set. Clients can tell profiles apart via `GET /status`.
|
|
7
|
+
* Default HTTP/WS port for the Spire API unless `API_PORT` / `apiPort` is set.
|
|
9
8
|
*/
|
|
10
9
|
export const DEFAULT_SPIRE_API_PORT = 16777;
|
|
11
10
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"spireListenPort.js","sourceRoot":"","sources":["../src/spireListenPort.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH
|
|
1
|
+
{"version":3,"file":"spireListenPort.js","sourceRoot":"","sources":["../src/spireListenPort.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH;;GAEG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG,KAAK,CAAC;AAE5C;;GAEG;AACH,MAAM,UAAU,sBAAsB,CAAC,QAA4B;IAC/D,IACI,OAAO,QAAQ,KAAK,QAAQ;QAC5B,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC;QACzB,QAAQ,GAAG,CAAC,EACd,CAAC;QACC,OAAO,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;IAChC,CAAC;IACD,OAAO,sBAAsB,CAAC;AAClC,CAAC"}
|
package/dist/utils/loadEnv.d.ts
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
* Licensed under AGPL-3.0. See LICENSE for details.
|
|
4
4
|
* Commercial licenses available at vex.wtf
|
|
5
5
|
*/
|
|
6
|
-
export declare function isSpireFipsEnabled(value: string | undefined): boolean;
|
|
7
6
|
export declare function loadEnv(): void;
|
|
8
7
|
export declare function normalizeEnvValue(value: string): string;
|
|
9
8
|
export declare function validateSpireRuntimeEnv(env: Record<string, string | undefined>): void;
|
package/dist/utils/loadEnv.js
CHANGED
|
@@ -9,7 +9,6 @@ const NORMALIZED_ENV_VARS = [
|
|
|
9
9
|
...REQUIRED_ENV_VARS,
|
|
10
10
|
"API_PORT",
|
|
11
11
|
"SPIRE_TRUST_PROXY_HOPS",
|
|
12
|
-
"SPIRE_FIPS",
|
|
13
12
|
"SPIRE_PASSKEY_RP_ID",
|
|
14
13
|
"SPIRE_PASSKEY_RP_NAME",
|
|
15
14
|
"SPIRE_PASSKEY_ORIGINS",
|
|
@@ -19,10 +18,6 @@ const NORMALIZED_ENV_VARS = [
|
|
|
19
18
|
];
|
|
20
19
|
const HEX_BYTES_RE = /^(?:[0-9a-fA-F]{2})+$/;
|
|
21
20
|
const TWEETNACL_SPK_HEX_LENGTH = 128;
|
|
22
|
-
export function isSpireFipsEnabled(value) {
|
|
23
|
-
const normalized = value == null ? "" : normalizeEnvValue(value);
|
|
24
|
-
return normalized === "1" || normalized === "true";
|
|
25
|
-
}
|
|
26
21
|
/* Populate process.env with vars from .env and verify required vars are present. */
|
|
27
22
|
export function loadEnv() {
|
|
28
23
|
config();
|
|
@@ -51,18 +46,13 @@ export function validateSpireRuntimeEnv(env) {
|
|
|
51
46
|
const spk = normalizeEnvValue(env["SPK"] ?? "");
|
|
52
47
|
const jwtSecret = normalizeEnvValue(env["JWT_SECRET"] ?? "");
|
|
53
48
|
if (!HEX_BYTES_RE.test(spk)) {
|
|
54
|
-
throw new Error("SPK must be an even-length hex string. Generate one with `pnpm --filter @vex-chat/spire gen-spk
|
|
55
|
-
}
|
|
56
|
-
if (isSpireFipsEnabled(env["SPIRE_FIPS"])) {
|
|
57
|
-
if (spk.length === TWEETNACL_SPK_HEX_LENGTH) {
|
|
58
|
-
throw new Error("SPIRE_FIPS=true requires an SPK from `pnpm --filter @vex-chat/spire gen-spk-fips`; the configured SPK looks like a tweetnacl key.");
|
|
59
|
-
}
|
|
49
|
+
throw new Error("SPK must be an even-length hex string. Generate one with `pnpm --filter @vex-chat/spire gen-spk`.");
|
|
60
50
|
}
|
|
61
|
-
|
|
62
|
-
throw new Error("
|
|
51
|
+
if (spk.length !== TWEETNACL_SPK_HEX_LENGTH) {
|
|
52
|
+
throw new Error("SPK must be 128 hex characters from `pnpm --filter @vex-chat/spire gen-spk`.");
|
|
63
53
|
}
|
|
64
54
|
if (jwtSecret.length === 0) {
|
|
65
|
-
throw new Error("JWT_SECRET must be set. Generate one with `pnpm --filter @vex-chat/spire gen-spk
|
|
55
|
+
throw new Error("JWT_SECRET must be set. Generate one with `pnpm --filter @vex-chat/spire gen-spk`.");
|
|
66
56
|
}
|
|
67
57
|
if (jwtSecret.length < 32) {
|
|
68
58
|
throw new Error("JWT_SECRET must contain at least 32 characters.");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"loadEnv.js","sourceRoot":"","sources":["../../src/utils/loadEnv.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEhC,MAAM,iBAAiB,GAAG,CAAC,SAAS,EAAE,YAAY,EAAE,KAAK,CAAU,CAAC;AACpE,MAAM,mBAAmB,GAAG;IACxB,GAAG,iBAAiB;IACpB,UAAU;IACV,wBAAwB;IACxB,
|
|
1
|
+
{"version":3,"file":"loadEnv.js","sourceRoot":"","sources":["../../src/utils/loadEnv.ts"],"names":[],"mappings":"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"}
|
|
@@ -3,7 +3,5 @@
|
|
|
3
3
|
* Licensed under AGPL-3.0. See LICENSE for details.
|
|
4
4
|
* Commercial licenses available at vex.wtf
|
|
5
5
|
*/
|
|
6
|
-
/**
|
|
7
|
-
* Ed25519 detached-verify open: sync on tweetnacl, async for FIPS (Web Crypto).
|
|
8
|
-
*/
|
|
6
|
+
/** Ed25519 detached-verify open. */
|
|
9
7
|
export declare function spireXSignOpenAsync(signedMessage: Uint8Array, publicKey: Uint8Array): Promise<null | Uint8Array>;
|
|
@@ -3,14 +3,9 @@
|
|
|
3
3
|
* Licensed under AGPL-3.0. See LICENSE for details.
|
|
4
4
|
* Commercial licenses available at vex.wtf
|
|
5
5
|
*/
|
|
6
|
-
import {
|
|
7
|
-
/**
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
export async function spireXSignOpenAsync(signedMessage, publicKey) {
|
|
11
|
-
if (getCryptoProfile() === "fips") {
|
|
12
|
-
return xSignOpenAsync(signedMessage, publicKey);
|
|
13
|
-
}
|
|
14
|
-
return xSignOpen(signedMessage, publicKey);
|
|
6
|
+
import { xSignOpen } from "@vex-chat/crypto";
|
|
7
|
+
/** Ed25519 detached-verify open. */
|
|
8
|
+
export function spireXSignOpenAsync(signedMessage, publicKey) {
|
|
9
|
+
return Promise.resolve(xSignOpen(signedMessage, publicKey));
|
|
15
10
|
}
|
|
16
11
|
//# sourceMappingURL=spireXSignOpenAsync.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"spireXSignOpenAsync.js","sourceRoot":"","sources":["../../src/utils/spireXSignOpenAsync.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"spireXSignOpenAsync.js","sourceRoot":"","sources":["../../src/utils/spireXSignOpenAsync.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAE7C,oCAAoC;AACpC,MAAM,UAAU,mBAAmB,CAC/B,aAAyB,EACzB,SAAqB;IAErB,OAAO,OAAO,CAAC,OAAO,CAAC,SAAS,CAAC,aAAa,EAAE,SAAS,CAAC,CAAC,CAAC;AAChE,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vex-chat/spire",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "5.0.0",
|
|
4
4
|
"description": "Vex server implementation in NodeJS.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"typescript-eslint": "8.58.1",
|
|
56
56
|
"vitest": "4.1.4",
|
|
57
57
|
"@vex-chat/eslint-config": "0.0.1",
|
|
58
|
-
"@vex-chat/libvex": "^
|
|
58
|
+
"@vex-chat/libvex": "^10.0.0"
|
|
59
59
|
},
|
|
60
60
|
"typeCoverage": {
|
|
61
61
|
"atLeast": 95,
|
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
"uuid": "^14.0.0",
|
|
84
84
|
"ws": "8.21.0",
|
|
85
85
|
"zod": "^4.3.6",
|
|
86
|
-
"@vex-chat/crypto": "^
|
|
86
|
+
"@vex-chat/crypto": "^12.0.0",
|
|
87
87
|
"@vex-chat/types": "^4.3.3"
|
|
88
88
|
},
|
|
89
89
|
"scripts": {
|
|
@@ -108,7 +108,6 @@
|
|
|
108
108
|
"stress:docs-pack": "pnpm run integration:docs-pack",
|
|
109
109
|
"stress:llm-triage": "pnpm run integration:llm-triage",
|
|
110
110
|
"gen-spk": "node scripts/gen-spk.js",
|
|
111
|
-
"gen-spk-fips": "node scripts/gen-spk-fips.js",
|
|
112
111
|
"docs": "node ./scripts/generate-docs.js",
|
|
113
112
|
"service:status-monitor": "node ./services/status-monitor/index.js",
|
|
114
113
|
"service:status-monitor:clear-history": "node ./services/status-monitor/clear-history.js",
|
package/src/Database.ts
CHANGED
|
@@ -41,11 +41,7 @@ import * as fs from "node:fs/promises";
|
|
|
41
41
|
import path from "node:path";
|
|
42
42
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
43
43
|
|
|
44
|
-
import {
|
|
45
|
-
fipsEcdhRawPublicKeyFromEcdsaSpkiAsync,
|
|
46
|
-
getCryptoProfile,
|
|
47
|
-
XUtils,
|
|
48
|
-
} from "@vex-chat/crypto";
|
|
44
|
+
import { XUtils } from "@vex-chat/crypto";
|
|
49
45
|
import {
|
|
50
46
|
ACCOUNT_PASSWORD_MAX_LENGTH,
|
|
51
47
|
ACCOUNT_PASSWORD_MIN_LENGTH,
|
|
@@ -677,15 +673,10 @@ export class Database extends EventEmitter {
|
|
|
677
673
|
if (!preKey) {
|
|
678
674
|
throw new Error("Failed to get prekey.");
|
|
679
675
|
}
|
|
680
|
-
const signKeyBytes = XUtils.decodeHex(device.signKey);
|
|
681
|
-
const signKey =
|
|
682
|
-
getCryptoProfile() === "fips"
|
|
683
|
-
? await fipsEcdhRawPublicKeyFromEcdsaSpkiAsync(signKeyBytes)
|
|
684
|
-
: signKeyBytes;
|
|
685
676
|
const keyBundle: KeyBundle = {
|
|
686
677
|
otk,
|
|
687
678
|
preKey,
|
|
688
|
-
signKey,
|
|
679
|
+
signKey: XUtils.decodeHex(device.signKey),
|
|
689
680
|
};
|
|
690
681
|
return keyBundle;
|
|
691
682
|
}
|
package/src/Spire.ts
CHANGED
|
@@ -25,10 +25,8 @@ import express from "express";
|
|
|
25
25
|
import { XUtils } from "@vex-chat/crypto";
|
|
26
26
|
import {
|
|
27
27
|
type KeyPair,
|
|
28
|
-
setCryptoProfile,
|
|
29
28
|
xRandomBytes,
|
|
30
29
|
xSignKeyPairFromSecret,
|
|
31
|
-
xSignKeyPairFromSecretAsync,
|
|
32
30
|
} from "@vex-chat/crypto";
|
|
33
31
|
import {
|
|
34
32
|
ACCOUNT_PASSWORD_MAX_LENGTH,
|
|
@@ -238,52 +236,18 @@ const getCommitSha = (): string => {
|
|
|
238
236
|
export interface SpireOptions {
|
|
239
237
|
/**
|
|
240
238
|
* TCP port for the HTTP/WS server. If omitted, `run.ts` + `resolveSpireListenPort`
|
|
241
|
-
* use the default (16777
|
|
239
|
+
* use the default (16777). Env: `API_PORT`.
|
|
242
240
|
*/
|
|
243
241
|
apiPort?: number;
|
|
244
|
-
/** Default `tweetnacl`. For `fips`, use `Spire.createAsync` (FIPS key load is async). */
|
|
245
|
-
cryptoProfile?: "fips" | "tweetnacl";
|
|
246
242
|
dbType?: "mysql" | "sqlite3" | "sqlite3mem" | "sqlite";
|
|
247
243
|
}
|
|
248
244
|
|
|
249
|
-
export function resolveTrustProxyHops(value: string | undefined): number {
|
|
250
|
-
if (value === undefined || value.trim() === "") {
|
|
251
|
-
return 0;
|
|
252
|
-
}
|
|
253
|
-
const hops = Number(value);
|
|
254
|
-
if (!Number.isInteger(hops) || hops < 0 || hops > 10) {
|
|
255
|
-
throw new Error(
|
|
256
|
-
"SPIRE_TRUST_PROXY_HOPS must be an integer from 0 to 10.",
|
|
257
|
-
);
|
|
258
|
-
}
|
|
259
|
-
return hops;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
/**
|
|
263
|
-
* Masks identifying material in the access-log URL: hyphenated UUIDs, and
|
|
264
|
-
* `/device/...` path segments that hold public-key material (32B ed25519 hex, or
|
|
265
|
-
* a longer P-256 SPKI hex in FIPS) — not the same pattern as a UUID.
|
|
266
|
-
*/
|
|
267
|
-
function redactAccessLogUrl(url: string): string {
|
|
268
|
-
let s = url.replace(
|
|
269
|
-
/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi,
|
|
270
|
-
"[uuid]",
|
|
271
|
-
);
|
|
272
|
-
// Device id is a hex public key, not a UUID: strip the segment after /device/
|
|
273
|
-
s = s.replace(/(\/device\/)([0-9a-fA-F]{16,})(?=[/?#]|$)/g, "$1[device]");
|
|
274
|
-
return s;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
/** FIPS: sign key loaded inside `Spire.createAsync` before the constructor runs. */
|
|
278
|
-
let pendingFipsKeyPair: KeyPair | null = null;
|
|
279
|
-
|
|
280
245
|
export class Spire extends EventEmitter {
|
|
281
246
|
private actionTokens = new Map<string, ActionToken>();
|
|
282
247
|
private api = express();
|
|
283
248
|
private calls: CallManager;
|
|
284
249
|
private clients: ClientManager[] = [];
|
|
285
250
|
private readonly commitSha = getCommitSha();
|
|
286
|
-
private readonly cryptoProfile: "fips" | "tweetnacl";
|
|
287
251
|
private db: Database;
|
|
288
252
|
private dbReady = false;
|
|
289
253
|
private deviceChallenges = new Map<
|
|
@@ -309,19 +273,7 @@ export class Spire extends EventEmitter {
|
|
|
309
273
|
|
|
310
274
|
constructor(SK: string, options?: SpireOptions) {
|
|
311
275
|
super();
|
|
312
|
-
this.
|
|
313
|
-
if (pendingFipsKeyPair) {
|
|
314
|
-
this.signKeys = pendingFipsKeyPair;
|
|
315
|
-
pendingFipsKeyPair = null;
|
|
316
|
-
} else {
|
|
317
|
-
if (this.cryptoProfile === "fips") {
|
|
318
|
-
throw new Error(
|
|
319
|
-
'FIPS: use `await Spire.createAsync(secretKeyHex, { ...options, cryptoProfile: "fips" })` instead of `new Spire()`.',
|
|
320
|
-
);
|
|
321
|
-
}
|
|
322
|
-
setCryptoProfile(this.cryptoProfile);
|
|
323
|
-
this.signKeys = xSignKeyPairFromSecret(XUtils.decodeHex(SK));
|
|
324
|
-
}
|
|
276
|
+
this.signKeys = xSignKeyPairFromSecret(XUtils.decodeHex(SK));
|
|
325
277
|
|
|
326
278
|
// Proxy trust must match the deployment topology. Defaulting to zero
|
|
327
279
|
// prevents direct deployments from accepting attacker-supplied
|
|
@@ -363,33 +315,6 @@ export class Spire extends EventEmitter {
|
|
|
363
315
|
this.init(resolveSpireListenPort(options?.apiPort));
|
|
364
316
|
}
|
|
365
317
|
|
|
366
|
-
/**
|
|
367
|
-
* Construct Spire when the crypto profile is `fips` (async sign-key derivation).
|
|
368
|
-
* For `tweetnacl` (default) you can use `new Spire()` directly.
|
|
369
|
-
*/
|
|
370
|
-
public static async createAsync(
|
|
371
|
-
secretKeyHex: string,
|
|
372
|
-
options?: SpireOptions,
|
|
373
|
-
): Promise<Spire> {
|
|
374
|
-
if ((options?.cryptoProfile ?? "tweetnacl") !== "fips") {
|
|
375
|
-
return new Spire(secretKeyHex, options);
|
|
376
|
-
}
|
|
377
|
-
if (typeof globalThis.crypto.subtle !== "object") {
|
|
378
|
-
throw new Error(
|
|
379
|
-
"FIPS: Spire requires `globalThis.crypto.subtle` (e.g. Node 20+ with global Web Crypto).",
|
|
380
|
-
);
|
|
381
|
-
}
|
|
382
|
-
setCryptoProfile("fips");
|
|
383
|
-
try {
|
|
384
|
-
pendingFipsKeyPair = await xSignKeyPairFromSecretAsync(
|
|
385
|
-
XUtils.decodeHex(secretKeyHex),
|
|
386
|
-
);
|
|
387
|
-
return new Spire(secretKeyHex, options);
|
|
388
|
-
} finally {
|
|
389
|
-
pendingFipsKeyPair = null;
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
|
|
393
318
|
public async close(): Promise<void> {
|
|
394
319
|
if (this.mailPruneInterval) {
|
|
395
320
|
clearInterval(this.mailPruneInterval);
|
|
@@ -671,7 +596,7 @@ export class Spire extends EventEmitter {
|
|
|
671
596
|
|
|
672
597
|
const ok = dbHealthy;
|
|
673
598
|
if (!devApiKeySkipsRateLimits(req)) {
|
|
674
|
-
res.json({
|
|
599
|
+
res.json({ ok });
|
|
675
600
|
return;
|
|
676
601
|
}
|
|
677
602
|
const canaryEnv = process.env["CANARY"]?.trim().toLowerCase();
|
|
@@ -681,7 +606,6 @@ export class Spire extends EventEmitter {
|
|
|
681
606
|
canaryEnv === "true" ||
|
|
682
607
|
canaryEnv === "yes",
|
|
683
608
|
checkDurationMs,
|
|
684
|
-
cryptoProfile: this.cryptoProfile,
|
|
685
609
|
now: new Date(),
|
|
686
610
|
ok,
|
|
687
611
|
version: this.version,
|
|
@@ -1438,7 +1362,7 @@ export class Spire extends EventEmitter {
|
|
|
1438
1362
|
const message =
|
|
1439
1363
|
err instanceof Error ? err.message : String(err);
|
|
1440
1364
|
console.error(
|
|
1441
|
-
`[spire] /register failed requestId=${requestId}
|
|
1365
|
+
`[spire] /register failed requestId=${requestId} message=${message}`,
|
|
1442
1366
|
);
|
|
1443
1367
|
if (err instanceof Error && err.stack) {
|
|
1444
1368
|
console.error(err.stack);
|
|
@@ -1510,6 +1434,19 @@ export class Spire extends EventEmitter {
|
|
|
1510
1434
|
}
|
|
1511
1435
|
}
|
|
1512
1436
|
|
|
1437
|
+
export function resolveTrustProxyHops(value: string | undefined): number {
|
|
1438
|
+
if (value === undefined || value.trim() === "") {
|
|
1439
|
+
return 0;
|
|
1440
|
+
}
|
|
1441
|
+
const hops = Number(value);
|
|
1442
|
+
if (!Number.isInteger(hops) || hops < 0 || hops > 10) {
|
|
1443
|
+
throw new Error(
|
|
1444
|
+
"SPIRE_TRUST_PROXY_HOPS must be an integer from 0 to 10.",
|
|
1445
|
+
);
|
|
1446
|
+
}
|
|
1447
|
+
return hops;
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1513
1450
|
// Usernames are case-insensitive at the protocol level — `User` and
|
|
1514
1451
|
// `user` must resolve to the same account. We canonicalize to
|
|
1515
1452
|
// lowercase at the registration boundary so the persisted row, the
|
|
@@ -1518,3 +1455,17 @@ export class Spire extends EventEmitter {
|
|
|
1518
1455
|
function normalizeRegistrationUsername(username: string): string {
|
|
1519
1456
|
return username.trim().toLowerCase();
|
|
1520
1457
|
}
|
|
1458
|
+
|
|
1459
|
+
/**
|
|
1460
|
+
* Masks identifying material in the access-log URL: hyphenated UUIDs, and
|
|
1461
|
+
* `/device/...` path segments that hold Ed25519 public-key material.
|
|
1462
|
+
*/
|
|
1463
|
+
function redactAccessLogUrl(url: string): string {
|
|
1464
|
+
let s = url.replace(
|
|
1465
|
+
/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi,
|
|
1466
|
+
"[uuid]",
|
|
1467
|
+
);
|
|
1468
|
+
// Device id is a hex public key, not a UUID: strip the segment after /device/
|
|
1469
|
+
s = s.replace(/(\/device\/)([0-9a-fA-F]{16,})(?=[/?#]|$)/g, "$1[device]");
|
|
1470
|
+
return s;
|
|
1471
|
+
}
|
|
@@ -44,15 +44,25 @@ describe("CLI passkey page", () => {
|
|
|
44
44
|
expect(res.headers.get("permissions-policy")).toContain(
|
|
45
45
|
"publickey-credentials-create=(self)",
|
|
46
46
|
);
|
|
47
|
-
expect(html).toContain("Continue with your passkey
|
|
47
|
+
expect(html).toContain("Continue with your passkey");
|
|
48
|
+
expect(html).toContain("--accent: #e5484d");
|
|
49
|
+
expect(html).not.toContain("radial-gradient");
|
|
50
|
+
expect(html).toContain('role="status" aria-live="polite"');
|
|
48
51
|
expect(html).toContain("resolveTrustedApiBase");
|
|
49
52
|
expect(html).toContain("Passkey link API origin is not trusted.");
|
|
53
|
+
expect(html).toContain("resolveCompletionCallback");
|
|
54
|
+
expect(html).toContain('callback.protocol !== "vex:" ||');
|
|
55
|
+
expect(html).toContain('callback.pathname !== "/complete" ||');
|
|
50
56
|
expect(html).toContain("window.history.replaceState");
|
|
51
57
|
expect(html).toContain('mode === "register-handoff"');
|
|
52
58
|
expect(html).toContain('mode === "authenticate-handoff"');
|
|
53
59
|
expect(html).toContain('"/auth/passkey/browser-registration/"');
|
|
54
60
|
expect(html).toContain('"/auth/passkey/browser-authentication/"');
|
|
55
61
|
expect(html).toContain("action.disabled = nextBusy || completed");
|
|
62
|
+
expect(html).toContain("showCompletion(");
|
|
63
|
+
expect(html).toContain("action.hidden = true");
|
|
64
|
+
expect(html).toContain('err.name === "NotAllowedError"');
|
|
65
|
+
expect(html).toContain("apiRequestError(response.status, payload)");
|
|
56
66
|
expect(html).toContain("navigator.credentials.create");
|
|
57
67
|
expect(html).toContain("navigator.credentials.get");
|
|
58
68
|
});
|
|
@@ -7,13 +7,11 @@
|
|
|
7
7
|
import { describe, expect, it } from "vitest";
|
|
8
8
|
|
|
9
9
|
import {
|
|
10
|
-
isSpireFipsEnabled,
|
|
11
10
|
normalizeEnvValue,
|
|
12
11
|
validateSpireRuntimeEnv,
|
|
13
12
|
} from "../utils/loadEnv.ts";
|
|
14
13
|
|
|
15
14
|
const TWEETNACL_SPK = "ab".repeat(64);
|
|
16
|
-
const FIPS_SPK = "cd".repeat(90);
|
|
17
15
|
const JWT_SECRET = "ef".repeat(32);
|
|
18
16
|
|
|
19
17
|
describe("normalizeEnvValue", () => {
|
|
@@ -30,22 +28,11 @@ describe("normalizeEnvValue", () => {
|
|
|
30
28
|
});
|
|
31
29
|
});
|
|
32
30
|
|
|
33
|
-
describe("isSpireFipsEnabled", () => {
|
|
34
|
-
it("accepts true and 1 only", () => {
|
|
35
|
-
expect(isSpireFipsEnabled("true")).toBe(true);
|
|
36
|
-
expect(isSpireFipsEnabled('"true"')).toBe(true);
|
|
37
|
-
expect(isSpireFipsEnabled("1")).toBe(true);
|
|
38
|
-
expect(isSpireFipsEnabled("false")).toBe(false);
|
|
39
|
-
expect(isSpireFipsEnabled(undefined)).toBe(false);
|
|
40
|
-
});
|
|
41
|
-
});
|
|
42
|
-
|
|
43
31
|
describe("validateSpireRuntimeEnv", () => {
|
|
44
32
|
it("accepts quoted compose-safe tweetnacl keys", () => {
|
|
45
33
|
expect(() => {
|
|
46
34
|
validateSpireRuntimeEnv({
|
|
47
35
|
JWT_SECRET: `"${JWT_SECRET}"`,
|
|
48
|
-
SPIRE_FIPS: "false",
|
|
49
36
|
SPK: `"${TWEETNACL_SPK}"`,
|
|
50
37
|
});
|
|
51
38
|
}).not.toThrow();
|
|
@@ -60,22 +47,11 @@ describe("validateSpireRuntimeEnv", () => {
|
|
|
60
47
|
}).toThrow(/SPK must be an even-length hex string/);
|
|
61
48
|
});
|
|
62
49
|
|
|
63
|
-
it("rejects
|
|
64
|
-
expect(() => {
|
|
65
|
-
validateSpireRuntimeEnv({
|
|
66
|
-
JWT_SECRET,
|
|
67
|
-
SPIRE_FIPS: "true",
|
|
68
|
-
SPK: TWEETNACL_SPK,
|
|
69
|
-
});
|
|
70
|
-
}).toThrow(/requires an SPK from .*gen-spk-fips/);
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it("rejects a FIPS-sized SPK without the FIPS flag", () => {
|
|
50
|
+
it("rejects an SPK with the wrong length", () => {
|
|
74
51
|
expect(() => {
|
|
75
52
|
validateSpireRuntimeEnv({
|
|
76
53
|
JWT_SECRET,
|
|
77
|
-
|
|
78
|
-
SPK: FIPS_SPK,
|
|
54
|
+
SPK: "cd".repeat(90),
|
|
79
55
|
});
|
|
80
56
|
}).toThrow(/SPK must be 128 hex characters/);
|
|
81
57
|
});
|
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import {
|
|
8
|
-
setCryptoProfile,
|
|
9
8
|
xBoxKeyPairAsync,
|
|
10
9
|
xPreKeySignaturePayload,
|
|
11
10
|
xSignAsync,
|
|
@@ -13,7 +12,7 @@ import {
|
|
|
13
12
|
XUtils,
|
|
14
13
|
} from "@vex-chat/crypto";
|
|
15
14
|
|
|
16
|
-
import {
|
|
15
|
+
import { describe, expect, it } from "vitest";
|
|
17
16
|
|
|
18
17
|
import {
|
|
19
18
|
verifyDevicePayloadPreKeySignature,
|
|
@@ -31,10 +30,6 @@ async function makeSignedPreKey() {
|
|
|
31
30
|
}
|
|
32
31
|
|
|
33
32
|
describe("prekey signature validation", () => {
|
|
34
|
-
beforeEach(() => {
|
|
35
|
-
setCryptoProfile("tweetnacl");
|
|
36
|
-
});
|
|
37
|
-
|
|
38
33
|
it("accepts a prekey signed by the device signing key", async () => {
|
|
39
34
|
const { preKey, signature, signKeys } = await makeSignedPreKey();
|
|
40
35
|
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
} from "../spireListenPort.ts";
|
|
13
13
|
|
|
14
14
|
describe("resolveSpireListenPort", () => {
|
|
15
|
-
it("uses default
|
|
15
|
+
it("uses the default when no explicit port is provided", () => {
|
|
16
16
|
expect(resolveSpireListenPort(undefined)).toBe(DEFAULT_SPIRE_API_PORT);
|
|
17
17
|
});
|
|
18
18
|
|
package/src/run.ts
CHANGED
|
@@ -9,7 +9,7 @@ import type { SpireOptions } from "./Spire.ts";
|
|
|
9
9
|
import { Spire } from "./Spire.ts";
|
|
10
10
|
import { loadEnv } from "./utils/loadEnv.ts";
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
function main() {
|
|
13
13
|
// load the environment variables — loadEnv() exits if required vars are missing
|
|
14
14
|
loadEnv();
|
|
15
15
|
|
|
@@ -29,21 +29,12 @@ async function main() {
|
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
const dbType = parseDbType(process.env["DB_TYPE"]);
|
|
32
|
-
const fips =
|
|
33
|
-
process.env["SPIRE_FIPS"] === "1" ||
|
|
34
|
-
process.env["SPIRE_FIPS"] === "true";
|
|
35
|
-
|
|
36
32
|
const options: SpireOptions = {
|
|
37
33
|
...(apiPort !== undefined ? { apiPort } : {}),
|
|
38
34
|
...(dbType !== undefined ? { dbType } : {}),
|
|
39
|
-
...(fips ? { cryptoProfile: "fips" } : {}),
|
|
40
35
|
};
|
|
41
36
|
|
|
42
|
-
|
|
43
|
-
await Spire.createAsync(spk, options);
|
|
44
|
-
} else {
|
|
45
|
-
new Spire(spk, options);
|
|
46
|
-
}
|
|
37
|
+
new Spire(spk, options);
|
|
47
38
|
}
|
|
48
39
|
|
|
49
40
|
function parseDbType(value: string | undefined): SpireOptions["dbType"] {
|
|
@@ -58,7 +49,9 @@ function parseDbType(value: string | undefined): SpireOptions["dbType"] {
|
|
|
58
49
|
}
|
|
59
50
|
}
|
|
60
51
|
|
|
61
|
-
|
|
52
|
+
try {
|
|
53
|
+
main();
|
|
54
|
+
} catch (err: unknown) {
|
|
62
55
|
console.error(err);
|
|
63
56
|
process.exit(1);
|
|
64
|
-
}
|
|
57
|
+
}
|