@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 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. In a multi-replica service, the supplied secret-store integration
52
- must serialize rotations.
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
- return { checkpoint, registry, rotate };
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
- return { checkpoint, registry, rotate };
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 = required("EVIDENCE_WITNESS_SIGNING_STATE_JSON");
404
+ var encodedSigningState = await loadOrBootstrap(signingStateName);
357
405
  var signingState = parseEvidenceWitnessSigningState(JSON.parse(encodedSigningState));
358
- var tokens = JSON.parse(required("EVIDENCE_WITNESS_TOKENS_JSON"));
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 () => encodedSigningState,
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
- throw new Error("Server-managed key rotation requires a durable secret-store integration");
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.0",
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
  },