Skip to content

Claude/review todos rpq zl#9

Open
wparad wants to merge 32 commits intomainfrom
claude/review-todos-rpqZL
Open

Claude/review todos rpq zl#9
wparad wants to merge 32 commits intomainfrom
claude/review-todos-rpqZL

Conversation

@wparad
Copy link
Copy Markdown
Contributor

@wparad wparad commented May 6, 2026

No description provided.

claude added 30 commits May 6, 2026 08:10
All POST/PATCH handlers now use zParse() with typed schemas defined in
src/api/requests.ts. Replaces ad-hoc c.req.json() casts and manual field
checks with validated, type-safe input at the handler boundary.

Also removes POST /views/reorder and POST /rules/reorder — Views and Rules
already carry a numeric position field that clients use for runtime ordering.

https://claude.ai/code/session_017BVu9Us578XPZiYSLDXGCn
…able

Rules now carry status ("enabled"|"disabled") and priorityOrder (6-digit
zero-padded integer). The accounts DynamoDB table gains a gsi1 index with
gsi1sk = RULE#<status>#<000000-padded priority>#<id> so listEnabledRules
queries only enabled rules already ordered by priority — no in-memory sort.

User rules default to priorityOrder 100+ leaving room for system rules at
1–14. The processor calls listEnabledRules (renamed from listRules in
ProcessorDatabase) to skip disabled rules at the query level.
reorderRules removed from AccountDatabase — it was dead code after the
reorder endpoints were dropped in the previous commit.

https://claude.ai/code/session_017BVu9Us578XPZiYSLDXGCn
saveSignal now writes quarantined signals to QUARANTINED#<accountId> GSI
partition (separate from BLOCKED#<accountId>) so each status bucket is
queryable independently without a filter scan.

New listPreArcSignals(accountId, status, params) queries the correct GSI
partition and is wired through ApiDatabase + ApiDatabaseAdapter.

GET /accounts/:accountId/signals?status=quarantined|blocked returns a
paginated signals envelope. Missing or invalid status → 400.

Also fixes two adapter regressions from the previous commit:
- ProcessorDatabaseAdapter.listRules → listEnabledRules
- ApiDatabaseAdapter.reorderRules removed (method was deleted)

https://claude.ai/code/session_017BVu9Us578XPZiYSLDXGCn
…nals

Allows a signal (status → active): derives grouping key from signal
metadata, tries findArcByGroupingKey to attach to an existing arc, falls
back to creating a new arc directly — bypassing rule evaluation so the
signal cannot be re-quarantined. Updates arc.lastSignalAt if the signal
is newer. Optionally approves sender / updates filter mode in the same
request.

Blocks a signal (status → blocked): moves from QUARANTINED# to BLOCKED#
GSI partition so it disappears from the quarantine view.

Returns 400 when the signal is already active/draft (not reviewable).
New blockSignal DB method handles the GSI partition flip atomically.
findArcByGroupingKey and saveArc added to ApiDatabase + adapter.

https://claude.ai/code/session_017BVu9Us578XPZiYSLDXGCn
PUT /signals/:id/quarantineResponse replaces the previous /status endpoint.
Body is now just { status: "active" | "blocked" } — approveSender and
updateFilterMode removed since callers can hit the aliases endpoint
separately if needed.

https://claude.ai/code/session_017BVu9Us578XPZiYSLDXGCn
applyRules now returns { outcome, matchedRules } alongside the processing
outcome. Each MatchedRuleResult records the ruleId, ruleName, non-disabled
actions, any labels added by assign_label actions, and the first status
change (blocked/quarantined/archived/deleted) caused by the rule.

matchedRules is written to both quarantined/blocked signals and active
signals. Since GET /signals/:id already returns the full Signal, the data
is immediately available on the existing endpoint with no further API
changes needed.

https://claude.ai/code/session_017BVu9Us578XPZiYSLDXGCn
…allel

applyRules now returns only MatchedRuleResult[] — the single source of
truth. A new deriveOutcome() function aggregates the outcome from those
results, eliminating the redundancy of building both structures in the
same loop. The assign_workflow arc mutation is kept in applyRules as it
is an evaluation-time side effect needed for correct condition checking
in subsequent rules.

https://claude.ai/code/session_017BVu9Us578XPZiYSLDXGCn
ruleName is redundant given ruleId. disabled is always false in the
stored actions since applyRules filters them out before building the
result, so the field is noise. Actions are now Pick<RuleAction, "type"|"value">.

https://claude.ai/code/session_017BVu9Us578XPZiYSLDXGCn
- SR-05 changed from archive → block for all status workflow emails (ToS
  updates, privacy policies, compliance notices, phishing-warning bulletins,
  government notices) — silent drop rather than archiving
- Remove SR-06 (suppress_notification for status) — redundant when blocking
- Fix block/quarantine precedence: block wins (outcome.block ? "blocked" :
  "quarantined") so explicit block rules can't be downgraded to quarantine by
  a co-firing quarantine rule (e.g. SR-02 + SR-05 on untrusted-sender status email)
- Rewrite CLASSIFICATION_SYSTEM_PROMPT to use the 15 consolidated workflow
  names from types/index.ts, eliminating the runtime mismatch where the model
  returned workflow values (notice, government, personal, invoice, order,
  financial, newsletter, promotions, social, security, developer, subscription)
  that don't exist in the Workflow union type
- Extend StatusData.statusType with data_processor | cookie_policy | compliance
  to cover subtypes from the former notice workflow
- Classifier prompt adds a phishing-warning clarification: bulk "beware of
  phishing" emails from banks → status/compliance, not high-spamScore auth
- Update processor.spec.ts: two "notice arc archived" tests replaced with
  "status email blocked silently" assertions

https://claude.ai/code/session_017BVu9Us578XPZiYSLDXGCn
…re SR-02

Rule evaluation now uses first-rule-wins for status-changing actions (block,
quarantine, archive, delete): deriveOutcome stops accepting further status
actions once the first one is set. Non-status actions (suppress_notification,
assign_label, forward, etc.) continue to accumulate from all matching rules.

SYSTEM_RULES reordered so SR-05 (block status, priority 3) fires before SR-02
(quarantine untrusted senders, priority 4). This ensures status/notice emails
from untrusted senders are silently blocked rather than quarantined, regardless
of sender reputation.

SR-06 (suppress_notification for status) restored at priority 7. It is not
redundant: when a user disables SR-05, status emails pass through to the arc
system; SR-06 still suppresses the notification for them independently.

The block/quarantine status ternary reverts to quarantine-first since the two
are now mutually exclusive under first-rule-wins. Added test asserting that an
untrusted-sender status email is blocked (not quarantined) by SR-05 priority.

https://claude.ai/code/session_017BVu9Us578XPZiYSLDXGCn
Eliminates the combined if + ternary pattern. Each path now reads directly:
outcome.block → save blocked signal and return; outcome.quarantine → save
quarantined signal, notify, and return. Also extracts buildArgs to avoid
repeating the large buildSignal argument object across both branches.

https://claude.ai/code/session_017BVu9Us578XPZiYSLDXGCn
…kflows

Previously all three fell through to "normal" or had incomplete logic.

conversation: requiresReply + (urgent|negative sentiment) → high;
requiresReply + other sentiment → normal; no-reply + positive → low;
no-reply + other → normal.

crm: contract/proposal type → high (needs a decision); urgency field
maps high→high, medium→normal, low→low for all other crmTypes.

support: lifecycle eventTypes override priority — ticket_opened/resolved/
closed → low (no action needed); awaiting_response → high (agent is
waiting). Priority-based fallback: urgent→critical, high→high,
low→low, else→normal.

https://claude.ai/code/session_017BVu9Us578XPZiYSLDXGCn
…SR-24)

