@percayso-community/cayso-contracts 0.23.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.
Files changed (41) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +114 -0
  3. package/dist/assertion.d.ts +867 -0
  4. package/dist/assertion.js +63 -0
  5. package/dist/assurance.d.ts +353 -0
  6. package/dist/assurance.js +551 -0
  7. package/dist/citizen-market.d.ts +468 -0
  8. package/dist/citizen-market.js +16 -0
  9. package/dist/citizenship.d.ts +215 -0
  10. package/dist/citizenship.js +26 -0
  11. package/dist/client.d.ts +288 -0
  12. package/dist/client.js +370 -0
  13. package/dist/commercial-payment.d.ts +243 -0
  14. package/dist/commercial-payment.js +16 -0
  15. package/dist/contract-schema.generated.d.ts +14 -0
  16. package/dist/contract-schema.generated.js +3107 -0
  17. package/dist/errors.d.ts +171 -0
  18. package/dist/errors.js +28 -0
  19. package/dist/index.d.ts +621 -0
  20. package/dist/index.js +624 -0
  21. package/dist/marketplace-receipt.d.ts +306 -0
  22. package/dist/marketplace-receipt.js +116 -0
  23. package/dist/marketplace-settlement.d.ts +298 -0
  24. package/dist/marketplace-settlement.js +16 -0
  25. package/dist/money.d.ts +84 -0
  26. package/dist/money.js +34 -0
  27. package/dist/organisation.d.ts +60 -0
  28. package/dist/organisation.js +16 -0
  29. package/dist/purchase.d.ts +531 -0
  30. package/dist/purchase.js +107 -0
  31. package/dist/restriction.d.ts +262 -0
  32. package/dist/restriction.js +316 -0
  33. package/dist/routes.d.ts +155 -0
  34. package/dist/routes.js +128 -0
  35. package/dist/schema-types.d.ts +101 -0
  36. package/dist/schema-types.js +16 -0
  37. package/dist/schema.d.ts +24 -0
  38. package/dist/schema.js +16 -0
  39. package/dist/wallet.d.ts +92 -0
  40. package/dist/wallet.js +59 -0
  41. package/package.json +41 -0
