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.
- package/LICENSE +674 -0
- package/README.md +92 -0
- package/README.zh-CN.md +90 -0
- package/SECURITY.md +7 -0
- package/bin/pactium.mjs +121 -0
- package/docs/LICOLITE-ASPECT.md +57 -0
- package/docs/README.md +13 -0
- package/docs/TERM.md +289 -0
- package/docs/architecture/ARCHITECTURE.md +62 -0
- package/docs/protocols/PROFILE.md +124 -0
- package/docs/protocols/PROTOCOLS.md +62 -0
- package/examples/record-operation.mjs +26 -0
- package/package.json +69 -0
- package/src/README.md +13 -0
- package/src/aspects/licolite/aspect.js +278 -0
- package/src/aspects/licolite/constants.js +13 -0
- package/src/aspects/licolite/evidence.js +47 -0
- package/src/aspects/licolite/index.d.ts +51 -0
- package/src/aspects/licolite/index.js +19 -0
- package/src/aspects/licolite/signing.js +78 -0
- package/src/canonical/value.js +40 -0
- package/src/core/append-condition.js +102 -0
- package/src/core/pactium-core.js +1073 -0
- package/src/core/tracking-cursor.js +68 -0
- package/src/http.js +99 -0
- package/src/index-engine/snapshot-merkle-index.js +994 -0
- package/src/index.d.ts +244 -0
- package/src/index.js +73 -0
- package/src/ledger/signed-head.js +204 -0
- package/src/ledger/transparency-log.js +702 -0
- package/src/maintenance/task-engine.js +36 -0
- package/src/proof/bundle-format.js +265 -0
- package/src/proof/bundle.js +77 -0
- package/src/proof/envelope.js +548 -0
- package/src/proof/registry.js +18 -0
- package/src/protocol/constants.js +69 -0
- package/src/protocol/hashing.js +47 -0
- package/src/quality/profile-runner.js +291 -0
- package/src/repair/planner.js +62 -0
- package/src/shared/records.js +32 -0
- package/src/storage/local-json-storage-port.js +360 -0
- 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
|
+
}
|