tapimo 0.4.1 → 0.4.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.
Files changed (263) hide show
  1. package/README.md +1 -1
  2. package/dist/ApiKey.d.ts +1 -9
  3. package/dist/ApiKey.d.ts.map +1 -1
  4. package/dist/ApiKey.js +2 -3
  5. package/dist/ApiKey.js.map +1 -1
  6. package/dist/ApiKeys.d.ts +53 -27
  7. package/dist/ApiKeys.d.ts.map +1 -1
  8. package/dist/ApiKeys.js +32 -17
  9. package/dist/ApiKeys.js.map +1 -1
  10. package/dist/App.d.ts +1990 -1442
  11. package/dist/App.d.ts.map +1 -1
  12. package/dist/App.js +23 -9
  13. package/dist/App.js.map +1 -1
  14. package/dist/Assets.d.ts +10 -0
  15. package/dist/Assets.d.ts.map +1 -1
  16. package/dist/Assets.js +4 -0
  17. package/dist/Assets.js.map +1 -1
  18. package/dist/Auth.d.ts +2 -1
  19. package/dist/Auth.d.ts.map +1 -1
  20. package/dist/Auth.js +2 -1
  21. package/dist/Auth.js.map +1 -1
  22. package/dist/Scope.d.ts +40 -15
  23. package/dist/Scope.d.ts.map +1 -1
  24. package/dist/Scope.js +44 -13
  25. package/dist/Scope.js.map +1 -1
  26. package/dist/admin/App.d.ts +560 -9
  27. package/dist/admin/App.d.ts.map +1 -1
  28. package/dist/admin/App.js +38 -6
  29. package/dist/admin/App.js.map +1 -1
  30. package/dist/admin/apps/api-keys.d.ts +10 -24
  31. package/dist/admin/apps/api-keys.d.ts.map +1 -1
  32. package/dist/admin/apps/api-keys.js +10 -3
  33. package/dist/admin/apps/api-keys.js.map +1 -1
  34. package/dist/admin/apps/organizations.d.ts +833 -0
  35. package/dist/admin/apps/organizations.d.ts.map +1 -0
  36. package/dist/admin/apps/organizations.js +733 -0
  37. package/dist/admin/apps/organizations.js.map +1 -0
  38. package/dist/admin/apps/scopes.d.ts +8 -0
  39. package/dist/admin/apps/scopes.d.ts.map +1 -1
  40. package/dist/admin/apps/verified-tokens.d.ts +57 -57
  41. package/dist/admin/ui.generated.d.ts +1 -1
  42. package/dist/admin/ui.generated.d.ts.map +1 -1
  43. package/dist/admin/ui.generated.js +53 -13
  44. package/dist/admin/ui.generated.js.map +1 -1
  45. package/dist/analytics/Analytics.d.ts +10 -0
  46. package/dist/analytics/Analytics.d.ts.map +1 -1
  47. package/dist/analytics/Analytics.js +8 -0
  48. package/dist/analytics/Analytics.js.map +1 -1
  49. package/dist/analytics/tables/requestEvents.d.ts +9 -0
  50. package/dist/analytics/tables/requestEvents.d.ts.map +1 -1
  51. package/dist/analytics/tables/requestEvents.js +14 -0
  52. package/dist/analytics/tables/requestEvents.js.map +1 -1
  53. package/dist/apps/data/App.d.ts +1304 -1304
  54. package/dist/apps/data/FundingRouteProviders.d.ts +4 -22
  55. package/dist/apps/data/FundingRouteProviders.d.ts.map +1 -1
  56. package/dist/apps/data/FundingRouteProviders.js +10 -120
  57. package/dist/apps/data/FundingRouteProviders.js.map +1 -1
  58. package/dist/apps/data/routes/activities.d.ts +38 -38
  59. package/dist/apps/data/routes/addresses.d.ts +24 -24
  60. package/dist/apps/data/routes/blocks.d.ts +39 -39
  61. package/dist/apps/data/routes/fee-amm.d.ts +20 -20
  62. package/dist/apps/data/routes/funding.js +7 -7
  63. package/dist/apps/data/routes/funding.js.map +1 -1
  64. package/dist/apps/data/routes/indexer.d.ts +5 -5
  65. package/dist/apps/data/routes/tokenlist.d.ts +12 -12
  66. package/dist/apps/data/routes/verified-tokens.d.ts +36 -36
  67. package/dist/apps/management/App.d.ts +808 -267
  68. package/dist/apps/management/App.d.ts.map +1 -1
  69. package/dist/apps/management/App.js +2 -2
  70. package/dist/apps/management/Billing.d.ts +3 -0
  71. package/dist/apps/management/Billing.d.ts.map +1 -1
  72. package/dist/apps/management/Billing.js +7 -2
  73. package/dist/apps/management/Billing.js.map +1 -1
  74. package/dist/apps/management/routes/api-keys.d.ts +116 -100
  75. package/dist/apps/management/routes/api-keys.d.ts.map +1 -1
  76. package/dist/apps/management/routes/api-keys.js +41 -30
  77. package/dist/apps/management/routes/api-keys.js.map +1 -1
  78. package/dist/apps/management/routes/billing.d.ts +182 -50
  79. package/dist/apps/management/routes/billing.d.ts.map +1 -1
  80. package/dist/apps/management/routes/billing.js +14 -14
  81. package/dist/apps/management/routes/billing.js.map +1 -1
  82. package/dist/apps/management/routes/invitations.d.ts +93 -20
  83. package/dist/apps/management/routes/invitations.d.ts.map +1 -1
  84. package/dist/apps/management/routes/invitations.js +12 -15
  85. package/dist/apps/management/routes/invitations.js.map +1 -1
  86. package/dist/apps/management/routes/members.d.ts +89 -18
  87. package/dist/apps/management/routes/members.d.ts.map +1 -1
  88. package/dist/apps/management/routes/members.js +10 -10
  89. package/dist/apps/management/routes/members.js.map +1 -1
  90. package/dist/apps/management/routes/orgs.d.ts +100 -40
  91. package/dist/apps/management/routes/orgs.d.ts.map +1 -1
  92. package/dist/apps/management/routes/orgs.js +19 -19
  93. package/dist/apps/management/routes/orgs.js.map +1 -1
  94. package/dist/apps/management/routes/projects.d.ts +122 -33
  95. package/dist/apps/management/routes/projects.d.ts.map +1 -1
  96. package/dist/apps/management/routes/projects.js +12 -13
  97. package/dist/apps/management/routes/projects.js.map +1 -1
  98. package/dist/apps/management/routes/scopes.d.ts +4 -18
  99. package/dist/apps/management/routes/scopes.d.ts.map +1 -1
  100. package/dist/apps/management/routes/scopes.js +6 -7
  101. package/dist/apps/management/routes/scopes.js.map +1 -1
  102. package/dist/apps/management/routes/usage.d.ts +63 -19
  103. package/dist/apps/management/routes/usage.d.ts.map +1 -1
  104. package/dist/apps/management/routes/usage.js +9 -10
  105. package/dist/apps/management/routes/usage.js.map +1 -1
  106. package/dist/apps/mcp.d.ts.map +1 -1
  107. package/dist/apps/mcp.js +38 -4
  108. package/dist/apps/mcp.js.map +1 -1
  109. package/dist/apps/relay/App.d.ts +26 -4
  110. package/dist/apps/relay/App.d.ts.map +1 -1
  111. package/dist/apps/relay/App.js +17 -14
  112. package/dist/apps/relay/App.js.map +1 -1
  113. package/dist/cloudflare.d.ts +2 -0
  114. package/dist/cloudflare.d.ts.map +1 -1
  115. package/dist/cloudflare.js +4 -0
  116. package/dist/cloudflare.js.map +1 -1
  117. package/dist/db/Db.d.ts +5 -3
  118. package/dist/db/Db.d.ts.map +1 -1
  119. package/dist/db/Db.js +52 -0
  120. package/dist/db/Db.js.map +1 -1
  121. package/dist/db/Schema.d.ts +13 -0
  122. package/dist/db/Schema.d.ts.map +1 -1
  123. package/dist/db/Schema.js +16 -1
  124. package/dist/db/Schema.js.map +1 -1
  125. package/dist/db/tables/adminAuditLogs.d.ts +27 -0
  126. package/dist/db/tables/adminAuditLogs.d.ts.map +1 -0
  127. package/dist/db/tables/adminAuditLogs.js +19 -0
  128. package/dist/db/tables/adminAuditLogs.js.map +1 -0
  129. package/dist/db/tables/invitations.d.ts +1 -1
  130. package/dist/db/tables/invitations.d.ts.map +1 -1
  131. package/dist/db/tables/memberships.d.ts +32 -4
  132. package/dist/db/tables/memberships.d.ts.map +1 -1
  133. package/dist/db/tables/memberships.js +31 -0
  134. package/dist/db/tables/memberships.js.map +1 -1
  135. package/dist/db/tables/organizations.d.ts +43 -1
  136. package/dist/db/tables/organizations.d.ts.map +1 -1
  137. package/dist/db/tables/organizations.js +103 -0
  138. package/dist/db/tables/organizations.js.map +1 -1
  139. package/dist/db/tables/projects.d.ts +25 -0
  140. package/dist/db/tables/projects.d.ts.map +1 -1
  141. package/dist/db/tables/projects.js +22 -0
  142. package/dist/db/tables/projects.js.map +1 -1
  143. package/dist/db/tables/webhookDeliveries.d.ts +26 -0
  144. package/dist/db/tables/webhookDeliveries.d.ts.map +1 -1
  145. package/dist/db/tables/webhookDeliveries.js +37 -0
  146. package/dist/db/tables/webhookDeliveries.js.map +1 -1
  147. package/dist/internal/Auth.d.ts +46 -40
  148. package/dist/internal/Auth.d.ts.map +1 -1
  149. package/dist/internal/Auth.js +118 -69
  150. package/dist/internal/Auth.js.map +1 -1
  151. package/dist/internal/Cursor.d.ts +1 -1
  152. package/dist/internal/Cursor.d.ts.map +1 -1
  153. package/dist/internal/Cursor.js +4 -0
  154. package/dist/internal/Cursor.js.map +1 -1
  155. package/dist/internal/EdgeCache.d.ts.map +1 -1
  156. package/dist/internal/EdgeCache.js +2 -0
  157. package/dist/internal/EdgeCache.js.map +1 -1
  158. package/dist/internal/Errors.d.ts +6 -0
  159. package/dist/internal/Errors.d.ts.map +1 -0
  160. package/dist/internal/Errors.js +16 -0
  161. package/dist/internal/Errors.js.map +1 -0
  162. package/dist/internal/FundingRoutes.d.ts +15 -43
  163. package/dist/internal/FundingRoutes.d.ts.map +1 -1
  164. package/dist/internal/FundingRoutes.js +8 -17
  165. package/dist/internal/FundingRoutes.js.map +1 -1
  166. package/dist/internal/Log.d.ts +16 -1
  167. package/dist/internal/Log.d.ts.map +1 -1
  168. package/dist/internal/Log.js +5 -3
  169. package/dist/internal/Log.js.map +1 -1
  170. package/dist/internal/Store.d.ts +61 -16
  171. package/dist/internal/Store.d.ts.map +1 -1
  172. package/dist/internal/Store.js +72 -17
  173. package/dist/internal/Store.js.map +1 -1
  174. package/dist/internal/Viem.d.ts +1 -1
  175. package/dist/internal/index.d.ts +27 -0
  176. package/dist/internal/index.d.ts.map +1 -0
  177. package/dist/internal/index.js +27 -0
  178. package/dist/internal/index.js.map +1 -0
  179. package/package.json +8 -3
  180. package/src/ApiKey.ts +2 -3
  181. package/src/ApiKeys.ts +95 -17
  182. package/src/App.test.ts +104 -6
  183. package/src/App.ts +29 -9
  184. package/src/Assets.ts +8 -0
  185. package/src/Auth.test-d.ts +36 -0
  186. package/src/Auth.ts +20 -1
  187. package/src/Client.test-d.ts +17 -17
  188. package/src/Scope.test.ts +34 -0
  189. package/src/Scope.ts +53 -17
  190. package/src/admin/App.test.ts +33 -3
  191. package/src/admin/App.ts +49 -14
  192. package/src/admin/apps/api-keys.ts +10 -3
  193. package/src/admin/apps/organizations.test.ts +455 -0
  194. package/src/admin/apps/organizations.ts +953 -0
  195. package/src/admin/apps/scopes.test.ts +10 -0
  196. package/src/admin/ui/plugin.ts +1 -1
  197. package/src/admin/ui/src/components/MobileProfile.tsx +2 -0
  198. package/src/admin/ui/src/components/ThemeToggle.tsx +20 -0
  199. package/src/admin/ui/src/lib/api-keys.ts +36 -19
  200. package/src/admin/ui/src/lib/config.ts +2 -3
  201. package/src/admin/ui/src/lib/organizations.test.ts +89 -0
  202. package/src/admin/ui/src/lib/organizations.ts +262 -0
  203. package/src/admin/ui/src/lib/query-client.ts +3 -1
  204. package/src/admin/ui/src/lib/theme.ts +57 -0
  205. package/src/admin/ui/src/routeTree.gen.ts +47 -4
  206. package/src/admin/ui/src/routes/__root.tsx +18 -3
  207. package/src/admin/ui/src/routes/api-keys.tsx +52 -7
  208. package/src/admin/ui/src/routes/index.tsx +1 -1
  209. package/src/admin/ui/src/routes/organizations.tsx +158 -0
  210. package/src/admin/ui/src/routes/organizations_.$orgId.tsx +710 -0
  211. package/src/admin/ui/src/start.ts +6 -0
  212. package/src/admin/ui.generated.ts +63 -13
  213. package/src/analytics/Analytics.ts +25 -0
  214. package/src/analytics/tables/requestEvents.ts +33 -0
  215. package/src/apps/composition.test.ts +82 -2
  216. package/src/apps/data/FundingRouteProviders.test.ts +37 -93
  217. package/src/apps/data/FundingRouteProviders.ts +14 -161
  218. package/src/apps/data/routes/funding.test.ts +7 -42
  219. package/src/apps/data/routes/funding.ts +8 -8
  220. package/src/apps/data/routes/indexer.test.ts +2 -2
  221. package/src/apps/data/routes/webhooks.test.ts +2 -2
  222. package/src/apps/management/App.ts +2 -2
  223. package/src/apps/management/Billing.ts +10 -2
  224. package/src/apps/management/routes/api-keys.test.ts +137 -1
  225. package/src/apps/management/routes/api-keys.ts +63 -38
  226. package/src/apps/management/routes/billing.ts +14 -14
  227. package/src/apps/management/routes/invitations.test.ts +102 -1
  228. package/src/apps/management/routes/invitations.ts +13 -15
  229. package/src/apps/management/routes/members.ts +10 -10
  230. package/src/apps/management/routes/orgs.test.ts +78 -16
  231. package/src/apps/management/routes/orgs.ts +21 -20
  232. package/src/apps/management/routes/projects.ts +12 -13
  233. package/src/apps/management/routes/scopes.test.ts +16 -0
  234. package/src/apps/management/routes/scopes.ts +6 -7
  235. package/src/apps/management/routes/usage.ts +9 -14
  236. package/src/apps/mcp.test.ts +72 -7
  237. package/src/apps/mcp.ts +46 -4
  238. package/src/apps/relay/App.test.ts +23 -18
  239. package/src/apps/relay/App.ts +35 -14
  240. package/src/cloudflare.ts +5 -0
  241. package/src/db/Db.ts +59 -3
  242. package/src/db/Schema.ts +20 -1
  243. package/src/db/tables/adminAuditLogs.ts +47 -0
  244. package/src/db/tables/invitations.ts +1 -1
  245. package/src/db/tables/memberships.ts +59 -3
  246. package/src/db/tables/organizations.ts +148 -1
  247. package/src/db/tables/projects.ts +47 -0
  248. package/src/db/tables/webhookDeliveries.ts +83 -0
  249. package/src/internal/Auth.test.ts +571 -23
  250. package/src/internal/Auth.ts +166 -94
  251. package/src/internal/Cursor.test.ts +10 -0
  252. package/src/internal/Cursor.ts +5 -1
  253. package/src/internal/EdgeCache.test.ts +11 -0
  254. package/src/internal/EdgeCache.ts +2 -0
  255. package/src/internal/FundingRoutes.test.ts +8 -8
  256. package/src/internal/FundingRoutes.ts +25 -29
  257. package/src/internal/Log.test.ts +41 -3
  258. package/src/internal/Log.ts +23 -4
  259. package/src/internal/Store.test-d.ts +15 -0
  260. package/src/internal/Store.test.ts +153 -2
  261. package/src/internal/Store.ts +157 -41
  262. package/src/internal/index.test.ts +34 -0
  263. package/src/internal/index.ts +26 -0
