tapimo 0.0.2 → 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 +11 -2
- 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 +98 -209
- 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 +1 -1
- package/dist/apps/balances.d.ts +2 -2
- package/dist/apps/receipts.d.ts +43 -43
- 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 +72 -12
- 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 +65 -3
- 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/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 +7 -2
- 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 +1 -1
- package/src/apps/balances.test.ts +4 -3
- package/src/apps/blocks.test.ts +1 -1
- package/src/apps/exchanges.test.ts +1 -1
- package/src/apps/indexer.test.ts +1 -1
- 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 +130 -143
- 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 +21 -21
- package/src/internal/Auth.ts +92 -19
- package/src/internal/RateLimit.test.ts +1 -1
- package/src/internal/RpcSchema.ts +10 -0
- package/src/internal/Store.test.ts +98 -10
- package/src/internal/Store.ts +112 -6
- package/src/internal/VerifiedTokens.test.ts +99 -115
- package/src/internal/VerifiedTokens.ts +123 -175
- 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
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as ApiKey from '../../ApiKey.js'
|
|
2
|
+
import * as core_ApiKeys from '../../ApiKeys.js'
|
|
2
3
|
|
|
3
4
|
import * as TestAdmin from '../../../test/Admin.js'
|
|
4
5
|
import * as TestApp from '../../../test/App.js'
|
|
@@ -9,7 +10,7 @@ const validBody = { orgId: 'org_test', scopes: ['data:read'] } as const
|
|
|
9
10
|
|
|
10
11
|
describe('POST /api-keys', () => {
|
|
11
12
|
test('mints a key, returns the token once, persists a hashed record', async () => {
|
|
12
|
-
const { client,
|
|
13
|
+
const { client, store } = TestAdmin.setup()
|
|
13
14
|
const response = await client['api-keys'].$post({ json: { ...validBody, name: 'CI' } })
|
|
14
15
|
const body = await TestApp.json(response, ApiKeys.schema.createApiKey.Response)
|
|
15
16
|
|
|
@@ -34,26 +35,26 @@ describe('POST /api-keys', () => {
|
|
|
34
35
|
}
|
|
35
36
|
`)
|
|
36
37
|
|
|
37
|
-
// The token resolves through the
|
|
38
|
+
// The token resolves through the module (proving the record persisted),
|
|
38
39
|
// and at rest it is keyed by the token hash — never the plaintext token.
|
|
39
|
-
const resolved = await
|
|
40
|
+
const resolved = await core_ApiKeys.resolve(store, body.token)
|
|
40
41
|
expect(resolved?.orgId).toBe('org_test')
|
|
41
42
|
expect(await store.get(body.token)).toBeNull()
|
|
42
43
|
expect(await store.get(ApiKey.keyFor(body.token))).not.toBeNull()
|
|
43
44
|
})
|
|
44
45
|
|
|
45
46
|
test('defaults to the production environment', async () => {
|
|
46
|
-
const { client,
|
|
47
|
+
const { client, store } = TestAdmin.setup()
|
|
47
48
|
const response = await client['api-keys'].$post({ json: validBody })
|
|
48
49
|
const body = await TestApp.json(response, ApiKeys.schema.createApiKey.Response)
|
|
49
50
|
|
|
50
51
|
expect(response.status).toBe(200)
|
|
51
52
|
expect(body.environment).toBe('production')
|
|
52
|
-
expect((await
|
|
53
|
+
expect((await core_ApiKeys.list(store))[0]?.environment).toBe('production')
|
|
53
54
|
})
|
|
54
55
|
|
|
55
56
|
test('persists a sandbox environment when provided', async () => {
|
|
56
|
-
const { client,
|
|
57
|
+
const { client, store } = TestAdmin.setup()
|
|
57
58
|
const response = await client['api-keys'].$post({
|
|
58
59
|
json: { ...validBody, environment: 'sandbox' },
|
|
59
60
|
})
|
|
@@ -63,19 +64,19 @@ describe('POST /api-keys', () => {
|
|
|
63
64
|
expect(body.environment).toBe('sandbox')
|
|
64
65
|
// Sandbox tokens carry a distinct prefix for recognizability/leak detection.
|
|
65
66
|
expect(body.token).toMatch(/^tempo_sandbox:sk:[a-f0-9]{48}$/)
|
|
66
|
-
const resolved = await
|
|
67
|
+
const resolved = await core_ApiKeys.resolve(store, body.token)
|
|
67
68
|
expect(resolved?.environment).toBe('sandbox')
|
|
68
69
|
})
|
|
69
70
|
|
|
70
71
|
test('records the expiry when provided', async () => {
|
|
71
|
-
const { client,
|
|
72
|
+
const { client, store } = TestAdmin.setup()
|
|
72
73
|
const expiresAt = '2100-01-01T00:00:00.000Z'
|
|
73
74
|
const response = await client['api-keys'].$post({ json: { ...validBody, expiresAt } })
|
|
74
75
|
const body = await TestApp.json(response, ApiKeys.schema.createApiKey.Response)
|
|
75
76
|
|
|
76
77
|
expect(response.status).toBe(200)
|
|
77
78
|
expect(body.expiresAt).toBe(expiresAt)
|
|
78
|
-
expect((await
|
|
79
|
+
expect((await core_ApiKeys.list(store))[0]?.expiresAt).toBe(expiresAt)
|
|
79
80
|
})
|
|
80
81
|
|
|
81
82
|
test('rejects an unknown scope with 400', async () => {
|
|
@@ -106,8 +107,8 @@ describe('POST /api-keys', () => {
|
|
|
106
107
|
|
|
107
108
|
describe('GET /api-keys', () => {
|
|
108
109
|
test('lists records as metadata only (never the token)', async () => {
|
|
109
|
-
const { client,
|
|
110
|
-
const { token } = await
|
|
110
|
+
const { client, store } = TestAdmin.setup()
|
|
111
|
+
const { token } = await core_ApiKeys.mint(store, validBody)
|
|
111
112
|
|
|
112
113
|
const response = await client['api-keys'].$get({ query: {} })
|
|
113
114
|
const body = await TestApp.json(response, ApiKeys.schema.listApiKeys.Response)
|
|
@@ -120,9 +121,9 @@ describe('GET /api-keys', () => {
|
|
|
120
121
|
})
|
|
121
122
|
|
|
122
123
|
test('filters by orgId', async () => {
|
|
123
|
-
const { client,
|
|
124
|
-
await
|
|
125
|
-
await
|
|
124
|
+
const { client, store } = TestAdmin.setup()
|
|
125
|
+
await core_ApiKeys.mint(store, { orgId: 'org_a', scopes: ['data:read'] })
|
|
126
|
+
await core_ApiKeys.mint(store, { orgId: 'org_b', scopes: ['data:read'] })
|
|
126
127
|
|
|
127
128
|
const response = await client['api-keys'].$get({ query: { orgId: 'org_a' } })
|
|
128
129
|
const body = await TestApp.json(response, ApiKeys.schema.listApiKeys.Response)
|
|
@@ -133,16 +134,16 @@ describe('GET /api-keys', () => {
|
|
|
133
134
|
|
|
134
135
|
describe('DELETE /api-keys/:id', () => {
|
|
135
136
|
test('revokes a key, after which it no longer resolves or lists', async () => {
|
|
136
|
-
const { client,
|
|
137
|
-
const { record, token } = await
|
|
137
|
+
const { client, store } = TestAdmin.setup()
|
|
138
|
+
const { record, token } = await core_ApiKeys.mint(store, validBody)
|
|
138
139
|
|
|
139
140
|
const response = await client['api-keys'][':id'].$delete({ param: { id: record.id } })
|
|
140
141
|
const body = await TestApp.json(response, ApiKeys.schema.revokeApiKey.Response)
|
|
141
142
|
|
|
142
143
|
expect(response.status).toBe(200)
|
|
143
144
|
expect(body).toEqual({ id: record.id })
|
|
144
|
-
expect(await
|
|
145
|
-
expect(await
|
|
145
|
+
expect(await core_ApiKeys.resolve(store, token)).toBeNull()
|
|
146
|
+
expect(await core_ApiKeys.list(store)).toEqual([])
|
|
146
147
|
})
|
|
147
148
|
|
|
148
149
|
test('unknown id → 404', async () => {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Hono } from 'hono'
|
|
2
2
|
import * as z from 'zod/mini'
|
|
3
3
|
|
|
4
|
-
import * as
|
|
4
|
+
import * as ApiKeys from '../../ApiKeys.js'
|
|
5
5
|
import * as OpenApi from '../../internal/OpenApi.js'
|
|
6
6
|
import * as Response from '../../internal/Response.js'
|
|
7
7
|
import * as Schema from '../../internal/Schema.js'
|
|
@@ -16,13 +16,13 @@ export namespace schema {
|
|
|
16
16
|
* server sets from the verified identity so it can't be spoofed).
|
|
17
17
|
*/
|
|
18
18
|
export const Body = Schema.describe(
|
|
19
|
-
z.omit(
|
|
19
|
+
z.omit(ApiKeys.schema.MintInput, { createdBy: true }),
|
|
20
20
|
'Fields accepted when minting an API key.',
|
|
21
21
|
)
|
|
22
22
|
|
|
23
23
|
/** Response: the record plus the one-time plaintext token. */
|
|
24
24
|
export const Response = Schema.describe(
|
|
25
|
-
z.extend(
|
|
25
|
+
z.extend(ApiKeys.schema.Record, {
|
|
26
26
|
token: z
|
|
27
27
|
.string()
|
|
28
28
|
.check(z.describe('The plaintext token, shown once and unrecoverable afterward.')),
|
|
@@ -46,7 +46,7 @@ export namespace schema {
|
|
|
46
46
|
export const Response = Schema.describe(
|
|
47
47
|
z.object({
|
|
48
48
|
data: z
|
|
49
|
-
.array(
|
|
49
|
+
.array(ApiKeys.schema.Record)
|
|
50
50
|
.check(z.describe('API-key records, newest first (metadata only — never tokens).')),
|
|
51
51
|
}),
|
|
52
52
|
'A list of API-key records.',
|
|
@@ -70,8 +70,7 @@ export namespace schema {
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
/**
|
|
73
|
-
* Provisioning routes for API keys, backed by the
|
|
74
|
-
* {@link App.Environment} `source`:
|
|
73
|
+
* Provisioning routes for API keys, backed by the app's KV state store:
|
|
75
74
|
*
|
|
76
75
|
* - `POST /` mints a key, recording `createdBy` from the verified identity and
|
|
77
76
|
* returning the one-time token.
|
|
@@ -100,7 +99,7 @@ export function apiKeys() {
|
|
|
100
99
|
async (c) => {
|
|
101
100
|
// `createdBy` is the verified admin email, never client-supplied.
|
|
102
101
|
// `requireAuth` guarantees identity on this gated route.
|
|
103
|
-
const { record, token } = await c.get('
|
|
102
|
+
const { record, token } = await ApiKeys.mint(c.get('kv').store, {
|
|
104
103
|
...c.req.valid('json'),
|
|
105
104
|
createdBy: c.get('identity')!.email,
|
|
106
105
|
})
|
|
@@ -123,7 +122,7 @@ export function apiKeys() {
|
|
|
123
122
|
}),
|
|
124
123
|
async (c) => {
|
|
125
124
|
const { orgId } = c.req.valid('query')
|
|
126
|
-
const data = await c.get('
|
|
125
|
+
const data = await ApiKeys.list(c.get('kv').store, orgId ? { orgId } : {})
|
|
127
126
|
return c.json(Response.validated(schema.listApiKeys.Response, { data }))
|
|
128
127
|
},
|
|
129
128
|
)
|
|
@@ -144,7 +143,7 @@ export function apiKeys() {
|
|
|
144
143
|
}),
|
|
145
144
|
async (c) => {
|
|
146
145
|
const { id } = c.req.valid('param')
|
|
147
|
-
const revoked = await c.get('
|
|
146
|
+
const revoked = await ApiKeys.revoke(c.get('kv').store, id)
|
|
148
147
|
if (!revoked)
|
|
149
148
|
return Response.error(c, {
|
|
150
149
|
code: 'not_found',
|
|
@@ -22,6 +22,10 @@ describe('GET /scopes', () => {
|
|
|
22
22
|
"description": "Run ad-hoc queries against the raw indexer passthrough.",
|
|
23
23
|
"scope": "indexer:query",
|
|
24
24
|
},
|
|
25
|
+
{
|
|
26
|
+
"description": "Fill and submit transactions through the relay.",
|
|
27
|
+
"scope": "rpc-relay",
|
|
28
|
+
},
|
|
25
29
|
{
|
|
26
30
|
"description": "Read webhook subscriptions.",
|
|
27
31
|
"scope": "webhooks:read",
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { Store } from 'cadent'
|
|
2
|
-
|
|
3
1
|
import * as TestAdmin from '../../../test/Admin.js'
|
|
4
2
|
import * as TestApp from '../../../test/App.js'
|
|
5
3
|
import * as Verified from './verified-tokens.js'
|
|
@@ -15,29 +13,14 @@ const novel = {
|
|
|
15
13
|
|
|
16
14
|
const chainId = '42431' as const
|
|
17
15
|
|
|
18
|
-
/** Write-enabled admin client backed by a fresh
|
|
16
|
+
/** Write-enabled admin client backed by a fresh schema-isolated database. */
|
|
19
17
|
function writes() {
|
|
20
|
-
return TestAdmin.setup(
|
|
18
|
+
return TestAdmin.setup()
|
|
21
19
|
}
|
|
22
20
|
|
|
23
|
-
describe('feature gate', () => {
|
|
24
|
-
test('write routes return 404 when no store is configured', async () => {
|
|
25
|
-
const { client } = TestAdmin.setup()
|
|
26
|
-
const response = await client['verified-tokens'].$post({
|
|
27
|
-
json: { ...novel, chainId: 42431 },
|
|
28
|
-
})
|
|
29
|
-
expect(response.status).toBe(404)
|
|
30
|
-
const body = (await response.json()) as { error?: { code?: string } }
|
|
31
|
-
expect(body.error?.code).toBe('verified_tokens_not_enabled')
|
|
32
|
-
})
|
|
33
|
-
})
|
|
34
|
-
|
|
35
21
|
describe('authentication', () => {
|
|
36
22
|
test('denied identity → 401', async () => {
|
|
37
|
-
const { client } = TestAdmin.setup({
|
|
38
|
-
identify: () => null,
|
|
39
|
-
verifiedTokens: { store: Store.memory() },
|
|
40
|
-
})
|
|
23
|
+
const { client } = TestAdmin.setup({ identify: () => null })
|
|
41
24
|
const response = await client['verified-tokens'].$post({
|
|
42
25
|
json: { ...novel, chainId: 42431 },
|
|
43
26
|
})
|
|
@@ -46,12 +29,6 @@ describe('authentication', () => {
|
|
|
46
29
|
})
|
|
47
30
|
|
|
48
31
|
describe('list', () => {
|
|
49
|
-
test('returns 404 when no store is configured', async () => {
|
|
50
|
-
const { client } = TestAdmin.setup()
|
|
51
|
-
const response = await client['verified-tokens'].$get({ query: { chainId } })
|
|
52
|
-
expect(response.status).toBe(404)
|
|
53
|
-
})
|
|
54
|
-
|
|
55
32
|
test('empty list → data [] and null version', async () => {
|
|
56
33
|
const { client } = writes()
|
|
57
34
|
const response = await client['verified-tokens'].$get({ query: { chainId } })
|
|
@@ -176,7 +153,6 @@ describe('create with inline logo', () => {
|
|
|
176
153
|
get: async (key) => stored.find((entry) => entry.key === key)?.content,
|
|
177
154
|
put: async (key, content) => void stored.push({ content, key }),
|
|
178
155
|
},
|
|
179
|
-
verifiedTokens: { store: Store.memory() },
|
|
180
156
|
})
|
|
181
157
|
return { client, stored }
|
|
182
158
|
}
|
|
@@ -219,7 +195,6 @@ describe('create with inline logo', () => {
|
|
|
219
195
|
throw new Error('upload failed')
|
|
220
196
|
},
|
|
221
197
|
},
|
|
222
|
-
verifiedTokens: { store: Store.memory() },
|
|
223
198
|
})
|
|
224
199
|
const response = await client['verified-tokens'].$post({
|
|
225
200
|
json: { ...novel, chainId: 42431, logo: svg },
|
|
@@ -297,7 +272,6 @@ describe('PUT /verified-tokens/:address/logo', () => {
|
|
|
297
272
|
stored.push({ content, key })
|
|
298
273
|
},
|
|
299
274
|
},
|
|
300
|
-
verifiedTokens: { store: Store.memory() },
|
|
301
275
|
})
|
|
302
276
|
return { client, stored }
|
|
303
277
|
}
|
|
@@ -375,7 +349,6 @@ describe('GET /verified-tokens/:address/logo', () => {
|
|
|
375
349
|
get: async (key) => objects.get(key),
|
|
376
350
|
put: async (key, content) => void objects.set(key, content),
|
|
377
351
|
},
|
|
378
|
-
verifiedTokens: { store: Store.memory() },
|
|
379
352
|
})
|
|
380
353
|
return { client, objects }
|
|
381
354
|
}
|
|
@@ -4,7 +4,7 @@ import * as z from 'zod/mini'
|
|
|
4
4
|
import * as OpenApi from '../../internal/OpenApi.js'
|
|
5
5
|
import * as Response from '../../internal/Response.js'
|
|
6
6
|
import * as Schema from '../../internal/Schema.js'
|
|
7
|
-
import
|
|
7
|
+
import * as Db from '../../db/Db.js'
|
|
8
8
|
import * as VerifiedTokens from '../../internal/VerifiedTokens.js'
|
|
9
9
|
import { schema as readSchema } from '../../apps/verified-tokens.js'
|
|
10
10
|
import type * as App from '../App.js'
|
|
@@ -149,9 +149,9 @@ export namespace schema {
|
|
|
149
149
|
}
|
|
150
150
|
|
|
151
151
|
/**
|
|
152
|
-
* Management routes for the curated verified-token list, backed by the
|
|
153
|
-
*
|
|
154
|
-
*
|
|
152
|
+
* Management routes for the curated verified-token list, backed by the app
|
|
153
|
+
* database (snapshot rows). Mutations accept an optional `If-Match`
|
|
154
|
+
* precondition (`412` on mismatch).
|
|
155
155
|
*
|
|
156
156
|
* - `POST /` creates a token (`409` on a duplicate address or symbol).
|
|
157
157
|
* - `PUT /` replaces a chain's entire list (seed/rollback).
|
|
@@ -170,7 +170,6 @@ export function verifiedTokens() {
|
|
|
170
170
|
OpenApi.describeRoute({
|
|
171
171
|
operationId: 'listVerifiedTokens',
|
|
172
172
|
responses: OpenApi.responses({
|
|
173
|
-
errors: { 404: featureDisabledDescription },
|
|
174
173
|
success: {
|
|
175
174
|
description: 'Curated verified tokens.',
|
|
176
175
|
schema: schema.listVerifiedTokens.Response,
|
|
@@ -180,10 +179,9 @@ export function verifiedTokens() {
|
|
|
180
179
|
tags: ['Verified Tokens'],
|
|
181
180
|
}),
|
|
182
181
|
async (c) => {
|
|
183
|
-
const
|
|
184
|
-
if (!store) return notEnabled(c)
|
|
182
|
+
const db = Db.get(c.get('db'))
|
|
185
183
|
const { chainId } = c.req.valid('query')
|
|
186
|
-
const snapshot = await VerifiedTokens.read(
|
|
184
|
+
const snapshot = await VerifiedTokens.read(db, chainId)
|
|
187
185
|
return c.json(
|
|
188
186
|
Response.validated(schema.listVerifiedTokens.Response, {
|
|
189
187
|
data: snapshot?.list ?? [],
|
|
@@ -202,7 +200,6 @@ export function verifiedTokens() {
|
|
|
202
200
|
operationId: 'createVerifiedToken',
|
|
203
201
|
responses: OpenApi.responses({
|
|
204
202
|
errors: {
|
|
205
|
-
404: featureDisabledDescription,
|
|
206
203
|
409: 'Duplicate address or symbol.',
|
|
207
204
|
413: 'Logo exceeds the size limit.',
|
|
208
205
|
415: 'Logo is not valid SVG.',
|
|
@@ -216,22 +213,21 @@ export function verifiedTokens() {
|
|
|
216
213
|
tags: ['Verified Tokens'],
|
|
217
214
|
}),
|
|
218
215
|
async (c) => {
|
|
219
|
-
const
|
|
220
|
-
if (!store) return notEnabled(c)
|
|
216
|
+
const db = Db.get(c.get('db'))
|
|
221
217
|
const { logo, ...body } = c.req.valid('json')
|
|
222
218
|
try {
|
|
223
219
|
if (logo !== undefined) {
|
|
224
220
|
const assets = c.get('assets')
|
|
225
221
|
if (!assets?.put) return assetsNotEnabled(c)
|
|
226
222
|
const svg = encodeLogo(logo)
|
|
227
|
-
const duplicate = await findDuplicate(
|
|
223
|
+
const duplicate = await findDuplicate(db, body)
|
|
228
224
|
if (duplicate) throw duplicate
|
|
229
225
|
await assets.put(logoKey(body.chainId, body.address), {
|
|
230
226
|
body: svg,
|
|
231
227
|
contentType: 'image/svg+xml',
|
|
232
228
|
})
|
|
233
229
|
}
|
|
234
|
-
const { snapshot, token } = await VerifiedTokens.create(
|
|
230
|
+
const { snapshot, token } = await VerifiedTokens.create(db, body)
|
|
235
231
|
return c.json(
|
|
236
232
|
Response.validated(schema.createVerifiedToken.Response, {
|
|
237
233
|
data: token,
|
|
@@ -257,7 +253,6 @@ export function verifiedTokens() {
|
|
|
257
253
|
operationId: 'replaceVerifiedTokens',
|
|
258
254
|
responses: OpenApi.responses({
|
|
259
255
|
errors: {
|
|
260
|
-
404: featureDisabledDescription,
|
|
261
256
|
409: 'Duplicate address or symbol.',
|
|
262
257
|
412: 'Version precondition failed.',
|
|
263
258
|
},
|
|
@@ -270,12 +265,11 @@ export function verifiedTokens() {
|
|
|
270
265
|
tags: ['Verified Tokens'],
|
|
271
266
|
}),
|
|
272
267
|
async (c) => {
|
|
273
|
-
const
|
|
274
|
-
if (!store) return notEnabled(c)
|
|
268
|
+
const db = Db.get(c.get('db'))
|
|
275
269
|
const { chainId } = c.req.valid('query')
|
|
276
270
|
const { tokens } = c.req.valid('json')
|
|
277
271
|
try {
|
|
278
|
-
const { snapshot } = await VerifiedTokens.replace(
|
|
272
|
+
const { snapshot } = await VerifiedTokens.replace(db, chainId, tokens, {
|
|
279
273
|
...ifMatch(c),
|
|
280
274
|
})
|
|
281
275
|
return c.json(
|
|
@@ -320,13 +314,12 @@ export function verifiedTokens() {
|
|
|
320
314
|
tags: ['Verified Tokens'],
|
|
321
315
|
}),
|
|
322
316
|
async (c) => {
|
|
323
|
-
const
|
|
324
|
-
if (!store) return notEnabled(c)
|
|
317
|
+
const db = Db.get(c.get('db'))
|
|
325
318
|
const { address } = c.req.valid('param')
|
|
326
319
|
const { chainId } = c.req.valid('query')
|
|
327
320
|
const body = c.req.valid('json')
|
|
328
321
|
try {
|
|
329
|
-
const { snapshot, token } = await VerifiedTokens.patch(
|
|
322
|
+
const { snapshot, token } = await VerifiedTokens.patch(db, chainId, address, body, {
|
|
330
323
|
...ifMatch(c),
|
|
331
324
|
})
|
|
332
325
|
return c.json(
|
|
@@ -366,12 +359,11 @@ export function verifiedTokens() {
|
|
|
366
359
|
tags: ['Verified Tokens'],
|
|
367
360
|
}),
|
|
368
361
|
async (c) => {
|
|
369
|
-
const
|
|
370
|
-
if (!store) return notEnabled(c)
|
|
362
|
+
const db = Db.get(c.get('db'))
|
|
371
363
|
const { address } = c.req.valid('param')
|
|
372
364
|
const { chainId } = c.req.valid('query')
|
|
373
365
|
try {
|
|
374
|
-
const { snapshot } = await VerifiedTokens.remove(
|
|
366
|
+
const { snapshot } = await VerifiedTokens.remove(db, chainId, address, {
|
|
375
367
|
...ifMatch(c),
|
|
376
368
|
})
|
|
377
369
|
return c.json(
|
|
@@ -476,9 +468,6 @@ export function verifiedTokens() {
|
|
|
476
468
|
/** Largest accepted logo upload (SVG icons are tiny; this is generous). */
|
|
477
469
|
const maxLogoBytes = 256 * 1024
|
|
478
470
|
|
|
479
|
-
/** OpenAPI description shared by write routes' 404 when the feature is disabled. */
|
|
480
|
-
const featureDisabledDescription = 'Verified-token management is not enabled (or not found).'
|
|
481
|
-
|
|
482
471
|
/** Asset-store key the main API's loader reads for a token's curated icon. */
|
|
483
472
|
function logoKey(chainId: number, address: string): string {
|
|
484
473
|
return `${chainId}/icons/${address.toLowerCase()}`
|
|
@@ -511,10 +500,10 @@ function encodeLogo(logo: string): ArrayBuffer {
|
|
|
511
500
|
// Returns the duplicate-address/symbol error a create would throw, or undefined,
|
|
512
501
|
// so the logo upload can be skipped before it overwrites an existing icon.
|
|
513
502
|
async function findDuplicate(
|
|
514
|
-
|
|
503
|
+
db: Db.Db,
|
|
515
504
|
token: { address: string; chainId: VerifiedTokens.ChainId; symbol: string },
|
|
516
505
|
): Promise<Error | undefined> {
|
|
517
|
-
const snapshot = await VerifiedTokens.read(
|
|
506
|
+
const snapshot = await VerifiedTokens.read(db, token.chainId)
|
|
518
507
|
if (!snapshot) return undefined
|
|
519
508
|
if (snapshot.byAddress.has(token.address.toLowerCase()))
|
|
520
509
|
return new VerifiedTokens.DuplicateAddressError({ address: token.address, chainId: token.chainId }) // prettier-ignore
|
|
@@ -556,11 +545,3 @@ function mutationError(c: Context<App.Environment>, cause: unknown) {
|
|
|
556
545
|
return Response.error(c, { code: 'version_mismatch', message: cause.message, status: 412 })
|
|
557
546
|
return Response.upstream(c, cause)
|
|
558
547
|
}
|
|
559
|
-
|
|
560
|
-
function notEnabled(c: Context<App.Environment>) {
|
|
561
|
-
return Response.error(c, {
|
|
562
|
-
code: 'verified_tokens_not_enabled',
|
|
563
|
-
message: 'Verified-token management is not enabled for this deployment.',
|
|
564
|
-
status: 404,
|
|
565
|
-
})
|
|
566
|
-
}
|
|
@@ -11,7 +11,7 @@ export type Asset = {
|
|
|
11
11
|
|
|
12
12
|
/** The prerendered SPA shell HTML (assets normalized to relative URLs). */
|
|
13
13
|
export const shell =
|
|
14
|
-
'<!DOCTYPE html><html lang="en" data-theme="dark" data-regen-radius="none"><head><meta charSet="utf-8"/><meta content="width=device-width, initial-scale=1" name="viewport"/><link rel="stylesheet" href="./assets/styles-Cd7Fp5b_.css" data-precedence="default"/><title>Tempo API Admin</title><link rel="modulepreload" href="./assets/index-CPTihwDe.js"/><link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20269%20269%22%3E%3Crect%20width%3D%22269%22%20height%3D%22269%22%20fill%3D%22%23000%22%2F%3E%3Cpath%20d%3D%22M123.273%20190.794H93.445L121.09%20105.318H85.7334L93.445%2080.2642H191.95L184.238%20105.318H150.773L123.273%20190.794Z%22%20fill%3D%22%23fff%22%2F%3E%3C%2Fsvg%3E"/></head><body><div id="root"></div><script class="$tsr" id="$tsr-stream-barrier">(self.$R=self.$R||{})["tsr"]=[];self.$_TSR={h(){this.hydrated=!0,this.c()},e(){this.streamEnded=!0,this.c()},c(){this.hydrated&&this.streamEnded&&(delete self.$_TSR,delete self.$R.tsr)},p(e){this.initialized?e():this.buffer.push(e)},buffer:[]};$_TSR.router=($R=>$R[0]={manifest:$R[1]={routes:$R[2]={__root__:$R[3]={preloads:$R[4]=["./assets/index-CPTihwDe.js"],scripts:$R[5]=[$R[6]={attrs:$R[7]={type:"module",async:!0,src:"./assets/index-CPTihwDe.js"}}]}}},matches:$R[8]=[$R[9]={i:"__root__\u0000",u:
|
|
14
|
+
'<!DOCTYPE html><html lang="en" data-theme="dark" data-regen-radius="none"><head><meta charSet="utf-8"/><meta content="width=device-width, initial-scale=1" name="viewport"/><link rel="stylesheet" href="./assets/styles-Cd7Fp5b_.css" data-precedence="default"/><title>Tempo API Admin</title><link rel="modulepreload" href="./assets/index-CPTihwDe.js"/><link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20269%20269%22%3E%3Crect%20width%3D%22269%22%20height%3D%22269%22%20fill%3D%22%23000%22%2F%3E%3Cpath%20d%3D%22M123.273%20190.794H93.445L121.09%20105.318H85.7334L93.445%2080.2642H191.95L184.238%20105.318H150.773L123.273%20190.794Z%22%20fill%3D%22%23fff%22%2F%3E%3C%2Fsvg%3E"/></head><body><div id="root"></div><script class="$tsr" id="$tsr-stream-barrier">(self.$R=self.$R||{})["tsr"]=[];self.$_TSR={h(){this.hydrated=!0,this.c()},e(){this.streamEnded=!0,this.c()},c(){this.hydrated&&this.streamEnded&&(delete self.$_TSR,delete self.$R.tsr)},p(e){this.initialized?e():this.buffer.push(e)},buffer:[]};$_TSR.router=($R=>$R[0]={manifest:$R[1]={routes:$R[2]={__root__:$R[3]={preloads:$R[4]=["./assets/index-CPTihwDe.js"],scripts:$R[5]=[$R[6]={attrs:$R[7]={type:"module",async:!0,src:"./assets/index-CPTihwDe.js"}}]}}},matches:$R[8]=[$R[9]={i:"__root__\u0000",u:1783028662119,s:"success",ssr:!0}],lastMatchId:"__root__\u0000"})($R["tsr"]);$_TSR.e();document.currentScript.remove()</script><script type="module" async="" src="./assets/index-CPTihwDe.js"></script></body></html>'
|
|
15
15
|
|
|
16
16
|
/** Content-hashed UI assets, keyed by their path under the mount (e.g. `assets/index-*.js`). */
|
|
17
17
|
export const assets: Record<string, Asset> = {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Schema } from '
|
|
1
|
+
import { Schema } from 'tapimo'
|
|
2
2
|
import { Value as core_Value } from 'ox'
|
|
3
3
|
|
|
4
4
|
import * as TestApp from '../../test/App.js'
|
|
@@ -12,9 +12,10 @@ import * as Tokens from './tokens.js'
|
|
|
12
12
|
// verified tokens the bundle used to provide; a test can still override
|
|
13
13
|
// `verifiedTokens`.
|
|
14
14
|
const runtime = Runtime.get()
|
|
15
|
-
const
|
|
15
|
+
const db = TestApp.database()
|
|
16
|
+
await TestApp.verifiedSeed(db, runtime.chainId)
|
|
16
17
|
function app(options: TestApp.create.Options = {}) {
|
|
17
|
-
return TestApp.client({
|
|
18
|
+
return TestApp.client({ db, ...options })
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
async function holderWithBalance(client: ReturnType<typeof app>) {
|
package/src/apps/blocks.test.ts
CHANGED
package/src/apps/indexer.test.ts
CHANGED
package/src/apps/rpc.test.ts
CHANGED
|
@@ -227,16 +227,14 @@ describe('POST /rpc', () => {
|
|
|
227
227
|
try {
|
|
228
228
|
const app = TestApp.create({
|
|
229
229
|
auth: {
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
],
|
|
239
|
-
}),
|
|
230
|
+
keys: [
|
|
231
|
+
{
|
|
232
|
+
id: 'key_indexer_only',
|
|
233
|
+
orgId: 'org_test',
|
|
234
|
+
scopes: ['indexer:query'],
|
|
235
|
+
token: 'secret_indexer_key',
|
|
236
|
+
},
|
|
237
|
+
],
|
|
240
238
|
},
|
|
241
239
|
})
|
|
242
240
|
const response = await app.fetch(
|
|
@@ -266,6 +264,98 @@ describe('POST /rpc', () => {
|
|
|
266
264
|
})
|
|
267
265
|
})
|
|
268
266
|
|
|
267
|
+
describe('POST /rpc/relay', () => {
|
|
268
|
+
test('rejects anonymous callers', async () => {
|
|
269
|
+
const app = TestApp.create()
|
|
270
|
+
|
|
271
|
+
const response = await app.fetch(
|
|
272
|
+
new Request('http://tempo-api.test/rpc/relay', {
|
|
273
|
+
body: JSON.stringify({ id: 1, jsonrpc: '2.0', method: 'eth_chainId' }),
|
|
274
|
+
method: 'POST',
|
|
275
|
+
}),
|
|
276
|
+
)
|
|
277
|
+
const body = (await response.json()) as Record<string, unknown>
|
|
278
|
+
const { requestId, ...stable } = body
|
|
279
|
+
|
|
280
|
+
expect(response.status).toMatchInlineSnapshot(`401`)
|
|
281
|
+
expect(typeof requestId).toMatchInlineSnapshot(`"string"`)
|
|
282
|
+
expect(stable).toMatchInlineSnapshot(`
|
|
283
|
+
{
|
|
284
|
+
"error": {
|
|
285
|
+
"code": "api_key_missing",
|
|
286
|
+
"message": "Missing API key",
|
|
287
|
+
},
|
|
288
|
+
}
|
|
289
|
+
`)
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
test('rejects keys missing the relay scope', async () => {
|
|
293
|
+
// The shared test key holds the data/indexer scopes only.
|
|
294
|
+
const app = TestApp.create()
|
|
295
|
+
|
|
296
|
+
const response = await app.fetch(
|
|
297
|
+
new Request('http://tempo-api.test/rpc/relay', {
|
|
298
|
+
body: JSON.stringify({ id: 1, jsonrpc: '2.0', method: 'eth_chainId' }),
|
|
299
|
+
headers: { 'tempo-api-key': TestApp.key.token },
|
|
300
|
+
method: 'POST',
|
|
301
|
+
}),
|
|
302
|
+
)
|
|
303
|
+
const body = (await response.json()) as Record<string, unknown>
|
|
304
|
+
const { requestId, ...stable } = body
|
|
305
|
+
|
|
306
|
+
expect(response.status).toMatchInlineSnapshot(`403`)
|
|
307
|
+
expect(typeof requestId).toMatchInlineSnapshot(`"string"`)
|
|
308
|
+
expect(stable).toMatchInlineSnapshot(`
|
|
309
|
+
{
|
|
310
|
+
"error": {
|
|
311
|
+
"code": "api_key_forbidden",
|
|
312
|
+
"message": "API key missing required scope",
|
|
313
|
+
},
|
|
314
|
+
}
|
|
315
|
+
`)
|
|
316
|
+
})
|
|
317
|
+
|
|
318
|
+
test('proxies for keys holding rpc-relay', async () => {
|
|
319
|
+
const fetch = mockRpc()
|
|
320
|
+
|
|
321
|
+
try {
|
|
322
|
+
const app = TestApp.create({
|
|
323
|
+
auth: {
|
|
324
|
+
keys: [
|
|
325
|
+
{
|
|
326
|
+
id: 'key_relay',
|
|
327
|
+
orgId: 'org_test',
|
|
328
|
+
scopes: ['rpc-relay'],
|
|
329
|
+
token: 'secret_relay_key',
|
|
330
|
+
},
|
|
331
|
+
],
|
|
332
|
+
},
|
|
333
|
+
rpc: { url: () => 'https://internal.rpc.test/json-rpc' },
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
const response = await app.fetch(
|
|
337
|
+
new Request('http://tempo-api.test/rpc/relay', {
|
|
338
|
+
body: JSON.stringify({ id: 1, jsonrpc: '2.0', method: 'eth_chainId' }),
|
|
339
|
+
headers: { 'tempo-api-key': 'secret_relay_key' },
|
|
340
|
+
method: 'POST',
|
|
341
|
+
}),
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
expect(response.status).toMatchInlineSnapshot(`200`)
|
|
345
|
+
expect(await response.json()).toMatchInlineSnapshot(`
|
|
346
|
+
{
|
|
347
|
+
"id": 1,
|
|
348
|
+
"jsonrpc": "2.0",
|
|
349
|
+
"result": "0xa5bf",
|
|
350
|
+
}
|
|
351
|
+
`)
|
|
352
|
+
expect(fetch.calls[0]?.url).toMatchInlineSnapshot(`"https://internal.rpc.test/json-rpc"`)
|
|
353
|
+
} finally {
|
|
354
|
+
fetch.restore()
|
|
355
|
+
}
|
|
356
|
+
})
|
|
357
|
+
})
|
|
358
|
+
|
|
269
359
|
function mockRpc() {
|
|
270
360
|
const original = globalThis.fetch
|
|
271
361
|
const calls: {
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { Store } from 'cadent'
|
|
2
|
-
|
|
3
1
|
import * as TestApp from '../../test/App.js'
|
|
4
2
|
import * as Runtime from '../../test/runtime.js'
|
|
5
3
|
import * as VerifiedTokens from '../internal/VerifiedTokens.js'
|
|
@@ -11,7 +9,7 @@ const reader = {
|
|
|
11
9
|
orgId: 'org_test',
|
|
12
10
|
scopes: ['data:read'],
|
|
13
11
|
token: 'secret_reader',
|
|
14
|
-
} satisfies TestApp.
|
|
12
|
+
} satisfies TestApp.kvStore.Key
|
|
15
13
|
|
|
16
14
|
/** A known list published into the store before exercising the read endpoint. */
|
|
17
15
|
const seedList = [
|
|
@@ -27,11 +25,11 @@ function as(key: { token: string }) {
|
|
|
27
25
|
|
|
28
26
|
/** Client whose verified-token store holds a published {@link seedList} snapshot. */
|
|
29
27
|
async function seeded() {
|
|
30
|
-
const
|
|
31
|
-
await VerifiedTokens.replace(
|
|
28
|
+
const db = TestApp.database()
|
|
29
|
+
await VerifiedTokens.replace(db, chainId, seedList)
|
|
32
30
|
return TestApp.client({
|
|
33
|
-
auth: {
|
|
34
|
-
|
|
31
|
+
auth: { keys: [reader] },
|
|
32
|
+
db,
|
|
35
33
|
})
|
|
36
34
|
}
|
|
37
35
|
|