tapimo 0.5.2 → 0.6.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 (299) hide show
  1. package/dist/App.d.ts +13987 -5755
  2. package/dist/App.d.ts.map +1 -1
  3. package/dist/App.js.map +1 -1
  4. package/dist/admin/App.d.ts +910 -412
  5. package/dist/admin/App.d.ts.map +1 -1
  6. package/dist/admin/App.js +4 -0
  7. package/dist/admin/App.js.map +1 -1
  8. package/dist/admin/apps/early-access.d.ts +148 -0
  9. package/dist/admin/apps/early-access.d.ts.map +1 -0
  10. package/dist/admin/apps/early-access.js +133 -0
  11. package/dist/admin/apps/early-access.js.map +1 -0
  12. package/dist/admin/apps/verified-tokens.d.ts +643 -118
  13. package/dist/admin/apps/verified-tokens.d.ts.map +1 -1
  14. package/dist/admin/apps/verified-tokens.js +214 -1
  15. package/dist/admin/apps/verified-tokens.js.map +1 -1
  16. package/dist/admin/ui.generated.d.ts +1 -1
  17. package/dist/admin/ui.generated.d.ts.map +1 -1
  18. package/dist/admin/ui.generated.js +45 -33
  19. package/dist/admin/ui.generated.js.map +1 -1
  20. package/dist/apps/data/App.d.ts +3733 -2428
  21. package/dist/apps/data/App.d.ts.map +1 -1
  22. package/dist/apps/data/App.js +11 -6
  23. package/dist/apps/data/App.js.map +1 -1
  24. package/dist/apps/data/FundingQuoteProviders.d.ts +34 -1
  25. package/dist/apps/data/FundingQuoteProviders.d.ts.map +1 -1
  26. package/dist/apps/data/FundingQuoteProviders.js +193 -2
  27. package/dist/apps/data/FundingQuoteProviders.js.map +1 -1
  28. package/dist/apps/data/FxOracle.d.ts +71 -0
  29. package/dist/apps/data/FxOracle.d.ts.map +1 -0
  30. package/dist/apps/data/FxOracle.js +97 -0
  31. package/dist/apps/data/FxOracle.js.map +1 -0
  32. package/dist/apps/data/index.d.ts +1 -0
  33. package/dist/apps/data/index.d.ts.map +1 -1
  34. package/dist/apps/data/index.js +1 -0
  35. package/dist/apps/data/index.js.map +1 -1
  36. package/dist/apps/data/routes/activities.d.ts +3575 -1358
  37. package/dist/apps/data/routes/activities.d.ts.map +1 -1
  38. package/dist/apps/data/routes/activities.js +230 -88
  39. package/dist/apps/data/routes/activities.js.map +1 -1
  40. package/dist/apps/data/routes/addresses.d.ts +963 -342
  41. package/dist/apps/data/routes/addresses.d.ts.map +1 -1
  42. package/dist/apps/data/routes/addresses.js +11 -9
  43. package/dist/apps/data/routes/addresses.js.map +1 -1
  44. package/dist/apps/data/routes/balances.d.ts +108 -57
  45. package/dist/apps/data/routes/balances.d.ts.map +1 -1
  46. package/dist/apps/data/routes/balances.js +88 -45
  47. package/dist/apps/data/routes/balances.js.map +1 -1
  48. package/dist/apps/data/routes/coingecko.d.ts +169 -169
  49. package/dist/apps/data/routes/exchanges.d.ts +952 -201
  50. package/dist/apps/data/routes/exchanges.d.ts.map +1 -1
  51. package/dist/apps/data/routes/exchanges.js +518 -76
  52. package/dist/apps/data/routes/exchanges.js.map +1 -1
  53. package/dist/apps/data/routes/fee-amm.d.ts +24 -24
  54. package/dist/apps/data/routes/funding.d.ts +8 -2
  55. package/dist/apps/data/routes/funding.d.ts.map +1 -1
  56. package/dist/apps/data/routes/funding.js +32 -6
  57. package/dist/apps/data/routes/funding.js.map +1 -1
  58. package/dist/apps/data/routes/indexer.d.ts +5 -5
  59. package/dist/apps/data/routes/receipts.d.ts +392 -315
  60. package/dist/apps/data/routes/receipts.d.ts.map +1 -1
  61. package/dist/apps/data/routes/receipts.js +152 -77
  62. package/dist/apps/data/routes/receipts.js.map +1 -1
  63. package/dist/apps/data/routes/tokens.d.ts +166 -173
  64. package/dist/apps/data/routes/tokens.d.ts.map +1 -1
  65. package/dist/apps/data/routes/tokens.js +14 -12
  66. package/dist/apps/data/routes/tokens.js.map +1 -1
  67. package/dist/apps/data/routes/transactions.d.ts +277 -536
  68. package/dist/apps/data/routes/transactions.d.ts.map +1 -1
  69. package/dist/apps/data/routes/transactions.js +96 -77
  70. package/dist/apps/data/routes/transactions.js.map +1 -1
  71. package/dist/apps/data/routes/transfers.d.ts +82 -17
  72. package/dist/apps/data/routes/transfers.d.ts.map +1 -1
  73. package/dist/apps/data/routes/transfers.js +70 -13
  74. package/dist/apps/data/routes/transfers.js.map +1 -1
  75. package/dist/apps/data/routes/valuation.d.ts +307 -0
  76. package/dist/apps/data/routes/valuation.d.ts.map +1 -0
  77. package/dist/apps/data/routes/valuation.js +292 -0
  78. package/dist/apps/data/routes/valuation.js.map +1 -0
  79. package/dist/apps/data/routes/verified-tokens.d.ts.map +1 -1
  80. package/dist/apps/data/routes/verified-tokens.js +7 -5
  81. package/dist/apps/data/routes/verified-tokens.js.map +1 -1
  82. package/dist/apps/data/routes/webhooks.d.ts +459 -479
  83. package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
  84. package/dist/apps/data/routes/webhooks.js +124 -25
  85. package/dist/apps/data/routes/webhooks.js.map +1 -1
  86. package/dist/apps/index.d.ts +1 -1
  87. package/dist/apps/index.d.ts.map +1 -1
  88. package/dist/apps/index.js +1 -1
  89. package/dist/apps/index.js.map +1 -1
  90. package/dist/apps/management/App.d.ts +8606 -1681
  91. package/dist/apps/management/App.d.ts.map +1 -1
  92. package/dist/apps/management/App.js +12 -1
  93. package/dist/apps/management/App.js.map +1 -1
  94. package/dist/apps/management/routes/api-keys.d.ts +23 -0
  95. package/dist/apps/management/routes/api-keys.d.ts.map +1 -1
  96. package/dist/apps/management/routes/api-keys.js +5 -3
  97. package/dist/apps/management/routes/api-keys.js.map +1 -1
  98. package/dist/apps/management/routes/billing.d.ts +88 -48
  99. package/dist/apps/management/routes/billing.d.ts.map +1 -1
  100. package/dist/apps/management/routes/billing.js +10 -2
  101. package/dist/apps/management/routes/billing.js.map +1 -1
  102. package/dist/apps/management/routes/faucet.d.ts +218 -0
  103. package/dist/apps/management/routes/faucet.d.ts.map +1 -0
  104. package/dist/apps/management/routes/faucet.js +139 -0
  105. package/dist/apps/management/routes/faucet.js.map +1 -0
  106. package/dist/apps/management/routes/invitations.d.ts +121 -121
  107. package/dist/apps/management/routes/invitations.d.ts.map +1 -1
  108. package/dist/apps/management/routes/invitations.js +3 -0
  109. package/dist/apps/management/routes/invitations.js.map +1 -1
  110. package/dist/apps/management/routes/invite-links.d.ts +197 -168
  111. package/dist/apps/management/routes/invite-links.d.ts.map +1 -1
  112. package/dist/apps/management/routes/invite-links.js +26 -1
  113. package/dist/apps/management/routes/invite-links.js.map +1 -1
  114. package/dist/apps/management/routes/me.d.ts +2 -0
  115. package/dist/apps/management/routes/me.d.ts.map +1 -1
  116. package/dist/apps/management/routes/me.js +4 -0
  117. package/dist/apps/management/routes/me.js.map +1 -1
  118. package/dist/apps/management/routes/members.d.ts +49 -49
  119. package/dist/apps/management/routes/orgs.d.ts +91 -91
  120. package/dist/apps/management/routes/projects.d.ts +115 -115
  121. package/dist/apps/management/routes/usage.d.ts +35 -32
  122. package/dist/apps/management/routes/usage.d.ts.map +1 -1
  123. package/dist/apps/management/routes/usage.js +4 -0
  124. package/dist/apps/management/routes/usage.js.map +1 -1
  125. package/dist/apps/management/routes/verified-token-requests.d.ts +697 -0
  126. package/dist/apps/management/routes/verified-token-requests.d.ts.map +1 -0
  127. package/dist/apps/management/routes/verified-token-requests.js +271 -0
  128. package/dist/apps/management/routes/verified-token-requests.js.map +1 -0
  129. package/dist/apps/management/routes/webhooks.d.ts +6977 -0
  130. package/dist/apps/management/routes/webhooks.d.ts.map +1 -0
  131. package/dist/apps/management/routes/webhooks.js +573 -0
  132. package/dist/apps/management/routes/webhooks.js.map +1 -0
  133. package/dist/apps/mcp.d.ts +9 -2
  134. package/dist/apps/mcp.d.ts.map +1 -1
  135. package/dist/apps/mcp.js +9 -4
  136. package/dist/apps/mcp.js.map +1 -1
  137. package/dist/cli/Cli.d.ts.map +1 -1
  138. package/dist/cli/Cli.js +7 -2
  139. package/dist/cli/Cli.js.map +1 -1
  140. package/dist/db/Db.d.ts +4 -0
  141. package/dist/db/Db.d.ts.map +1 -1
  142. package/dist/db/Db.js +83 -0
  143. package/dist/db/Db.js.map +1 -1
  144. package/dist/db/Schema.d.ts +41 -0
  145. package/dist/db/Schema.d.ts.map +1 -1
  146. package/dist/db/Schema.js +45 -0
  147. package/dist/db/Schema.js.map +1 -1
  148. package/dist/db/tables/earlyAccess.d.ts +50 -0
  149. package/dist/db/tables/earlyAccess.d.ts.map +1 -0
  150. package/dist/db/tables/earlyAccess.js +63 -0
  151. package/dist/db/tables/earlyAccess.js.map +1 -0
  152. package/dist/db/tables/invitations.d.ts +12 -1
  153. package/dist/db/tables/invitations.d.ts.map +1 -1
  154. package/dist/db/tables/invitations.js +23 -0
  155. package/dist/db/tables/invitations.js.map +1 -1
  156. package/dist/db/tables/inviteLinks.d.ts +10 -2
  157. package/dist/db/tables/inviteLinks.d.ts.map +1 -1
  158. package/dist/db/tables/inviteLinks.js +10 -1
  159. package/dist/db/tables/inviteLinks.js.map +1 -1
  160. package/dist/db/tables/sponsoredTransactions.d.ts +2 -0
  161. package/dist/db/tables/sponsoredTransactions.d.ts.map +1 -1
  162. package/dist/db/tables/sponsoredTransactions.js +2 -0
  163. package/dist/db/tables/sponsoredTransactions.js.map +1 -1
  164. package/dist/db/tables/verifiedTokenRequests.d.ts +94 -0
  165. package/dist/db/tables/verifiedTokenRequests.d.ts.map +1 -0
  166. package/dist/db/tables/verifiedTokenRequests.js +120 -0
  167. package/dist/db/tables/verifiedTokenRequests.js.map +1 -0
  168. package/dist/db/tables/webhookSubscriptions.d.ts +14 -0
  169. package/dist/db/tables/webhookSubscriptions.d.ts.map +1 -1
  170. package/dist/db/tables/webhookSubscriptions.js +16 -0
  171. package/dist/db/tables/webhookSubscriptions.js.map +1 -1
  172. package/dist/internal/Auth.d.ts +43 -2
  173. package/dist/internal/Auth.d.ts.map +1 -1
  174. package/dist/internal/Auth.js +60 -45
  175. package/dist/internal/Auth.js.map +1 -1
  176. package/dist/internal/FundingQuotes.d.ts +69 -15
  177. package/dist/internal/FundingQuotes.d.ts.map +1 -1
  178. package/dist/internal/FundingQuotes.js +74 -25
  179. package/dist/internal/FundingQuotes.js.map +1 -1
  180. package/dist/internal/Log.d.ts +19 -1
  181. package/dist/internal/Log.d.ts.map +1 -1
  182. package/dist/internal/Log.js +2 -0
  183. package/dist/internal/Log.js.map +1 -1
  184. package/dist/internal/MetricSink.d.ts.map +1 -1
  185. package/dist/internal/MetricSink.js +9 -0
  186. package/dist/internal/MetricSink.js.map +1 -1
  187. package/dist/internal/OpenApi.d.ts +2 -2
  188. package/dist/internal/OpenApi.d.ts.map +1 -1
  189. package/dist/internal/OpenApi.js +3 -0
  190. package/dist/internal/OpenApi.js.map +1 -1
  191. package/dist/internal/Schema.d.ts +14 -5
  192. package/dist/internal/Schema.d.ts.map +1 -1
  193. package/dist/internal/Schema.js +23 -5
  194. package/dist/internal/Schema.js.map +1 -1
  195. package/dist/internal/Value.d.ts +18 -0
  196. package/dist/internal/Value.d.ts.map +1 -1
  197. package/dist/internal/Value.js +11 -1
  198. package/dist/internal/Value.js.map +1 -1
  199. package/dist/internal/VerifiedTokens.d.ts.map +1 -1
  200. package/dist/internal/VerifiedTokens.js +10 -1
  201. package/dist/internal/VerifiedTokens.js.map +1 -1
  202. package/dist/internal/Viem.d.ts +1 -1
  203. package/dist/internal/Webhooks.d.ts +24 -3
  204. package/dist/internal/Webhooks.d.ts.map +1 -1
  205. package/dist/internal/Webhooks.js +34 -8
  206. package/dist/internal/Webhooks.js.map +1 -1
  207. package/package.json +2 -2
  208. package/src/App.ts +9 -1
  209. package/src/Client.test-d.ts +638 -6
  210. package/src/admin/App.ts +4 -0
  211. package/src/admin/apps/early-access.test.ts +125 -0
  212. package/src/admin/apps/early-access.ts +175 -0
  213. package/src/admin/apps/verified-tokens.test.ts +201 -0
  214. package/src/admin/apps/verified-tokens.ts +532 -286
  215. package/src/admin/ui/plugin.ts +2 -1
  216. package/src/admin/ui/src/lib/early-access.ts +63 -0
  217. package/src/admin/ui/src/lib/verified-tokens.ts +79 -0
  218. package/src/admin/ui/src/routeTree.gen.ts +33 -2
  219. package/src/admin/ui/src/routes/__root.tsx +2 -1
  220. package/src/admin/ui/src/routes/early-access.tsx +157 -0
  221. package/src/admin/ui/src/routes/verified-tokens.tsx +230 -19
  222. package/src/admin/ui.generated.ts +48 -33
  223. package/src/apps/data/App.ts +22 -14
  224. package/src/apps/data/FundingQuoteProviders.test.ts +298 -0
  225. package/src/apps/data/FundingQuoteProviders.ts +257 -4
  226. package/src/apps/data/FxOracle.test.ts +118 -0
  227. package/src/apps/data/FxOracle.ts +153 -0
  228. package/src/apps/data/index.ts +1 -0
  229. package/src/apps/data/routes/activities.test.ts +173 -3
  230. package/src/apps/data/routes/activities.ts +403 -132
  231. package/src/apps/data/routes/addresses.ts +19 -9
  232. package/src/apps/data/routes/balances.test.ts +144 -11
  233. package/src/apps/data/routes/balances.ts +140 -51
  234. package/src/apps/data/routes/exchanges.test.ts +540 -9
  235. package/src/apps/data/routes/exchanges.ts +690 -103
  236. package/src/apps/data/routes/funding.test.ts +84 -0
  237. package/src/apps/data/routes/funding.ts +38 -6
  238. package/src/apps/data/routes/receipts.test.ts +125 -23
  239. package/src/apps/data/routes/receipts.ts +205 -100
  240. package/src/apps/data/routes/tokens.test.ts +43 -0
  241. package/src/apps/data/routes/tokens.ts +27 -12
  242. package/src/apps/data/routes/transactions.test.ts +72 -19
  243. package/src/apps/data/routes/transactions.ts +162 -92
  244. package/src/apps/data/routes/transfers.test.ts +146 -4
  245. package/src/apps/data/routes/transfers.ts +91 -13
  246. package/src/apps/data/routes/valuation.test.ts +422 -0
  247. package/src/apps/data/routes/valuation.ts +423 -0
  248. package/src/apps/data/routes/verified-tokens.ts +14 -2
  249. package/src/apps/data/routes/webhooks.test.ts +56 -8
  250. package/src/apps/data/routes/webhooks.ts +144 -27
  251. package/src/apps/index.ts +1 -1
  252. package/src/apps/management/App.ts +13 -0
  253. package/src/apps/management/routes/api-keys.test.ts +87 -0
  254. package/src/apps/management/routes/api-keys.ts +4 -3
  255. package/src/apps/management/routes/billing.test.ts +112 -1
  256. package/src/apps/management/routes/billing.ts +7 -2
  257. package/src/apps/management/routes/faucet.test.ts +79 -0
  258. package/src/apps/management/routes/faucet.ts +185 -0
  259. package/src/apps/management/routes/invitations.test.ts +172 -0
  260. package/src/apps/management/routes/invitations.ts +3 -0
  261. package/src/apps/management/routes/invite-links.test.ts +117 -0
  262. package/src/apps/management/routes/invite-links.ts +43 -1
  263. package/src/apps/management/routes/me.test.ts +140 -88
  264. package/src/apps/management/routes/me.ts +6 -0
  265. package/src/apps/management/routes/usage.test.ts +6 -0
  266. package/src/apps/management/routes/usage.ts +7 -0
  267. package/src/apps/management/routes/verified-token-requests.test.ts +377 -0
  268. package/src/apps/management/routes/verified-token-requests.ts +397 -0
  269. package/src/apps/management/routes/webhooks.test.ts +426 -0
  270. package/src/apps/management/routes/webhooks.ts +672 -0
  271. package/src/apps/mcp.test.ts +13 -0
  272. package/src/apps/mcp.ts +18 -5
  273. package/src/cli/Cli.test.ts +36 -0
  274. package/src/cli/Cli.ts +7 -2
  275. package/src/db/Db.test.ts +19 -0
  276. package/src/db/Db.ts +95 -0
  277. package/src/db/Schema.ts +53 -0
  278. package/src/db/tables/earlyAccess.ts +86 -0
  279. package/src/db/tables/invitations.ts +26 -1
  280. package/src/db/tables/inviteLinks.ts +22 -3
  281. package/src/db/tables/sponsoredTransactions.ts +4 -0
  282. package/src/db/tables/verifiedTokenRequests.test.ts +200 -0
  283. package/src/db/tables/verifiedTokenRequests.ts +182 -0
  284. package/src/db/tables/webhookSubscriptions.ts +31 -0
  285. package/src/internal/Auth.ts +73 -45
  286. package/src/internal/FundingQuotes.test.ts +143 -1
  287. package/src/internal/FundingQuotes.ts +95 -28
  288. package/src/internal/Log.test-d.ts +21 -0
  289. package/src/internal/Log.test.ts +50 -0
  290. package/src/internal/Log.ts +24 -1
  291. package/src/internal/MetricSink.test.ts +160 -0
  292. package/src/internal/MetricSink.ts +10 -0
  293. package/src/internal/OpenApi.ts +3 -0
  294. package/src/internal/Schema.ts +50 -5
  295. package/src/internal/Value.test.ts +20 -0
  296. package/src/internal/Value.ts +24 -1
  297. package/src/internal/VerifiedTokens.ts +9 -1
  298. package/src/internal/Webhooks.test.ts +140 -0
  299. package/src/internal/Webhooks.ts +53 -9
