@coffre/cli 0.1.10 → 0.1.12

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.
@@ -1,2 +1,2 @@
1
- import { n as openDatabase, r as postgresConnection, t as engineOfUrl } from "./connect-BONR52al.js";
1
+ import { n as openDatabase, r as postgresConnection, t as engineOfUrl } from "./connect-tekAM4hC.js";
2
2
  export { engineOfUrl, openDatabase, postgresConnection };
@@ -1,9 +1,10 @@
1
1
  import { d as esm_default$1, m as __toESM, p as require_pg_connection_string } from "./esm-Bts-esAl.js";
2
2
  import { n as ROLE_NAMES, r as assignableToEnvironment } from "./access-CO1kGjWW.js";
3
- import { A as bigint, G as sql, H as SQL, O as customType, T as integer, ct as entityKind, f as primaryKey, g as uuid, it as foreignKey, k as boolean, lt as is, m as pgTable, rt as unique, tt as IndexedColumn, x as text, y as timestamp } from "./relations-CJCGPRst.js";
4
- import { r as drizzle } from "./node-postgres-_y-EeSHQ.js";
5
- import { a as trackCommits, i as relationsOf, n as asPostgresDatabase, r as ACTIVE_SUBJECT } from "./portable-CPh2thNN.js";
3
+ import { D as customType, O as boolean, V as SQL, W as sql, b as text, ct as is, et as IndexedColumn, h as uuid, k as bigint, nt as unique, p as pgTable, rt as foreignKey, st as entityKind, v as timestamp, w as integer } from "./relations-BuTStNq-.js";
4
+ import { r as drizzle } from "./node-postgres-BHgGDfvg.js";
5
+ import { a as trackCommits, i as relationsOf, n as asPostgresDatabase, r as ACTIVE_SUBJECT } from "./portable-9wWdi5e1.js";
6
6
  import { resolve } from "node:path";
7
+ import { AsyncLocalStorage } from "node:async_hooks";
7
8
  import { checkServerIdentity } from "node:tls";
8
9
  //#region ../../node_modules/.pnpm/drizzle-orm@0.45.2_@cloudflare+workers-types@5.20260920.1_@libsql+client@0.18.0_@types+pg@8.20.0_pg@8.16.3/node_modules/drizzle-orm/pg-core/checks.js
9
10
  var CheckBuilder = class {
@@ -150,10 +151,6 @@ var schema_exports = /* @__PURE__ */ __exportAll({
150
151
  secretVersions: () => secretVersions,
151
152
  secrets: () => secrets,
152
153
  secretsRelations: () => secretsRelations,
153
- syncKeys: () => syncKeys,
154
- syncKeysRelations: () => syncKeysRelations,
155
- syncs: () => syncs,
156
- syncsRelations: () => syncsRelations,
157
154
  vaultGrants: () => vaultGrants,
158
155
  vaultMembers: () => vaultMembers
159
156
  });
@@ -502,86 +499,67 @@ const deviceAuthorizations = pgTable("device_authorizations", {
502
499
  foreignColumns: [vaultMembers.principal]
503
500
  }).onDelete("restrict")
504
501
  ]);
