@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.
- package/dist/cjs/apps/contracts/index.js +15 -8
- package/dist/cjs/apps/contracts/state-machines/contract-agreement.js +89 -46
- package/dist/cjs/apps/contracts/state-machines/contract-escrow.js +152 -45
- package/dist/cjs/apps/contracts/state-machines/contract-universal.js +40 -8
- package/dist/cjs/apps/corporate/index.js +2 -2
- package/dist/cjs/apps/corporate/state-machines/corp-board.js +169 -59
- package/dist/cjs/apps/corporate/state-machines/corp-entity.js +245 -47
- package/dist/cjs/apps/corporate/state-machines/corp-securities.js +407 -99
- package/dist/cjs/apps/corporate/state-machines/corp-shareholders.js +151 -37
- package/dist/cjs/apps/governance/index.js +14 -14
- package/dist/cjs/apps/governance/state-machines/dao-multisig.js +85 -43
- package/dist/cjs/apps/governance/state-machines/dao-reputation.js +73 -43
- package/dist/cjs/apps/governance/state-machines/dao-single.js +55 -14
- package/dist/cjs/apps/governance/state-machines/dao-token.js +103 -45
- package/dist/cjs/apps/governance/state-machines/governance-simple.js +129 -65
- package/dist/cjs/apps/governance/state-machines/governance-universal.js +29 -5
- package/dist/cjs/apps/identity/constants.js +55 -14
- package/dist/cjs/apps/identity/index.js +27 -11
- package/dist/cjs/apps/identity/state-machines/identity-agent.js +57 -9
- package/dist/cjs/apps/identity/state-machines/identity-oracle.js +70 -27
- package/dist/cjs/apps/identity/state-machines/identity-registry.js +288 -0
- package/dist/cjs/apps/identity/state-machines/identity-universal.js +30 -6
- package/dist/cjs/apps/identity/state-machines/index.js +3 -1
- package/dist/cjs/apps/index.js +19 -8
- package/dist/cjs/apps/lending/assets.js +223 -0
- package/dist/cjs/apps/lending/credit-scoring.js +87 -0
- package/dist/cjs/apps/lending/eligibility.js +121 -0
- package/dist/cjs/apps/lending/index.js +91 -0
- package/dist/cjs/apps/lending/state-machines/index.js +9 -0
- package/dist/cjs/apps/lending/state-machines/lending-zk-loan.js +412 -0
- package/dist/cjs/apps/markets/index.js +21 -11
- package/dist/cjs/apps/markets/state-machines/market-auction.js +44 -19
- package/dist/cjs/apps/markets/state-machines/market-crowdfund.js +54 -29
- package/dist/cjs/apps/markets/state-machines/market-group-buy.js +53 -31
- package/dist/cjs/apps/markets/state-machines/market-prediction.js +120 -61
- package/dist/cjs/apps/markets/state-machines/market-universal.js +50 -10
- package/dist/cjs/apps/oracles/index.js +2 -2
- package/dist/cjs/errors.js +3 -3
- package/dist/cjs/generated/google/protobuf/struct.js +4 -4
- package/dist/cjs/generated/index.js +11 -7
- package/dist/cjs/generated/openapi.js +6 -0
- package/dist/cjs/generated/ottochain/apps/contracts/v1/contract.js +50 -50
- package/dist/cjs/generated/ottochain/apps/corporate/v1/corporate.js +19 -20
- package/dist/cjs/generated/ottochain/apps/governance/v1/governance.js +13 -13
- package/dist/cjs/generated/ottochain/apps/identity/v1/attestation.js +4 -4
- package/dist/cjs/generated/ottochain/apps/identity/v1/identity.js +124 -130
- package/dist/cjs/generated/ottochain/apps/markets/v1/market.js +107 -107
- package/dist/cjs/generated/ottochain/v1/common.js +1722 -1
- package/dist/cjs/generated/ottochain/v1/fiber.js +355 -5
- package/dist/cjs/generated/ottochain/v1/messages.js +775 -3
- package/dist/cjs/generated/ottochain/v1/records.js +350 -3
- package/dist/cjs/index.js +45 -7
- package/dist/cjs/openapi.js +2 -0
- package/dist/cjs/ottochain/drop-nulls.js +1 -2
- package/dist/cjs/ottochain/genesis-manifest.js +193 -0
- package/dist/cjs/ottochain/index.js +27 -13
- package/dist/cjs/ottochain/metagraph-client.js +65 -8
- package/dist/cjs/ottochain/snapshot.js +11 -12
- package/dist/cjs/ottochain/transaction.js +57 -14
- package/dist/cjs/ottochain/types.js +19 -1
- package/dist/cjs/privacy/index.js +14 -0
- package/dist/cjs/privacy/sealed-bid.js +210 -0
- package/dist/cjs/privacy/shield-app.js +131 -0
- package/dist/cjs/schema/effects.js +40 -0
- package/dist/cjs/schema/fiber-app.js +19 -13
- package/dist/cjs/schema/guard-lint.js +352 -0
- package/dist/cjs/schema/guards.js +246 -0
- package/dist/cjs/signing.js +80 -0
- package/dist/cjs/types.js +2 -3
- package/dist/cjs/validation.js +8 -8
- package/dist/cjs/verify.js +9 -4
- package/dist/cjs/zk/commit.js +53 -0
- package/dist/cjs/zk/guard.js +107 -0
- package/dist/cjs/zk/index.js +48 -0
- package/dist/cjs/zk/preimage.js +37 -0
- package/dist/cjs/zk/prover.js +87 -0
- package/dist/cjs/zk/registry.js +62 -0
- package/dist/cjs/zk/types.js +28 -0
- package/dist/cjs/zk/witness.js +19 -0
- package/dist/esm/apps/contracts/index.js +7 -3
- package/dist/esm/apps/contracts/state-machines/contract-agreement.js +89 -46
- package/dist/esm/apps/contracts/state-machines/contract-escrow.js +152 -45
- package/dist/esm/apps/contracts/state-machines/contract-universal.js +40 -8
- package/dist/esm/apps/corporate/state-machines/corp-board.js +169 -59
- package/dist/esm/apps/corporate/state-machines/corp-entity.js +245 -47
- package/dist/esm/apps/corporate/state-machines/corp-securities.js +407 -99
- package/dist/esm/apps/corporate/state-machines/corp-shareholders.js +151 -37
- package/dist/esm/apps/governance/state-machines/dao-multisig.js +85 -43
- package/dist/esm/apps/governance/state-machines/dao-reputation.js +73 -43
- package/dist/esm/apps/governance/state-machines/dao-single.js +55 -14
- package/dist/esm/apps/governance/state-machines/dao-token.js +103 -45
- package/dist/esm/apps/governance/state-machines/governance-simple.js +129 -65
- package/dist/esm/apps/governance/state-machines/governance-universal.js +29 -5
- package/dist/esm/apps/identity/constants.js +51 -12
- package/dist/esm/apps/identity/index.js +12 -7
- package/dist/esm/apps/identity/state-machines/identity-agent.js +57 -9
- package/dist/esm/apps/identity/state-machines/identity-oracle.js +70 -27
- package/dist/esm/apps/identity/state-machines/identity-registry.js +285 -0
- package/dist/esm/apps/identity/state-machines/identity-universal.js +30 -6
- package/dist/esm/apps/identity/state-machines/index.js +1 -0
- package/dist/esm/apps/index.js +1 -0
- package/dist/esm/apps/lending/assets.js +207 -0
- package/dist/esm/apps/lending/credit-scoring.js +81 -0
- package/dist/esm/apps/lending/eligibility.js +115 -0
- package/dist/esm/apps/lending/index.js +69 -0
- package/dist/esm/apps/lending/state-machines/index.js +5 -0
- package/dist/esm/apps/lending/state-machines/lending-zk-loan.js +409 -0
- package/dist/esm/apps/markets/index.js +8 -4
- package/dist/esm/apps/markets/state-machines/market-auction.js +44 -19
- package/dist/esm/apps/markets/state-machines/market-crowdfund.js +54 -29
- package/dist/esm/apps/markets/state-machines/market-group-buy.js +53 -31
- package/dist/esm/apps/markets/state-machines/market-prediction.js +120 -61
- package/dist/esm/apps/markets/state-machines/market-universal.js +50 -10
- package/dist/esm/generated/index.js +9 -5
- package/dist/esm/generated/openapi.js +5 -0
- package/dist/esm/generated/ottochain/apps/contracts/v1/contract.js +46 -46
- package/dist/esm/generated/ottochain/apps/identity/v1/identity.js +114 -120
- package/dist/esm/generated/ottochain/apps/markets/v1/market.js +100 -100
- package/dist/esm/generated/ottochain/v1/common.js +1718 -0
- package/dist/esm/generated/ottochain/v1/fiber.js +351 -1
- package/dist/esm/generated/ottochain/v1/messages.js +774 -2
- package/dist/esm/generated/ottochain/v1/records.js +349 -2
- package/dist/esm/index.js +23 -2
- package/dist/esm/openapi.js +1 -0
- package/dist/esm/ottochain/genesis-manifest.js +189 -0
- package/dist/esm/ottochain/index.js +4 -3
- package/dist/esm/ottochain/metagraph-client.js +65 -8
- package/dist/esm/ottochain/snapshot.js +4 -4
- package/dist/esm/ottochain/transaction.js +43 -3
- package/dist/esm/ottochain/types.js +18 -0
- package/dist/esm/privacy/index.js +6 -0
- package/dist/esm/privacy/sealed-bid.js +206 -0
- package/dist/esm/privacy/shield-app.js +127 -0
- package/dist/esm/schema/effects.js +35 -0
- package/dist/esm/schema/fiber-app.js +13 -6
- package/dist/esm/schema/guard-lint.js +346 -0
- package/dist/esm/schema/guards.js +229 -0
- package/dist/esm/signing.js +74 -0
- package/dist/esm/verify.js +8 -2
- package/dist/esm/zk/commit.js +44 -0
- package/dist/esm/zk/guard.js +99 -0
- package/dist/esm/zk/index.js +19 -0
- package/dist/esm/zk/preimage.js +30 -0
- package/dist/esm/zk/prover.js +82 -0
- package/dist/esm/zk/registry.js +55 -0
- package/dist/esm/zk/types.js +25 -0
- package/dist/esm/zk/witness.js +15 -0
- package/dist/types/apps/contracts/index.d.ts +159 -227
- package/dist/types/apps/contracts/state-machines/contract-agreement.d.ts +50 -101
- package/dist/types/apps/contracts/state-machines/contract-escrow.d.ts +86 -120
- package/dist/types/apps/contracts/state-machines/contract-universal.d.ts +20 -4
- package/dist/types/apps/corporate/index.d.ts +1040 -304
- package/dist/types/apps/corporate/state-machines/corp-board.d.ts +188 -52
- package/dist/types/apps/corporate/state-machines/corp-entity.d.ts +267 -62
- package/dist/types/apps/corporate/state-machines/corp-securities.d.ts +355 -131
- package/dist/types/apps/corporate/state-machines/corp-shareholders.d.ts +225 -53
- package/dist/types/apps/governance/index.d.ts +396 -417
- package/dist/types/apps/governance/state-machines/dao-multisig.d.ts +92 -95
- package/dist/types/apps/governance/state-machines/dao-reputation.d.ts +57 -69
- package/dist/types/apps/governance/state-machines/dao-single.d.ts +40 -50
- package/dist/types/apps/governance/state-machines/dao-token.d.ts +80 -53
- package/dist/types/apps/governance/state-machines/governance-simple.d.ts +107 -121
- package/dist/types/apps/governance/state-machines/governance-universal.d.ts +17 -5
- package/dist/types/apps/identity/constants.d.ts +32 -4
- package/dist/types/apps/identity/index.d.ts +449 -100
- package/dist/types/apps/identity/state-machines/identity-agent.d.ts +30 -0
- package/dist/types/apps/identity/state-machines/identity-oracle.d.ts +41 -87
- package/dist/types/apps/identity/state-machines/identity-registry.d.ts +379 -0
- package/dist/types/apps/identity/state-machines/identity-universal.d.ts +15 -0
- package/dist/types/apps/identity/state-machines/index.d.ts +1 -0
- package/dist/types/apps/index.d.ts +1 -0
- package/dist/types/apps/lending/assets.d.ts +258 -0
- package/dist/types/apps/lending/credit-scoring.d.ts +80 -0
- package/dist/types/apps/lending/eligibility.d.ts +83 -0
- package/dist/types/apps/lending/index.d.ts +548 -0
- package/dist/types/apps/lending/state-machines/index.d.ts +5 -0
- package/dist/types/apps/lending/state-machines/lending-zk-loan.d.ts +528 -0
- package/dist/types/apps/markets/index.d.ts +184 -204
- package/dist/types/apps/markets/state-machines/market-auction.d.ts +25 -37
- package/dist/types/apps/markets/state-machines/market-crowdfund.d.ts +26 -34
- package/dist/types/apps/markets/state-machines/market-group-buy.d.ts +31 -43
- package/dist/types/apps/markets/state-machines/market-prediction.d.ts +67 -72
- package/dist/types/apps/markets/state-machines/market-universal.d.ts +30 -10
- package/dist/types/generated/index.d.ts +5 -5
- package/dist/types/generated/openapi.d.ts +1257 -0
- package/dist/types/generated/ottochain/apps/contracts/v1/contract.d.ts +5 -5
- package/dist/types/generated/ottochain/apps/identity/v1/identity.d.ts +12 -12
- package/dist/types/generated/ottochain/apps/markets/v1/market.d.ts +11 -11
- package/dist/types/generated/ottochain/v1/common.d.ts +228 -0
- package/dist/types/generated/ottochain/v1/fiber.d.ts +32 -1
- package/dist/types/generated/ottochain/v1/messages.d.ts +84 -0
- package/dist/types/generated/ottochain/v1/records.d.ts +34 -0
- package/dist/types/index.d.ts +8 -2
- package/dist/types/openapi.d.ts +22 -0
- package/dist/types/ottochain/genesis-manifest.d.ts +130 -0
- package/dist/types/ottochain/index.d.ts +6 -5
- package/dist/types/ottochain/metagraph-client.d.ts +49 -7
- package/dist/types/ottochain/snapshot.d.ts +6 -6
- package/dist/types/ottochain/transaction.d.ts +33 -1
- package/dist/types/ottochain/types.d.ts +602 -11
- package/dist/types/privacy/index.d.ts +6 -0
- package/dist/types/privacy/sealed-bid.d.ts +348 -0
- package/dist/types/privacy/shield-app.d.ts +48 -0
- package/dist/types/schema/effects.d.ts +34 -0
- package/dist/types/schema/fiber-app.d.ts +41 -3
- package/dist/types/schema/guard-lint.d.ts +111 -0
- package/dist/types/schema/guards.d.ts +134 -0
- package/dist/types/signing.d.ts +45 -0
- package/dist/types/validation.d.ts +22 -217
- package/dist/types/verify.d.ts +4 -0
- package/dist/types/zk/commit.d.ts +28 -0
- package/dist/types/zk/guard.d.ts +46 -0
- package/dist/types/zk/index.d.ts +19 -0
- package/dist/types/zk/preimage.d.ts +10 -0
- package/dist/types/zk/prover.d.ts +34 -0
- package/dist/types/zk/registry.d.ts +34 -0
- package/dist/types/zk/types.d.ts +33 -0
- package/dist/types/zk/witness.d.ts +40 -0
- package/package.json +52 -35
- package/dist/cjs/ottochain/normalize.js +0 -186
- package/dist/esm/ottochain/normalize.js +0 -179
- 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
|
+
}
|
package/dist/esm/verify.js
CHANGED
|
@@ -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
|
-
|
|
16
|
+
const isDU = isDataUpdate ?? mode === 'dataUpdate';
|
|
17
|
+
const target = isDU ? { ...rest, value: dropNulls(signed.value) } : rest;
|
|
18
|
+
return _verify(target, isDU);
|
|
13
19
|
}
|