Skip to content

Feat/connector 6 webhook in - #359

Merged
th3-br41n merged 2 commits into
mainfrom
feat/connector-6-webhook-in
Jul 29, 2026
Merged

Feat/connector 6 webhook in#359
th3-br41n merged 2 commits into
mainfrom
feat/connector-6-webhook-in

Conversation

@th3-br41n

Copy link
Copy Markdown
Contributor

No description provided.

th3-br41n and others added 2 commits July 28, 2026 15:40
… 11b.8 ingress plane

An installed connector's SyncMapping can now register a shell-minted inbound
webhook endpoint: an authenticated POST to /wh/<routeId>/<secret> on the
EXISTING 11b.8 loopback listener (one HTTP server, never a second) acts as a
doorbell that dispatches connectors.sync(mappingRef) — the payload is
discarded, so hostile body bytes can never enter the projection; the
provider's delta API stays the source of truth (doc 56 §Sync model).

Security posture:
- Custody: hash-only at rest. registry.db connector_webhooks stores the
  SHA-256 of the endpoint secret; the plaintext exists exactly once, inside
  the webhookRegister response URL. status never returns it; hits never
  carry it; the ingress plane verifies by hashing the presented value and
  timingSafeEqual over fixed-length digests.
- Fail-closed gates: server-side connectors.webhook cap on every method;
  registration additionally requires the RESOLVED connector app's runtime
  network.ingress grant (11b.8 model); envelope.app is pinned to the
  mapping's owner (a sibling app cannot mint/see/kill another connector's
  endpoint); routes only register while the grant holds (per-app filter at
  hydrate); missing ledger/store reads as no routes/Unavailable.
- Bounded ingress: rides the 11b.8 listener unchanged (127.0.0.1-only bind,
  POST-only, 256 KiB cap, oracle-free 404, constant-time auth); dispatch is
  per-mapping coalesced (one in-flight sync + one trailing run), so sync
  rate is capped by sync duration, not inbound request rate.
- Lifecycle: endpoints survive restart via the typed
  ConnectorWebhooksRepository (registry.db v12), die with the mapping
  (webhookRevoke) and with the account (revoke cascade on disconnect);
  rehydrate() drops/adds routes live and can bind the listener mid-session
  for the first mint (fixed a latent host gap where a listener set after
  start() registered routes but never subscribed).

Surface: connectors.webhookRegister / webhookStatus / webhookRevoke (SDK
proxy + sdk-types); WebhookRoute/WebhookHit generalized with a
WebhookTargetKind enum (workflow | connector-sync) across listener, relay
port (desktop-side re-verify incl. hash routes), fan-in, and host.

Scoped OUT (documented boundary): public reachability stays owner-ops —
the relay node route/deploy from 11b.8 is unchanged and dormant until a
transport is paired; this PR ships loopback (127.0.0.1) ingress only.
Provider-specific HMAC signature schemes (e.g. X-Hub-Signature) are not
implemented; the shell-minted per-endpoint secret is the authentication.
Endpoints are device-local (registry.db never syncs), matching the
designated-automation-host ingress model.

Tests: +45 across repo (mint/rotate/replace, hash-only custody, restart
persistence, revoke cascades), listener (digest auth, digest-as-secret
rejected, no-verifier fail-closed), host dispatch (doorbell, coalescing,
revoke race, failure re-arm), deployment wiring (real loopback end-to-end,
per-app grant fail-closed, live revoke, mid-session first-mint bind), and
service (cap denial, ingress-grant denial, owner pinning, reveal-once,
Unavailable without vault).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…heck, oracle-free denial, type resolve

The /security-review + /pentester gates on #352 found five issues; all are
fixed here and the adversarial probes are kept as regression tests.

C1 (Medium) — a revoked `network.ingress` left live endpoints pulling. The
ingress route table re-derives only on an automation-entity write or an
explicit rehydrate(), and `ledger:revoke` did neither, so a user who revoked
the grant in Settings kept an endpoint that answered 202 AND still ran
`connectors.sync` (provider egress + entity writes) indefinitely. Closed twice
over: `AutomationsHost` now re-checks the grant against the live ledger before
every webhook-driven sync (fail-closed on throw, webhook path only — Connector-4's
scheduled mapping fires are untouched), and `ledger:revoke` re-derives the route
table so the URL actually 404s. Red-checked by reverting the first half.

C2 (Low) — `Invalid: unknown mapping` vs `Denied: does not own mapping` gave
any `connectors.webhook` holder a cross-app mapping-existence oracle. Now one
uniform Denied.

C3 (Low) — `resolveMapping` is purely structural, so an endpoint could be
minted against any entity carrying `accountRef` + `pull`. `resolveMappingOwner`
now asserts SYNC_MAPPING_TYPE_URL server-side.

C4 (Low) — `mint`'s DELETE+INSERT was unwrapped; a failed insert destroyed the
old endpoint with no replacement. Now in a transaction.

C5 (Low, process) — the branch did not typecheck (2x TS2349 in the Connector-6
tests: a `let` assigned only inside a callback narrows to null). CI would have
rejected the PR.

+19 adversarial probes (connector-webhook-attack.test.ts x2) covering
owner-pinning bypass, forged envelope.app, digest-as-secret replay,
timingSafeEqual length confusion, 9 path-traversal/injection shapes,
byte-identical 404 differentiation, the method oracle, oversized doorbell
bodies, WebhookTargetKind confusion in both directions, coalescer starvation
and cross-mapping fairness, and hostile argument shapes incl. __proto__.

87 files / 901 tests green; lint + typecheck clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@th3-br41n
th3-br41n merged commit 6ddc697 into main Jul 29, 2026
2 of 3 checks passed
@th3-br41n
th3-br41n deleted the feat/connector-6-webhook-in branch July 29, 2026 10:29
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.

1 participant