@percayso-community/cayso-contracts 0.24.0 → 0.27.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
@@ -12,26 +12,45 @@ literal `0` to `number` — so the one structural guarantee in that surface dies
12
12
  the HTTP boundary. The fix is a single published package whose types a consumer
13
13
  imports rather than retypes.
14
14
 
15
- ## The Piper-facing surface only
16
-
17
- Published here: the record and response types, the closed failure-reason union,
18
- the assertion claims and the route constants — the surface Piper reads.
19
-
20
- **Deliberately not published:** peer types, admin types, holdings and carves.
21
- They are not a consumer's business, and putting them on the public surface widens
22
- it for nothing.
15
+ ## The surface of every consumer outside this tree
16
+
17
+ Published here: the record and response types, the closed failure-reason unions,
18
+ the assertion claims and the route constants — the surface every consumer outside
19
+ this repository reads (CAY-0026, Amendment 3). Piper is the first consumer. The
20
+ Percayso Cloud is the second: its Contribution Store presents the settlement
21
+ assertion to Cayso directly, and the Cloud generates its types from the `schema`
22
+ entry point rather than typing Cayso's half by hand. The Cloud's two seams live in
23
+ `cloud-settlement`, and the settlement path, which a registered issuer calls and
24
+ no person does, is `CLOUD_SETTLEMENT_ROUTE`, published apart from `ROUTES`.
25
+
26
+ **The operator read surface, on its own entry point** (CAY-0026, Amendment 4). Three
27
+ reads an operator app's *server* calls — the movement list, the purchase list and the
28
+ quickening ladder — are published at `@percayso-community/cayso-contracts/operator`:
29
+ their paths and scopes, query inputs (wallet id, a half-open `[from, to)` window, a
30
+ keyset cursor), rows and pages, and their closed refusal and not-configured bodies.
31
+ Money is `Cents`, a wallet is named by its wallet id, and no Percayso identity
32
+ appears on any row. It is not re-exported from the barrel, so Piper and the Cloud
33
+ never import an operator vocabulary by accident, and the schema describes it under
34
+ its own `operator` key rather than among the person-facing types. Cayso sends no
35
+ CORS headers: these reads are for a server, never a browser. Widening the entry
36
+ point beyond these three needs a further amendment.
37
+
38
+ **Deliberately not published:** peer types, holdings and carves, and every operator
39
+ route but those three reads. They are not a consumer's business, and putting them on
40
+ the public surface widens it for nothing.
23
41
 
24
42
  **This package carries no numbers.** Amounts, rates, tiers, schedules and
25
43
  thresholds live in the canon PRD and are read from one place. None of them are
26
44
  restated here.
27
45
 
28
- ## Three entry points
46
+ ## Four entry points
29
47
 
30
48
  | Import | What it gives you |
31
49
  |---|---|
32
50
  | `@percayso-community/cayso-contracts` | Types and constants. No runtime. |
33
51
  | `@percayso-community/cayso-contracts/client` | The thin client — fetch, assertion header, idempotent retry. |
34
52
  | `@percayso-community/cayso-contracts/schema` | A machine-readable descriptor of the published surface. |
53
+ | `@percayso-community/cayso-contracts/operator` | The three operator reads' types and route constants, for an operator app's server. |
35
54
 
36
55
  The split is the point: a consumer wanting only the types takes on no runtime at
37
56
  all. The client owns the fetch, the assertion header and the idempotent retry.
@@ -63,9 +82,14 @@ describes. The thin client's `getVersion()` reads it and warns once, per client
63
82
  instance, when the server's contract *major* differs from the one the client was
64
83
  built against.
65
84
 
85
+ After a minor bump that is additive on the wire, a deployment keeps the previous
86
+ minor in `supportedContractVersions` for one release, so a consumer still pinned
87
+ to it keeps working while it moves. A breaking bump drops it, and says so in the
88
+ bump's changelog.
89
+
66
90
  Alongside it, `GET /api/version/schema` serves the same machine-readable
