@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 +120 -0
- package/README.md +69 -0
- package/THIRD-PARTY-NOTICES +52 -0
- package/dist/client.d.ts +197 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +323 -0
- package/dist/client.js.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -0
- package/package.json +56 -0
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
|
+
----------------------------------------------------------------------------
|
package/dist/client.d.ts
ADDED
|
@@ -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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"}
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|