@percayso-community/cayso-contracts 0.30.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -26,20 +26,32 @@ Cloud verifies Cayso's authorisation acknowledgement under a key it reads from
26
26
  `ACKNOWLEDGEMENT_KEYS_ROUTE`, an unauthenticated read of public keys, published
27
27
  apart from both.
28
28
 
29
- **The operator read surface, on its own entry point** (CAY-0026, Amendment 4). Three
30
- reads an operator app's *server* calls — the movement list, the purchase list and the
31
- quickening ladder — are published at `@percayso-community/cayso-contracts/operator`:
29
+ **Percayso BizID's authority assertion** (CAY-0100) is published ahead of the
30
+ binding that accepts it. `AuthorityClaims` is its own type, under the identity
31
+ assertion's `AssertionHeader`, with no verification or assurance fields. It travels
32
+ in the `AUTHORITY_ASSERTION_HEADER` header, beside `Authorization`, and the thin
33
+ client sends it only on an Organisation operation given a per-request provider for
34
+ it. The binding's refusals are the closed `AuthorityBindingRefusal`.
35
+ `AssertionClaims` declares `auth_time`, which Percayso ID mints and the binding
36
+ compares. `organisationAuthority` and `mandateFor` are retired and still published,
37
+ until the breaking step that removes them.
38
+
39
+ **The operator read surface, on its own entry point** (CAY-0026, Amendments 4 and 5).
40
+ Four reads an operator app's *server* calls — the movement list, the purchase list, the
41
+ quickening ladder and the economy summary — are published at `@percayso-community/cayso-contracts/operator`:
32
42
  their paths and scopes, query inputs (wallet id, a half-open `[from, to)` window, a
33
43
  keyset cursor), rows and pages, and their closed refusal and not-configured bodies.
34
44
  Money is `Cents`, a wallet is named by its wallet id, and no Percayso identity
35
45
  appears on any row. It is not re-exported from the barrel, so Piper and the Cloud
36
46
  never import an operator vocabulary by accident, and the schema describes it under
37
47
  its own `operator` key rather than among the person-facing types. Cayso sends no
38
- CORS headers: these reads are for a server, never a browser. Widening the entry
39
- point beyond these three needs a further amendment.
48
+ CORS headers: these reads are for a server, never a browser. The economy summary
49
+ (Amendment 5) answers aggregates only: whole-number counts and cent totals, with no
50
+ wallet id, identity or row of any kind. Widening the entry point beyond these four
51
+ needs a further amendment.
40
52
 
41
53
  **Deliberately not published:** peer types, holdings and carves, and every operator
42
- route but those three reads. They are not a consumer's business, and putting them on
54
+ route but those four reads. They are not a consumer's business, and putting them on
43
55
  the public surface widens it for nothing.
44
56
 
45
57
  **This package carries no numbers.** Amounts, rates, tiers, schedules and
@@ -53,7 +65,7 @@ restated here.
53
65
  | `@percayso-community/cayso-contracts` | Types and constants. No runtime. |
54
66
  | `@percayso-community/cayso-contracts/client` | The thin client — fetch, assertion header, idempotent retry. |
55
67
  | `@percayso-community/cayso-contracts/schema` | A machine-readable descriptor of the published surface: the published, versioned surface a consumer in another language generates its types from. |
56
- | `@percayso-community/cayso-contracts/operator` | The three operator reads' types and route constants, for an operator app's server. |
68
+ | `@percayso-community/cayso-contracts/operator` | The four operator reads' types and route constants, for an operator app's server. |
57
69
 
58
70
  The split is the point: a consumer wanting only the types takes on no runtime at
59
71
  all. The client owns the fetch, the assertion header and the idempotent retry.
@@ -84,8 +96,10 @@ see the version endpoint, below.
84
96
  ## Independent versioning
85
97
 
86
98
  `CONTRACT_VERSION` is exported from the barrel and is **independent** — its own
87
- semantic version, starting `0.1.0`, not a repo-wide constant shared with anything
88
- else. Piper's own contracts share one global version bumped across all of them
99
+ semantic version, not a repo-wide constant shared with anything else. It started at
100
+ `0.1.0`, and from `1.0.0` it follows ordinary semantic versioning, Cayso's adoption of CMTY-0032
101
+ ([CAY-0101](../../docs/architectural-decision-record/CAY-0101-the-contracts-follow-ordinary-semantic-versioning-from-1-0-0.md)):
102
+ a minor is additive and drops nothing, and a major is the only breaking kind. Piper's own contracts share one global version bumped across all of them
89
103
  together; independent versioning across a service boundary is exactly what this
