tapimo 0.4.3 → 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 (77) hide show
  1. package/dist/App.d.ts +2491 -2861
  2. package/dist/App.d.ts.map +1 -1
  3. package/dist/admin/apps/verified-tokens.d.ts +57 -57
  4. package/dist/apps/data/App.d.ts +2354 -2724
  5. package/dist/apps/data/App.d.ts.map +1 -1
  6. package/dist/apps/data/FundingRouteProviders.d.ts +3 -110
  7. package/dist/apps/data/FundingRouteProviders.d.ts.map +1 -1
  8. package/dist/apps/data/FundingRouteProviders.js +1 -302
  9. package/dist/apps/data/FundingRouteProviders.js.map +1 -1
  10. package/dist/apps/data/routes/exchanges.d.ts +455 -455
  11. package/dist/apps/data/routes/fee-amm.d.ts +92 -92
  12. package/dist/apps/data/routes/funding.d.ts +138 -730
  13. package/dist/apps/data/routes/funding.d.ts.map +1 -1
  14. package/dist/apps/data/routes/funding.js +7 -241
  15. package/dist/apps/data/routes/funding.js.map +1 -1
  16. package/dist/apps/data/routes/tokenlist.d.ts +12 -12
  17. package/dist/apps/data/routes/transactions.d.ts +281 -281
  18. package/dist/apps/data/routes/transfers.d.ts +58 -58
  19. package/dist/apps/data/routes/verified-tokens.d.ts +86 -86
  20. package/dist/apps/management/App.d.ts +855 -855
  21. package/dist/apps/management/routes/api-keys.d.ts +40 -40
  22. package/dist/apps/management/routes/billing.d.ts +207 -207
  23. package/dist/apps/management/routes/me.d.ts +6 -6
  24. package/dist/apps/management/routes/members.d.ts +30 -30
  25. package/dist/apps/management/routes/orgs.d.ts +92 -92
  26. package/dist/apps/management/routes/projects.d.ts +28 -28
  27. package/dist/apps/management/routes/usage.d.ts +24 -24
  28. package/dist/apps/mcp.d.ts.map +1 -1
  29. package/dist/apps/mcp.js +3 -1
  30. package/dist/apps/mcp.js.map +1 -1
  31. package/dist/cloudflare.d.ts +2 -0
  32. package/dist/cloudflare.d.ts.map +1 -1
  33. package/dist/cloudflare.js +4 -0
  34. package/dist/cloudflare.js.map +1 -1
  35. package/dist/internal/Auth.d.ts +33 -22
  36. package/dist/internal/Auth.d.ts.map +1 -1
  37. package/dist/internal/Auth.js +40 -11
  38. package/dist/internal/Auth.js.map +1 -1
  39. package/dist/internal/Errors.d.ts +6 -0
  40. package/dist/internal/Errors.d.ts.map +1 -0
  41. package/dist/internal/Errors.js +16 -0
  42. package/dist/internal/Errors.js.map +1 -0
  43. package/dist/internal/FundingRoutes.d.ts +15 -213
  44. package/dist/internal/FundingRoutes.d.ts.map +1 -1
  45. package/dist/internal/FundingRoutes.js +1 -72
  46. package/dist/internal/FundingRoutes.js.map +1 -1
  47. package/dist/internal/Store.d.ts +61 -16
  48. package/dist/internal/Store.d.ts.map +1 -1
  49. package/dist/internal/Store.js +72 -17
  50. package/dist/internal/Store.js.map +1 -1
  51. package/dist/internal/Viem.d.ts +1 -1
  52. package/dist/internal/index.d.ts +27 -0
  53. package/dist/internal/index.d.ts.map +1 -0
  54. package/dist/internal/index.js +27 -0
  55. package/dist/internal/index.js.map +1 -0
  56. package/package.json +8 -3
  57. package/src/App.test.ts +60 -4
  58. package/src/Client.test-d.ts +0 -47
  59. package/src/apps/data/FundingRouteProviders.test.ts +37 -297
  60. package/src/apps/data/FundingRouteProviders.ts +3 -435
  61. package/src/apps/data/routes/funding.test.ts +7 -109
  62. package/src/apps/data/routes/funding.ts +8 -323
  63. package/src/apps/data/routes/indexer.test.ts +2 -2
  64. package/src/apps/data/routes/webhooks.test.ts +2 -2
  65. package/src/apps/mcp.test.ts +55 -14
  66. package/src/apps/mcp.ts +3 -1
  67. package/src/cloudflare.ts +5 -0
  68. package/src/internal/Auth.test.ts +397 -16
  69. package/src/internal/Auth.ts +76 -32
  70. package/src/internal/FundingRoutes.test.ts +8 -34
  71. package/src/internal/FundingRoutes.ts +1 -174
  72. package/src/internal/Log.test.ts +4 -4
  73. package/src/internal/Store.test-d.ts +15 -0
  74. package/src/internal/Store.test.ts +153 -2
  75. package/src/internal/Store.ts +157 -41
  76. package/src/internal/index.test.ts +34 -0
  77. package/src/internal/index.ts +26 -0
