diff --git a/drizzle/0013_api_key_hash_index.sql b/drizzle/0013_api_key_hash_index.sql new file mode 100644 index 0000000..969a107 --- /dev/null +++ b/drizzle/0013_api_key_hash_index.sql @@ -0,0 +1,6 @@ +-- #41 Unique index on publishers.api_key_hash so every authenticated request +-- uses an index scan instead of a sequential scan. +-- CONCURRENTLY lets the index build without locking the table, so the +-- migration is safe to apply on a live database. +CREATE UNIQUE INDEX CONCURRENTLY IF NOT EXISTS "idx_publishers_api_key_hash" + ON "publishers" ("api_key_hash"); diff --git a/package-lock.json b/package-lock.json index 9f21b41..c9e88e6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1125,9 +1125,6 @@ "cpu": [ "arm" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1144,9 +1141,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1163,9 +1157,6 @@ "cpu": [ "ppc64" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1182,9 +1173,6 @@ "cpu": [ "riscv64" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1201,9 +1189,6 @@ "cpu": [ "s390x" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1220,9 +1205,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1239,9 +1221,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1258,9 +1237,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1277,9 +1253,6 @@ "cpu": [ "arm" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1302,9 +1275,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1327,9 +1297,6 @@ "cpu": [ "ppc64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1352,9 +1319,6 @@ "cpu": [ "riscv64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1377,9 +1341,6 @@ "cpu": [ "s390x" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1402,9 +1363,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1427,9 +1385,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1452,9 +1407,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1693,6 +1645,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.7.1.tgz", "integrity": "sha512-QAqIj32AtK6+pEVNG7EOVxHdE06RP+FM5qpiEJ4RtDcFIqKUZHYhl7/7UY5efhwmwNAg7j8QbJVBLxMerc0+gw==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, @@ -2582,6 +2535,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.7.1.tgz", "integrity": "sha512-NAYIlsF8MPUsKqJMiDQJTMPOmlbawC1Iz/omMLygZ1C9am8fTKYjTaI+OZM+WTY3t3Glo0wnOg/6/pac6RGPPw==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/resources": "2.7.1", @@ -2758,6 +2712,7 @@ "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.6.1.tgz", "integrity": "sha512-/KCsg3xSlR+nCK8/8ZYSknYxvXHwubJrU82F3Lm1Fp6789VQ0/3RJKfsmRXjqfaTA++23CvC3hqmqe/2GEt6Kw==", "license": "MIT", + "peer": true, "dependencies": { "cluster-key-slot": "1.1.2", "generic-pool": "3.9.0", @@ -2913,9 +2868,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2933,9 +2885,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2953,9 +2902,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2973,9 +2919,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2993,9 +2936,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3013,9 +2953,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -4039,6 +3976,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.19.0.tgz", "integrity": "sha512-oJlA3XiRm7Cyk6qFD2Jn8ak9B7jSy0qy00ADO3+8dpT0LSjFihQYv4C02LFCSYJV3Q37xYwwRy5m+IpIiUzqWw==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5035,6 +4973,7 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", + "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -5910,9 +5849,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -5934,9 +5870,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -5958,9 +5891,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -5982,9 +5912,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -6445,6 +6372,7 @@ "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -6532,6 +6460,7 @@ "resolved": "https://registry.npmjs.org/postgres/-/postgres-3.4.9.tgz", "integrity": "sha512-GD3qdB0x1z9xgFI6cdRD6xu2Sp2WCOEoe3mtnyB5Ee0XrrL5Pe+e4CCnJrRMnL1zYtRDZmQQVbvOttLnKDLnaw==", "license": "Unlicense", + "peer": true, "engines": { "node": ">=12" }, @@ -7966,6 +7895,7 @@ "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -8027,6 +7957,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "@noble/curves": "1.9.1", "@noble/hashes": "1.8.0", @@ -8170,10 +8101,9 @@ "cpu": [ "ppc64" ], - "extraneous": true, "dev": true, - "optional": true, "license": "MIT", + "optional": true, "os": [ "aix" ], @@ -8188,10 +8118,9 @@ "cpu": [ "arm" ], - "extraneous": true, "dev": true, - "optional": true, "license": "MIT", + "optional": true, "os": [ "android" ], @@ -8206,10 +8135,9 @@ "cpu": [ "arm64" ], - "extraneous": true, "dev": true, - "optional": true, "license": "MIT", + "optional": true, "os": [ "android" ], @@ -8224,10 +8152,9 @@ "cpu": [ "x64" ], - "extraneous": true, "dev": true, - "optional": true, "license": "MIT", + "optional": true, "os": [ "android" ], @@ -8242,10 +8169,9 @@ "cpu": [ "arm64" ], - "extraneous": true, "dev": true, - "optional": true, "license": "MIT", + "optional": true, "os": [ "darwin" ], @@ -8260,10 +8186,9 @@ "cpu": [ "x64" ], - "extraneous": true, "dev": true, - "optional": true, "license": "MIT", + "optional": true, "os": [ "darwin" ], @@ -8278,10 +8203,9 @@ "cpu": [ "arm64" ], - "extraneous": true, "dev": true, - "optional": true, "license": "MIT", + "optional": true, "os": [ "freebsd" ], @@ -8296,10 +8220,9 @@ "cpu": [ "x64" ], - "extraneous": true, "dev": true, - "optional": true, "license": "MIT", + "optional": true, "os": [ "freebsd" ], @@ -8314,10 +8237,9 @@ "cpu": [ "arm" ], - "extraneous": true, "dev": true, - "optional": true, "license": "MIT", + "optional": true, "os": [ "linux" ], @@ -8332,10 +8254,9 @@ "cpu": [ "arm64" ], - "extraneous": true, "dev": true, - "optional": true, "license": "MIT", + "optional": true, "os": [ "linux" ], @@ -8350,10 +8271,9 @@ "cpu": [ "ia32" ], - "extraneous": true, "dev": true, - "optional": true, "license": "MIT", + "optional": true, "os": [ "linux" ], @@ -8368,10 +8288,9 @@ "cpu": [ "loong64" ], - "extraneous": true, "dev": true, - "optional": true, "license": "MIT", + "optional": true, "os": [ "linux" ], @@ -8386,10 +8305,9 @@ "cpu": [ "mips64el" ], - "extraneous": true, "dev": true, - "optional": true, "license": "MIT", + "optional": true, "os": [ "linux" ], @@ -8404,10 +8322,9 @@ "cpu": [ "ppc64" ], - "extraneous": true, "dev": true, - "optional": true, "license": "MIT", + "optional": true, "os": [ "linux" ], @@ -8422,10 +8339,9 @@ "cpu": [ "riscv64" ], - "extraneous": true, "dev": true, - "optional": true, "license": "MIT", + "optional": true, "os": [ "linux" ], @@ -8440,10 +8356,9 @@ "cpu": [ "s390x" ], - "extraneous": true, "dev": true, - "optional": true, "license": "MIT", + "optional": true, "os": [ "linux" ], @@ -8458,10 +8373,9 @@ "cpu": [ "x64" ], - "extraneous": true, "dev": true, - "optional": true, "license": "MIT", + "optional": true, "os": [ "linux" ], @@ -8476,10 +8390,9 @@ "cpu": [ "arm64" ], - "extraneous": true, "dev": true, - "optional": true, "license": "MIT", + "optional": true, "os": [ "netbsd" ], @@ -8494,10 +8407,9 @@ "cpu": [ "x64" ], - "extraneous": true, "dev": true, - "optional": true, "license": "MIT", + "optional": true, "os": [ "netbsd" ], @@ -8512,10 +8424,9 @@ "cpu": [ "arm64" ], - "extraneous": true, "dev": true, - "optional": true, "license": "MIT", + "optional": true, "os": [ "openbsd" ], @@ -8530,10 +8441,9 @@ "cpu": [ "x64" ], - "extraneous": true, "dev": true, - "optional": true, "license": "MIT", + "optional": true, "os": [ "openbsd" ], @@ -8548,10 +8458,9 @@ "cpu": [ "arm64" ], - "extraneous": true, "dev": true, - "optional": true, "license": "MIT", + "optional": true, "os": [ "openharmony" ], @@ -8566,10 +8475,9 @@ "cpu": [ "x64" ], - "extraneous": true, "dev": true, - "optional": true, "license": "MIT", + "optional": true, "os": [ "sunos" ], @@ -8584,10 +8492,9 @@ "cpu": [ "arm64" ], - "extraneous": true, "dev": true, - "optional": true, "license": "MIT", + "optional": true, "os": [ "win32" ], @@ -8602,10 +8509,9 @@ "cpu": [ "ia32" ], - "extraneous": true, "dev": true, - "optional": true, "license": "MIT", + "optional": true, "os": [ "win32" ], @@ -8620,10 +8526,9 @@ "cpu": [ "x64" ], - "extraneous": true, "dev": true, - "optional": true, "license": "MIT", + "optional": true, "os": [ "win32" ], @@ -8658,54 +8563,13 @@ } } }, - "node_modules/vitest/node_modules/esbuild": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", - "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", - "extraneous": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.28.2", - "@esbuild/android-arm": "0.28.2", - "@esbuild/android-arm64": "0.28.2", - "@esbuild/android-x64": "0.28.2", - "@esbuild/darwin-arm64": "0.28.2", - "@esbuild/darwin-x64": "0.28.2", - "@esbuild/freebsd-arm64": "0.28.2", - "@esbuild/freebsd-x64": "0.28.2", - "@esbuild/linux-arm": "0.28.2", - "@esbuild/linux-arm64": "0.28.2", - "@esbuild/linux-ia32": "0.28.2", - "@esbuild/linux-loong64": "0.28.2", - "@esbuild/linux-mips64el": "0.28.2", - "@esbuild/linux-ppc64": "0.28.2", - "@esbuild/linux-riscv64": "0.28.2", - "@esbuild/linux-s390x": "0.28.2", - "@esbuild/linux-x64": "0.28.2", - "@esbuild/netbsd-arm64": "0.28.2", - "@esbuild/netbsd-x64": "0.28.2", - "@esbuild/openbsd-arm64": "0.28.2", - "@esbuild/openbsd-x64": "0.28.2", - "@esbuild/openharmony-arm64": "0.28.2", - "@esbuild/sunos-x64": "0.28.2", - "@esbuild/win32-arm64": "0.28.2", - "@esbuild/win32-ia32": "0.28.2", - "@esbuild/win32-x64": "0.28.2" - } - }, "node_modules/vitest/node_modules/vite": { "version": "8.3.3", "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.3.tgz", "integrity": "sha512-cTAldKPImjg6c+gk48U19POPn3GCBzZwpdsN8ZMEEcbpes+6/wvfqUd0C2y3qYY4wsj8PwgFwrZ/1jBPVttMSg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "lightningcss": "^1.33.0", "picomatch": "^4.0.7", @@ -8844,6 +8708,7 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", "license": "MIT", + "peer": true, "engines": { "node": ">=10.0.0" }, @@ -8922,6 +8787,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.5.tgz", "integrity": "sha512-v5l/aFXZQeai4awLbOpSoHecE9UiMrnfx75tEXLjNonXVARxQ5mOeipTjROUchszUNCqnE+hqAMujRsRHsut2Q==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/src/db/schema.ts b/src/db/schema.ts index 775e87f..c08f6db 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -7,6 +7,7 @@ import { timestamp, pgEnum, index, + uniqueIndex, jsonb, } from "drizzle-orm/pg-core"; import { createId } from "@paralleldrive/cuid2"; @@ -41,28 +42,37 @@ export const thumbnailStatusEnum = pgEnum("thumbnail_status_enum", [ ]); // Publishers — humans or AI agents that publish resources -export const publishers = pgTable("publishers", { - id: text("id") - .primaryKey() - .$defaultFn(() => createId()), - name: text("name").notNull(), - email: text("email").notNull().unique(), - walletAddress: text("wallet_address").notNull(), - apiKeyHash: text("api_key_hash").notNull(), - // Per-publisher rate limit in requests per minute. Null means the global - // default applies. - rateLimitRpm: integer("rate_limit_rpm"), - // Webhook delivery configuration. Nullable so publishers can opt in later. - webhookUrl: text("webhook_url"), - webhookSecret: text("webhook_secret"), - // Subscribed WebhookEvent values; null/empty means all events. - webhookEvents: text("webhook_events").array(), - webhookEnabled: boolean("webhook_enabled").notNull().default(false), - // Set when the publisher rotates their API key. Any key whose hash matches - // a row with a non-null api_key_revoked_at is rejected by apiKeyAuth (#40). - apiKeyRevokedAt: timestamp("api_key_revoked_at"), - createdAt: timestamp("created_at").defaultNow().notNull(), -}); +export const publishers = pgTable( + "publishers", + { + id: text("id") + .primaryKey() + .$defaultFn(() => createId()), + name: text("name").notNull(), + email: text("email").notNull().unique(), + walletAddress: text("wallet_address").notNull(), + apiKeyHash: text("api_key_hash").notNull(), + // Per-publisher rate limit in requests per minute. Null means the global + // default applies. + rateLimitRpm: integer("rate_limit_rpm"), + // Webhook delivery configuration. Nullable so publishers can opt in later. + webhookUrl: text("webhook_url"), + webhookSecret: text("webhook_secret"), + // Subscribed WebhookEvent values; null/empty means all events. + webhookEvents: text("webhook_events").array(), + webhookEnabled: boolean("webhook_enabled").notNull().default(false), + // Set when the publisher rotates their API key. Any key whose hash matches + // a row with a non-null api_key_revoked_at is rejected by apiKeyAuth (#40). + apiKeyRevokedAt: timestamp("api_key_revoked_at"), + createdAt: timestamp("created_at").defaultNow().notNull(), + }, + (table) => ({ + // Every authenticated request looks up a publisher by api_key_hash. + // The unique index makes that lookup O(log n) instead of a sequential + // scan, and also enforces hash uniqueness at the DB level (#41). + apiKeyHashIdx: uniqueIndex("idx_publishers_api_key_hash").on(table.apiKeyHash), + }), +); // Resources — digital assets published on the marketplace export const resources = pgTable(