baseUrgency() now returns "normal" for these three workflows; the nuanced
urgency mapping (sentiment, requiresReply, crmType, priority, eventType) lives
in ten new SYSTEM_RULES at priorityOrder 9–18. Label-based urgency fallbacks
(SR-08–SR-12) shift to 19–23 so workflow-specific rules always fire first.

set_urgency is now first-rule-wins in deriveOutcome (urgencySet flag), matching
the existing statusSet pattern. Rules that set urgency below "high" carry a
!system:replied guard so the hasSentMessages promotion (which adds
system:urgency:high via assignSystemLabels) is not suppressed.

New helpers wf_() and wfData_() simplify JSONLogic conditions that key off
signal.workflow and signal.workflowData fields.

https://claude.ai/code/session_017BVu9Us578XPZiYSLDXGCn
- Signal gains urgency?: ArcUrgency
- Processor resolves signal urgency as: set_urgency rule outcome ?? arc.urgency ?? "normal"
- On new arc creation, arc.urgency = signal urgency; existing arcs are never mutated for urgency
- arc.urgency is user-settable via the API after creation

Removed:
- system:urgency:* from SystemLabel union — urgency is not a label
- SR-08–SR-12 (label-to-urgency conversion rules) — no longer needed
- assignSystemLabels no longer computes or emits urgency labels
- priorityCalculator removed — hasSentMessages urgency promotion is gone; the SR-16/19/23/24 guards handle degradation prevention

https://claude.ai/code/session_017BVu9Us578XPZiYSLDXGCn
The previous condition (positive sentiment + no reply needed + not a reply
thread) was over-specified. The real intent is distribution list / community
emails where the user has never engaged — captured cleanly as
conversation + !system:replied → low.

