marketplace: minesweeper in the catalogue

Without an entry the plugin is invisible to every server but this one — the
catalogue is what a remote host reads, and the bundled constant is only the
fallback when marketplace.officer.dev cannot be reached.

Its git remote was the tailnet address, which marketplace-sync would have
published verbatim: a clone URL that works on this network and nowhere else,
failing at authentication in a way that reads as a broken plugin rather than a
wrong catalogue. Repointed at gitea.officer.dev, which is the rule the sync
script's own comment already spells out.

Synced. 18/18, and the entry is live with the right source, permissions and six
osDependencies; the repo answers an anonymous https clone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
This commit is contained in:
2026-08-23 18:07:50 +00:00
co-authored by Claude Opus 5
parent 1ff271bb5e
commit 6e092c32ea
10 changed files with 196 additions and 115 deletions
+12 -3
View File
@@ -1,5 +1,5 @@
import type { Handler } from 'hono';
import { getUserCount, createUser, replaceRoleGrants, USER_ROLES } from 'officerdb';
import { getUserCount, createUser, replaceRoleGrants, listRoles } from 'officerdb';
import { DEFAULT_ROLE_PERMISSIONS } from '@@/permissions/registry';
import argon2 from 'argon2';
import * as errors from '@@/custom-errors';
@@ -49,12 +49,21 @@ export const bootstrapHandler: Handler = async function (ctx) {
// bootstrap is the one moment that happens exactly once per install, so seeding cannot fight a later
// revocation — take one of these away and nothing puts it back.
//
// Read from the `roles` table rather than the old USER_ROLES list (docs/roles-and-teams.md phase 2 part
// 4) — `scripts/seed-roles.ts` runs as part of `postdb:push`, before the server ever serves a request,
// so by the time this handler can run the four built-in rows already exist. Filtered to `isSystem`
// deliberately: this is seeding the platform's own baseline roles, not every role that happens to exist
// — a role created at runtime through /api/roles before anyone has bootstrapped (which `roles` being
// owner-gated makes unreachable today, but nothing here should assume that stays true) gets no grants
// from this loop, same as it would get none from any other seed step.
//
// Non-fatal. An owner who exists but whose roles hold nothing is a working server with a one-click fix;
// failing bootstrap over it would leave a platform with no account at all.
try {
for (const role of USER_ROLES.filter((r) => r !== 'Super Admin')) {
const builtIn = (await listRoles()).filter((r) => r.isSystem && r.name !== 'Super Admin');
for (const role of builtIn) {
await replaceRoleGrants(
role,
role.name,
DEFAULT_ROLE_PERMISSIONS.map((permission) => ({ permission, level: 'write' as const })),
);
}