@private.me/xcontinuity 2.1.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.
Files changed (59) hide show
  1. package/AGENTS.md +123 -0
  2. package/LICENSE.md +26 -0
  3. package/MIGRATING.md +77 -0
  4. package/README.md +601 -0
  5. package/dist/adjudicator.d.ts +75 -0
  6. package/dist/adjudicator.js +184 -0
  7. package/dist/cascade.d.ts +157 -0
  8. package/dist/cascade.js +323 -0
  9. package/dist/chronicle.d.ts +76 -0
  10. package/dist/chronicle.js +173 -0
  11. package/dist/cjs/adjudicator.js +189 -0
  12. package/dist/cjs/cascade.js +328 -0
  13. package/dist/cjs/chronicle.js +178 -0
  14. package/dist/cjs/enforcement.js +108 -0
  15. package/dist/cjs/errors.js +72 -0
  16. package/dist/cjs/index.js +108 -0
  17. package/dist/cjs/memory-runtime.js +129 -0
  18. package/dist/cjs/memory-session.js +134 -0
  19. package/dist/cjs/mission.js +178 -0
  20. package/dist/cjs/package.json +1 -0
  21. package/dist/cjs/provenance.js +192 -0
  22. package/dist/cjs/ratification.js +322 -0
  23. package/dist/cjs/reverse-xorida.js +506 -0
  24. package/dist/cjs/session.js +273 -0
  25. package/dist/cjs/state-serializer.js +300 -0
  26. package/dist/cjs/store-memory.js +33 -0
  27. package/dist/cjs/trust.js +133 -0
  28. package/dist/cjs/types.js +59 -0
  29. package/dist/enforcement.d.ts +40 -0
  30. package/dist/enforcement.js +104 -0
  31. package/dist/errors.d.ts +25 -0
  32. package/dist/errors.js +68 -0
  33. package/dist/index.d.ts +34 -0
  34. package/dist/index.js +43 -0
  35. package/dist/memory-runtime.d.ts +36 -0
  36. package/dist/memory-runtime.js +125 -0
  37. package/dist/memory-session.d.ts +38 -0
  38. package/dist/memory-session.js +97 -0
  39. package/dist/mission.d.ts +68 -0
  40. package/dist/mission.js +172 -0
  41. package/dist/provenance.d.ts +54 -0
  42. package/dist/provenance.js +182 -0
  43. package/dist/ratification.d.ts +113 -0
  44. package/dist/ratification.js +317 -0
  45. package/dist/reverse-xorida.d.ts +174 -0
  46. package/dist/reverse-xorida.js +490 -0
  47. package/dist/session.d.ts +102 -0
  48. package/dist/session.js +269 -0
  49. package/dist/state-serializer.d.ts +37 -0
  50. package/dist/state-serializer.js +294 -0
  51. package/dist/store-memory.d.ts +18 -0
  52. package/dist/store-memory.js +29 -0
  53. package/dist/trust.d.ts +76 -0
  54. package/dist/trust.js +121 -0
  55. package/dist/types.d.ts +320 -0
  56. package/dist/types.js +56 -0
  57. package/llms.txt +43 -0
  58. package/package.json +125 -0
  59. package/share1.dat +0 -0