https://claude.ai/code/session_017BVu9Us578XPZiYSLDXGCn
- Mark urgency refactor, classifier alignment, and status-block work as done
- Remove stale references to old workflow names (notice, invoice, order, financial,
  developer, subscription) throughout UI sections
- Fix stale urgency promotion claim in Inbox section (backend no longer promotes
  on sentMessageIds — system:replied label is the signal)
- Minor typo fixes

https://claude.ai/code/session_017BVu9Us578XPZiYSLDXGCn
Rule.tags?: Record<string, string> added to the type, CreateRuleRequest,
UpdateRuleRequest, createRule, and updateRule. Stored as-is in DynamoDB,
never read by the rule evaluator.

https://claude.ai/code/session_017BVu9Us578XPZiYSLDXGCn
A1: Hot partition fix — arc/signal pk encodes item id to distribute DynamoDB writes
A2: Alias sender normalization — separate AliasSender items replace approvedSenders array
A3: Audit table + audit-database.ts — write-before pattern for all config mutations

B1: GET /aliases?domain= filter by createdForOrigin
B2: updateArc urgency persistence
B3: Alias address rename (renameAlias + PATCH /aliases/:address { newAddress })
B4: Merge GET /search into GET /arcs?q=; delete /search endpoint
B5: Email templates + auto_reply/auto_draft rule actions + draft signal CRUD
B6: Web Push subscriptions (savePushSubscription/listPushSubscriptions/deletePushSubscription) + pushNotify in notifier (VAPID, 410 cleanup)

C: JS rule execution via quickjs-emscripten (evalCondition, js: prefix in rule conditions)
D: OpenAPI spec generation (@hono/zod-openapi, scripts/openapi.ts, GET /openapi.json)
E: GitHub Actions CI workflows for backend, site, and extension (path-filtered)
F: WebSocket API Gateway infra (aws_apigatewayv2_api, connect/disconnect/default routes)
G: Domain health monitoring (dns-checker.ts, POST /domains/:id/verify, domain-health-job.ts, EventBridge weekly cron)

Also fixes: sender trust logic (notify_new mode marks unknown senders as untrusted), new-address quarantine (block_until_approved ignores sender entries when alias absent), Zod v4 z.record two-arg form, exactOptionalPropertyTypes spread patterns throughout.

All 285 tests pass.

https://claude.ai/code/session_017BVu9Us578XPZiYSLDXGCn
Replace the four-value SenderFilterMode (strict, sender_match, notify_new,
allow_all) with a clean model that separates disposition from notification
intent: allow_all | quarantine_notify | quarantine_silent | block.

Key changes:
- Remove SR-02 (system rule that quarantined on system:sender:untrusted) —
  the filter mode is now applied directly as a fallback after all rules run,
  only when no rule has already set a status outcome.
- Add post-rule fallback in processor: if !hasStatusOutcome and the arc
  carries system:sender:untrusted, switch on effectiveFilterMode to set
  block, quarantine+suppress, or quarantine.
- Gate quarantine notification on !outcome.suppressNotification so
  quarantine_silent mode does not email the user.
- system:sender:untrusted label assignment is unchanged — still emitted
  before rules so users can write rules against it; suppressed only in
  allow_all mode.
- Update all hardcoded "notify_new" defaults to "quarantine_notify".

All 286 tests pass.

https://claude.ai/code/session_017BVu9Us578XPZiYSLDXGCn
SignalStatus: 'quarantined' → 'quarantine_visible' | 'quarantine_hidden'
SenderFilterMode: 'quarantine_notify' | 'quarantine_silent' → 'quarantine_visible' | 'quarantine_hidden'
RuleActionType: add 'quarantine_hidden' alongside plain 'quarantine'

Semantics:
- quarantine_visible: signal stored and surfaced in the review queue
- quarantine_hidden: signal stored but not surfaced in the review queue
- Plain 'quarantine' action (and SR-03 high-spam) → quarantine_visible
- SR-02 (untrusted sender) → quarantine_hidden; filter-mode fallback also sets quarantineHidden when mode = quarantine_hidden
- notifyBlocked only called when !quarantineHidden

Both quarantine variants share the QUARANTINED#${accountId} GSI partition
so callers can query all quarantined signals with a single DynamoDB query.
The ?status=quarantined query param returns both variants.

All 288 tests pass.

https://claude.ai/code/session_017BVu9Us578XPZiYSLDXGCn
SR-02 ("Quarantine untrusted senders") is replaced by a post-rule fallback
that fires only when no rule produces a status outcome and the sender has the
system:sender:untrusted label. The fallback maps the per-alias (or account-
default) SenderFilterMode to the appropriate outcome:

  quarantine_visible → quarantine, notifier called
  quarantine_hidden  → quarantine, notifier suppressed
  block              → blocked
  allow_all          → signal proceeds as active (label is never set)