@@ -41,7 +41,7 @@ export namespace schema {
41
41
  .optional(z.record(z.string(), z.string()))
42
42
  .check(
43
43
  z.describe('Provider-specific chain ids Tempo uses when querying route providers.'),
44
- z.meta({ examples: [{ across: '8453', relay: '8453' }] }),
44
+ z.meta({ examples: [{ bridge: 'base', relay: '8453' }] }),
45
45
  ),
46
46
  }),
47
47
  'A normalized chain reference for funding routes.',
@@ -122,7 +122,7 @@ export namespace schema {
122
122
  /** Provider summary embedded in a route candidate. */
123
123
  export const ProviderRef = Schema.describe(
124
124
  z.object({
125
- id: z.string().check(z.describe('Stable provider id.'), z.meta({ examples: ['across'] })),
125
+ id: z.string().check(z.describe('Stable provider id.'), z.meta({ examples: ['relay'] })),
126
126
  kind: z
127
127
  .string()
128
128
  .check(
@@ -131,7 +131,7 @@ export namespace schema {
131
131
  ),
132
132
  name: z
133
133
  .string()
134
- .check(z.describe('Human-readable provider name.'), z.meta({ examples: ['Across'] })),
134
+ .check(z.describe('Human-readable provider name.'), z.meta({ examples: ['Relay'] })),
135
135
  requiresApiKey: z
136
136
  .boolean()
137
137
  .check(
@@ -283,7 +283,7 @@ export namespace schema {
283
283
  .string()
284
284
  .check(
285
285
  z.describe('Stable deterministic route candidate id.'),
286
- z.meta({ examples: ['base-usdc-tempo-usdce-across'] }),
286
+ z.meta({ examples: ['base-usdc-tempo-usdce-relay'] }),
287
287
  ),
288
288
  provider: ProviderRef,
289
289
  quality: Quality,
@@ -369,9 +369,9 @@ export namespace schema {
369
369
  .optional(z.string())
370
370
  .check(
371
371
  z.describe(
372
- 'Only return candidates from this provider id, such as `across`, `relay`, or `bridge`. Planned providers can return an empty list.',
372
+ 'Only return candidates from this provider id, such as `relay`, `bridge`, or `bungee`. Planned providers can return an empty list.',
373
373
  ),
374
- z.meta({ examples: ['across', 'bridge'] }),
374
+ z.meta({ examples: ['relay', 'bridge'] }),
375
375
  ),
376
376
  sourceAmount: z
377
377
  .string()
@@ -463,12 +463,12 @@ export namespace schema {
463
463
  const fundingRouteResponseExamples = {
464
464
  baseUsdc: {
465
465
  description:
466
- '`GET /v1/funding/routes?sourceChain=base&sourceToken=USDC&sourceAmount=1000000&provider=across`',
466
+ '`GET /v1/funding/routes?sourceChain=base&sourceToken=USDC&sourceAmount=1000000&provider=relay`',
467
467
  summary: 'Base USDC',
468
468
  value: {
469
469
  data: FundingRoutes.listStaticRoutes({
470
470
  limit: 10,
471
- provider: 'across',
471
+ provider: 'relay',
472
472
  sourceAmount: '1000000',
473
473
  sourceAmountUnits: 'baseUnits',
474
474
  sourceChain: 'base',
@@ -131,14 +131,14 @@ describe('GET /indexer/query', () => {
131
131
  const app = TestApp.create({
132
132
  rateLimit: { store: rateLimitStore },
133
133
  mpp: {
134
- charge: {
134
+ secretKey: 'secret_test_key_0123456789abcdef',
135
+ session: {
135
136
  chainId: Tempo.chain.id,
136
137
  currency: Tempo.currency,
137
138
  decimals: 6,
138
139
  getClient: () => Tempo.client,
139
140
  recipient: Tempo.accounts[2].address,
140
141
  },
141
- secretKey: 'secret_test_key_0123456789abcdef',
142
142
  },
143
143
  })
144
144
  await exhaustQuota(RateLimit.memory({ store: rateLimitStore }), 'public:anonymous', {
@@ -1100,14 +1100,14 @@ describe('MPP access (disabled for now — API-key-only)', () => {
1100
1100
  const app = TestApp.create({
1101
1101
  auth: { keys: [] },
1102
1102
  mpp: {
1103
- charge: {
1103
+ secretKey: 'secret_test_key_0123456789abcdef',
1104
+ session: {
1104
1105
  chainId: Tempo.chain.id,
1105
1106
  currency: Tempo.currency,
1106
1107
  decimals: 6,
1107
1108
  getClient: () => Tempo.client,
1108
1109
  recipient: Tempo.accounts[2].address,
1109
1110
  },
1110
- secretKey: 'secret_test_key_0123456789abcdef',
1111
1111
  },
1112
1112
  })
1113
1113
 
@@ -87,8 +87,8 @@ export type Environment = {
87
87
  * invitations, and API-key management. Every route reads its dependencies from
88
88
  * request context set by {@link App.create}.
89
89
  *
90
- * The session-auth surface (`/v1/auth/siwe` + `session` capability) is owned by
91
- * {@link App.create}, so management routes gate on a session without owning it.
90
+ * The session-auth surface is owned by {@link App.create}; management routes
91
+ * reference it alongside scoped API-key access.
92
92
  *
93
93
  * ```ts
94
94
  * App.create(options).route('/', management())
@@ -7,6 +7,7 @@ import type * as BillingSettings from '../../db/tables/billingSettings.js'
7
7
  import * as Db from '../../db/Db.js'
8
8
  import * as Fees from '../../internal/Fees.js'
9
9
  import * as RequestUsage from '../../db/tables/requestUsage.js'
10
+ import type * as Scope from '../../Scope.js'
10
11
  import * as SponsoredTransactions from '../../db/tables/sponsoredTransactions.js'
11
12
  import * as StripeCustomers from '../../db/tables/stripeCustomers.js'
12
13
  import * as Viem from '../../internal/Viem.js'
@@ -641,7 +642,7 @@ export async function listActive(
641
642
  export function createKeyBillingSyncer(
642
643
  options: createKeyBillingSyncer.Options,
643
644
  ): createKeyBillingSyncer.Syncer {
644
- const { db, environments = ['sandbox'], kv } = options
645
+ const { db, environments = ['sandbox'], kv, scopeCatalog } = options
645
646
  return {
646
647
  async tick() {
647
648
  // Resolve per tick so Workers factories build a fresh Hyperdrive-pooled
@@ -655,7 +656,12 @@ export function createKeyBillingSyncer(
655
656
  checked += 1
656
657
  // `setBillingActive` writes only drifted records, so a converged
657
658
  // fleet costs list reads and no writes.
658
- updated += await ApiKeys.setBillingActive(kv, { active, environment, orgId })
659
+ updated += await ApiKeys.setBillingActive(kv, {
660
+ active,
661
+ environment,
662
+ orgId,
663
+ scopeCatalog,
664
+ })
659
665
  }
660
666
  }
661
667
  return { checked, updated }
@@ -672,6 +678,8 @@ export declare namespace createKeyBillingSyncer {
672
678
  environments?: readonly StripeCustomers.Record['environment'][] | undefined
673
679
  /** KV state store holding API-key records. */
674
680
  kv: Store.State
681
+ /** Scope catalog accepted while reconciling API-key records. */
682
+ scopeCatalog?: Scope.Catalog | undefined
675
683
  }
676
684
 
677
685
  /** A scheduled snapshot-reconcile job. */
@@ -1,8 +1,13 @@
1
1
  import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts'
2
2
 
3
3
  import * as ApiKeys from '../../../ApiKeys.js'
4
+ import * as Auth from '../../../internal/Auth.js'
5
+ import * as OpenApi from '../../../internal/OpenApi.js'
6
+ import * as Scope from '../../../Scope.js'
4
7
  import * as TestApp from '../../../../test/App.js'
8
+ import * as Memberships from '../../../db/tables/memberships.js'
5
9
  import * as Keys from './api-keys.js'
10
+ import * as Me from './me.js'
6
11
  import * as Orgs from './orgs.js'
7
12
  import * as Projects from './projects.js'
8
13
 
@@ -33,10 +38,18 @@ function superAdminJson(method: 'PATCH' | 'POST', body: unknown) {
33
38
  // Resolve-cache disabled so revocation assertions observe the store directly.
34
39
  type State = ReturnType<typeof TestApp.kvStore>
35
40
 
36
- function createApp(options: { kv?: { store: State } } = {}) {
41
+ function createApp(
42
+ options: {
43
+ db?: ReturnType<typeof TestApp.database>
44
+ kv?: { store: State }
45
+ scopes?: readonly Scope.Entry[]
46
+ } = {},
47
+ ) {
37
48
  return TestApp.create({
38
49
  auth: { cache: false, superAdmin: { secret } },
50
+ db: options.db ?? TestApp.database(),
39
51
  ...(options.kv ? { kv: options.kv } : {}),
52
+ ...(options.scopes ? { scopes: options.scopes } : {}),
40
53
  session: { wallet: { origin } },
41
54
  })
42
55
  }
@@ -70,6 +83,77 @@ async function fixture(app: ReturnType<typeof createApp>, cookie: string) {
70
83
  }
71
84
 
72
85
  describe('keys', () => {
86
+ test('requires an owner to issue management write keys', async () => {
87
+ const db = TestApp.database()
88
+ const app = createApp({ db })
89
+ const owner = await session(app)
90
+ const { org, project } = await fixture(app, owner)
91
+ const admin = await session(app)
92
+ const user = await TestApp.json(
93
+ await app.request('/v1/me', { headers: { cookie: admin } }),
94
+ Me.schema.User,
95
+ )
96
+ await Memberships.create(db, {
97
+ orgId: org.id,
98
+ role: 'admin',
99
+ userId: user.id,
100
+ })
101
+ const base = `/v1/orgs/${org.id}/projects/${project.id}/api-keys`
102
+
103
+ const read = await app.request(base, json('POST', admin, { scopes: ['management:read'] }))
104
+ expect(read.status).toBe(200)
105
+
106
+ const denied = await app.request(base, json('POST', admin, { scopes: ['management:write'] }))
107
+ expect(denied.status).toBe(403)
108
+ expect(await denied.json()).toMatchObject({ error: { code: 'scope_not_issuable' } })
109
+
110
+ const write = await app.request(base, json('POST', owner, { scopes: ['management:write'] }))
111
+ expect(write.status).toBe(200)
112
+ })
113
+
114
+ test('lets management keys manage org credentials without delegating issuance', async () => {
115
+ const app = createApp()
116
+ const cookie = await session(app)
117
+ const { org, project } = await fixture(app, cookie)
118
+ const other = await TestApp.json(
119
+ await app.request(`/v1/orgs/${org.id}/projects`, json('POST', cookie, { name: 'Payments' })),
120
+ Projects.schema.Project,
121
+ )
122
+ const base = `/v1/orgs/${org.id}/projects/${project.id}/api-keys`
123
+ const siblingBase = `/v1/orgs/${org.id}/projects/${other.id}/api-keys`
124
+ const manager = await TestApp.json(
125
+ await app.request(
126
+ base,
127
+ superAdminJson('POST', {
128
+ scopes: ['management:read', 'management:write'],
129
+ }),
130
+ ),
131
+ Keys.schema.createApiKey.Response,
132
+ )
133
+ const target = await TestApp.json(
134
+ await app.request(siblingBase, superAdminJson('POST', { scopes: ['data:read'] })),
135
+ Keys.schema.createApiKey.Response,
136
+ )
137
+
138
+ const listed = await app.request(siblingBase, {
139
+ headers: { 'tempo-api-key': manager.token },
140
+ })
141
+ expect(listed.status).toBe(200)
142
+
143
+ const delegated = await app.request(base, {
144
+ body: JSON.stringify({ scopes: ['management:read'] }),
145
+ headers: { 'content-type': 'application/json', 'tempo-api-key': manager.token },
146
+ method: 'POST',
147
+ })
148
+ expect(delegated.status).toBe(403)
149
+
150
+ const revoked = await app.request(`${siblingBase}/${target.id}`, {
151
+ headers: { 'tempo-api-key': manager.token },
152
+ method: 'DELETE',
153
+ })
154
+ expect(revoked.status).toBe(200)
155
+ })
156
+
73
157
  test('mints, lists, authenticates, and revokes a project key', async () => {
74
158
  const app = createApp()
75
159
  const cookie = await session(app)
@@ -187,6 +271,58 @@ describe('keys', () => {
187
271
  expect(created.createdBy).not.toBe('super_admin')
188
272
  })
189
273
 
274
+ test('mints and authenticates host scopes', async () => {
275
+ const app = createApp({
276
+ scopes: [
277
+ {
278
+ description: 'Read project accounts.',
279
+ scope: 'accounts:read',
280
+ selfServe: true,
281
+ },
282
+ ],
283
+ }).get(
284
+ '/custom/accounts',
285
+ Auth.policy({ apiKey: { scopes: ['accounts:read'] } }),
286
+ OpenApi.describeRoute({
287
+ operationId: 'getCustomAccounts',
288
+ responses: { 200: { description: 'Custom accounts response.' } },
289
+ summary: 'Get custom accounts',
290
+ }),
291
+ (c) => c.json({ ok: true }, 200),
292
+ )
293
+ const cookie = await session(app)
294
+ const { org, project } = await fixture(app, cookie)
295
+
296
+ const created = await TestApp.json(
297
+ await app.request(
298
+ `/v1/orgs/${org.id}/projects/${project.id}/api-keys`,
299
+ json('POST', cookie, { scopes: ['accounts:read'] }),
300
+ ),
301
+ Keys.schema.createApiKey.Response,
302
+ )
303
+ const response = await app.request('/custom/accounts', {
304
+ headers: { 'tempo-api-key': created.token },
305
+ })
306
+
307
+ expect(created.scopes).toEqual(['accounts:read'])
308
+ expect(response.status).toBe(200)
309
+ expect(await response.json()).toEqual({ ok: true })
310
+ })
311
+
312
+ test('rejects unregistered host scopes', async () => {
313
+ const app = createApp()
314
+ const cookie = await session(app)
315
+ const { org, project } = await fixture(app, cookie)
316
+
317
+ const response = await app.request(
318
+ `/v1/orgs/${org.id}/projects/${project.id}/api-keys`,
319
+ superAdminJson('POST', { scopes: ['accounts:read'] }),
320
+ )
321
+
322
+ expect(response.status).toBe(400)
323
+ expect(await response.json()).toMatchObject({ error: { code: 'body_invalid' } })
324
+ })
325
+
190
326
  test('rejects session mints of non-self-serve scopes (webhooks)', async () => {
191
327
  const app = createApp()
192
328
  const cookie = await session(app)
@@ -53,11 +53,11 @@ export namespace schema {
53
53
  z.meta({ examples: ['org_1a2b3c4d5e6f7g8h9j0k1m2n'] }),
54
54
  ),
55
55
  projectId: z.optional(z.string()).check(
56
- z.describe('Owning project id (`prj_…`).'), // prettier-ignore
56
+ z.describe('Attributed project id (`prj_…`).'), // prettier-ignore
57
57
  z.meta({ examples: ['prj_1a2b3c4d5e6f7g8h9j0k1m2n'] }),
58
58
  ),
59
59
  scopes: z
60
- .array(z.enum([...Scope.catalog.map((entry) => entry.scope), Scope.wildcard]))
60
+ .array(z.string().check(z.minLength(1)))
61
61
  .check(
62
62
  z.describe(
63
63
  "Granted scopes. '*' (grant-all) appears only on keys minted by the super admin.",
@@ -138,7 +138,15 @@ export namespace schema {
138
138
  .optional(z.string().check(z.minLength(1), z.maxLength(100)))
139
139
  .check(z.describe('Human-readable key name.'), z.meta({ examples: ['CI'] })),
140
140
  scopes: z
141
- ._default(z.array(z.enum(Scope.catalog.map((entry) => entry.scope))), [])
141
+ ._default(
142
+ z.array(
143
+ z.string().check(
144
+ z.minLength(1),
145
+ z.refine((scope) => scope !== Scope.wildcard),
146
+ ),
147
+ ),
148
+ [],
149
+ )
142
150
  .check(z.describe('Scopes to grant.'), z.meta({ examples: [['data:read']] })),
143
151
  })
144
152
  .check(z.describe('Fields for minting an API key.'))
@@ -210,11 +218,7 @@ export namespace schema {
210
218
  }
211
219
 
212
220
  /**
213
- * Mounts the `/orgs/:orgId/projects/:projectId/api-keys` resource
214
- * (project-scoped mint/list/revoke) plus the org-wide read
215
- * `/orgs/:orgId/api-keys`, over the KV state store. Access follows the
216
- * org/project scope (owning session or super admin); the token appears once,
217
- * at mint.
221
+ * Mounts project-attributed API key mint, list, and revoke routes plus the org-wide list. Access follows the caller's organization ownership.
218
222
  */
219
223
  export function apiKeys(options: apiKeys.Options = {}) {
220
224
  const hidden = !options.enabled
@@ -233,7 +237,7 @@ export function apiKeys(options: apiKeys.Options = {}) {
233
237
  message: 'Check the request body and try again.',
234
238
  }),
235
239
  OpenApi.describeRoute({
236
- description: 'Mint a project-scoped API key. The token appears once and is never shown again.', // prettier-ignore
240
+ description: 'Mint a project-attributed API key. The token appears once and is never shown again.', // prettier-ignore
237
241
  hide: hidden,
238
242
  operationId: 'createApiKey',
239
243
  responses: OpenApi.responses({
@@ -273,20 +277,19 @@ export function apiKeys(options: apiKeys.Options = {}) {
273
277
 
274
278
  const body = c.req.valid('json')
275
279
 
276
- // Sessions mint only self-issuable scopes; the super admin grants the
277
- // full catalog. Self-serve covers the whole catalog now that billing
278
- // ships, so this is a safety net: a future non-self-serve scope would
279
- // be refused here without further changes.
280
- if (createdBy !== 'super_admin') {
281
- const denied = body.scopes.filter((scope) => !Scope.selfServe.includes(scope))
282
- if (denied.length > 0)
283
- return Response.error(c, {
284
- code: 'scope_not_issuable',
285
- message: 'The requested scopes are not issuable.',
286
- status: 403,
287
- })
288
- }
280
+ if (!body.scopes.every((scope) => Scope.includes(c.get('scopeCatalog'), scope)))
281
+ return Response.error(c, {
282
+ code: 'body_invalid',
283
+ message: 'Check the request body and try again.',
284
+ status: 400,
285
+ })
289
286
 
287
+ if (!canIssue(c, body.scopes))
288
+ return Response.error(c, {
289
+ code: 'scope_not_issuable',
290
+ message: 'The requested scopes are not issuable.',
291
+ status: 403,
292
+ })
290
293
  try {
291
294
  const orgId = Auth.org(c).id
292
295
  // Snapshot billing onto the record so the auth path throttles a
@@ -296,15 +299,19 @@ export function apiKeys(options: apiKeys.Options = {}) {
296
299
  body.environment === 'sandbox'
297
300
  ? await Billing.active(Db.get(c.get('db')), orgId, 'sandbox')
298
301
  : undefined
299
- const { record, token } = await ApiKeys.mint(kv.store, {
300
- createdBy,
301
- environment: body.environment,
302
- ...(billingActive === undefined ? {} : { billingActive }),
303
- ...(body.name === undefined ? {} : { name: body.name }),
304
- orgId,
305
- projectId: Auth.project(c).id,
306
- scopes: body.scopes,
307
- })
302
+ const { record, token } = await ApiKeys.mint(
303
+ kv.store,
304
+ {
305
+ createdBy,
306
+ environment: body.environment,
307
+ ...(billingActive === undefined ? {} : { billingActive }),
308
+ ...(body.name === undefined ? {} : { name: body.name }),
309
+ orgId,
310
+ projectId: Auth.project(c).id,
311
+ scopes: body.scopes,
312
+ },
313
+ { scopeCatalog: c.get('scopeCatalog') },
314
+ )
308
315
  return c.json(
309
316
  Response.validated(schema.createApiKey.Response, { ...serializeKey(record), token }),
310
317
  200,
@@ -316,7 +323,7 @@ export function apiKeys(options: apiKeys.Options = {}) {
316
323
  )
317
324
  .get(
318
325
  '/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/projects/:projectId{prj_[A-Za-z0-9_-]+}/api-keys',
319
- Auth.policy({ session: true }),
326
+ Auth.policy({ apiKey: { scopes: ['management:read'] }, session: true }),
320
327
  Auth.ensureOrg(),
321
328
  Auth.ensureProject(),
322
329
  OpenApi.validate('param', schema.Params, {
@@ -344,7 +351,7 @@ export function apiKeys(options: apiKeys.Options = {}) {
344
351
  tags: ['API Keys'],
345
352
  }),
346
353
  async (c) => {
347
- if (Auth.narrowAccess) return Auth.superAdminAccessError(c)
354
+ if (Auth.narrowAccess) return Auth.accessError(c)
348
355
  if (Auth.narrowScope) return Auth.ensureProjectError(c)
349
356
  if (OpenApi.narrowValidation)
350
357
  return OpenApi.validationError(c, {
@@ -363,6 +370,7 @@ export function apiKeys(options: apiKeys.Options = {}) {
363
370
  const records = await ApiKeys.listByOrg(kv.store, Auth.org(c).id, {
364
371
  environment: c.req.valid('query').environment,
365
372
  projectId: Auth.project(c).id,
373
+ scopeCatalog: c.get('scopeCatalog'),
366
374
  })
367
375
  return c.json(
368
376
  Response.validated(schema.listApiKeys.Response, {
@@ -377,7 +385,7 @@ export function apiKeys(options: apiKeys.Options = {}) {
377
385
  )
378
386
  .get(
379
387
  '/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/api-keys',
380
- Auth.policy({ session: true }),
388
+ Auth.policy({ apiKey: { scopes: ['management:read'] }, session: true }),
381
389
  Auth.ensureOrg(),
382
390
  OpenApi.validate('param', schema.OrgParams, {
383
391
  code: 'param_invalid',
@@ -405,7 +413,7 @@ export function apiKeys(options: apiKeys.Options = {}) {
405
413
  tags: ['API Keys'],
406
414
  }),
407
415
  async (c) => {
408
- if (Auth.narrowAccess) return Auth.superAdminAccessError(c)
416
+ if (Auth.narrowAccess) return Auth.accessError(c)
409
417
  if (Auth.narrowScope) return Auth.ensureOrgError(c)
410
418
  if (OpenApi.narrowValidation)
411
419
  return OpenApi.validationError(c, {
@@ -423,6 +431,7 @@ export function apiKeys(options: apiKeys.Options = {}) {
423
431
  try {
424
432
  const records = await ApiKeys.listByOrg(kv.store, Auth.org(c).id, {
425
433
  environment: c.req.valid('query').environment,
434
+ scopeCatalog: c.get('scopeCatalog'),
426
435
  })
427
436
  return c.json(
428
437
  Response.validated(schema.listOrgApiKeys.Response, {
@@ -437,7 +446,7 @@ export function apiKeys(options: apiKeys.Options = {}) {
437
446
  )
438
447
  .delete(
439
448
  '/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/projects/:projectId{prj_[A-Za-z0-9_-]+}/api-keys/:keyId{key_[A-Za-z0-9_-]+}',
440
- Auth.policy({ session: true }),
449
+ Auth.policy({ apiKey: { scopes: ['management:write'] }, session: true }),
441
450
  Auth.ensureOrg({ role: 'admin' }),
442
451
  Auth.ensureProject(),
443
452
  OpenApi.validate('param', schema.KeyParams, {
@@ -459,7 +468,7 @@ export function apiKeys(options: apiKeys.Options = {}) {
459
468
  tags: ['API Keys'],
460
469
  }),
461
470
  async (c) => {
462
- if (Auth.narrowAccess) return Auth.superAdminAccessError(c)
471
+ if (Auth.narrowAccess) return Auth.accessError(c)
463
472
  if (Auth.narrowScope) return Auth.ensureProjectError(c)
464
473
  if (OpenApi.narrowValidation)
465
474
  return OpenApi.validationError(c, {
@@ -473,7 +482,9 @@ export function apiKeys(options: apiKeys.Options = {}) {
473
482
  try {
474
483
  // Bind the key to the scoped project before revoking: a key id from
475
484
  // another org or project must be indistinguishable from a missing one.
476
- const record = await ApiKeys.get(kv.store, keyId)
485
+ const record = await ApiKeys.get(kv.store, keyId, {
486
+ scopeCatalog: c.get('scopeCatalog'),
487
+ })
477
488
  if (!record || record.orgId !== Auth.org(c).id || record.projectId !== Auth.project(c).id)
478
489
  return keyNotFound(c)
479
490
  const revoked = await ApiKeys.revoke(kv.store, keyId)
@@ -518,6 +529,20 @@ function mintedBy(c: Context<App.Environment>) {
518
529
  return undefined
519
530
  }
520
531
 
532
+ /** Checks whether the caller may grant every requested scope. */
533
+ function canIssue(c: Context<App.Environment>, scopes: readonly Scope.Id[]) {
534
+ const principal = Auth.getPrincipal(c)
535
+ if (principal?.type === 'super_admin') return true
536
+ if (principal?.type === 'session') {
537
+ const selfServe = Scope.selfServeFrom(c.get('scopeCatalog'))
538
+ return (
539
+ scopes.every((scope) => selfServe.includes(scope)) &&
540
+ (!scopes.includes('management:write') || Auth.membership(c)?.role === 'owner')
541
+ )
542
+ }
543
+ return false
544
+ }
545
+
521
546
  function keyNotFound(c: Context<App.Environment>) {
522
547
  return Response.error(c, {
523
548
  code: 'api_key_not_found',
@@ -234,14 +234,13 @@ export namespace schema {
234
234
  }
235
235
 
236
236
  /**
237
- * Mounts the billing surface: an organization's billing status. Orgs without
238
- * a Stripe billing source read as `none`. Session lane; reads for members.
237
+ * Mounts billing routes. Reads require membership or scoped API key access; organizations without Stripe state return `none`.
239
238
  */
240
239
  export function billing() {
241
240
  return new Hono<Environment>()
242
241
  .get(
243
242
  '/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/billing',
244
- Auth.policy({ session: true }),
243
+ Auth.policy({ apiKey: { scopes: ['management:read'] }, session: true }),
245
244
  Auth.ensureOrg(),
246
245
  OpenApi.validate('param', schema.Params, {
247
246
  code: 'param_invalid',
@@ -264,7 +263,7 @@ export function billing() {
264
263
  tags: ['Billing'],
265
264
  }),
266
265
  async (c) => {
267
- if (Auth.narrowAccess) return Auth.superAdminAccessError(c)
266
+ if (Auth.narrowAccess) return Auth.accessError(c)
268
267
  if (Auth.narrowScope) return Auth.ensureOrgError(c)
269
268
  if (OpenApi.narrowValidation)
270
269
  return OpenApi.validationError(c, {
@@ -294,7 +293,7 @@ export function billing() {
294
293
  )
295
294
  .patch(
296
295
  '/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/billing',
297
- Auth.policy({ session: true }),
296
+ Auth.policy({ apiKey: { scopes: ['management:write'] }, session: true }),
298
297
  Auth.ensureOrg({ role: 'owner' }),
299
298
  OpenApi.validate('param', schema.Params, {
300
299
  code: 'param_invalid',
@@ -325,7 +324,7 @@ export function billing() {
325
324
  tags: ['Billing'],
326
325
  }),
327
326
  async (c) => {
328
- if (Auth.narrowAccess) return Auth.superAdminAccessError(c)
327
+ if (Auth.narrowAccess) return Auth.accessError(c)
329
328
  if (Auth.narrowScope) return Auth.ensureOrgError(c)
330
329
  if (OpenApi.narrowValidation)
331
330
  return OpenApi.validationError(c, {
@@ -381,7 +380,7 @@ export function billing() {
381
380
  )
382
381
  .post(
383
382
  '/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/billing/stripe/checkout',
384
- Auth.policy({ session: true }),
383
+ Auth.policy({ apiKey: { scopes: ['management:write'] }, session: true }),
385
384
  Auth.ensureOrg({ role: 'owner' }),
386
385
  OpenApi.validate('param', schema.Params, {
387
386
  code: 'param_invalid',
@@ -408,7 +407,7 @@ export function billing() {
408
407
  tags: ['Billing'],
409
408
  }),
410
409
  async (c) => {
411
- if (Auth.narrowAccess) return Auth.superAdminAccessError(c)
410
+ if (Auth.narrowAccess) return Auth.accessError(c)
412
411
  if (Auth.narrowScope) return Auth.ensureOrgError(c)
413
412
  if (OpenApi.narrowValidation)
414
413
  return OpenApi.validationError(c, {
@@ -466,7 +465,7 @@ export function billing() {
466
465
  )
467
466
  .post(
468
467
  '/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/billing/stripe/manage',
469
- Auth.policy({ session: true }),
468
+ Auth.policy({ apiKey: { scopes: ['management:write'] }, session: true }),
470
469
  Auth.ensureOrg({ role: 'owner' }),
471
470
  OpenApi.validate('param', schema.Params, {
472
471
  code: 'param_invalid',
@@ -493,7 +492,7 @@ export function billing() {
493
492
  tags: ['Billing'],
494
493
  }),
495
494
  async (c) => {
496
- if (Auth.narrowAccess) return Auth.superAdminAccessError(c)
495
+ if (Auth.narrowAccess) return Auth.accessError(c)
497
496
  if (Auth.narrowScope) return Auth.ensureOrgError(c)
498
497
  if (OpenApi.narrowValidation)
499
498
  return OpenApi.validationError(c, {
@@ -529,7 +528,7 @@ export function billing() {
529
528
  )
530
529
  .get(
531
530
  '/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/billing/payment-methods',
532
- Auth.policy({ session: true }),
531
+ Auth.policy({ apiKey: { scopes: ['management:read'] }, session: true }),
533
532
  Auth.ensureOrg(),
534
533
  OpenApi.validate('param', schema.Params, {
535
534
  code: 'param_invalid',
@@ -555,7 +554,7 @@ export function billing() {
555
554
  tags: ['Billing'],
556
555
  }),
557
556
  async (c) => {
558
- if (Auth.narrowAccess) return Auth.superAdminAccessError(c)
557
+ if (Auth.narrowAccess) return Auth.accessError(c)
559
558
  if (Auth.narrowScope) return Auth.ensureOrgError(c)
560
559
  if (OpenApi.narrowValidation)
561
560
  return OpenApi.validationError(c, {
@@ -615,7 +614,7 @@ export function billing() {
615
614
  )
616
615
  .delete(
617
616
  '/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/billing/payment-methods/:methodId{pm_[A-Za-z0-9_-]+}',
618
- Auth.policy({ session: true }),
617
+ Auth.policy({ apiKey: { scopes: ['management:write'] }, session: true }),
619
618
  Auth.ensureOrg({ role: 'owner' }),
620
619
  OpenApi.validate('param', schema.MethodParams, {
621
620
  code: 'param_invalid',
@@ -642,7 +641,7 @@ export function billing() {
642
641
  tags: ['Billing'],
643
642
  }),
644
643
  async (c) => {
645
- if (Auth.narrowAccess) return Auth.superAdminAccessError(c)
644
+ if (Auth.narrowAccess) return Auth.accessError(c)
646
645
  if (Auth.narrowScope) return Auth.ensureOrgError(c)
647
646
  if (OpenApi.narrowValidation)
648
647
  return OpenApi.validationError(c, {
@@ -797,6 +796,7 @@ async function syncKeyBilling(
797
796
  active: options.status === 'active',
798
797
  environment: 'sandbox',
799
798
  orgId: options.orgId,
799
+ scopeCatalog: c.get('scopeCatalog'),
800
800
  })
801
801
  }
802
802