tapimo 0.0.1 → 0.1.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.
- package/README.md +13 -3
- package/dist/ApiKey.d.ts +12 -11
- package/dist/ApiKey.d.ts.map +1 -1
- package/dist/ApiKey.js +10 -12
- package/dist/ApiKey.js.map +1 -1
- package/dist/ApiKeys.d.ts +139 -0
- package/dist/ApiKeys.d.ts.map +1 -0
- package/dist/ApiKeys.js +166 -0
- package/dist/ApiKeys.js.map +1 -0
- package/dist/App.d.ts +920 -910
- package/dist/App.d.ts.map +1 -1
- package/dist/App.js +36 -12
- package/dist/App.js.map +1 -1
- package/dist/Handler.d.ts +86 -0
- package/dist/Handler.d.ts.map +1 -0
- package/dist/Handler.js +83 -0
- package/dist/Handler.js.map +1 -0
- package/dist/Scope.d.ts +4 -1
- package/dist/Scope.d.ts.map +1 -1
- package/dist/Scope.js +5 -1
- package/dist/Scope.js.map +1 -1
- package/dist/Webhooks.d.ts +3 -5
- package/dist/Webhooks.d.ts.map +1 -1
- package/dist/Webhooks.js +9 -2
- package/dist/Webhooks.js.map +1 -1
- package/dist/admin/Access.d.ts +1 -3
- package/dist/admin/Access.d.ts.map +1 -1
- package/dist/admin/Access.js +1 -3
- package/dist/admin/Access.js.map +1 -1
- package/dist/admin/App.d.ts +49 -160
- package/dist/admin/App.d.ts.map +1 -1
- package/dist/admin/App.js +2 -4
- package/dist/admin/App.js.map +1 -1
- package/dist/admin/apps/api-keys.d.ts +7 -5
- package/dist/admin/apps/api-keys.d.ts.map +1 -1
- package/dist/admin/apps/api-keys.js +8 -9
- package/dist/admin/apps/api-keys.js.map +1 -1
- package/dist/admin/apps/scopes.d.ts +3 -0
- package/dist/admin/apps/scopes.d.ts.map +1 -1
- package/dist/admin/apps/verified-tokens.d.ts +101 -215
- package/dist/admin/apps/verified-tokens.d.ts.map +1 -1
- package/dist/admin/apps/verified-tokens.js +17 -38
- package/dist/admin/apps/verified-tokens.js.map +1 -1
- package/dist/admin/ui.generated.d.ts +1 -1
- package/dist/admin/ui.generated.js +13 -13
- package/dist/admin/ui.generated.js.map +1 -1
- package/dist/apps/balances.d.ts +43 -43
- package/dist/apps/coingecko.d.ts +176 -176
- package/dist/apps/coingecko.js +1 -1
- package/dist/apps/coingecko.js.map +1 -1
- package/dist/apps/receipts.d.ts +43 -43
- package/dist/apps/tokenlist.d.ts +36 -36
- package/dist/apps/tokens.d.ts +454 -454
- package/dist/apps/transactions.d.ts +43 -43
- package/dist/apps/transfers.d.ts +29 -29
- package/dist/apps/webhooks.d.ts +5 -2
- package/dist/apps/webhooks.d.ts.map +1 -1
- package/dist/apps/webhooks.js +38 -34
- package/dist/apps/webhooks.js.map +1 -1
- package/dist/{Cli.d.ts → cli/Cli.d.ts} +13 -2
- package/dist/cli/Cli.d.ts.map +1 -0
- package/dist/cli/Cli.js +19 -0
- package/dist/cli/Cli.js.map +1 -0
- package/dist/cli/bin.d.ts +3 -0
- package/dist/cli/bin.d.ts.map +1 -0
- package/dist/cli/bin.js +4 -0
- package/dist/cli/bin.js.map +1 -0
- package/dist/cli/commands/admin.d.ts +15 -0
- package/dist/cli/commands/admin.d.ts.map +1 -0
- package/dist/cli/commands/admin.js +60 -0
- package/dist/cli/commands/admin.js.map +1 -0
- package/dist/cloudflare.d.ts +7 -0
- package/dist/cloudflare.d.ts.map +1 -1
- package/dist/cloudflare.js +9 -0
- package/dist/cloudflare.js.map +1 -1
- package/dist/db/Db.d.ts +103 -0
- package/dist/db/Db.d.ts.map +1 -0
- package/dist/db/Db.js +274 -0
- package/dist/db/Db.js.map +1 -0
- package/dist/db/Schema.d.ts +88 -0
- package/dist/db/Schema.d.ts.map +1 -0
- package/dist/db/Schema.js +130 -0
- package/dist/db/Schema.js.map +1 -0
- package/dist/db/tables/organizations.d.ts +79 -0
- package/dist/db/tables/organizations.d.ts.map +1 -0
- package/dist/db/tables/organizations.js +81 -0
- package/dist/db/tables/organizations.js.map +1 -0
- package/dist/db/tables/verifiedTokens.d.ts +53 -0
- package/dist/db/tables/verifiedTokens.d.ts.map +1 -0
- package/dist/db/tables/verifiedTokens.js +74 -0
- package/dist/db/tables/verifiedTokens.js.map +1 -0
- package/dist/db/tables/webhookDeliveries.d.ts +71 -0
- package/dist/db/tables/webhookDeliveries.d.ts.map +1 -0
- package/dist/db/tables/webhookDeliveries.js +119 -0
- package/dist/db/tables/webhookDeliveries.js.map +1 -0
- package/dist/db/tables/webhookSubscriptions.d.ts +158 -0
- package/dist/db/tables/webhookSubscriptions.d.ts.map +1 -0
- package/dist/db/tables/webhookSubscriptions.js +272 -0
- package/dist/db/tables/webhookSubscriptions.js.map +1 -0
- package/dist/handlers/internal/executionError.d.ts +25 -0
- package/dist/handlers/internal/executionError.d.ts.map +1 -0
- package/dist/handlers/internal/executionError.js +184 -0
- package/dist/handlers/internal/executionError.js.map +1 -0
- package/dist/handlers/internal/multisig.d.ts +237 -0
- package/dist/handlers/internal/multisig.d.ts.map +1 -0
- package/dist/handlers/internal/multisig.js +567 -0
- package/dist/handlers/internal/multisig.js.map +1 -0
- package/dist/handlers/internal/requestListener.d.ts +124 -0
- package/dist/handlers/internal/requestListener.d.ts.map +1 -0
- package/dist/handlers/internal/requestListener.js +173 -0
- package/dist/handlers/internal/requestListener.js.map +1 -0
- package/dist/handlers/internal/sponsorship.d.ts +83 -0
- package/dist/handlers/internal/sponsorship.d.ts.map +1 -0
- package/dist/handlers/internal/sponsorship.js +103 -0
- package/dist/handlers/internal/sponsorship.js.map +1 -0
- package/dist/handlers/internal/types.d.ts +284 -0
- package/dist/handlers/internal/types.d.ts.map +1 -0
- package/dist/handlers/internal/types.js +2 -0
- package/dist/handlers/internal/types.js.map +1 -0
- package/dist/handlers/internal/utils.d.ts +34 -0
- package/dist/handlers/internal/utils.d.ts.map +1 -0
- package/dist/handlers/internal/utils.js +131 -0
- package/dist/handlers/internal/utils.js.map +1 -0
- package/dist/handlers/relay.d.ts +119 -0
- package/dist/handlers/relay.d.ts.map +1 -0
- package/dist/handlers/relay.js +1264 -0
- package/dist/handlers/relay.js.map +1 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/internal/Auth.d.ts +16 -10
- package/dist/internal/Auth.d.ts.map +1 -1
- package/dist/internal/Auth.js +78 -13
- package/dist/internal/Auth.js.map +1 -1
- package/dist/internal/RpcSchema.d.ts +10 -0
- package/dist/internal/RpcSchema.d.ts.map +1 -0
- package/dist/internal/RpcSchema.js +2 -0
- package/dist/internal/RpcSchema.js.map +1 -0
- package/dist/internal/Store.d.ts +42 -3
- package/dist/internal/Store.d.ts.map +1 -1
- package/dist/internal/Store.js +87 -8
- package/dist/internal/Store.js.map +1 -1
- package/dist/internal/VerifiedTokens.d.ts +15 -34
- package/dist/internal/VerifiedTokens.d.ts.map +1 -1
- package/dist/internal/VerifiedTokens.js +115 -159
- package/dist/internal/VerifiedTokens.js.map +1 -1
- package/dist/internal/Viem.d.ts +81 -7
- package/dist/internal/Viem.d.ts.map +1 -1
- package/dist/internal/Webhooks.d.ts +49 -35
- package/dist/internal/Webhooks.d.ts.map +1 -1
- package/dist/internal/Webhooks.js +92 -205
- package/dist/internal/Webhooks.js.map +1 -1
- package/package.json +10 -5
- package/src/ApiKey.test.ts +3 -3
- package/src/ApiKey.ts +10 -12
- package/src/ApiKeys.test.ts +175 -0
- package/src/ApiKeys.ts +205 -0
- package/src/App.test-d.ts +1 -1
- package/src/App.test.ts +10768 -351
- package/src/App.ts +79 -36
- package/src/Client.test-d.ts +1 -1
- package/src/Handler.ts +154 -0
- package/src/Scope.ts +5 -1
- package/src/Webhooks.test.ts +8 -7
- package/src/Webhooks.ts +11 -5
- package/src/admin/Access.ts +1 -3
- package/src/admin/App.ts +19 -24
- package/src/admin/apps/api-keys.test.ts +19 -18
- package/src/admin/apps/api-keys.ts +8 -9
- package/src/admin/apps/scopes.test.ts +4 -0
- package/src/admin/apps/verified-tokens.test.ts +3 -30
- package/src/admin/apps/verified-tokens.ts +17 -36
- package/src/admin/ui.generated.ts +13 -13
- package/src/apps/balances.test.ts +4 -3
- package/src/apps/blocks.test.ts +1 -1
- package/src/apps/coingecko.ts +1 -1
- package/src/apps/exchanges.test.ts +1 -1
- package/src/apps/indexer.test.ts +2 -2
- package/src/apps/receipts.test.ts +1 -1
- package/src/apps/rpc.test.ts +100 -10
- package/src/apps/tokenlist.test.ts +5 -7
- package/src/apps/tokens.test.ts +4 -3
- package/src/apps/transactions.test.ts +1 -1
- package/src/apps/transfers.test.ts +1 -1
- package/src/apps/verified-tokens.test.ts +24 -12
- package/src/apps/webhooks.test.ts +131 -144
- package/src/apps/webhooks.ts +45 -47
- package/src/{Cli.test.ts → cli/Cli.test.ts} +10 -9
- package/src/{Cli.ts → cli/Cli.ts} +16 -9
- package/src/cli/bin.ts +4 -0
- package/src/cli/commands/admin.test.ts +71 -0
- package/src/cli/commands/admin.ts +60 -0
- package/src/cloudflare.ts +10 -0
- package/src/db/Db.test.ts +84 -0
- package/src/db/Db.ts +383 -0
- package/src/db/Schema.ts +164 -0
- package/src/db/tables/organizations.test.ts +124 -0
- package/src/db/tables/organizations.ts +127 -0
- package/src/db/tables/verifiedTokens.ts +111 -0
- package/src/db/tables/webhookDeliveries.ts +159 -0
- package/src/db/tables/webhookSubscriptions.ts +358 -0
- package/src/handlers/internal/executionError.ts +205 -0
- package/src/handlers/internal/multisig.localnet.test.ts +146 -0
- package/src/handlers/internal/multisig.test.ts +744 -0
- package/src/handlers/internal/multisig.ts +858 -0
- package/src/handlers/internal/relay.localnet.test.ts +2035 -0
- package/src/handlers/internal/requestListener.ts +273 -0
- package/src/handlers/internal/sponsorship.ts +186 -0
- package/src/handlers/internal/types.ts +378 -0
- package/src/handlers/internal/utils.ts +153 -0
- package/src/handlers/relay.ts +1605 -0
- package/src/index.ts +4 -2
- package/src/internal/Auth.test.ts +78 -20
- package/src/internal/Auth.ts +98 -20
- package/src/internal/RateLimit.test.ts +1 -1
- package/src/internal/RpcSchema.ts +10 -0
- package/src/internal/Store.test.ts +179 -7
- package/src/internal/Store.ts +136 -11
- package/src/internal/VerifiedTokens.test.ts +99 -115
- package/src/internal/VerifiedTokens.ts +123 -175
- package/src/internal/Viem.ts +2 -1
- package/src/internal/Webhooks.test.ts +107 -107
- package/src/internal/Webhooks.ts +102 -220
- package/dist/ApiKeySource.d.ts +0 -166
- package/dist/ApiKeySource.d.ts.map +0 -1
- package/dist/ApiKeySource.js +0 -234
- package/dist/ApiKeySource.js.map +0 -1
- package/dist/Cli.d.ts.map +0 -1
- package/dist/Cli.js +0 -13
- package/dist/Cli.js.map +0 -1
- package/seeds/31318/icons/0x20c0000000000000000000000000000000000000.svg +0 -1
- package/seeds/31318/icons/0x20c0000000000000000000000000000000000001.svg +0 -1
- package/seeds/31318/icons/0x20c0000000000000000000000000000000000002.svg +0 -1
- package/seeds/31318/icons/0x20c0000000000000000000000000000000000003.svg +0 -1
- package/seeds/31318/verified-tokens.json +0 -30
- package/seeds/4217/icons/0x20c0000000000000000000000000000000000000.svg +0 -25
- package/seeds/4217/icons/0x20c000000000000000000000048c8f36df1c9a4a.svg +0 -1
- package/seeds/4217/icons/0x20c0000000000000000000000520792dcccccccc.svg +0 -1
- package/seeds/4217/icons/0x20c0000000000000000000000a6da882d075a4c3.svg +0 -4
- package/seeds/4217/icons/0x20c000000000000000000000111111111e910f0f.svg +0 -30
- package/seeds/4217/icons/0x20c00000000000000000000014f22ca97301eb73.svg +0 -1
- package/seeds/4217/icons/0x20c0000000000000000000001621e21f71cf12fb.svg +0 -1
- package/seeds/4217/icons/0x20c0000000000000000000002f52d5cc21a3207b.svg +0 -1
- package/seeds/4217/icons/0x20c0000000000000000000003158081efd85bfc2.svg +0 -11
- package/seeds/4217/icons/0x20c0000000000000000000003554d28269e0f3c2.svg +0 -1
- package/seeds/4217/icons/0x20c000000000000000000000383a23bacb546ab9.svg +0 -1
- package/seeds/4217/icons/0x20c00000000000000000000042109aef2f8b28e1.png +0 -0
- package/seeds/4217/icons/0x20c0000000000000000000005c0bac7cef389a11.svg +0 -1
- package/seeds/4217/icons/0x20c0000000000000000000006fd9a167923ba194.png +0 -0
- package/seeds/4217/icons/0x20c0000000000000000000007f7ba549dd0251b9.svg +0 -1
- package/seeds/4217/icons/0x20c0000000000000000000008191667423f70e67.svg +0 -5
- package/seeds/4217/icons/0x20c0000000000000000000008ee4fcff88888888.svg +0 -1
- package/seeds/4217/icons/0x20c0000000000000000000009a4a4b17e0dc6651.svg +0 -1
- package/seeds/4217/icons/0x20c000000000000000000000ab02d39df30bd17e.svg +0 -1
- package/seeds/4217/icons/0x20c000000000000000000000ae247a1130450f09.svg +0 -1
- package/seeds/4217/icons/0x20c000000000000000000000aeed2ec36a54d0e5.svg +0 -1
- package/seeds/4217/icons/0x20c000000000000000000000b9537d11c60e8b50.svg +0 -1
- package/seeds/4217/icons/0x20c000000000000000000000bd95bfb69fbe6ce3.svg +0 -1
- package/seeds/4217/icons/0x20c000000000000000000000c412ec89d0c08be5.svg +0 -1
- package/seeds/4217/icons/0x20c000000000000000000000f047dd7018e50367.png +0 -0
- package/seeds/4217/verified-tokens.json +0 -177
- package/seeds/42431/icons/0x20c0000000000000000000000000000000000000.svg +0 -25
- package/seeds/42431/icons/0x20c0000000000000000000000000000000000001.svg +0 -1
- package/seeds/42431/icons/0x20c0000000000000000000000000000000000002.svg +0 -1
- package/seeds/42431/icons/0x20c0000000000000000000000000000000000003.svg +0 -1
- package/seeds/42431/icons/0x20c0000000000000000000009e8d7eb59b783726.svg +0 -1
- package/seeds/42431/icons/0x20c000000000000000000000d72572838bbee59c.svg +0 -1
- package/seeds/42431/verified-tokens.json +0 -44
- package/seeds/openrpc/README.md +0 -27
- package/seeds/openrpc/execution-apis.json +0 -8989
- package/src/ApiKeySource.test-d.ts +0 -11
- package/src/ApiKeySource.test.ts +0 -295
- package/src/ApiKeySource.ts +0 -303
package/src/index.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
export * as ApiKey from './ApiKey.js'
|
|
2
|
-
export * as
|
|
2
|
+
export * as ApiKeys from './ApiKeys.js'
|
|
3
3
|
export * as App from './App.js'
|
|
4
4
|
export * as Assets from './Assets.js'
|
|
5
5
|
export * as Auth from './Auth.js'
|
|
6
|
-
export * as Cli from './Cli.js'
|
|
6
|
+
export * as Cli from './cli/Cli.js'
|
|
7
7
|
export * as Client from './Client.js'
|
|
8
|
+
export * as Db from './db/Db.js'
|
|
9
|
+
export * as Handler from './Handler.js'
|
|
8
10
|
export * as Log from './internal/Log.js'
|
|
9
11
|
export * as Metrics from './Metrics.js'
|
|
10
12
|
export * as RateLimit from './internal/RateLimit.js'
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Hono } from 'hono'
|
|
2
2
|
import { requestId } from 'hono/request-id'
|
|
3
3
|
import { Mppx, tempo } from 'mppx/hono'
|
|
4
|
-
import { RateLimit } from '
|
|
4
|
+
import { RateLimit } from 'tapimo'
|
|
5
5
|
import * as ApiKey from '../ApiKey.js'
|
|
6
6
|
import * as Auth from './Auth.js'
|
|
7
7
|
import * as Log from './Log.js'
|
|
@@ -96,7 +96,7 @@ describe('require', () => {
|
|
|
96
96
|
// the downstream validator rejects it as `query_invalid` and the legacy
|
|
97
97
|
// `?key=...` lane breaks on every strict-query endpoint.
|
|
98
98
|
const auth = {
|
|
99
|
-
apiKey: {
|
|
99
|
+
apiKey: { resolve: TestApp.resolver({ keys: [{ ...key, token }] }) },
|
|
100
100
|
rateLimit: RateLimit.memory(),
|
|
101
101
|
}
|
|
102
102
|
const app = new Hono<Auth.Environment>()
|
|
@@ -241,7 +241,7 @@ describe('require', () => {
|
|
|
241
241
|
const app = createApp({
|
|
242
242
|
auth: {
|
|
243
243
|
apiKey: {
|
|
244
|
-
|
|
244
|
+
resolve: TestApp.resolver({
|
|
245
245
|
keys: [{ ...key, scopes: [Scope.wildcard], token }],
|
|
246
246
|
}),
|
|
247
247
|
},
|
|
@@ -273,7 +273,7 @@ describe('require', () => {
|
|
|
273
273
|
function createChainApp(options: { public?: boolean } = {}) {
|
|
274
274
|
const auth: Auth.Context = {
|
|
275
275
|
apiKey: {
|
|
276
|
-
|
|
276
|
+
resolve: TestApp.resolver({
|
|
277
277
|
keys: [
|
|
278
278
|
{ ...key, scopes: [Scope.wildcard], token },
|
|
279
279
|
{
|
|
@@ -399,7 +399,7 @@ describe('require', () => {
|
|
|
399
399
|
const mpp = createMppServer()
|
|
400
400
|
const app = createApp({
|
|
401
401
|
auth: {
|
|
402
|
-
apiKey: {
|
|
402
|
+
apiKey: { resolve: TestApp.resolver({ keys: [{ ...key, token }] }) },
|
|
403
403
|
mpp,
|
|
404
404
|
rateLimit: overQuota(),
|
|
405
405
|
},
|
|
@@ -420,7 +420,7 @@ describe('require', () => {
|
|
|
420
420
|
const mpp = createMppServer()
|
|
421
421
|
const app = createApp({
|
|
422
422
|
auth: {
|
|
423
|
-
apiKey: {
|
|
423
|
+
apiKey: { resolve: TestApp.resolver({ keys: [{ ...key, token }] }) },
|
|
424
424
|
mpp,
|
|
425
425
|
rateLimit: overQuotaOnce(),
|
|
426
426
|
},
|
|
@@ -451,7 +451,7 @@ describe('require', () => {
|
|
|
451
451
|
const mpp = createMppServer()
|
|
452
452
|
const app = createApp({
|
|
453
453
|
auth: {
|
|
454
|
-
apiKey: {
|
|
454
|
+
apiKey: { resolve: TestApp.resolver({ keys: [{ ...key, token }] }) },
|
|
455
455
|
mpp,
|
|
456
456
|
rateLimit: overQuota(),
|
|
457
457
|
},
|
|
@@ -477,7 +477,7 @@ describe('require', () => {
|
|
|
477
477
|
const mpp = createMppServer()
|
|
478
478
|
const app = createApp({
|
|
479
479
|
auth: {
|
|
480
|
-
apiKey: {
|
|
480
|
+
apiKey: { resolve: TestApp.resolver({ keys: [{ ...key, token }] }) },
|
|
481
481
|
mpp,
|
|
482
482
|
rateLimit: overQuotaOnce(),
|
|
483
483
|
},
|
|
@@ -537,7 +537,7 @@ describe('require', () => {
|
|
|
537
537
|
test('returns rate-limit error without payment context', async () => {
|
|
538
538
|
const app = createApp({
|
|
539
539
|
auth: {
|
|
540
|
-
apiKey: {
|
|
540
|
+
apiKey: { resolve: TestApp.resolver({ keys: [{ ...key, token }] }) },
|
|
541
541
|
rateLimit: overQuota(),
|
|
542
542
|
},
|
|
543
543
|
policy: { public: { rateLimit: { perMinute: 1 } } },
|
|
@@ -555,6 +555,64 @@ describe('require', () => {
|
|
|
555
555
|
}
|
|
556
556
|
`)
|
|
557
557
|
})
|
|
558
|
+
|
|
559
|
+
test('fails open when the public rate-limit store errors', async () => {
|
|
560
|
+
const error = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
561
|
+
try {
|
|
562
|
+
const app = createApp({
|
|
563
|
+
auth: {
|
|
564
|
+
apiKey: { resolve: TestApp.resolver({ keys: [{ ...key, token }] }) },
|
|
565
|
+
rateLimit: {
|
|
566
|
+
async consume() {
|
|
567
|
+
throw new Error('Network connection lost.')
|
|
568
|
+
},
|
|
569
|
+
},
|
|
570
|
+
},
|
|
571
|
+
policy: { public: { rateLimit: { perMinute: 1 } } },
|
|
572
|
+
})
|
|
573
|
+
const response = await app.request('/protected', {
|
|
574
|
+
headers: { 'x-forwarded-for': '203.0.113.10' },
|
|
575
|
+
})
|
|
576
|
+
const body = await response.json()
|
|
577
|
+
|
|
578
|
+
expect(response.status).toMatchInlineSnapshot(`200`)
|
|
579
|
+
expect(response.headers.has('ratelimit-limit')).toMatchInlineSnapshot(`false`)
|
|
580
|
+
expect(body).toMatchInlineSnapshot(`
|
|
581
|
+
{
|
|
582
|
+
"id": "203.0.113.10",
|
|
583
|
+
"type": "public",
|
|
584
|
+
}
|
|
585
|
+
`)
|
|
586
|
+
expect(error.mock.calls.length).toMatchInlineSnapshot(`1`)
|
|
587
|
+
} finally {
|
|
588
|
+
error.mockRestore()
|
|
589
|
+
}
|
|
590
|
+
})
|
|
591
|
+
|
|
592
|
+
test('fails open when the API-key rate-limit store errors', async () => {
|
|
593
|
+
const error = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
594
|
+
try {
|
|
595
|
+
const app = createApp({
|
|
596
|
+
auth: {
|
|
597
|
+
apiKey: { resolve: TestApp.resolver({ keys: [{ ...key, token }] }) },
|
|
598
|
+
rateLimit: {
|
|
599
|
+
async consume() {
|
|
600
|
+
throw new Error('Network connection lost.')
|
|
601
|
+
},
|
|
602
|
+
},
|
|
603
|
+
},
|
|
604
|
+
})
|
|
605
|
+
const response = await app.request('/protected', {
|
|
606
|
+
headers: { 'tempo-api-key': token },
|
|
607
|
+
})
|
|
608
|
+
|
|
609
|
+
expect(response.status).toMatchInlineSnapshot(`200`)
|
|
610
|
+
expect(response.headers.has('ratelimit-limit')).toMatchInlineSnapshot(`false`)
|
|
611
|
+
expect(error.mock.calls.length).toMatchInlineSnapshot(`1`)
|
|
612
|
+
} finally {
|
|
613
|
+
error.mockRestore()
|
|
614
|
+
}
|
|
615
|
+
})
|
|
558
616
|
})
|
|
559
617
|
|
|
560
618
|
describe('rateLimit per-scope', () => {
|
|
@@ -574,7 +632,7 @@ describe('require', () => {
|
|
|
574
632
|
const auth = {
|
|
575
633
|
apiKey: {
|
|
576
634
|
rateLimits: { 'data:read': { perMinute: 1 }, 'webhooks:write': { perMinute: 1 } },
|
|
577
|
-
|
|
635
|
+
resolve: TestApp.resolver({
|
|
578
636
|
keys: [{ ...key, scopes: [Scope.wildcard], token }],
|
|
579
637
|
}),
|
|
580
638
|
},
|
|
@@ -628,7 +686,7 @@ describe('require', () => {
|
|
|
628
686
|
const auth = {
|
|
629
687
|
apiKey: {
|
|
630
688
|
rateLimits: { 'data:read': { perMinute: 1_000 } },
|
|
631
|
-
|
|
689
|
+
resolve: TestApp.resolver({
|
|
632
690
|
keys: [{ ...key, scopes: [Scope.wildcard], token }],
|
|
633
691
|
}),
|
|
634
692
|
},
|
|
@@ -652,7 +710,7 @@ describe('require', () => {
|
|
|
652
710
|
})
|
|
653
711
|
|
|
654
712
|
test('per-key default overrides config default but not a route cap', async () => {
|
|
655
|
-
const
|
|
713
|
+
const resolve = TestApp.resolver({
|
|
656
714
|
keys: [
|
|
657
715
|
{
|
|
658
716
|
...key,
|
|
@@ -665,7 +723,7 @@ describe('require', () => {
|
|
|
665
723
|
|
|
666
724
|
const open = createScopedApp({
|
|
667
725
|
auth: {
|
|
668
|
-
apiKey: { rateLimits: { '*': { perMinute: 1_000 } },
|
|
726
|
+
apiKey: { rateLimits: { '*': { perMinute: 1_000 } }, resolve },
|
|
669
727
|
rateLimit: RateLimit.memory(),
|
|
670
728
|
},
|
|
671
729
|
routes: [{ path: '/protected', policy: { apiKey: { scopes: ['data:read'] } } }],
|
|
@@ -676,7 +734,7 @@ describe('require', () => {
|
|
|
676
734
|
|
|
677
735
|
const capped = createScopedApp({
|
|
678
736
|
auth: {
|
|
679
|
-
apiKey: { rateLimits: { '*': { perMinute: 1_000 } },
|
|
737
|
+
apiKey: { rateLimits: { '*': { perMinute: 1_000 } }, resolve },
|
|
680
738
|
rateLimit: RateLimit.memory(),
|
|
681
739
|
},
|
|
682
740
|
routes: [
|
|
@@ -699,7 +757,7 @@ describe('require', () => {
|
|
|
699
757
|
test('per-key per-scope overrides everything including a route cap', async () => {
|
|
700
758
|
const auth = {
|
|
701
759
|
apiKey: {
|
|
702
|
-
|
|
760
|
+
resolve: TestApp.resolver({
|
|
703
761
|
keys: [
|
|
704
762
|
{
|
|
705
763
|
...key,
|
|
@@ -732,7 +790,7 @@ describe('require', () => {
|
|
|
732
790
|
const entries: Log.Entry[] = []
|
|
733
791
|
const app = createApp({
|
|
734
792
|
auth: {
|
|
735
|
-
apiKey: {
|
|
793
|
+
apiKey: { resolve: TestApp.resolver({ keys: [{ ...key, token }] }) },
|
|
736
794
|
rateLimit: overQuota(),
|
|
737
795
|
},
|
|
738
796
|
logger: (entry) => void entries.push(entry),
|
|
@@ -759,7 +817,7 @@ describe('require', () => {
|
|
|
759
817
|
const mpp = createMppServer()
|
|
760
818
|
const app = createApp({
|
|
761
819
|
auth: {
|
|
762
|
-
apiKey: {
|
|
820
|
+
apiKey: { resolve: TestApp.resolver({ keys: [{ ...key, token }] }) },
|
|
763
821
|
mpp,
|
|
764
822
|
rateLimit: overQuota(),
|
|
765
823
|
},
|
|
@@ -788,7 +846,7 @@ function createScopedApp(options: {
|
|
|
788
846
|
}) {
|
|
789
847
|
const auth = options.auth ?? {
|
|
790
848
|
apiKey: {
|
|
791
|
-
|
|
849
|
+
resolve: TestApp.resolver({ keys: [{ ...key, scopes: [Scope.wildcard], token }] }),
|
|
792
850
|
},
|
|
793
851
|
rateLimit: RateLimit.memory(),
|
|
794
852
|
}
|
|
@@ -814,7 +872,7 @@ type Options = {
|
|
|
814
872
|
|
|
815
873
|
function createApp(options: Options = {}) {
|
|
816
874
|
const auth = options.auth ?? {
|
|
817
|
-
apiKey: {
|
|
875
|
+
apiKey: { resolve: TestApp.resolver({ keys: [{ ...key, token }] }) },
|
|
818
876
|
rateLimit: RateLimit.memory(),
|
|
819
877
|
}
|
|
820
878
|
const app = new Hono<Auth.Environment>()
|
|
@@ -845,7 +903,7 @@ function createMppServer() {
|
|
|
845
903
|
}),
|
|
846
904
|
],
|
|
847
905
|
realm: 'tempo-api-test',
|
|
848
|
-
secretKey: '
|
|
906
|
+
secretKey: 'secret_test_key_0123456789abcdef',
|
|
849
907
|
})
|
|
850
908
|
}
|
|
851
909
|
|
package/src/internal/Auth.ts
CHANGED
|
@@ -7,8 +7,9 @@ import { findTargetHandler } from 'hono/utils/handler'
|
|
|
7
7
|
import { Credential, type Method } from 'mppx'
|
|
8
8
|
import { Mppx, tempo } from 'mppx/hono'
|
|
9
9
|
|
|
10
|
-
import * as ApiKeySource from '../ApiKeySource.js'
|
|
11
10
|
import * as ApiKey from '../ApiKey.js'
|
|
11
|
+
import * as ApiKeys from '../ApiKeys.js'
|
|
12
|
+
import type * as Store from './Store.js'
|
|
12
13
|
import * as OpenApi from './OpenApi.js'
|
|
13
14
|
import * as RateLimit from './RateLimit.js'
|
|
14
15
|
import * as Response from './Response.js'
|
|
@@ -29,8 +30,8 @@ export type Context = {
|
|
|
29
30
|
* per-key {@link ApiKey.ApiKey.rateLimits} shape.
|
|
30
31
|
*/
|
|
31
32
|
rateLimits?: Record<string, RateLimit.Limit> | undefined
|
|
32
|
-
/**
|
|
33
|
-
|
|
33
|
+
/** Resolves a presented token against the app's KV store on `c`. */
|
|
34
|
+
resolve: (c: hono_Context, token: string) => Promise<ApiKey.ApiKey | null>
|
|
34
35
|
}
|
|
35
36
|
| undefined
|
|
36
37
|
/** MPP payment configuration. */
|
|
@@ -217,14 +218,20 @@ export function middleware<
|
|
|
217
218
|
const { charge = {}, ...mpp } = options.mpp === false ? {} : (options.mpp ?? {})
|
|
218
219
|
const defaults = laneDefaults(options)
|
|
219
220
|
const base = basePolicy(options, defaults)
|
|
221
|
+
// Keys resolve against the app's KV state store on request context
|
|
222
|
+
// (`App.create({ kv })`), so the middleware needs no storage wiring; when no
|
|
223
|
+
// store is configured, every token resolves to `null` (closed).
|
|
220
224
|
// Resolution is cached per isolate by default (positive 60s / negative 10s,
|
|
221
|
-
// keyed by token sha256
|
|
222
|
-
//
|
|
223
|
-
//
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
225
|
+
// keyed by token sha256) so hot keys skip the backend read on the auth path
|
|
226
|
+
// of every request. Pass `apiKey.cache: false` when instant revocation
|
|
227
|
+
// matters more than the per-request read.
|
|
228
|
+
const resolve = (() => {
|
|
229
|
+
const uncached = (c: hono_Context, token: string) => {
|
|
230
|
+
const kv = contextKv(c)
|
|
231
|
+
return kv ? ApiKeys.resolve(kv.store, token) : Promise.resolve(null)
|
|
232
|
+
}
|
|
233
|
+
if (options.apiKey === false || options.apiKey?.cache === false) return uncached
|
|
234
|
+
return cachedResolve(uncached, options.apiKey?.cache ?? {})
|
|
228
235
|
})()
|
|
229
236
|
const auth: Context = {
|
|
230
237
|
...(options.apiKey === false
|
|
@@ -232,7 +239,7 @@ export function middleware<
|
|
|
232
239
|
: {
|
|
233
240
|
apiKey: {
|
|
234
241
|
...(options.apiKey?.rateLimits ? { rateLimits: options.apiKey.rateLimits } : {}),
|
|
235
|
-
|
|
242
|
+
resolve,
|
|
236
243
|
},
|
|
237
244
|
}),
|
|
238
245
|
...(options.mpp === false
|
|
@@ -280,16 +287,14 @@ export declare namespace middleware {
|
|
|
280
287
|
apiKey?:
|
|
281
288
|
| {
|
|
282
289
|
/**
|
|
283
|
-
* Resolve-cache tuning
|
|
284
|
-
*
|
|
285
|
-
*
|
|
286
|
-
*
|
|
290
|
+
* Resolve-cache tuning (per-isolate, positive 60s / negative 10s by
|
|
291
|
+
* default), or `false` to resolve uncached on every request — e.g.
|
|
292
|
+
* when instant revocation matters more than the per-request backend
|
|
293
|
+
* read.
|
|
287
294
|
*/
|
|
288
|
-
cache?:
|
|
295
|
+
cache?: Cache | false | undefined
|
|
289
296
|
/** API-key quotas keyed by quota scope (reserved `'*'` is the config default). */
|
|
290
297
|
rateLimits?: Record<string, RateLimit.Limit> | undefined
|
|
291
|
-
/** Backing API-key source. Defaults to an empty source (rejects all). */
|
|
292
|
-
source?: ApiKeySource.Source | undefined
|
|
293
298
|
/** Required API-key scopes. */
|
|
294
299
|
scopes?: readonly Scope.Id[] | undefined
|
|
295
300
|
}
|
|
@@ -311,6 +316,16 @@ export declare namespace middleware {
|
|
|
311
316
|
rateLimit?: RateLimit.memory.Options | undefined
|
|
312
317
|
}
|
|
313
318
|
|
|
319
|
+
/** Resolve-cache tuning for API-key resolution. */
|
|
320
|
+
type Cache = {
|
|
321
|
+
/** Time-to-live for negative (`null`) results in milliseconds. */
|
|
322
|
+
negativeTtl?: number | undefined
|
|
323
|
+
/** Clock used for expiry. */
|
|
324
|
+
now?: (() => number) | undefined
|
|
325
|
+
/** Time-to-live for positive results in milliseconds. */
|
|
326
|
+
ttl?: number | undefined
|
|
327
|
+
}
|
|
328
|
+
|
|
314
329
|
/** MPP payment options used by auth middleware. */
|
|
315
330
|
type MppOptions = Omit<NonNullable<Parameters<typeof Mppx.create>[0]>, 'methods'> & {
|
|
316
331
|
/** Global Tempo charge options passed to the MPP method instantiator. */
|
|
@@ -612,7 +627,7 @@ export function require<environment extends Environment = Environment>(
|
|
|
612
627
|
|
|
613
628
|
const hasPayment = hasPaymentCredential(c) && isPaymentEnabled(auth, policy)
|
|
614
629
|
if (credential) {
|
|
615
|
-
const apiKey = await auth.apiKey?.
|
|
630
|
+
const apiKey = await auth.apiKey?.resolve(c, credential.token)
|
|
616
631
|
if (!apiKey)
|
|
617
632
|
return jsonError(c, {
|
|
618
633
|
code: 'api_key_invalid',
|
|
@@ -796,7 +811,12 @@ function consumeRateLimit(
|
|
|
796
811
|
options: RateLimit.Store.ConsumeOptions,
|
|
797
812
|
): Promise<RateLimit.Result | null> {
|
|
798
813
|
if (!auth.rateLimit) return Promise.resolve(null)
|
|
799
|
-
|
|
814
|
+
// Fail open: a rate-limit store error (e.g. a lost Durable Object
|
|
815
|
+
// connection) degrades to unenforced quota instead of failing the request.
|
|
816
|
+
return auth.rateLimit.consume(options).catch((error) => {
|
|
817
|
+
console.error('[auth] rate-limit consume failed; failing open', error)
|
|
818
|
+
return null
|
|
819
|
+
})
|
|
800
820
|
}
|
|
801
821
|
|
|
802
822
|
/**
|
|
@@ -1005,3 +1025,61 @@ function setRateLimitHeaders(c: hono_Context, result: RateLimit.Result) {
|
|
|
1005
1025
|
c.header('RateLimit-Remaining', String(result.remaining))
|
|
1006
1026
|
c.header('RateLimit-Reset', String(result.reset))
|
|
1007
1027
|
}
|
|
1028
|
+
|
|
1029
|
+
// Reads the app's KV state store from request context (set by `App.create`
|
|
1030
|
+
// from its `kv` option); `undefined` leaves API-key auth closed.
|
|
1031
|
+
function contextKv(c: hono_Context): { store: Store.State } | undefined {
|
|
1032
|
+
return (c.get as (key: string) => unknown)('kv') as { store: Store.State } | undefined
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
/** Maximum resolve-cache entries held per isolate before eviction. */
|
|
1036
|
+
const cachedMaxEntries = 10_000
|
|
1037
|
+
|
|
1038
|
+
/**
|
|
1039
|
+
* Wraps token resolution with a short-TTL per-isolate cache so hot keys skip
|
|
1040
|
+
* the backend read on every request. Positive results cache for `ttl`
|
|
1041
|
+
* (default 60s), negative for `negativeTtl` (default 10s) so a freshly minted
|
|
1042
|
+
* key is usable quickly; concurrent misses for one token are single-flighted.
|
|
1043
|
+
* Entries are keyed by the token's sha256 (`ApiKey.hash`) — never the
|
|
1044
|
+
* plaintext. A revoked key may stay resolvable for up to `ttl` — the accepted
|
|
1045
|
+
* trade-off for removing a backend read from the auth path.
|
|
1046
|
+
*/
|
|
1047
|
+
function cachedResolve(
|
|
1048
|
+
resolve: (c: hono_Context, token: string) => Promise<ApiKey.ApiKey | null>,
|
|
1049
|
+
options: middleware.Cache,
|
|
1050
|
+
): (c: hono_Context, token: string) => Promise<ApiKey.ApiKey | null> {
|
|
1051
|
+
const { negativeTtl = 10_000, now = Date.now, ttl = 60_000 } = options
|
|
1052
|
+
type Cell = { expiresAt: number; value: ApiKey.ApiKey | null }
|
|
1053
|
+
const cells = new Map<string, Cell>()
|
|
1054
|
+
const flights = new Map<string, Promise<ApiKey.ApiKey | null>>()
|
|
1055
|
+
|
|
1056
|
+
return async (c, token) => {
|
|
1057
|
+
// Versioned so a future record-shape change can't serve stale shapes.
|
|
1058
|
+
const key = `v1:${ApiKey.hash(token)}`
|
|
1059
|
+
const cell = cells.get(key)
|
|
1060
|
+
if (cell && cell.expiresAt > now()) return cell.value
|
|
1061
|
+
if (cell) cells.delete(key)
|
|
1062
|
+
|
|
1063
|
+
const existing = flights.get(key)
|
|
1064
|
+
if (existing) return existing
|
|
1065
|
+
|
|
1066
|
+
const flight = (async () => {
|
|
1067
|
+
const value = await resolve(c, token)
|
|
1068
|
+
// Bound the cache: random invalid tokens each add a (negative) entry,
|
|
1069
|
+
// so sweep expired cells at the cap and then drop the oldest.
|
|
1070
|
+
if (cells.size >= cachedMaxEntries) {
|
|
1071
|
+
const time = now()
|
|
1072
|
+
for (const [name, entry] of cells) if (entry.expiresAt <= time) cells.delete(name)
|
|
1073
|
+
while (cells.size >= cachedMaxEntries) cells.delete(cells.keys().next().value as string)
|
|
1074
|
+
}
|
|
1075
|
+
cells.set(key, { expiresAt: now() + (value ? ttl : negativeTtl), value })
|
|
1076
|
+
return value
|
|
1077
|
+
})()
|
|
1078
|
+
flights.set(key, flight)
|
|
1079
|
+
try {
|
|
1080
|
+
return await flight
|
|
1081
|
+
} finally {
|
|
1082
|
+
flights.delete(key)
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { RpcSchema } from 'ox'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Ox-compatible JSON-RPC schema union used to type the relay handler's incoming
|
|
5
|
+
* requests (`RpcRequest.RpcRequest<Ox>`). The relay switches on `request.method`
|
|
6
|
+
* string literals and casts params per method, so a permissive schema whose
|
|
7
|
+
* `params` is an array is sufficient — the full method-by-method schema union
|
|
8
|
+
* from the accounts SDK is not needed here.
|
|
9
|
+
*/
|
|
10
|
+
export type Ox = RpcSchema.Generic<string, readonly unknown[]>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Store } from '
|
|
1
|
+
import { Store } from 'tapimo'
|
|
2
2
|
|
|
3
3
|
describe('from', () => {
|
|
4
4
|
const ops = {
|
|
@@ -68,7 +68,89 @@ describe('increment', () => {
|
|
|
68
68
|
})
|
|
69
69
|
})
|
|
70
70
|
|
|
71
|
+
describe('update', () => {
|
|
72
|
+
test('falls back to a get + put read-modify-write without a native swap', async () => {
|
|
73
|
+
const cells = new Map<string, string>()
|
|
74
|
+
const store = Store.from({
|
|
75
|
+
async delete(key) {
|
|
76
|
+
cells.delete(key)
|
|
77
|
+
},
|
|
78
|
+
async get(key) {
|
|
79
|
+
return cells.get(key) ?? null
|
|
80
|
+
},
|
|
81
|
+
async list() {
|
|
82
|
+
return { keys: [] }
|
|
83
|
+
},
|
|
84
|
+
async put(key, value) {
|
|
85
|
+
cells.set(key, value)
|
|
86
|
+
},
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
expect(await Store.update(store, 'value', (current) => `${current ?? 'start'}+`)).toMatchInlineSnapshot(`"start+"`) // prettier-ignore
|
|
90
|
+
expect(await Store.update(store, 'value', (current) => `${current}+`)).toMatchInlineSnapshot(`"start++"`) // prettier-ignore
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
test('retries the transform when the compare-and-swap loses', async () => {
|
|
94
|
+
const store = Store.memory()
|
|
95
|
+
await store.put('value', 'a')
|
|
96
|
+
|
|
97
|
+
// Interleave a competing write between the read and the swap once.
|
|
98
|
+
let raced = false
|
|
99
|
+
const get = store.get.bind(store)
|
|
100
|
+
store.get = async (key) => {
|
|
101
|
+
const current = await get(key)
|
|
102
|
+
if (!raced) {
|
|
103
|
+
raced = true
|
|
104
|
+
await store.put('value', 'b')
|
|
105
|
+
}
|
|
106
|
+
return current
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const transforms: (null | string)[] = []
|
|
110
|
+
const written = await Store.update(store, 'value', (current) => {
|
|
111
|
+
transforms.push(current)
|
|
112
|
+
return `${current}c`
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
expect(written).toMatchInlineSnapshot(`"bc"`)
|
|
116
|
+
expect(transforms).toMatchInlineSnapshot(`
|
|
117
|
+
[
|
|
118
|
+
"a",
|
|
119
|
+
"b",
|
|
120
|
+
]
|
|
121
|
+
`)
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
test('fails after exhausting compare-and-swap attempts', async () => {
|
|
125
|
+
const store = Store.memory()
|
|
126
|
+
await store.put('value', 'a')
|
|
127
|
+
|
|
128
|
+
// Every read is immediately invalidated by a competing write.
|
|
129
|
+
const get = store.get.bind(store)
|
|
130
|
+
store.get = async (key) => {
|
|
131
|
+
const current = await get(key)
|
|
132
|
+
await store.put('value', `${current}!`)
|
|
133
|
+
return current
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
await expect(
|
|
137
|
+
Store.update(store, 'value', (current) => `${current}c`, { attempts: 2 }),
|
|
138
|
+
).rejects.toThrowErrorMatchingInlineSnapshot(
|
|
139
|
+
`[Store.UpdateContentionError: Store update on "value" lost 2 compare-and-swap attempts.]`,
|
|
140
|
+
)
|
|
141
|
+
})
|
|
142
|
+
})
|
|
143
|
+
|
|
71
144
|
describe('memory', () => {
|
|
145
|
+
test('swaps atomically against the expected value', async () => {
|
|
146
|
+
const store = Store.memory()
|
|
147
|
+
|
|
148
|
+
expect(await store.swap!('value', null, 'first')).toMatchInlineSnapshot(`true`)
|
|
149
|
+
expect(await store.swap!('value', null, 'clobber')).toMatchInlineSnapshot(`false`)
|
|
150
|
+
expect(await store.swap!('value', 'first', 'second')).toMatchInlineSnapshot(`true`)
|
|
151
|
+
expect(await store.get('value')).toMatchInlineSnapshot(`"second"`)
|
|
152
|
+
})
|
|
153
|
+
|
|
72
154
|
test('treats expired entries as absent', async () => {
|
|
73
155
|
const store = Store.memory()
|
|
74
156
|
|
|
@@ -193,6 +275,16 @@ describe('cloudflareKv', () => {
|
|
|
193
275
|
})
|
|
194
276
|
|
|
195
277
|
describe('durableObject', () => {
|
|
278
|
+
// The stub contract requires native `increment` and `swap` (our DO class
|
|
279
|
+
// implements them); derive them for the in-memory fakes.
|
|
280
|
+
function stubFor(backing: Store.State): Store.durableObject.Stub {
|
|
281
|
+
return {
|
|
282
|
+
...backing,
|
|
283
|
+
increment: (key, options) => Store.increment(backing, key, options),
|
|
284
|
+
swap: (key, expected, next, options) => backing.swap!(key, expected, next, options),
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
196
288
|
test('adapts a Durable Object namespace without caching stubs', async () => {
|
|
197
289
|
const calls: string[] = []
|
|
198
290
|
const backing = Store.memory({
|
|
@@ -201,12 +293,7 @@ describe('durableObject', () => {
|
|
|
201
293
|
const namespace = {
|
|
202
294
|
getByName(name) {
|
|
203
295
|
calls.push(name)
|
|
204
|
-
|
|
205
|
-
// implements it); derive one for the in-memory fake.
|
|
206
|
-
return {
|
|
207
|
-
...backing,
|
|
208
|
-
increment: (key, options) => Store.increment(backing, key, options),
|
|
209
|
-
}
|
|
296
|
+
return stubFor(backing)
|
|
210
297
|
},
|
|
211
298
|
} satisfies Store.durableObject.Namespace
|
|
212
299
|
const store = Store.durableObject(namespace, { name: 'tempo-api' })
|
|
@@ -250,6 +337,7 @@ describe('durableObject', () => {
|
|
|
250
337
|
increment: (key, options) => Store.increment(backing, key, options),
|
|
251
338
|
list: (options) => backing.list(options),
|
|
252
339
|
put: (key, value, options) => backing.put(key, value, options),
|
|
340
|
+
swap: (key, expected, next, options) => backing.swap!(key, expected, next, options),
|
|
253
341
|
} satisfies Store.durableObject.Stub
|
|
254
342
|
return new Proxy(stub, {
|
|
255
343
|
get(target, method: string) {
|
|
@@ -286,6 +374,90 @@ describe('durableObject', () => {
|
|
|
286
374
|
)
|
|
287
375
|
})
|
|
288
376
|
|
|
377
|
+
test('retries a retryable stub error once on a fresh stub', async () => {
|
|
378
|
+
let stubs = 0
|
|
379
|
+
let failures = 1
|
|
380
|
+
const backing = Store.memory()
|
|
381
|
+
const namespace = {
|
|
382
|
+
getByName() {
|
|
383
|
+
stubs += 1
|
|
384
|
+
return {
|
|
385
|
+
...stubFor(backing),
|
|
386
|
+
increment: (key, options) => {
|
|
387
|
+
if (failures > 0) {
|
|
388
|
+
failures -= 1
|
|
389
|
+
throw Object.assign(new Error('Network connection lost.'), { retryable: true })
|
|
390
|
+
}
|
|
391
|
+
return Store.increment(backing, key, options)
|
|
392
|
+
},
|
|
393
|
+
}
|
|
394
|
+
},
|
|
395
|
+
} satisfies Store.durableObject.Namespace
|
|
396
|
+
const store = Store.durableObject(namespace, { name: 'tempo-api' })
|
|
397
|
+
|
|
398
|
+
expect(await Store.increment(store, 'counter')).toMatchInlineSnapshot(`1`)
|
|
399
|
+
// One stub per attempt: a stub that threw is disconnected.
|
|
400
|
+
expect(stubs).toMatchInlineSnapshot(`2`)
|
|
401
|
+
})
|
|
402
|
+
|
|
403
|
+
test('propagates a retryable stub error when the retry also fails', async () => {
|
|
404
|
+
let attempts = 0
|
|
405
|
+
const namespace = {
|
|
406
|
+
getByName() {
|
|
407
|
+
return {
|
|
408
|
+
...stubFor(Store.memory()),
|
|
409
|
+
increment: () => {
|
|
410
|
+
attempts += 1
|
|
411
|
+
throw Object.assign(new Error('Network connection lost.'), { retryable: true })
|
|
412
|
+
},
|
|
413
|
+
}
|
|
414
|
+
},
|
|
415
|
+
} satisfies Store.durableObject.Namespace
|
|
416
|
+
const store = Store.durableObject(namespace, { name: 'tempo-api' })
|
|
417
|
+
|
|
418
|
+
await expect(Store.increment(store, 'counter')).rejects.toThrowErrorMatchingInlineSnapshot(
|
|
419
|
+
`[Error: Network connection lost.]`,
|
|
420
|
+
)
|
|
421
|
+
expect(attempts).toMatchInlineSnapshot(`2`)
|
|
422
|
+
})
|
|
423
|
+
|
|
424
|
+
test('does not retry non-retryable or overloaded stub errors', async () => {
|
|
425
|
+
let attempts = 0
|
|
426
|
+
const namespaceFor = (error: Error) =>
|
|
427
|
+
({
|
|
428
|
+
getByName() {
|
|
429
|
+
return {
|
|
430
|
+
...stubFor(Store.memory()),
|
|
431
|
+
increment: () => {
|
|
432
|
+
attempts += 1
|
|
433
|
+
throw error
|
|
434
|
+
},
|
|
435
|
+
}
|
|
436
|
+
},
|
|
437
|
+
}) satisfies Store.durableObject.Namespace
|
|
438
|
+
|
|
439
|
+
const plain = Store.durableObject(namespaceFor(new Error('boom')), { name: 'tempo-api' })
|
|
440
|
+
await expect(Store.increment(plain, 'counter')).rejects.toThrowErrorMatchingInlineSnapshot(
|
|
441
|
+
`[Error: boom]`,
|
|
442
|
+
)
|
|
443
|
+
expect(attempts).toMatchInlineSnapshot(`1`)
|
|
444
|
+
|
|
445
|
+
attempts = 0
|
|
446
|
+
const overloaded = Store.durableObject(
|
|
447
|
+
namespaceFor(
|
|
448
|
+
Object.assign(new Error('Durable Object is overloaded.'), {
|
|
449
|
+
overloaded: true,
|
|
450
|
+
retryable: true,
|
|
451
|
+
}),
|
|
452
|
+
),
|
|
453
|
+
{ name: 'tempo-api' },
|
|
454
|
+
)
|
|
455
|
+
await expect(Store.increment(overloaded, 'counter')).rejects.toThrowErrorMatchingInlineSnapshot(
|
|
456
|
+
`[Error: Durable Object is overloaded.]`,
|
|
457
|
+
)
|
|
458
|
+
expect(attempts).toMatchInlineSnapshot(`1`)
|
|
459
|
+
})
|
|
460
|
+
|
|
289
461
|
test('defers missing namespace failures until an operation is attempted', async () => {
|
|
290
462
|
const store = Store.durableObject(undefined as unknown as Store.durableObject.Namespace, {
|
|
291
463
|
name: 'tempo-api',
|