') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); fix(plugin-auth): ensureDefaultOrganization resolves the L4 config-derived owner when no grant row exists (#13514 follow-through) by hotlong · Pull Request #13708 · objectstack-ai/objectstack · GitHub
Skip to content

fix(plugin-auth): ensureDefaultOrganization resolves the L4 config-derived owner when no grant row exists (#13514 follow-through) - #13708

Closed
hotlong wants to merge 1 commit into
mainfrom
fix/d1-config-derived-owner
Closed

fix(plugin-auth): ensureDefaultOrganization resolves the L4 config-derived owner when no grant row exists (#13514 follow-through)#13708
hotlong wants to merge 1 commit into
mainfrom
fix/d1-config-derived-owner

Conversation

@hotlong

Copy link
Copy Markdown
Contributor

What

The ADR-0081 D1 default-org bootstrap still resolved the platform admin by the grant row (admin_full_access set → oldest cross-tenant grant). #13514 (L4) retired that row under walled postures — standing is config-derived at the one derivation site — so on every walled deployment the helper dead-ends on no_admin forever. The open AuthPlugin never hits this (its runner is gated on non-walled postures), but the enterprise organizations package invokes this same helper under walled postures by design (the runner comment: "every WALLED posture keeps its existing owner: the enterprise organizations package").

How it surfaced: cloud's pin bump to current main took its EE guided-path suite red — signup-membership-policy.e2e: "the platform admin never received their default-organization owner row" — the walled founder signs up, verifies, and no default org ever appears.

The fix

When the grant lookup answers nothing, fall back to the declared verified owner, asked with the same public predicates the derivation site asks: resolvePlatformAdminEmails() + isConfiguredPlatformAdminEmail (@objectstack/core) + the #11343 verified-email allow-list (@objectstack/types); oldest verified owner wins — the bootstrap's own tiebreak. The grant row stays primary where it exists. Fail-closed everywhere else: no declared owner, unverified match, or no matching row all still answer no_admin.

Pins

Four new cases in the package suite (16/16 green): the walled L4 shape resolves the owner · unverified stays no_admin · undeclared stays no_admin · a grant row wins over the config fallback.

Consumer chain verified end-to-end on cloud's rig with this exact patch: the EE guided-path suite goes 5/5, full EE pass 156/156, full cloud pass 285/285 (cloud pairs this with a trigger widening in its organizations plugin — re-run ensure on the verifying sys_user write, since no permission-set insert exists to listen to any more).

Gates

plugin-auth build + check-dts-emitted green · full-repo lint green · changeset (patch).

🤖 Generated with Claude Code

…rived owner when no grant row exists (#13514 follow-through)
#13514 (L4) retired the walled grant row: bootstrapPlatformAdmin writes no
sys_user_permission_set row under walled postures, standing is config-derived
at the one derivation site. The ADR-0081 D1 default-org bootstrap still
resolved the admin BY THE ROW (admin_full_access set → oldest cross-tenant
grant) — so on every walled deployment it dead-ended on `no_admin` forever.
The open AuthPlugin never hits this (its runner is gated on non-walled
postures), but the enterprise organizations package invokes this same helper
under walled postures by design ("every WALLED posture keeps its existing
owner: the enterprise organizations package" — the runner comment); cloud's
EE guided-path suite caught the dead end on its pin bump
(signup-membership-policy.e2e: "the platform admin never received their
default-organization owner row").
The fallback asks the same public predicates the derivation site asks —
resolvePlatformAdminEmails() + isConfiguredPlatformAdminEmail from core,
the #11343 verified-email allow-list from types — oldest verified owner
wins (the bootstrap's own tiebreak), and the grant row stays PRIMARY where
it exists. Fail-closed in every other shape: no declared owner, unverified
match, or no row all still answer `no_admin`.
Four new pins in the package suite (16/16 green): the walled L4 shape
resolves the owner; unverified stays no_admin; undeclared stays no_admin;
a grant row wins over the config fallback.
Gates: plugin-auth build + check-dts-emitted green, full-repo lint green,
changeset (patch) present.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@hotlong
hotlong enabled auto-merge August 31, 2026 07:48
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/plugin-auth, touching 5 documentable anchor(s).

11 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/error-catalog.mdx(via sys_permission_set (literal))
  • content/docs/automation/approvals.mdx(via admin_full_access (literal))
  • content/docs/data-modeling/objects.mdx(via sys_user_permission_set (literal))
  • content/docs/deployment/environment-variables.mdx(via sys_permission_set (literal), sys_user_permission_set (literal))
  • content/docs/deployment/self-hosting.mdx(via admin_full_access (literal))
  • content/docs/permissions/authorization.mdx(via admin_full_access (literal), sys_permission_set (literal), sys_user_permission_set (literal))
  • content/docs/permissions/delegated-administration.mdx(via sys_permission_set (literal), sys_user_permission_set (literal))
  • content/docs/permissions/permission-sets.mdx(via admin_full_access (literal), sys_permission_set (literal), sys_user_permission_set (literal))
  • content/docs/permissions/permissions-matrix.mdx(via admin_full_access (literal))
  • content/docs/permissions/sharing-rules.mdx(via admin_full_access (literal))
  • content/docs/ui/audience-based-interfaces.mdx(via admin_full_access (literal))

7 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/implementation-status.mdx(via sys_user_permission_set (literal))
  • content/docs/releases/v12.mdx(via sys_permission_set (literal))
  • content/docs/releases/v13.mdx(via sys_permission_set (literal), sys_user_permission_set (literal))
  • content/docs/releases/v14.mdx(via admin_full_access (literal), sys_user_permission_set (literal))
  • content/docs/releases/v15.mdx(via sys_permission_set (literal))
  • content/docs/releases/v16.mdx(via admin_full_access (literal), sys_user_permission_set (literal))
  • content/docs/releases/v17.mdx(via admin_full_access (literal), sys_permission_set (literal), sys_user_permission_set (literal))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 anchor(s) matched too much of the corpus to be a work list: sys_user (literal, 30 pages)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 11 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 76fa3553208777f2eb8caf360cf36e37be84cd0dpackageMentionDocs.

Which tree this was computed on

This run read content/docs from 6633d21062a22200e236175dcbcfc17bba5810ca — the merge of head d6af213b5b001315b92f7ad6b0ccde073f4ddda0 into base 76fa3553208777f2eb8caf360cf36e37be84cd0d, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 6633d21062a22200e236175dcbcfc17bba5810ca && git checkout 6633d21062a22200e236175dcbcfc17bba5810ca
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 76fa3553208777f2eb8caf360cf36e37be84cd0d d6af213b5b001315b92f7ad6b0ccde073f4ddda0 && git checkout -B drift-repro 76fa3553208777f2eb8caf360cf36e37be84cd0d && git merge --no-ff d6af213b5b001315b92f7ad6b0ccde073f4ddda0
node scripts/docs-audit/affected-docs.mjs --json 76fa3553208777f2eb8caf360cf36e37be84cd0d

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 76fa3553208777f2eb8caf360cf36e37be84cd0d → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actionsgithub-actionsBot added size/m documentation Improvements or additions to documentation tests tooling labels Aug 31, 2026
@hotlong
hotlong added this pull request to the merge queueAug 31, 2026
@github-merge-queue
github-merge-queueBot removed this pull request from the merge queue due to a conflict with the base branch Aug 31, 2026
@hotlong

Copy link
Copy Markdown
ContributorAuthor

Superseded by #13685 (L3 re-anchor), which landed while this was in the queue — and landed the better shape.

I filed this after cloud's pin bump took the EE guided-path suite red on the walled dead end (no_admin forever once #13514 retired the grant row). #13685 fixes the same defect two ways better:

  • the config anchor is the preferred anchor there, not a fallback behind the grant read (mine kept the grant row primary — defensible, but the re-anchor's ordering is the ruled one and matches resolve-authz-context.ts §6b-config's own preference);
  • it exports isDefaultOrganizationBootstrapTrigger, so the consuming wirings share ONE predicate instead of each re-deriving the trigger set. My cloud-side change had hand-written exactly those arms and would have drifted from this one — it now consumes the export instead. It also covers an arm I missed: an update touching email (not just email_verified) moves the population answer too.

Verified against #13685's implementation on cloud's rig rather than mine: EE guided-path 5/5. Closing with no residue; the four pins I wrote here are subsumed by that PR's own coverage.

@hotlonghotlong closed this Aug 31, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@hotlong