@chaindaddy/mcp 0.5.0 → 0.6.1

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.
@@ -0,0 +1,106 @@
1
+ ---
2
+ name: verify-receipts-and-sessions
3
+ description: Verify what Chain Daddy signs on your own server before acting on it - store purchase receipts (iap-receipt+v1), Crown App session tokens (app-session+v1), Sign in with Chain Daddy ID tokens (signin+v1), and webhook signatures - with the exact keys, claims and checks, plus published test vectors. Use when writing server code that grants items, credits a purchase, trusts a player's wallet, or receives Chain Daddy webhooks.
4
+ license: MIT
5
+ ---
6
+
7
+ # Verify receipts, sessions and webhooks
8
+
9
+ Never act on something because a client sent it. Each kind below has its own keys and its own `typ` claim,
10
+ so one can never pass for another - as long as you check `typ`.
11
+
12
+ ## Which token is which
13
+
14
+ | You have | `typ` claim | Algorithm | `iss` | Keys (JWKS) | Also check |
15
+ |---|---|---|---|---|---|
16
+ | Store receipt (a confirmed purchase) | `iap-receipt+v1` | EdDSA (Ed25519) | `https://chaindaddy.io` | `https://api.chaindaddy.io/api/v2/iap/jwks` | `storeId` is your store; `livemode` is `true` in production; credit each `orderId` once |
17
+ | Crown App session token (`session-token` action) | `app-session+v1` | EdDSA | `https://chaindaddy.io` | `https://api.chaindaddy.io/api/v2/apps/jwks` | `aud` is your app id (`<developerId>/<name>`); `exp` (5 min, allow 30 s skew); refuse a `jti` you have seen; `idv === true` if you need a verified person |
18
+ | Sign in with Chain Daddy ID token | `signin+v1` | ES256 | `https://api.chaindaddy.io` | `https://api.chaindaddy.io/api/v2/oauth/jwks` | `aud` is your client id; `nonce`; `exp` (5 min) |
19
+
20
+ Notes:
21
+ - The JWS **header** `typ` is a plain `JWT`. What the token is lives in the `typ` **claim**. Check the claim.
22
+ - Pick the key by the header's `kid`. Each set lists the current key first and keeps the previous one, so a
23
+ cached remote key set that refetches on an unknown `kid` survives rotation with no work.
24
+ - `sub` is the wallet in all three: lowercase `0x…` on EVM, base58 on Solana.
25
+
26
+ ## TypeScript (jose)
27
+
28
+ ```ts
29
+ import { createRemoteJWKSet, jwtVerify } from 'jose';
30
+
31
+ const API = 'https://api.chaindaddy.io';
32
+ const ISSUER = 'https://chaindaddy.io';
33
+ const receiptKeys = createRemoteJWKSet(new URL('/api/v2/iap/jwks', API));
34
+ const sessionKeys = createRemoteJWKSet(new URL('/api/v2/apps/jwks', API));
35
+
36
+ export async function verifyReceipt(jws: string, storeId: string) {
37
+ const { payload } = await jwtVerify(jws, receiptKeys, { algorithms: ['EdDSA'], issuer: ISSUER });
38
+ if (payload.typ !== 'iap-receipt+v1') throw new Error('not a store receipt');
39
+ if (payload.storeId !== storeId) throw new Error('receipt is for another store');
40
+ if (payload.livemode === false && process.env.NODE_ENV === 'production') throw new Error('test receipt');
41
+ return payload; // grant by payload.sku, payload.quantity, payload.wallet; dedupe on payload.orderId
42
+ }
43
+
44
+ export async function verifySession(token: string, appId: string) {
45
+ const { payload } = await jwtVerify(token, sessionKeys, {
46
+ algorithms: ['EdDSA'], issuer: ISSUER, audience: appId, clockTolerance: 30,
47
+ });
48
+ if (payload.typ !== 'app-session+v1') throw new Error('not an app session token');
49
+ if (!payload.exp) throw new Error('no expiry');
50
+ return payload; // payload.sub is the wallet; check payload.idv === true to require a verified person
51
+ }
52
+ ```
53
+
54
+ For the ID token, use your OpenID Connect library (see `sign-in-with-chain-daddy`), or `jwtVerify` with the
55
+ oauth key set, `issuer: 'https://api.chaindaddy.io'`, `audience: <client id>`, `algorithms: ['ES256']`, then
56
+ check `typ === 'signin+v1'` and the nonce. Go: `server/cdapi` in https://gitlab.com/opencrown/flux-gp-kit.
57
+
58
+ ## Refunds: offline is not enough for everything
59
+
60
+ An offline receipt check can't see a refund made since. For anything that matters after the fact, also check
61
+ online - `POST https://api.chaindaddy.io/api/v2/iap/receipts/verify` with `{"receipt": "…"}` returns
62
+ `valid` or `revoked` - or read the store's item ledger (`GET …/entitlements?wallet=`), which is the source of
63
+ truth for what a wallet owns now.
64
+
65
+ ## Webhook signatures
66
+
67
+ | Webhook | Header | How to check |
68
+ |---|---|---|
69
+ | Store webhook (`iap.*`) | `X-Webhook-Signature` | Hex HMAC-SHA256 of the **raw body**, keyed with the `whsec_…` secret as given |
70
+ | Crown App runner events | `X-App-Signature` | Hex HMAC-SHA256 of the raw body, keyed with the runner secret **hex-decoded to bytes** |
71
+
72
+ ```js
73
+ import crypto from 'node:crypto';
74
+ function verifyStoreWebhook(rawBody, signature, secret) {
75
+ const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
76
+ return signature?.length === expected.length &&
77
+ crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
78
+ }
79
+ ```
80
+
81
+ Check against the raw bytes before parsing JSON, compare in constant time, and ignore a repeated event `id`
82
+ (retries reuse it). Answer 2xx within 5 seconds.
83
+
84
+ ## Prove your checker with the test vectors
85
+
86
+ https://docs.chaindaddy.io/vectors/tokens.json holds two key sets (`jwks.session`, `jwks.receipts`), a fixed
87
+ `now`, an `appId`, a `storeId` and 15 tokens marked `valid` or not (expired, clock skew, another app, another
88
+ issuer, `typ` only in the header, no expiry, a forgery, a receipt offered as a session and the reverse,
89
+ another store). Load the sets with `createLocalJWKSet`, run every case at `now`, and pass only when your
90
+ code accepts exactly the valid ones. Add this as a unit test.
91
+
92
+ ## Mistakes to avoid
93
+
94
+ - Checking the header `typ` instead of the claim, or not checking `typ` at all.
95
+ - Verifying a session token against the receipt keys, or the other way round: they use different key sets.
96
+ - Using `iss: https://api.chaindaddy.io` for receipts and sessions. Only Sign in ID tokens use the `api.` issuer.
97
+ - Accepting a test receipt (`livemode: false`) or another store's receipt in production.
98
+ - Crediting the same `orderId` twice when the webhook and your poller both see it.
99
+ - Parsing the webhook body before checking its signature, or comparing with `===`.
100
+ - Treating a session token as API access: it only proves presence to your server.
101
+
102
+ ## Docs
103
+
104
+ - https://docs.chaindaddy.io/developer/verifying-tokens
105
+ - https://docs.chaindaddy.io/developer/store-and-rewards#verifying-receipts-on-your-server
106
+ - https://docs.chaindaddy.io/developer/security#session-tokens