@moonglade/provenance 2.6.0 → 2.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +19 -3
- package/SPEC.md +175 -175
- package/dist/artifact-cli.js +7 -2
- package/dist/hookInstaller.d.ts +75 -0
- package/dist/hookInstaller.js +410 -0
- package/package.json +6 -1
- package/trust/root-public.asc +10 -10
- package/wasm/mgc_wasm.js +1 -2
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 Moonglade AI
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Moonglade AI
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,7 +1,19 @@
|
|
|
1
|
-
# @moonglade/provenance 2.6.
|
|
1
|
+
# @moonglade/provenance 2.6.1
|
|
2
2
|
|
|
3
3
|
Ed25519 signatures, canonical file hashes, RFC 3161 timestamp checks and Merkle inclusion proofs for Node 22 and 24.
|
|
4
4
|
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Install directly from npm:
|
|
8
|
+
```sh
|
|
9
|
+
npm install @moonglade/provenance
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Or install globally for the CLI and MCP tools:
|
|
13
|
+
```sh
|
|
14
|
+
npm install -g @moonglade/provenance
|
|
15
|
+
```
|
|
16
|
+
|
|
5
17
|
## Build from source
|
|
6
18
|
|
|
7
19
|
Run these commands from the repository root:
|
|
@@ -11,7 +23,11 @@ npm ci
|
|
|
11
23
|
npm test
|
|
12
24
|
```
|
|
13
25
|
|
|
14
|
-
The
|
|
26
|
+
Run the built-in self-test with `provenance test` (no file argument; add `--json` for a machine-readable report). It exits non-zero on any failure. Programmatic access: `require("@moonglade/provenance/self-test")` exposes `runSelfTests` and `printSelfTestReport`. The WASM core is available as `require("@moonglade/provenance/wasm")`. `provenance install-hooks [--pre-commit] [--mcp-verify-read] [--mcp-auto-seal] [--force]` installs only what you select (it asks per option in a terminal, and does nothing without flags otherwise). An existing pre-commit hook that this tool did not install is never overwritten: the command refuses, and with `--force` first copies it to `<hook>.bak-<UTC timestamp>`. A hook counts as ours only if its whole content equals a hook this tool generated; an edited copy of it, or any other hook that merely mentions the tool, is refused (and backed up under `--force`). An existing `.provenance-hooks.json` that is not in the tool's own shape is backed up before it is rewritten, and MCP choices recorded earlier are kept. The generated hook needs bash. It reads NUL-delimited staged names, so names with spaces or non-ASCII characters are verified, and it blocks the commit if the staged list cannot be read. `--mcp-verify-read` and `--mcp-auto-seal` only record your choice in `.provenance-hooks.json` and print an MCP configuration snippet; nothing in this package reads that file yet. The hooks directory is resolved by `git rev-parse --git-path hooks`, so linked worktrees and `core.hooksPath` work. A hook installed from one linked worktree applies to every worktree of that repository, because they share one hooks directory. If `core.hooksPath` points at a shared or global directory, the hook is written there and affects every repository that uses it; the command prints the path it wrote. Ending input (Ctrl-D) at a prompt cancels with a non-zero exit and changes nothing.
|
|
27
|
+
|
|
28
|
+
## WASM core provenance
|
|
29
|
+
|
|
30
|
+
`wasm/` is built from the Rust crate `mgc-wasm` (with `mgc-core`) in the private Moonglade monorepo, commit `84ff58c679c64c723f91b101012e7ba884e93020`, with rustc 1.98.1, wasm-bindgen 0.2.129 and `wasm-bindgen --target nodejs`; no wasm-opt pass. Rebuilt from a clean export of that commit, all four files in `wasm/` are byte-identical to that rebuild: `mgc_wasm_bg.wasm` (sha256 `c7f0f294a3de9913c9e71e2a35926d60eb79fa6c45856aa08cf7f2f552f3d1d0`), `mgc_wasm.js` (`63d38046e9e00418043a6291937b03b817df75b333c9b00ee3f2899b5a3c93e2`), `mgc_wasm.d.ts` and `mgc_wasm_bg.wasm.d.ts`. No file is hand-edited. The private source is not published here; hashes and the build commands are in `PUBLIC_SOURCE_IMPORT.json` under `wasm_provenance`.
|
|
15
31
|
|
|
16
32
|
## Sign and verify
|
|
17
33
|
|
|
@@ -72,6 +88,6 @@ Gate assertions check the supplied signature and policy; they do not establish h
|
|
|
72
88
|
|
|
73
89
|
`dist/index.d.ts` declares the public exports: the engine, vault, verification policy, canonicalization and bounded reads; Merkle helpers; timestamp parsing, verification and acquisition; badge helpers; artifact and envelope utilities; read-only MCP tools and self-test.
|
|
74
90
|
|
|
75
|
-
The built-in verifier returns `INDETERMINATE` for OpenPGP and SSHSIG attestations. The legacy library OpenPGP signing helper is not covered by the default test suite; its keyring selection is caller-controlled.
|
|
91
|
+
The built-in verifier returns `INDETERMINATE` for OpenPGP and SSHSIG attestations. The legacy library OpenPGP signing helper is not covered by the default test suite; its keyring selection is caller-controlled. Platform keystores and sibling packages are excluded. The optional hook installer (`provenance install-hooks`) installs only a git pre-commit verification gate and writes a manifest of the options you chose. Usage is described in the build section above.
|
|
76
92
|
|
|
77
93
|
See [SPEC.md](SPEC.md), [TIMESTAMP.md](TIMESTAMP.md), [NOTICE.md](NOTICE.md) and [trust/README.md](trust/README.md). GPG checks the OpenPGP release identity separately. Artifact verification uses raw Ed25519 SPKI PEM keys. Candidate documentation is intentionally unsealed. The root release key is not an agent key.
|
package/SPEC.md
CHANGED
|
@@ -1,176 +1,176 @@
|
|
|
1
|
-
# Attestation Bundle v3 (Extended): Generic Claims, Dual-Hash Binding & Multi-Party Countersigning
|
|
2
|
-
|
|
3
|
-
**Status:** Implemented in `@moonglade/provenance` v2.6.0+ / v3-Extended Engine
|
|
4
|
-
**Author:** Marc Ottley (Moonglade AI)
|
|
5
|
-
**Supersedes:** Bundle v2 Specification (`SPEC.md`)
|
|
6
|
-
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
## 1. Motivation & Design Intent
|
|
10
|
-
|
|
11
|
-
Attestation Bundle v2 solved field binding and domain separation for local-first document attestation:
|
|
12
|
-
```text
|
|
13
|
-
moonglade-provenance/v2\n
|
|
14
|
-
scope:<scope>\n
|
|
15
|
-
sha256:<sha256 of canonical bytes>\n
|
|
16
|
-
signer:<identity>\n
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
While v2 securely bound the document's content hash to its repository scope and signer, real-world systems require attaching **rich, structured domain metadata** that must also be tamper-evident:
|
|
20
|
-
- **Photography & Generative Media:** EXIF camera telemetry, focal length, color space, creator royalty splits, and Creative Commons / commercial licensing terms.
|
|
21
|
-
- **Enterprise PDF & Legal Documents:** Contract identifiers, governing jurisdictions, classification levels, and counterparty entities.
|
|
22
|
-
- **Forensic Science & Chain of Custody:** Evidence acquisition timestamps, laboratory operator IDs, case numbers, and forensic instrument serial numbers.
|
|
23
|
-
- **Agentic AI Governance:** Model version, generation prompt hash, hallucination audit logs, and parent artifact DAG pointers.
|
|
24
|
-
|
|
25
|
-
Attestation Bundle v3 introduces **Generic Signed Claims** with **Dual-Hash Cryptographic Preimage Binding** and **Multi-Party Countersigning**, preserving 100% backward compatibility with v2 bundles.
|
|
26
|
-
|
|
27
|
-
---
|
|
28
|
-
|
|
29
|
-
## 2. Generic Claims Architecture
|
|
30
|
-
|
|
31
|
-
### 2.1 Arbitrary Datatype Support
|
|
32
|
-
Claims are not restricted to predefined strings. Any valid JSON data structure can be used as a claim key or value:
|
|
33
|
-
- **Primitive types:** Strings, numbers (safe integers only; carry a decimal as a string or a scaled integer), booleans, null.
|
|
34
|
-
- **Structured types:** Arrays, nested objects, associative lookup tables.
|
|
35
|
-
|
|
36
|
-
### 2.2 Deterministic Canonicalization (RFC 8785 JCS)
|
|
37
|
-
To ensure that claims serialize identically across different programming languages and runtimes (Node.js, Python, Rust, Go, browser extensions):
|
|
38
|
-
1. **Object Key Sorting:** All object keys are sorted lexicographically by UTF-16 code units.
|
|
39
|
-
2. **Whitespace Stripping:** No whitespace is permitted between keys, colons, commas, or values.
|
|
40
|
-
3. **Number Formatting:** Numbers MUST be IEEE 754 integers within -(2^53-1) to 2^53-1, judged by value (draft -03 section 3.3): `1.0` and `1e2` are accepted and written `1` and `100`, `-0` is written `0`. Non-integral numbers (such as `1.5`), values beyond the safe range, `NaN` and `Infinity` are rejected by the TypeScript, Python and Rust engines alike.
|
|
41
|
-
4. **String Escaping:** Characters below `0x20` are escaped using standard escape sequences (`\\n`, `\\r`, `\\t`) or `\\u00xx`.
|
|
42
|
-
5. **Undefined Exclusion:** Undefined object properties are omitted from serialization.
|
|
43
|
-
|
|
44
|
-
Formally:
|
|
45
|
-
$$\\text{ClaimsHash} = \\text{SHA-256}(\\text{JCS}(\\text{claims}))$$
|
|
46
|
-
|
|
47
|
-
---
|
|
48
|
-
|
|
49
|
-
## 3. The 5-Line Signing Preimage Framing
|
|
50
|
-
|
|
51
|
-
### 3.1 Framing Specification
|
|
52
|
-
The digital signature covers an unambiguous, newline-delimited framing payload that binds both the artifact bytes and the claims:
|
|
53
|
-
|
|
54
|
-
```text
|
|
55
|
-
moonglade-provenance/v3\n
|
|
56
|
-
scope:<scope-uri>\n
|
|
57
|
-
target-sha256:<64-hex of canonical artifact bytes>\n
|
|
58
|
-
signer:<signer-identity>\n
|
|
59
|
-
claims-sha256:<64-hex of JCS canonical claims map>\n
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
### 3.2 ABNF Definition
|
|
63
|
-
|
|
64
|
-
```abnf
|
|
65
|
-
V3-Payload = "moonglade-provenance/v3" LF
|
|
66
|
-
"scope:" Scope-URI LF
|
|
67
|
-
"target-sha256:" 64HEXDIG LF
|
|
68
|
-
"signer:" Identity-String LF
|
|
69
|
-
"claims-sha256:" 64HEXDIG LF
|
|
70
|
-
|
|
71
|
-
Scope-URI = 1*(ALPHA / DIGIT / "-" / "_" / "." / "/" / ":")
|
|
72
|
-
Identity-String = 1*(VCHAR)
|
|
73
|
-
64HEXDIG = 64HEXDIG
|
|
74
|
-
LF = %x0A
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
### 3.3 Security Invariants
|
|
78
|
-
1. **Domain Separation:** The header `"moonglade-provenance/v3\n"` guarantees that v3 signatures cannot be replayed or downgraded to v2 or v1 formats.
|
|
79
|
-
2. **Dual-Hash Binding:** The signature cryptographically binds the canonical artifact bytes (`target-sha256`) and the claims payload (`claims-sha256`) into a single atomic assertion. Tampering with either the artifact OR the claims invalidates the signature.
|
|
80
|
-
3. **Control Character Rejection:** Control characters (`0x00` through `0x1F` and `0x7F`) in any field abort signing immediately, preventing delimiter injection attacks.
|
|
81
|
-
|
|
82
|
-
---
|
|
83
|
-
|
|
84
|
-
## 4. Sidecar Schema (v3)
|
|
85
|
-
|
|
86
|
-
```json
|
|
87
|
-
{
|
|
88
|
-
"standard": "Moonglade Provenance Attestation Bundle v3",
|
|
89
|
-
"artifact": "bathsheba_twilight_raw.jpg",
|
|
90
|
-
"claims": {
|
|
91
|
-
"photographer": "Marc Ottley",
|
|
92
|
-
"focal_length_mm": 85,
|
|
93
|
-
"camera_make": "Sony",
|
|
94
|
-
"camera_model": "ILCE-7RM5",
|
|
95
|
-
"iso": 100,
|
|
96
|
-
"aperture": "1.8",
|
|
97
|
-
"shutter_speed": "1/250",
|
|
98
|
-
"license": "CC-BY-NC-ND-4.0",
|
|
99
|
-
"royalty_split": {
|
|
100
|
-
"photographer_pct": 70,
|
|
101
|
-
"gallery_fund_pct": 30
|
|
102
|
-
},
|
|
103
|
-
"tags": ["bathsheba", "east-coast", "golden-hour", "raw"]
|
|
104
|
-
},
|
|
105
|
-
"claims_sha256": "3c89b27e8d1a49f5...",
|
|
106
|
-
"attestations": [
|
|
107
|
-
{
|
|
108
|
-
"method": "moonglade-ed25519-v3",
|
|
109
|
-
"signer": "marc.ottley@moongladeai.net",
|
|
110
|
-
"scope": "moonglade:vault:gallery/bathsheba_twilight_raw.jpg",
|
|
111
|
-
"target_sha256": "9f83a2...",
|
|
112
|
-
"claims_sha256": "3c89b27e8d1a49f5...",
|
|
113
|
-
"created": "2026-09-14T14:35:00Z",
|
|
114
|
-
"git_commit": "c4d2e1f",
|
|
115
|
-
"signature": "base64-ed25519-sig..."
|
|
116
|
-
},
|
|
117
|
-
{
|
|
118
|
-
"method": "moonglade-ed25519-v3",
|
|
119
|
-
"signer": "gallery.curator@moongladeai.net",
|
|
120
|
-
"scope": "moonglade:vault:gallery/bathsheba_twilight_raw.jpg",
|
|
121
|
-
"target_sha256": "9f83a2...",
|
|
122
|
-
"claims_sha256": "3c89b27e8d1a49f5...",
|
|
123
|
-
"created": "2026-09-14T14:40:00Z",
|
|
124
|
-
"git_commit": "c4d2e1f",
|
|
125
|
-
"signature": "base64-ed25519-countersig..."
|
|
126
|
-
},
|
|
127
|
-
{
|
|
128
|
-
"method": "rfc3161",
|
|
129
|
-
"tier": "L1_CRYPTO_PRIMARY",
|
|
130
|
-
"tsa": "https://freetsa.org/tsr",
|
|
131
|
-
"tsa_time": "2026-09-14T14:35:03Z",
|
|
132
|
-
"token_b64": "MIIB..."
|
|
133
|
-
}
|
|
134
|
-
],
|
|
135
|
-
"sha256_at_last_write": "9f83a2...",
|
|
136
|
-
"updated": "2026-09-14T14:40:00Z",
|
|
137
|
-
"status": "canonical"
|
|
138
|
-
}
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
---
|
|
142
|
-
|
|
143
|
-
## 5. Multi-Party Countersigning Protocol
|
|
144
|
-
|
|
145
|
-
In distributed workflows (e.g. Creator to Publisher, Laboratory Analyst to Quality Inspector):
|
|
146
|
-
1. **Primary Seal:** The creator seals the artifact with their private key, attaching the canonical claims.
|
|
147
|
-
2. **Review & Countersign:**
|
|
148
|
-
- The countersigner reads `<artifact>.provenance.json`.
|
|
149
|
-
- The engine validates that `target_sha256` matches the artifact bytes and `claims_sha256` matches the JCS-canonicalized claims.
|
|
150
|
-
- The countersigner executes `countersignDocument()`:
|
|
151
|
-
$$\\text{Sig}_{\\text{counter}} = \\text{Sign}_{K_{\\text{counter}}}(\\text{V3-Payload})$$
|
|
152
|
-
- Appends the countersignature to `attestations`.
|
|
153
|
-
3. **Independent Keyring Verification:**
|
|
154
|
-
- Recipients pass a keyring mapping signer identities to public keys:
|
|
155
|
-
```typescript
|
|
156
|
-
const keyring = {
|
|
157
|
-
"marc.ottley@moongladeai.net": marcPublicKeyPem,
|
|
158
|
-
"gallery.curator@moongladeai.net": galleryPublicKeyPem
|
|
159
|
-
};
|
|
160
|
-
const result = await ProvenanceEngine.verifyDocument(path, keyring);
|
|
161
|
-
```
|
|
162
|
-
- Every signature in the bundle must verify against its corresponding key. If any key is missing or forged, the verification fails closed.
|
|
163
|
-
|
|
164
|
-
---
|
|
165
|
-
|
|
166
|
-
## 6. Threat Model & Verification Guarantees
|
|
167
|
-
|
|
168
|
-
| Attack Vector | Detection Mechanism | Result |
|
|
169
|
-
| :--- | :--- | :--- |
|
|
170
|
-
| **Artifact Byte Mutation** | Target hash mismatch | `Hash mismatch: Document has been mutated` |
|
|
171
|
-
| **Claims Value Tampering** | Recomputed JCS hash mismatch | `Claims hash mismatch: claims payload has been mutated` |
|
|
172
|
-
| **Claims Substitution** | Preimage `claims-sha256` mismatch | `Signature validation failed` |
|
|
173
|
-
| **Scope Smuggling** | Strict POSIX relative URI binding | `Attestation records a different hash / scope mismatch` |
|
|
174
|
-
| **Signature Downgrade** | Version header framing (`moonglade-provenance/v3`) | Legacy v1/v2 parsers fail closed on version string |
|
|
175
|
-
| **Forged Countersignature** | Keyring verification fails on invalid signature | `Signature validation failed for signer` |
|
|
1
|
+
# Attestation Bundle v3 (Extended): Generic Claims, Dual-Hash Binding & Multi-Party Countersigning
|
|
2
|
+
|
|
3
|
+
**Status:** Implemented in `@moonglade/provenance` v2.6.0+ / v3-Extended Engine
|
|
4
|
+
**Author:** Marc Ottley (Moonglade AI)
|
|
5
|
+
**Supersedes:** Bundle v2 Specification (`SPEC.md`)
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 1. Motivation & Design Intent
|
|
10
|
+
|
|
11
|
+
Attestation Bundle v2 solved field binding and domain separation for local-first document attestation:
|
|
12
|
+
```text
|
|
13
|
+
moonglade-provenance/v2\n
|
|
14
|
+
scope:<scope>\n
|
|
15
|
+
sha256:<sha256 of canonical bytes>\n
|
|
16
|
+
signer:<identity>\n
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
While v2 securely bound the document's content hash to its repository scope and signer, real-world systems require attaching **rich, structured domain metadata** that must also be tamper-evident:
|
|
20
|
+
- **Photography & Generative Media:** EXIF camera telemetry, focal length, color space, creator royalty splits, and Creative Commons / commercial licensing terms.
|
|
21
|
+
- **Enterprise PDF & Legal Documents:** Contract identifiers, governing jurisdictions, classification levels, and counterparty entities.
|
|
22
|
+
- **Forensic Science & Chain of Custody:** Evidence acquisition timestamps, laboratory operator IDs, case numbers, and forensic instrument serial numbers.
|
|
23
|
+
- **Agentic AI Governance:** Model version, generation prompt hash, hallucination audit logs, and parent artifact DAG pointers.
|
|
24
|
+
|
|
25
|
+
Attestation Bundle v3 introduces **Generic Signed Claims** with **Dual-Hash Cryptographic Preimage Binding** and **Multi-Party Countersigning**, preserving 100% backward compatibility with v2 bundles.
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## 2. Generic Claims Architecture
|
|
30
|
+
|
|
31
|
+
### 2.1 Arbitrary Datatype Support
|
|
32
|
+
Claims are not restricted to predefined strings. Any valid JSON data structure can be used as a claim key or value:
|
|
33
|
+
- **Primitive types:** Strings, numbers (safe integers only; carry a decimal as a string or a scaled integer), booleans, null.
|
|
34
|
+
- **Structured types:** Arrays, nested objects, associative lookup tables.
|
|
35
|
+
|
|
36
|
+
### 2.2 Deterministic Canonicalization (RFC 8785 JCS)
|
|
37
|
+
To ensure that claims serialize identically across different programming languages and runtimes (Node.js, Python, Rust, Go, browser extensions):
|
|
38
|
+
1. **Object Key Sorting:** All object keys are sorted lexicographically by UTF-16 code units.
|
|
39
|
+
2. **Whitespace Stripping:** No whitespace is permitted between keys, colons, commas, or values.
|
|
40
|
+
3. **Number Formatting:** Numbers MUST be IEEE 754 integers within -(2^53-1) to 2^53-1, judged by value (draft -03 section 3.3): `1.0` and `1e2` are accepted and written `1` and `100`, `-0` is written `0`. Non-integral numbers (such as `1.5`), values beyond the safe range, `NaN` and `Infinity` are rejected by the TypeScript, Python and Rust engines alike.
|
|
41
|
+
4. **String Escaping:** Characters below `0x20` are escaped using standard escape sequences (`\\n`, `\\r`, `\\t`) or `\\u00xx`.
|
|
42
|
+
5. **Undefined Exclusion:** Undefined object properties are omitted from serialization.
|
|
43
|
+
|
|
44
|
+
Formally:
|
|
45
|
+
$$\\text{ClaimsHash} = \\text{SHA-256}(\\text{JCS}(\\text{claims}))$$
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## 3. The 5-Line Signing Preimage Framing
|
|
50
|
+
|
|
51
|
+
### 3.1 Framing Specification
|
|
52
|
+
The digital signature covers an unambiguous, newline-delimited framing payload that binds both the artifact bytes and the claims:
|
|
53
|
+
|
|
54
|
+
```text
|
|
55
|
+
moonglade-provenance/v3\n
|
|
56
|
+
scope:<scope-uri>\n
|
|
57
|
+
target-sha256:<64-hex of canonical artifact bytes>\n
|
|
58
|
+
signer:<signer-identity>\n
|
|
59
|
+
claims-sha256:<64-hex of JCS canonical claims map>\n
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### 3.2 ABNF Definition
|
|
63
|
+
|
|
64
|
+
```abnf
|
|
65
|
+
V3-Payload = "moonglade-provenance/v3" LF
|
|
66
|
+
"scope:" Scope-URI LF
|
|
67
|
+
"target-sha256:" 64HEXDIG LF
|
|
68
|
+
"signer:" Identity-String LF
|
|
69
|
+
"claims-sha256:" 64HEXDIG LF
|
|
70
|
+
|
|
71
|
+
Scope-URI = 1*(ALPHA / DIGIT / "-" / "_" / "." / "/" / ":")
|
|
72
|
+
Identity-String = 1*(VCHAR)
|
|
73
|
+
64HEXDIG = 64HEXDIG
|
|
74
|
+
LF = %x0A
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### 3.3 Security Invariants
|
|
78
|
+
1. **Domain Separation:** The header `"moonglade-provenance/v3\n"` guarantees that v3 signatures cannot be replayed or downgraded to v2 or v1 formats.
|
|
79
|
+
2. **Dual-Hash Binding:** The signature cryptographically binds the canonical artifact bytes (`target-sha256`) and the claims payload (`claims-sha256`) into a single atomic assertion. Tampering with either the artifact OR the claims invalidates the signature.
|
|
80
|
+
3. **Control Character Rejection:** Control characters (`0x00` through `0x1F` and `0x7F`) in any field abort signing immediately, preventing delimiter injection attacks.
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## 4. Sidecar Schema (v3)
|
|
85
|
+
|
|
86
|
+
```json
|
|
87
|
+
{
|
|
88
|
+
"standard": "Moonglade Provenance Attestation Bundle v3",
|
|
89
|
+
"artifact": "bathsheba_twilight_raw.jpg",
|
|
90
|
+
"claims": {
|
|
91
|
+
"photographer": "Marc Ottley",
|
|
92
|
+
"focal_length_mm": 85,
|
|
93
|
+
"camera_make": "Sony",
|
|
94
|
+
"camera_model": "ILCE-7RM5",
|
|
95
|
+
"iso": 100,
|
|
96
|
+
"aperture": "1.8",
|
|
97
|
+
"shutter_speed": "1/250",
|
|
98
|
+
"license": "CC-BY-NC-ND-4.0",
|
|
99
|
+
"royalty_split": {
|
|
100
|
+
"photographer_pct": 70,
|
|
101
|
+
"gallery_fund_pct": 30
|
|
102
|
+
},
|
|
103
|
+
"tags": ["bathsheba", "east-coast", "golden-hour", "raw"]
|
|
104
|
+
},
|
|
105
|
+
"claims_sha256": "3c89b27e8d1a49f5...",
|
|
106
|
+
"attestations": [
|
|
107
|
+
{
|
|
108
|
+
"method": "moonglade-ed25519-v3",
|
|
109
|
+
"signer": "marc.ottley@moongladeai.net",
|
|
110
|
+
"scope": "moonglade:vault:gallery/bathsheba_twilight_raw.jpg",
|
|
111
|
+
"target_sha256": "9f83a2...",
|
|
112
|
+
"claims_sha256": "3c89b27e8d1a49f5...",
|
|
113
|
+
"created": "2026-09-14T14:35:00Z",
|
|
114
|
+
"git_commit": "c4d2e1f",
|
|
115
|
+
"signature": "base64-ed25519-sig..."
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
"method": "moonglade-ed25519-v3",
|
|
119
|
+
"signer": "gallery.curator@moongladeai.net",
|
|
120
|
+
"scope": "moonglade:vault:gallery/bathsheba_twilight_raw.jpg",
|
|
121
|
+
"target_sha256": "9f83a2...",
|
|
122
|
+
"claims_sha256": "3c89b27e8d1a49f5...",
|
|
123
|
+
"created": "2026-09-14T14:40:00Z",
|
|
124
|
+
"git_commit": "c4d2e1f",
|
|
125
|
+
"signature": "base64-ed25519-countersig..."
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
"method": "rfc3161",
|
|
129
|
+
"tier": "L1_CRYPTO_PRIMARY",
|
|
130
|
+
"tsa": "https://freetsa.org/tsr",
|
|
131
|
+
"tsa_time": "2026-09-14T14:35:03Z",
|
|
132
|
+
"token_b64": "MIIB..."
|
|
133
|
+
}
|
|
134
|
+
],
|
|
135
|
+
"sha256_at_last_write": "9f83a2...",
|
|
136
|
+
"updated": "2026-09-14T14:40:00Z",
|
|
137
|
+
"status": "canonical"
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## 5. Multi-Party Countersigning Protocol
|
|
144
|
+
|
|
145
|
+
In distributed workflows (e.g. Creator to Publisher, Laboratory Analyst to Quality Inspector):
|
|
146
|
+
1. **Primary Seal:** The creator seals the artifact with their private key, attaching the canonical claims.
|
|
147
|
+
2. **Review & Countersign:**
|
|
148
|
+
- The countersigner reads `<artifact>.provenance.json`.
|
|
149
|
+
- The engine validates that `target_sha256` matches the artifact bytes and `claims_sha256` matches the JCS-canonicalized claims.
|
|
150
|
+
- The countersigner executes `countersignDocument()`:
|
|
151
|
+
$$\\text{Sig}_{\\text{counter}} = \\text{Sign}_{K_{\\text{counter}}}(\\text{V3-Payload})$$
|
|
152
|
+
- Appends the countersignature to `attestations`.
|
|
153
|
+
3. **Independent Keyring Verification:**
|
|
154
|
+
- Recipients pass a keyring mapping signer identities to public keys:
|
|
155
|
+
```typescript
|
|
156
|
+
const keyring = {
|
|
157
|
+
"marc.ottley@moongladeai.net": marcPublicKeyPem,
|
|
158
|
+
"gallery.curator@moongladeai.net": galleryPublicKeyPem
|
|
159
|
+
};
|
|
160
|
+
const result = await ProvenanceEngine.verifyDocument(path, keyring);
|
|
161
|
+
```
|
|
162
|
+
- Every signature in the bundle must verify against its corresponding key. If any key is missing or forged, the verification fails closed.
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## 6. Threat Model & Verification Guarantees
|
|
167
|
+
|
|
168
|
+
| Attack Vector | Detection Mechanism | Result |
|
|
169
|
+
| :--- | :--- | :--- |
|
|
170
|
+
| **Artifact Byte Mutation** | Target hash mismatch | `Hash mismatch: Document has been mutated` |
|
|
171
|
+
| **Claims Value Tampering** | Recomputed JCS hash mismatch | `Claims hash mismatch: claims payload has been mutated` |
|
|
172
|
+
| **Claims Substitution** | Preimage `claims-sha256` mismatch | `Signature validation failed` |
|
|
173
|
+
| **Scope Smuggling** | Strict POSIX relative URI binding | `Attestation records a different hash / scope mismatch` |
|
|
174
|
+
| **Signature Downgrade** | Version header framing (`moonglade-provenance/v3`) | Legacy v1/v2 parsers fail closed on version string |
|
|
175
|
+
| **Forged Countersignature** | Keyring verification fails on invalid signature | `Signature validation failed for signer` |
|
|
176
176
|
| **Timestamp Forgery** | RFC 3161 X.509 cert chain and ASN.1 message imprint | `Timestamp token verification failed` |\n
|
package/dist/artifact-cli.js
CHANGED
|
@@ -5,16 +5,21 @@ const index_1 = require("./index");
|
|
|
5
5
|
const artifactHook_1 = require("./artifactHook");
|
|
6
6
|
const self_test_1 = require("./self-test");
|
|
7
7
|
const badge_1 = require("./badge");
|
|
8
|
+
const hookInstaller_1 = require("./hookInstaller");
|
|
8
9
|
async function main() {
|
|
9
10
|
const [, , action, file, dest] = process.argv;
|
|
10
11
|
if (action === '--help' || action === '-h') {
|
|
11
|
-
console.log('provenance <verify|badge|observe|seal-provisional|move|test> <file> [dest]. Keys: MOONGLADE_PUBLIC_KEY_PEM / MOONGLADE_PRIVATE_KEY_PEM. Verification root: MOONGLADE_ROOT or current directory.');
|
|
12
|
+
console.log('provenance <verify|badge|observe|seal-provisional|move|test|install-hooks> <file> [dest]. Keys: MOONGLADE_PUBLIC_KEY_PEM / MOONGLADE_PRIVATE_KEY_PEM. Verification root: MOONGLADE_ROOT or current directory.');
|
|
12
13
|
return;
|
|
13
14
|
}
|
|
14
15
|
if (action === '--version') {
|
|
15
16
|
console.log(require('../package.json').version);
|
|
16
17
|
return;
|
|
17
18
|
}
|
|
19
|
+
if (action === 'install-hooks' || action === 'hooks') {
|
|
20
|
+
process.exitCode = await (0, hookInstaller_1.runInstallHooksCli)(process.argv.slice(3));
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
18
23
|
if (action === 'verify') {
|
|
19
24
|
if (!file || !process.env.MOONGLADE_PUBLIC_KEY_PEM)
|
|
20
25
|
throw new Error('verify requires a file and MOONGLADE_PUBLIC_KEY_PEM');
|
|
@@ -78,7 +83,7 @@ async function main() {
|
|
|
78
83
|
return;
|
|
79
84
|
}
|
|
80
85
|
if (!action || !file || !['observe', 'seal-provisional', 'move'].includes(action)) {
|
|
81
|
-
console.error('usage: provenance <observe|seal-provisional|move|test|badge> <file> [dest]');
|
|
86
|
+
console.error('usage: provenance <observe|seal-provisional|move|test|badge|install-hooks> <file> [dest]');
|
|
82
87
|
process.exitCode = 2;
|
|
83
88
|
return;
|
|
84
89
|
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
export interface HookSelection {
|
|
2
|
+
preCommit: boolean;
|
|
3
|
+
mcpVerifyRead: boolean;
|
|
4
|
+
mcpAutoSeal: boolean;
|
|
5
|
+
}
|
|
6
|
+
export interface InstallOptions extends HookSelection {
|
|
7
|
+
cwd?: string;
|
|
8
|
+
/** Replace a hook that was not installed by this tool, after backing it up. */
|
|
9
|
+
force?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export interface HookResult {
|
|
12
|
+
path: string;
|
|
13
|
+
action: 'created' | 'unchanged' | 'updated' | 'replaced';
|
|
14
|
+
backup?: string;
|
|
15
|
+
}
|
|
16
|
+
export interface InstallResult {
|
|
17
|
+
/** What the manifest now records (selected options merged with earlier recorded state). */
|
|
18
|
+
selection: HookSelection;
|
|
19
|
+
hooksDir: string | null;
|
|
20
|
+
hook: HookResult | null;
|
|
21
|
+
manifestPath: string;
|
|
22
|
+
manifestBackup?: string;
|
|
23
|
+
}
|
|
24
|
+
/** Marker line (line 2 of the generated hook). A hook is ours only if its whole content equals a known generated version. */
|
|
25
|
+
export declare const HOOK_MARKER = "# moonglade-provenance-hook v1";
|
|
26
|
+
export declare class HookConflictError extends Error {
|
|
27
|
+
readonly hookPath: string;
|
|
28
|
+
constructor(hookPath: string);
|
|
29
|
+
}
|
|
30
|
+
export declare function findGitRoot(startDir?: string): Promise<string | null>;
|
|
31
|
+
/**
|
|
32
|
+
* Hooks directory as git itself resolves it (`git rev-parse --git-path hooks`), so linked worktrees,
|
|
33
|
+
* submodules and core.hooksPath work. Falls back to a `.git` directory only when git is not installed.
|
|
34
|
+
* Returns null when cwd is not inside a repository.
|
|
35
|
+
*/
|
|
36
|
+
export declare function resolveHooksDir(cwd?: string): Promise<string | null>;
|
|
37
|
+
/**
|
|
38
|
+
* The generated pre-commit hook. Written for bash (not POSIX sh): it reads NUL-delimited names (`read -d ''`)
|
|
39
|
+
* so file names with spaces, non-ASCII characters (git would quote them otherwise) or newlines are all checked.
|
|
40
|
+
* Fail closed: if the staged list cannot be read, the commit is blocked.
|
|
41
|
+
*/
|
|
42
|
+
export declare const PRE_COMMIT_SCRIPT = "#!/usr/bin/env bash\n# moonglade-provenance-hook v1\n# Moonglade Provenance - Git Pre-Commit Verification Gate (requires bash)\n# Blocks the commit when a staged artifact that has a provenance sidecar no longer matches it.\n\necho \"Moonglade Provenance: Verifying staged artifacts...\"\n\nTMP_LIST=$(mktemp) || exit 1\ntrap 'rm -f \"$TMP_LIST\"' EXIT\nif ! git diff --cached --name-only -z --diff-filter=ACMR > \"$TMP_LIST\"; then\n echo \"Moonglade Provenance: could not list staged files; commit aborted.\"\n exit 1\nfi\n\nFAILED=0\nwhile IFS= read -r -d '' FILE; do\n case \"$FILE\" in\n *.provenance.json|*.provenance-registry.json) continue ;;\n *.md|*.csv|*.json|*.sql|*.yml|*.yaml) ;;\n *) continue ;;\n esac\n [ -f \"$FILE\" ] || continue\n [ -f \"$FILE.provenance.json\" ] || continue\n TARGET=\"$FILE\"\n case \"$TARGET\" in -*) TARGET=\"./$TARGET\" ;; esac\n if ! npx --no-install @moonglade/provenance badge \"$TARGET\" --short < /dev/null > /dev/null 2>&1; then\n echo \"[PROVENANCE DRIFT] $FILE has mutated or failed verification!\"\n FAILED=1\n fi\ndone < \"$TMP_LIST\"\n\nif [ \"$FAILED\" -ne 0 ]; then\n echo \"\"\n echo \"Commit aborted by Moonglade Provenance Gate.\"\n echo \"Please re-seal mutated artifacts before committing:\"\n echo \" npx @moonglade/provenance seal-provisional <file>\"\n echo \"\"\n exit 1\nfi\n\necho \"All staged artifacts verified.\"\nexit 0\n";
|
|
43
|
+
/**
|
|
44
|
+
* Install one hook script. An existing hook is replaced only if its content equals a known generated version
|
|
45
|
+
* (`known`, default: the shipped hook). Anything else, including our own hook with an edit or a foreign hook
|
|
46
|
+
* that merely mentions the marker, is refused unless `force`, and a forced replace is backed up first.
|
|
47
|
+
*/
|
|
48
|
+
export declare function installGitHook(hooksDir: string, hookName: string, scriptContent: string, opts?: {
|
|
49
|
+
force?: boolean;
|
|
50
|
+
known?: string[];
|
|
51
|
+
}): Promise<HookResult>;
|
|
52
|
+
/**
|
|
53
|
+
* Write the manifest. An existing manifest that is not exactly in the shape this tool writes is backed up first.
|
|
54
|
+
*/
|
|
55
|
+
export declare function writeProvenanceConfig(dir: string, selection: HookSelection): Promise<{
|
|
56
|
+
path: string;
|
|
57
|
+
backup?: string;
|
|
58
|
+
}>;
|
|
59
|
+
/**
|
|
60
|
+
* Install exactly the selected gates. Nothing is enabled that was not selected. The manifest records the
|
|
61
|
+
* selected options merged with earlier recorded MCP choices (never switched off silently) and with reality:
|
|
62
|
+
* preCommit is true when an unmodified generated pre-commit hook is installed.
|
|
63
|
+
*/
|
|
64
|
+
export declare function installHooks(opts: InstallOptions): Promise<InstallResult>;
|
|
65
|
+
export declare const HOOK_FLAGS: readonly ["--pre-commit", "--mcp-verify-read", "--mcp-auto-seal", "--force"];
|
|
66
|
+
export declare const HOOK_USAGE: string;
|
|
67
|
+
/** CLI entry. Returns the process exit code. */
|
|
68
|
+
export declare function runInstallHooksCli(args: string[], cwd?: string, io?: PromptIo): Promise<number>;
|
|
69
|
+
interface PromptIo {
|
|
70
|
+
input?: NodeJS.ReadableStream;
|
|
71
|
+
output?: NodeJS.WritableStream;
|
|
72
|
+
/** Force interactive (true) or non-interactive (false) mode; default is whether stdin and stdout are terminals. */
|
|
73
|
+
interactive?: boolean;
|
|
74
|
+
}
|
|
75
|
+
export {};
|
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.HOOK_USAGE = exports.HOOK_FLAGS = exports.PRE_COMMIT_SCRIPT = exports.HookConflictError = exports.HOOK_MARKER = void 0;
|
|
37
|
+
exports.findGitRoot = findGitRoot;
|
|
38
|
+
exports.resolveHooksDir = resolveHooksDir;
|
|
39
|
+
exports.installGitHook = installGitHook;
|
|
40
|
+
exports.writeProvenanceConfig = writeProvenanceConfig;
|
|
41
|
+
exports.installHooks = installHooks;
|
|
42
|
+
exports.runInstallHooksCli = runInstallHooksCli;
|
|
43
|
+
const fs = __importStar(require("fs/promises"));
|
|
44
|
+
const path = __importStar(require("path"));
|
|
45
|
+
const readline = __importStar(require("readline"));
|
|
46
|
+
const child_process_1 = require("child_process");
|
|
47
|
+
/** Marker line (line 2 of the generated hook). A hook is ours only if its whole content equals a known generated version. */
|
|
48
|
+
exports.HOOK_MARKER = '# moonglade-provenance-hook v1';
|
|
49
|
+
class HookConflictError extends Error {
|
|
50
|
+
hookPath;
|
|
51
|
+
constructor(hookPath) {
|
|
52
|
+
super(`${hookPath} already exists and is not an unmodified hook generated by Moonglade Provenance (it may be foreign or edited). It was left unchanged. Re-run with --force to back it up and replace it.`);
|
|
53
|
+
this.hookPath = hookPath;
|
|
54
|
+
this.name = 'HookConflictError';
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
exports.HookConflictError = HookConflictError;
|
|
58
|
+
function git(cwd, args) {
|
|
59
|
+
return new Promise(resolve => {
|
|
60
|
+
(0, child_process_1.execFile)('git', args, { cwd, windowsHide: true }, (err, stdout) => {
|
|
61
|
+
if (err)
|
|
62
|
+
resolve({ ok: false, missing: err.code === 'ENOENT', out: '' });
|
|
63
|
+
else
|
|
64
|
+
resolve({ ok: true, missing: false, out: String(stdout).trim() });
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
async function findGitRoot(startDir = process.cwd()) {
|
|
69
|
+
let curr = path.resolve(startDir);
|
|
70
|
+
while (true) {
|
|
71
|
+
try {
|
|
72
|
+
const stat = await fs.stat(path.join(curr, '.git'));
|
|
73
|
+
if (stat.isDirectory())
|
|
74
|
+
return curr;
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
// not in this dir
|
|
78
|
+
}
|
|
79
|
+
const parent = path.dirname(curr);
|
|
80
|
+
if (parent === curr)
|
|
81
|
+
return null;
|
|
82
|
+
curr = parent;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Hooks directory as git itself resolves it (`git rev-parse --git-path hooks`), so linked worktrees,
|
|
87
|
+
* submodules and core.hooksPath work. Falls back to a `.git` directory only when git is not installed.
|
|
88
|
+
* Returns null when cwd is not inside a repository.
|
|
89
|
+
*/
|
|
90
|
+
async function resolveHooksDir(cwd = process.cwd()) {
|
|
91
|
+
const r = await git(cwd, ['rev-parse', '--git-path', 'hooks']);
|
|
92
|
+
if (r.ok)
|
|
93
|
+
return path.resolve(cwd, r.out);
|
|
94
|
+
if (r.missing) {
|
|
95
|
+
const root = await findGitRoot(cwd);
|
|
96
|
+
return root ? path.join(root, '.git', 'hooks') : null;
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
async function resolveTopLevel(cwd) {
|
|
101
|
+
const r = await git(cwd, ['rev-parse', '--show-toplevel']);
|
|
102
|
+
return r.ok ? path.resolve(cwd, r.out) : path.resolve(cwd);
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* The generated pre-commit hook. Written for bash (not POSIX sh): it reads NUL-delimited names (`read -d ''`)
|
|
106
|
+
* so file names with spaces, non-ASCII characters (git would quote them otherwise) or newlines are all checked.
|
|
107
|
+
* Fail closed: if the staged list cannot be read, the commit is blocked.
|
|
108
|
+
*/
|
|
109
|
+
exports.PRE_COMMIT_SCRIPT = `#!/usr/bin/env bash
|
|
110
|
+
${exports.HOOK_MARKER}
|
|
111
|
+
# Moonglade Provenance - Git Pre-Commit Verification Gate (requires bash)
|
|
112
|
+
# Blocks the commit when a staged artifact that has a provenance sidecar no longer matches it.
|
|
113
|
+
|
|
114
|
+
echo "Moonglade Provenance: Verifying staged artifacts..."
|
|
115
|
+
|
|
116
|
+
TMP_LIST=$(mktemp) || exit 1
|
|
117
|
+
trap 'rm -f "$TMP_LIST"' EXIT
|
|
118
|
+
if ! git diff --cached --name-only -z --diff-filter=ACMR > "$TMP_LIST"; then
|
|
119
|
+
echo "Moonglade Provenance: could not list staged files; commit aborted."
|
|
120
|
+
exit 1
|
|
121
|
+
fi
|
|
122
|
+
|
|
123
|
+
FAILED=0
|
|
124
|
+
while IFS= read -r -d '' FILE; do
|
|
125
|
+
case "$FILE" in
|
|
126
|
+
*.provenance.json|*.provenance-registry.json) continue ;;
|
|
127
|
+
*.md|*.csv|*.json|*.sql|*.yml|*.yaml) ;;
|
|
128
|
+
*) continue ;;
|
|
129
|
+
esac
|
|
130
|
+
[ -f "$FILE" ] || continue
|
|
131
|
+
[ -f "$FILE.provenance.json" ] || continue
|
|
132
|
+
TARGET="$FILE"
|
|
133
|
+
case "$TARGET" in -*) TARGET="./$TARGET" ;; esac
|
|
134
|
+
if ! npx --no-install @moonglade/provenance badge "$TARGET" --short < /dev/null > /dev/null 2>&1; then
|
|
135
|
+
echo "[PROVENANCE DRIFT] $FILE has mutated or failed verification!"
|
|
136
|
+
FAILED=1
|
|
137
|
+
fi
|
|
138
|
+
done < "$TMP_LIST"
|
|
139
|
+
|
|
140
|
+
if [ "$FAILED" -ne 0 ]; then
|
|
141
|
+
echo ""
|
|
142
|
+
echo "Commit aborted by Moonglade Provenance Gate."
|
|
143
|
+
echo "Please re-seal mutated artifacts before committing:"
|
|
144
|
+
echo " npx @moonglade/provenance seal-provisional <file>"
|
|
145
|
+
echo ""
|
|
146
|
+
exit 1
|
|
147
|
+
fi
|
|
148
|
+
|
|
149
|
+
echo "All staged artifacts verified."
|
|
150
|
+
exit 0
|
|
151
|
+
`;
|
|
152
|
+
/** Generated hook contents this tool has shipped. A hook is ours only if its content equals one of these exactly. */
|
|
153
|
+
const KNOWN_GENERATED_HOOKS = [exports.PRE_COMMIT_SCRIPT];
|
|
154
|
+
function backupStamp() {
|
|
155
|
+
return new Date().toISOString().replace(/\.\d+Z$/, 'Z').replace(/[-:]/g, '');
|
|
156
|
+
}
|
|
157
|
+
/** Copy a file to `<file>.bak-<UTC timestamp>` (exclusive create, counter suffix on collision). */
|
|
158
|
+
async function backupFile(file) {
|
|
159
|
+
const stamp = backupStamp();
|
|
160
|
+
for (let i = 0;; i++) {
|
|
161
|
+
const candidate = `${file}.bak-${stamp}${i ? '-' + i : ''}`;
|
|
162
|
+
try {
|
|
163
|
+
await fs.copyFile(file, candidate, 1 /* COPYFILE_EXCL */);
|
|
164
|
+
return candidate;
|
|
165
|
+
}
|
|
166
|
+
catch (e) {
|
|
167
|
+
if (e.code !== 'EEXIST')
|
|
168
|
+
throw e;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Install one hook script. An existing hook is replaced only if its content equals a known generated version
|
|
174
|
+
* (`known`, default: the shipped hook). Anything else, including our own hook with an edit or a foreign hook
|
|
175
|
+
* that merely mentions the marker, is refused unless `force`, and a forced replace is backed up first.
|
|
176
|
+
*/
|
|
177
|
+
async function installGitHook(hooksDir, hookName, scriptContent, opts = {}) {
|
|
178
|
+
await fs.mkdir(hooksDir, { recursive: true });
|
|
179
|
+
const hookPath = path.join(hooksDir, hookName);
|
|
180
|
+
const known = opts.known ?? KNOWN_GENERATED_HOOKS;
|
|
181
|
+
let existing = null;
|
|
182
|
+
try {
|
|
183
|
+
existing = await fs.readFile(hookPath, 'utf8');
|
|
184
|
+
}
|
|
185
|
+
catch (e) {
|
|
186
|
+
if (e.code !== 'ENOENT')
|
|
187
|
+
throw e;
|
|
188
|
+
}
|
|
189
|
+
let action = 'created';
|
|
190
|
+
let backup;
|
|
191
|
+
if (existing !== null) {
|
|
192
|
+
if (existing === scriptContent) {
|
|
193
|
+
action = 'unchanged';
|
|
194
|
+
}
|
|
195
|
+
else if (known.includes(existing)) {
|
|
196
|
+
action = 'updated';
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
if (!opts.force)
|
|
200
|
+
throw new HookConflictError(hookPath);
|
|
201
|
+
action = 'replaced';
|
|
202
|
+
backup = await backupFile(hookPath);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (action !== 'unchanged') {
|
|
206
|
+
await fs.writeFile(hookPath, scriptContent, { mode: 0o755 });
|
|
207
|
+
}
|
|
208
|
+
await fs.chmod(hookPath, 0o755);
|
|
209
|
+
return backup ? { path: hookPath, action, backup } : { path: hookPath, action };
|
|
210
|
+
}
|
|
211
|
+
function manifestPayload(selection) {
|
|
212
|
+
const payload = {
|
|
213
|
+
standard: MANIFEST_STANDARD,
|
|
214
|
+
hooks: selection
|
|
215
|
+
};
|
|
216
|
+
if (selection.mcpVerifyRead || selection.mcpAutoSeal) {
|
|
217
|
+
payload.mcpServer = { command: 'npx', args: ['-y', '@moonglade/provenance', 'provenance-gate'] };
|
|
218
|
+
}
|
|
219
|
+
return payload;
|
|
220
|
+
}
|
|
221
|
+
/** Previously recorded choices from an existing manifest, or null when absent or unreadable. */
|
|
222
|
+
async function readRecordedHooks(file) {
|
|
223
|
+
let raw;
|
|
224
|
+
try {
|
|
225
|
+
raw = await fs.readFile(file, 'utf8');
|
|
226
|
+
}
|
|
227
|
+
catch (e) {
|
|
228
|
+
if (e.code === 'ENOENT')
|
|
229
|
+
return { parsed: null, raw: null };
|
|
230
|
+
throw e;
|
|
231
|
+
}
|
|
232
|
+
try {
|
|
233
|
+
const parsed = JSON.parse(raw);
|
|
234
|
+
return { parsed: parsed && typeof parsed === 'object' ? parsed : null, raw };
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
return { parsed: null, raw };
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
const MANIFEST_STANDARD = 'Moonglade Provenance Hook Manifest v1';
|
|
241
|
+
/** True only for a manifest of exactly the shape this tool writes. Anything else (user keys, other standard, bad JSON) is backed up before a rewrite. */
|
|
242
|
+
function isOurManifest(parsed) {
|
|
243
|
+
if (!parsed || parsed.standard !== MANIFEST_STANDARD)
|
|
244
|
+
return false;
|
|
245
|
+
const allowed = new Set(['standard', 'installedAt', 'hooks', 'mcpServer']);
|
|
246
|
+
if (!Object.keys(parsed).every(k => allowed.has(k)))
|
|
247
|
+
return false;
|
|
248
|
+
const hooks = parsed.hooks;
|
|
249
|
+
if (!hooks || typeof hooks !== 'object')
|
|
250
|
+
return false;
|
|
251
|
+
const hk = new Set(['preCommit', 'mcpVerifyRead', 'mcpAutoSeal']);
|
|
252
|
+
return Object.keys(hooks).every(k => hk.has(k) && typeof hooks[k] === 'boolean');
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Write the manifest. An existing manifest that is not exactly in the shape this tool writes is backed up first.
|
|
256
|
+
*/
|
|
257
|
+
async function writeProvenanceConfig(dir, selection) {
|
|
258
|
+
const configPath = path.join(dir, '.provenance-hooks.json');
|
|
259
|
+
const payload = manifestPayload(selection);
|
|
260
|
+
const { parsed, raw } = await readRecordedHooks(configPath);
|
|
261
|
+
let backup;
|
|
262
|
+
if (raw !== null && !isOurManifest(parsed))
|
|
263
|
+
backup = await backupFile(configPath);
|
|
264
|
+
const out = { standard: payload.standard, installedAt: new Date().toISOString(), ...payload };
|
|
265
|
+
await fs.writeFile(configPath, JSON.stringify(out, null, 2) + '\n', 'utf8');
|
|
266
|
+
return backup ? { path: configPath, backup } : { path: configPath };
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Install exactly the selected gates. Nothing is enabled that was not selected. The manifest records the
|
|
270
|
+
* selected options merged with earlier recorded MCP choices (never switched off silently) and with reality:
|
|
271
|
+
* preCommit is true when an unmodified generated pre-commit hook is installed.
|
|
272
|
+
*/
|
|
273
|
+
async function installHooks(opts) {
|
|
274
|
+
const cwd = path.resolve(opts.cwd ?? process.cwd());
|
|
275
|
+
const selected = {
|
|
276
|
+
preCommit: opts.preCommit === true,
|
|
277
|
+
mcpVerifyRead: opts.mcpVerifyRead === true,
|
|
278
|
+
mcpAutoSeal: opts.mcpAutoSeal === true
|
|
279
|
+
};
|
|
280
|
+
let hooksDir = null;
|
|
281
|
+
let hook = null;
|
|
282
|
+
if (selected.preCommit) {
|
|
283
|
+
hooksDir = await resolveHooksDir(cwd);
|
|
284
|
+
if (!hooksDir)
|
|
285
|
+
throw new Error(`pre-commit hook requested but ${cwd} is not inside a git repository`);
|
|
286
|
+
hook = await installGitHook(hooksDir, 'pre-commit', exports.PRE_COMMIT_SCRIPT, { force: opts.force });
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
hooksDir = await resolveHooksDir(cwd);
|
|
290
|
+
}
|
|
291
|
+
const top = await resolveTopLevel(cwd);
|
|
292
|
+
const recorded = (await readRecordedHooks(path.join(top, '.provenance-hooks.json'))).parsed?.hooks;
|
|
293
|
+
let installed = false;
|
|
294
|
+
if (hooksDir) {
|
|
295
|
+
try {
|
|
296
|
+
installed = KNOWN_GENERATED_HOOKS.includes(await fs.readFile(path.join(hooksDir, 'pre-commit'), 'utf8'));
|
|
297
|
+
}
|
|
298
|
+
catch { /* no hook */ }
|
|
299
|
+
}
|
|
300
|
+
const selection = {
|
|
301
|
+
preCommit: selected.preCommit || installed,
|
|
302
|
+
mcpVerifyRead: selected.mcpVerifyRead || recorded?.mcpVerifyRead === true,
|
|
303
|
+
mcpAutoSeal: selected.mcpAutoSeal || recorded?.mcpAutoSeal === true
|
|
304
|
+
};
|
|
305
|
+
const manifest = await writeProvenanceConfig(top, selection);
|
|
306
|
+
return { selection, hooksDir, hook, manifestPath: manifest.path, manifestBackup: manifest.backup };
|
|
307
|
+
}
|
|
308
|
+
exports.HOOK_FLAGS = ['--pre-commit', '--mcp-verify-read', '--mcp-auto-seal', '--force'];
|
|
309
|
+
exports.HOOK_USAGE = 'usage: provenance install-hooks [--pre-commit] [--mcp-verify-read] [--mcp-auto-seal] [--force]\n' +
|
|
310
|
+
' --pre-commit install the git pre-commit verification gate\n' +
|
|
311
|
+
' --mcp-verify-read record the fail-closed verify-on-read MCP option\n' +
|
|
312
|
+
' --mcp-auto-seal record the provisional auto-seal-on-agent-write MCP option\n' +
|
|
313
|
+
' --force replace an existing hook that is not ours, after backing it up to <hook>.bak-<UTC timestamp>\n' +
|
|
314
|
+
'With no choice flag, the command asks about each option when run in a terminal and does nothing otherwise.';
|
|
315
|
+
/** CLI entry. Returns the process exit code. */
|
|
316
|
+
async function runInstallHooksCli(args, cwd = process.cwd(), io = {}) {
|
|
317
|
+
const unknown = args.filter(a => !exports.HOOK_FLAGS.includes(a));
|
|
318
|
+
if (unknown.length) {
|
|
319
|
+
console.error(`unknown argument: ${unknown.join(' ')}\n${exports.HOOK_USAGE}`);
|
|
320
|
+
return 2;
|
|
321
|
+
}
|
|
322
|
+
const has = (f) => args.includes(f);
|
|
323
|
+
let selection = { preCommit: has('--pre-commit'), mcpVerifyRead: has('--mcp-verify-read'), mcpAutoSeal: has('--mcp-auto-seal') };
|
|
324
|
+
if (!selection.preCommit && !selection.mcpVerifyRead && !selection.mcpAutoSeal) {
|
|
325
|
+
const interactive = io.interactive ?? (Boolean(process.stdin.isTTY) && Boolean(process.stdout.isTTY));
|
|
326
|
+
if (!interactive) {
|
|
327
|
+
console.error(`no option selected and no terminal to ask.\n${exports.HOOK_USAGE}`);
|
|
328
|
+
return 2;
|
|
329
|
+
}
|
|
330
|
+
try {
|
|
331
|
+
selection = await askInteractively(io);
|
|
332
|
+
}
|
|
333
|
+
catch (e) {
|
|
334
|
+
if (e instanceof PromptCancelled) {
|
|
335
|
+
console.error(`install-hooks ${e.message}.`);
|
|
336
|
+
return 1;
|
|
337
|
+
}
|
|
338
|
+
throw e;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
try {
|
|
342
|
+
const r = await installHooks({ cwd, ...selection, force: has('--force') });
|
|
343
|
+
if (r.hook) {
|
|
344
|
+
console.log(`${r.hook.action}: ${r.hook.path}`);
|
|
345
|
+
if (r.hook.backup)
|
|
346
|
+
console.log(`backup of previous hook: ${r.hook.backup}`);
|
|
347
|
+
}
|
|
348
|
+
console.log(`manifest: ${r.manifestPath}`);
|
|
349
|
+
if (r.manifestBackup)
|
|
350
|
+
console.log(`backup of previous manifest: ${r.manifestBackup}`);
|
|
351
|
+
console.log(`enabled: ${JSON.stringify(r.selection)}`);
|
|
352
|
+
if (selection.mcpVerifyRead || selection.mcpAutoSeal) {
|
|
353
|
+
console.log('MCP configuration snippet:');
|
|
354
|
+
console.log(JSON.stringify({ mcpServers: { 'provenance-gate': { command: 'npx', args: ['-y', '@moonglade/provenance', 'provenance-gate'] } } }, null, 2));
|
|
355
|
+
}
|
|
356
|
+
return 0;
|
|
357
|
+
}
|
|
358
|
+
catch (e) {
|
|
359
|
+
console.error(e.message);
|
|
360
|
+
return 1;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
class PromptCancelled extends Error {
|
|
364
|
+
constructor() {
|
|
365
|
+
super('cancelled, nothing changed');
|
|
366
|
+
this.name = 'PromptCancelled';
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
async function askInteractively(io = {}) {
|
|
370
|
+
const rl = readline.createInterface({ input: io.input ?? process.stdin, output: io.output ?? process.stdout });
|
|
371
|
+
// Queue lines from the start so piped input delivered in one chunk is not lost; end of input cancels.
|
|
372
|
+
const lines = [];
|
|
373
|
+
let waiter = null;
|
|
374
|
+
let closed = false;
|
|
375
|
+
rl.on('line', l => { if (waiter) {
|
|
376
|
+
const w = waiter;
|
|
377
|
+
waiter = null;
|
|
378
|
+
w.resolve(l);
|
|
379
|
+
}
|
|
380
|
+
else
|
|
381
|
+
lines.push(l); });
|
|
382
|
+
rl.on('close', () => { closed = true; if (waiter) {
|
|
383
|
+
const w = waiter;
|
|
384
|
+
waiter = null;
|
|
385
|
+
w.reject(new PromptCancelled());
|
|
386
|
+
} });
|
|
387
|
+
const nextLine = () => new Promise((resolve, reject) => {
|
|
388
|
+
if (lines.length)
|
|
389
|
+
return resolve(lines.shift());
|
|
390
|
+
if (closed)
|
|
391
|
+
return reject(new PromptCancelled());
|
|
392
|
+
waiter = { resolve, reject };
|
|
393
|
+
});
|
|
394
|
+
const ask = async (q, def) => {
|
|
395
|
+
rl.setPrompt(`${q} (${def ? 'Y/n' : 'y/N'}): `);
|
|
396
|
+
rl.prompt();
|
|
397
|
+
const t = (await nextLine()).trim().toLowerCase();
|
|
398
|
+
return t === '' ? def : t === 'y' || t === 'yes';
|
|
399
|
+
};
|
|
400
|
+
try {
|
|
401
|
+
return {
|
|
402
|
+
preCommit: await ask('Install the git pre-commit verification gate?', true),
|
|
403
|
+
mcpVerifyRead: await ask('Record the fail-closed verify-on-read MCP option?', false),
|
|
404
|
+
mcpAutoSeal: await ask('Record the provisional auto-seal-on-agent-write MCP option?', false)
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
finally {
|
|
408
|
+
rl.close();
|
|
409
|
+
}
|
|
410
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@moonglade/provenance",
|
|
3
|
-
"version": "2.6.
|
|
3
|
+
"version": "2.6.1",
|
|
4
4
|
"description": "Local-first cryptographic proof for markdown files and agentic AI memory substrates.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -67,6 +67,11 @@
|
|
|
67
67
|
"require": "./dist/index.js",
|
|
68
68
|
"default": "./dist/index.js"
|
|
69
69
|
},
|
|
70
|
+
"./self-test": {
|
|
71
|
+
"types": "./dist/self-test.d.ts",
|
|
72
|
+
"require": "./dist/self-test.js",
|
|
73
|
+
"default": "./dist/self-test.js"
|
|
74
|
+
},
|
|
70
75
|
"./wasm": {
|
|
71
76
|
"types": "./wasm/mgc_wasm.d.ts",
|
|
72
77
|
"require": "./wasm/mgc_wasm.js",
|
package/trust/root-public.asc
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
|
2
|
-
|
|
3
|
-
mDMEapbUuBYJKwYBBAHaRw8BAQdAULJ9to5djqb63TWkuBsylWfKgrujIbevcM5L
|
|
4
|
-
acOhv1C0O01hcmMgT3R0bGV5IChGb3VuZGVyLCBNb29uZ2xhZGUgQUkpIDxtb29u
|
|
5
|
-
Z2xhZGVhaUBnbWFpbC5jb20+iJkEExYKAEEWIQRef6n2xbCmW8I6dXBP8sLeacDE
|
|
6
|
-
owUCapbUuAIbIwUJBaOagAULCQgHAgIiAgYVCgkICwIEFgIDAQIeBwIXgAAKCRBP
|
|
7
|
-
8sLeacDEowfNAQC1o+XcHAHwhpc1MMSZClFb/ylagyPo7Y7pdJkgPRWHzAEA3Owi
|
|
8
|
-
zID5JKGznM+xVhNA7LaiHIYMEKx3KgLJbsUeRAc=
|
|
9
|
-
=gFm3
|
|
10
|
-
-----END PGP PUBLIC KEY BLOCK-----
|
|
1
|
+
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
|
2
|
+
|
|
3
|
+
mDMEapbUuBYJKwYBBAHaRw8BAQdAULJ9to5djqb63TWkuBsylWfKgrujIbevcM5L
|
|
4
|
+
acOhv1C0O01hcmMgT3R0bGV5IChGb3VuZGVyLCBNb29uZ2xhZGUgQUkpIDxtb29u
|
|
5
|
+
Z2xhZGVhaUBnbWFpbC5jb20+iJkEExYKAEEWIQRef6n2xbCmW8I6dXBP8sLeacDE
|
|
6
|
+
owUCapbUuAIbIwUJBaOagAULCQgHAgIiAgYVCgkICwIEFgIDAQIeBwIXgAAKCRBP
|
|
7
|
+
8sLeacDEowfNAQC1o+XcHAHwhpc1MMSZClFb/ylagyPo7Y7pdJkgPRWHzAEA3Owi
|
|
8
|
+
zID5JKGznM+xVhNA7LaiHIYMEKx3KgLJbsUeRAc=
|
|
9
|
+
=gFm3
|
|
10
|
+
-----END PGP PUBLIC KEY BLOCK-----
|
package/wasm/mgc_wasm.js
CHANGED
|
@@ -380,8 +380,7 @@ if (!('encodeInto' in cachedTextEncoder)) {
|
|
|
380
380
|
|
|
381
381
|
let WASM_VECTOR_LEN = 0;
|
|
382
382
|
|
|
383
|
-
const
|
|
384
|
-
const wasmPath = path.join(__dirname, 'mgc_wasm_bg.wasm');
|
|
383
|
+
const wasmPath = `${__dirname}/mgc_wasm_bg.wasm`;
|
|
385
384
|
const wasmBytes = require('fs').readFileSync(wasmPath);
|
|
386
385
|
const wasmModule = new WebAssembly.Module(wasmBytes);
|
|
387
386
|
let wasmInstance = new WebAssembly.Instance(wasmModule, __wbg_get_imports());
|