@absolutejs/vulnerabilities-witness 0.1.0 → 0.2.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 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,9 +62,33 @@ 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
70
+
71
+ The authenticated `GET /v1/status` endpoint reports the verified key registry
72
+ and optional backup restoration evidence. Store
73
+ `EVIDENCE_WITNESS_BACKUP_VERIFICATION_JSON` in the encrypted secret adapter
74
+ after a drill has restored both the PostgreSQL artifact and encrypted signing
75
+ state. The record must use the
76
+ `absolutejs.vulnerability-evidence-witness-backup-verification/v1` contract,
77
+ contain SHA-256 digests for both artifacts, and record both restoration times.
78
+
64
79
  - `PORT`, defaulting to `3000`
65
80
 
81
+ The two JSON values bootstrap the encrypted file only when their entries do not
82
+ already exist. Later key rotations are written atomically to the encrypted file
83
+ before the new identity becomes active.
84
+
66
85
  Run independent instances under different administrative and infrastructure
67
86
  boundaries. Clients should pin each genesis witness key through an independent
68
87
  channel and require a quorum with
69
88
  `verifyEvidenceWitnessQuorum` from `@absolutejs/vulnerabilities`.
89
+
90
+ The repository publishes a deployable container to
91
+ `ghcr.io/absolutejs/vulnerabilities-witness`. See
92
+ [`deploy/README.md`](deploy/README.md) for the independence requirements,
93
+ bootstrap sequence, PAAS quorum configuration, and repeatable Admin drill
94
+ workflow.
package/dist/index.js CHANGED
@@ -114,6 +114,33 @@ var verifyEvidenceWitnessKeyRegistry = (input) => {
114
114
 
115
115
  // src/service.ts
116
116
  var EVIDENCE_WITNESS_REQUEST_CONTRACT = "absolutejs.vulnerability-evidence-witness-request/v1";
117
+ var EVIDENCE_WITNESS_BACKUP_VERIFICATION_CONTRACT = "absolutejs.vulnerability-evidence-witness-backup-verification/v1";
118
+ var EVIDENCE_WITNESS_STATUS_CONTRACT = "absolutejs.vulnerability-evidence-witness-status/v1";
119
+ var timestamp = (value, label) => {
120
+ if (!Number.isFinite(Date.parse(value)))
121
+ throw new Error(`${label} is invalid`);
122
+ return value;
123
+ };
124
+ var digest = (value, label) => {
125
+ if (!/^sha256:[a-f0-9]{64}$/u.test(value))
126
+ throw new Error(`${label} is invalid`);
127
+ return value;
128
+ };
129
+ var parseEvidenceWitnessBackupVerification = (value) => {
130
+ if (!value || typeof value !== "object" || Array.isArray(value))
131
+ throw new Error("Evidence witness backup verification is invalid");
132
+ const input = value;
133
+ if (input.contract !== EVIDENCE_WITNESS_BACKUP_VERIFICATION_CONTRACT)
134
+ throw new Error("Evidence witness backup verification is unsupported");
135
+ return {
136
+ contract: input.contract,
137
+ databaseArtifactDigest: digest(input.databaseArtifactDigest ?? "", "Evidence witness database backup digest"),
138
+ databaseRestoredAt: timestamp(input.databaseRestoredAt ?? "", "Evidence witness database restore time"),
139
+ signingStateArtifactDigest: digest(input.signingStateArtifactDigest ?? "", "Evidence witness signing-state backup digest"),
140
+ signingStateRestoredAt: timestamp(input.signingStateRestoredAt ?? "", "Evidence witness signing-state restore time"),
141
+ verifiedAt: timestamp(input.verifiedAt ?? "", "Evidence witness backup verification time")
142
+ };
143
+ };
117
144
  var createEvidenceWitnessService = (options) => {
118
145
  const origin = new URL(options.origin);
119
146
  if (origin.protocol !== "https:")
@@ -128,6 +155,12 @@ var createEvidenceWitnessService = (options) => {
128
155
  await refresh();
129
156
  return evidenceWitnessKeyRegistryFrom(state);
130
157
  };
158
+ const status = async () => ({
159
+ backup: options.loadBackupVerification ? await options.loadBackupVerification().then((value) => value === null ? null : parseEvidenceWitnessBackupVerification(value)) : null,
160
+ checkedAt: new Date().toISOString(),
161
+ contract: EVIDENCE_WITNESS_STATUS_CONTRACT,
162
+ registry: await registry()
163
+ });
131
164
  const checkpoint = async (subject, request) => {
132
165
  if (request.contract !== EVIDENCE_WITNESS_REQUEST_CONTRACT)
133
166
  throw new Error("Evidence witness request contract is unsupported");
@@ -152,7 +185,7 @@ var createEvidenceWitnessService = (options) => {
152
185
  });
153
186
  const observed = await options.store.observe({
154
187
  checkpoint: signed,
155
- history: request.log.entries.map(({ digest }) => digest),
188
+ history: request.log.entries.map(({ digest: digest2 }) => digest2),
156
189
  subject: normalizedSubject
157
190
  });
158
191
  return {
@@ -175,7 +208,28 @@ var createEvidenceWitnessService = (options) => {
175
208
  });
176
209
  return queued;
177
210
  };
178
- return { checkpoint, registry, rotate };
211
+ const maintain = (input) => {
212
+ if (!Number.isSafeInteger(input.maxKeyAgeMs) || input.maxKeyAgeMs < 1)
213
+ throw new Error("Evidence witness maximum key age is invalid");
214
+ const now = input.now ?? new Date;
215
+ const queued = mutationQueue.then(async () => {
216
+ await refresh();
217
+ const ageMs = now.getTime() - Date.parse(state.identity.createdAt);
218
+ if (ageMs < input.maxKeyAgeMs)
219
+ return evidenceWitnessKeyRegistryFrom(state);
220
+ const next = rotateEvidenceWitnessSigningState(state, now.toISOString());
221
+ await options.storeSigningState(encodeEvidenceWitnessSigningState(next));
222
+ state = next;
223
+ return evidenceWitnessKeyRegistryFrom(state);
224
+ });
225
+ mutationQueue = queued.then(() => {
226
+ return;
227
+ }, () => {
228
+ return;
229
+ });
230
+ return queued;
231
+ };
232
+ return { checkpoint, maintain, registry, rotate, status };
179
233
  };
