tapimo 0.10.0 → 0.10.1
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/dist/ApiKey.d.ts +2 -1
- package/dist/ApiKey.d.ts.map +1 -1
- package/dist/ApiKeys.d.ts +4 -2
- package/dist/ApiKeys.d.ts.map +1 -1
- package/dist/ApiKeys.js +26 -1
- package/dist/ApiKeys.js.map +1 -1
- package/dist/App.d.ts +3346 -2575
- package/dist/App.d.ts.map +1 -1
- package/dist/App.js +1 -0
- package/dist/App.js.map +1 -1
- package/dist/Webhooks.d.ts.map +1 -1
- package/dist/Webhooks.js +5 -5
- package/dist/Webhooks.js.map +1 -1
- package/dist/admin/App.d.ts +91 -84
- package/dist/admin/App.d.ts.map +1 -1
- package/dist/admin/apps/api-keys.d.ts +26 -17
- package/dist/admin/apps/api-keys.d.ts.map +1 -1
- package/dist/admin/apps/api-keys.js +6 -0
- package/dist/admin/apps/api-keys.js.map +1 -1
- package/dist/admin/apps/organizations.d.ts +4 -2
- package/dist/admin/apps/organizations.d.ts.map +1 -1
- package/dist/admin/ui.generated.d.ts +1 -1
- package/dist/admin/ui.generated.js +69 -69
- package/dist/admin/ui.generated.js.map +1 -1
- package/dist/analytics/Analytics.d.ts.map +1 -1
- package/dist/analytics/Analytics.js +26 -0
- package/dist/analytics/Analytics.js.map +1 -1
- package/dist/apps/Zones.d.ts +4266 -82
- package/dist/apps/Zones.d.ts.map +1 -1
- package/dist/apps/Zones.js +27 -8
- package/dist/apps/Zones.js.map +1 -1
- package/dist/apps/data/App.d.ts +3467 -2696
- package/dist/apps/data/App.d.ts.map +1 -1
- package/dist/apps/data/routes/activities.d.ts +2999 -100
- package/dist/apps/data/routes/activities.d.ts.map +1 -1
- package/dist/apps/data/routes/activities.js +340 -2
- package/dist/apps/data/routes/activities.js.map +1 -1
- package/dist/apps/data/routes/addresses.d.ts +850 -336
- package/dist/apps/data/routes/addresses.d.ts.map +1 -1
- package/dist/apps/data/routes/balances.d.ts +26 -26
- package/dist/apps/data/routes/balances.d.ts.map +1 -1
- package/dist/apps/data/routes/balances.js +11 -3
- package/dist/apps/data/routes/balances.js.map +1 -1
- package/dist/apps/data/routes/blocks.d.ts +4 -4
- package/dist/apps/data/routes/coingecko.d.ts +176 -176
- package/dist/apps/data/routes/earn.d.ts +75 -75
- package/dist/apps/data/routes/earn.js +2 -2
- package/dist/apps/data/routes/earn.js.map +1 -1
- package/dist/apps/data/routes/exchanges.d.ts +120 -120
- package/dist/apps/data/routes/fee-amm.d.ts +24 -24
- package/dist/apps/data/routes/indexer.d.ts +35 -34
- package/dist/apps/data/routes/indexer.d.ts.map +1 -1
- package/dist/apps/data/routes/indexer.js +3 -2
- package/dist/apps/data/routes/indexer.js.map +1 -1
- package/dist/apps/data/routes/rpc.d.ts.map +1 -1
- package/dist/apps/data/routes/rpc.js +5 -1
- package/dist/apps/data/routes/rpc.js.map +1 -1
- package/dist/apps/data/routes/tokenlist.d.ts +12 -12
- package/dist/apps/data/routes/transactions.d.ts +281 -281
- package/dist/apps/data/routes/verified-tokens.d.ts +37 -37
- package/dist/apps/data/routes/webhooks.d.ts +418 -364
- package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
- package/dist/apps/data/routes/webhooks.js +161 -14
- package/dist/apps/data/routes/webhooks.js.map +1 -1
- package/dist/apps/data/routes/zones.d.ts +31 -31
- package/dist/apps/data/routes/zones.js +1 -1
- package/dist/apps/data/routes/zones.js.map +1 -1
- package/dist/apps/funding/App.d.ts +96 -96
- package/dist/apps/funding/routes/deposit-addresses.d.ts +52 -52
- package/dist/apps/funding/routes/deposit-addresses.js +1 -1
- package/dist/apps/funding/routes/deposit-addresses.js.map +1 -1
- package/dist/apps/funding/routes/providers.d.ts +13 -13
- package/dist/apps/funding/routes/transfers.d.ts +32 -32
- package/dist/apps/funding/routes/transfers.js +1 -1
- package/dist/apps/funding/routes/transfers.js.map +1 -1
- package/dist/apps/management/App.d.ts +418 -418
- package/dist/apps/management/routes/api-keys.d.ts +50 -50
- package/dist/apps/management/routes/billing.d.ts +72 -72
- package/dist/apps/management/routes/billing.js +1 -1
- package/dist/apps/management/routes/billing.js.map +1 -1
- package/dist/apps/management/routes/faucet.d.ts +37 -37
- package/dist/apps/management/routes/faucet.js +2 -2
- package/dist/apps/management/routes/faucet.js.map +1 -1
- package/dist/apps/management/routes/invite-links.d.ts +42 -42
- package/dist/apps/management/routes/me.d.ts +6 -6
- package/dist/apps/management/routes/members.d.ts +75 -75
- package/dist/apps/management/routes/projects.d.ts +40 -40
- package/dist/apps/management/routes/usage.d.ts +76 -76
- package/dist/apps/management/routes/webhooks.d.ts +49 -49
- package/dist/apps/mcp.d.ts.map +1 -1
- package/dist/apps/mcp.js +4 -1
- package/dist/apps/mcp.js.map +1 -1
- package/dist/db/Db.d.ts +2 -0
- package/dist/db/Db.d.ts.map +1 -1
- package/dist/db/Db.js +58 -0
- package/dist/db/Db.js.map +1 -1
- package/dist/db/Schema.d.ts +19 -0
- package/dist/db/Schema.d.ts.map +1 -1
- package/dist/db/Schema.js +24 -0
- package/dist/db/Schema.js.map +1 -1
- package/dist/db/tables/webhookQueueEvents.d.ts +122 -0
- package/dist/db/tables/webhookQueueEvents.d.ts.map +1 -0
- package/dist/db/tables/webhookQueueEvents.js +160 -0
- package/dist/db/tables/webhookQueueEvents.js.map +1 -0
- package/dist/db/tables/webhookSubscriptions.d.ts +2 -0
- package/dist/db/tables/webhookSubscriptions.d.ts.map +1 -1
- package/dist/db/tables/webhookSubscriptions.js +10 -0
- package/dist/db/tables/webhookSubscriptions.js.map +1 -1
- package/dist/internal/Auth.d.ts +3 -0
- package/dist/internal/Auth.d.ts.map +1 -1
- package/dist/internal/Auth.js +6 -4
- package/dist/internal/Auth.js.map +1 -1
- package/dist/internal/MetricSink.d.ts +27 -0
- package/dist/internal/MetricSink.d.ts.map +1 -1
- package/dist/internal/MetricSink.js +29 -2
- package/dist/internal/MetricSink.js.map +1 -1
- package/dist/internal/RateLimit.d.ts +4 -6
- package/dist/internal/RateLimit.d.ts.map +1 -1
- package/dist/internal/RateLimit.js +15 -14
- package/dist/internal/RateLimit.js.map +1 -1
- package/dist/internal/Store.d.ts +2 -1
- package/dist/internal/Store.d.ts.map +1 -1
- package/dist/internal/Store.js +57 -18
- package/dist/internal/Store.js.map +1 -1
- package/dist/internal/Value.d.ts +2 -0
- package/dist/internal/Value.d.ts.map +1 -1
- package/dist/internal/Value.js +25 -0
- package/dist/internal/Value.js.map +1 -1
- package/dist/internal/WebhookTransfer.d.ts +20 -0
- package/dist/internal/WebhookTransfer.d.ts.map +1 -1
- package/dist/internal/WebhookTransfer.js +7 -4
- package/dist/internal/WebhookTransfer.js.map +1 -1
- package/dist/internal/Webhooks.d.ts +139 -0
- package/dist/internal/Webhooks.d.ts.map +1 -1
- package/dist/internal/Webhooks.js +129 -7
- package/dist/internal/Webhooks.js.map +1 -1
- package/dist/internal/funding/providers/relay.d.ts +3 -4
- package/dist/internal/funding/providers/relay.d.ts.map +1 -1
- package/dist/internal/funding/providers/relay.js +59 -8
- package/dist/internal/funding/providers/relay.js.map +1 -1
- package/package.json +3 -3
- package/src/ApiKeys.test.ts +20 -0
- package/src/ApiKeys.ts +28 -1
- package/src/App.test.ts +10 -8
- package/src/App.ts +1 -0
- package/src/Client.test-d.ts +1 -0
- package/src/Webhooks.ts +19 -17
- package/src/admin/apps/api-keys.test.ts +9 -0
- package/src/admin/apps/api-keys.ts +15 -0
- package/src/admin/ui/src/lib/query-client.ts +3 -2
- package/src/admin/ui/src/routes/api-keys.tsx +31 -9
- package/src/admin/ui.generated.ts +69 -69
- package/src/analytics/Analytics.test.ts +8 -0
- package/src/analytics/Analytics.ts +26 -0
- package/src/apps/Zones.test.ts +20 -3
- package/src/apps/Zones.ts +28 -8
- package/src/apps/composition.test.ts +1 -0
- package/src/apps/data/App.test.ts +2 -2
- package/src/apps/data/routes/activities.bench.ts +41 -0
- package/src/apps/data/routes/activities.test.ts +103 -2
- package/src/apps/data/routes/activities.ts +401 -2
- package/src/apps/data/routes/balances.bench.ts +21 -0
- package/src/apps/data/routes/balances.ts +11 -3
- package/src/apps/data/routes/blocks.bench.ts +34 -0
- package/src/apps/data/routes/coingecko.bench.ts +53 -0
- package/src/apps/data/routes/earn.bench.ts +71 -0
- package/src/apps/data/routes/earn.ts +2 -2
- package/src/apps/data/routes/exchanges.bench.ts +85 -0
- package/src/apps/data/routes/fee-amm.bench.ts +13 -0
- package/src/apps/data/routes/indexer.bench.ts +8 -0
- package/src/apps/data/routes/indexer.test.ts +2 -1
- package/src/apps/data/routes/indexer.ts +3 -2
- package/src/apps/data/routes/receipts.bench.ts +39 -0
- package/src/apps/data/routes/rpc.bench.ts +14 -0
- package/src/apps/data/routes/rpc.test.ts +27 -0
- package/src/apps/data/routes/rpc.ts +5 -1
- package/src/apps/data/routes/tokenlist.bench.ts +3 -0
- package/src/apps/data/routes/tokens.bench.ts +76 -0
- package/src/apps/data/routes/transactions.bench.ts +40 -0
- package/src/apps/data/routes/transfers.bench.ts +28 -0
- package/src/apps/data/routes/valuation.bench.ts +14 -0
- package/src/apps/data/routes/verified-tokens.bench.ts +8 -0
- package/src/apps/data/routes/webhooks.bench.ts +26 -0
- package/src/apps/data/routes/webhooks.scan.test.ts +326 -0
- package/src/apps/data/routes/webhooks.test.ts +53 -6
- package/src/apps/data/routes/webhooks.ts +221 -14
- package/src/apps/data/routes/zones.bench.ts +32 -0
- package/src/apps/data/routes/zones.test.ts +1 -1
- package/src/apps/data/routes/zones.ts +1 -1
- package/src/apps/funding/routes/chains.test.ts +1 -1
- package/src/apps/funding/routes/deposit-addresses.test.ts +1 -1
- package/src/apps/funding/routes/deposit-addresses.ts +1 -1
- package/src/apps/funding/routes/providers.test.ts +1 -1
- package/src/apps/funding/routes/quotes.test.ts +1 -1
- package/src/apps/funding/routes/transfers.test.ts +3 -6
- package/src/apps/funding/routes/transfers.ts +1 -1
- package/src/apps/management/routes/billing.ts +1 -1
- package/src/apps/management/routes/faucet.ts +2 -2
- package/src/apps/management/routes/me.test.ts +31 -0
- package/src/apps/mcp.ts +4 -1
- package/src/db/Db.ts +81 -0
- package/src/db/Schema.ts +32 -0
- package/src/db/tables/webhookQueueEvents.ts +285 -0
- package/src/db/tables/webhookSubscriptions.ts +15 -0
- package/src/internal/Auth.test.ts +61 -50
- package/src/internal/Auth.ts +9 -4
- package/src/internal/EarnVaults.test.ts +2 -2
- package/src/internal/MetricSink.test.ts +58 -1
- package/src/internal/MetricSink.ts +60 -2
- package/src/internal/RateLimit.test.ts +23 -18
- package/src/internal/RateLimit.ts +23 -15
- package/src/internal/Store.test.ts +99 -13
- package/src/internal/Store.ts +52 -17
- package/src/internal/Tidx.test.ts +4 -4
- package/src/internal/Value.test.ts +25 -0
- package/src/internal/Value.ts +29 -0
- package/src/internal/WebhookTransfer.test.ts +89 -0
- package/src/internal/WebhookTransfer.ts +11 -7
- package/src/internal/Webhooks.test.ts +479 -0
- package/src/internal/Webhooks.ts +281 -7
- package/src/internal/funding/Provider.test.ts +1 -1
- package/src/internal/funding/providers/index.test.ts +73 -3
- package/src/internal/funding/providers/relay.test.ts +1 -5
- package/src/internal/funding/providers/relay.ts +56 -12
|
@@ -664,7 +664,7 @@ export namespace schema {
|
|
|
664
664
|
.datetime()
|
|
665
665
|
.check(
|
|
666
666
|
z.describe('Block timestamp as an ISO 8601 string.'),
|
|
667
|
-
z.meta({ examples: ['2026-01-14T18:38:03.
|
|
667
|
+
z.meta({ examples: ['2026-01-14T18:38:03.685Z'] }),
|
|
668
668
|
),
|
|
669
669
|
token: z.optional(
|
|
670
670
|
z
|
|
@@ -790,7 +790,7 @@ export namespace schema {
|
|
|
790
790
|
.datetime()
|
|
791
791
|
.check(
|
|
792
792
|
z.describe('When the block was produced, as an ISO 8601 timestamp.'),
|
|
793
|
-
z.meta({ examples: ['2026-01-14T18:38:03.
|
|
793
|
+
z.meta({ examples: ['2026-01-14T18:38:03.685Z'] }),
|
|
794
794
|
),
|
|
795
795
|
topics: z.array(Schema.Hash).check(
|
|
796
796
|
z.describe('Log topics, starting with the event signature hash (topic 0).'),
|
|
@@ -881,7 +881,7 @@ export namespace schema {
|
|
|
881
881
|
.datetime()
|
|
882
882
|
.check(
|
|
883
883
|
z.describe('When the block was produced, as an ISO 8601 timestamp.'),
|
|
884
|
-
z.meta({ examples: ['2026-01-14T18:38:03.
|
|
884
|
+
z.meta({ examples: ['2026-01-14T18:38:03.685Z'] }),
|
|
885
885
|
),
|
|
886
886
|
})
|
|
887
887
|
.check(z.describe('Payload for a `block:created` event (a new block header).'))
|
|
@@ -2303,7 +2303,7 @@ export async function poll(options: poll.Options): Promise<poll.Result> {
|
|
|
2303
2303
|
const dispatchables: poll.Dispatchable[] = []
|
|
2304
2304
|
for (const item of page.items) dispatchables.push(await buildDispatchable(item))
|
|
2305
2305
|
options.signal?.throwIfAborted()
|
|
2306
|
-
const delivered = await dispatchBatch(dispatchables)
|
|
2306
|
+
const delivered = await dispatchBatch(dispatchables, { db })
|
|
2307
2307
|
if (delivered.ok) {
|
|
2308
2308
|
accepted = page.items.at(-1)?.cursor
|
|
2309
2309
|
result.dispatched += dispatchables.length
|
|
@@ -2406,7 +2406,9 @@ export declare namespace poll {
|
|
|
2406
2406
|
*/
|
|
2407
2407
|
dispatch?: ((input: Dispatchable) => Promise<Webhooks.Result>) | undefined
|
|
2408
2408
|
/** Batch delivery transport. A successful result confirms every row in the input. */
|
|
2409
|
-
dispatchBatch?:
|
|
2409
|
+
dispatchBatch?:
|
|
2410
|
+
| ((input: readonly Dispatchable[], context: DispatchContext) => Promise<Webhooks.Result>)
|
|
2411
|
+
| undefined
|
|
2410
2412
|
/** Event types to poll (default all). */
|
|
2411
2413
|
eventTypes?: readonly EventType[] | undefined
|
|
2412
2414
|
/** Current chain head; the poller emits up to `head - confirmations`. */
|
|
@@ -2426,6 +2428,12 @@ export declare namespace poll {
|
|
|
2426
2428
|
/** Stops scanning and dispatch when the caller loses its execution lease. */
|
|
2427
2429
|
signal?: AbortSignal | undefined
|
|
2428
2430
|
}
|
|
2431
|
+
|
|
2432
|
+
/** Runtime context shared with a batch delivery transport. */
|
|
2433
|
+
type DispatchContext = {
|
|
2434
|
+
/** Database instance already resolved for this poll. */
|
|
2435
|
+
db: Db.Db
|
|
2436
|
+
}
|
|
2429
2437
|
/** Summary counters for one poll tick. */
|
|
2430
2438
|
type Result = {
|
|
2431
2439
|
/** Chain that was polled. */
|
|
@@ -3124,7 +3132,12 @@ async function scanLogs(options: {
|
|
|
3124
3132
|
if (blockNumber === undefined || logIndex === undefined) continue
|
|
3125
3133
|
if (atOrBefore(blockNumber, logIndex, window.skip)) continue
|
|
3126
3134
|
if (!matchesLog(log, f, event)) continue
|
|
3127
|
-
const data = toLogEvent(
|
|
3135
|
+
const data = toLogEvent(
|
|
3136
|
+
log,
|
|
3137
|
+
Value.blockToIso(await cache.header(blockNumber)),
|
|
3138
|
+
f.signature,
|
|
3139
|
+
event,
|
|
3140
|
+
)
|
|
3128
3141
|
if (data) {
|
|
3129
3142
|
items.push({ cursor: [data.blockNumber, data.logIndex], data })
|
|
3130
3143
|
if (items.length >= batchSize) return { hasMore: true, items }
|
|
@@ -3178,12 +3191,6 @@ function matchesLog(
|
|
|
3178
3191
|
|
|
3179
3192
|
// block:created (getBlock)
|
|
3180
3193
|
|
|
3181
|
-
/** Converts a viem block's `timestamp` (seconds) to an ISO 8601 string. */
|
|
3182
|
-
function blockIso(block: RpcBlock | undefined): string | undefined {
|
|
3183
|
-
if (!block || typeof block.timestamp !== 'bigint') return undefined
|
|
3184
|
-
return new Date(Number(block.timestamp) * 1000).toISOString()
|
|
3185
|
-
}
|
|
3186
|
-
|
|
3187
3194
|
type BlockEventData = {
|
|
3188
3195
|
number: number
|
|
3189
3196
|
hash: string
|
|
@@ -3203,7 +3210,7 @@ function toBlockEvent(block: RpcBlock): BlockEventData | undefined {
|
|
|
3203
3210
|
const miner = Schema.Address.safeParse(block.miner)
|
|
3204
3211
|
const gasUsed = typeof block.gasUsed === 'bigint' ? Number(block.gasUsed) : undefined
|
|
3205
3212
|
const gasLimit = typeof block.gasLimit === 'bigint' ? Number(block.gasLimit) : undefined
|
|
3206
|
-
const timestamp =
|
|
3213
|
+
const timestamp = Value.blockToIso(block)
|
|
3207
3214
|
const transactionCount = Array.isArray(block.transactions) ? block.transactions.length : 0
|
|
3208
3215
|
if (
|
|
3209
3216
|
number === undefined ||
|
|
@@ -3318,6 +3325,9 @@ async function scanTxs(options: {
|
|
|
3318
3325
|
? (block['transactions'] as unknown[])
|
|
3319
3326
|
: []
|
|
3320
3327
|
const timestamp = asString(block['timestamp'])
|
|
3328
|
+
// A transaction's own `blockTimestamp` is seconds-only, so the delivered
|
|
3329
|
+
// timestamp comes from the enclosing block, which carries milliseconds.
|
|
3330
|
+
const blockTime = Value.blockToIso(block)
|
|
3321
3331
|
for (let offset = 0; offset < transactions.length; offset++) {
|
|
3322
3332
|
const transaction = transactions[offset] as Record<string, unknown>
|
|
3323
3333
|
const transactionIndex = Value.hexToNumber(transaction['transactionIndex']) ?? offset
|
|
@@ -3325,7 +3335,8 @@ async function scanTxs(options: {
|
|
|
3325
3335
|
if (!matchesTx(transaction, f, includeCalls, timestamp, blockNumber)) continue
|
|
3326
3336
|
const parsed = Transactions.schema.Rpc.Transaction.safeParse(transaction)
|
|
3327
3337
|
if (!parsed.success) continue
|
|
3328
|
-
const
|
|
3338
|
+
const humanized = Transactions.humanizeTransaction(parsed.data)
|
|
3339
|
+
const data = blockTime === undefined ? humanized : { ...humanized, timestamp: blockTime }
|
|
3329
3340
|
items.push({ cursor: [blockNumber, transactionIndex], data })
|
|
3330
3341
|
if (items.length >= batchSize) return { hasMore: true, items }
|
|
3331
3342
|
}
|
|
@@ -3482,6 +3493,202 @@ function unauthorized(c: Context<App.Environment>) {
|
|
|
3482
3493
|
})
|
|
3483
3494
|
}
|
|
3484
3495
|
|
|
3496
|
+
// Pure per-block scanning (no RPC, no cursors, no pagination).
|
|
3497
|
+
|
|
3498
|
+
// Pure over its inputs: the caller fetches the raw block (with transactions)
|
|
3499
|
+
// and its logs; enrichment happens downstream. Rows require a parsable block number.
|
|
3500
|
+
/**
|
|
3501
|
+
* Matches one fetched block against a subscription set, producing the rows
|
|
3502
|
+
* each event type delivers. Invalid stored filters fail closed into `invalid`.
|
|
3503
|
+
*/
|
|
3504
|
+
export function scanBlock<const subscription extends scanBlock.Subscription>(
|
|
3505
|
+
options: scanBlock.Options<subscription>,
|
|
3506
|
+
): scanBlock.Result<subscription> {
|
|
3507
|
+
const { block, logs, subscriptions } = options
|
|
3508
|
+
const blockNumber = Value.hexToNumber(block['number'])
|
|
3509
|
+
const timestamp = Value.blockToIso(block)
|
|
3510
|
+
const header = toScanHeader(block)
|
|
3511
|
+
const viemLogs = logs.map(fromRpcLog)
|
|
3512
|
+
const matches: scanBlock.Match<subscription>[] = []
|
|
3513
|
+
const invalid: scanBlock.Invalid<subscription>[] = []
|
|
3514
|
+
|
|
3515
|
+
for (const subscription of subscriptions) {
|
|
3516
|
+
const filters = (() => {
|
|
3517
|
+
try {
|
|
3518
|
+
return parseFilters(subscription.eventType, subscription.filters)
|
|
3519
|
+
} catch (error) {
|
|
3520
|
+
invalid.push({ error, subscription })
|
|
3521
|
+
return undefined
|
|
3522
|
+
}
|
|
3523
|
+
})()
|
|
3524
|
+
if (filters === undefined) continue
|
|
3525
|
+
const matched = matches.length
|
|
3526
|
+
const push = (data: unknown, logIndex: number) => {
|
|
3527
|
+
if (blockNumber !== undefined) matches.push({ blockNumber, data, logIndex, subscription })
|
|
3528
|
+
}
|
|
3529
|
+
|
|
3530
|
+
// A throwing matcher invalidates only this subscription, rolling back any
|
|
3531
|
+
// rows it matched before the failure.
|
|
3532
|
+
try {
|
|
3533
|
+
switch (subscription.eventType) {
|
|
3534
|
+
case 'block:created': {
|
|
3535
|
+
const f = filters as z.output<typeof schema.BlockFilters>
|
|
3536
|
+
if (!matchesBlock(header, f)) break
|
|
3537
|
+
const data = toBlockEvent(header)
|
|
3538
|
+
if (data) push(data, 0)
|
|
3539
|
+
break
|
|
3540
|
+
}
|
|
3541
|
+
case 'log:emitted': {
|
|
3542
|
+
const f = filters as z.output<typeof schema.LogFilters>
|
|
3543
|
+
const event = parseEvent(f.signature)
|
|
3544
|
+
if (f.signature && (!event || !/^[A-Za-z_]\w*$/.test(event.name))) {
|
|
3545
|
+
invalid.push({
|
|
3546
|
+
error: new Webhooks.InvalidFilterError('log:emitted', [
|
|
3547
|
+
{ message: 'Unsupported event signature.', path: ['signature'] },
|
|
3548
|
+
]),
|
|
3549
|
+
subscription,
|
|
3550
|
+
})
|
|
3551
|
+
break
|
|
3552
|
+
}
|
|
3553
|
+
// Validate arg names up front so a bad filter fails closed even
|
|
3554
|
+
// when the block has no decodable logs.
|
|
3555
|
+
const unknown =
|
|
3556
|
+
event && f.args
|
|
3557
|
+
? Object.keys(f.args).find(
|
|
3558
|
+
(name) => !event.inputs.some((input) => input.name === name),
|
|
3559
|
+
)
|
|
3560
|
+
: undefined
|
|
3561
|
+
if (unknown !== undefined) {
|
|
3562
|
+
invalid.push({
|
|
3563
|
+
error: new Webhooks.InvalidFilterError('log:emitted', [
|
|
3564
|
+
{ message: `Unknown event argument: ${unknown}.`, path: ['args', unknown] },
|
|
3565
|
+
]),
|
|
3566
|
+
subscription,
|
|
3567
|
+
})
|
|
3568
|
+
break
|
|
3569
|
+
}
|
|
3570
|
+
for (const log of viemLogs) {
|
|
3571
|
+
const decoded = event ? decodeScanLog(log, event) : log
|
|
3572
|
+
const logIndex = decoded.logIndex ?? undefined
|
|
3573
|
+
if (logIndex === undefined) continue
|
|
3574
|
+
if (!matchesLog(decoded, f, event)) continue
|
|
3575
|
+
const data = toLogEvent(decoded, timestamp, f.signature, event)
|
|
3576
|
+
if (data) push(data, logIndex)
|
|
3577
|
+
}
|
|
3578
|
+
break
|
|
3579
|
+
}
|
|
3580
|
+
case 'token:transfer': {
|
|
3581
|
+
if (timestamp === undefined) break
|
|
3582
|
+
const f = filters as WebhookTransfer.Filters
|
|
3583
|
+
for (const log of logs) {
|
|
3584
|
+
const data = WebhookTransfer.decode(log, timestamp)
|
|
3585
|
+
if (!data || !WebhookTransfer.matches(data, f)) continue
|
|
3586
|
+
push(data, data.logIndex)
|
|
3587
|
+
}
|
|
3588
|
+
break
|
|
3589
|
+
}
|
|
3590
|
+
case 'transaction:included': {
|
|
3591
|
+
const f = filters as z.output<typeof schema.TxFilters>
|
|
3592
|
+
const includeCalls = f.includeCalls ?? true
|
|
3593
|
+
const transactions = Array.isArray(block['transactions'])
|
|
3594
|
+
? (block['transactions'] as unknown[])
|
|
3595
|
+
: []
|
|
3596
|
+
const rawTimestamp = asString(block['timestamp'])
|
|
3597
|
+
for (let offset = 0; offset < transactions.length; offset++) {
|
|
3598
|
+
const transaction = transactions[offset]
|
|
3599
|
+
if (!transaction || typeof transaction !== 'object') continue
|
|
3600
|
+
const tx = transaction as Record<string, unknown>
|
|
3601
|
+
const transactionIndex = Value.hexToNumber(tx['transactionIndex']) ?? offset
|
|
3602
|
+
if (!matchesTx(tx, f, includeCalls, rawTimestamp, blockNumber ?? 0)) continue
|
|
3603
|
+
const parsed = Transactions.schema.Rpc.Transaction.safeParse(tx)
|
|
3604
|
+
if (!parsed.success) continue
|
|
3605
|
+
const humanized = Transactions.humanizeTransaction(parsed.data)
|
|
3606
|
+
// A transaction's own timestamp is seconds-only; the enclosing
|
|
3607
|
+
// block carries milliseconds.
|
|
3608
|
+
push(
|
|
3609
|
+
timestamp === undefined ? humanized : { ...humanized, timestamp },
|
|
3610
|
+
transactionIndex,
|
|
3611
|
+
)
|
|
3612
|
+
}
|
|
3613
|
+
break
|
|
3614
|
+
}
|
|
3615
|
+
}
|
|
3616
|
+
} catch (error) {
|
|
3617
|
+
matches.length = matched
|
|
3618
|
+
invalid.push({ error, subscription })
|
|
3619
|
+
}
|
|
3620
|
+
}
|
|
3621
|
+
return { invalid, matches }
|
|
3622
|
+
}
|
|
3623
|
+
|
|
3624
|
+
/** Normalizes a raw RPC header to the viem-shaped fields the block matchers read. */
|
|
3625
|
+
function toScanHeader(block: Record<string, unknown>): RpcBlock {
|
|
3626
|
+
const quantity = (value: unknown) => {
|
|
3627
|
+
try {
|
|
3628
|
+
return typeof value === 'string' || typeof value === 'number' || typeof value === 'bigint'
|
|
3629
|
+
? BigInt(value)
|
|
3630
|
+
: undefined
|
|
3631
|
+
} catch {
|
|
3632
|
+
return undefined
|
|
3633
|
+
}
|
|
3634
|
+
}
|
|
3635
|
+
return {
|
|
3636
|
+
...block,
|
|
3637
|
+
gasLimit: quantity(block['gasLimit']),
|
|
3638
|
+
gasUsed: quantity(block['gasUsed']),
|
|
3639
|
+
number: quantity(block['number']),
|
|
3640
|
+
timestamp: quantity(block['timestamp']),
|
|
3641
|
+
} as RpcBlock
|
|
3642
|
+
}
|
|
3643
|
+
|
|
3644
|
+
export declare namespace scanBlock {
|
|
3645
|
+
/** The subscription fields matching consumes; extra fields flow through. */
|
|
3646
|
+
type Subscription = {
|
|
3647
|
+
/** Event type selecting the matcher and payload shape. */
|
|
3648
|
+
eventType: EventType
|
|
3649
|
+
/** Stored filter predicates, validated fail-closed per event type. */
|
|
3650
|
+
filters: Record<string, unknown>
|
|
3651
|
+
}
|
|
3652
|
+
|
|
3653
|
+
/** One fetched block plus the subscriptions to match it against. */
|
|
3654
|
+
type Options<subscription extends Subscription> = {
|
|
3655
|
+
/** Raw `eth_getBlockByNumber(_, true)` result: header plus raw transactions. */
|
|
3656
|
+
block: Record<string, unknown>
|
|
3657
|
+
/** Raw `eth_getLogs` entries for exactly this block, in log order. */
|
|
3658
|
+
logs: readonly RpcLog[]
|
|
3659
|
+
/** Active subscriptions for the chain; any mix of event types. */
|
|
3660
|
+
subscriptions: readonly subscription[]
|
|
3661
|
+
}
|
|
3662
|
+
|
|
3663
|
+
/** One deliverable row for one subscription. */
|
|
3664
|
+
type Match<subscription extends Subscription> = {
|
|
3665
|
+
/** Block the row was produced from. */
|
|
3666
|
+
blockNumber: number
|
|
3667
|
+
/** Payload the matching read endpoint returns (pre-enrichment). */
|
|
3668
|
+
data: unknown
|
|
3669
|
+
/** Row position within the block (log or transaction index; 0 for blocks). */
|
|
3670
|
+
logIndex: number
|
|
3671
|
+
/** The matched subscription, passed through verbatim. */
|
|
3672
|
+
subscription: subscription
|
|
3673
|
+
}
|
|
3674
|
+
|
|
3675
|
+
/** A subscription whose stored filters failed validation. */
|
|
3676
|
+
type Invalid<subscription extends Subscription> = {
|
|
3677
|
+
/** The validation failure. */
|
|
3678
|
+
error: unknown
|
|
3679
|
+
/** The subscription with invalid filters. */
|
|
3680
|
+
subscription: subscription
|
|
3681
|
+
}
|
|
3682
|
+
|
|
3683
|
+
/** Matches plus fail-closed filter rejections. */
|
|
3684
|
+
type Result<subscription extends Subscription> = {
|
|
3685
|
+
/** Subscriptions skipped because their stored filters failed validation. */
|
|
3686
|
+
invalid: readonly Invalid<subscription>[]
|
|
3687
|
+
/** Deliverable rows in subscription order, block order within each type. */
|
|
3688
|
+
matches: readonly Match<subscription>[]
|
|
3689
|
+
}
|
|
3690
|
+
}
|
|
3691
|
+
|
|
3485
3692
|
class BlockUnavailableError extends Error {
|
|
3486
3693
|
override name = 'Webhooks.BlockUnavailableError'
|
|
3487
3694
|
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import * as Tidx from '../../../internal/Tidx.js'
|
|
2
|
+
import * as Viem from '../../../internal/Viem.js'
|
|
3
|
+
import * as TestApp from '../../../../test/App.js'
|
|
4
|
+
import * as QueryBench from '../../../../test/QueryBench.js'
|
|
5
|
+
|
|
6
|
+
const senderTag = `0x${'00'.repeat(32)}`
|
|
7
|
+
const zoneChainIds = {
|
|
8
|
+
4217: 421_700_002,
|
|
9
|
+
42431: 424_310_002,
|
|
10
|
+
} as const
|
|
11
|
+
|
|
12
|
+
QueryBench.bench({
|
|
13
|
+
chainId: false,
|
|
14
|
+
name: 'listZoneWithdrawals',
|
|
15
|
+
path: {
|
|
16
|
+
4217: `/v1/zones/withdrawals/${senderTag}?chainId=${zoneChainIds[4217]}`,
|
|
17
|
+
42431: `/v1/zones/withdrawals/${senderTag}?chainId=${zoneChainIds[42431]}`,
|
|
18
|
+
},
|
|
19
|
+
zones: ({ chainId }) => {
|
|
20
|
+
const rpcUrl = Viem.resolveUrl(process.env.RPC_URL, chainId) ?? Viem.url[chainId]
|
|
21
|
+
const tidxUrl = Viem.resolveUrl(process.env.TIDX_URL, chainId) ?? Tidx.url[chainId]
|
|
22
|
+
if (!rpcUrl || !tidxUrl) throw new Error(`Missing Zone upstream for chain ${chainId}.`)
|
|
23
|
+
return [
|
|
24
|
+
TestApp.zone({
|
|
25
|
+
chainId: zoneChainIds[chainId],
|
|
26
|
+
rpcUrl,
|
|
27
|
+
sourceChainId: chainId,
|
|
28
|
+
tidxUrl,
|
|
29
|
+
}),
|
|
30
|
+
]
|
|
31
|
+
},
|
|
32
|
+
})
|
|
@@ -43,7 +43,7 @@ export namespace schema {
|
|
|
43
43
|
.strictObject({
|
|
44
44
|
chainId: Schema.ChainId.check(
|
|
45
45
|
z.describe('The chain id of the Zone where the withdrawal originated.'),
|
|
46
|
-
z.meta({ examples: [
|
|
46
|
+
z.meta({ examples: [1_424_310_001] }),
|
|
47
47
|
),
|
|
48
48
|
})
|
|
49
49
|
.check(z.describe('Query parameters used to select the originating Zone.'))
|
|
@@ -22,7 +22,7 @@ const providers = [
|
|
|
22
22
|
integratorId: '0xdead',
|
|
23
23
|
userAddress: '0x1111111111111111111111111111111111111111',
|
|
24
24
|
}),
|
|
25
|
-
Relay.relay({
|
|
25
|
+
Relay.relay({ userAddresses: TestFunding.relayQuoteUsers }),
|
|
26
26
|
Rhino.rhino(),
|
|
27
27
|
Squid.squid({
|
|
28
28
|
integratorId: 'squid-integrator-id',
|
|
@@ -45,7 +45,7 @@ const providers = [
|
|
|
45
45
|
integratorId: '0xdead',
|
|
46
46
|
userAddress: '0x1111111111111111111111111111111111111111',
|
|
47
47
|
}),
|
|
48
|
-
Relay.relay({
|
|
48
|
+
Relay.relay({ userAddresses: TestFunding.relayQuoteUsers }),
|
|
49
49
|
Rhino.rhino(),
|
|
50
50
|
Squid.squid({
|
|
51
51
|
integratorId: 'squid-integrator-id',
|
|
@@ -712,7 +712,7 @@ export function depositAddresses(options: depositAddresses.Options = {}) {
|
|
|
712
712
|
.post(
|
|
713
713
|
'/v1/funding/deposit-addresses/:id{fda_[A-Za-z0-9_-]+}/reconcile',
|
|
714
714
|
Auth.policy({
|
|
715
|
-
apiKey: { rateLimit: {
|
|
715
|
+
apiKey: { rateLimit: { limit: 10, period: 'minute' }, scopes: ['funding:write'] },
|
|
716
716
|
}),
|
|
717
717
|
OpenApi.validate('param', schema.reconcileFundingDepositAddress.Params, {
|
|
718
718
|
code: 'param_invalid',
|
|
@@ -28,7 +28,7 @@ const providers = [
|
|
|
28
28
|
integratorId: '0xdead',
|
|
29
29
|
userAddress: '0x1111111111111111111111111111111111111111',
|
|
30
30
|
}),
|
|
31
|
-
Relay.relay({
|
|
31
|
+
Relay.relay({ userAddresses: TestFunding.relayQuoteUsers }),
|
|
32
32
|
Rhino.rhino(),
|
|
33
33
|
Squid.squid({
|
|
34
34
|
integratorId: 'squid-integrator-id',
|
|
@@ -28,7 +28,7 @@ const providers = [
|
|
|
28
28
|
integratorId: '0xdead',
|
|
29
29
|
userAddress: '0x1111111111111111111111111111111111111111',
|
|
30
30
|
}),
|
|
31
|
-
Relay.relay({
|
|
31
|
+
Relay.relay({ userAddresses: TestFunding.relayQuoteUsers }),
|
|
32
32
|
Rhino.rhino(),
|
|
33
33
|
Squid.squid({
|
|
34
34
|
integratorId: 'squid-integrator-id',
|
|
@@ -27,11 +27,8 @@ const db = TestApp.database()
|
|
|
27
27
|
/** Providers exercised against executable calls on a forked source chain. */
|
|
28
28
|
const executableProviders = [
|
|
29
29
|
{
|
|
30
|
-
create: (
|
|
31
|
-
Relay.relay({
|
|
32
|
-
...(process.env.RELAY_API_KEY ? { apiKey: process.env.RELAY_API_KEY } : {}),
|
|
33
|
-
userAddress: sender,
|
|
34
|
-
}),
|
|
30
|
+
create: (_sender: string) =>
|
|
31
|
+
Relay.relay(process.env.RELAY_API_KEY ? { apiKey: process.env.RELAY_API_KEY } : {}),
|
|
35
32
|
id: 'relay',
|
|
36
33
|
},
|
|
37
34
|
] as const
|
|
@@ -282,7 +279,7 @@ describe('POST /funding/transfers', () => {
|
|
|
282
279
|
providerResponse(),
|
|
283
280
|
) {
|
|
284
281
|
return FundingProvider.from({
|
|
285
|
-
...Relay.relay(
|
|
282
|
+
...Relay.relay(),
|
|
286
283
|
prepareTransfer,
|
|
287
284
|
})
|
|
288
285
|
}
|
|
@@ -110,7 +110,7 @@ export namespace schema {
|
|
|
110
110
|
.check(
|
|
111
111
|
z.regex(/^eip155:\d+$/),
|
|
112
112
|
z.describe('Configured Zone CAIP-2 id.'),
|
|
113
|
-
z.meta({ examples: ['eip155:
|
|
113
|
+
z.meta({ examples: ['eip155:1424310001'] }),
|
|
114
114
|
),
|
|
115
115
|
destinationToken: FundingTransfer.schema.CreateRequest.shape.destinationToken,
|
|
116
116
|
mode: destinationCreateFields.mode,
|
|
@@ -699,7 +699,7 @@ export function billing() {
|
|
|
699
699
|
Auth.policy({
|
|
700
700
|
apiKey: false,
|
|
701
701
|
mpp: false,
|
|
702
|
-
public: { rateLimit: {
|
|
702
|
+
public: { rateLimit: { limit: 600, period: 'minute' } },
|
|
703
703
|
session: false,
|
|
704
704
|
}),
|
|
705
705
|
OpenApi.describeRoute({ hide: true, summary: 'Stripe webhook' }),
|
|
@@ -10,8 +10,8 @@ import * as Value from '../../../internal/Value.js'
|
|
|
10
10
|
import * as Viem from '../../../internal/Viem.js'
|
|
11
11
|
import type { Environment } from '../App.js'
|
|
12
12
|
|
|
13
|
-
const addressRateLimit = {
|
|
14
|
-
const userRateLimit = {
|
|
13
|
+
const addressRateLimit = { limit: 1, period: 'minute' } satisfies RateLimit.Limit
|
|
14
|
+
const userRateLimit = { limit: 3, period: 'minute' } satisfies RateLimit.Limit
|
|
15
15
|
|
|
16
16
|
/** OpenAPI schemas owned by the sandbox faucet. */
|
|
17
17
|
export namespace schema {
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { serve } from '@hono/node-server'
|
|
2
2
|
import * as Accounts from 'accounts/server'
|
|
3
3
|
import { exportJWK, generateKeyPair } from 'jose'
|
|
4
|
+
import { P256 } from 'ox'
|
|
4
5
|
import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts'
|
|
6
|
+
import { Account } from 'viem/tempo'
|
|
7
|
+
import { tempoTestnet } from 'viem/tempo/chains'
|
|
5
8
|
|
|
6
9
|
import * as TestApp from '../../../../test/App.js'
|
|
7
10
|
import * as Me from './me.js'
|
|
@@ -18,6 +21,34 @@ function createApp() {
|
|
|
18
21
|
})
|
|
19
22
|
}
|
|
20
23
|
|
|
24
|
+
describe('SIWE verification', () => {
|
|
25
|
+
test('surfaces configured RPC failures as dependency errors', async () => {
|
|
26
|
+
const account = Account.fromHeadlessWebAuthn(P256.randomPrivateKey(), {
|
|
27
|
+
origin,
|
|
28
|
+
rpId: 'localhost',
|
|
29
|
+
})
|
|
30
|
+
let chainId_seen: number | undefined
|
|
31
|
+
const app = TestApp.create({
|
|
32
|
+
rpc: ({ chainId }) => {
|
|
33
|
+
chainId_seen = chainId
|
|
34
|
+
return { url: 'http://127.0.0.1:1' }
|
|
35
|
+
},
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
const { verifyResponse } = await TestApp.signIn(app, account, {
|
|
39
|
+
chainId: tempoTestnet.id,
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
expect(verifyResponse.status).toBe(502)
|
|
43
|
+
expect(chainId_seen).toMatchInlineSnapshot(`42431`)
|
|
44
|
+
expect(await verifyResponse.json()).toMatchInlineSnapshot(`
|
|
45
|
+
{
|
|
46
|
+
"error": "signature verification unavailable",
|
|
47
|
+
}
|
|
48
|
+
`)
|
|
49
|
+
})
|
|
50
|
+
})
|
|
51
|
+
|
|
21
52
|
describe('GET /me', () => {
|
|
22
53
|
test('returns the signed-in user', async () => {
|
|
23
54
|
const app = createApp()
|
package/src/apps/mcp.ts
CHANGED
|
@@ -138,7 +138,10 @@ export function mcp(options: mcp.Options) {
|
|
|
138
138
|
const app = new Hono<App.Environment>()
|
|
139
139
|
.post(
|
|
140
140
|
'/mcp',
|
|
141
|
-
Auth.policy({
|
|
141
|
+
Auth.policy({
|
|
142
|
+
apiKey: { scopes: [] },
|
|
143
|
+
public: { rateLimit: { limit: 100, period: 'minute' } },
|
|
144
|
+
}),
|
|
142
145
|
OpenApi.describeRoute({
|
|
143
146
|
description:
|
|
144
147
|
"A stateless streamable-HTTP MCP server: send JSON-RPC 2.0 messages as `POST` with `Accept: application/json, text/event-stream`. Tools mirror the API's data domain, plus `docs_*` documentation search. Anonymous requests allow 100 requests per minute; send `Authorization: Bearer <api key>` for protected tools.",
|
package/src/db/Db.ts
CHANGED
|
@@ -36,6 +36,7 @@ import type * as Users from './tables/users.js'
|
|
|
36
36
|
import type * as VerifiedTokenRequests from './tables/verifiedTokenRequests.js'
|
|
37
37
|
import type * as VerifiedTokens from './tables/verifiedTokens.js'
|
|
38
38
|
import type * as WebhookDeliveries from './tables/webhookDeliveries.js'
|
|
39
|
+
import type * as WebhookQueueEvents from './tables/webhookQueueEvents.js'
|
|
39
40
|
import type * as WebhookSubscriptions from './tables/webhookSubscriptions.js'
|
|
40
41
|
|
|
41
42
|
/**
|
|
@@ -82,6 +83,7 @@ export type Database = {
|
|
|
82
83
|
verified_token_requests: VerifiedTokenRequests.Table
|
|
83
84
|
verified_tokens: VerifiedTokens.Table
|
|
84
85
|
webhook_deliveries: WebhookDeliveries.Table
|
|
86
|
+
webhook_queue_events: WebhookQueueEvents.Table
|
|
85
87
|
webhook_subscriptions: WebhookSubscriptions.Table
|
|
86
88
|
}
|
|
87
89
|
|
|
@@ -1281,6 +1283,85 @@ export const migrations: readonly Migration[] = [
|
|
|
1281
1283
|
.on('fundingDepositAddresses')
|
|
1282
1284
|
.columns(['orgId', 'environment', 'address', 'projectId']),
|
|
1283
1285
|
},
|
|
1286
|
+
{
|
|
1287
|
+
name: '0115_webhook_queue_events',
|
|
1288
|
+
up: (db) =>
|
|
1289
|
+
db.schema
|
|
1290
|
+
.createTable('webhookQueueEvents')
|
|
1291
|
+
.ifNotExists()
|
|
1292
|
+
.addColumn('subscriptionId', 'text', (c) =>
|
|
1293
|
+
c.notNull().references('webhookSubscriptions.id').onDelete('cascade'),
|
|
1294
|
+
)
|
|
1295
|
+
.addColumn('eventId', 'text', (c) => c.notNull())
|
|
1296
|
+
.addColumn('envelope', 'jsonb', (c) => c.notNull())
|
|
1297
|
+
.addColumn('createdAt', 'text', (c) => c.notNull())
|
|
1298
|
+
.addColumn('expiresAt', 'text', (c) => c.notNull())
|
|
1299
|
+
.addPrimaryKeyConstraint('webhook_queue_events_pkey', ['subscriptionId', 'eventId']),
|
|
1300
|
+
},
|
|
1301
|
+
{
|
|
1302
|
+
name: '0116_webhook_queue_events_expires_index',
|
|
1303
|
+
up: (db) =>
|
|
1304
|
+
db.schema
|
|
1305
|
+
.createIndex('webhook_queue_events_expires_idx')
|
|
1306
|
+
.ifNotExists()
|
|
1307
|
+
.on('webhookQueueEvents')
|
|
1308
|
+
.column('expiresAt'),
|
|
1309
|
+
},
|
|
1310
|
+
{
|
|
1311
|
+
name: '0117_webhook_queue_events_ledger_columns',
|
|
1312
|
+
up: (db) =>
|
|
1313
|
+
raw(
|
|
1314
|
+
db,
|
|
1315
|
+
sql`ALTER TABLE webhook_queue_events
|
|
1316
|
+
ADD COLUMN IF NOT EXISTS status text NOT NULL DEFAULT 'pending',
|
|
1317
|
+
ADD COLUMN IF NOT EXISTS attempt_count integer NOT NULL DEFAULT 0,
|
|
1318
|
+
ADD COLUMN IF NOT EXISTS attempting_at text,
|
|
1319
|
+
ADD COLUMN IF NOT EXISTS next_attempt_at text`,
|
|
1320
|
+
),
|
|
1321
|
+
},
|
|
1322
|
+
{
|
|
1323
|
+
name: '0118_webhook_queue_events_status_check',
|
|
1324
|
+
up: (db) =>
|
|
1325
|
+
raw(
|
|
1326
|
+
db,
|
|
1327
|
+
sql`ALTER TABLE webhook_queue_events DROP CONSTRAINT IF EXISTS webhook_queue_events_status_check, ADD CONSTRAINT webhook_queue_events_status_check CHECK (status IN ('failed', 'pending', 'skipped', 'succeeded'))`,
|
|
1328
|
+
),
|
|
1329
|
+
},
|
|
1330
|
+
{
|
|
1331
|
+
name: '0119_webhook_queue_events_pending_index',
|
|
1332
|
+
up: (db) =>
|
|
1333
|
+
// NULLS FIRST matches the due-sweep order (null = due immediately);
|
|
1334
|
+
// INCLUDE lets the pending gauge take min(created_at) index-only.
|
|
1335
|
+
raw(
|
|
1336
|
+
db,
|
|
1337
|
+
sql`CREATE INDEX IF NOT EXISTS webhook_queue_events_pending_idx ON webhook_queue_events (next_attempt_at NULLS FIRST) INCLUDE (created_at) WHERE status = 'pending'`,
|
|
1338
|
+
),
|
|
1339
|
+
},
|
|
1340
|
+
{
|
|
1341
|
+
name: '0120_webhook_queue_events_pending_index_drop',
|
|
1342
|
+
up: (db) => raw(db, sql`DROP INDEX IF EXISTS webhook_queue_events_pending_idx`),
|
|
1343
|
+
},
|
|
1344
|
+
{
|
|
1345
|
+
name: '0121_webhook_queue_events_pending_index_expiry',
|
|
1346
|
+
up: (db) =>
|
|
1347
|
+
// Recreated with expires_at so claimable and gauge queries, which all
|
|
1348
|
+
// filter expiry at query time, stay index-only.
|
|
1349
|
+
raw(
|
|
1350
|
+
db,
|
|
1351
|
+
sql`CREATE INDEX IF NOT EXISTS webhook_queue_events_pending_idx ON webhook_queue_events (next_attempt_at NULLS FIRST) INCLUDE (created_at, expires_at) WHERE status = 'pending'`,
|
|
1352
|
+
),
|
|
1353
|
+
},
|
|
1354
|
+
{
|
|
1355
|
+
name: '0122_webhook_queue_events_expired_pending_cleanup',
|
|
1356
|
+
up: (db) =>
|
|
1357
|
+
// Pre-ledger rows already past their staging TTL were dead under the
|
|
1358
|
+
// old read path; pending rows no longer expire, so drop them once
|
|
1359
|
+
// instead of resurrecting them as eternal obligations.
|
|
1360
|
+
raw(
|
|
1361
|
+
db,
|
|
1362
|
+
sql`DELETE FROM webhook_queue_events WHERE status = 'pending' AND attempting_at IS NULL AND expires_at <= to_char(now() AT TIME ZONE 'utc', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')`,
|
|
1363
|
+
),
|
|
1364
|
+
},
|
|
1284
1365
|
]
|
|
1285
1366
|
|
|
1286
1367
|
/**
|
package/src/db/Schema.ts
CHANGED
|
@@ -874,3 +874,35 @@ export const WebhookDelivery = z.object({
|
|
|
874
874
|
|
|
875
875
|
/** Columns of the `webhook_deliveries` table. */
|
|
876
876
|
export type WebhookDelivery = z.output<typeof WebhookDelivery>
|
|
877
|
+
|
|
878
|
+
/** A durable webhook envelope staged before its compact Queue reference is admitted. */
|
|
879
|
+
export const WebhookQueueEvent = z.object({
|
|
880
|
+
attemptCount: z.number().check(z.describe('Delivery attempts claimed so far.')),
|
|
881
|
+
attemptingAt: z
|
|
882
|
+
.nullable(z.string())
|
|
883
|
+
.check(z.describe('ISO timestamp of the current claim, or null when unclaimed.')),
|
|
884
|
+
createdAt: z.string().check(z.describe('ISO timestamp when the envelope was staged.')),
|
|
885
|
+
envelope: z
|
|
886
|
+
.custom<Webhooks.Envelope>()
|
|
887
|
+
.check(z.describe('The full webhook envelope retained outside Cloudflare Queues.')),
|
|
888
|
+
eventId: z.string().check(z.describe('Idempotent event id (`evt_...`).')),
|
|
889
|
+
expiresAt: z
|
|
890
|
+
.string()
|
|
891
|
+
.check(
|
|
892
|
+
z.describe(
|
|
893
|
+
'ISO dedupe retention deadline, restarted at terminal transitions; inert while pending.',
|
|
894
|
+
),
|
|
895
|
+
),
|
|
896
|
+
nextAttemptAt: z
|
|
897
|
+
.nullable(z.string())
|
|
898
|
+
.check(z.describe('ISO retry due time, or null when due immediately.')),
|
|
899
|
+
status: z
|
|
900
|
+
.enum(['failed', 'pending', 'skipped', 'succeeded'])
|
|
901
|
+
.check(z.describe('Delivery-obligation lifecycle status.')),
|
|
902
|
+
subscriptionId: z
|
|
903
|
+
.string()
|
|
904
|
+
.check(z.describe('Owning subscription id (`wh_...`); FK with `ON DELETE CASCADE`.')),
|
|
905
|
+
})
|
|
906
|
+
|
|
907
|
+
/** Columns of the `webhook_queue_events` table. */
|
|
908
|
+
export type WebhookQueueEvent = z.output<typeof WebhookQueueEvent>
|