tapimo 0.4.3 → 0.5.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 (192) hide show
  1. package/dist/App.d.ts +7222 -6316
  2. package/dist/App.d.ts.map +1 -1
  3. package/dist/App.js +4 -0
  4. package/dist/App.js.map +1 -1
  5. package/dist/Scope.d.ts +18 -1
  6. package/dist/Scope.d.ts.map +1 -1
  7. package/dist/Scope.js +24 -3
  8. package/dist/Scope.js.map +1 -1
  9. package/dist/admin/apps/verified-tokens.d.ts +116 -116
  10. package/dist/apps/data/App.d.ts +2541 -2607
  11. package/dist/apps/data/App.d.ts.map +1 -1
  12. package/dist/apps/data/App.js +135 -44
  13. package/dist/apps/data/App.js.map +1 -1
  14. package/dist/apps/data/FundingQuoteProviders.d.ts +36 -0
  15. package/dist/apps/data/FundingQuoteProviders.d.ts.map +1 -0
  16. package/dist/apps/data/FundingQuoteProviders.js +190 -0
  17. package/dist/apps/data/FundingQuoteProviders.js.map +1 -0
  18. package/dist/apps/data/index.d.ts +1 -1
  19. package/dist/apps/data/index.js +1 -1
  20. package/dist/apps/data/routes/activities.d.ts +242 -242
  21. package/dist/apps/data/routes/addresses.d.ts +227 -227
  22. package/dist/apps/data/routes/balances.d.ts +12 -12
  23. package/dist/apps/data/routes/balances.d.ts.map +1 -1
  24. package/dist/apps/data/routes/balances.js +67 -149
  25. package/dist/apps/data/routes/balances.js.map +1 -1
  26. package/dist/apps/data/routes/blocks.d.ts +125 -126
  27. package/dist/apps/data/routes/blocks.d.ts.map +1 -1
  28. package/dist/apps/data/routes/blocks.js +3 -5
  29. package/dist/apps/data/routes/blocks.js.map +1 -1
  30. package/dist/apps/data/routes/coingecko.d.ts +55 -54
  31. package/dist/apps/data/routes/coingecko.d.ts.map +1 -1
  32. package/dist/apps/data/routes/coingecko.js +3 -8
  33. package/dist/apps/data/routes/coingecko.js.map +1 -1
  34. package/dist/apps/data/routes/exchanges.d.ts +455 -455
  35. package/dist/apps/data/routes/fee-amm.d.ts +92 -92
  36. package/dist/apps/data/routes/funding.d.ts +91 -864
  37. package/dist/apps/data/routes/funding.d.ts.map +1 -1
  38. package/dist/apps/data/routes/funding.js +138 -450
  39. package/dist/apps/data/routes/funding.js.map +1 -1
  40. package/dist/apps/data/routes/rpc.d.ts.map +1 -1
  41. package/dist/apps/data/routes/rpc.js +86 -16
  42. package/dist/apps/data/routes/rpc.js.map +1 -1
  43. package/dist/apps/data/routes/tokenlist.d.ts +36 -36
  44. package/dist/apps/data/routes/webhooks.d.ts +340 -0
  45. package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
  46. package/dist/apps/data/routes/webhooks.js +13 -1
  47. package/dist/apps/data/routes/webhooks.js.map +1 -1
  48. package/dist/apps/index.d.ts +1 -1
  49. package/dist/apps/index.js +1 -1
  50. package/dist/apps/management/App.d.ts +1843 -861
  51. package/dist/apps/management/App.d.ts.map +1 -1
  52. package/dist/apps/management/App.js +4 -0
  53. package/dist/apps/management/App.js.map +1 -1
  54. package/dist/apps/management/routes/api-keys.d.ts +121 -121
  55. package/dist/apps/management/routes/api-keys.js +2 -2
  56. package/dist/apps/management/routes/api-keys.js.map +1 -1
  57. package/dist/apps/management/routes/billing.d.ts +2 -2
  58. package/dist/apps/management/routes/invite-links.d.ts +1100 -0
  59. package/dist/apps/management/routes/invite-links.d.ts.map +1 -0
  60. package/dist/apps/management/routes/invite-links.js +411 -0
  61. package/dist/apps/management/routes/invite-links.js.map +1 -0
  62. package/dist/apps/management/routes/me.d.ts +13 -13
  63. package/dist/apps/management/routes/members.d.ts +77 -77
  64. package/dist/apps/management/routes/orgs.d.ts +96 -96
  65. package/dist/apps/management/routes/projects.d.ts +62 -62
  66. package/dist/apps/management/routes/scopes.d.ts +11 -11
  67. package/dist/apps/management/routes/scopes.d.ts.map +1 -1
  68. package/dist/apps/management/routes/scopes.js +2 -2
  69. package/dist/apps/management/routes/scopes.js.map +1 -1
  70. package/dist/apps/management/routes/usage.d.ts +79 -79
  71. package/dist/apps/mcp.d.ts.map +1 -1
  72. package/dist/apps/mcp.js +3 -1
  73. package/dist/apps/mcp.js.map +1 -1
  74. package/dist/apps/relay/App.d.ts.map +1 -1
  75. package/dist/apps/relay/App.js +42 -4
  76. package/dist/apps/relay/App.js.map +1 -1
  77. package/dist/cloudflare.d.ts +2 -0
  78. package/dist/cloudflare.d.ts.map +1 -1
  79. package/dist/cloudflare.js +4 -0
  80. package/dist/cloudflare.js.map +1 -1
  81. package/dist/db/Db.d.ts +3 -0
  82. package/dist/db/Db.d.ts.map +1 -1
  83. package/dist/db/Db.js +57 -0
  84. package/dist/db/Db.js.map +1 -1
  85. package/dist/db/Schema.d.ts +31 -0
  86. package/dist/db/Schema.d.ts.map +1 -1
  87. package/dist/db/Schema.js +31 -0
  88. package/dist/db/Schema.js.map +1 -1
  89. package/dist/db/tables/inviteLinks.d.ts +42 -0
  90. package/dist/db/tables/inviteLinks.d.ts.map +1 -0
  91. package/dist/db/tables/inviteLinks.js +148 -0
  92. package/dist/db/tables/inviteLinks.js.map +1 -0
  93. package/dist/db/tables/organizations.d.ts.map +1 -1
  94. package/dist/db/tables/organizations.js +2 -0
  95. package/dist/db/tables/organizations.js.map +1 -1
  96. package/dist/handlers/relay.d.ts.map +1 -1
  97. package/dist/handlers/relay.js +13 -3
  98. package/dist/handlers/relay.js.map +1 -1
  99. package/dist/internal/Auth.d.ts +42 -23
  100. package/dist/internal/Auth.d.ts.map +1 -1
  101. package/dist/internal/Auth.js +169 -20
  102. package/dist/internal/Auth.js.map +1 -1
  103. package/dist/internal/EdgeCache.d.ts +3 -1
  104. package/dist/internal/EdgeCache.d.ts.map +1 -1
  105. package/dist/internal/EdgeCache.js +17 -2
  106. package/dist/internal/EdgeCache.js.map +1 -1
  107. package/dist/internal/Errors.d.ts +6 -0
  108. package/dist/internal/Errors.d.ts.map +1 -0
  109. package/dist/internal/Errors.js +16 -0
  110. package/dist/internal/Errors.js.map +1 -0
  111. package/dist/internal/FundingQuotes.d.ts +644 -0
  112. package/dist/internal/FundingQuotes.d.ts.map +1 -0
  113. package/dist/internal/{FundingRoutes.js → FundingQuotes.js} +93 -289
  114. package/dist/internal/FundingQuotes.js.map +1 -0
  115. package/dist/internal/Store.d.ts +61 -16
  116. package/dist/internal/Store.d.ts.map +1 -1
  117. package/dist/internal/Store.js +72 -17
  118. package/dist/internal/Store.js.map +1 -1
  119. package/dist/internal/Viem.d.ts +2 -0
  120. package/dist/internal/Viem.d.ts.map +1 -1
  121. package/dist/internal/Viem.js +11 -2
  122. package/dist/internal/Viem.js.map +1 -1
  123. package/dist/internal/index.d.ts +27 -0
  124. package/dist/internal/index.d.ts.map +1 -0
  125. package/dist/internal/index.js +27 -0
  126. package/dist/internal/index.js.map +1 -0
  127. package/package.json +8 -3
  128. package/src/ApiKeys.test.ts +23 -0
  129. package/src/App.test-d.ts +2 -0
  130. package/src/App.test.ts +60 -4
  131. package/src/App.ts +6 -0
  132. package/src/Client.test-d.ts +16 -54
  133. package/src/Scope.test.ts +1 -0
  134. package/src/Scope.ts +30 -4
  135. package/src/apps/data/App.test.ts +354 -0
  136. package/src/apps/data/App.ts +159 -52
  137. package/src/apps/data/FundingQuoteProviders.test.ts +151 -0
  138. package/src/apps/data/FundingQuoteProviders.ts +251 -0
  139. package/src/apps/data/index.ts +1 -1
  140. package/src/apps/data/routes/balances.test.ts +64 -18
  141. package/src/apps/data/routes/balances.ts +76 -177
  142. package/src/apps/data/routes/blocks.ts +3 -5
  143. package/src/apps/data/routes/coingecko.ts +3 -8
  144. package/src/apps/data/routes/funding.test.ts +149 -306
  145. package/src/apps/data/routes/funding.ts +154 -604
  146. package/src/apps/data/routes/indexer.test.ts +99 -3
  147. package/src/apps/data/routes/rpc.test.ts +219 -0
  148. package/src/apps/data/routes/rpc.ts +92 -17
  149. package/src/apps/data/routes/webhooks.test.ts +23 -2
  150. package/src/apps/data/routes/webhooks.ts +13 -1
  151. package/src/apps/index.ts +1 -1
  152. package/src/apps/management/App.ts +4 -0
  153. package/src/apps/management/routes/api-keys.test.ts +47 -3
  154. package/src/apps/management/routes/api-keys.ts +2 -2
  155. package/src/apps/management/routes/invite-links.test.ts +385 -0
  156. package/src/apps/management/routes/invite-links.ts +570 -0
  157. package/src/apps/management/routes/scopes.test.ts +3 -2
  158. package/src/apps/management/routes/scopes.ts +4 -2
  159. package/src/apps/mcp.test.ts +55 -14
  160. package/src/apps/mcp.ts +3 -1
  161. package/src/apps/relay/App.test.ts +324 -4
  162. package/src/apps/relay/App.ts +40 -4
  163. package/src/cloudflare.ts +5 -0
  164. package/src/db/Db.ts +65 -0
  165. package/src/db/Schema.ts +39 -0
  166. package/src/db/tables/inviteLinks.ts +187 -0
  167. package/src/db/tables/organizations.ts +2 -0
  168. package/src/handlers/relay.ts +16 -4
  169. package/src/internal/Auth.test.ts +528 -37
  170. package/src/internal/Auth.ts +246 -45
  171. package/src/internal/EdgeCache.test.ts +24 -3
  172. package/src/internal/EdgeCache.ts +18 -2
  173. package/src/internal/FundingQuotes.test.ts +174 -0
  174. package/src/internal/FundingQuotes.ts +735 -0
  175. package/src/internal/Log.test.ts +4 -4
  176. package/src/internal/Store.test-d.ts +15 -0
  177. package/src/internal/Store.test.ts +153 -2
  178. package/src/internal/Store.ts +157 -41
  179. package/src/internal/Viem.ts +14 -5
  180. package/src/internal/index.test.ts +34 -0
  181. package/src/internal/index.ts +26 -0
  182. package/dist/apps/data/FundingRouteProviders.d.ts +0 -143
  183. package/dist/apps/data/FundingRouteProviders.d.ts.map +0 -1
  184. package/dist/apps/data/FundingRouteProviders.js +0 -546
  185. package/dist/apps/data/FundingRouteProviders.js.map +0 -1
  186. package/dist/internal/FundingRoutes.d.ts +0 -1085
  187. package/dist/internal/FundingRoutes.d.ts.map +0 -1
  188. package/dist/internal/FundingRoutes.js.map +0 -1
  189. package/src/apps/data/FundingRouteProviders.test.ts +0 -485
  190. package/src/apps/data/FundingRouteProviders.ts +0 -744
  191. package/src/internal/FundingRoutes.test.ts +0 -239
  192. package/src/internal/FundingRoutes.ts +0 -1067
