@ottochain/sdk 2.6.0 → 2.7.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.
@@ -112,6 +112,11 @@ function createMintAssetPayload(params) {
112
112
  /**
113
113
  * Apply a typed morphism (Transfer / Burn / Compose / ...). asset-model.md §7.
114
114
  * Transfer = `{ kind: "Transfer", recipient }`; Burn (repay) = `{ kind: "Burn" }`.
115
+ *
116
+ * C2 — a `Compose`/`Pool` that folds in a counter-party the signer does NOT own is now rejected on-chain
117
+ * unless a live `AuthorizeCompose` nonce authorizes it: pass that nonce here (`nonce`) and build the commit
118
+ * half with {@link createAuthorizeComposePayload}. A same-holder compose (all parts signer-owned) needs no
119
+ * nonce. `otherAssets` must be duplicate-free and must not include `assetId`.
115
120
  */
116
121
  function createApplyMorphismPayload(params) {
117
122
  return {
@@ -128,7 +133,12 @@ function createApplyMorphismPayload(params) {
128
133
  },
129
134
  };
130
135
  }
131
- /** Commit half of a two-party compose. asset-model.md §7/§8. */
136
+ /**
137
+ * Commit half of a two-party (cross-holder) compose. asset-model.md §7/§8. As of chain finding C2 this
138
+ * handshake is MANDATORY: a cross-holder `Compose`/`Pool` is rejected unless a matching, unexpired nonce
139
+ * from this call is live for the counter-party. The composing party then echoes `nonce` in its
140
+ * `ApplyMorphism`. A same-holder compose (all parts owned by the signer) does not need this.
141
+ */
132
142
  function createAuthorizeComposePayload(params) {
133
143
  return {
134
144
  AuthorizeCompose: {
package/dist/cjs/index.js CHANGED
@@ -31,8 +31,8 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
31
31
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
32
32
  };
33
33
  Object.defineProperty(exports, "__esModule", { value: true });
34
- exports.IMMUTABLE_POLICY = exports.immutable = exports.unconstrained = exports.constrained = exports.defineFiberApp = exports.toProtoDefinition = exports.OttoMetagraphClient = exports.assert = exports.safeParse = exports.validateKeyPair = exports.validateAddress = exports.validatePublicKey = exports.validatePrivateKey = exports.validate = exports.CompleteContractRequestSchema = exports.AcceptContractRequestSchema = exports.ProposeContractRequestSchema = exports.ContractTermsSchema = exports.PlatformLinkSchema = exports.AgentIdentityRegistrationSchema = exports.TransferParamsSchema = exports.CurrencyTransactionSchema = exports.CurrencyTransactionValueSchema = exports.TransactionReferenceSchema = exports.SignedSchema = exports.SignatureProofSchema = exports.KeyPairSchema = exports.PublicKeySchema = exports.PrivateKeySchema = exports.DagAddressSchema = exports.wrapError = exports.isErrorCode = exports.ErrorCode = exports.TransactionError = exports.SigningError = exports.ValidationError = exports.NetworkError = exports.OttoChainError = exports.GENESIS_MANIFEST_VERSION = exports.buildGenesisManifest = exports.dropNulls = exports.MetagraphNetworkError = exports.HttpClient = exports.createMetagraphClient = exports.MetagraphClient = exports.batchSign = exports.addSignature = exports.createSignedObject = exports.signDataUpdate = exports.verify = void 0;
35
- exports.SHIELDED_POOL_STATE = exports.shieldApp = exports.RESERVED_EFFECT_KEYS = exports.toWallet = exports.toFiber = exports.scriptCall = exports.emit = exports.spawn = exports.triggers = exports.transferAsset = exports.setDependencyActive = exports.addDependency = exports.actorNotInArray = exports.depInState = exports.signerHasRoleVia = exports.signerHasReputationVia = exports.signerHasRole = exports.signerHasReputation = exports.actorHasEntry = exports.actorInSet = exports.actorIsSigner = exports.assetSignerIs = exports.signerHasEntry = exports.signerIsNotParty = exports.signerInSet = exports.signerIsAnyParty = exports.signerIsParty = exports.projectFiberPolicy = void 0;
34
+ exports.constrained = exports.defineFiberApp = exports.toProtoDefinition = exports.SNAPSHOT_FINALIZED_EVENT = exports.OttoMetagraphClient = exports.assert = exports.safeParse = exports.validateKeyPair = exports.validateAddress = exports.validatePublicKey = exports.validatePrivateKey = exports.validate = exports.CompleteContractRequestSchema = exports.AcceptContractRequestSchema = exports.ProposeContractRequestSchema = exports.ContractTermsSchema = exports.PlatformLinkSchema = exports.AgentIdentityRegistrationSchema = exports.TransferParamsSchema = exports.CurrencyTransactionSchema = exports.CurrencyTransactionValueSchema = exports.TransactionReferenceSchema = exports.SignedSchema = exports.SignatureProofSchema = exports.KeyPairSchema = exports.PublicKeySchema = exports.PrivateKeySchema = exports.DagAddressSchema = exports.wrapError = exports.isErrorCode = exports.ErrorCode = exports.TransactionError = exports.SigningError = exports.ValidationError = exports.NetworkError = exports.OttoChainError = exports.GENESIS_MANIFEST_VERSION = exports.buildGenesisManifest = exports.MORPHISM_LINT_CODES = exports.lintApplyMorphism = exports.dropNulls = exports.MetagraphNetworkError = exports.HttpClient = exports.createMetagraphClient = exports.MetagraphClient = exports.batchSign = exports.addSignature = exports.createSignedObject = exports.signDataUpdate = exports.verify = void 0;
35
+ exports.SHIELDED_POOL_STATE = exports.shieldApp = exports.RESERVED_EFFECT_KEYS = exports.toWallet = exports.toFiber = exports.scriptCall = exports.emit = exports.spawn = exports.triggers = exports.transferAsset = exports.setDependencyActive = exports.addDependency = exports.actorNotInArray = exports.depInState = exports.signerHasRoleVia = exports.signerHasReputationVia = exports.signerHasRole = exports.signerHasReputation = exports.actorHasEntry = exports.actorInSet = exports.actorIsSigner = exports.assetSignerIs = exports.signerHasEntry = exports.signerIsNotParty = exports.signerInSet = exports.signerIsAnyParty = exports.signerIsParty = exports.projectFiberPolicy = exports.IMMUTABLE_POLICY = exports.immutable = exports.unconstrained = void 0;
36
36
  // ─── Core metagraph SDK ───────────────────────────────────────────────────────
37
37
  // Consumers can also import directly from '@constellation-network/metagraph-sdk'
38
38
  __exportStar(require("@constellation-network/metagraph-sdk"), exports);
@@ -62,6 +62,10 @@ __exportStar(require("./types.js"), exports);
62
62
  __exportStar(require("./ottochain/transaction.js"), exports);
63
63
  var drop_nulls_js_1 = require("./ottochain/drop-nulls.js");
64
64
  Object.defineProperty(exports, "dropNulls", { enumerable: true, get: function () { return drop_nulls_js_1.dropNulls; } });
65
+ // Message-layer advisory lint for ApplyMorphism (Compose/Pool consent + inflation checks).
66
+ var morphism_lint_js_1 = require("./ottochain/morphism-lint.js");
67
+ Object.defineProperty(exports, "lintApplyMorphism", { enumerable: true, get: function () { return morphism_lint_js_1.lintApplyMorphism; } });
68
+ Object.defineProperty(exports, "MORPHISM_LINT_CODES", { enumerable: true, get: function () { return morphism_lint_js_1.MORPHISM_LINT_CODES; } });
65
69
  // ─── Genesis manifest exporter (std-app pre-registration content) ─────────────
66
70
  var genesis_manifest_js_1 = require("./ottochain/genesis-manifest.js");
67
71
  Object.defineProperty(exports, "buildGenesisManifest", { enumerable: true, get: function () { return genesis_manifest_js_1.buildGenesisManifest; } });
@@ -108,6 +112,8 @@ Object.defineProperty(exports, "assert", { enumerable: true, get: function () {
108
112
  // ─── OttoChain metagraph client ───────────────────────────────────────────────
109
113
  var metagraph_client_js_1 = require("./ottochain/metagraph-client.js");
110
114
  Object.defineProperty(exports, "OttoMetagraphClient", { enumerable: true, get: function () { return metagraph_client_js_1.MetagraphClient; } });
115
+ var webhook_notifications_js_1 = require("./ottochain/webhook-notifications.js");
116
+ Object.defineProperty(exports, "SNAPSHOT_FINALIZED_EVENT", { enumerable: true, get: function () { return webhook_notifications_js_1.SNAPSHOT_FINALIZED_EVENT; } });
111
117
  // Proto definition helper
112
118
  var fiber_app_js_1 = require("./schema/fiber-app.js");
113
119
  Object.defineProperty(exports, "toProtoDefinition", { enumerable: true, get: function () { return fiber_app_js_1.toProtoDefinition; } });
@@ -40,7 +40,7 @@ var __importStar = (this && this.__importStar) || (function () {
40
40
  };
41
41
  })();
42
42
  Object.defineProperty(exports, "__esModule", { value: true });
43
- exports.GENESIS_MANIFEST_VERSION = exports.buildGenesisManifest = exports.dropNulls = exports.createAuthorizeComposePayload = exports.createApplyMorphismPayload = exports.createMintAssetPayload = exports.createAssetPolicyPayload = exports.createDataTransactionRequest = exports.createScriptPayload = exports.createStateMachinePayload = exports.getPublicKeyForRegistration = exports.addTransactionSignature = exports.signTransaction = exports.createInvokeScriptPayload = exports.createArchivePayload = exports.createTransitionPayload = exports.MetagraphClient = exports.extractOnChainState = exports.getScriptInvocations = exports.getEventReceipts = exports.getLogsForFiber = exports.getLatestOnChainState = exports.getSnapshotOnChainState = exports.decodeOnChainState = exports.TOKEN_BEHAVIOR_BITS = exports.OTTOCHAIN_MESSAGE_TYPES = exports.proto = void 0;
43
+ exports.GENESIS_MANIFEST_VERSION = exports.buildGenesisManifest = exports.dropNulls = exports.MORPHISM_LINT_CODES = exports.lintApplyMorphism = exports.createAuthorizeComposePayload = exports.createApplyMorphismPayload = exports.createMintAssetPayload = exports.createAssetPolicyPayload = exports.createDataTransactionRequest = exports.createScriptPayload = exports.createStateMachinePayload = exports.getPublicKeyForRegistration = exports.addTransactionSignature = exports.signTransaction = exports.createInvokeScriptPayload = exports.createArchivePayload = exports.createTransitionPayload = exports.SNAPSHOT_FINALIZED_EVENT = exports.commitKeyPath = exports.verifyMptInclusion = exports.verifyStateProof = exports.MetagraphClient = exports.extractOnChainState = exports.getScriptInvocations = exports.getEventReceipts = exports.getLogsForFiber = exports.getLatestOnChainState = exports.getSnapshotOnChainState = exports.decodeOnChainState = exports.TOKEN_BEHAVIOR_BITS = exports.OTTOCHAIN_MESSAGE_TYPES = exports.proto = void 0;
44
44
  // Re-export generated protobuf types (for binary encoding)
45
45
  exports.proto = __importStar(require("../generated/index.js"));
46
46
  // Runtime message type validation
@@ -57,6 +57,12 @@ Object.defineProperty(exports, "getScriptInvocations", { enumerable: true, get:
57
57
  Object.defineProperty(exports, "extractOnChainState", { enumerable: true, get: function () { return snapshot_js_1.extractOnChainState; } });
58
58
  var metagraph_client_js_1 = require("./metagraph-client.js");
59
59
  Object.defineProperty(exports, "MetagraphClient", { enumerable: true, get: function () { return metagraph_client_js_1.MetagraphClient; } });
60
+ var state_proof_js_1 = require("./state-proof.js");
61
+ Object.defineProperty(exports, "verifyStateProof", { enumerable: true, get: function () { return state_proof_js_1.verifyStateProof; } });
62
+ Object.defineProperty(exports, "verifyMptInclusion", { enumerable: true, get: function () { return state_proof_js_1.verifyMptInclusion; } });
63
+ Object.defineProperty(exports, "commitKeyPath", { enumerable: true, get: function () { return state_proof_js_1.commitKeyPath; } });
64
+ var webhook_notifications_js_1 = require("./webhook-notifications.js");
65
+ Object.defineProperty(exports, "SNAPSHOT_FINALIZED_EVENT", { enumerable: true, get: function () { return webhook_notifications_js_1.SNAPSHOT_FINALIZED_EVENT; } });
60
66
  // Transaction helpers (state machine payloads, signing)
61
67
  var transaction_js_1 = require("./transaction.js");
62
68
  Object.defineProperty(exports, "createTransitionPayload", { enumerable: true, get: function () { return transaction_js_1.createTransitionPayload; } });
@@ -72,6 +78,10 @@ Object.defineProperty(exports, "createAssetPolicyPayload", { enumerable: true, g
72
78
  Object.defineProperty(exports, "createMintAssetPayload", { enumerable: true, get: function () { return transaction_js_1.createMintAssetPayload; } });
73
79
  Object.defineProperty(exports, "createApplyMorphismPayload", { enumerable: true, get: function () { return transaction_js_1.createApplyMorphismPayload; } });
74
80
  Object.defineProperty(exports, "createAuthorizeComposePayload", { enumerable: true, get: function () { return transaction_js_1.createAuthorizeComposePayload; } });
81
+ // Message-layer advisory lint for ApplyMorphism (Compose/Pool C2 mistakes the chain rejects).
82
+ var morphism_lint_js_1 = require("./morphism-lint.js");
83
+ Object.defineProperty(exports, "lintApplyMorphism", { enumerable: true, get: function () { return morphism_lint_js_1.lintApplyMorphism; } });
84
+ Object.defineProperty(exports, "MORPHISM_LINT_CODES", { enumerable: true, get: function () { return morphism_lint_js_1.MORPHISM_LINT_CODES; } });
75
85
  // Data utilities
76
86
  var drop_nulls_js_1 = require("./drop-nulls.js");
77
87
  Object.defineProperty(exports, "dropNulls", { enumerable: true, get: function () { return drop_nulls_js_1.dropNulls; } });
@@ -0,0 +1,99 @@
1
+ "use strict";
2
+ /**
3
+ * Message-layer advisory lint for an {@link ApplyMorphism} transaction (asset-model.md §7; chain
4
+ * audit finding C2, enforced in `AssetCombiner`).
5
+ *
6
+ * Purpose
7
+ * -------
8
+ * The definition-scoped {@link ../schema/guard-lint.ts | guard-lint} walks fiber-app DEFINITIONS; it
9
+ * never sees an `ApplyMorphism` MESSAGE, so a malformed / no-consent `Compose` slips past it. This
10
+ * linter closes that gap at the transaction-builder layer: hand it the `ApplyMorphism` you are about
11
+ * to sign and it flags the two classes of C2 mistake the chain rejects.
12
+ *
13
+ * The chain rule being mirrored (C2, `Compose`/`Pool` only):
14
+ * - `otherAssetIds` must be free of duplicates and must NOT include the source `assetId`
15
+ * (the old self/duplicate-inflation path — rejected unconditionally, holder-independent).
16
+ * - every counter-party the signer does NOT hold requires a live {@link AuthorizeCompose} `nonce`;
17
+ * a same-holder / all-signer-owned compose needs none.
18
+ *
19
+ * ERRORS are statically certain (the chain rejects regardless of who holds what). WARNINGS are
20
+ * heuristic — the signer's holdings/ownership are unknown at build time, so a missing consent nonce
21
+ * cannot be proven fatal (a same-holder compose is legitimately nonce-less).
22
+ *
23
+ * This is a PURE, non-throwing advisory. It is deliberately NOT wired into
24
+ * {@link createApplyMorphismPayload} (which must stay non-breaking and side-effect-free); call it
25
+ * explicitly before signing, or in your app's own pre-flight checks.
26
+ */
27
+ Object.defineProperty(exports, "__esModule", { value: true });
28
+ exports.MORPHISM_LINT_CODES = void 0;
29
+ exports.lintApplyMorphism = lintApplyMorphism;
30
+ /** Stable rule codes for {@link lintApplyMorphism} — referenced by tests and report tooling. */
31
+ exports.MORPHISM_LINT_CODES = {
32
+ /** `otherAssetIds` contains a duplicate id (chain rejects — inflation path). */
33
+ DUPLICATE_OTHER_ID: 'morphism-duplicate-other-id',
34
+ /** `otherAssetIds` includes the source `assetId` (self-composition — chain rejects). */
35
+ SELF_COMPOSE: 'morphism-self-compose',
36
+ /** `otherAssetIds` is non-empty but `kind` ignores it (not Compose/Pool — likely a mistake). */
37
+ OTHER_IDS_IGNORED: 'morphism-other-ids-ignored',
38
+ /** Compose/Pool with counter-parties and no consent `nonce` (rejected IF any part is not signer-owned). */
39
+ COMPOSE_NO_NONCE: 'morphism-compose-no-nonce',
40
+ };
41
+ /** The morphism kinds whose semantics READ `otherAssetIds` (fold counter-party parts into a composite). */
42
+ const COMPOSING_KINDS = new Set(['COMPOSE', 'POOL']);
43
+ function finding(severity, code, message, path) {
44
+ return { severity, code, message, path };
45
+ }
46
+ /**
47
+ * Lint a single {@link ApplyMorphism} message. Pure and non-throwing. Returns every finding
48
+ * (`error` + `warn`); callers decide the policy (e.g. block on any `error`, surface `warn`s).
49
+ *
50
+ * ERRORS (statically certain — the chain rejects unconditionally, holder-independent):
51
+ * - {@link MORPHISM_LINT_CODES.DUPLICATE_OTHER_ID} — `otherAssetIds` contains a duplicate id.
52
+ * - {@link MORPHISM_LINT_CODES.SELF_COMPOSE} — `otherAssetIds` includes the source `assetId`.
53
+ * - {@link MORPHISM_LINT_CODES.OTHER_IDS_IGNORED} — `otherAssetIds` is non-empty but `kind` is not
54
+ * `COMPOSE`/`POOL` (the field is ignored for other kinds — a likely authoring mistake).
55
+ *
56
+ * WARNINGS (heuristic — holder/ownership unknown at build time):
57
+ * - {@link MORPHISM_LINT_CODES.COMPOSE_NO_NONCE} — a `COMPOSE`/`POOL` with a non-empty
58
+ * `otherAssetIds` and NO `nonce`. IF any counter-party is not signer-owned the chain rejects it
59
+ * (C2); attach the reveal half of an {@link AuthorizeCompose} handshake as `nonce`. A same-holder
60
+ * compose (all parts signer-owned) is legitimately nonce-less — hence a warning, not an error.
61
+ */
62
+ function lintApplyMorphism(msg) {
63
+ const out = [];
64
+ const ids = msg.otherAssetIds ?? [];
65
+ const isComposing = COMPOSING_KINDS.has(msg.kind);
66
+ // ---- error: duplicate ids in otherAssetIds ---------------------------------
67
+ // One finding per DISTINCT duplicated id (path points at its first repeat occurrence).
68
+ const seen = new Set();
69
+ const dupReported = new Set();
70
+ ids.forEach((id, i) => {
71
+ if (seen.has(id) && !dupReported.has(id)) {
72
+ dupReported.add(id);
73
+ out.push(finding('error', exports.MORPHISM_LINT_CODES.DUPLICATE_OTHER_ID, `otherAssetIds contains duplicate id "${id}". The chain rejects a Compose/Pool whose ` +
74
+ `otherAssetIds has duplicates (the old inflation path). List each counter-party asset once.`, `otherAssetIds[${i}]`));
75
+ }
76
+ seen.add(id);
77
+ });
78
+ // ---- error: otherAssetIds includes the source assetId (self-composition) ----
79
+ const selfIdx = ids.indexOf(msg.assetId);
80
+ if (selfIdx !== -1) {
81
+ out.push(finding('error', exports.MORPHISM_LINT_CODES.SELF_COMPOSE, `otherAssetIds includes the source assetId "${msg.assetId}" (self-composition). The chain ` +
82
+ `rejects this unconditionally. otherAssetIds must list only the OTHER parts folded into the composite.`, `otherAssetIds[${selfIdx}]`));
83
+ }
84
+ // ---- error: otherAssetIds set but kind ignores it ---------------------------
85
+ if (ids.length > 0 && !isComposing) {
86
+ out.push(finding('error', exports.MORPHISM_LINT_CODES.OTHER_IDS_IGNORED, `otherAssetIds is non-empty but kind is "${msg.kind}", which ignores otherAssetIds entirely ` +
87
+ `(only COMPOSE/POOL fold in counter-party parts). This is almost certainly a mistake — set ` +
88
+ `kind to COMPOSE/POOL, or drop otherAssetIds.`, 'otherAssetIds'));
89
+ }
90
+ // ---- warn: composing with counter-parties but no consent nonce --------------
91
+ // nonce === 0 is a valid live nonce; only an OMITTED nonce (undefined/null) triggers the warning.
92
+ if (isComposing && ids.length > 0 && msg.nonce == null) {
93
+ out.push(finding('warn', exports.MORPHISM_LINT_CODES.COMPOSE_NO_NONCE, `${msg.kind} folds in otherAssetIds but carries no consent nonce. If ANY counter-party asset ` +
94
+ `is not held by the signer, the chain REJECTS this (C2): attach the reveal half of an ` +
95
+ `AuthorizeCompose handshake as \`nonce\`. A same-holder compose (all parts signer-owned) ` +
96
+ `needs none, so this is advisory.`, 'nonce'));
97
+ }
98
+ return out;
99
+ }
@@ -0,0 +1,118 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.commitKeyPath = commitKeyPath;
4
+ exports.verifyMptInclusion = verifyMptInclusion;
5
+ exports.verifyStateProof = verifyStateProof;
6
+ /**
7
+ * Light-client verification of committed state proofs.
8
+ *
9
+ * The chain's `/state-machines|scripts|assets/{id}/state-proof` endpoints return a
10
+ * {@link StateProof}: the record plus a Merkle-Patricia inclusion proof against `mptRoot`,
11
+ * whose combined hash IS the snapshot's consensus-signed `calculatedStateProof`. Verifying
12
+ * the proof locally means trusting NO serving node — only snapshot consensus.
13
+ *
14
+ * The fold mirrors metakit's `MerklePatriciaVerifier` byte-for-byte (validated against live
15
+ * cluster proofs):
16
+ *
17
+ * - the response `witness` is LEAF-FIRST; verification walks it reversed (root-first);
18
+ * - every node's digest = `sha256(typePrefix ++ utf8(JCS(dropNulls(contents))))` over the
19
+ * commitment's SUBTYPE (contents-only) encoding — prefixes: leaf `0x00`, branch `0x01`,
20
+ * extension `0x02`;
21
+ * - a Branch consumes one path nibble via `pathsDigest`; an Extension consumes its `shared`
22
+ * nibbles and continues at `childDigest`;
23
+ * - the terminal Leaf must match the remaining path (`remaining`) and bind
24
+ * `dataDigest == sha256(JCS(dropNulls(record)))` (no prefix);
25
+ * - the trie path of a committed key is `hex(utf8(key))` (metakit `CommitKey.toHex`), e.g.
26
+ * `fiber/<uuid>` → `66696265722f…`.
27
+ *
28
+ * NOTE: this intentionally does NOT route through the JLVM `mpt_verify` opcode — the
29
+ * TypeScript port of that opcode mis-canonicalizes non-scalar leaf values (fiber records)
30
+ * and returns false; its conformance vectors only cover scalar leaves. The Scala (chain)
31
+ * side is correct. Once the port is fixed upstream this helper stays valid either way: it
32
+ * is the same algorithm without the JsonLogic value-domain round-trip.
33
+ */
34
+ const metagraph_sdk_1 = require("@constellation-network/metagraph-sdk");
35
+ const sha256_js_1 = require("@noble/hashes/sha256.js");
36
+ const utf8 = (s) => new TextEncoder().encode(s);
37
+ const toHexStr = (b) => Array.from(b, (x) => x.toString(16).padStart(2, '0')).join('');
38
+ /** `sha256(prefixByte? ++ utf8(JCS(dropNulls(value))))` — the chain's content-digest rule. */
39
+ function jcsSha256(value, prefix) {
40
+ const canonical = utf8((0, metagraph_sdk_1.canonicalize)(value));
41
+ if (prefix === undefined)
42
+ return toHexStr((0, sha256_js_1.sha256)(canonical));
43
+ const framed = new Uint8Array(1 + canonical.length);
44
+ framed[0] = prefix;
45
+ framed.set(canonical, 1);
46
+ return toHexStr((0, sha256_js_1.sha256)(framed));
47
+ }
48
+ /** The MPT trie path of a committed key — `hex(utf8(key))` (metakit `CommitKey.toHex`). */
49
+ function commitKeyPath(key) {
50
+ return toHexStr(utf8(key));
51
+ }
52
+ const NODE_PREFIX = { Leaf: 0, Branch: 1, Extension: 2 };
53
+ /**
54
+ * Verify a Merkle-Patricia inclusion proof: `record` is committed at `key` under `mptRoot`.
55
+ *
56
+ * Pure and side-effect free; never throws on malformed input (returns `{ ok: false }`).
57
+ */
58
+ function verifyMptInclusion(mptRoot, key, record, proof) {
59
+ const fail = (reason) => ({ ok: false, reason });
60
+ if (!proof || typeof proof.path !== 'string' || !Array.isArray(proof.witness) || proof.witness.length === 0)
61
+ return fail('malformed proof (missing path/witness)');
62
+ const expectedPath = commitKeyPath(key);
63
+ if (proof.path.toLowerCase() !== expectedPath)
64
+ return fail(`trie path != hex(utf8("${key}"))`);
65
+ let digest = String(mptRoot).replace(/^0x/, '').toLowerCase();
66
+ let path = proof.path.toLowerCase();
67
+ const nodes = [...proof.witness].reverse(); // response is leaf-first; fold root-first
68
+ for (const [depth, node] of nodes.entries()) {
69
+ const isLast = depth === nodes.length - 1;
70
+ const prefix = NODE_PREFIX[node?.type];
71
+ if (prefix === undefined)
72
+ return fail(`unknown witness node type '${String(node?.type)}'`);
73
+ if (jcsSha256(node.contents, prefix) !== digest)
74
+ return fail(`${node.type.toLowerCase()} commitment mismatch at depth ${depth}`);
75
+ if (node.type === 'Branch') {
76
+ const child = node.contents.pathsDigest?.[path[0]];
77
+ if (!child)
78
+ return fail(`branch has no child at nibble '${path[0]}' (depth ${depth})`);
79
+ digest = child.toLowerCase();
80
+ path = path.slice(1);
81
+ }
82
+ else if (node.type === 'Extension') {
83
+ const shared = String(node.contents.shared ?? '');
84
+ if (!path.startsWith(shared.toLowerCase()))
85
+ return fail(`extension shared-nibble mismatch at depth ${depth}`);
86
+ path = path.slice(shared.length);
87
+ digest = String(node.contents.childDigest ?? '').toLowerCase();
88
+ }
89
+ else {
90
+ // Leaf
91
+ if (!isLast)
92
+ return fail('leaf before end of witness');
93
+ if (String(node.contents.remaining ?? '').toLowerCase() !== path)
94
+ return fail('leaf remaining-path mismatch');
95
+ if (String(node.contents.dataDigest ?? '').toLowerCase() !== jcsSha256(record))
96
+ return fail('leaf dataDigest != sha256(JCS(record))');
97
+ return { ok: true };
98
+ }
99
+ }
100
+ return fail('witness does not terminate in a leaf');
101
+ }
102
+ /**
103
+ * Verify a whole {@link StateProof} response for a committed key.
104
+ *
105
+ * Checks the trie inclusion of `record` at `key` under the response's `mptRoot` (see
106
+ * {@link verifyMptInclusion}). The caller supplies the key it EXPECTS (`fiber/<id>`,
107
+ * `script/<id>`, `asset/<id>`, …) so a proof for some other record cannot be substituted;
108
+ * the response's own `key` field must agree.
109
+ *
110
+ * Trust anchor: `committedRoot` (= `sha256(mptRoot ++ catalogRoot)`) rides in the
111
+ * consensus-signed snapshot as `calculatedStateProof`; binding `mptRoot` to a snapshot is
112
+ * the caller's (or a snapshot-following light client's) responsibility.
113
+ */
114
+ function verifyStateProof(resp, expectedKey) {
115
+ if (resp.key !== expectedKey)
116
+ return { ok: false, reason: `response key '${resp.key}' != expected '${expectedKey}'` };
117
+ return verifyMptInclusion(String(resp.mptRoot), expectedKey, resp.record, resp.proof);
118
+ }
@@ -166,7 +166,13 @@ function createAssetPolicyPayload(params) {
166
166
  function createMintAssetPayload(params) {
167
167
  return { MintAsset: params };
168
168
  }
169
- /** Wrap an {@link ApplyMorphism} (apply a typed morphism to an asset instance). */
169
+ /**
170
+ * Wrap an {@link ApplyMorphism} (apply a typed morphism to an asset instance).
171
+ *
172
+ * @see lintApplyMorphism (`./morphism-lint.ts`) — pure, non-throwing advisory that flags the
173
+ * Compose/Pool mistakes the chain rejects (duplicate / self-referential `otherAssetIds`; a
174
+ * cross-holder compose with no consent `nonce`). Run it on `params` before signing.
175
+ */
170
176
  function createApplyMorphismPayload(params) {
171
177
  return { ApplyMorphism: params };
172
178
  }
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ /**
3
+ * Webhook notification PAYLOAD types — the server-initiated push the chain POSTs to a subscribed
4
+ * callback, NOT part of the client's request surface.
5
+ *
6
+ * These are hand-authored (not generated from the OpenAPI contract) because the push is an OUTBOUND
7
+ * POST from the metagraph to the subscriber's callback URL — it is not an endpoint the chain's tapir
8
+ * `ApiEndpoints` serves, so it never appears in `openapi/ottochain-openapi-ml0.json`. They mirror the
9
+ * chain's shared network DTOs byte-for-byte:
10
+ *
11
+ * chain: modules/models/src/main/scala/xyz/kd5ujc/schema/api/webhooks/Subscriber.scala
12
+ * (SnapshotNotification / NotificationStats / SnapshotRejection)
13
+ * emit: modules/l0/.../webhooks/WebhookDispatcher.scala — `notification.asJson.noSpaces`
14
+ *
15
+ * Field-shape notes tied to the chain source:
16
+ * - The dispatcher serializes with a plain derived encoder (`.asJson`, NO `dropNullValues`), so an
17
+ * `Option[Long]` that is `None` is emitted as an explicit `null` — the KEY is always present, the
18
+ * VALUE is nullable. Hence `number | null` (a required key), not `number | undefined` (an optional
19
+ * key). Consumers must handle `null`.
20
+ * - `Long` → JSON number; `Instant` → ISO-8601 string; `UUID` → string.
21
+ * - The dispatcher only ever emits `"snapshot.finalized"` today (there is a dead, never-dispatched
22
+ * `RejectionNotification`/`transaction.rejected` type in the chain models — deliberately NOT mirrored
23
+ * here, because the SDK should model only what is actually sent). Rejections now ride the finalized
24
+ * snapshot's `rejections[]`, drained from its committed `RejectionReceipt`s.
25
+ *
26
+ * @see modules/models/.../schema/api/webhooks/Subscriber.scala
27
+ * @see modules/l0/.../webhooks/WebhookDispatcher.scala
28
+ * @packageDocumentation
29
+ */
30
+ Object.defineProperty(exports, "__esModule", { value: true });
31
+ exports.SNAPSHOT_FINALIZED_EVENT = void 0;
32
+ /** The only event the chain's `WebhookDispatcher` currently emits. */
33
+ exports.SNAPSHOT_FINALIZED_EVENT = 'snapshot.finalized';
@@ -121,11 +121,20 @@ exports.triggers = triggers;
121
121
  * MUST be a literal {@link ProtoStateMachineDefinition} (e.g. a nested `machine().wireDefinition()` /
122
122
  * `toProtoDefinition(child)` output) — NOT an expression the engine would evaluate at runtime.
123
123
  *
124
- * F8 gotcha — `owners` is load-bearing. A spawned child's transitions are gated by
125
- * `owners authorizedSigners` (`riverdale-economy/README.md`), so EVERY party that will later drive
126
- * the child (e.g. every bidder on a spawned auction) MUST be listed in `owners`, or their events are
127
- * rejected. `owners` may be a literal id array or an expression (e.g. `{ var: "event.auctionOwners" }`);
128
- * `childId` / `initialData` likewise accept literals or expressions.
124
+ * H1 constraintchild `owners` MUST be a SUBSET of the SPAWNING PARENT fiber's `owners`. As of the
125
+ * fiber-engine permissionless-hardening (chain branch `fix/fiber-engine-permissionless-hardening`, audit
126
+ * finding H1) the chain FAILS-CLOSED it aborts the whole transition under EVERY `spawnOwnerPolicy` if
127
+ * a `_spawn`'s resolved child `owners` are not the parent's owners. Assigning arbitrary, unsigned owners
128
+ * (the old "auction owned by all its bidders" pattern, where the bidders are NOT parent owners) is no
129
+ * longer valid: those events are not silently rejected, the spawn itself is rejected.
130
+ *
131
+ * So DO NOT try to enroll later participants by listing them here. A spawned child's transitions are gated
132
+ * by `owners ∪ authorizedSigners` for the DIRECT (wallet-signed) path; admit additional drivers through
133
+ * the child's OWN transitions (`authorizedSigners`), or drive it via the cascade (`_triggers`) subject to
134
+ * its `acceptedCallers` — never by widening `owners` beyond the parent's set. `owners` may be a literal id
135
+ * array or an expression (e.g. `{ var: "state.owners" }` — resolve it WITHIN the parent's owners), but a
136
+ * value the parent does not own (e.g. `{ var: "event.auctionOwners" }` supplied by the caller) will be
137
+ * rejected on-chain. `childId` / `initialData` likewise accept literals or expressions.
129
138
  */
130
139
  const spawn = (ds) => ({ _spawn: ds });
131
140
  exports.spawn = spawn;
@@ -33,6 +33,11 @@
33
33
  *
34
34
  * leading-dot — `{"var":".foo"}` resolves to null on chain.
35
35
  *
36
+ * H1 — a `_spawn` whose child `owners` are not provably a SUBSET of the spawning
37
+ * parent's owners. The chain now fails-closed (aborts the transition) under
38
+ * every spawnOwnerPolicy when child owners ⊄ parent owners; this advisory
39
+ * flags the strong not-subset signals (hardcoded / `event.*`-derived owners).
40
+ *
36
41
  * This is a STANDALONE validator. It is deliberately NOT wired into
37
42
  * `defineFiberApp` / `toProtoDefinition`: doing so would break the build until
38
43
  * every app is remediated. Run it via `scripts/lint-apps.mjs`.
@@ -56,6 +61,7 @@ exports.LINT_CODES = {
56
61
  WITNESS_IN_TRANSITION: 'witness-in-transition', // rule 3
57
62
  DROPPED_DIRECTIVE: 'dropped-directive', // rule 4 (A3)
58
63
  LEADING_DOT_VAR: 'leading-dot-var', // rule 5
64
+ SPAWN_OWNERS: 'spawn-owners', // rule 6 (H1) — child owners not provably ⊆ parent
59
65
  };
60
66
  // =============================================================================
61
67
  // Rule data
@@ -293,6 +299,101 @@ function lintTransitionStructure(t, app, transition, path) {
293
299
  return out;
294
300
  }
295
301
  // =============================================================================
302
+ // _spawn owner-subset rule (rule 6 / H1)
303
+ // =============================================================================
304
+ /** Collect every string `var` reference (path or `[path, default]` head) at or below `node`. */
305
+ function collectVarRefs(node, acc) {
306
+ if (Array.isArray(node)) {
307
+ node.forEach((child) => collectVarRefs(child, acc));
308
+ return;
309
+ }
310
+ if (node === null || typeof node !== 'object')
311
+ return;
312
+ const obj = node;
313
+ const keys = Object.keys(obj);
314
+ if (keys.length === 1 && keys[0] === 'var') {
315
+ const ref = obj.var;
316
+ if (typeof ref === 'string')
317
+ acc.push(ref);
318
+ else if (Array.isArray(ref)) {
319
+ if (typeof ref[0] === 'string')
320
+ acc.push(ref[0]);
321
+ if (ref.length > 1)
322
+ collectVarRefs(ref[1], acc);
323
+ }
324
+ return;
325
+ }
326
+ for (const k of keys)
327
+ collectVarRefs(obj[k], acc);
328
+ }
329
+ /**
330
+ * Classify a `_spawn` child `owners` expression for the H1 subset risk. Returns a short reason string when
331
+ * the owners are NOT provably ⊆ the parent's owners from the definition alone, else `undefined`.
332
+ *
333
+ * The linter cannot see the parent fiber's runtime `owners` (they are set at `create()` time, not in the
334
+ * definition), so it flags only the two STRONG "not-subset" signals — keeping false positives low:
335
+ * - a literal array carrying ≥1 concrete address string (a hardcoded external owner list); and
336
+ * - any `event.*` reference (caller-supplied — the exact H1 owner-forgery vector).
337
+ * Owners derived purely from the parent's own `state.*` / `machineId` / `$`-context are assumed in-set and
338
+ * are NOT flagged.
339
+ */
340
+ function spawnOwnersRisk(owners) {
341
+ if (Array.isArray(owners) && owners.some((o) => typeof o === 'string' && o.length > 0)) {
342
+ return 'a literal array with hardcoded owner address(es)';
343
+ }
344
+ const refs = [];
345
+ collectVarRefs(owners, refs);
346
+ if (refs.some((r) => r === 'event' || r.startsWith('event.'))) {
347
+ return "an 'event' reference (caller-supplied owners)";
348
+ }
349
+ return undefined;
350
+ }
351
+ /**
352
+ * rule 6 (H1) — scan a transition effect for `_spawn` directives whose child `owners` are not provably a
353
+ * SUBSET of the spawning parent's owners. The chain now fails-closed (aborts the transition) under every
354
+ * `spawnOwnerPolicy` when a child's resolved owners are not ⊆ the parent's. Advisory (warn): the linter
355
+ * cannot prove the subset statically, so it flags only the strong not-subset signals (see
356
+ * {@link spawnOwnersRisk}).
357
+ */
358
+ function lintSpawnOwners(node, app, transition, path) {
359
+ const out = [];
360
+ if (Array.isArray(node)) {
361
+ node.forEach((child, i) => out.push(...lintSpawnOwners(child, app, transition, `${path}[${i}]`)));
362
+ return out;
363
+ }
364
+ if (node === null || typeof node !== 'object')
365
+ return out;
366
+ const obj = node;
367
+ for (const [k, v] of Object.entries(obj)) {
368
+ if (k === '_spawn' && Array.isArray(v)) {
369
+ v.forEach((entry, i) => {
370
+ if (entry && typeof entry === 'object' && !Array.isArray(entry) && 'owners' in entry) {
371
+ const reason = spawnOwnersRisk(entry.owners);
372
+ if (reason) {
373
+ out.push({
374
+ app,
375
+ transition,
376
+ severity: 'warn',
377
+ code: exports.LINT_CODES.SPAWN_OWNERS,
378
+ message: `_spawn child 'owners' must be a SUBSET of the spawning parent fiber's owners — the chain ` +
379
+ `now FAILS-CLOSED (aborts the transition) under every spawnOwnerPolicy if they are not ⊆ the ` +
380
+ `parent's (audit H1). This 'owners' is ${reason} and cannot be proven ⊆ the parent. Admit ` +
381
+ `later participants via the child's own transitions / authorizedSigners / acceptedCallers ` +
382
+ `instead of listing non-parent owners.`,
383
+ path: `${path}.${k}[${i}].owners`,
384
+ });
385
+ }
386
+ }
387
+ });
388
+ // Do not descend into the spawn entry (its nested child definition's owners are relative to that
389
+ // child, not to this parent).
390
+ continue;
391
+ }
392
+ out.push(...lintSpawnOwners(v, app, transition, `${path}.${k}`));
393
+ }
394
+ return out;
395
+ }
396
+ // =============================================================================
296
397
  // Top-level entry point
297
398
  // =============================================================================
298
399
  function appLabel(def) {
@@ -323,6 +424,7 @@ function lintFiberApp(def) {
323
424
  }
324
425
  if (t.effect !== undefined) {
325
426
  out.push(...lintGuardExpression(t.effect, ctx, `${tPath}.effect`));
427
+ out.push(...lintSpawnOwners(t.effect, app, transition, `${tPath}.effect`));
326
428
  }
327
429
  out.push(...lintTransitionStructure(t, app, transition, tPath));
328
430
  });
@@ -96,6 +96,11 @@ export function createMintAssetPayload(params) {
96
96
  /**
97
97
  * Apply a typed morphism (Transfer / Burn / Compose / ...). asset-model.md §7.
98
98
  * Transfer = `{ kind: "Transfer", recipient }`; Burn (repay) = `{ kind: "Burn" }`.
99
+ *
100
+ * C2 — a `Compose`/`Pool` that folds in a counter-party the signer does NOT own is now rejected on-chain
101
+ * unless a live `AuthorizeCompose` nonce authorizes it: pass that nonce here (`nonce`) and build the commit
102
+ * half with {@link createAuthorizeComposePayload}. A same-holder compose (all parts signer-owned) needs no
103
+ * nonce. `otherAssets` must be duplicate-free and must not include `assetId`.
99
104
  */
100
105
  export function createApplyMorphismPayload(params) {
101
106
  return {
@@ -112,7 +117,12 @@ export function createApplyMorphismPayload(params) {
112
117
  },
113
118
  };
114
119
  }
115
- /** Commit half of a two-party compose. asset-model.md §7/§8. */
120
+ /**
121
+ * Commit half of a two-party (cross-holder) compose. asset-model.md §7/§8. As of chain finding C2 this
122
+ * handshake is MANDATORY: a cross-holder `Compose`/`Pool` is rejected unless a matching, unexpired nonce
123
+ * from this call is live for the counter-party. The composing party then echoes `nonce` in its
124
+ * `ApplyMorphism`. A same-holder compose (all parts owned by the signer) does not need this.
125
+ */
116
126
  export function createAuthorizeComposePayload(params) {
117
127
  return {
118
128
  AuthorizeCompose: {
package/dist/esm/index.js CHANGED
@@ -34,6 +34,8 @@ export * from './types.js';
34
34
  // ─── OttoChain-specific transaction helpers ───────────────────────────────────
35
35
  export * from './ottochain/transaction.js';
36
36
  export { dropNulls } from './ottochain/drop-nulls.js';
37
+ // Message-layer advisory lint for ApplyMorphism (Compose/Pool consent + inflation checks).
38
+ export { lintApplyMorphism, MORPHISM_LINT_CODES } from './ottochain/morphism-lint.js';
37
39
  // ─── Genesis manifest exporter (std-app pre-registration content) ─────────────
38
40
  export { buildGenesisManifest, GENESIS_MANIFEST_VERSION } from './ottochain/genesis-manifest.js';
39
41
  // ─── Generated protobuf types (canonical definitions) ────────────────────────
@@ -48,6 +50,7 @@ DagAddressSchema, PrivateKeySchema, PublicKeySchema, KeyPairSchema, SignaturePro
48
50
  validate, validatePrivateKey, validatePublicKey, validateAddress, validateKeyPair, safeParse, assert, } from './validation.js';
49
51
  // ─── OttoChain metagraph client ───────────────────────────────────────────────
50
52
  export { MetagraphClient as OttoMetagraphClient } from './ottochain/metagraph-client.js';
53
+ export { SNAPSHOT_FINALIZED_EVENT } from './ottochain/webhook-notifications.js';
51
54
  // Proto definition helper
52
55
  export { toProtoDefinition, defineFiberApp,
53
56
  // Fiber policy (the fiber constitution): mirrors the chain ADT
@@ -11,8 +11,12 @@ export * as proto from '../generated/index.js';
11
11
  export { OTTOCHAIN_MESSAGE_TYPES, TOKEN_BEHAVIOR_BITS } from './types.js';
12
12
  export { decodeOnChainState, getSnapshotOnChainState, getLatestOnChainState, getLogsForFiber, getEventReceipts, getScriptInvocations, extractOnChainState, } from './snapshot.js';
13
13
  export { MetagraphClient } from './metagraph-client.js';
14
+ export { verifyStateProof, verifyMptInclusion, commitKeyPath } from './state-proof.js';
15
+ export { SNAPSHOT_FINALIZED_EVENT } from './webhook-notifications.js';
14
16
  // Transaction helpers (state machine payloads, signing)
15
17
  export { createTransitionPayload, createArchivePayload, createInvokeScriptPayload, signTransaction, addTransactionSignature, getPublicKeyForRegistration, createStateMachinePayload, createScriptPayload, createDataTransactionRequest, createAssetPolicyPayload, createMintAssetPayload, createApplyMorphismPayload, createAuthorizeComposePayload, } from './transaction.js';
18
+ // Message-layer advisory lint for ApplyMorphism (Compose/Pool C2 mistakes the chain rejects).
19
+ export { lintApplyMorphism, MORPHISM_LINT_CODES } from './morphism-lint.js';
16
20
  // Data utilities
17
21
  export { dropNulls } from './drop-nulls.js';
18
22
  // Genesis manifest exporter (std-app pre-registration content)