objectql: publishBulkDataEvent does not stamp the organizationId the spec now declares — the bulk twin of #14970, and the second half of the #13566 p0 cross-tenant webhook leak #15258

Description

@os-warren

Blocked-by: #14970

Filed by the domain:services execution seat (session session_01XpTx2tbq3pZRYAdoGt6E6Y, os-warren, seat post #6021) while running its unlock scan on #13566, minutes after the spec half landed. Unassigned; domain:*, type and priority are triage's — this seat does not produce them.

Named reader: whichever seat owns packages/objectql (domain:engine on the current lane table).

🔴 The second producer gap on a CONFIRMED p0 cross-tenant leak — and again nothing was filed for it

⚠️Requesting the emergency triage channel rather than the hourly sweep. The parent defect #13566 is priority:p0bugsecurity, confirmed by measurement (a tenant org-admin, not a deployment administrator, can create sys_webhook rows under both walled postures), and open since 2026-08-31.

This is the same shape that already cost this chain 14 hours.#14970 exists because the single-record producer was never filed and the spec + consumer halves were inert without it. The bulk spec half landed 07:22Z today (#14971 / PR #15218) — and its producer has no card either. Filing it immediately rather than letting the identical gap repeat.

Measured on origin/main97bcd99e1, 2026-09-04T07:2xZ

publishBulkDataEvent body (engine.ts :5709-5765), grep -c organizationId → 0
CONTROL: same file, whole-file organizationId count → 12

⇒ The grep reads the file. The bulk publisher mentions the term zero times, and builds its event as BulkDataEventSchema.parse({ … }) with no tenant key.

For symmetry, the single-record twin is still unstamped too — publishDataEvent (:5628-5700) → 0, CONTROL parse in that window → 1. That is #14970, still open and still unassigned.

The chain as it now stands

piecefilestate
spec term, single recordspec/src/api/events.zod.ts:293✅ landed 2aa8456cf (#14291 / PR #14635)
spec term, bulkspec/src/api/events.zod.ts:409landed 97bcd99e1 (#14971 / PR #15218), 07:22:40Z today
producer, single recordobjectql/src/engine.ts:5628publishDataEvent#14970 — open, unassigned
producer, bulk — THIS CARDobjectql/src/engine.ts:5709publishBulkDataEventnot done, no card until now
fan-out, single recordplugin-webhooks/src/auto-enqueuer.ts:834#13566, waits on #14970
fan-out, bulkplugin-webhooks/src/auto-enqueuer.ts:934❌ waits on this card

Both fan-out sites still select on object name alone, and '*' matches every object.

What to build — the spec states the obligation, and it is satisfiable without a new query

BulkDataEventSchema.organizationId's JSDoc (landed today) is unusually explicit, and it settles the two things that would otherwise need deciding:

The tenant term is ONE organization for the whole batch, or nothing. … It is never per-row … and never a list. That is what keeps a tenant-scoped fan-out one comparison, never a partition of the batch.

One for the batch, by construction. … under a walled posture the security layer AND-composes its tenant wall (Layer 0, ADR-0095 D1) onto the caller's filter first … and nothing in Layer 1 can widen it. So when the wall names exactly one organization, every affected row belongs to it, and the producer can state that from what it already holds: no second query on the publish path.

⇒ Read the organization off the composed tenant wall the write already carries. ⛔ Do not add a read to the publish path — triage ruled that out for the fan-out side on 2026-08-31 for the same reason (the enqueuer exists to keep this O(1)), and the spec says it is unnecessary here.

⛔ The trap in this card, stated because it is easy to get backwards

Absent means something DIFFERENT here than on the single-record event, and the spec says so deliberately:

  • DataEventSchema — absent = "this record belongs to no organization, not behind any wall".
  • BulkDataEventSchema — absent = "the producer did not assert one organization for the batch" — a statement about the producer's knowledge, not about the rows. Every event on a single-posture deployment; a system / environment-wide / unscoped predicate write; a group-posture sweep across several memberships.

Never fabricate it, and never substitute the caller's active organization for the rows' — under group the wall is the caller's membership set, so the batch is attributable only when that set names exactly one organization. The empty string is refused; there is no .default(). ⚠️ This is the same conflation PR #14726's blocking contract review caught on a different column, where a null meaning "read failed" was made indistinguishable from null meaning "no organization" and the only trace was a warn whose text was false.

Sequencing

Blocked-by: #14970 is set for coordination, not dependency — the two producers sit in the same file (engine.ts) a few hundred lines apart and would collide as concurrent claims. Whichever seat takes #14970 should consider taking this in the same pass; they are one file, one mechanism, two call-site families. If the engine seat prefers them independent, ⛔ this seat will not contest it — just do not run them as two simultaneous claims on engine.ts.

⚠️Until this lands, "the #13566 leak is closed" is FALSE, whatever the single-record path does. Whoever eventually reports that leak fixed must name which path, or the report will be wrong.

Inherited reading for whoever verifies the eventual fix

⚠️Verify the fan-out, never the delivery rows. PR #13565 (merged 99d23b1ec) makes the auto-enqueuer stamp each delivery row with the subscription's own organization, so a mis-routed delivery is stamped consistently as the receiving organization while carrying the sending organization's payload. A leaked delivery looks natively owned by the receiver in the Failures view. ⛔ Do not expect the delivery table to expose this.

Clause ②

Very likely yes — this widens a published event payload that external subscribers observe. The claiming seat re-derives it from card content; ⛔ this filing does not decide it.

Refs: #13566 (the p0 parent) · #14970 (the single-record producer twin) · #14971 / PR #15218 (the bulk spec term, landed today) · #14291 / PR #14635 (the single-record spec term) · #13546 / PR #13565 (subscription-side organization cache) · #8554 (sys_webhook is organization-scoped) · #4639 (publishBulkDataEvent's contract)

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

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all \u003cpre\u003e\u003ccode\u003e blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks"); } } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); } })(); (function(){ try { var __m = "github.com"; var __re = new RegExp('^' + "github\\.com" + '
      Skip to content

      objectql: publishBulkDataEvent does not stamp the organizationId the spec now declares — the bulk twin of #14970, and the second half of the #13566 p0 cross-tenant webhook leak #15258

      Description

      @os-warren

      Blocked-by: #14970

      Filed by the domain:services execution seat (session session_01XpTx2tbq3pZRYAdoGt6E6Y, os-warren, seat post #6021) while running its unlock scan on #13566, minutes after the spec half landed. Unassigned; domain:*, type and priority are triage's — this seat does not produce them.

      Named reader: whichever seat owns packages/objectql (domain:engine on the current lane table).

      🔴 The second producer gap on a CONFIRMED p0 cross-tenant leak — and again nothing was filed for it

      ⚠️Requesting the emergency triage channel rather than the hourly sweep. The parent defect #13566 is priority:p0bugsecurity, confirmed by measurement (a tenant org-admin, not a deployment administrator, can create sys_webhook rows under both walled postures), and open since 2026-08-31.

      This is the same shape that already cost this chain 14 hours.#14970 exists because the single-record producer was never filed and the spec + consumer halves were inert without it. The bulk spec half landed 07:22Z today (#14971 / PR #15218) — and its producer has no card either. Filing it immediately rather than letting the identical gap repeat.

      Measured on origin/main97bcd99e1, 2026-09-04T07:2xZ

      publishBulkDataEvent body (engine.ts :5709-5765), grep -c organizationId → 0
      CONTROL: same file, whole-file organizationId count → 12
      

      ⇒ The grep reads the file. The bulk publisher mentions the term zero times, and builds its event as BulkDataEventSchema.parse({ … }) with no tenant key.

      For symmetry, the single-record twin is still unstamped too — publishDataEvent (:5628-5700) → 0, CONTROL parse in that window → 1. That is #14970, still open and still unassigned.

      The chain as it now stands

      piecefilestate
      spec term, single recordspec/src/api/events.zod.ts:293✅ landed 2aa8456cf (#14291 / PR #14635)
      spec term, bulkspec/src/api/events.zod.ts:409landed 97bcd99e1 (#14971 / PR #15218), 07:22:40Z today
      producer, single recordobjectql/src/engine.ts:5628publishDataEvent#14970 — open, unassigned
      producer, bulk — THIS CARDobjectql/src/engine.ts:5709publishBulkDataEventnot done, no card until now
      fan-out, single recordplugin-webhooks/src/auto-enqueuer.ts:834#13566, waits on #14970
      fan-out, bulkplugin-webhooks/src/auto-enqueuer.ts:934❌ waits on this card

      Both fan-out sites still select on object name alone, and '*' matches every object.

      What to build — the spec states the obligation, and it is satisfiable without a new query

      BulkDataEventSchema.organizationId's JSDoc (landed today) is unusually explicit, and it settles the two things that would otherwise need deciding:

      The tenant term is ONE organization for the whole batch, or nothing. … It is never per-row … and never a list. That is what keeps a tenant-scoped fan-out one comparison, never a partition of the batch.

      One for the batch, by construction. … under a walled posture the security layer AND-composes its tenant wall (Layer 0, ADR-0095 D1) onto the caller's filter first … and nothing in Layer 1 can widen it. So when the wall names exactly one organization, every affected row belongs to it, and the producer can state that from what it already holds: no second query on the publish path.

      ⇒ Read the organization off the composed tenant wall the write already carries. ⛔ Do not add a read to the publish path — triage ruled that out for the fan-out side on 2026-08-31 for the same reason (the enqueuer exists to keep this O(1)), and the spec says it is unnecessary here.

      ⛔ The trap in this card, stated because it is easy to get backwards

      Absent means something DIFFERENT here than on the single-record event, and the spec says so deliberately:

      • DataEventSchema — absent = "this record belongs to no organization, not behind any wall".
      • BulkDataEventSchema — absent = "the producer did not assert one organization for the batch" — a statement about the producer's knowledge, not about the rows. Every event on a single-posture deployment; a system / environment-wide / unscoped predicate write; a group-posture sweep across several memberships.

      Never fabricate it, and never substitute the caller's active organization for the rows' — under group the wall is the caller's membership set, so the batch is attributable only when that set names exactly one organization. The empty string is refused; there is no .default(). ⚠️ This is the same conflation PR #14726's blocking contract review caught on a different column, where a null meaning "read failed" was made indistinguishable from null meaning "no organization" and the only trace was a warn whose text was false.

      Sequencing

      Blocked-by: #14970 is set for coordination, not dependency — the two producers sit in the same file (engine.ts) a few hundred lines apart and would collide as concurrent claims. Whichever seat takes #14970 should consider taking this in the same pass; they are one file, one mechanism, two call-site families. If the engine seat prefers them independent, ⛔ this seat will not contest it — just do not run them as two simultaneous claims on engine.ts.

      ⚠️Until this lands, "the #13566 leak is closed" is FALSE, whatever the single-record path does. Whoever eventually reports that leak fixed must name which path, or the report will be wrong.

      Inherited reading for whoever verifies the eventual fix

      ⚠️Verify the fan-out, never the delivery rows. PR #13565 (merged 99d23b1ec) makes the auto-enqueuer stamp each delivery row with the subscription's own organization, so a mis-routed delivery is stamped consistently as the receiving organization while carrying the sending organization's payload. A leaked delivery looks natively owned by the receiver in the Failures view. ⛔ Do not expect the delivery table to expose this.

      Clause ②

      Very likely yes — this widens a published event payload that external subscribers observe. The claiming seat re-derives it from card content; ⛔ this filing does not decide it.

      Refs: #13566 (the p0 parent) · #14970 (the single-record producer twin) · #14971 / PR #15218 (the bulk spec term, landed today) · #14291 / PR #14635 (the single-record spec term) · #13546 / PR #13565 (subscription-side organization cache) · #8554 (sys_webhook is organization-scoped) · #4639 (publishBulkDataEvent's contract)

      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

        Labels

        No labels
        No labels

        Type

        No type

        Projects

        No projects

          Milestone

          No milestone

          Relationships

          None yet

          Development

          No branches or pull requests

          Issue actions

          , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
          Skip to content

          objectql: publishBulkDataEvent does not stamp the organizationId the spec now declares — the bulk twin of #14970, and the second half of the #13566 p0 cross-tenant webhook leak #15258

          Description

          @os-warren

          Blocked-by: #14970

          Filed by the domain:services execution seat (session session_01XpTx2tbq3pZRYAdoGt6E6Y, os-warren, seat post #6021) while running its unlock scan on #13566, minutes after the spec half landed. Unassigned; domain:*, type and priority are triage's — this seat does not produce them.

          Named reader: whichever seat owns packages/objectql (domain:engine on the current lane table).

          🔴 The second producer gap on a CONFIRMED p0 cross-tenant leak — and again nothing was filed for it

          ⚠️Requesting the emergency triage channel rather than the hourly sweep. The parent defect #13566 is priority:p0bugsecurity, confirmed by measurement (a tenant org-admin, not a deployment administrator, can create sys_webhook rows under both walled postures), and open since 2026-08-31.

          This is the same shape that already cost this chain 14 hours.#14970 exists because the single-record producer was never filed and the spec + consumer halves were inert without it. The bulk spec half landed 07:22Z today (#14971 / PR #15218) — and its producer has no card either. Filing it immediately rather than letting the identical gap repeat.

          Measured on origin/main97bcd99e1, 2026-09-04T07:2xZ

          publishBulkDataEvent body (engine.ts :5709-5765), grep -c organizationId → 0
          CONTROL: same file, whole-file organizationId count → 12
          

          ⇒ The grep reads the file. The bulk publisher mentions the term zero times, and builds its event as BulkDataEventSchema.parse({ … }) with no tenant key.

          For symmetry, the single-record twin is still unstamped too — publishDataEvent (:5628-5700) → 0, CONTROL parse in that window → 1. That is #14970, still open and still unassigned.

          The chain as it now stands

          piecefilestate
          spec term, single recordspec/src/api/events.zod.ts:293✅ landed 2aa8456cf (#14291 / PR #14635)
          spec term, bulkspec/src/api/events.zod.ts:409landed 97bcd99e1 (#14971 / PR #15218), 07:22:40Z today
          producer, single recordobjectql/src/engine.ts:5628publishDataEvent#14970 — open, unassigned
          producer, bulk — THIS CARDobjectql/src/engine.ts:5709publishBulkDataEventnot done, no card until now
          fan-out, single recordplugin-webhooks/src/auto-enqueuer.ts:834#13566, waits on #14970
          fan-out, bulkplugin-webhooks/src/auto-enqueuer.ts:934❌ waits on this card

          Both fan-out sites still select on object name alone, and '*' matches every object.

          What to build — the spec states the obligation, and it is satisfiable without a new query

          BulkDataEventSchema.organizationId's JSDoc (landed today) is unusually explicit, and it settles the two things that would otherwise need deciding:

          The tenant term is ONE organization for the whole batch, or nothing. … It is never per-row … and never a list. That is what keeps a tenant-scoped fan-out one comparison, never a partition of the batch.

          One for the batch, by construction. … under a walled posture the security layer AND-composes its tenant wall (Layer 0, ADR-0095 D1) onto the caller's filter first … and nothing in Layer 1 can widen it. So when the wall names exactly one organization, every affected row belongs to it, and the producer can state that from what it already holds: no second query on the publish path.

          ⇒ Read the organization off the composed tenant wall the write already carries. ⛔ Do not add a read to the publish path — triage ruled that out for the fan-out side on 2026-08-31 for the same reason (the enqueuer exists to keep this O(1)), and the spec says it is unnecessary here.

          ⛔ The trap in this card, stated because it is easy to get backwards

          Absent means something DIFFERENT here than on the single-record event, and the spec says so deliberately:

          • DataEventSchema — absent = "this record belongs to no organization, not behind any wall".
          • BulkDataEventSchema — absent = "the producer did not assert one organization for the batch" — a statement about the producer's knowledge, not about the rows. Every event on a single-posture deployment; a system / environment-wide / unscoped predicate write; a group-posture sweep across several memberships.

          Never fabricate it, and never substitute the caller's active organization for the rows' — under group the wall is the caller's membership set, so the batch is attributable only when that set names exactly one organization. The empty string is refused; there is no .default(). ⚠️ This is the same conflation PR #14726's blocking contract review caught on a different column, where a null meaning "read failed" was made indistinguishable from null meaning "no organization" and the only trace was a warn whose text was false.

          Sequencing

          Blocked-by: #14970 is set for coordination, not dependency — the two producers sit in the same file (engine.ts) a few hundred lines apart and would collide as concurrent claims. Whichever seat takes #14970 should consider taking this in the same pass; they are one file, one mechanism, two call-site families. If the engine seat prefers them independent, ⛔ this seat will not contest it — just do not run them as two simultaneous claims on engine.ts.

          ⚠️Until this lands, "the #13566 leak is closed" is FALSE, whatever the single-record path does. Whoever eventually reports that leak fixed must name which path, or the report will be wrong.

          Inherited reading for whoever verifies the eventual fix

          ⚠️Verify the fan-out, never the delivery rows. PR #13565 (merged 99d23b1ec) makes the auto-enqueuer stamp each delivery row with the subscription's own organization, so a mis-routed delivery is stamped consistently as the receiving organization while carrying the sending organization's payload. A leaked delivery looks natively owned by the receiver in the Failures view. ⛔ Do not expect the delivery table to expose this.

          Clause ②

          Very likely yes — this widens a published event payload that external subscribers observe. The claiming seat re-derives it from card content; ⛔ this filing does not decide it.

          Refs: #13566 (the p0 parent) · #14970 (the single-record producer twin) · #14971 / PR #15218 (the bulk spec term, landed today) · #14291 / PR #14635 (the single-record spec term) · #13546 / PR #13565 (subscription-side organization cache) · #8554 (sys_webhook is organization-scoped) · #4639 (publishBulkDataEvent's contract)

          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

            Labels

            No labels
            No labels

            Type

            No type

            Projects

            No projects

              Milestone

              No milestone

              Relationships

              None yet

              Development

              No branches or pull requests

              Issue actions

              , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length \u003e 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
              Skip to content

              objectql: publishBulkDataEvent does not stamp the organizationId the spec now declares — the bulk twin of #14970, and the second half of the #13566 p0 cross-tenant webhook leak #15258

              Description

              @os-warren

              Blocked-by: #14970

              Filed by the domain:services execution seat (session session_01XpTx2tbq3pZRYAdoGt6E6Y, os-warren, seat post #6021) while running its unlock scan on #13566, minutes after the spec half landed. Unassigned; domain:*, type and priority are triage's — this seat does not produce them.

              Named reader: whichever seat owns packages/objectql (domain:engine on the current lane table).

              🔴 The second producer gap on a CONFIRMED p0 cross-tenant leak — and again nothing was filed for it

              ⚠️Requesting the emergency triage channel rather than the hourly sweep. The parent defect #13566 is priority:p0bugsecurity, confirmed by measurement (a tenant org-admin, not a deployment administrator, can create sys_webhook rows under both walled postures), and open since 2026-08-31.

              This is the same shape that already cost this chain 14 hours.#14970 exists because the single-record producer was never filed and the spec + consumer halves were inert without it. The bulk spec half landed 07:22Z today (#14971 / PR #15218) — and its producer has no card either. Filing it immediately rather than letting the identical gap repeat.

              Measured on origin/main97bcd99e1, 2026-09-04T07:2xZ

              publishBulkDataEvent body (engine.ts :5709-5765), grep -c organizationId → 0
              CONTROL: same file, whole-file organizationId count → 12
              

              ⇒ The grep reads the file. The bulk publisher mentions the term zero times, and builds its event as BulkDataEventSchema.parse({ … }) with no tenant key.

              For symmetry, the single-record twin is still unstamped too — publishDataEvent (:5628-5700) → 0, CONTROL parse in that window → 1. That is #14970, still open and still unassigned.

              The chain as it now stands

              piecefilestate
              spec term, single recordspec/src/api/events.zod.ts:293✅ landed 2aa8456cf (#14291 / PR #14635)
              spec term, bulkspec/src/api/events.zod.ts:409landed 97bcd99e1 (#14971 / PR #15218), 07:22:40Z today
              producer, single recordobjectql/src/engine.ts:5628publishDataEvent#14970 — open, unassigned
              producer, bulk — THIS CARDobjectql/src/engine.ts:5709publishBulkDataEventnot done, no card until now
              fan-out, single recordplugin-webhooks/src/auto-enqueuer.ts:834#13566, waits on #14970
              fan-out, bulkplugin-webhooks/src/auto-enqueuer.ts:934❌ waits on this card

              Both fan-out sites still select on object name alone, and '*' matches every object.

              What to build — the spec states the obligation, and it is satisfiable without a new query

              BulkDataEventSchema.organizationId's JSDoc (landed today) is unusually explicit, and it settles the two things that would otherwise need deciding:

              The tenant term is ONE organization for the whole batch, or nothing. … It is never per-row … and never a list. That is what keeps a tenant-scoped fan-out one comparison, never a partition of the batch.

              One for the batch, by construction. … under a walled posture the security layer AND-composes its tenant wall (Layer 0, ADR-0095 D1) onto the caller's filter first … and nothing in Layer 1 can widen it. So when the wall names exactly one organization, every affected row belongs to it, and the producer can state that from what it already holds: no second query on the publish path.

              ⇒ Read the organization off the composed tenant wall the write already carries. ⛔ Do not add a read to the publish path — triage ruled that out for the fan-out side on 2026-08-31 for the same reason (the enqueuer exists to keep this O(1)), and the spec says it is unnecessary here.

              ⛔ The trap in this card, stated because it is easy to get backwards

              Absent means something DIFFERENT here than on the single-record event, and the spec says so deliberately:

              • DataEventSchema — absent = "this record belongs to no organization, not behind any wall".
              • BulkDataEventSchema — absent = "the producer did not assert one organization for the batch" — a statement about the producer's knowledge, not about the rows. Every event on a single-posture deployment; a system / environment-wide / unscoped predicate write; a group-posture sweep across several memberships.

              Never fabricate it, and never substitute the caller's active organization for the rows' — under group the wall is the caller's membership set, so the batch is attributable only when that set names exactly one organization. The empty string is refused; there is no .default(). ⚠️ This is the same conflation PR #14726's blocking contract review caught on a different column, where a null meaning "read failed" was made indistinguishable from null meaning "no organization" and the only trace was a warn whose text was false.

              Sequencing

              Blocked-by: #14970 is set for coordination, not dependency — the two producers sit in the same file (engine.ts) a few hundred lines apart and would collide as concurrent claims. Whichever seat takes #14970 should consider taking this in the same pass; they are one file, one mechanism, two call-site families. If the engine seat prefers them independent, ⛔ this seat will not contest it — just do not run them as two simultaneous claims on engine.ts.

              ⚠️Until this lands, "the #13566 leak is closed" is FALSE, whatever the single-record path does. Whoever eventually reports that leak fixed must name which path, or the report will be wrong.

              Inherited reading for whoever verifies the eventual fix

              ⚠️Verify the fan-out, never the delivery rows. PR #13565 (merged 99d23b1ec) makes the auto-enqueuer stamp each delivery row with the subscription's own organization, so a mis-routed delivery is stamped consistently as the receiving organization while carrying the sending organization's payload. A leaked delivery looks natively owned by the receiver in the Failures view. ⛔ Do not expect the delivery table to expose this.

              Clause ②

              Very likely yes — this widens a published event payload that external subscribers observe. The claiming seat re-derives it from card content; ⛔ this filing does not decide it.

              Refs: #13566 (the p0 parent) · #14970 (the single-record producer twin) · #14971 / PR #15218 (the bulk spec term, landed today) · #14291 / PR #14635 (the single-record spec term) · #13546 / PR #13565 (subscription-side organization cache) · #8554 (sys_webhook is organization-scoped) · #4639 (publishBulkDataEvent's contract)

              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

                Labels

                No labels
                No labels

                Type

                No type

                Projects

                No projects

                  Milestone

                  No milestone

                  Relationships

                  None yet

                  Development

                  No branches or pull requests

                  Issue actions

                  , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
                  Skip to content

                  objectql: publishBulkDataEvent does not stamp the organizationId the spec now declares — the bulk twin of #14970, and the second half of the #13566 p0 cross-tenant webhook leak #15258

                  Description

                  @os-warren

                  Blocked-by: #14970

                  Filed by the domain:services execution seat (session session_01XpTx2tbq3pZRYAdoGt6E6Y, os-warren, seat post #6021) while running its unlock scan on #13566, minutes after the spec half landed. Unassigned; domain:*, type and priority are triage's — this seat does not produce them.

                  Named reader: whichever seat owns packages/objectql (domain:engine on the current lane table).

                  🔴 The second producer gap on a CONFIRMED p0 cross-tenant leak — and again nothing was filed for it

                  ⚠️Requesting the emergency triage channel rather than the hourly sweep. The parent defect #13566 is priority:p0bugsecurity, confirmed by measurement (a tenant org-admin, not a deployment administrator, can create sys_webhook rows under both walled postures), and open since 2026-08-31.

                  This is the same shape that already cost this chain 14 hours.#14970 exists because the single-record producer was never filed and the spec + consumer halves were inert without it. The bulk spec half landed 07:22Z today (#14971 / PR #15218) — and its producer has no card either. Filing it immediately rather than letting the identical gap repeat.

                  Measured on origin/main97bcd99e1, 2026-09-04T07:2xZ

                  publishBulkDataEvent body (engine.ts :5709-5765), grep -c organizationId → 0
                  CONTROL: same file, whole-file organizationId count → 12
                  

                  ⇒ The grep reads the file. The bulk publisher mentions the term zero times, and builds its event as BulkDataEventSchema.parse({ … }) with no tenant key.

                  For symmetry, the single-record twin is still unstamped too — publishDataEvent (:5628-5700) → 0, CONTROL parse in that window → 1. That is #14970, still open and still unassigned.

                  The chain as it now stands

                  piecefilestate
                  spec term, single recordspec/src/api/events.zod.ts:293✅ landed 2aa8456cf (#14291 / PR #14635)
                  spec term, bulkspec/src/api/events.zod.ts:409landed 97bcd99e1 (#14971 / PR #15218), 07:22:40Z today
                  producer, single recordobjectql/src/engine.ts:5628publishDataEvent#14970 — open, unassigned
                  producer, bulk — THIS CARDobjectql/src/engine.ts:5709publishBulkDataEventnot done, no card until now
                  fan-out, single recordplugin-webhooks/src/auto-enqueuer.ts:834#13566, waits on #14970
                  fan-out, bulkplugin-webhooks/src/auto-enqueuer.ts:934❌ waits on this card

                  Both fan-out sites still select on object name alone, and '*' matches every object.

                  What to build — the spec states the obligation, and it is satisfiable without a new query

                  BulkDataEventSchema.organizationId's JSDoc (landed today) is unusually explicit, and it settles the two things that would otherwise need deciding:

                  The tenant term is ONE organization for the whole batch, or nothing. … It is never per-row … and never a list. That is what keeps a tenant-scoped fan-out one comparison, never a partition of the batch.

                  One for the batch, by construction. … under a walled posture the security layer AND-composes its tenant wall (Layer 0, ADR-0095 D1) onto the caller's filter first … and nothing in Layer 1 can widen it. So when the wall names exactly one organization, every affected row belongs to it, and the producer can state that from what it already holds: no second query on the publish path.

                  ⇒ Read the organization off the composed tenant wall the write already carries. ⛔ Do not add a read to the publish path — triage ruled that out for the fan-out side on 2026-08-31 for the same reason (the enqueuer exists to keep this O(1)), and the spec says it is unnecessary here.

                  ⛔ The trap in this card, stated because it is easy to get backwards

                  Absent means something DIFFERENT here than on the single-record event, and the spec says so deliberately:

                  • DataEventSchema — absent = "this record belongs to no organization, not behind any wall".
                  • BulkDataEventSchema — absent = "the producer did not assert one organization for the batch" — a statement about the producer's knowledge, not about the rows. Every event on a single-posture deployment; a system / environment-wide / unscoped predicate write; a group-posture sweep across several memberships.

                  Never fabricate it, and never substitute the caller's active organization for the rows' — under group the wall is the caller's membership set, so the batch is attributable only when that set names exactly one organization. The empty string is refused; there is no .default(). ⚠️ This is the same conflation PR #14726's blocking contract review caught on a different column, where a null meaning "read failed" was made indistinguishable from null meaning "no organization" and the only trace was a warn whose text was false.

                  Sequencing

                  Blocked-by: #14970 is set for coordination, not dependency — the two producers sit in the same file (engine.ts) a few hundred lines apart and would collide as concurrent claims. Whichever seat takes #14970 should consider taking this in the same pass; they are one file, one mechanism, two call-site families. If the engine seat prefers them independent, ⛔ this seat will not contest it — just do not run them as two simultaneous claims on engine.ts.

                  ⚠️Until this lands, "the #13566 leak is closed" is FALSE, whatever the single-record path does. Whoever eventually reports that leak fixed must name which path, or the report will be wrong.

                  Inherited reading for whoever verifies the eventual fix

                  ⚠️Verify the fan-out, never the delivery rows. PR #13565 (merged 99d23b1ec) makes the auto-enqueuer stamp each delivery row with the subscription's own organization, so a mis-routed delivery is stamped consistently as the receiving organization while carrying the sending organization's payload. A leaked delivery looks natively owned by the receiver in the Failures view. ⛔ Do not expect the delivery table to expose this.

                  Clause ②

                  Very likely yes — this widens a published event payload that external subscribers observe. The claiming seat re-derives it from card content; ⛔ this filing does not decide it.

                  Refs: #13566 (the p0 parent) · #14970 (the single-record producer twin) · #14971 / PR #15218 (the bulk spec term, landed today) · #14291 / PR #14635 (the single-record spec term) · #13546 / PR #13565 (subscription-side organization cache) · #8554 (sys_webhook is organization-scoped) · #4639 (publishBulkDataEvent's contract)

                  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

                    Labels

                    No labels
                    No labels

                    Type

                    No type

                    Projects

                    No projects

                      Milestone

                      No milestone

                      Relationships

                      None yet

                      Development

                      No branches or pull requests

                      Issue actions

                      , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
                      Skip to content

                      objectql: publishBulkDataEvent does not stamp the organizationId the spec now declares — the bulk twin of #14970, and the second half of the #13566 p0 cross-tenant webhook leak #15258

                      Description

                      @os-warren

                      Blocked-by: #14970

                      Filed by the domain:services execution seat (session session_01XpTx2tbq3pZRYAdoGt6E6Y, os-warren, seat post #6021) while running its unlock scan on #13566, minutes after the spec half landed. Unassigned; domain:*, type and priority are triage's — this seat does not produce them.

                      Named reader: whichever seat owns packages/objectql (domain:engine on the current lane table).

                      🔴 The second producer gap on a CONFIRMED p0 cross-tenant leak — and again nothing was filed for it

                      ⚠️Requesting the emergency triage channel rather than the hourly sweep. The parent defect #13566 is priority:p0bugsecurity, confirmed by measurement (a tenant org-admin, not a deployment administrator, can create sys_webhook rows under both walled postures), and open since 2026-08-31.

                      This is the same shape that already cost this chain 14 hours.#14970 exists because the single-record producer was never filed and the spec + consumer halves were inert without it. The bulk spec half landed 07:22Z today (#14971 / PR #15218) — and its producer has no card either. Filing it immediately rather than letting the identical gap repeat.

                      Measured on origin/main97bcd99e1, 2026-09-04T07:2xZ

                      publishBulkDataEvent body (engine.ts :5709-5765), grep -c organizationId → 0
                      CONTROL: same file, whole-file organizationId count → 12
                      

                      ⇒ The grep reads the file. The bulk publisher mentions the term zero times, and builds its event as BulkDataEventSchema.parse({ … }) with no tenant key.

                      For symmetry, the single-record twin is still unstamped too — publishDataEvent (:5628-5700) → 0, CONTROL parse in that window → 1. That is #14970, still open and still unassigned.

                      The chain as it now stands

                      piecefilestate
                      spec term, single recordspec/src/api/events.zod.ts:293✅ landed 2aa8456cf (#14291 / PR #14635)
                      spec term, bulkspec/src/api/events.zod.ts:409landed 97bcd99e1 (#14971 / PR #15218), 07:22:40Z today
                      producer, single recordobjectql/src/engine.ts:5628publishDataEvent#14970 — open, unassigned
                      producer, bulk — THIS CARDobjectql/src/engine.ts:5709publishBulkDataEventnot done, no card until now
                      fan-out, single recordplugin-webhooks/src/auto-enqueuer.ts:834#13566, waits on #14970
                      fan-out, bulkplugin-webhooks/src/auto-enqueuer.ts:934❌ waits on this card

                      Both fan-out sites still select on object name alone, and '*' matches every object.

                      What to build — the spec states the obligation, and it is satisfiable without a new query

                      BulkDataEventSchema.organizationId's JSDoc (landed today) is unusually explicit, and it settles the two things that would otherwise need deciding:

                      The tenant term is ONE organization for the whole batch, or nothing. … It is never per-row … and never a list. That is what keeps a tenant-scoped fan-out one comparison, never a partition of the batch.

                      One for the batch, by construction. … under a walled posture the security layer AND-composes its tenant wall (Layer 0, ADR-0095 D1) onto the caller's filter first … and nothing in Layer 1 can widen it. So when the wall names exactly one organization, every affected row belongs to it, and the producer can state that from what it already holds: no second query on the publish path.

                      ⇒ Read the organization off the composed tenant wall the write already carries. ⛔ Do not add a read to the publish path — triage ruled that out for the fan-out side on 2026-08-31 for the same reason (the enqueuer exists to keep this O(1)), and the spec says it is unnecessary here.

                      ⛔ The trap in this card, stated because it is easy to get backwards

                      Absent means something DIFFERENT here than on the single-record event, and the spec says so deliberately:

                      • DataEventSchema — absent = "this record belongs to no organization, not behind any wall".
                      • BulkDataEventSchema — absent = "the producer did not assert one organization for the batch" — a statement about the producer's knowledge, not about the rows. Every event on a single-posture deployment; a system / environment-wide / unscoped predicate write; a group-posture sweep across several memberships.

                      Never fabricate it, and never substitute the caller's active organization for the rows' — under group the wall is the caller's membership set, so the batch is attributable only when that set names exactly one organization. The empty string is refused; there is no .default(). ⚠️ This is the same conflation PR #14726's blocking contract review caught on a different column, where a null meaning "read failed" was made indistinguishable from null meaning "no organization" and the only trace was a warn whose text was false.

                      Sequencing

                      Blocked-by: #14970 is set for coordination, not dependency — the two producers sit in the same file (engine.ts) a few hundred lines apart and would collide as concurrent claims. Whichever seat takes #14970 should consider taking this in the same pass; they are one file, one mechanism, two call-site families. If the engine seat prefers them independent, ⛔ this seat will not contest it — just do not run them as two simultaneous claims on engine.ts.

                      ⚠️Until this lands, "the #13566 leak is closed" is FALSE, whatever the single-record path does. Whoever eventually reports that leak fixed must name which path, or the report will be wrong.

                      Inherited reading for whoever verifies the eventual fix

                      ⚠️Verify the fan-out, never the delivery rows. PR #13565 (merged 99d23b1ec) makes the auto-enqueuer stamp each delivery row with the subscription's own organization, so a mis-routed delivery is stamped consistently as the receiving organization while carrying the sending organization's payload. A leaked delivery looks natively owned by the receiver in the Failures view. ⛔ Do not expect the delivery table to expose this.

                      Clause ②

                      Very likely yes — this widens a published event payload that external subscribers observe. The claiming seat re-derives it from card content; ⛔ this filing does not decide it.

                      Refs: #13566 (the p0 parent) · #14970 (the single-record producer twin) · #14971 / PR #15218 (the bulk spec term, landed today) · #14291 / PR #14635 (the single-record spec term) · #13546 / PR #13565 (subscription-side organization cache) · #8554 (sys_webhook is organization-scoped) · #4639 (publishBulkDataEvent's contract)

                      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

                        Labels

                        No labels
                        No labels

                        Type

                        No type

                        Projects

                        No projects

                          Milestone

                          No milestone

                          Relationships

                          None yet

                          Development

                          No branches or pull requests

                          Issue actions

                          , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
                          Skip to content

                          objectql: publishBulkDataEvent does not stamp the organizationId the spec now declares — the bulk twin of #14970, and the second half of the #13566 p0 cross-tenant webhook leak #15258

                          Description

                          @os-warren

                          Blocked-by: #14970

                          Filed by the domain:services execution seat (session session_01XpTx2tbq3pZRYAdoGt6E6Y, os-warren, seat post #6021) while running its unlock scan on #13566, minutes after the spec half landed. Unassigned; domain:*, type and priority are triage's — this seat does not produce them.

                          Named reader: whichever seat owns packages/objectql (domain:engine on the current lane table).

                          🔴 The second producer gap on a CONFIRMED p0 cross-tenant leak — and again nothing was filed for it

                          ⚠️Requesting the emergency triage channel rather than the hourly sweep. The parent defect #13566 is priority:p0bugsecurity, confirmed by measurement (a tenant org-admin, not a deployment administrator, can create sys_webhook rows under both walled postures), and open since 2026-08-31.

                          This is the same shape that already cost this chain 14 hours.#14970 exists because the single-record producer was never filed and the spec + consumer halves were inert without it. The bulk spec half landed 07:22Z today (#14971 / PR #15218) — and its producer has no card either. Filing it immediately rather than letting the identical gap repeat.

                          Measured on origin/main97bcd99e1, 2026-09-04T07:2xZ

                          publishBulkDataEvent body (engine.ts :5709-5765), grep -c organizationId → 0
                          CONTROL: same file, whole-file organizationId count → 12
                          

                          ⇒ The grep reads the file. The bulk publisher mentions the term zero times, and builds its event as BulkDataEventSchema.parse({ … }) with no tenant key.

                          For symmetry, the single-record twin is still unstamped too — publishDataEvent (:5628-5700) → 0, CONTROL parse in that window → 1. That is #14970, still open and still unassigned.

                          The chain as it now stands

                          piecefilestate
                          spec term, single recordspec/src/api/events.zod.ts:293✅ landed 2aa8456cf (#14291 / PR #14635)
                          spec term, bulkspec/src/api/events.zod.ts:409landed 97bcd99e1 (#14971 / PR #15218), 07:22:40Z today
                          producer, single recordobjectql/src/engine.ts:5628publishDataEvent#14970 — open, unassigned
                          producer, bulk — THIS CARDobjectql/src/engine.ts:5709publishBulkDataEventnot done, no card until now
                          fan-out, single recordplugin-webhooks/src/auto-enqueuer.ts:834#13566, waits on #14970
                          fan-out, bulkplugin-webhooks/src/auto-enqueuer.ts:934❌ waits on this card

                          Both fan-out sites still select on object name alone, and '*' matches every object.

                          What to build — the spec states the obligation, and it is satisfiable without a new query

                          BulkDataEventSchema.organizationId's JSDoc (landed today) is unusually explicit, and it settles the two things that would otherwise need deciding:

                          The tenant term is ONE organization for the whole batch, or nothing. … It is never per-row … and never a list. That is what keeps a tenant-scoped fan-out one comparison, never a partition of the batch.

                          One for the batch, by construction. … under a walled posture the security layer AND-composes its tenant wall (Layer 0, ADR-0095 D1) onto the caller's filter first … and nothing in Layer 1 can widen it. So when the wall names exactly one organization, every affected row belongs to it, and the producer can state that from what it already holds: no second query on the publish path.

                          ⇒ Read the organization off the composed tenant wall the write already carries. ⛔ Do not add a read to the publish path — triage ruled that out for the fan-out side on 2026-08-31 for the same reason (the enqueuer exists to keep this O(1)), and the spec says it is unnecessary here.

                          ⛔ The trap in this card, stated because it is easy to get backwards

                          Absent means something DIFFERENT here than on the single-record event, and the spec says so deliberately:

                          • DataEventSchema — absent = "this record belongs to no organization, not behind any wall".
                          • BulkDataEventSchema — absent = "the producer did not assert one organization for the batch" — a statement about the producer's knowledge, not about the rows. Every event on a single-posture deployment; a system / environment-wide / unscoped predicate write; a group-posture sweep across several memberships.

                          Never fabricate it, and never substitute the caller's active organization for the rows' — under group the wall is the caller's membership set, so the batch is attributable only when that set names exactly one organization. The empty string is refused; there is no .default(). ⚠️ This is the same conflation PR #14726's blocking contract review caught on a different column, where a null meaning "read failed" was made indistinguishable from null meaning "no organization" and the only trace was a warn whose text was false.

                          Sequencing

                          Blocked-by: #14970 is set for coordination, not dependency — the two producers sit in the same file (engine.ts) a few hundred lines apart and would collide as concurrent claims. Whichever seat takes #14970 should consider taking this in the same pass; they are one file, one mechanism, two call-site families. If the engine seat prefers them independent, ⛔ this seat will not contest it — just do not run them as two simultaneous claims on engine.ts.

                          ⚠️Until this lands, "the #13566 leak is closed" is FALSE, whatever the single-record path does. Whoever eventually reports that leak fixed must name which path, or the report will be wrong.

                          Inherited reading for whoever verifies the eventual fix

                          ⚠️Verify the fan-out, never the delivery rows. PR #13565 (merged 99d23b1ec) makes the auto-enqueuer stamp each delivery row with the subscription's own organization, so a mis-routed delivery is stamped consistently as the receiving organization while carrying the sending organization's payload. A leaked delivery looks natively owned by the receiver in the Failures view. ⛔ Do not expect the delivery table to expose this.

                          Clause ②

                          Very likely yes — this widens a published event payload that external subscribers observe. The claiming seat re-derives it from card content; ⛔ this filing does not decide it.

                          Refs: #13566 (the p0 parent) · #14970 (the single-record producer twin) · #14971 / PR #15218 (the bulk spec term, landed today) · #14291 / PR #14635 (the single-record spec term) · #13546 / PR #13565 (subscription-side organization cache) · #8554 (sys_webhook is organization-scoped) · #4639 (publishBulkDataEvent's contract)

                          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

                            Labels

                            No labels
                            No labels

                            Type

                            No type

                            Projects

                            No projects

                              Milestone

                              No milestone

                              Relationships

                              None yet

                              Development

                              No branches or pull requests

                              Issue actions

                              , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
                              Skip to content

                              objectql: publishBulkDataEvent does not stamp the organizationId the spec now declares — the bulk twin of #14970, and the second half of the #13566 p0 cross-tenant webhook leak #15258

                              Description

                              @os-warren

                              Blocked-by: #14970

                              Filed by the domain:services execution seat (session session_01XpTx2tbq3pZRYAdoGt6E6Y, os-warren, seat post #6021) while running its unlock scan on #13566, minutes after the spec half landed. Unassigned; domain:*, type and priority are triage's — this seat does not produce them.

                              Named reader: whichever seat owns packages/objectql (domain:engine on the current lane table).

                              🔴 The second producer gap on a CONFIRMED p0 cross-tenant leak — and again nothing was filed for it

                              ⚠️Requesting the emergency triage channel rather than the hourly sweep. The parent defect #13566 is priority:p0bugsecurity, confirmed by measurement (a tenant org-admin, not a deployment administrator, can create sys_webhook rows under both walled postures), and open since 2026-08-31.

                              This is the same shape that already cost this chain 14 hours.#14970 exists because the single-record producer was never filed and the spec + consumer halves were inert without it. The bulk spec half landed 07:22Z today (#14971 / PR #15218) — and its producer has no card either. Filing it immediately rather than letting the identical gap repeat.

                              Measured on origin/main97bcd99e1, 2026-09-04T07:2xZ

                              publishBulkDataEvent body (engine.ts :5709-5765), grep -c organizationId → 0
                              CONTROL: same file, whole-file organizationId count → 12
                              

                              ⇒ The grep reads the file. The bulk publisher mentions the term zero times, and builds its event as BulkDataEventSchema.parse({ … }) with no tenant key.

                              For symmetry, the single-record twin is still unstamped too — publishDataEvent (:5628-5700) → 0, CONTROL parse in that window → 1. That is #14970, still open and still unassigned.

                              The chain as it now stands

                              piecefilestate
                              spec term, single recordspec/src/api/events.zod.ts:293✅ landed 2aa8456cf (#14291 / PR #14635)
                              spec term, bulkspec/src/api/events.zod.ts:409landed 97bcd99e1 (#14971 / PR #15218), 07:22:40Z today
                              producer, single recordobjectql/src/engine.ts:5628publishDataEvent#14970 — open, unassigned
                              producer, bulk — THIS CARDobjectql/src/engine.ts:5709publishBulkDataEventnot done, no card until now
                              fan-out, single recordplugin-webhooks/src/auto-enqueuer.ts:834#13566, waits on #14970
                              fan-out, bulkplugin-webhooks/src/auto-enqueuer.ts:934❌ waits on this card

                              Both fan-out sites still select on object name alone, and '*' matches every object.

                              What to build — the spec states the obligation, and it is satisfiable without a new query

                              BulkDataEventSchema.organizationId's JSDoc (landed today) is unusually explicit, and it settles the two things that would otherwise need deciding:

                              The tenant term is ONE organization for the whole batch, or nothing. … It is never per-row … and never a list. That is what keeps a tenant-scoped fan-out one comparison, never a partition of the batch.

                              One for the batch, by construction. … under a walled posture the security layer AND-composes its tenant wall (Layer 0, ADR-0095 D1) onto the caller's filter first … and nothing in Layer 1 can widen it. So when the wall names exactly one organization, every affected row belongs to it, and the producer can state that from what it already holds: no second query on the publish path.

                              ⇒ Read the organization off the composed tenant wall the write already carries. ⛔ Do not add a read to the publish path — triage ruled that out for the fan-out side on 2026-08-31 for the same reason (the enqueuer exists to keep this O(1)), and the spec says it is unnecessary here.

                              ⛔ The trap in this card, stated because it is easy to get backwards

                              Absent means something DIFFERENT here than on the single-record event, and the spec says so deliberately:

                              • DataEventSchema — absent = "this record belongs to no organization, not behind any wall".
                              • BulkDataEventSchema — absent = "the producer did not assert one organization for the batch" — a statement about the producer's knowledge, not about the rows. Every event on a single-posture deployment; a system / environment-wide / unscoped predicate write; a group-posture sweep across several memberships.

                              Never fabricate it, and never substitute the caller's active organization for the rows' — under group the wall is the caller's membership set, so the batch is attributable only when that set names exactly one organization. The empty string is refused; there is no .default(). ⚠️ This is the same conflation PR #14726's blocking contract review caught on a different column, where a null meaning "read failed" was made indistinguishable from null meaning "no organization" and the only trace was a warn whose text was false.

                              Sequencing

                              Blocked-by: #14970 is set for coordination, not dependency — the two producers sit in the same file (engine.ts) a few hundred lines apart and would collide as concurrent claims. Whichever seat takes #14970 should consider taking this in the same pass; they are one file, one mechanism, two call-site families. If the engine seat prefers them independent, ⛔ this seat will not contest it — just do not run them as two simultaneous claims on engine.ts.

                              ⚠️Until this lands, "the #13566 leak is closed" is FALSE, whatever the single-record path does. Whoever eventually reports that leak fixed must name which path, or the report will be wrong.

                              Inherited reading for whoever verifies the eventual fix

                              ⚠️Verify the fan-out, never the delivery rows. PR #13565 (merged 99d23b1ec) makes the auto-enqueuer stamp each delivery row with the subscription's own organization, so a mis-routed delivery is stamped consistently as the receiving organization while carrying the sending organization's payload. A leaked delivery looks natively owned by the receiver in the Failures view. ⛔ Do not expect the delivery table to expose this.

                              Clause ②

                              Very likely yes — this widens a published event payload that external subscribers observe. The claiming seat re-derives it from card content; ⛔ this filing does not decide it.

                              Refs: #13566 (the p0 parent) · #14970 (the single-record producer twin) · #14971 / PR #15218 (the bulk spec term, landed today) · #14291 / PR #14635 (the single-record spec term) · #13546 / PR #13565 (subscription-side organization cache) · #8554 (sys_webhook is organization-scoped) · #4639 (publishBulkDataEvent's contract)

                              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

                                Labels

                                No labels
                                No labels

                                Type

                                No type

                                Projects

                                No projects

                                  Milestone

                                  No milestone

                                  Relationships

                                  None yet

                                  Development

                                  No branches or pull requests

                                  Issue actions