@@ -0,0 +1,570 @@
1
+ import { type Context, Hono } from 'hono'
2
+ import * as z from 'zod/mini'
3
+
4
+ import * as Db from '../../../db/Db.js'
5
+ import * as InviteLinks from '../../../db/tables/inviteLinks.js'
6
+ import * as Organizations from '../../../db/tables/organizations.js'
7
+ import * as Auth from '../../../internal/Auth.js'
8
+ import * as OpenApi from '../../../internal/OpenApi.js'
9
+ import * as Response from '../../../internal/Response.js'
10
+ import * as Schema from '../../../internal/Schema.js'
11
+ import type { Environment } from '../App.js'
12
+
13
+ /** OpenAPI schemas owned by reusable invite links. */
14
+ export namespace schema {
15
+ const nullableExpiry = z
16
+ .nullable(z.iso.datetime({ offset: true }))
17
+ .check(
18
+ z.describe('When the link expires (ISO 8601), or null for no expiry.'),
19
+ z.meta({ examples: ['2026-01-08T00:00:00.000Z', null] }),
20
+ )
21
+ const nullableUses = z
22
+ .nullable(z.number().check(z.int(), z.positive()))
23
+ .check(
24
+ z.describe('Maximum memberships the link may create, or null for unlimited.'),
25
+ z.meta({ examples: [25, null] }),
26
+ )
27
+ const role = z
28
+ .literal('member')
29
+ .check(z.describe('Role granted by the link.'), z.meta({ examples: ['member'] }))
30
+
31
+ /** Management representation, including the recopyable bearer id. */
32
+ export const InviteLink = Schema.describe(
33
+ z.object({
34
+ createdAt: z
35
+ .string()
36
+ .check(
37
+ z.describe('When the link was created (ISO 8601).'),
38
+ z.meta({ examples: ['2026-01-01T00:00:00.000Z'] }),
39
+ ),
40
+ createdBy: z
41
+ .string()
42
+ .check(
43
+ z.describe('User id that created the link.'),
44
+ z.meta({ examples: ['usr_1a2b3c4d5e6f7g8h9j0k1m2n'] }),
45
+ ),
46
+ enabled: z
47
+ .boolean()
48
+ .check(z.describe('Whether the link is enabled.'), z.meta({ examples: [true] })),
49
+ expiresAt: nullableExpiry,
50
+ id: z
51
+ .string()
52
+ .check(
53
+ z.describe('Opaque invite-link resource id (`iln_…`).'),
54
+ z.meta({ examples: ['iln_1a2b3c4d5e6f7g8h9j0k1m2n'] }),
55
+ ),
56
+ lastUsedAt: z
57
+ .nullable(z.string())
58
+ .check(
59
+ z.describe('When the link last created a membership, or null.'),
60
+ z.meta({ examples: ['2026-01-02T00:00:00.000Z', null] }),
61
+ ),
62
+ maxUses: nullableUses,
63
+ name: z
64
+ .string()
65
+ .check(z.describe('Human-readable link name.'), z.meta({ examples: ['Community'] })),
66
+ orgId: z
67
+ .string()
68
+ .check(
69
+ z.describe('Organization id (`org_…`) the link joins.'),
70
+ z.meta({ examples: ['org_1a2b3c4d5e6f7g8h9j0k1m2n'] }),
71
+ ),
72
+ role,
73
+ status: z
74
+ .enum(['active', 'deleted', 'disabled', 'exhausted', 'expired'])
75
+ .check(z.describe('Current link availability.'), z.meta({ examples: ['active'] })),
76
+ token: z
77
+ .string()
78
+ .check(
79
+ z.describe('Opaque bearer token embedded in the recipient URL (`lnk_…`).'),
80
+ z.meta({ examples: ['lnk_1a2b3c4d5e6f7g8h9j0k1m2n'] }),
81
+ ),
82
+ updatedAt: z
83
+ .string()
84
+ .check(
85
+ z.describe('When the link was last changed (ISO 8601).'),
86
+ z.meta({ examples: ['2026-01-01T00:00:00.000Z'] }),
87
+ ),
88
+ useCount: z
89
+ .number()
90
+ .check(
91
+ z.int(),
92
+ z.nonnegative(),
93
+ z.describe('Memberships created through the link.'),
94
+ z.meta({ examples: [3] }),
95
+ ),
96
+ }),
97
+ 'A reusable organization invite link.',
98
+ )
99
+
100
+ /** Redemption audit representation (never includes a bearer URL). */
101
+ export const Redemption = Schema.describe(
102
+ z.object({
103
+ createdAt: z
104
+ .string()
105
+ .check(
106
+ z.describe('When the membership was created (ISO 8601).'),
107
+ z.meta({ examples: ['2026-01-02T00:00:00.000Z'] }),
108
+ ),
109
+ email: z
110
+ .string()
111
+ .check(
112
+ z.describe('Verified email used for redemption.'),
113
+ z.meta({ examples: ['dev@example.com'] }),
114
+ ),
115
+ id: z
116
+ .string()
117
+ .check(
118
+ z.describe('Opaque redemption id (`ilr_…`).'),
119
+ z.meta({ examples: ['ilr_1a2b3c4d5e6f7g8h9j0k1m2n'] }),
120
+ ),
121
+ inviteLinkId: z
122
+ .string()
123
+ .check(
124
+ z.describe('Invite-link resource id used for redemption.'),
125
+ z.meta({ examples: ['iln_1a2b3c4d5e6f7g8h9j0k1m2n'] }),
126
+ ),
127
+ inviteLinkName: z
128
+ .string()
129
+ .check(z.describe('Link name at redemption time.'), z.meta({ examples: ['Community'] })),
130
+ orgId: z
131
+ .string()
132
+ .check(
133
+ z.describe('Organization id joined through the link.'),
134
+ z.meta({ examples: ['org_1a2b3c4d5e6f7g8h9j0k1m2n'] }),
135
+ ),
136
+ userId: z
137
+ .string()
138
+ .check(
139
+ z.describe('User id that redeemed the link.'),
140
+ z.meta({ examples: ['usr_1a2b3c4d5e6f7g8h9j0k1m2n'] }),
141
+ ),
142
+ }),
143
+ 'An invite-link redemption audit record.',
144
+ )
145
+
146
+ /** Create body. */
147
+ export const CreateBody = z
148
+ .object({
149
+ expiresAt: z.optional(nullableExpiry),
150
+ maxUses: z.optional(nullableUses),
151
+ name: z.optional(
152
+ z
153
+ .string()
154
+ .check(
155
+ z.trim(),
156
+ z.minLength(1),
157
+ z.maxLength(100),
158
+ z.describe('Human-readable link name.'),
159
+ z.meta({ examples: ['Community'] }),
160
+ ),
161
+ ),
162
+ role: z
163
+ ._default(role, 'member')
164
+ .check(z.describe('Role granted by the link.'), z.meta({ examples: ['member'] })),
165
+ })
166
+ .check(z.describe('Fields for creating an invite link.'))
167
+
168
+ /** Update body. */
169
+ export const UpdateBody = z
170
+ .object({
171
+ enabled: z
172
+ .boolean()
173
+ .check(z.describe('Whether the link is enabled.'), z.meta({ examples: [false] })),
174
+ })
175
+ .check(z.describe('Fields for updating an invite link.'))
176
+
177
+ /** Bearer-token body for recipient operations. */
178
+ export const TokenBody = z
179
+ .object({
180
+ token: z
181
+ .string()
182
+ .check(
183
+ z.describe('Opaque invite-link bearer token.'),
184
+ z.meta({ examples: ['lnk_1a2b3c4d5e6f7g8h9j0k1m2n'] }),
185
+ ),
186
+ })
187
+ .check(z.describe('An invite-link bearer token.'))
188
+
189
+ /** Public active-link summary. */
190
+ export const ResolveResponse = Schema.describe(
191
+ z.object({
192
+ orgName: z
193
+ .string()
194
+ .check(
195
+ z.describe('Human-readable name of the organization.'),
196
+ z.meta({ examples: ['Acme, Inc.'] }),
197
+ ),
198
+ role,
199
+ }),
200
+ 'A public summary of an active invite link.',
201
+ )
202
+
203
+ /** Successful acceptance. */
204
+ export const AcceptResponse = Schema.describe(
205
+ z.object({
206
+ orgId: z
207
+ .string()
208
+ .check(
209
+ z.describe('Organization id joined through the link.'),
210
+ z.meta({ examples: ['org_1a2b3c4d5e6f7g8h9j0k1m2n'] }),
211
+ ),
212
+ role,
213
+ userId: z
214
+ .string()
215
+ .check(
216
+ z.describe('Member user id.'),
217
+ z.meta({ examples: ['usr_1a2b3c4d5e6f7g8h9j0k1m2n'] }),
218
+ ),
219
+ }),
220
+ 'The membership granted through an invite link.',
221
+ )
222
+
223
+ /** Link collection response. */
224
+ export const ListResponse = Schema.describe(
225
+ z.object({
226
+ data: z
227
+ .array(InviteLink)
228
+ .check(z.describe('Invite links, newest first.'), z.meta({ examples: [[]] })),
229
+ }),
230
+ 'A non-paginated list of invite links.',
231
+ )
232
+
233
+ /** Redemption collection response. */
234
+ export const RedemptionsResponse = Schema.describe(
235
+ z.object({
236
+ data: z
237
+ .array(Redemption)
238
+ .check(z.describe('Redemptions, newest first.'), z.meta({ examples: [[]] })),
239
+ }),
240
+ 'A non-paginated list of invite-link redemptions.',
241
+ )
242
+
243
+ /** Path parameters addressing an organization's invite links. */
244
+ export const Params = z
245
+ .object({
246
+ orgId: z
247
+ .string()
248
+ .check(
249
+ z.describe('Organization id (`org_…`).'),
250
+ z.meta({ examples: ['org_1a2b3c4d5e6f7g8h9j0k1m2n'] }),
251
+ ),
252
+ })
253
+ .check(z.describe("Path parameters for an organization's invite links."))
254
+
255
+ /** Path parameters addressing one organization invite link. */
256
+ export const LinkParams = z
257
+ .object({
258
+ inviteLinkId: z
259
+ .string()
260
+ .check(
261
+ z.describe('Invite-link resource id (`iln_…`).'),
262
+ z.meta({ examples: ['iln_1a2b3c4d5e6f7g8h9j0k1m2n'] }),
263
+ ),
264
+ orgId: z
265
+ .string()
266
+ .check(
267
+ z.describe('Organization id (`org_…`).'),
268
+ z.meta({ examples: ['org_1a2b3c4d5e6f7g8h9j0k1m2n'] }),
269
+ ),
270
+ })
271
+ .check(z.describe('Path parameters for one organization invite link.'))
272
+
273
+ /** Delete confirmation. */
274
+ export const DeleteResponse = z
275
+ .object({
276
+ id: z
277
+ .string()
278
+ .check(
279
+ z.describe('Deleted invite-link resource id.'),
280
+ z.meta({ examples: ['iln_1a2b3c4d5e6f7g8h9j0k1m2n'] }),
281
+ ),
282
+ })
283
+ .check(z.describe('Confirmation that an invite link was deleted.'))
284
+ }
285
+
286
+ const errors = { 404: 'No accessible invite link was found.' } as const
287
+ const bodyValidation = {
288
+ code: 'body_invalid',
289
+ message: 'Check the request body and try again.',
290
+ } as const
291
+ const paramValidation = {
292
+ code: 'param_invalid',
293
+ message: 'Check the path parameters and try again.',
294
+ } as const
295
+
296
+ /** Mounts reusable invite-link management and recipient operations. */
297
+ export function inviteLinks() {
298
+ return new Hono<Environment>()
299
+ .post(
300
+ '/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/invite-links',
301
+ Auth.policy({ session: true }),
302
+ Auth.ensureOrg({ role: 'admin' }),
303
+ OpenApi.validate('param', schema.Params, paramValidation),
304
+ OpenApi.validate('json', schema.CreateBody, bodyValidation),
305
+ OpenApi.describeRoute({
306
+ operationId: 'createInviteLink',
307
+ responses: OpenApi.responses({
308
+ errors,
309
+ success: { description: 'Created link.', schema: schema.InviteLink },
310
+ }),
311
+ summary: 'Create invite link',
312
+ tags: ['Invite Links'],
313
+ }),
314
+ async (c) => {
315
+ if (Auth.narrowAccess) return Auth.superAdminAccessError(c)
316
+ if (Auth.narrowScope) return Auth.ensureOrgError(c)
317
+ if (OpenApi.narrowValidation) return OpenApi.validationError(c, bodyValidation)
318
+ const body = c.req.valid('json')
319
+ try {
320
+ const record = await InviteLinks.create(Db.get(c.get('db')), {
321
+ createdBy: Auth.membership(c)?.userId ?? 'super_admin',
322
+ expiresAt:
323
+ body.expiresAt === undefined
324
+ ? new Date(Date.now() + 7 * 86_400_000).toISOString()
325
+ : normalizeExpiry(body.expiresAt),
326
+ maxUses: body.maxUses ?? null,
327
+ ...(body.name === undefined ? {} : { name: body.name }),
328
+ orgId: Auth.org(c).id,
329
+ role: body.role,
330
+ })
331
+ return c.json(Response.validated(schema.InviteLink, serialize(record)), 200)
332
+ } catch (cause) {
333
+ return Response.upstream(c, cause)
334
+ }
335
+ },
336
+ )
337
+ .get(
338
+ '/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/invite-links',
339
+ Auth.policy({ session: true }),
340
+ Auth.ensureOrg({ role: 'admin' }),
341
+ OpenApi.validate('param', schema.Params, paramValidation),
342
+ OpenApi.describeRoute({
343
+ operationId: 'listInviteLinks',
344
+ responses: OpenApi.responses({
345
+ errors,
346
+ success: { description: 'Invite links.', schema: schema.ListResponse },
347
+ }),
348
+ summary: 'List invite links',
349
+ tags: ['Invite Links'],
350
+ }),
351
+ async (c) => {
352
+ if (Auth.narrowAccess) return Auth.superAdminAccessError(c)
353
+ if (Auth.narrowScope) return Auth.ensureOrgError(c)
354
+ if (OpenApi.narrowValidation) return OpenApi.validationError(c, paramValidation)
355
+ try {
356
+ return c.json(
357
+ Response.validated(schema.ListResponse, {
358
+ data: (await InviteLinks.list(Db.get(c.get('db')), Auth.org(c).id)).map(serialize),
359
+ }),
360
+ 200,
361
+ )
362
+ } catch (cause) {
363
+ return Response.upstream(c, cause)
364
+ }
365
+ },
366
+ )
367
+ .patch(
368
+ '/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/invite-links/:inviteLinkId{iln_[A-Za-z0-9_-]+}',
369
+ Auth.policy({ session: true }),
370
+ Auth.ensureOrg({ role: 'admin' }),
371
+ OpenApi.validate('param', schema.LinkParams, paramValidation),
372
+ OpenApi.validate('json', schema.UpdateBody, bodyValidation),
373
+ OpenApi.describeRoute({
374
+ operationId: 'updateInviteLink',
375
+ responses: OpenApi.responses({
376
+ errors,
377
+ success: { description: 'Updated link.', schema: schema.InviteLink },
378
+ }),
379
+ summary: 'Update invite link',
380
+ tags: ['Invite Links'],
381
+ }),
382
+ async (c) => {
383
+ if (Auth.narrowAccess) return Auth.superAdminAccessError(c)
384
+ if (Auth.narrowScope) return Auth.ensureOrgError(c)
385
+ if (OpenApi.narrowValidation) return OpenApi.validationError(c, bodyValidation)
386
+ const body = c.req.valid('json')
387
+ try {
388
+ const record = await InviteLinks.update(
389
+ Db.get(c.get('db')),
390
+ Auth.org(c).id,
391
+ c.req.valid('param').inviteLinkId,
392
+ body.enabled,
393
+ )
394
+ if (!record) return notFound(c)
395
+ return c.json(Response.validated(schema.InviteLink, serialize(record)), 200)
396
+ } catch (cause) {
397
+ return Response.upstream(c, cause)
398
+ }
399
+ },
400
+ )
401
+ .delete(
402
+ '/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/invite-links/:inviteLinkId{iln_[A-Za-z0-9_-]+}',
403
+ Auth.policy({ session: true }),
404
+ Auth.ensureOrg({ role: 'admin' }),
405
+ OpenApi.validate('param', schema.LinkParams, paramValidation),
406
+ OpenApi.describeRoute({
407
+ operationId: 'deleteInviteLink',
408
+ responses: OpenApi.responses({
409
+ errors,
410
+ success: { description: 'Deleted link.', schema: schema.DeleteResponse },
411
+ }),
412
+ summary: 'Delete invite link',
413
+ tags: ['Invite Links'],
414
+ }),
415
+ async (c) => {
416
+ if (Auth.narrowAccess) return Auth.superAdminAccessError(c)
417
+ if (Auth.narrowScope) return Auth.ensureOrgError(c)
418
+ if (OpenApi.narrowValidation) return OpenApi.validationError(c, paramValidation)
419
+ try {
420
+ const id = c.req.valid('param').inviteLinkId
421
+ if (!(await InviteLinks.remove(Db.get(c.get('db')), Auth.org(c).id, id)))
422
+ return notFound(c)
423
+ return c.json({ id }, 200)
424
+ } catch (cause) {
425
+ return Response.upstream(c, cause)
426
+ }
427
+ },
428
+ )
429
+ .get(
430
+ '/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/invite-links/:inviteLinkId{iln_[A-Za-z0-9_-]+}/redemptions',
431
+ Auth.policy({ session: true }),
432
+ Auth.ensureOrg({ role: 'admin' }),
433
+ OpenApi.validate('param', schema.LinkParams, paramValidation),
434
+ OpenApi.describeRoute({
435
+ operationId: 'listInviteLinkRedemptions',
436
+ responses: OpenApi.responses({
437
+ errors,
438
+ success: { description: 'Redemptions.', schema: schema.RedemptionsResponse },
439
+ }),
440
+ summary: 'List link redemptions',
441
+ tags: ['Invite Links'],
442
+ }),
443
+ async (c) => {
444
+ if (Auth.narrowAccess) return Auth.superAdminAccessError(c)
445
+ if (Auth.narrowScope) return Auth.ensureOrgError(c)
446
+ if (OpenApi.narrowValidation) return OpenApi.validationError(c, paramValidation)
447
+ try {
448
+ const db = Db.get(c.get('db'))
449
+ const id = c.req.valid('param').inviteLinkId
450
+ const link = await InviteLinks.get(db, id)
451
+ if (!link || link.orgId !== Auth.org(c).id) return notFound(c)
452
+ return c.json(
453
+ Response.validated(schema.RedemptionsResponse, {
454
+ data: await InviteLinks.listRedemptions(db, Auth.org(c).id, id),
455
+ }),
456
+ 200,
457
+ )
458
+ } catch (cause) {
459
+ return Response.upstream(c, cause)
460
+ }
461
+ },
462
+ )
463
+ .post(
464
+ '/v1/invite-links/resolve',
465
+ Auth.policy({ public: true, session: true }),
466
+ OpenApi.validate('json', schema.TokenBody, bodyValidation),
467
+ OpenApi.describeRoute({
468
+ operationId: 'resolveInviteLink',
469
+ responses: OpenApi.responses({
470
+ errors,
471
+ success: { description: 'Active link summary.', schema: schema.ResolveResponse },
472
+ }),
473
+ summary: 'Resolve invite link',
474
+ tags: ['Invite Links'],
475
+ }),
476
+ async (c) => {
477
+ if (Auth.narrowAccess) return Auth.superAdminAccessError(c)
478
+ if (OpenApi.narrowValidation) return OpenApi.validationError(c, bodyValidation)
479
+ try {
480
+ const db = Db.get(c.get('db'))
481
+ const link = await InviteLinks.getByToken(db, c.req.valid('json').token)
482
+ if (!link || InviteLinks.status(link) !== 'active') return notFound(c)
483
+ const org = await Organizations.get(db, link.orgId)
484
+ if (!org) return notFound(c)
485
+ return c.json(
486
+ Response.validated(schema.ResolveResponse, { orgName: org.name, role: link.role }),
487
+ 200,
488
+ )
489
+ } catch (cause) {
490
+ return Response.upstream(c, cause)
491
+ }
492
+ },
493
+ )
494
+ .post(
495
+ '/v1/invite-links/accept',
496
+ Auth.policy({ session: true }),
497
+ OpenApi.validate('json', schema.TokenBody, bodyValidation),
498
+ OpenApi.describeRoute({
499
+ operationId: 'acceptInviteLink',
500
+ responses: OpenApi.responses({
501
+ errors: { 403: 'A verified session email is required.', ...errors },
502
+ success: { description: 'Membership.', schema: schema.AcceptResponse },
503
+ }),
504
+ summary: 'Accept invite link',
505
+ tags: ['Invite Links'],
506
+ }),
507
+ async (c) => {
508
+ if (Auth.narrowAccess) return Auth.superAdminAccessError(c)
509
+ if (OpenApi.narrowValidation) return OpenApi.validationError(c, bodyValidation)
510
+ const principal = Auth.getPrincipal(c)
511
+ if (principal?.type !== 'session' || !principal.email)
512
+ return Response.error(c, {
513
+ code: 'forbidden',
514
+ message: 'Verified session email required',
515
+ status: 403,
516
+ })
517
+ try {
518
+ const accepted = await InviteLinks.accept(
519
+ Db.get(c.get('db')),
520
+ c.req.valid('json').token,
521
+ principal.id,
522
+ principal.email,
523
+ )
524
+ if (!accepted) return notFound(c)
525
+ return c.json(
526
+ Response.validated(schema.AcceptResponse, {
527
+ orgId: accepted.link.orgId,
528
+ role: accepted.link.role,
529
+ userId: principal.id,
530
+ }),
531
+ 200,
532
+ )
533
+ } catch (cause) {
534
+ return Response.upstream(c, cause)
535
+ }
536
+ },
537
+ )
538
+ }
539
+
540
+ function serialize(record: InviteLinks.Record) {
541
+ return {
542
+ createdAt: record.createdAt,
543
+ createdBy: record.createdBy,
544
+ enabled: record.enabled,
545
+ expiresAt: record.expiresAt,
546
+ id: record.id,
547
+ lastUsedAt: record.lastUsedAt,
548
+ maxUses: record.maxUses,
549
+ name: record.name,
550
+ orgId: record.orgId,
551
+ role: record.role,
552
+ status: InviteLinks.status(record),
553
+ token: record.token,
554
+ updatedAt: record.updatedAt,
555
+ useCount: record.useCount,
556
+ }
557
+ }
558
+
559
+ /** Normalizes accepted offset timestamps so persisted expiry comparisons remain chronological. */
560
+ function normalizeExpiry(value: string | null) {
561
+ return value === null ? null : new Date(value).toISOString()
562
+ }
563
+
564
+ function notFound(c: Context<Environment>) {
565
+ return Response.error(c, {
566
+ code: 'invite_link_not_found',
567
+ message: 'Invite link not found',
568
+ status: 404,
569
+ })
570
+ }
@@ -14,7 +14,7 @@ describe('scopes', () => {
14
14
  expect(response.headers.get('cache-control')).toMatchInlineSnapshot(
15
15
  `"private, max-age=300, stale-while-revalidate=3600"`,
16
16
  )
17
- expect(body.data).toEqual(Scope.catalog.map((entry) => ({ ...entry })))
17
+ expect(body.data).toEqual([...Scope.catalog, Scope.zoneEntry].map((entry) => ({ ...entry })))
18
18
  expect(body.data.map((entry) => entry.scope)).toMatchInlineSnapshot(`
19
19
  [
20
20
  "data:read",
@@ -25,6 +25,7 @@ describe('scopes', () => {
25
25
  "rpc-relay:sponsor",
26
26
  "webhooks:read",
27
27
  "webhooks:write",
28
+ "zone:<chainId>:read",
28
29
  ]
29
30
  `)
30
31
  })