@@ -0,0 +1,200 @@
1
+ import * as TestApp from '../../../test/App.js'
2
+ import * as Organizations from './organizations.js'
3
+ import * as VerifiedTokenRequests from './verifiedTokenRequests.js'
4
+
5
+ const input = {
6
+ address: '0x20c00000000000000000000000000000000000aa',
7
+ chainId: 42431,
8
+ currency: 'USD',
9
+ decimals: 6,
10
+ name: 'Test USD',
11
+ orgId: 'org_1a2b3c4d5e6f7g8h9j0k1m2n',
12
+ requestedBy: 'usr_1a2b3c4d5e6f7g8h9j0k1m2n',
13
+ symbol: 'tUSD',
14
+ } as const
15
+
16
+ describe('create', () => {
17
+ test('inserts a pending request', async () => {
18
+ const db = TestApp.database()
19
+ const record = await VerifiedTokenRequests.create(db, { ...input, note: 'We are the issuer.' })
20
+ expect(record).toBeDefined()
21
+ const { createdAt, id, updatedAt, ...rest } = record!
22
+ expect(id).toMatch(/^vtr_/)
23
+ expect(Date.parse(createdAt)).not.toBeNaN()
24
+ expect(updatedAt).toBe(createdAt)
25
+ expect(rest).toMatchInlineSnapshot(`
26
+ {
27
+ "address": "0x20c00000000000000000000000000000000000aa",
28
+ "chainId": 42431,
29
+ "currency": "USD",
30
+ "decimals": 6,
31
+ "logo": null,
32
+ "logoUri": null,
33
+ "name": "Test USD",
34
+ "note": "We are the issuer.",
35
+ "orgId": "org_1a2b3c4d5e6f7g8h9j0k1m2n",
36
+ "requestedBy": "usr_1a2b3c4d5e6f7g8h9j0k1m2n",
37
+ "reviewNote": null,
38
+ "reviewedAt": null,
39
+ "reviewedBy": null,
40
+ "status": "pending",
41
+ "symbol": "tUSD",
42
+ }
43
+ `)
44
+ })
45
+
46
+ test('a duplicate pending (chain, address) yields undefined, even cross-org', async () => {
47
+ const db = TestApp.database()
48
+ await VerifiedTokenRequests.create(db, input)
49
+ const duplicate = await VerifiedTokenRequests.create(db, {
50
+ ...input,
51
+ orgId: 'org_2b3c4d5e6f7g8h9j0k1m2n3p',
52
+ })
53
+ expect(duplicate).toBeUndefined()
54
+ })
55
+
56
+ test('a reviewed request frees the (chain, address) slot', async () => {
57
+ const db = TestApp.database()
58
+ const first = await VerifiedTokenRequests.create(db, input)
59
+ await VerifiedTokenRequests.review(db, first!.id, {
60
+ reviewedBy: 'admin@tempo.xyz',
61
+ status: 'denied',
62
+ })
63
+ const second = await VerifiedTokenRequests.create(db, input)
64
+ expect(second).toBeDefined()
65
+ })
66
+
67
+ test('the same address on another chain is independent', async () => {
68
+ const db = TestApp.database()
69
+ await VerifiedTokenRequests.create(db, input)
70
+ const other = await VerifiedTokenRequests.create(db, { ...input, chainId: 4217 })
71
+ expect(other).toBeDefined()
72
+ })
73
+ })
74
+
75
+ describe('list', () => {
76
+ test('returns only the organization, newest first', async () => {
77
+ const db = TestApp.database()
78
+ const first = await VerifiedTokenRequests.create(db, input)
79
+ const second = await VerifiedTokenRequests.create(db, {
80
+ ...input,
81
+ address: '0x20c00000000000000000000000000000000000bb',
82
+ symbol: 'tEUR',
83
+ })
84
+ await VerifiedTokenRequests.create(db, {
85
+ ...input,
86
+ address: '0x20c00000000000000000000000000000000000cc',
87
+ orgId: 'org_2b3c4d5e6f7g8h9j0k1m2n3p',
88
+ symbol: 'tGBP',
89
+ })
90
+ const records = await VerifiedTokenRequests.list(db, input.orgId)
91
+ expect(records.map(({ id }) => id).sort()).toEqual([first!.id, second!.id].sort())
92
+ expect(records.every(({ orgId }) => orgId === input.orgId)).toBe(true)
93
+ })
94
+ })
95
+
96
+ describe('listByStatus', () => {
97
+ test('filters by status and joins the current org name', async () => {
98
+ const db = TestApp.database()
99
+ const org = await Organizations.create(db, { name: 'Acme' })
100
+ const pending = await VerifiedTokenRequests.create(db, { ...input, orgId: org.id })
101
+ const denied = await VerifiedTokenRequests.create(db, {
102
+ ...input,
103
+ address: '0x20c00000000000000000000000000000000000bb',
104
+ symbol: 'tEUR',
105
+ })
106
+ await VerifiedTokenRequests.review(db, denied!.id, {
107
+ reviewedBy: 'admin@tempo.xyz',
108
+ status: 'denied',
109
+ })
110
+
111
+ const queue = await VerifiedTokenRequests.listByStatus(db, 'pending')
112
+ expect(queue.map(({ id, orgName }) => ({ id, orgName }))).toEqual([
113
+ { id: pending!.id, orgName: 'Acme' },
114
+ ])
115
+ // The denied row's org has no organizations row; the join yields null.
116
+ const reviewed = await VerifiedTokenRequests.listByStatus(db, 'denied')
117
+ expect(reviewed.map(({ id, orgName }) => ({ id, orgName }))).toEqual([
118
+ { id: denied!.id, orgName: null },
119
+ ])
120
+ })
121
+ })
122
+
123
+ describe('countPending', () => {
124
+ test('counts only the organization pending requests', async () => {
125
+ const db = TestApp.database()
126
+ await VerifiedTokenRequests.create(db, input)
127
+ const denied = await VerifiedTokenRequests.create(db, {
128
+ ...input,
129
+ address: '0x20c00000000000000000000000000000000000bb',
130
+ symbol: 'tEUR',
131
+ })
132
+ await VerifiedTokenRequests.review(db, denied!.id, {
133
+ reviewedBy: 'admin@tempo.xyz',
134
+ status: 'denied',
135
+ })
136
+ await VerifiedTokenRequests.create(db, {
137
+ ...input,
138
+ address: '0x20c00000000000000000000000000000000000cc',
139
+ orgId: 'org_2b3c4d5e6f7g8h9j0k1m2n3p',
140
+ symbol: 'tGBP',
141
+ })
142
+ expect(await VerifiedTokenRequests.countPending(db, input.orgId)).toBe(1)
143
+ })
144
+ })
145
+
146
+ describe('remove', () => {
147
+ test('deletes only in-org pending requests', async () => {
148
+ const db = TestApp.database()
149
+ const record = await VerifiedTokenRequests.create(db, input)
150
+ expect(
151
+ await VerifiedTokenRequests.remove(db, 'org_2b3c4d5e6f7g8h9j0k1m2n3p', record!.id),
152
+ ).toBeUndefined()
153
+ expect(await VerifiedTokenRequests.remove(db, input.orgId, record!.id)).toBeDefined()
154
+ expect(await VerifiedTokenRequests.get(db, record!.id)).toBeUndefined()
155
+ })
156
+
157
+ test('leaves reviewed requests in place', async () => {
158
+ const db = TestApp.database()
159
+ const record = await VerifiedTokenRequests.create(db, input)
160
+ await VerifiedTokenRequests.review(db, record!.id, {
161
+ reviewedBy: 'admin@tempo.xyz',
162
+ status: 'approved',
163
+ })
164
+ expect(await VerifiedTokenRequests.remove(db, input.orgId, record!.id)).toBeUndefined()
165
+ expect(await VerifiedTokenRequests.get(db, record!.id)).toBeDefined()
166
+ })
167
+ })
168
+
169
+ describe('review', () => {
170
+ test('marks a pending request with reviewer identity', async () => {
171
+ const db = TestApp.database()
172
+ const record = await VerifiedTokenRequests.create(db, input)
173
+ const reviewed = await VerifiedTokenRequests.review(db, record!.id, {
174
+ reviewNote: 'Logo unreachable.',
175
+ reviewedBy: 'admin@tempo.xyz',
176
+ status: 'denied',
177
+ })
178
+ expect(reviewed).toMatchObject({
179
+ reviewNote: 'Logo unreachable.',
180
+ reviewedBy: 'admin@tempo.xyz',
181
+ status: 'denied',
182
+ })
183
+ expect(Date.parse(reviewed!.reviewedAt!)).not.toBeNaN()
184
+ })
185
+
186
+ test('yields undefined once reviewed', async () => {
187
+ const db = TestApp.database()
188
+ const record = await VerifiedTokenRequests.create(db, input)
189
+ await VerifiedTokenRequests.review(db, record!.id, {
190
+ reviewedBy: 'admin@tempo.xyz',
191
+ status: 'approved',
192
+ })
193
+ expect(
194
+ await VerifiedTokenRequests.review(db, record!.id, {
195
+ reviewedBy: 'admin@tempo.xyz',
196
+ status: 'denied',
197
+ }),
198
+ ).toBeUndefined()
199
+ })
200
+ })
@@ -0,0 +1,182 @@
1
+ import { type Selectable, sql } from 'kysely'
2
+
3
+ import * as Id from '../../internal/Id.js'
4
+ import type * as Db from '../Db.js'
5
+ import type * as db_Schema from '../Schema.js'
6
+
7
+ /** Columns of the `verified_token_requests` table. */
8
+ export type Table = db_Schema.VerifiedTokenRequest
9
+ /** A stored verification request. */
10
+ export type Record = Selectable<Table>
11
+ /** Review status of a request. */
12
+ export type Status = Record['status']
13
+
14
+ /**
15
+ * Inserts a pending request; `undefined` when a pending request for
16
+ * (chainId, address) already exists.
17
+ */
18
+ export function create(db: Db.Db, input: create.Input): Promise<Record | undefined> {
19
+ const now = new Date().toISOString()
20
+ return db.kysely
21
+ .insertInto('verified_token_requests')
22
+ .values({
23
+ address: input.address,
24
+ chainId: input.chainId,
25
+ createdAt: now,
26
+ currency: input.currency,
27
+ decimals: input.decimals,
28
+ id: Id.generate('vtr'),
29
+ logo: input.logo ?? null,
30
+ logoUri: input.logoUri ?? null,
31
+ name: input.name,
32
+ note: input.note ?? null,
33
+ orgId: input.orgId,
34
+ requestedBy: input.requestedBy,
35
+ reviewNote: null,
36
+ reviewedAt: null,
37
+ reviewedBy: null,
38
+ status: 'pending',
39
+ symbol: input.symbol,
40
+ updatedAt: now,
41
+ })
42
+ .onConflict((oc) =>
43
+ oc
44
+ .columns(['chainId', 'address'])
45
+ // `sql.lit` keeps the predicate literal; a bound parameter would
46
+ // defeat partial-index inference for the conflict target.
47
+ .where('status', '=', sql.lit('pending'))
48
+ .doNothing(),
49
+ )
50
+ .returningAll()
51
+ .executeTakeFirst()
52
+ }
53
+
54
+ export declare namespace create {
55
+ /** Fields accepted when inserting a request. */
56
+ type Input = {
57
+ /** Requested token contract address (lowercase). */
58
+ address: string
59
+ /** Chain the token belongs to. */
60
+ chainId: number
61
+ /** Display currency, e.g. `USD`. */
62
+ currency: string
63
+ /** Decimal precision. */
64
+ decimals: number
65
+ /** Inline SVG logo markup uploaded by the requester. */
66
+ logo?: string | undefined
67
+ /** HTTPS logo URL from chain metadata. */
68
+ logoUri?: string | undefined
69
+ /** Display name. */
70
+ name: string
71
+ /** Requester note to reviewers. */
72
+ note?: string | undefined
73
+ /** Requesting organization id (`org_…`). */
74
+ orgId: string
75
+ /** User id that submitted the request, or `super_admin`. */
76
+ requestedBy: string
77
+ /** Ticker symbol. */
78
+ symbol: string
79
+ }
80
+ }
81
+
82
+ /** Lists an organization's requests, newest first. */
83
+ export function list(db: Db.Db, orgId: string): Promise<Record[]> {
84
+ return db.kysely
85
+ .selectFrom('verified_token_requests')
86
+ .selectAll()
87
+ .where('orgId', '=', orgId)
88
+ .orderBy('createdAt', 'desc')
89
+ .execute()
90
+ }
91
+
92
+ /**
93
+ * Lists requests in one status globally, oldest first (review-queue order),
94
+ * with the requesting organization's current name when it still exists.
95
+ */
96
+ export function listByStatus(
97
+ db: Db.Db,
98
+ status: Status,
99
+ ): Promise<(Record & { orgName: string | null })[]> {
100
+ return db.kysely
101
+ .selectFrom('verified_token_requests')
102
+ .leftJoin('organizations', 'organizations.id', 'verified_token_requests.orgId')
103
+ .selectAll('verified_token_requests')
104
+ .select('organizations.name as orgName')
105
+ .where('status', '=', status)
106
+ .orderBy('verified_token_requests.createdAt', 'asc')
107
+ .execute()
108
+ }
109
+
110
+ /** Gets a request by id. */
111
+ export function get(db: Db.Db, id: string): Promise<Record | undefined> {
112
+ return db.kysely
113
+ .selectFrom('verified_token_requests')
114
+ .selectAll()
115
+ .where('id', '=', id)
116
+ .executeTakeFirst()
117
+ }
118
+
119
+ /** Counts an organization's pending requests. */
120
+ export async function countPending(db: Db.Db, orgId: string): Promise<number> {
121
+ const row = await db.kysely
122
+ .selectFrom('verified_token_requests')
123
+ .select(({ fn }) => fn.countAll<string>().as('count'))
124
+ .where('orgId', '=', orgId)
125
+ .where('status', '=', 'pending')
126
+ .executeTakeFirstOrThrow()
127
+ return Number(row.count)
128
+ }
129
+
130
+ /** Hard-deletes an org-scoped request while pending; `undefined` otherwise. */
131
+ export function remove(db: Db.Db, orgId: string, id: string): Promise<Record | undefined> {
132
+ return db.kysely
133
+ .deleteFrom('verified_token_requests')
134
+ .where('id', '=', id)
135
+ .where('orgId', '=', orgId)
136
+ .where('status', '=', 'pending')
137
+ .returningAll()
138
+ .executeTakeFirst()
139
+ }
140
+
141
+ /**
142
+ * Marks a pending request approved or denied with reviewer identity;
143
+ * `undefined` when absent or already reviewed.
144
+ */
145
+ export function review(db: Db.Db, id: string, input: review.Input): Promise<Record | undefined> {
146
+ const now = new Date().toISOString()
147
+ return db.kysely
148
+ .updateTable('verified_token_requests')
149
+ .set({
150
+ reviewNote: input.reviewNote ?? null,
151
+ reviewedAt: now,
152
+ reviewedBy: input.reviewedBy,
153
+ status: input.status,
154
+ updatedAt: now,
155
+ })
156
+ .where('id', '=', id)
157
+ .where('status', '=', 'pending')
158
+ .returningAll()
159
+ .executeTakeFirst()
160
+ }
161
+
162
+ export declare namespace review {
163
+ /** Review outcome fields. */
164
+ type Input = {
165
+ /** Reviewer note shown on denial. */
166
+ reviewNote?: string | undefined
167
+ /** Reviewing admin email. */
168
+ reviewedBy: string
169
+ /** Review outcome. */
170
+ status: 'approved' | 'denied'
171
+ }
172
+ }
173
+
174
+ /** Maps a stored row to the wire shape (null `logo`/`logoUri` map to absent). */
175
+ export function toRequest(record: Record) {
176
+ const { logo, logoUri, ...rest } = record
177
+ return {
178
+ ...rest,
179
+ ...(logo === null ? {} : { logo }),
180
+ ...(logoUri === null ? {} : { logoUri }),
181
+ }
182
+ }
@@ -5,6 +5,8 @@ import type * as Webhooks from '../../internal/Webhooks.js'
5
5
  import type * as Db from '../Db.js'
