@homeflare/kit 0.1.0 → 0.1.1

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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @homeflare/kit
2
2
 
3
- Shared primitives for the HomeFlare estate. Bun-native to author, runtime-neutral to
3
+ Runtime-neutral primitives: environment parsing, errors, and HTTP. Bun-native to
4
4
  consume — the published bundle runs on Cloudflare Workers, Node and Bun alike.
5
5
 
6
6
  ```sh
package/dist/env.d.ts CHANGED
@@ -49,11 +49,11 @@ export declare class EnvError extends Error {
49
49
  * the four-character STRING "null", because the binding is present and String(null) is
50
50
  * truthy. Code that checks only for '' therefore accepts it, and the request goes out
51
51
  * as `Authorization: Bearer null`.
52
- * 🔴 MEASURED IN THIS ESTATE, 2026-09-02. Twenty hand-rolled clients; this guard was in
52
+ * 🔴 MEASURED IN PRODUCTION, 2026-09-02. Twenty hand-rolled clients; this guard was in
53
53
  * four of them. The other sixteen surfaced a bare 401 from upstream — which reads as
54
54
  * "the token is wrong" and sends an operator to rotate a perfectly good credential.
55
55
  * The litellm server spent a day authenticating with nothing while /health answered 200.
56
- * Ported from house/mcp-servers/packages/kit/src/http.ts, where it was paid for once.
56
+ * Ported from an internal HTTP client, where this lesson was paid for once.
57
57
  */
58
58
  export declare function isUnset(value: string): boolean;
59
59
  /**
package/dist/errors.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Errors a CALLER can act on — an agent, a retry loop, or a person reading a log.
3
3
  *
4
- * ★ PORTED FROM house/mcp-servers/packages/kit/src/errors.ts, where 34 packages proved
5
- * the shape. Generalised: the estate version is MCP-specific (`HouseToolError`), this
4
+ * ★ PORTED FROM AN INTERNAL MCP TOOLKIT, where 34 packages proved
5
+ * the shape. Generalised: that version is MCP-specific (`HouseToolError`), this
6
6
  * one is not, so a Worker or a script can use the same vocabulary.
7
7
  *
8
8
  * ⛔ THE RULE THIS ENFORCES: a failure must never be silent, and never ambiguous with a
@@ -30,7 +30,7 @@ export type ErrorKind =
30
30
  /**
31
31
  * Over a rate limit. The IDENTICAL call succeeds after the stated delay.
32
32
  * 🔴 A DISTINCT KIND BECAUSE IT WAS LANDING AS `bad_arguments`, measured 2026-09-07 in
33
- * the estate: a 429 mapped to "your arguments are wrong", so a throttled agent
33
+ * production: a 429 mapped to "your arguments are wrong", so a throttled agent
34
34
  * rewrote a correct call, was throttled again, and concluded the tool was broken.
35
35
  * This is the refusal agents meet most — Cloudflare frontier models are 20 rpm.
36
36
  */
package/dist/index.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  /**
2
- * @homeflare/kit — the estate's shared primitives.
2
+ * @homeflare/kit — runtime-neutral shared primitives.
3
3
  *
4
4
  * ⛔ THIS ENTRYPOINT STAYS RUNTIME-NEUTRAL. Bun is how the kit is authored, built and
5
- * tested; it is not what consumers run. The monorepo ships this code to Cloudflare
5
+ * tested; it is not what consumers run. Consumers ship this code to Cloudflare
6
6
  * Workers, so anything reaching for `bun:*`, `node:fs` or a filesystem belongs behind
7
7
  * its own subpath export (`@homeflare/kit/<area>`), never here.
8
8
  * The failure this prevents is a deploy-time one: a `bun:sqlite` import that resolves
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/version.ts
2
- var VERSION = "0.1.0";
2
+ var VERSION = "0.1.1";
3
3
  // src/env.ts
4
4
  class EnvError extends Error {
5
5
  key;
@@ -110,5 +110,5 @@ export {
110
110
  parseEnv
111
111
  };
112
112
 
113
- //# debugId=A318350C97DF929364756E2164756E21
113
+ //# debugId=A55C522EE736511464756E2164756E21
114
114
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -2,12 +2,12 @@
2
2
  "version": 3,
3
3
  "sources": ["../src/version.ts", "../src/env.ts", "../src/errors.ts", "../src/http.ts"],
4
4
  "sourcesContent": [
5
- "// ⛔ GENERATED by scripts/sync-versions.ts — do not edit. Source: package.json.\nexport const VERSION: string = '0.1.0';\n",
6
- "/**\n * Environment parsing — the one thing every HomeFlare app does before anything else.\n *\n * ★ WHY THIS EXISTS RATHER THAN zod. A Worker reads its config from a plain object\n * (`env`), not from `process.env`, and it does so on every request. Pulling a schema\n * library in for that costs bundle size on a hot path to validate perhaps six keys.\n * This is deliberately small and dependency-free; when a shape genuinely needs\n * unions, refinement or codecs, reach for zod in the consuming app instead of\n * growing this.\n *\n * ⚠️ THIS VALIDATES SHAPE, NOT SECRECY. A value arriving here has already been handed\n * over by the platform — a Worker binding, an OpenBao-minted lease. Nothing in this\n * file should ever log a value, and callers should not either: a thrown error names\n * the KEY that was wrong and never what it contained.\n */\n\n/** How one environment key is read, and what it becomes. */\nexport type EnvSpec =\n | { readonly type: 'string'; readonly optional?: boolean; readonly default?: string }\n | { readonly type: 'number'; readonly optional?: boolean; readonly default?: number }\n | { readonly type: 'boolean'; readonly optional?: boolean; readonly default?: boolean };\n\n/** A description of every key an app needs, keyed by the name it appears under. */\nexport type EnvSchema = Readonly<Record<string, EnvSpec>>;\n\n/** What a schema produces once parsed: each key narrowed to its declared type. */\ntype Parsed<S extends EnvSchema> = {\n readonly [K in keyof S]: S[K] extends { type: 'number' }\n ? number\n : S[K] extends { type: 'boolean' }\n ? boolean\n : string;\n};\n\n/** Raised when a key is missing or cannot be read as its declared type. */\nexport class EnvError extends Error {\n /** The offending key. Never the value — see the file header. */\n readonly key: string;\n\n constructor(key: string, reason: string) {\n super(`env ${key}: ${reason}`);\n this.name = 'EnvError';\n this.key = key;\n }\n}\n\n/**\n * ⛔ THE THREE VALUES THAT MEAN \"ABSENT\", AND WHY IT IS NOT JUST THE EMPTY STRING.\n * An unbound workerd binding does NOT arrive as undefined — `fromEnvironment` produces\n * the four-character STRING \"null\", because the binding is present and String(null) is\n * truthy. Code that checks only for '' therefore accepts it, and the request goes out\n * as `Authorization: Bearer null`.\n * 🔴 MEASURED IN THIS ESTATE, 2026-09-02. Twenty hand-rolled clients; this guard was in\n * four of them. The other sixteen surfaced a bare 401 from upstream — which reads as\n * \"the token is wrong\" and sends an operator to rotate a perfectly good credential.\n * The litellm server spent a day authenticating with nothing while /health answered 200.\n * Ported from house/mcp-servers/packages/kit/src/http.ts, where it was paid for once.\n */\nexport function isUnset(value: string): boolean {\n return value === '' || value === 'null' || value === 'undefined';\n}\n\n/**\n * ⚠️ 'false' IS TRUTHY AS A STRING, and that is the bug this function exists to stop.\n * `Boolean(env.FEATURE)` on the string 'false' is `true`, so a flag turned off in a\n * dashboard stays on in production and nothing in the logs says why. Only the listed\n * spellings are accepted; anything else throws rather than guessing.\n */\nconst TRUE = new Set(['1', 'true', 'yes', 'on']);\nconst FALSE = new Set(['0', 'false', 'no', 'off']);\n\n/**\n * Read `source` against `schema`, throwing `EnvError` on the first key that is missing\n * or malformed.\n *\n * ★ THROWS RATHER THAN RETURNING A RESULT, deliberately: this runs once at startup or\n * at the top of a fetch handler, where a misconfigured deployment should fail loudly\n * and immediately rather than produce a half-configured app that fails later somewhere\n * less obvious.\n */\nexport function parseEnv<const S extends EnvSchema>(\n schema: S,\n source: Readonly<Record<string, unknown>>,\n): Parsed<S> {\n const out: Record<string, string | number | boolean> = {};\n\n for (const key of Object.keys(schema)) {\n const spec = schema[key] as EnvSpec;\n const raw = source[key];\n\n // ⛔ isUnset, not `=== ''`. See its comment: \"null\" is what an unbound workerd\n // binding actually looks like, and treating it as a value is a day-long debug.\n if (raw === undefined || raw === null || isUnset(String(raw).trim())) {\n if (spec.default !== undefined) {\n out[key] = spec.default;\n continue;\n }\n if (spec.optional === true) continue;\n throw new EnvError(key, 'required but not set');\n }\n\n // ⚠️ Trimmed: a rendered env file with a trailing newline is the common case, and an\n // untrimmed token fails upstream with the same unhelpful 401.\n const text = String(raw).trim();\n\n if (spec.type === 'number') {\n const n = Number(text);\n // ⚠️ Number('') is 0 and Number(' ') is 0; the empty check above is what makes\n // this safe. NaN is the only remaining failure and it is caught here.\n if (!Number.isFinite(n)) throw new EnvError(key, 'expected a number');\n out[key] = n;\n } else if (spec.type === 'boolean') {\n const lowered = text.toLowerCase();\n if (TRUE.has(lowered)) out[key] = true;\n else if (FALSE.has(lowered)) out[key] = false;\n else throw new EnvError(key, 'expected one of 1/true/yes/on or 0/false/no/off');\n } else {\n out[key] = text;\n }\n }\n\n return out as Parsed<S>;\n}\n",
7
- "/**\n * Errors a CALLER can act on — an agent, a retry loop, or a person reading a log.\n *\n * ★ PORTED FROM house/mcp-servers/packages/kit/src/errors.ts, where 34 packages proved\n * the shape. Generalised: the estate version is MCP-specific (`HouseToolError`), this\n * one is not, so a Worker or a script can use the same vocabulary.\n *\n * ⛔ THE RULE THIS ENFORCES: a failure must never be silent, and never ambiguous with a\n * DIFFERENT failure. Both halves were learned from incidents, not from principle:\n * - An Access provider read its token at module scope and, when that failed, silently\n * omitted the auth header. Cloudflare answered a bare 401 — indistinguishable from a\n * bad credential — and sent an operator to debug the wrong subsystem. Omitting is\n * worse than throwing.\n * - A gate reported success on a corrupted file because it compared the file to itself.\n * A check that cannot fail is worse than no check, because it is trusted.\n *\n * ⚠️ THE AUDIENCE MAY BE AN AGENT, not a human with a debugger. An agent cannot open a\n * dashboard or ask a colleague, so everything it needs to pick its next action must be\n * in the error. One that reads \"permission denied\" retries forever; one that reads\n * \"the token lacks Grafana Read; ask an operator to widen the grant\" stops and reports.\n */\n\n/** Which class of failure this is. Callers branch on it, so the set stays small. */\nexport type ErrorKind =\n /** Not who it claims, or claims nothing. Retrying unchanged fails. */\n | 'unauthenticated'\n /** Known, but lacks the grant. ⛔ NEVER retry — a human must widen a scope. */\n | 'forbidden'\n /** The upstream said no. The caller may be able to fix its arguments and retry. */\n | 'upstream_rejected'\n /**\n * Over a rate limit. The IDENTICAL call succeeds after the stated delay.\n * 🔴 A DISTINCT KIND BECAUSE IT WAS LANDING AS `bad_arguments`, measured 2026-09-07 in\n * the estate: a 429 mapped to \"your arguments are wrong\", so a throttled agent\n * rewrote a correct call, was throttled again, and concluded the tool was broken.\n * This is the refusal agents meet most — Cloudflare frontier models are 20 rpm.\n */\n | 'rate_limited'\n /** Could not be reached at all. Usually transient; a retry may work. */\n | 'upstream_unreachable'\n /** Called with arguments it cannot accept. Correct them and retry. */\n | 'bad_arguments'\n /** We are misconfigured. ⛔ A caller cannot fix this; it must report it. */\n | 'misconfigured';\n\n/**\n * What each kind means, for anything that has to explain them.\n *\n * ⛔ TYPED AS Record<ErrorKind, string>, WHICH IS THE POINT: adding a kind above without\n * describing it here is a COMPILE ERROR. A hand-kept list beside a union goes stale,\n * and a documented error contract with a missing case is worse than none — it is\n * trusted.\n */\nexport const KIND_DESCRIPTIONS: Record<ErrorKind, string> = {\n unauthenticated: 'The credential was missing, malformed, or rejected.',\n forbidden: 'The credential is valid but lacks the required grant. Do not retry.',\n upstream_rejected: 'The upstream refused the request as made.',\n rate_limited: 'Throttled. The identical call succeeds after the stated delay.',\n upstream_unreachable: 'The upstream could not be reached. A retry may succeed.',\n bad_arguments: 'The arguments were not acceptable. Correct them and retry.',\n misconfigured: 'This service is misconfigured and cannot serve the request.',\n};\n\nexport interface KitErrorOptions {\n /** Which system failed. ⛔ Name it — \"request failed\" tells a caller nothing. */\n readonly system: string;\n /** What would make it work. This is what stops an agent retrying forever. */\n readonly remedy?: string;\n /** For `rate_limited`: how long to wait, in milliseconds. */\n readonly retryAfterMs?: number;\n readonly cause?: unknown;\n}\n\n/** A failure with enough shape for a caller to decide what to do next. */\nexport class KitError extends Error {\n readonly kind: ErrorKind;\n readonly system: string;\n readonly remedy: string | undefined;\n readonly retryAfterMs: number | undefined;\n\n constructor(kind: ErrorKind, message: string, options: KitErrorOptions) {\n super(`${options.system}: ${message}`, { cause: options.cause });\n this.name = 'KitError';\n this.kind = kind;\n this.system = options.system;\n this.remedy = options.remedy;\n this.retryAfterMs = options.retryAfterMs;\n }\n\n /** True when retrying the identical call could plausibly succeed. */\n get retryable(): boolean {\n return this.kind === 'rate_limited' || this.kind === 'upstream_unreachable';\n }\n}\n\n/**\n * Map an HTTP status to a kind.\n *\n * ⚠️ 429 IS CHECKED EXPLICITLY, not left to fall through. It used to reach\n * `upstream_rejected` here and `bad_arguments` elsewhere — both of which tell a caller\n * to CHANGE ITS ARGUMENTS, which is precisely wrong for a throttle.\n */\nexport function kindForStatus(status: number): ErrorKind {\n if (status === 401) return 'unauthenticated';\n if (status === 403) return 'forbidden';\n if (status === 429) return 'rate_limited';\n if (status >= 500) return 'upstream_unreachable';\n return 'upstream_rejected';\n}\n",
5
+ "// ⛔ GENERATED by scripts/sync-versions.ts — do not edit. Source: package.json.\nexport const VERSION: string = '0.1.1';\n",
6
+ "/**\n * Environment parsing — the one thing every HomeFlare app does before anything else.\n *\n * ★ WHY THIS EXISTS RATHER THAN zod. A Worker reads its config from a plain object\n * (`env`), not from `process.env`, and it does so on every request. Pulling a schema\n * library in for that costs bundle size on a hot path to validate perhaps six keys.\n * This is deliberately small and dependency-free; when a shape genuinely needs\n * unions, refinement or codecs, reach for zod in the consuming app instead of\n * growing this.\n *\n * ⚠️ THIS VALIDATES SHAPE, NOT SECRECY. A value arriving here has already been handed\n * over by the platform — a Worker binding, an OpenBao-minted lease. Nothing in this\n * file should ever log a value, and callers should not either: a thrown error names\n * the KEY that was wrong and never what it contained.\n */\n\n/** How one environment key is read, and what it becomes. */\nexport type EnvSpec =\n | { readonly type: 'string'; readonly optional?: boolean; readonly default?: string }\n | { readonly type: 'number'; readonly optional?: boolean; readonly default?: number }\n | { readonly type: 'boolean'; readonly optional?: boolean; readonly default?: boolean };\n\n/** A description of every key an app needs, keyed by the name it appears under. */\nexport type EnvSchema = Readonly<Record<string, EnvSpec>>;\n\n/** What a schema produces once parsed: each key narrowed to its declared type. */\ntype Parsed<S extends EnvSchema> = {\n readonly [K in keyof S]: S[K] extends { type: 'number' }\n ? number\n : S[K] extends { type: 'boolean' }\n ? boolean\n : string;\n};\n\n/** Raised when a key is missing or cannot be read as its declared type. */\nexport class EnvError extends Error {\n /** The offending key. Never the value — see the file header. */\n readonly key: string;\n\n constructor(key: string, reason: string) {\n super(`env ${key}: ${reason}`);\n this.name = 'EnvError';\n this.key = key;\n }\n}\n\n/**\n * ⛔ THE THREE VALUES THAT MEAN \"ABSENT\", AND WHY IT IS NOT JUST THE EMPTY STRING.\n * An unbound workerd binding does NOT arrive as undefined — `fromEnvironment` produces\n * the four-character STRING \"null\", because the binding is present and String(null) is\n * truthy. Code that checks only for '' therefore accepts it, and the request goes out\n * as `Authorization: Bearer null`.\n * 🔴 MEASURED IN PRODUCTION, 2026-09-02. Twenty hand-rolled clients; this guard was in\n * four of them. The other sixteen surfaced a bare 401 from upstream — which reads as\n * \"the token is wrong\" and sends an operator to rotate a perfectly good credential.\n * The litellm server spent a day authenticating with nothing while /health answered 200.\n * Ported from an internal HTTP client, where this lesson was paid for once.\n */\nexport function isUnset(value: string): boolean {\n return value === '' || value === 'null' || value === 'undefined';\n}\n\n/**\n * ⚠️ 'false' IS TRUTHY AS A STRING, and that is the bug this function exists to stop.\n * `Boolean(env.FEATURE)` on the string 'false' is `true`, so a flag turned off in a\n * dashboard stays on in production and nothing in the logs says why. Only the listed\n * spellings are accepted; anything else throws rather than guessing.\n */\nconst TRUE = new Set(['1', 'true', 'yes', 'on']);\nconst FALSE = new Set(['0', 'false', 'no', 'off']);\n\n/**\n * Read `source` against `schema`, throwing `EnvError` on the first key that is missing\n * or malformed.\n *\n * ★ THROWS RATHER THAN RETURNING A RESULT, deliberately: this runs once at startup or\n * at the top of a fetch handler, where a misconfigured deployment should fail loudly\n * and immediately rather than produce a half-configured app that fails later somewhere\n * less obvious.\n */\nexport function parseEnv<const S extends EnvSchema>(\n schema: S,\n source: Readonly<Record<string, unknown>>,\n): Parsed<S> {\n const out: Record<string, string | number | boolean> = {};\n\n for (const key of Object.keys(schema)) {\n const spec = schema[key] as EnvSpec;\n const raw = source[key];\n\n // ⛔ isUnset, not `=== ''`. See its comment: \"null\" is what an unbound workerd\n // binding actually looks like, and treating it as a value is a day-long debug.\n if (raw === undefined || raw === null || isUnset(String(raw).trim())) {\n if (spec.default !== undefined) {\n out[key] = spec.default;\n continue;\n }\n if (spec.optional === true) continue;\n throw new EnvError(key, 'required but not set');\n }\n\n // ⚠️ Trimmed: a rendered env file with a trailing newline is the common case, and an\n // untrimmed token fails upstream with the same unhelpful 401.\n const text = String(raw).trim();\n\n if (spec.type === 'number') {\n const n = Number(text);\n // ⚠️ Number('') is 0 and Number(' ') is 0; the empty check above is what makes\n // this safe. NaN is the only remaining failure and it is caught here.\n if (!Number.isFinite(n)) throw new EnvError(key, 'expected a number');\n out[key] = n;\n } else if (spec.type === 'boolean') {\n const lowered = text.toLowerCase();\n if (TRUE.has(lowered)) out[key] = true;\n else if (FALSE.has(lowered)) out[key] = false;\n else throw new EnvError(key, 'expected one of 1/true/yes/on or 0/false/no/off');\n } else {\n out[key] = text;\n }\n }\n\n return out as Parsed<S>;\n}\n",
7
+ "/**\n * Errors a CALLER can act on — an agent, a retry loop, or a person reading a log.\n *\n * ★ PORTED FROM AN INTERNAL MCP TOOLKIT, where 34 packages proved\n * the shape. Generalised: that version is MCP-specific (`HouseToolError`), this\n * one is not, so a Worker or a script can use the same vocabulary.\n *\n * ⛔ THE RULE THIS ENFORCES: a failure must never be silent, and never ambiguous with a\n * DIFFERENT failure. Both halves were learned from incidents, not from principle:\n * - An Access provider read its token at module scope and, when that failed, silently\n * omitted the auth header. Cloudflare answered a bare 401 — indistinguishable from a\n * bad credential — and sent an operator to debug the wrong subsystem. Omitting is\n * worse than throwing.\n * - A gate reported success on a corrupted file because it compared the file to itself.\n * A check that cannot fail is worse than no check, because it is trusted.\n *\n * ⚠️ THE AUDIENCE MAY BE AN AGENT, not a human with a debugger. An agent cannot open a\n * dashboard or ask a colleague, so everything it needs to pick its next action must be\n * in the error. One that reads \"permission denied\" retries forever; one that reads\n * \"the token lacks Grafana Read; ask an operator to widen the grant\" stops and reports.\n */\n\n/** Which class of failure this is. Callers branch on it, so the set stays small. */\nexport type ErrorKind =\n /** Not who it claims, or claims nothing. Retrying unchanged fails. */\n | 'unauthenticated'\n /** Known, but lacks the grant. ⛔ NEVER retry — a human must widen a scope. */\n | 'forbidden'\n /** The upstream said no. The caller may be able to fix its arguments and retry. */\n | 'upstream_rejected'\n /**\n * Over a rate limit. The IDENTICAL call succeeds after the stated delay.\n * 🔴 A DISTINCT KIND BECAUSE IT WAS LANDING AS `bad_arguments`, measured 2026-09-07 in\n * production: a 429 mapped to \"your arguments are wrong\", so a throttled agent\n * rewrote a correct call, was throttled again, and concluded the tool was broken.\n * This is the refusal agents meet most — Cloudflare frontier models are 20 rpm.\n */\n | 'rate_limited'\n /** Could not be reached at all. Usually transient; a retry may work. */\n | 'upstream_unreachable'\n /** Called with arguments it cannot accept. Correct them and retry. */\n | 'bad_arguments'\n /** We are misconfigured. ⛔ A caller cannot fix this; it must report it. */\n | 'misconfigured';\n\n/**\n * What each kind means, for anything that has to explain them.\n *\n * ⛔ TYPED AS Record<ErrorKind, string>, WHICH IS THE POINT: adding a kind above without\n * describing it here is a COMPILE ERROR. A hand-kept list beside a union goes stale,\n * and a documented error contract with a missing case is worse than none — it is\n * trusted.\n */\nexport const KIND_DESCRIPTIONS: Record<ErrorKind, string> = {\n unauthenticated: 'The credential was missing, malformed, or rejected.',\n forbidden: 'The credential is valid but lacks the required grant. Do not retry.',\n upstream_rejected: 'The upstream refused the request as made.',\n rate_limited: 'Throttled. The identical call succeeds after the stated delay.',\n upstream_unreachable: 'The upstream could not be reached. A retry may succeed.',\n bad_arguments: 'The arguments were not acceptable. Correct them and retry.',\n misconfigured: 'This service is misconfigured and cannot serve the request.',\n};\n\nexport interface KitErrorOptions {\n /** Which system failed. ⛔ Name it — \"request failed\" tells a caller nothing. */\n readonly system: string;\n /** What would make it work. This is what stops an agent retrying forever. */\n readonly remedy?: string;\n /** For `rate_limited`: how long to wait, in milliseconds. */\n readonly retryAfterMs?: number;\n readonly cause?: unknown;\n}\n\n/** A failure with enough shape for a caller to decide what to do next. */\nexport class KitError extends Error {\n readonly kind: ErrorKind;\n readonly system: string;\n readonly remedy: string | undefined;\n readonly retryAfterMs: number | undefined;\n\n constructor(kind: ErrorKind, message: string, options: KitErrorOptions) {\n super(`${options.system}: ${message}`, { cause: options.cause });\n this.name = 'KitError';\n this.kind = kind;\n this.system = options.system;\n this.remedy = options.remedy;\n this.retryAfterMs = options.retryAfterMs;\n }\n\n /** True when retrying the identical call could plausibly succeed. */\n get retryable(): boolean {\n return this.kind === 'rate_limited' || this.kind === 'upstream_unreachable';\n }\n}\n\n/**\n * Map an HTTP status to a kind.\n *\n * ⚠️ 429 IS CHECKED EXPLICITLY, not left to fall through. It used to reach\n * `upstream_rejected` here and `bad_arguments` elsewhere — both of which tell a caller\n * to CHANGE ITS ARGUMENTS, which is precisely wrong for a throttle.\n */\nexport function kindForStatus(status: number): ErrorKind {\n if (status === 401) return 'unauthenticated';\n if (status === 403) return 'forbidden';\n if (status === 429) return 'rate_limited';\n if (status >= 500) return 'upstream_unreachable';\n return 'upstream_rejected';\n}\n",
8
8
  "/**\n * HTTP client — a configured `ky`, not a wrapper around it.\n *\n * ★ WHY ky AND NOT A HAND-ROLLED fetch+retry. Retry, timeout and backoff are each easy\n * to write and easy to write subtly wrong (retrying a POST, retrying a 4xx, a timeout\n * that leaks the AbortController). ky is zero-dependency, TypeScript-native and\n * fetch-based — measured 2026-09-15: `npm view ky dependencies` is empty — so it adds\n * nothing to a consumer's tree.\n * ⚠️ ky is described as browser-first, which matters only in that it assumes a global\n * `fetch`. workerd, Node 18+ and Bun all provide one, so there is nothing to polyfill.\n * ⛔ NOT ofetch (pulls destr + node-fetch-native) and NOT wretch, which is BROKEN on\n * Workers out of the box: Cloudflare's Response has no `.type` and wretch throws\n * reading it, needing a middleware patch to work at all.\n *\n * ★ THIS MODULE IS DELIBERATELY THIN. It re-exports ky's own types rather than inventing\n * parallel ones, so a caller who needs something not covered here uses ky directly and\n * loses nothing.\n */\nimport ky, { HTTPError, TimeoutError, type KyInstance, type Options } from 'ky';\n\nexport { HTTPError, TimeoutError };\nexport type { KyInstance, Options };\n\n/**\n * The house defaults.\n *\n * ⚠️ ky retries only IDEMPOTENT methods by default (GET/PUT/HEAD/DELETE/OPTIONS/TRACE) —\n * POST is excluded, which is the behaviour you want and the one a hand-rolled retry\n * usually gets wrong. Retried status codes are 408/413/429/500/502/503/504.\n * ⚠️ `timeout` is PER ATTEMPT, not for the whole call. With limit 3 and a 10s timeout the\n * worst case is ~30s plus backoff — shorten both for a Worker on a request path, where\n * the CPU-time limit is unforgiving.\n */\nexport const http: KyInstance = ky.extend({\n retry: { limit: 3, backoffLimit: 3_000 },\n timeout: 10_000,\n});\n\n/**\n * A client bound to one base URL, for talking to a specific service.\n *\n * const linear = client('https://api.linear.app', { headers: { authorization } });\n * const issues = await linear.get('/graphql').json<Issues>();\n *\n * ⚠️ `baseUrl`, NOT `prefixUrl`. ky 2.x renamed the option and changed its semantics:\n * `baseUrl` resolves per the URL standard, so a leading slash is fine and an absolute\n * input bypasses the base. (ky 1.x had `prefixUrl`, which threw on a leading slash —\n * measured 2026-09-15, the old name is a type error, not a silent no-op.)\n */\nexport function client(baseUrl: string, options?: Options): KyInstance {\n return http.extend({ ...options, baseUrl });\n}\n"
9
9
  ],
10
10
  "mappings": "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",
11
- "debugId": "A318350C97DF929364756E2164756E21",
11
+ "debugId": "A55C522EE736511464756E2164756E21",
12
12
  "names": []
13
13
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@homeflare/kit",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Runtime-neutral HomeFlare primitives: env parsing, errors, logging, HTTP.",
5
5
  "homepage": "https://github.com/taslabs-net/homeflare-kit/tree/main/packages/kit#readme",
6
6
  "bugs": "https://github.com/taslabs-net/homeflare-kit/issues",
package/src/env.ts CHANGED
@@ -50,11 +50,11 @@ export class EnvError extends Error {
50
50
  * the four-character STRING "null", because the binding is present and String(null) is
51
51
  * truthy. Code that checks only for '' therefore accepts it, and the request goes out
52
52
  * as `Authorization: Bearer null`.
53
- * 🔴 MEASURED IN THIS ESTATE, 2026-09-02. Twenty hand-rolled clients; this guard was in
53
+ * 🔴 MEASURED IN PRODUCTION, 2026-09-02. Twenty hand-rolled clients; this guard was in
54
54
  * four of them. The other sixteen surfaced a bare 401 from upstream — which reads as
55
55
  * "the token is wrong" and sends an operator to rotate a perfectly good credential.
56
56
  * The litellm server spent a day authenticating with nothing while /health answered 200.
57
- * Ported from house/mcp-servers/packages/kit/src/http.ts, where it was paid for once.
57
+ * Ported from an internal HTTP client, where this lesson was paid for once.
58
58
  */
59
59
  export function isUnset(value: string): boolean {
60
60
  return value === '' || value === 'null' || value === 'undefined';
package/src/errors.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Errors a CALLER can act on — an agent, a retry loop, or a person reading a log.
3
3
  *
4
- * ★ PORTED FROM house/mcp-servers/packages/kit/src/errors.ts, where 34 packages proved
5
- * the shape. Generalised: the estate version is MCP-specific (`HouseToolError`), this
4
+ * ★ PORTED FROM AN INTERNAL MCP TOOLKIT, where 34 packages proved
5
+ * the shape. Generalised: that version is MCP-specific (`HouseToolError`), this
6
6
  * one is not, so a Worker or a script can use the same vocabulary.
7
7
  *
8
8
  * ⛔ THE RULE THIS ENFORCES: a failure must never be silent, and never ambiguous with a
@@ -31,7 +31,7 @@ export type ErrorKind =
31
31
  /**
32
32
  * Over a rate limit. The IDENTICAL call succeeds after the stated delay.
33
33
  * 🔴 A DISTINCT KIND BECAUSE IT WAS LANDING AS `bad_arguments`, measured 2026-09-07 in
34
- * the estate: a 429 mapped to "your arguments are wrong", so a throttled agent
34
+ * production: a 429 mapped to "your arguments are wrong", so a throttled agent
35
35
  * rewrote a correct call, was throttled again, and concluded the tool was broken.
36
36
  * This is the refusal agents meet most — Cloudflare frontier models are 20 rpm.
37
37
  */
package/src/index.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  /**
2
- * @homeflare/kit — the estate's shared primitives.
2
+ * @homeflare/kit — runtime-neutral shared primitives.
3
3
  *
4
4
  * ⛔ THIS ENTRYPOINT STAYS RUNTIME-NEUTRAL. Bun is how the kit is authored, built and
5
- * tested; it is not what consumers run. The monorepo ships this code to Cloudflare
5
+ * tested; it is not what consumers run. Consumers ship this code to Cloudflare
6
6
  * Workers, so anything reaching for `bun:*`, `node:fs` or a filesystem belongs behind
7
7
  * its own subpath export (`@homeflare/kit/<area>`), never here.
8
8
  * The failure this prevents is a deploy-time one: a `bun:sqlite` import that resolves
package/src/version.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  // ⛔ GENERATED by scripts/sync-versions.ts — do not edit. Source: package.json.
2
- export const VERSION: string = '0.1.0';
2
+ export const VERSION: string = '0.1.1';