@@ -40,6 +41,6 @@ describe('scopes', () => {
40
41
  const body = await TestApp.json(response, Scopes.schema.getScopes.Response)
41
42
 
42
43
  expect(response.status).toBe(200)
43
- expect(body.data).toEqual([...Scope.catalog, custom])
44
+ expect(body.data).toEqual([...Scope.catalog, Scope.zoneEntry, custom])
44
45
  })
45
46
  })
@@ -23,7 +23,9 @@ export namespace schema {
23
23
  scope: z
24
24
  .string()
25
25
  .check(
26
- z.describe('Scope identifier used in API-key grants and route policies.'),
26
+ z.describe(
27
+ 'Scope identifier used in API-key grants and route policies. `zone:<chainId>:read` is a template: grants carry a concrete zone chain id.',
28
+ ),
27
29
  z.meta({ examples: ['data:read'] }),
28
30
  ),
29
31
  selfServe: z
@@ -64,7 +66,7 @@ export function scopes() {
64
66
  }),
65
67
  Cache.response<App.Environment>({
66
68
  cacheControl: Cache.policies.stable,
67
- name: 'tempo-api:scopes:v2',
69
+ name: 'tempo-api:scopes:v3',
68
70
  key: (c) => {
69
71
  const url = new URL(c.req.url)
70
72
  url.search = ''