@ottochain/sdk 2.2.5 → 2.4.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 (222) hide show
  1. package/dist/cjs/apps/contracts/index.js +15 -8
  2. package/dist/cjs/apps/contracts/state-machines/contract-agreement.js +89 -46
  3. package/dist/cjs/apps/contracts/state-machines/contract-escrow.js +152 -45
  4. package/dist/cjs/apps/contracts/state-machines/contract-universal.js +40 -8
  5. package/dist/cjs/apps/corporate/index.js +2 -2
  6. package/dist/cjs/apps/corporate/state-machines/corp-board.js +169 -59
  7. package/dist/cjs/apps/corporate/state-machines/corp-entity.js +245 -47
  8. package/dist/cjs/apps/corporate/state-machines/corp-securities.js +407 -99
  9. package/dist/cjs/apps/corporate/state-machines/corp-shareholders.js +151 -37
  10. package/dist/cjs/apps/governance/index.js +14 -14
  11. package/dist/cjs/apps/governance/state-machines/dao-multisig.js +85 -43
  12. package/dist/cjs/apps/governance/state-machines/dao-reputation.js +73 -43
  13. package/dist/cjs/apps/governance/state-machines/dao-single.js +55 -14
  14. package/dist/cjs/apps/governance/state-machines/dao-token.js +103 -45
  15. package/dist/cjs/apps/governance/state-machines/governance-simple.js +129 -65
  16. package/dist/cjs/apps/governance/state-machines/governance-universal.js +29 -5
  17. package/dist/cjs/apps/identity/constants.js +55 -14
  18. package/dist/cjs/apps/identity/index.js +27 -11
  19. package/dist/cjs/apps/identity/state-machines/identity-agent.js +57 -9
  20. package/dist/cjs/apps/identity/state-machines/identity-oracle.js +70 -27
  21. package/dist/cjs/apps/identity/state-machines/identity-registry.js +288 -0
  22. package/dist/cjs/apps/identity/state-machines/identity-universal.js +30 -6
  23. package/dist/cjs/apps/identity/state-machines/index.js +3 -1
  24. package/dist/cjs/apps/index.js +19 -8
  25. package/dist/cjs/apps/lending/assets.js +223 -0
  26. package/dist/cjs/apps/lending/credit-scoring.js +87 -0
  27. package/dist/cjs/apps/lending/eligibility.js +121 -0
  28. package/dist/cjs/apps/lending/index.js +91 -0
  29. package/dist/cjs/apps/lending/state-machines/index.js +9 -0
  30. package/dist/cjs/apps/lending/state-machines/lending-zk-loan.js +412 -0
  31. package/dist/cjs/apps/markets/index.js +21 -11
  32. package/dist/cjs/apps/markets/state-machines/market-auction.js +44 -19
  33. package/dist/cjs/apps/markets/state-machines/market-crowdfund.js +54 -29
  34. package/dist/cjs/apps/markets/state-machines/market-group-buy.js +53 -31
  35. package/dist/cjs/apps/markets/state-machines/market-prediction.js +120 -61
  36. package/dist/cjs/apps/markets/state-machines/market-universal.js +50 -10
  37. package/dist/cjs/apps/oracles/index.js +2 -2
  38. package/dist/cjs/errors.js +3 -3
  39. package/dist/cjs/generated/google/protobuf/struct.js +4 -4
  40. package/dist/cjs/generated/index.js +11 -7
  41. package/dist/cjs/generated/openapi.js +6 -0
  42. package/dist/cjs/generated/ottochain/apps/contracts/v1/contract.js +50 -50
  43. package/dist/cjs/generated/ottochain/apps/corporate/v1/corporate.js +19 -20
  44. package/dist/cjs/generated/ottochain/apps/governance/v1/governance.js +13 -13
  45. package/dist/cjs/generated/ottochain/apps/identity/v1/attestation.js +4 -4
  46. package/dist/cjs/generated/ottochain/apps/identity/v1/identity.js +124 -130
  47. package/dist/cjs/generated/ottochain/apps/markets/v1/market.js +107 -107
  48. package/dist/cjs/generated/ottochain/v1/common.js +1722 -1
  49. package/dist/cjs/generated/ottochain/v1/fiber.js +355 -5
  50. package/dist/cjs/generated/ottochain/v1/messages.js +775 -3
  51. package/dist/cjs/generated/ottochain/v1/records.js +350 -3
  52. package/dist/cjs/index.js +45 -7
  53. package/dist/cjs/openapi.js +2 -0
  54. package/dist/cjs/ottochain/drop-nulls.js +1 -2
  55. package/dist/cjs/ottochain/genesis-manifest.js +193 -0
  56. package/dist/cjs/ottochain/index.js +27 -13
  57. package/dist/cjs/ottochain/metagraph-client.js +65 -8
  58. package/dist/cjs/ottochain/snapshot.js +11 -12
  59. package/dist/cjs/ottochain/transaction.js +57 -14
  60. package/dist/cjs/ottochain/types.js +19 -1
  61. package/dist/cjs/privacy/index.js +14 -0
  62. package/dist/cjs/privacy/sealed-bid.js +210 -0
  63. package/dist/cjs/privacy/shield-app.js +131 -0
  64. package/dist/cjs/schema/effects.js +40 -0
  65. package/dist/cjs/schema/fiber-app.js +19 -13
  66. package/dist/cjs/schema/guard-lint.js +352 -0
  67. package/dist/cjs/schema/guards.js +246 -0
  68. package/dist/cjs/signing.js +80 -0
  69. package/dist/cjs/types.js +2 -3
  70. package/dist/cjs/validation.js +8 -8
  71. package/dist/cjs/verify.js +9 -4
  72. package/dist/cjs/zk/commit.js +53 -0
  73. package/dist/cjs/zk/guard.js +107 -0
  74. package/dist/cjs/zk/index.js +48 -0
  75. package/dist/cjs/zk/preimage.js +37 -0
  76. package/dist/cjs/zk/prover.js +87 -0
  77. package/dist/cjs/zk/registry.js +62 -0
  78. package/dist/cjs/zk/types.js +28 -0
  79. package/dist/cjs/zk/witness.js +19 -0
  80. package/dist/esm/apps/contracts/index.js +7 -3
  81. package/dist/esm/apps/contracts/state-machines/contract-agreement.js +89 -46
  82. package/dist/esm/apps/contracts/state-machines/contract-escrow.js +152 -45
  83. package/dist/esm/apps/contracts/state-machines/contract-universal.js +40 -8
  84. package/dist/esm/apps/corporate/state-machines/corp-board.js +169 -59
  85. package/dist/esm/apps/corporate/state-machines/corp-entity.js +245 -47
  86. package/dist/esm/apps/corporate/state-machines/corp-securities.js +407 -99
  87. package/dist/esm/apps/corporate/state-machines/corp-shareholders.js +151 -37
  88. package/dist/esm/apps/governance/state-machines/dao-multisig.js +85 -43
  89. package/dist/esm/apps/governance/state-machines/dao-reputation.js +73 -43
  90. package/dist/esm/apps/governance/state-machines/dao-single.js +55 -14
  91. package/dist/esm/apps/governance/state-machines/dao-token.js +103 -45
  92. package/dist/esm/apps/governance/state-machines/governance-simple.js +129 -65
  93. package/dist/esm/apps/governance/state-machines/governance-universal.js +29 -5
  94. package/dist/esm/apps/identity/constants.js +51 -12
  95. package/dist/esm/apps/identity/index.js +12 -7
  96. package/dist/esm/apps/identity/state-machines/identity-agent.js +57 -9
  97. package/dist/esm/apps/identity/state-machines/identity-oracle.js +70 -27
  98. package/dist/esm/apps/identity/state-machines/identity-registry.js +285 -0
  99. package/dist/esm/apps/identity/state-machines/identity-universal.js +30 -6
  100. package/dist/esm/apps/identity/state-machines/index.js +1 -0
  101. package/dist/esm/apps/index.js +1 -0
  102. package/dist/esm/apps/lending/assets.js +207 -0
  103. package/dist/esm/apps/lending/credit-scoring.js +81 -0
  104. package/dist/esm/apps/lending/eligibility.js +115 -0
  105. package/dist/esm/apps/lending/index.js +69 -0
  106. package/dist/esm/apps/lending/state-machines/index.js +5 -0
  107. package/dist/esm/apps/lending/state-machines/lending-zk-loan.js +409 -0
  108. package/dist/esm/apps/markets/index.js +8 -4
  109. package/dist/esm/apps/markets/state-machines/market-auction.js +44 -19
  110. package/dist/esm/apps/markets/state-machines/market-crowdfund.js +54 -29
  111. package/dist/esm/apps/markets/state-machines/market-group-buy.js +53 -31
  112. package/dist/esm/apps/markets/state-machines/market-prediction.js +120 -61
  113. package/dist/esm/apps/markets/state-machines/market-universal.js +50 -10
  114. package/dist/esm/generated/index.js +9 -5
  115. package/dist/esm/generated/openapi.js +5 -0
  116. package/dist/esm/generated/ottochain/apps/contracts/v1/contract.js +46 -46
  117. package/dist/esm/generated/ottochain/apps/identity/v1/identity.js +114 -120
  118. package/dist/esm/generated/ottochain/apps/markets/v1/market.js +100 -100
  119. package/dist/esm/generated/ottochain/v1/common.js +1718 -0
  120. package/dist/esm/generated/ottochain/v1/fiber.js +351 -1
  121. package/dist/esm/generated/ottochain/v1/messages.js +774 -2
  122. package/dist/esm/generated/ottochain/v1/records.js +349 -2
  123. package/dist/esm/index.js +23 -2
  124. package/dist/esm/openapi.js +1 -0
  125. package/dist/esm/ottochain/genesis-manifest.js +189 -0
  126. package/dist/esm/ottochain/index.js +4 -3
  127. package/dist/esm/ottochain/metagraph-client.js +65 -8
  128. package/dist/esm/ottochain/snapshot.js +4 -4
  129. package/dist/esm/ottochain/transaction.js +43 -3
  130. package/dist/esm/ottochain/types.js +18 -0
  131. package/dist/esm/privacy/index.js +6 -0
  132. package/dist/esm/privacy/sealed-bid.js +206 -0
  133. package/dist/esm/privacy/shield-app.js +127 -0
  134. package/dist/esm/schema/effects.js +35 -0
  135. package/dist/esm/schema/fiber-app.js +13 -6
  136. package/dist/esm/schema/guard-lint.js +346 -0
  137. package/dist/esm/schema/guards.js +229 -0
  138. package/dist/esm/signing.js +74 -0
  139. package/dist/esm/verify.js +8 -2
  140. package/dist/esm/zk/commit.js +44 -0
  141. package/dist/esm/zk/guard.js +99 -0
  142. package/dist/esm/zk/index.js +19 -0
  143. package/dist/esm/zk/preimage.js +30 -0
  144. package/dist/esm/zk/prover.js +82 -0
  145. package/dist/esm/zk/registry.js +55 -0
  146. package/dist/esm/zk/types.js +25 -0
  147. package/dist/esm/zk/witness.js +15 -0
  148. package/dist/types/apps/contracts/index.d.ts +159 -227
  149. package/dist/types/apps/contracts/state-machines/contract-agreement.d.ts +50 -101
  150. package/dist/types/apps/contracts/state-machines/contract-escrow.d.ts +86 -120
  151. package/dist/types/apps/contracts/state-machines/contract-universal.d.ts +20 -4
  152. package/dist/types/apps/corporate/index.d.ts +1040 -304
  153. package/dist/types/apps/corporate/state-machines/corp-board.d.ts +188 -52
  154. package/dist/types/apps/corporate/state-machines/corp-entity.d.ts +267 -62
  155. package/dist/types/apps/corporate/state-machines/corp-securities.d.ts +355 -131
  156. package/dist/types/apps/corporate/state-machines/corp-shareholders.d.ts +225 -53
  157. package/dist/types/apps/governance/index.d.ts +396 -417
  158. package/dist/types/apps/governance/state-machines/dao-multisig.d.ts +92 -95
  159. package/dist/types/apps/governance/state-machines/dao-reputation.d.ts +57 -69
  160. package/dist/types/apps/governance/state-machines/dao-single.d.ts +40 -50
  161. package/dist/types/apps/governance/state-machines/dao-token.d.ts +80 -53
  162. package/dist/types/apps/governance/state-machines/governance-simple.d.ts +107 -121
  163. package/dist/types/apps/governance/state-machines/governance-universal.d.ts +17 -5
  164. package/dist/types/apps/identity/constants.d.ts +32 -4
  165. package/dist/types/apps/identity/index.d.ts +449 -100
  166. package/dist/types/apps/identity/state-machines/identity-agent.d.ts +30 -0
  167. package/dist/types/apps/identity/state-machines/identity-oracle.d.ts +41 -87
  168. package/dist/types/apps/identity/state-machines/identity-registry.d.ts +379 -0
  169. package/dist/types/apps/identity/state-machines/identity-universal.d.ts +15 -0
  170. package/dist/types/apps/identity/state-machines/index.d.ts +1 -0
  171. package/dist/types/apps/index.d.ts +1 -0
  172. package/dist/types/apps/lending/assets.d.ts +258 -0
  173. package/dist/types/apps/lending/credit-scoring.d.ts +80 -0
  174. package/dist/types/apps/lending/eligibility.d.ts +83 -0
  175. package/dist/types/apps/lending/index.d.ts +548 -0
  176. package/dist/types/apps/lending/state-machines/index.d.ts +5 -0
  177. package/dist/types/apps/lending/state-machines/lending-zk-loan.d.ts +528 -0
  178. package/dist/types/apps/markets/index.d.ts +184 -204
  179. package/dist/types/apps/markets/state-machines/market-auction.d.ts +25 -37
  180. package/dist/types/apps/markets/state-machines/market-crowdfund.d.ts +26 -34
  181. package/dist/types/apps/markets/state-machines/market-group-buy.d.ts +31 -43
  182. package/dist/types/apps/markets/state-machines/market-prediction.d.ts +67 -72
  183. package/dist/types/apps/markets/state-machines/market-universal.d.ts +30 -10
  184. package/dist/types/generated/index.d.ts +5 -5
  185. package/dist/types/generated/openapi.d.ts +1257 -0
  186. package/dist/types/generated/ottochain/apps/contracts/v1/contract.d.ts +5 -5
  187. package/dist/types/generated/ottochain/apps/identity/v1/identity.d.ts +12 -12
  188. package/dist/types/generated/ottochain/apps/markets/v1/market.d.ts +11 -11
  189. package/dist/types/generated/ottochain/v1/common.d.ts +228 -0
  190. package/dist/types/generated/ottochain/v1/fiber.d.ts +32 -1
  191. package/dist/types/generated/ottochain/v1/messages.d.ts +84 -0
  192. package/dist/types/generated/ottochain/v1/records.d.ts +34 -0
  193. package/dist/types/index.d.ts +8 -2
  194. package/dist/types/openapi.d.ts +22 -0
  195. package/dist/types/ottochain/genesis-manifest.d.ts +130 -0
  196. package/dist/types/ottochain/index.d.ts +6 -5
  197. package/dist/types/ottochain/metagraph-client.d.ts +49 -7
  198. package/dist/types/ottochain/snapshot.d.ts +6 -6
  199. package/dist/types/ottochain/transaction.d.ts +33 -1
  200. package/dist/types/ottochain/types.d.ts +602 -11
  201. package/dist/types/privacy/index.d.ts +6 -0
  202. package/dist/types/privacy/sealed-bid.d.ts +348 -0
  203. package/dist/types/privacy/shield-app.d.ts +48 -0
  204. package/dist/types/schema/effects.d.ts +34 -0
  205. package/dist/types/schema/fiber-app.d.ts +41 -3
  206. package/dist/types/schema/guard-lint.d.ts +111 -0
  207. package/dist/types/schema/guards.d.ts +134 -0
  208. package/dist/types/signing.d.ts +45 -0
  209. package/dist/types/validation.d.ts +22 -217
  210. package/dist/types/verify.d.ts +4 -0
  211. package/dist/types/zk/commit.d.ts +28 -0
  212. package/dist/types/zk/guard.d.ts +46 -0
  213. package/dist/types/zk/index.d.ts +19 -0
  214. package/dist/types/zk/preimage.d.ts +10 -0
  215. package/dist/types/zk/prover.d.ts +34 -0
  216. package/dist/types/zk/registry.d.ts +34 -0
  217. package/dist/types/zk/types.d.ts +33 -0
  218. package/dist/types/zk/witness.d.ts +40 -0
  219. package/package.json +52 -35
  220. package/dist/cjs/ottochain/normalize.js +0 -186
  221. package/dist/esm/ottochain/normalize.js +0 -179
  222. package/dist/types/ottochain/normalize.d.ts +0 -79
