Hermes 284313f908
Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Initial import of AlgaPSA codebase from PSA server
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz

Source: /opt/alga-psa on psa.joliet.tech
2026-06-22 16:12:17 -05:00

315 lines
17 KiB
JSON

[
{
"id": "F001",
"description": "Add a required `namespace: string` first parameter to TokenBucketRateLimiter.tryConsume, getState, getBucketKey, and getBucketConfig; bucket key becomes `${prefix}${namespace}:${tenantId}[:${userId}]`.",
"implemented": true,
"prdRefs": ["Data / API / Integrations"]
},
{
"id": "F002",
"description": "Widen the BucketConfigGetter signature from `(tenantId) => BucketConfig` to `(tenantId, subjectId?) => BucketConfig` so per-API-key and per-webhook overrides can be resolved.",
"implemented": true,
"prdRefs": ["Data / API / Integrations"]
},
{
"id": "F003",
"description": "Change TokenBucketRateLimiter.initialize to accept a `Record<namespace, BucketConfigGetter>` map and route lookups to the namespace-specific getter.",
"implemented": true,
"prdRefs": ["Data / API / Integrations"]
},
{
"id": "F004",
"description": "Update server/src/lib/initializeApp.ts to register both the existing email getter (namespace 'email') and a placeholder 'api' getter so the singleton wires up multi-namespace at startup.",
"implemented": true,
"prdRefs": ["Rollout / Migration"]
},
{
"id": "F005",
"description": "Update packages/email/src/TenantEmailService.ts:155 to pass 'email' as the first arg to tryConsume; the existing userId arg moves to second position.",
"implemented": true,
"prdRefs": ["Rollout / Migration"]
},
{
"id": "F006",
"description": "Add an optional `headers?: Record<string,string>` field to the ApiError interface in apiMiddleware.ts and have handleApiError merge them into the NextResponse.json options.",
"implemented": true,
"prdRefs": ["Data / API / Integrations"]
},
{
"id": "F007",
"description": "Add an optional `extraHeaders?: Record<string,string>` parameter to createSuccessResponse and createPaginatedResponse so callers can attach X-RateLimit-* headers to successful responses.",
"implemented": true,
"prdRefs": ["Functional Requirements"]
},
{
"id": "F008",
"description": "Create the api_rate_limit_settings table via a knex .cjs migration (cols: tenant, api_key_id NULL, max_tokens, refill_per_min, timestamps; UNIQUE (tenant, api_key_id)).",
"implemented": true,
"prdRefs": ["Data / API / Integrations"]
},
{
"id": "F009",
"description": "In the same create migration, gate on `pg_extension WHERE extname='citus'` and call create_distributed_table('api_rate_limit_settings', 'tenant', colocate_with => 'tenants') when present. `exports.config = { transaction: false }` so the call doesn't run inside a tx (matches the current repo pattern in 20260429133000_create_asset_facts_table.cjs).",
"implemented": true,
"prdRefs": ["Data / API / Integrations", "Rollout / Migration"]
},
{
"id": "F010",
"description": "Implement a small DAL (apiRateLimitSettingsModel) with getForKey(tenant, apiKeyId), upsertForKey, upsertForTenant, and clearForKey, and a fall-back resolver: (tenant, apiKeyId) → (tenant, NULL) → hard-coded defaults.",
"implemented": true,
"prdRefs": ["Data / API / Integrations"]
},
{
"id": "F011",
"description": "Implement an in-process LRU-cached apiRateLimitConfigGetter (max 1000 entries, 30s TTL) with an invalidateApiRateLimitConfig(tenant, apiKeyId?) helper that clears tenant-wide entries when apiKeyId is omitted.",
"implemented": true,
"prdRefs": ["Non-functional Requirements"]
},
{
"id": "F012",
"description": "Add a shared enforceApiRateLimit(req, context) helper at server/src/lib/api/rateLimit/enforce.ts that resolves namespace 'api' bucket, applies the SSRF-style bypass list, computes header values, and either throws TooManyRequestsError or returns a RateLimitDecision with limit/remaining/resetAt.",
"implemented": true,
"prdRefs": ["Functional Requirements"]
},
{
"id": "F013",
"description": "Honor the RATE_LIMIT_ENFORCE env var (default false until Stage 3): in observation mode, emit a structured WARN log + headers but do not throw on a denial.",
"implemented": true,
"prdRefs": ["Functional Requirements", "Rollout / Migration"]
},
{
"id": "F014",
"description": "Use sentinel subjectId 'nm_store' for the NM Store global-key path inside withApiKeyAuth so its calls share one tenant-scoped bucket instead of being unbucketed.",
"implemented": true,
"prdRefs": ["Security / Permissions"]
},
{
"id": "F015",
"description": "Implement shouldBypassRateLimit(pathname) covering health/version, internal-runner, and mobile-auth prefixes; mirror the existing shouldSkipApiKeyAuth shape.",
"implemented": true,
"prdRefs": ["Functional Requirements"]
},
{
"id": "F016",
"description": "Reuse TooManyRequestsError (apiMiddleware.ts:101) for 429s; do not introduce a parallel RateLimitError class. Populate `details: { retry_after_ms, remaining }` and the `headers` field.",
"implemented": true,
"prdRefs": ["Functional Requirements"]
},
{
"id": "F017",
"description": "Call enforceApiRateLimit from ApiBaseController.authenticate after `apiRequest.context = {...}`; stash decision on apiRequest.context.rateLimit.",
"implemented": true,
"prdRefs": ["Functional Requirements"]
},
{
"id": "F018",
"description": "Call enforceApiRateLimit from withApiKeyAuth (both NM-Store and default branches) and withAuth in apiMiddleware.ts after `req.context = {...}`; stash decision on req.context.rateLimit.",
"implemented": true,
"prdRefs": ["Functional Requirements"]
},
{
"id": "F019",
"description": "Emit X-RateLimit-Limit and X-RateLimit-Remaining headers on successful responses by reading req.context.rateLimit and passing extraHeaders to createSuccessResponse / createPaginatedResponse.",
"implemented": true,
"prdRefs": ["Functional Requirements"]
},
{
"id": "F020",
"description": "Audit /api/v1/* routes that bypass all three auth surfaces (no controller, no withApiKeyAuth, no withAuth) and add enforceApiRateLimit to any that should be protected. Remove the 501-stub /api/v1/rbac/audit/route.ts from this scope (already a stub).",
"implemented": true,
"prdRefs": ["Functional Requirements"]
},
{
"id": "F021",
"description": "Add server actions: getApiRateLimitForKey, setApiRateLimitForKey, setTenantDefaultApiRateLimit, clearApiRateLimitForKey. Each invalidates the LRU cache after writes.",
"implemented": true,
"prdRefs": ["UX / UI Notes"]
},
{
"id": "F022",
"description": "Add a Rate Limit column + inline edit form to AdminApiKeysSetup; show effective per-key limit, override input, and current remaining tokens via TokenBucketRateLimiter.getState.",
"implemented": true,
"prdRefs": ["UX / UI Notes"]
},
{
"id": "F023",
"description": "Extend webhookEventTypeSchema (server/src/lib/api/schemas/webhookSchemas.ts) with 'ticket.comment.added' so create-webhook validation accepts it.",
"implemented": true,
"prdRefs": ["Data / API / Integrations"]
},
{
"id": "F024",
"description": "Create the webhooks table via a .cjs migration with: webhook_id, tenant, name, url, method, event_types text[], custom_headers jsonb, signing_secret_vault_path text NOT NULL, security_type, verify_ssl, retry_config jsonb, rate_limit_per_min, is_active, total/successful/failed_deliveries, last_*_at timestamps, auto_disabled_at, created_by_user_id, created_at, updated_at.",
"implemented": true,
"prdRefs": ["Data / API / Integrations"]
},
{
"id": "F025",
"description": "Create the webhook_deliveries table via the same migration with: delivery_id, tenant, webhook_id, event_id, event_type, request/response capture, status, attempt_number, duration_ms, error_message, next_retry_at, is_test boolean, attempted_at, completed_at; plus indexes on (tenant, webhook_id, attempted_at DESC), (tenant, event_id), and a partial index on status IN ('pending','retrying') with next_retry_at.",
"implemented": true,
"prdRefs": ["Data / API / Integrations"]
},
{
"id": "F026",
"description": "In the same create migration, gate on Citus and call create_distributed_table('webhooks','tenant', colocate_with => 'tenants') + create_distributed_table('webhook_deliveries','tenant', colocate_with => 'tenants') under a `transaction: false` config (matches the current repo pattern).",
"implemented": true,
"prdRefs": ["Data / API / Integrations", "Rollout / Migration"]
},
{
"id": "F027",
"description": "Implement webhookModel DAL with getById(webhookId, tenant), listForEventType(tenant, eventType), insert (with vault-path resolution), update, delete, recordDelivery, updateStats, markAbandoned, plus getSigningSecret(webhookId, tenant) that resolves signing_secret_vault_path via getSecretProviderInstance().",
"implemented": true,
"prdRefs": ["Data / API / Integrations", "Security / Permissions"]
},
{
"id": "F028",
"description": "Replace the mocked WebhookService.performWebhookDelivery (line 950) with a real HTTP call (undici/node:fetch), 10s timeout, capturing status/response headers/truncated body (8KB), and surfacing distinct error types for DNS/connect/TLS errors. Honor verify_ssl.",
"implemented": true,
"prdRefs": ["Functional Requirements"]
},
{
"id": "F029",
"description": "Add an assertSafeWebhookTarget(url) helper that DNS-resolves the hostname and rejects RFC1918 (10/8, 172.16/12, 192.168/16), loopback (127/8, ::1), link-local (169.254/16, fe80::/10), CGNAT (100.64/10), non-http(s) schemes, and http://localhost variants. Allow override via WEBHOOK_SSRF_ALLOW_PRIVATE=true.",
"implemented": true,
"prdRefs": ["Security / Permissions"]
},
{
"id": "F030",
"description": "Implement HMAC-SHA256 signing: signRequest(secret, body, ts) returns 'X-Alga-Signature: t=<ts>,v1=<hex>' over `${ts}.${body}`. Use crypto.createHmac.",
"implemented": true,
"prdRefs": ["Functional Requirements", "Security / Permissions"]
},
{
"id": "F031",
"description": "Replace the broken WebhookService.checkRateLimit (line 1056, queries non-existent table) with TokenBucketRateLimiter.tryConsume('webhook-out', tenant, webhookId). Default config from webhooks.rate_limit_per_min.",
"implemented": true,
"prdRefs": ["Functional Requirements"]
},
{
"id": "F032",
"description": "Implement webhookEventMap.ts with TICKET_INTERNAL_TO_PUBLIC mapping (TICKET_CREATED → ticket.created, TICKET_UPDATED → ticket.updated, TICKET_STATUS_CHANGED → ticket.status_changed, TICKET_ASSIGNED → ticket.assigned, TICKET_CLOSED → ticket.closed, TICKET_COMMENT_ADDED → ticket.comment.added) and publicEventsFor(eventType).",
"implemented": true,
"prdRefs": ["Data / API / Integrations"]
},
{
"id": "F033",
"description": "Implement buildTicketWebhookPayload(internalEvent, knex) in server/src/lib/eventBus/subscribers/webhook/webhookTicketPayload.ts with the curated field set from PRD §Functional Requirements; include short-lived in-memory cache (60s TTL) keyed by ticket_id+tenant to avoid N joins on fan-out.",
"implemented": true,
"prdRefs": ["Functional Requirements", "Non-functional Requirements"]
},
{
"id": "F034",
"description": "Include `previous_status_id` and `previous_status_name` in `ticket.status_changed` payloads, derived from TICKET_STATUS_CHANGED.payload.changes when available.",
"implemented": true,
"prdRefs": ["Open Questions", "Functional Requirements"]
},
{
"id": "F035",
"description": "Implement webhookSubscriber.ts subscribing to TICKET_CREATED, TICKET_UPDATED, TICKET_STATUS_CHANGED, TICKET_CLOSED, TICKET_ASSIGNED, TICKET_COMMENT_ADDED. Look up active webhooks in the same tenant matching the public event type and enqueue a delivery job.",
"implemented": true,
"prdRefs": ["Functional Requirements"]
},
{
"id": "F036",
"description": "Register webhookSubscriber in server/src/lib/eventBus/subscribers/index.ts alongside ticketEmail / projectEmail / etc.; provide registerWebhookSubscriber and unregisterWebhookSubscriber lifecycle hooks.",
"implemented": true,
"prdRefs": ["Functional Requirements"]
},
{
"id": "F037",
"description": "Implement WebhookDeliveryQueue (Redis ZSET singleton mirrored on DelayedEmailQueue) with enqueue(job), poller (zRangeByScore + zRem to claim, in-process semaphore cap of 50 concurrent deliveries), retry re-zAdd with computeBackoff, and graceful shutdown on SIGTERM with a 30s drain.",
"implemented": true,
"prdRefs": ["Data / API / Integrations", "Non-functional Requirements"]
},
{
"id": "F038",
"description": "Initialize WebhookDeliveryQueue.getInstance().initialize(getRedisClient, processFn) in server/src/lib/initializeApp.ts alongside the existing DelayedEmailQueue block.",
"implemented": true,
"prdRefs": ["Rollout / Migration"]
},
{
"id": "F039",
"description": "Implement computeBackoff(attempt) returning the schedule 1m, 5m, 30m, 2h, 12h.",
"implemented": true,
"prdRefs": ["Functional Requirements"]
},
{
"id": "F040",
"description": "Implement maybeAutoDisable(webhook): if all deliveries in the past 24h failed, set webhook.is_active=false and webhook.auto_disabled_at=now(); email the owning user via the existing notification path.",
"implemented": true,
"prdRefs": ["Functional Requirements"]
},
{
"id": "F052",
"description": "Persist `event_filter` on webhook rows (migration + DAL mapping) so `event_filter.entity_ids` survives create/update/read and the entity-id subscriber filter has durable input data.",
"implemented": true,
"prdRefs": ["Open Questions", "Data / API / Integrations"]
},
{
"id": "F041",
"description": "Implement webhook subscription filtering by entity_ids — when a webhook row has filter.entity_ids set, drop events whose entity_id is not in the set. Skip generic conditions for v1.",
"implemented": true,
"prdRefs": ["Open Questions", "Functional Requirements"]
},
{
"id": "F042",
"description": "Implement controller endpoint rotateWebhookSecret (currently a TODO at ApiWebhookController.ts:1202): generate crypto.randomBytes(32).toString('base64url'), store via secret provider, replace signing_secret_vault_path, return plaintext once.",
"implemented": true,
"prdRefs": ["Security / Permissions"]
},
{
"id": "F043",
"description": "Implement controller endpoint verifyWebhookSignature (TODO at line 1175): take signature header + body + secret-vault-path, recompute, return match/no-match.",
"implemented": true,
"prdRefs": ["Security / Permissions"]
},
{
"id": "F044",
"description": "Implement controller endpoints getDeliveryDetails (line 575), getWebhookHealth (line 778), getWebhookSubscriptions (line 878 — return webhook.event_types), and listAvailableEvents (line 1298 — return the public enum).",
"implemented": true,
"prdRefs": ["UX / UI Notes", "Functional Requirements"]
},
{
"id": "F045",
"description": "Remove the route handlers for deferred TODO endpoints rather than leaving 501s in OpenAPI: testPayloadTransformation, testEventFilter, validateWebhookConfiguration, getSystemWebhookHealth, createWebhookSubscription, bulkWebhookOperation, searchWebhooks, exportWebhooks, triggerWebhookEvent.",
"implemented": true,
"prdRefs": ["Non-goals", "UX / UI Notes"]
},
{
"id": "F046",
"description": "Implement POST /api/v1/webhooks/[id]/test: send a synthetic webhook.test payload to the configured URL with the live signing secret; record in webhook_deliveries with is_test=true; skip rate-limit token consumption; apply assertSafeWebhookTarget.",
"implemented": true,
"prdRefs": ["Functional Requirements", "UX / UI Notes"]
},
{
"id": "F047",
"description": "Build the Webhooks settings UI: list view (status, last delivery, success rate), create/edit form (name, URL, event types multi-select, custom headers, retry config), delivery history (paginated with response status/body/retry button), reveal-on-create + rotate secret, pause/resume/delete.",
"implemented": true,
"prdRefs": ["UX / UI Notes"]
},
{
"id": "F048",
"description": "Add a 15-minute scheduled job (initializeScheduledJobs) that deletes webhook_deliveries rows older than 30 days in batches of 10000.",
"implemented": true,
"prdRefs": ["Rollout / Migration"]
},
{
"id": "F049",
"description": "Emit rate-limiter metrics: api_rate_limit_consumed_total{tenant,api_key_id,outcome}, api_rate_limit_remaining{tenant,api_key_id}, api_rate_limit_redis_unavailable_total. Use whatever metric backend the codebase exposes; logger WARN counts are the fallback.",
"implemented": true,
"prdRefs": ["Observability"]
},
{
"id": "F050",
"description": "Emit webhook metrics: webhook_deliveries_total{tenant,webhook_id,outcome}, webhook_delivery_duration_ms histogram, webhook_queue_depth gauge (zCard), webhook_auto_disabled_total{tenant,webhook_id}.",
"implemented": true,
"prdRefs": ["Observability"]
},
{
"id": "F051",
"description": "Public-facing API documentation: 429 semantics + Retry-After + X-RateLimit-* headers, default limits, observation-vs-enforce mode; webhook event payloads with example JSON per event; HMAC verification recipes in Node and Python; idempotency/ordering guarantees; retry behavior table.",
"implemented": true,
"prdRefs": ["Rollout / Migration"]
}
]