6
6
  import type * as db_Schema from '../Schema.js'
7
7
 
8
+ const subscriptionLimitLockClass = 1464353612
9
+
8
10
  /** Columns of the `webhook_subscriptions` table, derived from `Schema.WebhookSubscription`. */
9
11
  export type Table = db_Schema.WebhookSubscription
10
12
 
@@ -69,6 +71,35 @@ export async function insert(db: Db.Db, subscription: Webhooks.Subscription): Pr
69
71
  .execute()
70
72
  }
71
73
 
74
+ /**
75
+ * Inserts a subscription when its owner remains below the configured cap.
76
+ * An owner-scoped transaction lock serializes the count and insert.
77
+ */
78
+ export async function insertWithinLimit(
79
+ db: Db.Db,
80
+ subscription: Webhooks.Subscription,
81
+ options: insertWithinLimit.Options,
82
+ ): Promise<boolean> {
83
+ return db.transaction(async (tx) => {
84
+ const owner = `${subscription.owner.type}:${ownerId(subscription.owner)}`
85
+ await sql`select pg_advisory_xact_lock(${subscriptionLimitLockClass}, hashtext(${owner}))`.execute(tx.kysely) // prettier-ignore
86
+ const existing = await count(tx, subscription.owner, options.now)
87
+ if (existing >= options.maxPerOwner) return false
88
+ await insert(tx, subscription)
89
+ return true
90
+ })
91
+ }
92
+
93
+ export declare namespace insertWithinLimit {
94
+ /** Options for an atomic capped insert. */
95
+ type Options = {
96
+ /** Maximum live subscriptions the owner may hold. */
97
+ maxPerOwner: number
98
+ /** ISO timestamp used to exclude expired rows from the count. */
99
+ now: string
100
+ }
101
+ }
102
+
72
103
  /**
73
104
  * Reads an owner's subscription by id; expired rows never match (query-time
74
105
  * expiry, same read semantics as the old store TTL).
@@ -6,7 +6,6 @@ import { createMiddleware } from 'hono/factory'
6
6
  import { matchedRoutes } from 'hono/route'
7
7
  import type { MergePath, RouterRoute, Schema as core_Schema } from 'hono/types'
8
8
  import { findTargetHandler } from 'hono/utils/handler'
9
- import { decodeJwt } from 'jose'
10
9
  import { Challenge, Credential, type Method } from 'mppx'
11
10
  import { Mppx, tempo } from 'mppx/hono'
12
11
  import { Addresses } from 'viem/tempo'
@@ -17,6 +16,7 @@ import * as ApiKey from '../ApiKey.js'
17
16
  import * as ApiKeys from '../ApiKeys.js'
18
17
  import * as Db from '../db/Db.js'
19
18
  import type * as Store from './Store.js'
19
+ import * as EarlyAccess from '../db/tables/earlyAccess.js'
20
20
  import * as Invitations from '../db/tables/invitations.js'
21
21
  import * as Memberships from '../db/tables/memberships.js'
22
22
  import * as Metadata from '../apps/metadata.js'
@@ -159,6 +159,8 @@ export type PolicyOverride = {
159
159
  | undefined
160
160
  /** Session lane: `true` allows signed-in wallet sessions, `false` disables. */
