@jimhoyd/urlcode 0.4.1 → 0.4.6

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 (212) hide show
  1. package/.claude/skills/urlcode-authoring/SKILL.md +60 -7
  2. package/.claude/skills/urlcode-operations/SKILL.md +4 -0
  3. package/README.md +19 -15
  4. package/SECURITY.md +5 -3
  5. package/dist/BUILD-MANIFEST.json +31 -28
  6. package/dist/agent-context.js +82 -0
  7. package/dist/agents-guide.js +42 -42
  8. package/dist/authoring.js +12 -2
  9. package/dist/body-schema.js +159 -0
  10. package/dist/build-cloudflare.js +2 -0
  11. package/dist/capabilities.js +1 -1
  12. package/dist/cli.js +29 -12
  13. package/dist/config.js +74 -6
  14. package/dist/context.js +4 -6
  15. package/dist/errors.js +3 -1
  16. package/dist/examples.js +1 -1
  17. package/dist/extensions.js +138 -3
  18. package/dist/http-policy.js +19 -4
  19. package/dist/http-response.js +2 -2
  20. package/dist/init-with.js +71 -9
  21. package/dist/mcp.js +15 -2
  22. package/dist/pattern-guard.js +32 -0
  23. package/dist/policies/security.js +0 -0
  24. package/dist/policy.js +16 -0
  25. package/dist/project-tests.js +35 -11
  26. package/dist/readiness.js +188 -32
  27. package/dist/recipes.js +1 -1
  28. package/dist/router.js +17 -0
  29. package/dist/runtime.js +26 -3
  30. package/dist/scaffold.js +0 -0
  31. package/dist/server.js +26 -3
  32. package/dist/site.js +0 -0
  33. package/dist/tooling.js +3 -1
  34. package/dist/types/agent-context.d.ts +44 -0
  35. package/dist/types/authoring.d.ts +3 -1
  36. package/dist/types/body-schema.d.ts +58 -0
  37. package/dist/types/config.d.ts +10 -2
  38. package/dist/types/context.d.ts +1 -1
  39. package/dist/types/errors.d.ts +9 -1
  40. package/dist/types/examples.d.ts +1 -1
  41. package/dist/types/extensions.d.ts +105 -2
  42. package/dist/types/http-policy.d.ts +3 -0
  43. package/dist/types/init-with.d.ts +10 -1
  44. package/dist/types/pattern-guard.d.ts +10 -0
  45. package/dist/types/project-tests.d.ts +9 -0
  46. package/dist/types/readiness.d.ts +71 -0
  47. package/dist/types/recipes.d.ts +1 -1
  48. package/dist/types/runtime.d.ts +4 -0
  49. package/dist/types/server.d.ts +9 -1
  50. package/dist/types/tooling.d.ts +2 -0
  51. package/dist/types/types.d.ts +13 -0
  52. package/dist/types.js +10 -2
  53. package/dist/typescript-authoring.js +5 -3
  54. package/dist/verify-deployment.js +16 -10
  55. package/examples/body-validation/README.md +16 -0
  56. package/examples/body-validation/example.yaml +17 -0
  57. package/examples/body-validation/tests/requests.json +15 -0
  58. package/examples/body-validation/urlcode.yaml +34 -0
  59. package/examples/coverage-waiver/README.md +8 -0
  60. package/examples/coverage-waiver/example.yaml +16 -0
  61. package/examples/coverage-waiver/functions/notes.mjs +2 -0
  62. package/examples/coverage-waiver/tests/requests.json +3 -0
  63. package/examples/coverage-waiver/urlcode.yaml +10 -0
  64. package/examples/data-dir/README.md +39 -0
  65. package/examples/data-dir/data/welcome.txt +1 -0
  66. package/examples/data-dir/example.yaml +22 -0
  67. package/examples/data-dir/functions/note.mjs +18 -0
  68. package/examples/data-dir/tests/requests.json +6 -0
  69. package/examples/data-dir/urlcode.yaml +6 -0
  70. package/examples/lifecycle/README.md +15 -0
  71. package/examples/lifecycle/example.yaml +19 -0
  72. package/examples/lifecycle/functions/notes.mjs +29 -0
  73. package/examples/lifecycle/tests/requests.json +12 -0
  74. package/examples/lifecycle/urlcode.yaml +29 -0
  75. package/examples/not-found/README.md +10 -0
  76. package/examples/not-found/example.yaml +17 -0
  77. package/examples/not-found/public/404.html +3 -0
  78. package/examples/not-found/public/index.html +3 -0
  79. package/examples/not-found/tests/requests.json +7 -0
  80. package/examples/not-found/urlcode.yaml +7 -0
  81. package/examples/shared-blocks/README.md +11 -0
  82. package/examples/shared-blocks/example.yaml +17 -0
  83. package/examples/shared-blocks/tests/requests.json +7 -0
  84. package/examples/shared-blocks/urlcode.yaml +37 -0
  85. package/llms-full.txt +436 -149
  86. package/llms.txt +44 -6
  87. package/package.json +15 -22
  88. package/recipes/static-page/README.md +9 -0
  89. package/recipes/static-page/public/index.html +11 -0
  90. package/recipes/static-page/recipe.yaml +21 -0
  91. package/recipes/static-page/tests/requests.json +22 -0
  92. package/recipes/static-page/urlcode.yaml +7 -0
  93. package/recipes/static-plus-api/README.md +6 -0
  94. package/recipes/static-plus-api/urlcode.yaml +4 -0
  95. package/recipes/store-crud/README.md +53 -0
  96. package/recipes/store-crud/recipe.yaml +31 -0
  97. package/recipes/store-crud/tests/requests.json +18 -0
  98. package/recipes/store-crud/urlcode.yaml +18 -0
  99. package/schemas/urlcode.schema.json +125 -60
  100. package/skills/urlcode/SKILL.md +53 -26
  101. package/starters/default/AGENTS.md +43 -43
  102. package/starters/page/README.md +14 -0
  103. package/starters/page/public/index.html +12 -0
  104. package/starters/page/tests/requests.json +17 -0
  105. package/starters/page/urlcode.yaml +6 -0
  106. package/.claude-plugin/marketplace.json +0 -18
  107. package/CONTRIBUTING.md +0 -112
  108. package/ROADMAP.md +0 -66
  109. package/docs/AI-AUTHORING.md +0 -338
  110. package/docs/ASSETS.md +0 -107
  111. package/docs/AUTH-BACKUP.md +0 -32
  112. package/docs/AWS.md +0 -86
  113. package/docs/BEST-PRACTICES.md +0 -276
  114. package/docs/BULK.md +0 -79
  115. package/docs/CAPABILITIES.md +0 -192
  116. package/docs/CAPACITY.md +0 -305
  117. package/docs/CI-FOLLOWUP-2026-09-19.md +0 -97
  118. package/docs/CI-RELEASE-AUDIT-2026-09-19.md +0 -322
  119. package/docs/CI.md +0 -147
  120. package/docs/CLOUDFLARE.md +0 -109
  121. package/docs/CODEBASE-AUDIT-2026-09-20.md +0 -284
  122. package/docs/COMPLIANCE.md +0 -239
  123. package/docs/COMPOSING-A-SITE.md +0 -278
  124. package/docs/CONDITIONS.md +0 -74
  125. package/docs/DEPLOYMENT-CHECKS.md +0 -108
  126. package/docs/DEVELOPMENT-PIPELINE.md +0 -270
  127. package/docs/EGRESS.md +0 -125
  128. package/docs/EXTENSIONS.md +0 -438
  129. package/docs/FRAMEWORK.md +0 -217
  130. package/docs/FUNCTION-SECURITY.md +0 -254
  131. package/docs/HTTP.md +0 -129
  132. package/docs/INSTALL.md +0 -128
  133. package/docs/INTERCHANGE.md +0 -134
  134. package/docs/LOAD-TESTING.md +0 -91
  135. package/docs/LOCAL-DEVELOPMENT.md +0 -102
  136. package/docs/MIDDLEWARE-EXAMPLES.md +0 -75
  137. package/docs/MIDDLEWARE.md +0 -102
  138. package/docs/MONITORING.md +0 -115
  139. package/docs/OBSERVABILITY.md +0 -222
  140. package/docs/OPEN-DECISIONS.md +0 -224
  141. package/docs/OPERATIONAL-PROOF.md +0 -41
  142. package/docs/OPERATIONS.md +0 -201
  143. package/docs/ORGANIZATION.md +0 -135
  144. package/docs/PERFORMANCE.md +0 -72
  145. package/docs/PLUGINS.md +0 -271
  146. package/docs/POLICIES.md +0 -211
  147. package/docs/PRERENDER.md +0 -245
  148. package/docs/PROJECT-DIRECTION.md +0 -118
  149. package/docs/PROVIDER-VERIFICATION.md +0 -84
  150. package/docs/READINESS.md +0 -150
  151. package/docs/README.md +0 -87
  152. package/docs/RECIPES.md +0 -99
  153. package/docs/RELEASE-0.4.0-alpha.3.md +0 -50
  154. package/docs/RELEASE-0.4.1.md +0 -73
  155. package/docs/RELEASE-READINESS.md +0 -117
  156. package/docs/RELEASE-SECURITY.md +0 -96
  157. package/docs/RESILIENCE.md +0 -161
  158. package/docs/ROUTING.md +0 -92
  159. package/docs/SANDBOX-REVIEW.md +0 -72
  160. package/docs/SCAFFOLDING.md +0 -70
  161. package/docs/SECURITY-AUDIT.md +0 -164
  162. package/docs/SITE.md +0 -150
  163. package/docs/SPECIFICATION.md +0 -359
  164. package/docs/SPIKE-AI-FRAMEWORK-BENCHMARK.md +0 -288
  165. package/docs/SPIKE-BUSINESS-SUITE.md +0 -1029
  166. package/docs/SPIKE-CORE-LAYERING.md +0 -368
  167. package/docs/SPIKE-DEFAULT-TRUST-MODEL.md +0 -211
  168. package/docs/STANDARDS.md +0 -311
  169. package/docs/STARTERS.md +0 -83
  170. package/docs/STATIC.md +0 -105
  171. package/docs/TOOLING.md +0 -298
  172. package/docs/TUNNELS.md +0 -72
  173. package/docs/TYPESCRIPT-AUTHORING.md +0 -87
  174. package/docs/TYPESCRIPT.md +0 -123
  175. package/docs/VERCEL.md +0 -114
  176. package/docs/VERSION-ALIGNMENT.md +0 -80
  177. package/docs/YAML-GUIDE.md +0 -57
  178. package/docs/YAML-REFERENCE.md +0 -449
  179. package/docs/archive/2026-09-19/EXTENSION-IMPLEMENTATION.md +0 -68
  180. package/docs/archive/2026-09-19/MANAGEMENT-SECURITY.md +0 -102
  181. package/docs/archive/2026-09-19/NEXT-PHASE-PLAN.md +0 -108
  182. package/docs/archive/2026-09-19/NEXT-STEPS.md +0 -646
  183. package/docs/archive/2026-09-19/OPEN-DECISIONS.md +0 -277
  184. package/docs/archive/2026-09-19/RELEASE-SECURITY.md +0 -186
  185. package/docs/archive/2026-09-19/ROADMAP.md +0 -387
  186. package/docs/archive/2026-09-19/SPIKE-EXTENSION-MODEL.md +0 -430
  187. package/docs/archive/2026-09-19/SPIKE-EXTENSIONS.md +0 -492
  188. package/docs/archive/2026-09-19/SPIKE-LAMBDA-COMPILE.md +0 -365
  189. package/docs/archive/2026-09-19/SPIKE-MONOREPO.md +0 -778
  190. package/docs/archive/2026-09-19/USABILITY-REVIEW.md +0 -139
  191. package/docs/archive/README.md +0 -28
  192. package/docs/policies/agents.md +0 -182
  193. package/docs/policies/cache.md +0 -152
  194. package/docs/policies/compression.md +0 -169
  195. package/docs/policies/contract.md +0 -52
  196. package/docs/policies/hardened.md +0 -56
  197. package/docs/policies/interoperability.md +0 -169
  198. package/docs/policies/operations.md +0 -45
  199. package/docs/policies/security.md +0 -161
  200. package/docs/policies/throttle.md +0 -103
  201. package/docs/yaml/assets.md +0 -36
  202. package/docs/yaml/conditions.md +0 -20
  203. package/docs/yaml/functions.md +0 -168
  204. package/docs/yaml/middleware.md +0 -31
  205. package/docs/yaml/organization.md +0 -74
  206. package/docs/yaml/policies.md +0 -37
  207. package/docs/yaml/redirects.md +0 -64
  208. package/docs/yaml/responses.md +0 -57
  209. package/docs/yaml/site.md +0 -24
  210. package/packaging/claude-plugin/.claude-plugin/plugin.json +0 -19
  211. package/packaging/claude-plugin/skills/urlcode-authoring/SKILL.md +0 -120
  212. package/packaging/claude-plugin/skills/urlcode-operations/SKILL.md +0 -108