67
- descriptor as `@percayso-community/cayso-contracts/schema` — the Piper-facing surface only, with no
68
- operator, webhook or peer route. It exists for tooling and diagnostics.
91
+ descriptor as `@percayso-community/cayso-contracts/schema` — the surface outside consumers read, with
92
+ no operator, webhook or peer route. It exists for tooling and diagnostics.
69
93
 
70
94
  **It is not a substitute for depending on this package.** The descriptor is
71
95
  data; the guarantee is the *types*, and the types do not exist at runtime — they
@@ -319,6 +319,13 @@
319
319
  * was served, as `'provision-citizenship'` was, and it is served at
320
320
  * `ROUTES.commercialPayment`.
321
321
  *
322
+ * `'make-citizen-transfer'` is a Citizen transferring Cayso from their own Citizen
323
+ * Wallet to another Citizen's (CAY-0088). It is not a second use of
324
+ * `'make-commercial-payment'`, which always has an organisation on one side, nor of
325
+ * `'place-quote'`, which prices cents against sterling: a transfer quotes nothing. A
326
+ * credential minted to place a quote must not thereby move cents to somebody without
327
+ * a match, nor the reverse. It is served at `ROUTES.citizenTransfer`.
328
+ *
322
329
  * `'place-quote'`, `'withdraw-quote'` and `'request-buy-back'` are the citizen
323
330
  * market's three acts (CAY-0080, Open decision Q). They are three operations
324
331
  * because they are three acts: a credential minted to withdraw a quote must not
@@ -353,15 +360,27 @@
353
360
  * checked. Neither is a second use of `'authorise-purchase'`, which brings sterling
354
361
  * in, or of `'make-commercial-payment'`, which is organisation-to-Citizen only.
355
362
  * Both are classified before they are served, and no route serves either yet.
363
+ *
364
+ * `'present-standing-authorisation'` is an owner delegating spending to their
365
+ * Instance on Percayso Cloud services (CMTY-0018 §1): the Instance presents the
366
+ * owner's standing authorisation, signed by the Instance key, under the owner's
367
+ * assertion, and Cayso answers with an acknowledgement the Cloud meters against.
368
+ * It is not a second use of `'make-commercial-payment'`: nothing moves when it is
369
+ * presented, and what it authorises is spending in later periods, settled by the
370
+ * Contribution Store's assertion, which no person presents and which therefore has
371
+ * no operation here. It is classified before it is served.
356
372
  */
357
- export type ContractOperation = 'read-own-wallet' | 'read-purchase-result' | 'authorise-purchase' | 'cancel-purchase' | 'provision-citizenship' | 'read-own-citizenship' | 'provision-organisation' | 'claim-founder-wallet' | 'make-commercial-payment' | 'place-quote' | 'withdraw-quote' | 'request-buy-back' | 'read-citizen-market' | 'read-marketplace-receipt' | 'settle-marketplace-order' | 'settle-marketplace-refund';
373
+ export type ContractOperation = 'read-own-wallet' | 'read-purchase-result' | 'authorise-purchase' | 'cancel-purchase' | 'provision-citizenship' | 'read-own-citizenship' | 'provision-organisation' | 'claim-founder-wallet' | 'make-commercial-payment' | 'make-citizen-transfer' | 'place-quote' | 'withdraw-quote' | 'request-buy-back' | 'read-citizen-market' | 'read-marketplace-receipt' | 'settle-marketplace-order' | 'settle-marketplace-refund' | 'present-standing-authorisation';
358
374
  /**
359
375
  * An operation key belonging to a deployment's OWN surfaces rather than to this
360
376
  * contract — operator work, above all. Deliberately not enumerated: this package
361
377
  * is published and carries the Piper-facing surface only (CAY-0026), and naming
362
378
  * an internal vocabulary here would publish a set of keys no consumer of this
363
379
  * contract may ever present. Where the closed set is kept instead is stated on
364
- * {@link ScopeKey}.
380
+ * {@link ScopeKey}. The three operator READ scopes are published, but on the
381
+ * `./operator` entry point (`OperatorReadScope`, CAY-0026 Amendment 4), for an
382
+ * operator app's server and not for this contract's consumers — so this type
383
+ * stays open, and the rest of the operator vocabulary stays unpublished.
365
384
  */
