@pithy-sh/core 0.8.0 → 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 };
@@ -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.8.0";
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.8.0";
7
+ const PACKAGE_VERSION = "0.9.0";
8
8
  //#endregion
9
9
  export { PACKAGE_NAME, PACKAGE_VERSION };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pithy-sh/core",
3
- "version": "0.8.0",
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>;
@@ -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.8.0";
16
+ export const PACKAGE_VERSION = "0.9.0";