@nexart/cli 0.16.0 → 1.0.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/CHANGELOG.md CHANGED
@@ -1,5 +1,75 @@
1
1
  # @nexart/cli — Changelog
2
2
 
3
+ ## v1.0.0 (2026-06-18) — First stable release (includes `verify-offline`)
4
+
5
+ > **Stabilisation release.** Marks the `@nexart/cli` command surface stable under
6
+ > semantic versioning. No cryptographic, hashing, canonicalisation, or exit-code
7
+ > change. Builds on `@nexart/ai-execution@^1.0.0` and `@nexart/codemode-sdk@^1.12.0`,
8
+ > both stable. The CLI continues to implement **zero** independent verification logic —
9
+ > it delegates entirely to the SDK verification core. The `verify-offline` command
10
+ > introduced for this release (below) is the only functional addition.
11
+
12
+ ### Included in this release
13
+ The new `verify-offline` command — a UX-only entry point over the existing offline
14
+ verification path:
15
+
16
+ - **New `verify-offline <record.json>` command.** A convenience alias for
17
+ `verify-record --offline`: it runs the **same** verification core (integrity via
18
+ certificate-hash recompute, Ed25519 receipt signature, timestamp reporting) and
19
+ **never contacts a NexArt node**. The signing key comes from `--keys` (an alias for
20
+ `--public-key`) or a key embedded in the record. Reads from a file path or `-` (stdin).
21
+ - **Offline report labelling.** A passing record prints
22
+ **`FINAL STATUS: AUTHENTIC (OFFLINE)`** with an explicit **`Mode: Offline Verification`**
23
+ line (the redaction note is still appended for privacy-redacted records). A real
24
+ integrity mismatch or invalid signature stays **`FINAL STATUS: FAILED`** with exit `1`.
25
+ `--json` gains an additive `"mode": "offline"` field; online output is unchanged.
26
+ - **No new verification logic.** Hashing, the version-dispatched `canonicalForProtocol()`
27
+ canonicalisation, the 3-state core, and exit codes are byte-for-byte identical to
28
+ `verify-record`. As before, a missing or unverifiable timestamp is *reported* but never
29
+ gates the verdict or exit code. `verify-bundle` is untouched.
30
+
31
+ ## v0.16.1 (2026-06-17) — `verify-record`: 3-state verification (VERIFIED / AUTHENTIC (REDACTED) / FAILED)
32
+
33
+ UX release. `verify-bundle` is **byte-for-byte unchanged**; the new third state applies
34
+ to `verify-record` (and the `verify-url` / `verify-hash` commands that render through it).
35
+
36
+ - **Privacy-redacted records are no longer reported as FAILED.** A public record served
37
+ from a node may have the snapshot's `input` / `output` *content* stripped while the
38
+ `inputHash` / `outputHash` anchors are kept. Because `certificateHash` is sealed over the
39
+ full snapshot, recompute is impossible on a redacted snapshot — but that is **not
40
+ tampering**. Such records now report **`FINAL STATUS: AUTHENTIC (REDACTED)`** with
41
+ **`⚠ Integrity: NOT RECOMPUTABLE (redacted)`** and **exit 0**, provided the receipt
42
+ signature still verifies.
43
+ - **Three exit-stable states.** `VERIFIED` (exit 0) → content present, hash recomputes;
44
+ `AUTHENTIC (REDACTED)` (exit 0) → content stripped, hashes kept, signature passes;
45
+ `FAILED` (exit 1) → a real content mismatch or an invalid signature. A genuine mismatch
46
+ is **never** softened to redacted, and a redacted record whose hash still recomputes
47
+ stays **VERIFIED**.
48
+ - **Report.** For a redacted record the Signature line is shown before the Integrity line,
49
+ and a short **Public Verification (Redacted)** note explains that full integrity
50
+ verification requires the original (un-redacted) execution data. `--short` now emits
51
+ `✔ VERIFIED` / `⚠ AUTHENTIC (REDACTED)` / `✖ FAILED`. `--json` gains a `redacted`
52
+ boolean, an `authentic-redacted` `status`, and a `redacted` `integrity` value.
53
+ - **No cryptographic change.** `certificateHash` computation, canonicalisation, and the
54
+ signature / timestamp logic are untouched — this is purely a classification and
55
+ presentation layer. `verify-bundle` does not opt into redaction classification and is
56
+ byte-for-byte unchanged.
57
+ - **UX hardening.** The redacted context block now names the redaction source
58
+ (`Redaction: Applied by certification node (privacy mode)`) and sits tight under the
59
+ headline. A present-but-unverifiable timestamp (parse failure, or verification not
60
+ attempted) reports `⚠ Timestamp: PRESENT BUT NOT VERIFIED` instead of
61
+ `INVALID (malformed token)`, and a missing one reports `Timestamp: NOT INCLUDED` — the
62
+ timestamp still never affects `FINAL STATUS`. Section order is fixed: Signature →
63
+ Integrity → Timestamp → Node → Identity → Execution.
64
+ - **Timestamp compatibility (no longer "malformed token").** RFC 3161 timestamp
65
+ verification now accepts the full `TimeStampResp` returned by the certification node
66
+ (PKIStatusInfo + `timeStampToken`) in addition to a bare `TimeStampToken` (CMS
67
+ `ContentInfo`). The wrapper is detected structurally and unwrapped to the inner
68
+ `ContentInfo` before the unchanged signature / imprint / chain checks run. This is a
69
+ parsing-layer fix only — `certificateHash`, the imprint binding, the signature logic,
70
+ and the "timestamp never affects `FINAL STATUS`/exit code" invariant are all
71
+ unchanged; an unparseable token still degrades to `PRESENT BUT NOT VERIFIED`.
72
+
3
73
  ## v0.16.0 (2026-06-17) — `verify-record`: fully automatic, audit-grade, self-contained
