Skip to content

[comp] Production Deploy - #3521

Merged
tofikwest merged 15 commits into
releasefrom
main
Jul 29, 2026
Merged

[comp] Production Deploy#3521
tofikwest merged 15 commits into
releasefrom
main

Conversation

@github-actions

@github-actions github-actions Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

This is an automated pull request to release the candidate branch into production, which will trigger a deployment.
It was created by the [Production PR] action.


Summary by cubic

Ships universal 2FA hand‑off, the security‑questionnaire Chrome extension, stricter route‑scoped CORS/origin policy, a policies bulk upload flow, a resumable connect strip, and connection longevity with a one‑click “Make permanent” 2FA flow. Also hardens sign‑in classification and aligns take‑over messaging, fixes policy unlinking/visibility and training completion auth, adds a HIPAA completion email endpoint, requires logGroupName in CloudTrail→CloudWatch remediation, and shows the AWS “+ Add account” form in a dialog.

  • New Features

    • 2FA: classify the verification method and return it from sign‑in; update the connect UI to give exact take‑over guidance (enter a code, switch off a passkey, or passkey‑only notice).
    • Extension: introduce @trycompai/security-questionnaire-extension (MV3) with vendor/Google Sheets/Docs support, a release workflow, and optional Google OAuth; add an origin policy plus custom CORS with HTTPS‑only wildcards, COMP_EXTENSION_TRUSTED_ORIGINS, and route‑scoped allowlists.
    • Policies: bulk upload in the policies tab to create multiple drafts and attach PDFs, with per‑file isolation and retry‑friendly results.
    • Training: new endpoint to send the HIPAA training completion email with certificate.
    • Connect: unfinished vendor connects show as a resumable strip instead of auto‑reopening full‑screen; starting a new connect clears stale state.
    • Connections: show longevity per connection (stays signed in, signed in for now, SSO, reconnect needed) from live vault status; add a focused “Make permanent” sheet to store the authenticator setup key (Base32 or otpauth://); add batch status endpoint GET /v1/browserbase/profiles/totp-statuses.
  • Bug Fixes

    • 2FA: when a TOTP is stored, auto‑switch passkey/security‑key prompts to the authenticator‑code method; for human take‑overs, reveal other methods without auto‑selecting; classify methods on unclear outcomes so guidance stays accurate; treat the current URL as untrusted data (http/https only, XML‑escaped) in the classifier; tighten method detection so device approvals/push/email links are “other”; only classify a passkey as switchable when a code‑based method exists; align the toast and panel to one decision.
    • Extension: don’t overwrite approved/edited answers; serialize queue writes; keep failed questions retryable and restore prior status; closed‑shadow domain confirmation; Google Docs “copy answers”; tighter detection; optional Google OAuth; CORS adds Vary headers, keeps HEAD allowed, and restricts wildcard origins to HTTPS; tests lock in production CORS behavior.
    • Policies: hide user‑archived policies in control/framework reads (isArchived:false) and always sever framework join rows on unlink, including join‑only cases.
    • Training: remove the RBAC gate from “mark training complete” so members can complete videos without portal:update; log completion‑email failures in production; defer sending the completion email until after the response with a timeout; share canonical training video IDs via @trycompai/company and key app video lists by those IDs to prevent drift.
    • Questionnaire API: require and validate organizationId when saving generated answers.
    • Bulk Upload: isolate thrown post() calls so one failure doesn’t reject the whole batch, and preserve any created policyId for retry.
    • Cloud Security: require logGroupName for CreateLogGroupCommand to fail fast; only backfill a name when derivable from CloudWatchLogsLogGroupArn to avoid false successes; add tests.
    • Connections/TOTP: server‑side normalize and validate the authenticator setup key; keep the parser linear on untrusted input; treat unreadable 2FA status as unknown so the UI never prompts to overwrite an existing key.
    • Integrations: show the AWS “+ Add account” form inside a dialog and scroll it into view so the form is visible on click.

Written for commit fecb556. Summary will update on new commits.

Review in cubic

github-actions Bot and others added 2 commits July 28, 2026 16:39
…age asks for (#3520)

* fix(api): switch a passkey 2FA prompt to the authenticator-code method

Vendors like GitHub default the 2FA step to a passkey / security-key prompt
(github.com/sessions/two-factor/webauthn) — which we can't use and which has no
6-digit field, so the panel said 'enter the code' while the page showed no place
to type it. After sign-in, best-effort switch to the authenticator-app option
(More options -> Use authenticator app / Enter a code) so the code field is
actually shown — for us to fill a stored code, or for the user to type one during
take-over. +test.

* fix: tailor the 2FA take-over to what the page actually asks for

Make the two-factor take-over universal instead of assuming an authenticator
code:

- Classify the verification page into how it wants the user to verify (a code,
  a passkey that can be switched away from, a passkey-only login, or another
  challenge) and return it from the sign-in result.
- When Comp AI has a stored code, auto-switch off a passkey so the code field
  appears, then fill it. When a human is taking over, don't pick a method for
  them — just reveal the vendor's other options so they choose the one they can
  complete.
- Show exact guidance in the take-over panel from that classification: enter the
  code, switch off the passkey, or a plain notice that a passkey-only login
  can't be automated (with the confirm button hidden, since there's nothing to
  complete). No vendor names are hard-coded.

Adds unit coverage for the classifier, the take-over vs stored-code login
behavior, and the panel copy for each method state.

* refactor: address review on the 2FA take-over changes

- Split the passkey→code switch into two single-action steps (reveal the other
  options, then choose the authenticator/code method). act() performs one action
  per call, so a combined 'click More options, then Use authenticator app' could
  stop after the first click and never reach the code field.
- Trust an explicit method classification when routing the take-over panel: an
  'other' verification (device approval, CAPTCHA, link) now uses the generic
  finish panel instead of code-entry guidance, even when the outcome is
  needs_2fa. Falls back to the failure code only when no method was detected.
- Extract the page classifiers (classifyLoginOutcome, classifyTwoFactorMethod,
  safeOriginAndPath) into a focused browser-login-classifier module, bringing the
  login file back under the 300-line limit.

Tests moved/updated alongside; 186 browserbase tests green, typecheck clean.
@vercel

vercel Bot commented Jul 28, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
app (staging) Ready Ready Preview, Comment Jul 28, 2026 10:01pm
comp-framework-editor (staging) Ready Ready Preview, Comment Jul 28, 2026 10:01pm
portal (staging) Ready Ready Preview, Comment Jul 28, 2026 10:01pm

Request Review

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 9 files

Reply with feedback, questions, or to request a fix.

Fix all with cubic | Re-trigger cubic

Comment thread apps/api/src/browserbase/browser-login-classifier.ts
Comment thread apps/api/src/browserbase/browser-login-classifier.ts Outdated
Comment thread apps/api/src/browserbase/browser-login-classifier.ts Outdated
A passkey prompt (e.g. GitHub's webauthn step) asks for a security key rather
than a code, so the outcome classifier can land on 'unknown' instead of
'needs_2fa'. Classify the verification method on 'unknown' as well so the
take-over panel still recognizes the passkey and guides the user to a code
method. The classifier degrades to 'other' for a genuinely non-verification
page, so the panel stays generic there.
* feat(security-questionnaire): add browser extension

* fix(security-questionnaire-extension): address review findings

- Use a closed shadow root for the domain confirmation dialog so the host
  page cannot reach its controls.
- Escape leading formula characters when building Sheets paste TSV, so
  answers starting with "-" stay text instead of being evaluated.
- Restrict the wildcard trusted-origin match to HTTPS; explicit localhost
  entries are unaffected.
- Stop generate-all from overwriting approved or manually edited answers.
- Hold the inline preview locally so concurrent generates cannot render a
  result into another field's preview.
- Finalize queue item status when single generation fails, instead of
  leaving the item stuck in "generating".
- Implement the Google Docs footer copy action, which had no handler, and
  label/enable the button to match.

Adds coverage for the paste plan, batch candidate selection, docs footer,
clipboard text, and the origin policy.

* fix(security-questionnaire-extension): make Google OAuth optional in release, add docs

The release workflow required a Google OAuth client id and failed the build
when the manifest had none, so the extension could not ship without direct
Google Sheets API access. Treat it as optional: verify the manifest only
when the secret is set, and drop it from the required-secret check.

Adds the extension documentation page and links it from the docs nav.

* fix(security-questionnaire-extension): resolve remaining review findings

Detection and data correctness:
- Stop dropping questionnaire rows whose text looks like a column label.
  Any cell under 48 characters containing "control", "requirement" or
  "question" was discarded, which silently lost common questions such as
  "Are security controls documented?".
- Ignore answer-style statements in the keyword fallback so filled-in
  answers are not re-detected as new questions.
- Reject row/column 0 when parsing sheet targets; A1 notation is 1-based.
- Reject negative sheet ids when building formatting requests.

Concurrency and data loss:
- Apply batch results to the stored queue rather than a snapshot, and skip
  items the user edited or approved while generation was in flight.
- Serialize storage read-merge-writes so concurrent updates are not lost.
- Keep the sheet mapping dialog populated when a save fails.
- Surface autosave failures instead of dropping them.

Robustness:
- Guard JSON parsing in the API client, GViz table parsing and gid decoding.
- Let one Sheets endpoint failure fall through to the next.
- Fall back to local detection when the background message fails.
- Add Vary headers to CORS responses, including rejected origins.
- Trim organization ids and reject empty question ids and whitespace-only
  approvals.
- Tighten isItemResponse to validate the queue it claims to carry.

Cleanup: drop the unused ReviewPanel and the unhandled
comp:generate-visible-page message, dedupe getHost and surface validation.

* fix(api): keep HEAD in the CORS allowed methods

The replacement CORS middleware dropped HEAD, which app.enableCors allowed
by default, so cross-origin HEAD preflights would have started failing.

* fix(security-questionnaire-extension): publish via service account on the working API

Switches Chrome Web Store auth from a personal refresh token to a service
account JWT, which does not expire and is not tied to an individual account.

Verified against the live API: the token exchange succeeds and the service
account can read the store item. Every v2 endpoint returns 404 for this
publisher while v1.1 works with the same token, so the upload, status and
publish calls now use v1.1. That API is supported until 2026-10-15.

Adds a publish_target choice so a manual run can ship to trusted testers
before going public.

* fix(security-questionnaire-extension): isolate failed queue writes in batch generation

A rejected save left the serialized write chain permanently rejected, so
every answer generated afterwards was computed and then silently dropped.
Keep the chain alive, collect per-item write failures instead of throwing
from a pool worker (which abandoned the questions queued behind it), and
report them once the batch has persisted everything it could.

* fix(security-questionnaire-extension): repair the manifest version check

The check embedded a JS template literal inside a double-quoted shell
string, so bash tried to expand ${process.env.VERSION} and aborted the
step with 'bad substitution'. The release workflow has never run, so this
would have failed on the first publish. Use string concatenation instead.

* fix(security-questionnaire-extension): drop the unused activeTab permission

Every activeTab occurrence in the source is a local variable name; the
permission itself is never relied on, and the <all_urls> host permission
already covers the same access. The store rejects versions that request
permissions they do not need.

Also refuses to publish a package whose manifest still points at
localhost, which is what the build produces when the public URL env vars
are missing.

* fix(security-questionnaire-extension): keep failed questions retryable

When a queue write failed the question stayed marked `generating`, and
that status is excluded from batch candidates — so every later Generate
All skipped it and the answer could never be recovered. Reset the item to
pending after a failed write.

* fix(security-questionnaire-extension): restore prior status after a failed write

Resetting a failed question to pending discarded whatever state it had
before the batch, so a transient storage error downgraded a question that
already held a generated or flagged answer. Restore the status captured
before the batch marked it generating.
…ins (#3523)

* test(api): lock in that production origins still pass CORS

The CORS layer was replaced in #3064 to support the questionnaire
extension. These assert the four origin classes that actually reach
production — the app, the portal, an arbitrary trycomp.ai subdomain, and a
customer custom domain — still receive the right headers, that an
untrusted origin is still rejected, and that resolution does not depend on
AUTH_TRUSTED_ORIGINS, which the API is not given in production.

Tests only; no production code changes.

* test(api): use reserved example domains in the auth specs

The specs hardcoded a developer's real local unpacked-extension id and
used evil.com / acmecorp.com, both of which are real registrable domains,
in a public repository.

Replace them with a synthetic extension id and RFC 2606 reserved names
(untrusted.example, trust.example.com), and drop the attack framing.
Behaviour of the tests is unchanged.
* fix(training): remove rbac gate from mark-complete endpoint

## Problem

A user with a custom role lacking portal:update permission is unable to mark training videos complete, receiving a 403 error and seeing "failed to mark video as completed" in the UI. The user can accept policies and complete device setup successfully, but gets blocked at the training endpoint.

## Root cause

The POST /v1/training/completions/:videoId/complete endpoint is gated by @RequirePermission('portal','update') in training.controller.ts. This creates an inconsistent permission model: policy acceptance uses session + member-ownership only (no RBAC check), while training completion enforces a portal:update requirement that custom roles may lack. Existing custom roles created before permission enforcement was added still lack this permission.

## Fix

Remove the @RequirePermission('portal','update') guard from the mark-complete endpoint. Training completion is a user action on their own record (like policy acceptance), not a privileged administrative operation, so it should authenticate by session + member-ownership only, not RBAC.

## Explicitly NOT touched

Policy enforcement at role creation time (from PR #3455) remains in place. Other training endpoints and their permission gates are unchanged. Portal write permissions on other operations are unaffected.

## Verification

Added regression test asserting that a user with a custom role lacking portal:update can successfully mark a training video complete. Existing training controller unit tests pass locally ✅.

* fix(todo): address cubic review

Address review findings.

* fix(todo): address cubic review

Address review findings.

* fix: address cubic review

Address review findings.

* fix(portal): log training completion-email failures in production (CS-774)

The completion-email failure path logged via the `logger` util, which no-ops
outside development — so in production a failed completion email was swallowed
silently (negating the response.ok check). Use console.error for this
operational error so it is visible in prod. Test now asserts console.error.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
… unlinking (#3513)

* fix(policies): remove archived policies from framework controls after unlinking

## Problem
A customer manually unlinked archived policies from controls. However the policies still appear in the control's policy list, even though viewing the policy directly shows it as unlinked from any controls. This creates a contradiction where the control says the policy is linked but the policy says it is not.

## Root cause
Two bugs compound the issue.

(1) removePolicyControl in policies.controller.ts only deletes the FrameworkControlPolicyLink join row for custom frameworks (`customFrameworkId:{not:null}` guard at line 1027). Platform framework links (like SOC 2) are never deleted from the join table. Meanwhile findOneForFramework in controls.service.ts reads a platform control's policy list entirely from this join table, so stale rows remain visible.

(2) Control and framework reads filter policies only on `archivedAt:null`, omitting the `isArchived:false` check. This contradicts the policy.prisma rule (lines 37-41) which hides user-archived policies. So policies marked as archived by the user still display and score against framework progress.

## Fix
Removed the `customFrameworkId:{not:null}` gate in removePolicyControl so the join row deletion runs for all framework types, not just custom ones. Relaxed the before-guard to let deletion proceed when the m2m link exists. Added `isArchived:false` filter to all control/framework/score policy queries to respect the archive state.

## Explicitly NOT touched
No schema migration. No RBAC or organizationId scoping change. Auth rules remain unchanged.

## Verification
Added regression test asserting that unlinking a policy from a platform framework (SOC 2) removes it from both the join table and the control's returned policy list. Confirmed that filtering on `isArchived:false` prevents user-archived policies from appearing. Unit tests pass locally ✅

* fix(policies): always sever framework join rows on unlink (CS-780 join-only links)

The unlink still gated the FrameworkControlPolicyLink deleteMany on an implicit
m2m link existing (`before?.controls.length`). Framework-scoped links created via
LinkPolicySheet make ONLY a FrameworkControlPolicyLink (no implicit m2m), so
unlinking one skipped the delete and the policy kept showing against the control —
the same CS-780 contradiction. Run the org-scoped deleteMany unconditionally
(idempotent; the org-scoped policy.update still aborts for foreign/missing
policies). Regression test covers the no-m2m-link case.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
@vercel
vercel Bot temporarily deployed to staging – portal July 28, 2026 19:30 Inactive
@vercel
vercel Bot temporarily deployed to staging – app July 28, 2026 19:30 Inactive
…creen step (#3525)

Returning to a task with an unfinished vendor connect used to auto-reopen that
sign-in full-screen, trapping the user in it — there was no way to create a new
(or different) automation without cancelling out.

Now the in-progress connect is surfaced as a resumable band (mirroring the
existing drafts strip): the user lands on their automations and chooses — Resume
to finish it, Discard to drop it, or just Create/Connect a new one. Starting a
fresh connect clears any half-finished one so the flow never resumes into a stale
draft. Same resumable steps as before (analysis / method choice); only the
forced takeover is gone.

Adds ResumeConnectStrip + a test.
* feat: surface connection longevity and a 'Make permanent' 2FA flow

Make it obvious, per connection, whether a saved vendor sign-in keeps running on
its own — and give a one-click way to fix the ones that don't.

Connections page:
- Each connection now shows a longevity state read live from the vault:
  Stays signed in (password + authenticator), Signed in for now (password, no
  key — may pause when the vendor asks for a code), Signed in (SSO — occasional
  re-sign-in), or Reconnect needed. Each with a plain-language hint.
- A dismissible banner nudges when connections can be made permanent.
- A focused 'Make permanent' sheet stores the authenticator setup key (with
  per-vendor guidance, key validation, and a success state). We never change the
  vendor's 2FA — the user enables it and pastes the one-time setup key.
- New batch endpoint GET /profiles/totp-statuses returns 2FA status for all
  password connections in one round-trip (reuses the live vault read; no schema
  change).

Task flow:
- The connection menu gains 'Keep signed in (2FA)', reusing the same sheet, so a
  connection can be made permanent without leaving the task.

Tests: batch status service (per-connection, degrades unreadable items); row
states + actions; sheet validation and save/success. API + app typecheck clean.

* fix: address review on the connection-permanence changes

- Scope the 2FA-status SWR cache to the org so switching orgs can't show another
  org's statuses.
- Treat an unreadable/failed status as UNKNOWN, never 'no key': the batch endpoint
  omits connections it couldn't read (instead of reporting false), the hook
  surfaces its error, and rows show 'Status unavailable' with no 'Make permanent'
  prompt — so an outage never invites overwriting an existing key. The at-risk
  banner counts only confirmed 'no key' connections.
- Bound the 1Password fan-out in the batch endpoint (was unbounded per org).
- Refresh statuses after a connect/reconnect so a new row isn't stale.
- Treat a never-verified session as 'reconnect', not 'stays signed in', even with
  a stored key.
- Make the 'Make permanent' sheet resilient: a rejected save (or a failed status
  refresh after a successful save) returns to the form with an error instead of
  hanging on 'Saving…'; store the normalized Base32 key (spaces/hyphens stripped).
- Remove the task-flow 'Keep signed in (2FA)' menu item: ConnectionManageMenu is
  not mounted anywhere and connection management deliberately lives on the
  Connections page, so it was dead code.

Deferred: splitting the browser-auth-profiles controller (already over the file
limit before this change) into a focused module.

* refactor(api): extract the 2FA (TOTP) endpoints into a focused controller

Moves the four TOTP routes (batch status, per-connection get/set/clear) out of
BrowserAuthProfilesController into a dedicated BrowserAuthTotpController, bringing
the profiles controller back under the 300-line limit and grouping the 2FA routes.
Same paths, guards, permissions, and behavior — no route or contract change.

* fix(api): validate the authenticator setup key server-side + cover the TOTP controller

- setProfileTotp now validates and normalizes the key on the server (the client
  check is only UX): it accepts a Base32 secret or an otpauth:// URI and rejects a
  rotating one-time code or junk, so a direct API caller can't silently replace a
  working seed with something that can't generate codes — which would quietly
  break unattended 2FA. Extracted as normalizeTotpSecret with unit tests.
- Add controller-level tests for BrowserAuthTotpController: organization + profile
  ids reach the storage service on all four routes, and reads require
  integration:read while writes require integration:update.

* fix(api): keep the setup-key parser linear on untrusted input

Strip trailing Base32 padding with a plain scan instead of a start-anchorless
regex (which can backtrack polynomially on untrusted input), and bound the input
length up front. Behavior is unchanged for valid keys; adds padding + long-input
tests. Clears the CodeQL polynomial-regexp alert.
…o view (#3526)

## Problem

Clicking the + Add button on the AWS Integrations page appears unresponsive. No modal opens, no feedback is given, and the page does not navigate or scroll. Users cannot see the form to add a new AWS account.

## Root cause

When a user clicks + Add on an AWS integration card, the form to add a new account is rendered but placed outside the page's main Stack container it mounts thousands of pixels below the fold after the evidence tasks section, cloud tests link, and the 46-card AWS services grid. Without a Dialog wrapper or scrollIntoView call, the viewport never changes and the form remains invisible to the user. The React #418 hydration error in the console is a secondary red herring; the click handler binds correctly (focus ring confirms the click reached the button), so the real blocker is the off-screen form rendering.

## Fix

Wrapped the AWS add account form in a Dialog component and call scrollIntoView on mount. This ensures the form appears on top of the page content in a modal, is immediately visible to the user, and provides the expected UX feedback when the button is clicked.

## Explicitly NOT touched

- OAuth integrations or multi-step auth flows
- AWS manifest configuration or connection logic
- Other integration providers
- Hydration or SSR behavior

## Verification

- Added regression test asserting that clicking + Add renders the form inside a Dialog and the form is visible in the viewport
- Existing AWS integration unit tests pass ✅
* fix: harden sign-in classification and align take-over messaging

Addresses review findings on the browser sign-in path:
- Treat the browser's current URL as untrusted data in the classification prompt
  (labeled, delimited) so page-path text can't steer the outcome; the query and
  fragment were already stripped.
- Tighten the 2FA-method classifier so a device approval, push notification, or
  email/SMS link is classified as 'other' rather than over-matching a passkey.
- Align the take-over toast and panel to one decision, so they can't contradict
  (e.g. an 'enter your code' toast over a 'finish sign-in' panel).
- Add direct unit tests for classifyLoginOutcome (each outcome, throw -> unknown,
  URL passed as untrusted with secrets stripped, content-only when no URL).

* fix(api): keep the URL hint as data — ignore non-http schemes + XML-escape

A non-http(s) page URL (data:, about:, javascript:, …) isn't percent-encoded and
could carry characters that break out of the <current_url> delimiter. Restrict the
URL hint to http(s) origins and XML-escape the value before interpolation, so it
always stays inside its tag as data. Adds tests for both.
#3528)

Tighten the 2FA-method classifier so 'passkey' requires a CODE-based alternative
(authenticator app, SMS, or email code). A passkey / security-key prompt whose
only alternative is a device approval, push, or email/SMS link is now 'other' —
so the take-over panel doesn't tell the user to 'enter a code' when no code method
is actually available.
#3529)

* fix(training): defer completion email and share canonical training IDs

The portal's complete-training route awaited the completion-certificate
email before responding. That API call resolves the member, renders a PDF
and calls the email provider, so every final-video completion made the
employee wait seconds for work whose outcome they never see — and Node's
fetch has no default request timeout, so an unreachable API could stall
the response for minutes even though the completion was already durable.

Defer it with `after` so it runs once the response is sent, and bound the
request with an AbortSignal timeout so a hung API can't pin the
invocation open. The email path performs no DB or storage writes, only an
in-memory PDF render and a send, so nothing the UI reads afterwards
depends on it having finished.

The route also restated the valid-video-ID list and the HIPAA eligibility
rule that apps/api owns, a fifth copy of constants already duplicated
across both apps and two API modules. Move them to @trycompai/company —
the existing shared-definitions package for api/app/portal — so the two
completion paths cannot disagree about which videos exist or which orgs
may complete HIPAA training.

The two apps' training data files link to the canonical IDs by type only.
They are imported by client components and by a Trigger.dev task, and
@trycompai/company requires @trycompai/db at runtime, so a value
re-export would have pulled Prisma into those bundles. The type-level tie
still fails the build on drift, in both directions: an unknown ID in a
rendered list, or a canonical ID that no longer matches.

Also fixes the pre-existing training-hipaa spec, whose @db mock was never
given frameworkInstance when the HIPAA gate was added, and adds coverage
for the gate's rejection path.

* fix(training): make the canonical/rendered video lists exhaustive both ways

`satisfies readonly CanonicalTrainingVideo[]` only proved that every
rendered video is canonical. The reverse — a canonical ID with no
rendered video — still compiled, and that is the dangerous direction:
completion requires a row for every canonical ID, so adding one without
a matching video would leave employees permanently unable to finish
training.

Key the data by the canonical union instead. `Record<GeneralTrainingVideoId,
...>` is exhaustive by construction, so both drifts now fail the build:
a missing key (TS2741) and an unknown key (TS2353). Verified by
introducing each drift and compiling.

The `Exclude<GeneralTrainingVideoId, (typeof trainingVideos)[number]['id']>`
form would not have worked here — the `readonly TrainingVideo[]`
annotation widens `id` to `string`, so the Exclude resolves to `never`
and the guard passes vacuously.
@claudfuen

Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 3.111.0 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants