pactium 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.
Files changed (42) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +92 -0
  3. package/README.zh-CN.md +90 -0
  4. package/SECURITY.md +7 -0
  5. package/bin/pactium.mjs +121 -0
  6. package/docs/LICOLITE-ASPECT.md +57 -0
  7. package/docs/README.md +13 -0
  8. package/docs/TERM.md +289 -0
  9. package/docs/architecture/ARCHITECTURE.md +62 -0
  10. package/docs/protocols/PROFILE.md +124 -0
  11. package/docs/protocols/PROTOCOLS.md +62 -0
  12. package/examples/record-operation.mjs +26 -0
  13. package/package.json +69 -0
  14. package/src/README.md +13 -0
  15. package/src/aspects/licolite/aspect.js +278 -0
  16. package/src/aspects/licolite/constants.js +13 -0
  17. package/src/aspects/licolite/evidence.js +47 -0
  18. package/src/aspects/licolite/index.d.ts +51 -0
  19. package/src/aspects/licolite/index.js +19 -0
  20. package/src/aspects/licolite/signing.js +78 -0
  21. package/src/canonical/value.js +40 -0
  22. package/src/core/append-condition.js +102 -0
  23. package/src/core/pactium-core.js +1073 -0
  24. package/src/core/tracking-cursor.js +68 -0
  25. package/src/http.js +99 -0
  26. package/src/index-engine/snapshot-merkle-index.js +994 -0
  27. package/src/index.d.ts +244 -0
  28. package/src/index.js +73 -0
  29. package/src/ledger/signed-head.js +204 -0
  30. package/src/ledger/transparency-log.js +702 -0
  31. package/src/maintenance/task-engine.js +36 -0
  32. package/src/proof/bundle-format.js +265 -0
  33. package/src/proof/bundle.js +77 -0
  34. package/src/proof/envelope.js +548 -0
  35. package/src/proof/registry.js +18 -0
  36. package/src/protocol/constants.js +69 -0
  37. package/src/protocol/hashing.js +47 -0
  38. package/src/quality/profile-runner.js +291 -0
  39. package/src/repair/planner.js +62 -0
  40. package/src/shared/records.js +32 -0
  41. package/src/storage/local-json-storage-port.js +360 -0
  42. package/src/verification/failure.js +31 -0