366
385
  export type InternalOperation = string;
367
386
  /**
package/dist/assurance.js CHANGED
@@ -325,6 +325,24 @@ export const ASSURANCE_POLICY = {
325
325
  'money-grade: anything but the development bypass. The bar applies on both routes, and to ' +
326
326
  'an Authorised User it applies again at every payment, not only when the allowance was granted.',
327
327
  },
328
+ // A Citizen transferring to another Citizen (CAY-0088 §2). Money-grade, at the
329
+ // purchase path's bar, for the commercial payment's reason: value leaves a holder
330
+ // for somebody else, and a bar no issuer can clear would close the transfer on
331
+ // every deployment. An allowlist, so a future method is admissible only when
332
+ // somebody decides it here.
333
+ 'make-citizen-transfer': {
334
+ operation: 'make-citizen-transfer',
335
+ assurance: 'required',
336
+ decisions: {
337
+ // Asserts nothing; permanently refused wherever "identity-verified" is meant.
338
+ 'dev-bypass': 'refused',
339
+ document: 'accepted',
340
+ 'document-and-biometric': 'accepted',
341
+ },
342
+ rationale: 'A Citizen transfer moves value out of one holder to another, so it is money-grade: ' +
343
+ 'anything but the development bypass. An unverified holder must never be able to send, ' +
344
+ 'because sending is how value reaches somebody else.',
345
+ },
328
346
  // The citizen market's three acts (CAY-0080 §7, Open decision Q). All three are
329
347
  // money-grade and all three are allowlists, at the purchase path's bar: the acting
330
348
  // party is a Citizen, and a bar no issuer can yet clear would shut the market on
@@ -452,6 +470,27 @@ export const ASSURANCE_POLICY = {
452
470
  "Platform's signed refund decision; the assertion shows the buyer is present, and it is held " +
453
471
  'to the same bar as the sale it refunds.',
454
472
  },
473
+ // An owner delegating spending to their Instance (CMTY-0018 §1). Money-grade, in
474
+ // the pattern of the commercial payment: what is authorised is cents leaving the
475
+ // holder's wallet in every later period, up to a cap, without the holder present.
476
+ // The passkey ceremony is the assertion's verification method, and the bar is
477
+ // the purchase path's, so a bar no issuer can yet clear does not shut the Cloud
478
+ // on every deployment. What limits the spending is the cap and its canon ceiling,
479
+ // and neither is an assurance question.
480
+ 'present-standing-authorisation': {
481
+ operation: 'present-standing-authorisation',
482
+ assurance: 'required',
483
+ decisions: {
484
+ // Asserts nothing; permanently refused wherever "identity-verified" is meant.
485
+ 'dev-bypass': 'refused',
486
+ document: 'accepted',
487
+ 'document-and-biometric': 'accepted',
488
+ },
489
+ rationale: "Presenting a standing authorisation delegates spending from the holder's wallet to their " +
490
+ 'Instance, up to a cap in every period, so it is money-grade: anything but the development ' +
491
+ 'bypass. Nothing moves when it is presented; what it authorises moves later, without the ' +
492
+ 'holder present, which is why the bar is applied here.',
493
+ },
455
494
  };
456
495
  /**
457
496
  * Every operation, as a value. Derived from {@link ASSURANCE_POLICY} for the same
@@ -484,11 +523,13 @@ export const OPERATION_FOR_ROUTE = {
484
523
  purchaseResult: 'read-purchase-result',
485
524
  purchaseCancel: 'cancel-purchase',
486
525
  commercialPayment: 'make-commercial-payment',
526
+ citizenTransfer: 'make-citizen-transfer',
487
527
  citizenMarket: 'read-citizen-market',
488
528
  placeQuote: 'place-quote',
489
529
  withdrawQuote: 'withdraw-quote',
490
530
  requestBuyBack: 'request-buy-back',
491
531
  marketplaceOrder: 'settle-marketplace-order',
532
+ standingAuthorisation: 'present-standing-authorisation',
492
533
  version: null,
493
534
  };
494
535
  /**
@@ -0,0 +1,110 @@
1
+ /**
2
+ * `citizen-transfer` — the request, the answer and the closed refusal union for a
3
+ * Citizen transferring Cayso to another Citizen (CAY-0088).
4
+ *
5
+ * A Citizen transfer is unrestricted cents moving directly from the caller's own
6
+ * Citizen Wallet to another Citizen Wallet, as one `transfer` movement. It is not a
7
+ * quoted sale and it is not the citizen market: no price is quoted, no sterling moves,
8
+ * and the Treasury takes no part. A transfer between a pair that mirrors market fills
9
+ * is watched for by the transfer-pattern alarm (CAY-0080 §5.8) and is reported, never
10
+ * reversed. Served at `ROUTES.citizenTransfer`.
11
+ *
12
+ * ## One movement or nothing, so there is no status
13
+ *
14
+ * As with the commercial payment (CAY-0079 §5) there is no sterling leg and no
15
+ * provider, so there is nothing to authorise first and capture later. The answer is a
16
+ * {@link CitizenTransferMade}, carrying the one movement that was written, or a
17
+ * {@link CitizenTransferRefused}, carrying one reason and meaning nothing was written.
18
+ *
19
+ * ## Replay under one key
20
+ *
21
+ * The key is unique together with the sending wallet. A repeat with the same key, the
22
+ * same payee and the same cents answers the movement already written, in the same
23
+ * shape; a repeat with the same key and anything different is refused with
24
+ * `idempotency_key_reused_for_a_different_transfer`. A transfer is never reversed: a
25
+ * Citizen who wants cents back asks for a new transfer the other way, made by the
26
+ * other Citizen under their own key.
27
+ *
28
+ * ## The sender is never named in the request
29
+ *
30
+ * The sending wallet is the verified assertion's subject's own Citizen Wallet,
31
+ * resolved inside the service (CAY-0021). A request that named the sending wallet
32
+ * would be the caller's claim about whose cents these are. The payee is a wallet id,
33
+ * given by the payee as they would give an address, and never a Percayso identity.
34
+ *
35
+ * ## What this file must not decide
36
+ *
37
+ * - **No figures.** No ceiling, rate or threshold is restated here (CAY-0088 §4).
38
+ * - **One payee.** A split is not designed.
39
+ * - **Restricted cents never move.** A restricted lot is not transferable (CAY-0074),
40
+ * so a wallet short of unrestricted cents is short, whatever else it holds.
41
+ */
42
+ import type { Cents, Millis } from './wallet.js';
43
+ /** A Citizen transferring cents from their own Citizen Wallet to another, under one key. */
44
+ export interface CitizenTransferRequest {
45
+ /** The Citizen Wallet the cents arrive in. A wallet id, never a Percayso identity. */
46
+ readonly payeeWalletId: string;
47
+ /** Whole cents, above zero. Anything else is refused `amount_not_positive_whole_cents`. */
48
+ readonly cents: Cents;
49
+ /** Caller-generated, unique per sending wallet. The same key never moves cents twice; a repeat REPLAYS. */
50
+ readonly idempotencyKey: string;
51
+ }
52
+ /**
53
+ * The one movement a Citizen transfer wrote. Final and append-only. It names the
54
+ * sending wallet because the caller is owed the handle their own cents left from, and
55
+ * names no person.
56
+ */
57
+ export interface CitizenTransfer {
58
+ /** The movement that carried the cents. */
59
+ readonly movementId: string;
60
+ readonly fromWalletId: string;
61
+ readonly payeeWalletId: string;
62
+ readonly cents: Cents;
63
+ readonly transferredAt: Millis;
64
+ }
65
+ /** The transfer was made. Answered for the request that made it and for every replay of its key. */
66
+ export interface CitizenTransferMade {
67
+ readonly outcome: 'transferred';
68
+ readonly transfer: CitizenTransfer;
69
+ }
70
+ /** The transfer was refused, with one member of the closed union. Nothing was written. */
71
+ export interface CitizenTransferRefused {
72
+ readonly outcome: 'refused';
73
+ readonly error: CitizenTransferRefusal;
74
+ }
75
+ /** The ONE declared answer to a Citizen transfer, discriminated by `outcome`. */
76
+ export type CitizenTransferResponse = CitizenTransferMade | CitizenTransferRefused;
77
+ /**
78
+ * Why a Citizen transfer was refused: CAY-0088 §3's set, closed. A consumer renders a
79
+ * member by its name. Every member means the same thing about money: nothing moved.
80
+ */
81
+ export type CitizenTransferRefusal =
82
+ /**
83
+ * The verification method is outside the money-grade assurance profile
84
+ * (`ASSURANCE_POLICY['make-citizen-transfer']`). `dev-bypass` is never inside it.
85
+ */
86
+ 'assurance_not_money_grade'
87
+ /** The verified subject resolves to no Citizen Wallet. A transfer never provisions one. */
88
+ | 'sender_has_no_citizen_wallet'
89
+ /** No wallet has the payee's id. */
90
+ | 'payee_wallet_not_found'
91
+ /** The payee's wallet is an Organisation Wallet or the Treasury. Organisations are paid by commercial payment. */
92
+ | 'payee_wallet_not_a_citizens'
93
+ /** No citizen is bound to the payee's wallet now, for example because the binding was erased. */
94
+ | 'payee_wallet_has_no_live_binding'
95
+ /** The payee's wallet is the sender's own. */
96
+ | 'payee_is_the_sender'
97
+ /** The cents are zero, negative or not a whole number. */
98
+ | 'amount_not_positive_whole_cents'
99
+ /**
100
+ * The sender's unrestricted cents are short. Restricted cents are never counted and
101
+ * never drawn (CAY-0074), so a wallet holding enough only once restricted cents are
102
+ * included is still short.
103
+ */
104
+ | 'unrestricted_cents_insufficient'
105
+ /** A holding the transfer would draw is frozen. */
106
+ | 'cents_frozen'
107
+ /** The key was already used by this sending wallet for a different payee or a different number of cents. */
108
+ | 'idempotency_key_reused_for_a_different_transfer'
109
+ /** Too many concurrent draws on the sending wallet. Nothing moved; retry under the same key. */
110
+ | 'carve_contended';
@@ -0,0 +1,16 @@
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
+ export {};
package/dist/client.d.ts CHANGED
@@ -69,6 +69,8 @@
69
69
  */