180
234
 
181
235
  // src/store.ts
@@ -248,6 +302,12 @@ var createEvidenceWitnessHttpHandler = (options) => async (request) => {
248
302
  return json({ status: "ok" });
249
303
  if (request.method === "GET" && url.pathname === "/v1/keys")
250
304
  return json(await options.service.registry());
305
+ if (request.method === "GET" && url.pathname === "/v1/status") {
306
+ const subject2 = await options.authenticate(bearer(request));
307
+ if (!subject2)
308
+ return json({ error: "unauthorized" }, 401);
309
+ return json(await options.service.status());
310
+ }
251
311
  if (request.method !== "POST" || url.pathname !== "/v1/checkpoints")
252
312
  return json({ error: "not_found" }, 404);
253
313
  const subject = await options.authenticate(bearer(request));
@@ -273,6 +333,7 @@ export {
273
333
  verifyEvidenceWitnessKeyRegistry,
274
334
  rotateEvidenceWitnessSigningState,
275
335
  parseEvidenceWitnessSigningState,
336
+ parseEvidenceWitnessBackupVerification,
276
337
  evidenceWitnessKeyRegistryFrom,
277
338
  encodeEvidenceWitnessSigningState,
278
339
  createMemoryEvidenceWitnessStore,
@@ -281,5 +342,7 @@ export {
281
342
  createEvidenceWitnessHttpHandler,
282
343
  EvidenceWitnessRollbackError,
283
344
  EvidenceWitnessEquivocationError,
284
- EVIDENCE_WITNESS_REQUEST_CONTRACT
345
+ EVIDENCE_WITNESS_STATUS_CONTRACT,
346
+ EVIDENCE_WITNESS_REQUEST_CONTRACT,
347
+ EVIDENCE_WITNESS_BACKUP_VERIFICATION_CONTRACT
285
348
  };
package/dist/server.js CHANGED
@@ -116,6 +116,33 @@ var verifyEvidenceWitnessKeyRegistry = (input) => {
116
116
 
117
117
  // src/service.ts
118
118
  var EVIDENCE_WITNESS_REQUEST_CONTRACT = "absolutejs.vulnerability-evidence-witness-request/v1";
119
+ var EVIDENCE_WITNESS_BACKUP_VERIFICATION_CONTRACT = "absolutejs.vulnerability-evidence-witness-backup-verification/v1";
120
+ var EVIDENCE_WITNESS_STATUS_CONTRACT = "absolutejs.vulnerability-evidence-witness-status/v1";
121
+ var timestamp = (value, label) => {
122
+ if (!Number.isFinite(Date.parse(value)))
123
+ throw new Error(`${label} is invalid`);
124
+ return value;
125
+ };
126
+ var digest = (value, label) => {
127
+ if (!/^sha256:[a-f0-9]{64}$/u.test(value))
128
+ throw new Error(`${label} is invalid`);
129
+ return value;
130
+ };
131
+ var parseEvidenceWitnessBackupVerification = (value) => {
132
+ if (!value || typeof value !== "object" || Array.isArray(value))
133
+ throw new Error("Evidence witness backup verification is invalid");
134
+ const input = value;
135
+ if (input.contract !== EVIDENCE_WITNESS_BACKUP_VERIFICATION_CONTRACT)
136
+ throw new Error("Evidence witness backup verification is unsupported");
137
+ return {
138
+ contract: input.contract,
139
+ databaseArtifactDigest: digest(input.databaseArtifactDigest ?? "", "Evidence witness database backup digest"),
140
+ databaseRestoredAt: timestamp(input.databaseRestoredAt ?? "", "Evidence witness database restore time"),
141
+ signingStateArtifactDigest: digest(input.signingStateArtifactDigest ?? "", "Evidence witness signing-state backup digest"),
142
+ signingStateRestoredAt: timestamp(input.signingStateRestoredAt ?? "", "Evidence witness signing-state restore time"),
143
+ verifiedAt: timestamp(input.verifiedAt ?? "", "Evidence witness backup verification time")
144
+ };
145
+ };
119
146
  var createEvidenceWitnessService = (options) => {
120
147
  const origin = new URL(options.origin);
121
148
  if (origin.protocol !== "https:")
@@ -130,6 +157,12 @@ var createEvidenceWitnessService = (options) => {
130
157
  await refresh();
131
158
  return evidenceWitnessKeyRegistryFrom(state);
132
159
  };
160
+ const status = async () => ({
161
+ backup: options.loadBackupVerification ? await options.loadBackupVerification().then((value) => value === null ? null : parseEvidenceWitnessBackupVerification(value)) : null,
162
+ checkedAt: new Date().toISOString(),
163
+ contract: EVIDENCE_WITNESS_STATUS_CONTRACT,
164
+ registry: await registry()
165
+ });
133
166
  const checkpoint = async (subject, request) => {
134
167
  if (request.contract !== EVIDENCE_WITNESS_REQUEST_CONTRACT)
135
168
  throw new Error("Evidence witness request contract is unsupported");
@@ -154,7 +187,7 @@ var createEvidenceWitnessService = (options) => {
154
187
  });
155
188
  const observed = await options.store.observe({
156
189
  checkpoint: signed,
157
- history: request.log.entries.map(({ digest }) => digest),
190
+ history: request.log.entries.map(({ digest: digest2 }) => digest2),
158
191
  subject: normalizedSubject
159
192
  });
160
193
  return {
@@ -177,7 +210,28 @@ var createEvidenceWitnessService = (options) => {
177
210
  });
178
211
  return queued;
179
212
  };
180
- return { checkpoint, registry, rotate };
213
+ const maintain = (input) => {
214
+ if (!Number.isSafeInteger(input.maxKeyAgeMs) || input.maxKeyAgeMs < 1)
215
+ throw new Error("Evidence witness maximum key age is invalid");
216
+ const now = input.now ?? new Date;
217
+ const queued = mutationQueue.then(async () => {
218
+ await refresh();
219
+ const ageMs = now.getTime() - Date.parse(state.identity.createdAt);
220
+ if (ageMs < input.maxKeyAgeMs)
221
+ return evidenceWitnessKeyRegistryFrom(state);
222
+ const next = rotateEvidenceWitnessSigningState(state, now.toISOString());
223
+ await options.storeSigningState(encodeEvidenceWitnessSigningState(next));
224
+ state = next;
225
+ return evidenceWitnessKeyRegistryFrom(state);
226
+ });
227
+ mutationQueue = queued.then(() => {
228
+ return;
229
+ }, () => {
230
+ return;
231
+ });
232
+ return queued;
233
+ };
234
+ return { checkpoint, maintain, registry, rotate, status };
181
235
  };
182
236
 
183
237
  // src/store.ts
@@ -250,6 +304,12 @@ var createEvidenceWitnessHttpHandler = (options) => async (request) => {
250
304
  return json({ status: "ok" });
251
305
  if (request.method === "GET" && url.pathname === "/v1/keys")
252
306
  return json(await options.service.registry());
307
+ if (request.method === "GET" && url.pathname === "/v1/status") {
308
+ const subject2 = await options.authenticate(bearer(request));
309
+ if (!subject2)
310
+ return json({ error: "unauthorized" }, 401);
311
+ return json(await options.service.status());
312
+ }
253
313
  if (request.method !== "POST" || url.pathname !== "/v1/checkpoints")
254
314
  return json({ error: "not_found" }, 404);
255
315
  const subject = await options.authenticate(bearer(request));
@@ -345,25 +405,64 @@ var createPostgresEvidenceWitnessStore = (sql) => {
345
405
 
346
406
  // src/server.ts
347
407
  var {SQL } = globalThis.Bun;
408
+ import { encryptedFileAdapter } from "@absolutejs/secrets/broker";
348
409
  var required = (name) => {
349
410
  const value = process.env[name]?.trim();
350
411
  if (!value)
351
412
  throw new Error(`Missing required environment: ${name}`);
352
413
  return value;
353
414
  };
415
+ var positiveInteger = (name, fallback) => {
416
+ const encoded = process.env[name]?.trim();
417
+ if (!encoded)
418
+ return fallback;
419
+ const value = Number(encoded);
420
+ if (!Number.isSafeInteger(value) || value < 1)
421
+ throw new Error(`${name} must be a positive integer`);
422
+ return value;
423
+ };
424
+ var signingStateName = "EVIDENCE_WITNESS_SIGNING_STATE_JSON";
425
+ var tokensName = "EVIDENCE_WITNESS_TOKENS_JSON";
426
+ var backupVerificationName = "EVIDENCE_WITNESS_BACKUP_VERIFICATION_JSON";
427
+ var masterPassphrase = required("EVIDENCE_WITNESS_SECRETS_PASSPHRASE");
428
+ var secrets = encryptedFileAdapter({
429
+ key: { passphrase: masterPassphrase, type: "passphrase" },
430
+ path: required("EVIDENCE_WITNESS_SECRETS_PATH")
431
+ });
432
+ var loadOrBootstrap = async (name) => {
433
+ const stored = await secrets.fetch(name);
434
+ if (stored)
435
+ return stored;
436
+ const bootstrap = required(name);
437
+ if (!secrets.put)
438
+ throw new Error("Encrypted secret adapter is read-only");
439
+ await secrets.put(name, bootstrap);
440
+ return bootstrap;
441
+ };
354
442
  var database = new SQL(required("DATABASE_URL"));
355
443
  await ensurePostgresEvidenceWitnessSchema(database);
356
- var encodedSigningState = required("EVIDENCE_WITNESS_SIGNING_STATE_JSON");
444
+ var encodedSigningState = await loadOrBootstrap(signingStateName);
357
445
  var signingState = parseEvidenceWitnessSigningState(JSON.parse(encodedSigningState));
358
- var tokens = JSON.parse(required("EVIDENCE_WITNESS_TOKENS_JSON"));
446
+ var tokens = JSON.parse(await loadOrBootstrap(tokensName));
359
447
  var subjectsByToken = new Map(Object.entries(tokens).map(([subject, token]) => [token, subject]));
360
448
  var service2 = createEvidenceWitnessService({
361
- loadSigningState: async () => encodedSigningState,
449
+ loadBackupVerification: async () => {
450
+ const encoded = await secrets.fetch(backupVerificationName);
451
+ return encoded ? JSON.parse(encoded) : null;
452
+ },
453
+ loadSigningState: async () => {
454
+ const encoded = await secrets.fetch(signingStateName);
455
+ if (!encoded)
456
+ throw new Error("Evidence witness signing state is missing");
457
+ return encoded;
458
+ },
362
459
  origin: required("EVIDENCE_WITNESS_ORIGIN"),
363
460
  signingState,
364
461
  store: createPostgresEvidenceWitnessStore(database),
365
- storeSigningState: async () => {
366
- throw new Error("Server-managed key rotation requires a durable secret-store integration");
462
+ storeSigningState: async (encoded) => {
463
+ if (!secrets.put)
464
+ throw new Error("Encrypted secret adapter is read-only");
465
+ await secrets.put(signingStateName, encoded);
367
466
  }
368
467
  });
369
468
  var handler = createEvidenceWitnessHttpHandler({
@@ -374,3 +473,10 @@ var port = Number(process.env.PORT ?? "3000");
374
473
  if (!Number.isSafeInteger(port) || port < 1 || port > 65535)
375
474
  throw new Error("PORT must be a valid TCP port");
376
475
  Bun.serve({ fetch: handler, port });
476
+ var dayMs = 86400000;
477
+ var maxKeyAgeMs = positiveInteger("EVIDENCE_WITNESS_KEY_MAX_AGE_MS", 90 * dayMs);
478
+ var maintenanceIntervalMs = positiveInteger("EVIDENCE_WITNESS_MAINTENANCE_INTERVAL_MS", 3600000);
479
+ var maintain = () => service2.maintain({ maxKeyAgeMs }).catch((error) => console.error("Evidence witness key maintenance failed", error));
480
+ var maintenanceTimer = setInterval(() => void maintain(), maintenanceIntervalMs);
481
+ maintenanceTimer.unref();
482
+ maintain();
package/dist/service.d.ts CHANGED
@@ -10,7 +10,25 @@ export type EvidenceWitnessResponse = {
10
10
  checkpoint: Awaited<ReturnType<EvidenceWitnessStore["observe"]>>["checkpoint"];
11
11
  registry: EvidenceWitnessKeyRegistry;
12
12
  };
13
+ export declare const EVIDENCE_WITNESS_BACKUP_VERIFICATION_CONTRACT: "absolutejs.vulnerability-evidence-witness-backup-verification/v1";
14
+ export declare const EVIDENCE_WITNESS_STATUS_CONTRACT: "absolutejs.vulnerability-evidence-witness-status/v1";
15
+ export type EvidenceWitnessBackupVerification = {
16
+ contract: typeof EVIDENCE_WITNESS_BACKUP_VERIFICATION_CONTRACT;
17
+ databaseArtifactDigest: `sha256:${string}`;
18
+ databaseRestoredAt: string;
19
+ signingStateArtifactDigest: `sha256:${string}`;
20
+ signingStateRestoredAt: string;
21
+ verifiedAt: string;
22
+ };
23
+ export type EvidenceWitnessStatus = {
24
+ backup: EvidenceWitnessBackupVerification | null;
25
+ checkedAt: string;
26
+ contract: typeof EVIDENCE_WITNESS_STATUS_CONTRACT;
27
+ registry: EvidenceWitnessKeyRegistry;
28
+ };
29
+ export declare const parseEvidenceWitnessBackupVerification: (value: unknown) => EvidenceWitnessBackupVerification;
13
30
  export declare const createEvidenceWitnessService: (options: {
31
+ loadBackupVerification?: () => Promise<unknown | null>;
14
32
  loadSigningState: () => Promise<string>;
15
33
  origin: string;
16
34
  signingState: EvidenceWitnessSigningState;
@@ -18,6 +36,11 @@ export declare const createEvidenceWitnessService: (options: {
18
36
  storeSigningState: (encoded: string) => Promise<void>;
19
37
  }) => {
20
38
  checkpoint: (subject: string, request: EvidenceWitnessRequest) => Promise<EvidenceWitnessResponse>;
39
+ maintain: (input: {
40
+ maxKeyAgeMs: number;
41
+ now?: Date;
42
+ }) => Promise<EvidenceWitnessKeyRegistry>;
21
43
  registry: () => Promise<EvidenceWitnessKeyRegistry>;
22
44
  rotate: (rotatedAt?: string) => Promise<EvidenceWitnessKeyRegistry>;
45
+ status: () => Promise<EvidenceWitnessStatus>;
23
46
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@absolutejs/vulnerabilities-witness",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
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,7 +61,8 @@
61
61
  ],
62
62
  "dependencies": {
63
63
  "@absolutejs/manifest": "0.3.0",
64
- "@absolutejs/vulnerabilities": "0.13.0",
64
+ "@absolutejs/secrets": "0.7.2",
65
+ "@absolutejs/vulnerabilities": "0.14.0",
65
66
  "@sinclair/typebox": "0.34.52"
66
67
  },
67
68
  "devDependencies": {