@pithy-sh/core 0.7.4 → 0.9.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.
@@ -9,6 +9,19 @@ import { type AmbientEnv } from "../env/ambient";
9
9
  *
10
10
  * The one test for "is this a host" that an origin read from a stamp and an origin typed on a command line
11
11
  * both pass, so the two cannot accept different things.
12
+ *
13
+ * **It answers the same on every runtime, and that is why the punycode rule is stated here.** It used to be
14
+ * borrowed: `seedHostOrigin` asked `new URL` to parse the host and took a throw as the answer, and an invalid
15
+ * A-label like `xn--a.test` was refused because the parser refused it. Node 24.20.0 bumped Ada from 3.4.4 to
16
+ * 4.0.0, Ada 4 stopped rejecting invalid punycode, and the same `pithy seed --host` began succeeding on Node 24
17
+ * while still failing on Node 22 — one command, two answers, decided by the adopter's Node. A rule borrowed from
18
+ * a parser changes under you, so this owns it.
19
+ *
20
+ * **Every A-label is refused, valid or not.** Telling a good one from a bad one is UTS-46, which needs IDNA
21
+ * mapping tables this package is not carrying into a Worker for the sake of a hostname check — and
22
+ * {@link HOSTNAME_PATTERN} is ASCII-only anyway, so a Unicode domain never reached here to begin with. This
23
+ * closes the hand-encoded spelling, and internationalized domains are a feature with a specification behind
24
+ * them rather than a side effect of a parser version.
12
25
  */
13
26
  export declare function isPublicHostname(hostname: string): boolean;
14
27
  /** The environments a domain may be declared for — every managed one, never `dev`. */