4
74
 
5
75
  Feature release. `verify-bundle` is **byte-for-byte unchanged** — the redesigned
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # @nexart/cli v0.16.0
1
+ # @nexart/cli v1.0.0
2
2
 
3
3
  Command-line interface for NexArt — run, replay, and verify deterministic generative art, plus AI execution certification commands.
4
4
 
@@ -34,7 +34,9 @@ authenticity without relying on NexArt systems or source code.
34
34
  ### What this verifies
35
35
 
36
36
  - **Integrity** — the record's `certificateHash` is recomputed from the record's
37
- contents and compared to the stored value.
37
+ contents and compared to the stored value. A privacy-redacted public record
38
+ (input/output content stripped, hashes kept) is reported as **AUTHENTIC
39
+ (REDACTED)** rather than failed — see the [3-state model](#verifying-a-full-cer-record-nexart-verify-record).
38
40
  - **Authenticity** — the Ed25519 signature over the node's receipt is verified,
39
41
  when a signature is present.
40
42
 
@@ -691,13 +693,16 @@ nexart verify-bundle cer.json --verify-timestamp --tsa-roots tsa-roots.pem
691
693
  not established.
692
694
 
693
695
  3. **Timestamp (RFC 3161)** *(optional, additive — `--verify-timestamp`)* — when
694
- the bundle carries an RFC 3161 TimeStampToken (in `trustedTimestamps[]` or a
696
+ the bundle carries an RFC 3161 timestamp (in `trustedTimestamps[]` or a
695
697
  singular `timestamp`), this **offline** check confirms the token anchors the
696
- bundle to a point in time. It verifies, in order: (1) the TSA's CMS signature
697
- over the embedded `TSTInfo`; (2) that the token's message imprint equals the
698
- bundle's `certificateHash` digest; (3) that the TSA signing certificate chains
699
- to a root supplied via `--tsa-roots`; (4) that the certificate is valid at the
700
- timestamp; and (5) that the timestamp is at or after the bundle's `createdAt`.
698
+ bundle to a point in time. Both wire shapes are accepted: a full
699
+ `TimeStampResp` (the certification node's output) and a bare `TimeStampToken`
700
+ (e.g. OpenSSL's `-token_out`) the response wrapper is detected and unwrapped
701
+ automatically. It verifies, in order: (1) the TSA's CMS signature over the
702
+ embedded `TSTInfo`; (2) that the token's message imprint equals the bundle's
703
+ `certificateHash` digest; (3) that the TSA signing certificate chains to a root
704
+ supplied via `--tsa-roots`; (4) that the certificate is valid at the timestamp;
705
+ and (5) that the timestamp is at or after the bundle's `createdAt`.
701
706
 
702
707
  This layer is **strictly additive**: it never modifies `certificateHash`, is
703
708
  never included in canonicalisation, and **never affects the Integrity or
@@ -813,7 +818,7 @@ nexart verify-record record.json --json
813
818
  # Read the record from stdin (use "-" as the path)
814
819
  curl -s https://node.example/records/abc.json | nexart verify-record -
815
820
 
816
- # One-line verdict only (✔ VERIFIED / ✖ FAILED)
821
+ # One-line verdict only (✔ VERIFIED / ⚠ AUTHENTIC (REDACTED) / ✖ FAILED)
817
822
  nexart verify-record record.json --short
818
823
 
819
824
  # Fully offline, with an explicit signing key
@@ -825,6 +830,35 @@ record root is treated as the bundle itself (a certified bundle with its receipt
825
830
  signature embedded works directly). There is no "missing bundle" error — a
826
831
  structurally invalid record simply reports a clean **Integrity: FAIL**.
827
832
 
833
+ **Three-state model (since v0.16.1).** A public record served from a node may be
834
+ **privacy-redacted**: the snapshot's `input` / `output` *content* is stripped while the
835
+ `inputHash` / `outputHash` anchors are kept. Because `certificateHash` is sealed over the
836
+ full snapshot, it can no longer be recomputed from a redacted snapshot — but that is
837
+ **not tampering**. `verify-record` (and `verify-url` / `verify-hash`) detect this case and
838
+ report a third outcome:
839
+
840
+ | Outcome | When | Integrity line | Exit |
841
+ | --- | --- | --- | --- |
842
+ | **VERIFIED** | content present, `certificateHash` recomputes, signature not invalid | `✔ Integrity: PASS` | `0` |
843
+ | **AUTHENTIC (REDACTED)** | content stripped (hashes kept), recompute not possible, signature **passes** | `⚠ Integrity: NOT RECOMPUTABLE (redacted)` | `0` |
844
+ | **FAILED** | a real integrity mismatch (content present but hash differs) or an invalid signature | `✖ Integrity: FAIL` | `1` |
845
+
846
+ A redacted record is reclassified **only** when the snapshot is genuinely content-stripped
847
+ *and* the signature still verifies — a true content mismatch is **never** softened to
848
+ redacted, an invalid signature is always **FAILED**, and a redacted record whose hash still
849
+ recomputes simply stays **VERIFIED**. The `certificateHash` computation, canonicalisation,
850
+ and signature/timestamp logic are **unchanged**; this is purely a classification and
851
+ presentation layer.
852
+
853
+ > **What AUTHENTIC (REDACTED) does and does not prove.** It proves the record carries a
854
+ > valid node signature over the sealed `certificateHash` — i.e. it is authentic. It does
855
+ > **not** recompute the full snapshot, so it cannot prove the *content* behind the kept
856
+ > `inputHash` / `outputHash` was not altered before redaction. Redaction detection is
857
+ > structural (content absent, hashes present), so treat **AUTHENTIC (REDACTED)** as
858
+ > trustworthy only when the record comes from an authenticated node source (e.g. the
859
+ > node's public endpoint over TLS). For the strongest guarantee, obtain the full,
860
+ > un-redacted record and re-run `verify-record` to get **VERIFIED**.
861
+
828
862
  **Signing key resolution (by priority).**
829
863
 
830
864
  1. **`--public-key <file>`** — an explicit override (PEM, JWK, NodeKeysDocument, or
@@ -853,14 +887,28 @@ a timestamp *status*, never a crash.
853
887
  **Input from a file or stdin.** Pass a file path, or **`-`** to read the record from
854
888
  standard input — handy for pipelines such as `curl -s … | nexart verify-record -`.
855
889
 
856
- **Report.** The default output is an audit-grade summary **Integrity**,
857
- **Signature**, **Timestamp** (`VERIFIED` / `INVALID (<reason>)` / `PRESENT BUT NOT
858
- VERIFIED` / `NOT INCLUDED IN RECORD`, plus generation time and authority), **Node**
859
- (node id, key id, key source, operator), **Identity** (asserted identity, or none),
860
- and **Execution** (protocol, provider, model, input/output hashes, parameters,
861
- created-at). It ends with a single headline line **`FINAL STATUS: VERIFIED EVIDENCE
862
- RECORD`** or **`FINAL STATUS: FAILED`**. `--json` emits the same information as a
863
- structured object; `--short` prints only `✔ VERIFIED` or `✖ FAILED`.
890
+ **Report.** The default output is an audit-grade summary rendered in a fixed section
891
+ order — **Signature**, **Integrity**, **Timestamp** (`VERIFIED` / `INVALID (<reason>)` /
892
+ `PRESENT BUT NOT VERIFIED` / `NOT INCLUDED`, plus generation time and authority),
893
+ **Node** (node id, key id, key source, operator), **Identity** (asserted identity, or
894
+ none), and **Execution** (protocol, provider, model, input/output hashes, parameters,
895
+ created-at). A present-but-unverifiable token (a parse failure, or verification not
896
+ attempted) is shown as a caution — `⚠ Timestamp: PRESENT BUT NOT VERIFIED` never as an
897
+ error, and the timestamp **never** affects `FINAL STATUS`. The report ends with a single
898
+ headline line — **`FINAL STATUS: VERIFIED EVIDENCE RECORD`**, **`FINAL STATUS: AUTHENTIC
899
+ (REDACTED)`**, or **`FINAL STATUS: FAILED`**. For a redacted record the Signature line is
900
+ shown before the Integrity line and a tight context block follows the headline:
901
+
902
+ ```
903
+ FINAL STATUS: AUTHENTIC (REDACTED)
904
+ Mode: Public Verification (Redacted)
905
+ Redaction: Applied by certification node (privacy mode)
906
+ ! Full integrity verification requires access to original execution data
907
+ ```
908
+
909
+ `--json` emits the same information as a structured object (with `status` of `verified` /
910
+ `authentic-redacted` / `failed`, `integrity` including `redacted`, and a `redacted`
911
+ boolean); `--short` prints only `✔ VERIFIED`, `⚠ AUTHENTIC (REDACTED)`, or `✖ FAILED`.
864
912
 
865
913
  | Option | Description |
866
914
  | --- | --- |
@@ -869,7 +917,7 @@ structured object; `--short` prints only `✔ VERIFIED` or `✖ FAILED`.
869
917
  | `--kid <id>` | Key id to select from the key set (default: the receipt's `attestorKeyId`). |
870
918
  | `--tsa-roots <file>` | Override the default DigiCert TSA trust roots with custom root certificate(s) (PEM). |
871
919
  | `--json` | Emit a machine-readable report. |
872
- | `--short` | Print only a single verdict line (`✔ VERIFIED` / `✖ FAILED`). |
920
+ | `--short` | Print only a single verdict line (`✔ VERIFIED` / `⚠ AUTHENTIC (REDACTED)` / `✖ FAILED`). |
873
921
  | `--verbose` | Show intermediate verification steps. |
874
922
 
875
923
  > **`verify-bundle` is unchanged.** The redesigned output and automatic key/timestamp
@@ -879,6 +927,43 @@ structured object; `--short` prints only `✔ VERIFIED` or `✖ FAILED`.
879
927
 
880
928
  ---
881
929
 
930
+ ## Offline verification (`nexart verify-offline`)
931
+
932
+ `nexart verify-offline <record.json>` is a convenience alias for
933
+ `verify-record --offline` for relying parties who want a single, unambiguous
934
+ "verify this locally, never touch the network" command. It runs the **exact same**
935
+ verification core — integrity (certificate-hash recompute), Ed25519 receipt
936
+ signature, and timestamp reporting — and **never contacts a NexArt node**. The
937
+ signing key must come from `--keys` (an alias for `--public-key`) or be embedded in
938
+ the record.
939
+
940
+ ```bash
941
+ # Verify a record locally with an explicit node key file
942
+ nexart verify-offline record.json --keys node-keys.json
943
+
944
+ # Verify a record that embeds its own signing key
945
+ nexart verify-offline record.json
946
+
947
+ # Read the record from stdin
948
+ curl -s https://node.example/records/abc.json | nexart verify-offline - --keys node-keys.json
949
+ ```
950
+
951
+ It differs from `verify-record` only in presentation: a passing record reports
952
+ **`FINAL STATUS: AUTHENTIC (OFFLINE)`** with an explicit **`Mode: Offline
953
+ Verification`** line (the redaction note is still appended for a privacy-redacted
954
+ record), while a real mismatch or invalid signature stays **`FINAL STATUS:
955
+ FAILED`** with exit `1`. `--json` adds `"mode": "offline"`. **Hashing,
956
+ canonicalisation, the 3-state core, and exit codes are identical to
957
+ `verify-record`** — this is a UX wrapper, not new verification logic. As with
958
+ `verify-record`, a missing or unverifiable timestamp is *reported* but never
959
+ changes the verdict or exit code.
960
+
961
+ > **This verifies authenticity and integrity, not re-execution.** A pass confirms
962
+ > the record was signed by a NexArt node and has not been tampered with; it does
963
+ > **not** re-run the workload.
964
+
965
+ ---
966
+
882
967
  ## Verifying from a URL (`nexart verify-url`)
883
968
 
884
969
  `nexart verify-url <url>` is the one-command verification experience: it **fetches** a
@@ -889,7 +974,7 @@ automatic key/timestamp resolution and audit-grade renderer as `verify-record`.
889
974
  # Fetch and fully verify a published record
890
975
  nexart verify-url https://node.example/records/abc.json
891
976
 
892
- # One-line PASS/FAIL verdict
977
+ # One-line verdict (✔ VERIFIED / ⚠ AUTHENTIC (REDACTED) / ✖ FAILED)
893
978
  nexart verify-url https://node.example/records/abc.json --short
894
979
 
895
980
  # Machine-readable report
@@ -917,7 +1002,7 @@ through `verify-url`, so the **output is identical** to `verify-url`.
917
1002
  # Resolve and fully verify a record by certificate hash
918
1003
  nexart verify-hash sha256:abc123…
919
1004
 
920
- # One-line PASS/FAIL verdict
1005
+ # One-line verdict (✔ VERIFIED / ⚠ AUTHENTIC (REDACTED) / ✖ FAILED)
921
1006
  nexart verify-hash sha256:abc123… --short
922
1007
 
923
1008
  # Machine-readable report
package/dist/index.d.ts CHANGED
@@ -253,6 +253,7 @@ export interface RecordVerifyOpts {
253
253
  offline?: boolean;
254
254
  wellKnownUrl?: string;
255
255
  short?: boolean;
256
+ offlineMode?: boolean;
256
257
  }
257
258
  export declare function verifyRecordStandaloneCommand(file: string, opts: RecordVerifyOpts): Promise<StandaloneVerifyResult>;
258
259
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsDG;AAcH,OAAO,EAGL,KAAK,sBAAsB,EAC5B,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EAGL,cAAc,IAAI,iBAAiB,EAqBpC,MAAM,sBAAsB,CAAC;AAi0B9B;;GAEG;AACH,wBAAsB,aAAa,IAAI,OAAO,CAAC,MAAM,CAAC,CAerD;AAED;;GAEG;AACH,wBAAsB,aAAa,CACjC,IAAI,EAAE,MAAM,GAAG,SAAS,EACxB,WAAW,GAAE,MAAM,OAAO,CAAC,MAAM,CAAiB,GACjD,OAAO,CAAC,OAAO,CAAC,CA0BlB;AAED;;;GAGG;AACH,wBAAsB,WAAW,CAC/B,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,OAAO,EACb,MAAM,CAAC,EAAE,MAAM,EACf,OAAO,GAAE,OAAO,KAAwB,GACvC,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,OAAO,CAAA;CAAE,CAAC,CAoB5C;AAED;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,cAAc,0BAAoB,CAAC;AAEhD;;;GAGG;AACH,wBAAgB,eAAe,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,EAAE,CAwB9D;AAoED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAsB,aAAa,CACjC,IAAI,EAAE,MAAM,GAAG,SAAS,EACxB,IAAI,EAAE;IACJ,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B,EACD,WAAW,GAAE,MAAM,OAAO,CAAC,MAAM,CAAiB,GACjD,OAAO,CAAC,IAAI,CAAC,CAuCf;AAED;;;;GAIG;AACH,wBAAsB,eAAe,CACnC,IAAI,EAAE,MAAM,GAAG,SAAS,EACxB,IAAI,EAAE;IACJ,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,EACD,OAAO,GAAE,OAAO,KAAwB,EACxC,WAAW,GAAE,MAAM,OAAO,CAAC,MAAM,CAAiB,GACjD,OAAO,CAAC,IAAI,CAAC,CA8Cf;AAED;;;;;GAKG;AACH,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,MAAM,GAAG,SAAS,EACxB,IAAI,EAAE;IACJ,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,OAAO,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,EACD,OAAO,GAAE,OAAO,KAAwB,EACxC,WAAW,GAAE,MAAM,OAAO,CAAC,MAAM,CAAiB,GACjD,OAAO,CAAC,IAAI,CAAC,CA8Df;AAqCD,wBAAsB,eAAe,CACnC,IAAI,EAAE,MAAM,GAAG,SAAS,EACxB,IAAI,EAAE;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAAC,UAAU,CAAC,EAAE,OAAO,CAAA;CAAE,EAChE,WAAW,GAAE,MAAM,OAAO,CAAC,MAAM,CAAiB,GACjD,OAAO,CAAC,IAAI,CAAC,CA+Hf;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,qBAAqB,CACzC,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GACrB,OAAO,CAAC,IAAI,CAAC,CAoCf;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,yBAAyB,CAC7C,IAAI,EAAE,MAAM,GAAG,SAAS,EACxB,IAAI,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,EAC7D,WAAW,GAAE,MAAM,OAAO,CAAC,MAAM,CAAiB,GACjD,OAAO,CAAC,IAAI,CAAC,CAqCf;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAsB,6BAA6B,CACjD,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAAC,eAAe,CAAC,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,GAC1H,OAAO,CAAC,sBAAsB,CAAC,CAqCjC;AAiHD;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,MAAM,WAAW,gBAAgB;IAC/B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,wBAAsB,6BAA6B,CACjD,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,gBAAgB,GACrB,OAAO,CAAC,sBAAsB,CAAC,CAKjC;AAED;;;;;GAKG;AACH,wBAAsB,sBAAsB,CAC1C,MAAM,EAAE,OAAO,EACf,IAAI,EAAE,gBAAgB,GACrB,OAAO,CAAC,sBAAsB,CAAC,CAmHjC;AAyBD;;;;;GAKG;AACH,wBAAsB,gBAAgB,CACpC,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,gBAAgB,GACrB,OAAO,CAAC,sBAAsB,CAAC,CA6BjC;AAeD;;;;;GAKG;AACH,wBAAsB,iBAAiB,CACrC,eAAe,EAAE,MAAM,EACvB,IAAI,EAAE,gBAAgB,GACrB,OAAO,CAAC,sBAAsB,CAAC,CASjC;AA8RD;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,OAAO,gBAAgB,EAAE,SAAS,GAAG,MAAM,CAyBtF;AAmDD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAsB,sBAAsB,CAC1C,IAAI,EAAE,MAAM,GAAG,SAAS,EACxB,IAAI,EAAE;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,EACvB,WAAW,GAAE,MAAM,OAAO,CAAC,MAAM,CAAiB,GACjD,OAAO,CAAC,IAAI,CAAC,CAwBf"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -1559,6 +1559,9 @@ export async function verifyRecordFromObject(record, opts) {
1559
1559
  publicKeyMaterial,
1560
1560
  kid: attestorKeyId,
1561
1561
  verbose: opts.verbose,
1562
+ // Public records may be privacy-redacted (input/output content stripped). Classify
1563
+ // those as `integrity: 'redacted'` rather than a tamper FAIL.
1564
+ classifyRedacted: true,
1562
1565
  });
1563
1566
  // If no override / well-known key was used but the signature still verified, the
1564
1567
  // verifier must have used key material embedded in the bundle.
@@ -1594,7 +1597,8 @@ export async function verifyRecordFromObject(record, opts) {
1594
1597
  opts,
1595
1598
  });
1596
1599
  // Exit code is determined SOLELY by integrity + signature — never by the timestamp.
1597
- process.exitCode = result.status === 'verified' ? 0 : 1;
1600
+ // VERIFIED and AUTHENTIC (REDACTED) both exit 0; only FAILED exits 1.
1601
+ process.exitCode = result.status === 'failed' ? 1 : 0;
1598
1602
  return result;
1599
1603
  }
1600
1604
  /**
@@ -1750,7 +1754,7 @@ function keySourceLabel(source) {
1750
1754
  function formatRecordTimestampLine(status) {
1751
1755
  const CHECK = '\u2714';
1752
1756
  const CROSS = '\u2716';
1753
- const CIRCLE = '\u25CB';
1757
+ const WARN = '\u26A0';
1754
1758
  switch (status) {
1755
1759
  case 'valid':
1756
1760
  return `${CHECK} Timestamp: VERIFIED`;
@@ -1764,13 +1768,15 @@ function formatRecordTimestampLine(status) {
1764
1768
  return `${CROSS} Timestamp: INVALID (expired)`;
1765
1769
  case 'invalid-time':
1766
1770
  return `${CROSS} Timestamp: INVALID (time)`;
1771
+ // A present-but-unverifiable token (parse failure, or verification not attempted) is
1772
+ // NOT a broken record — it is advisory only and never affects FINAL STATUS. Report it
1773
+ // as a caution, not an error, so users don't read it as tampering.
1767
1774
  case 'malformed':
1768
- return `${CROSS} Timestamp: INVALID (malformed token)`;
1769
1775
  case 'present-unverified':
1770
- return `${CIRCLE} Timestamp: PRESENT BUT NOT VERIFIED`;
1776
+ return `${WARN} Timestamp: PRESENT BUT NOT VERIFIED`;
1771
1777
  case 'not-present':
1772
1778
  default:
1773
- return `${CIRCLE} Timestamp: NOT INCLUDED IN RECORD`;
1779
+ return `${CROSS} Timestamp: NOT INCLUDED`;
1774
1780
  }
1775
1781
  }
1776
1782
  /**
@@ -1795,7 +1801,9 @@ function renderRecordVerification(result, tsaResult, ctx) {
1795
1801
  if (ctx.opts.json) {
1796
1802
  process.stdout.write(JSON.stringify({
1797
1803
  status: result.status,
1804
+ ...(ctx.opts.offlineMode ? { mode: 'offline' } : {}),
1798
1805
  integrity: result.integrity,
1806
+ redacted: result.integrity === 'redacted',
1799
1807
  signature: result.signature,
1800
1808
  protocolVersion: result.protocolVersion,
1801
1809
  certificateHash: result.expectedCertificateHash,
@@ -1824,11 +1832,16 @@ function renderRecordVerification(result, tsaResult, ctx) {
1824
1832
  }, null, 2) + '\n');
1825
1833
  return;
1826
1834
  }
1827
- const mark = (p) => p === 'pass' ? '\u2714' : p === 'fail' ? '\u2716' : '\u25CB';
1828
- const label = (p) => p.toUpperCase();
1829
- // --short: a single machine-glanceable verdict line, nothing else.
1835
+ const WARN = '\u26A0';
1836
+ const mark = (p) => p === 'pass' ? '\u2714' : p === 'redacted' ? WARN : p === 'fail' ? '\u2716' : '\u25CB';
1837
+ const label = (p) => p === 'redacted' ? 'NOT RECOMPUTABLE (redacted)' : p.toUpperCase();
1838
+ // --short: a single machine-glanceable verdict line, nothing else. 3-state.
1830
1839
  if (ctx.opts.short) {
1831
- console.log(result.status === 'verified' ? '\u2714 VERIFIED' : '\u2716 FAILED');
1840
+ console.log(result.status === 'verified'
1841
+ ? '\u2714 VERIFIED'
1842
+ : result.status === 'authentic-redacted'
1843
+ ? `${WARN} AUTHENTIC (REDACTED)`
1844
+ : '\u2716 FAILED');
1832
1845
  return;
1833
1846
  }
1834
1847
  if (ctx.opts.verbose && result.steps.length > 0) {
@@ -1837,9 +1850,10 @@ function renderRecordVerification(result, tsaResult, ctx) {
1837
1850
  console.log(` - ${s}`);
1838
1851
  console.log('');
1839
1852
  }
1840
- // ── Verdict block ──
1841
- console.log(`${mark(result.integrity)} Integrity: ${label(result.integrity)}`);
1853
+ // ── Verdict block ── Signature FIRST: authenticity establishes trust up front,
1854
+ // then integrity (recomputable vs redacted), then the (advisory) timestamp.
1842
1855
  console.log(`${mark(result.signature)} Signature: ${label(result.signature)}`);
1856
+ console.log(`${mark(result.integrity)} Integrity: ${label(result.integrity)}`);
1843
1857
  console.log(formatRecordTimestampLine(tsaResult.status));
1844
1858
  if (tsaResult.status !== 'not-present' && genTimeIso) {
1845
1859
  console.log(` Time: ${genTimeIso}`);
@@ -1885,11 +1899,34 @@ function renderRecordVerification(result, tsaResult, ctx) {
1885
1899
  }
1886
1900
  if (str(ctx.bundle.createdAt))
1887
1901
  console.log(` Created: ${str(ctx.bundle.createdAt)}`);
1888
- // ── Final verdict line ──
1902
+ // ── Final verdict line (3-state) ──
1903
+ // `verify-offline` (offlineMode) is a DISPLAY wrapper: a non-failed record is labelled
1904
+ // "AUTHENTIC (OFFLINE)" regardless of the redacted/verified split, while a failure stays
1905
+ // "FAILED". The 3-state core, exit code, and crypto are untouched.
1906
+ const offline = ctx.opts.offlineMode === true;
1889
1907
  console.log('');
1890
- console.log(result.status === 'verified'
1891
- ? 'FINAL STATUS: VERIFIED EVIDENCE RECORD'
1892
- : 'FINAL STATUS: FAILED');
1908
+ console.log(result.status === 'failed'
1909
+ ? 'FINAL STATUS: FAILED'
1910
+ : offline
1911
+ ? 'FINAL STATUS: AUTHENTIC (OFFLINE)'
1912
+ : result.status === 'verified'
1913
+ ? 'FINAL STATUS: VERIFIED EVIDENCE RECORD'
1914
+ : 'FINAL STATUS: AUTHENTIC (REDACTED)');
1915
+ // Mode / context block. In offline mode an explicit "Mode: Offline Verification" line
1916
+ // is always shown for a passing record; the redaction note is appended when the public
1917
+ // record was privacy-redacted. Online keeps the original redacted-only context block.
1918
+ if (offline && result.status !== 'failed') {
1919
+ console.log('Mode: Offline Verification');
1920
+ if (result.status === 'authentic-redacted') {
1921
+ console.log('Redaction: Applied by certification node (privacy mode)');
1922
+ console.log('! Full integrity verification requires access to original execution data');
1923
+ }
1924
+ }
1925
+ else if (result.status === 'authentic-redacted') {
1926
+ console.log('Mode: Public Verification (Redacted)');
1927
+ console.log('Redaction: Applied by certification node (privacy mode)');
1928
+ console.log('! Full integrity verification requires access to original execution data');
1929
+ }
1893
1930
  // ── Failure reasons (stderr) ──
1894
1931
  if (result.status === 'failed' || result.reasons.length > 0) {
1895
1932
  for (const r of result.reasons)
@@ -2544,7 +2581,7 @@ if (_isMainModule) {
2544
2581
  default: false,
2545
2582
  })
2546
2583
  .option('short', {
2547
- describe: 'Print only a single verdict line (✔ VERIFIED / ✖ FAILED)',
2584
+ describe: 'Print only a single verdict line (✔ VERIFIED / ⚠ AUTHENTIC (REDACTED) / ✖ FAILED)',
2548
2585
  type: 'boolean',
2549
2586
  default: false,
2550
2587
  })
@@ -2574,6 +2611,62 @@ if (_isMainModule) {
2574
2611
  offline: argv.offline,
2575
2612
  tsaRoots: argv['tsa-roots'],
2576
2613
  });
2614
+ })
2615
+ .command('verify-offline <file>', 'Verify a CER record locally without contacting a NexArt node (offline-only alias of verify-record --offline)', (yargs) => {
2616
+ return yargs
2617
+ .positional('file', {
2618
+ describe: 'Path to the CER record JSON, or "-" to read it from stdin ({ bundle, verification?, timestamp? } or a bare bundle)',
2619
+ type: 'string',
2620
+ demandOption: true,
2621
+ })
2622
+ .option('keys', {
2623
+ alias: 'public-key',
2624
+ describe: 'Node public key(s) for signature verification (PEM, JWK, NodeKeysDocument, or raw base64url). Falls back to a key embedded in the record',
2625
+ type: 'string',
2626
+ })
2627
+ .option('kid', {
2628
+ describe: 'Key id to select from the key set (default: the receipt attestorKeyId)',
2629
+ type: 'string',
2630
+ })
2631
+ .option('json', {
2632
+ describe: 'Output machine-readable JSON',
2633
+ type: 'boolean',
2634
+ default: false,
2635
+ })
2636
+ .option('short', {
2637
+ describe: 'Print only a single verdict line (✔ VERIFIED / ⚠ AUTHENTIC (REDACTED) / ✖ FAILED)',
2638
+ type: 'boolean',
2639
+ default: false,
2640
+ })
2641
+ .option('verbose', {
2642
+ describe: 'Show intermediate verification steps',
2643
+ type: 'boolean',
2644
+ default: false,
2645
+ })
2646
+ .option('tsa-roots', {
2647
+ describe: 'Override the default DigiCert TSA trust roots with custom root certificate(s) (PEM)',
2648
+ type: 'string',
2649
+ })
2650
+ .example('$0 verify-offline record.json --keys node-keys.json', 'Fully offline verification with an explicit key file')
2651
+ .example('$0 verify-offline record.json', 'Offline verification using a key embedded in the record')
2652
+ .epilogue('This verifies that the record was signed by a NexArt node and has not been tampered with. ' +
2653
+ 'It does not re-execute the workload, and never contacts a node.');
2654
+ }, async (argv) => {
2655
+ // yargs collapses a bare "-" positional to an empty string; restore the stdin sentinel.
2656
+ const file = argv.file === '' ? '-' : argv.file;
2657
+ await verifyRecordStandaloneCommand(file, {
2658
+ // `keys` is the alias for `public-key`; yargs populates both, prefer either.
2659
+ publicKey: argv.keys ?? argv['public-key'],
2660
+ kid: argv.kid,
2661
+ json: argv.json,
2662
+ short: argv.short,
2663
+ verbose: argv.verbose,
2664
+ tsaRoots: argv['tsa-roots'],
2665
+ // verify-offline is offline by definition: no network for keys or anything else.
2666
+ offline: true,
2667
+ // Display-only: label the report as an offline verification.
2668
+ offlineMode: true,
2669
+ });
2577
2670
  })
2578
2671
  .command('verify-hash <certificate_hash>', 'Resolve a certificate hash to its public CER record and verify it — one-command verification', (yargs) => {
2579
2672
  return yargs
@@ -2601,7 +2694,7 @@ if (_isMainModule) {
2601
2694
  default: false,
2602
2695
  })
2603
2696
  .option('short', {
2604
- describe: 'Print only a single verdict line (✔ VERIFIED / ✖ FAILED)',
2697
+ describe: 'Print only a single verdict line (✔ VERIFIED / ⚠ AUTHENTIC (REDACTED) / ✖ FAILED)',
2605
2698
  type: 'boolean',
2606
2699
  default: false,
2607
2700
  })
@@ -2654,7 +2747,7 @@ if (_isMainModule) {
2654
2747
  default: false,
2655
2748
  })
2656
2749
  .option('short', {
2657
- describe: 'Print only a single verdict line (✔ VERIFIED / ✖ FAILED)',
2750
+ describe: 'Print only a single verdict line (✔ VERIFIED / ⚠ AUTHENTIC (REDACTED) / ✖ FAILED)',
2658
2751
  type: 'boolean',
2659
2752
  default: false,
2660
2753
  })