@devdogsuga/devtools 0.1.12 → 0.1.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/bin/devtools-ci.mjs +15 -1
  2. package/bin/devtools.mjs +15 -1
  3. package/dist/catalog.d.ts +7 -5
  4. package/dist/catalog.js +1 -25
  5. package/dist/cf/build.d.ts +13 -0
  6. package/dist/cf/build.js +65 -0
  7. package/dist/cf/commands.js +6 -16
  8. package/dist/cf/local-env.d.ts +14 -0
  9. package/dist/cf/local-env.js +29 -0
  10. package/dist/ci.js +8 -1
  11. package/dist/cli.js +134 -138
  12. package/dist/commands.d.ts +10 -4
  13. package/dist/commands.js +40 -30
  14. package/dist/db/generated-env.d.ts +69 -0
  15. package/dist/db/generated-env.js +113 -0
  16. package/dist/db/seed-production.js +12 -6
  17. package/dist/docs/index-pages.js +2 -2
  18. package/dist/emails/commands.js +3 -3
  19. package/dist/env-entry.d.ts +49 -0
  20. package/dist/env-entry.js +204 -0
  21. package/dist/environment-doctor.d.ts +9 -0
  22. package/dist/environment-doctor.js +18 -5
  23. package/dist/environment.d.ts +2 -2
  24. package/dist/environment.js +5 -28
  25. package/dist/images/commands.d.ts +3 -6
  26. package/dist/images/commands.js +26 -30
  27. package/dist/images/events.d.ts +27 -16
  28. package/dist/images/events.js +32 -68
  29. package/dist/images/graphics.js +2 -1
  30. package/dist/images/prompts.js +2 -2
  31. package/dist/instance.d.ts +29 -27
  32. package/dist/instance.js +49 -76
  33. package/dist/launch-ci.js +5 -5
  34. package/dist/launch.d.ts +2 -1
  35. package/dist/launch.js +74 -111
  36. package/dist/menu.d.ts +17 -5
  37. package/dist/menu.js +34 -11
  38. package/dist/moderation.d.ts +48 -40
  39. package/dist/moderation.js +95 -194
  40. package/dist/oauth/wizard.d.ts +2 -2
  41. package/dist/oauth/wizard.js +6 -26
  42. package/dist/persona.d.ts +18 -0
  43. package/dist/persona.js +221 -0
  44. package/dist/repo/peers.d.ts +2 -0
  45. package/dist/repo/peers.js +3 -0
  46. package/dist/repo/supabase-project.d.ts +60 -0
  47. package/dist/repo/supabase-project.js +124 -0
  48. package/dist/setup.js +2 -2
  49. package/dist/stack.js +142 -29
  50. package/dist/telemetry.d.ts +18 -5
  51. package/dist/telemetry.js +26 -5
  52. package/dist/ui.d.ts +15 -0
  53. package/dist/ui.js +20 -0
  54. package/dist/workflows/commands.d.ts +10 -0
  55. package/dist/workflows/commands.js +119 -25
  56. package/package.json +10 -5
package/dist/launch.js CHANGED
@@ -16,9 +16,27 @@
16
16
  * This module settles the tier ONCE, before any command's imports — let alone
17
17
  * its code — run, using the shared policy in `@devdogsuga/env/session`. By
18
18
  * the time `cli.ts` is imported, `process.env.DEPLOY_ENV` already names the
19
- * tier and its env files are already loaded, so `cli.ts`, `menu.ts` and every
20
- * runner underneath them just read `process.env` like any other command would
21
- * under `with-env` — this module IS the replacement for `with-env`'s job.
19
+ * tier, so `cli.ts`, `menu.ts` and every runner underneath them just read
20
+ * `process.env.DEPLOY_ENV`/`DEV_DB` like any other command would under
21
+ * `with-env` — this module IS the replacement for `with-env`'s job.
22
+ *
23
+ * ## Entering the environment: at launch, or deferred to the menu
24
+ *
25
+ * "Settling the tier" and "entering it" (loading `.env.<tier>` and its
26
+ * `DEV_DB` overlay onto `process.env`, via `@devdogsuga/env/session`'s
27
+ * `enterEnvironment`) are two different steps, and a MENU invocation —
28
+ * a bare `pnpm devtools`, or a bare group like `pnpm devtools db` resumed
29
+ * through `bareGroupStartPath` — only gets the first one here. Entering holds
30
+ * the env files hostage to whatever was true at the moment this process
31
+ * started; a contributor who starts the local stack in another terminal
32
+ * while still choosing from the wizard would otherwise have that missed
33
+ * entirely. So for a menu invocation this module resolves the tier, exports
34
+ * `DEPLOY_ENV`/`DEV_DB` directly (skipping `enterEnvironment` itself), and
35
+ * registers `env-entry.ts`'s menu hook instead of entering — `menu.ts`'s
36
+ * `runMenu` calls it right before dispatching the ONE command the reader
37
+ * chose, so the env files loaded are current as of that moment, not this
38
+ * one. A typed command line still enters right here, unchanged: there is no
39
+ * wizard delay to protect it from.
22
40
  *