@@ -0,0 +1,346 @@
1
+ /**
2
+ * Regression-prevention lint for OttoChain std-app fiber definitions.
3
+ *
4
+ * Purpose
5
+ * -------
6
+ * Catch — at *authoring* time — the classes of SDK↔chain drift that the chain
7
+ * accepts structurally but then silently mis-parses, drops, or evaluates to a
8
+ * dead (always-true / always-false / always-throw) guard. Without this lint
9
+ * those mistakes ship as live, exploitable holes or as silent data corruption
10
+ * because nothing in `defineFiberApp` / `toProtoDefinition` rejects them.
11
+ *
12
+ * The drift classes (cross-referenced to docs/reviews/fiber-app-alignment-audit-2026-06.md):
13
+ *
14
+ * A1 — `$timestamp` (and any non-injected `$`-key). `ReservedKeys` injects ONLY
15
+ * `$ordinal`, `$lastSnapshotHash`, `$epochProgress`. `{"var":"$timestamp"}`
16
+ * resolves to null → `0` in numeric contexts → deadline guards die silently.
17
+ *
18
+ * A2 — Nonexistent JLVM opcodes used as operator tags: `size`, `getKey`,
19
+ * `setKey`, `deleteKey`. metakit decodes an unknown single-key object as a
20
+ * LITERAL Map, not an operator, so the guard is always-true/false and the
21
+ * effect writes a junk key. (Canonical good set: `KNOWN_OPERATORS`.)
22
+ *
23
+ * A3 — SDK directives the chain's `toProtoDefinition` silently drops:
24
+ * transition-level `emits` / `spawns`, and object-form `dependencies`
25
+ * entries `{machine,instanceRef,requiredState}` (chain parses `Set[UUID]`).
26
+ * Authors believe these gate/emit; on-chain they evaporate.
27
+ *
28
+ * per-context — `{"var":"witness.*"}` in a fiber TRANSITION. `witness` is only
29
+ * injected in ASSET-guard contexts; a transition gets `event` (the proof
30
+ * rides under `event.witness.*`). Reading `witness.*` resolves to null →
31
+ * fail-closed (zk gate un-passable) or silently bypassed.
32
+ *
33
+ * leading-dot — `{"var":".foo"}` resolves to null on chain.
34
+ *
35
+ * This is a STANDALONE validator. It is deliberately NOT wired into
36
+ * `defineFiberApp` / `toProtoDefinition`: doing so would break the build until
37
+ * every app is remediated. Run it via `scripts/lint-apps.mjs`.
38
+ */
39
+ import { KNOWN_OPERATORS as KNOWN_OPERATORS_TYPED } from '@constellation-network/metagraph-sdk-jlvm';
40
+ /**
41
+ * Canonical set of valid JLVM operator tags, imported from metakit (the same
42
+ * `KNOWN_OPERATORS` the on-chain evaluator is built from). Widened to
43
+ * `ReadonlySet<string>` so we can membership-test arbitrary object keys.
44
+ */
45
+ const KNOWN_OPERATORS = KNOWN_OPERATORS_TYPED;
46
+ /** Stable rule codes — referenced by tests and by report tooling. */
47
+ export const LINT_CODES = {
48
+ UNKNOWN_RESERVED_VAR: 'unknown-reserved-var', // rule 1 (A1)
49
+ UNKNOWN_OPERATOR: 'unknown-operator', // rule 2 (A2)
50
+ WITNESS_IN_TRANSITION: 'witness-in-transition', // rule 3
51
+ DROPPED_DIRECTIVE: 'dropped-directive', // rule 4 (A3)
52
+ LEADING_DOT_VAR: 'leading-dot-var', // rule 5
53
+ };
54
+ // =============================================================================
55
+ // Rule data
56
+ // =============================================================================
57
+ /**
58
+ * The ONLY `$`-prefixed keys the engine injects (`ReservedKeys`). Anything else
59
+ * (`$timestamp`, `$now`, ...) resolves to null on chain.
60
+ */
61
+ export const INJECTED_RESERVED_VARS = new Set([
62
+ '$ordinal',
63
+ '$lastSnapshotHash',
64
+ '$epochProgress',
65
+ ]);
66
+ /**
67
+ * Tags that LOOK like opcodes and appear in real definitions but are NOT valid
68
+ * JLVM operators — metakit mis-decodes each as a literal Map. These are always
69
+ * flagged as errors regardless of position (they are not plausible data fields).
70
+ * Mapped to the correct replacement opcode for the message.
71
+ */
72
+ export const KNOWN_BAD_OPERATORS = new Map([
73
+ ['size', 'length (collections) or count; for a Map use length[keys[...]]'],
74
+ ['getKey', 'get (value) / has (membership)'],
75
+ ['setKey', 'merge (or restructure the field as an array appended with cat/merge)'],
76
+ ['deleteKey', 'filter the collection (model the field as an array of records)'],
77
+ ]);
78
+ /**
79
+ * Structural keys that JSON-Logic uses as single-key objects but are NOT
80
+ * operator tags. Used to avoid false-positive "unknown operator" warnings.
81
+ * `var` is the data-access form; the rest are common rule scaffolding seen in
82
+ * std apps that are handled by the evaluator or are intentional literals.
83
+ */
84
+ const NON_OPERATOR_SINGLE_KEYS = new Set([
85
+ 'var', // data access — handled explicitly
86
+ ]);
87
+ /**
88
+ * Recursively lint a single JSON-Logic expression (a guard or an effect, or any
89
+ * sub-node). Walks every nested operand. Pushes a {@link LintViolation} for each
90
+ * rule-1 / rule-2 / rule-3 / rule-5 hit found at or below `node`.
91
+ *
92
+ * `inDataLiteral` tracks whether `node` sits in a position the evaluator treats
93
+ * as DATA rather than as an expression to evaluate (e.g. the 2nd operand of
94
+ * `merge`/`cat`, or a value nested inside such an operand). In a data-literal
95
+ * position an unknown single-key object is a legitimate field map, so the
96
+ * *generic* unknown-operator warning is suppressed there — but `$`-var keys,
97
+ * leading-dot vars, witness reads, and the known-BAD opcode tags are still
98
+ * flagged (those are bugs wherever they appear).
99
+ */
100
+ export function lintGuardExpression(node, ctx, path, inDataLiteral = false) {
101
+ const out = [];
102
+ // Arrays: walk each element, preserving the data-literal flag.
103
+ if (Array.isArray(node)) {
104
+ node.forEach((child, i) => {
105
+ out.push(...lintGuardExpression(child, ctx, `${path}[${i}]`, inDataLiteral));
106
+ });
107
+ return out;
108
+ }
109
+ // Primitives carry no structure to lint.
110
+ if (node === null || typeof node !== 'object') {
111
+ return out;
112
+ }
113
+ const obj = node;
114
+ const keys = Object.keys(obj);
115
+ // ---- `var` access: rules 1, 3, 5 -------------------------------------
116
+ if (keys.length === 1 && keys[0] === 'var') {
117
+ const ref = obj.var;
118
+ // The reference may be a string, or `[path, default]`, or (rarely) a
119
+ // nested expression; only string refs carry the patterns we check.
120
+ const refStr = typeof ref === 'string'
121
+ ? ref
122
+ : Array.isArray(ref) && typeof ref[0] === 'string'
123
+ ? ref[0]
124
+ : undefined;
125
+ if (refStr !== undefined) {
126
+ // rule 1 — unknown reserved `$`-key
127
+ if (refStr.startsWith('$') && !INJECTED_RESERVED_VARS.has(refStr)) {
128
+ out.push({
129
+ app: ctx.app,
130
+ transition: ctx.transition,
131
+ severity: 'error',
132
+ code: LINT_CODES.UNKNOWN_RESERVED_VAR,
133
+ message: `{"var":"${refStr}"} references a reserved key the engine never injects. ` +
134
+ `Only ${[...INJECTED_RESERVED_VARS].join(', ')} are injected; ` +
135
+ `"${refStr}" resolves to null (→ 0 in numeric contexts, defeating deadline guards). ` +
136
+ `Use $ordinal and model time fields as ordinal deltas/bounds.`,
137
+ path: `${path}.var`,
138
+ });
139
+ }
140
+ // rule 5 — leading-dot relative path
141
+ if (refStr.startsWith('.')) {
142
+ out.push({
143
+ app: ctx.app,
144
+ transition: ctx.transition,
145
+ severity: 'error',
146
+ code: LINT_CODES.LEADING_DOT_VAR,
147
+ message: `{"var":"${refStr}"} uses a leading-dot relative path, which resolves to null on chain. ` +
148
+ `Use the bare element field name ("${refStr.slice(1)}") inside map/filter/some scopes.`,
149
+ path: `${path}.var`,
150
+ });
151
+ }
152
+ // rule 3 — `witness.*` read inside a fiber transition
153
+ if (ctx.flagWitness &&
154
+ (refStr === 'witness' || refStr.startsWith('witness.'))) {
155
+ out.push({
156
+ app: ctx.app,
157
+ transition: ctx.transition,
158
+ severity: 'error',
159
+ code: LINT_CODES.WITNESS_IN_TRANSITION,
160
+ message: `{"var":"${refStr}"} reads the 'witness' context, which is injected ONLY in asset-guard ` +
161
+ `contexts — never in a fiber transition (the engine injects 'event'). It resolves to null ` +
162
+ `→ the zk gate is un-passable (fail-closed). Read 'event.${refStr}' instead, or move the ` +
163
+ `semi-private guard onto the asset mintPolicy/burnPolicy.`,
164
+ path: `${path}.var`,
165
+ });
166
+ }
167
+ }
168
+ // The `var` default operand (ref[1]) may itself be an expression; walk it.
169
+ if (Array.isArray(ref) && ref.length > 1) {
170
+ out.push(...lintGuardExpression(ref[1], ctx, `${path}.var[1]`, inDataLiteral));
171
+ }
172
+ return out;
173
+ }
174
+ // ---- single-key object: candidate operator (rule 2) ------------------
175
+ if (keys.length === 1) {
176
+ const tag = keys[0];
177
+ const operand = obj[tag];
178
+ // rule 2a — known-BAD opcode tag. Always an error (these are never plausible
179
+ // data field names, and metakit silently mis-decodes them as a literal Map).
180
+ if (KNOWN_BAD_OPERATORS.has(tag)) {
181
+ out.push({
182
+ app: ctx.app,
183
+ transition: ctx.transition,
184
+ severity: 'error',
185
+ code: LINT_CODES.UNKNOWN_OPERATOR,
186
+ message: `'${tag}' is not a JLVM operator — metakit decodes {"${tag}":...} as a literal Map, ` +
187
+ `so this guard is always-true/false and any effect writes a junk key. ` +
188
+ `Use ${KNOWN_BAD_OPERATORS.get(tag)}.`,
189
+ path: `${path}.${tag}`,
190
+ });
191
+ // Still descend into the operand (it may carry further violations).
192
+ out.push(...lintGuardExpression(operand, ctx, `${path}.${tag}`, inDataLiteral));
193
+ return out;
194
+ }
195
+ // Recognized operator: descend. `merge`/`cat` 2nd+ operands are DATA literals
196
+ // (field maps to write), so mark them to suppress the generic op warning below.
197
+ if (KNOWN_OPERATORS.has(tag)) {
198
+ const literalDataOp = tag === 'merge' || tag === 'cat';
199
+ if (literalDataOp && Array.isArray(operand)) {
200
+ operand.forEach((child, i) => {
201
+ // First operand is the base collection (an expression); the rest are
202
+ // the literal field maps merged/appended onto it.
203
+ out.push(...lintGuardExpression(child, ctx, `${path}.${tag}[${i}]`, i > 0 ? true : inDataLiteral));
204
+ });
205
+ }
206
+ else {
207
+ out.push(...lintGuardExpression(operand, ctx, `${path}.${tag}`, inDataLiteral));
208
+ }
209
+ return out;
210
+ }
211
+ // rule 2b (heuristic warn) — an unknown single-key tag that LOOKS like an
212
+ // operator (no spaces, not `var`, not a known data scaffold) and is NOT in a
213
+ // data-literal position. Keep this a WARNING to hold false positives low:
214
+ // in a data-literal position a one-key object is a legitimate `{field: ...}`.
215
+ if (!inDataLiteral &&
216
+ !NON_OPERATOR_SINGLE_KEYS.has(tag) &&
217
+ !tag.includes(' ') &&
218
+ !tag.startsWith('$') &&
219
+ !tag.startsWith('_') // reserved effect directives (_emit/_spawn/...) are handled structurally
220
+ ) {
221
+ out.push({
222
+ app: ctx.app,
223
+ transition: ctx.transition,
224
+ severity: 'warn',
225
+ code: LINT_CODES.UNKNOWN_OPERATOR,
226
+ message: `single-key object {"${tag}":...} in an expression position has a key that is not a known ` +
227
+ `JLVM operator. If '${tag}' was meant as an operator it will mis-decode as a literal Map; ` +
228
+ `if it is a literal field, ignore. Known operators: see KNOWN_OPERATORS.`,
229
+ path: `${path}.${tag}`,
230
+ });
231
+ }
232
+ out.push(...lintGuardExpression(operand, ctx, `${path}.${tag}`, inDataLiteral));
233
+ return out;
234
+ }
235
+ // ---- multi-key object: a DATA literal (a field map). Descend into values,
236
+ // marking children as data-literal so their inner one-key objects are not
237
+ // mistaken for operators. (`$`/leading-dot/witness `var`s inside are still
238
+ // flagged by the recursion.)
239
+ for (const k of keys) {
240
+ out.push(...lintGuardExpression(obj[k], ctx, `${path}.${k}`, true));
241
+ }
242
+ return out;
243
+ }
244
+ // =============================================================================
245
+ // Transition-level structural rules (rule 4 / A3)
246
+ // =============================================================================
247
+ function isObjectDependency(dep) {
248
+ return typeof dep === 'object' && dep !== null && !Array.isArray(dep);
249
+ }
250
+ function lintTransitionStructure(t, app, transition, path) {
251
+ const out = [];
252
+ const raw = t;
253
+ // rule 4a — transition-level `emits`
254
+ if (raw.emits !== undefined) {
255
+ out.push({
256
+ app,
257
+ transition,
258
+ severity: 'error',
259
+ code: LINT_CODES.DROPPED_DIRECTIVE,
260
+ message: `transition-level 'emits' is silently dropped by toProtoDefinition (the chain Transition has ` +
261
+ `no such field). Emit from INSIDE the effect result under the reserved key '_emit' ` +
262
+ `({name,data,destination}), or '_triggers' for a state-changing cross-machine call.`,
263
+ path: `${path}.emits`,
264
+ });
265
+ }
266
+ // rule 4b — transition-level `spawns`
267
+ if (raw.spawns !== undefined) {
268
+ out.push({
269
+ app,
270
+ transition,
271
+ severity: 'error',
272
+ code: LINT_CODES.DROPPED_DIRECTIVE,
273
+ message: `transition-level 'spawns' is silently dropped by toProtoDefinition — the child machine is ` +
274
+ `never created. Spawn from INSIDE the effect result under the reserved key '_spawn' ` +
275
+ `(with a full inline definition, a distinct childId, and initialData).`,
276
+ path: `${path}.spawns`,
277
+ });
278
+ }
279
+ // rule 4c — object-form `dependencies` entries (chain accepts Set[UUID] only)
280
+ if (Array.isArray(t.dependencies)) {
281
+ t.dependencies.forEach((dep, i) => {
282
+ if (isObjectDependency(dep)) {
283
+ out.push({
284
+ app,
285
+ transition,
286
+ severity: 'error',
287
+ code: LINT_CODES.DROPPED_DIRECTIVE,
288
+ message: `dependencies[${i}] is an object ({machine,instanceRef,requiredState}); the chain parses ` +
289
+ `'dependencies' as Set[UUID] and drops the object, so 'requiredState' gating NEVER happens. ` +
290
+ `Pass a bare fiber-UUID string and assert machines.<uuid>.currentStateId=="<state>" in the guard.`,
291
+ path: `${path}.dependencies[${i}]`,
292
+ });
293
+ }
294
+ });
295
+ }
296
+ return out;
297
+ }
298
+ // =============================================================================
299
+ // Top-level entry point
300
+ // =============================================================================
301
+ function appLabel(def) {
302
+ const m = def.metadata;
303
+ if (!m)
304
+ return undefined;
305
+ if (m.app && m.type)
306
+ return `${m.app}/${m.type}`;
307
+ return m.name ?? m.app ?? m.type ?? undefined;
308
+ }
309
+ /**
310
+ * Lint a complete fiber app definition. Walks every transition guard + effect
311
+ * (rules 1/2/3/5) and every transition's structural directives (rule 4), plus a
312
+ * light pass over `states` to catch stray reserved-key / operator drift embedded
313
+ * in state metadata. Returns ALL violations (errors and warnings); callers
314
+ * decide the exit policy (e.g. fail on any `error`).
315
+ */
316
+ export function lintFiberApp(def) {
317
+ const out = [];
318
+ const app = appLabel(def);
319
+ // Transitions.
320
+ (def.transitions ?? []).forEach((t, i) => {
321
+ const transition = t.eventName ?? `${t.from}->${t.to}`;
322
+ const tPath = `transitions[${i}]`;
323
+ const ctx = { app, transition, flagWitness: true };
324
+ if (t.guard !== undefined) {
325
+ out.push(...lintGuardExpression(t.guard, ctx, `${tPath}.guard`));
326
+ }
327
+ if (t.effect !== undefined) {
328
+ out.push(...lintGuardExpression(t.effect, ctx, `${tPath}.effect`));
329
+ }
330
+ out.push(...lintTransitionStructure(t, app, transition, tPath));
331
+ });
332
+ // States — usually inert metadata, but guard against reserved-key drift here
333
+ // too (states never run in an asset context, so witness reads would be bugs).
334
+ Object.entries(def.states ?? {}).forEach(([name, state]) => {
335
+ const ctx = { app, transition: `state:${name}`, flagWitness: true };
336
+ out.push(...lintGuardExpression(state, ctx, `states.${name}`));
337
+ });
338
+ return out;
339
+ }
340
+ /**
341
+ * Convenience: lint many apps at once, returning a flat list. Each app's
342
+ * violations already carry its `app` label.
343
+ */
344
+ export function lintFiberApps(defs) {
345
+ return defs.flatMap((d) => lintFiberApp(d));
346
+ }
@@ -0,0 +1,229 @@
1
+ /**
2
+ * Canonical authorization-guard builders — bind "who may act" to the CHAIN-VERIFIED signer of the op,
3
+ * never to an attacker-supplied payload field.
4
+ *
5
+ * The forgeable anti-pattern (security audit class F1): `{"===":[{"var":"event.agent"}, {"var":"state.party"}]}`.
6
+ * The fiber engine injects `event` as the RAW transition payload (ContextProvider: `EVENT -> payload`),
7
+ * so `event.agent` is whatever the submitter wrote — a different party's address forges authorization.
8
+ * The chain ALSO injects `proofs` = `[{address, id, signature}]` where `address = id.toAddress` (the
9
+ * VERIFIED signer addresses). Authorization must bind to THAT. For ASSET ops (mint/burn/morphism) the
10
+ * context has no `event`/`proofs`; it injects `signers` (an array of verified address strings) instead.
11
+ *
12
+ * Use these so the safe pattern is consistent, greppable, and impossible to get subtly wrong.
13
+ */
14
+ /**
15
+ * FIBER-transition authorization: the pinned `partyVar` (a `state` path holding an address, e.g.
16
+ * `"state.borrower"`) MUST be among the op's verified signers (`proofs[].address`). The replay-safe
17
+ * replacement for `{"===":[{"var":"event.agent"}, {"var":partyVar}]}`.
18
+ */
19
+ export const signerIsParty = (partyVar) => ({
20
+ in: [{ var: partyVar }, { map: [{ var: "proofs" }, { var: "address" }] }],
21
+ });
22
+ /** FIBER-transition authorization where ANY of the pinned parties signed (e.g. borrower OR lender). */
23
+ export const signerIsAnyParty = (partyVars) => ({
24
+ or: partyVars.map(signerIsParty),
25
+ });
26
+ /**
27
+ * FIBER-transition authorization where the actor must be a MEMBER of a pinned set: at least one
28
+ * VERIFIED signer is in the `setVar` array (e.g. `"state.signers"`, `"state.members"`, `"state.oracles"`).
29
+ * The replay-safe replacement for `{"in":[{"var":"event.agent"}, {"var":setVar}]}`.
30
+ */
31
+ export const signerInSet = (setVar) => ({
32
+ some: [
33
+ { map: [{ var: "proofs" }, { var: "address" }] },
34
+ { in: [{ var: "" }, { var: setVar }] },
35
+ ],
36
+ });
37
+ /**
38
+ * FIBER-transition ANTI-SELF guard: NO verified signer is the pinned party (e.g. a proposal author may
39
+ * not vote on their own proposal). The replay-safe replacement for `{"!==":[{"var":"event.agent"}, {"var":partyVar}]}`.
40
+ */
41
+ export const signerIsNotParty = (partyVar) => ({
42
+ "!": [signerIsParty(partyVar)],
43
+ });
44
+ /**
45
+ * FIBER-transition authorization / dedup over a state MAP keyed by address: at least one VERIFIED signer
46
+ * is a key in `mapVar` (e.g. `"state.members"`, `"state.balances"`). The replay-safe replacement for the
47
+ * forgeable `{"getKey":[{"var":mapVar}, {"var":"event.agent"}]}` membership/presence check (note metakit
48
+ * has no `getKey`; use `get`/`has`). For a "no signer has acted yet" dedup, negate: `{"!":[signerHasEntry(...)]}`.
49
+ * For a per-actor VALUE threshold (e.g. balance >= N), build the `some` directly:
50
+ * `{"some":[{"map":[proofs,address]},{">=":[{"get":[mapVar,{"var":""}]}, N]}]}`.
51
+ */
52
+ export const signerHasEntry = (mapVar) => ({
53
+ some: [
54
+ { map: [{ var: "proofs" }, { var: "address" }] },
55
+ { has: [{ var: mapVar }, { var: "" }] },
56
+ ],
57
+ });
58
+ /**
59
+ * ASSET-op authorization (mintPolicy / burnPolicy / MorphismSpec.guard): the address at `addressVar`
60
+ * (e.g. `"holder.Wallet.address"`) MUST be among the op's verified `signers`. The asset context has no
61
+ * `event` or `proofs` — it injects `signers` directly.
62
+ */
63
+ export const assetSignerIs = (addressVar) => ({
64
+ in: [{ var: addressVar }, { var: "signers" }],
65
+ });
66
+ /**
67
+ * EFFECT-KEY BINDING: prove the event's claimed actor (`actorVar`, default `event.agent`) is a
68
+ * CHAIN-VERIFIED signer, so that field is SAFE to use as a dynamic map key / array element in the
69
+ * EFFECT (`{"set":[map,{"var":"event.agent"},v]}`, `{"cat":[arr,[{"var":"event.agent"}]]}`).
70
+ *
71
+ * This is the coupling clause for the map-write remediation: a guard may authorize via membership /
72
+ * balance / reputation, but if the EFFECT writes under `event.agent` WITHOUT this clause, an attacker
73
+ * sets `event.agent` to a victim's address and writes under the victim's key (security class S1).
74
+ * Pair it with the authorization check expressed on the SAME `actorVar` — together they prove the actor
75
+ * both signed and is authorized, and the effect can only write under that one verified key.
76
+ * Structurally identical to {@link signerIsParty}; named for intent + greppability at write sites.
77
+ */
78
+ export const actorIsSigner = (actorVar = "event.agent") => signerIsParty(actorVar);
79
+ /**
80
+ * EFFECT-KEY-BINDING membership: the event's claimed actor (`actorVar`, default `event.agent`) is BOTH
81
+ * a CHAIN-VERIFIED signer AND a member of the pinned set `setVar` (e.g. `"state.signers"`,
82
+ * `"state.members"`). Use when the EFFECT writes a map/array keyed by that actor and authorization is
83
+ * set-membership. It proves the EXACT actor whose key is written both signed and is authorized — closing
84
+ * the S1 forge AND the subtler gap {@link signerInSet} leaves open: `signerInSet` only proves SOME
85
+ * verified signer is a member, so an op co-signed by an authorized signer could still write a DIFFERENT
86
+ * verified-but-unauthorized address as the key (padding a signature/vote tally). `actorInSet` pins both
87
+ * to the same `actorVar`.
88
+ */
89
+ export const actorInSet = (setVar, actorVar = "event.agent") => ({
90
+ and: [actorIsSigner(actorVar), { in: [{ var: actorVar }, { var: setVar }] }],
91
+ });
92
+ /**
93
+ * EFFECT-KEY-BINDING map membership: the claimed actor (`actorVar`, default `event.agent`) is BOTH a
94
+ * CHAIN-VERIFIED signer AND a key in the pinned state MAP `mapVar` (e.g. `"state.members"`,
95
+ * `"state.balances"`). The map analog of {@link actorInSet} — use it when the membership set is a dict
96
+ * keyed by address, where `in` does not apply and `has` checks key presence. Use when the EFFECT writes
97
+ * `mapVar` (or a per-actor tally) keyed by that actor: it proves the EXACT written key both signed and
98
+ * is an authorized member, closing the vote/signature-stuffing gap that bare {@link signerHasEntry}
99
+ * leaves open (which only proves SOME verified signer is a key, not that the written key is).
100
+ */
101
+ export const actorHasEntry = (mapVar, actorVar = "event.agent") => ({
102
+ and: [actorIsSigner(actorVar), { has: [{ var: mapVar }, { var: actorVar }] }],
103
+ });
104
+ /**
105
+ * IDENTITY-REGISTRY reputation gate: at least one VERIFIED signer has a reputation in the registry map
106
+ * at `repMapVar` (a declared-dependency read, e.g. `"machines.<registryDep>.state.reputations"`, shaped
107
+ * `{ <address>: int }`) that is `>=` the bar read from `thresholdVar` (a state path, e.g.
108
+ * `"state.voteThreshold"`). A missing entry reads as null → 0 numerically, so unregistered signers
109
+ * fail-closed for any positive bar. The replay-safe replacement for the forgeable
110
+ * `{">=":[{"var":"event.agentReputation"}, bar]}` (security class S1). See
111
+ * docs/design/app-hardening-identity-integration.md §3–§4.1.
112
+ */
113
+ export const signerHasReputation = (repMapVar, thresholdVar) => ({
114
+ some: [
115
+ { map: [{ var: "proofs" }, { var: "address" }] },
116
+ {
117
+ ">=": [{ get: [{ var: repMapVar }, { var: "" }] }, { var: thresholdVar }],
118
+ },
119
+ ],
120
+ });
121
+ /**
122
+ * IDENTITY-REGISTRY role gate: at least one VERIFIED signer holds an active role attestation, i.e. is a
123
+ * key in the registry's flat per-role map at `roleMapVar` (a declared-dependency read, e.g.
124
+ * `"machines.<registryDep>.state.arbiters"` / `".slashers"` / `".issuers"` / `".boardMembers"`, each
125
+ * shaped `{ <address>: true }`). The replay-safe replacement for bare `event.judicialRuling` / role
126
+ * escapes and `{"==":[1,1]}` missing-auth (security class S2). Roles are FLAT per-role maps (not a
127
+ * nested `roles[addr][ROLE]`) because metakit `get`/`has` on a null inner map ERROR rather than
128
+ * returning null; a flat map keeps the read total + fail-closed. See app-hardening §4.2.
129
+ */
130
+ export const signerHasRole = (roleMapVar) => signerHasEntry(roleMapVar);
131
+ /**
132
+ * DYNAMIC identity-registry reputation gate — for when the registry instance is bound at RUNTIME via the
133
+ * fiber-engine `_addDependency` directive (#24) rather than hardcoded into the `machines.<uuid>` path.
134
+ * `registryIdVar` is a state/event path holding the registry fiber id (e.g. `"state.registryId"`); the
135
+ * read addresses `machines[<that id>].state.reputations[<signer>]` with dynamic `get`s. Guarded by a
136
+ * presence check so an UNBOUND registry yields a clean `false` (fail-closed) instead of an evaluation
137
+ * error. REQUIRES the registry dependency to have been added in a PRIOR transition (`_addDependency`),
138
+ * because the `machines` context is built before the effect runs (two-phase: bind, then read).
139
+ */
140
+ export const signerHasReputationVia = (registryIdVar, thresholdVar) => ({
141
+ if: [
142
+ { has: [{ var: "machines" }, { var: registryIdVar }] },
143
+ {
144
+ some: [
145
+ { map: [{ var: "proofs" }, { var: "address" }] },
146
+ {
147
+ ">=": [
148
+ {
149
+ get: [
150
+ {
151
+ get: [
152
+ {
153
+ get: [
154
+ { get: [{ var: "machines" }, { var: registryIdVar }] },
155
+ "state",
156
+ ],
157
+ },
158
+ "reputations",
159
+ ],
160
+ },
161
+ { var: "" },
162
+ ],
163
+ },
164
+ { var: thresholdVar },
165
+ ],
166
+ },
167
+ ],
168
+ },
169
+ false,
170
+ ],
171
+ });
172
+ /**
173
+ * DYNAMIC identity-registry role gate — the runtime-bound (`_addDependency`, #24) counterpart of
174
+ * {@link signerHasRole}. `registryIdVar` is a state/event path holding the registry fiber id;
175
+ * `roleField` is the registry's flat per-role state map name (`"arbiters"` / `"slashers"` / `"issuers"` /
176
+ * `"boardMembers"` — see REGISTRY_ROLE_MAP). Reads `machines[<id>].state.<roleField>[<signer>]`, guarded
177
+ * by a presence check so an UNBOUND registry yields a clean `false`. Same two-phase requirement as
178
+ * {@link signerHasReputationVia}.
179
+ */
180
+ export const signerHasRoleVia = (registryIdVar, roleField) => ({
181
+ if: [
182
+ { has: [{ var: "machines" }, { var: registryIdVar }] },
183
+ {
184
+ some: [
185
+ { map: [{ var: "proofs" }, { var: "address" }] },
186
+ {
187
+ has: [
188
+ {
189
+ get: [
190
+ {
191
+ get: [
192
+ { get: [{ var: "machines" }, { var: registryIdVar }] },
193
+ "state",
194
+ ],
195
+ },
196
+ roleField,
197
+ ],
198
+ },
199
+ { var: "" },
200
+ ],
201
+ },
202
+ ],
203
+ },
204
+ false,
205
+ ],
206
+ });
207
+ /**
208
+ * CROSS-FIBER STATE GATE — assert a RUNTIME-bound dependency fiber is in a required lifecycle state. The
209
+ * replay-safe replacement for the dropped object-form dependency
210
+ * `{machine, instanceRef, requiredState}` (which the chain silently drops — `requiredState` gating never
211
+ * happens). `refVar` is a state/event path holding the dependency's fiber id; reads
212
+ * `machines[<refVar>].currentStateId == requiredState`, guarded by a presence check so an UNBOUND
213
+ * dependency yields a clean `false` (fail-closed). TWO-PHASE (#24): the dependency fiber must have been
214
+ * bound by an `_addDependency` in a PRIOR transition (the `machines` context is built before the effect),
215
+ * so a single gated transition is split into a bind step then this assert step. See
216
+ * docs/design/app-hardening-identity-integration.md §5–§6.
217
+ */
218
+ export const depInState = (refVar, requiredState) => ({
219
+ if: [
220
+ { has: [{ var: "machines" }, { var: refVar }] },
221
+ {
222
+ "==": [
223
+ { get: [{ get: [{ var: "machines" }, { var: refVar }] }, "currentStateId"] },
224
+ requiredState,
225
+ ],
226
+ },
227
+ false,
228
+ ],
229
+ });
@@ -0,0 +1,74 @@
1
+ /**
2
+ * DataUpdate Signing Wrappers — dropNulls applied internally
3
+ *
4
+ * Metakit (>= 1.8) computes content hashes by recursively dropping
5
+ * null-valued OBJECT fields (array nulls are preserved) before RFC 8785
6
+ * canonicalization (`JsonBinaryCodec.dropNulls`). This makes
7
+ * `Option = None` indistinguishable from an absent field, so schema
8
+ * evolution never changes prior hashes.
9
+ *
10
+ * The upstream `@constellation-network/metagraph-sdk` signs over the
11
+ * payload as-is, so a payload containing explicit nulls would be signed
12
+ * over different bytes than the metagraph hashes — and the node rejects
13
+ * the signature (HTTP 400).
14
+ *
15
+ * These wrappers shadow the upstream dataUpdate signing surface and apply
16
+ * `dropNulls` internally, unconditionally. Standard-mode signing
17
+ * (currency transactions etc.) is passed through unchanged.
18
+ */
19
+ import { signDataUpdate as baseSignDataUpdate, createSignedObject as baseCreateSignedObject, addSignature as baseAddSignature, batchSign as baseBatchSign, } from '@constellation-network/metagraph-sdk';
20
+ import { dropNulls } from './ottochain/drop-nulls.js';
21
+ function resolveMode(options = {}) {
22
+ if (options.mode)
23
+ return options.mode;
24
+ if (options.isDataUpdate)
25
+ return 'dataUpdate';
26
+ return 'standard';
27
+ }
28
+ /**
29
+ * Sign data as a DataUpdate, hashing over null-dropped canonical bytes.
30
+ *
31
+ * Applies `dropNulls` internally so that explicit-null and absent optional
32
+ * fields produce identical signatures, matching metakit's content-hash rule
33
+ * (drop null object fields, preserve array nulls, then RFC 8785).
34
+ */
35
+ export function signDataUpdate(data, privateKey) {
36
+ return baseSignDataUpdate(dropNulls(data), privateKey);
37
+ }
38
+ /**
39
+ * Create a signed object. In `dataUpdate` mode the signature is computed
40
+ * over the null-dropped canonical bytes (the returned `value` is the
41
+ * caller's original payload, untouched).
42
+ */
43
+ export function createSignedObject(value, privateKey, options = {}) {
44
+ if (resolveMode(options) === 'dataUpdate') {
45
+ const signed = baseCreateSignedObject(dropNulls(value), privateKey, options);
46
+ return { ...signed, value };
47
+ }
48
+ return baseCreateSignedObject(value, privateKey, options);
49
+ }
50
+ /**
51
+ * Add an additional signature to an existing signed object. In
52
+ * `dataUpdate` mode the new signature is computed over the null-dropped
53
+ * canonical bytes of `signed.value`.
54
+ */
55
+ export function addSignature(signed, privateKey, options) {
56
+ const mode = options ? resolveMode(options) : (signed.mode ?? 'standard');
57
+ if (mode === 'dataUpdate') {
58
+ const result = baseAddSignature({ ...signed, value: dropNulls(signed.value) }, privateKey, options);
59
+ return { ...result, value: signed.value };
60
+ }
61
+ return baseAddSignature(signed, privateKey, options);
62
+ }
63
+ /**
64
+ * Create a signed object with multiple signatures at once. In
65
+ * `dataUpdate` mode all signatures are computed over the null-dropped
66
+ * canonical bytes (the returned `value` is the caller's original payload).
67
+ */
68
+ export function batchSign(value, privateKeys, options = {}) {
69
+ if (resolveMode(options) === 'dataUpdate') {
70
+ const signed = baseBatchSign(dropNulls(value), privateKeys, options);
71
+ return { ...signed, value };
72
+ }
73
+ return baseBatchSign(value, privateKeys, options);
74
+ }
@@ -4,10 +4,16 @@
4
4
  * The package embeds `mode: "standard"|"dataUpdate"` in signed objects and
5
5
  * its verify() ignores the `isDataUpdate` parameter when `mode` is present.
6
6
  * This wrapper strips `mode` so callers' explicit `isDataUpdate` always wins.
7
+ *
8
+ * In dataUpdate mode, verification happens over the null-dropped canonical
9
+ * bytes (drop null object fields, preserve array nulls, then RFC 8785) —
10
+ * mirroring the SDK's dataUpdate signers and metakit's content-hash rule.
7
11
  */
8
12
  import { verify as _verify } from '@constellation-network/metagraph-sdk';
13
+ import { dropNulls } from './ottochain/drop-nulls.js';
9
14
  export function verify(signed, isDataUpdate) {
10
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
11
15
  const { mode, ...rest } = signed;
12
- return _verify(rest, isDataUpdate);
16
+ const isDU = isDataUpdate ?? mode === 'dataUpdate';
17
+ const target = isDU ? { ...rest, value: dropNulls(signed.value) } : rest;
18
+ return _verify(target, isDU);
13
19
  }