delegation-authority-store.mjs
1,802 lines 60.6 KB
Raw
sha256:fbe982a22c05c6fe2e93876f250deecdb43d883f648b4e1810c7546caa9f17db docs: activate KNOWTATION- board identity and preserve livi… Human minor ⚠ breaking 3 days ago
1 /**
2 * Transactional DelegationAuthorityStore (RHF-b-KN1).
3 *
4 * Hosted writes use Netlify Blobs CAS (`getWithMetadata` + `onlyIfMatch` / `onlyIfNew`).
5 * Local/tests use a file-backed CAS adapter with the same semantics.
6 *
7 * Forbidden: hydrate/mutate/persist blind overwrite for renewal, validate, revoke.
8 *
9 * @see ~/scooling/docs/reviews/2026-08-27-retail-helper-finish.md §B2–B7
10 */
11
12 import fs from 'fs';
13 import path from 'path';
14 import {
15 createHash,
16 createHmac,
17 hkdfSync,
18 randomBytes,
19 timingSafeEqual,
20 } from 'crypto';
21
22 import {
23 DELEGATION_AUTHORITY_ENVELOPE_SCHEMA,
24 DELEGATION_AUTHORITY_MARKER_SCHEMA,
25 DELEGATION_AUTHORITY_UNAVAILABLE,
26 computeDelegationAuthorityStateHash,
27 delegationAuthorityMarkerBlobKey,
28 delegationAuthorityMarkerFileName,
29 validateDelegationAuthorityEnvelope,
30 validateDelegationAuthorityMarker,
31 } from './delegation-authority-compat.mjs';
32 import { getTrustedCatalogIdentity } from './trusted-external-provider-catalog.mjs';
33 import {
34 DELEGATION_CONSENT_SCHEMA,
35 DELEGATION_GRANT_SCHEMA,
36 DELEGATION_GRANT_MINT_SCHEMA,
37 DELEGATION_CONSENTS_FILE,
38 DELEGATION_GRANTS_FILE,
39 DELEGATION_IDENTITIES_FILE,
40 GRANT_BEARER_PREFIX,
41 GRANT_ID_PREFIX,
42 hashGrantBearer,
43 hashPrincipalRef,
44 grantForClient,
45 loadConsentsStore,
46 loadGrantsStore,
47 loadIdentitiesStore,
48 } from './delegation.mjs';
49
50 export const DELEGATION_VALIDATION_SCHEMA = 'knowtation.delegation_validation/v1';
51 export const DELEGATION_ERROR_SCHEMA = 'knowtation.delegation_error/v1';
52 export const HELPER_ACCESS_SCHEMA = 'knowtation.helper_access/v1';
53
54 export const DELEGATION_REQUEST_INVALID = 'DELEGATION_REQUEST_INVALID';
55 export const DELEGATION_SESSION_REQUIRED = 'DELEGATION_SESSION_REQUIRED';
56 export const DELEGATION_HELPER_CONSENT_REQUIRED = 'DELEGATION_HELPER_CONSENT_REQUIRED';
57 export const DELEGATION_HELPER_ACTOR_DENIED = 'DELEGATION_HELPER_ACTOR_DENIED';
58 export const DELEGATION_AUTHORITY_DENIED = 'DELEGATION_AUTHORITY_DENIED';
59 export const DELEGATION_AUTHORITY_CONFLICT = 'DELEGATION_AUTHORITY_CONFLICT';
60 export const DELEGATION_HELPER_RENEW_RATE_LIMITED = 'DELEGATION_HELPER_RENEW_RATE_LIMITED';
61 export { DELEGATION_AUTHORITY_UNAVAILABLE };
62 export const DELEGATION_AUTHORITY_CAPACITY = 'DELEGATION_AUTHORITY_CAPACITY';
63
64 export const UTC_TIMESTAMP_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{1,6})?Z$/;
65
66 export const RETAIL_ACTOR_ID = 'agent_codex_retail';
67 export const RENEW_TTL_SECONDS = 900;
68 export const RENEW_MAX_ACTIONS = 64;
69 export const RENEW_RATE_LIMIT = 12;
70 export const RENEW_RATE_WINDOW_MS = 5 * 60 * 1000;
71
72 export const MAX_CONSENTS = 256;
73 export const MAX_GRANTS = 3072;
74 export const MAX_RATE_BUCKETS = 64;
75 export const MAX_AUDIT_OUTBOX = 128;
76 export const MAX_ENVELOPE_BYTES = 4 * 1024 * 1024;
77 export const CAS_MAX_RETRIES = 3;
78 export const RATE_BUCKET_CLOSED_PRUNE_MS = 10 * 60 * 1000;
79
80 const ENVELOPE_SCHEMA_VERSION = 1;
81 const MARKER_SCHEMA_VERSION = 1;
82 const SHA256_PREFIX = 'sha256:';
83
84 /**
85 * Read options for authority CAS gets.
86 *
87 * Netlify Functions Lambda-compat (`connectLambda`) has no `uncachedEdgeURL`, so
88 * `consistency: 'strong'` throws `BlobsConsistencyError` and helper-access returns 503
89 * while legacy grant list (eventual `get`) still succeeds. Match gateway-auth /
90 * CONNECT-GITHUB guidance: eventual on Lambda; strong only on persistent hosts.
91 *
92 * CAS write safety still comes from `getWithMetadata` etags + `onlyIfMatch` / `onlyIfNew`.
93 *
94 * @returns {{ type: 'text', consistency?: 'strong' }}
95 */
96 export function authorityBlobGetOpts() {
97 const onLambda = Boolean(process.env.NETLIFY || process.env.AWS_LAMBDA_FUNCTION_NAME);
98 return onLambda
99 ? Object.freeze({ type: 'text' })
100 : Object.freeze({ type: 'text', consistency: 'strong' });
101 }
102 const HKDF_SALT = 'scooling-delegation-authority/v1';
103 const HKDF_INFO = 'principal-subject-key';
104
105 /**
106 * @param {string} vaultId
107 * @returns {string}
108 */
109 export function delegationAuthorityEnvelopeBlobKey(vaultId) {
110 return `delegation/authority/v1/${encodeURIComponent(vaultId)}/envelope`;
111 }
112
113 /**
114 * @param {string} vaultId
115 * @returns {string}
116 */
117 export function delegationAuthorityCandidateBlobKey(vaultId) {
118 return `delegation/authority/v1/${encodeURIComponent(vaultId)}/candidate`;
119 }
120
121 /**
122 * @param {string} vaultId
123 * @returns {string}
124 */
125 export function delegationAuthorityEnvelopeFileName(vaultId) {
126 return `hub_delegation_authority_envelope_${vaultId}.json`;
127 }
128
129 /**
130 * @param {string} vaultId
131 * @returns {string}
132 */
133 export function delegationAuthorityCandidateFileName(vaultId) {
134 return `hub_delegation_authority_candidate_${vaultId}.json`;
135 }
136
137 /**
138 * @param {unknown} value
139 * @returns {boolean}
140 */
141 export function isStrictUtcTimestamp(value) {
142 if (typeof value !== 'string' || !UTC_TIMESTAMP_RE.test(value)) return false;
143 const ms = Date.parse(value);
144 return Number.isFinite(ms);
145 }
146
147 /**
148 * Optional empty expiry is allowed (no expiry). Nonempty malformed → invalid.
149 *
150 * @param {unknown} value
151 * @returns {{ ok: true, absent: boolean } | { ok: false }}
152 */
153 export function parseOptionalStrictUtc(value) {
154 if (value == null || value === '') return { ok: true, absent: true };
155 if (!isStrictUtcTimestamp(value)) return { ok: false };
156 return { ok: true, absent: false };
157 }
158
159 /**
160 * Active when not revoked and not expired at now >= expires_at.
161 *
162 * @param {object} consent
163 * @param {number} [nowMs]
164 * @returns {boolean}
165 */
166 export function isConsentActiveStrict(consent, nowMs = Date.now()) {
167 if (!consent || typeof consent !== 'object') return false;
168 if (consent.revoked_at) return false;
169 if (consent.expires_at) {
170 if (!isStrictUtcTimestamp(consent.expires_at)) return false;
171 if (nowMs >= Date.parse(consent.expires_at)) return false;
172 }
173 return true;
174 }
175
176 /**
177 * @param {object} grant
178 * @param {number} [nowMs]
179 * @returns {boolean}
180 */
181 export function isGrantActiveStrict(grant, nowMs = Date.now()) {
182 if (!grant || typeof grant !== 'object') return false;
183 if (grant.revoked_at) return false;
184 if (!isStrictUtcTimestamp(grant.expires_at)) return false;
185 if (nowMs >= Date.parse(grant.expires_at)) return false;
186 const max = typeof grant.max_actions === 'number' ? grant.max_actions : Infinity;
187 const count = typeof grant.action_count === 'number' ? grant.action_count : 0;
188 if (count >= max) return false;
189 return true;
190 }
191
192 /**
193 * @param {string} a
194 * @param {string} b
195 * @returns {boolean}
196 */
197 export function constantTimeEqualHexOrString(a, b) {
198 if (typeof a !== 'string' || typeof b !== 'string') return false;
199 const ba = Buffer.from(a, 'utf8');
200 const bb = Buffer.from(b, 'utf8');
201 if (ba.length !== bb.length) return false;
202 return timingSafeEqual(ba, bb);
203 }
204
205 /**
206 * @param {string} principalRef
207 * @param {string} actorId
208 * @returns {string}
209 */
210 export function principalActorKey(principalRef, actorId) {
211 return `${principalRef}\u0000${actorId}`;
212 }
213
214 /**
215 * @param {Buffer|string} ikm
216 * @returns {Buffer}
217 */
218 export function derivePrincipalSubjectKey(ikm) {
219 return Buffer.from(hkdfSync('sha256', ikm, HKDF_SALT, HKDF_INFO, 32));
220 }
221
222 /**
223 * @param {string} value
224 * @returns {Buffer}
225 */
226 function lengthPrefixedUtf8(value) {
227 const body = Buffer.from(value, 'utf8');
228 const len = Buffer.alloc(4);
229 len.writeUInt32BE(body.length, 0);
230 return Buffer.concat([len, body]);
231 }
232
233 /**
234 * HKDF + HMAC principal-bound subject → 43-char unpadded base64url.
235 *
236 * @param {{
237 * sessionSecret: string,
238 * sessionSecretPrevious?: string|null,
239 * uid: string,
240 * vaultId: string,
241 * actorId: string,
242 * }} input
243 * @returns {{ key_id: string, value: string }[]}
244 */
245 export function buildAuthoritySubjects(input) {
246 const subjects = [];
247 const make = (secret, keyId) => {
248 const key = derivePrincipalSubjectKey(secret);
249 const h = createHmac('sha256', key);
250 h.update(Buffer.from('v1', 'utf8'));
251 h.update(lengthPrefixedUtf8(input.uid));
252 h.update(lengthPrefixedUtf8(input.vaultId));
253 h.update(lengthPrefixedUtf8(input.actorId));
254 subjects.push({ key_id: keyId, value: h.digest('base64url') });
255 };
256 make(input.sessionSecret, 'current');
257 if (typeof input.sessionSecretPrevious === 'string' && input.sessionSecretPrevious.trim()) {
258 make(input.sessionSecretPrevious.trim(), 'previous');
259 }
260 return subjects;
261 }
262
263 /**
264 * @param {number} [byteLen]
265 * @returns {string}
266 */
267 function randomIdToken(byteLen = 16) {
268 return randomBytes(byteLen).toString('base64url').replace(/[^a-z0-9]/gi, '').toLowerCase().slice(0, 24);
269 }
270
271 /**
272 * @param {string} prefix
273 * @param {number} [byteLen]
274 * @returns {string}
275 */
276 function randomPrefixedId(prefix, byteLen = 16) {
277 const token = randomIdToken(byteLen);
278 return prefix + (token.length >= 8 ? token : token.padEnd(8, '0'));
279 }
280
281 /**
282 * @param {unknown} err
283 * @param {number} status
284 * @param {string} code
285 * @param {string} [message]
286 */
287 function fail(status, code, message) {
288 return {
289 ok: false,
290 status,
291 code,
292 error: message || code,
293 schema: DELEGATION_ERROR_SCHEMA,
294 };
295 }
296
297 /**
298 * In-memory / file CAS backend compatible with Netlify Blobs conditional writes.
299 */
300 export class MemoryCasBlobStore {
301 constructor() {
302 /** @type {Map<string, { data: string, etag: string, metadata: object }>} */
303 this.map = new Map();
304 this._seq = 0;
305 }
306
307 /**
308 * @param {string} key
309 * @param {{ type?: string, etag?: string }} [opts]
310 */
311 async getWithMetadata(key, opts = {}) {
312 const entry = this.map.get(key);
313 if (!entry) return null;
314 if (opts.etag && opts.etag === entry.etag) {
315 return { data: null, etag: entry.etag, metadata: entry.metadata };
316 }
317 return { data: entry.data, etag: entry.etag, metadata: entry.metadata };
318 }
319
320 /**
321 * @param {string} key
322 * @param {{ type?: string, consistency?: string }} [opts]
323 */
324 async get(key, _opts = {}) {
325 const entry = this.map.get(key);
326 if (!entry) return null;
327 return entry.data;
328 }
329
330 /**
331 * @param {string} key
332 * @param {string} value
333 * @param {{ onlyIfMatch?: string, onlyIfNew?: boolean }} [opts]
334 */
335 async set(key, value, opts = {}) {
336 const existing = this.map.get(key);
337 if (opts.onlyIfNew) {
338 if (existing) return { modified: false };
339 } else if (opts.onlyIfMatch) {
340 if (!existing || existing.etag !== opts.onlyIfMatch) return { modified: false };
341 }
342 const etag = `etag-${++this._seq}`;
343 this.map.set(key, { data: value, etag, metadata: {} });
344 return { modified: true, etag };
345 }
346 }
347
348 /**
349 * File-backed CAS for local DATA_DIR (etag sidecar).
350 */
351 export class FileCasBlobStore {
352 /**
353 * @param {string} dataDir
354 */
355 constructor(dataDir) {
356 this.dataDir = dataDir;
357 fs.mkdirSync(dataDir, { recursive: true });
358 }
359
360 /**
361 * @param {string} key
362 * @returns {string}
363 */
364 _pathFor(key) {
365 const safe = key.replace(/[/\\]/g, '__');
366 return path.join(this.dataDir, `cas_${safe}`);
367 }
368
369 /**
370 * @param {string} key
371 * @returns {string}
372 */
373 _etagPathFor(key) {
374 return `${this._pathFor(key)}.etag`;
375 }
376
377 /**
378 * @param {string} key
379 * @param {{ type?: string, etag?: string }} [opts]
380 */
381 async getWithMetadata(key, opts = {}) {
382 const fp = this._pathFor(key);
383 const ep = this._etagPathFor(key);
384 if (!fs.existsSync(fp)) return null;
385 let data;
386 let etag;
387 try {
388 data = fs.readFileSync(fp, 'utf8');
389 etag = fs.existsSync(ep) ? fs.readFileSync(ep, 'utf8').trim() : 'etag-0';
390 } catch {
391 return null;
392 }
393 if (opts.etag && opts.etag === etag) {
394 return { data: null, etag, metadata: {} };
395 }
396 return { data, etag, metadata: {} };
397 }
398
399 /**
400 * @param {string} key
401 * @param {{ type?: string }} [opts]
402 */
403 async get(key, _opts = {}) {
404 const got = await this.getWithMetadata(key);
405 return got ? got.data : null;
406 }
407
408 /**
409 * @param {string} key
410 * @param {string} value
411 * @param {{ onlyIfMatch?: string, onlyIfNew?: boolean }} [opts]
412 */
413 async set(key, value, opts = {}) {
414 const fp = this._pathFor(key);
415 const ep = this._etagPathFor(key);
416 const exists = fs.existsSync(fp);
417 let currentEtag = null;
418 if (exists) {
419 try {
420 currentEtag = fs.readFileSync(ep, 'utf8').trim();
421 } catch {
422 currentEtag = 'etag-0';
423 }
424 }
425 if (opts.onlyIfNew) {
426 if (exists) return { modified: false };
427 } else if (opts.onlyIfMatch) {
428 if (!exists || currentEtag !== opts.onlyIfMatch) return { modified: false };
429 }
430 const nextEtag = `etag-${Date.now()}-${randomBytes(4).toString('hex')}`;
431 fs.writeFileSync(fp, value, 'utf8');
432 fs.writeFileSync(ep, nextEtag, 'utf8');
433 return { modified: true, etag: nextEtag };
434 }
435 }
436
437 /**
438 * @param {object|null|undefined} blobStore
439 * @param {string} dataDir
440 * @returns {{ getWithMetadata: Function, set: Function, get?: Function }}
441 */
442 export function resolveAuthorityCasStore(blobStore, dataDir) {
443 if (
444 blobStore &&
445 typeof blobStore.getWithMetadata === 'function' &&
446 typeof blobStore.set === 'function'
447 ) {
448 return blobStore;
449 }
450 return new FileCasBlobStore(dataDir);
451 }
452
453 /**
454 * @param {object} envelope
455 * @returns {object}
456 */
457 export function sealEnvelopeStateHash(envelope) {
458 const next = { ...envelope };
459 delete next.state_hash;
460 next.state_hash = computeDelegationAuthorityStateHash(next);
461 return next;
462 }
463
464 /**
465 * @param {object} envelope
466 * @returns {{ ok: true } | { ok: false, reason: string }}
467 */
468 export function validateEnvelopeInternalIntegrity(envelope) {
469 if (!envelope || typeof envelope !== 'object') return { ok: false, reason: 'missing' };
470 if (envelope.schema !== DELEGATION_AUTHORITY_ENVELOPE_SCHEMA) {
471 return { ok: false, reason: 'schema' };
472 }
473 for (const field of [
474 'identities_by_id',
475 'consents_by_id',
476 'grants_by_id',
477 'grant_id_by_bearer_hash',
478 'newest_active_consent_id_by_principal_actor',
479 'rate_buckets_by_principal_actor',
480 'audit_outbox_by_id',
481 ]) {
482 if (envelope[field] == null) continue;
483 if (typeof envelope[field] !== 'object' || Array.isArray(envelope[field])) {
484 return { ok: false, reason: field };
485 }
486 }
487
488 for (const consent of Object.values(envelope.consents_by_id || {})) {
489 if (!consent || typeof consent !== 'object') return { ok: false, reason: 'consent' };
490 if (!isStrictUtcTimestamp(consent.created)) return { ok: false, reason: 'consent.created' };
491 const exp = parseOptionalStrictUtc(consent.expires_at);
492 if (!exp.ok) return { ok: false, reason: 'consent.expires_at' };
493 if (consent.revoked_at != null && consent.revoked_at !== '') {
494 if (!isStrictUtcTimestamp(consent.revoked_at)) return { ok: false, reason: 'consent.revoked_at' };
495 }
496 }
497 for (const grant of Object.values(envelope.grants_by_id || {})) {
498 if (!grant || typeof grant !== 'object') return { ok: false, reason: 'grant' };
499 for (const field of ['issued_at', 'expires_at']) {
500 if (!isStrictUtcTimestamp(grant[field])) return { ok: false, reason: `grant.${field}` };
501 }
502 if (grant.revoked_at != null && grant.revoked_at !== '') {
503 if (!isStrictUtcTimestamp(grant.revoked_at)) return { ok: false, reason: 'grant.revoked_at' };
504 }
505 }
506
507 const expected = computeDelegationAuthorityStateHash(envelope);
508 if (expected !== envelope.state_hash) return { ok: false, reason: 'state_hash' };
509 return { ok: true };
510 }
511
512 /**
513 * @param {object} envelope
514 * @returns {boolean}
515 */
516 function underCapacity(envelope) {
517 const consents = Object.keys(envelope.consents_by_id || {}).length;
518 const grants = Object.keys(envelope.grants_by_id || {}).length;
519 const buckets = Object.keys(envelope.rate_buckets_by_principal_actor || {}).length;
520 const outbox = Object.keys(envelope.audit_outbox_by_id || {}).length;
521 if (consents > MAX_CONSENTS) return false;
522 if (grants > MAX_GRANTS) return false;
523 if (buckets > MAX_RATE_BUCKETS) return false;
524 if (outbox > MAX_AUDIT_OUTBOX) return false;
525 const serialized = JSON.stringify(envelope);
526 if (Buffer.byteLength(serialized, 'utf8') > MAX_ENVELOPE_BYTES) return false;
527 return true;
528 }
529
530 /**
531 * @param {object} entry
532 * @returns {string}
533 */
534 function hashAuditEvent(entry) {
535 const body = JSON.stringify({
536 operation_id: entry.operation_id,
537 record_kind: entry.record_kind,
538 record_id: entry.record_id,
539 sequence: entry.sequence,
540 prior_audit_event_hash: entry.prior_audit_event_hash ?? null,
541 });
542 return `${SHA256_PREFIX}${createHash('sha256').update(body, 'utf8').digest('hex')}`;
543 }
544
545 /**
546 * Contiguous materializer: only last_materialized + 1 is eligible (freeze §B2).
547 * Writes external audit key with onlyIfNew; advances last_materialized on success.
548 *
549 * @param {object} envelope
550 * @param {{ getWithMetadata?: Function, set?: Function, get?: Function }} cas
551 * @returns {Promise<object>}
552 */
553 export async function materializeAuditOutbox(envelope, cas) {
554 let next = { ...envelope, audit_outbox_by_id: { ...(envelope.audit_outbox_by_id || {}) } };
555 const chainHeads = { ...(next.event_chain_heads_by_record || {}) };
556
557 const records = [
558 ...Object.values(next.consents_by_id || {}).map((r) => ({ kind: 'consent', record: r })),
559 ...Object.values(next.grants_by_id || {}).map((r) => ({ kind: 'grant', record: r })),
560 ];
561
562 for (const { kind, record } of records) {
563 if (!record) continue;
564 const lastMat = record.last_materialized_audit_sequence || 0;
565 const targetSeq = lastMat + 1;
566 if ((record.audit_sequence || 0) < targetSeq) continue;
567
568 const entry = Object.values(next.audit_outbox_by_id).find(
569 (e) =>
570 e &&
571 e.record_kind === kind &&
572 e.record_id === record[kind === 'consent' ? 'consent_id' : 'grant_id'] &&
573 e.sequence === targetSeq,
574 );
575 if (!entry) continue; // out-of-order later entries remain pending
576
577 const auditKey = `delegation/audit/v1/${entry.operation_id}`;
578 const eventHash = hashAuditEvent(entry);
579 const payload = JSON.stringify({
580 schema: 'knowtation.delegation_audit_event/v1',
581 ...entry,
582 event_hash: eventHash,
583 });
584
585 if (cas && typeof cas.set === 'function') {
586 const existing = await cas.get(auditKey, authorityBlobGetOpts()).catch(() => null);
587 if (typeof existing === 'string' && existing.trim()) {
588 if (existing.trim() !== payload) {
589 // mismatched content blocks materialization for this record
590 continue;
591 }
592 } else {
593 const write = await cas.set(auditKey, payload, { onlyIfNew: true });
594 if (write && write.modified === false) {
595 const again = await cas.get(auditKey, authorityBlobGetOpts()).catch(() => null);
596 if (typeof again !== 'string' || again.trim() !== payload) continue;
597 }
598 }
599 }
600
601 const recordKey = kind === 'consent' ? 'consent_id' : 'grant_id';
602 const id = record[recordKey];
603 if (kind === 'grant') {
604 next.grants_by_id = {
605 ...next.grants_by_id,
606 [id]: {
607 ...record,
608 last_materialized_audit_sequence: targetSeq,
609 pending_audit_count: Math.max(0, (record.pending_audit_count || 1) - 1),
610 },
611 };
612 } else {
613 next.consents_by_id = {
614 ...next.consents_by_id,
615 [id]: {
616 ...record,
617 last_materialized_audit_sequence: targetSeq,
618 pending_audit_count: Math.max(0, (record.pending_audit_count || 1) - 1),
619 },
620 };
621 }
622 chainHeads[`${kind}:${id}`] = eventHash;
623 const outbox = { ...next.audit_outbox_by_id };
624 delete outbox[entry.operation_id];
625 next.audit_outbox_by_id = outbox;
626 }
627
628 next.event_chain_heads_by_record = chainHeads;
629 return next;
630 }
631
632 /**
633 * @param {object} record
634 * @param {string} operationId
635 * @param {string|null} priorHash
636 * @returns {{ record: object, outboxEntry: object }}
637 */
638 function bumpRecordAudit(record, operationId, priorHash) {
639 const audit_sequence = (record.audit_sequence || 0) + 1;
640 const pending_audit_count = (record.pending_audit_count || 0) + 1;
641 const last_materialized_audit_sequence =
642 typeof record.last_materialized_audit_sequence === 'number'
643 ? record.last_materialized_audit_sequence
644 : 0;
645 return {
646 record: {
647 ...record,
648 audit_sequence,
649 pending_audit_count,
650 last_materialized_audit_sequence,
651 },
652 outboxEntry: {
653 operation_id: operationId,
654 sequence: audit_sequence,
655 prior_audit_event_hash: priorHash,
656 },
657 };
658 }
659
660 /**
661 * Prune closed rate buckets older than 10 minutes; prune expired grants only when audit-complete.
662 *
663 * @param {object} envelope
664 * @param {number} nowMs
665 * @returns {object}
666 */
667 export function pruneAuthorityEnvelope(envelope, nowMs = Date.now()) {
668 const next = structuredClone(envelope);
669 const buckets = { ...(next.rate_buckets_by_principal_actor || {}) };
670 for (const [key, bucket] of Object.entries(buckets)) {
671 const stamps = Array.isArray(bucket?.renew_at_ms) ? bucket.renew_at_ms : [];
672 const live = stamps.filter((t) => nowMs - t < RENEW_RATE_WINDOW_MS);
673 if (live.length === 0) {
674 const closedAt = typeof bucket?.closed_at_ms === 'number' ? bucket.closed_at_ms : nowMs;
675 if (nowMs - closedAt >= RATE_BUCKET_CLOSED_PRUNE_MS) {
676 delete buckets[key];
677 continue;
678 }
679 buckets[key] = { renew_at_ms: [], closed_at_ms: closedAt };
680 } else {
681 buckets[key] = { renew_at_ms: live };
682 }
683 }
684 next.rate_buckets_by_principal_actor = buckets;
685
686 const grants = { ...(next.grants_by_id || {}) };
687 const byHash = { ...(next.grant_id_by_bearer_hash || {}) };
688 const outbox = next.audit_outbox_by_id || {};
689 for (const [grantId, grant] of Object.entries(grants)) {
690 if (isGrantActiveStrict(grant, nowMs)) continue;
691 const pending = grant.pending_audit_count || 0;
692 const lastMat = grant.last_materialized_audit_sequence || 0;
693 const seq = grant.audit_sequence || 0;
694 if (pending !== 0) continue;
695 if (lastMat !== seq) continue;
696 const referenced = Object.values(outbox).some(
697 (e) => e && e.record_kind === 'grant' && e.record_id === grantId,
698 );
699 if (referenced) continue;
700 const hash = grant.grant_bearer_hash;
701 delete grants[grantId];
702 if (hash && byHash[hash] === grantId) delete byHash[hash];
703 }
704 next.grants_by_id = grants;
705 next.grant_id_by_bearer_hash = byHash;
706 return next;
707 }
708
709 /**
710 * Rebuild newest-active consent index.
711 *
712 * @param {object} envelope
713 * @param {number} [nowMs]
714 * @returns {object}
715 */
716 export function rebuildNewestActiveConsentIndex(envelope, nowMs = Date.now()) {
717 /** @type {Record<string, string>} */
718 const index = {};
719 /** @type {Record<string, object[]>} */
720 const groups = {};
721 for (const consent of Object.values(envelope.consents_by_id || {})) {
722 if (!isConsentActiveStrict(consent, nowMs)) continue;
723 if (consent.scope !== 'personal') continue;
724 const key = principalActorKey(consent.principal_ref, consent.delegate_agent_id);
725 if (!groups[key]) groups[key] = [];
726 groups[key].push(consent);
727 }
728 for (const [key, list] of Object.entries(groups)) {
729 list.sort((a, b) => {
730 const ca = Date.parse(a.created);
731 const cb = Date.parse(b.created);
732 if (cb !== ca) return cb - ca;
733 return String(a.consent_id).localeCompare(String(b.consent_id));
734 });
735 index[key] = list[0].consent_id;
736 }
737 return { ...envelope, newest_active_consent_id_by_principal_actor: index };
738 }
739
740 /**
741 * Select active personal consent for principal+actor (newest created, then consent_id asc).
742 *
743 * @param {object} envelope
744 * @param {string} principalRef
745 * @param {string} actorId
746 * @param {number} [nowMs]
747 * @returns {object|null}
748 */
749 export function selectActivePersonalConsent(envelope, principalRef, actorId, nowMs = Date.now()) {
750 const key = principalActorKey(principalRef, actorId);
751 const indexedId = envelope.newest_active_consent_id_by_principal_actor?.[key];
752 if (indexedId) {
753 const c = envelope.consents_by_id?.[indexedId];
754 if (c && isConsentActiveStrict(c, nowMs) && c.scope === 'personal') return c;
755 }
756 const candidates = Object.values(envelope.consents_by_id || {}).filter(
757 (c) =>
758 c &&
759 c.principal_ref === principalRef &&
760 c.delegate_agent_id === actorId &&
761 c.scope === 'personal' &&
762 isConsentActiveStrict(c, nowMs),
763 );
764 if (!candidates.length) return null;
765 candidates.sort((a, b) => {
766 const ca = Date.parse(a.created);
767 const cb = Date.parse(b.created);
768 if (cb !== ca) return cb - ca;
769 return String(a.consent_id).localeCompare(String(b.consent_id));
770 });
771 return candidates[0];
772 }
773
774 /**
775 * @param {object} catalogIdentity
776 * @returns {boolean}
777 */
778 export function isRetailCodexIdentity(catalogIdentity) {
779 return Boolean(
780 catalogIdentity &&
781 catalogIdentity.agent_id === RETAIL_ACTOR_ID &&
782 catalogIdentity.kind === 'external_provider' &&
783 catalogIdentity.provider === 'codex' &&
784 catalogIdentity.scope_ceiling === 'personal' &&
785 catalogIdentity.status === 'active',
786 );
787 }
788
789 /**
790 * @param {{
791 * dataDir: string,
792 * vaultId: string,
793 * blobStore?: object|null,
794 * nowMs?: number,
795 * sessionSecret?: string,
796 * sessionSecretPrevious?: string|null,
797 * operatorAuthorizedMarker?: boolean,
798 * }} opts
799 */
800 export function createDelegationAuthorityStore(opts) {
801 const dataDir = opts.dataDir;
802 const vaultId = opts.vaultId;
803 const cas = resolveAuthorityCasStore(opts.blobStore ?? null, dataDir);
804 const envelopeKey = delegationAuthorityEnvelopeBlobKey(vaultId);
805 const candidateKey = delegationAuthorityCandidateBlobKey(vaultId);
806 const markerKey = delegationAuthorityMarkerBlobKey(vaultId);
807 const hostedBlob = Boolean(opts.blobStore);
808
809 /**
810 * @returns {Promise<
811 * | { ok: true, envelope: object, etag: string|null, mode: 'envelope' }
812 * | { ok: false, status: number, code: string, error: string, schema: string }
813 * >}
814 */
815 async function readActiveEnvelope() {
816 let markerRaw;
817 try {
818 markerRaw = await cas.get(markerKey, authorityBlobGetOpts());
819 } catch {
820 // Fail closed — never fall back to a stale local mirror (BV-1).
821 return fail(503, DELEGATION_AUTHORITY_UNAVAILABLE, 'Authority marker read error');
822 }
823 if (!markerRaw || (typeof markerRaw === 'string' && !markerRaw.trim())) {
824 // Local-only FileCas path may still read marker files. Hosted blob: absent = inactive.
825 if (hostedBlob) {
826 return fail(503, DELEGATION_AUTHORITY_UNAVAILABLE, 'Authority envelope not active');
827 }
828 const localMarker = path.join(dataDir, delegationAuthorityMarkerFileName(vaultId));
829 if (!fs.existsSync(localMarker)) {
830 return fail(503, DELEGATION_AUTHORITY_UNAVAILABLE, 'Authority envelope not active');
831 }
832 let marker;
833 try {
834 marker = JSON.parse(fs.readFileSync(localMarker, 'utf8'));
835 } catch {
836 return fail(503, DELEGATION_AUTHORITY_UNAVAILABLE, 'Authority marker unreadable');
837 }
838 const mv = validateDelegationAuthorityMarker(marker, vaultId);
839 if (!mv.ok) return fail(503, DELEGATION_AUTHORITY_UNAVAILABLE, mv.reason);
840 const localEnvPath = path.join(
841 dataDir,
842 path.basename(marker.envelope_key) || delegationAuthorityEnvelopeFileName(vaultId),
843 );
844 if (!fs.existsSync(localEnvPath)) {
845 return fail(503, DELEGATION_AUTHORITY_UNAVAILABLE, 'Authority envelope missing');
846 }
847 let envelope;
848 try {
849 envelope = JSON.parse(fs.readFileSync(localEnvPath, 'utf8'));
850 } catch {
851 return fail(503, DELEGATION_AUTHORITY_UNAVAILABLE, 'Authority envelope unreadable');
852 }
853 const ev = validateDelegationAuthorityEnvelope(envelope, marker, vaultId);
854 if (!ev.ok) return fail(503, DELEGATION_AUTHORITY_UNAVAILABLE, ev.reason);
855 const iv = validateEnvelopeInternalIntegrity(envelope);
856 if (!iv.ok) return fail(503, DELEGATION_AUTHORITY_UNAVAILABLE, iv.reason);
857 // Promote into CAS so subsequent writes always have an etag (no etag:null mutate).
858 const seeded = await cas.set(envelopeKey, JSON.stringify(envelope), { onlyIfNew: true });
859 const got = await cas.getWithMetadata(envelopeKey, authorityBlobGetOpts()).catch(() => null);
860 return {
861 ok: true,
862 envelope,
863 etag: got?.etag || seeded?.etag || null,
864 mode: 'envelope',
865 };
866 }
867
868 let marker;
869 try {
870 marker = typeof markerRaw === 'string' ? JSON.parse(markerRaw) : markerRaw;
871 } catch {
872 return fail(503, DELEGATION_AUTHORITY_UNAVAILABLE, 'Authority marker parse error');
873 }
874 const mv = validateDelegationAuthorityMarker(marker, vaultId);
875 if (!mv.ok) return fail(503, DELEGATION_AUTHORITY_UNAVAILABLE, mv.reason);
876
877 let got;
878 try {
879 got = await cas.getWithMetadata(marker.envelope_key || envelopeKey, authorityBlobGetOpts());
880 } catch {
881 return fail(503, DELEGATION_AUTHORITY_UNAVAILABLE, 'Authority envelope read error');
882 }
883 if (!got || got.data == null) {
884 return fail(503, DELEGATION_AUTHORITY_UNAVAILABLE, 'Authority envelope missing');
885 }
886 let envelope;
887 try {
888 envelope = typeof got.data === 'string' ? JSON.parse(got.data) : got.data;
889 } catch {
890 return fail(503, DELEGATION_AUTHORITY_UNAVAILABLE, 'Authority envelope parse error');
891 }
892 const ev = validateDelegationAuthorityEnvelope(envelope, marker, vaultId);
893 if (!ev.ok) return fail(503, DELEGATION_AUTHORITY_UNAVAILABLE, ev.reason);
894 const iv = validateEnvelopeInternalIntegrity(envelope);
895 if (!iv.ok) return fail(503, DELEGATION_AUTHORITY_UNAVAILABLE, iv.reason);
896 return { ok: true, envelope, etag: got.etag ?? null, mode: 'envelope' };
897 }
898
899 /**
900 * @param {(envelope: object) =>
901 * | { ok: true, envelope: object, result: object }
902 * | { ok: false, status: number, code: string, error?: string }
903 * } transform
904 */
905 async function mutateEnvelope(transform) {
906 let attempt = 0;
907 while (attempt < CAS_MAX_RETRIES) {
908 attempt += 1;
909 const loaded = await readActiveEnvelope();
910 if (!loaded.ok) return loaded;
911
912 let working = pruneAuthorityEnvelope(loaded.envelope, opts.nowMs ?? Date.now());
913 working = rebuildNewestActiveConsentIndex(working, opts.nowMs ?? Date.now());
914
915 const transformed = transform(working);
916 if (!transformed.ok) {
917 return fail(transformed.status, transformed.code, transformed.error || transformed.code);
918 }
919
920 let next = transformed.envelope;
921 if (!underCapacity(next)) {
922 return fail(507, DELEGATION_AUTHORITY_CAPACITY, 'Authority envelope at capacity');
923 }
924
925 const priorHash = loaded.envelope.state_hash;
926 next = {
927 ...next,
928 revision: (loaded.envelope.revision || 0) + 1,
929 previous_state_hash: priorHash,
930 };
931 next = sealEnvelopeStateHash(next);
932
933 const iv = validateEnvelopeInternalIntegrity(next);
934 if (!iv.ok) {
935 return fail(503, DELEGATION_AUTHORITY_UNAVAILABLE, `Envelope invalid after transform: ${iv.reason}`);
936 }
937
938 const payload = JSON.stringify(next);
939 if (Buffer.byteLength(payload, 'utf8') > MAX_ENVELOPE_BYTES) {
940 return fail(507, DELEGATION_AUTHORITY_CAPACITY, 'Authority envelope exceeds size limit');
941 }
942
943 try {
944 let etag = loaded.etag;
945 if (!etag) {
946 // Recover etag via strong read — never blind-overwrite.
947 const fresh = await cas.getWithMetadata(envelopeKey, authorityBlobGetOpts()).catch(() => null);
948 if (fresh && fresh.etag) {
949 etag = fresh.etag;
950 } else {
951 const created = await cas.set(envelopeKey, payload, { onlyIfNew: true });
952 if (!created || created.modified === false) {
953 const jitter = 5 + Math.floor(Math.random() * 20) * attempt;
954 await new Promise((r) => setTimeout(r, jitter));
955 continue;
956 }
957 let drained = await materializeAuditOutbox(next, cas);
958 drained = {
959 ...drained,
960 revision: (next.revision || 0) + 1,
961 previous_state_hash: next.state_hash,
962 };
963 drained = sealEnvelopeStateHash(drained);
964 if (created.etag) {
965 await cas.set(envelopeKey, JSON.stringify(drained), { onlyIfMatch: created.etag });
966 }
967 next = drained;
968 if (!hostedBlob) {
969 const localPath = path.join(dataDir, path.basename(envelopeKey));
970 fs.writeFileSync(localPath, JSON.stringify(next), 'utf8');
971 }
972 return { ok: true, ...transformed.result, envelope: next };
973 }
974 }
975 const write = await cas.set(envelopeKey, payload, { onlyIfMatch: etag });
976 if (!write || write.modified === false) {
977 const jitter = 5 + Math.floor(Math.random() * 20) * attempt;
978 await new Promise((r) => setTimeout(r, jitter));
979 continue;
980 }
981 // Materialize only AFTER authority CAS (crash before CAS => no external audit).
982 let drained = await materializeAuditOutbox(next, cas);
983 drained = {
984 ...drained,
985 revision: (next.revision || 0) + 1,
986 previous_state_hash: next.state_hash,
987 };
988 drained = sealEnvelopeStateHash(drained);
989 const drainPayload = JSON.stringify(drained);
990 const drainEtag = write.etag;
991 if (drainEtag) {
992 const drainWrite = await cas.set(envelopeKey, drainPayload, { onlyIfMatch: drainEtag });
993 if (drainWrite && drainWrite.modified !== false) {
994 next = drained;
995 }
996 // If drain CAS conflicts, pending outbox remains — resumable next mutation.
997 } else {
998 next = drained;
999 await cas.set(envelopeKey, drainPayload).catch(() => {});
1000 }
1001 if (!hostedBlob) {
1002 const localPath = path.join(dataDir, path.basename(envelopeKey));
1003 fs.writeFileSync(localPath, JSON.stringify(next), 'utf8');
1004 }
1005 } catch {
1006 return fail(503, DELEGATION_AUTHORITY_UNAVAILABLE, 'Authority envelope write error');
1007 }
1008
1009 return { ok: true, ...transformed.result, envelope: next };
1010 }
1011 return fail(409, DELEGATION_AUTHORITY_CONFLICT, 'Authority envelope conflict');
1012 }
1013
1014 /**
1015 * @param {string} uid
1016 * @param {string} actorId
1017 */
1018 async function readHelperAccess(uid, actorId) {
1019 const actor = typeof actorId === 'string' ? actorId.trim() : '';
1020 if (actor !== RETAIL_ACTOR_ID) {
1021 return fail(403, DELEGATION_HELPER_ACTOR_DENIED, 'Helper actor denied');
1022 }
1023 const catalog = getTrustedCatalogIdentity(RETAIL_ACTOR_ID);
1024 if (!isRetailCodexIdentity(catalog)) {
1025 return fail(403, DELEGATION_HELPER_ACTOR_DENIED, 'Helper actor denied');
1026 }
1027 const loaded = await readActiveEnvelope();
1028 if (!loaded.ok) return loaded;
1029 const principal = hashPrincipalRef(uid);
1030 const nowMs = opts.nowMs ?? Date.now();
1031 const consent = selectActivePersonalConsent(loaded.envelope, principal, actor, nowMs);
1032 if (!consent) {
1033 return {
1034 ok: true,
1035 payload: {
1036 schema: HELPER_ACCESS_SCHEMA,
1037 actor_agent_id: RETAIL_ACTOR_ID,
1038 state: 'consent_required',
1039 },
1040 };
1041 }
1042 const gKey = principalActorKey(principal, actor);
1043 const indexedGrantId = loaded.envelope.active_grant_id_by_principal_actor?.[gKey];
1044 let activeGrant = indexedGrantId ? loaded.envelope.grants_by_id?.[indexedGrantId] : null;
1045 if (!activeGrant || !isGrantActiveStrict(activeGrant, nowMs) || activeGrant.principal_ref !== principal) {
1046 activeGrant = null;
1047 }
1048 return {
1049 ok: true,
1050 payload: {
1051 schema: HELPER_ACCESS_SCHEMA,
1052 actor_agent_id: RETAIL_ACTOR_ID,
1053 state: activeGrant ? 'ready' : 'renewable',
1054 },
1055 };
1056 }
1057
1058 /**
1059 * @param {string} uid
1060 * @param {string} actorId
1061 */
1062 async function renewPersonal(uid, actorId) {
1063 const actor = typeof actorId === 'string' ? actorId.trim() : '';
1064 if (actor !== RETAIL_ACTOR_ID) {
1065 return fail(403, DELEGATION_HELPER_ACTOR_DENIED, 'Helper actor denied');
1066 }
1067 const catalog = getTrustedCatalogIdentity(RETAIL_ACTOR_ID);
1068 if (!isRetailCodexIdentity(catalog)) {
1069 return fail(403, DELEGATION_HELPER_ACTOR_DENIED, 'Helper actor denied');
1070 }
1071 const principal = hashPrincipalRef(uid);
1072 const nowMs = opts.nowMs ?? Date.now();
1073
1074 return mutateEnvelope((envelope) => {
1075 const consent = selectActivePersonalConsent(envelope, principal, actor, nowMs);
1076 if (!consent) {
1077 return {
1078 ok: false,
1079 status: 403,
1080 code: DELEGATION_HELPER_CONSENT_REQUIRED,
1081 error: 'Helper consent required',
1082 };
1083 }
1084
1085 const bucketKey = principalActorKey(principal, actor);
1086 const buckets = { ...(envelope.rate_buckets_by_principal_actor || {}) };
1087 const bucket = buckets[bucketKey] || { renew_at_ms: [] };
1088 const recent = (bucket.renew_at_ms || []).filter((t) => nowMs - t < RENEW_RATE_WINDOW_MS);
1089 if (recent.length >= RENEW_RATE_LIMIT) {
1090 return {
1091 ok: false,
1092 status: 429,
1093 code: DELEGATION_HELPER_RENEW_RATE_LIMITED,
1094 error: 'Helper renew rate limited',
1095 };
1096 }
1097 if (
1098 Object.keys(buckets).length >= MAX_RATE_BUCKETS &&
1099 !buckets[bucketKey]
1100 ) {
1101 return {
1102 ok: false,
1103 status: 507,
1104 code: DELEGATION_AUTHORITY_CAPACITY,
1105 error: 'Rate bucket capacity',
1106 };
1107 }
1108
1109 if (Object.keys(envelope.grants_by_id || {}).length >= MAX_GRANTS) {
1110 return {
1111 ok: false,
1112 status: 507,
1113 code: DELEGATION_AUTHORITY_CAPACITY,
1114 error: 'Grant capacity',
1115 };
1116 }
1117
1118 const issuedAt = new Date(nowMs).toISOString();
1119 if (!isStrictUtcTimestamp(issuedAt)) {
1120 return {
1121 ok: false,
1122 status: 503,
1123 code: DELEGATION_AUTHORITY_UNAVAILABLE,
1124 error: 'Clock unavailable',
1125 };
1126 }
1127 const expiresAt = new Date(nowMs + RENEW_TTL_SECONDS * 1000).toISOString();
1128 const grantId = randomPrefixedId(GRANT_ID_PREFIX);
1129 const bearer = randomPrefixedId(GRANT_BEARER_PREFIX, 24);
1130 const bearerHash = hashGrantBearer(bearer);
1131 const operationId = randomBytes(16).toString('hex');
1132 const chainKey = `grant:${grantId}`;
1133 const priorHash = envelope.event_chain_heads_by_record?.[chainKey] ?? null;
1134
1135 let grant = {
1136 schema: DELEGATION_GRANT_SCHEMA,
1137 grant_id: grantId,
1138 consent_id: consent.consent_id,
1139 actor_agent_id: actor,
1140 principal_ref: principal,
1141 scope: 'personal',
1142 expires_at: expiresAt,
1143 revoked_at: null,
1144 max_actions: RENEW_MAX_ACTIONS,
1145 action_count: 0,
1146 issued_at: issuedAt,
1147 grant_bearer_hash: bearerHash,
1148 audit_sequence: 0,
1149 last_materialized_audit_sequence: 0,
1150 pending_audit_count: 0,
1151 };
1152
1153 const bumped = bumpRecordAudit(grant, operationId, priorHash);
1154 grant = bumped.record;
1155
1156 if (Object.keys(envelope.audit_outbox_by_id || {}).length >= MAX_AUDIT_OUTBOX) {
1157 return {
1158 ok: false,
1159 status: 507,
1160 code: DELEGATION_AUTHORITY_CAPACITY,
1161 error: 'Audit outbox full',
1162 };
1163 }
1164
1165 const grants_by_id = { ...(envelope.grants_by_id || {}), [grantId]: grant };
1166 const grant_id_by_bearer_hash = {
1167 ...(envelope.grant_id_by_bearer_hash || {}),
1168 [bearerHash]: grantId,
1169 };
1170 const active_grant_id_by_principal_actor = {
1171 ...(envelope.active_grant_id_by_principal_actor || {}),
1172 [bucketKey]: grantId,
1173 };
1174 const audit_outbox_by_id = {
1175 ...(envelope.audit_outbox_by_id || {}),
1176 [operationId]: {
1177 operation_id: operationId,
1178 record_kind: 'grant',
1179 record_id: grantId,
1180 sequence: bumped.outboxEntry.sequence,
1181 prior_audit_event_hash: priorHash,
1182 created_at: issuedAt,
1183 },
1184 };
1185 buckets[bucketKey] = { renew_at_ms: [...recent, nowMs] };
1186
1187 const next = {
1188 ...envelope,
1189 grants_by_id,
1190 grant_id_by_bearer_hash,
1191 active_grant_id_by_principal_actor,
1192 audit_outbox_by_id,
1193 rate_buckets_by_principal_actor: buckets,
1194 };
1195
1196 return {
1197 ok: true,
1198 envelope: next,
1199 result: {
1200 payload: {
1201 schema: DELEGATION_GRANT_MINT_SCHEMA,
1202 grant: grantForClient(grant),
1203 bearer,
1204 expires_at: expiresAt,
1205 },
1206 },
1207 };
1208 });
1209 }
1210
1211 /**
1212 * @param {{
1213 * uid: string,
1214 * bearer: string,
1215 * actorId: string,
1216 * visitHandle: string,
1217 * }} input
1218 */
1219 async function validateAndConsume(input) {
1220 const actor = typeof input.actorId === 'string' ? input.actorId.trim() : '';
1221 const bearer = typeof input.bearer === 'string' ? input.bearer.trim() : '';
1222 const visitHandle = typeof input.visitHandle === 'string' ? input.visitHandle.trim() : '';
1223 if (!bearer || !actor || !visitHandle) {
1224 return fail(400, DELEGATION_REQUEST_INVALID, 'Invalid validation request');
1225 }
1226 if (actor !== RETAIL_ACTOR_ID) {
1227 return fail(403, DELEGATION_AUTHORITY_DENIED, 'Authority denied');
1228 }
1229 if (!/^[A-Za-z0-9_-]{43}$/.test(visitHandle)) {
1230 return fail(400, DELEGATION_REQUEST_INVALID, 'Invalid visit handle');
1231 }
1232 const catalog = getTrustedCatalogIdentity(RETAIL_ACTOR_ID);
1233 if (!isRetailCodexIdentity(catalog)) {
1234 return fail(403, DELEGATION_AUTHORITY_DENIED, 'Authority denied');
1235 }
1236 if (!opts.sessionSecret) {
1237 return fail(503, DELEGATION_AUTHORITY_UNAVAILABLE, 'Session secret unavailable');
1238 }
1239
1240 const principal = hashPrincipalRef(input.uid);
1241 const bearerHash = hashGrantBearer(bearer);
1242 const nowMs = opts.nowMs ?? Date.now();
1243
1244 const mutated = await mutateEnvelope((envelope) => {
1245 const grantId = envelope.grant_id_by_bearer_hash?.[bearerHash];
1246 if (!grantId) {
1247 return { ok: false, status: 403, code: DELEGATION_AUTHORITY_DENIED, error: 'Authority denied' };
1248 }
1249 const grant = envelope.grants_by_id?.[grantId];
1250 if (!grant || !constantTimeEqualHexOrString(grant.grant_bearer_hash, bearerHash)) {
1251 return { ok: false, status: 403, code: DELEGATION_AUTHORITY_DENIED, error: 'Authority denied' };
1252 }
1253 if (
1254 grant.principal_ref !== principal ||
1255 grant.actor_agent_id !== actor ||
1256 grant.scope !== 'personal' ||
1257 !isGrantActiveStrict(grant, nowMs)
1258 ) {
1259 return { ok: false, status: 403, code: DELEGATION_AUTHORITY_DENIED, error: 'Authority denied' };
1260 }
1261 const consent = envelope.consents_by_id?.[grant.consent_id];
1262 if (
1263 !consent ||
1264 !isConsentActiveStrict(consent, nowMs) ||
1265 consent.principal_ref !== principal ||
1266 consent.delegate_agent_id !== actor ||
1267 consent.scope !== 'personal'
1268 ) {
1269 return { ok: false, status: 403, code: DELEGATION_AUTHORITY_DENIED, error: 'Authority denied' };
1270 }
1271
1272 const operationId = randomBytes(16).toString('hex');
1273 const issuedAt = new Date(nowMs).toISOString();
1274 const chainKey = `grant:${grantId}`;
1275 const priorHash = envelope.event_chain_heads_by_record?.[chainKey] ?? null;
1276 let nextGrant = {
1277 ...grant,
1278 action_count: (grant.action_count || 0) + 1,
1279 };
1280 const bumped = bumpRecordAudit(nextGrant, operationId, priorHash);
1281 nextGrant = bumped.record;
1282
1283 if (Object.keys(envelope.audit_outbox_by_id || {}).length >= MAX_AUDIT_OUTBOX) {
1284 return {
1285 ok: false,
1286 status: 507,
1287 code: DELEGATION_AUTHORITY_CAPACITY,
1288 error: 'Audit outbox full',
1289 };
1290 }
1291
1292 const grants_by_id = { ...(envelope.grants_by_id || {}), [grantId]: nextGrant };
1293 const audit_outbox_by_id = {
1294 ...(envelope.audit_outbox_by_id || {}),
1295 [operationId]: {
1296 operation_id: operationId,
1297 record_kind: 'grant',
1298 record_id: grantId,
1299 sequence: bumped.outboxEntry.sequence,
1300 prior_audit_event_hash: priorHash,
1301 created_at: issuedAt,
1302 },
1303 };
1304
1305 return {
1306 ok: true,
1307 envelope: { ...envelope, grants_by_id, audit_outbox_by_id },
1308 result: { consumed: true },
1309 };
1310 });
1311
1312 if (!mutated.ok) return mutated;
1313
1314 const subjects = buildAuthoritySubjects({
1315 sessionSecret: opts.sessionSecret,
1316 sessionSecretPrevious: opts.sessionSecretPrevious ?? null,
1317 uid: input.uid,
1318 vaultId,
1319 actorId: actor,
1320 });
1321
1322 return {
1323 ok: true,
1324 payload: {
1325 schema: DELEGATION_VALIDATION_SCHEMA,
1326 authority_subjects: subjects,
1327 },
1328 };
1329 }
1330
1331 /**
1332 * @param {string} uid
1333 * @param {string} consentId
1334 */
1335 async function revokeConsent(uid, consentId) {
1336 const id = typeof consentId === 'string' ? consentId.trim() : '';
1337 if (!id) return fail(400, DELEGATION_REQUEST_INVALID, 'consent_id required');
1338 const principal = hashPrincipalRef(uid);
1339 const nowMs = opts.nowMs ?? Date.now();
1340 const issuedAt = new Date(nowMs).toISOString();
1341 return mutateEnvelope((envelope) => {
1342 const consent = envelope.consents_by_id?.[id];
1343 if (!consent) return { ok: false, status: 404, code: DELEGATION_REQUEST_INVALID, error: 'unknown consent' };
1344 if (consent.principal_ref !== principal) {
1345 return { ok: false, status: 403, code: DELEGATION_HELPER_ACTOR_DENIED, error: 'Consent principal mismatch' };
1346 }
1347 if (consent.revoked_at) {
1348 return { ok: true, envelope, result: { payload: { schema: DELEGATION_ERROR_SCHEMA, code: 'already_revoked' } } };
1349 }
1350 const operationId = randomBytes(16).toString('hex');
1351 const priorHash = envelope.event_chain_heads_by_record?.[`consent:${id}`] ?? null;
1352 const bumped = bumpRecordAudit(
1353 { ...consent, revoked_at: issuedAt },
1354 operationId,
1355 priorHash,
1356 );
1357 if (Object.keys(envelope.audit_outbox_by_id || {}).length >= MAX_AUDIT_OUTBOX) {
1358 return { ok: false, status: 507, code: DELEGATION_AUTHORITY_CAPACITY, error: 'Audit outbox full' };
1359 }
1360 const consents_by_id = { ...envelope.consents_by_id, [id]: bumped.record };
1361 const audit_outbox_by_id = {
1362 ...(envelope.audit_outbox_by_id || {}),
1363 [operationId]: {
1364 operation_id: operationId,
1365 record_kind: 'consent',
1366 record_id: id,
1367 sequence: bumped.outboxEntry.sequence,
1368 prior_audit_event_hash: priorHash,
1369 created_at: issuedAt,
1370 },
1371 };
1372 return {
1373 ok: true,
1374 envelope: rebuildNewestActiveConsentIndex({ ...envelope, consents_by_id, audit_outbox_by_id }, nowMs),
1375 result: { payload: { revoked: true, consent_id: id } },
1376 };
1377 });
1378 }
1379
1380 /**
1381 * Privileged grant revoke (operator / admin path).
1382 *
1383 * @param {string} _privilegedActor
1384 * @param {string} grantId
1385 */
1386 async function revokeGrant(_privilegedActor, grantId) {
1387 const id = typeof grantId === 'string' ? grantId.trim() : '';
1388 if (!id) return fail(400, DELEGATION_REQUEST_INVALID, 'grant_id required');
1389 const nowMs = opts.nowMs ?? Date.now();
1390 const issuedAt = new Date(nowMs).toISOString();
1391 return mutateEnvelope((envelope) => {
1392 const grant = envelope.grants_by_id?.[id];
1393 if (!grant) return { ok: false, status: 404, code: DELEGATION_REQUEST_INVALID, error: 'unknown grant' };
1394 if (grant.revoked_at) {
1395 return { ok: true, envelope, result: { payload: { revoked: true, grant_id: id } } };
1396 }
1397 const operationId = randomBytes(16).toString('hex');
1398 const priorHash = envelope.event_chain_heads_by_record?.[`grant:${id}`] ?? null;
1399 const bumped = bumpRecordAudit(
1400 { ...grant, revoked_at: issuedAt },
1401 operationId,
1402 priorHash,
1403 );
1404 if (Object.keys(envelope.audit_outbox_by_id || {}).length >= MAX_AUDIT_OUTBOX) {
1405 return { ok: false, status: 507, code: DELEGATION_AUTHORITY_CAPACITY, error: 'Audit outbox full' };
1406 }
1407 const grants_by_id = { ...envelope.grants_by_id, [id]: bumped.record };
1408 const audit_outbox_by_id = {
1409 ...(envelope.audit_outbox_by_id || {}),
1410 [operationId]: {
1411 operation_id: operationId,
1412 record_kind: 'grant',
1413 record_id: id,
1414 sequence: bumped.outboxEntry.sequence,
1415 prior_audit_event_hash: priorHash,
1416 created_at: issuedAt,
1417 },
1418 };
1419 const active = { ...(envelope.active_grant_id_by_principal_actor || {}) };
1420 const gKey = principalActorKey(grant.principal_ref, grant.actor_agent_id);
1421 if (active[gKey] === id) delete active[gKey];
1422 return {
1423 ok: true,
1424 envelope: {
1425 ...envelope,
1426 grants_by_id,
1427 audit_outbox_by_id,
1428 active_grant_id_by_principal_actor: active,
1429 },
1430 result: { payload: { revoked: true, grant_id: id } },
1431 };
1432 });
1433 }
1434
1435 /**
1436 * Snapshot legacy stores into a candidate envelope (ignored until marker).
1437 * Does not activate production reads.
1438 *
1439 * @returns {Promise<object>}
1440 */
1441 async function createOrVerifyCandidate() {
1442 const existingRaw = await cas.get(candidateKey, { type: 'text' }).catch(() => null);
1443 if (typeof existingRaw === 'string' && existingRaw.trim()) {
1444 let existing;
1445 try {
1446 existing = JSON.parse(existingRaw);
1447 } catch {
1448 return fail(503, DELEGATION_AUTHORITY_UNAVAILABLE, 'Candidate parse error');
1449 }
1450 const iv = validateEnvelopeInternalIntegrity(existing);
1451 if (!iv.ok) {
1452 return fail(409, DELEGATION_AUTHORITY_CONFLICT, 'Mismatched candidate envelope');
1453 }
1454 return {
1455 ok: true,
1456 state: 'candidate_verified',
1457 lineage_id: existing.lineage_id,
1458 origin_snapshot_hash: existing.origin_snapshot_hash,
1459 };
1460 }
1461
1462 const identities = loadIdentitiesStore(dataDir);
1463 const consents = loadConsentsStore(dataDir);
1464 const grants = loadGrantsStore(dataDir);
1465 const vaultIdentities = identities.vaults?.[vaultId]?.identities || [];
1466 const vaultConsents = consents.vaults?.[vaultId]?.consents || [];
1467 const vaultGrants = grants.vaults?.[vaultId]?.grants || [];
1468
1469 /** @type {Record<string, object>} */
1470 const identities_by_id = {};
1471 for (const id of vaultIdentities) {
1472 if (!id || typeof id !== 'object') {
1473 return fail(503, DELEGATION_AUTHORITY_UNAVAILABLE, 'Malformed identity in snapshot');
1474 }
1475 if (id.agent_id === RETAIL_ACTOR_ID) {
1476 return fail(409, DELEGATION_AUTHORITY_CONFLICT, 'Reserved catalog id collision');
1477 }
1478 identities_by_id[id.agent_id] = { ...id };
1479 }
1480
1481 /** @type {Record<string, object>} */
1482 const consents_by_id = {};
1483 for (const c of vaultConsents) {
1484 if (!c || typeof c !== 'object') {
1485 return fail(503, DELEGATION_AUTHORITY_UNAVAILABLE, 'Malformed consent in snapshot');
1486 }
1487 if (!isStrictUtcTimestamp(c.created)) {
1488 return fail(503, DELEGATION_AUTHORITY_UNAVAILABLE, 'Malformed consent timestamp');
1489 }
1490 const exp = parseOptionalStrictUtc(c.expires_at);
1491 if (!exp.ok) {
1492 return fail(503, DELEGATION_AUTHORITY_UNAVAILABLE, 'Malformed consent expiry');
1493 }
1494 consents_by_id[c.consent_id] = {
1495 ...c,
1496 audit_sequence: 0,
1497 last_materialized_audit_sequence: 0,
1498 pending_audit_count: 0,
1499 };
1500 }
1501
1502 /** @type {Record<string, object>} */
1503 const grants_by_id = {};
1504 /** @type {Record<string, string>} */
1505 const grant_id_by_bearer_hash = {};
1506 /** @type {Record<string, string>} */
1507 const active_grant_id_by_principal_actor = {};
1508 const nowMs = opts.nowMs ?? Date.now();
1509 for (const g of vaultGrants) {
1510 if (!g || typeof g !== 'object') {
1511 return fail(503, DELEGATION_AUTHORITY_UNAVAILABLE, 'Malformed grant in snapshot');
1512 }
1513 if (!isStrictUtcTimestamp(g.issued_at) || !isStrictUtcTimestamp(g.expires_at)) {
1514 return fail(503, DELEGATION_AUTHORITY_UNAVAILABLE, 'Malformed grant timestamp');
1515 }
1516 grants_by_id[g.grant_id] = {
1517 ...g,
1518 audit_sequence: 0,
1519 last_materialized_audit_sequence: 0,
1520 pending_audit_count: 0,
1521 };
1522 if (typeof g.grant_bearer_hash === 'string') {
1523 grant_id_by_bearer_hash[g.grant_bearer_hash] = g.grant_id;
1524 }
1525 if (isGrantActiveStrict(g, nowMs) && g.scope === 'personal') {
1526 const gKey = principalActorKey(g.principal_ref, g.actor_agent_id);
1527 const existingId = active_grant_id_by_principal_actor[gKey];
1528 if (!existingId) {
1529 active_grant_id_by_principal_actor[gKey] = g.grant_id;
1530 } else {
1531 const existing = grants_by_id[existingId];
1532 const a = Date.parse(existing?.issued_at || '') || 0;
1533 const b = Date.parse(g.issued_at) || 0;
1534 if (b >= a) active_grant_id_by_principal_actor[gKey] = g.grant_id;
1535 }
1536 }
1537 }
1538
1539 const snapshotBody = JSON.stringify({
1540 identities: vaultIdentities,
1541 consents: vaultConsents,
1542 grants: vaultGrants,
1543 });
1544 const origin_snapshot_hash = `${SHA256_PREFIX}${createHash('sha256').update(snapshotBody, 'utf8').digest('hex')}`;
1545 const lineage_id = `lineage_${randomIdToken(12)}`;
1546
1547 let envelope = {
1548 schema: DELEGATION_AUTHORITY_ENVELOPE_SCHEMA,
1549 schema_version: ENVELOPE_SCHEMA_VERSION,
1550 vault_id: vaultId,
1551 lineage_id,
1552 origin_snapshot_hash,
1553 revision: 0,
1554 previous_state_hash: null,
1555 identities_by_id,
1556 consents_by_id,
1557 newest_active_consent_id_by_principal_actor: {},
1558 grants_by_id,
1559 grant_id_by_bearer_hash,
1560 active_grant_id_by_principal_actor,
1561 rate_buckets_by_principal_actor: {},
1562 audit_outbox_by_id: {},
1563 event_chain_heads_by_record: {},
1564 };
1565 envelope = rebuildNewestActiveConsentIndex(envelope, opts.nowMs ?? Date.now());
1566 envelope = sealEnvelopeStateHash(envelope);
1567
1568 const iv = validateEnvelopeInternalIntegrity(envelope);
1569 if (!iv.ok) {
1570 return fail(503, DELEGATION_AUTHORITY_UNAVAILABLE, `Candidate invalid: ${iv.reason}`);
1571 }
1572
1573 const payload = JSON.stringify(envelope);
1574 const write = await cas.set(candidateKey, payload, { onlyIfNew: true });
1575 if (!write || write.modified === false) {
1576 // Race: re-read and verify
1577 return createOrVerifyCandidate();
1578 }
1579
1580 // Mirror candidate to local file for inspectability (still ignored by readers).
1581 try {
1582 fs.writeFileSync(
1583 path.join(dataDir, delegationAuthorityCandidateFileName(vaultId)),
1584 payload,
1585 'utf8',
1586 );
1587 } catch {
1588 /* non-fatal */
1589 }
1590
1591 const readBack = await cas.get(candidateKey, { type: 'text' });
1592 if (typeof readBack !== 'string' || !readBack.trim()) {
1593 return fail(503, DELEGATION_AUTHORITY_UNAVAILABLE, 'Candidate read-back failed');
1594 }
1595 let verified;
1596 try {
1597 verified = JSON.parse(readBack);
1598 } catch {
1599 return fail(503, DELEGATION_AUTHORITY_UNAVAILABLE, 'Candidate read-back parse failed');
1600 }
1601 if (
1602 verified.origin_snapshot_hash !== origin_snapshot_hash ||
1603 verified.state_hash !== envelope.state_hash
1604 ) {
1605 return fail(503, DELEGATION_AUTHORITY_UNAVAILABLE, 'Candidate hash verification failed');
1606 }
1607
1608 return {
1609 ok: true,
1610 state: 'candidate_created',
1611 lineage_id,
1612 origin_snapshot_hash,
1613 envelope_key: envelopeKey,
1614 };
1615 }
1616
1617 /**
1618 * Activate candidate via immutable marker. Requires explicit operator authorization.
1619 *
1620 * @param {{ operatorAuthorized: boolean }} auth
1621 */
1622 async function activateMarker(auth) {
1623 if (!auth || auth.operatorAuthorized !== true) {
1624 return fail(403, DELEGATION_HELPER_ACTOR_DENIED, 'Marker activation requires operator authorization');
1625 }
1626 // Dual gate: call-site operatorAuthorized AND store option or env after Tier-3.
1627 // Production must never set these without explicit operator authorization.
1628 const storeAuthorized = opts.operatorAuthorizedMarker === true;
1629 const envAuthorized = process.env.RHF_AUTHORITY_MARKER_AUTHORIZED === '1';
1630 if (!storeAuthorized && !envAuthorized) {
1631 return fail(
1632 403,
1633 DELEGATION_HELPER_ACTOR_DENIED,
1634 'Marker activation blocked — Tier-3 authorization required (operatorAuthorizedMarker or RHF_AUTHORITY_MARKER_AUTHORIZED)',
1635 );
1636 }
1637
1638 const existingMarker = await cas.get(markerKey, { type: 'text' }).catch(() => null);
1639 if (typeof existingMarker === 'string' && existingMarker.trim()) {
1640 let marker;
1641 try {
1642 marker = JSON.parse(existingMarker);
1643 } catch {
1644 return fail(409, DELEGATION_AUTHORITY_CONFLICT, 'Mismatched marker');
1645 }
1646 const mv = validateDelegationAuthorityMarker(marker, vaultId);
1647 if (!mv.ok) return fail(409, DELEGATION_AUTHORITY_CONFLICT, 'Mismatched marker');
1648 return { ok: true, state: 'marker_already_active', marker };
1649 }
1650
1651 const candidateResult = await createOrVerifyCandidate();
1652 if (!candidateResult.ok) return candidateResult;
1653
1654 const candidateRaw = await cas.get(candidateKey, { type: 'text' });
1655 if (typeof candidateRaw !== 'string' || !candidateRaw.trim()) {
1656 return fail(503, DELEGATION_AUTHORITY_UNAVAILABLE, 'Candidate missing for activation');
1657 }
1658 let candidate;
1659 try {
1660 candidate = JSON.parse(candidateRaw);
1661 } catch {
1662 return fail(503, DELEGATION_AUTHORITY_UNAVAILABLE, 'Candidate unreadable');
1663 }
1664
1665 // Promote candidate → envelope key (onlyIfNew), then marker.
1666 const envWrite = await cas.set(envelopeKey, candidateRaw, { onlyIfNew: true });
1667 if (envWrite && envWrite.modified === false) {
1668 const existingEnv = await cas.get(envelopeKey, { type: 'text' });
1669 if (typeof existingEnv === 'string' && existingEnv.trim()) {
1670 let existing;
1671 try {
1672 existing = JSON.parse(existingEnv);
1673 } catch {
1674 return fail(409, DELEGATION_AUTHORITY_CONFLICT, 'Envelope conflict');
1675 }
1676 if (
1677 existing.lineage_id !== candidate.lineage_id ||
1678 existing.origin_snapshot_hash !== candidate.origin_snapshot_hash
1679 ) {
1680 return fail(409, DELEGATION_AUTHORITY_CONFLICT, 'Envelope conflict');
1681 }
1682 }
1683 }
1684
1685 const marker = {
1686 schema: DELEGATION_AUTHORITY_MARKER_SCHEMA,
1687 vault_id: vaultId,
1688 envelope_key: envelopeKey,
1689 envelope_schema_version: MARKER_SCHEMA_VERSION,
1690 lineage_id: candidate.lineage_id,
1691 origin_snapshot_hash: candidate.origin_snapshot_hash,
1692 };
1693 const markerWrite = await cas.set(markerKey, JSON.stringify(marker), { onlyIfNew: true });
1694 if (!markerWrite || markerWrite.modified === false) {
1695 const again = await cas.get(markerKey, { type: 'text' });
1696 if (typeof again === 'string' && again.trim()) {
1697 try {
1698 const m = JSON.parse(again);
1699 if (
1700 m.lineage_id === marker.lineage_id &&
1701 m.origin_snapshot_hash === marker.origin_snapshot_hash
1702 ) {
1703 return { ok: true, state: 'marker_already_active', marker: m };
1704 }
1705 } catch {
1706 /* fall through */
1707 }
1708 }
1709 return fail(409, DELEGATION_AUTHORITY_CONFLICT, 'Marker conflict');
1710 }
1711
1712 try {
1713 fs.writeFileSync(
1714 path.join(dataDir, delegationAuthorityMarkerFileName(vaultId)),
1715 JSON.stringify(marker),
1716 'utf8',
1717 );
1718 fs.writeFileSync(
1719 path.join(dataDir, path.basename(envelopeKey)),
1720 candidateRaw,
1721 'utf8',
1722 );
1723 } catch {
1724 /* CAS is authoritative when available */
1725 }
1726
1727 return { ok: true, state: 'marker_activated', marker };
1728 }
1729
1730 return {
1731 readHelperAccess,
1732 renewPersonal,
1733 validateAndConsume,
1734 revokeConsent,
1735 revokeGrant,
1736 createOrVerifyCandidate,
1737 activateMarker,
1738 readActiveEnvelope,
1739 mutateEnvelope,
1740 cas,
1741 envelopeKey,
1742 candidateKey,
1743 markerKey,
1744 };
1745 }
1746
1747 /**
1748 * Test helper: write marker + sealed envelope into a CAS store and local files.
1749 *
1750 * @param {{
1751 * dataDir: string,
1752 * vaultId: string,
1753 * cas?: MemoryCasBlobStore,
1754 * envelopeOverrides?: object,
1755 * }} input
1756 */
1757 export async function seedActiveAuthorityEnvelope(input) {
1758 const cas = input.cas || new MemoryCasBlobStore();
1759 const vaultId = input.vaultId;
1760 let envelope = {
1761 schema: DELEGATION_AUTHORITY_ENVELOPE_SCHEMA,
1762 schema_version: 1,
1763 vault_id: vaultId,
1764 lineage_id: 'lineage_test_kn1',
1765 origin_snapshot_hash:
1766 'sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
1767 revision: 0,
1768 previous_state_hash: null,
1769 identities_by_id: {},
1770 consents_by_id: {},
1771 newest_active_consent_id_by_principal_actor: {},
1772 grants_by_id: {},
1773 grant_id_by_bearer_hash: {},
1774 active_grant_id_by_principal_actor: {},
1775 rate_buckets_by_principal_actor: {},
1776 audit_outbox_by_id: {},
1777 event_chain_heads_by_record: {},
1778 ...(input.envelopeOverrides || {}),
1779 };
1780 envelope = rebuildNewestActiveConsentIndex(envelope);
1781 envelope = sealEnvelopeStateHash(envelope);
1782
1783 const envelopeKey = delegationAuthorityEnvelopeBlobKey(vaultId);
1784 const markerKey = delegationAuthorityMarkerBlobKey(vaultId);
1785 await cas.set(envelopeKey, JSON.stringify(envelope));
1786 const marker = {
1787 schema: DELEGATION_AUTHORITY_MARKER_SCHEMA,
1788 vault_id: vaultId,
1789 envelope_key: envelopeKey,
1790 envelope_schema_version: 1,
1791 lineage_id: envelope.lineage_id,
1792 origin_snapshot_hash: envelope.origin_snapshot_hash,
1793 };
1794 await cas.set(markerKey, JSON.stringify(marker));
1795 fs.mkdirSync(input.dataDir, { recursive: true });
1796 fs.writeFileSync(
1797 path.join(input.dataDir, delegationAuthorityMarkerFileName(vaultId)),
1798 JSON.stringify(marker),
1799 );
1800 fs.writeFileSync(path.join(input.dataDir, path.basename(envelopeKey)), JSON.stringify(envelope));
1801 return { cas, envelope, marker };
1802 }
File History 1 commit
sha256:fbe982a22c05c6fe2e93876f250deecdb43d883f648b4e1810c7546caa9f17db docs: activate KNOWTATION- board identity and preserve livi… Human minor 3 days ago