diff --git a/CHANGELOG.md b/CHANGELOG.md index a1269123..0274e44b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,16 @@ Historical entries retain their original delivery coordinates. ## Unreleased +- The packaged type sources compile again under a consumer's strict + compiler flags. Consumers that set `erasableSyntaxOnly` and narrow + `ProcessEnv.NODE_ENV` to its literal union, as Next.js does, can now + typecheck the shipped sources cleanly: the last constructor parameter + property is an explicit field, the persistent helper's environment keeps + `NODE_ENV` literal, and portable request bodies omit `body` instead of + passing an explicit `undefined`. The clean-consumer package smoke now + typechecks the public entrypoints under those same flags, and + `@hraness/local-custody` moves to the immutable 0.9.1 release, whose + shipped sources made the same erasable-syntax cleanup. - Move the shared clip, capture, and URL-intelligence runtime from `@hraness/kb` 0.19.6 to the immutable `@hraness/wordcell` 0.24.0 release. The reviewed dynamic-resolution boundary now pins the two bundled modules diff --git a/bun.lock b/bun.lock index b48b6c49..c57229c6 100644 --- a/bun.lock +++ b/bun.lock @@ -8,7 +8,7 @@ "@1password/sdk": "0.5.0", "@hraness/accounts-cli": "github:hraness/accounts-cli#v0.1.3", "@hraness/desktop-foundation": "https://github.com/hraness/desktop-foundation/releases/download/v0.8.0/hraness-desktop-foundation-0.8.0.tgz", - "@hraness/local-custody": "https://github.com/hraness/local-custody/releases/download/v0.9.0/hraness-local-custody-0.9.0.tgz", + "@hraness/local-custody": "https://github.com/hraness/local-custody/releases/download/v0.9.1/hraness-local-custody-0.9.1.tgz", "@hraness/message-like-me": "github:hraness/textbutler#83453cc7c17b49bb53fdfd89ccb69b8b44b30af1", "@hraness/support-foundation": "github:hraness/support-foundation#8bb514d24b79dc3f305390700ae312cab88e7ad2", "@hraness/wordcell": "https://github.com/hraness/wordcell/releases/download/v0.24.0/hraness-wordcell-0.24.0.tgz", @@ -132,7 +132,7 @@ "@hraness/direct": ["@hraness/direct@https://github.com/hraness/direct/releases/download/v0.7.21/hraness-direct-0.7.21.tgz", { "peerDependencies": { "@antithesishq/bombadil": "0.7.2", "agent-browser": "0.32.3", "effect": "3.22.1", "react": ">=18 <20" }, "optionalPeers": ["@antithesishq/bombadil", "agent-browser", "effect", "react"] }, "sha512-zxnxv7Uvloxk7yQjxKukC7gygfDuoYLR6UGayLpaJplq4MdxQ5kFNkRufYOp2QjyzuPmxNkRks91/FCchxBGww=="], - "@hraness/local-custody": ["@hraness/local-custody@https://github.com/hraness/local-custody/releases/download/v0.9.0/hraness-local-custody-0.9.0.tgz", {}, "sha512-f9hmxozyIrP45lfTfVE1dXjiHkWyluxuxn7SY5cDybgnvhAXXPKSiiXT4C6+Qpf97ypuaLgKdKxrvkgNWeEyYw=="], + "@hraness/local-custody": ["@hraness/local-custody@https://github.com/hraness/local-custody/releases/download/v0.9.1/hraness-local-custody-0.9.1.tgz", {}, "sha512-Zx6P4HaQ5zIdvuJxgQtgyfvTOQYr7NbmdFPBk307qUUb1nQAxwrW9S4b0TheOJWIkMwnp6F7ED42ftl6xre18w=="], "@hraness/message-like-me": ["@hraness/message-like-me@github:hraness/textbutler#83453cc", { "bin": { "messagelikeme": "./dist/cli.js" } }, "hraness-textbutler-83453cc", "sha512-hMz3GrVAAl+QeNsbaRhz5PSAN5dUwT98ihpM3QZrK1k9xZnO2avuoALLBcQs4CHSofwduGC+z6HtNmF++TGMxA=="], diff --git a/package.json b/package.json index b7a8fb20..e109d66c 100644 --- a/package.json +++ b/package.json @@ -688,7 +688,7 @@ "dependencies": { "@hraness/accounts-cli": "github:hraness/accounts-cli#v0.1.3", "@hraness/wordcell": "https://github.com/hraness/wordcell/releases/download/v0.24.0/hraness-wordcell-0.24.0.tgz", - "@hraness/local-custody": "https://github.com/hraness/local-custody/releases/download/v0.9.0/hraness-local-custody-0.9.0.tgz", + "@hraness/local-custody": "https://github.com/hraness/local-custody/releases/download/v0.9.1/hraness-local-custody-0.9.1.tgz", "@hraness/message-like-me": "github:hraness/textbutler#83453cc7c17b49bb53fdfd89ccb69b8b44b30af1", "@hraness/support-foundation": "github:hraness/support-foundation#8bb514d24b79dc3f305390700ae312cab88e7ad2", "buffer-from": "1.1.2", diff --git a/scripts/npm-release-workflow.test.ts b/scripts/npm-release-workflow.test.ts index eeb1cb99..18c13bb0 100644 --- a/scripts/npm-release-workflow.test.ts +++ b/scripts/npm-release-workflow.test.ts @@ -1220,7 +1220,7 @@ describe("npm publication contract", () => { (MAX_UNPACKED_BYTES + MAX_PACKED_ENTRIES * 1_023 + 1_024) / 512, ) * 512, ); - expect(MAX_PACKAGE_TAR_BYTES).toBe(24_573_440); + expect(MAX_PACKAGE_TAR_BYTES).toBe(24_574_464); expect(MAX_PACKAGE_TAR_BYTES % 512).toBe(0); expect(artifact).toContain("maxOutputLength: MAX_PACKAGE_TAR_BYTES"); expect(artifact).not.toContain("const maximumTarBytes"); @@ -1424,10 +1424,11 @@ describe("npm publication contract", () => { expect(budget).toContain("90ef33da70559db4674510c401466fe74a240967f8d719a4cfb3f9e012af4227"); expect(budget).toContain("00ead58f3e0268855e0face59da2460faa723c68c18e19542c37fd891cd4431f"); expect(budget).toContain("81b82626d55fcc0ef960ac59c3dfc0e90ed6417756d614b00796d5c4122b5072"); + expect(budget).toContain("d5681ab13f0bc005bcd4bbf4b18152de887c062e7e8be3d0ecc28f14e11177d5"); expect(Object.isFrozen(repairPackageMeasurement)).toBeTrue(); expect(repairPackageMeasurement).toMatchObject({ - archiveSha256: "81b82626d55fcc0ef960ac59c3dfc0e90ed6417756d614b00796d5c4122b5072", - packedBytes: 12_125_239, unpackedBytes: 23_939_528, entryCount: 618, + archiveSha256: "d5681ab13f0bc005bcd4bbf4b18152de887c062e7e8be3d0ecc28f14e11177d5", + packedBytes: 12_125_748, unpackedBytes: 23_940_758, entryCount: 618, packedPlatformProjection: 12_387, packedPortabilityAllowance: 4_096, payloadPlatformProjection: 353, payloadAllowance: 65, }); @@ -1437,8 +1438,8 @@ describe("npm publication contract", () => { expect(budget).toContain("12,141,169 packed; 23,937,025 + 353 + 65 = 23,937,443 unpacked"); expect(budget).toContain("23,930,250 + 353 + 65 = 23,930,668 unpacked"); expect(budget).toContain("12,141,373 packed; 23,937,545 + 353 + 65 = 23,937,963 unpacked"); - expect(MAX_PACKED_BYTES).toBe(12_141_722); - expect(MAX_PACKED_BYTES).toBe(12_125_239 + 12_387 + 4_096); + expect(MAX_PACKED_BYTES).toBe(12_142_231); + expect(MAX_PACKED_BYTES).toBe(12_125_748 + 12_387 + 4_096); expect(budget).toContain("aa127b3193c9bb3b0cb5deece5927be60ccb7111a50169320d322ffdeaa13f39"); expect(budget).toContain("0c331bab3ab3df69a108e18f5f29845b0db90c281cbd6455c0d90fa0b24081e2"); expect(budget).toContain("873cad8139fda303e2d19c6afd61cf549cf9b4d1d76b2a1d6d632a6afe6bd0d1"); @@ -1559,7 +1560,7 @@ describe("npm publication contract", () => { expect(budget).toContain("47684b3e2eb5cf3ed07fbb520aade8c7251d993f75262fbf1af627d9081a1a5f"); expect(budget).toContain("23,688,277 + 353 + 65 = 23,688,695"); expect(budget).toContain("23,759,283 + 353 + 65 = 23,759,701"); - expect(MAX_UNPACKED_BYTES).toBe(23_939_946); + expect(MAX_UNPACKED_BYTES).toBe(23_941_176); expect(budget).toContain("23,037,873 + 65 = 23,037,938"); expect(budget).toContain("f9f3ab38a682690ceaa2699a7309997512030f0fa500a9dc29dcd108123dc41f"); expect(budget).toContain("23,038,557 + 65 = 23,038,622"); @@ -1592,7 +1593,7 @@ describe("npm publication contract", () => { expect(budget).toContain("01875f12ab73a49d6c7d6bf520dc3d318db816addee2fa7981889f35c958cf7c"); expect(budget).toContain("b12909f08f7c19460ced56e30619f4860a1183f4b0106170c07837dae577a937"); expect(budget).toContain("0b212ac291218528dcf979370110a36f10850e046ca90a536057d9a44e807d1d"); - expect(MAX_UNPACKED_BYTES).toBe(23_939_528 + 353 + 65); + expect(MAX_UNPACKED_BYTES).toBe(23_940_758 + 353 + 65); expect(budget).toContain("22,794,052 + 65 = 22,794,117"); expect(budget).toContain("c482efe748f880e3717727d6d39fd92a68953e6eea766642b329ba47ae772d80"); expect(budget).toContain("22,759,423 + 65 = 22,759,488"); @@ -1628,8 +1629,8 @@ describe("npm publication contract", () => { expect(packageArtifactBudget).toEqual({ entryCount: { min: 618, max: 618 }, fileCount: { min: 618, max: 618 }, - packedBytes: { min: 1_600_000, max: 12_141_722 }, - unpackedBytes: { min: 9_000_000, max: 23_939_946 }, + packedBytes: { min: 1_600_000, max: 12_142_231 }, + unpackedBytes: { min: 9_000_000, max: 23_941_176 }, }); }); diff --git a/scripts/package-budget.ts b/scripts/package-budget.ts index 3c44ede5..ecbce43b 100644 --- a/scripts/package-budget.ts +++ b/scripts/package-budget.ts @@ -2065,14 +2065,32 @@ // Retain the same platform projections and allowances: // 12,125,239 + 12,387 + 4,096 = 12,141,722 packed; // 23,939,528 + 353 + 65 = 23,939,946 unpacked. +// +// Strict packaged-source compatibility for `erasableSyntaxOnly` +// consumers removes the remaining constructor parameter property from +// src/cookie-safe-storage.ts, keeps the persistent-helper environment +// typed with literal NODE_ENV values so narrowed consumer ProcessEnv +// unions still admit it, builds RequestInit bodies without explicit +// undefined under exactOptionalPropertyTypes, teaches the package smoke +// to compile the packed public entrypoints with the strict consumer +// flags, and repins @hraness/local-custody to the immutable 0.9.1 +// release archive: three changed packed sources plus their rebuilt +// bundles over the unchanged 618-file inventory. After `bun run build`, +// a clean npm 11.19.0 pack --ignore-scripts with Node 24.18.1 on darwin +// arm64 measured 618 entries, 12,125,748 packed bytes, and 23,940,758 +// unpacked bytes; archive SHA-256 +// d5681ab13f0bc005bcd4bbf4b18152de887c062e7e8be3d0ecc28f14e11177d5. +// Retain the same platform projections and allowances: +// 12,125,748 + 12,387 + 4,096 = 12,142,231 packed; +// 23,940,758 + 353 + 65 = 23,941,176 unpacked. export const repairPackageMeasurement = Object.freeze({ - scope: "Wordcell 0.24.0 runtime adoption over @hraness/kb 0.19.6 with split dynamic-resolution review pins", + scope: "erasableSyntaxOnly packaged-source compatibility and @hraness/local-custody 0.9.1 pin", command: "npm pack --ignore-scripts", npmVersion: "11.19.0", platform: "darwin-arm64", - archiveSha256: "81b82626d55fcc0ef960ac59c3dfc0e90ed6417756d614b00796d5c4122b5072", - packedBytes: 12_125_239, - unpackedBytes: 23_939_528, + archiveSha256: "d5681ab13f0bc005bcd4bbf4b18152de887c062e7e8be3d0ecc28f14e11177d5", + packedBytes: 12_125_748, + unpackedBytes: 23_940_758, entryCount: 618, packedPlatformProjection: 12_387, packedPortabilityAllowance: 4_096, diff --git a/scripts/package-smoke.ts b/scripts/package-smoke.ts index a8b30c47..aedaaab8 100644 --- a/scripts/package-smoke.ts +++ b/scripts/package-smoke.ts @@ -989,7 +989,11 @@ void [ parseMessagingRoutesV2, ]; `); - await writeFile(join(consumer, "tsconfig.bundler.json"), "{\n \"compilerOptions\": {\n \"target\": \"ES2023\",\n \"lib\": [\n \"ES2023\",\n \"DOM\",\n \"DOM.Iterable\"\n ],\n \"types\": [\n \"bun\",\n \"node\"\n ],\n \"strict\": true,\n \"noEmit\": true,\n \"skipLibCheck\": false,\n \"module\": \"Preserve\",\n \"moduleResolution\": \"Bundler\"\n },\n \"include\": [\n \"index.ts\"\n ]\n}"); + // Strict consumers narrow NodeJS.ProcessEnv.NODE_ENV to the literal union + // (for example a Next.js ambient declaration), so packaged sources must keep + // NODE_ENV assignments literal instead of widening to string. + await writeFile(join(consumer, "process-env.d.ts"), "declare global {\n namespace NodeJS {\n interface ProcessEnv {\n readonly NODE_ENV: \"development\" | \"test\" | \"production\";\n }\n }\n}\n\nexport {};\n"); + await writeFile(join(consumer, "tsconfig.bundler.json"), "{\n \"compilerOptions\": {\n \"target\": \"ES2023\",\n \"lib\": [\n \"ES2023\",\n \"DOM\",\n \"DOM.Iterable\"\n ],\n \"types\": [\n \"bun\",\n \"node\"\n ],\n \"strict\": true,\n \"noUncheckedIndexedAccess\": true,\n \"exactOptionalPropertyTypes\": true,\n \"noImplicitOverride\": true,\n \"noImplicitReturns\": true,\n \"noFallthroughCasesInSwitch\": true,\n \"useUnknownInCatchVariables\": true,\n \"erasableSyntaxOnly\": true,\n \"verbatimModuleSyntax\": true,\n \"isolatedModules\": true,\n \"moduleDetection\": \"force\",\n \"allowImportingTsExtensions\": true,\n \"noEmit\": true,\n \"skipLibCheck\": false,\n \"module\": \"Preserve\",\n \"moduleResolution\": \"Bundler\"\n },\n \"include\": [\n \"index.ts\",\n \"process-env.d.ts\"\n ]\n}"); await logConsumerToolchain(consumer); await run([process.execPath, "x", "tsc", "-p", "./tsconfig.bundler.json"], consumer); diff --git a/scripts/standalone-smoke.ts b/scripts/standalone-smoke.ts index aab49a4d..c54bf385 100644 --- a/scripts/standalone-smoke.ts +++ b/scripts/standalone-smoke.ts @@ -29,7 +29,7 @@ const expectedClosureRuntimeDependencies = Object.freeze({ "@hraness/accounts-cli": "github:hraness/accounts-cli#v0.1.3", "@hraness/desktop-foundation": "https://github.com/hraness/desktop-foundation/releases/download/v0.8.0/hraness-desktop-foundation-0.8.0.tgz", "@hraness/wordcell": "https://github.com/hraness/wordcell/releases/download/v0.24.0/hraness-wordcell-0.24.0.tgz", - "@hraness/local-custody": "https://github.com/hraness/local-custody/releases/download/v0.9.0/hraness-local-custody-0.9.0.tgz", + "@hraness/local-custody": "https://github.com/hraness/local-custody/releases/download/v0.9.1/hraness-local-custody-0.9.1.tgz", "@hraness/message-like-me": "github:hraness/textbutler#83453cc7c17b49bb53fdfd89ccb69b8b44b30af1", "@hraness/support-foundation": "github:hraness/support-foundation#8bb514d24b79dc3f305390700ae312cab88e7ad2", // The browser cookie reader, also pinned by @hraness/wordcell. Ghostget wraps it @@ -886,7 +886,7 @@ try { root: installedLocalCustodyRoot, sha256: "8266fec81ffa715f1edc845c022a35939547d093b6670b6b36a137b90cf6e88b", - version: "0.9.0", + version: "0.9.1", }), assertInstalledClosurePackage({ keyFile: "dist/atomic-publish.js", @@ -894,9 +894,9 @@ try { root: installedLocalCustodyRoot, sha256: "f74453ce9bb37e71996c8763a05b3d299f0e60a25a0a7710f6c30c24fd832177", - version: "0.9.0", + version: "0.9.1", }), - // local-custody v0.9.0 stages the Rust sidecar and its per-target + // local-custody v0.9.1 stages the Rust sidecar and its per-target // manifest inside the published tarball; admission pins each staged // byte-exact file the same way as the JS entry points. assertInstalledClosurePackage({ @@ -904,8 +904,8 @@ try { name: "@hraness/local-custody", root: installedLocalCustodyRoot, sha256: - "81eec495cae2314a66acb023c4742b34bd8d2f522a7b3567c1500f02ecab717c", - version: "0.9.0", + "00f47282e5e9f51343ec5e0c63cb0faa3019f8352e6d0709b3b062ae3a217076", + version: "0.9.1", }), assertInstalledClosurePackage({ keyFile: @@ -913,8 +913,8 @@ try { name: "@hraness/local-custody", root: installedLocalCustodyRoot, sha256: - "0eda257b175120daf5eb85766bd59e347c0a2d94f1ccfa07d2056ad0f0f35f7e", - version: "0.9.0", + "8a26839249060eb7822d79be39a3399b16e74250f4f289b73bfee122f5301b6a", + version: "0.9.1", }), assertInstalledClosurePackage({ keyFile: @@ -922,8 +922,8 @@ try { name: "@hraness/local-custody", root: installedLocalCustodyRoot, sha256: - "0a3ccd963067837df7c433246f20417cdb2e4b31e8bc161f6d7aa799943c7693", - version: "0.9.0", + "f0f2cfa818073ebdb03eafa3acad48bac2bbbe1a0d14a6d6814c6c4bd6f55d57", + version: "0.9.1", }), assertInstalledClosurePackage({ keyFile: @@ -932,7 +932,7 @@ try { root: installedLocalCustodyRoot, sha256: "eb445c00af1ec82ed220e01a7c3161f33e4ab8e08e2276a6fcc61f38382608ca", - version: "0.9.0", + version: "0.9.1", }), assertInstalledClosurePackage({ keyFile: @@ -941,7 +941,7 @@ try { root: installedLocalCustodyRoot, sha256: "35de4d107e30da2440fe1ac926581fd18fdf130d80c02e95694e23993e5d982d", - version: "0.9.0", + version: "0.9.1", }), assertInstalledClosurePackage({ keyFile: "dist/message-bundle-v1.js", diff --git a/src/cookie-safe-storage.ts b/src/cookie-safe-storage.ts index 4d5c81c3..0b81f550 100644 --- a/src/cookie-safe-storage.ts +++ b/src/cookie-safe-storage.ts @@ -107,12 +107,14 @@ const APP_RESULT_FRAME_BYTES = 64 * 1024; export class LocalAppError extends Error { override readonly name = "LocalAppError"; + readonly code: "identity-unavailable" | "assemble-failed" | "helper-mismatch" | "runner-failed"; constructor( - readonly code: "identity-unavailable" | "assemble-failed" | "helper-mismatch" | "runner-failed", + code: "identity-unavailable" | "assemble-failed" | "helper-mismatch" | "runner-failed", message: string, options?: { readonly cause?: unknown }, ) { super(message, options?.cause === undefined ? undefined : { cause: options.cause }); + this.code = code; } } diff --git a/src/provider-plugin-portable-runtime.ts b/src/provider-plugin-portable-runtime.ts index e9b35673..1c787cd9 100644 --- a/src/provider-plugin-portable-runtime.ts +++ b/src/provider-plugin-portable-runtime.ts @@ -1045,9 +1045,12 @@ function capabilityHost(options: { { method: request.method, headers, + // A defined body stays literal for exactOptionalPropertyTypes + // consumers: RequestInit["body"] takes BodyInit | null, never + // an explicit undefined. ...(request.body.kind === "none" ? {} - : { body: bodyBytes(request.body) }), + : { body: bodyBytes(request.body) ?? null }), redirect: "error", signal: requestDeadline.signal, }, diff --git a/src/storage.ts b/src/storage.ts index a2cb596c..c3c0305d 100644 --- a/src/storage.ts +++ b/src/storage.ts @@ -139,9 +139,12 @@ const pathHelperPath = join(dirname(fileURLToPath(import.meta.url)), "path-helpe const stateCrashPlanForTest = process.env.NODE_ENV === "test" ? process.env.GHOSTGET_TEST_STATE_CRASH_PLAN : undefined; +// NODE_ENV must stay a literal: consumers whose ambient types narrow +// NodeJS.ProcessEnv.NODE_ENV to "development" | "test" | "production" reject a +// widened string on the spawn options env field. const helperEnvironment = stateCrashPlanForTest === undefined - ? { NODE_ENV: "production" } - : { NODE_ENV: "test", GHOSTGET_TEST_STATE_CRASH_PLAN: stateCrashPlanForTest }; + ? { NODE_ENV: "production" as const } + : { NODE_ENV: "test" as const, GHOSTGET_TEST_STATE_CRASH_PLAN: stateCrashPlanForTest }; const helperPreloadForTest: readonly string[] = stateCrashPlanForTest === undefined ? [] : ["--preload", join(dirname(fileURLToPath(import.meta.url)), "state-crash-preload.test-support.ts")];