@ziffer-io/client 0.1.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/LICENSE ADDED
@@ -0,0 +1,120 @@
1
+ ZIFFER SDK LICENSE
2
+
3
+ Copyright (c) 2026 code75 SASU, Paris, France. All rights reserved.
4
+ ZIFFER is a registered trademark of code75 SASU ("code75") — code75.io · ziffer.io
5
+
6
+ This licence governs the software package it accompanies (the "SDK"): the ZIFFER client
7
+ libraries, receipt verifier, wire types and MCP server published by code75. The SDK is
8
+ proprietary software. It is not open source.
9
+
10
+ 1. DEFINITIONS
11
+ "Agreement" means the written contract under which code75 provides the ZIFFER service to
12
+ you or the organisation you act for. code75's standard form of that contract is the ZIFFER
13
+ Customer Agreement, docs/legal/ziffer-customer-agreement.md, v0.1, provided on request.
14
+ "Service" means the ZIFFER service so provided. "You" means the person or organisation
15
+ using the SDK under an Agreement.
16
+
17
+ 2. LICENCE
18
+ Subject to the Agreement and to this licence, code75 grants you a limited, non-exclusive,
19
+ non-transferable, non-sublicensable, revocable licence to install and run the SDK, in
20
+ unmodified form, solely to interact with the Service and to verify the receipts it issues,
21
+ for the term of the Agreement.
22
+
23
+ 3. RESTRICTIONS
24
+ Except as expressly permitted by section 2 or by a law that cannot be excluded by
25
+ contract, you shall not, and shall not permit anyone else to:
26
+ (a) copy, distribute, publish, sublicense, sell, rent, lease or lend the SDK, or make it
27
+ available to any third party;
28
+ (b) modify, adapt, translate or create derivative works of the SDK;
29
+ (c) reverse engineer, decompile, disassemble or otherwise attempt to derive the source
30
+ code, algorithms or protocols of the SDK or the Service;
31
+ (d) use the SDK to build, train or benchmark a product or service that competes with the
32
+ Service, or to access the Service other than through the interfaces code75 documents;
33
+ (e) circumvent, disable or interfere with any security, verification or usage-control
34
+ mechanism of the SDK or the Service;
35
+ (f) remove, obscure or alter any copyright, trademark or other proprietary notice on or
36
+ in the SDK.
37
+
38
+ 4. OWNERSHIP AND INTELLECTUAL PROPERTY
39
+ The SDK is licensed, not sold. code75 and its licensors own and retain all right, title
40
+ and interest in and to the SDK and the Service, including all copyright, patent, trade
41
+ secret, trademark and other intellectual property rights, and all improvements and
42
+ derivative works, by whomever made. Nothing in this licence transfers any such right to
43
+ you. All rights not expressly granted are reserved. Any suggestion, idea or feedback you
44
+ provide about the SDK or the Service may be used by code75 without restriction or
45
+ compensation. "ZIFFER" and "code75" and the associated logos are brands and marks of
46
+ code75; this licence grants no right to use them.
47
+
48
+ 5. THIRD-PARTY COMPONENTS
49
+ The SDK includes third-party open-source components listed in the accompanying file
50
+ THIRD-PARTY-NOTICES. Those components are licensed under their own terms, which govern
51
+ them and prevail over this licence for those components only. This licence does not
52
+ apply to them.
53
+
54
+ 6. UPDATES
55
+ code75 may release updated versions of the SDK. This licence applies to each version you
56
+ install unless a later version is accompanied by a different licence. code75 has no
57
+ obligation to provide updates or support except as stated in the Agreement.
58
+
59
+ 7. YOUR RESPONSIBILITY FOR YOUR ACTIONS AND YOUR DATA
60
+ The Service issues decisions and signed receipts; it does not perform any action. You
61
+ alone decide what your systems do with a decision, you alone hold the credentials with
62
+ which any action is performed, and you alone are responsible for every action your
63
+ systems take or fail to take, for the policy you write and sign, for the data you submit
64
+ to the Service, and for your compliance with applicable law, including data protection,
65
+ export control and sanctions laws. A decision or receipt is not legal, regulatory,
66
+ financial or professional advice and is not a guarantee that any action is lawful,
67
+ appropriate or safe.
68
+
69
+ 7A. INDEMNITY
70
+ You shall defend, indemnify and hold harmless code75, its officers, employees and
71
+ contractors from and against any claim, loss, liability, damage, cost or expense
72
+ (including reasonable legal fees) arising out of or relating to: (a) your use of the
73
+ SDK or the Service; (b) any action taken or not taken by your systems; (c) the policy,
74
+ data or content you provide; or (d) your breach of this licence, the Agreement or
75
+ applicable law.
76
+
77
+ 7B. PRE-RELEASE SOFTWARE
78
+ The SDK and the Service may be provided in a pre-release, evaluation or limited form.
79
+ You accept that such software may contain errors and may change or be withdrawn, and
80
+ you use it at your own risk.
81
+
82
+ 8. NO WARRANTY
83
+ THE SDK IS PROVIDED "AS IS" AND "AS AVAILABLE", WITHOUT WARRANTY OF ANY KIND, EXPRESS,
84
+ IMPLIED OR STATUTORY, INCLUDING WITHOUT LIMITATION ANY WARRANTY OF MERCHANTABILITY,
85
+ FITNESS FOR A PARTICULAR PURPOSE, TITLE OR NON-INFRINGEMENT, TO THE MAXIMUM EXTENT
86
+ PERMITTED BY LAW.
87
+
88
+ 9. LIMITATION OF LIABILITY
89
+ TO THE MAXIMUM EXTENT PERMITTED BY LAW, CODE75 SHALL NOT BE LIABLE FOR ANY INDIRECT,
90
+ INCIDENTAL, SPECIAL, CONSEQUENTIAL OR PUNITIVE DAMAGES, OR FOR ANY LOSS OF PROFITS,
91
+ REVENUE, DATA OR GOODWILL, ARISING OUT OF OR RELATING TO THE SDK, HOWEVER CAUSED AND
92
+ UNDER ANY THEORY OF LIABILITY, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
93
+ CODE75'S TOTAL LIABILITY UNDER THIS LICENCE SHALL NOT EXCEED THE AMOUNT STATED IN THE
94
+ AGREEMENT OR, IF NONE IS STATED, ONE HUNDRED EUROS (EUR 100). Nothing in this licence
95
+ excludes or limits liability that cannot be excluded or limited under applicable law.
96
+
97
+ 10. TERMINATION
98
+ This licence terminates automatically, without notice, if you breach it or when the
99
+ Agreement ends. On termination you shall stop all use of the SDK and destroy every copy
100
+ in your possession or control. Sections 3, 4, 7, 7A, 8, 9, 10, 11 and 12 survive
101
+ termination.
102
+
103
+ 11. GOVERNING LAW AND JURISDICTION
104
+ This licence is governed by the laws of France, without regard to conflict-of-law rules.
105
+ Any dispute arising out of or relating to this licence is submitted to the exclusive
106
+ jurisdiction of the competent courts of Paris, France, unless the Agreement states
107
+ otherwise.
108
+
109
+ 12. GENERAL
110
+ This licence, the Agreement and THIRD-PARTY-NOTICES are the entire terms for the SDK.
111
+ Where this licence and the Agreement conflict, the Agreement prevails. If any provision
112
+ of this licence is held unenforceable, it is enforced to the maximum extent permitted
113
+ and the remainder stays in effect. No failure or delay by code75 in exercising a right
114
+ is a waiver of it. You may not assign this licence without code75's written consent;
115
+ code75 may assign it to a successor of the ZIFFER business. Any claim by you relating to
116
+ the SDK must be brought within one (1) year after it arises, to the extent the law
117
+ allows. This licence is written in English; a translation is for convenience only and
118
+ the English text prevails.
119
+
120
+ Contact: code75 SASU, Paris — code75.io · ziffer.io
package/README.md ADDED
@@ -0,0 +1,69 @@
1
+ # `@ziffer-io/client`
2
+
3
+ The TypeScript SDK for the ZIFFER decision API: submit a proposal, wait for the decision, and
4
+ verify the receipt before you act on it.
5
+
6
+ ```bash
7
+ npm install @ziffer-io/client
8
+ ```
9
+
10
+ That one line brings the wire types (`@ziffer-io/types`) and the verifier (`@ziffer-io/verify`)
11
+ with it. `verifyReceipt` is **re-exported unchanged** from the verifier's own package, so there is
12
+ one verifier in your dependency tree rather than two that can disagree — the tests assert it is
13
+ the same function object, so the re-export cannot decay into a copy quietly.
14
+
15
+ ## The whole integration
16
+
17
+ ```ts
18
+ import { ZifferClient, verifyReceipt, Refusal, type TrustAnchor } from '@ziffer-io/client';
19
+
20
+ const client = new ZifferClient(process.env.ZIFFER_API_URL, process.env.ZIFFER_API_KEY);
21
+ const anchor: TrustAnchor = await loadAnchor(process.env.ZIFFER_TRUST_ANCHOR);
22
+
23
+ const submitted = await client.propose(proposal);
24
+ const decision = await client.wait(submitted.decision_id, { timeoutMs: 30_000 });
25
+ if (decision.outcome !== 'ALLOW') throw new Error(`ziffer refused: ${decision.clause}`);
26
+
27
+ verifyReceipt(decision.receipt, new TextEncoder().encode(JSON.stringify(proposal)), anchor);
28
+ await bank.transfer(amount, toAccount); // your line, unchanged
29
+ ```
30
+
31
+ `verifyReceipt` throws on any defect. Catch `Refusal` and read `.clause` — narrow with
32
+ `instanceof`, never a cast: a `catch` binds `unknown` and also sees the errors you did not plan
33
+ for, so an `as Refusal` would read a `clause` off a `TypeError` and report a protocol refusal that
34
+ never happened.
35
+
36
+ `docs/onboarding/sdk.md` section 8 is the same integration at length, with what each refusal means.
37
+
38
+ ## What it does not do
39
+
40
+ - **It computes no security value, and that is deliberate.** The proposal goes out as you wrote it
41
+ and the receipt comes back as the gateway stored it. A compromised client writes the whole
42
+ message, so nothing a client derives about its own request is evidence (RES-8). Every check that
43
+ matters happens inside `verifyReceipt`, over bytes you supply.
44
+ - **`ALLOW` is not permission to act; the verified receipt is.** The outcome field is transmitted
45
+ data. Nothing is gated until `verifyReceipt` has returned without throwing, and a handler that
46
+ calls it without branching on it is not gated at all.
47
+ - **It does not fetch your trust anchor.** `TrustAnchor` is an argument. Obtain the
48
+ `acp-bundle pubkey` document out of band; an anchor the API served you proves nothing about the
49
+ API.
50
+ - **It does not choose your signature-suite floor.** There is no default, deliberately.
51
+ - **It has no `approve` and no sandbox shortcut that mints a receipt.** Approval happens inside
52
+ the deployment, under keys your process never holds.
53
+ - **A pass from `verifyReceipt` is the stateless half of §9.3 only.** `@ziffer-io/verify`'s README
54
+ lists the six steps that need a signed bundle, a ledger and a context store, and are absent
55
+ rather than approximated.
56
+
57
+ ## Development
58
+
59
+ ```sh
60
+ pnpm --filter @ziffer-io/client test # tsc -b && node --test
61
+ ```
62
+
63
+ ## Licence
64
+
65
+ Proprietary. Copyright (c) 2026 code75 SASU, Paris, France. ZIFFER is a registered trademark of code75
66
+ SASU. This package is **not open source**: it is licensed for use with the ZIFFER service under
67
+ your agreement with code75, on the terms in `LICENSE` beside this file. The third-party
68
+ open-source components it redistributes are listed in `THIRD-PARTY-NOTICES` and are governed by
69
+ their own licences.
@@ -0,0 +1,52 @@
1
+ THIRD-PARTY NOTICES for @ziffer-io/client
2
+
3
+ GENERATED FILE -- do not edit by hand.
4
+ Regenerate with: node tools/third-party-notices.mjs
5
+ tools/release-npm.sh refuses to publish when this file is out of date.
6
+
7
+ The LICENSE beside this file governs the ZIFFER SDK itself, which is
8
+ proprietary software of code75 SASU. The components below are third-party
9
+ open-source, are redistributed under their own licences, and those licences
10
+ govern them and prevail for those components only (SDK licence, section 5).
11
+ Each entry is read from the component as it is installed, at the version
12
+ this release resolves.
13
+
14
+ 6 component(s):
15
+
16
+ ----------------------------------------------------------------------------
17
+ @noble/ciphers 2.3.0
18
+ licence: MIT
19
+ Copyright (c) 2022 Paul Miller (https://paulmillr.com)
20
+ full text: LICENSE, as distributed in the package
21
+
22
+ ----------------------------------------------------------------------------
23
+ @noble/curves 2.3.0
24
+ licence: MIT
25
+ Copyright (c) 2022 Paul Miller (https://paulmillr.com)
26
+ full text: LICENSE, as distributed in the package
27
+
28
+ ----------------------------------------------------------------------------
29
+ @noble/hashes 2.3.0
30
+ licence: MIT
31
+ Copyright (c) 2022 Paul Miller (https://paulmillr.com)
32
+ full text: LICENSE, as distributed in the package
33
+
34
+ ----------------------------------------------------------------------------
35
+ @noble/post-quantum 0.7.0
36
+ licence: MIT
37
+ Copyright (c) 2024 Paul Miller (https://paulmillr.com)
38
+ full text: LICENSE, as distributed in the package
39
+
40
+ ----------------------------------------------------------------------------
41
+ @ziffer-io/types link:../types
42
+ licence: SEE LICENSE IN LICENSE
43
+ Copyright (c) 2026 code75 SASU, Paris, France. All rights reserved.
44
+ full text: LICENSE, as distributed in the package
45
+
46
+ ----------------------------------------------------------------------------
47
+ @ziffer-io/verify link:../acp-verify
48
+ licence: SEE LICENSE IN LICENSE
49
+ Copyright (c) 2026 code75 SASU, Paris, France. All rights reserved.
50
+ full text: LICENSE, as distributed in the package
51
+
52
+ ----------------------------------------------------------------------------
@@ -0,0 +1,197 @@
1
+ /**
2
+ * The TypeScript client for the public decision API (ACP-197 §1) — the
3
+ * sibling of the Python SDK's `Client`, speaking the same two routes:
4
+ *
5
+ * POST /v1/proposals submit one wire Proposal
6
+ * GET /v1/decisions/{id} poll a decision; the ONE place receipts are served
7
+ *
8
+ * One rule, carried over from the scaffold this file replaced: this client
9
+ * may never compute or assert a security value on the control plane's
10
+ * behalf. It carries a proposal; it does not carry a risk grade, a
11
+ * reversibility claim, or an authorisation decision. Every one of those is
12
+ * recomputed by the Executor from the signed bundle (RES-8), because a
13
+ * compromised client writes the whole message. The same rule points the
14
+ * other way too: the client sends the caller's proposal EXACTLY as given —
15
+ * no defaulting, no rewriting, no "helpful" normalisation — because the
16
+ * proposal is signed material downstream and a client that edits it has
17
+ * become its author (the §1 gateway refuses a tenant rewrite for the same
18
+ * reason).
19
+ *
20
+ * # R / B / T for what this module handles
21
+ *
22
+ * - `decision_id` is a LOCATOR, classified T (§1). Nothing about it is
23
+ * evidence. The binding claim — "this receipt is about my proposal" — is
24
+ * verified by `verifyReceipt` recomputing `proposal_hash` from the
25
+ * caller's own bytes; the id only tells the client which row to fetch.
26
+ * - `receipt` is served verbatim by the gateway (parse-free passthrough).
27
+ * This client necessarily parses the enclosing JSON response, so what it
28
+ * hands back is the parsed VALUE, unmodified — and `verifyReceipt`
29
+ * canonicalises the body itself, so a parsed value loses nothing the
30
+ * verifier needs. The client never inspects, normalises or re-encodes it.
31
+ * - Verification has ONE home: `@ziffer-io/verify`, re-exported from this
32
+ * package's index. Zero verification logic lives here (ACP-197 §6).
33
+ *
34
+ * # Failure surface — named, never silent
35
+ *
36
+ * - A gateway refusal (`{"error": name}` with a non-2xx status) throws
37
+ * {@link ApiRefusal} carrying the name VERBATIM. The set of names is the
38
+ * gateway's and is open; this client closes nothing, because a client
39
+ * that filtered names would turn a new server refusal into a silent one.
40
+ * - An answer that is not §1's shape throws {@link ResponseMalformed}. Fail
41
+ * closed: a client guessing at a malformed answer is a client inventing a
42
+ * decision status.
43
+ * - A poll that outlives its deadline throws {@link WaitTimeout}. A timeout
44
+ * is "no answer yet", never "the answer was no" (ingress-low's rule for
45
+ * its own dependency, one layer out).
46
+ */
47
+ import type { wire } from '@ziffer-io/types';
48
+ /**
49
+ * The header every SUCCESSFUL answer carries (ACP-256 §5): the instant the key
50
+ * that authenticated the call ends, RFC 3339 in UTC. Lowercase because that is
51
+ * how `Headers.get` and Node's server spell names; the gateway writes it as
52
+ * `X-Ziffer-Api-Key-Expires` and header names are case-insensitive.
53
+ *
54
+ * Never on a refusal. An expired key is refused exactly as an unknown one, so
55
+ * the only moment a caller can learn its key is about to die is while it still
56
+ * works -- which is why this client reads the header off 2xx answers only and
57
+ * would be looking for an oracle if it read it off anything else.
58
+ */
59
+ export declare const API_KEY_EXPIRES_HEADER = "x-ziffer-api-key-expires";
60
+ /** How close to its end a key has to be before the warning fires. */
61
+ export declare const API_KEY_EXPIRY_WARNING_DAYS = 14;
62
+ /** Test seam only -- not re-exported from the package index. */
63
+ export declare function _resetApiKeyExpiryWarning(): void;
64
+ /**
65
+ * The §1 POST body: one wire Proposal, snake_case on the wire as
66
+ * `spec/schemas/wire/proposal.schema.json` spells it.
67
+ *
68
+ * Field types are INDEXED from the generated `wire.Proposal` rather than
69
+ * restated — `services/approval/src/door.ts`'s `WireRenderedSummary` idiom.
70
+ * The generated model is camelCase; the wire keys are the schema's. Indexing
71
+ * keeps one definition of each field's domain, so this interface cannot
72
+ * drift from the schemas without `tsc` catching it (a retyped field here
73
+ * would be a second definition of a schema object — the encoding-split
74
+ * defect at the source level).
75
+ */
76
+ export interface WireProposal {
77
+ readonly schema_id: wire.Proposal['schemaId'];
78
+ readonly schema_version: wire.Proposal['schemaVersion'];
79
+ readonly schema_hash: wire.Proposal['schemaHash'];
80
+ readonly fidelity: wire.Proposal['fidelity'];
81
+ readonly tenant_id: wire.Proposal['tenantId'];
82
+ readonly payload: WireProposalPayload;
83
+ }
84
+ /** PR-1's closed payload, snake_case as the schema spells it — same indexing
85
+ * rule as {@link WireProposal}. */
86
+ export interface WireProposalPayload {
87
+ readonly task_type: wire.ProposalPayload['taskType'];
88
+ readonly operator: wire.ProposalPayload['operator'];
89
+ readonly targets: wire.ProposalPayload['targets'];
90
+ readonly params: wire.ProposalPayload['params'];
91
+ readonly cidrs: wire.ProposalPayload['cidrs'];
92
+ }
93
+ /** The two §1 states. `decided` means the relay recorded a verdict; it does
94
+ * NOT mean a receipt exists — `receipt` is present iff one does. */
95
+ export type DecisionStatus = 'pending' | 'decided';
96
+ /**
97
+ * What both §1 routes answer. Refusals expose exactly `{outcome, clause}` —
98
+ * the fields ingress-low's `relayed_response` exposes — and an absent
99
+ * `clause` is ABSENT, never `null`: one object, one encoding.
100
+ */
101
+ export interface Decision {
102
+ /** The locator (T). Compare nothing against it; fetch with it. */
103
+ readonly decision_id: string;
104
+ readonly status: DecisionStatus;
105
+ /** The engine's outcome type — a fourth spelling of ALLOW / ATTEST / DENY
106
+ * would be a fourth definition of the object every component agrees on. */
107
+ readonly outcome?: wire.DecisionOutcome;
108
+ readonly clause?: string;
109
+ /**
110
+ * The signed receipt, verbatim from the one route that serves receipts
111
+ * (GET). Deliberately `unknown`: its ONLY consumer is `verifyReceipt`,
112
+ * which takes `unknown` and refuses by name — typing it here would invite
113
+ * reading fields out of an unverified receipt.
114
+ */
115
+ readonly receipt?: unknown;
116
+ }
117
+ /** §1 names, exported so callers and tests never retype the strings. The set
118
+ * is OPEN — the gateway may name more; {@link ApiRefusal} carries any name
119
+ * verbatim and this list closes nothing. */
120
+ export declare const ERROR_API_KEY_UNKNOWN = "ApiKeyUnknown";
121
+ export declare const ERROR_TENANT_MISMATCH = "TenantMismatch";
122
+ export declare const ERROR_PROPOSAL_MALFORMED = "ProposalMalformed";
123
+ export declare const ERROR_DECISION_UNKNOWN = "DecisionUnknown";
124
+ export declare const ERROR_ADMISSION_UNAVAILABLE = "AdmissionUnavailable";
125
+ /**
126
+ * The gateway answered, and the answer was a named refusal. `error` is the
127
+ * gateway's name, verbatim — the machine-readable half, as `Refusal.clause`
128
+ * is for verification. `status` is carried for the operator; the NAME is the
129
+ * contract.
130
+ */
131
+ export declare class ApiRefusal extends Error {
132
+ /** The gateway's refusal name, e.g. `ApiKeyUnknown`. Verbatim. */
133
+ readonly error: string;
134
+ /** The HTTP status the name arrived under. */
135
+ readonly status: number;
136
+ constructor(status: number, error: string);
137
+ }
138
+ /**
139
+ * The answer was not §1's shape — not JSON, missing or mistyped fields, a
140
+ * receipt where §1 says none can be, an id that is not the one asked for.
141
+ * Thrown instead of guessed at: an SDK that repairs a malformed answer is
142
+ * an SDK that invents decision state.
143
+ */
144
+ export declare class ResponseMalformed extends Error {
145
+ constructor(message: string);
146
+ }
147
+ /** The deadline passed with the decision still `pending`. Not a refusal and
148
+ * not an answer — the decision may still decide; the id remains fetchable. */
149
+ export declare class WaitTimeout extends Error {
150
+ readonly decisionId: string;
151
+ constructor(decisionId: string, timeoutMs: number);
152
+ }
153
+ /** Options for {@link ZifferClient.wait}. */
154
+ export interface WaitOptions {
155
+ /** Give up (throw {@link WaitTimeout}) after this long. Default 30 000. */
156
+ readonly timeoutMs?: number;
157
+ /** Delay between polls. Default 500. */
158
+ readonly intervalMs?: number;
159
+ }
160
+ /**
161
+ * The client. One instance per (gateway, key); the KEY determines the tenant
162
+ * server-side (§1), so there is nothing tenant-shaped to configure here —
163
+ * a client-side tenant setting would be a value the server must ignore.
164
+ */
165
+ export declare class ZifferClient {
166
+ private readonly baseUrl;
167
+ private readonly apiKey;
168
+ constructor(baseUrl: string, apiKey: string);
169
+ /**
170
+ * POST /v1/proposals. The proposal is serialised as given — the caller's
171
+ * values, no edits — and the answer never carries a receipt (§1): fetch it
172
+ * with {@link decision} once decided.
173
+ */
174
+ propose(proposal: WireProposal): Promise<Decision>;
175
+ /**
176
+ * GET /v1/decisions/{id}. `receipt` is present iff a signed receipt
177
+ * exists; hand it to `verifyReceipt` with your OWN copy of the proposal
178
+ * bytes — the id proves nothing (T), the recomputed hash is the binding.
179
+ */
180
+ decision(id: string): Promise<Decision>;
181
+ /**
182
+ * Poll {@link decision} until `decided` or the deadline. A `WaitTimeout`
183
+ * is "no answer yet", never a verdict; every named refusal (404 included)
184
+ * propagates immediately — retrying `DecisionUnknown` would be the client
185
+ * deciding the server was wrong.
186
+ */
187
+ wait(id: string, opts?: WaitOptions): Promise<Decision>;
188
+ /**
189
+ * One request, one parse, one narrowing. 2xx returns the parsed body for
190
+ * the caller's guard; anything else must be §1's `{"error": name}` and
191
+ * throws {@link ApiRefusal} with the name verbatim. A non-JSON or unnamed
192
+ * error body throws {@link ResponseMalformed} — an intermediary's HTML 502
193
+ * is not the gateway's answer and is never dressed up as one.
194
+ */
195
+ private request;
196
+ }
197
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
package/dist/client.js ADDED
@@ -0,0 +1,323 @@
1
+ /**
2
+ * The TypeScript client for the public decision API (ACP-197 §1) — the
3
+ * sibling of the Python SDK's `Client`, speaking the same two routes:
4
+ *
5
+ * POST /v1/proposals submit one wire Proposal
6
+ * GET /v1/decisions/{id} poll a decision; the ONE place receipts are served
7
+ *
8
+ * One rule, carried over from the scaffold this file replaced: this client
9
+ * may never compute or assert a security value on the control plane's
10
+ * behalf. It carries a proposal; it does not carry a risk grade, a
11
+ * reversibility claim, or an authorisation decision. Every one of those is
12
+ * recomputed by the Executor from the signed bundle (RES-8), because a
13
+ * compromised client writes the whole message. The same rule points the
14
+ * other way too: the client sends the caller's proposal EXACTLY as given —
15
+ * no defaulting, no rewriting, no "helpful" normalisation — because the
16
+ * proposal is signed material downstream and a client that edits it has
17
+ * become its author (the §1 gateway refuses a tenant rewrite for the same
18
+ * reason).
19
+ *
20
+ * # R / B / T for what this module handles
21
+ *
22
+ * - `decision_id` is a LOCATOR, classified T (§1). Nothing about it is
23
+ * evidence. The binding claim — "this receipt is about my proposal" — is
24
+ * verified by `verifyReceipt` recomputing `proposal_hash` from the
25
+ * caller's own bytes; the id only tells the client which row to fetch.
26
+ * - `receipt` is served verbatim by the gateway (parse-free passthrough).
27
+ * This client necessarily parses the enclosing JSON response, so what it
28
+ * hands back is the parsed VALUE, unmodified — and `verifyReceipt`
29
+ * canonicalises the body itself, so a parsed value loses nothing the
30
+ * verifier needs. The client never inspects, normalises or re-encodes it.
31
+ * - Verification has ONE home: `@ziffer-io/verify`, re-exported from this
32
+ * package's index. Zero verification logic lives here (ACP-197 §6).
33
+ *
34
+ * # Failure surface — named, never silent
35
+ *
36
+ * - A gateway refusal (`{"error": name}` with a non-2xx status) throws
37
+ * {@link ApiRefusal} carrying the name VERBATIM. The set of names is the
38
+ * gateway's and is open; this client closes nothing, because a client
39
+ * that filtered names would turn a new server refusal into a silent one.
40
+ * - An answer that is not §1's shape throws {@link ResponseMalformed}. Fail
41
+ * closed: a client guessing at a malformed answer is a client inventing a
42
+ * decision status.
43
+ * - A poll that outlives its deadline throws {@link WaitTimeout}. A timeout
44
+ * is "no answer yet", never "the answer was no" (ingress-low's rule for
45
+ * its own dependency, one layer out).
46
+ */
47
+ // ---------------------------------------------------------------- key expiry
48
+ /**
49
+ * The header every SUCCESSFUL answer carries (ACP-256 §5): the instant the key
50
+ * that authenticated the call ends, RFC 3339 in UTC. Lowercase because that is
51
+ * how `Headers.get` and Node's server spell names; the gateway writes it as
52
+ * `X-Ziffer-Api-Key-Expires` and header names are case-insensitive.
53
+ *
54
+ * Never on a refusal. An expired key is refused exactly as an unknown one, so
55
+ * the only moment a caller can learn its key is about to die is while it still
56
+ * works -- which is why this client reads the header off 2xx answers only and
57
+ * would be looking for an oracle if it read it off anything else.
58
+ */
59
+ export const API_KEY_EXPIRES_HEADER = 'x-ziffer-api-key-expires';
60
+ /** How close to its end a key has to be before the warning fires. */
61
+ export const API_KEY_EXPIRY_WARNING_DAYS = 14;
62
+ /**
63
+ * ONCE per process: a line per call is noise the operator filters out, and the
64
+ * one that mattered goes with it. `console.warn` and not a logger of this
65
+ * package's own, because that is the channel a deployment already routes.
66
+ */
67
+ let expiryWarned = false;
68
+ /** Test seam only -- not re-exported from the package index. */
69
+ export function _resetApiKeyExpiryWarning() {
70
+ expiryWarned = false;
71
+ }
72
+ const RFC3339_UTC = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/;
73
+ /**
74
+ * `null` is an answer with no such header -- a gateway from before it existed
75
+ * -- and says nothing. A header that is present but not the store's rendering
76
+ * is reported ONCE by name rather than ignored: silence there is exactly how a
77
+ * customer would stop being warned without anyone noticing the drift.
78
+ */
79
+ function noteKeyExpiry(value) {
80
+ if (value === null || expiryWarned) {
81
+ return;
82
+ }
83
+ const at = RFC3339_UTC.test(value) ? Date.parse(value) : Number.NaN;
84
+ if (Number.isNaN(at)) {
85
+ expiryWarned = true;
86
+ console.warn(`the gateway's ${API_KEY_EXPIRES_HEADER} header is not RFC 3339 UTC (${JSON.stringify(value)}); ` +
87
+ 'this client cannot tell when the API key expires');
88
+ return;
89
+ }
90
+ const msLeft = at - Date.now();
91
+ if (msLeft < API_KEY_EXPIRY_WARNING_DAYS * 86_400_000) {
92
+ expiryWarned = true;
93
+ // Whole days ROUNDED UP: 2 days 23 hours is "3 day(s) left" to the person
94
+ // reading it.
95
+ const days = Math.max(Math.ceil(msLeft / 86_400_000), 0);
96
+ console.warn(`the ZIFFER API key expires on ${value} (${days} day(s) left). An expired key is ` +
97
+ 'refused exactly like an unknown one, so rotate BEFORE then: ask for a successor ' +
98
+ "(tools/mint-api-key.py --rotate <this key's key_hash>), deploy it, then have this one revoked");
99
+ }
100
+ }
101
+ // ------------------------------------------------------------ error surface
102
+ /** §1 names, exported so callers and tests never retype the strings. The set
103
+ * is OPEN — the gateway may name more; {@link ApiRefusal} carries any name
104
+ * verbatim and this list closes nothing. */
105
+ export const ERROR_API_KEY_UNKNOWN = 'ApiKeyUnknown';
106
+ export const ERROR_TENANT_MISMATCH = 'TenantMismatch';
107
+ export const ERROR_PROPOSAL_MALFORMED = 'ProposalMalformed';
108
+ export const ERROR_DECISION_UNKNOWN = 'DecisionUnknown';
109
+ export const ERROR_ADMISSION_UNAVAILABLE = 'AdmissionUnavailable';
110
+ /**
111
+ * The gateway answered, and the answer was a named refusal. `error` is the
112
+ * gateway's name, verbatim — the machine-readable half, as `Refusal.clause`
113
+ * is for verification. `status` is carried for the operator; the NAME is the
114
+ * contract.
115
+ */
116
+ export class ApiRefusal extends Error {
117
+ /** The gateway's refusal name, e.g. `ApiKeyUnknown`. Verbatim. */
118
+ error;
119
+ /** The HTTP status the name arrived under. */
120
+ status;
121
+ constructor(status, error) {
122
+ super(`${error} (HTTP ${status})`);
123
+ this.name = 'ApiRefusal';
124
+ this.error = error;
125
+ this.status = status;
126
+ }
127
+ }
128
+ /**
129
+ * The answer was not §1's shape — not JSON, missing or mistyped fields, a
130
+ * receipt where §1 says none can be, an id that is not the one asked for.
131
+ * Thrown instead of guessed at: an SDK that repairs a malformed answer is
132
+ * an SDK that invents decision state.
133
+ */
134
+ export class ResponseMalformed extends Error {
135
+ constructor(message) {
136
+ super(message);
137
+ this.name = 'ResponseMalformed';
138
+ }
139
+ }
140
+ /** The deadline passed with the decision still `pending`. Not a refusal and
141
+ * not an answer — the decision may still decide; the id remains fetchable. */
142
+ export class WaitTimeout extends Error {
143
+ decisionId;
144
+ constructor(decisionId, timeoutMs) {
145
+ super(`decision ${decisionId} still pending after ${timeoutMs}ms`);
146
+ this.name = 'WaitTimeout';
147
+ this.decisionId = decisionId;
148
+ }
149
+ }
150
+ // ------------------------------------------------------------------- guards
151
+ /** The engine's three outcomes, typed from the generated union so a drift in
152
+ * the schema surfaces here as a compile error, not a runtime disagreement. */
153
+ const OUTCOMES = ['ALLOW', 'ATTEST', 'DENY'];
154
+ function isOutcome(v) {
155
+ return typeof v === 'string' && OUTCOMES.some((o) => o === v);
156
+ }
157
+ function isStatus(v) {
158
+ return v === 'pending' || v === 'decided';
159
+ }
160
+ function isRecord(v) {
161
+ return typeof v === 'object' && v !== null && !Array.isArray(v);
162
+ }
163
+ /**
164
+ * Narrow one §1 response body to a {@link Decision}, refusing by name on any
165
+ * departure. `receiptAllowed` is false on the POST path: §1 says the receipt
166
+ * is NEVER in the POST response — one place serves receipts — and a client
167
+ * that tolerated one there would quietly stand up a second serving place.
168
+ */
169
+ function decisionFromBody(body, receiptAllowed) {
170
+ if (!isRecord(body)) {
171
+ throw new ResponseMalformed('decision response is not a JSON object');
172
+ }
173
+ const id = body['decision_id'];
174
+ if (typeof id !== 'string' || id.length === 0) {
175
+ throw new ResponseMalformed('decision response carries no decision_id');
176
+ }
177
+ const status = body['status'];
178
+ if (!isStatus(status)) {
179
+ throw new ResponseMalformed('decision response status is not "pending" or "decided"');
180
+ }
181
+ let decision = { decision_id: id, status };
182
+ if ('outcome' in body) {
183
+ const outcome = body['outcome'];
184
+ if (!isOutcome(outcome)) {
185
+ // An answer this client cannot read as one of the three is not a
186
+ // verdict and never becomes one (ingress-low's rule for the same
187
+ // value, one hop earlier).
188
+ throw new ResponseMalformed('decision outcome is not ALLOW, ATTEST or DENY');
189
+ }
190
+ decision = { ...decision, outcome };
191
+ }
192
+ if ('clause' in body) {
193
+ const clause = body['clause'];
194
+ if (typeof clause !== 'string') {
195
+ // Includes null: an absent clause is spelled ABSENT (§1's parity with
196
+ // ingress-low) — accepting null here would admit the second encoding.
197
+ throw new ResponseMalformed('decision clause is not a string');
198
+ }
199
+ decision = { ...decision, clause };
200
+ }
201
+ if ('receipt' in body) {
202
+ if (!receiptAllowed) {
203
+ throw new ResponseMalformed('receipt in a POST response: one place serves receipts (GET)');
204
+ }
205
+ decision = { ...decision, receipt: body['receipt'] };
206
+ }
207
+ return decision;
208
+ }
209
+ /**
210
+ * The client. One instance per (gateway, key); the KEY determines the tenant
211
+ * server-side (§1), so there is nothing tenant-shaped to configure here —
212
+ * a client-side tenant setting would be a value the server must ignore.
213
+ */
214
+ export class ZifferClient {
215
+ baseUrl;
216
+ apiKey;
217
+ constructor(baseUrl, apiKey) {
218
+ if (baseUrl.length === 0) {
219
+ throw new TypeError('ZifferClient: baseUrl is empty');
220
+ }
221
+ if (apiKey.length === 0) {
222
+ // Refused here, not sent: an empty bearer token is a caller bug, and
223
+ // mailing it to the server converts a local defect into a remote 401
224
+ // whose log line points at the wrong component.
225
+ throw new TypeError('ZifferClient: apiKey is empty');
226
+ }
227
+ this.baseUrl = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
228
+ this.apiKey = apiKey;
229
+ }
230
+ /**
231
+ * POST /v1/proposals. The proposal is serialised as given — the caller's
232
+ * values, no edits — and the answer never carries a receipt (§1): fetch it
233
+ * with {@link decision} once decided.
234
+ */
235
+ async propose(proposal) {
236
+ const body = await this.request('POST', '/v1/proposals', JSON.stringify(proposal));
237
+ return decisionFromBody(body, false);
238
+ }
239
+ /**
240
+ * GET /v1/decisions/{id}. `receipt` is present iff a signed receipt
241
+ * exists; hand it to `verifyReceipt` with your OWN copy of the proposal
242
+ * bytes — the id proves nothing (T), the recomputed hash is the binding.
243
+ */
244
+ async decision(id) {
245
+ if (id.length === 0) {
246
+ throw new TypeError('ZifferClient.decision: id is empty');
247
+ }
248
+ const body = await this.request('GET', `/v1/decisions/${encodeURIComponent(id)}`, null);
249
+ const d = decisionFromBody(body, true);
250
+ if (d.decision_id !== id) {
251
+ // The id is a locator, but an answer ABOUT A DIFFERENT LOCATOR is not
252
+ // an answer to this question — surfacing it as one would let a
253
+ // confused (or malicious) proxy substitute decisions silently.
254
+ throw new ResponseMalformed(`decision response is about ${d.decision_id}, not ${id}`);
255
+ }
256
+ return d;
257
+ }
258
+ /**
259
+ * Poll {@link decision} until `decided` or the deadline. A `WaitTimeout`
260
+ * is "no answer yet", never a verdict; every named refusal (404 included)
261
+ * propagates immediately — retrying `DecisionUnknown` would be the client
262
+ * deciding the server was wrong.
263
+ */
264
+ async wait(id, opts) {
265
+ const timeoutMs = opts?.timeoutMs ?? 30_000;
266
+ const intervalMs = opts?.intervalMs ?? 500;
267
+ if (timeoutMs <= 0 || intervalMs <= 0) {
268
+ throw new TypeError('ZifferClient.wait: timeoutMs and intervalMs must be positive');
269
+ }
270
+ const deadline = Date.now() + timeoutMs;
271
+ for (;;) {
272
+ const d = await this.decision(id);
273
+ if (d.status === 'decided') {
274
+ return d;
275
+ }
276
+ if (Date.now() + intervalMs > deadline) {
277
+ throw new WaitTimeout(id, timeoutMs);
278
+ }
279
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
280
+ }
281
+ }
282
+ /**
283
+ * One request, one parse, one narrowing. 2xx returns the parsed body for
284
+ * the caller's guard; anything else must be §1's `{"error": name}` and
285
+ * throws {@link ApiRefusal} with the name verbatim. A non-JSON or unnamed
286
+ * error body throws {@link ResponseMalformed} — an intermediary's HTML 502
287
+ * is not the gateway's answer and is never dressed up as one.
288
+ */
289
+ async request(method, path, body) {
290
+ const headers = {
291
+ authorization: `Bearer ${this.apiKey}`,
292
+ };
293
+ if (body !== null) {
294
+ headers['content-type'] = 'application/json';
295
+ }
296
+ const res = await fetch(`${this.baseUrl}${path}`, {
297
+ method,
298
+ headers,
299
+ ...(body !== null ? { body } : {}),
300
+ });
301
+ const text = await res.text();
302
+ let parsed;
303
+ try {
304
+ parsed = JSON.parse(text);
305
+ }
306
+ catch {
307
+ throw new ResponseMalformed(`HTTP ${res.status} with a non-JSON body from ${method} ${path}`);
308
+ }
309
+ if (res.ok) {
310
+ // Only here, on a 2xx (see API_KEY_EXPIRES_HEADER).
311
+ noteKeyExpiry(res.headers.get(API_KEY_EXPIRES_HEADER));
312
+ return parsed;
313
+ }
314
+ if (isRecord(parsed)) {
315
+ const name = parsed['error'];
316
+ if (typeof name === 'string' && name.length > 0) {
317
+ throw new ApiRefusal(res.status, name);
318
+ }
319
+ }
320
+ throw new ResponseMalformed(`HTTP ${res.status} from ${method} ${path} names no error`);
321
+ }
322
+ }
323
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * @ziffer-io/client — the TypeScript SDK for the public decision API (ACP-197 §6).
3
+ *
4
+ * Two halves, one boundary:
5
+ *
6
+ * - `ZifferClient` (./client.js) carries proposals out and decisions back.
7
+ * It computes no security value: the proposal goes as the caller wrote it,
8
+ * the receipt comes back as the gateway stored it (RES-8 — a compromised
9
+ * client writes the whole message, so nothing a client derives is
10
+ * evidence).
11
+ * - `verifyReceipt` is RE-EXPORTED from `@ziffer-io/verify`, unchanged. The runbook
12
+ * rule this line implements: verification has ONE home, and the client
13
+ * adds none of its own — a second verifier behind a client-shaped API
14
+ * would be the two-definitions defect with a signature on it. The tests
15
+ * assert the re-export is the SAME function object, so this cannot decay
16
+ * into a copy silently.
17
+ */
18
+ export { API_KEY_EXPIRES_HEADER, API_KEY_EXPIRY_WARNING_DAYS, ApiRefusal, ERROR_ADMISSION_UNAVAILABLE, ERROR_API_KEY_UNKNOWN, ERROR_DECISION_UNKNOWN, ERROR_PROPOSAL_MALFORMED, ERROR_TENANT_MISMATCH, ResponseMalformed, WaitTimeout, ZifferClient, type Decision, type DecisionStatus, type WaitOptions, type WireProposal, type WireProposalPayload, } from './client.js';
19
+ export { Refusal, verifyReceipt, type TrustAnchor, type Verified, type VerifyOptions, } from '@ziffer-io/verify';
20
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EACL,sBAAsB,EACtB,2BAA2B,EAC3B,UAAU,EACV,2BAA2B,EAC3B,qBAAqB,EACrB,sBAAsB,EACtB,wBAAwB,EACxB,qBAAqB,EACrB,iBAAiB,EACjB,WAAW,EACX,YAAY,EACZ,KAAK,QAAQ,EACb,KAAK,cAAc,EACnB,KAAK,WAAW,EAChB,KAAK,YAAY,EACjB,KAAK,mBAAmB,GACzB,MAAM,aAAa,CAAC;AAOrB,OAAO,EACL,OAAO,EACP,aAAa,EACb,KAAK,WAAW,EAChB,KAAK,QAAQ,EACb,KAAK,aAAa,GACnB,MAAM,mBAAmB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,25 @@
1
+ /**
2
+ * @ziffer-io/client — the TypeScript SDK for the public decision API (ACP-197 §6).
3
+ *
4
+ * Two halves, one boundary:
5
+ *
6
+ * - `ZifferClient` (./client.js) carries proposals out and decisions back.
7
+ * It computes no security value: the proposal goes as the caller wrote it,
8
+ * the receipt comes back as the gateway stored it (RES-8 — a compromised
9
+ * client writes the whole message, so nothing a client derives is
10
+ * evidence).
11
+ * - `verifyReceipt` is RE-EXPORTED from `@ziffer-io/verify`, unchanged. The runbook
12
+ * rule this line implements: verification has ONE home, and the client
13
+ * adds none of its own — a second verifier behind a client-shaped API
14
+ * would be the two-definitions defect with a signature on it. The tests
15
+ * assert the re-export is the SAME function object, so this cannot decay
16
+ * into a copy silently.
17
+ */
18
+ export { API_KEY_EXPIRES_HEADER, API_KEY_EXPIRY_WARNING_DAYS, ApiRefusal, ERROR_ADMISSION_UNAVAILABLE, ERROR_API_KEY_UNKNOWN, ERROR_DECISION_UNKNOWN, ERROR_PROPOSAL_MALFORMED, ERROR_TENANT_MISMATCH, ResponseMalformed, WaitTimeout, ZifferClient, } from './client.js';
19
+ // The verify surface a receipt-holding caller needs, and ONLY that: the
20
+ // verifier itself, its refusal type, and the types its signature names.
21
+ // The rest of @ziffer-io/verify (canon, the strict Ed25519 predicate, suite
22
+ // algebra) stays importable from its own home — re-exporting internals here
23
+ // would hand this package an API surface it does not implement.
24
+ export { Refusal, verifyReceipt, } from '@ziffer-io/verify';
25
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EACL,sBAAsB,EACtB,2BAA2B,EAC3B,UAAU,EACV,2BAA2B,EAC3B,qBAAqB,EACrB,sBAAsB,EACtB,wBAAwB,EACxB,qBAAqB,EACrB,iBAAiB,EACjB,WAAW,EACX,YAAY,GAMb,MAAM,aAAa,CAAC;AAErB,wEAAwE;AACxE,wEAAwE;AACxE,4EAA4E;AAC5E,4EAA4E;AAC5E,gEAAgE;AAChE,OAAO,EACL,OAAO,EACP,aAAa,GAId,MAAM,mBAAmB,CAAC"}
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@ziffer-io/client",
3
+ "version": "0.1.0",
4
+ "description": "The ZIFFER decision API client for TypeScript, with receipt verification re-exported from one home.",
5
+ "author": "code75 SASU",
6
+ "license": "SEE LICENSE IN LICENSE",
7
+ "comment-license": "The SDK is proprietary (code75 SASU). \"license\" is the SPDX escape hatch for exactly this case: there is no SPDX identifier for these terms, so the field points at the file that states them, and LICENSE ships in the tarball beside THIRD-PARTY-NOTICES. Do not put an OSI identifier here -- Apache-2.0 stood in these four files until ACP-214 and was wrong the whole time.",
8
+ "type": "module",
9
+ "main": "./dist/index.js",
10
+ "types": "./dist/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "default": "./dist/index.js"
15
+ }
16
+ },
17
+ "sideEffects": false,
18
+ "files": [
19
+ "dist",
20
+ "!dist/**/*.test.*",
21
+ "THIRD-PARTY-NOTICES"
22
+ ],
23
+ "engines": {
24
+ "node": ">=22"
25
+ },
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "git+https://github.com/ziffer-hq/ziffer.git",
29
+ "directory": "packages/acp-client"
30
+ },
31
+ "homepage": "https://ziffer.io",
32
+ "publishConfig": {
33
+ "access": "public"
34
+ },
35
+ "comment-no-provenance": "There is deliberately no \"provenance\": true here, and it was removed rather than never added. npm generates a provenance attestation only from a recognised CI runner, and its documentation states plainly that provenance is NOT SUPPORTED for private repositories (docs.npmjs.com/trusted-publishers, read 2026-09-03) -- ziffer-hq/ziffer is private (`gh api` says so). Setting the flag does not degrade to a warning: npm attempts the attestation and the publish FAILS, so the field would have broken the operator's very first publish from a laptop and every CI publish after it. tools/release-npm.sh asserts the field stays absent, and that assertion is the thing to delete on the day this repository becomes public -- at which point trusted publishing generates provenance on its own, with no flag at all.",
36
+ "ziffer": {
37
+ "enginePin": "fed43d10b427e0a4435a8f04e474334d88a5aa6e"
38
+ },
39
+ "comment-enginePin": "The engine commit whose wire types this client speaks and whose verifier it re-exports. A COPY of the rev in Cargo.toml, which is the one authority tools/guard.sh reads; tools/release-npm.sh refuses to release when the two differ, by name. tools/bump-pin.sh does not move this field -- see packages/types/package.json for why and what closing it costs.",
40
+ "comment-deps": "Exactly two: @ziffer-io/types because the wire format is the one permitted shared definition (it IS the schemas, generated), and @ziffer-io/verify because verification has ONE home and this client re-exports it rather than growing a second (ACP-197 runbook section 6). No HTTP dependency: Node's own fetch carries two routes; a framework here is surface without a claim.",
41
+ "comment-types-was-a-git-dependency": "Until ACP-214 the first of those two was `git+https://ziffer-hq@github.com/ziffer-hq/agent-control-plane.git#<pin>&path:/packages/acp-types`. That is a private repository, so the published client would have been uninstallable rather than merely awkward; and pnpm runs `prepare` for a git dependency, so a consumer who could reach it would have compiled our wire types on their own machine. It is now a workspace dependency on packages/types, which `pnpm pack` rewrites to that package's exact published version -- measured, and asserted again by tools/release-npm.sh over every tarball.",
42
+ "dependencies": {
43
+ "@ziffer-io/verify": "0.1.0",
44
+ "@ziffer-io/types": "0.1.0"
45
+ },
46
+ "devDependencies": {
47
+ "@types/node": "^22.15.0",
48
+ "typescript": "^5.9.2"
49
+ },
50
+ "scripts": {
51
+ "build": "tsc -b",
52
+ "typecheck": "tsc -b",
53
+ "pretest": "tsc -b && if ! find dist -name '*.test.js' -print -quit | grep -q .; then echo 'ACP-248: no compiled test file under dist/ -- node --test reports 0 tests and exits 0, so this package would go green having run nothing' >&2; exit 1; fi",
54
+ "test": "tsc -b && node --test \"dist/**/*.test.js\""
55
+ }
56
+ }