70
70
  import type { BuyBackMade, BuyBackRequest, CitizenMarketViewed, QuotePlaced, QuoteRequest, QuoteWithdrawalRequest, QuoteWithdrawn } from './citizen-market.js';
71
71
  import type { CitizenshipFounderClaimed, CitizenshipView } from './citizenship.js';
72
+ import type { StandingAuthorisationAcknowledged, StandingAuthorisationRequest } from './cloud-settlement.js';
73
+ import type { CitizenTransferMade, CitizenTransferRequest } from './citizen-transfer.js';
72
74
  import type { CommercialPaymentMade, CommercialPaymentRequest } from './commercial-payment.js';
73
75
  import type { MarketplaceOrderRequest, MarketplaceOrderSettled } from './marketplace-settlement.js';
74
76
  import type { OrganisationView } from './organisation.js';
@@ -212,6 +214,17 @@ export interface CaysoClient {
212
214
  * refusals do. A refusal means nothing moved.
213
215
  */
214
216
  makeCommercialPayment(req: CommercialPaymentRequest): Promise<CommercialPaymentMade>;
217
+ /**
218
+ * Transfer unrestricted cents from the caller's own Citizen Wallet to another
219
+ * Citizen Wallet — or REPLAY a transfer already made under the same key (CAY-0088).
220
+ *
221
+ * The sending wallet is never named: it is the verified holder's own. A replay with
222
+ * the same key, payee and cents answers the movement already written. A refusal
223
+ * arrives as a thrown {@link CaysoApiError} whose `code` is a
224
+ * `CitizenTransferRefusal` member, one of the gate's codes, or a seam code. A
225
+ * refusal means nothing moved.
226
+ */
227
+ makeCitizenTransfer(req: CitizenTransferRequest): Promise<CitizenTransferMade>;
215
228
  /**
216
229
  * Read the citizen market as the caller sees it (CAY-0080 §1.4): the published
217
230
  * value and its band, both bounds per quote unit, the best bid and the best ask with
@@ -264,6 +277,18 @@ export interface CaysoClient {
264
277
  * means nothing moved.
265
278
  */
266
279
  settleMarketplaceOrder(req: MarketplaceOrderRequest): Promise<MarketplaceOrderSettled>;
280
+ /**
281
+ * Present the owner's standing authorisation, signed by the Instance key, under the
282
+ * owner's assertion (`present-standing-authorisation`, CMTY-0018 §1), and receive
283
+ * Cayso's signed acknowledgement for the Percayso Cloud.
284
+ *
285
+ * Safe to repeat: the authorisation's `jti` is its own key, and presenting the same
286
+ * authorisation again binds nothing twice. A refusal arrives as a thrown
287
+ * {@link CaysoApiError} whose `code` is a `StandingAuthorisationRefusal` member, one
288
+ * of the gate's codes, or a seam code, and means nothing was bound. Published before
289
+ * the route is served.
290
+ */
291
+ presentStandingAuthorisation(req: StandingAuthorisationRequest): Promise<StandingAuthorisationAcknowledged>;
267
292
  getVersion(): Promise<CaysoVersion>;
268
293
  }