@@ -8,7 +8,6 @@ import * as FundingRoutes from '../../../internal/FundingRoutes.js'
8
8
  import * as OpenApi from '../../../internal/OpenApi.js'
9
9
  import * as Response from '../../../internal/Response.js'
10
10
  import * as Schema from '../../../internal/Schema.js'
11
- import * as FundingRouteProviders from '../FundingRouteProviders.js'
12
11
 
13
12
  /** Zod schemas owned by the funding routes resource. */
14
13
  export namespace schema {
@@ -42,7 +41,7 @@ export namespace schema {
42
41
  .optional(z.record(z.string(), z.string()))
43
42
  .check(
44
43
  z.describe('Provider-specific chain ids Tempo uses when querying route providers.'),
45
- z.meta({ examples: [{ across: '8453', relay: '8453' }] }),
44
+ z.meta({ examples: [{ bridge: 'base', relay: '8453' }] }),
46
45
  ),
47
46
  }),
48
47
  'A normalized chain reference for funding routes.',
@@ -123,7 +122,7 @@ export namespace schema {
123
122
  /** Provider summary embedded in a route candidate. */
124
123
  export const ProviderRef = Schema.describe(
125
124
  z.object({
126
- 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'] })),
127
126
  kind: z
128
127
  .string()
129
128
  .check(
@@ -132,7 +131,7 @@ export namespace schema {
132
131
  ),
133
132
  name: z
134
133
  .string()
135
- .check(z.describe('Human-readable provider name.'), z.meta({ examples: ['Across'] })),
134
+ .check(z.describe('Human-readable provider name.'), z.meta({ examples: ['Relay'] })),
136
135
  requiresApiKey: z
137
136
  .boolean()
138
137
  .check(
@@ -269,111 +268,6 @@ export namespace schema {
269
268
  'Provider-derived quote, limit, and freshness data for a route candidate.',
270
269
  )
271
270
 
272
- /** Token reference attached to a normalized quote fee. */
273
- export const QuoteFeeToken = Schema.describe(
274
- z.object({
275
- address: z
276
- .string()
277
- .check(
278
- z.describe('Contract address of the fee token.'),
279
- z.meta({ examples: ['0x833589fcd6edb6e08f4c7c32d4f71b54bda02913'] }),
280
- ),
281
- chainId: z
282
- .string()
283
- .check(z.describe('CAIP-2 chain id of the fee token.'), z.meta({ examples: ['eip155:8453'] })), // prettier-ignore
284
- decimals: z
285
- .number()
286
- .check(
287
- z.int(),
288
- z.nonnegative(),
289
- z.describe('Number of decimal places used by the fee token.'),
290
- z.meta({ examples: [6] }),
291
- ),
292
- symbol: z
293
- .string()
294
- .check(z.describe('Ticker symbol of the fee token.'), z.meta({ examples: ['USDC'] })),
295
- }),
296
- 'Token in which a normalized quote fee is denominated.',
297
- )
298
-
299
- /** Normalized fee summary for an explicit funding quote. */
300
- export const QuoteFee = Schema.describe(
301
- z.object({
302
- amount: Schema.DecimalString.check(
303
- z.describe("Fee amount in the fee token's smallest unit."),
304
- z.meta({ examples: ['1200'] }),
305
- ),
306
- amountUsd: z
307
- .nullable(z.string().check(z.regex(/^\d+(\.\d+)?$/)))
308
- .check(
309
- z.describe('Provider-estimated USD value of the fee.'),
310
- z.meta({ examples: ['0.0012'] }),
311
- ),
312
- bps: z
313
- .nullable(z.number().check(z.int(), z.nonnegative()))
314
- .check(z.describe('Fee as basis points of the quoted input.'), z.meta({ examples: [12] })),
315
- token: QuoteFeeToken,
316
- }),
317
- 'Provider-neutral fee amount and denomination.',
318
- )
319
-
320
- /** Explicit depositor-specific quote for one curated funding route. */
321
- export const FundingQuote = Schema.describe(
322
- z.object({
323
- destinationChain: Chain.check(z.describe('Destination chain for the quoted route.')),
324
- destinationToken: Token.check(z.describe('Token received on the destination chain.')),
325
- estimatedSeconds: z
326
- .nullable(z.number().check(z.int(), z.nonnegative()))
327
- .check(z.describe('Estimated settlement time in seconds.'), z.meta({ examples: [2] })),
328
- expiresAt: z.iso
329
- .datetime()
330
- .check(
331
- z.describe('Timestamp after which the quote must not be used.'),
332
- z.meta({ examples: ['2026-01-01T00:01:00Z'] }),
333
- ),
334
- fees: z
335
- .object({
336
- total: QuoteFee.check(z.describe('Current estimated total fee.')),
337
- totalMax: QuoteFee.check(z.describe('Maximum total fee allowed by the quote.')),
338
- })
339
- .check(z.describe('Current and maximum provider fee summaries.')),
340
- inputAmount: Schema.DecimalString.check(
341
- z.describe('Exact source amount quoted in base units.'),
342
- z.meta({ examples: ['1000000'] }),
343
- ),
344
- minimumOutputAmount: Schema.DecimalString.check(
345
- z.describe('Slippage-adjusted minimum destination amount in base units.'),
346
- z.meta({ examples: ['988000'] }),
347
- ),
348
- outputAmount: Schema.DecimalString.check(
349
- z.describe('Expected destination amount in base units.'),
350
- z.meta({ examples: ['990000'] }),
351
- ),
352
- outputAmountFormatted: z
353
- .string()
354
- .check(
355
- z.describe('Expected destination amount rendered with token decimals.'),
356
- z.meta({ examples: ['0.99'] }),
357
- ),
358
- provider: ProviderRef.check(z.describe('Provider that produced the quote.')),
359
- routeId: z
360
- .string()
361
- .check(
362
- z.describe('Stable id of the curated route that was quoted.'),
363
- z.meta({ examples: ['base-usdc-tempo-usdce-across'] }),
364
- ),
365
- sampledAt: z.iso
366
- .datetime()
367
- .check(
368
- z.describe('Timestamp when Tempo sampled the provider quote.'),
369
- z.meta({ examples: ['2026-01-01T00:00:00Z'] }),
370
- ),
371
- sourceChain: Chain.check(z.describe('Source chain for the quoted route.')),
372
- sourceToken: Token.check(z.describe('Token spent on the source chain.')),
373
- }),
374
- 'Normalized quote information without provider execution payloads.',
375
- )
376
-
377
271
  /** Static or live funding route candidate. */
378
272
  export const RouteCandidate = Schema.describe(
379
273
  z.object({
@@ -389,7 +283,7 @@ export namespace schema {
389
283
  .string()
390
284
  .check(
391
285
  z.describe('Stable deterministic route candidate id.'),
392
- z.meta({ examples: ['base-usdc-tempo-usdce-across'] }),
286
+ z.meta({ examples: ['base-usdc-tempo-usdce-relay'] }),
393
287
  ),
394
288
  provider: ProviderRef,
395
289
  quality: Quality,
@@ -475,9 +369,9 @@ export namespace schema {
475
369
  .optional(z.string())
476
370
  .check(
477
371
  z.describe(
478
- '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.',
479
373
  ),
480
- z.meta({ examples: ['across', 'bridge'] }),
374
+ z.meta({ examples: ['relay', 'bridge'] }),
481
375
  ),
482
376
  sourceAmount: z
483
377
  .string()
@@ -528,63 +422,6 @@ export namespace schema {
528
422
  )
529
423
  }
530
424
 
531
- /** Schemas for the quoteFundingRoute operation. */
532
- export namespace quoteFundingRoute {
533
- /** JSON body for an explicit funding route quote. */
534
- export const Body = z
535
- .strictObject({
536
- depositor: Schema.Address.check(
537
- z.describe('Address that would submit the funding transaction.'),
538
- ),
539
- destinationToken: z
540
- .optional(z.string())
541
- .check(
542
- z.describe('Tempo destination token symbol, address, or token key.'),
543
- z.meta({ examples: ['USDC.e'] }),
544
- ),
545
- provider: z
546
- .literal('across')
547
- .check(
548
- z.describe('Provider id selected from funding route discovery.'),
549
- z.meta({ examples: ['across'] }),
550
- ),
551
- recipient: z
552
- .optional(Schema.Address)
553
- .check(z.describe('Optional destination recipient; defaults to the depositor.')),
554
- sourceAmount: z
555
- .string()
556
- .check(
557
- z.minLength(1),
558
- z.describe('Exact source amount to quote.'),
559
- z.meta({ examples: ['1000000'] }),
560
- ),
561
- sourceAmountUnits: z
562
- ._default(z.enum(['baseUnits', 'decimal']), 'baseUnits')
563
- .check(
564
- z.describe('Whether `sourceAmount` uses base units or decimal notation.'),
565
- z.meta({ examples: ['baseUnits'] }),
566
- ),
567
- sourceChain: z
568
- .string()
569
- .check(
570
- z.minLength(1),
571
- z.describe('Source chain alias or CAIP-2 id.'),
572
- z.meta({ examples: ['base'] }),
573
- ),
574
- sourceToken: z
575
- .string()
576
- .check(
577
- z.minLength(1),
578
- z.describe('Source token symbol, address, mint, or token key.'),
579
- z.meta({ examples: ['USDC'] }),
580
- ),
581
- })
582
- .check(z.describe('Parameters for a depositor-specific exact-input funding quote.'))
583
-
584
- /** Normalized funding quote without provider execution payloads. */
585
- export const Response = FundingQuote
586
- }
587
-
588
425
  /** Schemas for the listFundingChains operation. */
589
426
  export namespace listFundingChains {
590
427
  /** Non-paginated source-chain inventory. */
@@ -626,12 +463,12 @@ export namespace schema {
626
463
  const fundingRouteResponseExamples = {
627
464
  baseUsdc: {
628
465
  description:
629
- '`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`',
630
467
  summary: 'Base USDC',
631
468
  value: {
632
469
  data: FundingRoutes.listStaticRoutes({
633
470
  limit: 10,
634
- provider: 'across',
471
+ provider: 'relay',
635
472
  sourceAmount: '1000000',
636
473
  sourceAmountUnits: 'baseUnits',
637
474
  sourceChain: 'base',
@@ -681,56 +518,6 @@ const providersResponseExample = {
681
518
  data: FundingRoutes.listProviders(),
682
519
  } satisfies z.output<typeof schema.listProviders.Response>
683
520
 
684
- const [fundingQuoteRouteExample] = FundingRoutes.listStaticRoutes({
685
- destinationToken: 'USDC.e',
686
- limit: 1,
687
- provider: 'across',
688
- sourceAmount: '1000000',
689
- sourceAmountUnits: 'baseUnits',
690
- sourceChain: 'base',
691
- sourceToken: 'USDC',
692
- })
693
-
694
- const fundingQuoteResponseExample = {
695
- destinationChain: fundingQuoteRouteExample!.destinationChain,
696
- destinationToken: fundingQuoteRouteExample!.destinationToken,
697
- estimatedSeconds: 2,
698
- expiresAt: '2026-01-01T00:01:00Z',
699
- fees: {
700
- total: {
701
- amount: '1200',
702
- amountUsd: '0.0012',
703
- bps: 12,
704
- token: {
705
- address: fundingQuoteRouteExample!.sourceToken.address,
706
- chainId: fundingQuoteRouteExample!.sourceChain.id,
707
- decimals: fundingQuoteRouteExample!.sourceToken.decimals,
708
- symbol: fundingQuoteRouteExample!.sourceToken.symbol,
709
- },
710
- },
711
- totalMax: {
712
- amount: '1800',
713
- amountUsd: '0.0018',
714
- bps: 18,
715
- token: {
716
- address: fundingQuoteRouteExample!.sourceToken.address,
717
- chainId: fundingQuoteRouteExample!.sourceChain.id,
718
- decimals: fundingQuoteRouteExample!.sourceToken.decimals,
719
- symbol: fundingQuoteRouteExample!.sourceToken.symbol,
720
- },
721
- },
722
- },
723
- inputAmount: '1000000',
724
- minimumOutputAmount: '988000',
725
- outputAmount: '990000',
726
- outputAmountFormatted: '0.99',
727
- provider: fundingQuoteRouteExample!.provider,
728
- routeId: fundingQuoteRouteExample!.id,
729
- sampledAt: '2026-01-01T00:00:00Z',
730
- sourceChain: fundingQuoteRouteExample!.sourceChain,
731
- sourceToken: fundingQuoteRouteExample!.sourceToken,
732
- } satisfies z.output<typeof schema.quoteFundingRoute.Response>
733
-
734
521
  /** Mounts funding route discovery and generic provider inventory endpoints. */
735
522
  export function funding(options: funding.Options = {}) {
736
523
  return new Hono<App.Environment>()
@@ -798,103 +585,6 @@ export function funding(options: funding.Options = {}) {
798
585
  }
799
586
  },
800
587
  )
801
- .post(
802
- '/v1/funding/routes/quote',
803
- Auth.policy({ apiKey: { scopes: ['data:read'] }, mpp: true, public: true }),
804
- OpenApi.validate('json', schema.quoteFundingRoute.Body, {
805
- code: 'body_invalid',
806
- message: 'Invalid quote parameters',
807
- }),
808
- OpenApi.describeRoute({
809
- description:
810
- 'Gets a depositor-specific exact-input quote for one curated funding route. The response excludes provider transaction calldata, approvals, and wallet balance details.',
811
- operationId: 'quoteFundingRoute',
812
- responses: OpenApi.responses({
813
- errors: {
814
- 400: { codes: ['body_invalid'] },
815
- 404: { codes: ['route_not_found', 'quote_unavailable'] },
816
- 501: { codes: ['provider_unconfigured'] },
817
- 502: { codes: ['provider_error', 'provider_rate_limited', 'provider_timeout'] },
818
- },
819
- success: {
820
- description: 'Normalized route quote without provider execution payloads.',
821
- example: fundingQuoteResponseExample,
822
- schema: schema.quoteFundingRoute.Response,
823
- },
824
- }),
825
- summary: 'Quote funding route',
826
- tags: ['Funding'],
827
- }),
828
- async (c) => {
829
- if (Auth.narrowAccess) return Auth.paidAccessError(c)
830
- if (OpenApi.narrowValidation)
831
- return OpenApi.validationError(c, {
832
- code: 'body_invalid',
833
- message: 'Invalid quote parameters',
834
- })
835
-
836
- const body = c.req.valid('json')
837
- try {
838
- const quote = await FundingRoutes.quoteRoute({
839
- depositor: body.depositor,
840
- destinationToken: body.destinationToken,
841
- provider: body.provider,
842
- quote: options.acrossQuote,
843
- quoteTimeoutMs: options.quoteTimeoutMs,
844
- recipient: body.recipient,
845
- sourceAmount: body.sourceAmount,
846
- sourceAmountUnits: body.sourceAmountUnits,
847
- sourceChain: body.sourceChain,
848
- sourceToken: body.sourceToken,
849
- })
850
- c.header('Cache-Control', 'no-store')
851
- return c.json(Response.validated(schema.quoteFundingRoute.Response, quote), 200)
852
- } catch (cause) {
853
- const message = requestErrorMessage(cause)
854
- if (message) return Response.error(c, { code: 'body_invalid', message, status: 400 })
855
- if (cause instanceof FundingRoutes.FundingRouteNotFoundError)
856
- return Response.error(c, {
857
- code: 'route_not_found',
858
- message: 'No curated funding route matches the request.',
859
- status: 404,
860
- })
861
- if (cause instanceof FundingRoutes.ProviderQuoteUnavailableError)
862
- return Response.error(c, {
863
- code: 'quote_unavailable',
864
- message: 'The provider cannot quote this route.',
865
- status: 404,
866
- })
867
- if (cause instanceof FundingRoutes.ProviderNotConfiguredError)
868
- return Response.error(c, {
869
- code: 'provider_unconfigured',
870
- message: 'The provider quote integration is not configured.',
871
- status: 501,
872
- })
873
- if (cause instanceof FundingRoutes.ProviderRateLimitError)
874
- return Response.error(c, {
875
- code: 'provider_rate_limited',
876
- message: 'The provider temporarily rate-limited this quote.',
877
- status: 502,
878
- })
879
- if (cause instanceof FundingRoutes.ProviderAdapterTimeoutError)
880
- return Response.error(c, {
881
- code: 'provider_timeout',
882
- message: 'The provider quote timed out.',
883
- status: 502,
884
- })
885
- if (cause instanceof Error) {
886
- const providerFailure = FundingRouteProviders.acrossQuoteFailure({ cause })
887
- if (providerFailure) c.set('providerFailure', providerFailure)
888
- return Response.error(c, {
889
- code: 'provider_error',
890
- message: 'The provider could not complete the quote.',
891
- status: 502,
892
- })
893
- }
894
- throw cause
895
- }
896
- },
897
- )
898
588
  .get(
899
589
  '/v1/funding/chains',
900
590
  Auth.policy({ apiKey: { scopes: ['data:read'] }, mpp: true, public: true }),
@@ -968,7 +658,6 @@ export function funding(options: funding.Options = {}) {
968
658
  data: FundingRoutes.listProviders({
969
659
  adapters: options.adapters,
970
660
  capability: query.capability,
971
- configuredProviders: options.acrossQuote ? ['across'] : [],
972
661
  }),
973
662
  }),
974
663
  200,
@@ -980,14 +669,10 @@ export function funding(options: funding.Options = {}) {
980
669
  export declare namespace funding {
981
670
  /** Options for funding route discovery. */
982
671
  type Options = {
983
- /** Across explicit quote capability. */
984
- acrossQuote?: FundingRoutes.ProviderQuote | undefined
985
672
  /** Provider adapters used to enrich static inventory with live data. */
986
673
  adapters?: FundingRoutes.ProviderAdapters | undefined
987
674
  /** Per-provider quote timeout in milliseconds. */
988
675
  adapterTimeoutMs?: number | undefined
989
- /** Timeout for depositor-specific provider quotes. */
990
- quoteTimeoutMs?: number | undefined
991
676
  }
992
677
  }
993
678
 
@@ -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
 
@@ -91,6 +91,46 @@ async function toolsCall(
91
91
  }
92
92
  }
93
93
 
94
+ /** Searches every page of the progressive tool catalog. */
95
+ async function toolsSearch(host: Host) {
96
+ const tools: { annotations?: { readOnlyHint?: boolean }; name: string }[] = []
97
+ let offset = 0
98
+ while (true) {
99
+ const result = resultJson(await toolsCall(host, 'search_tools', { limit: 20, offset })) as {
100
+ nextOffset?: number
101
+ tools: typeof tools
102
+ }
103
+ tools.push(...result.tools)
104
+ if (result.nextOffset === undefined) return tools
105
+ offset = result.nextOffset
106
+ }
107
+ }
108
+
109
+ /** Loads the complete schema and metadata for one catalog tool. */
110
+ async function toolDetails(host: Host, name: string) {
111
+ return resultJson(await toolsCall(host, 'get_tool_details', { name })) as {
112
+ annotations?: { readOnlyHint?: boolean }
113
+ inputSchema: { properties?: Record<string, { description?: string }> }
114
+ name: string
115
+ }
116
+ }
117
+
118
+ /** Inspects and executes a generated tool through its matching read or write gate. */
119
+ async function generatedToolCall(
120
+ host: Host,
121
+ name: string,
122
+ args: Record<string, unknown>,
123
+ headers: Record<string, string> = {},
124
+ ) {
125
+ const details = await toolDetails(host, name)
126
+ return toolsCall(
127
+ host,
128
+ details.annotations?.readOnlyHint === true ? 'call_read_tool' : 'call_write_tool',
129
+ { arguments: args, name },
130
+ headers,
131
+ )
132
+ }
133
+
94
134
  /** Parses a tool result: structured content when present, else the JSON text item. */
95
135
  function resultJson(result: Awaited<ReturnType<typeof toolsCall>>) {
96
136
  return result.structuredContent ?? JSON.parse(result.content[0]!.text)
@@ -108,7 +148,13 @@ test('scopes tools to the data domain', async () => {
108
148
  expect(Object.keys(spec.paths).some((path) => path.startsWith('/v1/orgs'))).toBe(true)
109
149
 
110
150
  await initialize(host)
111
- const tools = await toolsList(host)
151
+ expect(await toolsList(host)).toEqual([
152
+ 'search_tools',
153
+ 'get_tool_details',
154
+ 'call_read_tool',
155
+ 'call_write_tool',
156
+ ])
157
+ const tools = (await toolsSearch(host)).map((tool) => tool.name)
112
158
 
113
159
  expect(tools).toContain('v1_tokens_get')
114
160
  expect(tools).toContain('docs_search')
@@ -125,13 +171,8 @@ test('keeps tool input schemas compact', async () => {
125
171
  const { host } = createHost()
126
172
  await initialize(host)
127
173
 
128
- const body = await request(host, { id: 2, jsonrpc: '2.0', method: 'tools/list', params: {} })
129
- const tools = body.result.tools as {
130
- name: string
131
- inputSchema: { properties?: Record<string, { description?: string }> }
132
- }[]
133
- const tool = tools.find((entry) => entry.name === 'v1_tokens_get')
134
- const properties = tool?.inputSchema.properties ?? {}
174
+ const tool = await toolDetails(host, 'v1_tokens_get')
175
+ const properties = tool.inputSchema.properties ?? {}
135
176
 
136
177
  // Credential inputs stay off the schema; auth rides the forwarded header.
137
178
  expect(properties['authorization']).toBeUndefined()
@@ -142,7 +183,7 @@ test('keeps tool input schemas compact', async () => {
142
183
  'Keyset cursor from a previous response `nextCursor`; omit for the first page.',
143
184
  )
144
185
  // `compact` strips examples from generated schemas.
145
- expect(JSON.stringify(tool?.inputSchema)).not.toContain('"examples"')
186
+ expect(JSON.stringify(tool.inputSchema)).not.toContain('"examples"')
146
187
  })
147
188
 
148
189
  test('executes generated tools against the composed app', async () => {
@@ -152,7 +193,7 @@ test('executes generated tools against the composed app', async () => {
152
193
  const { host } = createHost({ app: TestApp.create({ db }) })
153
194
 
154
195
  await initialize(host)
155
- const result = await toolsCall(host, 'v1_tokens_get', { verified: true })
196
+ const result = await generatedToolCall(host, 'v1_tokens_get', { verified: true })
156
197
 
157
198
  expect(result.isError).toBeFalsy()
158
199
  const body = resultJson(result) as { data: { verified: boolean }[]; nextCursor: unknown }
@@ -164,10 +205,10 @@ test('forwards the caller Authorization header to authenticated tools', async ()
164
205
  const { host } = createHost({ app: TestApp.create({ auth: { keys: [reader] } }) })
165
206
  await initialize(host)
166
207
 
167
- const denied = await toolsCall(host, 'v1_webhooks_get', {})
208
+ const denied = await generatedToolCall(host, 'v1_webhooks_get', {})
168
209
  expect(denied.isError).toBe(true)
169
210
 
170
- const allowed = await toolsCall(
211
+ const allowed = await generatedToolCall(
171
212
  host,
172
213
  'v1_webhooks_get',
173
214
  {},
@@ -181,7 +222,7 @@ test('proxies docs tool calls to the remote MCP source', async () => {
181
222
  const { host } = createHost({ docs: docsStub() })
182
223
  await initialize(host)
183
224
 
184
- const result = await toolsCall(host, 'docs_search', { query: 'fees' })
225
+ const result = await generatedToolCall(host, 'docs_search', { query: 'fees' })
185
226
 
186
227
  expect(result.isError).toBeFalsy()
187
228
  expect(resultJson(result)).toMatchObject({ hits: ['fees'] })
@@ -210,7 +251,7 @@ test('documents POST /mcp and keeps it open under auth enforcement', async () =>
210
251
  // Documenting the route makes it auth-enforced; the public lane keeps it
211
252
  // anonymously reachable, and the endpoint never surfaces itself as a tool.
212
253
  await initialize(app)
213
- const tools = await toolsList(app)
254
+ const tools = (await toolsSearch(app)).map((tool) => tool.name)
214
255
  expect(tools.length).toBeGreaterThan(0)
215
256
  expect(tools.some((name) => name.startsWith('mcp'))).toBe(false)
216
257
 
package/src/apps/mcp.ts CHANGED
@@ -147,7 +147,9 @@ export function mcp(options: mcp.Options) {
147
147
  example: {
148
148
  jsonrpc: '2.0',
149
149
  id: 1,
150
- result: { tools: [{ name: 'v1_tokens_get', description: 'List tokens' }] },
150
+ result: {
151
+ tools: [{ name: 'search_tools', description: 'Search available tools' }],
152
+ },
151
153
  },
152
154
  schema: OpenApi.resolver(schema.Response),
153
155
  },
package/src/cloudflare.ts CHANGED
@@ -26,6 +26,11 @@ export class DurableObject extends CloudflareDurableObject {
26
26
  await this.#store.delete(key)
27
27
  }
28
28
 
29
+ /** Deletes a key only when its value matches `expected`. */
30
+ async deleteIf(key: string, expected: null | string) {
31
+ return this.#store.deleteIf!(key, expected)
32
+ }
33
+
29
34
  /** Gets a string value. Returns `null` for missing or expired keys. */
30
35
  async get(key: string) {
31
36
  return this.#store.get(key)