@jimhoyd/urlcode 0.4.2 → 0.4.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/urlcode-authoring/SKILL.md +57 -14
- package/.claude/skills/urlcode-operations/SKILL.md +4 -0
- package/README.md +42 -18
- package/SECURITY.md +5 -3
- package/dist/BUILD-MANIFEST.json +30 -27
- package/dist/agent-context.js +82 -0
- package/dist/agents-guide.js +42 -42
- package/dist/authoring.js +12 -2
- package/dist/body-schema.js +159 -0
- package/dist/build-cloudflare.js +2 -0
- package/dist/capabilities.js +1 -1
- package/dist/cli.js +28 -12
- package/dist/config.js +74 -6
- package/dist/context.js +4 -6
- package/dist/errors.js +3 -1
- package/dist/examples.js +1 -1
- package/dist/extensions.js +60 -2
- package/dist/http-policy.js +19 -4
- package/dist/http-response.js +2 -2
- package/dist/init-with.js +71 -9
- package/dist/mcp.js +15 -2
- package/dist/pattern-guard.js +32 -0
- package/dist/policies/security.js +0 -0
- package/dist/project-tests.js +35 -11
- package/dist/readiness.js +184 -32
- package/dist/recipes.js +1 -1
- package/dist/router.js +17 -0
- package/dist/runtime.js +26 -3
- package/dist/scaffold.js +0 -0
- package/dist/server.js +26 -3
- package/dist/site.js +0 -0
- package/dist/tooling.js +2 -1
- package/dist/types/agent-context.d.ts +44 -0
- package/dist/types/authoring.d.ts +3 -1
- package/dist/types/body-schema.d.ts +58 -0
- package/dist/types/config.d.ts +10 -2
- package/dist/types/context.d.ts +1 -1
- package/dist/types/errors.d.ts +9 -1
- package/dist/types/examples.d.ts +1 -1
- package/dist/types/extensions.d.ts +47 -2
- package/dist/types/http-policy.d.ts +3 -0
- package/dist/types/init-with.d.ts +10 -1
- package/dist/types/pattern-guard.d.ts +10 -0
- package/dist/types/project-tests.d.ts +9 -0
- package/dist/types/readiness.d.ts +68 -0
- package/dist/types/recipes.d.ts +1 -1
- package/dist/types/runtime.d.ts +4 -0
- package/dist/types/server.d.ts +9 -1
- package/dist/types/tooling.d.ts +1 -0
- package/dist/types/types.d.ts +13 -0
- package/dist/types.js +10 -2
- package/dist/typescript-authoring.js +5 -3
- package/dist/verify-deployment.js +16 -10
- package/examples/body-validation/README.md +16 -0
- package/examples/body-validation/example.yaml +17 -0
- package/examples/body-validation/tests/requests.json +15 -0
- package/examples/body-validation/urlcode.yaml +34 -0
- package/examples/coverage-waiver/README.md +8 -0
- package/examples/coverage-waiver/example.yaml +16 -0
- package/examples/coverage-waiver/functions/notes.mjs +2 -0
- package/examples/coverage-waiver/tests/requests.json +3 -0
- package/examples/coverage-waiver/urlcode.yaml +10 -0
- package/examples/data-dir/README.md +39 -0
- package/examples/data-dir/data/welcome.txt +1 -0
- package/examples/data-dir/example.yaml +22 -0
- package/examples/data-dir/functions/note.mjs +18 -0
- package/examples/data-dir/tests/requests.json +6 -0
- package/examples/data-dir/urlcode.yaml +6 -0
- package/examples/lifecycle/README.md +15 -0
- package/examples/lifecycle/example.yaml +19 -0
- package/examples/lifecycle/functions/notes.mjs +29 -0
- package/examples/lifecycle/tests/requests.json +12 -0
- package/examples/lifecycle/urlcode.yaml +29 -0
- package/examples/not-found/README.md +10 -0
- package/examples/not-found/example.yaml +17 -0
- package/examples/not-found/public/404.html +3 -0
- package/examples/not-found/public/index.html +3 -0
- package/examples/not-found/tests/requests.json +7 -0
- package/examples/not-found/urlcode.yaml +7 -0
- package/examples/shared-blocks/README.md +11 -0
- package/examples/shared-blocks/example.yaml +17 -0
- package/examples/shared-blocks/tests/requests.json +7 -0
- package/examples/shared-blocks/urlcode.yaml +37 -0
- package/llms-full.txt +364 -49
- package/llms.txt +44 -6
- package/package.json +15 -26
- package/recipes/static-page/README.md +9 -0
- package/recipes/static-page/public/index.html +11 -0
- package/recipes/static-page/recipe.yaml +21 -0
- package/recipes/static-page/tests/requests.json +22 -0
- package/recipes/static-page/urlcode.yaml +7 -0
- package/recipes/static-plus-api/README.md +6 -0
- package/recipes/static-plus-api/urlcode.yaml +4 -0
- package/recipes/store-crud/README.md +53 -0
- package/recipes/store-crud/recipe.yaml +31 -0
- package/recipes/store-crud/tests/requests.json +18 -0
- package/recipes/store-crud/urlcode.yaml +18 -0
- package/schemas/urlcode.schema.json +125 -60
- package/skills/urlcode/SKILL.md +47 -27
- package/starters/default/AGENTS.md +43 -43
- package/starters/page/README.md +14 -0
- package/starters/page/public/index.html +12 -0
- package/starters/page/tests/requests.json +17 -0
- package/starters/page/urlcode.yaml +6 -0
- package/.claude-plugin/marketplace.json +0 -18
- package/CONTRIBUTING.md +0 -148
- package/ROADMAP.md +0 -70
- package/docs/AI-AUTHORING.md +0 -339
- package/docs/ASSETS.md +0 -107
- package/docs/AUTH-BACKUP.md +0 -32
- package/docs/AWS.md +0 -86
- package/docs/BEST-PRACTICES.md +0 -276
- package/docs/BULK.md +0 -79
- package/docs/CAPABILITIES.md +0 -192
- package/docs/CAPACITY.md +0 -305
- package/docs/CI-FOLLOWUP-2026-09-19.md +0 -97
- package/docs/CI-RELEASE-AUDIT-2026-09-19.md +0 -322
- package/docs/CI.md +0 -147
- package/docs/CLOUDFLARE.md +0 -109
- package/docs/CODEBASE-AUDIT-2026-09-20.md +0 -284
- package/docs/COMPLIANCE.md +0 -239
- package/docs/COMPOSING-A-SITE.md +0 -287
- package/docs/CONDITIONS.md +0 -74
- package/docs/CONTAINER-PROMOTION.md +0 -74
- package/docs/DEPLOYMENT-CHECKS.md +0 -108
- package/docs/DEVELOPMENT-PIPELINE.md +0 -304
- package/docs/EGRESS.md +0 -125
- package/docs/EXTENSIONS.md +0 -403
- package/docs/FRAMEWORK.md +0 -217
- package/docs/FUNCTION-SECURITY.md +0 -251
- package/docs/HTTP.md +0 -129
- package/docs/INSTALL.md +0 -128
- package/docs/INTERCHANGE.md +0 -134
- package/docs/LOAD-TESTING.md +0 -91
- package/docs/LOCAL-DEVELOPMENT.md +0 -102
- package/docs/MIDDLEWARE-EXAMPLES.md +0 -75
- package/docs/MIDDLEWARE.md +0 -102
- package/docs/MONITORING.md +0 -115
- package/docs/OBSERVABILITY.md +0 -222
- package/docs/OPEN-DECISIONS.md +0 -149
- package/docs/OPERATIONAL-PROOF.md +0 -41
- package/docs/OPERATIONS.md +0 -201
- package/docs/ORGANIZATION.md +0 -135
- package/docs/PERFORMANCE.md +0 -72
- package/docs/PLUGINS.md +0 -271
- package/docs/POLICIES.md +0 -211
- package/docs/PRERENDER.md +0 -245
- package/docs/PROJECT-DIRECTION.md +0 -118
- package/docs/PROVIDER-VERIFICATION.md +0 -84
- package/docs/READINESS.md +0 -153
- package/docs/README.md +0 -93
- package/docs/RECIPES.md +0 -99
- package/docs/RELEASE-0.4.0-alpha.3.md +0 -50
- package/docs/RELEASE-0.4.1.md +0 -73
- package/docs/RELEASE-0.4.2.md +0 -30
- package/docs/RELEASE-READINESS.md +0 -146
- package/docs/RELEASE-SECURITY.md +0 -100
- package/docs/RESILIENCE.md +0 -161
- package/docs/ROUTING.md +0 -92
- package/docs/SANDBOX-REVIEW.md +0 -72
- package/docs/SCAFFOLDING.md +0 -70
- package/docs/SECURITY-AUDIT.md +0 -164
- package/docs/SITE.md +0 -150
- package/docs/SPECIFICATION.md +0 -359
- package/docs/SPIKE-AI-FRAMEWORK-BENCHMARK.md +0 -288
- package/docs/SPIKE-BUSINESS-SUITE.md +0 -1029
- package/docs/SPIKE-CORE-LAYERING.md +0 -368
- package/docs/SPIKE-DEFAULT-TRUST-MODEL.md +0 -207
- package/docs/STANDARDS.md +0 -311
- package/docs/STARTERS.md +0 -83
- package/docs/STATIC.md +0 -105
- package/docs/TOOLING.md +0 -298
- package/docs/TUNNELS.md +0 -72
- package/docs/TYPESCRIPT-AUTHORING.md +0 -87
- package/docs/TYPESCRIPT.md +0 -123
- package/docs/VERCEL.md +0 -114
- package/docs/VERSION-ALIGNMENT.md +0 -88
- package/docs/YAML-GUIDE.md +0 -57
- package/docs/YAML-REFERENCE.md +0 -449
- package/docs/archive/2026-09-19/EXTENSION-IMPLEMENTATION.md +0 -68
- package/docs/archive/2026-09-19/MANAGEMENT-SECURITY.md +0 -102
- package/docs/archive/2026-09-19/NEXT-PHASE-PLAN.md +0 -108
- package/docs/archive/2026-09-19/NEXT-STEPS.md +0 -646
- package/docs/archive/2026-09-19/OPEN-DECISIONS.md +0 -277
- package/docs/archive/2026-09-19/RELEASE-SECURITY.md +0 -186
- package/docs/archive/2026-09-19/ROADMAP.md +0 -387
- package/docs/archive/2026-09-19/SPIKE-EXTENSION-MODEL.md +0 -430
- package/docs/archive/2026-09-19/SPIKE-EXTENSIONS.md +0 -492
- package/docs/archive/2026-09-19/SPIKE-LAMBDA-COMPILE.md +0 -365
- package/docs/archive/2026-09-19/SPIKE-MONOREPO.md +0 -778
- package/docs/archive/2026-09-19/USABILITY-REVIEW.md +0 -139
- package/docs/archive/2026-09-20/OPEN-DECISIONS-COMPLETED.md +0 -116
- package/docs/archive/README.md +0 -29
- package/docs/policies/agents.md +0 -182
- package/docs/policies/cache.md +0 -152
- package/docs/policies/compression.md +0 -169
- package/docs/policies/contract.md +0 -52
- package/docs/policies/hardened.md +0 -56
- package/docs/policies/interoperability.md +0 -169
- package/docs/policies/operations.md +0 -45
- package/docs/policies/security.md +0 -161
- package/docs/policies/throttle.md +0 -103
- package/docs/yaml/assets.md +0 -36
- package/docs/yaml/conditions.md +0 -20
- package/docs/yaml/functions.md +0 -168
- package/docs/yaml/middleware.md +0 -31
- package/docs/yaml/organization.md +0 -74
- package/docs/yaml/policies.md +0 -37
- package/docs/yaml/redirects.md +0 -64
- package/docs/yaml/responses.md +0 -57
- package/docs/yaml/site.md +0 -24
- package/packaging/claude-plugin/.claude-plugin/plugin.json +0 -19
- package/packaging/claude-plugin/skills/urlcode-authoring/SKILL.md +0 -130
- package/packaging/claude-plugin/skills/urlcode-operations/SKILL.md +0 -108
package/dist/types/config.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type {
|
|
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
|
-
|
|
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";
|
package/dist/types/context.d.ts
CHANGED
|
@@ -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
|
|
65
|
+
/** Estimated size of the shipped offline documentation bundle, for comparison with an emitted context. */
|
|
66
66
|
export declare function documentationTokens(): Promise<number>;
|
package/dist/types/errors.d.ts
CHANGED
|
@@ -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
|
-
|
|
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;
|
package/dist/types/examples.d.ts
CHANGED
|
@@ -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
|
/**
|
|
@@ -82,6 +82,27 @@ export interface ExtensionHookContract {
|
|
|
82
82
|
inputSchema: object;
|
|
83
83
|
outputSchema?: object;
|
|
84
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
|
+
}
|
|
85
106
|
export type LoadedExtensionHooks<T extends string = string> = Partial<Record<T, (input: unknown) => unknown>>;
|
|
86
107
|
/** Shared schema for project hook references. Omission means trusted execution. */
|
|
87
108
|
export declare const extensionHookReferenceSchema: {
|
|
@@ -133,6 +154,8 @@ export interface RuntimeExtension {
|
|
|
133
154
|
immutableAssets?: ExtensionImmutableAssets;
|
|
134
155
|
/** Project customization points, exposed by CLI/MCP for authors and agents. */
|
|
135
156
|
hooks?: readonly ExtensionHookContract[];
|
|
157
|
+
/** Supported project-owned customization surfaces, exposed by CLI/MCP. */
|
|
158
|
+
authoring?: ExtensionAuthoringContract;
|
|
136
159
|
/**
|
|
137
160
|
* Reviewed, operator-declared cache sensitivity for `policies.extensions.<name>`
|
|
138
161
|
* routes (never for an `extension:` mount, which is always treated as
|
|
@@ -160,8 +183,15 @@ export interface ScaffoldRequest {
|
|
|
160
183
|
project: string;
|
|
161
184
|
/** Absolute path of the combined host module core writes, `<directory>/host.mjs`. */
|
|
162
185
|
hostFile: string;
|
|
163
|
-
/** Every extension name being scaffolded together, in
|
|
186
|
+
/** Every extension name being scaffolded together, including this one, in a canonical (sorted) order that is independent of the `--with` spelling. */
|
|
164
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[];
|
|
165
195
|
}
|
|
166
196
|
export interface ScaffoldFile {
|
|
167
197
|
path: string;
|
|
@@ -171,6 +201,21 @@ export interface ScaffoldFile {
|
|
|
171
201
|
export interface ScaffoldResult {
|
|
172
202
|
/** Must equal the requested name. */
|
|
173
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[];
|
|
174
219
|
/** Fragments merged into the project's top-level `extensions` and `routes`; duplicate keys are refused. */
|
|
175
220
|
extensions: Record<string, unknown>;
|
|
176
221
|
routes: Record<string, unknown>;
|
|
@@ -181,7 +226,7 @@ export interface ScaffoldResult {
|
|
|
181
226
|
hostClose?: string[];
|
|
182
227
|
/** Files written relative to `directory` with their modes; never inside the project, never overwriting. */
|
|
183
228
|
files: ScaffoldFile[];
|
|
184
|
-
/** Markdown appended to README.md under a heading core adds; the numbered steps merged in
|
|
229
|
+
/** Markdown appended to README.md under a heading core adds; the numbered steps merged in the resolved order. */
|
|
185
230
|
readme: string;
|
|
186
231
|
nextSteps: string[];
|
|
187
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,
|
|
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;
|
|
@@ -60,8 +85,12 @@ export interface AuditReport {
|
|
|
60
85
|
/** Empty when `ready`; otherwise one stable code per failed condition:
|
|
61
86
|
* `no-active-routes`, `route-count-mismatch`, `failed-checks`, `uncovered-route-methods`. */
|
|
62
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`. */
|
|
63
90
|
counts: {
|
|
64
91
|
configured: number;
|
|
92
|
+
declared: number;
|
|
93
|
+
generated: number;
|
|
65
94
|
active: number;
|
|
66
95
|
disabled: number;
|
|
67
96
|
expired: number;
|
|
@@ -78,6 +107,24 @@ export interface AuditReport {
|
|
|
78
107
|
route: string;
|
|
79
108
|
method: string;
|
|
80
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
|
+
}[];
|
|
81
128
|
policies: Record<string, PolicyInventory>;
|
|
82
129
|
compliance: ComplianceReport | null;
|
|
83
130
|
/** Non-blocking `audit` observations, e.g. a route that looks webhook-shaped
|
|
@@ -126,7 +173,28 @@ export declare const hasRedirect: (route: CompiledRoute) => route is CompiledRou
|
|
|
126
173
|
};
|
|
127
174
|
export declare const probeAgent = "Mozilla/5.0 (compatible; RouteProbe/0.1)";
|
|
128
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. */
|
|
129
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>;
|
|
130
198
|
export declare function benchmarkTarget(value: string): BenchmarkTarget;
|
|
131
199
|
export declare function hit(app: AuditableApp, test: RequestCase, agent: Agent, target?: BenchmarkTarget): Promise<HitResult>;
|
|
132
200
|
export declare function auditProject(app: AuditableApp, { expectRoutes, log, compliance }?: AuditOptions): Promise<AuditReport>;
|
package/dist/types/recipes.d.ts
CHANGED
|
@@ -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>;
|
package/dist/types/runtime.d.ts
CHANGED
|
@@ -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 {
|
package/dist/types/server.d.ts
CHANGED
|
@@ -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
|
-
|
|
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>;
|
package/dist/types/tooling.d.ts
CHANGED
package/dist/types/types.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
@@ -5,7 +5,7 @@ import { randomBytes } from 'node:crypto';
|
|
|
5
5
|
import { assert } from './errors.js';
|
|
6
6
|
import { createRuntime } from './runtime.js';
|
|
7
7
|
|
|
8
|
-
import { benchmarkTarget, hit,
|
|
8
|
+
import { benchmarkTarget, hit, readFixtures, runFixtures, isStepsFixture, probeAgent } from './readiness.js';
|
|
9
9
|
|
|
10
10
|
import { runCompliance, severities } from './compliance.js';
|
|
11
11
|
|
|
@@ -45,7 +45,7 @@ const CONCURRENCY = 4;
|
|
|
45
45
|
const MAX_REQUESTS = 10000;
|
|
46
46
|
const BODY_LIMIT = 1048576;
|
|
47
47
|
const SNIPPET = 200;
|
|
48
|
-
const siteTypes = { 'site.robots': 'text/plain', 'site.sitemap': 'application/xml', 'site.securityTxt': 'text/plain', 'site.llms': 'text/plain' };
|
|
48
|
+
const siteTypes = { 'site.robots': 'text/plain', 'site.sitemap': 'application/xml', 'site.securityTxt': 'text/plain', 'site.llms': 'text/plain', 'site.notFound': 'text/html' };
|
|
49
49
|
const isRecord = (value ) => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
50
50
|
const snippet = (body ) => body.length > SNIPPET ? `${body.length} bytes: ${body.subarray(0, SNIPPET).toString('utf8')}` : body.toString('utf8');
|
|
51
51
|
const tlsCode = /CERT|TLS|SSL|SELF_SIGNED/;
|
|
@@ -129,12 +129,12 @@ export async function verifyDeployment(project , { target, origin, expect
|
|
|
129
129
|
const routes = { local: runtime.count, observed: null, expected: expectRoutes ?? null };
|
|
130
130
|
let complianceReport = null;
|
|
131
131
|
try {
|
|
132
|
-
const plan = runtime.testPlan(), fixtures = await
|
|
132
|
+
const plan = runtime.testPlan(), fixtures = await readFixtures(runtime.root, true);
|
|
133
133
|
const loaded = await loadDocument(runtime.root);
|
|
134
134
|
await applySite(loaded, { origin: publicOrigin, log: () => {} });
|
|
135
|
-
const
|
|
135
|
+
const fixtureRequests = fixtures.reduce((sum, fixture) => sum + (isStepsFixture(fixture) ? fixture.steps.filter(step => !('restart' in step)).length : 1), 0);
|
|
136
136
|
const literal = plan.inventory.filter(route => route.state === 'active' && !route.path.includes('{'));
|
|
137
|
-
assert(4 + cases.length + literal.length * 4 <= MAX_REQUESTS, `Verification would send more than ${MAX_REQUESTS} requests`);
|
|
137
|
+
assert(4 + plan.cases.length + fixtureRequests + literal.length * 4 <= MAX_REQUESTS, `Verification would send more than ${MAX_REQUESTS} requests`);
|
|
138
138
|
check(expectRoutes === undefined || plan.inventory.length === expectRoutes, { check: 'probes', severity: 'high', message: 'configured route count differs from --expect-routes', expected: String(expectRoutes), observed: String(plan.inventory.length) });
|
|
139
139
|
|
|
140
140
|
// 1. Probes. A transport failure on the health probe ends the run: nothing
|
|
@@ -162,13 +162,19 @@ export async function verifyDeployment(project , { target, origin, expect
|
|
|
162
162
|
// 2. Fixtures and generated cases, exactly as `urlcode test --target`
|
|
163
163
|
// would send them: sequentially, through hit(), against the target.
|
|
164
164
|
const stub = { address: { address: '127.0.0.1', family: 'IPv4', port: 0 }, root: runtime.root, testPlan: () => plan };
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
165
|
+
// A fixture with a restart step cannot run here: a live deployment is not ours to close and
|
|
166
|
+
// start. It is skipped as a whole, never partly, and named in the report and the log.
|
|
167
|
+
const record = (n , source , test , original , result ) => {
|
|
168
168
|
let route ; try { route = plan.resolve(test.path); } catch { /* an invalid-path negative fixture */ }
|
|
169
|
-
|
|
169
|
+
// The label prints the fixture as written: a substituted path may hold a captured value.
|
|
170
|
+
const label = `${source} case ${n} ${original.method ?? 'GET'} ${original.path}`;
|
|
170
171
|
check(result.pass, { check: 'fixtures', severity: 'high', ...(route === undefined ? {} : { route }), message: result.error ? `${label}: ${result.error} error` : `${label}: response did not match the case`, expected: String(test.status), observed: String(result.status) });
|
|
171
|
-
}
|
|
172
|
+
};
|
|
173
|
+
for (const [i, test] of plan.cases.entries()) { requests++; record(i + 1, 'generated', test, test, await hit(stub, test, agent, destination)); }
|
|
174
|
+
await runFixtures(fixtures, {
|
|
175
|
+
app: stub, agent, target: destination,
|
|
176
|
+
skipped: (fixture, reason) => { notes.push(`fixture ${fixture} ${reason}; none of its requests were sent and it was not verified`); log({ event: 'skipped', check: 'fixtures', fixture, reason: 'restart' }); },
|
|
177
|
+
}, step => { if (step.result.error !== 'skipped' && step.result.error !== 'unresolved') requests++; record(step.case, 'fixture', step.test, step.original, step.result); }, plan.cases.length + 1);
|
|
172
178
|
|
|
173
179
|
// 3. Declared versus observed, per active literal route.
|
|
174
180
|
await each(literal, async route => {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Body validation
|
|
2
|
+
|
|
3
|
+
Declares the shape of a JSON request body and two input formats in YAML, so a
|
|
4
|
+
route needs no hand-written validation code.
|
|
5
|
+
|
|
6
|
+
- `request.body.schema` accepts a small JSON Schema subset. A body that breaks it
|
|
7
|
+
answers **422**; malformed JSON is still 400 and a wrong media type 415.
|
|
8
|
+
- Parameter `format: uuid` and `pattern` reject bad path and query values with 400.
|
|
9
|
+
- `pattern` must set `maxLength` (at most 128) and is refused when it repeats a
|
|
10
|
+
group, uses lookaround or a backreference, or has more than three unbounded
|
|
11
|
+
quantifiers. See [HTTP configuration](../../docs/HTTP.md).
|
|
12
|
+
|
|
13
|
+
The 422 answer is plain text listing only paths the schema declares; it never
|
|
14
|
+
echoes what the client sent. A client that sends `Accept: application/json`
|
|
15
|
+
receives a structured body instead, with each issue's `pointer`, `keyword` and
|
|
16
|
+
expected constraint (never the value); `*/*` and browsers keep the text.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
id: body-validation
|
|
2
|
+
description: Declarative JSON body validation with request.body.schema, and parameter format uuid and a bounded pattern, with no validation code.
|
|
3
|
+
tags: [request, body, validation, schema, json, parameters, uuid, pattern, "422"]
|
|
4
|
+
complexity: intermediate
|
|
5
|
+
capabilities: [enabled, methods, parameters, request.body, respond]
|
|
6
|
+
targets: {self-hosted: compatible, aws: compatible, vercel: compatible, cloudflare: compatible, static: refused}
|
|
7
|
+
routes: 3
|
|
8
|
+
files: [urlcode.yaml, tests/requests.json, README.md]
|
|
9
|
+
tests:
|
|
10
|
+
fixtures: tests/requests.json
|
|
11
|
+
commands:
|
|
12
|
+
- urlcode validate --local --project examples/body-validation
|
|
13
|
+
- urlcode test --project examples/body-validation
|
|
14
|
+
- urlcode audit --project examples/body-validation --expect-routes 3
|
|
15
|
+
behavior:
|
|
16
|
+
- POST /todos with a valid JSON body answers 201; a body that breaks the schema answers 422 with the declared paths that failed; malformed JSON answers 400; a wrong media type answers 415
|
|
17
|
+
- a path id that is not a uuid, or a slug that does not match the declared pattern, answers 400
|