@@ -1,4 +1,5 @@
1
- import type { LoadedDocument, ProjectDocument, RouteConfig } from './types.ts';
1
+ import type { ErrorObject } from 'ajv';
2
+ import type { LoadedDocument, ProjectDocument, RouteConfig, SharedBlock } from './types.ts';
2
3
  /** What config-worker.ts posts back: the loaded document, or the ConfigError message. */
3
4
  export type ConfigWorkerResult = {
4
5
  value: LoadedDocument;
@@ -10,7 +11,14 @@ export interface ConfigWorkerData {
10
11
  }
11
12
  export declare const MAX_CONFIG_BYTES: number;
12
13
  export declare function parseYaml(text: string): unknown;
13
- export declare function validateDocument(data: unknown): ProjectDocument;
14
+ /**
15
+ * One line for the first schema violation. Closed-key-set failures name the offending key and the keys the
16
+ * schema allows, and required failures name the missing key, because a bare keyword sends the reader hunting.
17
+ * Only key names, which come from the schema or the author's own mapping keys, are echoed, never values
18
+ * (values may hold secrets), and never more than MAX_NAMED_KEY characters of a key.
19
+ */
20
+ export declare function describeSchemaError(e: ErrorObject): string;
21
+ export declare function validateDocument(data: unknown, inherited?: Record<string, SharedBlock>): ProjectDocument;
14
22
  /** The input declaration a short-form function gets for each `{param}` it does not declare itself. */
15
23
  export declare const SHORT_FORM_PATH_SCHEMA: {
16
24
  readonly type: "string";
@@ -62,5 +62,5 @@ export declare function estimateTokens(text: string): number;
62
62
  export declare function renderContext(context: ProjectContext): string;
63
63
  /** Derived only from the compiled project and the capability catalog, never from prose. Key order is fixed. */
64
64
  export declare function buildContext(project: string, options?: ContextOptions): Promise<ProjectContext>;
65
- /** Estimated size of the shipped documentation (docs/*.md and llms.txt), for comparison with an emitted context. */
65
+ /** Estimated size of the shipped offline documentation bundle, for comparison with an emitted context. */
66
66
  export declare function documentationTokens(): Promise<number>;
@@ -2,6 +2,14 @@ export declare class ConfigError extends Error {
2
2
  }
3
3
  export declare class HttpError extends Error {
4
4
  readonly status: number;
5
- constructor(status: number, message: string);
5
+ /** A pre-rendered, fixed-content answer that replaces the plain-text message (used for negotiated 422 bodies). */
6
+ readonly answer: {
7
+ contentType: string;
8
+ text: string;
9
+ } | undefined;
10
+ constructor(status: number, message: string, answer?: {
11
+ contentType: string;
12
+ text: string;
13
+ });
6
14
  }
7
15
  export declare function assert(condition: unknown, message: string): asserts condition;
@@ -31,7 +31,7 @@ export interface ExampleSearchResult {
31
31
  matchedRoutes: RouteIndexEntry[];
32
32
  })[];
33
33
  }
