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/exportincredential.controller.ts(respects the samesearch/status/credentialTypefilters as the list endpoint; UTF-8 BOM so Excel opens it cleanly; org-scoped via JWT) -
Frontend: "Export CSV" button on
credentials/index.tsxheader (downloadscredentials-YYYY-MM-DD.csv, carries the current search filter);exportCredentials()added tocredential.service.ts -
✅ Candidate-centric search & view (GAP-04) — DONE 2026-07-14
- Backend: new
CandidateController/CandidateService—GET /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:
/candidatessearch 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 existingthemeJSONB; update audited asorg.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_keyleft 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.jsoninto the Curo-REC site — per the requirements PDF. - This is exactly what's built today (
organization.service.ts:165requires a manually-suppliedorganizationDid;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
AuditLogController—GET /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-logspage — event/date-range filters, readable event chips, pagination, CSV export; "Audit Logs" nav item for PLATFORM_ADMIN -
New action
org.branding_updatedadded to the label map alongside the original 8 -
✅ Reminder/nudge email system (GAP-06) — DONE 2026-07-14
- Found:
NotificationService.sendCredentialReminder()+CREDENTIAL_REMINDERtemplate 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 afterREMINDER_AFTER_DAYS(3), then everyREMINDER_INTERVAL_DAYS(3), maxREMINDER_MAX_COUNT(2) per offer,REMINDER_ENABLED=falsekill-switch; reminder history derived fromemail_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 — seeREC_Platform_CICD_Deployment_Strategies.md) is decided- Unblocking fix: removed 3 dead, non-compiling components (
AdminSetupStep/ContactAddressStep/OrganizationDetailsStep.tsx— superseded byStep1OrganizationAndAdminetc., imported nowhere, referenced types that don't exist). They were breakingnext 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
accountstable — DONE 2026-07-14 account.entity.ts:access_token,refresh_token,id_tokennow useencryptedColumn(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 tokens — DONE 2026-07-14
-
session.entity.ts:auth0_session_idencrypted (column widened totext). 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; candidatephone; userphone - Deliberately NOT encrypted: first/last name, email, employeeId on employees/candidates/users — these power
ILIKEsearch (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.payload— DONE 2026-07-14 -
New
EncryptedJsonColumnTransformer; column convertedjsonb→text(migration1783987200000-EncryptSensitiveColumns.ts). No JSONB-operator queries existed against it -
✅ Encrypt
audit_logsJSONB state — DONE 2026-07-14 before/afterconvertedjsonb→text, encrypted viaEncryptedJsonColumnTransformer-
audit_logs_readableview 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 policy — DONE 2026-07-14
- New
RetentionPolicyService(shared/services/retention-policy.service.ts) +@nestjs/schedule(ScheduleModule.forRoot()inapp.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);0disables 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 vars — DONE (code side) 2026-07-14
- Found & fixed: Auth0 Management API client ID + secret were hardcoded in
auth.service.ts:21-22— now read fromAUTH0_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 credentials — NOW MANDATORY — ops action
-
The pair removed from
auth.service.tslives 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 validation — DONE 2026-07-14
jwt.strategy.tsnow passesaudience: auth0Audienceto passport-jwt; tokens with wrong/missing audience are rejected-
Local dev tokens (
auth0-token.service.tsgenerateFakeToken()) now mint with theaudclaim so they keep working -
✅ JWT organization-context validation — DONE 2026-07-14
jwt.strategy.tsvalidate()now cross-checks the token's Auth0org_idclaim against the user's organization'sauth0OrgId(Organization repo injected viaauth.module.ts); mismatch → 401-
Skips the check when the token has no
org_id, the user has no organization, or the org record has noauth0OrgId(nothing to compare) -
Real service endpoint URL (not placeholder) — 30 min — verified 2026-07-14: nothing actionable in code
-
The only
example.comendpoint in the codebase is inside the dev-only VNF mock (Fetcher.ts, guarded byisPocMockEnvironment()). 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.logfrom production code — DONE 2026-07-14 - Removed the production-path
console.errorcalls invnf-sdk/.../Fetcher.ts(errors rethrow with context; upstream services log viaLoggerService) - 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 invelocity-staging.config.ts/encrypted-column.transformer.ts(run before DI is up), and dev-only[POC MOCK]logs guarded byisPocMockEnvironment()
Open Decisions Needed Before Locking the Estimate¶
- GAP-15 scope — confirm Drop 1 (basic stats) vs Drop 2 (full analytics) per the Task 18 / Task 24 conflict above.
- ~~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)¶
-
✅ Claim status sync — DONE 2026-07-16 — resolved via CIH polling (CIH v2 has no webhooks; its
credentials/getexposesacceptedAtper credential). NewClaimSyncService: 5-min cron +POST /issuer/credentials/sync-claims; verified live — claimed offer flipped toclaimedwith 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 globalVNF_TENANT_ID/VNF_ISSUER_DIDenv pins instead of the org's storedvelocity_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. -
⚠️ GAP-15 dashboard stats — still blocked on the Drop 1 vs Drop 2 scope decision (see Open Decisions)
- ⚠️ 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_SECRETin deployed environments - Deploy prerequisites for today's changes:
- Set
COLUMN_ENCRYPTION_KEY(openssl rand -hex 32) in every environment — encrypted columns won't read/write without it - Run migration
1783987200000-EncryptSensitiveColumnsin production (dev withDB_SYNCHRONIZE=truepicks up column type changes automatically, but the migration also recreates theaudit_logs_readableview) - Confirm the "Real service endpoint URL" item with ops (nothing actionable was found in code) and close it
- Wire the real rollout into
deploy.ymlonce the hosting target is decided
Cross-References¶
- 18-master-execution-plan.md — original P2 effort estimates
- 10-gap-analysis-v3-requirements.md — GAP-01 through GAP-18 definitions
- M9-01b-logging-tracking-reconciliation.md — audit logging phased plan (A/B/C/D)
- ../../Curo-REC-Technical-Architecture-Document.md — v1.1 architecture reference (some claims reconciled here, e.g. encryption)