23
41
  * ## `--tier`, a global flag
24
42
  *
@@ -30,10 +48,13 @@
30
48
  * already been made, and `resolveTier` (see `tier.ts`) falls back to reading
31
49
  * it off `process.env.DEPLOY_ENV` rather than asking again.
32
50
  */
33
- import { confirm, select } from "@clack/prompts";
51
+ import { select } from "@clack/prompts";
34
52
  import { findCommand } from "./commands.js";
53
+ import { enterSessionEnvironment, realEnvEntryDeps, setMenuEnvHook, } from "./env-entry.js";
54
+ import { bareGroupStartPath } from "./menu.js";
35
55
  import { discoverRepoRoot, findRepoRoot, RepoNotFoundError } from "./repo/root.js";
36
56
  import { loadEnvLoad, loadEnvSession } from "./repo/peers.js";
57
+ import { captureDevtoolsError, initDevtoolsTelemetry } from "./telemetry.js";
37
58
  import { errorMessage, unwrap } from "./ui.js";
38
59
  /**
39
60
  * Pulls a global `--tier <t>` out of `argv`, wherever it sits, leaving every
@@ -104,11 +125,14 @@ async function dispatch(argv) {
104
125
  }
105
126
  catch (err) {
106
127
  process.stderr.write(`devtools: ${errorMessage(err)}\n`);
128
+ // Report BEFORE exiting — see `captureDevtoolsError`'s header.
129
+ await captureDevtoolsError(err);
107
130
  process.exit(1);
108
131
  }
109
132
  }
110
133
  /**
111
- * Resolves the session's deploy tier, enters it, and hands off to `cli.ts`.
134
+ * Resolves the session's deploy tier, enters it (or defers entry to the
135
+ * menu — see this file's header), and hands off to `cli.ts`.
112
136
  *
113
137
  * Exits the process directly on a refusal from `resolveSessionTier` — there
114
138
  * is no command dispatched yet for a caller to fall back to, so there is
@@ -116,6 +140,20 @@ async function dispatch(argv) {
116
140
  */