90
104
  money surface gains that theirs lack (CAY-0026). The constant and the package's
91
105
  `version` field must agree, and a test fails when they drift.
@@ -103,10 +117,16 @@ describes. The thin client's `getVersion()` reads it and warns once, per client
103
117
  instance, when the server's contract *major* differs from the one the client was
104
118
  built against.
105
119
 
106
- After a minor bump that is additive on the wire, a deployment keeps the previous
107
- minor in `supportedContractVersions` for one release, so a consumer still pinned
108
- to it keeps working while it moves. A breaking bump drops it, and says so in the
109
- bump's changelog.
120
+ A deployment keeps serving every earlier minor of its major and lists them in
121
+ `supportedContractVersions`, so a consumer pinned to any of them keeps working; a
122
+ consumer's handshake accepts a Cayso if `contractVersion` or any entry of
123
+ `supportedContractVersions` is on its own line: from `1.0.0` the same major, below it the
124
+ same minor. Only a
125
+ major breaks, and its first release lists nothing from an older major and says so in
126
+ the bump's changelog. An earlier major is listed only during a declared transition:
127
+ `1.0.0` keeps `0.32.0`, `0.31.0`, `0.30.0` and `0.29.0`, and the next major drops them
128
+ ([CAY-0101](../../docs/architectural-decision-record/CAY-0101-the-contracts-follow-ordinary-semantic-versioning-from-1-0-0.md),
129
+ amending CAY-0086).
110
130
 
111
131
  Alongside it, `GET /api/version/schema` serves the same machine-readable
112
132
  descriptor as `@percayso-community/cayso-contracts/schema` — the surface outside consumers read, with