161
161
  session?: boolean | undefined
162
+ /** Temporary early-access gate: `true` rejects sessions without early access ({@link tmp_earlyAccess}). TODO: remove after GA. */
163
+ tmp_earlyAccess?: boolean | undefined
162
164
  }
163
165
 
164
166
  /** Authenticated or anonymous caller. */
@@ -227,6 +229,8 @@ export type Variables = {
227
229
  principal?: Principal | undefined
228
230
  /** Project resolved by {@link ensureProject} for the current request. */
229
231
  project?: Projects.Record | undefined
232
+ /** Temporary early-access gate toggle; unset or false grants everyone early access. TODO: remove after GA. */
233
+ tmp_earlyAccess?: boolean | undefined
230
234
  }
231
235
 
232
236
  /** Reads the authenticated principal from a Hono context. */
@@ -346,6 +350,9 @@ export function ensureProject(): MiddlewareHandler {
346
350
  */
347
351
  export const narrowScope = false as boolean
348
352
 
353
+ /** Always false at runtime; lets handlers include organization role errors in inferred response unions. */
354
+ export const narrowOrgRole = false as boolean
355
+
349
356
  /** Typed org-scope error response for Hono client inference. Never reached at runtime. */
350
357
  export function ensureOrgError(c: hono_Context) {
351
358
  return Response.error(c, {
@@ -355,6 +362,15 @@ export function ensureOrgError(c: hono_Context) {
355
362
  })
356
363
  }
357
364
 
365
+ /** Typed organization-role error response for Hono client inference. Never reached at runtime. */
366
+ export function ensureOrgRoleError(c: hono_Context) {
367
+ return Response.error(c, {
368
+ code: 'forbidden',
369
+ message: 'Insufficient organization role',
370
+ status: 403,
371
+ })
372
+ }
373
+
358
374
  /** Typed project-scope error response for Hono client inference. Never reached at runtime. */
359
375
  export function ensureProjectError(c: hono_Context) {
360
376
  // Nested project routes run the org scope first, so either not-found can surface.
@@ -369,6 +385,42 @@ export function ensureProjectError(c: hono_Context) {
369
385
  })
370
386
  }
371
387
 
388
+ /** Whether the caller can grant early access, independent of the temporary route gate. TODO: remove after GA. */
389
+ export async function tmp_earlyAccessGrant(c: hono_Context): Promise<boolean> {
390
+ const principal = getPrincipal(c)
391
+ // Trust chain: `management:write` keys are only mintable by early-access owners.
392
+ if (principal?.type === 'api_key' || principal?.type === 'super_admin') return true
393
+ if (principal?.type !== 'session' || !principal.email) return false
394
+ const email = principal.email.trim().toLowerCase()
395
+ const db = Db.get(contextDb(c))
396
+ if (await EarlyAccess.matches(db, email)) return true
397
+ return Invitations.hasEarlyAccessGrant(db, email)
398
+ }
399
+
400
+ /** Whether the caller has effective early access, including a dormant temporary gate. TODO: remove after GA. */
401
+ export async function tmp_earlyAccess(c: hono_Context): Promise<boolean> {
402
+ const enabled = (c.get as (key: string) => unknown)('tmp_earlyAccess') as boolean | undefined
403
+ if (!enabled) return true
404
+ return tmp_earlyAccessGrant(c)
405
+ }
406
+
407
+ /**
408
+ * Always false at runtime, but typed as `boolean` so handlers can include a
409
+ * never-reached early-access-error branch in Hono's inferred response union
410
+ * (the `tmp_earlyAccess` policy flag produces this `403` before the handler
411
+ * runs). TODO: remove after GA.
412
+ */
413
+ export const tmp_narrowEarlyAccess = false as boolean
414
+
415
+ /** Typed early-access error response for Hono client inference; also the live gate rejection. TODO: remove after GA. */
416
+ export function tmp_ensureEarlyAccessError(c: hono_Context) {
417
+ return Response.error(c, {
418
+ code: 'early_access_required',
419
+ message: 'Requires early access. Ask a teammate with access to invite you by email, or contact Tempo.', // prettier-ignore
420
+ status: 403,
421
+ })
422
+ }
423
+
372
424
  /** Reads the caller's membership resolved by the organization or project guard; `undefined` for API keys and `super_admin`. */
373
425
  export function membership<environment extends Environment>(
374
426
  c: hono_Context<environment>,
@@ -577,6 +629,9 @@ export function middleware<
577
629
  if (management && (!auth.mpp || !endpointPolicy.mpp)) return next()
578
630
 
579
631
  c.set('auth', auth)
632
+ // Consumed by `tmp_earlyAccess`; absent (or false) leaves the gate dormant.
633
+ // TODO: remove after GA.
634
+ c.set('tmp_earlyAccess', options.tmp_earlyAccess)
580
635
  return require<environment>(endpointPolicy)(c, next)
581
636
  })