505
- /**
506
- * An environment kept in step with a third-party service: GitHub Actions
507
- * secrets, Vercel or Railway variables, Worker secrets.
508
- *
509
- * The destination's API token is itself a coffre secret, referenced by id, so
510
- * it is encrypted, versioned and audited like everything else and never sits
511
- * in this table.
512
- */
513
- const syncs = pgTable("syncs", {
514
- id: uuid().primaryKey().defaultRandom(),
515
- projectId: uuid("project_id").notNull(),
516
- environmentId: uuid("environment_id").notNull(),
517
- provider: text().notNull(),
518
- config: text().notNull(),
519
- credentialSecretId: uuid("credential_secret_id").notNull(),
520
- createdAt: createdAt(),
521
- createdBy: text("created_by").notNull(),
522
- pausedAt: timestamp("paused_at", { withTimezone: true }),
523
- archivedAt: timestamp("archived_at", { withTimezone: true }),
524
- leaseUntil: timestamp("lease_until", { withTimezone: true }),
525
- lastRunAt: timestamp("last_run_at", { withTimezone: true }),
526
- lastStatus: text("last_status"),
527
- lastError: text("last_error")
528
- }, (table) => [
529
- check("syncs_config_check", sql`${table.config}::jsonb IS NOT NULL`),
530
- check("syncs_last_status_check", sql`${table.lastStatus} IS NULL OR ${table.lastStatus} IN ('ok', 'partial', 'failed')`),
531
- foreignKey({
532
- name: "syncs_environment_in_project",
533
- columns: [table.environmentId, table.projectId],
534
- foreignColumns: [environments.id, environments.projectId]
535
- }).onDelete("restrict"),
536
- foreignKey({
537
- name: "syncs_credential_secret_id_fkey",
538
- columns: [table.credentialSecretId],
539
- foreignColumns: [secrets.id]
540
- }).onDelete("restrict"),
541
- index("syncs_environment_idx").on(table.environmentId)
542
- ]);
543
- /**
544
- * What a sync last pushed, one row per key.
545
- *
546
- * Most destinations are write-only, so coffre cannot diff against them. It
547
- * diffs against this instead: a key is stale when its secret has moved past
548
- * the version recorded here. It is also the list of keys coffre may delete at
549
- * the destination; a key it never pushed is never removed.
550
- */
551
- const syncKeys = pgTable("sync_keys", {
552
- syncId: uuid("sync_id").notNull(),
553
- key: text().notNull(),
554
- secretVersionId: uuid("secret_version_id"),
555
- pushedAt: timestamp("pushed_at", { withTimezone: true }).notNull().defaultNow(),
556
- removedAt: timestamp("removed_at", { withTimezone: true })
557
- }, (table) => [
558
- primaryKey({
559
- name: "sync_keys_pkey",
560
- columns: [table.syncId, table.key]
561
- }),
562
- foreignKey({
563
- name: "sync_keys_sync_id_fkey",
564
- columns: [table.syncId],
565
- foreignColumns: [syncs.id]
566
- }).onDelete("restrict"),
567
- foreignKey({
568
- name: "sync_keys_secret_version_id_fkey",
569
- columns: [table.secretVersionId],
570
- foreignColumns: [secretVersions.id]
571
- }).onDelete("restrict")
572
- ]);
573
- const { environmentsRelations, secretsRelations, syncsRelations, syncKeysRelations } = relationsOf({
502
+ const { environmentsRelations, secretsRelations } = relationsOf({
574
503
  projects,
575
504
  environments,
576
505
  secrets,
577
- secretVersions,
578
- syncs,
579
- syncKeys
506
+ secretVersions
580
507
  });
581
508
  //#endregion
582
509
  //#region ../db/dist/index.js
510
+ /**
511
+ * A query made on the database from inside one of its own transactions,
512
+ * rather than through the transaction. With one connection per call, as on
513
+ * Workers, it would wait for the transaction, which waits for it: a call
514
+ * Cloudflare cancels as hung, and nothing says why. So it is refused at
515
+ * once, with this, wherever the database runs.
516
+ */
517
+ var QueryOutsideTransaction = class extends Error {
518
+ constructor() {
519
+ super("query outside its transaction: use tx. A query on the database inside one of its own transactions waits for that transaction, which waits for it");
520
+ this.name = "QueryOutsideTransaction";
521
+ }
522
+ };
523
+ /** The transaction a flow of work runs inside, with the pool it holds, while it is open. */
524
+ const inside = new AsyncLocalStorage();
525
+ /**
526
+ * `pool`, refusing a query or a transaction asked from inside a transaction
527
+ * it is already running. Work from any other flow, a concurrent request's,
528
+ * or what the transaction left running after it ended, goes on as before.
529
+ */
530
+ function guarded(pool) {
531
+ return new Proxy(pool, { get(target, key) {
532
+ const value = Reflect.get(target, key, target);
533
+ if (typeof value !== "function") return value;
534
+ if (key !== "query" && key !== "connect") return value.bind(target);
535
+ return (...args) => {
536
+ const open = inside.getStore();
537
+ if (open?.open === true && open.pool === target) throw new QueryOutsideTransaction();
538
+ return value.apply(target, args);
539
+ };
540
+ } });
541
+ }
542
+ /** The refusal `error` is, or was caused by. */
543
+ function brokenRule(error) {
544
+ for (let cause = error; cause instanceof Error; cause = cause.cause) if (cause instanceof QueryOutsideTransaction) return cause;
545
+ }
583
546
  function createDatabase(pool) {
584
- return trackCommits(drizzle(pool, { schema: schema_exports }));
547
+ const db = drizzle(guarded(pool), { schema: schema_exports });
548
+ const run = db.transaction.bind(db);
549
+ db.transaction = (async (work, config) => {
550
+ const open = {
551
+ pool,
552
+ open: true
553
+ };
554
+ try {
555
+ return await run((tx) => inside.run(open, () => work(tx)), config);
556
+ } catch (error) {
557
+ throw brokenRule(error) ?? error;
558
+ } finally {
559
+ open.open = false;
560
+ }
561
+ });
562
+ return trackCommits(db);
585
563
  }
586
564
  //#endregion
587
565
  //#region ../../node_modules/.pnpm/pg-connection-string@2.14.0/node_modules/pg-connection-string/esm/index.mjs
@@ -642,7 +620,7 @@ async function openDatabase(url) {
642
620
  const [{ createClient }, { drizzle }, schema] = await Promise.all([
643
621
  import("@libsql/client"),
644
622
  import("drizzle-orm/libsql"),
645
- import("./schema-sqlite-Byp7B3yI.js")
623
+ import("./schema-sqlite-BaFfy9lC.js")
646
624
  ]);
647
625
  const client = createClient({
648
626
  url,
package/dist/main.js CHANGED
@@ -8,30 +8,6 @@ import { homedir, hostname } from "node:os";
8
8
  import { dirname, join, resolve } from "node:path";
9
9
  import { createCipheriv, createDecipheriv, createHash, hkdfSync, randomBytes, timingSafeEqual } from "node:crypto";
10
10
  //#region ../client/dist/index.js
11
- /**
12
- * The config from `name=value` arguments, as the CLI takes them. A field with
13
- * several options takes them comma-separated, and one left out gets what the
14
- * form preselects. Text goes as typed, for the server to check.
15
- */
16
- function configFromArguments(provider, assignments) {
17
- const config = {};
18
- for (const assignment of assignments) {
19
- const equals = assignment.indexOf("=");
20
- const name = equals === -1 ? assignment : assignment.slice(0, equals);
21
- const field = provider.fields.find((candidate) => candidate.name === name);
22
- if (equals === -1 || field === void 0) {
23
- const names = provider.fields.map((candidate) => candidate.name).join(", ");
24
- throw new Error(`${provider.label} takes ${names} as name=value; got ${JSON.stringify(assignment)}`);
25
- }
26
- if (Object.hasOwn(config, name)) throw new Error(`${name} is given twice`);
27
- const value = assignment.slice(equals + 1);
28
- if (field.type === "text") config[name] = value;
29
- else if (field.multiple) config[name] = value.split(",").map((item) => item.trim()).filter((item) => item !== "");
30
- else config[name] = value;
31
- }
32
- for (const field of provider.fields) if (field.type === "options" && !Object.hasOwn(config, field.name)) config[field.name] = field.multiple ? field.initial : field.initial[0];
33
- return config;
34
- }
35
11
  /** The API's error shape, `{ error, message }`, with the HTTP status. */
36
12
  const COFFRE_ERROR = Symbol.for("@coffre/client:CoffreError");
37
13
  var CoffreError = class extends Error {
@@ -174,15 +150,6 @@ set: (member, access) => call("PATCH /access/:member", { member }, access) },
174
150
  get: (code) => call("GET /device-logins/:code", { code }),
175
151
  decide: (code, approve) => call("POST /device-logins/:code", { code }, { approve })
176
152
  },
177
- syncs: {
178
- /** Where a sync can push on this instance, and what each asks for. */
179
- providers: () => call("GET /syncs/providers", {}),
180
- list: (path) => call("GET /syncs/:project/:environment", place(path)),
181
- add: (path, input) => call("POST /syncs/:project/:environment", place(path), input),
182
- update: (id, patch) => call("PATCH /syncs/by-id/:id", { id }, patch),
183
- remove: (id) => call("DELETE /syncs/by-id/:id", { id }),
184
- run: (id) => call("POST /syncs/by-id/:id/runs", { id })
185
- },
186
153
  audit: {
187
154
  list: (query = {}) => call("GET /audit", {}, query),
188
155
  verify: () => call("GET /audit/verification", {}),
@@ -521,8 +488,6 @@ function getCalls({ places, secrets, members, services }) {
521
488
  "GET /sessions": [{ params: {} }],
522
489
  "GET /identities": [{ params: {} }],
523
490
  "GET /device-logins/:code": [{ params: { code: "BCDF-GHJK" } }],
524
- "GET /syncs/providers": [{ params: {} }],
525
- "GET /syncs/:project/:environment": places.map((params) => ({ params })),
526
491
  "GET /audit": [{
527
492
  params: {},
528
493
  input: { limit: 500 }
@@ -574,12 +539,6 @@ const EVERY_ROUTE = {
574
539
  "DELETE /identities/:id": true,
575
540
  "GET /device-logins/:code": true,
576
541
  "POST /device-logins/:code": true,
577
- "GET /syncs/providers": true,
578
- "GET /syncs/:project/:environment": true,
579
- "POST /syncs/:project/:environment": true,
580
- "PATCH /syncs/by-id/:id": true,
581
- "DELETE /syncs/by-id/:id": true,
582
- "POST /syncs/by-id/:id/runs": true,
583
542
  "GET /audit": true,
584
543
  "GET /audit/verification": true,
585
544
  "GET /audit/keys": true
@@ -939,8 +898,7 @@ async function tokenScope(api, canary) {
939
898
  for (const path of [...others, nowhere]) {
940
899
  await refused(`the token listed ${path}`, api.secrets.list(path));
941
900
  await refused(`the token revealed ${path}`, api.secrets.reveal(path));
942
- await refused(`the token read ${path}'s syncs`, api.syncs.list(path));
943
- refusals += 3;
901
+ refusals += 2;
944
902
  }
945
903
  await refused(`the token read ${nowhere}'s audit`, api.audit.list({
946
904
  path: nowhere,
@@ -2325,13 +2283,6 @@ async function offboard(args) {
2325
2283
  report.exposed.forEach((entry, index) => {
2326
2284
  process.stdout.write(` ${names[index].padEnd(width)} v${String(entry.version).padEnd(4)} ${entry.how} ${entry.at.slice(0, 10)}\n`);
2327
2285
  });
2328
- if (report.syncs.length > 0) {
2329
- process.stdout.write(`\nSyncs ${they} set up, which keep pushing\n`);
2330
- for (const entry of report.syncs) {
2331
- const paused = entry.paused ? " (paused)" : "";
2332
- process.stdout.write(` ${entry.project}/${entry.environment} -> ${entry.providerLabel} ${entry.destination}${paused}\n`);
2333
- }
2334
- }
2335
2286
  if (report.issuedTokens.length > 0) {
2336
2287
  process.stdout.write(`\nService tokens ${they} issued, which still work\n`);
2337
2288
  for (const token of report.issuedTokens) {
@@ -2421,138 +2372,6 @@ async function verifyLog() {
2421
2372
  process.stderr.write(`audit log BROKEN${at}, found by the ${result.author}'s check (${through}): ${result.reason}\n`);
2422
2373
  process.exit(2);
2423
2374
  }
2424
- const SYNC_USAGE = `usage:
2425
- coffre sync providers
2426
- coffre sync list <project>/<environment>
2427
- coffre sync add <project>/<environment> <provider> name=value… --credential <project>/<environment>/<KEY>
2428
- coffre sync run <project>/<environment> <sync>
2429
- coffre sync pause <project>/<environment> <sync>
2430
- coffre sync resume <project>/<environment> <sync>
2431
- coffre sync remove <project>/<environment> <sync>
2432
-
2433
- \`coffre sync providers\` lists where this instance can sync to, and what each
2434
- provider takes. <sync> is the start of an id from \`coffre sync list\`, or the
2435
- provider's id when the environment syncs there only once.
2436
- `;
2437
- /** Where this instance can sync to, one provider to a paragraph. */
2438
- function printProviders(providers) {
2439
- if (providers.length === 0) {
2440
- process.stdout.write("This instance offers no sync providers.\n");
2441
- return;
2442
- }
2443
- process.stdout.write("What is in brackets can be left out. A choice left out is what comes\nbefore the first |, and several choices go comma-separated.\n");
2444
- for (const provider of providers) process.stdout.write(`\n${provider.id} ${provider.label}\n ${[...provider.fields.map(fieldUsage), `--credential ${provider.credential.placeholder}`].join(" ")}\n${indented(provider.credential.hint)}\n`);
2445
- }
2446
- /** Prose under a usage line, indented and wrapped to a terminal's 80 columns. */
2447
- function indented(text) {
2448
- const lines = [];
2449
- for (const word of text.split(/\s+/)) {
2450
- const last = lines.length - 1;
2451
- if (last >= 0 && lines[last].length + 1 + word.length <= 76) lines[last] += ` ${word}`;
2452
- else lines.push(word);
2453
- }
2454
- return lines.map((line) => ` ${line}`).join("\n");
2455
- }
2456
- function fieldUsage(field) {
2457
- if (field.type === "text") return field.optional ? `[${field.name}=…]` : `${field.name}=…`;
2458
- const others = field.options.map((option) => option.value).filter((value) => !field.initial.includes(value));
2459
- return `[${field.name}=${[field.initial.join(","), ...others].join("|")}]`;
2460
- }
2461
- /**
2462
- * Push an environment somewhere else and keep it current. The server does
2463
- * the work and checks every field; this names things and reports back.
2464
- */
2465
- async function sync(args) {
2466
- const [verb, path, ...rest] = args;
2467
- if (verb === "providers") {
2468
- const { providers } = await client().syncs.providers();
2469
- printProviders(providers);
2470
- return;
2471
- }
2472
- if (verb === void 0 || verb === "--help" || path === void 0) {
2473
- process.stdout.write(SYNC_USAGE);
2474
- process.exit(verb === void 0 || verb === "--help" ? 0 : 1);
2475
- }
2476
- const { project, environment, key } = parsePath(path);
2477
- if (key !== void 0) fail(`syncs belong to an environment: use ${project}/${environment}`);
2478
- const place = `${project}/${environment}`;
2479
- const coffre = client();
2480
- if (verb === "list") {
2481
- const { syncs } = await coffre.syncs.list(place);
2482
- if (syncs.length === 0) process.stdout.write(`${project}/${environment} is not synced anywhere\n`);
2483
- for (const entry of syncs) printSync(entry);
2484
- return;
2485
- }
2486
- if (verb === "add") {
2487
- const { values, positionals } = parseArgs({
2488
- args: rest,
2489
- options: { credential: { type: "string" } },
2490
- allowPositionals: true
2491
- });
2492
- const [id, ...assignments] = positionals;
2493
- const { providers } = await coffre.syncs.providers();
2494
- const provider = providers.find((candidate) => candidate.id === id);
2495
- if (provider === void 0) fail(`name a provider this instance offers (${providers.length === 0 ? "none" : providers.map((candidate) => candidate.id).join(", ")}); \`coffre sync providers\` shows what each takes`);
2496
- if (values.credential === void 0) fail(`--credential names the secret holding the ${provider.label} token, such as ${provider.credential.placeholder}`);
2497
- const config = attempt(() => configFromArguments(provider, assignments));
2498
- const created = await coffre.syncs.add(place, {
2499
- provider: provider.id,
2500
- config,
2501
- credential: values.credential
2502
- });
2503
- process.stdout.write(`Syncing ${project}/${environment} to ${created.destination} (${created.providerLabel}), id ${created.id.slice(0, 8)}.\nThe first push has started; \`coffre sync list ${project}/${environment}\` shows how it went.\n`);
2504
- return;
2505
- }
2506
- if (![
2507
- "run",
2508
- "pause",
2509
- "resume",
2510
- "remove"
2511
- ].includes(verb)) fail(`unknown sync command "${verb}"; see \`coffre sync --help\``);
2512
- const reference = rest[0];
2513
- if (reference === void 0) fail(`usage: coffre sync ${verb} ${project}/${environment} <sync>`);
2514
- const { syncs } = await coffre.syncs.list(place);
2515
- const chosen = attempt(() => pickSync(syncs, reference));
2516
- if (verb === "run") {
2517
- if (chosen.paused) fail(`syncing to ${chosen.destination} is paused; resume it first`);
2518
- const { sync: after, outcome } = await coffre.syncs.run(chosen.id);
2519
- if (outcome.status === "busy") fail(`a run to ${chosen.destination} is already under way`);
2520
- for (const failure of outcome.failed) process.stderr.write(` could not ${failure.operation === "upsert" ? "push" : "remove"} ${failure.key}: ${failure.message}\n`);
2521
- if (outcome.status === "failed") fail(`could not sync to ${after.destination}: ${outcome.error ?? "the run failed"}`);
2522
- const changed = outcome.upserted.length + outcome.deleted.length;
2523
- process.stdout.write(changed === 0 && outcome.failed.length === 0 ? `${after.destination} was already current\n` : `Pushed ${outcome.upserted.length}, removed ${outcome.deleted.length} at ${after.destination}\n`);
2524
- if (outcome.status === "partial") process.exit(1);
2525
- return;
2526
- }
2527
- if (verb === "remove") {
2528
- await coffre.syncs.remove(chosen.id);
2529
- process.stdout.write(`No longer syncing to ${chosen.destination}. Keys coffre pushed there stay; remove them at ${chosen.providerLabel} if they should go too.\n`);
2530
- return;
2531
- }
2532
- const paused = verb === "pause";
2533
- await coffre.syncs.update(chosen.id, { paused });
2534
- process.stdout.write(paused ? `Paused syncing to ${chosen.destination}\n` : `Resumed syncing to ${chosen.destination}; changes since the pause go out on the next run\n`);
2535
- }
2536
- /** By id prefix, or by provider when only one sync here goes there. */
2537
- function pickSync(syncs, reference) {
2538
- const byProvider = syncs.filter((entry) => entry.provider === reference);
2539
- if (byProvider.length === 1) return byProvider[0];
2540
- if (byProvider.length > 1) throw new Error(`more than one ${reference} sync here; name it by id (coffre sync list)`);
2541
- if (reference.length < 4) throw new Error("give at least 4 characters of a sync id, or a provider id");
2542
- const byId = syncs.filter((entry) => entry.id.startsWith(reference.toLowerCase()));
2543
- if (byId.length === 1) return byId[0];
2544
- if (byId.length > 1) throw new Error(`"${reference}" starts more than one sync id; give more of it`);
2545
- throw new Error(`no sync "${reference}" here; \`coffre sync list\` shows them`);
2546
- }
2547
- function printSync(entry) {
2548
- const state = entry.running ? "pushing" : entry.paused ? "paused" : entry.lastStatus === "failed" ? "failed" : entry.lastStatus === "partial" ? "partial" : entry.lastRunAt === null ? "not run yet" : entry.pending > 0 ? "behind" : "in sync";
2549
- const counts = [`${entry.synced} synced`, ...entry.pending > 0 ? [`${entry.pending} pending`] : []];
2550
- if (entry.lastRunAt !== null) counts.push(`last run ${entry.lastRunAt.slice(0, 16).replace("T", " ")}`);
2551
- process.stdout.write(`${entry.id.slice(0, 8)} ${entry.providerLabel.padEnd(18)} ${entry.destination}\n`);
2552
- process.stdout.write(` ${state}: ${counts.join(", ")}; token from ${entry.credential}\n`);
2553
- if (entry.lastError !== null && !entry.running) process.stdout.write(` ${entry.lastError}\n`);
2554
- for (const skipped of entry.skipped) process.stdout.write(` skips ${skipped.key}: ${skipped.reason}\n`);
2555
- }
2556
2375
  /** `coffre init --workers|--node [<dir>]`: a new deployment of coffre. */
2557
2376
  function initProject(args) {
2558
2377
  const { values, positionals } = parseArgs({
@@ -2609,11 +2428,6 @@ const USAGE = `coffre - secrets, with an audit log
2609
2428
  coffre offboard <principal> [--service] [--apply]
2610
2429
  what removing them revokes, and what to rotate
2611
2430
 
2612
- Syncs (coffre sync providers for where to, and what each takes)
2613
- coffre sync list <project>/<environment>
2614
- coffre sync add <project>/<environment> <provider> name=value…
2615
- --credential <project>/<environment>/<KEY>
2616
- coffre sync run|pause|resume|remove <project>/<environment> <sync>
2617
2431
 
2618
2432
  Audit
2619
2433
  coffre audit [--limit N] [--actor <id>] [--denied] [--detail]
@@ -2639,7 +2453,7 @@ switch (command) {
2639
2453
  await keys(rest);
2640
2454
  break;
2641
2455
  case "setup":
2642
- await (await import("./setup-D3SXt_P-.js")).setup(rest);
2456
+ await (await import("./setup-BMmFAR3N.js")).setup(rest);
2643
2457
  break;
2644
2458
  case "login":
2645
2459
  await login(rest);
@@ -2692,9 +2506,6 @@ switch (command) {
2692
2506
  case "offboard":
2693
2507
  await offboard(rest);
2694
2508
  break;
2695
- case "sync":
2696
- await sync(rest);
2697
- break;
2698
2509
  case "audit":
2699
2510
  await audit(rest);
2700
2511
  break;
@@ -0,0 +1,13 @@
1
+ -- Stop old app instances before upgrading. Never silently discard a configured
2
+ -- destination, including archived ones. Keep the audit and sealed legacy rows.
3
+ LOCK TABLE syncs, sync_keys IN ACCESS EXCLUSIVE MODE;
4
+ --> statement-breakpoint
5
+ DO $$ BEGIN
6
+ IF EXISTS (SELECT 1 FROM syncs) OR EXISTS (SELECT 1 FROM sync_keys) THEN
7
+ RAISE EXCEPTION 'syncs are removed: migrate destinations to service tokens, back up and clear syncs and sync_keys before upgrading';
8
+ END IF;
9
+ END $$;
10
+ --> statement-breakpoint
11
+ DROP TABLE sync_keys;
12
+ --> statement-breakpoint
13
+ DROP TABLE syncs;