@kedge/cli 0.10.0 → 0.11.0

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.
package/src/main.ts CHANGED
@@ -37,7 +37,7 @@ import {
37
37
  } from "./commands";
38
38
  import { ComposeError, compose, type Lock } from "./compose";
39
39
  import { type Executor, wranglerExecute } from "./d1-batch";
40
- import { credentialsChecks, doctor, doctorRemote, formatReport } from "./doctor";
40
+ import { credentialsChecks, doctor, doctorRemote, formatReport, readWrangler } from "./doctor";
41
41
  import { invariantsMarkdown, invariantsOf } from "./invariants";
42
42
  import { installedPackages } from "./manifest";
43
43
  import { emit, withJsonOutput } from "./output";
@@ -118,10 +118,18 @@ async function inviteOrigin(
118
118
  ): Promise<string | null> {
119
119
  const url = flag(args, "--url");
120
120
  if (url) return new URL(url).origin;
121
- if (!remote) return null;
121
+ // Local: kedge dev's own origin, the port it binds (p0.10.1 §1, 5; §3).
122
+ if (!remote) {
123
+ try {
124
+ const { devPort } = await import("./target");
125
+ return `http://localhost:${devPort(cwd)}`;
126
+ } catch {
127
+ return null;
128
+ }
129
+ }
122
130
  try {
123
- const worker = storeNames(cwd).names.worker;
124
- return `https://${worker}.${await (cf ?? (await cloudflareFor(env, cwd))).workersSubdomain()}.workers.dev`;
131
+ // Only where it's served: elsewhere (salve hosting) the link needs --url, the address its host gives.
132
+ return await (cf ?? (await cloudflareFor(env, cwd))).workersDevUrl(storeNames(cwd).names.worker);
125
133
  } catch {
126
134
  return null;
127
135
  }
@@ -360,7 +368,12 @@ async function run(argv: string[], cwd: string, deps: RunDeps & { json: boolean
360
368
  if (auth) {
361
369
  const cf = restCloudflare(d.env, fetch, wranglerBin(), auth);
362
370
  const lock = JSON.parse(readFileSync(join(cwd, "kedge.lock.json"), "utf8")) as Lock;
363
- const creds = await credentialsChecks({ cf, source: auth.source, base: cloudflareBase(d.env) });
371
+ const creds = await credentialsChecks({
372
+ cf,
373
+ source: auth.source,
374
+ base: cloudflareBase(d.env),
375
+ workersDev: readWrangler(cwd)?.config.workers_dev !== false,
376
+ });
364
377
  results.push(...creds);
365
378
  // Credentials that can't read the account would make every later check throw, not report (codex code r7).
366
379
  if (creds.some((c) => c.status === "fail"))
@@ -372,8 +385,17 @@ async function run(argv: string[], cwd: string, deps: RunDeps & { json: boolean
372
385
  else results.push(...(await doctorRemote({ cwd, cf, lock, logpushJobs: () => cf.logpushJobs() })));
373
386
  }
374
387
  }
375
- console.log(flags.includes("--json") ? JSON.stringify({ checks: results }, null, 2) : formatReport(results));
376
388
  const handoff = results.find((r) => r.handoff)?.handoff;
389
+ // The same envelope as every --json command (p0.10.1 §1, 7): ok, the checks, and a handoff when there is one.
390
+ console.log(
391
+ flags.includes("--json")
392
+ ? JSON.stringify({
393
+ ok: !results.some((r) => r.status === "fail"),
394
+ checks: results,
395
+ ...(handoff ? { handoff } : {}),
396
+ })
397
+ : formatReport(results),
398
+ );
377
399
  if (handoff) {
378
400
  console.error(`for you: ${handoff}`);
379
401
  return 3;
@@ -463,7 +485,8 @@ export async function main(argv: string[], cwd: string, deps: RunDeps = {}): Pro
463
485
  const f = failureOf(e);
464
486
  if (!f) throw e;
465
487
  console.error(f.line);
466
- if (json) (deps.emit ?? emit)(f.result);
488
+ // doctor keeps its own --json report, and a failure past it is still one object (p0.10.1, codex code r20).
489
+ if (json || (command === "doctor" && flags.includes("--json"))) (deps.emit ?? emit)(f.result);
467
490
  return f.code;
468
491
  }
469
492
  });
package/src/ops.ts CHANGED
@@ -53,6 +53,15 @@ export const OPS_SCHEMA = [
53
53
  classes TEXT NOT NULL DEFAULT '[]', destroyed_at INTEGER NOT NULL)`,
54
54
  ];
55
55
 
56
+ /** Every table and index OPS_SCHEMA makes, by name. */
57
+ const OPS_OBJECTS = OPS_SCHEMA.map((s) => (/IF NOT EXISTS (\w+)/.exec(s) as RegExpExecArray)[1] as string);
58
+
59
+ /**
60
+ * Rows per INSERT into `applied`: three bound parameters each, under D1's 100
61
+ * per query (https://developers.cloudflare.com/d1/platform/limits/).
62
+ */
63
+ const APPLIED_ROWS = 33;
64
+
56
65
  export class OperationHeld extends Error {
57
66
  constructor(readonly held: OperationRow) {
58
67
  super(
@@ -128,7 +137,13 @@ export class OpsStore {
128
137
  ) {}
129
138
 
130
139
  async init(store: string): Promise<void> {
131
- for (const s of OPS_SCHEMA) await this.q(s);
140
+ // Every object already there: the statements would change nothing, and a platform's rate limit counts
141
+ // each one (salve: 120 a minute per app, p0.11 row 16). The schema only ever gains objects, never columns.
142
+ const present = await this.q(
143
+ `SELECT name FROM sqlite_master WHERE name IN (${OPS_OBJECTS.map(() => "?").join(", ")})`,
144
+ OPS_OBJECTS,
145
+ );
146
+ if (present.length !== OPS_OBJECTS.length) for (const s of OPS_SCHEMA) await this.q(s);
132
147
  await this.q("INSERT INTO kedge_ops_meta (id, store, created_at) VALUES (1, ?, ?) ON CONFLICT (id) DO NOTHING", [
133
148
  store,
134
149
  this.now(),
@@ -344,12 +359,16 @@ export class OpsStore {
344
359
  return new Map((await this.q("SELECT file, sha256 FROM applied")).map((r) => [String(r.file), String(r.sha256)]));
345
360
  }
346
361
 
347
- async recordApplied(file: string, sha256: string): Promise<void> {
348
- await this.q("INSERT INTO applied (file, sha256, applied_at) VALUES (?, ?, ?) ON CONFLICT (file) DO NOTHING", [
349
- file,
350
- sha256,
351
- this.now(),
352
- ]);
362
+ /** Each applied file's hash, the first recorded kept: one statement per 33 files, not one per file (p0.11 row 16). */
363
+ async recordApplied(files: readonly (readonly [file: string, sha256: string])[]): Promise<void> {
364
+ const t = this.now();
365
+ for (let i = 0; i < files.length; i += APPLIED_ROWS) {
366
+ const chunk = files.slice(i, i + APPLIED_ROWS);
367
+ await this.q(
368
+ `INSERT INTO applied (file, sha256, applied_at) VALUES ${chunk.map(() => "(?, ?, ?)").join(", ")} ON CONFLICT (file) DO NOTHING`,
369
+ chunk.flatMap(([f, h]) => [f, h, t]),
370
+ );
371
+ }
353
372
  }
354
373
 
355
374
  // ---- the store's own resources (§6.1) ------------------------------------------------------------------
package/src/output.ts CHANGED
@@ -1,9 +1,11 @@
1
1
  // `--json` (p0.10 §5): stdout carries one object when the command ends, and
2
- // nothing else, so an agent parses it whole. Progress still reaches a person,
3
- // on stderr: console.log is pointed there for the command's run, and every
4
- // child the CLI starts (Wrangler, the store's checks and scenarios) writes its
5
- // stdout there too, since a child's line on stdout would break the object.
6
- import type { StdioOptions } from "node:child_process";
2
+ // nothing else, so an agent parses it whole. stderr carries only what the
3
+ // agent must read: a refusal, a handoff's `for you:` line, or a child's failure.
4
+ // Progress is dropped, and every child the CLI starts (Wrangler, the store's
5
+ // checks and scenarios) is captured and shown, its last lines on stderr, only
6
+ // when it fails: an agent's tool shows both streams as one, so anything chatty
7
+ // there reads as part of the answer (p0.10.1 §1, 7).
8
+ import type { SpawnSyncReturns, StdioOptions } from "node:child_process";
7
9
 
8
10
  let reserved = false;
9
11
 
@@ -13,8 +15,8 @@ export async function withJsonOutput<T>(on: boolean, work: () => Promise<T>): Pr
13
15
  const log = console.log;
14
16
  const info = console.info;
15
17
  reserved = true;
16
- console.log = console.error;
17
- console.info = console.error;
18
+ console.log = () => {};
19
+ console.info = () => {};
18
20
  try {
19
21
  return await work();
20
22
  } finally {
@@ -29,7 +31,14 @@ export function emit(result: Record<string, unknown>): void {
29
31
  process.stdout.write(`${JSON.stringify(result)}\n`);
30
32
  }
31
33
 
32
- /** A child's stdio: inherited, except that its stdout goes to stderr while stdout is reserved. */
34
+ /** A child's stdio: inherited, or under `--json` captured, for `childFailed` to show if it fails. */
33
35
  export function childStdio(): StdioOptions {
34
- return reserved ? ["inherit", 2, "inherit"] : "inherit";
36
+ return reserved ? ["ignore", "pipe", "pipe"] : "inherit";
37
+ }
38
+
39
+ /** Under `--json`, a captured child that failed shows its last lines on stderr; inherited output was already seen. */
40
+ export function childFailed(r: Pick<SpawnSyncReturns<string | Buffer>, "status" | "stdout" | "stderr">): void {
41
+ if (!reserved || r.status === 0) return;
42
+ const out = `${r.stdout?.toString() ?? ""}${r.stderr?.toString() ?? ""}`.trim();
43
+ if (out) process.stderr.write(`${out.split("\n").slice(-40).join("\n")}\n`);
35
44
  }
package/src/preview.ts CHANGED
@@ -33,7 +33,7 @@ import type { Lock } from "./compose";
33
33
  import { CORE_SECRETS, DeployRefused, openOps, stageAssets } from "./deploy";
34
34
  import { assetsShape, declaredClasses, readWrangler } from "./doctor";
35
35
  import { LostOwnership, OperationHeld, type OpsStore } from "./ops";
36
- import { childStdio } from "./output";
36
+ import { childFailed, childStdio } from "./output";
37
37
  import { hashTree, type StoreNames, sha256, type WranglerJson } from "./store";
38
38
  import { readTar, type TarEntry, TarRefused, writeTar } from "./tar";
39
39
 
@@ -200,6 +200,7 @@ export function previewConfig(source: WranglerJson, p: PreviewWorker): Record<st
200
200
  workers_dev: true,
201
201
  observability: {
202
202
  ...observability,
203
+ redact_query_string: true,
203
204
  logs: { ...((observability.logs as object | undefined) ?? {}), invocation_logs: false },
204
205
  },
205
206
  vars: { ...vars, ADMIN_ORIGINS: [...origins].join(","), KEDGE_ENVIRONMENT: "preview" },
@@ -720,6 +721,10 @@ class PreviewRun {
720
721
  // recreates under a new nonce, where a late landing can't replace it, and the sweep removes
721
722
  // what lands on the old name (codex code r4).
722
723
  if (e instanceof UnknownOutcome) await this.write("state = 'destroying', lost_create = 1", []);
724
+ // A host that serves one Worker per store refuses this upload (salve hosting), after the preview's
725
+ // database and queue exist: the way out is named, never inferred from the platform's words (p0.11 row 8).
726
+ if (e instanceof Error)
727
+ e.message += `\nThe preview's database and queue exist: remove what it made with kedge preview destroy --name ${this.name}. A host that serves one Worker per store (salve hosting) has no previews: test on kedge dev instead.`;
723
728
  throw e;
724
729
  } finally {
725
730
  if (secretsFile) rmSync(secretsFile, { force: true });
@@ -1018,7 +1023,9 @@ export function runScenarios(cwd: string, credentials: StoreCredentials, env: No
1018
1023
  cwd,
1019
1024
  stdio: childStdio(),
1020
1025
  env: { ...env, KEDGE_PREVIEW_CREDENTIALS: JSON.stringify(credentials) },
1026
+ maxBuffer: 1 << 26,
1021
1027
  });
1028
+ childFailed(r);
1022
1029
  return r.status ?? 1;
1023
1030
  } finally {
1024
1031
  rmSync(tmp, { recursive: true, force: true });
package/src/smoke.ts CHANGED
@@ -47,11 +47,25 @@ export interface SmokeResult {
47
47
  status: string;
48
48
  }
49
49
 
50
- const table = (db: DatabaseSync) =>
51
- db.exec(`CREATE TABLE IF NOT EXISTS smoke_run (
52
- name TEXT PRIMARY KEY, store TEXT NOT NULL, storefront_id TEXT NOT NULL,
50
+ const COLUMNS = `store TEXT NOT NULL, name TEXT NOT NULL, storefront_id TEXT NOT NULL,
53
51
  cart_id TEXT, cart_token TEXT, completion TEXT, order_id TEXT, order_token TEXT, number TEXT, abandoned TEXT,
54
- created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL)`);
52
+ created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL`;
53
+
54
+ /**
55
+ * Runs keyed by store and name (p0.10.1 §5): `first` on kedge dev and `first` on
56
+ * Cloudflare are two runs. A 0.10.0 checkout's `smoke_run`, keyed by name alone,
57
+ * is copied in whole the first time, every recorded moment with it, so a run it
58
+ * stopped resumes; the old table is left as it was.
59
+ */
60
+ function table(db: DatabaseSync) {
61
+ db.exec(`CREATE TABLE IF NOT EXISTS smoke_runs (${COLUMNS}, PRIMARY KEY (store, name))`);
62
+ const legacy = db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'smoke_run'").get();
63
+ if (legacy)
64
+ db.exec(`INSERT OR IGNORE INTO smoke_runs (store, name, storefront_id, cart_id, cart_token, completion, order_id,
65
+ order_token, number, abandoned, created_at, updated_at)
66
+ SELECT store, name, storefront_id, cart_id, cart_token, completion, order_id, order_token, number, abandoned,
67
+ created_at, updated_at FROM smoke_run`);
68
+ }
55
69
 
56
70
  /** The smoke run `name` on `target`: resumed, or answered again once it has its number. */
57
71
  export async function smoke(target: StoreTarget, name: string, d: SmokeDeps): Promise<SmokeResult> {
@@ -69,20 +83,18 @@ export async function smoke(target: StoreTarget, name: string, d: SmokeDeps): Pr
69
83
  async function run(db: DatabaseSync, target: StoreTarget, name: string, d: SmokeDeps): Promise<SmokeResult> {
70
84
  const now = d.now ?? Date.now;
71
85
  const store = identityOf(target);
72
- const get = () => db.prepare("SELECT * FROM smoke_run WHERE name = ?").get(name) as unknown as Run | undefined;
86
+ const get = () =>
87
+ db.prepare("SELECT * FROM smoke_runs WHERE store = ? AND name = ?").get(store, name) as unknown as Run | undefined;
73
88
  const set = (a: Partial<Run>) => {
74
89
  const cols = Object.keys(a);
75
- db.prepare(`UPDATE smoke_run SET ${cols.map((c) => `${c} = ?`).join(", ")}, updated_at = ? WHERE name = ?`).run(
76
- ...cols.map((c) => (a as Record<string, string | null>)[c] ?? null),
77
- now(),
78
- name,
79
- );
90
+ db.prepare(
91
+ `UPDATE smoke_runs SET ${cols.map((c) => `${c} = ?`).join(", ")}, updated_at = ? WHERE store = ? AND name = ?`,
92
+ ).run(...cols.map((c) => (a as Record<string, string | null>)[c] ?? null), now(), store, name);
80
93
  };
81
94
 
82
95
  let r = get();
83
96
  // A finished run answers its order again, sending nothing: a retried command can't place a second one.
84
97
  if (r?.number && r.order_id) {
85
- if (r.store !== store) throw new DeployRefused(`smoke run ${name} belongs to another store (${r.store})`);
86
98
  return { run: name, order_id: r.order_id, number: r.number, status: "open" };
87
99
  }
88
100
  if (r?.abandoned)
@@ -106,19 +118,15 @@ async function run(db: DatabaseSync, target: StoreTarget, name: string, d: Smoke
106
118
 
107
119
  if (!r) {
108
120
  const t = now();
109
- db.prepare("INSERT INTO smoke_run (name, store, storefront_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?)").run(
110
- name,
111
- store,
112
- storefront,
113
- t,
114
- t,
115
- );
121
+ db.prepare(
122
+ "INSERT INTO smoke_runs (store, name, storefront_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?)",
123
+ ).run(store, name, storefront, t, t);
116
124
  r = get() as Run;
117
125
  }
118
- // A cart admits only its own storefront's key: a record for another store or storefront is refused, not reused.
119
- if (r.store !== store || r.storefront_id !== storefront)
126
+ // A cart admits only its own storefront's key: a record for another storefront is refused, not reused.
127
+ if (r.storefront_id !== storefront)
120
128
  throw new DeployRefused(
121
- `smoke run ${name} was made for ${r.store === store ? `storefront ${r.storefront_id}` : r.store}: start another with --run <new name>`,
129
+ `smoke run ${name} was made for storefront ${r.storefront_id}: start another with --run <new name>`,
122
130
  );
123
131
 
124
132
  const shop = { "x-kedge-key": key };
package/src/store-api.ts CHANGED
@@ -18,11 +18,18 @@ export async function storeRequest(
18
18
  const write = (init.method ?? "GET") !== "GET";
19
19
  let r: Response;
20
20
  try {
21
- r = await f(url, init);
21
+ // No redirect is followed (p0.11 §3): a session is bound to the origin it was sent to, and the
22
+ // person names the store's URL, not something in front of it that sends the request elsewhere.
23
+ r = await f(url, { ...init, redirect: "manual" });
22
24
  } catch {
23
25
  if (write) throw new UnknownOutcome(what, "the store");
24
26
  throw new DeployRefused(`${what}: the store didn't answer`);
25
27
  }
28
+ // Kedge's API never redirects, so this answer came from in front of the Worker, which ran nothing.
29
+ if (r.status >= 300 && r.status < 400)
30
+ throw new DeployRefused(
31
+ `${what} answered a redirect${r.headers.get("location") ? ` to ${r.headers.get("location")}` : ""}: the CLI follows none, so give the store's own URL`,
32
+ );
26
33
  let body: Record<string, unknown>;
27
34
  try {
28
35
  body = (await r.json()) as Record<string, unknown>;
package/src/store.ts CHANGED
@@ -13,6 +13,17 @@ import { byCodePoint } from "./order";
13
13
  /** The starter's id until a first deploy writes the real one (§6.1). */
14
14
  export const PLACEHOLDER_ID = "00000000-0000-0000-0000-000000000000";
15
15
 
16
+ /**
17
+ * wrangler.jsonc with `id` written over the placeholder `database_id`, and
18
+ * only there: `preview_database_id` keeps the zeros, because Wrangler keys the
19
+ * local database by it, and rewriting it would open an empty local store after
20
+ * the first deploy (p0.11 row 17). The opening quote keeps the pattern off
21
+ * `"preview_database_id"`, so the keys' order doesn't matter.
22
+ */
23
+ export function withDatabaseId(text: string, id: string): string {
24
+ return text.replace(new RegExp(`("database_id"\\s*:\\s*")${PLACEHOLDER_ID}"`), `$1${id}"`);
25
+ }
26
+
16
27
  export interface StoreNames {
17
28
  worker: string;
18
29
  database: string;
@@ -24,7 +35,13 @@ export interface StoreNames {
24
35
  export type WranglerJson = Record<string, unknown> & {
25
36
  name?: string;
26
37
  main?: string;
27
- d1_databases?: { binding?: string; database_name?: string; database_id?: string; migrations_dir?: string }[];
38
+ d1_databases?: {
39
+ binding?: string;
40
+ database_name?: string;
41
+ database_id?: string;
42
+ preview_database_id?: string;
43
+ migrations_dir?: string;
44
+ }[];
28
45
  queues?: { producers?: { binding?: string; queue?: string }[]; consumers?: { queue?: string }[] };
29
46
  migrations?: Record<string, unknown>[];
30
47
  triggers?: { crons?: string[] };
@@ -75,14 +92,19 @@ export function git(cwd: string): Git {
75
92
  if (r.status !== 0) throw new Error("kedge deploy needs a Git repository with a commit");
76
93
  return r.stdout.trim();
77
94
  },
95
+ // A store may sit below its repository's root (create-kedge reuses a workspace's repository, p0.11
96
+ // row 12): status covers the store's own directory, and its paths are the store's, as `show` reads
97
+ // them (codex code r5).
78
98
  dirty() {
79
- return run(["status", "--porcelain"])
99
+ const prefix = run(["rev-parse", "--show-prefix"]).stdout.trim();
100
+ return run(["status", "--porcelain", "--", "."])
80
101
  .stdout.split("\n")
81
102
  .filter(Boolean)
82
- .map((l) => l.slice(3));
103
+ .map((l) => l.slice(3))
104
+ .map((p) => (p.startsWith(prefix) ? p.slice(prefix.length) : p));
83
105
  },
84
106
  show(path) {
85
- const r = run(["show", `HEAD:${path}`]);
107
+ const r = run(["show", `HEAD:./${path}`]);
86
108
  return r.status === 0 ? r.stdout : null;
87
109
  },
88
110
  isAncestor(older, newer) {
@@ -100,7 +122,7 @@ export function dirtyBeyondId(cwd: string, g: Git, databaseId: string | null): s
100
122
  return g.dirty().filter((p) => {
101
123
  if (p !== "wrangler.jsonc" || !databaseId) return true;
102
124
  const head = g.show("wrangler.jsonc");
103
- return head === null || head.replace(PLACEHOLDER_ID, databaseId) !== readFileSync(join(cwd, p), "utf8");
125
+ return head === null || withDatabaseId(head, databaseId) !== readFileSync(join(cwd, p), "utf8");
104
126
  });
105
127
  }
106
128
 
package/src/target.ts CHANGED
@@ -1,11 +1,13 @@
1
1
  // Which store a URL is (docs/proposals/p0.10-agent-path.md §4, §6.2). The CLI
2
2
  // sends a session, a saved key or a run's record only to a target it can name
3
3
  // by identity: production or a preview this store's `<store>-ops` records, or
4
- // `kedge dev` on this machine. Anything else gets nothing, so an unknown
5
- // endpoint never receives a token. A store is its account, Worker and
4
+ // `kedge dev` on this machine. Any other https URL may be production under
5
+ // another name (salve hosting's, a custom domain), and is proved before it
6
+ // gets anything but the proof's own session, bound to that URL's origin and so
7
+ // useless anywhere else (p0.11 §3). A store is its account, Worker and
6
8
  // database, not its URL: the account's owner can change the workers.dev
7
9
  // subdomain, so the URL is computed from the current one on every run.
8
- import type { Cloudflare } from "./cf";
10
+ import { type Cloudflare, NeedsPerson } from "./cf";
9
11
  import { openOps } from "./deploy";
10
12
  import { storeNames } from "./store";
11
13
 
@@ -17,6 +19,12 @@ export interface StoreTarget {
17
19
  identity: { account: string; worker: string; database: string };
18
20
  /** Where a CLI session's row is written: that target's own database, and no other. */
19
21
  database: { name: string; id: string | null };
22
+ /**
23
+ * A URL the person gave rather than one the CLI derived (salve's, a custom
24
+ * domain): it's proved to be this store before any other request goes there
25
+ * (p0.11 §3).
26
+ */
27
+ proof?: true;
20
28
  }
21
29
 
22
30
  /**
@@ -55,8 +63,7 @@ export async function resolveTarget(
55
63
  if (!(await cf.findD1(names.ops))) return null;
56
64
  const { ops } = await openOps(cf, names);
57
65
  const host = (worker: string, subdomain: string) => `https://${worker}.${subdomain}.workers.dev`;
58
- const subdomain = await cf.workersSubdomain();
59
- if (u.origin === host(names.worker, subdomain)) {
66
+ const production = async (proof: boolean): Promise<StoreTarget | null> => {
60
67
  const db = await ops.resource("d1", names.database);
61
68
  if (!db?.id) return null;
62
69
  return {
@@ -64,14 +71,35 @@ export async function resolveTarget(
64
71
  url: u.origin,
65
72
  identity: { account: cf.account, worker: names.worker, database: db.id },
66
73
  database: { name: names.database, id: db.id },
74
+ ...(proof ? { proof: true as const } : {}),
67
75
  };
76
+ };
77
+ // Derived only where the Worker is served at workers.dev: a platform's stand-in subdomain (salve's) names
78
+ // a host the store isn't on, which then gets the proof like any other URL (p0.11 row 2).
79
+ // A store served only at its own domain, on an account without a subdomain, still resolves (codex code r3).
80
+ const devUrl = await cf.workersDevUrl(names.worker).catch((e: unknown) => {
81
+ if (e instanceof NeedsPerson) return null;
82
+ throw e;
83
+ });
84
+ if (u.origin === devUrl) return production(false);
85
+ // A preview is only ever at workers.dev, so only such a host asks for the account's subdomain. The whole
86
+ // origin, as a preview's is made: a host that only begins with a preview's name isn't it.
87
+ let p: Record<string, unknown> | undefined;
88
+ if (u.hostname.endsWith(".workers.dev")) {
89
+ const subdomain = await cf.workersSubdomain().catch((e: unknown) => {
90
+ if (e instanceof NeedsPerson) return null;
91
+ throw e;
92
+ });
93
+ const rows = subdomain
94
+ ? await ops.query(
95
+ "SELECT worker, database_name, database_id FROM preview WHERE state = 'bootstrapped' AND database_id IS NOT NULL",
96
+ )
97
+ : [];
98
+ p = rows.find((r) => u.origin === host(String(r.worker), subdomain as string));
68
99
  }
69
- // The whole origin, as a preview's is made: a host that only begins with a preview's name isn't it.
70
- const rows = await ops.query(
71
- "SELECT worker, database_name, database_id FROM preview WHERE state = 'bootstrapped' AND database_id IS NOT NULL",
72
- );
73
- const p = rows.find((r) => u.origin === host(String(r.worker), subdomain));
74
- if (!p) return null;
100
+ // Any other https origin may be production under another name (salve hosting's, a custom domain):
101
+ // the proof decides, before a session's token goes there (p0.11 row 1).
102
+ if (!p) return production(true);
75
103
  return {
76
104
  kind: "preview",
77
105
  url: u.origin,