Files
Sebastien Melki a4d9b0a5fa feat(auth): user-facing API key management (create / list / revoke) (#3125)
* feat(auth): user-facing API key management (create / list / revoke)

Adds full-stack API key management so authenticated users can create,
list, and revoke their own API keys from the Settings UI.

Backend:
- Convex `userApiKeys` table with SHA-256 key hash storage
- Mutations: createApiKey, listApiKeys, revokeApiKey
- Internal query validateKeyByHash + touchKeyLastUsed for gateway
- HTTP endpoints: /api/api-keys (CRUD) + /api/internal-validate-api-key
- Gateway middleware validates user-owned keys via Convex + Redis cache

Frontend:
- New "API Keys" tab in UnifiedSettings (visible when signed in)
- Create form with copy-on-creation banner (key shown once)
- List with prefix display, timestamps, and revoke action
- Client-side key generation + hashing (plaintext never sent to DB)

Closes #3116

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(api-keys): address PR review — cache invalidation, prefix validation, revoked-key guard

- Invalidate Redis cache on key revocation so gateway rejects revoked keys
  immediately instead of waiting for 5-min TTL expiry (P1)
- Enforce `wm_` prefix format with regex instead of loose length check (P2)
- Skip `touchKeyLastUsed` for revoked keys to preserve clean audit trail (P2)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(api-keys): address consolidated PR review (P0–P3)

P0: gate createApiKey on pro entitlement (tier >= 1); isCallerPremium
now verifies key-owner tier instead of treating existence as premium.

P1: wire wm_ user keys into the domain gateway auth path with async
Convex-backed validation; user keys go through entitlement checks
(only admin keys bypass). Lower cache TTL 300s → 60s and await
revocation cache-bust instead of fire-and-forget.

P2: remove dead HTTP create/list/revoke path from convex/http.ts;
switch to cachedFetchJson (stampede protection, env-prefixed keys,
standard NEG_SENTINEL); add tenancy check on cache-invalidation
endpoint via new /api/internal-get-key-owner route; add 22 Convex
tests covering tier gate, per-user limit, duplicate hash, ownership
revoke guard, getKeyOwner, and touchKeyLastUsed debounce.

P3: tighten keyPrefix regex to exactly 5 hex chars; debounce
touchKeyLastUsed (5 min); surface PRO_REQUIRED in UI.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(api-keys): gate on apiAccess (not tier), wire wm_ keys through edge routes, harden error paths

- Gate API key creation/validation on features.apiAccess instead of tier >= 1.
  Pro (tier 1, apiAccess=false) can no longer mint keys — only API_STARTER+.
- Wire wm_ user keys through standalone edge routes (shipping/route-intelligence,
  shipping/webhooks) that were short-circuiting on validateApiKey before async
  Convex validation could run.
- Restore fail-soft behavior in validateUserApiKey: transient Convex/network
  errors degrade to unauthorized instead of bubbling a 500.
- Fail-closed on cache invalidation endpoint: ownership check errors now return
  503 instead of silently proceeding (surfaces Convex outages in logs).
- Tests updated: positive paths use api_starter (apiAccess=true), new test locks
  Pro-without-API-access rejection. 23 tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(webhooks): remove wm_ user key fallback from shipping webhooks

Webhook ownership is keyed to SHA-256(apiKey) via callerFingerprint(),
not to the user. With user-owned keys (up to 5 per user), this causes
cross-key blindness (webhooks invisible when calling with a different
key) and revoke-orphaning (revoking the creating key makes the webhook
permanently unmanageable). User keys remain supported on the read-only
route-intelligence endpoint. Webhook ownership migration to userId will
follow in a separate PR.

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Elie Habib <elie.habib@gmail.com>
2026-04-17 07:20:39 +04:00

176 lines
6.0 KiB
TypeScript

/* eslint-disable */
/**
* Generated `api` utility.
*
* THIS CODE IS AUTOMATICALLY GENERATED.
*
* To regenerate, run `npx convex dev`.
* @module
*/
import type * as alertRules from "../alertRules.js";
import type * as apiKeys from "../apiKeys.js";
import type * as config_productCatalog from "../config/productCatalog.js";
import type * as constants from "../constants.js";
import type * as contactMessages from "../contactMessages.js";
import type * as crons from "../crons.js";
import type * as emailSuppressions from "../emailSuppressions.js";
import type * as entitlements from "../entitlements.js";
import type * as http from "../http.js";
import type * as lib_auth from "../lib/auth.js";
import type * as lib_dodo from "../lib/dodo.js";
import type * as lib_entitlements from "../lib/entitlements.js";
import type * as lib_env from "../lib/env.js";
import type * as lib_identitySigning from "../lib/identitySigning.js";
import type * as notificationChannels from "../notificationChannels.js";
import type * as payments_billing from "../payments/billing.js";
import type * as payments_cacheActions from "../payments/cacheActions.js";
import type * as payments_checkout from "../payments/checkout.js";
import type * as payments_seedProductPlans from "../payments/seedProductPlans.js";
import type * as payments_subscriptionEmails from "../payments/subscriptionEmails.js";
import type * as payments_subscriptionHelpers from "../payments/subscriptionHelpers.js";
import type * as payments_webhookHandlers from "../payments/webhookHandlers.js";
import type * as payments_webhookMutations from "../payments/webhookMutations.js";
import type * as registerInterest from "../registerInterest.js";
import type * as resendWebhookHandler from "../resendWebhookHandler.js";
import type * as telegramPairingTokens from "../telegramPairingTokens.js";
import type * as userPreferences from "../userPreferences.js";
import type {
ApiFromModules,
FilterApi,
FunctionReference,
} from "convex/server";
declare const fullApi: ApiFromModules<{
alertRules: typeof alertRules;
apiKeys: typeof apiKeys;
"config/productCatalog": typeof config_productCatalog;
constants: typeof constants;
contactMessages: typeof contactMessages;
crons: typeof crons;
emailSuppressions: typeof emailSuppressions;
entitlements: typeof entitlements;
http: typeof http;
"lib/auth": typeof lib_auth;
"lib/dodo": typeof lib_dodo;
"lib/entitlements": typeof lib_entitlements;
"lib/env": typeof lib_env;
"lib/identitySigning": typeof lib_identitySigning;
notificationChannels: typeof notificationChannels;
"payments/billing": typeof payments_billing;
"payments/cacheActions": typeof payments_cacheActions;
"payments/checkout": typeof payments_checkout;
"payments/seedProductPlans": typeof payments_seedProductPlans;
"payments/subscriptionEmails": typeof payments_subscriptionEmails;
"payments/subscriptionHelpers": typeof payments_subscriptionHelpers;
"payments/webhookHandlers": typeof payments_webhookHandlers;
"payments/webhookMutations": typeof payments_webhookMutations;
registerInterest: typeof registerInterest;
resendWebhookHandler: typeof resendWebhookHandler;
telegramPairingTokens: typeof telegramPairingTokens;
userPreferences: typeof userPreferences;
}>;
/**
* A utility for referencing Convex functions in your app's public API.
*
* Usage:
* ```js
* const myFunctionReference = api.myModule.myFunction;
* ```
*/
export declare const api: FilterApi<
typeof fullApi,
FunctionReference<any, "public">
>;
/**
* A utility for referencing Convex functions in your app's internal API.
*
* Usage:
* ```js
* const myFunctionReference = internal.myModule.myFunction;
* ```
*/
export declare const internal: FilterApi<
typeof fullApi,
FunctionReference<any, "internal">
>;
export declare const components: {
dodopayments: {
lib: {
checkout: FunctionReference<
"action",
"internal",
{
apiKey: string;
environment: "test_mode" | "live_mode";
payload: {
allowed_payment_method_types?: Array<string>;
billing_address?: {
city?: string;
country: string;
state?: string;
street?: string;
zipcode?: string;
};
billing_currency?: string;
confirm?: boolean;
customer?:
| { email: string; name?: string; phone_number?: string }
| { customer_id: string };
customization?: {
force_language?: string;
show_on_demand_tag?: boolean;
show_order_details?: boolean;
theme?: string;
};
discount_code?: string;
feature_flags?: {
allow_currency_selection?: boolean;
allow_discount_code?: boolean;
allow_phone_number_collection?: boolean;
allow_tax_id?: boolean;
always_create_new_customer?: boolean;
};
force_3ds?: boolean;
metadata?: Record<string, string>;
product_cart: Array<{
addons?: Array<{ addon_id: string; quantity: number }>;
amount?: number;
product_id: string;
quantity: number;
}>;
return_url?: string;
show_saved_payment_methods?: boolean;
subscription_data?: {
on_demand?: {
adaptive_currency_fees_inclusive?: boolean;
mandate_only: boolean;
product_currency?: string;
product_description?: string;
product_price?: number;
};
trial_period_days?: number;
};
};
},
{ checkout_url: string }
>;
customerPortal: FunctionReference<
"action",
"internal",
{
apiKey: string;
dodoCustomerId: string;
environment: "test_mode" | "live_mode";
send_email?: boolean;
},
{ portal_url: string }
>;
};
};
};