@@ -756,6 +756,15 @@ export interface AssertionClaims {
756
756
  * THIS operation — the organisation-authority claim (CAY-0059 §3). OPTIONAL on
757
757
  * the wire, and its absence provisions nothing.
758
758
  *
759
+ * ## RETIRED by CAY-0100, and kept until #419
760
+ *
761
+ * BizID holds the Organisation, so the evidence for an act on an Organisation
762
+ * Wallet becomes BizID's `AuthorityClaims.organisationId`, and Percayso ID stops
763
+ * minting this claim. It stays declared, and read, until the previous `cayso-api`
764
+ * pin already runs the binding, because a revert must not bring back a reader of a
765
+ * claim nobody mints (CAY-0100 §7, Consequences). Its removal is the breaking
766
+ * contract step, #419. Nothing new may read it.
767
+ *
759
768
  * ## It is a second fact beside `sub`, and it replaces nothing
760
769
  *
761
770
  * `sub` stays what it has always been: the citizen Percayso ID authenticated
@@ -907,6 +916,14 @@ export interface AssertionClaims {
907
916
  * displays — never the Organisation ID of the holder behind it, and never an
908
917
  * Instance ID. Present when the holder may act for one or more organisations.
909
918
  *
919
+ * ## RETIRED by CAY-0100 §4a, and kept until #419
920
+ *
921
+ * The commercial payment's mandate route, the delegated purchase and an allowance
922
+ * grant on an Organisation Wallet authorise from BizID's `AuthorityClaims`, not from
923
+ * this claim. Percayso ID stops sending it only once no reader of it is left at the
924
+ * previous `cayso-api` pin (CAY-0100, Consequences), and its removal is the breaking
925
+ * contract step, #419. Nothing new may read it.
926
+ *
910
927
  * ## THIS IS NOT THE ORGANISATION-AUTHORITY CLAIM, AND THE TWO MUST NOT BE
911
928
  * OVERLOADED
912
929
  *
@@ -944,4 +961,30 @@ export interface AssertionClaims {
944
961
  * session, in seconds. Nothing in Phase A may branch on it (CAY-0008).
945
962
  */
946
963
  readonly sessionAgeSeconds?: number;
964
+ /**
965
+ * When the Citizen tapped their passkey for this assertion, in **seconds** since the
966
+ * Unix epoch: the issuer's convention, not `Millis`, as `exp` and `iat` are.
967
+ * OPTIONAL on the wire, declared ahead of its first reader (CAY-0100).
968
+ *
969
+ * Percayso ID mints it. BizID's authority assertion carries the tap it saw as
970
+ * `AuthorityClaims.auth_time`, and the binding (#417) refuses the two unless they
971
+ * are equal, so one tap serves both (CAY-0100 §4). Spelled `auth_time`, as OpenID
972
+ * Connect and that claim spell it, rather than in this surface's camel case, so the
973
+ * two sides of the comparison carry one name.
974
+ *
975
+ * ## Declared before Percayso ID mints it, and why that order is the point
976
+ *
977
+ * A verifier refuses any claim key it does not know. If Percayso ID minted
978
+ * `auth_time` before the `cayso-api` pin declared it, a Cloud revert to the previous
979
+ * pin would refuse every identity assertion. So Cayso declares it first, and
980
+ * Percayso ID mints it only once the previous pin already does (CAY-0100,
981
+ * Consequences). A verifier admits a non-negative integer here and refuses any other
982
+ * value as malformed.
983
+ *
984
+ * ## Nothing may branch on it yet
985
+ *
986
+ * Like {@link sessionAgeSeconds}, it is declared and depended on by nothing until the
987
+ * binding is built. Its absence refuses nothing, and its presence admits nothing.
988
+ */
989
+ readonly auth_time?: number;
947
990
  }
@@ -0,0 +1,221 @@
1
+ /**
2
+ * `authority` — the shape of Percayso BizID's authority assertion, and the closed
3
+ * reasons the binding refuses one for (CAY-0100 §2, §4).
4
+ *
5
+ * BizID holds the Organisation and Percayso ID holds people (CMTY-0031, BIZ-0010).
6
+ * So the evidence that a Citizen may act for an Organisation comes from BizID, a
7
+ * second issuer, in a credential of its own: {@link AuthorityClaims}. It says that
8
+ * this Citizen may do this operation for this Organisation, under its formed
9
+ * mandate. It is not a second identity assertion and shares no type with
10
+ * `AssertionClaims`.
11
+ *
12
+ * This file is the SHAPE ALONE, published so that Piper can build against it
13
+ * before Cayso accepts it. Nothing here reads, verifies or binds a credential.
14
+ * BizID's registration as an authority issuer, its verifier and the binding to
15
+ * Percayso ID's assertion are later slices (#415–#417), and until they are served
16
+ * an Organisation operation answers exactly as it did.
17
+ *
18
+ * ## The envelope is the identity assertion's
19
+ *
20
+ * Compact JWS, `EdDSA` over Ed25519 (CAY-0072), with a header of exactly
21
+ * `{alg, kid}`: the same {@link AssertionHeader}, not a second header type, so the
22
+ * one rule about what a header may say is written once. `iss` is BizID, `aud` is
23
+ * `'cayso'` and `jti` is fresh. `iat` and `exp` are in seconds, and `exp` sits
24
+ * within `MAX_ASSERTION_LIFETIME_SECONDS` (`assertion.ts`), the ceiling an identity
25
+ * assertion is held to. That ceiling is not restated here. Replay protection is on
26
+ * `(iss, jti)`, exactly as for identity assertions (CAY-0030).
27
+ *
28
+ * ## No verification or assurance fields
29
+ *
30
+ * BizID verifies no people. Money-grade assurance comes from Percayso ID's
31
+ * assertion, under the per-operation acceptance policy (CAY-0029), and nothing
32
+ * here can stand in for it. An `AuthorityClaims` alone authenticates no route: it
33
+ * is accepted only beside a Percayso ID assertion it binds to (CAY-0100 §3, §4).
34
+ *
35
+ * ## It travels in its own header, beside the identity assertion
36
+ *
37
+ * See {@link AUTHORITY_ASSERTION_HEADER}. The thin client sends it on the
38
+ * Organisation operations alone, fetched for each request and never cached, under
39
+ * the discipline the identity assertion is fetched under.
40
+ */
41
+ import type { AssertionHeader, ContractOperation } from './assertion.js';
42
+ /**
43
+ * The request header an authority assertion travels in, beside the identity
44
+ * assertion's `Authorization: Bearer` (CAY-0100 §4).
45
+ *
46
+ * ## Why this name
47
+ *
48
+ * - **Not `Authorization`.** That header carries Percayso ID's assertion and only
49
+ * it. A request has one `Authorization`, and the two credentials are verified by
50
+ * two verifiers against two registrations, so neither may be mistaken for the
51
+ * other by where it arrived.
52
+ * - **No `X-` prefix.** RFC 6648 retired it, and the one header this contract
53
+ * already names beside `Authorization` is `Idempotency-Key`, which has none. This
54
+ * follows that spelling: two words, each capitalised, hyphenated.
55
+ * - **The glossary's noun.** CAY-0100 calls the credential BizID's *authority
56
+ * assertion*, so the header says that and nothing more. It does not name BizID,
57
+ * for the reason `Authorization` does not name Percayso ID: who minted it is the
58
+ * credential's `iss`, checked against a registration, never a header's say-so.
59
+ *
60
+ * The value is the compact JWS alone, with no `Bearer` scheme: the scheme belongs to
61
+ * `Authorization`'s grammar, and this header has no other value to tell it from.
62
+ *
63
+ * Published as a value so a consumer that does not use the thin client spells it
64
+ * from the contract rather than from a retyped copy, and so the service that reads
65
+ * it reads the same string.
66
+ */
67
+ export declare const AUTHORITY_ASSERTION_HEADER: "Authority-Assertion";
68
+ /**
69
+ * A minted authority assertion as a whole: the identity assertion's header, and the
70
+ * claims BizID asserts (CAY-0100 §2).
71
+ *
72
+ * The header is {@link AssertionHeader} itself, exactly `{alg, kid}`, read first to
73
+ * choose a check and believed never. The split is the one `AssertionEnvelope` makes,
74
+ * for the same reason.
75
+ */
76
+ export interface AuthorityEnvelope {
77
+ /** How to check this credential. Read first, believed never. */
78
+ readonly header: AssertionHeader;
79
+ /** What BizID asserts, established only once the header's check has passed. */
80
+ readonly claims: AuthorityClaims;
81
+ }
82
+ /**
83
+ * The claims carried by BizID's authority assertion (CAY-0100 §2): this Citizen may
84
+ * perform this operation for this Organisation, under this version of its formed
85
+ * mandate.
86
+ *
87
+ * CLOSED. These ten keys and no others: a verifier refuses a key it does not know, as
88
+ * it does for an identity assertion. There are no verification or assurance fields,
89
+ * because BizID does not verify people.
90
+ *
91
+ * A claim to be CHECKED, never a claim to be trusted. Cayso verifies a minted
92
+ * assertion and never takes a caller's word for who they are, nor for whom they act
93
+ * (CLAUDE.md, Trust and authentication).
94
+ */
95
+ export interface AuthorityClaims {
96
+ /**
97
+ * Who minted this: Percayso BizID, registered as an authority issuer for this type
98
+ * alone (CAY-0100 §3). Typed `string` rather than a literal for the reason
99
+ * `AssertionClaims.iss` is: an unrecognised issuer must be representable in order
100
+ * to be refused.
101
+ */
102
+ readonly iss: string;
103
+ /** The intended recipient. Exactly `'cayso'`, and checked, not merely present. */
104
+ readonly aud: 'cayso';
105
+ /**
106
+ * This assertion's unique identity, fresh for each one. Replay is refused on
107
+ * `(iss, jti)`, exactly as for identity assertions (CAY-0030). It never names the
108
+ * identity assertion's `jti`, and the identity assertion never names it (CAY-0100
109
+ * §4): each is consumed as its own single use.
110
+ */
111
+ readonly jti: string;
112
+ /**
113
+ * Issued-at, in **seconds** since the Unix epoch, the issuer's convention and not
114
+ * `Millis`, as on `AssertionClaims`.
115
+ */
116
+ readonly iat: number;
117
+ /**
118
+ * Expiry, in **seconds** since the Unix epoch. No further ahead than
119
+ * `MAX_ASSERTION_LIFETIME_SECONDS`, the ceiling an identity assertion is held to
120
+ * (CAY-0100 §2). At or before now is expired, with no skew grace added.
121
+ */
122
+ readonly exp: number;
123
+ /**
124
+ * The Citizen this authority is for: the subject Percayso ID's assertion already
125
+ * named. **NEVER taken as an identity** (CAY-0100 §3). BizID attests no person, so
126
+ * this is read only as the value the binding compares with the identity assertion's
127
+ * `sub`, and a mismatch is `subject_mismatch`. Like that `sub`, it never reaches
128
+ * the chain (CAY-0006).
129
+ */
130
+ readonly sub: string;
131
+ /**
132
+ * The ONE operation this authority was minted for. SINGULAR, never a list: a list
133
+ * would be a menu, and a credential carrying a menu lets whatever holds it choose.
134
+ * It must be in Percayso ID's `scope` and be the operation of the route it is
135
+ * presented on, so an authority for one act cannot be spent on another (CAY-0100
136
+ * §4).
137
+ */
138
+ readonly operation: ContractOperation;
139
+ /**
140
+ * The Organisation ID Percayso BizID mints: the permanent external identity of the
141
+ * Organisation Wallet's holder (CAY-0100 §1).
142
+ *
143
+ * It reaches Cayso from THIS claim and from nowhere else: not a path, a body or a
144
+ * query, and not Percayso ID's assertion, which does not echo it because Percayso
145
+ * ID holds people, not Organisations (CAY-0100 §4). Nothing compares it against a
146
+ * Percayso ID claim.
147
+ *
148
+ * A plain `string`, for the reason `AssertionClaims.organisationAuthority` is one:
149
+ * this is the wire, JSON carries no brands, and a published type a consumer cannot
150
+ * build from a string it holds is a distinction that consumer cannot observe. The
151
+ * branded `OrganisationId`, which refuses an Instance ID at compile time, lives on
152
+ * the write path in `apps/api`, where the holder key is formed (CAY-0059 §5).
153
+ */
154
+ readonly organisationId: string;
155
+ /**
156
+ * The version of the Organisation's formed mandate BizID decided this authority
157
+ * under (CAY-0100 §5). Cayso records it with the movement, or in the act's audit
158
+ * row where the act writes no movement, and refuses an `AuthorityClaims` without
159
+ * one. It checks only that it is present and well formed: what the mandate allows is
160
+ * BizID's to decide, and Cayso does not interpret it.
161
+ *
162
+ * A JSON **integer, at least 1**: the mandate's version counter, which starts at 1 and
163
+ * rises by one each time the mandate is re-formed. The JWS payload carries a bare
164
+ * number (`"mandateVersion":1`), as `bizid-contracts` declares it and BIZ-0010 records
165
+ * it. {@link isMandateVersion} is the one check of that shape.
166
+ */
167
+ readonly mandateVersion: number;
168
+ /**
169
+ * When the Citizen tapped their Percayso ID passkey, in **seconds** since the Unix
170
+ * epoch: the tap BizID saw (CAY-0100 §2). Percayso ID's assertion carries the same
171
+ * `auth_time`, so one tap serves both, and the binding refuses two that differ as
172
+ * `auth_time_mismatch` (§4). Spelled `auth_time`, as OpenID Connect spells it and as
173
+ * BIZ-0010 does, rather than in this surface's camel case.
174
+ */
175
+ readonly auth_time: number;
176
+ }
177
+ /**
178
+ * Why the binding refused an Organisation operation: CAY-0100 §4's working shape,
179
+ * closed and spelled here.
180
+ *
181
+ * Published before anything produces it, as `founder_wallet_lapsed` was, so a
182
+ * consumer exhausting `CaysoApiErrorCode` renders it from the version that declared
183
+ * it. A new reason is a new member and a contract version, never a free-text field.
184
+ * Every member means the same thing: the operation did not happen.
185
+ *
186
+ * It is the BINDING's refusal, not the operation's. Each operation keeps its own
187
+ * union for what it refuses once the binding holds, and the identity assertion
188
+ * keeps the gate's codes.
189
+ */
190
+ export type AuthorityBindingRefusal =
191
+ /**
192
+ * An Organisation operation was presented with no authority assertion. Refused,
193
+ * and never read as "act as the Citizen alone" (CAY-0100 §4).
194
+ */
195
+ 'authority_assertion_missing'
196
+ /**
197
+ * The authority assertion did not verify: malformed, an issuer or key not
198
+ * registered for this type, a bad signature, expired, or already used. One member
199
+ * for all of them, so the refusal describes no part of the check to whoever is
200
+ * probing it.
201
+ */
202
+ | 'authority_assertion_invalid'
203
+ /** The two assertions name different subjects. */
204
+ | 'subject_mismatch'
205
+ /** The authority's `operation` is not in Percayso ID's `scope`. */
206
+ | 'operation_not_in_scope'
207
+ /**
208
+ * The authority's `operation` is not the operation of the route it was presented
209
+ * on, so an authority minted for one act cannot be spent on another.
210
+ */
211
+ | 'operation_not_the_routes'
212
+ /** The two assertions carry different `auth_time`s, so no one passkey tap serves both. */
213
+ | 'auth_time_mismatch'
214
+ /** The two assertions are not both unexpired at the same moment. */
215
+ | 'not_jointly_current';
216
+ /**
217
+ * Whether `value` is a well-formed {@link AuthorityClaims.mandateVersion}: a JSON integer
218
+ * of at least 1. The one statement of the shape, so the verifier (#416) and any consumer
219
+ * check it the same way. It says nothing about what the mandate allows.
220
+ */
221
+ export declare function isMandateVersion(value: unknown): value is number;
@@ -0,0 +1,49 @@
1
+ /*
2
+ * Copyright 2026 Percayso Community
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+ /**
17
+ * The request header an authority assertion travels in, beside the identity
18
+ * assertion's `Authorization: Bearer` (CAY-0100 §4).
19
+ *
20
+ * ## Why this name
21
+ *
22
+ * - **Not `Authorization`.** That header carries Percayso ID's assertion and only
23
+ * it. A request has one `Authorization`, and the two credentials are verified by
24
+ * two verifiers against two registrations, so neither may be mistaken for the
25
+ * other by where it arrived.
26
+ * - **No `X-` prefix.** RFC 6648 retired it, and the one header this contract
27
+ * already names beside `Authorization` is `Idempotency-Key`, which has none. This
28
+ * follows that spelling: two words, each capitalised, hyphenated.
29
+ * - **The glossary's noun.** CAY-0100 calls the credential BizID's *authority
30
+ * assertion*, so the header says that and nothing more. It does not name BizID,
31
+ * for the reason `Authorization` does not name Percayso ID: who minted it is the
32
+ * credential's `iss`, checked against a registration, never a header's say-so.
33
+ *
34
+ * The value is the compact JWS alone, with no `Bearer` scheme: the scheme belongs to
35
+ * `Authorization`'s grammar, and this header has no other value to tell it from.
36
+ *
37
+ * Published as a value so a consumer that does not use the thin client spells it
38
+ * from the contract rather than from a retyped copy, and so the service that reads
39
+ * it reads the same string.
40
+ */
41
+ export const AUTHORITY_ASSERTION_HEADER = 'Authority-Assertion';
42
+ /**
43
+ * Whether `value` is a well-formed {@link AuthorityClaims.mandateVersion}: a JSON integer
44
+ * of at least 1. The one statement of the shape, so the verifier (#416) and any consumer
45
+ * check it the same way. It says nothing about what the mandate allows.
46
+ */
47
+ export function isMandateVersion(value) {
48
+ return typeof value === 'number' && Number.isInteger(value) && value >= 1;
49
+ }
package/dist/client.d.ts CHANGED
@@ -24,6 +24,23 @@
24
24
  * so each attempt gets a fresh one. That the retry re-fetches is a decision, not
25
25
  * an accident.
26
26
  *
27
+ * ## A second assertion, on the Organisation operations alone
28
+ *
29
+ * An act on an Organisation Wallet needs BizID's authority assertion beside Percayso
30
+ * ID's (CAY-0100 §4). The three methods that can be one take an optional
31
+ * {@link AuthorityAssertionProvider} as their last argument, and nothing else does.
32
+ * Its `getAuthorityAssertion()` is called under exactly the discipline above: inside
33
+ * each attempt, beside `getAssertion()`, never cached, and re-fetched on a retry,
34
+ * because the authority assertion is short-lived and single-use too. It travels in
35
+ * its own header, {@link AUTHORITY_ASSERTION_HEADER}, never in `Authorization`.
36
+ *
37
+ * It is per CALL rather than a client option, and that is the point. BizID mints an
38
+ * authority for one operation and one Organisation, and these methods name no
39
+ * Organisation, so only the caller knows which Organisation this act is for. A
40
+ * client-wide provider would have to guess, or would hand every Organisation
41
+ * operation the same authority. Passing none sends no header, and the request is
42
+ * exactly the request a `0.30.0` client sent.
43
+ *
27
44
  * ## One method per route, no method that is not
28
45
  *
29
46
  * Every method reads its method and path from `ROUTES` — no URL is hand-written
@@ -107,6 +124,19 @@ export interface CaysoClientOptions {
107
124
  */
108
125
  warn?: (message: string) => void;
109
126
  }
127
+ /**
128
+ * Where an Organisation operation gets BizID's authority assertion for this one act
129
+ * (CAY-0100 §4): the second credential, beside the one `getAssertion` supplies.
130
+ *
131
+ * Called before EVERY attempt, never once and never cached, for the reason
132
+ * `getAssertion` is: the credential is short-lived and single-use, so one minted for
133
+ * a first attempt must not be presented again by a retry. The value is the compact
134
+ * JWS BizID minted, sent in {@link AUTHORITY_ASSERTION_HEADER} exactly as returned.
135
+ */
136
+ export interface AuthorityAssertionProvider {
137
+ /** Called before every attempt. Authority assertions are short-lived, so never cache one. */
138
+ getAuthorityAssertion(): Promise<string>;
139
+ }
110
140
  /**
111
141
  * One method per entry in `ROUTES`, and no method that is not. `getMyWallet`
112
142
  * answers `null` where the contract says a wallet may not exist yet, rather than
@@ -166,8 +196,12 @@ export interface CaysoClient {
166
196
  * carries no idempotency key and a repeat is the same act rather than a second
167
197
  * one. A credential carrying no organisation authority is refused; absence
168
198
  * provisions nothing.
199
+ *
200
+ * `authority` supplies BizID's authority assertion for `provision-organisation`
201
+ * (CAY-0100 §4). Optional, and sent only when given: no route reads it until the
202
+ * binding is served, and from then an Organisation operation without it is refused.
169
203
  */
170
- provisionOrganisation(): Promise<OrganisationView>;
204
+ provisionOrganisation(authority?: AuthorityAssertionProvider): Promise<OrganisationView>;
171
205
  /**
172
206
  * Authorise a purchase — or REPLAY one (CAY-0032). Posting the same
173
207
  * `idempotencyKey` twice returns the ORIGINAL authorisation, at the original
@@ -184,8 +218,12 @@ export interface CaysoClient {
184
218
  * `'terminal'` means the key's purchase has finished and will not change: a
185
219
  * repeat of it will keep answering `'terminal'` forever, and the remedy is a new
186
220
  * purchase under a NEW key.
221
+ *
222
+ * `authority` supplies BizID's authority assertion for a purchase on behalf of an
223
+ * Organisation Wallet, when `onBehalfOfWalletId` names one (CAY-0100 §4a). A
224
+ * Citizen's own purchase needs none and passes none.
187
225
  */
188
- authorisePurchase(req: PurchaseRequest): Promise<PurchaseAuthorised | PurchaseTerminal>;
226
+ authorisePurchase(req: PurchaseRequest, authority?: AuthorityAssertionProvider): Promise<PurchaseAuthorised | PurchaseTerminal>;
189
227
  getPurchase(purchaseId: string): Promise<PurchaseResult>;
190
228
  /**
191
229
  * Cancel a purchase the caller started, releasing its reservation (CAY-0032).
@@ -213,8 +251,11 @@ export interface CaysoClient {
213
251
  * thrown {@link CaysoApiError} whose `code` is a `CommercialPaymentRefusal`
214
252
  * member, one of the gate's codes, or a seam code, as {@link authorisePurchase}'s
215
253
  * refusals do. A refusal means nothing moved.
254
+ *
255
+ * `authority` supplies BizID's authority assertion on the `'mandate'` route
256
+ * (CAY-0100 §4a). The `'payment-allowance'` route needs none and passes none.
216
257
  */
217
- makeCommercialPayment(req: CommercialPaymentRequest): Promise<CommercialPaymentMade>;
258
+ makeCommercialPayment(req: CommercialPaymentRequest, authority?: AuthorityAssertionProvider): Promise<CommercialPaymentMade>;
218
259
  /**
219
260
  * Transfer unrestricted cents from the caller's own Citizen Wallet to another
220
261
  * Citizen Wallet — or REPLAY a transfer already made under the same key (CAY-0088).
package/dist/client.js CHANGED
@@ -13,7 +13,7 @@
13
13
  * See the License for the specific language governing permissions and
14
14
  * limitations under the License.
15
15
  */
16
- import { CONTRACT_VERSION, NOT_PROVISIONED } from './index.js';
16
+ import { AUTHORITY_ASSERTION_HEADER, CONTRACT_VERSION, NOT_PROVISIONED } from './index.js';
17
17
  import { ROUTES } from './routes.js';
18
18
  // --- The retry mechanism -----------------------------------------------------
19
19
  // These are this slice's own mechanism, not economics: an attempt count, a
@@ -110,6 +110,16 @@ function buildClient(options, internals) {
110
110
  if (spec.idempotencyKey !== undefined && spec.idempotencyKey.length > 0) {
111
111
  headers['Idempotency-Key'] = spec.idempotencyKey;
112
112
  }
113
+ // The second credential, under the first one's discipline: per attempt, never
114
+ // cached, and in a header of its own (CAY-0100 §4). Absent unless the caller
115
+ // passed a provider, so every other request is unchanged. An empty value is sent
116
+ // as no header, as an empty key is above, so the server reads it as missing
117
+ // rather than as present and malformed.
118
+ if (spec.authority !== undefined) {
119
+ const authority = await spec.authority.getAuthorityAssertion();
120
+ if (authority.length > 0)
121
+ headers[AUTHORITY_ASSERTION_HEADER] = authority;
122
+ }
113
123
  const init = { method: spec.method, headers };
114
124
  if (spec.body !== undefined)
115
125
  init.body = JSON.stringify(spec.body);
@@ -190,7 +200,7 @@ function buildClient(options, internals) {
190
200
  idempotent: true,
191
201
  });
192
202
  },
193
- async provisionOrganisation() {
203
+ async provisionOrganisation(authority) {
194
204
  return requestJson({
195
205
  method: ROUTES.organisationProvision.method,
196
206
  path: ROUTES.organisationProvision.path,
@@ -198,9 +208,10 @@ function buildClient(options, internals) {
198
208
  // is: the operation is idempotent on the attested Organisation ID, so a
199
209
  // repeat is the same act rather than a second one.
200
210
  idempotent: true,
211
+ authority,
201
212
  });
202
213
  },
203
- async authorisePurchase(req) {
214
+ async authorisePurchase(req, authority) {
204
215
  return requestJson({
205
216
  method: ROUTES.purchaseAuthorise.method,
206
217
  path: ROUTES.purchaseAuthorise.path,
@@ -208,9 +219,10 @@ function buildClient(options, internals) {
208
219
  idempotent: typeof req.idempotencyKey === 'string' && req.idempotencyKey.length > 0,
209
220
  body: req,
210
221
  idempotencyKey: req.idempotencyKey,
222
+ authority,
211
223
  });
212
224
  },
213
- async makeCommercialPayment(req) {
225
+ async makeCommercialPayment(req, authority) {
214
226
  return requestJson({
215
227
  method: ROUTES.commercialPayment.method,
216
228
  path: ROUTES.commercialPayment.path,
@@ -219,6 +231,7 @@ function buildClient(options, internals) {
219
231
  idempotent: typeof req.idempotencyKey === 'string' && req.idempotencyKey.length > 0,
220
232
  body: req,
221
233
  idempotencyKey: req.idempotencyKey,
234
+ authority,
222
235
  });
223
236
  },
224
237
  async makeCitizenTransfer(req) {
@@ -98,9 +98,11 @@
98
98
  * A **withheld** contribution row is carried and settles nothing: it writes no
99
99
  * movement and does not refuse the period. How a later period pays what was withheld
100
100
  * is open (CAY-0096 §5). A row of nought cents writes no movement either, though every
101
- * check on it still runs. A **Core Node**'s qualified row is refused with its own
102
- * member, `contribution_node_is_a_core_node`: a Core Node's owner is the Operator, and
103
- * where its contribution settles, if anywhere, is not designed (CAY-0096 §4).
101
+ * check on it still runs. A **Core Node**'s qualified row of any amount but nought is
102
+ * refused with its own member, `contribution_node_is_a_core_node`: a Core Node's owner is
103
+ * the Operator, and where its contribution settles, if anywhere, is not designed
104
+ * (CAY-0096 §4). A Core Node row of nought cents is skipped and settles nothing, an
105
+ * interim rule pending that design (30 October 2026, cloud.platform#357).
104
106
  *
105
107
  * ## What is not here
106
108
  *
@@ -734,8 +736,9 @@ export type ContributionRowRefusal =
734
736
  */
735
737
  'contribution_node_not_bound'
736
738
  /**
737
- * A qualified row names a Node this deployment is told is a Core Node. A Core
738
- * Node's owner is the Operator (CMTY-0018 §4), so it settles to no Citizen Wallet.
739
+ * A qualified row of more than nought cents names a Node this deployment is told is a
740
+ * Core Node. A Core Node's owner is the Operator (CMTY-0018 §4), so it settles to no
741
+ * Citizen Wallet. A nought row for a Core Node is skipped instead (interim, #357).
739
742
  */
740
743
  | 'contribution_node_is_a_core_node'
741
744
  /** The Treasury holds too few unrestricted cents to pay this row. */