@@ -0,0 +1,551 @@
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
+ import { ROUTES } from './routes.js';
17
+ // --- The profile's version ---------------------------------------------------
18
+ /**
19
+ * The version of the assurance profile this deployment speaks — the one an issuer
20
+ * stamps on a claim it mints, as `AssertionClaims.assuranceProfileVersion`.
21
+ *
22
+ * It names the standard as well as the version, because the standard is Cayso's
23
+ * own (see the file docblock) and a bare `1` would not say whose. Adopting an
24
+ * external scheme later, or changing what any method asserts, is a NEW version
25
+ * with a new identifier — never a redefinition of this one. Redefining a version
26
+ * in place is how a claim minted last month silently comes to mean something else.
27
+ *
28
+ * It is deliberately NOT the contract version: the contract version describes the
29
+ * wire surface and moves when a type changes, while this moves when the MEANING of
30
+ * evidence changes. Two clocks, two numbers, for the same reason the contract
31
+ * version is not the schema version (CONTEXT.md, Contract version).
32
+ */
33
+ export const ASSURANCE_PROFILE_VERSION = 'cayso-assurance-1';
34
+ /**
35
+ * Every assurance profile version this deployment recognises. A claim carrying
36
+ * anything else is refused ({@link assessAssurance}) rather than read as if it
37
+ * meant what this version means — fail closed, the posture every adapter in this
38
+ * tree takes (CLAUDE.md, Trust and authentication).
39
+ *
40
+ * A future version that is genuinely compatible is added here deliberately. It
41
+ * contains {@link ASSURANCE_PROFILE_VERSION} by construction: a deployment always
42
+ * recognises the profile it speaks.
43
+ */
44
+ export const RECOGNISED_ASSURANCE_PROFILE_VERSIONS = [ASSURANCE_PROFILE_VERSION];
45
+ /**
46
+ * What each method asserts. These statements are the contract: a consumer reads
47
+ * them to decide whether its evidence qualifies, and an issuer is bound by them
48
+ * when it mints.
49
+ */
50
+ export const EVIDENCE_STATEMENTS = {
51
+ 'dev-bypass': {
52
+ method: 'dev-bypass',
53
+ evidence: {
54
+ identityDocumentChecked: false,
55
+ biometricCaptured: false,
56
+ biometricComparedToDocumentPortrait: false,
57
+ },
58
+ asserts: 'Asserts NOTHING. No identity document was examined, no biometric was captured, and no ' +
59
+ 'comparison was performed. It marks an identity verified having checked nothing at all, ' +
60
+ 'and an identity marked this way keeps the method permanently.',
61
+ },
62
+ document: {
63
+ method: 'document',
64
+ evidence: {
65
+ identityDocumentChecked: true,
66
+ biometricCaptured: false,
67
+ biometricComparedToDocumentPortrait: false,
68
+ },
69
+ asserts: 'Asserts that a government-issued identity document was checked for validity. It asserts ' +
70
+ 'NO comparison between that document and the person presenting it: the document is sound, ' +
71
+ 'and nothing here says who was holding it.',
72
+ },
73
+ 'document-and-biometric': {
74
+ method: 'document-and-biometric',
75
+ evidence: {
76
+ identityDocumentChecked: true,
77
+ biometricCaptured: true,
78
+ biometricComparedToDocumentPortrait: true,
79
+ },
80
+ asserts: 'Asserts a valid government-issued identity document AND a live biometric capture compared ' +
81
+ "against that document's portrait, with a recorded match decision. The comparison is the " +
82
+ "whole of what distinguishes this from 'document' — a capture that was never compared is " +
83
+ "'document', not this.",
84
+ },
85
+ };
86
+ /**
87
+ * Every verification method, as a value. Derived from {@link EVIDENCE_STATEMENTS}
88
+ * rather than hand-listed: that table is a mapped type over the union, so its keys
89
+ * ARE the union and this list cannot drift from it. A hand-written array beside a
90
+ * union is a second home for the same fact, and the two disagree eventually.
91
+ */
92
+ export const VERIFICATION_METHODS = Object.keys(EVIDENCE_STATEMENTS);
93
+ /**
94
+ * Whether `evidence` supports asserting `method` — the issuer's check, and the
95
+ * whole of what makes an overclaiming claim REFUSABLE rather than merely
96
+ * discouraged.
97
+ *
98
+ * True when everything the method asserts actually happened. Evidence may exceed
99
+ * what the method asserts (a compared biometric supports `'document'` perfectly
100
+ * well); it may never fall short of it.
101
+ *
102
+ * The case this exists for: an issuer that checked a document and captured a face
103
+ * WITHOUT comparing them gets `false` for `'document-and-biometric'`. It must then
104
+ * mint `'document'` or mint nothing — it must not assert a match decision that was
105
+ * never recorded.
106
+ *
107
+ * `'dev-bypass'` asserts nothing, so nothing can fall short of it and this is
108
+ * vacuously true for it. That is correct and harmless: a method that claims
109
+ * nothing cannot overclaim, and it is refused at every gated operation anyway.
110
+ */
111
+ export function evidenceSupports(method, evidence) {
112
+ const asserted = EVIDENCE_STATEMENTS[method].evidence;
113
+ return ((!asserted.identityDocumentChecked || evidence.identityDocumentChecked) &&
114
+ (!asserted.biometricCaptured || evidence.biometricCaptured) &&
115
+ (!asserted.biometricComparedToDocumentPortrait || evidence.biometricComparedToDocumentPortrait));
116
+ }
117
+ /**
118
+ * The strongest method this evidence honestly supports, or `null` when it supports
119
+ * none. An issuer mints THIS, rather than the method it would like to have.
120
+ *
121
+ * `'dev-bypass'` is never returned. It is not a conclusion evidence supports — it
122
+ * is the marker of a verification that examined nothing, set by the bypass itself
123
+ * and never derived from evidence. Evidence that proves nothing yields `null`, and
124
+ * an issuer holding `null` has nothing to assert.
125
+ */
126
+ export function methodForEvidence(evidence) {
127
+ if (evidenceSupports('document-and-biometric', evidence))
128
+ return 'document-and-biometric';
129
+ if (evidenceSupports('document', evidence))
130
+ return 'document';
131
+ return null;
132
+ }
133
+ /**
134
+ * The acceptance policy: which methods each operation accepts. THE statement of
135
+ * record — a gate reads it rather than holding its own condition.
136
+ *
137
+ * `'dev-bypass'` is `'refused'` at every gated operation and appears in no
138
+ * accepted set (CAY-0021).
139
+ */
140
+ export const ASSURANCE_POLICY = {
141
+ // A holder reading their own balance. No bar: nothing moves, the data is the
142
+ // holder's own, and the wallet surface deliberately serves a development-bypassed
143
+ // identity its own wallet while refusing it a purchase. Reading is not a lesser
144
+ // form of buying — it is a different act, and this entry says so rather than
145
+ // pretending the bypass cleared something.
146
+ 'read-own-wallet': {
147
+ operation: 'read-own-wallet',
148
+ assurance: 'not-required',
149
+ rationale: 'A holder reading their own balance moves no value and reads nobody else, so no assurance ' +
150
+ 'bar is applied. Authentication is still required; that is a different gate.',
151
+ },
152
+ // Polling a purchase the caller started. Same reasoning: a read of one's own
153
+ // record. The purchase it reports on was itself gated when it was authorised.
154
+ 'read-purchase-result': {
155
+ operation: 'read-purchase-result',
156
+ assurance: 'not-required',
157
+ rationale: 'Polling the result of a purchase the caller already made moves no value; the assurance bar ' +
158
+ 'was applied when that purchase was authorised, and applying it again at the read would ' +
159
+ 'refuse a holder their own outcome.',
160
+ },
161
+ // The value-moving operation on this surface, and the bar the money gate reads.
162
+ 'authorise-purchase': {
163
+ operation: 'authorise-purchase',
164
+ assurance: 'required',
165
+ decisions: {
166
+ // Asserts nothing; permanently refused wherever "identity-verified" is meant.
167
+ 'dev-bypass': 'refused',
168
+ // A checked document, with no comparison to the person presenting it. Accepted:
169
+ // this is the bar the money gate has enforced since it was written, and this
170
+ // slice lifts that policy into the contract without changing it.
171
+ document: 'accepted',
172
+ 'document-and-biometric': 'accepted',
173
+ },
174
+ rationale: 'Buying moves value, so the identity must have been checked by something. A checked document ' +
175
+ 'is the standing bar; it is stated here rather than left as a condition inside the gate.',
176
+ },
177
+ // Terminating a purchase the holder started, before anything is captured
178
+ // (CAY-0032). REQUIRED, and at the same bar as authorising: cancelling releases
179
+ // reserved headroom and closes a money-path record for good, and a bar lower than
180
+ // the one that admitted the authorisation would let a weaker credential undo what
181
+ // a stronger one did. It is its OWN operation rather than a second use of
182
+ // 'authorise-purchase' because the acts differ — a credential minted so a
183
+ // confirmation page can finish a purchase should not thereby be able to end one —
184
+ // and because scope is checked per operation at the gate, so two acts sharing one
185
+ // key share one authorisation.
186
+ 'cancel-purchase': {
187
+ operation: 'cancel-purchase',
188
+ assurance: 'required',
189
+ decisions: {
190
+ // Asserts nothing; permanently refused wherever "identity-verified" is meant.
191
+ 'dev-bypass': 'refused',
192
+ document: 'accepted',
193
+ 'document-and-biometric': 'accepted',
194
+ },
195
+ rationale: 'Cancelling releases reserved headroom and closes a purchase for good, so it is money-grade: ' +
196
+ 'anything but the development bypass. The bar is deliberately the same as authorising — a ' +
197
+ 'credential that could not have bought cannot undo a purchase either.',
198
+ },
199
+ // Asking whether one's own citizenship has been provisioned. No bar, and for
200
+ // the same reason reading one's own wallet has none: the question moves no
201
+ // value, reads nobody else, and — since CAY-0031 — provisions nothing by being
202
+ // asked. It is deliberately NOT folded into 'provision-citizenship': putting
203
+ // the write's bar in front of the read would mean a citizen could not find out
204
+ // whether they had been provisioned without clearing the bar for provisioning
205
+ // them, which is the question answering itself.
206
+ 'read-own-citizenship': {
207
+ operation: 'read-own-citizenship',
208
+ assurance: 'not-required',
209
+ rationale: 'Asking whether your own citizenship has been provisioned moves no value, reads nobody ' +
210
+ 'else and writes nothing, so no assurance bar is applied. Authentication is still ' +
211
+ 'required; that is a different gate.',
212
+ },
213
+ // Binding a Percayso identity to a wallet for the first time, and granting the
214
+ // sign-up cents that arrive with it (CAY-0031). The policy below was decided in
215
+ // CAY-0029, before the route existed; CAY-0031 serves the route and reads this
216
+ // entry unchanged rather than reopening it.
217
+ 'provision-citizenship': {
218
+ operation: 'provision-citizenship',
219
+ assurance: 'required',
220
+ decisions: {
221
+ 'dev-bypass': 'refused',
222
+ // Accepted, and the reasoning is worth recording because the opposite is
223
+ // tempting. Provisioning IS the moment an identity becomes a holder, and it
224
+ // is the only moment at which "is this person the one the document
225
+ // describes" can be asked — so a bar requiring a recorded comparison is
226
+ // defensible on its merits. It is not taken, for a reason outside this
227
+ // file: the only issuer that exists asserts `'document'` and cannot assert
228
+ // more, so that bar would provision nobody. A wallet is a precondition of
229
+ // every other operation, so an unprovisionable citizen is not a narrow
230
+ // refusal — it closes the whole surface, including the purchase path this
231
+ // same table admits a checked document to. A gate stricter than the one
232
+ // behind it refuses everybody and protects no-one.
233
+ //
234
+ // Sybil farming of grants is the real risk this bar would have addressed,
235
+ // and it is addressed where the canon puts it — the Account Prohibitions —
236
+ // rather than by a bar that also excludes every honest citizen.
237
+ document: 'accepted',
238
+ 'document-and-biometric': 'accepted',
239
+ },
240
+ rationale: 'Provisioning binds an identity to a wallet once and every later act inherits that binding, ' +
241
+ 'so it is money-grade: anything but the development bypass. A recorded document-to-person ' +
242
+ 'comparison is not required, because a wallet is a precondition of every other operation ' +
243
+ 'and a bar no issuer can currently clear would provision nobody.',
244
+ },
245
+ // Binding an Organisation ID to an Organisation Wallet for the first time
246
+ // (CAY-0059). Its OWN operation rather than a second use of
247
+ // 'provision-citizenship': the two bind different holders from different facts —
248
+ // citizenship binds the authenticated `sub`, this binds the one Organisation ID
249
+ // the issuer attested — and scope is checked per operation at the gate, so one
250
+ // key covering both acts would let a credential minted to provision a citizen's
251
+ // own citizenship bring an Organisation Wallet into being.
252
+ //
253
+ // The bar is deliberately the SAME as provisioning a citizen, and for the same
254
+ // reason rather than a different one: the acting party is still a citizen, the
255
+ // only issuer that exists asserts 'document' and cannot assert more, and a bar
256
+ // no issuer can clear would provision nobody. It grants no cents — an
257
+ // organisation receives no sign-up grant — but it brings a wallet into being,
258
+ // and a wallet is the precondition of every later act against it.
259
+ 'provision-organisation': {
260
+ operation: 'provision-organisation',
261
+ assurance: 'required',
262
+ decisions: {
263
+ 'dev-bypass': 'refused',
264
+ document: 'accepted',
265
+ 'document-and-biometric': 'accepted',
266
+ },
267
+ rationale: 'Provisioning an organisation binds an Organisation ID to a wallet once and every later act ' +
268
+ 'against that wallet inherits the binding, so it is money-grade: anything but the development ' +
269
+ 'bypass. The bar matches provisioning a citizen because the acting party is a citizen and a ' +
270
+ 'stricter bar than the issuer can clear would provision nobody.',
271
+ },
272
+ // Binding a founder wallet, created at genesis and holding a founder's share of
273
+ // the founder pool, to the Citizen it was allocated to (CAY-0081). Its OWN
274
+ // operation rather than a second use of 'provision-citizenship': it delivers no
275
+ // sign-up grant, and a claimed founder must never receive one by any route.
276
+ //
277
+ // The bar is DELIBERATELY STRICTER than provisioning's, and the argument that
278
+ // keeps provisioning at 'document' does not carry over. A founder completes full
279
+ // identity proofing before claiming (CAY-0081 §9). 'Personally known' decides who
280
+ // is on the nomination list, not whether they are verified. Provisioning stays at
281
+ // 'document' because a bar no issuer can clear would provision nobody and close
282
+ // the whole surface. A bar no issuer can clear here closes the founder claim and
283
+ // nothing else. Until the issuer can assert a recorded document-to-person
284
+ // comparison, no founder can claim anywhere but a development stack, and that is
285
+ // correct. Whether 'document-and-biometric' is what Identity Platform's full
286
+ // proofing asserts is identity.platform#87's answer.
287
+ 'claim-founder-wallet': {
288
+ operation: 'claim-founder-wallet',
289
+ assurance: 'required',
290
+ decisions: {
291
+ // Asserts nothing; permanently refused wherever "identity-verified" is meant.
292
+ 'dev-bypass': 'refused',
293
+ // A checked document with no comparison to the person presenting it. Refused:
294
+ // it does not show the person claiming is the person the document describes,
295
+ // and the founder's allocation is the largest figure a claim can reach.
296
+ document: 'refused',
297
+ 'document-and-biometric': 'accepted',
298
+ },
299
+ rationale: 'Claiming binds a founder wallet, and the founder allocation it holds, to one Citizen for ' +
300
+ 'good, with no operator path to correct a mis-claim. So a founder must have completed full ' +
301
+ 'identity proofing: a checked document compared to the person presenting it. A bar the issuer ' +
302
+ 'cannot yet clear closes the founder claim and nothing else.',
303
+ },
304
+ // An Organisation paying a Citizen (CAY-0079 §1). Money-grade, on both routes and
305
+ // for different reasons: the mandate holder because value leaves the
306
+ // organisation for an individual, and the Authorised User because CAY-0037
307
+ // already requires the money-grade profile at every use of an allowance. It is an
308
+ // allowlist, so a future method is admissible only when somebody decides it here.
309
+ //
310
+ // The bar is the purchase path's rather than the founder claim's. The acting
311
+ // party is a citizen, the only issuer that exists asserts 'document', and a bar no
312
+ // issuer can clear would close the payment on every deployment. What limits how
313
+ // much can leave is the mandate, the allowance's limit and the Signatory /
314
+ // Authorised-User threshold, and none of them is an assurance question.
315
+ 'make-commercial-payment': {
316
+ operation: 'make-commercial-payment',
317
+ assurance: 'required',
318
+ decisions: {
319
+ // Asserts nothing; permanently refused wherever "identity-verified" is meant.
320
+ 'dev-bypass': 'refused',
321
+ document: 'accepted',
322
+ 'document-and-biometric': 'accepted',
323
+ },
324
+ rationale: 'A commercial payment moves value out of an organisation to an individual, so it is ' +
325
+ 'money-grade: anything but the development bypass. The bar applies on both routes, and to ' +
326
+ 'an Authorised User it applies again at every payment, not only when the allowance was granted.',
327
+ },
328
+ // The citizen market's three acts (CAY-0080 §7, Open decision Q). All three are
329
+ // money-grade and all three are allowlists, at the purchase path's bar: the acting
330
+ // party is a Citizen, and a bar no issuer can yet clear would shut the market on
331
+ // every deployment. What limits the market is its figures, which are canon and
332
+ // unset until the canon carries them, and none of them is an assurance question.
333
+ //
334
+ // During B7's exit window an excluded holder places no new quote, and the market
335
+ // withdraws their resting quotes; the buy-back stays open as an exit route. That is
336
+ // eligibility rather than assurance, and no exclusion signal reaches Cayso yet, so
337
+ // it is recorded here and enforced by nothing until the slice that delivers the
338
+ // signal (`citizen-market.ts`).
339
+ 'place-quote': {
340
+ operation: 'place-quote',
341
+ assurance: 'required',
342
+ decisions: {
343
+ // Asserts nothing; permanently refused wherever "identity-verified" is meant.
344
+ 'dev-bypass': 'refused',
345
+ document: 'accepted',
346
+ 'document-and-biometric': 'accepted',
347
+ },
348
+ rationale: 'Placing a quote offers cents for sterling or bids sterling for cents, and an ask reserves ' +
349
+ 'cents from entry, so it is money-grade: anything but the development bypass. During the ' +
350
+ 'exclusion exit window no new quote is placed, because entering the market is not an exit.',
351
+ },
352
+ // Gated at the SAME bar as placing, so that a lower-assurance credential cannot
353
+ // interfere with a holder's quotes. A holder who no longer clears it has their
354
+ // quotes withdrawn by the market rather than left stranded behind a bar they
355
+ // cannot reach — which is the market's act, not this operation.
356
+ 'withdraw-quote': {
357
+ operation: 'withdraw-quote',
358
+ assurance: 'required',
359
+ decisions: {
360
+ // Asserts nothing; permanently refused wherever "identity-verified" is meant.
361
+ 'dev-bypass': 'refused',
362
+ document: 'accepted',
363
+ 'document-and-biometric': 'accepted',
364
+ },
365
+ rationale: 'Withdrawing a quote releases reserved cents and changes what the book offers, so it is held ' +
366
+ 'to the same money-grade bar as placing one: a credential that could not have placed a quote ' +
367
+ 'cannot interfere with one either.',
368
+ },
369
+ // Selling unrestricted cents to the Treasury at the floor reference. It pays
370
+ // sterling out, so it is money-grade. It stays open during B7's exit window:
371
+ // Simon James named it, with transfer, as the way an excluded holder's
372
+ // unrestricted cents may leave.
373
+ 'request-buy-back': {
374
+ operation: 'request-buy-back',
375
+ assurance: 'required',
376
+ decisions: {
377
+ // Asserts nothing; permanently refused wherever "identity-verified" is meant.
378
+ 'dev-bypass': 'refused',
379
+ document: 'accepted',
380
+ 'document-and-biometric': 'accepted',
381
+ },
382
+ rationale: 'A buy-back pays sterling for cents the Treasury cancels, so it is money-grade: anything but ' +
383
+ 'the development bypass. It is an exit route during the exclusion exit window, and stays ' +
384
+ 'open to an excluded holder while the window lasts.',
385
+ },
386
+ // Reading the book. No bar, for read-own-wallet's reason: nothing moves, and what
387
+ // the read shows is the published value, the two bounds, the best bid and ask with
388
+ // their sizes, and the caller's own quotes. Placing and withdrawing are gated; a
389
+ // look at the book is a different act, and this entry says so rather than letting
390
+ // the read borrow the market's bar.
391
+ 'read-citizen-market': {
392
+ operation: 'read-citizen-market',
393
+ assurance: 'not-required',
394
+ rationale: 'Reading the book moves no value and names nobody: it shows the published value, the floor ' +
395
+ "reference, the best bid and ask with their sizes, and the caller's own quotes. " +
396
+ 'Authentication is still required; that is a different gate.',
397
+ },
398
+ // The marketplace status read (CAY-0083 §2). Read-scope, not money-grade, because
399
+ // nothing moves: it answers the signed receipt of the movement a key landed for the
400
+ // buyer's own wallet, or "no movement for this key", and writes no row. The bar was
401
+ // applied when the settlement it reports on was presented, as `read-purchase-result`
402
+ // leaves it at the authorisation. Holding the read to the money-grade bar would
403
+ // refuse a buyer the one answer that tells them whether their cents moved, which is
404
+ // the answer that stops a retry under a new key.
405
+ 'read-marketplace-receipt': {
406
+ operation: 'read-marketplace-receipt',
407
+ assurance: 'not-required',
408
+ rationale: "Asking what an idempotency key landed for the buyer's own wallet moves no value, reads " +
409
+ 'nobody else and writes nothing, so no assurance bar is applied; the bar was applied when ' +
410
+ 'the sale or refund was presented. Authentication is still required; that is a different gate.',
411
+ },
412
+ // The two marketplace operations Piper presents Store Platform's signed decisions
413
+ // under (CAY-0083 §3). Both money-grade, and both allowlists (CAY-0029), so a future
414
+ // method is admissible only when somebody decides it here.
415
+ //
416
+ // The bar is the purchase path's rather than the founder claim's, for the reason
417
+ // the commercial payment's is: the acting party is a Citizen, the only issuer that
418
+ // exists asserts 'document', and a bar no issuer can clear would close the
419
+ // marketplace on every deployment. What limits a sale is Store Platform's signed
420
+ // order and the buyer's unrestricted cents, and what limits a refund is Store
421
+ // Platform's signed decision and its refund policy. None of those is an assurance
422
+ // question.
423
+ 'settle-marketplace-order': {
424
+ operation: 'settle-marketplace-order',
425
+ assurance: 'required',
426
+ decisions: {
427
+ // Asserts nothing; permanently refused wherever "identity-verified" is meant.
428
+ 'dev-bypass': 'refused',
429
+ document: 'accepted',
430
+ 'document-and-biometric': 'accepted',
431
+ },
432
+ rationale: "Settling a marketplace sale moves the buyer's cents to a publisher and the Treasury, so it " +
433
+ 'is money-grade: anything but the development bypass. The signed order says what is bought ' +
434
+ 'and who is paid; the assertion says who is buying, and it is held to this bar.',
435
+ },
436
+ // A refund draws a publisher's cents back to the buyer on Store Platform's
437
+ // authority. The buyer is the payee, and their assertion shows who is present, so
438
+ // it is held to the SAME bar as the sale: a credential that could not have bought
439
+ // cannot be refunded either, and a lower bar here would let a weaker credential
440
+ // draw cents out of a publisher's wallet that a stronger one paid in.
441
+ 'settle-marketplace-refund': {
442
+ operation: 'settle-marketplace-refund',
443
+ assurance: 'required',
444
+ decisions: {
445
+ // Asserts nothing; permanently refused wherever "identity-verified" is meant.
446
+ 'dev-bypass': 'refused',
447
+ document: 'accepted',
448
+ 'document-and-biometric': 'accepted',
449
+ },
450
+ rationale: "Settling a marketplace refund draws cents from a publisher's wallet and the Treasury to the " +
451
+ 'buyer, so it is money-grade: anything but the development bypass. The authority is Store ' +
452
+ "Platform's signed refund decision; the assertion shows the buyer is present, and it is held " +
453
+ 'to the same bar as the sale it refunds.',
454
+ },
455
+ };
456
+ /**
457
+ * Every operation, as a value. Derived from {@link ASSURANCE_POLICY} for the same
458
+ * reason {@link VERIFICATION_METHODS} is derived from the statements: the mapped
459
+ * type makes the table's keys the union, so this cannot drift.
460
+ */
461
+ export const CONTRACT_OPERATIONS = Object.keys(ASSURANCE_POLICY);
462
+ /**
463
+ * Which operation each route on the contract surface exercises, or `null` for a
464
+ * route that carries no identity at all (the public version handshake — you cannot
465
+ * be assured before you can negotiate the contract).
466
+ *
467
+ * Adding a route to `ROUTES` is a compile error here until it is mapped, so "every
468
+ * operation on this surface is classified" is mechanical rather than asserted.
469
+ * The reverse direction is deliberately not total — an operation may be
470
+ * classified before it has a route, which is how `'provision-citizenship'` was
471
+ * decided here and then served, unchanged, by CAY-0031.
472
+ *
473
+ * The two citizenship routes map to two DIFFERENT operations, which is the whole
474
+ * reason they are two paths: one path carries one scope requirement at CAY-0030's
475
+ * gate, so a read sharing a path with a write would share its scope too.
476
+ */
477
+ export const OPERATION_FOR_ROUTE = {
478
+ wallet: 'read-own-wallet',
479
+ citizenship: 'read-own-citizenship',
480
+ citizenshipProvision: 'provision-citizenship',
481
+ citizenshipFounderClaim: 'claim-founder-wallet',
482
+ organisationProvision: 'provision-organisation',
483
+ purchaseAuthorise: 'authorise-purchase',
484
+ purchaseResult: 'read-purchase-result',
485
+ purchaseCancel: 'cancel-purchase',
486
+ commercialPayment: 'make-commercial-payment',
487
+ citizenMarket: 'read-citizen-market',
488
+ placeQuote: 'place-quote',
489
+ withdrawQuote: 'withdraw-quote',
490
+ requestBuyBack: 'request-buy-back',
491
+ version: null,
492
+ };
493
+ /**
494
+ * The closed set of methods an operation accepts, or `null` when it applies no
495
+ * assurance bar. `null` and `[]` are different answers and the caller must tell
496
+ * them apart: `[]` would mean "accepts nothing", which is true of no operation
497
+ * here.
498
+ *
499
+ * Derived from the decisions, never authored beside them — one table, read two
500
+ * ways, so an accepted set cannot disagree with the decision it came from.
501
+ */
502
+ export function acceptedMethods(operation) {
503
+ const policy = ASSURANCE_POLICY[operation];
504
+ if (policy.assurance === 'not-required')
505
+ return null;
506
+ return VERIFICATION_METHODS.filter((method) => policy.decisions[method] === 'accepted');
507
+ }
508
+ /** The single accepted value — one object, since it carries nothing. */
509
+ const ACCEPTED = { accepted: true };
510
+ /**
511
+ * May this claim perform this operation, on assurance grounds? The verifier half
512
+ * of the profile, and the function a gate calls instead of writing a condition.
513
+ *
514
+ * It reads exactly three fields — the profile version, the status and the method —
515
+ * and nothing else. Not `mandateFor`: whether a caller may act for a wallet is
516
+ * authority, not assurance, and the Signatory / Authorised-User threshold is a
517
+ * canon figure the gate for that operation reads from configuration. Not the reserved
518
+ * claims. Not the issuer or the key: whether the claim can be TRUSTED is the trust
519
+ * envelope's question, a separate gap, and a caller must answer it separately.
520
+ * This function assumes it has already been answered.
521
+ *
522
+ * The order of the checks is the order of the questions: do we understand this
523
+ * profile at all, does this operation apply a bar, was the identity verified, did
524
+ * it name a method, is that method accepted here.
525
+ */
526
+ export function assessAssurance(operation, claims) {
527
+ // An unrecognised profile is refused before anything else and whatever the
528
+ // operation: a method name minted under a profile we do not hold means whatever
529
+ // that profile said it meant, and reading it as if it meant this one's is the
530
+ // failure the version exists to prevent.
531
+ if (!RECOGNISED_ASSURANCE_PROFILE_VERSIONS.includes(claims.assuranceProfileVersion)) {
532
+ return { accepted: false, refusalReason: 'assurance_profile_unrecognised' };
533
+ }
534
+ const policy = ASSURANCE_POLICY[operation];
535
+ if (policy.assurance === 'not-required')
536
+ return ACCEPTED;
537
+ if (claims.verificationStatus !== 'verified') {
538
+ return { accepted: false, refusalReason: 'identity_unverified' };
539
+ }
540
+ // Verified while naming no method is an incoherent claim, and it is exactly the
541
+ // trap CAY-0021 names in a different guise: asking for "verified" and inheriting
542
+ // whatever that includes. There is no evidence to assess, so there is nothing to
543
+ // accept.
544
+ if (claims.verificationMethod === null) {
545
+ return { accepted: false, refusalReason: 'verification_method_absent' };
546
+ }
547
+ if (policy.decisions[claims.verificationMethod] !== 'accepted') {
548
+ return { accepted: false, refusalReason: 'verification_method_not_accepted' };
549
+ }
550
+ return ACCEPTED;
551
+ }