@@ -1 +1 @@
1
- {"version":3,"file":"domains.d.ts","sourceRoot":"","sources":["../../src/naming/domains.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,KAAK,UAAU,EAAc,MAAM,gBAAgB,CAAC;AAuC7D;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAE1D;AAED,sFAAsF;AACtF,eAAO,MAAM,mBAAmB,wBAA8D,CAAC;AAE/F,uDAAuD;AACvD,eAAO,MAAM,YAAY;;;iBA+BrB,CAAC;AACL,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC;AAExD;;;;;GAKG;AACH,eAAO,MAAM,aAAa;;;;;;;;;iBAOvB,CAAC;AACJ,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC;AAE1D;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,aAAa,GAAG,SAAS,EAAE,WAAW,EAAE,MAAM,GAAG,SAAS,GAAG,YAAY,GAAG,IAAI,CAKlH;AAED,gHAAgH;AAChH,wBAAgB,UAAU,CAAC,MAAM,EAAE,YAAY,GAAG,MAAM,CAEvD;AAED;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,YAAY,qBAAqB,CAAC;AAE/C,yFAAyF;AACzF,MAAM,WAAW,cAAc;IAC7B,qEAAqE;IACrE,MAAM,EAAE,MAAM,CAAC;IACf,iFAAiF;IACjF,QAAQ,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAetG;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,aAAa,CAC3B,WAAW,EAAE,MAAM,GAAG,SAAS,EAC/B,OAAO,EAAE,aAAa,GAAG,SAAS,EAClC,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,GAC3B,cAAc,CAShB;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,SAAS,CACvB,WAAW,EAAE,MAAM,GAAG,SAAS,EAC/B,OAAO,EAAE,aAAa,GAAG,SAAS,EAClC,GAAG,GAAE,UAAyB,GAC7B,MAAM,CAER"}
1
+ {"version":3,"file":"domains.d.ts","sourceRoot":"","sources":["../../src/naming/domains.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,KAAK,UAAU,EAAc,MAAM,gBAAgB,CAAC;AAgD7D;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAE1D;AAED,sFAAsF;AACtF,eAAO,MAAM,mBAAmB,wBAA8D,CAAC;AAE/F,uDAAuD;AACvD,eAAO,MAAM,YAAY;;;iBA+BrB,CAAC;AACL,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC;AAExD;;;;;GAKG;AACH,eAAO,MAAM,aAAa;;;;;;;;;iBAOvB,CAAC;AACJ,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC;AAE1D;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,aAAa,GAAG,SAAS,EAAE,WAAW,EAAE,MAAM,GAAG,SAAS,GAAG,YAAY,GAAG,IAAI,CAKlH;AAED,gHAAgH;AAChH,wBAAgB,UAAU,CAAC,MAAM,EAAE,YAAY,GAAG,MAAM,CAEvD;AAED;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,YAAY,qBAAqB,CAAC;AAE/C,yFAAyF;AACzF,MAAM,WAAW,cAAc;IAC7B,qEAAqE;IACrE,MAAM,EAAE,MAAM,CAAC;IACf,iFAAiF;IACjF,QAAQ,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAetG;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,aAAa,CAC3B,WAAW,EAAE,MAAM,GAAG,SAAS,EAC/B,OAAO,EAAE,aAAa,GAAG,SAAS,EAClC,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,GAC3B,cAAc,CAShB;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,SAAS,CACvB,WAAW,EAAE,MAAM,GAAG,SAAS,EAC/B,OAAO,EAAE,aAAa,GAAG,SAAS,EAClC,GAAG,GAAE,UAAyB,GAC7B,MAAM,CAER"}
@@ -39,15 +39,36 @@ import { z } from "zod";
39
39
  /** A hostname a Worker answers on. No scheme, no path, no port — wrangler's `routes` pattern is a host. */
40
40
  const HOSTNAME_PATTERN = /^(?!-)[a-z0-9-]{1,63}(?<!-)(\.(?!-)[a-z0-9-]{1,63}(?<!-))+$/;
41
41
  /**
42
+ * **A label that is a punycode A-label — the ASCII spelling of an internationalized name.**
43
+ *
44
+ * Anchored to a label boundary, so it is `xn--a.test` and `example.xn--p1ai` and never `myxn--a.test`:
45
+ * a name that merely reads that way is an ordinary name, and refusing it would be this rule reaching
46
+ * past what it is about.
47
+ */
48
+ const PUNYCODE_LABEL = /(^|\.)xn--/;
49
+ /**
42
50
  * Whether a string is a public hostname: dot-separated DNS labels, at least two, lowercase. No scheme, no
43
51
  * port, no trailing dot, and nothing a URL parser decoded into one — `%2e%2e` is `..` once parsed, which is an
44
52
  * empty label rather than a host (#643). `localhost` is not one: it has a single label, and it is this machine.
45
53
  *
46
54
  * The one test for "is this a host" that an origin read from a stamp and an origin typed on a command line
47
55
  * both pass, so the two cannot accept different things.
56
+ *
57
+ * **It answers the same on every runtime, and that is why the punycode rule is stated here.** It used to be
58
+ * borrowed: `seedHostOrigin` asked `new URL` to parse the host and took a throw as the answer, and an invalid
59
+ * A-label like `xn--a.test` was refused because the parser refused it. Node 24.20.0 bumped Ada from 3.4.4 to
60
+ * 4.0.0, Ada 4 stopped rejecting invalid punycode, and the same `pithy seed --host` began succeeding on Node 24
61
+ * while still failing on Node 22 — one command, two answers, decided by the adopter's Node. A rule borrowed from
62
+ * a parser changes under you, so this owns it.
63
+ *
64
+ * **Every A-label is refused, valid or not.** Telling a good one from a bad one is UTS-46, which needs IDNA
65
+ * mapping tables this package is not carrying into a Worker for the sake of a hostname check — and
66
+ * {@link HOSTNAME_PATTERN} is ASCII-only anyway, so a Unicode domain never reached here to begin with. This
67
+ * closes the hand-encoded spelling, and internationalized domains are a feature with a specification behind
68
+ * them rather than a side effect of a parser version.
48
69
  */
49
70
  function isPublicHostname(hostname) {
50
- return hostname.length <= 253 && HOSTNAME_PATTERN.test(hostname);
71
+ return hostname.length <= 253 && HOSTNAME_PATTERN.test(hostname) && !PUNYCODE_LABEL.test(hostname);
51
72
  }
52
73
  /** The environments a domain may be declared for — every managed one, never `dev`. */
53
74
  const DOMAIN_ENVIRONMENTS = ENVIRONMENTS.filter((environment) => environment !== "dev");
@@ -56,4 +56,37 @@ export declare const DevLogin: z.ZodObject<{
56
56
  expiresAt: z.ZodCodec<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodDate]>, z.ZodDate>;
57
57
  }, z.core.$strip>;
58
58
  export type DevLogin = z.output<typeof DevLogin>;
59
+ /**
60
+ * Every seeded dev login, keyed by `userId` — what `logs/dev-login.json` holds.
61
+ *
62
+ * **One entry per user the seed creates — `#667`.** It used to hold exactly one, named by a per-machine
63
+ * `dev.json`, so being a second seeded user meant reseeding or minting a claim by hand. Both are slow
64
+ * enough that people stop doing it, and the screens only one user can reach stop getting looked at. A
65
+ * claim is a signature over a user id and an expiry, so N of them cost no extra seeded rows and a few
66
+ * hundred bytes each.
67
+ *
68
+ * The key is the `userId`, which each entry also carries: the key is how the record is addressed and the
69
+ * field is what an entry means on its own, once something has picked one out of here.
70
+ *
71
+ * **Order is the file's, and for integer-like ids the language's.** `pithy dev` offers the identities in the
72
+ * order they are read, which is insertion order — except that JS enumerates canonical array-index keys
73
+ * first, in ascending numeric order, so a project whose user ids are stringified integers gets them
74
+ * numerically however the seed wrote them. Nothing depends on the order being the seed's; it is an offer
75
+ * order, not an identity.
76
+ *
77
+ * **The whole file is still one live credential, and the three properties that made one acceptable are
78
+ * what make N acceptable** — `logs/` is gitignored by the starter template, the artifact is written
79
+ * `0600`, and the writer refuses a symlink at the target.
80
+ *
81
+ * **Nothing migrates.** The single-entry file this replaced has strings where an entry belongs, so it does
82
+ * not parse, and every reader already answers "no dev login" to a file that does not — the banner stays
83
+ * quiet until the next `pithy seed`. The artifact is gitignored and regenerated, so that is the plan.
84
+ */
85
+ export declare const DevLogins: z.ZodRecord<z.ZodString, z.ZodObject<{
86
+ email: z.ZodString;
87
+ userId: z.ZodString;
88
+ claim: z.ZodString;
89
+ expiresAt: z.ZodCodec<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodDate]>, z.ZodDate>;
90
+ }, z.core.$strip>>;
91
+ export type DevLogins = z.output<typeof DevLogins>;
59
92
  //# sourceMappingURL=devLogin.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"devLogin.d.ts","sourceRoot":"","sources":["../../src/seed/devLogin.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB;;;;;;;;;;;;;;;;;;GAkBG;AAEH,2GAA2G;AAC3G,eAAO,MAAM,iBAAiB,SAAS,CAAC;AAExC,gHAAgH;AAChH,eAAO,MAAM,cAAc,mBAAmB,CAAC;AAE/C,uGAAuG;AACvG,eAAO,MAAM,cAAc,wBAA2C,CAAC;AAEvE;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,eAAe,uBAAuB,CAAC;AAEpD,uGAAuG;AACvG,eAAO,MAAM,qBAAqB,MAAM,CAAC;AAEzC;;;GAGG;AACH,eAAO,MAAM,QAAQ;;;;;iBAWqF,CAAC;AAC3G,MAAM,MAAM,QAAQ,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,QAAQ,CAAC,CAAC"}
