Skip to content

Webhook fan-out matches subscriptions by object name only — on a walled deployment one organization's record events reach another organization's webhook endpoints #13566

Description

@os-steve

Measurement first

AutoEnqueuer.handleEvent / handleBulkEvent (packages/plugins/plugin-webhooks/src/auto-enqueuer.ts) select the subscriptions to deliver to as:

const subs = [
    ...(this.subscriptions.get(event.object) ?? []),
    ...(this.subscriptions.get('*') ?? []),
];

Object name and trigger are the ONLY match terms. There is no organization term anywhere in the match, and there cannot be one today from the event side:

  • RealtimeEventPayload (packages/spec/src/contracts/realtime-service.ts) has no organization member;
  • the DataEvent payload (packages/spec/src/api/events.zod.ts) has no organization member either (grep for organization over that file: zero hits);
  • the delivered body embeds the full record under after for create/update events.

sys_webhook itself IS organization-scoped (#8554 — org-unique name, kernel-provisioned organization_id).

Consequence, if the walled-deployment posture is in scope for webhooks

On OS_TENANCY_POSTURE=isolated|group, organization A's webhook on contact receives organization B's contact creations — full record data, delivered to A's URL, signed with A's secret. That would be the same cross-organization family as the redeliver() wall (the stamping card, currently in repair), but on the fan-out side and arguably wider: it is not a replay of an existing row, it is first delivery of another tenant's record content.

Not graded here — it is possible sys_webhook authoring is deliberately global/admin-only on walled deployments and this is an accepted shape; that is a triage question, and the answer should be recorded either way (declared ≠ enforced cuts both directions).

What already exists toward a repair

The stamping card's repair (#13546, PR #13565) caches the subscription's own organization on CachedSubscription.organizationId — the subscription half of any future filter. The event half is the missing piece: either an organization on the published DataEvent (producer-side threading at the engine's publish site), or a fan-out-side resolution of the record's organization. Producer-side threading is the contract-first direction; a fan-out-side lookup per event would add a read to the hot path the enqueuer exists to keep O(1).

Measured while implementing the stamping repair; filed separately so it does not live only in that PR's margins. Unassigned, no labels — for triage.

Generated by Claude Code

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions