@charter-agreement-protocol/signer 0.2.2 → 0.3.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
@@ -3,6 +3,137 @@
3
3
  All notable public changes to `@charter-agreement-protocol/signer` are
4
4
  documented here.
5
5
 
6
+ ## [0.3.0] — October 14, 2026
7
+
8
+ ### Breaking
9
+
10
+ - `signAcceptance` and `signTermination` now take `(claims, keyHandle, chain,
11
+ opts)`. The verifier derives context from that chain; `revisionText` and
12
+ `descriptorCompacts` are removed from the signer input. Migration:
13
+
14
+ ```ts
15
+ // Before
16
+ await signAcceptance(claims, keyHandle, { revisionText, descriptorCompacts, chain }, opts);
17
+ // Now
18
+ await signAcceptance(claims, keyHandle, chain, opts);
19
+ await signTermination(terminationClaims, keyHandle, chain, opts);
20
+ ```
21
+
22
+ - The holder's own descriptor key is checked before signing. A wrong kid,
23
+ public key, or key algorithm returns
24
+ `{ ok: false, error: "refused", code: "signing_key_mismatch" }`.
25
+ `signing_key_invalid` maps to `invalid_key_handle`. For otherwise valid
26
+ descriptor claims, an absent or null successor predecessor, a valid predecessor
27
+ supplied to a genesis, broken signed lineage, or digest aliases that disagree
28
+ with the reverified lineage return `invalid_input`/`descriptor_chain_invalid`.
29
+ A non-object predecessor, both digest aliases absent, or missing, null, empty,
30
+ or non-string lineage return `invalid_input`/`invalid_type`. Throwing reads also
31
+ return `invalid_type`. History excluded by a profile or requiring unavailable
32
+ crypto preserves the profile or substrate code.
33
+ - Context, claims/context mismatches, profile exclusions, and compact size
34
+ limits reject before the signing callback. Missing or malformed context
35
+ types return `invalid_input`/`invalid_type`; malformed sets preserve the
36
+ verifier's code, including `compact_invalid`. `verification_failed` now
37
+ means only that `finishSigning` rejected what preparation approved.
38
+ - Unavailable crypto returns the top-level
39
+ `{ ok: false, error: "algorithm_unsupported_on_substrate" }` before signing,
40
+ including an ML-DSA-65 request on the browser bundle.
41
+ - Rename `invalid_handle` to `invalid_key_handle`. Update error comparisons
42
+ and exhaustive switches; the result shape is unchanged. Add the new substrate
43
+ error and refusal code to exhaustive handling.
44
+ - Options must be absent or a plain object containing only `algorithm`,
45
+ `limits`, `profile`, and (for `signDescriptor`) `predecessor`. Unknown keys,
46
+ non-plain objects, and unreadable getters return `invalid_input`/`invalid_type`.
47
+ Malformed limits and profiles preserve `invalid_limits` and `invalid_profile`.
48
+ Previously, a misspelled option key silently fell back to Ed25519.
49
+
50
+ ### Added
51
+
52
+ - `signDescriptor` accepts the verifier's `DescriptorPredecessor` as
53
+ `opts.predecessor`, with retained signed `lineage`, for successor descriptors.
54
+ - All four functions accept verifier `limits` and `profile` options. Limits
55
+ include the final compact and context artifacts and also apply to the custody
56
+ guard and completion. Profiles check the new artifact and retained context.
57
+ - `signReceipt` accepts bare revision text as well as a chain view. With a
58
+ bare revision, the custody guard checks the snapshot key and verifier facts
59
+ list `signature` in `not_verified`.
60
+ - Export `SigningOptions` and `DescriptorOptions`. Real-key tests cover
61
+ Ed25519 and ML-DSA-65 successors and pre-sign refusals, with zero signing calls
62
+ asserted on rejection. The browser bundle test runs the complete workshop flow.
63
+ - Include `SECURITY.md`, `CODE_OF_CONDUCT.md`, and `CONTRIBUTING.md` in the
64
+ npm package with this package's reporting and contribution practices.
65
+ - Run the shared signer table from `charter_agreement_signer` 0.5.0, copied byte for byte with
66
+ a SHA-256 check. All 121 rows use real seed keys and compare result shapes,
67
+ codes, snapshot counts, and custody counts. Twelve Ed25519 successes match
68
+ exact compacts; five ML-DSA-65 successes pass public verification. The explicit
69
+ list of unexpressible rows is empty, and the run count is asserted. Eight
70
+ shared requests or contexts needing ML-DSA-65 refuse before browser custody.
71
+
72
+ ### Changed
73
+
74
+ - Delegate preparation and completion to verifier 0.7.0's `prepareSigning`
75
+ and `finishSigning`; remove the signer's context derivation,
76
+ producer calls, assembly calls, and choice of post-sign verification function.
77
+ - Require `@charter-agreement-protocol/verifier` ^0.7.0. Its signing preparation
78
+ corpus uses the public input shapes, and signing plans can only be made through
79
+ `prepareSigning`. The signer already uses that path.
80
+ - Accept signature bytes from any JavaScript realm, including Buffer and plain
81
+ `Uint8Array`. Ed25519 snapshot keys with noncanonical y coordinates, no curve
82
+ point, x = 0 with the sign bit set, or small order return `invalid_key_handle`.
83
+ Invalid descriptor claim keys can fail earlier with `signing_input_invalid`.
84
+ The verifier checks option values before either key callback. Sparse profile
85
+ algorithm lists return `invalid_input`/`invalid_profile`; nonfinite or
86
+ explicitly undefined limit values return `invalid_input`/`invalid_limits`.
87
+ - Copy claims into plain JSON before the first `await`, without array
88
+ constructor hooks. Read defined context and predecessor members once and
89
+ retain artifact lists for the verifier. Lineage arrays have no JSON bounds.
90
+ Claims entry guards use the verifier's maximum JSON bounds. Caller limits and final compact size are
91
+ checked by `prepareSigning`; oversize claims return `signing_input_invalid`.
92
+ Predecessor lineage has no signer item limit.
93
+ - Read defined option members once with ordinary property access, including
94
+ inherited members and accepted class or Map properties. Keep invalid member
95
+ values opaque for the verifier's checks. Return every verifier option error,
96
+ including copy errors. Long algorithm lists retain an invalid length without
97
+ walking their entries.
98
+ - Run all 121 shared rows in Node and the browser bundle with no skips.
99
+ `ml-successor-ok` and `ml-successor-wrong` now use their table
100
+ `expected_limited`: `algorithm_unsupported_on_substrate`, one snapshot, and
101
+ zero custody calls because the browser cannot verify ML-DSA-65 predecessor
102
+ history. Node retains successor success and
103
+ `invalid_input`/`descriptor_chain_invalid`, respectively. Removing the duplicate
104
+ `termination-excluded-context` row
105
+ reduces the exact Ed25519 success count from 13 to 12 in each lane.
106
+ Existing row outcomes and the signer API remain unchanged with this build.
107
+ - Resolve limits, then profile, then algorithm, before taking the key snapshot.
108
+ Absent, `undefined`, and `null` algorithms select Ed25519. Explicit values are
109
+ `Ed25519` and `ML-DSA-65`; others return `invalid_input`/`algorithm_unsupported`
110
+ before either callback. The snapshot checks only a non-empty string kid and
111
+ a string public key; the verifier checks key encoding and shape.
112
+ - Keep version 0.3.0 and the workshop badge. Builds remove stale library output;
113
+ CI typechecks and builds the workshop on Linux with Node 24.8.0 and Node 24.
114
+ - SECURITY.md uses GitHub private vulnerability reporting. Evidence descriptions
115
+ name the checks that run; this signer has no telemetry.
116
+ - See the [profile scope](README.md#profile-scope) for the shared ruling.
117
+
118
+ ### Migration from CAP 0.4 to 0.5
119
+
120
+ - Revisions must name two distinct parties from different descriptor histories.
121
+ Replace fixtures that use one descriptor for both roles with two real parties.
122
+ Chain views supply explicit `revisions`, `acceptances`, `descriptors`, and
123
+ `terminations` arrays, including empty lists.
124
+
125
+ ### Migration from verifier 0.5 to 0.6
126
+
127
+ - Producer build failures surface as `invalid_input`/`signing_input_invalid`.
128
+ Termination claims omit `extensions` and other extra members.
129
+ - Digests are canonical base64url SHA-256 values and revision JSON is canonical.
130
+ Descriptor keys use canonical encodings with the selected algorithm's byte
131
+ length. Mixed party histories and unknown claims reject.
132
+ - Direct verifier callers pass retained signed `lineage` in predecessor facts
133
+ to `verifyDescriptor`; `{ digest, claims }` alone is insufficient. Genesis
134
+ calls omit the predecessor. This signer exposes that predecessor in options.
135
+ - Profiles and limits can reject context as well as the new artifact.
136
+
6
137
  ## [0.2.2] — October 6, 2026
7
138
 
8
139
  ### Changed
@@ -27,9 +158,8 @@ No API change: verifier 0.5.0 added consumer-side surface only (the
27
158
  capability probe, the signature registry identity, the descriptor
28
159
  timestamp floor, the 104-case corpus); the producer surface this package
29
160
  delegates to is unchanged. Typecheck strict, 18/18 tests against the
30
- resolved 0.5.0. The producer-side capability alignment (mirrored
31
- `capabilities()` with refuse-before-mint, declared minting metadata,
32
- profile-bound signing) is the next feature release on this line.
161
+ resolved 0.5.0. Corrected in 0.3.0: 0.2.2 did not add signer capability metadata,
162
+ profile options, or telemetry.
33
163
 
34
164
  ## [0.2.0] — 2026-09-17
35
165
 
@@ -0,0 +1,87 @@
1
+ <!-- Adapted from the Contributor Covenant version 2.1 (https://www.contributor-covenant.org),
2
+ the canonical text with the reporting contact substituted for this repository. -->
3
+
4
+ # Contributor Covenant Code of Conduct
5
+
6
+ ## Our Pledge
7
+
8
+ We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation.
9
+
10
+ We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
11
+
12
+ ## Our Standards
13
+
14
+ Examples of behavior that contributes to a positive environment for our community include:
15
+
16
+ * Demonstrating empathy and kindness toward other people
17
+ * Being respectful of differing opinions, viewpoints, and experiences
18
+ * Giving and gracefully accepting constructive feedback
19
+ * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
20
+ * Focusing on what is best not just for us as individuals, but for the overall community
21
+
22
+ Examples of unacceptable behavior include:
23
+
24
+ * The use of sexualized language or imagery, and sexual attention or advances of any kind
25
+ * Trolling, insulting or derogatory comments, and personal or political attacks
26
+ * Public or private harassment
27
+ * Publishing others' private information, such as a physical or email address, without their explicit permission
28
+ * Other conduct which could reasonably be considered inappropriate in a professional setting
29
+
30
+ ## Enforcement Responsibilities
31
+
32
+ Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
33
+
34
+ Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.
35
+
36
+ ## Scope
37
+
38
+ This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
39
+
40
+ ## Enforcement
41
+
42
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at the repository maintainer PRIVATELY at palermo777@gmail.com — conduct reports are kept
43
+ confidential; please do not open public issues for conduct reports. All complaints will be reviewed and investigated promptly and fairly.
44
+
45
+ All community leaders are obligated to respect the privacy and security of the reporter of any incident.
46
+
47
+ ## Enforcement Guidelines
48
+
49
+ Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
50
+
51
+ ### 1. Correction
52
+
53
+ **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.
54
+
55
+ **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.
56
+
57
+ ### 2. Warning
58
+
59
+ **Community Impact**: A violation through a single incident or series of actions.
60
+
61
+ **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.
62
+
63
+ ### 3. Temporary Ban
64
+
65
+ **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior.
66
+
67
+ **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
68
+
69
+ ### 4. Permanent Ban
70
+
71
+ **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
72
+
73
+ **Consequence**: A permanent ban from any sort of public interaction within the community.
74
+
75
+ ## Attribution
76
+
77
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.1, available at [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
78
+
79
+ Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder][Mozilla CoC].
80
+
81
+ For answers to common questions about this code of conduct, see the FAQ at [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at [https://www.contributor-covenant.org/translations][translations].
82
+
83
+ [homepage]: https://www.contributor-covenant.org
84
+ [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
85
+ [Mozilla CoC]: https://github.com/mozilla/diversity
86
+ [FAQ]: https://www.contributor-covenant.org/faq
87
+ [translations]: https://www.contributor-covenant.org/translations
@@ -0,0 +1,71 @@
1
+ # Contributing
2
+
3
+ Contributions must preserve the signer's boundaries: the library depends
4
+ only on the public verifier package, key identity comes from one atomic
5
+ snapshot, errors contain no caller values, and test handles do not ship.
6
+ Private keys stay behind the caller's callbacks.
7
+
8
+ ## Toolchain
9
+
10
+ Use Node 24, at least 24.8.0, and npm. `package-lock.json` pins the build
11
+ tools and verifier version. CI runs on Linux with Node 24.8.0 and the current
12
+ Node 24 line.
13
+
14
+ ## Before a pull request
15
+
16
+ Install with the frozen lockfile, then run the local checks:
17
+
18
+ ```console
19
+ npm ci
20
+ npm test
21
+ npm run typecheck
22
+ npm run typecheck:web
23
+ npm run build
24
+ npm run build:site
25
+ npm pack --dry-run
26
+ ```
27
+
28
+ The checks after installation run locally without a network connection.
29
+ The suite builds a browser bundle with the workshop's shims and exercises
30
+ real Ed25519 and ML-DSA-65 keys. The library build writes `dist/`; the site
31
+ build writes `site-dist/`.
32
+
33
+ ## Windows
34
+
35
+ Developer platforms are macOS and Linux. Windows developers use WSL2,
36
+ clone inside its filesystem and follow the Linux commands. CI runs on
37
+ Linux only. Native ML-DSA tests require the supported Node crypto runtime.
38
+
39
+ ## Commits
40
+
41
+ Use explicit paths when staging or committing. Keep tool directories and
42
+ local process state out of commits. Work on `main` in small commits with
43
+ plain messages unless the maintainer asks otherwise. Do not use `git stash`.
44
+
45
+ ## Tests and checks
46
+
47
+ - For behavior changes, write the test, observe it fail for the intended
48
+ reason, then make it pass. Record the observed failure in the commit.
49
+ - New checks must fail under a named temporary mutation. Restore the
50
+ original code and run the suite again before committing.
51
+ - Use real keys and real signing callbacks. Do not use mocks, fake crypto
52
+ or handles that return invented signatures.
53
+ - Search for tests of the old contract before changing behavior.
54
+
55
+ The local suite runs the [shared signer table](test/fixtures/README.md)
56
+ against the TypeScript verifier this checkout installs. It compares recorded
57
+ Elixir outcomes and exact Ed25519 compacts with real seed keys, then checks
58
+ every success through verification. It does not execute Elixir locally.
59
+ Keep the source table byte for byte and report disagreements with both outcomes.
60
+
61
+ ## Protocol dependency changes
62
+
63
+ Move the verifier requirement and lockfile together. Read its changelog,
64
+ test against the published package and document changes to input rules
65
+ or error codes. Keep signing fixtures canonical.
66
+
67
+ ## Docs
68
+
69
+ Update documentation with the behavior it describes. Keep error names,
70
+ supported options, package metadata and the workshop's version badge in
71
+ sync. Describe only checks that run and features that ship.
package/README.md CHANGED
@@ -7,15 +7,12 @@ Holder-side companion signer for the
7
7
 
8
8
  **CAP verifies; it never authorizes. This signer signs; it never takes
9
9
  custody.** You keep the key (HSM, OS keychain, key server) behind a handle
10
- object with two callbacks; the signer resolves ONE atomic `keyIdentity`
11
- snapshot, then delegates EVERYTHING pure to the verifier package's producer
12
- surface — the exact RFC 7515 signing input, the producer claims gate, the
13
- R1–R3 honest-signer refusal guards (claims-truth, no-equivocation,
14
- ancestry/governing coverage), and assembly — **before the key signs**, with
15
- exactly one implementation. What stays here is custody: the sign callback,
16
- the wrong-key guard against the snapshot's public key, and the post-sign
17
- verification of the assembled artifact through the
18
- `@charter-agreement-protocol/verifier` package.
10
+ with two callbacks. The signer resolves limits, profile, and algorithm, copies
11
+ claims and context at entry, and takes one atomic `keyIdentity` snapshot.
12
+ The verifier's `prepareSigning` checks framing, claims, context, profile,
13
+ limits, and the holder's own descriptor key before the callback signs.
14
+ The signer checks the signature against the snapshot public key, then calls
15
+ `finishSigning` to assemble and verify under the retained context and settings.
19
16
 
20
17
  ## Install
21
18
 
@@ -23,8 +20,8 @@ verification of the assembled artifact through the
23
20
  npm install @charter-agreement-protocol/signer
24
21
  ```
25
22
 
26
- Release **0.2.2** requires Node >= 24.8.0 (ML-DSA support in the Node builtins)
27
- and depends on `@charter-agreement-protocol/verifier` `^0.5.0`.
23
+ Version **0.3.0** requires Node >= 24.8.0 and
24
+ `@charter-agreement-protocol/verifier` ^0.7.0, for its pre-sign API.
28
25
 
29
26
  ## Quickstart
30
27
 
@@ -72,76 +69,175 @@ For ML-DSA-65 artifacts pass `{ algorithm: "ML-DSA-65" }` as the options
72
69
  argument with `protocol_revision: 3` claims (pure ML-DSA per RFC 9964, the
73
70
  empty context — your handle signs the exact message bytes as with Ed25519).
74
71
 
72
+ An absent, `undefined` or `null` algorithm selects Ed25519. Explicit
73
+ algorithms are `Ed25519` and `ML-DSA-65`; any other value returns
74
+ `invalid_input` with `code: "algorithm_unsupported"` before either key
75
+ callback runs.
76
+
75
77
  ## API
76
78
 
77
79
  | Export | What it does |
78
80
  |---|---|
79
- | `signDescriptor(claims, handle, opts?)` | Signs one Party Descriptor; post-verified via `verifyDescriptor` |
80
- | `signReceipt(claims, handle, chain, opts?)` | Signs one Receipt against the required issuing view; post-verified via `verifyReceipt` (the reference takes a context, never none) |
81
- | `signAcceptance(claims, handle, view, opts?)` | Signs one Acceptance against the caller's verified view — the R1–R3 refusal guards run over `view.chain` before the key signs |
82
- | `signTermination(claims, handle, view, opts?)` | Signs one Termination against the caller's verified view — the R1–R3 refusal guards run over `view.chain` before the key signs |
83
-
84
- The acceptance/termination `view` is `{ revisionText, descriptorCompacts,
85
- chain }`: the revision the claims name, the signing party's descriptor
86
- chain, and the caller's full `ChainView` (`{ revisions, acceptances,
87
- descriptors, terminations }`) the refusal guards verify and bind against.
88
-
89
- Errors are closed: `{ ok: false, error: "invalid_handle" \| "signing_failed"
90
- \| "invalid_input" \| "refused" \| "verification_failed" }` — `code` rides
91
- only on `invalid_input` and `refused`; `verification_failed` is bare
92
- (reference parity).
93
- `"refused"` is the honest-signer refusal: the claims contradict the caller's
94
- own verified view, and the handle's `sign` callback is never reached (the
95
- atomic `keyIdentity` snapshot resolves first — the reference ordering).
96
- `"verification_failed"` is the separate post-sign discipline: the assembled
97
- artifact did not verify against the caller's view (for example a key that is
98
- not in the party descriptor's `verification_keys`). Malformed claims are
99
- rejected pre-sign as `"invalid_input"` through the verifier package's
100
- producer claims gate — they never burn a key operation. A signing failure is
101
- never a silent pass.
81
+ | `signDescriptor(claims, handle, opts?)` | Signs a genesis or successor Party Descriptor; successors require `opts.predecessor` |
82
+ | `signReceipt(claims, handle, context, opts?)` | Signs a Receipt against a chain view or bare revision text |
83
+ | `signAcceptance(claims, handle, chain, opts?)` | Signs an Acceptance using the context the verifier derives from the chain |
84
+ | `signTermination(claims, handle, chain, opts?)` | Signs a Termination using the context the verifier derives from the chain |
85
+ | `SigningOptions`, `DescriptorOptions`, `ChainView`, `KeyHandle`, `KeySnapshot`, `SignError`, `SignResult` | Public TypeScript contracts |
86
+
87
+ A `ChainView` contains four string arrays: `{ revisions, acceptances,
88
+ descriptors, terminations }`. Pass all four, including explicit empty arrays.
89
+ Acceptance and termination use this chain directly. The verifier derives the
90
+ claimed revision and the holder's own descriptor history from its verified
91
+ contents. The signer copies claims at entry, before awaiting a key callback.
92
+ Claims copies contain fresh plain JSON objects and arrays, strings, finite
93
+ numbers, booleans, and null. Repeated objects are copied separately. Non-JSON
94
+ claims, cycles, and throwing reads return `invalid_input`/`invalid_type`.
95
+ Chain context and predecessor use their defined members with ordinary property
96
+ reads, and retained artifact lists get fresh arrays. Valid members are immutable
97
+ text; invalid member values stay opaque for the verifier.
98
+
99
+ Claims copies use an iterative traversal guarded by the verifier's maximum
100
+ supported JSON bounds, rather than the caller's limits. Values beyond those
101
+ maximums return `invalid_input`/`signing_input_invalid`. Smaller claims reach
102
+ `prepareSigning`, which owns all checks under caller limits, including final
103
+ compact size. Context list copies retain enough entries to exceed every
104
+ supported artifact item limit, letting the verifier return its own result.
105
+ Predecessor lineage has no signer item limit. The verifier checks immutable
106
+ artifact text under artifact byte limits, separately from parsed claims' string
107
+ limits.
108
+
109
+ Migration from the previous issuing view:
110
+
111
+ ```ts
112
+ // Before
113
+ await signAcceptance(claims, handle, { revisionText, descriptorCompacts, chain }, opts);
114
+ // Now
115
+ await signAcceptance(claims, handle, chain, opts);
116
+ await signTermination(terminationClaims, handle, chain, opts);
117
+ ```
118
+
119
+ For a successor descriptor, pass the verifier's `DescriptorPredecessor` facts
120
+ with retained signed `lineage`. The signing key must match an active key in
121
+ the predecessor:
122
+
123
+ ```ts
124
+ import { verifyDescriptor } from "@charter-agreement-protocol/verifier";
125
+
126
+ const previous = verifyDescriptor(genesisCompact);
127
+ if (!previous.ok) throw new Error(previous.code);
128
+ const next = await signDescriptor(successorClaims, handle, { predecessor: previous.facts });
129
+ ```
130
+
131
+ For a receipt against bare revision text, the verifier checks the receipt's
132
+ claims against that revision. The revision names no signing key, so the signer
133
+ uses its snapshot for the custody guard; receipt verification reports
134
+ `signature` in `not_verified`. A chain-backed receipt checks the key against
135
+ the issuing party's descriptor keys in the holder's own view.
136
+
137
+ `opts` is absent or an object with known own keys only: `algorithm`, `limits`,
138
+ and `profile`, plus `predecessor` for `signDescriptor`. Another root shape, an
139
+ unknown own key, or a throwing getter returns `invalid_input` with
140
+ `code: "invalid_type"`. Defined members use ordinary property reads, once at
141
+ entry, including inherited and nonenumerable members. Class instances and
142
+ `Map` objects work where the verifier accepts their properties; Map entries
143
+ are not option properties.
144
+
145
+ `limits` is the verifier's `Partial<Limits>` and `profile` is its `Profile`.
146
+ The signer reads only their defined shape. Invalid values stay opaque for the
147
+ verifier's synchronous checks; they are not recursively copied by the signer.
148
+ Limits are checked before profile, algorithm, and the key snapshot. Malformed
149
+ values, including bigint, symbol, and boxed numbers, retain the verifier's
150
+ `invalid_limits` or `invalid_profile` result. Sparse algorithm lists return
151
+ `invalid_profile`; nonfinite or explicitly undefined limit values return
152
+ `invalid_limits`. A list longer than the algorithm registry cannot be a valid
153
+ unique subset, so its invalid length reaches the verifier without reading its
154
+ entries. The verifier's copy failures are also returned unchanged.
155
+ Limits bound the final compact, including signature bytes, and
156
+ every context artifact read; raised limits also apply to assembly and the
157
+ custody guard. Profiles can restrict `algorithms` and/or
158
+ `revisions: { min, max }`; an omitted axis keeps the full supported range.
159
+ An EdDSA-only profile, an ML-DSA-44/87-only profile, or revisions 1..1 excludes
160
+ every artifact this signer emits. Exclusions return `algorithm_outside_profile`
161
+ or `revision_outside_profile`.
162
+
163
+ <a id="profile-scope"></a>
164
+
165
+ For acceptance and termination, the caller's profile covers the claimed
166
+ revision, the signing party's own descriptor history, and the new artifact.
167
+ The rest of the set is checked under the full default profile. For receipts,
168
+ the profile covers the new artifact and the whole retained set. For descriptors,
169
+ it covers the new artifact and predecessor history.
170
+
171
+ Errors are closed:
172
+
173
+ | Result error | Meaning |
174
+ |---|---|
175
+ | `invalid_input`, with `code` | The verifier rejected claims, context, limits, or profile, or options were invalid |
176
+ | `invalid_key_handle` | The snapshot callback failed, its shape was malformed, or the verifier returned `signing_key_invalid` |
177
+ | `refused`, with `code: "signing_refused"` | The claims contradict the holder's own verified view (R1–R3) |
178
+ | `refused`, with `code: "signing_key_mismatch"` | The kid, public key bytes, or key algorithm does not match the holder's own descriptor key |
179
+ | `algorithm_unsupported_on_substrate` | The runtime cannot verify the selected algorithm or a context artifact |
180
+ | `signing_failed` | The callback failed, returned a malformed signature, or failed the wrong-key guard |
181
+ | `verification_failed` | `finishSigning` rejected what preparation approved |
182
+
183
+ Every failure has `ok: false`. Only `invalid_input` and `refused` carry `code`.
184
+ The signing callback can return a Buffer or any `Uint8Array`, including one
185
+ from another JavaScript realm. The verifier rejects Ed25519 public keys with
186
+ noncanonical y coordinates, no curve point, x = 0 with the sign bit set, or
187
+ small order as `invalid_key_handle` when they are snapshot keys. Descriptor
188
+ claims containing such keys can fail earlier with `signing_input_invalid`.
189
+ All pre-sign rejections leave the `sign` callback untouched; the atomic key
190
+ snapshot precedes `prepareSigning`. A malformed context type returns
191
+ `invalid_input`/`invalid_type`. For otherwise valid descriptor claims, predecessor
192
+ errors return `invalid_input` with these codes before signing:
193
+
194
+ - `invalid_type`: a non-object predecessor, a missing digest alias (both `digest`
195
+ and `descriptor_digest` absent), or missing, null, empty, or non-string `lineage`.
196
+ Throwing reads also return this code.
197
+ - `descriptor_chain_invalid`: an absent or null predecessor for a successor,
198
+ a valid predecessor supplied to a genesis, a broken signed lineage, or digest
199
+ aliases that disagree with the reverified lineage.
200
+ - `algorithm_outside_profile`, `revision_outside_profile`, or
201
+ `algorithm_unsupported_on_substrate`: predecessor history excluded by the
202
+ profile or requiring unavailable crypto.
102
203
 
103
204
  ## The sibling verifier
104
205
 
105
- This package is one of two independent TypeScript siblings implementing the
106
- Charter Agreement Protocol — no Elixir code or dependency at runtime; the
107
- Elixir reference is the certification oracle (the verifier's vendored
108
- corpus is certified against it, and its CI cross-verifies
109
- TypeScript-signed artifacts from raw bytes).
110
-
111
- - **This package** — `@charter-agreement-protocol/signer`: key custody,
112
- the wrong-key guard, post-sign verification, and the closed error
113
- vocabulary — all protocol logic delegated to the verifier.
114
- - [`@charter-agreement-protocol/verifier`](https://www.npmjs.com/package/@charter-agreement-protocol/verifier)
115
- — verification and the pure producer surface (signing inputs, claims
116
- gate, refusal guards, assembly) that this package signs through.
117
-
118
- This signer depends on the verifier (`^0.5.0`, locked to 0.5.0). When changing
119
- that dependency, publish the verifier version first, then lock and release
120
- the signer against the published version. The packages have independent
121
- SemVer versions; signer documentation and build-tool updates can release
122
- without a verifier version change.
206
+ The signer and verifier are separate TypeScript packages implementing the
207
+ Charter Agreement Protocol, with no Elixir dependency at runtime. The
208
+ verifier ships a corpus certified against the Elixir protocol reference.
209
+ This signer tests its artifacts through the TypeScript verifier.
210
+ All protocol checks run in `prepareSigning` and `finishSigning`; the signer
211
+ keeps key custody callbacks, the signature guard, entry copies, option
212
+ resolution, and the closed error vocabulary.
123
213
 
124
214
  ## Evidence
125
215
 
126
- - The Elixir reference repository's gate verifies TypeScript-signed
127
- artifacts from raw bytes (cross-implementation agreement, enforced in CI).
128
- - Framing, the producer claims gate, refusal guards, and assembly are the
129
- verifier package's producer surface (0.5.0) — the same single
130
- implementation the independent verifier certifies. What remains here is
131
- custody plus input policing: the issuing-view discipline composes the
132
- verifier's own `verifyChain`, never a local reimplementation.
133
- - Post-sign verification goes through the
134
- [@charter-agreement-protocol/verifier](https://www.npmjs.com/package/@charter-agreement-protocol/verifier)
135
- package — the certified dual-implementation-verified verifier.
136
-
137
- ## The verify side
138
-
139
- This package tracks the published verify line: it depends on
140
- `@charter-agreement-protocol/verifier` `^0.5.0`. Verifier 0.5.0 added
141
- consumer-side surface only; the producer surface this package uses is
142
- unchanged. The producer-side capability alignment (mirrored
143
- `capabilities()` with refuse-before-mint, declared minting metadata,
144
- profile-bound signing) is the next feature release on this line.
216
+ - Linux CI runs the suite on Node 24.8.0 and the current Node 24 line.
217
+ Real Node crypto mints Ed25519 and ML-DSA-65 genesis and successor descriptors.
218
+ Pre-sign rejection tests count real signing calls and require zero calls.
219
+ Tests cover key mismatches, predecessor checks, malformed contexts, profiles,
220
+ final compact limits, receipt claims, entry copies, and unknown algorithms.
221
+ - The workshop's browser bundle mints Ed25519 and runs the complete workshop
222
+ flow. It cannot verify ML-DSA-65 and refuses that request before signing with
223
+ the top-level `algorithm_unsupported_on_substrate` error.
224
+ - The [shared table](test/fixtures/README.md) runs all 121 rows from the Elixir
225
+ signer, `charter_agreement_signer` 0.5.0, with its exact source bytes pinned by SHA-256. Real seed
226
+ keys check result shapes and codes, snapshot and custody counts, 12 exact
227
+ Ed25519 success compacts, and five ML-DSA-65 successes through verification.
228
+ All 121 rows run in both Node and the browser bundle with no skips. Eight
229
+ shared requests or contexts that need ML-DSA-65 also return a substrate error
230
+ before custody in the browser bundle.
231
+ This compares the recorded Elixir outcomes; it does not run Elixir locally.
232
+
233
+ ## Verifier compatibility and telemetry
234
+
235
+ The verifier requires canonical digests and revision JSON, distinct party
236
+ histories, and all four chain arrays. Build failures surface as `invalid_input`
237
+ with `code: "signing_input_invalid"`; termination claims must omit `extensions`.
238
+ See [CHANGELOG.md](CHANGELOG.md) for migration notes.
239
+
240
+ This package has no telemetry events or hooks and no signer capability report.
145
241
 
146
242
  ## SemVer
147
243
 
@@ -156,13 +252,14 @@ or changes behavior.
156
252
  ## Development
157
253
 
158
254
  Use Node 24 (at least 24.8.0) and npm. The lockfile selects TypeScript 7.0.2
159
- and `@types/node` 26.6.4. CI runs on Linux with Node 24 and a frozen install;
160
- the same npm commands are used for local development on macOS and Linux.
255
+ and `@types/node` 26.6.4. CI runs on Linux with Node 24.8.0 and Node 24
256
+ using a frozen install; the same npm commands are used for local development on macOS and Linux.
161
257
 
162
258
  ```console
163
259
  npm ci
164
260
  npm run typecheck
165
261
  npm test
262
+ npm run test:cases
166
263
  npm run build
167
264
  npm run typecheck:web
168
265
  npm run build:site
package/SECURITY.md ADDED
@@ -0,0 +1,55 @@
1
+ # Security policy
2
+
3
+ ## Supported versions
4
+
5
+ The latest published `0.x` line is supported. Wire compatibility follows
6
+ the public `@charter-agreement-protocol/verifier` package's contract.
7
+
8
+ ## Security invariants
9
+
10
+ - The library never solicits, processes or retains private key material.
11
+ Custody lives behind the caller's key-handle callbacks.
12
+ - Every signature is verified against the snapshot public key before
13
+ success. A signature from another key returns `signing_failed`.
14
+ - Key identity (`kid` and public key) comes from one atomic `keyIdentity`
15
+ snapshot per signing call. The signer supplies that snapshot's kid to
16
+ the producer.
17
+ - Errors contain closed names and codes, with no key material, message
18
+ bytes, claims content or callback exception text.
19
+ - The library's only runtime dependency is the public verifier package.
20
+ It contains no transport or HTTP client/server.
21
+ - Test handles stay in test files and do not ship in the npm package.
22
+
23
+ ## Reporting a vulnerability
24
+
25
+ Report it privately through GitHub's private vulnerability reporting for
26
+ `baselabs/charter_agreement_signer_typescript` (the repository's Security
27
+ tab, then "Report a vulnerability"). Do not open a public issue containing
28
+ an exploit, credential, private key or unreleased vulnerability detail.
29
+
30
+ A report should include:
31
+
32
+ - the affected version or commit;
33
+ - the violated property;
34
+ - a minimal reproduction with newly generated test keys and no production
35
+ claims or credentials;
36
+ - the expected security outcome.
37
+
38
+ ## Verifying a release
39
+
40
+ The [Release workflow](https://github.com/baselabs/charter_agreement_signer_typescript/blob/main/.github/workflows/release.yml)
41
+ checks the package before staging it on npm with provenance through
42
+ GitHub Actions OIDC. The owner approves the staged release on npm with 2FA.
43
+ Inspect the published package's source repository, archive integrity and
44
+ provenance metadata with:
45
+
46
+ ```console
47
+ npm view @charter-agreement-protocol/signer repository.url dist.integrity dist.attestations
48
+ ```
49
+
50
+ ## Acknowledgment
51
+
52
+ We will acknowledge reports within 7 days and send status updates at least
53
+ weekly until resolution, coordinating disclosure through a private advisory.
54
+ This is a best-effort cadence from a small maintainer team, not a contractual
55
+ SLA. Mark urgent reports clearly.
package/dist/index.d.ts CHANGED
@@ -1 +1 @@
1
- export { signDescriptor, signReceipt, signAcceptance, signTermination, type KeyHandle, type KeySnapshot, type ChainView, type SignError, type SignResult, } from "./signer.ts";
1
+ export { signDescriptor, signReceipt, signAcceptance, signTermination, type KeyHandle, type KeySnapshot, type ChainView, type SigningOptions, type DescriptorOptions, type SignError, type SignResult, } from "./signer.ts";
package/dist/index.js CHANGED
@@ -2,8 +2,6 @@
2
2
  //
3
3
  // Public surface of the holder-side companion signer: one signing function
4
4
  // per artifact kind, the key-handle contract, and the closed error
5
- // vocabulary. ALL protocol logic — framing, the producer claims gate, the
6
- // R1-R3 refusal guards, assembly, and verification of the assembled
7
- // artifact — goes through @charter-agreement-protocol/verifier: exactly
8
- // one implementation. This package holds the custody half only.
5
+ // vocabulary. Protocol checks run through prepareSigning and finishSigning
6
+ // in @charter-agreement-protocol/verifier. This package keeps custody.
9
7
  export { signDescriptor, signReceipt, signAcceptance, signTermination, } from "./signer.js";
package/dist/signer.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { type DescriptorPredecessor, type Limits, type Profile } from "@charter-agreement-protocol/verifier";
1
2
  export type KeySnapshot = {
2
3
  kid: string;
3
4
  publicKey: string;
@@ -7,7 +8,7 @@ export type KeyHandle = {
7
8
  sign(message: Uint8Array, handle: unknown): Uint8Array | Promise<Uint8Array>;
8
9
  };
9
10
  export type SignError = {
10
- error: "invalid_handle";
11
+ error: "invalid_key_handle";
11
12
  } | {
12
13
  error: "signing_failed";
13
14
  } | {
@@ -15,7 +16,9 @@ export type SignError = {
15
16
  code: string;
16
17
  } | {
17
18
  error: "refused";
18
- code: string;
19
+ code: "signing_refused" | "signing_key_mismatch";
20
+ } | {
21
+ error: "algorithm_unsupported_on_substrate";
19
22
  } | {
20
23
  error: "verification_failed";
21
24
  };
@@ -32,31 +35,23 @@ export type ChainView = {
32
35
  descriptors: string[];
33
36
  terminations: string[];
34
37
  };
35
- export declare function signDescriptor(claims: Record<string, unknown>, keyHandle: unknown, opts?: {
36
- algorithm?: string;
37
- }): Promise<SignResult<{
38
+ export type SigningOptions = {
39
+ algorithm?: string | null;
40
+ limits?: Partial<Limits>;
41
+ profile?: Profile;
42
+ };
43
+ export type DescriptorOptions = SigningOptions & {
44
+ predecessor?: DescriptorPredecessor | null;
45
+ };
46
+ export declare function signDescriptor(claims: Record<string, unknown>, keyHandle: unknown, opts?: DescriptorOptions): Promise<SignResult<{
38
47
  descriptor: string;
39
48
  }>>;
40
- export declare function signReceipt(claims: Record<string, unknown>, keyHandle: unknown, chain: ChainView, opts?: {
41
- algorithm?: string;
42
- }): Promise<SignResult<{
49
+ export declare function signReceipt(claims: Record<string, unknown>, keyHandle: unknown, context: ChainView | string, opts?: SigningOptions): Promise<SignResult<{
43
50
  receipt: string;
44
51
  }>>;
45
- export declare function signAcceptance(claims: Record<string, unknown>, keyHandle: unknown, view: {
46
- revisionText: string;
47
- descriptorCompacts: string[];
48
- chain: ChainView;
49
- }, opts?: {
50
- algorithm?: string;
51
- }): Promise<SignResult<{
52
+ export declare function signAcceptance(claims: Record<string, unknown>, keyHandle: unknown, chain: ChainView, opts?: SigningOptions): Promise<SignResult<{
52
53
  acceptance: string;
53
54
  }>>;
54
- export declare function signTermination(claims: Record<string, unknown>, keyHandle: unknown, view: {
55
- revisionText: string;
56
- descriptorCompacts: string[];
57
- chain: ChainView;
58
- }, opts?: {
59
- algorithm?: string;
60
- }): Promise<SignResult<{
55
+ export declare function signTermination(claims: Record<string, unknown>, keyHandle: unknown, chain: ChainView, opts?: SigningOptions): Promise<SignResult<{
61
56
  termination: string;
62
57
  }>>;
package/dist/signer.js CHANGED
@@ -1,180 +1,289 @@
1
1
  // CAP never authorizes.
2
- //
3
- // The holder-side companion signer for the Charter Agreement Protocol,
4
- // ported from the Elixir reference (charter_agreement_signer). The caller
5
- // owns key custody through a handle object with two callbacks:
6
- //
7
- // keyIdentity(handle) -> { kid, publicKey } // ONE atomic snapshot
8
- // sign(message, handle) -> signature bytes // the holder's job
9
- //
10
- // Framing, the producer claims gate, the honest-signer refusal guards
11
- // (R1–R3), and assembly are ALL the verifier package's producer surface —
12
- // exactly one implementation, exactly the reference signer's delegation.
13
- // This package adds the custody half: the atomic key snapshot, the sign
14
- // callback, the wrong-key guard against the snapshot's public key, and the
15
- // post-sign verification of the assembled artifact. It never verifies
16
- // third-party artifacts, never transports, never persists, and never sees
17
- // a private key.
18
- import { acceptanceSigningInput, algorithmRegistry, assembleCompact, descriptorSigningInput, receiptSigningInput, terminationSigningInput, verifyAcceptance, verifyChain, verifyDescriptor, verifyReceipt, verifySignature, verifyTermination, } from "@charter-agreement-protocol/verifier";
19
- function registryRow(algorithm) {
20
- return algorithmRegistry().find((row) => row.name === algorithm) ?? null;
2
+ // The holder supplies one key snapshot and signs the verifier's prepared
3
+ // message. The verifier owns framing, claims, context, limits, profile,
4
+ // key matching, assembly, and completion. This package keeps custody and
5
+ // the wrong-key guard.
6
+ import { DEFAULT_LIMITS, algorithmRegistry, MAXIMUM_LIMITS, defaultEmissionName, decodeCharterRevision, emissions, finishSigning, prepareSigning, verifySignature, } from "@charter-agreement-protocol/verifier";
7
+ const COPY_LIMIT = Symbol("copy limit");
8
+ // Read one child at a time without recursion or array constructor hooks.
9
+ // Only ancestors are retained, so repeated objects get separate copies.
10
+ function copyClaims(value) {
11
+ const limits = MAXIMUM_LIMITS;
12
+ const ancestors = new WeakSet();
13
+ const frames = [];
14
+ let bytes = 0;
15
+ const addBytes = (size) => {
16
+ bytes += size;
17
+ if (bytes > limits.max_bytes)
18
+ throw COPY_LIMIT;
19
+ };
20
+ const stringBytes = (text) => {
21
+ if (Buffer.byteLength(text) > limits.max_string_bytes)
22
+ throw COPY_LIMIT;
23
+ return Buffer.byteLength(JSON.stringify(text));
24
+ };
25
+ const copy = (item, depth) => {
26
+ if (item === null || typeof item === "boolean") {
27
+ addBytes(item === null || item === true ? 4 : 5);
28
+ return item;
29
+ }
30
+ if (typeof item === "string") {
31
+ addBytes(stringBytes(item));
32
+ return item;
33
+ }
34
+ if (typeof item === "number" && Number.isFinite(item)) {
35
+ addBytes(JSON.stringify(item).length);
36
+ return item;
37
+ }
38
+ if (typeof item !== "object" || ancestors.has(item))
39
+ throw new TypeError("invalid_type");
40
+ if (depth + 1 > limits.max_depth)
41
+ throw COPY_LIMIT;
42
+ const array = Array.isArray(item);
43
+ let keys = null;
44
+ if (!array) {
45
+ const prototype = Object.getPrototypeOf(item);
46
+ if ((prototype !== Object.prototype && prototype !== null) || Object.getOwnPropertySymbols(item).length > 0) {
47
+ throw new TypeError("invalid_type");
48
+ }
49
+ keys = Object.keys(item);
50
+ }
51
+ const size = array ? item.length : keys.length;
52
+ if (!Number.isSafeInteger(size) || size < 0 || array && size > 2 ** 32 - 1)
53
+ throw new TypeError("invalid_type");
54
+ if (size > (array ? limits.max_array_items : limits.max_object_members)) {
55
+ throw COPY_LIMIT;
56
+ }
57
+ addBytes(2 + Math.max(0, size - 1));
58
+ const target = array ? [] : Object.create(null);
59
+ ancestors.add(item);
60
+ frames.push({ source: item, target, keys, size, index: 0, depth: depth + 1 });
61
+ return target;
62
+ };
63
+ const result = copy(value, 0);
64
+ while (frames.length > 0) {
65
+ const frame = frames[frames.length - 1];
66
+ if (frame.index === frame.size) {
67
+ ancestors.delete(frame.source);
68
+ frames.pop();
69
+ continue;
70
+ }
71
+ const index = frame.index++;
72
+ const key = frame.keys ? frame.keys[index] : String(index);
73
+ if (frame.keys)
74
+ addBytes(stringBytes(key) + 1);
75
+ const child = frame.source[key];
76
+ const copied = copy(child, frame.depth);
77
+ if (Array.isArray(frame.target))
78
+ frame.target.push(copied);
79
+ else
80
+ frame.target[key] = copied;
81
+ }
82
+ return result;
83
+ }
84
+ function copyChainView(chain) {
85
+ const names = ["revisions", "acceptances", "descriptors", "terminations"];
86
+ const lists = Object.fromEntries(names.map((name) => [name, (value) => copyTextList(value, MAXIMUM_LIMITS.max_artifact_set_items + 1)]));
87
+ return optionMembers(chain, names, lists);
21
88
  }
22
89
  async function resolveKeyIdentity(handle) {
23
- if (handle === null || typeof handle !== "object" || !("keyIdentity" in handle)) {
24
- return { error: "invalid_handle" };
25
- }
26
90
  try {
91
+ if (handle === null || typeof handle !== "object")
92
+ return { error: "invalid_key_handle" };
27
93
  const snapshot = await handle.keyIdentity(handle);
28
- if (snapshot &&
29
- typeof snapshot.kid === "string" &&
30
- snapshot.kid.length > 0 &&
31
- typeof snapshot.publicKey === "string" &&
32
- Buffer.from(snapshot.publicKey, "base64url").length > 0) {
33
- return snapshot;
94
+ if (!snapshot || typeof snapshot !== "object" || Array.isArray(snapshot))
95
+ return { error: "invalid_key_handle" };
96
+ const { kid, publicKey } = snapshot;
97
+ if (typeof kid !== "string" || kid.length === 0 || typeof publicKey !== "string") {
98
+ return { error: "invalid_key_handle" };
34
99
  }
35
- return { error: "invalid_handle" };
100
+ return { kid, publicKey };
36
101
  }
37
102
  catch (_fault) {
38
- return { error: "invalid_handle" };
103
+ return { error: "invalid_key_handle" };
104
+ }
105
+ }
106
+ const OPTION_KEYS = new Set(["algorithm", "limits", "profile"]);
107
+ const byteArrayTag = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(Uint8Array.prototype), Symbol.toStringTag).get;
108
+ // Read the defined shape once, including inherited and nonenumerable members.
109
+ // Invalid member values stay opaque for the verifier to check synchronously.
110
+ function optionMembers(value, names, nested = {}) {
111
+ if (value === null || typeof value !== "object" || Array.isArray(value) || value instanceof Number)
112
+ return value;
113
+ const source = value;
114
+ const result = Object.create(null);
115
+ const unknown = Object.keys(source).find((name) => !names.includes(name));
116
+ if (unknown !== undefined)
117
+ result[unknown] = null;
118
+ for (const name of names) {
119
+ const member = source[name];
120
+ if (member !== undefined || name in source)
121
+ result[name] = nested[name] ? nested[name](member) : member;
122
+ }
123
+ return result;
124
+ }
125
+ function optionAlgorithms(value) {
126
+ if (!Array.isArray(value))
127
+ return value;
128
+ const length = value.length;
129
+ if (!Number.isSafeInteger(length) || length < 0)
130
+ throw new TypeError("invalid_type");
131
+ // A unique registry subset cannot be longer than the registry. Retain an
132
+ // invalid length for the verifier without reading an unbounded caller list.
133
+ if (length > MAXIMUM_LIMITS.max_artifact_set_items)
134
+ return Array(MAXIMUM_LIMITS.max_artifact_set_items + 1);
135
+ if (length > algorithmRegistry().length)
136
+ return value;
137
+ const result = [];
138
+ for (let i = 0; i < length; i += 1)
139
+ result.push(value[i]);
140
+ return result;
141
+ }
142
+ function copyTextList(value, maximum = 2 ** 32 - 1) {
143
+ if (!Array.isArray(value))
144
+ return value;
145
+ const length = value.length;
146
+ // A plain array cannot represent a length above 2^32 - 1.
147
+ if (!Number.isSafeInteger(length) || length < 0 || length > 2 ** 32 - 1)
148
+ throw new TypeError("invalid_type");
149
+ const result = [];
150
+ for (let i = 0; i < Math.min(length, maximum); i += 1)
151
+ result.push(value[i]);
152
+ return result;
153
+ }
154
+ function checkOptions(settings) {
155
+ // Empty text always gives invalid_syntax after successful option checks.
156
+ // Every earlier code comes from the verifier, including copy failures.
157
+ const checked = decodeCharterRevision("", settings);
158
+ return !checked.ok && checked.code !== "invalid_syntax" ? { error: "invalid_input", code: checked.code } : null;
159
+ }
160
+ function signingOptions(opts, descriptor) {
161
+ try {
162
+ if (opts === null || typeof opts !== "object" || Array.isArray(opts) || opts instanceof Number) {
163
+ return { error: "invalid_input", code: "invalid_type" };
164
+ }
165
+ if (!Reflect.ownKeys(opts).every((key) => typeof key === "string" &&
166
+ (OPTION_KEYS.has(key) || descriptor && key === "predecessor"))) {
167
+ return { error: "invalid_input", code: "invalid_type" };
168
+ }
169
+ const supplied = opts;
170
+ const settings = {
171
+ limits: optionMembers(supplied.limits, Object.keys(DEFAULT_LIMITS)),
172
+ };
173
+ const limitsError = checkOptions(settings);
174
+ if (limitsError)
175
+ return limitsError;
176
+ settings.profile = optionMembers(supplied.profile, ["algorithms", "revisions"], {
177
+ algorithms: optionAlgorithms,
178
+ revisions: (range) => optionMembers(range, ["min", "max"]),
179
+ });
180
+ const profileError = checkOptions(settings);
181
+ if (profileError)
182
+ return profileError;
183
+ const algorithm = supplied.algorithm ?? defaultEmissionName();
184
+ if (typeof algorithm !== "string" || !Object.hasOwn(emissions(), algorithm)) {
185
+ return { error: "invalid_input", code: "algorithm_unsupported" };
186
+ }
187
+ if (!descriptor)
188
+ return { ...settings, algorithm };
189
+ const previous = supplied.predecessor;
190
+ const predecessor = optionMembers(previous, ["digest", "descriptor_digest", "lineage"], {
191
+ lineage: (value) => copyTextList(value),
192
+ });
193
+ return { ...settings, algorithm, predecessor: predecessor };
194
+ }
195
+ catch (fault) {
196
+ return { error: "invalid_input", code: fault === COPY_LIMIT ? "limit_exceeded" : "invalid_type" };
39
197
  }
40
198
  }
41
- // The shared custody tail (the reference signer's): identity snapshot ->
42
- // producer (framing, claims schema, R1-R3 refusals - one implementation,
43
- // in the verifier package) -> sign via the handle -> wrong-key guard.
44
- // A refusal or producer rejection never touches the key.
45
- async function signProduced(keyHandle, produce, opts) {
199
+ function prepareError(code) {
200
+ switch (code) {
201
+ case "signing_refused":
202
+ case "signing_key_mismatch":
203
+ return { error: "refused", code };
204
+ case "signing_key_invalid":
205
+ return { error: "invalid_key_handle" };
206
+ case "algorithm_unsupported_on_substrate":
207
+ return { error: "algorithm_unsupported_on_substrate" };
208
+ default:
209
+ return { error: "invalid_input", code };
210
+ }
211
+ }
212
+ async function signPrepared(kind, claims, keyHandle, context, opts) {
213
+ const options = signingOptions(opts, kind === "descriptor");
214
+ if ("error" in options)
215
+ return options;
216
+ let retained;
217
+ try {
218
+ retained = kind === "descriptor" ? options.predecessor ?? null :
219
+ typeof context === "string" ? context : copyChainView(context);
220
+ }
221
+ catch (fault) {
222
+ return { error: "invalid_input", code: fault === COPY_LIMIT ? "limit_exceeded" : "invalid_type" };
223
+ }
224
+ try {
225
+ // Caller limits belong to preparation. The entry guard only stops values
226
+ // beyond every JSON limit the verifier can accept.
227
+ claims = copyClaims(claims);
228
+ }
229
+ catch (fault) {
230
+ return { error: "invalid_input", code: fault === COPY_LIMIT ? "signing_input_invalid" : "invalid_type" };
231
+ }
46
232
  const snapshot = await resolveKeyIdentity(keyHandle);
47
233
  if ("error" in snapshot)
48
234
  return snapshot;
49
- const produced = produce(snapshot.kid, opts?.algorithm);
50
- if (!produced.ok) {
51
- // signing_refused is the honest-signer refusal; every other producer
52
- // code stays caller input.
53
- return produced.code === "signing_refused"
54
- ? { error: "refused", code: produced.code }
55
- : { error: "invalid_input", code: produced.code };
56
- }
57
- // The holder signs the exact message; a fault or a wrong-length result is
58
- // a signing failure, never a silent pass.
235
+ const prepared = prepareSigning(kind, snapshot.kid, claims, snapshot.publicKey, retained, {
236
+ algorithm: options.algorithm,
237
+ limits: options.limits,
238
+ profile: options.profile,
239
+ });
240
+ if (!prepared.ok)
241
+ return prepareError(prepared.code);
242
+ const input = prepared.plan.input;
59
243
  let signature;
60
244
  try {
61
- signature = await keyHandle.sign(produced.input.message, keyHandle);
245
+ signature = await keyHandle.sign(Buffer.from(input.message), keyHandle);
62
246
  }
63
247
  catch (_fault) {
64
248
  return { error: "signing_failed" };
65
249
  }
66
- if (!(signature instanceof Uint8Array) || signature.length === 0) {
250
+ if (!ArrayBuffer.isView(signature) || byteArrayTag.call(signature) !== "Uint8Array") {
67
251
  return { error: "signing_failed" };
68
252
  }
69
- const row = registryRow(produced.input.alg);
70
- if (!row || signature.length !== row.signatureBytes) {
253
+ let checkedSignature;
254
+ try {
255
+ checkedSignature = Buffer.from(signature);
256
+ }
257
+ catch (_signature) {
71
258
  return { error: "signing_failed" };
72
259
  }
73
- // The wrong-key guard: the signature MUST verify against the snapshot's
74
- // public key under the minted algorithm.
75
- if (!verifySignature(produced.input.message, Buffer.from(signature), snapshot.publicKey, produced.input.alg)) {
260
+ // The verifier's signature guard owns signature length checks.
261
+ if (!verifySignature(prepared.plan.input.message, checkedSignature, snapshot.publicKey, input.alg, {
262
+ limits: options.limits,
263
+ profile: options.profile,
264
+ })) {
76
265
  return { error: "signing_failed" };
77
266
  }
78
- return { input: produced.input, signature: Buffer.from(signature) };
79
- }
80
- // Assembly goes through the verifier's assembleCompact: registry-row
81
- // length, framing re-decode, size gate - never a local string join.
82
- function compactOrInvalid(signed) {
83
- const assembled = assembleCompact(signed.input, signed.signature);
84
- if (!assembled.ok)
85
- return { error: "invalid_input", code: assembled.code };
86
- return { compact: assembled.compact };
87
- }
88
- // The view's chain is THIS package's public contract (the producer surface
89
- // validates its depth); the shallow shape is policed here so the closed
90
- // error vocabulary holds regardless of which verifier revision resolves.
91
- // A verifier crash past this gate stays loud - never a misleading error.
92
- function chainViewOrInvalid(chain) {
93
- if (!chain || typeof chain !== "object" || Array.isArray(chain)) {
94
- return { error: "invalid_input", code: "signing_input_invalid" };
95
- }
96
- return chain;
97
- }
98
- // A missing view object is caller input like a missing chain: the closed
99
- // error, never a dereference throw.
100
- function viewChainOrInvalid(view) {
101
- if (!view || typeof view !== "object")
102
- return { error: "invalid_input", code: "signing_input_invalid" };
103
- return chainViewOrInvalid(view.chain);
267
+ const finished = finishSigning(prepared.plan, checkedSignature);
268
+ // No real-crypto path reaches this mapping after preparation and the
269
+ // custody guard succeed; keep it for a verifier completion error.
270
+ if (!finished.ok)
271
+ return { error: "verification_failed" };
272
+ return { compact: finished.compact };
104
273
  }
105
- // ---------------------------------------------------------------------------
106
- // Public signing surface
107
- // ---------------------------------------------------------------------------
108
274
  export async function signDescriptor(claims, keyHandle, opts = {}) {
109
- const signed = await signProduced(keyHandle, (kid, algorithm) => descriptorSigningInput(kid, claims, algorithm), opts);
110
- if ("error" in signed)
111
- return { ok: false, ...signed };
112
- const assembled = compactOrInvalid(signed);
113
- if ("error" in assembled)
114
- return { ok: false, ...assembled };
115
- const verified = verifyDescriptor(assembled.compact);
116
- if (!verified.ok)
117
- return { ok: false, error: "verification_failed" };
118
- return { ok: true, result: { descriptor: assembled.compact } };
275
+ const signed = await signPrepared("descriptor", claims, keyHandle, null, opts);
276
+ return "error" in signed ? { ok: false, ...signed } : { ok: true, result: { descriptor: signed.compact } };
119
277
  }
120
- export async function signReceipt(claims, keyHandle, chain, opts = {}) {
121
- // The issuing view is REQUIRED (the reference takes ChainFacts or a
122
- // CharterRevision, never none) and its discipline runs after the build,
123
- // before the key - the reference's context is a verified ChainFacts by
124
- // construction, so a TS view that fails the chain discipline is caller
125
- // input, never a burned key operation.
126
- const signed = await signProduced(keyHandle, (kid, algorithm) => {
127
- const built = receiptSigningInput(kid, claims, algorithm);
128
- if (!built.ok)
129
- return built;
130
- const resolved = chainViewOrInvalid(chain);
131
- if ("error" in resolved)
132
- return { ok: false, code: resolved.code };
133
- if (!verifyChain(resolved).ok)
134
- return { ok: false, code: "chain_invalid" };
135
- return built;
136
- }, opts);
137
- if ("error" in signed)
138
- return { ok: false, ...signed };
139
- const assembled = compactOrInvalid(signed);
140
- if ("error" in assembled)
141
- return { ok: false, ...assembled };
142
- const verified = verifyReceipt(assembled.compact, chain);
143
- if (!verified.ok)
144
- return { ok: false, error: "verification_failed" };
145
- return { ok: true, result: { receipt: assembled.compact } };
278
+ export async function signReceipt(claims, keyHandle, context, opts = {}) {
279
+ const signed = await signPrepared("receipt", claims, keyHandle, context, opts);
280
+ return "error" in signed ? { ok: false, ...signed } : { ok: true, result: { receipt: signed.compact } };
146
281
  }
147
- export async function signAcceptance(claims, keyHandle, view, opts = {}) {
148
- const viewChain = viewChainOrInvalid(view);
149
- const signed = await signProduced(keyHandle, (kid, algorithm) => {
150
- if ("error" in viewChain)
151
- return { ok: false, code: viewChain.code };
152
- return acceptanceSigningInput(kid, claims, viewChain, algorithm);
153
- }, opts);
154
- if ("error" in signed)
155
- return { ok: false, ...signed };
156
- const assembled = compactOrInvalid(signed);
157
- if ("error" in assembled)
158
- return { ok: false, ...assembled };
159
- const verified = verifyAcceptance(assembled.compact, view.revisionText, view.descriptorCompacts);
160
- if (!verified.ok)
161
- return { ok: false, error: "verification_failed" };
162
- return { ok: true, result: { acceptance: assembled.compact } };
282
+ export async function signAcceptance(claims, keyHandle, chain, opts = {}) {
283
+ const signed = await signPrepared("acceptance", claims, keyHandle, chain, opts);
284
+ return "error" in signed ? { ok: false, ...signed } : { ok: true, result: { acceptance: signed.compact } };
163
285
  }
164
- export async function signTermination(claims, keyHandle, view, opts = {}) {
165
- const viewChain = viewChainOrInvalid(view);
166
- const signed = await signProduced(keyHandle, (kid, algorithm) => {
167
- if ("error" in viewChain)
168
- return { ok: false, code: viewChain.code };
169
- return terminationSigningInput(kid, claims, viewChain, algorithm);
170
- }, opts);
171
- if ("error" in signed)
172
- return { ok: false, ...signed };
173
- const assembled = compactOrInvalid(signed);
174
- if ("error" in assembled)
175
- return { ok: false, ...assembled };
176
- const verified = verifyTermination(assembled.compact, view.revisionText, view.descriptorCompacts);
177
- if (!verified.ok)
178
- return { ok: false, error: "verification_failed" };
179
- return { ok: true, result: { termination: assembled.compact } };
286
+ export async function signTermination(claims, keyHandle, chain, opts = {}) {
287
+ const signed = await signPrepared("termination", claims, keyHandle, chain, opts);
288
+ return "error" in signed ? { ok: false, ...signed } : { ok: true, result: { termination: signed.compact } };
180
289
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@charter-agreement-protocol/signer",
3
- "version": "0.2.2",
4
- "description": "Holder-side companion signer for the Charter Agreement Protocol (CAP) in TypeScript - key custody only: an atomic key snapshot, the sign callback, the wrong-key guard, and post-sign verification, with framing, the claims gate, the R1-R3 honest-signer refusals, and assembly all delegated to the @charter-agreement-protocol/verifier producer surface. CAP verifies; it never authorizes; this signer signs, it never verifies third-party artifacts or takes custody.",
3
+ "version": "0.3.0",
4
+ "description": "Holder-side companion signer for the Charter Agreement Protocol (CAP) in TypeScript - key custody only: an atomic key snapshot, the sign callback, the wrong-key guard, and post-sign verification, with framing, claims, context, key matching, limits, profile, and completion delegated to the @charter-agreement-protocol/verifier pre-sign API. CAP verifies; it never authorizes; this signer signs, it never verifies third-party artifacts or takes custody.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
7
7
  "main": "./dist/index.js",
@@ -15,24 +15,29 @@
15
15
  },
16
16
  "files": [
17
17
  "CHANGELOG.md",
18
+ "CODE_OF_CONDUCT.md",
19
+ "CONTRIBUTING.md",
18
20
  "dist",
19
21
  "LICENSE",
20
22
  "NOTICE",
21
- "README.md"
23
+ "README.md",
24
+ "SECURITY.md"
22
25
  ],
23
26
  "engines": {
24
27
  "node": ">=24.8.0"
25
28
  },
26
29
  "scripts": {
27
- "build": "tsc -p tsconfig.build.json",
30
+ "build": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\" && tsc -p tsconfig.build.json",
28
31
  "typecheck": "tsc --noEmit",
29
- "test": "node --test --experimental-strip-types --no-warnings signer.test.ts",
32
+ "test": "node --test --experimental-strip-types --no-warnings signer.test.ts signer.browser.test.ts signer.cases.test.ts",
33
+ "test:cost": "node --test --experimental-strip-types --no-warnings signer.cost.test.ts",
34
+ "test:cases": "node --test --experimental-strip-types --no-warnings signer.cases.test.ts",
30
35
  "prepack": "npm run build",
31
36
  "typecheck:web": "tsc -p tsconfig.web.json",
32
37
  "build:site": "node web/build.mjs && esbuild web/main.ts --bundle --minify --format=iife --target=es2022 --outfile=site-dist/app.js --alias:node:crypto=./web/shim/node-crypto.ts --alias:node:fs=./web/shim/node-fs.ts --alias:node:path=./web/shim/node-path.ts --inject:./web/shim/buffer-global.ts"
33
38
  },
34
39
  "dependencies": {
35
- "@charter-agreement-protocol/verifier": "^0.5.0"
40
+ "@charter-agreement-protocol/verifier": "^0.7.0"
36
41
  },
37
42
  "devDependencies": {
38
43
  "@noble/ed25519": "^3.2.0",