269
294
  /**
package/dist/client.js CHANGED
@@ -221,6 +221,16 @@ function buildClient(options, internals) {
221
221
  idempotencyKey: req.idempotencyKey,
222
222
  });
223
223
  },
224
+ async makeCitizenTransfer(req) {
225
+ return requestJson({
226
+ method: ROUTES.citizenTransfer.method,
227
+ path: ROUTES.citizenTransfer.path,
228
+ // Safe to repeat only because of the key: a repeat replays the transfer.
229
+ idempotent: typeof req.idempotencyKey === 'string' && req.idempotencyKey.length > 0,
230
+ body: req,
231
+ idempotencyKey: req.idempotencyKey,
232
+ });
233
+ },
224
234
  async getCitizenMarket() {
225
235
  return requestJson({
226
236
  method: ROUTES.citizenMarket.method,
@@ -270,6 +280,16 @@ function buildClient(options, internals) {
270
280
  body: req,
271
281
  });
272
282
  },
283
+ async presentStandingAuthorisation(req) {
284
+ return requestJson({
285
+ method: ROUTES.standingAuthorisation.method,
286
+ path: ROUTES.standingAuthorisation.path,
287
+ // Safe to repeat: the authorisation's `jti` is its key, and the same
288
+ // authorisation presented again binds nothing twice.
289
+ idempotent: true,
290
+ body: req,
291
+ });
292
+ },
273
293
  async getPurchase(purchaseId) {
274
294
  return requestJson({
275
295
  method: ROUTES.purchaseResult.method,