Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
5480f7e
feat(config)!: runtime_dir and max_revisions are core settings, not a…
zumbrunn Nov 6, 2026
0ea5229
feat(plugin-api)!: the admin declares where it is mounted (admin.pref…
zumbrunn Nov 6, 2026
439d7e4
chore: drop the deno.lock change that slipped into the step 2 commit
zumbrunn Nov 6, 2026
8ad89d5
feat(plugins)!: core no longer loads the admin, it is a plugin you li…
zumbrunn Nov 6, 2026
f72041b
feat(plugin-api)!: machine translation and the audit trail leave core…
zumbrunn Nov 6, 2026
3e609c4
feat(config)!: the admin's settings are the admin plugin's config, no…
zumbrunn Nov 7, 2026
145b7ab
feat(config)!: machine_translation moves into the admin plugin's conf…
zumbrunn Nov 7, 2026
c5de0fa
chore: drop the deno.lock change that slipped into the step 5 commits
zumbrunn Nov 7, 2026
b0d38c0
fix(authz)!: site users' roles grant editing, checked on app:site (#130)
zumbrunn Nov 7, 2026
2191190
fix(authz): a role granted or revoked in a running app takes effect a…
zumbrunn Nov 7, 2026
ca675bb
Merge origin/main into feat/explicit-admin-loading
zumbrunn Nov 7, 2026
bf41539
Merge feat/explicit-admin-loading into feat/mt-audit-to-admin
zumbrunn Nov 7, 2026
25067b4
Merge feat/mt-audit-to-admin into feat/admin-settings-in-plugin
zumbrunn Nov 7, 2026
87db9e4
Merge feat/admin-settings-in-plugin into fix/site-user-editing
zumbrunn Nov 7, 2026
d3e5c3a
feat(plugin-api)!: the contract keeps what core consumes; plugins dec…
zumbrunn Nov 7, 2026
292a3dd
feat(plugin-api)!: transforms read capabilities from ResponseTransfor…
zumbrunn Nov 7, 2026
3c1b2be
docs: the unprefixed capability namespaces are editor, scheduler and …
zumbrunn Nov 7, 2026
efdb350
feat(plugin-api)!: core gates /api/inline-edit/* and hands requests t…
zumbrunn Nov 7, 2026
929043b
docs: changelog for the inline-edit HTTP gate and page-event actor (#…
zumbrunn Nov 7, 2026
740da47
feat(plugin-api)!: typed capability keys instead of module augmentati…
zumbrunn Nov 8, 2026
e2a696e
docs: capability keys in the plugins module comment
zumbrunn Nov 8, 2026
f2cfb64
docs(skills): refresh for 0.36 (snake_case config keys, app:site, edi…
zumbrunn Nov 8, 2026
979fc52
fix: headless smoke boots without the admin entry; headless scaffold …
zumbrunn Nov 8, 2026
c5ed518
fix(auth): log a failed role-grant bootstrap, own-property override l…
zumbrunn Nov 8, 2026
5f85916
Merge branch 'feat/explicit-admin-loading' into feat/mt-audit-to-admin
zumbrunn Nov 8, 2026
0588f2b
Merge branch 'feat/mt-audit-to-admin' into feat/admin-settings-in-plugin
zumbrunn Nov 8, 2026
42f5f23
test: the overlay admin.enabled warning, with #131's manual-move exit
zumbrunn Nov 8, 2026
e648e84
Merge branch 'feat/admin-settings-in-plugin' into fix/site-user-editing
zumbrunn Nov 8, 2026
b204168
chore: keep deno.lock as on the base branch
zumbrunn Nov 8, 2026
2d41672
Merge branch 'fix/site-user-editing' into refactor/contract-split
zumbrunn Nov 8, 2026
2d57358
Merge branch 'refactor/contract-split' into feat/inline-edit-routes
zumbrunn Nov 8, 2026
423356c
Merge branch 'feat/inline-edit-routes' into docs/refresh-skills
zumbrunn Nov 8, 2026
caf77e6
feat(auth): live editing sessions respect page ownership; name where …
zumbrunn Nov 8, 2026
e9dce52
Merge branch 'feat/inline-edit-routes' into docs/refresh-skills
zumbrunn Nov 8, 2026
dfb9d5b
docs(skills): author ownership and the content-tier caveat for editin…
zumbrunn Nov 8, 2026
84547bd
fix(cli): headless scaffold registers plugin islands with registerIsland
zumbrunn Nov 8, 2026
7828f57
feat(auth): system.role_grants chooses which groups hold the admin-ti…
zumbrunn Nov 10, 2026
64d4fb0
test: pass the role grants to the editor resolver in the response-tra…
zumbrunn Nov 10, 2026
aba510e
Merge branch 'docs/refresh-skills' into feat/configurable-role-grants
zumbrunn Nov 10, 2026
cdddc15
docs(skills): system.role_grants
zumbrunn Nov 10, 2026
83b8207
Merge remote-tracking branch 'origin/feat/explicit-admin-loading' int…
zumbrunn Nov 10, 2026
db082f9
Merge origin/main into feat/mt-audit-to-admin
zumbrunn Nov 10, 2026
7445cfb
fix(migrate:config): keep core settings of a disabled admin block; ne…
zumbrunn Nov 10, 2026
89a25ec
Merge remote-tracking branch 'origin/feat/mt-audit-to-admin' into fea…
zumbrunn Nov 10, 2026
5672444
Merge remote-tracking branch 'origin/main' into feat/admin-settings-i…
zumbrunn Nov 10, 2026
2b50403
Merge remote-tracking branch 'origin/feat/admin-settings-in-plugin' i…
zumbrunn Nov 10, 2026
70f485b
Merge remote-tracking branch 'origin/main' into fix/site-user-editing
zumbrunn Nov 10, 2026
2cd6e29
Merge remote-tracking branch 'origin/fix/site-user-editing' into refa…
zumbrunn Nov 10, 2026
42a1cff
Merge origin/main into refactor/contract-split
zumbrunn Nov 10, 2026
53ef3a7
Merge remote-tracking branch 'origin/refactor/contract-split' into fe…
zumbrunn Nov 10, 2026
6cbd371
Merge origin/main into feat/inline-edit-routes
zumbrunn Nov 10, 2026
934c55e
docs: the authz schema's subject types are user and group
zumbrunn Nov 10, 2026
2f65c3e
Merge remote-tracking branch 'origin/feat/inline-edit-routes' into do…
zumbrunn Nov 10, 2026
2205791
Merge remote-tracking branch 'origin/main' into docs/refresh-skills
zumbrunn Nov 10, 2026
72e2cf7
Merge remote-tracking branch 'origin/docs/refresh-skills' into feat/c…
zumbrunn Nov 10, 2026
21d30d2
Merge origin/main into feat/configurable-role-grants
zumbrunn Nov 10, 2026
b89eb4f
fix(auth): the editor's relation is read from authz, so a default-nam…
zumbrunn Nov 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,17 @@ where the setting lives now (see `dune migrate:config`, which reports these too)
keyed by string. `ResponseTransformContext.barActions` and `.adminPrefix` are replaced
by `ResponseTransformContext.capabilities`, the same reader `MountApi.capabilities` offers: a
bar-owning plugin reads `list(BAR_ACTIONS)` and `get("admin.prefix")` per request.
- **`system.role_grants` chooses which groups hold the admin-tier relations (#139).** The default is
what the entry below describes: groups `admin`, `editor` and `author` each hold their own relation
on the site. Set your own group onto a relation (`staff: editor`), keep a subset, or `{}` to turn
the group-level grants off. The relations are fixed by the authz schema; this only chooses which
group gets which. The grants are reconciled at every start: a grant no longer configured is
revoked (every group → admin/editor/author tuple on the site belongs to this setting).
`EditorIdentity.relation` tells an inline-edit provider the relation core decided, so an `author`
limit follows the mapping, not role names: core reads the relation from the authz system (what the
user actually holds), so a default-named group remapped *down* (`editor: author`) is limited too.
`role_grants` in an environment overlay replaces the base mapping instead of merging into it. An
admin plugin's own accounts keep their rights whatever the mapping says. `dune doctor --boot` and the start-up note follow the mapping.
- **Holding `admin`, `editor` or `author` in `roles[]` carries editing rights for any signed-in user,
with or without the admin plugin (#130).** Before, a site user got them only after a restart and
only on a site that ran the admin with the local user store; now the roles are granted at group
Expand Down
2 changes: 1 addition & 1 deletion skills/dune-auth.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ Generated CRUD route handlers (from `dune codegen`) and other code that only has

## Site users can edit

A signed-in site user whose `roles` include `admin`, `editor` or `author` may edit pages in place (the editing bar and `/api/inline-edit/*` from `@dune/plugin-inline-edit`), with no admin plugin installed: core checks `pages.update` for them like for an admin account. Their role is also a group membership, kept in step with `roles[]` live, so changing a user's roles takes effect on their next request. Content-gating tags (`member`, `subscriber`) never grant editing. An `author` edits only the pages it created (`createdBy`); `admin` and `editor` edit any page. **Do not use `editor` or `author` as a content tier** (in `roles:` frontmatter, a payment product's `role`, a sign-up default): holding the name is what grants editing. Granting them on purpose, for example to paid contributors, is fine; core logs `auth.automatic_admin_tier_grant` at start and `dune doctor --boot` lists where the config does it. Responses for any signed-in user bypass the shared page cache.
A signed-in site user whose `roles` include `admin`, `editor` or `author` may edit pages in place (the editing bar and `/api/inline-edit/*` from `@dune/plugin-inline-edit`), with no admin plugin installed: core checks `pages.update` for them like for an admin account. Their role is also a group membership, kept in step with `roles[]` live, so changing a user's roles takes effect on their next request. Content-gating tags (`member`, `subscriber`) never grant editing. An `author` edits only the pages it created (`createdBy`); `admin` and `editor` edit any page. **Do not use `editor` or `author` as a content tier** (or set `role_grants` in `system.yaml`, a mapping of group to relation, to remap or switch off the group-level grants: `role_grants: {}`) (in `roles:` frontmatter, a payment product's `role`, a sign-up default): holding the name is what grants editing. Granting them on purpose, for example to paid contributors, is fine; core logs `auth.automatic_admin_tier_grant` at start and `dune doctor --boot` lists where the config does it. Responses for any signed-in user bypass the shared page cache.

---

Expand Down
2 changes: 1 addition & 1 deletion skills/dune-authz.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ const canEdit = await getAuthz().check({
});
```

Who holds what: a **site user** gets their admin-tier role (`admin`/`editor`/`author` in `roles[]`) through the role's group: Dune writes `(group:<role>, <role>, app:site)` once at startup (`bootstrapRoleGrants`) and the user is a `member` of `group:<role>`. The **admin plugin's own accounts** get direct tuples (`services.authz.seedAdminTuples([{ id, role }])` from a plugin). Either way `authz.check()` answers the same, so a signed-in site user with the `editor` role may edit inline with no admin plugin installed.
Who holds what: a **site user** gets their admin-tier role (`admin`/`editor`/`author` in `roles[]`) through the role's group: Dune writes `(group:<role>, <role>, app:site)` at startup (`bootstrapRoleGrants`), as `system.role_grants` in `system.yaml` says: a mapping of group to relation, default `admin: admin, editor: editor, author: author`. Remap your own group (`staff: editor`), keep a subset, or `{}` for none; grants no longer configured are revoked at start and the user is a `member` of `group:<role>`. The **admin plugin's own accounts** get direct tuples (`services.authz.seedAdminTuples([{ id, role }])` from a plugin). Either way `authz.check()` answers the same, so a signed-in site user with the `editor` role may edit inline with no admin plugin installed.

You rarely call this yourself: the admin's route guards (`requirePermission`) and core's inline-edit gate already do, and deny when `authz` is undefined. There is no role-table fallback.

Expand Down
12 changes: 11 additions & 1 deletion src/auth/authz-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
* - Inline editing — "edit" on object type "resource"
* - Resource-level grants — "owner" relation on object type "resource"
*
* Subject types: "user"
* Subject types: "user" | "group" (a group holds a relation too: see `bootstrapRoleGrants`)
* Object types: "group" | "app" | "resource"
*
* ## Content gating
Expand Down Expand Up @@ -45,6 +45,16 @@ import type { Role } from "../config/role.ts";
*/
export const SITE_RESOURCE = { type: "app", id: "site" } as const;

/**
* Built-in actions that tell the admin-tier relations apart, for code that must
* know which relation a user holds without the user's role names (those can be
* remapped by `system.role_grants`): `pages.delete` is satisfied by `admin`
* alone, `config.read` by `admin` and `editor`; anyone who can only
* `pages.update` holds `author`. A test pins these to
* {@link DUNE_BASE_AUTHZ_ACTIONS}.
*/
export const RELATION_PROBE_ACTIONS = { admin: "pages.delete", editor: "config.read" } as const;

/**
* A polizy relation name a plugin-contributed action can require. Limited
* to Dune's existing built-in relations (no plugin can define a *new*
Expand Down
52 changes: 38 additions & 14 deletions src/auth/authz.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
import { AuthSystem } from "polizy";
import type { AuthSchema } from "polizy";
import { duneAuthzSchema, SITE_RESOURCE } from "./authz-schema.ts";
import { DEFAULT_ROLE_GRANTS, GRANT_RELATIONS, type RoleGrants } from "./role-grants.ts";
import { AuthzLocalAdapter } from "./authz-adapter-local.ts";
import { AuthzDbAdapter } from "./authz-adapter-db.ts";
import type { StorageAdapter } from "../storage/types.ts";
Expand Down Expand Up @@ -178,6 +179,15 @@ export interface AuthzAdapterLike {
hasTuple(subject: { type: string; id: string }, relation: string, object: { type: string; id: string }): Promise<boolean>;
}

/** What {@link bootstrapRoleGrants} needs besides {@link AuthzAdapterLike}: finding who holds a relation. */
export interface RoleGrantAdapter extends AuthzAdapterLike {
findSubjects(
object: { type: string; id: string },
relation: string,
options?: { subjectType?: string },
): Promise<{ type: string; id: string }[]>;
}

/**
* Bootstrap role-based permission tuples from existing User `roles[]`.
*
Expand Down Expand Up @@ -236,24 +246,38 @@ export async function bootstrapAdminTuples(
}

/**
* Grant each admin-tier role its relation on {@link SITE_RESOURCE} once, at
* group level: `group:editor` holds `editor` on `app:site`. A site user whose
* role is `editor` is already a member of `group:editor` (site auth keeps
* that in sync, see {@link bootstrapRoleTuples}), so membership is all it
* takes, with no per-user tuple to write, forget, or leave behind on revoke.
* The admin's own accounts get direct tuples ({@link bootstrapAdminTuples});
* the two do not conflict, since `authz.check()` is true if either holds.
* Make the groups named in `grants` hold their relation on {@link SITE_RESOURCE},
* and nothing else: `group:editor` holds `editor` on `app:site` by default. A
* site user whose `roles[]` has `editor` is a member of `group:editor` (site
* auth keeps that in sync, see {@link bootstrapRoleTuples}), so membership is
* all it takes, with no per-user tuple to write, forget, or leave behind on
* revoke. `grants` comes from `system.role_grants` ({@link resolveRoleGrants}).
*
* Idempotent. Called wherever an authz system is created, so a site that has
* a login but no admin plugin still lets its editors edit.
* Reconciles, not only adds: every (group → admin/editor/author) tuple on the
* site belongs to this setting, so one that is no longer configured is revoked.
* A plugin that wants a group-level grant on the site writes it through
* `system.role_grants`, not by hand.
*
* An admin plugin's own accounts get direct tuples ({@link bootstrapAdminTuples});
* `authz.check()` is true if either holds. Idempotent. Called wherever an authz
* system is created, so a site that has a login but no admin plugin still lets
* its editors edit.
*/
export async function bootstrapRoleGrants(
authz: DuneAuthSystem,
adapter: AuthzAdapterLike,
adapter: RoleGrantAdapter,
grants: RoleGrants = DEFAULT_ROLE_GRANTS,
): Promise<void> {
for (const role of ["admin", "editor", "author"] as const) {
const group = { type: "group" as const, id: role };
if (await adapter.hasTuple(group, role, SITE_RESOURCE)) continue;
await authz.allow({ who: group, toBe: role, onWhat: SITE_RESOURCE });
for (const relation of GRANT_RELATIONS) {
for (const subject of await adapter.findSubjects(SITE_RESOURCE, relation, { subjectType: "group" })) {
if (grants[subject.id] !== relation) {
await authz.disallowAllMatching({ who: { type: "group", id: subject.id }, was: relation, onWhat: SITE_RESOURCE });
}
}
}
for (const [group, relation] of Object.entries(grants)) {
const who = { type: "group" as const, id: group };
if (await adapter.hasTuple(who, relation, SITE_RESOURCE)) continue;
await authz.allow({ who, toBe: relation, onWhat: SITE_RESOURCE });
}
}
24 changes: 14 additions & 10 deletions src/auth/automatic-grants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/

import type { DuneConfig } from "../config/types.ts";
import { resolveRoleGrants } from "./role-grants.ts";

/** One automatic path to an admin-tier role. */
export interface AutomaticGrant {
Expand All @@ -29,33 +30,36 @@ const CAN: Record<string, string> = {
author: "create pages and edit the pages they created",
};

const ADMIN_TIER = ["admin", "editor", "author"];

/** The automatic paths to an admin-tier role that `config` sets up. Empty for most sites. */
/** The automatic paths to an editing role that `config` sets up. Empty for most sites. */
export function automaticAdminTierGrants(config: DuneConfig): AutomaticGrant[] {
const out: AutomaticGrant[] = [];
const site = config.site;
const grants = resolveRoleGrants(config);

const payments = site?.payments;
if (payments?.secret_key) {
for (const product of payments.products ?? []) {
const role = product.role;
if (role && ADMIN_TIER.includes(role)) {
const relation = role ? grants[role] : undefined;
if (role && relation) {
out.push({
severity: role === "admin" ? "warning" : "info",
message: `payments product "${product.id}" gives every buyer the "${role}" role, which can ${CAN[role]}. ` +
`Intended? If "${role}" is only a content tier here, use another role name.`,
severity: relation === "admin" ? "warning" : "info",
message: `payments product "${product.id}" gives every buyer the "${role}" role, which ` +
`${role === relation ? "is" : `is mapped by system.role_grants to "${relation}" and`} can ${CAN[relation]}. ` +
`Intended? If "${role}" is only a content tier here, use another role name` +
`${role === relation ? "" : " or change role_grants"}.`,
});
}
}
}

if (site?.auth?.mode === "external-jwt") {
const mapped = Object.entries(grants);
if (site?.auth?.mode === "external-jwt" && mapped.length > 0) {
out.push({
severity: "info",
message: `the external-jwt login trusts the token's "${site.auth.jwt?.roles_claim ?? "roles"}" claim: a token that ` +
`carries "admin", "editor" or "author" grants editing rights (author: ${CAN.author}; editor: ${CAN.editor}; ` +
`admin: ${CAN.admin}). Make sure the identity provider only issues those to staff.`,
`carries ${mapped.map(([g, r]) => `"${g}" (${r}: ${CAN[r]})`).join(", ")} grants editing rights. ` +
`Make sure the identity provider only issues those to staff.`,
});
}
return out;
Expand Down
57 changes: 40 additions & 17 deletions src/auth/editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@
import type { CapabilityReader } from "../plugin-api/capabilities.ts";
import type { EditorIdentity } from "../inline-edit/types.ts";
import type { DuneAuthSystem } from "./authz.ts";
import { SITE_RESOURCE } from "./authz-schema.ts";
import { RELATION_PROBE_ACTIONS, SITE_RESOURCE } from "./authz-schema.ts";
import { getUser } from "./types.ts";
import type { GrantRelation } from "./role-grants.ts";
import { hasSessionCookie } from "../cli/admin-bar-inject.ts";

/** Outcome of {@link EditorResolver}: an editor, or why there is none. */
Expand Down Expand Up @@ -43,26 +44,46 @@ function displayName(user: { id: string; username?: string; name?: string; email
return user.username ?? user.name ?? user.email ?? user.id;
}

function canUpdatePages(authz: DuneAuthSystem | undefined, userId: string): Promise<boolean> {
/**
* The admin-tier relation `userId` actually holds on the site, read from the
* authz system, or null when they may not edit pages at all. Asking authz (and
* not inferring from role names) is what keeps this right under
* `system.role_grants`: a user in `group:editor` remapped to `author` holds
* `author`, whatever their role is called, and an account seeded with a direct
* tuple holds what the tuple says.
*/
async function editorRelation(authz: DuneAuthSystem | undefined, userId: string): Promise<GrantRelation | null> {
// Fail closed: no authz system means nobody is an editor.
if (!authz) return Promise.resolve(false);
return authz.check({
who: { type: "user", id: userId },
// deno-lint-ignore no-explicit-any
canThey: "pages.update" as any,
onWhat: SITE_RESOURCE,
}).catch(() => false);
if (!authz) return null;
const can = (action: string) =>
authz.check({
who: { type: "user", id: userId },
// deno-lint-ignore no-explicit-any
canThey: action as any,
onWhat: SITE_RESOURCE,
}).catch(() => false);
if (!await can("pages.update")) return null;
if (await can(RELATION_PROBE_ACTIONS.admin)) return "admin";
if (await can(RELATION_PROBE_ACTIONS.editor)) return "editor";
return "author";
}

/** Build the {@link EditorResolver} of a site. */
export function createEditorResolver(opts: EditorResolverOptions): EditorResolver {
return async (req) => {
const siteUser = getUser(req);
if (siteUser) {
if (!await canUpdatePages(opts.siteAuthz(), siteUser.id)) return { ok: false, status: 403 };
const relation = await editorRelation(opts.siteAuthz(), siteUser.id);
if (!relation) return { ok: false, status: 403 };
return {
ok: true,
editor: { id: siteUser.id, username: displayName(siteUser), roles: siteUser.roles, source: "site" },
editor: {
id: siteUser.id,
username: displayName(siteUser),
roles: siteUser.roles,
relation,
source: "site",
},
};
}

Expand All @@ -72,34 +93,36 @@ export function createEditorResolver(opts: EditorResolverOptions): EditorResolve
// deno-lint-ignore no-explicit-any
const user = (result as any)?.authenticated ? (result as any).user : null;
if (!user?.id) return { ok: false, status: 401 };
if (!await canUpdatePages(opts.sessionAuthz(), user.id as string)) return { ok: false, status: 403 };
const relation = await editorRelation(opts.sessionAuthz(), user.id as string);
if (!relation) return { ok: false, status: 403 };
return {
ok: true,
editor: {
id: user.id as string,
username: displayName(user),
roles: (user.roles as string[] | undefined) ?? [],
relation,
source: "session",
},
};
};
}

/**
* Whether `editor` may edit the page with this frontmatter. An `admin` or
* `editor` may edit any page; anyone else (an `author`) only the pages they
* created, recorded as `createdBy` in the frontmatter. `null` is a path that is
* Whether `editor` may edit the page with this frontmatter. An editor whose
* {@link EditorIdentity.relation} is `admin` or `editor` may edit any page; an
* `author` only the pages it created, recorded as `createdBy` in the frontmatter. `null` is a path that is
* not a page, which nobody may edit.
*
* The HTTP routes of the inline-edit provider apply the same rule at commit;
* core applies it at the WebSocket too, so a live session cannot be opened on a
* page whose commit would be refused.
*/
export function mayEditPage(
editor: Pick<EditorIdentity, "id" | "roles">,
editor: Pick<EditorIdentity, "id" | "relation">,
frontmatter: Record<string, unknown> | null,
): boolean {
if (!frontmatter) return false;
if (editor.roles.includes("admin") || editor.roles.includes("editor")) return true;
if (editor.relation === "admin" || editor.relation === "editor") return true;
return frontmatter.createdBy === editor.id;
}
5 changes: 3 additions & 2 deletions src/auth/mount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { type User, USER_HEADER } from "./types.ts";
import { copySocketAddr } from "../security/rate-limit.ts";
import { InMemoryMagicTokenStore } from "./magic-link.ts";
import { bootstrapRoleGrants, bootstrapRoleTuples, createDuneAuthSystem } from "./authz.ts";
import { resolveRoleGrants } from "./role-grants.ts";
import { withRoleSync } from "./sync-roles.ts";
import type { DuneAuthSystem } from "./authz.ts";
import type { AuthzLocalAdapter } from "./authz-adapter-local.ts";
Expand Down Expand Up @@ -204,7 +205,7 @@ export async function mountDuneAuth(
setGatingAuthz(bundle.authz, siteOrigin);
mountAuthz = bundle.authz;
mountAdapter = bundle.adapter;
await bootstrapRoleGrants(bundle.authz, bundle.adapter).catch((err) =>
await bootstrapRoleGrants(bundle.authz, bundle.adapter, resolveRoleGrants(config)).catch((err) =>
logger.error("auth.role_grants_failed", {
error: err instanceof Error ? err.message : String(err),
effect: "users with an admin-tier role are denied site-wide permissions until the next start succeeds",
Expand Down Expand Up @@ -232,7 +233,7 @@ export async function mountDuneAuth(
mountAdapter = bundle.adapter;
// Idempotent; bootstrap() already did this for its own bundle, a headless
// caller's fresh one has not been seeded yet.
await bootstrapRoleGrants(bundle.authz, bundle.adapter).catch((err) =>
await bootstrapRoleGrants(bundle.authz, bundle.adapter, resolveRoleGrants(config)).catch((err) =>
logger.error("auth.role_grants_failed", {
error: err instanceof Error ? err.message : String(err),
effect: "users with an admin-tier role are denied site-wide permissions until the next start succeeds",
Expand Down
Loading
Loading