117
141
  export async function launch(argv) {
118
142
  const { explicit, rest } = stripTierFlag(argv);
143
+ // Here rather than only in `cli.ts`'s `main()`, so a failure while
144
+ // resolving or entering the tier below is reported too. `main()`'s own
145
+ // call is then a no-op (`initDevtoolsTelemetry` is idempotent) and this
146
+ // tag, computed from the same argv `main()` receives, stands.
147
+ initDevtoolsTelemetry(rest[0] ?? "menu");
148
+ // Same test `cli.ts`'s own `main()` uses to decide whether a bare command
149
+ // group resumes the wizard instead of dispatching — computed here too so
150
+ // this function can decide, before resolving anything, whether entry gets
151
+ // deferred to `menu.ts`'s hook. A non-TTY caller never resumes the wizard
152
+ // (see `bareGroupStartPath`'s own doc), so `rest.length === 0` is the only
153
+ // menu shape it can hit — mirroring `main()`'s own `process.stdin.isTTY ?
154
+ // bareGroupStartPath(argv) : null`.
155
+ const isMenuInvocation = rest.length === 0 ||
156
+ (process.stdin.isTTY === true && bareGroupStartPath(rest) !== null);
119
157
  // `--help`/`-h` bypasses tier resolution entirely, BEFORE it can refuse.
120
158
  // `cli.ts`'s own `main()` already answers these with no env in play (see
121
159
  // its header comment); resolving a tier first anyway meant `pnpm devtools
@@ -133,6 +171,24 @@ export async function launch(argv) {
133
171
  }
134
172
  const envLoad = await loadEnvLoad();
135
173
  const envSession = await loadEnvSession();
174
+ // Records, once, which keys THIS shell actually exported before devtools
175
+ // touched anything — the fix for the staleness bug `enterEnvironment`'s own
176
+ // `override: false` comment below describes. `Object.keys(process.env)` at
177
+ // this exact point is still a faithful snapshot of the calling shell: the
178
+ // imports above load code, not env; nothing before this line has mutated
179
+ // `process.env`. See `SHELL_KEYS_ENV` (`@devdogsuga/env/load`) for how
180
+ // `loadEnvironment` narrows "a shell var beats the file" to just this list.
181
+ //
182
+ // Guarded on absence so a NESTED devtools launcher (one `pnpm devtools`
183
+ // command spawning another, e.g. the `db start` offer below re-dispatching
184
+ // through `dispatch()`) keeps the ORIGINAL list instead of recomputing one:
185
+ // by the time a nested launcher's `launch()` runs, `process.env` already
186
+ // holds this session's entered values, not the outer shell's, and
187
+ // recomputing from it would misclassify every one of them as a genuine
188
+ // shell export — the exact bug this marker exists to prevent, one layer in.
189
+ if (process.env[envLoad.SHELL_KEYS_ENV] === undefined) {
190
+ process.env[envLoad.SHELL_KEYS_ENV] = Object.keys(process.env).join(",");
191
+ }
136
192
  let tier;
137
193
  let devDatabase;
138
194
  if (rest[0] === "setup" || rest[0] === "completions" || isEnvFreeCommand(rest)) {
@@ -207,116 +263,23 @@ export async function launch(argv) {
207
263
  tier = resolution.tier;
208
264
  devDatabase = resolution.devDatabase;
209
265
  }
210
- // Mandatory, not chattiness: which database a command is about to touch
211
- // must never be a guess. Names the QUALIFIED session (`development:local`)
212
- // when one was resolved. See `session.ts`'s and `load.ts`'s own headers.
213
- const reportEntered = (files, label) => {
214
- process.stderr.write(`devtools: loaded ${files.length > 0 ? files.join(", ") : "no env files"} (${label})\n`);
215
- };
216
- const sessionLabel = devDatabase === undefined ? tier : `${tier}:${devDatabase}`;
217
- try {
218
- // `override: false` — a fresh process, so an already-exported shell
219
- // variable beats the file the same way `with-env` always let it, rather
220
- // than a stale `.env.<tier>` value silently winning over what the caller
221
- // just set for this one invocation.
222
- const entered = await envSession.enterEnvironment(tier, {
223
- override: false,
224
- devDatabase,
225
- });
226
- for (const warning of entered.warnings) {
227
- process.stderr.write(`devtools: ${warning}\n`);
228
- }
229
- reportEntered(entered.files, sessionLabel);
230
- }
231
- catch (err) {
232
- if (err instanceof envLoad.LocalStackOfflineError) {
233
- // The session explicitly means the local database and the stack is not
234
- // reachable.
235
- process.stderr.write(`devtools: ${err.message}\n`);
236
- // Rather than making the contributor stop, run `db start`, and re-run
237
- // whatever they meant, offer to bring the stack up right here and then
238
- // carry on with their original command. Only on a TTY (there is no one
239
- // to answer otherwise), and not when the command is ITSELF a stack
240
- // lifecycle command — `db start`/`stop`/`restart` do this on their own,
241
- // and `db stop` against an already-down stack must not be interrupted by
242
- // an offer to start it. The bare top menu is likewise left to its own
243
- // Database → start entry.
244
- const lifecycle = rest[0] === "db" &&
245
- (rest[1] === "start" || rest[1] === "stop" || rest[1] === "restart");
246
- if (process.stdin.isTTY === true && rest.length !== 0 && !lifecycle) {
247
- const start = unwrap(await confirm({
248
- message: "Start the local Supabase stack now?",
249
- }));
250
- if (start) {
251
- // Keep the session's local qualifier set so the env refresh that
252
- // `runStackCommand("start")` performs on success re-applies the
253
- // overlay under it, and the command we dispatch next resolves the
254
- // freshly-started local database.
255
- if (devDatabase !== undefined)
256
- process.env.DEV_DB = devDatabase;
257
- const { runStackCommand } = await import("./stack.js");
258
- const { code, lines } = await runStackCommand("start", null);
259
- for (const line of lines) {
260
- process.stderr.write(`devtools: ${line}\n`);
261
- }
262
- if (code === 0) {
263
- await dispatch(rest);
264
- return;
265
- }
266
- process.stderr.write("devtools: the stack did not start — see the Supabase CLI output " +
267
- "above.\n");
268
- process.exit(1);
269
- }
270
- }
271
- // Declined, or nobody to ask. `db` (whose `start` is the fix, and whose
272
- // data commands re-check the connection themselves) and the bare menu
273
- // (the road to `db start`) may continue in a degraded, unqualified
274
- // entry; anything else stops here, with the error's own troubleshooting,
275
- // instead of failing later against whatever `.env` happens to name.
276
- if (rest.length !== 0 && rest[0] !== "db")
277
- process.exit(1);
278
- process.stderr.write("devtools: continuing without the overlay so `db start` can fix this.\n");
279
- const inherited = process.env.DEV_DB;
280
- delete process.env.DEV_DB;
281
- const entered = await envSession.enterEnvironment(tier, { override: false });
282
- if (devDatabase !== undefined) {
283
- // Children spawned AFTER `db start` succeeds should still inherit
284
- // the session's answer; the refresh that follows a successful start
285
- // re-applies the overlay under this same variable.
286
- process.env.DEV_DB = devDatabase;
287
- }
288
- else if (inherited !== undefined) {
289
- process.env.DEV_DB = inherited;
290
- }
291
- for (const warning of entered.warnings) {
292
- process.stderr.write(`devtools: ${warning}\n`);
293
- }
294
- reportEntered(entered.files, tier);
295
- await dispatch(rest);
296
- return;
297
- }
298
- if (!(err instanceof envLoad.MissingEnvFileError))
299
- throw err;
300
- if (tier === "development") {
301
- // The clean-clone path: a fresh checkout has no `.env` at all, and
302
- // `pnpm devtools setup` is how one gets created — `MissingEnvFileError`
303
- // names it. Reporting and continuing here, rather than refusing, is
304
- // what lets `setup` itself run through this same entry point. See
305
- // `load.ts`'s `MissingEnvFileError` for the message this prints.
306
- process.stderr.write(`devtools: ${err.message}\n`);
307
- }
308
- else {
309
- // An EXPLICITLY selected staging/production tier with no env file is
310
- // not a first run — it is a real problem, and `env pull` is the fix
311
- // `MissingEnvFileError`'s own message already names. Fatal.
312
- process.stderr.write(`devtools: ${err.message}\n`);
313
- process.exit(1);
314
- }
266
+ if (isMenuInvocation) {
267
+ // Export the decision, not the files: `DEPLOY_ENV`/`DEV_DB` name the
268
+ // session the same way `enterEnvironment` would set them, but loading
269
+ // `.env.<tier>` itself — and everything `enterSessionEnvironment` does
270
+ // around that, including the offline-stack offer — waits for a command to
271
+ // actually be chosen. See this file's header.
272
+ process.env.DEPLOY_ENV = tier;
273
+ if (devDatabase !== undefined)
274
+ process.env.DEV_DB = devDatabase;
275
+ setMenuEnvHook((commandArgv, dispatchCommand) => enterSessionEnvironment(tier, devDatabase, commandArgv, realEnvEntryDeps(envLoad, envSession), dispatchCommand));
276
+ await dispatch(rest);
277
+ return;
315
278
  }
316
279
  // Caught here rather than left to `bin/devtools.mjs`'s `child.on("error",
317
280
  // …)`, which only ever sees a spawn failure, never a rejection thrown
318
281
  // inside this process.
319
- await dispatch(rest);
282
+ await enterSessionEnvironment(tier, devDatabase, rest, realEnvEntryDeps(envLoad, envSession), () => dispatch(rest));
320
283
  }
321
284
  // `bin/devtools.mjs` runs this file directly through tsx — `tsx
322
285
  // --conditions=devdogs-source src/launch.ts <argv…>` — so `process.argv`
package/dist/menu.d.ts CHANGED
@@ -20,11 +20,23 @@ export declare function bareGroupStartPath(argv: readonly string[]): string[] |
20
20
  * `options.startPath`, from `bareGroupStartPath`, skips straight to that
21
21
  * node's subcommand screen — see `walk`'s `startPath` branch.
22
22
  *
23
- * The deploy tier is NOT asked here any more. `src/launch.ts` resolves and
24
- * enters it before `cli.ts` — and therefore this module — is even imported,
25
- * so by the time the wizard opens the whole session is already running under
26
- * it, exactly as if `--tier` had been typed. Recording it for the "run it
27
- * directly next time" line only needs to read `process.env.DEPLOY_ENV` back.
23
+ * The deploy tier is NOT asked here any more. `src/launch.ts` resolves it
24
+ * before `cli.ts` — and therefore this module — is even imported, and
25
+ * exports `DEPLOY_ENV`/`DEV_DB` right away, so by the time the wizard opens
26
+ * `process.env` already names the session exactly as if `--tier` had been
27
+ * typed. Recording it for the "run it directly next time" line only needs to
28
+ * read `process.env.DEPLOY_ENV` back.
29
+ *
30
+ * Entering that tier — loading its `.env.<tier>` file onto `process.env` —
31
+ * is what's deferred: `launch.ts` registers a hook (`env-entry.ts`'s
32
+ * `setMenuEnvHook`) instead of entering up front, so whatever changed while
33
+ * the reader was still walking the menu (the local stack coming up in
34
+ * another terminal, `.env.generated` being rewritten) is picked up rather
35
+ * than missed. `takeMenuEnvHook()` below runs it right before the chosen
36
+ * command dispatches — the ONE place in a menu walk an env value is ever
37
+ * actually read. `undefined` (no hook registered — a typed command entered
38
+ * already, or a test drives `runMenu` directly) just dispatches straight
39
+ * through, unchanged from before this existed.
28
40
  */
29
41
  export declare function runMenu(dispatch: (argv: string[]) => Promise<string | null>, env?: Environment, options?: {
30
42
  startPath?: string[];
package/dist/menu.js CHANGED
@@ -19,6 +19,7 @@
19
19
  import { confirm, note, select, text } from "@clack/prompts";
20
20
  import { positionals } from "./args.js";
21
21
  import { findCommand, GROUPS, SCOPES, } from "./commands.js";
22
+ import { takeMenuEnvHook } from "./env-entry.js";
22
23
  import { blockedBecause, describeEnvironment, isOffered, probeEnvironment, } from "./environment.js";
23
24
  import { beginInvocation, recordEnteredTier } from "./invocation.js";
24
25
  import { unwrap } from "./ui.js";
@@ -41,10 +42,13 @@ function offered(nodes, env) {
41
42
  /**
42
43
  * The line beside a name, with whatever is standing in this command's way.
43
44
  *
44
- * The reader learns that `roundtrip` needs a stack that is not up *before*
45
- * choosing it, instead of after a spinner and a connection error. The command
46
- * stays selectable: the check is a probe, the probe can be wrong, and the
47
- * command's own failure message is the authority on whether it can run.
45
+ * The reader learns a command needs something standing in its way *before*
46
+ * choosing it, instead of after a spinner and a connection error — `db
47
+ * start`'s `when: "instance-stopped"` is the one still in the tree today. The
48
+ * command stays selectable when it merely `needs` rather than requires the
49
+ * condition to be shown at all: the check is a probe, the probe can be
50
+ * wrong, and the command's own failure message is the authority on whether
51
+ * it can run.
48
52
  */
49
53
  function hintFor(node, env) {
50
54
  const base = node.hint ?? node.summary;
@@ -115,8 +119,7 @@ async function pickSubcommand(node, env) {
115
119
  *
116
120
  * An empty array is a real answer, not a failure: a declined confirm adds no
117
121
  * flag, and a blank optional text means "let the command decide", which is
118
- * how `--db-url` falls back to `.env.production` and `--base-url` falls
119
- * through to the OAuth wizard's own prompt.
122
+ * how `--db-url` falls back to `.env.production`.
120
123
  */
121
124
  async function askOption(option) {
122
125
  const prompt = option.prompt;
@@ -241,11 +244,23 @@ export function bareGroupStartPath(argv) {
241
244
  * `options.startPath`, from `bareGroupStartPath`, skips straight to that
242
245
  * node's subcommand screen — see `walk`'s `startPath` branch.
243
246
  *
244
- * The deploy tier is NOT asked here any more. `src/launch.ts` resolves and
245
- * enters it before `cli.ts` — and therefore this module — is even imported,
246
- * so by the time the wizard opens the whole session is already running under
247
- * it, exactly as if `--tier` had been typed. Recording it for the "run it
248
- * directly next time" line only needs to read `process.env.DEPLOY_ENV` back.
247
+ * The deploy tier is NOT asked here any more. `src/launch.ts` resolves it
248
+ * before `cli.ts` — and therefore this module — is even imported, and
249
+ * exports `DEPLOY_ENV`/`DEV_DB` right away, so by the time the wizard opens
250
+ * `process.env` already names the session exactly as if `--tier` had been
251
+ * typed. Recording it for the "run it directly next time" line only needs to
252
+ * read `process.env.DEPLOY_ENV` back.
253
+ *
254
+ * Entering that tier — loading its `.env.<tier>` file onto `process.env` —
255
+ * is what's deferred: `launch.ts` registers a hook (`env-entry.ts`'s
256
+ * `setMenuEnvHook`) instead of entering up front, so whatever changed while
257
+ * the reader was still walking the menu (the local stack coming up in
258
+ * another terminal, `.env.generated` being rewritten) is picked up rather
259
+ * than missed. `takeMenuEnvHook()` below runs it right before the chosen
260
+ * command dispatches — the ONE place in a menu walk an env value is ever
261
+ * actually read. `undefined` (no hook registered — a typed command entered
262
+ * already, or a test drives `runMenu` directly) just dispatches straight
263
+ * through, unchanged from before this existed.
249
264
  */
250
265
  export async function runMenu(dispatch, env = probeEnvironment(), options = {}) {
251
266
  // Before the first question, not after a failure. Three lines saying what
@@ -273,5 +288,13 @@ export async function runMenu(dispatch, env = probeEnvironment(), options = {})
273
288
  recordEnteredTier(enteredTier === "development" && (devDb === "local" || devDb === "remote")
274
289
  ? `development:${devDb}`
275
290
  : enteredTier);
291
+ // The deferred entry, if `launch.ts` registered one — see this function's
292
+ // header. Read-and-cleared in one call so a nested launcher (this same
293
+ // command re-dispatching through its own `launch()`, e.g. `db start`'s
294
+ // offline-stack offer) never inherits a hook meant for this walk.
295
+ const enterEnvironment = takeMenuEnvHook();
296
+ if (enterEnvironment) {
297
+ return enterEnvironment(chosen.argv, () => dispatch(chosen.argv));
298
+ }
276
299
  return dispatch(chosen.argv);
277
300
  }
@@ -1,31 +1,28 @@
1
1
  /**
2
2
  * Does my app's moderation integration actually work?
3
3
  *
4
- * Two checks, and the second is the one that matters.
5
- *
6
4
  * `conformance()` asks the database what it derived from your schema: is the
7
5
  * table addressable, can an author be attributed, does quarantine have a column
8
6
  * to write to, can a client still write that column. All of it is answerable
9
7
  * from the catalog, and `platform.conformance_check()` does the answering.
10
8
  *
11
- * `quarantineRoundTrip()` answers the question the catalog cannot: *does
12
- * quarantine do anything?* Quarantine is the only moderation outcome whose
13
- * effect lives in the app's own policies rather than the platform's, so it is
14
- * the only one that can be wired up wrong while everything appears to work.
15
- * The platform records the decision, sets the column, and has no way to notice
16
- * nobody reads it. The only proof is to file, resolve, and then look.
17
- *
18
- * It runs against `platform.profile`, where quarantine means FREEZE rather than
19
- * hide: the display name is reset to the member's name of record and they lose
20
- * the ability to change it back. A profile cannot be hidden: rosters, teams
21
- * and standings all render the name, and the row is own-row-only over PostgREST
22
- * anyway, so there is nobody to hide it from. See
23
- * 20260808000000_platform_profile_moderation.sql.
9
+ * The end-to-end round trip that used to live here (`quarantineRoundTrip`,
10
+ * `moderation roundtrip`) is gone: the app repo's own CI covers that path —
11
+ * file, quarantine, and check the freeze — more thoroughly than a devtools
12
+ * command driven by hand ever did, and keeping two implementations of the
13
+ * same assertion around was a drift risk with no offsetting benefit.
24
14
  *
25
- * Both run as seeded personas over PostgREST rather than as `postgres` over
26
- * SQL, because RLS is the thing under test and a superuser bypasses it.
15
+ * Both `conformance()` and `catalog.ts`'s `readCatalog()` need to run as a
16
+ * moderator, subject to the same RLS and `canModerate` checks a real one
17
+ * would hit, rather than as `postgres` bypassing them. `withTemporaryModerator`
18
+ * below is what supplies that account: a throwaway user, created through the
19
+ * admin API and deleted again in a `finally`, rather than the seeded
20
+ * `moderator@devdogs.test` persona this package used to sign in as — that
21
+ * persona came from `supabase/seed/development/02_moderation.sql`, which only
22
+ * ever ran on a local reset and is going away along with the rest of
23
+ * `seed/development/` (see `persona.ts`'s header for the replacement).
27
24
  */
28
- import { type Instance } from "./instance.js";
25
+ import { type DevtoolsClient, type Instance } from "./instance.js";
29
26
  import type { CheckResult } from "./ui.js";
30
27
  interface ConformanceType {
31
28
  contentType: string;
@@ -33,30 +30,41 @@ interface ConformanceType {
33
30
  tableName: string;
34
31
  checks: CheckResult[];
35
32
  }
36
- /** Runs `platform.conformance_check()` as the seeded moderator. */
37
- export declare function conformance(instance: Instance, appSlug: string): Promise<ConformanceType[]>;
38
- /** The apps that have anything to check, for the picker. */
39
- export declare function listApps(instance: Instance): Promise<string[]>;
40
- export interface RoundTripStep {
41
- name: string;
42
- ok: boolean;
43
- detail: string;
44
- }
33
+ /** Runs `platform.conformance_check()` as the given moderator client. */
34
+ export declare function conformance(client: DevtoolsClient, appSlug: string): Promise<ConformanceType[]>;
35
+ /**
36
+ * The role every devtools-granted moderator holds — `moderation check`'s
37
+ * throwaway account and `persona moderator`'s longer-lived one alike, so
38
+ * both resolve to the SAME role row rather than each accumulating their own.
39
+ *
40
+ * Reuses the id `supabase/seed/development/02_moderation.sql` used to seed
41
+ * this same role under, on the theory that an existing local database that
42
+ * still has that seeded row should be upserted into agreement rather than
43
+ * given a second, redundant "Moderator" role.
44
+ */
45
+ export declare const MODERATOR_ROLE_ID = "00000000-0000-4000-9000-000000000001";
45
46
  /**
46
- * Files a report against a profile, resolves it with quarantine, and checks
47
- * that the freeze and the name reset both landed.
47
+ * Creates (or updates) the Moderator role definition, and returns its id.
48
48
  *
49
- * The subject is a THROWAWAY member created for the run, not one of the seeded
50
- * personas, and that is not squeamishness about touching fixtures.
51
- * `file_report` corroborates an existing open report rather than filing a
52
- * second one against the same content, and the seeds ship exactly such a report
53
- * against the author persona's profile. Reusing it would make this function's
54
- * first step silently become "add a corroboration", and every assertion after
55
- * it would be reading the seed's decision instead of its own.
49
+ * `canModerate: true` and nothing else — not Root, not any of the other
50
+ * `roles` permission columns — mirroring exactly what the seed persona held.
51
+ * Idempotent (`upsert`) so repeated calls, from either caller above, never
52
+ * race each other into two rows or fail on the second one.
53
+ */
54
+ export declare function ensureModeratorRole(admin: DevtoolsClient): Promise<string>;
55
+ /** Grants the Moderator role to `userId`, creating the role definition first
56
+ * if this is the first account to need it. */
57
+ export declare function grantModerator(admin: DevtoolsClient, userId: string): Promise<void>;
58
+ /**
59
+ * Runs `fn` with a client signed in as a throwaway moderator, created for
60
+ * this call and deleted again in a `finally` whether or not `fn` throws.
56
61
  *
57
- * Everything it creates, it deletes, including on the failure paths, which is
58
- * why the teardown is in a `finally`. A contributor running this twice should
59
- * get the same answer the second time.
62
+ * The admin API's `createUser` bypasses the app's `before_user_created`
63
+ * uga.edu hook (that hook only runs on the sign-up/OTP paths a real user
64
+ * takes, not the service-role admin API), which is why a `@persona.test`
65
+ * address works here at all — see `persona.ts`'s header for the same note
66
+ * in more detail, since that is the command a contributor is more likely to
67
+ * read first.
60
68
  */
61
- export declare function quarantineRoundTrip(instance: Instance): Promise<RoundTripStep[]>;
69
+ export declare function withTemporaryModerator<T>(instance: Instance, fn: (client: DevtoolsClient) => Promise<T>): Promise<T>;
62
70
  export {};