@chaindaddy/mcp 0.6.3 → 0.6.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chaindaddy/mcp",
3
- "version": "0.6.3",
3
+ "version": "0.6.4",
4
4
  "description": "MCP server for Chain Daddy: search tickers, launch a token or verify ownership of one, and run its creator store",
5
5
  "mcpName": "io.chaindaddy/mcp",
6
6
  "homepage": "https://docs.chaindaddy.io/developer/store-and-rewards#tools-for-agents",
@@ -51,7 +51,7 @@ Replace `YOUR_SITE` with the site's https origin. Nothing is registered with Cha
51
51
  document.querySelector('cd-signin-button').addEventListener('cd-signin', async (e) => {
52
52
  const { id_token } = await ChainDaddy.exchangeCode(e.detail);
53
53
  await fetch('/session', { method: 'POST', headers: { 'Content-Type': 'application/json' },
54
- body: JSON.stringify({ id_token, nonce: e.detail.nonce }) });
54
+ body: JSON.stringify({ id_token }) });
55
55
  location.reload();
56
56
  });
57
57
  </script>
@@ -66,7 +66,7 @@ Replace `YOUR_SITE` with the site's https origin. Nothing is registered with Cha
66
66
  if (!result) return; // popup: handed back and closed
67
67
  const { id_token } = await ChainDaddy.exchangeCode(result); // popup blocked: finish here
68
68
  await fetch('/session', { method: 'POST', headers: { 'Content-Type': 'application/json' },
69
- body: JSON.stringify({ id_token, nonce: result.nonce }) });
69
+ body: JSON.stringify({ id_token }) });
70
70
  location.replace('/');
71
71
  });
72
72
  </script>
@@ -77,18 +77,36 @@ Replace `YOUR_SITE` with the site's https origin. Nothing is registered with Cha
77
77
  ```ts
78
78
  import { createRemoteJWKSet, jwtVerify } from 'jose';
79
79
  const JWKS = createRemoteJWKSet(new URL('https://api.chaindaddy.io/api/v2/oauth/jwks'));
80
+ // ID tokens already accepted, until they expire. A real deployment keeps these in its session store or Redis.
81
+ const seen = new Map<string, number>();
80
82
 
81
- export async function verifyChainDaddy(idToken: string, nonce: string) {
83
+ export async function verifyChainDaddy(idToken: string) {
82
84
  const { payload } = await jwtVerify(idToken, JWKS, {
83
85
  issuer: 'https://api.chaindaddy.io', audience: 'https://YOUR_SITE/client.json', algorithms: ['ES256'],
84
86
  });
85
87
  if (payload.typ !== 'signin+v1') throw new Error('not a Chain Daddy sign-in');
86
- if (payload.nonce !== nonce) throw new Error('replayed sign-in');
88
+ const now = Date.now() / 1000;
89
+ seen.forEach((exp, jti) => { if (exp < now) seen.delete(jti); });
90
+ if (!payload.jti || seen.has(payload.jti)) throw new Error('replayed sign-in');
91
+ seen.set(payload.jti, payload.exp ?? now + 300);
87
92
  return payload; // sub: the person's id at this site; wallets[0]: the wallet they signed in with
88
93
  }
89
94
  ```
90
95
 
91
- To require a coin, add `claims='{"id_token":{"holdings":{"tokens":[{"chain":"base","address":"0xTOKEN","min":"100"}]}}}'` to the button and check `payload.holdings[0].held === true` on the server (`null` means the balance could not be read: ask again, never treat it as yes). `{"id_token":{"crowns":{"all":true}}}` lists the crowns they hold.
96
+ To require a coin, add `claims='{"id_token":{"holdings":{"tokens":[{"chain":"base","address":"0xTOKEN","min":"100"}]}}}'` to the button. That request passes through the visitor's browser, which can change the coin or the minimum, and the ID token answers whatever was asked. So the server never reads `payload.holdings[0].held`: it matches each answer against its own constants first.
97
+
98
+ ```ts
99
+ // What this site gates on, as the ID token writes it: token lowercase for an EVM address, min "0" if none was asked.
100
+ const GATE = [{ chain: 'base', token: '0xtoken', min: '100' }];
101
+
102
+ export function holdsCoin(payload: Record<string, unknown>) {
103
+ const holdings = Array.isArray(payload.holdings) ? payload.holdings : [];
104
+ return GATE.some((want) => holdings.some((h) =>
105
+ h?.chain === want.chain && h?.token === want.token && h?.min === want.min && h?.held === true));
106
+ }
107
+ ```
108
+
109
+ `held` is `null` when the balance could not be read: ask again, never treat it as yes. `{"id_token":{"crowns":{"all":true}}}` lists the crowns they hold.
92
110
 
93
111
  ## 1b. Or register a client (the human does this)
94
112
 
@@ -157,9 +175,11 @@ that throws unless `typ === 'signin+v1'`.
157
175
  | `profile` | `identity`: the handle they chose to publish |
158
176
 
159
177
  With the `claims` request (section 1): `holdings` (`held` true, false, or `null` when unreadable) and `crowns`.
178
+ Match a `holdings` entry against your own coin and minimum before reading `held` (section 1).
160
179
 
161
180
  Check on every ID token: signature (ES256, keys above), `iss` = `https://api.chaindaddy.io`, `aud` = your
162
- client id, `typ` = `signin+v1`, `nonce` = the one you sent, `exp` (5 minutes).
181
+ client id, `typ` = `signin+v1`, `exp` (5 minutes), and `nonce` = the one your server sent. With the button the
182
+ browser makes the nonce, so comparing it proves nothing: refuse a `jti` you have already accepted (section 1).
163
183
 
164
184
  ## After sign-in
165
185