Skip to content

Task 25: Phase A (Drop 1 Beta / Issuance) — Pending Task Checklist

Priority: Working checklist — go item by item Created: 2026-07-14 Purpose: Single, code-verified list of what's actually left to finish Phase A (Issuance / Drop 1 Beta). Cross-checked against the live codebase, not just against 18-master-execution-plan.md, 10-gap-analysis-v3-requirements.md, M9-01b-logging-tracking-reconciliation.md, and ../../Curo-REC-Technical-Architecture-Document.md (v1.1, 18 Apr 2026).

Status Legend

  • NOT STARTED — verified absent from codebase
  • 🟨 PARTIAL — some layer exists (DB/backend/utility), remainder pending
  • ⚠️ NEEDS DECISION — scope conflict between source docs, resolve before estimating
  • 🔺 PRIORITY — worth fixing regardless of Beta timeline (live risk)

Category 1: Essential Beta Features

  • CSV/Excel export of credentials + status (GAP-03) — DONE 2026-07-14
  • Backend: GET /issuer/credentials/export in credential.controller.ts (respects the same search/status/credentialType filters as the list endpoint; UTF-8 BOM so Excel opens it cleanly; org-scoped via JWT)
  • Frontend: "Export CSV" button on credentials/index.tsx header (downloads credentials-YYYY-MM-DD.csv, carries the current search filter); exportCredentials() added to credential.service.ts

  • Candidate-centric search & view (GAP-04) — DONE 2026-07-14

  • Backend: new CandidateController/CandidateServiceGET /issuer/candidates (search by first/last/full name or email, paginated, org-scoped, per-candidate offer counts) + GET /issuer/candidates/:id (candidate record with every credential offer sent)
  • Frontend: /candidates search page (debounced search, pagination, click-through) + /candidates/[id] record view (profile, offer stats, full offer history with status chips linking to credential detail); "Candidates" nav item added for ORG_ADMIN / COMPANY_ADMIN / ISSUER roles

  • Organisation branding UI (GAP-02) — DONE 2026-07-14

  • Backend (was actually missing, not "done" — entity existed but wasn't registered anywhere): new BrandingController (GET/PUT /admin/branding, org-scoped, ORG_ADMIN+) + BrandingService; logo/hero URLs, colors, T&Cs, Privacy Policy stored in the existing theme JSONB; update audited as org.branding_updated
  • Frontend: new "Branding" tab in admin/settings.tsx (components/branding/BrandingSettings.tsx) — logo URL with live preview, primary/secondary color pickers, T&Cs + Privacy Policy editors, save with validation feedback
  • Note: logo_s3_key/hero_image_s3_key left for a future direct-upload integration — no S3 infra exists in the codebase yet, so Beta uses URL-based images

  • Enhanced dashboard stats (GAP-15) — DONE (Option A / Drop 1 scope) 2026-07-17

  • Scope resolved: Option A (snapshot stats) chosen; full analytics (rollup table, time-series charts, exports) stays Drop 2 per Task 24 Phase C; auth failure rates explicitly deferred to Drop 4 (Auth0-hosted login — failures never reach our backend)
  • Backend: GET /superadmin/dashboard/stats (PlatformStatsService) — org counts by lifecycle status (whole platform, not per page), credential totals + per-org breakdown, login activity from audit trail, active sessions, and live integration health (CIH ping + latency, Auth0 JWKS ping + latency, SES config check; 4s timeouts so a slow dependency can't hang the dashboard)
  • Frontend: SuperAdmin dashboard — credential/activity stat cards, Integration Health panel (UP/DOWN/NOT SET chips with latency), Credentials-by-Organisation table
  • Bonus fix: the org-status cards previously counted only the visible page (wrong beyond 10 orgs) — now fed by the platform-wide endpoint
  • Verified live: real counts + all three integrations reporting UP with latencies

Category 2: Automation & Integration

  • Registrar API automation for org creation (GAP-09) — NOT NEEDED, client-confirmed
  • Client has explicitly confirmed the intended flow is: register the org manually on the Velocity Registrar dashboard, then upload the resulting DID + keys.json into the Curo-REC site — per the requirements PDF.
  • This is exactly what's built today (organization.service.ts:165 requires a manually-supplied organizationDid; VelocityRegistrarService.registerOrganization() exists but is intentionally not wired into auto-create).
  • Action: remove from the Phase A backlog entirely — current behavior is the target behavior, not a stopgap. No effort needed.
  • (Note: this reverses the earlier reading of PDF p.3 "...should be executed via a web form on the platform by the full Beta release" — client's direct confirmation supersedes that interpretation.)

  • Compliance/audit report UI (GAP-16 / Task 24 Phase B) — DONE 2026-07-14

  • Backend: new AuditLogControllerGET /admin/audit-logs (filters: action, orgId, actorUserId, resourceType, from/to, pagination; joins actor email + org name) + GET /admin/audit-logs/export (CSV). PLATFORM_ADMIN/ADMIN/SYSTEM_AUDITOR see all orgs; ORG_ADMIN hard-scoped to own org
  • Frontend: /superadmin/audit-logs page — event/date-range filters, readable event chips, pagination, CSV export; "Audit Logs" nav item for PLATFORM_ADMIN
  • New action org.branding_updated added to the label map alongside the original 8

  • Reminder/nudge email system (GAP-06) — DONE 2026-07-14

  • Found: NotificationService.sendCredentialReminder() + CREDENTIAL_REMINDER template already existed but had zero callers — the gap was the automation
  • New ReminderService (issuer/services/reminder.service.ts): daily 09:00 cron nudges unclaimed SENT offers — first reminder after REMINDER_AFTER_DAYS (3), then every REMINDER_INTERVAL_DAYS (3), max REMINDER_MAX_COUNT (2) per offer, REMINDER_ENABLED=false kill-switch; reminder history derived from email_logs (no schema change)
  • Manual nudge: POST /issuer/credentials/:id/remind (org-scoped)

  • CI/CD pipeline (GAP-17) — DONE (CI + image publishing) 2026-07-14

  • .github/workflows/ci.yml: on PR/push to main — backend typecheck + build + tests, frontend typecheck + next build + tests, Docker build checks for both images (lint runs informationally until the warning backlog is cleared). Both production builds verified green locally before commit
  • .github/workflows/deploy.yml: on merge to main / manual dispatch — builds and pushes versioned images (sha + latest) to GHCR; final rollout step is a documented placeholder until the hosting target (ECS vs Beanstalk vs VM — see REC_Platform_CICD_Deployment_Strategies.md) is decided
  • Unblocking fix: removed 3 dead, non-compiling components (AdminSetupStep/ContactAddressStep/OrganizationDetailsStep.tsx — superseded by Step1OrganizationAndAdmin etc., imported nowhere, referenced types that don't exist). They were breaking next build/CI; recoverable from git history if ever needed

Category 3: Security Hardening

⚠️ Docs-vs-reality note: The Architecture Document (§6.5, §12.3) states "Sensitive column data → AES-256-GCM" as if already implemented. Verified this is not true — treat every item below as fully pending regardless of what that table implies.

  • Encrypt OAuth tokens in accounts tableDONE 2026-07-14
  • account.entity.ts: access_token, refresh_token, id_token now use encryptedColumn (AES-256-GCM)
  • Note: NextAuth runs with strategy: 'jwt' and no DB adapter, so nothing currently writes this table from the frontend — transformer covers all backend paths

  • Encrypt session tokensDONE 2026-07-14

  • session.entity.ts: auth0_session_id encrypted (column widened to text). Sessions are only ever looked up by UUID primary key, so equality-search is unaffected

  • 🟨 Encrypt PII fields (employees, users, candidates) — DONE (scoped) 2026-07-14

  • Encrypted: employee phone, supervisor_name/email/title, salary_amount, performance_rating, reason_for_leaving, additional_notes; candidate phone; user phone
  • Deliberately NOT encrypted: first/last name, email, employeeId on employees/candidates/users — these power ILIKE search (employee.service.ts:136, credential.service.ts:59), duplicate-email checks, and the GAP-04 candidate search. Encrypting them (random-IV AES) breaks SQL search entirely. If they must be encrypted later, that requires deterministic encryption or a search-index sidecar — separate design task
  • Transformer now passes through legacy plaintext on read and re-encrypts on next save (lazy migration — no data rewrite needed)

  • Encrypt credential_offers.payloadDONE 2026-07-14

  • New EncryptedJsonColumnTransformer; column converted jsonbtext (migration 1783987200000-EncryptSensitiveColumns.ts). No JSONB-operator queries existed against it

  • Encrypt audit_logs JSONB stateDONE 2026-07-14

  • before/after converted jsonbtext, encrypted via EncryptedJsonColumnTransformer
  • audit_logs_readable view dropped/recreated by the migration (it doesn't expose before/after; raw-SQL inspection of state snapshots now requires going through the API, which decrypts transparently)

  • Implement baseline data retention policyDONE 2026-07-14

  • New RetentionPolicyService (shared/services/retention-policy.service.ts) + @nestjs/schedule (ScheduleModule.forRoot() in app.module.ts)
  • Daily 03:00 job purges: expired sessions > SESSION_RETENTION_DAYS (30), audit logs > AUDIT_LOG_RETENTION_DAYS (365), email logs > EMAIL_LOG_RETENTION_DAYS (180); 0 disables a purge. Env vars documented in .env.example
  • Business data (users/employees/candidates/offers) deliberately excluded — lifecycle owned by explicit user actions

Category 4: Deployment Readiness

  • Move hardcoded credentials to env varsDONE (code side) 2026-07-14
  • Found & fixed: Auth0 Management API client ID + secret were hardcoded in auth.service.ts:21-22 — now read from AUTH0_MGMT_CLIENT_ID / AUTH0_MGMT_CLIENT_SECRET (added to .env.example)
  • Full-source secret scan found no other hardcoded credentials
  • ⚠️ Remaining ops step: set the two new env vars in every deployed environment before the next deploy, or Management API calls will fail

  • ⚠️ Rotate exposed Auth0 Management API credentialsNOW MANDATORY — ops action

  • The pair removed from auth.service.ts lives in git history, so rotation in the Auth0 dashboard is required regardless of any prior rotation. Cannot be done from the codebase

  • Enable JWT audience validationDONE 2026-07-14

  • jwt.strategy.ts now passes audience: auth0Audience to passport-jwt; tokens with wrong/missing audience are rejected
  • Local dev tokens (auth0-token.service.ts generateFakeToken()) now mint with the aud claim so they keep working

  • JWT organization-context validationDONE 2026-07-14

  • jwt.strategy.ts validate() now cross-checks the token's Auth0 org_id claim against the user's organization's auth0OrgId (Organization repo injected via auth.module.ts); mismatch → 401
  • Skips the check when the token has no org_id, the user has no organization, or the org record has no auth0OrgId (nothing to compare)

  • Real service endpoint URL (not placeholder) — 30 min — verified 2026-07-14: nothing actionable in code

  • The only example.com endpoint in the codebase is inside the dev-only VNF mock (Fetcher.ts, guarded by isPocMockEnvironment()). If this item refers to the Velocity Registrar org service endpoint, that's configured during the (client-confirmed manual) Registrar registration — confirm with ops and close

  • Remove console.log from production codeDONE 2026-07-14

  • Removed the production-path console.error calls in vnf-sdk/.../Fetcher.ts (errors rethrow with context; upstream services log via LoggerService)
  • Remaining console usage is all justified and annotated: logger.service.ts (the logger's own sink), src/scripts/* + src/migrations/* (CLI tools), boot-time warnings in velocity-staging.config.ts / encrypted-column.transformer.ts (run before DI is up), and dev-only [POC MOCK] logs guarded by isPocMockEnvironment()

Open Decisions Needed Before Locking the Estimate

  1. GAP-15 scope — confirm Drop 1 (basic stats) vs Drop 2 (full analytics) per the Task 18 / Task 24 conflict above.
  2. ~~GAP-09 priority~~ — Resolved 2026-07-14: client confirmed manual Registrar + key upload is the intended flow, not a gap. Removed from backlog.

What Remains (as of 2026-07-14 — everything else above is done)

  1. Claim status sync — DONE 2026-07-16 — resolved via CIH polling (CIH v2 has no webhooks; its credentials/get exposes acceptedAt per credential). New ClaimSyncService: 5-min cron + POST /issuer/credentials/sync-claims; verified live — claimed offer flipped to claimed with the wallet's acceptance timestamp. Full story in 26-claim-webhook-registration.md. 0b. ⬜ NEW (found 2026-07-16): per-org tenant resolution — issuance/revocation/claim flows use global VNF_TENANT_ID/VNF_ISSUER_DID env pins instead of the org's stored velocity_registrar_id/organization_did; works only while there is exactly one org. Blocker for multi-org Beta — full analysis + fix design in 27-per-org-tenant-resolution.md. ~½–1 day + two-org test.

  2. ⚠️ GAP-15 dashboard stats — still blocked on the Drop 1 vs Drop 2 scope decision (see Open Decisions)

  3. ⚠️ Rotate Auth0 Management API credentials — ops action in the Auth0 dashboard (old pair is in git history); set AUTH0_MGMT_CLIENT_ID/AUTH0_MGMT_CLIENT_SECRET in deployed environments
  4. Deploy prerequisites for today's changes:
  5. Set COLUMN_ENCRYPTION_KEY (openssl rand -hex 32) in every environment — encrypted columns won't read/write without it
  6. Run migration 1783987200000-EncryptSensitiveColumns in production (dev with DB_SYNCHRONIZE=true picks up column type changes automatically, but the migration also recreates the audit_logs_readable view)
  7. Confirm the "Real service endpoint URL" item with ops (nothing actionable was found in code) and close it
  8. Wire the real rollout into deploy.yml once the hosting target is decided

Cross-References