@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.
- package/LICENSE +201 -0
- package/README.md +114 -0
- package/dist/assertion.d.ts +867 -0
- package/dist/assertion.js +63 -0
- package/dist/assurance.d.ts +353 -0
- package/dist/assurance.js +551 -0
- package/dist/citizen-market.d.ts +468 -0
- package/dist/citizen-market.js +16 -0
- package/dist/citizenship.d.ts +215 -0
- package/dist/citizenship.js +26 -0
- package/dist/client.d.ts +288 -0
- package/dist/client.js +370 -0
- package/dist/commercial-payment.d.ts +243 -0
- package/dist/commercial-payment.js +16 -0
- package/dist/contract-schema.generated.d.ts +14 -0
- package/dist/contract-schema.generated.js +3107 -0
- package/dist/errors.d.ts +171 -0
- package/dist/errors.js +28 -0
- package/dist/index.d.ts +621 -0
- package/dist/index.js +624 -0
- package/dist/marketplace-receipt.d.ts +306 -0
- package/dist/marketplace-receipt.js +116 -0
- package/dist/marketplace-settlement.d.ts +298 -0
- package/dist/marketplace-settlement.js +16 -0
- package/dist/money.d.ts +84 -0
- package/dist/money.js +34 -0
- package/dist/organisation.d.ts +60 -0
- package/dist/organisation.js +16 -0
- package/dist/purchase.d.ts +531 -0
- package/dist/purchase.js +107 -0
- package/dist/restriction.d.ts +262 -0
- package/dist/restriction.js +316 -0
- package/dist/routes.d.ts +155 -0
- package/dist/routes.js +128 -0
- package/dist/schema-types.d.ts +101 -0
- package/dist/schema-types.js +16 -0
- package/dist/schema.d.ts +24 -0
- package/dist/schema.js +16 -0
- package/dist/wallet.d.ts +92 -0
- package/dist/wallet.js +59 -0
- package/package.json +41 -0
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The marketplace receipt: what Cayso signs about a marketplace sale or refund,
|
|
3
|
+
* and the exact bytes the signature is computed over (CAY-0083 §1).
|
|
4
|
+
*
|
|
5
|
+
* Store Platform grants a paid entitlement only on a receipt it verifies itself,
|
|
6
|
+
* for the exact order digest (Store's invariant 8). Piper carries the receipt from
|
|
7
|
+
* Cayso to Store Platform (Store decision V), so the receipt has to verify with no
|
|
8
|
+
* call back to Cayso: it is a {@link MarketplaceReceiptStatement} and a signature
|
|
9
|
+
* over {@link encodeMarketplaceReceiptStatement} of it, made with a key Cayso alone
|
|
10
|
+
* holds and that signs receipts only.
|
|
11
|
+
*
|
|
12
|
+
* ## What is signed
|
|
13
|
+
*
|
|
14
|
+
* CAY-0083 §1's list, and nothing else: the movement id, the order digest, the
|
|
15
|
+
* idempotency key, the sale's kind, each leg as a wallet id and a whole number of
|
|
16
|
+
* cents, the allocation-policy version and the time the movement was written. The
|
|
17
|
+
* statement adds three things the list implies. `movementKind` says which of the
|
|
18
|
+
* ledger's acts the movement is, and so which way its legs run. `buyerWalletId`
|
|
19
|
+
* names the other end of every leg. `keyId` names the key the signature was made
|
|
20
|
+
* with, so selecting the key is part of what is signed.
|
|
21
|
+
*
|
|
22
|
+
* **A receipt names wallet ids only, never a Percayso identity** (CAY-0006,
|
|
23
|
+
* CAY-0054). There is no field a subject could be put in.
|
|
24
|
+
*
|
|
25
|
+
* ## Four statements, and a type that cannot express a fifth
|
|
26
|
+
*
|
|
27
|
+
* A sale is a `marketplace_settlement` when a third party published the release and
|
|
28
|
+
* a plain `charge` when the house publisher did (CAY-0082 §1, §2). A refund is a
|
|
29
|
+
* `marketplace_refund` of either (§5), and names the order digest of the sale it
|
|
30
|
+
* refunds. Each is its own interface, discriminated by `movementKind` and
|
|
31
|
+
* `saleKind` together:
|
|
32
|
+
*
|
|
33
|
+
* - a third-party statement has a publisher leg and a Community leg;
|
|
34
|
+
* - a house-publisher statement has a Community leg and **no publisher leg field
|
|
35
|
+
* at all**, so a house-publisher sale that pays a publisher is not a value of
|
|
36
|
+
* this type; and
|
|
37
|
+
* - there is one publisher leg or none, never a list, because a paid sale has
|
|
38
|
+
* exactly one publisher (CAY-0082 §8) and a split among publishers is not
|
|
39
|
+
* designed.
|
|
40
|
+
*
|
|
41
|
+
* On a sale every leg runs from the buyer's wallet to the leg's wallet. On a refund
|
|
42
|
+
* every leg runs from the leg's wallet to the buyer's. The Community leg's wallet is
|
|
43
|
+
* the Treasury, named by its wallet id like any other.
|
|
44
|
+
*
|
|
45
|
+
* ## The bytes that are signed
|
|
46
|
+
*
|
|
47
|
+
* {@link encodeMarketplaceReceiptStatement} is the one encoding, and the signer and
|
|
48
|
+
* every verifier call it: two implementations of "the same JSON" are two encodings
|
|
49
|
+
* that will one day disagree by a space, and a verifier that disagrees with its
|
|
50
|
+
* signer by a space refuses every receipt. It is the statement's fields in the order
|
|
51
|
+
* the interfaces declare them, as UTF-8 JSON with no whitespace.
|
|
52
|
+
*
|
|
53
|
+
* It is published here, where CAY-0072 keeps the assertion's encoding out, because
|
|
54
|
+
* the two cases differ in who owns the bytes. Percayso ID mints an assertion and
|
|
55
|
+
* owns its format; Cayso mints a receipt and owns this one, and writing it once
|
|
56
|
+
* where both sides import it is what stops the signer and Store Platform drifting.
|
|
57
|
+
* It pins no cryptographic dependency: it is JSON and a UTF-8 encoder.
|
|
58
|
+
*
|
|
59
|
+
* **The key set is closed, and the encoder refuses a statement that is not exactly
|
|
60
|
+
* one of the four.** A house-publisher statement carrying a `publisherLeg`, a leg
|
|
61
|
+
* carrying a field beside its wallet and cents, or any key a statement does not
|
|
62
|
+
* name is refused rather than dropped. Dropping it would leave a verifier holding an
|
|
63
|
+
* object with a field nobody signed, and a consumer that read it would be reading
|
|
64
|
+
* whatever the transport added.
|
|
65
|
+
*
|
|
66
|
+
* ## The algorithm and the key
|
|
67
|
+
*
|
|
68
|
+
* {@link MARKETPLACE_RECEIPT_SIGNATURE_ALGORITHM} is ECDSA over P-256 with SHA-256,
|
|
69
|
+
* written with its JOSE name, `ES256`. The signature is the fixed-length `r || s`
|
|
70
|
+
* concatenation, 64 bytes, as unpadded base64url, and never DER. That is the
|
|
71
|
+
* algorithm and encoding Store Platform already verifies its release statements
|
|
72
|
+
* with (Store `STO-0026`, `STO-0033`), so Store Platform checks a receipt with the
|
|
73
|
+
* code it already has (CAY-0083 §1).
|
|
74
|
+
*
|
|
75
|
+
* A {@link MarketplaceReceiptKeyId} is the RFC 7638 SHA-256 thumbprint of the key's
|
|
76
|
+
* public half as a P-256 JSON Web Key, unpadded base64url. Derived rather than
|
|
77
|
+
* minted, so a key id cannot be re-pointed at a different key.
|
|
78
|
+
*
|
|
79
|
+
* ## The status read: a receipt or "no movement", never "pending"
|
|
80
|
+
*
|
|
81
|
+
* The read exists for one case: the settle call went out and its answer did not
|
|
82
|
+
* come back (CAY-0083 §2). Piper asks it under the buyer's assertion
|
|
83
|
+
* (`read-marketplace-receipt`, read-scope in `ASSURANCE_POLICY`), and Store
|
|
84
|
+
* Platform never does. {@link MarketplaceReceiptReadResponse} has exactly two
|
|
85
|
+
* answers about the key and one refusal:
|
|
86
|
+
*
|
|
87
|
+
* - **`receipt`** — the signed {@link MarketplaceReceipt} of the movement the key
|
|
88
|
+
* landed, the same receipt the lost answer carried;
|
|
89
|
+
* - **`no-movement`** — nothing was written under this key for this wallet. It
|
|
90
|
+
* means **retry with the same key, never a new one**: the first attempt may
|
|
91
|
+
* still be in flight, and the same key lands it exactly once either way; and
|
|
92
|
+
* - **`refused`** — the question could not be asked, with one reason from
|
|
93
|
+
* {@link MarketplaceReceiptReadRefusal}. It is not a third answer about the
|
|
94
|
+
* key, and a caller must never read it as "no movement".
|
|
95
|
+
*
|
|
96
|
+
* **There is no `pending`, and no member could carry one.** A movement is written
|
|
97
|
+
* or it is not (CAY-0023): there is no status column to report, so a read that
|
|
98
|
+
* said "in progress" would be inventing a state the ledger does not have.
|
|
99
|
+
*
|
|
100
|
+
* **The paying wallet is the buyer's own, resolved from the verified subject**
|
|
101
|
+
* through the holders mapping, as settlement resolves it (CAY-0082 §1), and so
|
|
102
|
+
* {@link MarketplaceReceiptReadRequest} names no wallet. CAY-0083 keys the read by
|
|
103
|
+
* the paying wallet and the idempotency key; a request field for the wallet would
|
|
104
|
+
* be a claim about who the caller is, which Cayso never trusts (CAY-0021), and a
|
|
105
|
+
* place another holder's wallet id could be written to read their receipt. With no
|
|
106
|
+
* such field, a read of somebody else's receipt is not a request this type can
|
|
107
|
+
* express. When the subject has no live binding the read is refused rather than
|
|
108
|
+
* answered `no-movement`: the wallet cannot be resolved, so whether a movement
|
|
109
|
+
* exists is not known, and "no movement" would be a guess that invites a retry.
|
|
110
|
+
*
|
|
111
|
+
* The answer is one receipt, never a list, so it rests on a key naming at most one
|
|
112
|
+
* movement for one paying wallet. CAY-0082 §4 holds a sale's key to that across
|
|
113
|
+
* both sale kinds; a refund carries its own key, never the sale's, and the slice
|
|
114
|
+
* that stores a refund has to hold it to the same rule beside the sale's.
|
|
115
|
+
*
|
|
116
|
+
* **A read writes no row of its own and moves nothing**: no audit row, and nothing
|
|
117
|
+
* recording which key was asked. The one record it leaves is the one every verified
|
|
118
|
+
* assertion leaves, its use, which is what refuses a replay (CAY-0030). A holder
|
|
119
|
+
* reading their own record is not surveilled, as a holder reading their own balance
|
|
120
|
+
* is not.
|
|
121
|
+
*
|
|
122
|
+
* Every unconfigured seam the read meets is a `503` and is not in the refusal
|
|
123
|
+
* union, among them CAY-0083 §4's empty registry of Store Platform order keys,
|
|
124
|
+
* which closes all three marketplace operations. The spelling of that one is the
|
|
125
|
+
* registry's slice's to publish. A malformed key is `invalid_request`.
|
|
126
|
+
*
|
|
127
|
+
* ## What is not here
|
|
128
|
+
*
|
|
129
|
+
* No key material of any kind, and no key set: which receipt keys Store Platform
|
|
130
|
+
* trusts is Store Platform's configuration, empty by default, and how Cayso holds,
|
|
131
|
+
* rotates and revokes its key is Cayso's own, in `apps/api`. No verifier: Store
|
|
132
|
+
* Platform verifies. And no figure: a leg carries the cents the movement moved, and
|
|
133
|
+
* the allocation between publisher and Community is Store Platform's canon's.
|
|
134
|
+
*/
|
|
135
|
+
import type { Cents, Millis } from './wallet.js';
|
|
136
|
+
/**
|
|
137
|
+
* The algorithm every marketplace receipt is signed with: ECDSA P-256 with SHA-256,
|
|
138
|
+
* `r || s` encoded (JOSE `ES256`), CAY-0083 §1.
|
|
139
|
+
*/
|
|
140
|
+
export declare const MARKETPLACE_RECEIPT_SIGNATURE_ALGORITHM: "ES256";
|
|
141
|
+
/**
|
|
142
|
+
* The type of {@link MARKETPLACE_RECEIPT_SIGNATURE_ALGORITHM}. Written as the
|
|
143
|
+
* literal because the published schema describes literals and not `typeof`, and a
|
|
144
|
+
* test pins the two together.
|
|
145
|
+
*/
|
|
146
|
+
export type MarketplaceReceiptSignatureAlgorithm = 'ES256';
|
|
147
|
+
/**
|
|
148
|
+
* The id of one receipt signing key: the RFC 7638 thumbprint of its public half.
|
|
149
|
+
* Opaque to a consumer: compare it, never parse it. It names the key and is never
|
|
150
|
+
* the key.
|
|
151
|
+
*/
|
|
152
|
+
export type MarketplaceReceiptKeyId = string;
|
|
153
|
+
/**
|
|
154
|
+
* Who published the release a sale was for. A third party's sale is a marketplace
|
|
155
|
+
* settlement; the house publisher's is a charge (CAY-0082 §1, §2). Cayso decides it
|
|
156
|
+
* from Store Platform's signed order and never from the caller's word.
|
|
157
|
+
*/
|
|
158
|
+
export type MarketplaceSaleKind = 'third-party' | 'house-publisher';
|
|
159
|
+
/**
|
|
160
|
+
* One leg of a receipt: the wallet at the far end from the buyer, and the whole
|
|
161
|
+
* cents that crossed between them. On a sale the cents arrived there; on a refund
|
|
162
|
+
* they were drawn from there.
|
|
163
|
+
*/
|
|
164
|
+
export interface MarketplaceReceiptLeg {
|
|
165
|
+
readonly walletId: string;
|
|
166
|
+
readonly cents: Cents;
|
|
167
|
+
}
|
|
168
|
+
/** A third-party sale: one `marketplace_settlement`, paying the publisher and the Treasury. */
|
|
169
|
+
export interface MarketplaceSettlementReceiptStatement {
|
|
170
|
+
readonly movementKind: 'marketplace_settlement';
|
|
171
|
+
readonly saleKind: 'third-party';
|
|
172
|
+
readonly movementId: string;
|
|
173
|
+
/** The immutable digest of Store Platform's signed order this sale settled. */
|
|
174
|
+
readonly orderDigest: string;
|
|
175
|
+
/** The checkout's idempotency key, unique per paying wallet (CAY-0082 §4). */
|
|
176
|
+
readonly idempotencyKey: string;
|
|
177
|
+
/** The paying wallet. */
|
|
178
|
+
readonly buyerWalletId: string;
|
|
179
|
+
/** The publisher's share, to the publisher's Citizen Wallet. */
|
|
180
|
+
readonly publisherLeg: MarketplaceReceiptLeg;
|
|
181
|
+
/** The Community's share, straight to the Treasury. */
|
|
182
|
+
readonly communityLeg: MarketplaceReceiptLeg;
|
|
183
|
+
/** The allocation-policy version Store Platform's signed order named. */
|
|
184
|
+
readonly allocationPolicyVersion: string;
|
|
185
|
+
/** When the movement was written. */
|
|
186
|
+
readonly writtenAt: Millis;
|
|
187
|
+
/** The key the signature was made with. */
|
|
188
|
+
readonly keyId: MarketplaceReceiptKeyId;
|
|
189
|
+
}
|
|
190
|
+
/** A house-publisher sale: one `charge` to the Treasury, and no publisher leg. */
|
|
191
|
+
export interface HousePublisherChargeReceiptStatement {
|
|
192
|
+
readonly movementKind: 'charge';
|
|
193
|
+
readonly saleKind: 'house-publisher';
|
|
194
|
+
readonly movementId: string;
|
|
195
|
+
readonly orderDigest: string;
|
|
196
|
+
readonly idempotencyKey: string;
|
|
197
|
+
/** The paying wallet. */
|
|
198
|
+
readonly buyerWalletId: string;
|
|
199
|
+
/** The whole price, to the Treasury. */
|
|
200
|
+
readonly communityLeg: MarketplaceReceiptLeg;
|
|
201
|
+
readonly allocationPolicyVersion: string;
|
|
202
|
+
readonly writtenAt: Millis;
|
|
203
|
+
readonly keyId: MarketplaceReceiptKeyId;
|
|
204
|
+
}
|
|
205
|
+
/** The refund of a third-party sale: each leg drawn back from where it landed, to the buyer. */
|
|
206
|
+
export interface ThirdPartyRefundReceiptStatement {
|
|
207
|
+
readonly movementKind: 'marketplace_refund';
|
|
208
|
+
readonly saleKind: 'third-party';
|
|
209
|
+
readonly movementId: string;
|
|
210
|
+
/** The order digest of the sale this refunds. */
|
|
211
|
+
readonly orderDigest: string;
|
|
212
|
+
/** The refund's own idempotency key, never the sale's. */
|
|
213
|
+
readonly idempotencyKey: string;
|
|
214
|
+
/** The wallet refunded, which is the wallet that paid for the sale. */
|
|
215
|
+
readonly buyerWalletId: string;
|
|
216
|
+
/** The publisher's share, drawn from the publisher's wallet. */
|
|
217
|
+
readonly publisherLeg: MarketplaceReceiptLeg;
|
|
218
|
+
/** The Community's share, drawn from the Treasury. */
|
|
219
|
+
readonly communityLeg: MarketplaceReceiptLeg;
|
|
220
|
+
/** The allocation-policy version of the sale refunded. */
|
|
221
|
+
readonly allocationPolicyVersion: string;
|
|
222
|
+
readonly writtenAt: Millis;
|
|
223
|
+
readonly keyId: MarketplaceReceiptKeyId;
|
|
224
|
+
}
|
|
225
|
+
/** The refund of a house-publisher sale: the whole, drawn from the Treasury, to the buyer. */
|
|
226
|
+
export interface HousePublisherRefundReceiptStatement {
|
|
227
|
+
readonly movementKind: 'marketplace_refund';
|
|
228
|
+
readonly saleKind: 'house-publisher';
|
|
229
|
+
readonly movementId: string;
|
|
230
|
+
/** The order digest of the sale this refunds. */
|
|
231
|
+
readonly orderDigest: string;
|
|
232
|
+
/** The refund's own idempotency key, never the sale's. */
|
|
233
|
+
readonly idempotencyKey: string;
|
|
234
|
+
readonly buyerWalletId: string;
|
|
235
|
+
/** The whole, drawn from the Treasury. */
|
|
236
|
+
readonly communityLeg: MarketplaceReceiptLeg;
|
|
237
|
+
readonly allocationPolicyVersion: string;
|
|
238
|
+
readonly writtenAt: Millis;
|
|
239
|
+
readonly keyId: MarketplaceReceiptKeyId;
|
|
240
|
+
}
|
|
241
|
+
/** What Cayso signs about one marketplace movement. Exactly one of the four. */
|
|
242
|
+
export type MarketplaceReceiptStatement = MarketplaceSettlementReceiptStatement | HousePublisherChargeReceiptStatement | ThirdPartyRefundReceiptStatement | HousePublisherRefundReceiptStatement;
|
|
243
|
+
/** A receipt: the statement, and Cayso's signature over its encoding. */
|
|
244
|
+
export interface MarketplaceReceipt {
|
|
245
|
+
readonly statement: MarketplaceReceiptStatement;
|
|
246
|
+
/**
|
|
247
|
+
* The {@link MARKETPLACE_RECEIPT_SIGNATURE_ALGORITHM} signature over
|
|
248
|
+
* {@link encodeMarketplaceReceiptStatement}`(statement)`: `r || s`, 64 bytes, as
|
|
249
|
+
* unpadded base64url.
|
|
250
|
+
*/
|
|
251
|
+
readonly signature: string;
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Piper asking, for the buyer, what one idempotency key landed (CAY-0083 §2). The
|
|
255
|
+
* paying wallet is the verified subject's own and is never named here: see the file
|
|
256
|
+
* docblock.
|
|
257
|
+
*/
|
|
258
|
+
export interface MarketplaceReceiptReadRequest {
|
|
259
|
+
/** The key the sale or refund was presented under, exactly as it was presented. */
|
|
260
|
+
readonly idempotencyKey: string;
|
|
261
|
+
}
|
|
262
|
+
/** The key landed a movement: its signed receipt. */
|
|
263
|
+
export interface MarketplaceReceiptFound {
|
|
264
|
+
readonly outcome: 'receipt';
|
|
265
|
+
readonly receipt: MarketplaceReceipt;
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Nothing was written under this key for this wallet. Retry with the SAME key,
|
|
269
|
+
* never a new one: the first attempt may still be in flight, and the same key lands
|
|
270
|
+
* it exactly once either way. It is not "pending": there is no pending movement.
|
|
271
|
+
*/
|
|
272
|
+
export interface MarketplaceNoMovement {
|
|
273
|
+
readonly outcome: 'no-movement';
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Why the read could not be asked. Closed, and a refusal is never a statement about
|
|
277
|
+
* the key: a caller must not read one as "no movement".
|
|
278
|
+
*/
|
|
279
|
+
export type MarketplaceReceiptReadRefusal =
|
|
280
|
+
/**
|
|
281
|
+
* No wallet is bound to the verified subject now, for example after erasure, so
|
|
282
|
+
* there is no paying wallet to ask about and whether the key landed is not known.
|
|
283
|
+
*/
|
|
284
|
+
'wallet_has_no_live_binding';
|
|
285
|
+
/** The read was refused, with one reason. Nothing was written. */
|
|
286
|
+
export interface MarketplaceReceiptReadRefused {
|
|
287
|
+
readonly outcome: 'refused';
|
|
288
|
+
readonly error: MarketplaceReceiptReadRefusal;
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* The ONE declared answer to the status read, discriminated by `outcome`: a receipt,
|
|
292
|
+
* "no movement for this key", or a refusal. Never "pending".
|
|
293
|
+
*/
|
|
294
|
+
export type MarketplaceReceiptReadResponse = MarketplaceReceiptFound | MarketplaceNoMovement | MarketplaceReceiptReadRefused;
|
|
295
|
+
/**
|
|
296
|
+
* The exact bytes a marketplace receipt's signature is computed over.
|
|
297
|
+
*
|
|
298
|
+
* Builds a fresh object from the statement's named fields, in a fixed order, so the
|
|
299
|
+
* encoding depends on nothing but their values: not on the order a caller wrote
|
|
300
|
+
* them in. THROWS a `TypeError` for anything that is not exactly one of the four
|
|
301
|
+
* statements — a pair of kinds that is not admissible, a missing or extra field, a
|
|
302
|
+
* string field that is not a string, or cents or a time that is not a safe whole
|
|
303
|
+
* number no less than zero. A statement arrives from a transport before it is typed,
|
|
304
|
+
* and a value of the wrong kind would still encode, to something nobody signed.
|
|
305
|
+
*/
|
|
306
|
+
export declare function encodeMarketplaceReceiptStatement(statement: MarketplaceReceiptStatement): Uint8Array<ArrayBuffer>;
|
|
@@ -0,0 +1,116 @@
|
|
|
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 algorithm every marketplace receipt is signed with: ECDSA P-256 with SHA-256,
|
|
18
|
+
* `r || s` encoded (JOSE `ES256`), CAY-0083 §1.
|
|
19
|
+
*/
|
|
20
|
+
export const MARKETPLACE_RECEIPT_SIGNATURE_ALGORITHM = 'ES256';
|
|
21
|
+
// --- The encoding ------------------------------------------------------------
|
|
22
|
+
/** The statement's own fields, in the order they are encoded, before and after the legs. */
|
|
23
|
+
const LEADING_FIELDS = [
|
|
24
|
+
'movementKind',
|
|
25
|
+
'saleKind',
|
|
26
|
+
'movementId',
|
|
27
|
+
'orderDigest',
|
|
28
|
+
'idempotencyKey',
|
|
29
|
+
'buyerWalletId',
|
|
30
|
+
];
|
|
31
|
+
const TRAILING_FIELDS = ['allocationPolicyVersion', 'writtenAt', 'keyId'];
|
|
32
|
+
/** Every string field, each of which must be a string when it arrives. */
|
|
33
|
+
const STRING_FIELDS = [
|
|
34
|
+
'movementId',
|
|
35
|
+
'orderDigest',
|
|
36
|
+
'idempotencyKey',
|
|
37
|
+
'buyerWalletId',
|
|
38
|
+
'allocationPolicyVersion',
|
|
39
|
+
'keyId',
|
|
40
|
+
];
|
|
41
|
+
/**
|
|
42
|
+
* The four admissible pairs of `movementKind` and `saleKind`, and whether each has a
|
|
43
|
+
* publisher leg. A pair not listed here is not a receipt.
|
|
44
|
+
*/
|
|
45
|
+
const ADMISSIBLE = [
|
|
46
|
+
{ movementKind: 'marketplace_settlement', saleKind: 'third-party', publisherLeg: true },
|
|
47
|
+
{ movementKind: 'charge', saleKind: 'house-publisher', publisherLeg: false },
|
|
48
|
+
{ movementKind: 'marketplace_refund', saleKind: 'third-party', publisherLeg: true },
|
|
49
|
+
{ movementKind: 'marketplace_refund', saleKind: 'house-publisher', publisherLeg: false },
|
|
50
|
+
];
|
|
51
|
+
/** The fields of a leg, in the order they are encoded. */
|
|
52
|
+
const LEG_FIELDS = ['walletId', 'cents'];
|
|
53
|
+
/**
|
|
54
|
+
* The exact bytes a marketplace receipt's signature is computed over.
|
|
55
|
+
*
|
|
56
|
+
* Builds a fresh object from the statement's named fields, in a fixed order, so the
|
|
57
|
+
* encoding depends on nothing but their values: not on the order a caller wrote
|
|
58
|
+
* them in. THROWS a `TypeError` for anything that is not exactly one of the four
|
|
59
|
+
* statements — a pair of kinds that is not admissible, a missing or extra field, a
|
|
60
|
+
* string field that is not a string, or cents or a time that is not a safe whole
|
|
61
|
+
* number no less than zero. A statement arrives from a transport before it is typed,
|
|
62
|
+
* and a value of the wrong kind would still encode, to something nobody signed.
|
|
63
|
+
*/
|
|
64
|
+
export function encodeMarketplaceReceiptStatement(statement) {
|
|
65
|
+
const value = statement;
|
|
66
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
67
|
+
throw new TypeError('a marketplace receipt statement is an object');
|
|
68
|
+
}
|
|
69
|
+
const shape = ADMISSIBLE.find((candidate) => candidate.movementKind === value['movementKind'] && candidate.saleKind === value['saleKind']);
|
|
70
|
+
if (shape === undefined) {
|
|
71
|
+
throw new TypeError('a marketplace receipt statement names an admissible movement and sale kind');
|
|
72
|
+
}
|
|
73
|
+
const legFields = shape.publisherLeg ? ['publisherLeg', 'communityLeg'] : ['communityLeg'];
|
|
74
|
+
const fields = [...LEADING_FIELDS, ...legFields, ...TRAILING_FIELDS];
|
|
75
|
+
requireExactly(value, fields, 'a marketplace receipt statement carries exactly its own fields');
|
|
76
|
+
for (const field of STRING_FIELDS) {
|
|
77
|
+
if (typeof value[field] !== 'string') {
|
|
78
|
+
throw new TypeError('every identifier of a marketplace receipt statement is a string');
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
requireWholeNumber(value['writtenAt'], 'a marketplace receipt is written at whole milliseconds');
|
|
82
|
+
const canonical = {};
|
|
83
|
+
for (const field of LEADING_FIELDS)
|
|
84
|
+
canonical[field] = value[field];
|
|
85
|
+
for (const field of legFields)
|
|
86
|
+
canonical[field] = canonicalLeg(value[field]);
|
|
87
|
+
for (const field of TRAILING_FIELDS)
|
|
88
|
+
canonical[field] = value[field];
|
|
89
|
+
return new TextEncoder().encode(JSON.stringify(canonical));
|
|
90
|
+
}
|
|
91
|
+
/** A leg rebuilt in its fixed order, or a refusal. */
|
|
92
|
+
function canonicalLeg(leg) {
|
|
93
|
+
if (typeof leg !== 'object' || leg === null || Array.isArray(leg)) {
|
|
94
|
+
throw new TypeError('a marketplace receipt leg is an object');
|
|
95
|
+
}
|
|
96
|
+
const value = leg;
|
|
97
|
+
requireExactly(value, LEG_FIELDS, 'a marketplace receipt leg carries a wallet id and cents only');
|
|
98
|
+
if (typeof value['walletId'] !== 'string') {
|
|
99
|
+
throw new TypeError('a marketplace receipt leg names its wallet by id');
|
|
100
|
+
}
|
|
101
|
+
requireWholeNumber(value['cents'], 'a marketplace receipt leg carries whole cents');
|
|
102
|
+
return { walletId: value['walletId'], cents: value['cents'] };
|
|
103
|
+
}
|
|
104
|
+
/** Refuse an object whose own keys are not exactly `fields`. */
|
|
105
|
+
function requireExactly(value, fields, category) {
|
|
106
|
+
const keys = Object.keys(value);
|
|
107
|
+
if (keys.length !== fields.length || !fields.every((field) => Object.hasOwn(value, field))) {
|
|
108
|
+
throw new TypeError(category);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/** Refuse anything that is not a safe whole number no less than zero. */
|
|
112
|
+
function requireWholeNumber(value, category) {
|
|
113
|
+
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0) {
|
|
114
|
+
throw new TypeError(category);
|
|
115
|
+
}
|
|
116
|
+
}
|