Conversation
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
…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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.