582
637
  }
@@ -632,6 +687,12 @@ export declare namespace middleware {
632
687
  * checks — before the key lookup runs. Omit to disable the entry entirely.
633
688
  */
634
689
  superAdmin?: { secret: string } | undefined
690
+ /**
691
+ * Temporary invite-only gate for routes flagged via {@link policy}.
692
+ * Allowlist entries live in the `early_access` table (admin dash); unset
693
+ * grants everyone early access. TODO: remove after GA.
694
+ */
695
+ tmp_earlyAccess?: boolean | undefined
635
696
  }
636
697
 
637
698
  /** Resolve-cache tuning for API-key resolution. */
@@ -764,12 +825,6 @@ export function install(options: install.Options) {
764
825
  if (!options_session || !kv) return undefined
765
826
  const { issuer, origin, requireEmail, ttl } =
766
827
  (typeof options_session === 'object' ? options_session.wallet : undefined) ?? {}
767
- // Case-insensitive exact-match email-domain allowlist for new-user sign-ups.
768
- const emailAllowlist = (
769
- typeof options_session === 'object' ? options_session.emailAllowlist : undefined
770
- )
771
- ?.map((domain) => domain.trim().toLowerCase())
772
- .filter(Boolean)
773
828
 
774
829
  const handler = Accounts.Handler.auth({
775
830
  ...(issuer || requireEmail
@@ -834,38 +889,6 @@ export function install(options: install.Options) {
834
889
  const policy_session = policy({ public: true, session: true })
835
890
  const describe = OpenApi.describeRoute({ hide: true })
836
891
 
837
- // Sign-up gate: new addresses need an allowlisted identity email domain
838
- // or a pending invitation; existing users pass. The claim decodes
839
- // unverified; the mounted handler verifies the token before any session
840
- // or user is created, so a forged claim never authenticates.
841
- const gate_signup = createMiddleware<App.Environment>(async (c, next) => {
842
- if (!emailAllowlist?.length) return next()
843
- const { address, idToken } = (await c.req.json()) as z.output<typeof schema.verify.Body>
844
- const database = Db.get(db)
845
- const user = await Users.getByAddress(database, address)
846
- if (!user) {
847
- const email = (() => {
848
- if (!idToken) return undefined
849
- try {
850
- const { email } = decodeJwt<{ email?: string }>(idToken)
851
- return typeof email === 'string' ? email.toLowerCase() : undefined
852
- } catch {
853
- return undefined
854
- }
855
- })()
856
- const allowed = await (async () => {
857
- if (!email) return false
858
- const at = email.lastIndexOf('@')
859
- if (at > 0 && emailAllowlist.includes(email.slice(at + 1))) return true
860
- // A pending invitation admits the email regardless of domain.
861
- return (await Invitations.listByEmail(database, email)).length > 0
862
- })()
863
- if (!allowed)
864
- return c.json({ error: 'sign-ups are restricted to approved email domains or invited emails' }, 403) // prettier-ignore
865
- }
866
- return next()
867
- })
868
-
869
892
  const app = new Hono<App.Environment>()
870
893
  .use('/v1/auth/siwe', policy_public, describe)
871
894
  .use('/v1/auth/siwe/*', policy_public, describe)
@@ -906,7 +929,6 @@ export function install(options: install.Options) {
906
929
  code: 'body_invalid',
907
930
  message: 'Invalid request body',
908
931
  }),
909
- gate_signup,
910
932
  OpenApi.describeRoute({
911
933
  description:
912
934
  'Verifies the signed challenge and establishes a session. Sets a session cookie by default; pass `returnToken` to receive a bearer token instead.',
@@ -926,9 +948,6 @@ export function install(options: install.Options) {
926
948
  },
927
949
  400: handlerError('Invalid, expired, or mismatched challenge message; or a required identity token is missing.'), // prettier-ignore
928
950
  401: handlerError('Signature or identity verification failed.'),
929
- ...(emailAllowlist?.length
930
- ? { 403: handlerError('Sign-up rejected: the identity email is neither on an allowlisted domain nor pending an invitation.') } // prettier-ignore
931
- : {}),
932
951
  409: handlerError('Challenge nonce already used or unknown.'),
933
952
  429: OpenApi.standardError(429, 'Rate limited.'),
934
953
  500: OpenApi.standardError(500, 'Internal server error.'),
@@ -997,8 +1016,6 @@ export declare namespace install {
997
1016
  type Session =
998
1017
  | boolean
999
1018
  | {
1000
- /** Restrict new-user sign-ups to verified emails on these domains (case-insensitive exact match); invited emails and existing users sign in unaffected. */
1001
- emailAllowlist?: readonly string[] | undefined
1002
1019
  /** SIWE wallet sign-in configuration. */
1003
1020
  wallet?: Wallet | undefined
1004
1021
  }
@@ -1179,6 +1196,7 @@ function resolvePolicy(
1179
1196
  let mpp = base.mpp
1180
1197
  let public_ = base.public
1181
1198
  let session = base.session
1199
+ let tmp_earlyAccess = base.tmp_earlyAccess
1182
1200
 
1183
1201
  for (const override of overrides) {
1184
1202
  if (override === false) return undefined
@@ -1187,6 +1205,7 @@ function resolvePolicy(
1187
1205
  mpp = resolveMppLane(mpp, override.mpp, defaults.mpp)
1188
1206
  public_ = resolvePublicLane(public_, override.public, defaults.public)
1189
1207
  session = resolveSessionLane(session, override.session, defaults.session)
1208
+ tmp_earlyAccess = override.tmp_earlyAccess ?? tmp_earlyAccess
1190
1209
  }
1191
1210
 
1192
1211
  // The OpenAPI summary/description rides along as the payment-challenge
@@ -1202,6 +1221,7 @@ function resolvePolicy(
1202
1221
  ...(mpp ? { mpp } : {}),
1203
1222
  ...(public_ ? { public: public_ } : {}),
1204
1223
  ...(session ? { session } : {}),
1224
+ ...(tmp_earlyAccess ? { tmp_earlyAccess } : {}),
1205
1225
  }
1206
1226
  }
1207
1227
 
@@ -1424,6 +1444,12 @@ export function require<environment extends Environment = Environment>(
1424
1444
  )
1425
1445
  return Response.unsupportedChainId(c, chainId, supportedChainIds)
1426
1446
  const proceed = async () => {
1447
+ // Runs after principal resolution, so api_key and super_admin pass by
1448
+ // type; only gated sessions are rejected. TODO: remove after GA.
1449
+ if (policy.tmp_earlyAccess && !(await tmp_earlyAccess(c))) {
1450
+ c.res = tmp_ensureEarlyAccessError(c)
1451
+ return
1452
+ }
1427
1453
  await next()
1428
1454
  // Query credential responses must not enter shared caches.
1429
1455
  const cacheControl = c.res.headers.get('Cache-Control')?.toLowerCase()
@@ -1659,6 +1685,8 @@ export declare namespace require {
1659
1685
  | undefined
1660
1686
  /** Session access policy: `true` allows signed-in wallet sessions. */
1661
1687
  session?: boolean | undefined
1688
+ /** Temporary early-access gate for this route; see {@link tmp_earlyAccess}. TODO: remove after GA. */
1689
+ tmp_earlyAccess?: boolean | undefined
1662
1690
  }
1663
1691
  }
1664
1692