34
- export declare const exampleNames: readonly ["assets", "aws", "cloudflare", "compliance", "conditions", "cookbook", "egress", "extensions", "monitoring", "prerender", "provider-conformance", "tunnel", "vercel"];
34
+ export declare const exampleNames: readonly ["assets", "aws", "body-validation", "cloudflare", "compliance", "conditions", "cookbook", "coverage-waiver", "data-dir", "egress", "extensions", "lifecycle", "monitoring", "not-found", "prerender", "provider-conformance", "shared-blocks", "tunnel", "vercel"];
35
35
  export declare const routeIndexFile = "route-index.json";
36
36
  export declare function listExamples(): Promise<ExampleSummary[]>;
37
37
  /**
@@ -66,6 +66,83 @@ export interface ExtensionInstance {
66
66
  export interface ExtensionImmutableAssets {
67
67
  prefix: string;
68
68
  }
69
+ export interface ExtensionHookReference {
70
+ source: string;
71
+ export?: string;
72
+ sandbox?: boolean;
73
+ sandboxReason?: string;
74
+ }
75
+ export type ExtensionHookConfig = string | ExtensionHookReference;
76
+ export type ExtensionHookKind = 'filter' | 'action';
77
+ /** Machine-readable contract for one project hook an extension exposes. */
78
+ export interface ExtensionHookContract {
79
+ name: string;
80
+ kind: ExtensionHookKind;
81
+ description: string;
82
+ inputSchema: object;
83
+ outputSchema?: object;
84
+ }
85
+ export type ExtensionAuthoringKind = 'configuration' | 'theme' | 'copy' | 'component' | 'template' | 'stylesheet' | 'hook' | 'extension';
86
+ /** One project-owned customization surface, shown to people and authoring agents by CLI/MCP inspection. */
87
+ export interface ExtensionAuthoringSurface {
88
+ kind: ExtensionAuthoringKind;
89
+ name: string;
90
+ description: string;
91
+ /** Project-relative convention or configuration path, when the surface has one. */
92
+ path?: string;
93
+ /** A bounded local command that discovers, previews or checks the surface. */
94
+ command?: string;
95
+ }
96
+ /**
97
+ * Machine-readable guidance for changing an installed extension without
98
+ * copying its behavior into the application. This is descriptive only: it
99
+ * grants nothing and is never executed by the runtime.
100
+ */
101
+ export interface ExtensionAuthoringContract {
102
+ description: string;
103
+ surfaces: readonly ExtensionAuthoringSurface[];
104
+ fastChecks?: readonly string[];
105
+ }
106
+ export type LoadedExtensionHooks<T extends string = string> = Partial<Record<T, (input: unknown) => unknown>>;
107
+ /** Shared schema for project hook references. Omission means trusted execution. */
108
+ export declare const extensionHookReferenceSchema: {
109
+ readonly oneOf: readonly [{
110
+ readonly type: "string";
111
+ readonly minLength: 1;
112
+ readonly maxLength: 1024;
113
+ }, {
114
+ readonly type: "object";
115
+ readonly additionalProperties: false;
116
+ readonly required: readonly ["source"];
117
+ readonly properties: {
118
+ readonly source: {
119
+ readonly type: "string";
120
+ readonly minLength: 1;
121
+ readonly maxLength: 1024;
122
+ };
123
+ readonly export: {
124
+ readonly type: "string";
125
+ readonly pattern: "^[A-Za-z_][A-Za-z0-9_]*$";
126
+ };
127
+ readonly sandbox: {
128
+ readonly type: "boolean";
129
+ };
130
+ readonly sandboxReason: {
131
+ readonly type: "string";
132
+ readonly minLength: 1;
133
+ readonly maxLength: 512;
134
+ };
135
+ };
136
+ }];
137
+ };
138
+ /** Builds the strict `config.hooks` schema from an extension's declared hook names. */
139
+ export declare function extensionHooksSchema(contracts: readonly ExtensionHookContract[]): object;
140
+ /**
141
+ * Loads project hooks once per activation. Project hooks are trusted first-party
142
+ * code by default, matching function/middleware routes. Sandboxed arbitrary-value
143
+ * hooks are not part of contract v1 and are refused rather than run trusted.
144
+ */
145
+ export declare function loadExtensionHooks<T extends string>(config: Readonly<Record<string, unknown>> | undefined, contracts: readonly ExtensionHookContract[], context: Pick<ExtensionActivation, 'root'>): Promise<LoadedExtensionHooks<T>>;
69
146
  export interface RuntimeExtension {
70
147
  name: string;
71
148
  version: '1';
@@ -75,6 +152,10 @@ export interface RuntimeExtension {
75
152
  policySchema?: object;
76
153
  credentialHeaders?: string[];
77
154
  immutableAssets?: ExtensionImmutableAssets;
155
+ /** Project customization points, exposed by CLI/MCP for authors and agents. */
156
+ hooks?: readonly ExtensionHookContract[];
157
+ /** Supported project-owned customization surfaces, exposed by CLI/MCP. */
158
+ authoring?: ExtensionAuthoringContract;
78
159
  /**
79
160
  * Reviewed, operator-declared cache sensitivity for `policies.extensions.<name>`
80
161
  * routes (never for an `extension:` mount, which is always treated as
@@ -102,8 +183,15 @@ export interface ScaffoldRequest {
102
183
  project: string;
103
184
  /** Absolute path of the combined host module core writes, `<directory>/host.mjs`. */
104
185
  hostFile: string;
105
- /** Every extension name being scaffolded together, in `--with` order, including this one. */
186
+ /** Every extension name being scaffolded together, including this one, in a canonical (sorted) order that is independent of the `--with` spelling. */
106
187
  names: readonly string[];
188
+ /**
189
+ * Operator acknowledgements from repeated `--ack <extension>:<id>` flags, sorted and de-duplicated; empty when none. Core treats
190
+ * them as opaque strings and never invents one. An extension reads only the ones qualified with its own name. To require one, throw
191
+ * an Error carrying `acknowledgement: '<name>:<id>'` whose message states the risk; core appends the exact re-run command with
192
+ * `--ack <name>:<id>`. List each one the scaffold used in `ScaffoldResult.acknowledged`.
193
+ */
194
+ acknowledgements: readonly string[];
107
195
  }
108
196
  export interface ScaffoldFile {
109
197
  path: string;
@@ -113,6 +201,21 @@ export interface ScaffoldFile {
113
201
  export interface ScaffoldResult {
114
202
  /** Must equal the requested name. */
115
203
  name: string;
204
+ /**
205
+ * Declarative composition contract; `--with` is an unordered set and core derives the host, activation and README order from these.
206
+ * `provides`: capability names this extension offers (a name must not equal an extension name). `requires`: extensions or
207
+ * capabilities that must be in the set and are placed before this one; a missing one refuses. `after`: the same ordering
208
+ * without requiring presence. `conflicts`: extensions or capabilities that must not be in the set. A cycle refuses. Core never
209
+ * adds an extension or infers policy from these lists.
210
+ */
211
+ provides?: string[];
212
+ requires?: string[];
213
+ after?: string[];
214
+ conflicts?: string[];
215
+ /** The `<name>:<id>` acknowledgements this scaffold consumed. Core refuses any passed `--ack` that no scaffold lists here, so an acknowledgement cannot be passed with no effect. */
216
+ acknowledged?: string[];
217
+ /** One-line notes written as comments above this extension's routes in the route fragment (for example the selected access model). */
218
+ routeNotes?: string[];
116
219
  /** Fragments merged into the project's top-level `extensions` and `routes`; duplicate keys are refused. */
117
220
  extensions: Record<string, unknown>;
118
221
  routes: Record<string, unknown>;
@@ -123,7 +226,7 @@ export interface ScaffoldResult {
123
226
  hostClose?: string[];
124
227
  /** Files written relative to `directory` with their modes; never inside the project, never overwriting. */
125
228
  files: ScaffoldFile[];
126
- /** Markdown appended to README.md under a heading core adds; the numbered steps merged in `--with` order. */
229
+ /** Markdown appended to README.md under a heading core adds; the numbered steps merged in the resolved order. */
127
230
  readme: string;
128
231
  nextSteps: string[];
129
232
  /** Environment variables the host reads, with one-line descriptions. */
@@ -1,5 +1,6 @@
1
1
  import type { HandlerResult, HeaderPair } from './http-response.ts';
2
2
  import type { HeadersLike } from './match.ts';
3
+ import type { BodySchema } from './body-schema.ts';
3
4
  export interface RespondSpec {
4
5
  status?: number;
5
6
  json?: unknown;
@@ -10,6 +11,7 @@ export interface RequestBodyPolicy {
10
11
  required?: boolean;
11
12
  contentTypes?: string[];
12
13
  format?: 'json' | 'text';
14
+ schema?: BodySchema;
13
15
  }
14
16
  /** A static reply compiled from `respond`; the body is bytes so every host, including the Worker, shares the type. */
15
17
  export interface Reply {
@@ -32,6 +34,7 @@ export interface HttpRoute {
32
34
  responseHeaders?: HeaderPair[];
33
35
  reply?: Reply | undefined;
34
36
  }
37
+ export declare const reservedResponseHeaders: Set<string>;
35
38
  export declare function compileHttp(route: HttpRoute): void;
36
39
  export declare function checkRequest(route: HttpRoute, body: Uint8Array, headers: HeadersLike, counts?: Record<string, number>): void;
37
40
  export declare function decorateResponse(route: HttpRoute & {
@@ -1,3 +1,4 @@
1
+ import type { ScaffoldResult } from './extensions.ts';
1
2
  import type { DependencyPin } from './project-dependencies.ts';
2
3
  export interface InitWithOptions {
3
4
  cwd?: string | undefined;
@@ -5,6 +6,8 @@ export interface InitWithOptions {
5
6
  manifest?: boolean | undefined;
6
7
  /** `--pin <package>=<specifier>` overrides, for local tarballs, checkouts and mirrors. */
7
8
  pins?: ReadonlyMap<string, string> | undefined;
9
+ /** `--ack <extension>:<id>`, repeatable: opaque qualified acknowledgements handed to every scaffold. Core refuses one that no scaffold consumed. */
10
+ acknowledgements?: readonly string[] | undefined;
8
11
  }
9
12
  export interface InitWithResult {
10
13
  directory: string;
@@ -16,9 +19,15 @@ export interface InitWithResult {
16
19
  dependencies: DependencyPin[];
17
20
  }
18
21
  export declare function parseWithNames(value: string): string[];
22
+ /**
23
+ * Orders the requested set from the scaffolds' declared `requires`, `after`, `provides` and `conflicts`, never from
24
+ * the `--with` spelling. Kahn's algorithm with the lexically smallest ready extension first, so the result is
25
+ * deterministic and identical for every permutation. Refuses a missing requirement, a conflict or a cycle by name.
26
+ */
27
+ export declare function orderScaffolds(results: readonly ScaffoldResult[]): ScaffoldResult[];
19
28
  /**
20
29
  * `urlcode init <directory> --with a,b`: the starter under `app/`, every extension's fragments merged into one
21
30
  * `urlcode.yaml`, one `host.mjs`, one `README.md` and the extensions' own files. All packages are resolved and
22
31
  * their scaffolds computed before anything is written, so a refusal leaves no directory behind.
23
32
  */
24
- export declare function initProjectWith(destination: string, names: readonly string[], { cwd, manifest, pins }?: InitWithOptions): Promise<InitWithResult>;
33
+ export declare function initProjectWith(destination: string, requested: readonly string[], { cwd, manifest, pins, acknowledgements }?: InitWithOptions): Promise<InitWithResult>;
@@ -0,0 +1,10 @@
1
+ export declare const maxPatternLength = 128;
2
+ export declare const maxPatternInputLength = 128;
3
+ /**
4
+ * Accepts an author regex only when it is conservatively safe to run on every
5
+ * request in the host process. Node has no linear-time engine, so this refuses
6
+ * the constructs that make backtracking blow up — repeated groups, lookaround
7
+ * and backreferences — and caps unbounded quantifiers. It is a restriction, not
8
+ * a proof: callers must also bound the input length to `maxPatternInputLength`.
9
+ */
10
+ export declare function assertSafePattern(pattern: string): void;
@@ -1,4 +1,5 @@
1
1
  import type { ServerOptions } from './server.ts';
2
+ import type { RestartableApp } from './readiness.ts';
2
3
  import type { LogFn } from './types.ts';
3
4
  export interface ProjectTestOptions {
4
5
  extensions?: ServerOptions['extensions'];
@@ -11,4 +12,12 @@ export interface ProjectTestResult {
11
12
  total: number;
12
13
  failed: number;
13
14
  }
15
+ /**
16
+ * A server that fixture `restart` steps can close and start again on the same project and the
17
+ * same data directory. The data directory is fresh and empty, offered to the project as
18
+ * `URLCODE_DATA_DIR`, and removed by `close()`; on a filesystem where it cannot be created, none is offered. `restart()` gets a new port; read `address` after it.
19
+ */
20
+ export declare function startRestartable(options: ServerOptions): Promise<RestartableApp & {
21
+ close(): Promise<void>;
22
+ }>;
14
23
  export declare function runProjectTests(project: string, { log, permissions, origin, extensions, plugins }?: ProjectTestOptions): Promise<ProjectTestResult>;
@@ -23,17 +23,38 @@ export interface RequestCase {
23
23
  body?: string | undefined;
24
24
  expectHeaders?: Record<string, string> | undefined;
25
25
  expectBody?: string | undefined;
26
+ /** Only inside `steps`: values kept from this step's response for later steps' `{{name}}` references. */
27
+ capture?: Record<string, CaptureSpec> | undefined;
26
28
  }
29
+ /** Where a captured value comes from: a dotted path into a JSON response body, or one response header. */
30
+ export type CaptureSpec = {
31
+ json: string;
32
+ } | {
33
+ header: string;
34
+ };
35
+ /** A step that closes and restarts the runtime on the same project and data directory. */
36
+ export interface RestartStep {
37
+ restart: true;
38
+ }
39
+ /** An ordered fixture: requests that share captured values, optionally with restarts between them. */
40
+ export interface StepsFixture {
41
+ steps: (RequestCase | RestartStep)[];
42
+ }
43
+ export type Fixture = RequestCase | StepsFixture;
44
+ export declare const isStepsFixture: (fixture: Fixture) => fixture is StepsFixture;
45
+ export declare const isRestartable: (app: AuditableApp) => app is RestartableApp;
27
46
  export interface ProjectPlan {
28
47
  inventory: RouteInventory[];
29
48
  cases: RequestCase[];
30
49
  resolve: (path: string) => string | undefined;
31
50
  }
51
+ /** `captured` holds values from a step's `capture`; callers use it for substitution only and never print it. */
32
52
  export interface HitResult {
33
53
  pass: boolean;
34
54
  status: number;
35
55
  durationMs: number;
36
56
  error?: string;
57
+ captured?: Record<string, string>;
37
58
  }
38
59
  export interface BenchmarkTarget {
39
60
  protocol: string;
@@ -48,6 +69,10 @@ export interface AuditableApp {
48
69
  policies?: Record<string, PolicyInventory>;
49
70
  };
50
71
  }
72
+ /** An app that can also close and restart itself on the same project and data directory (fixture `restart` steps). */
73
+ export interface RestartableApp extends AuditableApp {
74
+ restart(): Promise<void>;
75
+ }
51
76
  export type { ComplianceOptions, ComplianceReport } from './compliance.ts';
52
77
  export interface AuditOptions {
53
78
  expectRoutes?: number | undefined;
@@ -57,8 +82,15 @@ export interface AuditOptions {
57
82
  export interface AuditReport {
58
83
  elapsedMs: number;
59
84
  ready: boolean;
85
+ /** Empty when `ready`; otherwise one stable code per failed condition:
86
+ * `no-active-routes`, `route-count-mismatch`, `failed-checks`, `uncovered-route-methods`. */
87
+ notReadyReasons: string[];
88
+ /** `configured` is every route in the table, including routes generated from `site` keys; `--expect-routes` compares against it.
89
+ * `declared` + `generated` always equals `configured`. */
60
90
  counts: {
61
91
  configured: number;
92
+ declared: number;
93
+ generated: number;
62
94
  active: number;
63
95
  disabled: number;
64
96
  expired: number;
@@ -75,6 +107,24 @@ export interface AuditReport {
75
107
  route: string;
76
108
  method: string;
77
109
  }[];
110
+ /** Route/method pairs excused by a `coveredElsewhere` waiver, with the reason. Shown even when `ready`. */
111
+ waivedRouteMethods: {
112
+ route: string;
113
+ method: string;
114
+ reason: string;
115
+ }[];
116
+ /** Waivers not honored (the route has no normally covered method, e.g. an error-only function route); their pairs stay in `uncovered`. */
117
+ ignoredWaivers: {
118
+ route: string;
119
+ method: string;
120
+ reason: string;
121
+ }[];
122
+ /** Waivers whose pair already has a passing normal-response fixture: remove them. Never blocks `ready`. */
123
+ redundantWaivers: {
124
+ route: string;
125
+ method: string;
126
+ reason: string;
127
+ }[];
78
128
  policies: Record<string, PolicyInventory>;
79
129
  compliance: ComplianceReport | null;
80
130
  /** Non-blocking `audit` observations, e.g. a route that looks webhook-shaped
@@ -123,7 +173,28 @@ export declare const hasRedirect: (route: CompiledRoute) => route is CompiledRou
123
173
  };
124
174
  export declare const probeAgent = "Mozilla/5.0 (compatible; RouteProbe/0.1)";
125
175
  export declare function projectPlan(compiled: CompiledRoutes<CompiledRoute>): ProjectPlan;
176
+ export declare function readFixtures(root: string, optional?: boolean): Promise<Fixture[]>;
177
+ /** Single-request fixtures only: the benchmark replays these and cannot run ordered steps. */
126
178
  export declare function readCases(root: string, optional?: boolean): Promise<RequestCase[]>;
179
+ /** One request a fixture run sent: `test` is the request as sent (captured values filled in), `original` as written. Print `original`, never `test`. */
180
+ export interface FixtureStep {
181
+ case: number;
182
+ fixture: number;
183
+ test: RequestCase;
184
+ original: RequestCase;
185
+ result: HitResult;
186
+ }
187
+ export interface FixtureHost {
188
+ app: AuditableApp;
189
+ agent: Agent;
190
+ target?: BenchmarkTarget | undefined;
191
+ /** Close and restart the runtime on the same project and data directory. Absent: the host cannot restart. */
192
+ restart?: (() => Promise<void>) | undefined;
193
+ /** Called with the 1-based fixture number and a reason when a fixture with a restart step is skipped because the host cannot restart. Absent: such a fixture is refused with an error. */
194
+ skipped?: ((fixture: number, reason: string) => void) | undefined;
195
+ }
196
+ /** Runs every fixture in file order. A step after a failed step in the same fixture is reported failed with error `skipped` and never sent, so a broken chain cannot pass. */
197
+ export declare function runFixtures(fixtures: Fixture[], host: FixtureHost, visit: (step: FixtureStep) => void | Promise<void>, firstCase?: number): Promise<void>;
127
198
  export declare function benchmarkTarget(value: string): BenchmarkTarget;
128
199
  export declare function hit(app: AuditableApp, test: RequestCase, agent: Agent, target?: BenchmarkTarget): Promise<HitResult>;
129
200
  export declare function auditProject(app: AuditableApp, { expectRoutes, log, compliance }?: AuditOptions): Promise<AuditReport>;
@@ -20,7 +20,7 @@ export interface RecipeSearchResult {
20
20
  matched: string[];
21
21
  })[];
22
22
  }
23
- export declare const recipeNames: readonly ["redirect", "json-api", "typescript", "middleware", "health-page", "static-plus-api", "cors-api", "webhook-receiver", "contact-form", "authenticated-json-api", "protected-download"];
23
+ export declare const recipeNames: readonly ["redirect", "json-api", "typescript", "middleware", "health-page", "static-page", "static-plus-api", "cors-api", "webhook-receiver", "contact-form", "authenticated-json-api", "protected-download", "store-crud"];
24
24
  export declare function listRecipes(): Promise<RecipeSummary[]>;
25
25
  /** Matches id, description, tags and capabilities locally; no service is consulted. */
26
26
  export declare function searchRecipes(text: string): Promise<RecipeSearchResult>;
@@ -34,6 +34,10 @@ export interface RuntimeOptions {
34
34
  * whole project denies every binding it grants. Prerendering uses it to render a
35
35
  * project too large for one function snapshot in passes (docs/PRERENDER.md). */
36
36
  only?: readonly string[] | undefined;
37
+ /** Test harness only (set by `startServer` when it is given a data directory): also grant every
38
+ * route's `{env: URLCODE_DATA_DIR}` binding, and only that name, for this project revision. The value
39
+ * is the harness's own directory, not an ambient secret. Never supplied by project YAML or guest code. */
40
+ grantDataDir?: boolean | undefined;
37
41
  }
38
42
  /** Per-request facts the host may read after handle() settles; never request text. */
39
43
  export interface RequestTrace {
@@ -16,6 +16,12 @@ export interface ServerOptions extends Omit<RuntimeOptions, 'observers'> {
16
16
  observers?: Observer[] | undefined;
17
17
  metrics?: boolean | undefined;
18
18
  metricsIntervalMs?: number | undefined;
19
+ /** Test helper. Directory the project may use for its own files, offered as the `URLCODE_DATA_DIR`
20
+ * environment value (a route reads it through a declared `env` binding). Created if absent and
21
+ * never deleted by `close()`, so a later server started on the same directory sees the same data. */
22
+ dataDir?: string | undefined;
23
+ /** Test helper. Create a fresh empty data directory, offer it as `URLCODE_DATA_DIR`, and remove it on `close()`. Exclusive with `dataDir`. */
24
+ isolateData?: boolean | undefined;
19
25
  }
20
26
  export interface Server {
21
27
  server: http.Server;
@@ -31,4 +37,6 @@ export interface Server {
31
37
  origin: string;
32
38
  close(): Promise<void>;
33
39
  }
34
- export declare function startServer({ project, host, port, watch, local, log, maxBodyBytes, maxInFlightRequests, maxInFlightHealthRequests, requestLog, trustRequestId, origin, trustedProxies, observers, metrics, metricsIntervalMs, ...runtimeOptions }?: ServerOptions): Promise<Server>;
40
+ /** Starts the server. `port: 0` picks a free port, so this is also the in-process helper for tests:
41
+ * `startServer({ project, port: 0, local: true, isolateData: true })`. */
42
+ export declare function startServer(options?: ServerOptions): Promise<Server>;
@@ -93,6 +93,8 @@ export interface ExtensionInspection {
93
93
  credentialHeaders: string[];
94
94
  schema: object;
95
95
  policySchema: object | null;
96
+ hooks: object[];
97
+ authoring: object | null;
96
98
  declared: boolean;
97
99
  revisionPinned: boolean;
98
100
  mounts: string[];
@@ -113,6 +113,7 @@ export interface SiteConfig {
113
113
  favicon?: string;
114
114
  securityTxt?: SecurityTxtConfig;
115
115
  llms?: string;
116
+ notFound?: string;
116
117
  }
117
118
  /** One route as declared in YAML (plus `generated`, which site.ts stamps on the routes it adds). */
118
119
  export interface ConditionalReply {
@@ -169,6 +170,8 @@ export interface RouteConfig {
169
170
  * Never inferred or enforced: the trust decision remains the author's judgment call
170
171
  * (docs/AI-AUTHORING.md, "Deciding when a route needs sandbox: true"). */
171
172
  sandboxReason?: string;
173
+ /** Per-method `audit` coverage waiver: method to a non-empty reason. Project file only (docs/READINESS.md). */
174
+ coveredElsewhere?: Record<string, string>;
172
175
  parameters?: ParameterConfig[];
173
176
  redirect?: RedirectConfig;
174
177
  function?: FunctionConfig;
@@ -186,9 +189,16 @@ export interface RouteConfig {
186
189
  respond?: RespondSpec;
187
190
  middleware?: MiddlewareConfig[];
188
191
  policies?: PoliciesConfig;
192
+ /** Name of a top-level `shared` block; expanded away at load time, so nothing downstream sees it. */
193
+ use?: string;
189
194
  /** Set by site.ts on a route it generated (`site.<key>`); never declared in YAML. */
190
195
  generated?: string;
191
196
  }
197
+ /** A named, reusable `request` and `response.headers` block a route selects with `use`. */
198
+ export interface SharedBlock {
199
+ request?: RouteConfig['request'];
200
+ response?: RouteConfig['response'];
201
+ }
192
202
  export interface ProjectDocument {
193
203
  version: '1';
194
204
  extensions?: Record<string, ExtensionDeclaration>;
@@ -196,6 +206,7 @@ export interface ProjectDocument {
196
206
  includes?: string[];
197
207
  policies?: PoliciesConfig;
198
208
  profiles?: Record<string, PolicyLayer>;
209
+ shared?: Record<string, SharedBlock>;
199
210
  site?: SiteConfig;
200
211
  }
201
212
  /** What config.ts returns: the entry document, the merged route table and the files it came from. */
@@ -394,6 +405,8 @@ export interface PlanInventoryEntry {
394
405
  sandbox?: boolean;
395
406
  /** The route's declared `sandboxReason`, when it has one. */
396
407
  sandboxReason?: string;
408
+ /** The route's declared `coveredElsewhere` audit waivers (method to reason), when it has any. */
409
+ coveredElsewhere?: Record<string, string>;
397
410
  }
398
411
  export interface TestPlan {
399
412
  inventory: PlanInventoryEntry[];
package/dist/types.js CHANGED
@@ -55,7 +55,7 @@
55
55
 
56
56
 
57
57
 
58
-
58
+
59
59
  /** One route as declared in YAML (plus `generated`, which site.ts stamps on the routes it adds). */
60
60
 
61
61
 
@@ -83,17 +83,23 @@
83
83
 
84
84
 
85
85
 
86
+
87
+
86
88
 
87
89
 
88
90
 
89
91
 
90
92
 
93
+
94
+
91
95
 
92
96
 
93
97
 
98
+ /** A named, reusable `request` and `response.headers` block a route selects with `use`. */
99
+
94
100
 
95
101
 
96
-
102
+
97
103
 
98
104
  /** What config.ts returns: the entry document, the merged route table and the files it came from. */
99
105
 
@@ -209,6 +215,8 @@
209
215
 
210
216
 
211
217
 
218
+
219
+
212
220
 
213
221
 
214
222
 
@@ -7,7 +7,7 @@ import {stringify} from 'yaml';
7
7
  import {loadDocument} from './config.js';
8
8
  import {collectFunctionSources,routeFunctions} from './function-sources.js';
9
9
  import {authoringPath,authoringFile,readAuthoringFile,publishAuthoringProject} from './authoring-files.js';
10
- import {assert} from './errors.js';
10
+ import {assert,ConfigError} from './errors.js';
11
11
 
12
12
 
13
13
  const emitted=(path )=>path.replace(/\.ts$/,'.js');
@@ -22,7 +22,9 @@ const SANDBOX_MODULE_LIMIT=128,SANDBOX_MODULE_BYTE_LIMIT=1048576,SANDBOX_TOTAL_B
22
22
  * plugins, package resolution, subprocesses or guest code execution are used. */
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
- const {default:ts}=await import('typescript');
25
+ const ts=await import('typescript').then(module=>module.default).catch(()=>{
26
+ throw new ConfigError('TypeScript authoring requires the optional typescript package (npm install --save-dev --save-exact typescript@6.0.3)');
27
+ });
26
28
  // Which trust levels a module has already been visited under, so a graph
27
29
  // reachable from both a sandboxed and a trusted route is validated under each
28
30
  // route's own rules exactly once instead of being refused outright. The
@@ -128,7 +130,7 @@ export async function buildTypeScriptProject(project ,output ,{dry
128
130
  for(const ref of [route.page,route.download])if(ref)await asset(ref.file);
129
131
  if(route.static)await directory(route.static.directory);
130
132
  }
131
- for(const ref of [loaded.document.site?.favicon,loaded.document.site?.llms])if(ref)await asset(ref);
133
+ for(const ref of [loaded.document.site?.favicon,loaded.document.site?.llms,loaded.document.site?.notFound])if(ref)await asset(ref);
132
134
  // Request fixtures describe behavior the emitted project must keep, so they travel with it.
133
135
  try{await asset('tests/requests.json');}catch(error){if(!(error instanceof Error && 'code' in error && error.code==='ENOENT'))throw error;}
134
136
  // Flatten includes using the loader's duplicate-checked route table. Original