@@ -0,0 +1,278 @@
1
+ import { PACTIUM_PROTOCOL } from "../../protocol/constants.js";
2
+ import { canonicalDecode } from "../../canonical/value.js";
3
+ import { envelopeSigningHash, verifyProofEnvelope } from "../../proof/envelope.js";
4
+ import { verifyProofBundle } from "../../proof/bundle.js";
5
+ import { createIndexedBundleResolver } from "../../proof/bundle-format.js";
6
+ import { createPactium } from "../../core/pactium-core.js";
7
+ import { createRepairPlanner } from "../../repair/planner.js";
8
+ import { createVerificationFailure } from "../../verification/failure.js";
9
+ import { asArray, safeText } from "../../shared/records.js";
10
+ import {
11
+ LICOLITE_ASPECT_PROTOCOL,
12
+ LICOLITE_CRITICAL_EXTENSIONS,
13
+ LICOLITE_POLICY_EXTENSION,
14
+ LICOLITE_SIGNATURE_EXTENSION,
15
+ LICOLITE_SUPPORTED_CRITICAL_EXTENSIONS,
16
+ LICOLITE_WORKSPACE_EFFECT_EXTENSION
17
+ } from "./constants.js";
18
+ import { createLicoLiteSigner } from "./signing.js";
19
+ import { materializeEvidenceExtension } from "./evidence.js";
20
+
21
+ async function attachSignature({ pactium, envelope, signer }) {
22
+ if (!signer) return envelope;
23
+ const signedEnvelopeHash = envelopeSigningHash(envelope);
24
+ const signature = await signer.sign(signedEnvelopeHash);
25
+ const signatureExtension = await pactium.createExtension({
26
+ name: LICOLITE_SIGNATURE_EXTENSION,
27
+ critical: false,
28
+ value: {
29
+ protocol: LICOLITE_ASPECT_PROTOCOL,
30
+ signerId: signer.signerId || "licolite-signer",
31
+ algorithm: signer.algorithm || "hmac-sha256",
32
+ signedEnvelopeHash,
33
+ signature
34
+ }
35
+ });
36
+ return pactium.storeEnvelope({
37
+ ...envelope,
38
+ extensions: [...asArray(envelope.extensions), signatureExtension]
39
+ });
40
+ }
41
+
42
+ function bundleBlockMap(bundle) {
43
+ if (!bundle) return null;
44
+ return createIndexedBundleResolver(bundle);
45
+ }
46
+
47
+ async function resolveMaterialBlock({ core, cid, bundleMap }) {
48
+ const bundled = await bundleMap?.get(cid);
49
+ if (bundled) {
50
+ return {
51
+ ...bundled,
52
+ bytes: Buffer.from(String(bundled.payloadBase64 || ""), "base64")
53
+ };
54
+ }
55
+ return core.storage.getBlock(cid);
56
+ }
57
+
58
+ export function createLicoLiteAspect({
59
+ pactium = null,
60
+ dataDir = "",
61
+ userDataPath = "",
62
+ inMemory = false,
63
+ evidencePolicy = "production",
64
+ signer = null,
65
+ signerSecret = ""
66
+ } = {}) {
67
+ const core = pactium || createPactium({ dataDir, userDataPath, inMemory });
68
+ const resolvedSigner = signer === false ? null : signer || createLicoLiteSigner({
69
+ secret: signerSecret || "licolite-development-signer"
70
+ });
71
+ const verifierSigner = resolvedSigner;
72
+ const repairPlanner = createRepairPlanner();
73
+
74
+ async function recordWorkspaceOperation(input = {}) {
75
+ const workspaceId = safeText(input.workspaceId || input.scope, "default");
76
+ const policyEvidence = input.policyEvidence ?? input.policy;
77
+ const effectEvidence = input.workspaceEffectEvidence ?? input.effectEvidence ?? input.workspaceEffect;
78
+ if (evidencePolicy === "production" && !policyEvidence) {
79
+ throw new Error("LicoLite production evidence policy requires policy evidence.");
80
+ }
81
+ if (evidencePolicy === "production" && !effectEvidence) {
82
+ throw new Error("LicoLite production evidence policy requires workspace effect evidence.");
83
+ }
84
+ const policyExtension = await materializeEvidenceExtension(core, {
85
+ name: LICOLITE_POLICY_EXTENSION,
86
+ evidence: policyEvidence || { missing: true, policy: "opportunistic" },
87
+ metadata: { workspaceId }
88
+ });
89
+ const effectExtension = await materializeEvidenceExtension(core, {
90
+ name: LICOLITE_WORKSPACE_EFFECT_EXTENSION,
91
+ evidence: effectEvidence || { missing: true, policy: "opportunistic" },
92
+ metadata: { workspaceId }
93
+ });
94
+ const envelope = await core.recordOperation({
95
+ ...input,
96
+ workspaceId,
97
+ extensions: [
98
+ policyExtension,
99
+ effectExtension,
100
+ ...asArray(input.extensions)
101
+ ],
102
+ stateMutations: input.stateMutations || input.state?.mutations || []
103
+ });
104
+ return attachSignature({ pactium: core, envelope, signer: resolvedSigner });
105
+ }
106
+
107
+ async function verifyLicoLiteEnvelope(envelope, options = {}) {
108
+ const bundleMap = bundleBlockMap(options.bundle || null);
109
+ const coreResult = await verifyProofEnvelope(envelope, {
110
+ storage: core.storage,
111
+ bundle: options.bundle || null,
112
+ supportedCriticalExtensions: LICOLITE_SUPPORTED_CRITICAL_EXTENSIONS,
113
+ proofVerifiers: options.proofVerifiers || {},
114
+ requireAllProofs: options.requireAllProofs !== false,
115
+ verifierManifest: options.verifierManifest || null,
116
+ ledgerHeadSignatures: options.ledgerHeadSignatures || []
117
+ });
118
+ const failures = [...coreResult.failures];
119
+ const extensionNames = new Set(asArray(envelope?.extensions).map((extension) => extension.name));
120
+ for (const required of LICOLITE_CRITICAL_EXTENSIONS) {
121
+ if (!extensionNames.has(required)) {
122
+ failures.push(createVerificationFailure({
123
+ layer: "licolite",
124
+ code: `missing_${required.replace(/\W+/g, "_")}`,
125
+ message: `LicoLite Proof Envelope is missing required critical extension ${required}.`,
126
+ evidenceRef: envelope?.envelopeId || "",
127
+ repairable: evidencePolicy !== "production"
128
+ }));
129
+ }
130
+ }
131
+ for (const extension of asArray(envelope?.extensions).filter((candidate) =>
132
+ candidate.name === LICOLITE_POLICY_EXTENSION || candidate.name === LICOLITE_WORKSPACE_EFFECT_EXTENSION
133
+ )) {
134
+ const extensionBlock = await resolveMaterialBlock({ core, cid: extension.valueRef, bundleMap });
135
+ const extensionValue = extensionBlock ? canonicalDecode(extensionBlock.bytes || Buffer.from(extensionBlock.payloadBase64, "base64")) : null;
136
+ const evidenceRef = extensionValue?.evidenceRef || extension.metadata?.evidenceRef || "";
137
+ const evidenceHash = extensionValue?.evidenceHash || extension.metadata?.evidenceHash || "";
138
+ if (!evidenceRef || !evidenceHash) {
139
+ failures.push(createVerificationFailure({
140
+ layer: "licolite.evidence",
141
+ code: "missing_evidence_ref",
142
+ evidenceRef: extension.valueRef,
143
+ repairable: true
144
+ }));
145
+ continue;
146
+ }
147
+ const evidenceBlock = await resolveMaterialBlock({ core, cid: evidenceRef, bundleMap });
148
+ if (!evidenceBlock) {
149
+ failures.push(createVerificationFailure({
150
+ layer: "licolite.evidence",
151
+ code: "missing_evidence_material",
152
+ evidenceRef,
153
+ repairable: true
154
+ }));
155
+ } else if (evidenceBlock.payloadHash !== evidenceHash) {
156
+ failures.push(createVerificationFailure({
157
+ layer: "licolite.evidence",
158
+ code: "bad_evidence_hash",
159
+ evidenceRef
160
+ }));
161
+ }
162
+ }
163
+ const signatureExtension = asArray(envelope?.extensions).find((extension) => extension.name === LICOLITE_SIGNATURE_EXTENSION);
164
+ if (!signatureExtension) {
165
+ failures.push(createVerificationFailure({
166
+ layer: "licolite.signing",
167
+ code: "missing_signature",
168
+ message: "LicoLite signing is enabled by default and no signature extension was found.",
169
+ evidenceRef: envelope?.envelopeId || "",
170
+ repairable: evidencePolicy !== "production"
171
+ }));
172
+ } else {
173
+ const block = await resolveMaterialBlock({ core, cid: signatureExtension.valueRef, bundleMap });
174
+ const value = block ? canonicalDecode(block.bytes || Buffer.from(block.payloadBase64, "base64")) : null;
175
+ if (!value) {
176
+ failures.push(createVerificationFailure({
177
+ layer: "licolite.signing",
178
+ code: "missing_signature_material",
179
+ evidenceRef: signatureExtension.valueRef,
180
+ repairable: true
181
+ }));
182
+ } else if (value.signedEnvelopeHash !== envelopeSigningHash(envelope)) {
183
+ failures.push(createVerificationFailure({
184
+ layer: "licolite.signing",
185
+ code: "bad_signed_envelope_hash",
186
+ evidenceRef: signatureExtension.valueRef
187
+ }));
188
+ } else if (verifierSigner && value.signerId !== (verifierSigner.signerId || "licolite-signer")) {
189
+ failures.push(createVerificationFailure({
190
+ layer: "licolite.signing",
191
+ code: "bad_signature_signer",
192
+ evidenceRef: signatureExtension.valueRef
193
+ }));
194
+ } else if (verifierSigner && value.algorithm !== (verifierSigner.algorithm || "hmac-sha256")) {
195
+ failures.push(createVerificationFailure({
196
+ layer: "licolite.signing",
197
+ code: "bad_signature_algorithm",
198
+ evidenceRef: signatureExtension.valueRef
199
+ }));
200
+ } else if (verifierSigner && !(await verifierSigner.verify(value.signedEnvelopeHash, value.signature))) {
201
+ failures.push(createVerificationFailure({
202
+ layer: "licolite.signing",
203
+ code: "bad_signature",
204
+ evidenceRef: signatureExtension.valueRef
205
+ }));
206
+ }
207
+ }
208
+ return {
209
+ protocol: PACTIUM_PROTOCOL,
210
+ aspect: LICOLITE_ASPECT_PROTOCOL,
211
+ envelopeId: envelope?.envelopeId || "",
212
+ ok: failures.length === 0,
213
+ failures,
214
+ checked: [
215
+ ...asArray(coreResult.checked),
216
+ "licolite-critical-policy-extension",
217
+ "licolite-critical-workspace-effect-extension",
218
+ "licolite-signature",
219
+ "licolite-workspace-projection"
220
+ ]
221
+ };
222
+ }
223
+
224
+ async function verifyLicoLiteBundle(bundle, options = {}) {
225
+ const bundleResult = await verifyProofBundle(bundle, {
226
+ supportedCriticalExtensions: LICOLITE_SUPPORTED_CRITICAL_EXTENSIONS,
227
+ ...options
228
+ });
229
+ const envelopeResult = await verifyLicoLiteEnvelope(bundle?.envelope || {}, {
230
+ ...options,
231
+ bundle
232
+ });
233
+ return {
234
+ protocol: PACTIUM_PROTOCOL,
235
+ aspect: LICOLITE_ASPECT_PROTOCOL,
236
+ ok: bundleResult.ok && envelopeResult.ok,
237
+ failures: [...bundleResult.failures, ...envelopeResult.failures],
238
+ bundle: bundleResult,
239
+ envelope: envelopeResult
240
+ };
241
+ }
242
+
243
+ function planRepair(failures = []) {
244
+ return repairPlanner.plan(failures);
245
+ }
246
+
247
+ return Object.freeze({
248
+ protocol: LICOLITE_ASPECT_PROTOCOL,
249
+ core,
250
+ evidencePolicy,
251
+ workspaceProjectionDefault: true,
252
+ criticalExtensions: LICOLITE_CRITICAL_EXTENSIONS,
253
+ supportedCriticalExtensions: LICOLITE_SUPPORTED_CRITICAL_EXTENSIONS,
254
+ signer: resolvedSigner,
255
+ recordWorkspaceOperation,
256
+ recordOperation: recordWorkspaceOperation,
257
+ verifyLicoLiteEnvelope,
258
+ verifyEnvelope: verifyLicoLiteEnvelope,
259
+ verifyLicoLiteBundle,
260
+ verifyBundle: verifyLicoLiteBundle,
261
+ planRepair,
262
+ getWorkspaceProjection: core.getWorkspaceProjection,
263
+ proveWorkspaceMembership: core.proveWorkspaceMembership,
264
+ exportProofBundle: core.exportProofBundle
265
+ });
266
+ }
267
+
268
+ export async function recordLicoLiteWorkspaceOperation(input = {}, options = {}) {
269
+ return createLicoLiteAspect(options).recordWorkspaceOperation(input);
270
+ }
271
+
272
+ export async function verifyLicoLiteEnvelope(envelope, options = {}) {
273
+ return createLicoLiteAspect(options).verifyLicoLiteEnvelope(envelope);
274
+ }
275
+
276
+ export async function verifyLicoLiteBundle(bundle, options = {}) {
277
+ return createLicoLiteAspect(options).verifyLicoLiteBundle(bundle);
278
+ }
@@ -0,0 +1,13 @@
1
+ export const LICOLITE_ASPECT_PROTOCOL = "pactium.v0.2.licolite-aspect";
2
+ export const LICOLITE_POLICY_EXTENSION = "licolite.policy";
3
+ export const LICOLITE_WORKSPACE_EFFECT_EXTENSION = "licolite.workspaceEffect";
4
+ export const LICOLITE_SIGNATURE_EXTENSION = "licolite.signature";
5
+ export const LICOLITE_CRITICAL_EXTENSIONS = Object.freeze([
6
+ LICOLITE_POLICY_EXTENSION,
7
+ LICOLITE_WORKSPACE_EFFECT_EXTENSION
8
+ ]);
9
+ export const LICOLITE_SUPPORTED_CRITICAL_EXTENSIONS = Object.freeze([
10
+ LICOLITE_POLICY_EXTENSION,
11
+ LICOLITE_WORKSPACE_EFFECT_EXTENSION,
12
+ LICOLITE_SIGNATURE_EXTENSION
13
+ ]);
@@ -0,0 +1,47 @@
1
+ import { protocolHash } from "../../protocol/hashing.js";
2
+ import { asRecord } from "../../shared/records.js";
3
+ import { LICOLITE_ASPECT_PROTOCOL, LICOLITE_POLICY_EXTENSION, LICOLITE_WORKSPACE_EFFECT_EXTENSION } from "./constants.js";
4
+
5
+ export async function materializeEvidenceExtension(pactium, {
6
+ name,
7
+ evidence,
8
+ critical = true,
9
+ metadata = {}
10
+ }) {
11
+ const block = await pactium.storage.putBlock(evidence || {}, {
12
+ kind: `licolite-evidence:${name}`
13
+ });
14
+ return pactium.createExtension({
15
+ name,
16
+ critical,
17
+ value: {
18
+ protocol: LICOLITE_ASPECT_PROTOCOL,
19
+ evidenceType: name,
20
+ evidenceRef: block.cid,
21
+ evidenceHash: block.payloadHash,
22
+ metadata
23
+ },
24
+ metadata: {
25
+ evidenceRef: block.cid,
26
+ evidenceHash: block.payloadHash
27
+ }
28
+ });
29
+ }
30
+
31
+ export function licoLitePolicyExtensionValue(input = {}) {
32
+ return {
33
+ protocol: LICOLITE_ASPECT_PROTOCOL,
34
+ evidenceType: LICOLITE_POLICY_EXTENSION,
35
+ decision: asRecord(input.decision),
36
+ evidenceHash: protocolHash("proof.extension", input.evidence || {})
37
+ };
38
+ }
39
+
40
+ export function licoLiteWorkspaceEffectExtensionValue(input = {}) {
41
+ return {
42
+ protocol: LICOLITE_ASPECT_PROTOCOL,
43
+ evidenceType: LICOLITE_WORKSPACE_EFFECT_EXTENSION,
44
+ effect: asRecord(input.effect),
45
+ evidenceHash: protocolHash("proof.extension", input.evidence || {})
46
+ };
47
+ }
@@ -0,0 +1,51 @@
1
+ import type {
2
+ PactiumCore,
3
+ PactiumProofBundle,
4
+ PactiumProofBundleExportOptions,
5
+ PactiumProofEnvelope,
6
+ PactiumRecord,
7
+ PactiumVerificationResult
8
+ } from "../../index.js";
9
+
10
+ export interface LicoLiteSigner {
11
+ protocol: string;
12
+ signerId: string;
13
+ algorithm: string;
14
+ sign(message: string): Promise<string>;
15
+ verify(message: string, signature: string): Promise<boolean>;
16
+ }
17
+
18
+ export interface LicoLiteAspect {
19
+ protocol: string;
20
+ core: PactiumCore;
21
+ evidencePolicy: string;
22
+ workspaceProjectionDefault: true;
23
+ criticalExtensions: readonly string[];
24
+ supportedCriticalExtensions: readonly string[];
25
+ signer: LicoLiteSigner | null;
26
+ recordWorkspaceOperation(input?: PactiumRecord): Promise<PactiumProofEnvelope>;
27
+ recordOperation(input?: PactiumRecord): Promise<PactiumProofEnvelope>;
28
+ verifyLicoLiteEnvelope(envelope: PactiumProofEnvelope, options?: PactiumRecord): Promise<PactiumVerificationResult>;
29
+ verifyEnvelope(envelope: PactiumProofEnvelope, options?: PactiumRecord): Promise<PactiumVerificationResult>;
30
+ verifyLicoLiteBundle(bundle: PactiumProofBundle, options?: PactiumRecord): Promise<PactiumVerificationResult>;
31
+ verifyBundle(bundle: PactiumProofBundle, options?: PactiumRecord): Promise<PactiumVerificationResult>;
32
+ planRepair(failures?: PactiumRecord[]): PactiumRecord;
33
+ getWorkspaceProjection(workspaceId?: string): Promise<PactiumRecord>;
34
+ proveWorkspaceMembership(input?: PactiumRecord): Promise<PactiumRecord>;
35
+ exportProofBundle(envelopeOrId: PactiumProofEnvelope | string, options?: PactiumProofBundleExportOptions): Promise<PactiumProofBundle>;
36
+ }
37
+
38
+ export const LICOLITE_ASPECT_PROTOCOL: string;
39
+ export const LICOLITE_POLICY_EXTENSION: "licolite.policy";
40
+ export const LICOLITE_WORKSPACE_EFFECT_EXTENSION: "licolite.workspaceEffect";
41
+ export const LICOLITE_SIGNATURE_EXTENSION: "licolite.signature";
42
+ export const LICOLITE_CRITICAL_EXTENSIONS: readonly string[];
43
+ export const LICOLITE_SUPPORTED_CRITICAL_EXTENSIONS: readonly string[];
44
+
45
+ export function createLicoLiteSigner(options?: PactiumRecord): LicoLiteSigner;
46
+ export function createLicoLiteAspect(options?: PactiumRecord & { pactium?: PactiumCore | null; signer?: LicoLiteSigner | false | null }): LicoLiteAspect;
47
+ export function recordLicoLiteWorkspaceOperation(input?: PactiumRecord, options?: PactiumRecord): Promise<PactiumProofEnvelope>;
48
+ export function verifyLicoLiteEnvelope(envelope: PactiumProofEnvelope, options?: PactiumRecord): Promise<PactiumVerificationResult>;
49
+ export function verifyLicoLiteBundle(bundle: PactiumProofBundle, options?: PactiumRecord): Promise<PactiumVerificationResult>;
50
+ export function licoLitePolicyExtensionValue(input?: PactiumRecord): PactiumRecord;
51
+ export function licoLiteWorkspaceEffectExtensionValue(input?: PactiumRecord): PactiumRecord;
@@ -0,0 +1,19 @@
1
+ export {
2
+ LICOLITE_ASPECT_PROTOCOL,
3
+ LICOLITE_CRITICAL_EXTENSIONS,
4
+ LICOLITE_POLICY_EXTENSION,
5
+ LICOLITE_SIGNATURE_EXTENSION,
6
+ LICOLITE_SUPPORTED_CRITICAL_EXTENSIONS,
7
+ LICOLITE_WORKSPACE_EFFECT_EXTENSION
8
+ } from "./constants.js";
9
+ export { createLicoLiteSigner } from "./signing.js";
10
+ export {
11
+ licoLitePolicyExtensionValue,
12
+ licoLiteWorkspaceEffectExtensionValue
13
+ } from "./evidence.js";
14
+ export {
15
+ createLicoLiteAspect,
16
+ recordLicoLiteWorkspaceOperation,
17
+ verifyLicoLiteBundle,
18
+ verifyLicoLiteEnvelope
19
+ } from "./aspect.js";
@@ -0,0 +1,78 @@
1
+ import crypto from "node:crypto";
2
+
3
+ import { LICOLITE_ASPECT_PROTOCOL } from "./constants.js";
4
+
5
+ function hmac(secret, text) {
6
+ return crypto.createHmac("sha256", String(secret || "")).update(String(text || "")).digest("hex");
7
+ }
8
+
9
+ function ed25519Sign(privateKey, text) {
10
+ if (!privateKey) throw new Error("Ed25519 LicoLite signer requires a privateKey for signing.");
11
+ return crypto.sign(null, Buffer.from(String(text || "")), privateKey).toString("base64");
12
+ }
13
+
14
+ function ed25519Verify(publicKey, text, signature) {
15
+ if (!publicKey || !String(signature || "").startsWith("ed25519:")) return false;
16
+ return crypto.verify(
17
+ null,
18
+ Buffer.from(String(text || "")),
19
+ publicKey,
20
+ Buffer.from(String(signature).slice("ed25519:".length), "base64")
21
+ );
22
+ }
23
+
24
+ function publicKeyFromPrivateKey(privateKey) {
25
+ if (!privateKey) return "";
26
+ return crypto.createPublicKey(privateKey).export({ type: "spki", format: "pem" });
27
+ }
28
+
29
+ /* node:coverage disable */
30
+ function asRecord(value) {
31
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
32
+ }
33
+
34
+ function asArray(value) {
35
+ return Array.isArray(value) ? value : [];
36
+ }
37
+
38
+ function safeText(value, fallback = "") {
39
+ const output = String(value ?? "").trim();
40
+ return output || fallback;
41
+ }
42
+ /* node:coverage enable */
43
+
44
+ export function createLicoLiteSigner({
45
+ signerId = "licolite-local",
46
+ secret = "licolite-development-signer",
47
+ algorithm = "",
48
+ privateKey = "",
49
+ publicKey = ""
50
+ } = {}) {
51
+ const resolvedAlgorithm = safeText(algorithm, privateKey || publicKey ? "ed25519" : "hmac-sha256");
52
+ if (resolvedAlgorithm === "ed25519") {
53
+ const verifierPublicKey = publicKey || publicKeyFromPrivateKey(privateKey);
54
+ return Object.freeze({
55
+ protocol: LICOLITE_ASPECT_PROTOCOL,
56
+ signerId,
57
+ algorithm: "ed25519",
58
+ publicKey: verifierPublicKey,
59
+ async sign(message) {
60
+ return `ed25519:${ed25519Sign(privateKey, message)}`;
61
+ },
62
+ async verify(message, signature) {
63
+ return ed25519Verify(verifierPublicKey, message, signature);
64
+ }
65
+ });
66
+ }
67
+ return Object.freeze({
68
+ protocol: LICOLITE_ASPECT_PROTOCOL,
69
+ signerId,
70
+ algorithm: "hmac-sha256",
71
+ async sign(message) {
72
+ return `hmac-sha256:${hmac(secret, message)}`;
73
+ },
74
+ async verify(message, signature) {
75
+ return signature === `hmac-sha256:${hmac(secret, message)}`;
76
+ }
77
+ });
78
+ }
@@ -0,0 +1,40 @@
1
+ const TEXT_ENCODER = new TextEncoder();
2
+ const TEXT_DECODER = new TextDecoder();
3
+
4
+ export function normalizeCanonicalValue(value) {
5
+ if (value === null) return null;
6
+ if (value === undefined) return null;
7
+ if (Buffer.isBuffer(value) || value instanceof Uint8Array) {
8
+ return { $bytes: Buffer.from(value).toString("base64") };
9
+ }
10
+ if (typeof value === "boolean" || typeof value === "string") return value;
11
+ if (typeof value === "number") {
12
+ if (!Number.isFinite(value)) {
13
+ throw new TypeError("Pactium Canonical Value only supports finite numbers.");
14
+ }
15
+ return Object.is(value, -0) ? 0 : value;
16
+ }
17
+ if (Array.isArray(value)) return value.map((item) => normalizeCanonicalValue(item));
18
+ if (typeof value === "object") {
19
+ return Object.fromEntries(
20
+ Object.keys(value)
21
+ .filter((key) => value[key] !== undefined)
22
+ .sort()
23
+ .map((key) => [key, normalizeCanonicalValue(value[key])])
24
+ );
25
+ }
26
+ throw new TypeError(`Unsupported Pactium Canonical Value type: ${typeof value}`);
27
+ }
28
+
29
+ export function canonicalString(value) {
30
+ return JSON.stringify(normalizeCanonicalValue(value));
31
+ }
32
+
33
+ export function canonicalEncode(value) {
34
+ return TEXT_ENCODER.encode(canonicalString(value));
35
+ }
36
+
37
+ export function canonicalDecode(bytes) {
38
+ const buffer = Buffer.isBuffer(bytes) ? bytes : Buffer.from(bytes || "");
39
+ return JSON.parse(TEXT_DECODER.decode(buffer));
40
+ }
@@ -0,0 +1,102 @@
1
+ import { PACTIUM_PROTOCOL, PACTIUM_SCHEMA_VERSION } from "../protocol/constants.js";
2
+ import { normalizeCanonicalValue } from "../canonical/value.js";
3
+ import { createId, protocolHash } from "../protocol/hashing.js";
4
+ import { asArray, asRecord, nowIso, safeText } from "../shared/records.js";
5
+ import { createVerificationFailure, PactiumLifecycleError } from "../verification/failure.js";
6
+
7
+ export function createAppendCondition(input = {}) {
8
+ const payload = {
9
+ protocol: PACTIUM_PROTOCOL,
10
+ schema: PACTIUM_SCHEMA_VERSION,
11
+ conditionType: "pactium.append-condition",
12
+ workspaceId: safeText(input.workspaceId, "default"),
13
+ requiredLedgerHead: safeText(input.requiredLedgerHead || input.ledgerHead),
14
+ requiredWorkspaceOrderRoot: safeText(input.requiredWorkspaceOrderRoot || input.workspaceOrderRoot),
15
+ requiredWorkspaceMembershipRoot: safeText(input.requiredWorkspaceMembershipRoot || input.workspaceMembershipRoot),
16
+ requiredOpenIntentState: normalizeCanonicalValue(asRecord(input.requiredOpenIntentState)),
17
+ requiredOutcomeState: normalizeCanonicalValue(asRecord(input.requiredOutcomeState)),
18
+ expectedCausalityRefs: asArray(input.expectedCausalityRefs).map(String),
19
+ allowMissingCausalityRefs: input.allowMissingCausalityRefs === true,
20
+ createdAt: input.createdAt || nowIso()
21
+ };
22
+ return {
23
+ ...payload,
24
+ conditionId: createId("append_condition", payload),
25
+ conditionHash: protocolHash("append.condition", payload)
26
+ };
27
+ }
28
+
29
+ function requiredStateMatches(required, actual) {
30
+ const state = asRecord(required);
31
+ if (Object.keys(state).length === 0) return true;
32
+ if (typeof state.exists === "boolean" && actual.exists !== state.exists) return false;
33
+ if (state.intentId && actual.intentId !== state.intentId) return false;
34
+ if (state.outcomeId && actual.outcomeId !== state.outcomeId) return false;
35
+ return true;
36
+ }
37
+
38
+ function fail(code, message, details = {}) {
39
+ throw new PactiumLifecycleError(message, createVerificationFailure({
40
+ layer: "append-condition",
41
+ code,
42
+ message,
43
+ repairable: false,
44
+ details
45
+ }));
46
+ }
47
+
48
+ export async function assertAppendCondition(condition, {
49
+ phase = "intent",
50
+ currentHead = {},
51
+ workspace = {},
52
+ openIntentState = null,
53
+ outcomeState = null,
54
+ knownCausalityRefs = new Set()
55
+ } = {}) {
56
+ if (!condition) return true;
57
+ if (condition.requiredLedgerHead &&
58
+ condition.requiredLedgerHead !== currentHead.headId &&
59
+ condition.requiredLedgerHead !== currentHead.root &&
60
+ condition.requiredLedgerHead !== currentHead.rootHash) {
61
+ fail("ledger_head_conflict", "Append condition required a different Ledger head.", {
62
+ requiredLedgerHead: condition.requiredLedgerHead,
63
+ currentHeadId: currentHead.headId || "",
64
+ currentRoot: currentHead.root || "",
65
+ currentRootHash: currentHead.rootHash || ""
66
+ });
67
+ }
68
+ if (condition.requiredWorkspaceOrderRoot && condition.requiredWorkspaceOrderRoot !== workspace.orderRoot) {
69
+ fail("workspace_order_root_conflict", "Append condition required a different workspace order root.", {
70
+ requiredWorkspaceOrderRoot: condition.requiredWorkspaceOrderRoot,
71
+ currentWorkspaceOrderRoot: workspace.orderRoot || ""
72
+ });
73
+ }
74
+ if (condition.requiredWorkspaceMembershipRoot && condition.requiredWorkspaceMembershipRoot !== workspace.membershipRoot) {
75
+ fail("workspace_membership_root_conflict", "Append condition required a different workspace membership root.", {
76
+ requiredWorkspaceMembershipRoot: condition.requiredWorkspaceMembershipRoot,
77
+ currentWorkspaceMembershipRoot: workspace.membershipRoot || ""
78
+ });
79
+ }
80
+ if (phase === "intent" && openIntentState && !requiredStateMatches(condition.requiredOpenIntentState, openIntentState)) {
81
+ fail("open_intent_state_conflict", "Append condition required a different open-intent state.", {
82
+ requiredOpenIntentState: condition.requiredOpenIntentState,
83
+ actual: openIntentState
84
+ });
85
+ }
86
+ if (phase === "outcome" && outcomeState && !requiredStateMatches(condition.requiredOutcomeState, outcomeState)) {
87
+ fail("outcome_state_conflict", "Append condition required a different outcome state.", {
88
+ requiredOutcomeState: condition.requiredOutcomeState,
89
+ actual: outcomeState
90
+ });
91
+ }
92
+ if (!condition.allowMissingCausalityRefs) {
93
+ for (const ref of asArray(condition.expectedCausalityRefs)) {
94
+ if (!knownCausalityRefs.has(ref)) {
95
+ fail("unknown_causality_ref", "Append condition referenced unknown causality material.", {
96
+ causalityRef: ref
97
+ });
98
+ }
99
+ }
100
+ }
101
+ return true;
102
+ }