@@ -0,0 +1,38 @@
1
+ /**
2
+ * @private.me/xcontinuity — Session Memory Layer
3
+ *
4
+ * Orchestration layer over serializer + reverse-xorida + store.
5
+ * Maintains lastSplitState for incremental updates.
6
+ */
7
+ import type { Result } from '@private.me/shared';
8
+ import type { AgentState, StateMetadata, StateSnapshot, SplitState, SplitConfig, StateStore } from './types.js';
9
+ import type { ContinuityError } from './errors.js';
10
+ export declare class SessionMemory {
11
+ private lastSplitState;
12
+ private lastSnapshot;
13
+ private readonly store;
14
+ private readonly splitConfig;
15
+ constructor(store: StateStore, splitConfig?: SplitConfig);
16
+ /**
17
+ * Save agent state: serialize, split (incremental if possible), persist to store.
18
+ *
19
+ * @param state - Agent state to persist
20
+ * @param metadata - State metadata
21
+ * @returns The state snapshot (with stateId for later retrieval)
22
+ */
23
+ save(state: AgentState, metadata: StateMetadata): Promise<Result<StateSnapshot, ContinuityError>>;
24
+ /**
25
+ * Load agent state from store by stateId.
26
+ * If no stateId provided, uses the last saved state.
27
+ *
28
+ * @param stateId - Optional state ID to load (defaults to last saved)
29
+ * @returns Reconstructed AgentState
30
+ */
31
+ load(stateId?: string): Promise<Result<AgentState, ContinuityError>>;
32
+ /** Get the last split state (for inspection/testing). */
33
+ getLastSplitState(): SplitState | null;
34
+ /** Get the last snapshot (for inspection/testing). */
35
+ getLastSnapshot(): StateSnapshot | null;
36
+ /** Reset internal tracking state. */
37
+ reset(): void;
38
+ }
@@ -0,0 +1,97 @@
1
+ /**
2
+ * @private.me/xcontinuity — Session Memory Layer
3
+ *
4
+ * Orchestration layer over serializer + reverse-xorida + store.
5
+ * Maintains lastSplitState for incremental updates.
6
+ */
7
+ import { ok, err } from '@private.me/shared';
8
+ import { DEFAULT_SPLIT_CONFIG } from './types.js';
9
+ import { continuityError } from './errors.js';
10
+ import { serializeState, deserializeState } from './state-serializer.js';
11
+ import { splitState, reconstructState, incrementalUpdate } from './reverse-xorida.js';
12
+ export class SessionMemory {
13
+ lastSplitState = null;
14
+ lastSnapshot = null;
15
+ store;
16
+ splitConfig;
17
+ constructor(store, splitConfig = DEFAULT_SPLIT_CONFIG) {
18
+ this.store = store;
19
+ this.splitConfig = splitConfig;
20
+ }
21
+ /**
22
+ * Save agent state: serialize, split (incremental if possible), persist to store.
23
+ *
24
+ * @param state - Agent state to persist
25
+ * @param metadata - State metadata
26
+ * @returns The state snapshot (with stateId for later retrieval)
27
+ */
28
+ async save(state, metadata) {
29
+ // Serialize
30
+ const serResult = await serializeState(state, metadata);
31
+ if (!serResult.ok)
32
+ return serResult;
33
+ const snapshot = serResult.value;
34
+ // Split (incremental if we have a previous split)
35
+ let splitResult;
36
+ if (this.lastSplitState && this.lastSnapshot) {
37
+ splitResult = await incrementalUpdate(this.lastSplitState, this.lastSnapshot, snapshot);
38
+ }
39
+ else {
40
+ splitResult = await splitState(snapshot, this.splitConfig);
41
+ }
42
+ if (!splitResult.ok)
43
+ return splitResult;
44
+ // Persist to store
45
+ await this.store.putShares(splitResult.value);
46
+ // Update tracking
47
+ this.lastSplitState = splitResult.value;
48
+ this.lastSnapshot = snapshot;
49
+ return ok(snapshot);
50
+ }
51
+ /**
52
+ * Load agent state from store by stateId.
53
+ * If no stateId provided, uses the last saved state.
54
+ *
55
+ * @param stateId - Optional state ID to load (defaults to last saved)
56
+ * @returns Reconstructed AgentState
57
+ */
58
+ async load(stateId) {
59
+ const targetId = stateId ?? this.lastSplitState?.stateId;
60
+ if (!targetId) {
61
+ return err(continuityError('NO_SNAPSHOTS', 'No state ID provided and no previous save'));
62
+ }
63
+ const splitState = await this.store.getShares(targetId);
64
+ if (!splitState) {
65
+ return err(continuityError('SNAPSHOT_NOT_FOUND', `State ${targetId} not found in store`));
66
+ }
67
+ // Reconstruct from shares
68
+ const reconResult = await reconstructState(splitState.shares);
69
+ if (!reconResult.ok)
70
+ return reconResult;
71
+ // Deserialize the snapshot
72
+ const snapshot = {
73
+ stateId: reconResult.value.stateId,
74
+ metadata: splitState.metadata,
75
+ serializedBytes: reconResult.value.serializedBytes,
76
+ checksum: new Uint8Array(0), // Will be recomputed during deserialization
77
+ };
78
+ // Compute fresh checksum for verification
79
+ const { computeChecksum } = await import('./state-serializer.js');
80
+ const freshChecksum = await computeChecksum(reconResult.value.serializedBytes);
81
+ const verifiedSnapshot = { ...snapshot, checksum: freshChecksum };
82
+ return deserializeState(verifiedSnapshot);
83
+ }
84
+ /** Get the last split state (for inspection/testing). */
85
+ getLastSplitState() {
86
+ return this.lastSplitState;
87
+ }
88
+ /** Get the last snapshot (for inspection/testing). */
89
+ getLastSnapshot() {
90
+ return this.lastSnapshot;
91
+ }
92
+ /** Reset internal tracking state. */
93
+ reset() {
94
+ this.lastSplitState = null;
95
+ this.lastSnapshot = null;
96
+ }
97
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * @private.me/xcontinuity — Mission (Human-Anchored Goals)
3
+ *
4
+ * Ensures AI agents operate within human-defined boundaries.
5
+ *
6
+ * Components:
7
+ * MissionAuthority — validates mission signatures, checks expiry/scope
8
+ * MissionGuard — pre-write check against active mission constraints
9
+ * AlignmentAdjudicator — wraps any Adjudicator, adding mission checks
10
+ */
11
+ import type { Result } from '@private.me/shared';
12
+ import type { MissionRecord, HardConstraint, ProposedAction, ConstraintResult, MemoryEntry, AdjudicatorResult } from './types.js';
13
+ import type { ContinuityError } from './errors.js';
14
+ import type { Adjudicator } from './adjudicator.js';
15
+ /**
16
+ * Validates mission records and checks authority.
17
+ */
18
+ export declare class MissionAuthority {
19
+ private activeMission;
20
+ /**
21
+ * Load a mission record after verifying its signature and validity.
22
+ */
23
+ loadMission(mission: MissionRecord): Promise<Result<void, ContinuityError>>;
24
+ /**
25
+ * Load a mission without signature verification (for testing or trusted sources).
26
+ */
27
+ loadTrustedMission(mission: MissionRecord): void;
28
+ /** Get the active mission. */
29
+ getActiveMission(): MissionRecord | null;
30
+ /** Clear the active mission. */
31
+ clearMission(): void;
32
+ /**
33
+ * Check if an action falls within the active mission's scopes.
34
+ */
35
+ isInScope(action: ProposedAction): Result<boolean, ContinuityError>;
36
+ }
37
+ /**
38
+ * Pre-write guard that evaluates actions against mission constraints.
39
+ */
40
+ export declare class MissionGuard {
41
+ private readonly authority;
42
+ private readonly constraints;
43
+ constructor(authority: MissionAuthority);
44
+ /** Add a hard constraint. */
45
+ addConstraint(constraint: HardConstraint): void;
46
+ /** Remove a constraint by ID. */
47
+ removeConstraint(constraintId: string): boolean;
48
+ /** Get all active constraints. */
49
+ getConstraints(): readonly HardConstraint[];
50
+ /**
51
+ * Evaluate a proposed action against all constraints and mission scope.
52
+ *
53
+ * @returns ConstraintResult — allowed:true if all pass, or first violation
54
+ */
55
+ evaluate(action: ProposedAction): Result<ConstraintResult, ContinuityError>;
56
+ }
57
+ /**
58
+ * Adjudicator wrapper that adds mission constraint checking.
59
+ *
60
+ * Delegates resolution to the inner adjudicator, but rejects
61
+ * any winning entry that violates active mission constraints.
62
+ */
63
+ export declare class AlignmentAdjudicator implements Adjudicator {
64
+ private readonly inner;
65
+ private readonly guard;
66
+ constructor(inner: Adjudicator, guard: MissionGuard);
67
+ resolve(key: string, candidates: readonly MemoryEntry[]): Result<AdjudicatorResult, ContinuityError>;
68
+ }
@@ -0,0 +1,172 @@
1
+ /**
2
+ * @private.me/xcontinuity — Mission (Human-Anchored Goals)
3
+ *
4
+ * Ensures AI agents operate within human-defined boundaries.
5
+ *
6
+ * Components:
7
+ * MissionAuthority — validates mission signatures, checks expiry/scope
8
+ * MissionGuard — pre-write check against active mission constraints
9
+ * AlignmentAdjudicator — wraps any Adjudicator, adding mission checks
10
+ */
11
+ import { ok, err } from '@private.me/shared';
12
+ import { continuityError } from './errors.js';
13
+ import { authorRefToPublicKey } from './provenance.js';
14
+ /**
15
+ * Validates mission records and checks authority.
16
+ */
17
+ export class MissionAuthority {
18
+ activeMission = null;
19
+ /**
20
+ * Load a mission record after verifying its signature and validity.
21
+ */
22
+ async loadMission(mission) {
23
+ // Check expiry
24
+ if (mission.expiresAt !== undefined && Date.now() > mission.expiresAt) {
25
+ return err(continuityError('AUTHORITY_EXPIRED', `Mission ${mission.missionId} has expired`));
26
+ }
27
+ // Verify signature
28
+ try {
29
+ const publicKey = await authorRefToPublicKey(mission.authority);
30
+ const encoder = new TextEncoder();
31
+ const missionBytes = encoder.encode(canonicalMissionString(mission));
32
+ const valid = await crypto.subtle.verify('Ed25519', publicKey, mission.signature.buffer, missionBytes.buffer);
33
+ if (!valid) {
34
+ return err(continuityError('INVALID_MISSION_SIGNATURE', `Mission ${mission.missionId} signature invalid`));
35
+ }
36
+ }
37
+ catch (e) {
38
+ return err(continuityError('INVALID_MISSION_SIGNATURE', `Signature verification failed: ${e instanceof Error ? e.message : String(e)}`));
39
+ }
40
+ this.activeMission = mission;
41
+ return ok(undefined);
42
+ }
43
+ /**
44
+ * Load a mission without signature verification (for testing or trusted sources).
45
+ */
46
+ loadTrustedMission(mission) {
47
+ this.activeMission = mission;
48
+ }
49
+ /** Get the active mission. */
50
+ getActiveMission() {
51
+ return this.activeMission;
52
+ }
53
+ /** Clear the active mission. */
54
+ clearMission() {
55
+ this.activeMission = null;
56
+ }
57
+ /**
58
+ * Check if an action falls within the active mission's scopes.
59
+ */
60
+ isInScope(action) {
61
+ if (!this.activeMission) {
62
+ return err(continuityError('NO_ACTIVE_MISSION', 'No mission loaded'));
63
+ }
64
+ // Check expiry
65
+ if (this.activeMission.expiresAt !== undefined && Date.now() > this.activeMission.expiresAt) {
66
+ return err(continuityError('AUTHORITY_EXPIRED', `Mission ${this.activeMission.missionId} has expired`));
67
+ }
68
+ // Empty scopes = all actions allowed
69
+ if (this.activeMission.scopes.length === 0) {
70
+ return ok(true);
71
+ }
72
+ // Check if action type or key matches any scope
73
+ const actionScope = `${action.type}:${action.key}`;
74
+ const inScope = this.activeMission.scopes.some(scope => actionScope.startsWith(scope) || action.type === scope || scope === '*');
75
+ return ok(inScope);
76
+ }
77
+ }
78
+ /**
79
+ * Pre-write guard that evaluates actions against mission constraints.
80
+ */
81
+ export class MissionGuard {
82
+ authority;
83
+ constraints = [];
84
+ constructor(authority) {
85
+ this.authority = authority;
86
+ }
87
+ /** Add a hard constraint. */
88
+ addConstraint(constraint) {
89
+ this.constraints.push(constraint);
90
+ }
91
+ /** Remove a constraint by ID. */
92
+ removeConstraint(constraintId) {
93
+ const idx = this.constraints.findIndex(c => c.constraintId === constraintId);
94
+ if (idx === -1)
95
+ return false;
96
+ this.constraints.splice(idx, 1);
97
+ return true;
98
+ }
99
+ /** Get all active constraints. */
100
+ getConstraints() {
101
+ return this.constraints.slice();
102
+ }
103
+ /**
104
+ * Evaluate a proposed action against all constraints and mission scope.
105
+ *
106
+ * @returns ConstraintResult — allowed:true if all pass, or first violation
107
+ */
108
+ evaluate(action) {
109
+ // Check mission scope first
110
+ const scopeResult = this.authority.isInScope(action);
111
+ if (!scopeResult.ok)
112
+ return scopeResult;
113
+ if (!scopeResult.value) {
114
+ return ok({
115
+ allowed: false,
116
+ reason: `Action "${action.type}:${action.key}" is outside mission scope`,
117
+ });
118
+ }
119
+ // Evaluate each constraint
120
+ for (const constraint of this.constraints) {
121
+ const result = constraint.evaluate(action);
122
+ if (!result.allowed) {
123
+ return ok(result);
124
+ }
125
+ }
126
+ return ok({ allowed: true });
127
+ }
128
+ }
129
+ /**
130
+ * Adjudicator wrapper that adds mission constraint checking.
131
+ *
132
+ * Delegates resolution to the inner adjudicator, but rejects
133
+ * any winning entry that violates active mission constraints.
134
+ */
135
+ export class AlignmentAdjudicator {
136
+ inner;
137
+ guard;
138
+ constructor(inner, guard) {
139
+ this.inner = inner;
140
+ this.guard = guard;
141
+ }
142
+ resolve(key, candidates) {
143
+ const result = this.inner.resolve(key, candidates);
144
+ if (!result.ok)
145
+ return result;
146
+ // Check if winning entry passes mission constraints
147
+ const action = {
148
+ author: result.value.winner.provenance?.author ?? '',
149
+ type: 'write',
150
+ key,
151
+ value: result.value.winner.value,
152
+ };
153
+ const guardResult = this.guard.evaluate(action);
154
+ if (!guardResult.ok)
155
+ return guardResult;
156
+ if (!guardResult.value.allowed) {
157
+ return err(continuityError('CONSTRAINT_VIOLATION', `Winner for "${key}" violates constraint: ${guardResult.value.reason}`));
158
+ }
159
+ return result;
160
+ }
161
+ }
162
+ /* ── Internal helpers ── */
163
+ function canonicalMissionString(mission) {
164
+ return [
165
+ mission.missionId,
166
+ mission.goal,
167
+ mission.authority,
168
+ String(mission.issuedAt),
169
+ String(mission.expiresAt ?? ''),
170
+ [...mission.scopes].sort().join(','),
171
+ ].join('\0');
172
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * @private.me/xcontinuity — Provenance (Ed25519 Signed Entries)
3
+ *
4
+ * Cryptographic provenance for chronicle entries using Ed25519 signatures
5
+ * via the Web Crypto API. Each entry can be signed by an agent, creating
6
+ * a verifiable chain of authorship and integrity.
7
+ *
8
+ * Uses native Web Crypto EdDSA (Node.js 16.9+, modern browsers).
9
+ * Zero external dependencies.
10
+ */
11
+ import type { Result } from '@private.me/shared';
12
+ import type { AuthorRef, MemoryEntry, ProvenanceRecord, StateValue } from './types.js';
13
+ import type { ContinuityError } from './errors.js';
14
+ /** Ed25519 key pair for signing and verification. */
15
+ export interface Ed25519KeyPair {
16
+ readonly publicKey: CryptoKey;
17
+ readonly privateKey: CryptoKey;
18
+ }
19
+ /**
20
+ * Generate a new Ed25519 key pair for provenance signing.
21
+ */
22
+ export declare function generateSigningKeyPair(): Promise<Ed25519KeyPair>;
23
+ /**
24
+ * Export the public key as an AuthorRef (base64url-encoded 32-byte key).
25
+ */
26
+ export declare function publicKeyToAuthorRef(publicKey: CryptoKey): Promise<AuthorRef>;
27
+ /**
28
+ * Import an AuthorRef back to a CryptoKey for verification.
29
+ */
30
+ export declare function authorRefToPublicKey(authorRef: AuthorRef): Promise<CryptoKey>;
31
+ /**
32
+ * Produce canonical bytes for a memory entry, suitable for signing.
33
+ *
34
+ * Deterministic serialization: sorted keys, UTF-8 encoded.
35
+ * Format: `key\0type\0valueBytes\0timestamp`
36
+ */
37
+ export declare function canonicalEntryBytes(key: string, value: StateValue, timestamp: number, parentHash?: Uint8Array): Uint8Array;
38
+ /**
39
+ * Sign a memory entry, producing a ProvenanceRecord.
40
+ */
41
+ export declare function signEntry(key: string, value: StateValue, privateKey: CryptoKey, publicKey: CryptoKey, parentHash?: Uint8Array): Promise<Result<ProvenanceRecord, ContinuityError>>;
42
+ /**
43
+ * Verify a provenance record against the entry data and author's public key.
44
+ */
45
+ export declare function verifyEntry(key: string, value: StateValue, provenance: ProvenanceRecord, publicKey: CryptoKey): Promise<Result<boolean, ContinuityError>>;
46
+ /**
47
+ * Compute the SHA-256 hash of a provenance record for chain linking.
48
+ */
49
+ export declare function hashProvenance(provenance: ProvenanceRecord): Promise<Uint8Array>;
50
+ /**
51
+ * Verify a parent hash chain link: the parentHash in the current entry
52
+ * must equal the hash of the previous entry's provenance.
53
+ */
54
+ export declare function verifyChainLink(current: MemoryEntry, previous: MemoryEntry): Promise<Result<boolean, ContinuityError>>;
@@ -0,0 +1,182 @@
1
+ /**
2
+ * @private.me/xcontinuity — Provenance (Ed25519 Signed Entries)
3
+ *
4
+ * Cryptographic provenance for chronicle entries using Ed25519 signatures
5
+ * via the Web Crypto API. Each entry can be signed by an agent, creating
6
+ * a verifiable chain of authorship and integrity.
7
+ *
8
+ * Uses native Web Crypto EdDSA (Node.js 16.9+, modern browsers).
9
+ * Zero external dependencies.
10
+ */
11
+ import { ok, err } from '@private.me/shared';
12
+ import { continuityError } from './errors.js';
13
+ /**
14
+ * Generate a new Ed25519 key pair for provenance signing.
15
+ */
16
+ export async function generateSigningKeyPair() {
17
+ const keyPair = await crypto.subtle.generateKey('Ed25519', true, ['sign', 'verify']);
18
+ return {
19
+ publicKey: keyPair.publicKey,
20
+ privateKey: keyPair.privateKey,
21
+ };
22
+ }
23
+ /**
24
+ * Export the public key as an AuthorRef (base64url-encoded 32-byte key).
25
+ */
26
+ export async function publicKeyToAuthorRef(publicKey) {
27
+ const raw = await crypto.subtle.exportKey('raw', publicKey);
28
+ return uint8ToBase64Url(new Uint8Array(raw));
29
+ }
30
+ /**
31
+ * Import an AuthorRef back to a CryptoKey for verification.
32
+ */
33
+ export async function authorRefToPublicKey(authorRef) {
34
+ const raw = base64UrlToUint8(authorRef);
35
+ return crypto.subtle.importKey('raw', raw.buffer, 'Ed25519', true, ['verify']);
36
+ }
37
+ /**
38
+ * Produce canonical bytes for a memory entry, suitable for signing.
39
+ *
40
+ * Deterministic serialization: sorted keys, UTF-8 encoded.
41
+ * Format: `key\0type\0valueBytes\0timestamp`
42
+ */
43
+ export function canonicalEntryBytes(key, value, timestamp, parentHash) {
44
+ const encoder = new TextEncoder();
45
+ const keyBytes = encoder.encode(key);
46
+ const valueStr = canonicalValueString(value);
47
+ const valueBytes = encoder.encode(valueStr);
48
+ const tsBytes = encoder.encode(String(timestamp));
49
+ // Total: key + \0 + value + \0 + timestamp [+ \0 + parentHash]
50
+ const parentLen = parentHash ? 1 + parentHash.length : 0;
51
+ const total = keyBytes.length + 1 + valueBytes.length + 1 + tsBytes.length + parentLen;
52
+ const buf = new Uint8Array(total);
53
+ let offset = 0;
54
+ buf.set(keyBytes, offset);
55
+ offset += keyBytes.length;
56
+ buf[offset++] = 0;
57
+ buf.set(valueBytes, offset);
58
+ offset += valueBytes.length;
59
+ buf[offset++] = 0;
60
+ buf.set(tsBytes, offset);
61
+ offset += tsBytes.length;
62
+ if (parentHash) {
63
+ buf[offset++] = 0;
64
+ buf.set(parentHash, offset);
65
+ }
66
+ return buf;
67
+ }
68
+ /**
69
+ * Sign a memory entry, producing a ProvenanceRecord.
70
+ */
71
+ export async function signEntry(key, value, privateKey, publicKey, parentHash) {
72
+ try {
73
+ const timestamp = Date.now();
74
+ const canonical = canonicalEntryBytes(key, value, timestamp, parentHash);
75
+ const signature = new Uint8Array(await crypto.subtle.sign('Ed25519', privateKey, canonical.buffer));
76
+ const author = await publicKeyToAuthorRef(publicKey);
77
+ return ok({
78
+ author,
79
+ timestamp,
80
+ signature,
81
+ parentHash,
82
+ });
83
+ }
84
+ catch (e) {
85
+ return err(continuityError('INVALID_SIGNATURE', `Failed to sign entry: ${e instanceof Error ? e.message : String(e)}`));
86
+ }
87
+ }
88
+ /**
89
+ * Verify a provenance record against the entry data and author's public key.
90
+ */
91
+ export async function verifyEntry(key, value, provenance, publicKey) {
92
+ try {
93
+ if (!provenance.author) {
94
+ return err(continuityError('MISSING_AUTHOR', 'Provenance record has no author'));
95
+ }
96
+ const canonical = canonicalEntryBytes(key, value, provenance.timestamp, provenance.parentHash);
97
+ const valid = await crypto.subtle.verify('Ed25519', publicKey, provenance.signature.buffer, canonical.buffer);
98
+ return ok(valid);
99
+ }
100
+ catch (e) {
101
+ return err(continuityError('INVALID_SIGNATURE', `Verification failed: ${e instanceof Error ? e.message : String(e)}`));
102
+ }
103
+ }
104
+ /**
105
+ * Compute the SHA-256 hash of a provenance record for chain linking.
106
+ */
107
+ export async function hashProvenance(provenance) {
108
+ const encoder = new TextEncoder();
109
+ const authorBytes = encoder.encode(provenance.author);
110
+ const tsBytes = encoder.encode(String(provenance.timestamp));
111
+ const parentLen = provenance.parentHash ? provenance.parentHash.length : 0;
112
+ const total = authorBytes.length + 1 + tsBytes.length + 1 + provenance.signature.length + parentLen;
113
+ const buf = new Uint8Array(total);
114
+ let offset = 0;
115
+ buf.set(authorBytes, offset);
116
+ offset += authorBytes.length;
117
+ buf[offset++] = 0;
118
+ buf.set(tsBytes, offset);
119
+ offset += tsBytes.length;
120
+ buf[offset++] = 0;
121
+ buf.set(provenance.signature, offset);
122
+ offset += provenance.signature.length;
123
+ if (provenance.parentHash) {
124
+ buf.set(provenance.parentHash, offset);
125
+ }
126
+ const hash = await crypto.subtle.digest('SHA-256', buf.buffer);
127
+ return new Uint8Array(hash);
128
+ }
129
+ /**
130
+ * Verify a parent hash chain link: the parentHash in the current entry
131
+ * must equal the hash of the previous entry's provenance.
132
+ */
133
+ export async function verifyChainLink(current, previous) {
134
+ if (!current.provenance?.parentHash) {
135
+ return ok(true); // No chain link to verify (root entry)
136
+ }
137
+ if (!previous.provenance) {
138
+ return err(continuityError('HASH_CHAIN_BREAK', `Previous entry "${previous.key}" has no provenance to hash`));
139
+ }
140
+ const expectedHash = await hashProvenance(previous.provenance);
141
+ const matches = constantTimeEqual(current.provenance.parentHash, expectedHash);
142
+ if (!matches) {
143
+ return err(continuityError('HASH_CHAIN_BREAK', `Parent hash mismatch for entry "${current.key}"`));
144
+ }
145
+ return ok(true);
146
+ }
147
+ /* ── Internal helpers ── */
148
+ function canonicalValueString(value) {
149
+ if (value === null)
150
+ return 'null';
151
+ if (value instanceof Uint8Array)
152
+ return `bytes:${uint8ToBase64Url(value)}`;
153
+ if (typeof value === 'object')
154
+ return JSON.stringify(value, Object.keys(value).sort());
155
+ return String(value);
156
+ }
157
+ function uint8ToBase64Url(data) {
158
+ let binary = '';
159
+ for (let i = 0; i < data.length; i++) {
160
+ binary += String.fromCharCode(data[i]);
161
+ }
162
+ return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
163
+ }
164
+ function base64UrlToUint8(b64url) {
165
+ const b64 = b64url.replace(/-/g, '+').replace(/_/g, '/');
166
+ const padded = b64 + '='.repeat((4 - (b64.length % 4)) % 4);
167
+ const binary = atob(padded);
168
+ const bytes = new Uint8Array(binary.length);
169
+ for (let i = 0; i < binary.length; i++) {
170
+ bytes[i] = binary.charCodeAt(i);
171
+ }
172
+ return bytes;
173
+ }
174
+ function constantTimeEqual(a, b) {
175
+ if (a.length !== b.length)
176
+ return false;
177
+ let diff = 0;
178
+ for (let i = 0; i < a.length; i++) {
179
+ diff |= a[i] ^ b[i];
180
+ }
181
+ return diff === 0;
182
+ }