1
+ {"version":3,"file":"devLogin.d.ts","sourceRoot":"","sources":["../../src/seed/devLogin.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB;;;;;;;;;;;;;;;;;;GAkBG;AAEH,2GAA2G;AAC3G,eAAO,MAAM,iBAAiB,SAAS,CAAC;AAExC,gHAAgH;AAChH,eAAO,MAAM,cAAc,mBAAmB,CAAC;AAE/C,uGAAuG;AACvG,eAAO,MAAM,cAAc,wBAA2C,CAAC;AAEvE;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,eAAe,uBAAuB,CAAC;AAEpD,uGAAuG;AACvG,eAAO,MAAM,qBAAqB,MAAM,CAAC;AAEzC;;;GAGG;AACH,eAAO,MAAM,QAAQ;;;;;iBAWqF,CAAC;AAC3G,MAAM,MAAM,QAAQ,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,QAAQ,CAAC,CAAC;AAEjD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,eAAO,MAAM,SAAS;;;;;kBAEuE,CAAC;AAC9F,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,SAAS,CAAC,CAAC"}
@@ -57,5 +57,32 @@ const DevLogin = z.object({
57
57
  claim: z.string().describe("The signed, URI-encoded claim naming the user to sign in as. A live credential for the local database — never logged, never committed, never in an error payload. **Not a session:** it names a user and the route mints a session against it, so revoking a session leaves it working."),
58
58
  expiresAt: JsonDate.describe("When the claim stops being accepted. ISO-8601 text on disk; a `Date` in app code.")
59
59
  }).describe("A seeded dev login: the signed claim for one seeded user, written to `logs/dev-login.json`.");
60
+ /**
61
+ * Every seeded dev login, keyed by `userId` — what `logs/dev-login.json` holds.
62
+ *
63
+ * **One entry per user the seed creates — `#667`.** It used to hold exactly one, named by a per-machine
64
+ * `dev.json`, so being a second seeded user meant reseeding or minting a claim by hand. Both are slow
65
+ * enough that people stop doing it, and the screens only one user can reach stop getting looked at. A
66
+ * claim is a signature over a user id and an expiry, so N of them cost no extra seeded rows and a few
67
+ * hundred bytes each.
68
+ *
69
+ * The key is the `userId`, which each entry also carries: the key is how the record is addressed and the
70
+ * field is what an entry means on its own, once something has picked one out of here.
71
+ *
72
+ * **Order is the file's, and for integer-like ids the language's.** `pithy dev` offers the identities in the
73
+ * order they are read, which is insertion order — except that JS enumerates canonical array-index keys
74
+ * first, in ascending numeric order, so a project whose user ids are stringified integers gets them
75
+ * numerically however the seed wrote them. Nothing depends on the order being the seed's; it is an offer
76
+ * order, not an identity.
77
+ *
78
+ * **The whole file is still one live credential, and the three properties that made one acceptable are
79
+ * what make N acceptable** — `logs/` is gitignored by the starter template, the artifact is written
80
+ * `0600`, and the writer refuses a symlink at the target.
81
+ *
82
+ * **Nothing migrates.** The single-entry file this replaced has strings where an entry belongs, so it does
83
+ * not parse, and every reader already answers "no dev login" to a file that does not — the banner stays
84
+ * quiet until the next `pithy seed`. The artifact is gitignored and regenerated, so that is the plan.
85
+ */
86
+ const DevLogins = z.record(z.string(), DevLogin).describe("Every seeded dev login, keyed by user id, as `logs/dev-login.json` holds them.");
60
87
  //#endregion
61
- export { DEV_LOGIN_CLAIM_PARAM, DEV_LOGIN_FILE, DEV_LOGIN_PATH, DEV_LOGIN_ROUTE, DevLogin, SEED_ARTIFACT_DIR };
88
+ export { DEV_LOGIN_CLAIM_PARAM, DEV_LOGIN_FILE, DEV_LOGIN_PATH, DEV_LOGIN_ROUTE, DevLogin, DevLogins, SEED_ARTIFACT_DIR };
@@ -178,6 +178,31 @@ export interface SeedPrepareContext {
178
178
  * can write and never read.
179
179
  */
180
180
  secret: (name: string) => Promise<string | undefined>;
181
+ /**
182
+ * The value **this run just minted** for a secret, or `undefined` — never what the environment holds.
183
+ *
184
+ * `pithy provision` creates a random value for every `cf-secrets-store` secret the registry declares
185
+ * mintable, writes it into the account's Secrets Store, and lets it go. The Secrets Store is write-only
186
+ * from the CLI, so that write is the only moment the value exists anywhere the toolchain can see it —
187
+ * and provisioning migrates and seeds in the same process, a few steps later. A fixture that must
188
+ * *seal* something at creation time (encrypt a private key under the environment's key-encryption
189
+ * secret, derive an id that the running app will re-derive) can only do it then. Afterwards the value
190
+ * is unrecoverable by design.
191
+ *
192
+ * **This is not {@link secret}, and the two are not interchangeable.** `secret` answers what the
193
+ * environment holds and refuses outside `dev`, absolutely (#159); that rule is untouched. This answers
194
+ * the strictly narrower question *did this run create one, a moment ago, in memory*. A set asking here
195
+ * for a secret this run did not mint gets `undefined` wherever it runs, in `dev` or anywhere else.
196
+ *
197
+ * **Synchronous, because it cannot fetch.** There is no file to open and no request to make; a promise
198
+ * in this signature would suggest otherwise.
199
+ *
200
+ * **Empty is ordinary.** A re-run mints nothing — absence is checked before anything is generated — and
201
+ * a standalone `pithy seed` mints nothing at all. Both answer `undefined` for every name, and neither
202
+ * is an error. A set that needs a minted value and is handed none says so itself; nothing is invented
203
+ * on its behalf.
204
+ */
205
+ mintedThisRun: (name: string) => string | undefined;
181
206
  /**
182
207
  * The developer's machine-local preferences for this project, read from the Pithy config directory
183
208
  * (`~/.config/pithy/<project>/dev.json`, or `%APPDATA%\pithy\<project>\dev.json` on Windows), parsed but
@@ -1 +1 @@
1
- {"version":3,"file":"seed.d.ts","sourceRoot":"","sources":["../../src/seed/seed.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"seed.d.ts","sourceRoot":"","sources":["../../src/seed/seed.ts"],"names":[],"mappings":"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"}
@@ -3,5 +3,5 @@
3
3
  /** This package's npm name — the join key against a release feed. */
4
4
  export declare const PACKAGE_NAME = "@pithy-sh/core";
5
5
  /** This package's version, stamped from its own package.json at generation time. */
6
- export declare const PACKAGE_VERSION = "0.7.4";
6
+ export declare const PACKAGE_VERSION = "0.9.0";
7
7
  //# sourceMappingURL=version.generated.d.ts.map
@@ -4,6 +4,6 @@
4
4
  /** This package's npm name — the join key against a release feed. */
5
5
  const PACKAGE_NAME = "@pithy-sh/core";
6
6
  /** This package's version, stamped from its own package.json at generation time. */
7
- const PACKAGE_VERSION = "0.7.4";
7
+ const PACKAGE_VERSION = "0.9.0";
8
8
  //#endregion
9
9
  export { PACKAGE_NAME, PACKAGE_VERSION };
@@ -65,10 +65,42 @@ export interface WorkflowFault {
65
65
  * test rather than a claim — and so the same function decides in a test, in Miniflare, and in production.
66
66
  */
67
67
  export declare function classifyWorkflowFault(error: unknown, policy: WorkflowRetryPolicy): WorkflowFault;
68
- /** The half of a Workflow step this kit uses: run a named body, or serve its journalled result. */
68
+ /**
69
+ * The half of a Workflow step this kit uses: run a named body, or serve its journalled result.
70
+ *
71
+ * **`sleep` is optional here and required on what {@link classifiedSteps} hands back, and the asymmetry
72
+ * is the truth rather than a convenience.** The platform's own `WorkflowStep` always offers one. What
73
+ * does not are this kit's hand-written test doubles, which exist to exercise the classification and
74
+ * nothing else — and demanding a primitive the classifier never classifies would cost every one of them
75
+ * a line of noise. Handing an optional one back instead would push a null check into the one caller that
76
+ * genuinely pauses.
77
+ */
69
78
  export interface WorkflowStepLike {
70
79
  /** Run a named step, or return its journalled result if this instance already completed it. */
71
80
  do<T>(name: string, fn: () => Promise<T>): Promise<T>;
81
+ /**
82
+ * Pause the instance for `durationMs` and resume it afterwards — possibly in a fresh isolate, which
83
+ * is the whole reason a caller reaches for it rather than for a timer. Absent only on a test double.
84
+ */
85
+ sleep?(name: string, durationMs: number): Promise<void>;
86
+ }
87
+ /**
88
+ * A step runner that can pause — what {@link classifiedSteps} hands back, always.
89
+ *
90
+ * The durable pause is why this type exists rather than being a convenience over the one above. A `sleep`
91
+ * is journalled, so a wait survives the instance being evicted during it; a `setTimeout` inside one step
92
+ * burns that step's wall clock and dies with the instance holding it.
93
+ *
94
+ * **It was introduced on a stronger claim, and the claim did not survive contact (pithy-sh/pithy#647).** The
95
+ * argument was that a Secrets Store binding answers the same for its isolate's whole life, so only a resume
96
+ * into a fresh isolate could ever see a write land. Measured against a real store on 2026-09-25, that is
97
+ * false: a binding refreshes inside a live isolate roughly 1.3-1.5s after the REST write, proved by holding
98
+ * two isolates warm through a write and watching each serve the old value before it and the new one after,
99
+ * over 328 samples. The durability argument above is the true one and is sufficient on its own.
100
+ */
101
+ export interface DurableStepLike extends WorkflowStepLike {
102
+ /** Pause the instance for `durationMs`, then resume. */
103
+ sleep(name: string, durationMs: number): Promise<void>;
72
104
  }
73
105
  /**
74
106
  * The platform's terminal-error class — `NonRetryableError` from `cloudflare:workflows`, taken as a
@@ -95,5 +127,5 @@ export interface TerminalErrorConstructor {
95
127
  * `runSendBatch`, `runAtRestKeyRotation`) takes this instead with no change: the classification is
96
128
  * declared once, at the entrypoint, and every step below it inherits the decision.
97
129
  */
98
- export declare function classifiedSteps(step: WorkflowStepLike, policy: WorkflowRetryPolicy, terminalError: TerminalErrorConstructor): WorkflowStepLike;
130
+ export declare function classifiedSteps(step: WorkflowStepLike, policy: WorkflowRetryPolicy, terminalError: TerminalErrorConstructor): DurableStepLike;
99
131
  //# sourceMappingURL=faults.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"faults.d.ts","sourceRoot":"","sources":["../../src/workflow/faults.ts"],"names":[],"mappings":"AAOA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,2FAA2F;AAC3F,MAAM,MAAM,wBAAwB,GAAG,OAAO,GAAG,UAAU,CAAC;AAE5D;;;;;;GAMG;AACH,MAAM,WAAW,mBAAmB;IAClC,iHAAiH;IACjH,UAAU,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,SAAS,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;CAC7C;AAED,2FAA2F;AAC3F,MAAM,WAAW,aAAa;IAC5B,sBAAsB;IACtB,WAAW,EAAE,wBAAwB,CAAC;IACtC,mFAAmF;IACnF,IAAI,EAAE,MAAM,CAAC;IACb,+EAA+E;IAC/E,MAAM,EAAE,MAAM,CAAC;CAChB;AAsDD;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,mBAAmB,GAAG,aAAa,CA0BhG;AAED,mGAAmG;AACnG,MAAM,WAAW,gBAAgB;IAC/B,+FAA+F;IAC/F,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;CACvD;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,wBAAwB;IACvC,KAAK,OAAO,EAAE,MAAM,GAAG,KAAK,CAAC;CAC9B;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAC7B,IAAI,EAAE,gBAAgB,EACtB,MAAM,EAAE,mBAAmB,EAC3B,aAAa,EAAE,wBAAwB,GACtC,gBAAgB,CA2BlB"}
1
+ {"version":3,"file":"faults.d.ts","sourceRoot":"","sources":["../../src/workflow/faults.ts"],"names":[],"mappings":"AAOA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,2FAA2F;AAC3F,MAAM,MAAM,wBAAwB,GAAG,OAAO,GAAG,UAAU,CAAC;AAE5D;;;;;;GAMG;AACH,MAAM,WAAW,mBAAmB;IAClC,iHAAiH;IACjH,UAAU,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,SAAS,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;CAC7C;AAED,2FAA2F;AAC3F,MAAM,WAAW,aAAa;IAC5B,sBAAsB;IACtB,WAAW,EAAE,wBAAwB,CAAC;IACtC,mFAAmF;IACnF,IAAI,EAAE,MAAM,CAAC;IACb,+EAA+E;IAC/E,MAAM,EAAE,MAAM,CAAC;CAChB;AAsDD;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,mBAAmB,GAAG,aAAa,CA0BhG;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,gBAAgB;IAC/B,+FAA+F;IAC/F,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IACtD;;;OAGG;IACH,KAAK,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACzD;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,eAAgB,SAAQ,gBAAgB;IACvD,wDAAwD;IACxD,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACxD;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,wBAAwB;IACvC,KAAK,OAAO,EAAE,MAAM,GAAG,KAAK,CAAC;CAC9B;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAC7B,IAAI,EAAE,gBAAgB,EACtB,MAAM,EAAE,mBAAmB,EAC3B,aAAa,EAAE,wBAAwB,GACtC,eAAe,CAmDjB"}
@@ -1,6 +1,6 @@
1
1
  // SPDX-FileCopyrightText: 2026 Pithy
2
2
  // SPDX-License-Identifier: MIT
3
- import { PithyError } from "../error/pithyError.js";
3
+ import { InternalError, PithyError } from "../error/pithyError.js";
4
4
  import { d1TransientFault } from "../data/withD1Retry.js";
5
5
  import { encodeWorkflowStepMessage } from "./stepMessage.js";
6
6
  //#region src/workflow/faults.ts
@@ -91,23 +91,47 @@ function classifyWorkflowFault(error, policy) {
91
91
  * declared once, at the entrypoint, and every step below it inherits the decision.
92
92
  */
93
93
  function classifiedSteps(step, policy, terminalError) {
94
- return { do(name, fn) {
95
- return step.do(name, async () => {
96
- try {
97
- return await fn();
98
- } catch (error) {
99
- const fault = classifyWorkflowFault(error, policy);
100
- if (fault.disposition === "retry") throw error;
101
- const terminal = new terminalError(encodeWorkflowStepMessage({
102
- code: fault.code,
103
- message: messageOf(error),
104
- action: pithyActionOf(error)
105
- }));
106
- terminal.cause = error;
107
- throw terminal;
108
- }
109
- });
110
- } };
94
+ const runner = {
95
+ do(name, fn) {
96
+ return step.do(name, async () => {
97
+ try {
98
+ return await fn();
99
+ } catch (error) {
100
+ const fault = classifyWorkflowFault(error, policy);
101
+ if (fault.disposition === "retry") throw error;
102
+ const terminal = new terminalError(encodeWorkflowStepMessage({
103
+ code: fault.code,
104
+ message: messageOf(error),
105
+ action: pithyActionOf(error)
106
+ }));
107
+ terminal.cause = error;
108
+ throw terminal;
109
+ }
110
+ });
111
+ },
112
+ /**
113
+ * Forwarded, and deliberately unclassified. A pause raises no fault of the capability's — the
114
+ * instance either resumes or it is gone — so there is nothing here for a policy to decide.
115
+ *
116
+ * A runner that cannot pause is refused, and the refusal is raised **through `do`** rather than
117
+ * thrown from here. A throw from here is a throw the engine never classifies, so it is free to
118
+ * re-drive the instance — and re-driving is the unbounded loop the caller is spending its sleeps to
119
+ * avoid. Going through `do` makes the refusal terminal on its first attempt, like every other
120
+ * answer that cannot change.
121
+ */
122
+ sleep(name, durationMs) {
123
+ const pause = step.sleep;
124
+ if (pause === void 0) return runner.do(`${name}/cannot-pause`, async () => {
125
+ throw new InternalError({
126
+ message: "This Workflow step runner cannot pause.",
127
+ action: "Give the step runner a `sleep`; the platform's own `WorkflowStep` always has one.",
128
+ detail: `step '${name}' asked to pause ${durationMs}ms through a runner that declares no sleep`
129
+ });
130
+ });
131
+ return pause.call(step, name, durationMs);
132
+ }
133
+ };
134
+ return runner;
111
135
  }
112
136
  //#endregion
113
137
  export { classifiedSteps, classifyWorkflowFault };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pithy-sh/core",
3
- "version": "0.7.4",
3
+ "version": "0.9.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -41,6 +41,15 @@ import { ENVIRONMENTS, FEATURE_ENVIRONMENT } from "./environment";
41
41
  /** A hostname a Worker answers on. No scheme, no path, no port — wrangler's `routes` pattern is a host. */
42
42
  const HOSTNAME_PATTERN = /^(?!-)[a-z0-9-]{1,63}(?<!-)(\.(?!-)[a-z0-9-]{1,63}(?<!-))+$/;
43
43
 
44
+ /**
45
+ * **A label that is a punycode A-label — the ASCII spelling of an internationalized name.**
46
+ *
47
+ * Anchored to a label boundary, so it is `xn--a.test` and `example.xn--p1ai` and never `myxn--a.test`:
48
+ * a name that merely reads that way is an ordinary name, and refusing it would be this rule reaching
49
+ * past what it is about.
50
+ */
51
+ const PUNYCODE_LABEL = /(^|\.)xn--/;
52
+
44
53
  /**
45
54
  * Whether a string is a public hostname: dot-separated DNS labels, at least two, lowercase. No scheme, no
46
55
  * port, no trailing dot, and nothing a URL parser decoded into one — `%2e%2e` is `..` once parsed, which is an
@@ -48,9 +57,22 @@ const HOSTNAME_PATTERN = /^(?!-)[a-z0-9-]{1,63}(?<!-)(\.(?!-)[a-z0-9-]{1,63}(?<!
48
57
  *
49
58
  * The one test for "is this a host" that an origin read from a stamp and an origin typed on a command line
50
59
  * both pass, so the two cannot accept different things.
60
+ *
61
+ * **It answers the same on every runtime, and that is why the punycode rule is stated here.** It used to be
62
+ * borrowed: `seedHostOrigin` asked `new URL` to parse the host and took a throw as the answer, and an invalid
63
+ * A-label like `xn--a.test` was refused because the parser refused it. Node 24.20.0 bumped Ada from 3.4.4 to
64
+ * 4.0.0, Ada 4 stopped rejecting invalid punycode, and the same `pithy seed --host` began succeeding on Node 24
65
+ * while still failing on Node 22 — one command, two answers, decided by the adopter's Node. A rule borrowed from
66
+ * a parser changes under you, so this owns it.
67
+ *
68
+ * **Every A-label is refused, valid or not.** Telling a good one from a bad one is UTS-46, which needs IDNA
69
+ * mapping tables this package is not carrying into a Worker for the sake of a hostname check — and
70
+ * {@link HOSTNAME_PATTERN} is ASCII-only anyway, so a Unicode domain never reached here to begin with. This
71
+ * closes the hand-encoded spelling, and internationalized domains are a feature with a specification behind
72
+ * them rather than a side effect of a parser version.
51
73
  */
52
74
  export function isPublicHostname(hostname: string): boolean {
53
- return hostname.length <= 253 && HOSTNAME_PATTERN.test(hostname);
75
+ return hostname.length <= 253 && HOSTNAME_PATTERN.test(hostname) && !PUNYCODE_LABEL.test(hostname);
54
76
  }
55
77
 
56
78
  /** The environments a domain may be declared for — every managed one, never `dev`. */
@@ -71,3 +71,34 @@ export const DevLogin = z
71
71
  })
72
72
  .describe("A seeded dev login: the signed claim for one seeded user, written to `logs/dev-login.json`.");
73
73
  export type DevLogin = z.output<typeof DevLogin>;
74
+
75
+ /**
76
+ * Every seeded dev login, keyed by `userId` — what `logs/dev-login.json` holds.
77
+ *
78
+ * **One entry per user the seed creates — `#667`.** It used to hold exactly one, named by a per-machine
79
+ * `dev.json`, so being a second seeded user meant reseeding or minting a claim by hand. Both are slow
80
+ * enough that people stop doing it, and the screens only one user can reach stop getting looked at. A
81
+ * claim is a signature over a user id and an expiry, so N of them cost no extra seeded rows and a few
82
+ * hundred bytes each.
83
+ *
84
+ * The key is the `userId`, which each entry also carries: the key is how the record is addressed and the
85
+ * field is what an entry means on its own, once something has picked one out of here.
86
+ *
87
+ * **Order is the file's, and for integer-like ids the language's.** `pithy dev` offers the identities in the
88
+ * order they are read, which is insertion order — except that JS enumerates canonical array-index keys
89
+ * first, in ascending numeric order, so a project whose user ids are stringified integers gets them
90
+ * numerically however the seed wrote them. Nothing depends on the order being the seed's; it is an offer
91
+ * order, not an identity.
92
+ *
93
+ * **The whole file is still one live credential, and the three properties that made one acceptable are
94
+ * what make N acceptable** — `logs/` is gitignored by the starter template, the artifact is written
95
+ * `0600`, and the writer refuses a symlink at the target.
96
+ *
97
+ * **Nothing migrates.** The single-entry file this replaced has strings where an entry belongs, so it does
98
+ * not parse, and every reader already answers "no dev login" to a file that does not — the banner stays
99
+ * quiet until the next `pithy seed`. The artifact is gitignored and regenerated, so that is the plan.
100
+ */
101
+ export const DevLogins = z
102
+ .record(z.string(), DevLogin)
103
+ .describe("Every seeded dev login, keyed by user id, as `logs/dev-login.json` holds them.");
104
+ export type DevLogins = z.output<typeof DevLogins>;
package/src/seed/seed.ts CHANGED
@@ -196,6 +196,31 @@ export interface SeedPrepareContext {
196
196
  * can write and never read.
197
197
  */
198
198
  secret: (name: string) => Promise<string | undefined>;
199
+ /**
200
+ * The value **this run just minted** for a secret, or `undefined` — never what the environment holds.
201
+ *
202
+ * `pithy provision` creates a random value for every `cf-secrets-store` secret the registry declares
203
+ * mintable, writes it into the account's Secrets Store, and lets it go. The Secrets Store is write-only
204
+ * from the CLI, so that write is the only moment the value exists anywhere the toolchain can see it —
205
+ * and provisioning migrates and seeds in the same process, a few steps later. A fixture that must
206
+ * *seal* something at creation time (encrypt a private key under the environment's key-encryption
207
+ * secret, derive an id that the running app will re-derive) can only do it then. Afterwards the value
208
+ * is unrecoverable by design.
209
+ *
210
+ * **This is not {@link secret}, and the two are not interchangeable.** `secret` answers what the
211
+ * environment holds and refuses outside `dev`, absolutely (#159); that rule is untouched. This answers
212
+ * the strictly narrower question *did this run create one, a moment ago, in memory*. A set asking here
213
+ * for a secret this run did not mint gets `undefined` wherever it runs, in `dev` or anywhere else.
214
+ *
215
+ * **Synchronous, because it cannot fetch.** There is no file to open and no request to make; a promise
216
+ * in this signature would suggest otherwise.
217
+ *
218
+ * **Empty is ordinary.** A re-run mints nothing — absence is checked before anything is generated — and
219
+ * a standalone `pithy seed` mints nothing at all. Both answer `undefined` for every name, and neither
220
+ * is an error. A set that needs a minted value and is handed none says so itself; nothing is invented
221
+ * on its behalf.
222
+ */
223
+ mintedThisRun: (name: string) => string | undefined;
199
224
  /**
200
225
  * The developer's machine-local preferences for this project, read from the Pithy config directory
201
226
  * (`~/.config/pithy/<project>/dev.json`, or `%APPDATA%\pithy\<project>\dev.json` on Windows), parsed but
@@ -13,4 +13,4 @@
13
13
  export const PACKAGE_NAME = "@pithy-sh/core";
14
14
 
15
15
  /** This package's version, stamped from its own package.json at generation time. */
16
- export const PACKAGE_VERSION = "0.7.4";
16
+ export const PACKAGE_VERSION = "0.9.0";
@@ -2,7 +2,7 @@
2
2
  // SPDX-License-Identifier: MIT
3
3
 
4
4
  import { d1TransientFault } from "../data/withD1Retry";
5
- import { PithyError } from "../error/pithyError";
5
+ import { InternalError, PithyError } from "../error/pithyError";
6
6
  import { encodeWorkflowStepMessage } from "./stepMessage";
7
7
 
8
8
  /**
@@ -153,10 +153,43 @@ export function classifyWorkflowFault(error: unknown, policy: WorkflowRetryPolic
153
153
  };
154
154
  }
155
155
 
156
- /** The half of a Workflow step this kit uses: run a named body, or serve its journalled result. */
156
+ /**
157
+ * The half of a Workflow step this kit uses: run a named body, or serve its journalled result.
158
+ *
159
+ * **`sleep` is optional here and required on what {@link classifiedSteps} hands back, and the asymmetry
160
+ * is the truth rather than a convenience.** The platform's own `WorkflowStep` always offers one. What
161
+ * does not are this kit's hand-written test doubles, which exist to exercise the classification and
162
+ * nothing else — and demanding a primitive the classifier never classifies would cost every one of them
163
+ * a line of noise. Handing an optional one back instead would push a null check into the one caller that
164
+ * genuinely pauses.
165
+ */
157
166
  export interface WorkflowStepLike {
158
167
  /** Run a named step, or return its journalled result if this instance already completed it. */
159
168
  do<T>(name: string, fn: () => Promise<T>): Promise<T>;
169
+ /**
170
+ * Pause the instance for `durationMs` and resume it afterwards — possibly in a fresh isolate, which
171
+ * is the whole reason a caller reaches for it rather than for a timer. Absent only on a test double.
172
+ */
173
+ sleep?(name: string, durationMs: number): Promise<void>;
174
+ }
175
+
176
+ /**
177
+ * A step runner that can pause — what {@link classifiedSteps} hands back, always.
178
+ *
179
+ * The durable pause is why this type exists rather than being a convenience over the one above. A `sleep`
180
+ * is journalled, so a wait survives the instance being evicted during it; a `setTimeout` inside one step
181
+ * burns that step's wall clock and dies with the instance holding it.
182
+ *
183
+ * **It was introduced on a stronger claim, and the claim did not survive contact (pithy-sh/pithy#647).** The
184
+ * argument was that a Secrets Store binding answers the same for its isolate's whole life, so only a resume
185
+ * into a fresh isolate could ever see a write land. Measured against a real store on 2026-09-25, that is
186
+ * false: a binding refreshes inside a live isolate roughly 1.3-1.5s after the REST write, proved by holding
187
+ * two isolates warm through a write and watching each serve the old value before it and the new one after,
188
+ * over 328 samples. The durability argument above is the true one and is sufficient on its own.
189
+ */
190
+ export interface DurableStepLike extends WorkflowStepLike {
191
+ /** Pause the instance for `durationMs`, then resume. */
192
+ sleep(name: string, durationMs: number): Promise<void>;
160
193
  }
161
194
 
162
195
  /**
@@ -189,8 +222,8 @@ export function classifiedSteps(
189
222
  step: WorkflowStepLike,
190
223
  policy: WorkflowRetryPolicy,
191
224
  terminalError: TerminalErrorConstructor,
192
- ): WorkflowStepLike {
193
- return {
225
+ ): DurableStepLike {
226
+ const runner: DurableStepLike = {
194
227
  do<T>(name: string, fn: () => Promise<T>): Promise<T> {
195
228
  return step.do(name, async () => {
196
229
  try {
@@ -215,5 +248,29 @@ export function classifiedSteps(
215
248
  }
216
249
  });
217
250
  },
251
+ /**
252
+ * Forwarded, and deliberately unclassified. A pause raises no fault of the capability's — the
253
+ * instance either resumes or it is gone — so there is nothing here for a policy to decide.
254
+ *
255
+ * A runner that cannot pause is refused, and the refusal is raised **through `do`** rather than
256
+ * thrown from here. A throw from here is a throw the engine never classifies, so it is free to
257
+ * re-drive the instance — and re-driving is the unbounded loop the caller is spending its sleeps to
258
+ * avoid. Going through `do` makes the refusal terminal on its first attempt, like every other
259
+ * answer that cannot change.
260
+ */
261
+ sleep(name: string, durationMs: number): Promise<void> {
262
+ const pause = step.sleep;
263
+ if (pause === undefined) {
264
+ return runner.do<void>(`${name}/cannot-pause`, async () => {
265
+ throw new InternalError({
266
+ message: "This Workflow step runner cannot pause.",
267
+ action: "Give the step runner a `sleep`; the platform's own `WorkflowStep` always has one.",
268
+ detail: `step '${name}' asked to pause ${durationMs}ms through a runner that declares no sleep`,
269
+ });
270
+ });
271
+ }
272
+ return pause.call(step, name, durationMs);
273
+ },
218
274
  };
275
+ return runner;
219
276
  }