Feat/project#91
Conversation
…race, scoped clearClients
…eAITokenUsageDataSchema
Greptile SummaryThis PR introduces full multi-tenancy by adding a
Confidence Score: 3/5Not safe to merge yet — the dashboard onboarding form will be completely broken on deploy, and the SDK's metadata query will return errors for all existing consumers. The server now enforces a required
Important Files Changed
|
| projectId: uuid("project_id") | ||
| .references(() => projectsTable.id) | ||
| .notNull(), | ||
| eventId: uuid("event_id").notNull(), | ||
| idempotencyKey: text("idempotency_key").notNull().unique(), |
There was a problem hiding this comment.
idempotencyKey stays globally unique even though basic usage events now belong to projects. If two projects send the same idempotency key, the second project's valid event will hit a duplicate-key error and fail ingestion. The unique constraint should include projectId so idempotency is isolated per project.
| projectId: uuid("project_id") | ||
| .references(() => projectsTable.id) | ||
| .notNull(), | ||
| eventId: uuid("event_id").notNull(), | ||
| idempotencyKey: text("idempotency_key").notNull().unique(), |
There was a problem hiding this comment.
| "div", | ||
| "tag", | ||
| "expr", | ||
| "inputTokens", | ||
| "outputTokens", | ||
| "inputCacheTokens", | ||
| "outputCacheTokens", | ||
| "inputtokens", |
There was a problem hiding this comment.
Validation lowercases function names before checking ALLOWED_FUNCTIONS, and this change adds lowercase token placeholder names. That means inputtokens() now passes validation, but resolveTokenPlaceholders only replaces exact camelCase forms like inputTokens(), so evaluation later sees an unknown function and the event fails. Either reject the lowercase spelling or normalize replacements case-insensitively.
Artifacts
Repro: focused Bun harness for lowercase token placeholder validation and evaluation
- Contains supporting evidence from the run (text/typescript; charset=utf-8).
Repro: Bun harness output showing validation success, unresolved replacement, and evaluation error
- Keeps the command output available without making the summary code-heavy.
Repro: focused Vitest test for lowercase token placeholder behavior
- Contains supporting evidence from the run (text/typescript; charset=utf-8).
Repro: Vitest startup output captured during attempted focused test run
- Keeps the command output available without making the summary code-heavy.
| return {}; | ||
| } | ||
|
|
||
| const projectId = randomUUID(); | ||
|
|
||
| const existing = await getPostgresDB() | ||
| .select({ id: projectsTable.id }) | ||
| .from(projectsTable) | ||
| .where(eq(projectsTable.name, validated.name)) | ||
| .limit(1); | ||
|
|
||
| if (existing.length > 0) { | ||
| builder.setError(409, { | ||
| type: "ConflictError", | ||
| message: `Project with name '${validated.name}' already exists`, | ||
| }); | ||
| reply.code(409); | ||
| return {}; | ||
| } |
There was a problem hiding this comment.
Make project names unique atomically
The duplicate-name check runs outside the transaction, and the schema does not add a unique constraint on projects.name. Two concurrent onboarding requests with the same name can both pass this check and create duplicate projects with separate dashboard keys. Add a unique index on project name and handle the insert conflict in the transaction.
| return { error: "Invalid mode query parameter" }; | ||
| } | ||
|
|
||
| if (!projectId) { | ||
| builder.setError(400, { | ||
| type: "ValidationError", | ||
| message: "Missing 'projectId' query parameter.", | ||
| }); | ||
| reply.code(400); | ||
| return { error: "Missing projectId query parameter" }; | ||
| } | ||
|
|
||
| if ( | ||
| !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test( | ||
| projectId | ||
| ) | ||
| ) { |
There was a problem hiding this comment.
The payment webhook now rejects every request without a projectId query parameter before processing it. Existing Dodo webhooks created before this change use only mode, so after upgrading they will return 400 and leave successful payments unprocessed until every provider webhook URL is migrated.
| .notNull(), | ||
| last_run_at: timestamp("last_run_at", { | ||
| withTimezone: true, | ||
| mode: "string", | ||
| }), | ||
| dodo_live_api_key: text("dodo_live_api_key").notNull(), | ||
| dodo_test_api_key: text("dodo_test_api_key").notNull(), | ||
| dodo_live_product_id: text("dodo_live_product_id").notNull(), | ||
| dodo_test_product_id: text("dodo_test_product_id").notNull(), | ||
| dodo_live_webhook_secret: text("dodo_live_webhook_secret").notNull(), | ||
| dodo_test_webhook_secret: text("dodo_test_webhook_secret").notNull(), | ||
| currency: text("currency").notNull().default("usd"), |
There was a problem hiding this comment.
| if ( | ||
| incomingHash.length !== storedHash.length || | ||
| !timingSafeEqual(Buffer.from(incomingHash), Buffer.from(storedHash)) | ||
| ) { |
There was a problem hiding this comment.
The comparison checks string length but does not validate that MASTER_API_KEY_HASH is a 64-character hex digest, and Buffer.from(...) uses UTF-8 rather than hex decoding. A malformed stored hash silently changes the comparison target instead of failing configuration validation.
|
|
||
| export const onboardingSchema = z.object({ | ||
| name: z.string().min(1, "Project name is required").max(255), |
There was a problem hiding this comment.
Dashboard onboarding will fail —
name field not sent
The onboardingSchema now requires a name field, but scrawn.js/dashboard's submitOnboarding neither collects nor forwards it — the request body is built from { dodoLiveApiKey, dodoTestApiKey, ... } with no name. Every onboarding submission will return a Zod validation error and the onboarding UI will be broken until the dashboard is updated to collect and send this field.
No description provided.