@jimhoyd/urlcode 0.4.0-alpha.2 → 0.4.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.
Files changed (147) hide show
  1. package/.claude/skills/urlcode-authoring/SKILL.md +17 -19
  2. package/.claude/skills/urlcode-operations/SKILL.md +9 -9
  3. package/.claude-plugin/marketplace.json +1 -1
  4. package/CONTRIBUTING.md +20 -16
  5. package/README.md +59 -64
  6. package/ROADMAP.md +66 -376
  7. package/dist/BUILD-MANIFEST.json +26 -25
  8. package/dist/agents-guide.js +6 -6
  9. package/dist/authoring.js +15 -1
  10. package/dist/build-static.js +2 -0
  11. package/dist/capability-query.js +0 -1
  12. package/dist/catalog.js +0 -1
  13. package/dist/cli.js +25 -9
  14. package/dist/config.js +1 -1
  15. package/dist/explain-cli.js +4 -2
  16. package/dist/explain.js +8 -2
  17. package/dist/extensions.js +1 -1
  18. package/dist/http-response.js +1 -1
  19. package/dist/index.js +1 -0
  20. package/dist/init-with.js +36 -11
  21. package/dist/manifest.js +8 -2
  22. package/dist/mcp-authoring.js +4 -4
  23. package/dist/mcp.js +1 -1
  24. package/dist/policies/cache.js +2 -2
  25. package/dist/policies.js +3 -1
  26. package/dist/prerender.js +4 -0
  27. package/dist/project-dependencies.js +305 -0
  28. package/dist/readiness.js +3 -0
  29. package/dist/route-diff.js +12 -5
  30. package/dist/runtime.js +1 -1
  31. package/dist/trusted-functions.js +4 -5
  32. package/dist/types/authoring.d.ts +9 -1
  33. package/dist/types/capability-query.d.ts +0 -1
  34. package/dist/types/catalog.d.ts +0 -4
  35. package/dist/types/config.d.ts +1 -9
  36. package/dist/types/explain.d.ts +6 -1
  37. package/dist/types/extensions.d.ts +1 -1
  38. package/dist/types/http-response.d.ts +0 -1
  39. package/dist/types/index.d.ts +1 -0
  40. package/dist/types/init-with.d.ts +7 -13
  41. package/dist/types/manifest.d.ts +5 -2
  42. package/dist/types/project-dependencies.d.ts +78 -0
  43. package/dist/types/readiness.d.ts +2 -0
  44. package/dist/types/trusted-functions.d.ts +1 -4
  45. package/dist/types/types.d.ts +8 -1
  46. package/dist/types.js +8 -1
  47. package/dist/typescript-authoring.js +18 -7
  48. package/docs/AI-AUTHORING.md +15 -7
  49. package/docs/ASSETS.md +2 -1
  50. package/docs/AUTH-BACKUP.md +32 -0
  51. package/docs/AWS.md +9 -0
  52. package/docs/BEST-PRACTICES.md +17 -9
  53. package/docs/CAPABILITIES.md +1 -1
  54. package/docs/CI-FOLLOWUP-2026-09-19.md +97 -0
  55. package/docs/CI-RELEASE-AUDIT-2026-09-19.md +322 -0
  56. package/docs/CI.md +8 -3
  57. package/docs/CODEBASE-AUDIT-2026-09-20.md +284 -0
  58. package/docs/COMPOSING-A-SITE.md +278 -0
  59. package/docs/DEVELOPMENT-PIPELINE.md +270 -0
  60. package/docs/EXTENSIONS.md +51 -11
  61. package/docs/FRAMEWORK.md +67 -48
  62. package/docs/FUNCTION-SECURITY.md +44 -0
  63. package/docs/INSTALL.md +13 -8
  64. package/docs/LOCAL-DEVELOPMENT.md +3 -0
  65. package/docs/MIDDLEWARE.md +10 -4
  66. package/docs/OPEN-DECISIONS.md +224 -212
  67. package/docs/OPERATIONAL-PROOF.md +3 -3
  68. package/docs/OPERATIONS.md +3 -3
  69. package/docs/POLICIES.md +13 -5
  70. package/docs/PRERENDER.md +23 -11
  71. package/docs/PROJECT-DIRECTION.md +3 -3
  72. package/docs/READINESS.md +10 -5
  73. package/docs/README.md +20 -44
  74. package/docs/RELEASE-0.4.0-alpha.3.md +50 -0
  75. package/docs/RELEASE-0.4.1.md +73 -0
  76. package/docs/RELEASE-READINESS.md +6 -6
  77. package/docs/RELEASE-SECURITY.md +96 -181
  78. package/docs/RESILIENCE.md +3 -3
  79. package/docs/ROUTING.md +3 -4
  80. package/docs/SECURITY-AUDIT.md +2 -2
  81. package/docs/SPECIFICATION.md +12 -6
  82. package/docs/SPIKE-AI-FRAMEWORK-BENCHMARK.md +6 -5
  83. package/docs/SPIKE-BUSINESS-SUITE.md +14 -6
  84. package/docs/SPIKE-CORE-LAYERING.md +49 -18
  85. package/docs/SPIKE-DEFAULT-TRUST-MODEL.md +7 -5
  86. package/docs/STARTERS.md +17 -5
  87. package/docs/STATIC.md +14 -3
  88. package/docs/TOOLING.md +10 -7
  89. package/docs/TYPESCRIPT-AUTHORING.md +6 -1
  90. package/docs/VERCEL.md +10 -2
  91. package/docs/VERSION-ALIGNMENT.md +76 -201
  92. package/docs/archive/2026-09-19/EXTENSION-IMPLEMENTATION.md +68 -0
  93. package/docs/archive/2026-09-19/MANAGEMENT-SECURITY.md +102 -0
  94. package/docs/{NEXT-PHASE-PLAN.md → archive/2026-09-19/NEXT-PHASE-PLAN.md} +15 -5
  95. package/docs/{NEXT-STEPS.md → archive/2026-09-19/NEXT-STEPS.md} +15 -3
  96. package/docs/archive/2026-09-19/OPEN-DECISIONS.md +277 -0
  97. package/docs/archive/2026-09-19/RELEASE-SECURITY.md +186 -0
  98. package/docs/archive/2026-09-19/ROADMAP.md +387 -0
  99. package/docs/{SPIKE-EXTENSION-MODEL.md → archive/2026-09-19/SPIKE-EXTENSION-MODEL.md} +11 -0
  100. package/docs/{SPIKE-EXTENSIONS.md → archive/2026-09-19/SPIKE-EXTENSIONS.md} +25 -14
  101. package/docs/archive/2026-09-19/SPIKE-LAMBDA-COMPILE.md +365 -0
  102. package/docs/archive/2026-09-19/SPIKE-MONOREPO.md +778 -0
  103. package/docs/{USABILITY-REVIEW.md → archive/2026-09-19/USABILITY-REVIEW.md} +12 -2
  104. package/docs/archive/README.md +28 -0
  105. package/docs/policies/agents.md +1 -1
  106. package/docs/policies/compression.md +3 -2
  107. package/docs/policies/security.md +3 -2
  108. package/docs/yaml/functions.md +10 -2
  109. package/docs/yaml/middleware.md +5 -3
  110. package/examples/assets/example.yaml +1 -1
  111. package/examples/cookbook/middleware/envelope.mjs +4 -2
  112. package/examples/cookbook/route-index.json +1 -1
  113. package/examples/cookbook/routes/middleware.yaml +1 -1
  114. package/examples/prerender/README.md +14 -6
  115. package/examples/prerender/functions/page.mjs +4 -2
  116. package/examples/prerender/middleware/template.mjs +1 -1
  117. package/examples/prerender/prerender.mjs +1 -1
  118. package/examples/prerender/urlcode.yaml +8 -4
  119. package/llms-full.txt +503 -88
  120. package/llms.txt +6 -4
  121. package/package.json +27 -4
  122. package/packaging/claude-plugin/.claude-plugin/plugin.json +2 -2
  123. package/packaging/claude-plugin/skills/urlcode-authoring/SKILL.md +17 -19
  124. package/packaging/claude-plugin/skills/urlcode-operations/SKILL.md +9 -9
  125. package/recipes/authenticated-json-api/README.md +4 -3
  126. package/recipes/authenticated-json-api/functions/profile.mjs +2 -1
  127. package/recipes/authenticated-json-api/recipe.yaml +1 -1
  128. package/recipes/contact-form/functions/contact.mjs +2 -1
  129. package/recipes/contact-form/recipe.yaml +2 -2
  130. package/recipes/cors-api/README.md +2 -2
  131. package/recipes/cors-api/recipe.yaml +1 -1
  132. package/recipes/health-page/README.md +1 -1
  133. package/recipes/json-api/README.md +1 -1
  134. package/recipes/json-api/recipe.yaml +3 -3
  135. package/recipes/middleware/README.md +8 -4
  136. package/recipes/middleware/middleware/envelope.mjs +4 -2
  137. package/recipes/protected-download/README.md +1 -1
  138. package/recipes/protected-download/recipe.yaml +1 -1
  139. package/recipes/static-plus-api/README.md +2 -2
  140. package/recipes/static-plus-api/public/index.html +1 -1
  141. package/recipes/static-plus-api/recipe.yaml +1 -1
  142. package/recipes/static-plus-api/urlcode.yaml +1 -1
  143. package/recipes/typescript/recipe.yaml +4 -4
  144. package/skills/urlcode/SKILL.md +6 -6
  145. package/starters/default/AGENTS.md +6 -6
  146. package/docs/SPIKE-LAMBDA-COMPILE.md +0 -201
  147. package/docs/SPIKE-MONOREPO.md +0 -322
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Exact dependency pins for a generated application.
3
+ *
4
+ * `urlcode init --with` resolves whatever `@jimhoyd/urlcode-<name>` packages are already installed beside the
5
+ * invoking directory. Without a manifest the generated site records nothing about which versions it was built
6
+ * against, so a later `npm install @jimhoyd/urlcode-auth` in that site can resolve a different set (#212). This
7
+ * module reads the versions that were actually resolved, validates the whole set against the packages' own
8
+ * declared `peerDependencies`, and renders a `package.json` pinning every one of them exactly.
9
+ *
10
+ * It never runs a package manager: generating a lockfile stays an explicit `npm install` the operator runs after
11
+ * reviewing the manifest. It also never imports an extension implementation -- only package metadata is read, by
12
+ * a name the caller supplied -- so core's generic extension boundary is unchanged.
13
+ */
14
+ export declare const CORE_PACKAGE = "@jimhoyd/urlcode";
15
+ interface Version {
16
+ major: number;
17
+ minor: number;
18
+ patch: number;
19
+ pre: readonly (string | number)[];
20
+ }
21
+ export declare function parseVersion(value: string): Version | null;
22
+ export declare function compareVersions(a: Version, b: Version): number;
23
+ export declare function satisfiesRange(version: string, range: string, context?: string): boolean;
24
+ export interface InstalledPackage {
25
+ name: string;
26
+ version: string;
27
+ directory: string;
28
+ peers: Record<string, string>;
29
+ optionalPeers: ReadonlySet<string>;
30
+ node: string | undefined;
31
+ }
32
+ /**
33
+ * Walks `node_modules` upwards from the invoking directory, exactly like Node's own resolution but reading the
34
+ * package's manifest rather than its entry point. Reading the manifest directly (instead of resolving the entry)
35
+ * means a package whose `exports` does not expose `./package.json` is still inspectable, and nothing in the
36
+ * package is loaded or executed.
37
+ */
38
+ export declare function findInstalledPackage(name: string, from: string): Promise<InstalledPackage | null>;
39
+ /** The version of the runtime executing this command; that is the version a generated site is pinned to. */
40
+ export declare function runningCore(): Promise<InstalledPackage>;
41
+ export interface DependencyPin {
42
+ name: string;
43
+ version: string;
44
+ specifier: string;
45
+ /** True when the specifier is a local path or tarball rather than a registry version. */
46
+ local: boolean;
47
+ /** `runtime` is core, `extension` was named in --with, `peer` was pulled in by a package's peerDependencies. */
48
+ role: 'runtime' | 'extension' | 'peer';
49
+ }
50
+ export interface DependencySet {
51
+ pins: DependencyPin[];
52
+ /** Exactly what goes into the generated `dependencies` block, sorted by name. */
53
+ dependencies: Record<string, string>;
54
+ /** Highest recognized `engines.node` floor across the set, or undefined when none was expressed as `>=x.y.z`. */
55
+ node: string | undefined;
56
+ /** True when any pin points at a local path or tarball. */
57
+ local: boolean;
58
+ }
59
+ export interface DependencyOptions {
60
+ cwd?: string | undefined;
61
+ /** `--pin <package>=<specifier>`: an operator-chosen specifier, for local tarballs and mirrors. */
62
+ overrides?: ReadonlyMap<string, string> | undefined;
63
+ }
64
+ export declare function parsePin(value: string): [string, string];
65
+ /**
66
+ * Resolves core plus every named extension and their declared peers, then validates the whole set against every
67
+ * declared peer range before returning. Compatibility is judged as a set: a mismatch anywhere refuses, listing
68
+ * every mismatch rather than the first.
69
+ */
70
+ export declare function collectDependencySet(names: readonly string[], packageNames: readonly string[], { cwd, overrides }?: DependencyOptions): Promise<DependencySet>;
71
+ /** The generated manifest: private, module type, exact pins, and nothing that runs a package manager. */
72
+ export declare function renderPackageManifest(directory: string, set: DependencySet): string;
73
+ /**
74
+ * The install step is printed, never run: generating `package-lock.json` executes a package manager, which
75
+ * resolves and downloads code, so it stays the operator's explicit action after reviewing the manifest.
76
+ */
77
+ export declare function installSteps(directory: string, set: DependencySet): string[];
78
+ export {};
@@ -8,6 +8,8 @@ export type HandlerName = 'extension' | 'proxy' | 'conditional' | 'redirect' | '
8
8
  /** One configured route as the inventory reports it: a PlanInventoryEntry with the handler kind named. */
9
9
  export interface RouteInventory extends PlanInventoryEntry {
10
10
  handler: HandlerName | undefined;
11
+ /** Always reported here, so a trust change is visible in `routes` and its diff. */
12
+ sandbox: boolean;
11
13
  /** Non-blocking `audit` observations about this route (e.g. a webhook-shaped
12
14
  * route with no declared `sandbox`/`sandboxReason`); never affects `ready`. */
13
15
  advisories?: string[];
@@ -2,11 +2,9 @@ import type { FunctionRoute } from './function-sources.ts';
2
2
  import type { FunctionContext, FunctionResult } from './functions.ts';
3
3
  import type { GuestRequestPayload } from './guest-api.ts';
4
4
  import type { HandlerResult } from './http-response.ts';
5
- import type { LogFn } from './types.ts';
6
5
  export interface TrustedFunctionsOptions {
7
6
  timeoutMs?: number | undefined;
8
7
  maxBytes?: number | undefined;
9
- log?: LogFn | undefined;
10
8
  }
11
9
  interface TrustedDefinition {
12
10
  source: string;
@@ -16,9 +14,8 @@ export type TrustedRoute = FunctionRoute<TrustedDefinition>;
16
14
  export declare class TrustedFunctions {
17
15
  timeoutMs: number;
18
16
  maxBytes: number;
19
- log: LogFn;
20
17
  private readonly epoch;
21
- constructor({ timeoutMs, maxBytes, log }?: TrustedFunctionsOptions);
18
+ constructor({ timeoutMs, maxBytes }?: TrustedFunctionsOptions);
22
19
  start(routes: TrustedRoute[]): Promise<this>;
23
20
  private loadExport;
24
21
  execute(route: TrustedRoute, request: GuestRequestPayload, context: FunctionContext, native: HandlerResult | undefined): Promise<FunctionResult>;
@@ -378,7 +378,7 @@ export type PolicyChain = {
378
378
  describe: PolicyInventory;
379
379
  } & Partial<PolicyStates>;
380
380
  export type RouteState = 'active' | 'disabled' | 'expired';
381
- /** One route in the audit inventory: its handler kind, methods and lifecycle state. */
381
+ /** One route in the audit inventory: its handler kind, methods, execution mode and lifecycle state. */
382
382
  export interface PlanInventoryEntry {
383
383
  path: string;
384
384
  handler: string | undefined;
@@ -387,6 +387,13 @@ export interface PlanInventoryEntry {
387
387
  policies: string[];
388
388
  generated?: string;
389
389
  state: RouteState;
390
+ /** The route's execution mode: `true` when its `function`/`middleware` chain runs in the
391
+ * QuickJS sandbox, `false` when it runs trusted in-process. Route-level, because the mode
392
+ * applies to the whole chain — a native handler with `middleware` has one too. Optional
393
+ * only at the report-parsing boundary: reports written before it existed omit it. */
394
+ sandbox?: boolean;
395
+ /** The route's declared `sandboxReason`, when it has one. */
396
+ sandboxReason?: string;
390
397
  }
391
398
  export interface TestPlan {
392
399
  inventory: PlanInventoryEntry[];
package/dist/types.js CHANGED
@@ -199,9 +199,16 @@
199
199
  // consume it need not import the host module that produces it.
200
200
 
201
201
 
202
- /** One route in the audit inventory: its handler kind, methods and lifecycle state. */
202
+ /** One route in the audit inventory: its handler kind, methods, execution mode and lifecycle state. */
203
203
 
204
204
 
205
+
206
+
207
+
208
+
209
+
210
+
211
+
205
212
 
206
213
 
207
214
 
@@ -23,16 +23,20 @@ const SANDBOX_MODULE_LIMIT=128,SANDBOX_MODULE_BYTE_LIMIT=1048576,SANDBOX_TOTAL_B
23
23
  export async function buildTypeScriptProject(project ,output ,{dryRun=false} ={}) {
24
24
  const root=await realpath(project),loaded=await loadDocument(root),files=new Map (),modules=new Map ();
25
25
  const {default:ts}=await import('typescript');
26
- // Per-path trust, so a module visited once (recursively, from its importer)
27
- // is never reprocessed under a different trust level.
28
- const moduleTrust=new Map ();
26
+ // Which trust levels a module has already been visited under, so a graph
27
+ // reachable from both a sandboxed and a trusted route is validated under each
28
+ // route's own rules exactly once instead of being refused outright. The
29
+ // module is still emitted once; `emittedBy` keeps target-name collisions
30
+ // between *different* sources refused.
31
+ const moduleTrust=new Map (),emittedBy=new Map ();
29
32
  let sandboxedSourceBytes=0,sandboxedModuleCount=0,assetBytes=0;
30
33
  await init;
31
34
  async function collect(path ,sandboxed ) {
32
35
  authoringPath(path);assert(/\.(?:ts|js|mjs)$/.test(path) && !path.endsWith('.d.ts'),'Guest source must be .ts, .js or .mjs');
33
- const seen=moduleTrust.get(path);
34
- if(seen!==undefined){assert(seen===sandboxed,'Module is imported by both a sandboxed and a trusted route');return;}
35
- moduleTrust.set(path,sandboxed);
36
+ let visited=moduleTrust.get(path);
37
+ if(!visited){visited=new Set();moduleTrust.set(path,visited);}
38
+ if(visited.has(sandboxed))return;
39
+ visited.add(sandboxed);
36
40
  let bytes ;
37
41
  if(sandboxed){
38
42
  assert(sandboxedModuleCount<SANDBOX_MODULE_LIMIT,'Function module limit exceeded');sandboxedModuleCount++;
@@ -93,7 +97,14 @@ export async function buildTypeScriptProject(project ,output ,{dry
93
97
  }
94
98
  }
95
99
  for(const edit of edits.sort((a,b)=>b.start-a.start))code=code.slice(0,edit.start)+edit.value+code.slice(edit.end);
96
- const target=emitted(path);assert(!files.has(target),'Guest output module collision');files.set(target,code);
100
+ // A module reachable from both trust levels is transpiled twice (once per
101
+ // rule set) but emitted once. The two results agree: the sandbox rules are
102
+ // strictly stricter, so anything the trusted pass would leave alone —
103
+ // bare/dynamic specifiers, `import.meta` — fails the sandboxed pass and
104
+ // aborts the whole build before either emission is published.
105
+ const target=emitted(path),previous=emittedBy.get(target);
106
+ assert(previous===undefined||previous===path,'Guest output module collision');
107
+ emittedBy.set(target,path);files.set(target,code);
97
108
  }
98
109
  for(const route of Object.values(loaded.routes))for(const definition of routeFunctions(route)){
99
110
  await collect(definition.source,!!route.sandbox);definition.source=emitted(definition.source);
@@ -25,7 +25,11 @@ fields or bypass target limits or operator grants. See [the design principle](PR
25
25
  6. [Readiness](READINESS.md), [capacity](CAPACITY.md), [DDoS/recovery](RESILIENCE.md).
26
26
  7. [The framework](FRAMEWORK.md) for accounts, administration and presentation:
27
27
  `extensions.<name>` blocks and `extension` mounts are the only YAML those
28
- packages need; their configuration is documented in their own repositories.
28
+ packages need. [Composing a site](COMPOSING-A-SITE.md) is the map of what a
29
+ consumer may then change: the `config` each package accepts, the
30
+ presentation overrides under `ui/`, the project functions its lifecycle
31
+ hooks call, and when a requirement instead needs a new extension in
32
+ TypeScript.
29
33
 
30
34
  The root [llms.txt](../llms.txt) is a compact discovery index; the generated
31
35
  [llms-full.txt](../llms-full.txt) concatenates the authoring documents above in
@@ -106,7 +110,7 @@ The benchmark operates locally; it is not a load test of an external deployment.
106
110
  | Parameter validation and JSON body syntax checks | Full OpenAPI or JSON Schema validation of request bodies |
107
111
  | Local test/audit/benchmark | Route-local YAML tests, managed monitoring, production load certification |
108
112
  | Local/self-hosted runtime; limited AWS/Vercel/Cloudflare implementations with local tests | Verified provider deployments or full cross-provider parity |
109
- | File authoring and snapshot reload | General guest storage broker; stored short links (moving to a future `urlcode-dynamic-link` extension package, not yet published) |
113
+ | File authoring and snapshot reload | General guest storage broker; stored short links (no supported package; the `urlcode-dynamic-link` extension was retired) |
110
114
  | Optional host `policies` (`throttle`, `agents`, `security`, `compression`, `cache`) and reusable `profiles` | Plugins named in YAML, shared multi-instance counters, CORS, verified-bot checks |
111
115
  | Optional top-level `site` (`robots`, `sitemap`, `favicon`, `securityTxt`, `llms`) generating native routes | Per-route `noindex` field, sitemap index files, `humans.txt`, signed `security.txt` |
112
116
 
@@ -240,9 +244,12 @@ Put that justification where tooling can see it, not only in a source
240
244
  comment: an optional `sandboxReason` string on the route (up to 500
241
245
  characters, `schemas/urlcode.schema.json`) records why a route needs
242
246
  isolation, or why it is safe to trust, regardless of whether `sandbox` is
243
- `true` or `false`. `urlcode explain`/`context` surface it next to the
244
- route's `sandbox` boolean, so the trust decision has a reviewable trail
245
- without reading every route's source file:
247
+ `true` or `false`. `urlcode explain`/`context`, the manifest and the
248
+ `routes` inventory all surface it next to the route's `sandbox` boolean —
249
+ per route, not per handler, so a native handler that runs `middleware`
250
+ reports its execution mode too, and `routes --compare` shows a flip between
251
+ trusted and sandboxed execution as a changed route. The trust decision has a
252
+ reviewable trail without reading every route's source file:
246
253
 
247
254
  ```yaml
248
255
  routes:
@@ -323,8 +330,9 @@ trust (adding `sandbox: true` without saying why, or relying on the trusted
323
330
  default for code that plainly needed isolation) to work around them.
324
331
 
325
332
  There is no native `link` handler or `dynamicLinks` project flag; both were
326
- removed. Report a request for live stored links as a gap pending the future
327
- `urlcode-dynamic-link` extension package rather than inventing a `link` field.
333
+ removed. The `urlcode-dynamic-link` extension package that briefly owned them
334
+ has been retired and unpublished, so there is no supported replacement. Report a
335
+ request for live stored links as a gap rather than inventing a `link` field.
328
336
 
329
337
  See [capabilities and normalized route representation](CAPABILITIES.md) for the target catalog,
330
338
  programmatic compatibility analysis and provider verification limits.
package/docs/ASSETS.md CHANGED
@@ -2,7 +2,8 @@
2
2
 
3
3
  Native file handlers are implemented in 0.1.0-alpha.3. They work in the local
4
4
  Node runtime and self-hosted process/container. Provider adapters remain planned.
5
- They do not run user functions or expose filesystem APIs to sandboxed code.
5
+ They do not run user functions or expose filesystem APIs to guest code, trusted
6
+ or sandboxed.
6
7
 
7
8
  ```yaml
8
9
  version: "1"
@@ -0,0 +1,32 @@
1
+ # Auth backup and restore platform guarantees
2
+
3
+ `createBackup({database,destination,projectRoot})` and
4
+ `restoreBackup({backup,destination,projectRoot})` use SQLite's online backup API
5
+ in a bounded worker. They include committed WAL pages, validate integrity and
6
+ foreign keys, and publish to a new path without overwriting an existing file.
7
+ Both source and destination must remain outside the served project. See the
8
+ [auth operator commands](../packages/auth/README.md#operations-and-recovery).
9
+
10
+ The completed snapshot is flushed through a writable file handle before it is
11
+ linked into its destination. Windows requires write access for this flush;
12
+ opening the snapshot read-only fails with `EPERM`. File flush errors fail the
13
+ operation rather than being ignored.
14
+
15
+ On POSIX systems the containing directory is also flushed after publication.
16
+ Node's filesystem API does not provide the equivalent directory-handle flush
17
+ used here on Windows, so Windows does not receive that extra directory-entry
18
+ crash-durability guarantee. A successful Windows backup verifies and flushes
19
+ file contents; it is not proof that the new filename survives abrupt power
20
+ loss. Verify the backup exists and perform an isolated restore rehearsal before
21
+ depending on it. CI does not simulate power loss.
22
+
23
+ POSIX directory/file modes are checked where supported. Windows operators must
24
+ restrict the operator data and backup directories with filesystem ACLs; POSIX
25
+ mode bits cannot establish Windows privacy. Keep encryption and CSRF keys and
26
+ reviewed configuration separately backed up. Restore to an isolated new path,
27
+ and review restored sessions and revocation state before reopening traffic.
28
+
29
+ A failed auth service initialization waits for its SQLite worker to terminate
30
+ before rejecting. After a configuration rejection, callers can retry or clean up
31
+ the database without racing that failed opener's file handle. Configuration
32
+ identity checks and their error codes are unchanged.
package/docs/AWS.md CHANGED
@@ -11,6 +11,15 @@ self-hosted Node lifecycle, and a `sandbox: true` route would pay worker and
11
11
  WASM startup on every cold start. Both are refused at activation with the route
12
12
  named, never per request, trusted or sandboxed alike.
13
13
 
14
+ That refusal is a settled position, not a gap awaiting an adapter: per-route
15
+ Lambda compilation was considered and declined
16
+ ([the decision](OPEN-DECISIONS.md#accepted-one-node-deployment-per-project),
17
+ [the analysis behind it](archive/2026-09-19/SPIKE-LAMBDA-COMPILE.md)). A project that uses
18
+ `function` or `middleware` deploys instead as one trusted Node process — a
19
+ container or a VM running the project as it runs locally — which supports every
20
+ route type today. That process can run on AWS: ECS, EC2 and App Runner all
21
+ serve it. The decision is about the execution model, not about avoiding AWS.
22
+
14
23
  A working project is in [`examples/aws/`](../examples/aws/).
15
24
 
16
25
  ## Set it up
@@ -165,8 +165,10 @@ accepting any user-controlled URL. Keep modules free of top-level work other tha
165
165
  simple definitions: initialization runs during validation and fresh invocations.
166
166
 
167
167
  Prefer pure helpers with explicit inputs and outputs. Module globals are not a
168
- cache, database, session store or rate limiter: guest state resets per request.
169
- Review [sandbox constraints](FUNCTION-SECURITY.md) before choosing dependencies.
168
+ cache, database, session store or rate limiter: guest state resets per request
169
+ regardless of trust mode. If a route declares `sandbox: true`, review
170
+ [sandbox constraints](FUNCTION-SECURITY.md) before choosing dependencies —
171
+ trusted (default) routes have ordinary Node module access instead.
170
172
 
171
173
  ## Middleware should have one clear responsibility
172
174
 
@@ -182,8 +184,10 @@ export default async function responseHeaders(request, context, next) {
182
184
 
183
185
  Use middleware for reusable behavior around a handler, not to conceal the entire
184
186
  application flow. Prefer YAML headers for fixed route-specific headers; this
185
- example demonstrates a shared wrapper, but native YAML avoids sandbox overhead
186
- when no custom code is needed. Keep middleware order explicit in each route.
187
+ example demonstrates a shared wrapper, but native YAML avoids any
188
+ function/middleware invocation overhead when no custom code is needed
189
+ including the extra cost of `sandbox: true` where that is declared. Keep
190
+ middleware order explicit in each route.
187
191
 
188
192
  Always return a Response. Call `await next()` once when continuing, or return
189
193
  an early Response when intentionally stopping. Do not launch unawaited work or
@@ -217,8 +221,9 @@ For the two-route feature layout above:
217
221
  ```
218
222
 
219
223
  Additional ordinary JavaScript unit tests for pure helpers are your project's
220
- choice. Unit tests alone do not verify sandbox compatibility: always exercise
221
- HTTP behavior through URLCode too. Keep large fixture generation explicit and
224
+ choice. Unit tests alone do not verify runtime compatibility including the
225
+ guest API restrictions of a route declaring `sandbox: true` or HTTP framing:
226
+ always exercise HTTP behavior through URLCode too. Keep large fixture generation explicit and
222
227
  deterministic if you add your own tooling; nested test directories and JSON
223
228
  fragments are not automatically discovered or merged by URLCode.
224
229
 
@@ -238,9 +243,12 @@ Update expected route counts deliberately when adding or removing a route.
238
243
  YAML anchors, shell expansion or generated credentials for convenience.
239
244
  - Deployment limits, TLS, DNS, DDoS filters and worker tuning belong to operations,
240
245
  not invented route fields. Document them separately from portable behavior.
241
- - Live short-code records need durable storage core does not have; that is
242
- moving to a future `urlcode-dynamic-link` extension package, not yet
243
- published. General session/application storage remains future work.
246
+ - Live short-code records need durable storage core does not have. The
247
+ `urlcode-dynamic-link` extension provided it and has been retired; its
248
+ published `0.1.0-alpha.1` pins core `0.4.0-alpha.1` exactly and so cannot be
249
+ installed beside `0.4.0-alpha.2`. Treat stored short links as unsupported
250
+ until that work lands somewhere else. General session/application storage
251
+ remains future work.
244
252
 
245
253
  ## Refactor without changing the public contract
246
254
 
@@ -166,7 +166,7 @@ proxy headers and signal definitions. Resolved egress headers are private runtim
166
166
  state and must never be serialized. Capability analysis itself adds no authority
167
167
  and does not run in the request path.
168
168
 
169
- See the [repository review and incremental plan](NEXT-PHASE-PLAN.md) for the
169
+ See the [repository review and incremental plan](archive/2026-09-19/NEXT-PHASE-PLAN.md) for the
170
170
  implementation status. Provider deployment tests, independent security review and
171
171
  operational soak/recovery proof are separate work.
172
172
 
@@ -0,0 +1,97 @@
1
+ # CI and release follow-up, September 19, 2026
2
+
3
+ This is the evidence and disposition for [#185](https://github.com/jimhoyd-com/urlcode/issues/185),
4
+ after the Windows fixes (#190) and matrix change (#192). It does not authorize
5
+ publication or change required checks.
6
+
7
+ ## Reproducible measurements
8
+
9
+ ```sh
10
+ npm run ci:history -- 100 2026-09-19 > /tmp/ci-history.json
11
+ npm run ci:report -- 35482828280
12
+ ```
13
+
14
+ The history helper reads GitHub through `gh`, with four concurrent requests at
15
+ most. It does not dispatch new runs. Increase the limit (maximum 1,000) as more
16
+ history accumulates. Use a date cutoff after the rollout when comparing the
17
+ same workflow generation. It groups by event, inferred lane and exact matrix,
18
+ reports counts and nearest-rank p50/p95, and flags groups with fewer than 20
19
+ successful first attempts. Failures/cancellations remain visible but are excluded
20
+ from successful-run percentiles. Reruns are excluded because run creation time
21
+ includes the human delay before retrying. Earlier attempts are not a complete
22
+ usage ledger. Raw job/step durations and run URLs remain in the output.
23
+
24
+ Workflow elapsed time ends at the last active job's completion. Runner-minutes
25
+ sum overlapping job execution durations: they are neither wall time nor billed
26
+ minutes. Job-creation-to-start is an observed scheduling interval, not a guarantee
27
+ of pure runner queue time. Workflow-creation-to-start also includes dependencies.
28
+ Missing timestamps remain missing; they are never treated as zero.
29
+
30
+ The initial 100-run sample spans September 19 04:59 UTC through September 20
31
+ 02:00 UTC. Successful first-attempt results:
32
+
33
+ | Configuration | Samples | Wall p50 / p95 | Runner-minute p50 / p95 |
34
+ | --- | ---: | --- | --- |
35
+ | Historical combined main, 9 jobs | 20 | 8m27s / 9m04s | 49.82 / 53.62 |
36
+ | Historical combined PR, 3 jobs | 40 | 6m12s / 8m52s | 17.28 / 24.57 |
37
+ | Split full main, 18 jobs | 1 | 7m36s / 7m36s | 74.30 / 74.30 |
38
+ | Split compact main, 10 jobs | 1 | 6m48s / 6m48s | 43.20 / 43.20 |
39
+ | Split compact PR, 10 jobs | 2 | 6m38s / 7m15s | 40.23 / 43.58 |
40
+
41
+ Historical groups contain changing code and workflow revisions; they are context,
42
+ not a controlled experiment. The sample includes no classified docs-only runs.
43
+ It cannot satisfy the 20-run acceptance criterion for the new lanes. Do not
44
+ manufacture 20 redundant workflow runs to fill the sample.
45
+
46
+ The [compact main run](https://github.com/jimhoyd-com/urlcode/actions/runs/35482828280)
47
+ used 31.1 fewer runner-minutes than the preceding
48
+ [full matrix main run](https://github.com/jimhoyd-com/urlcode/actions/runs/35482515877),
49
+ an observed 41.9% reduction between two runs, not an established long-term rate.
50
+ Its Windows core job took 377 seconds: 258 in `npm test`, 58 in package smoke,
51
+ 13 in installation. Linux Node 22 spent 300 of its 355 seconds in `npm test`.
52
+ The next useful optimization target is the runtime suite, not removing static,
53
+ container, audit or provenance checks. CLI subcommands are separate steps in the
54
+ raw report. File-level profiling is still needed before choosing balanced shards;
55
+ extra shards would add setup and runner pressure.
56
+
57
+ ## Changes delivered in this follow-up
58
+
59
+ - A Windows validation failure exposed rejected auth initialization returning
60
+ before its worker terminated. The caller now awaits termination before receiving
61
+ the original error; a deterministic regression test proves the ordering.
62
+ - `ci:history` makes the remaining baseline measurable without new CI jobs.
63
+ - Auth/admin release instructions now use scoped monorepo tags and the correct
64
+ trusted-publisher workflow filenames. Package agent guides file issues here
65
+ and point documentation at root `docs/`.
66
+ - Optional evals checks its credential before checkout/setup/install, and retains
67
+ artifacts for 14 days. This avoids unused setup; it does not remove the weekly
68
+ quality check or claim measured savings from a workflow with no prior runs.
69
+ - The manual signed candidate builds all four tarballs and installs them together
70
+ in a temporary consumer outside the workspace. It verifies the peer dependency
71
+ tree, installed versions, public imports, and `init --with ui,auth,admin`.
72
+ `train.json` records package SHA-512 integrity and the source commit; the
73
+ candidate manifest/checksums and provenance include the extension archives.
74
+ Failure stops the candidate before attestation/upload. Nothing is published.
75
+
76
+ The candidate validates the proposed package set, not live-provider behavior or
77
+ registry OIDC. Per-package release workflows still prepare their own archives
78
+ and retain their original bytes for retries; candidate archives are not silently
79
+ substituted for published release bytes. Before a release, dispatch the candidate
80
+ at the selected main commit, then the full verification workflow, and inspect
81
+ both results before using the authorized coordinator.
82
+
83
+ ## Remaining decisions and external validation
84
+
85
+ | Item | Disposition |
86
+ | --- | --- |
87
+ | Windows packaging/process coverage | Delivered in #190/#192; Node 24 on relevant PRs/main, all supported versions nightly/manually. |
88
+ | 20-run baseline per new lane | Wait for organic runs, then rerun the helper with a rollout cutoff. |
89
+ | Dependency-aware package selection | Keep conservative coverage for now. UI feeds auth/admin; auth feeds admin; core extension/CLI/scaffold changes affect the full composition. A package map must include integration, generated styles, peers and tooling, not just changed directory names. |
90
+ | Changeset/no-release enforcement | Still open. Define explicit core release intent as well as workspace Changesets; require reviewed reasons for no-release cases before implementing a gate. Blanket source-path rules would misclassify tests/tooling and root core is not versioned by Changesets. |
91
+ | Move core into `packages/core` | Separate migration, not a prerequisite for fast CI. Root-relative build, package files, CLI, Docker and starter paths make this higher risk than keeping the explicit root inventory. |
92
+ | OIDC and retained-artifact retry | Needs the next explicitly authorized release. No synthetic run proves npm's trust configuration or partial publication recovery. |
93
+ | Historical GHCR image labels/digests | Blocked on read access: anonymous registry lookup returned 403; organization package API explicitly requires `read:packages`. No labels/digests were verified and no images or publication settings changed. The repository has no `PUBLISH_CONTAINER` variable; inherited organization variables could not be inspected with this credential. |
94
+ | Dependabot grouping / unchanged nightly reuse | No change yet. Measure update PR fanout before grouping; unchanged source can still acquire new advisory findings, so reusing old verification indiscriminately would hide them. |
95
+
96
+ Keep #185 open for these acceptance items. The remaining items are not evidence
97
+ that a publish or recovery rehearsal has already succeeded.