This keeps filter-mode as pure policy rather than a system rule, and lets
user-written rules that match system:sender:untrusted always win.

https://claude.ai/code/session_017BVu9Us578XPZiYSLDXGCn
Both quarantine_visible and quarantine_hidden live in the same
QUARANTINED# GSI partition. When the caller requests a specific variant,
filter the page results in memory after the DB call rather than needing
separate partitions.

https://claude.ai/code/session_017BVu9Us578XPZiYSLDXGCn
…sh with WebSocket

Audit table:
- SK now resource-first: <resourceType>#<resourceId>#<timestamp>#<eventId>
  enables begins_with filter for per-resource history on the main table
- GSI1 PK is now AUDIT#<accountId> (was RESOURCE#... with no account isolation)
- GSI1 SK is <timestamp>#<eventId> for time-ordered account activity feed
- listAuditEvents uses GSI1; listResourceHistory uses main table begins_with
- listResourceHistory now requires accountId parameter

WebSocket connections (replaces Web Push):
- WsConnection type replaces PushSubscription in types/index.ts
- ACCOUNTS_TABLE stores connections at CONN#<connectionId> SK
- saveWsConnection / listWsConnections / deleteWsConnection in account-database
- ses-notifier.ts queries CONN# prefix and posts via API Gateway Management API
  (WS_API_ENDPOINT env var); removes web-push / VAPID entirely
- HTTP API: push-subscriptions endpoints removed; GET /connections added for debug
- $connect/$disconnect route handlers are a separate WebSocket Lambda (TODO: infra)

https://claude.ai/code/session_017BVu9Us578XPZiYSLDXGCn
…ns endpoint

$connect stores a WsConnection in ACCOUNTS_TABLE (CONN#<connectionId> SK).
$disconnect deletes it. $default is a no-op for now.
accountId comes from the Lambda authorizer injected into requestContext.authorizer.
TTL is set to 2 h (API Gateway's idle timeout is 10 min, but explicit TTL keeps
the table clean if a disconnect event is missed).

The ApiDatabase interface no longer exposes WsConnection methods; the WebSocket
handler calls accountDb directly. GET /connections endpoint removed.

https://claude.ai/code/session_017BVu9Us578XPZiYSLDXGCn
- Add audit DynamoDB table ARN to Lambda IAM DynamoDB statement
- Add AUDIT_TABLE env var
- Add execute-api:ManageConnections IAM statement scoped to WS API connections
- Add WS_API_ENDPOINT env var (resolved from ws API id + stage + region)
- Add data.aws_region.current for region interpolation

https://claude.ai/code/session_017BVu9Us578XPZiYSLDXGCn
The same Lambda handles the authorizer event (detected by type=REQUEST + methodArn).
Token is read from ?token= query param (browsers can't set headers on WS upgrade)
with Authorization header as fallback. On success, accountId and userId are injected
into requestContext.authorizer context and available to all subsequent route handlers
($connect, $disconnect, $default) for the lifetime of the connection.

Infra: REQUEST authorizer on the WS API, TTL 300 s (cached per token), Lambda
permission scoped to authorizers/*, \$connect route wired to CUSTOM auth.

https://claude.ai/code/session_017BVu9Us578XPZiYSLDXGCn
claude added 2 commits May 7, 2026 22:12
…MS encryption

- ses.tf: replace hardcoded eu-west-1 in MX/bounce records with data.aws_region.current.name
- outputs.tf: add dynamodb_audit_table, feedback_queue_url, ws_api_endpoint
- storage.tf: add dead_letter_config on EventBridge domain-health target (reuses signals_dlq);
  add SQS queue policy granting EventBridge permission to send to the DLQ
- storage.tf: upgrade S3 email bucket from AES256 to SSE-KMS (bucket_key_enabled = true
  to reduce KMS API calls)
- kms.tf: add explicit key policy — account root admin, S3 service principal for SES
  email writes, Lambda IAM role for decrypt/encrypt; removes implicit default-only policy

https://claude.ai/code/session_017BVu9Us578XPZiYSLDXGCn
Reverts:
- S3 encryption back to SSE-S3 (AES256) — no KMS needed for emails
- KMS key back to default policy — no explicit policy needed
- EventBridge domain-health DLQ removed — EventBridge retries delivery on its
  own schedule; a DLQ on a weekly cron adds no value

Adds:
- CF_ORIGIN_SECRET env var wired from random_password.cf_origin_secret in lambda.tf
- app.ts middleware validates x-origin-verify header against CF_ORIGIN_SECRET
  before auth runs; skipped when env var is unset (local dev / tests)

https://claude.ai/code/session_017BVu9Us578XPZiYSLDXGCn
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants