@absolutejs/vulnerabilities-witness 0.1.0 → 0.1.1
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 +12 -2
- package/dist/index.js +22 -1
- package/dist/server.js +68 -6
- package/dist/service.d.ts +4 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -48,8 +48,9 @@ Bun.serve({ fetch, port: 3000 });
|
|
|
48
48
|
The signing identity contains a private key and belongs in a durable secret
|
|
49
49
|
broker, not PostgreSQL or source control. `service.rotate()` stores the new
|
|
50
50
|
secret state before activating it and publishes the cross-signed transition in
|
|
51
|
-
the registry.
|
|
52
|
-
|
|
51
|
+
the registry. `service.maintain()` performs the same rotation only after the
|
|
52
|
+
configured maximum key age. In a multi-replica service, the supplied
|
|
53
|
+
secret-store integration must serialize rotations.
|
|
53
54
|
|
|
54
55
|
## Standalone service
|
|
55
56
|
|
|
@@ -61,8 +62,17 @@ observations and accepts these deployment secrets:
|
|
|
61
62
|
- `EVIDENCE_WITNESS_SIGNING_STATE_JSON`
|
|
62
63
|
- `EVIDENCE_WITNESS_TOKENS_JSON`, an object mapping stable subjects to bearer
|
|
63
64
|
tokens
|
|
65
|
+
- `EVIDENCE_WITNESS_SECRETS_PATH`, the durable encrypted secret file
|
|
66
|
+
- `EVIDENCE_WITNESS_SECRETS_PASSPHRASE`, the master passphrase kept outside the
|
|
67
|
+
file
|
|
68
|
+
- `EVIDENCE_WITNESS_KEY_MAX_AGE_MS`, defaulting to 90 days
|
|
69
|
+
- `EVIDENCE_WITNESS_MAINTENANCE_INTERVAL_MS`, defaulting to one hour
|
|
64
70
|
- `PORT`, defaulting to `3000`
|
|
65
71
|
|
|
72
|
+
The two JSON values bootstrap the encrypted file only when their entries do not
|
|
73
|
+
already exist. Later key rotations are written atomically to the encrypted file
|
|
74
|
+
before the new identity becomes active.
|
|
75
|
+
|
|
66
76
|
Run independent instances under different administrative and infrastructure
|
|
67
77
|
boundaries. Clients should pin each genesis witness key through an independent
|
|
68
78
|
channel and require a quorum with
|
package/dist/index.js
CHANGED
|
@@ -175,7 +175,28 @@ var createEvidenceWitnessService = (options) => {
|
|
|
175
175
|
});
|
|
176
176
|
return queued;
|
|
177
177
|
};
|
|
178
|
-
|
|
178
|
+
const maintain = (input) => {
|
|
179
|
+
if (!Number.isSafeInteger(input.maxKeyAgeMs) || input.maxKeyAgeMs < 1)
|
|
180
|
+
throw new Error("Evidence witness maximum key age is invalid");
|
|
181
|
+
const now = input.now ?? new Date;
|
|
182
|
+
const queued = mutationQueue.then(async () => {
|
|
183
|
+
await refresh();
|
|
184
|
+
const ageMs = now.getTime() - Date.parse(state.identity.createdAt);
|
|
185
|
+
if (ageMs < input.maxKeyAgeMs)
|
|
186
|
+
return evidenceWitnessKeyRegistryFrom(state);
|
|
187
|
+
const next = rotateEvidenceWitnessSigningState(state, now.toISOString());
|
|
188
|
+
await options.storeSigningState(encodeEvidenceWitnessSigningState(next));
|
|
189
|
+
state = next;
|
|
190
|
+
return evidenceWitnessKeyRegistryFrom(state);
|
|
191
|
+
});
|
|
192
|
+
mutationQueue = queued.then(() => {
|
|
193
|
+
return;
|
|
194
|
+
}, () => {
|
|
195
|
+
return;
|
|
196
|
+
});
|
|
197
|
+
return queued;
|
|
198
|
+
};
|
|
199
|
+
return { checkpoint, maintain, registry, rotate };
|
|
179
200
|
};
|
|
180
201
|
|
|
181
202
|
// src/store.ts
|
package/dist/server.js
CHANGED
|
@@ -177,7 +177,28 @@ var createEvidenceWitnessService = (options) => {
|
|
|
177
177
|
});
|
|
178
178
|
return queued;
|
|
179
179
|
};
|
|
180
|
-
|
|
180
|
+
const maintain = (input) => {
|
|
181
|
+
if (!Number.isSafeInteger(input.maxKeyAgeMs) || input.maxKeyAgeMs < 1)
|
|
182
|
+
throw new Error("Evidence witness maximum key age is invalid");
|
|
183
|
+
const now = input.now ?? new Date;
|
|
184
|
+
const queued = mutationQueue.then(async () => {
|
|
185
|
+
await refresh();
|
|
186
|
+
const ageMs = now.getTime() - Date.parse(state.identity.createdAt);
|
|
187
|
+
if (ageMs < input.maxKeyAgeMs)
|
|
188
|
+
return evidenceWitnessKeyRegistryFrom(state);
|
|
189
|
+
const next = rotateEvidenceWitnessSigningState(state, now.toISOString());
|
|
190
|
+
await options.storeSigningState(encodeEvidenceWitnessSigningState(next));
|
|
191
|
+
state = next;
|
|
192
|
+
return evidenceWitnessKeyRegistryFrom(state);
|
|
193
|
+
});
|
|
194
|
+
mutationQueue = queued.then(() => {
|
|
195
|
+
return;
|
|
196
|
+
}, () => {
|
|
197
|
+
return;
|
|
198
|
+
});
|
|
199
|
+
return queued;
|
|
200
|
+
};
|
|
201
|
+
return { checkpoint, maintain, registry, rotate };
|
|
181
202
|
};
|
|
182
203
|
|
|
183
204
|
// src/store.ts
|
|
@@ -345,25 +366,59 @@ var createPostgresEvidenceWitnessStore = (sql) => {
|
|
|
345
366
|
|
|
346
367
|
// src/server.ts
|
|
347
368
|
var {SQL } = globalThis.Bun;
|
|
369
|
+
import { encryptedFileAdapter } from "@absolutejs/secrets/broker";
|
|
348
370
|
var required = (name) => {
|
|
349
371
|
const value = process.env[name]?.trim();
|
|
350
372
|
if (!value)
|
|
351
373
|
throw new Error(`Missing required environment: ${name}`);
|
|
352
374
|
return value;
|
|
353
375
|
};
|
|
376
|
+
var positiveInteger = (name, fallback) => {
|
|
377
|
+
const encoded = process.env[name]?.trim();
|
|
378
|
+
if (!encoded)
|
|
379
|
+
return fallback;
|
|
380
|
+
const value = Number(encoded);
|
|
381
|
+
if (!Number.isSafeInteger(value) || value < 1)
|
|
382
|
+
throw new Error(`${name} must be a positive integer`);
|
|
383
|
+
return value;
|
|
384
|
+
};
|
|
385
|
+
var signingStateName = "EVIDENCE_WITNESS_SIGNING_STATE_JSON";
|
|
386
|
+
var tokensName = "EVIDENCE_WITNESS_TOKENS_JSON";
|
|
387
|
+
var masterPassphrase = required("EVIDENCE_WITNESS_SECRETS_PASSPHRASE");
|
|
388
|
+
var secrets = encryptedFileAdapter({
|
|
389
|
+
key: { passphrase: masterPassphrase, type: "passphrase" },
|
|
390
|
+
path: required("EVIDENCE_WITNESS_SECRETS_PATH")
|
|
391
|
+
});
|
|
392
|
+
var loadOrBootstrap = async (name) => {
|
|
393
|
+
const stored = await secrets.fetch(name);
|
|
394
|
+
if (stored)
|
|
395
|
+
return stored;
|
|
396
|
+
const bootstrap = required(name);
|
|
397
|
+
if (!secrets.put)
|
|
398
|
+
throw new Error("Encrypted secret adapter is read-only");
|
|
399
|
+
await secrets.put(name, bootstrap);
|
|
400
|
+
return bootstrap;
|
|
401
|
+
};
|
|
354
402
|
var database = new SQL(required("DATABASE_URL"));
|
|
355
403
|
await ensurePostgresEvidenceWitnessSchema(database);
|
|
356
|
-
var encodedSigningState =
|
|
404
|
+
var encodedSigningState = await loadOrBootstrap(signingStateName);
|
|
357
405
|
var signingState = parseEvidenceWitnessSigningState(JSON.parse(encodedSigningState));
|
|
358
|
-
var tokens = JSON.parse(
|
|
406
|
+
var tokens = JSON.parse(await loadOrBootstrap(tokensName));
|
|
359
407
|
var subjectsByToken = new Map(Object.entries(tokens).map(([subject, token]) => [token, subject]));
|
|
360
408
|
var service2 = createEvidenceWitnessService({
|
|
361
|
-
loadSigningState: async () =>
|
|
409
|
+
loadSigningState: async () => {
|
|
410
|
+
const encoded = await secrets.fetch(signingStateName);
|
|
411
|
+
if (!encoded)
|
|
412
|
+
throw new Error("Evidence witness signing state is missing");
|
|
413
|
+
return encoded;
|
|
414
|
+
},
|
|
362
415
|
origin: required("EVIDENCE_WITNESS_ORIGIN"),
|
|
363
416
|
signingState,
|
|
364
417
|
store: createPostgresEvidenceWitnessStore(database),
|
|
365
|
-
storeSigningState: async () => {
|
|
366
|
-
|
|
418
|
+
storeSigningState: async (encoded) => {
|
|
419
|
+
if (!secrets.put)
|
|
420
|
+
throw new Error("Encrypted secret adapter is read-only");
|
|
421
|
+
await secrets.put(signingStateName, encoded);
|
|
367
422
|
}
|
|
368
423
|
});
|
|
369
424
|
var handler = createEvidenceWitnessHttpHandler({
|
|
@@ -374,3 +429,10 @@ var port = Number(process.env.PORT ?? "3000");
|
|
|
374
429
|
if (!Number.isSafeInteger(port) || port < 1 || port > 65535)
|
|
375
430
|
throw new Error("PORT must be a valid TCP port");
|
|
376
431
|
Bun.serve({ fetch: handler, port });
|
|
432
|
+
var dayMs = 86400000;
|
|
433
|
+
var maxKeyAgeMs = positiveInteger("EVIDENCE_WITNESS_KEY_MAX_AGE_MS", 90 * dayMs);
|
|
434
|
+
var maintenanceIntervalMs = positiveInteger("EVIDENCE_WITNESS_MAINTENANCE_INTERVAL_MS", 3600000);
|
|
435
|
+
var maintain = () => service2.maintain({ maxKeyAgeMs }).catch((error) => console.error("Evidence witness key maintenance failed", error));
|
|
436
|
+
var maintenanceTimer = setInterval(() => void maintain(), maintenanceIntervalMs);
|
|
437
|
+
maintenanceTimer.unref();
|
|
438
|
+
maintain();
|
package/dist/service.d.ts
CHANGED
|
@@ -18,6 +18,10 @@ export declare const createEvidenceWitnessService: (options: {
|
|
|
18
18
|
storeSigningState: (encoded: string) => Promise<void>;
|
|
19
19
|
}) => {
|
|
20
20
|
checkpoint: (subject: string, request: EvidenceWitnessRequest) => Promise<EvidenceWitnessResponse>;
|
|
21
|
+
maintain: (input: {
|
|
22
|
+
maxKeyAgeMs: number;
|
|
23
|
+
now?: Date;
|
|
24
|
+
}) => Promise<EvidenceWitnessKeyRegistry>;
|
|
21
25
|
registry: () => Promise<EvidenceWitnessKeyRegistry>;
|
|
22
26
|
rotate: (rotatedAt?: string) => Promise<EvidenceWitnessKeyRegistry>;
|
|
23
27
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@absolutejs/vulnerabilities-witness",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Independent durable transparency witness with rollback, equivocation, key-rotation, and HTTP service primitives for AbsoluteJS vulnerability evidence.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "BSL-1.1",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"format": "prettier --write \"./**/*.{ts,json,md}\"",
|
|
49
49
|
"typecheck": "tsc --noEmit",
|
|
50
50
|
"test": "bun test",
|
|
51
|
-
"build": "rm -rf dist && bun build --root src src/index.ts src/postgres.ts src/server.ts src/manifest.ts --outdir dist --target=bun --external @absolutejs/manifest --external @absolutejs/vulnerabilities --external @sinclair/typebox && tsc -p tsconfig.build.json && absolute-manifest emit",
|
|
51
|
+
"build": "rm -rf dist && bun build --root src src/index.ts src/postgres.ts src/server.ts src/manifest.ts --outdir dist --target=bun --external @absolutejs/manifest --external @absolutejs/secrets --external @absolutejs/vulnerabilities --external @sinclair/typebox && tsc -p tsconfig.build.json && absolute-manifest emit",
|
|
52
52
|
"check:package": "bun run format && bun run typecheck && bun run test && bun run build"
|
|
53
53
|
},
|
|
54
54
|
"keywords": [
|
|
@@ -61,6 +61,7 @@
|
|
|
61
61
|
],
|
|
62
62
|
"dependencies": {
|
|
63
63
|
"@absolutejs/manifest": "0.3.0",
|
|
64
|
+
"@absolutejs/secrets": "0.7.2",
|
|
64
65
|
"@absolutejs/vulnerabilities": "0.13.0",
|
|
65
66
|
"@sinclair/typebox": "0.34.52"
|
|
66
67
|
},
|