@jimhoyd/urlcode 0.4.6 → 0.4.8
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 +8 -0
- package/README.md +28 -5
- package/dist/BUILD-MANIFEST.json +10 -9
- package/dist/agents-guide.js +1 -1
- package/dist/build-cloudflare.js +4 -2
- package/dist/cli.js +29 -9
- package/dist/cloudflare.js +6 -3
- package/dist/context.js +71 -1
- package/dist/extension-artifacts.js +140 -0
- package/dist/index.js +2 -2
- package/dist/mcp.js +11 -4
- package/dist/site.js +19 -1
- package/dist/tooling.js +2 -2
- package/dist/types/cloudflare.d.ts +1 -0
- package/dist/types/context.d.ts +44 -0
- package/dist/types/extension-artifacts.d.ts +74 -0
- package/dist/types/index.d.ts +2 -2
- package/dist/types/site.d.ts +2 -0
- package/dist/types/tooling.d.ts +2 -2
- package/llms-full.txt +103 -11
- package/llms.txt +22 -2
- package/package.json +5 -6
- package/recipes/redirect/README.md +19 -5
- package/recipes/redirect/recipe.yaml +12 -10
- package/recipes/redirect/tests/requests.json +22 -0
- package/recipes/redirect/urlcode.yaml +11 -1
- package/skills/urlcode/SKILL.md +2 -0
- package/starters/default/AGENTS.md +1 -1
package/dist/mcp.js
CHANGED
|
@@ -2,12 +2,13 @@ import {realpath} from 'node:fs/promises';
|
|
|
2
2
|
|
|
3
3
|
import {once} from 'node:events';
|
|
4
4
|
import {Ajv} from 'ajv';
|
|
5
|
-
import {inspectProject,validateProject,explainRoute,getCapabilities,getCapability,getSchemaFragment,previewImport,previewExport,listRecipes,showRecipe,searchRecipes,searchExamples,describeExtensions,buildContext} from './tooling.js';
|
|
5
|
+
import {inspectProject,validateProject,explainRoute,getCapabilities,getCapability,getSchemaFragment,previewImport,previewExport,listRecipes,showRecipe,searchRecipes,searchExamples,describeExtensions,buildContext,buildTaskContext} from './tooling.js';
|
|
6
6
|
import {loadOperatorHost} from './operator-host.js';
|
|
7
7
|
import {buildManifest} from './manifest.js';
|
|
8
8
|
|
|
9
9
|
import {authoringDefinitions,callAuthoringTool} from './mcp-authoring.js';
|
|
10
10
|
import {listSkills,getSkill,searchDocs,getExample,validateYaml,explainError} from './agent-context.js';
|
|
11
|
+
import {describeArtifactCache,readArtifactMember} from './extension-artifacts.js';
|
|
11
12
|
const protocolVersion='2025-11-25';
|
|
12
13
|
const maxBytes=1048576;
|
|
13
14
|
const text={type:'string',maxLength:8192};
|
|
@@ -32,7 +33,9 @@ const definitions=[
|
|
|
32
33
|
{name:'get_example',description:'Return the README and urlcode.yaml from one bundled runnable example.',properties:{name:{type:'string',maxLength:64}},required:['name']},
|
|
33
34
|
{name:'validate_yaml',description:'Validate supplied URLCode YAML syntax and schema only. It never reads includes, source files, bindings or a project directory.',properties:{yaml:{type:'string',maxLength:524288}},required:['yaml']},
|
|
34
35
|
{name:'explain_error',description:'Give deterministic next-step guidance for supplied URLCode validation output.',properties:{error:{type:'string',maxLength:8192}},required:['error']},
|
|
35
|
-
{name:'
|
|
36
|
+
{name:'get_extension_artifacts',description:'Validate and list the project\'s locked declarative extension artifacts and their allowlisted files. Artifacts are inert data and do not activate extension code.',properties:{}},
|
|
37
|
+
{name:'get_extension_artifact',description:'Read one bounded JSON or Markdown file from a verified cached declarative extension artifact. The artifact name and member path must exist in the project lock/cache.',properties:{name:{type:'string',maxLength:64},path:{type:'string',maxLength:128}},required:['name','path']},
|
|
38
|
+
{name:'get_context',description:'Emit the compact project context an authoring agent needs: versions, project summary, constraints, target support and exact commands, derived from the compiled project. Pass `task: "redirects"` for a bounded, redirect-focused call instead (supported/gap shapes, exact YAML, this project\'s redirects). Optional token budget drops sections in a fixed order.',properties:{target:text,task:{enum:['redirects']},budget:{type:'integer',minimum:1}}},
|
|
36
39
|
];
|
|
37
40
|
// Only the operator's own --host-file exposes registered extension contracts; no tool argument can name one.
|
|
38
41
|
const hostDefinition={name:'get_extensions',description:'List operator-registered extension contracts, schemas, hooks, and supported project-owned customization surfaces with fast checks; use these before generating replacement framework code. Activates nothing.',properties:{}};
|
|
@@ -76,8 +79,12 @@ export async function serveMcp(options ) {
|
|
|
76
79
|
case 'get_example':return getExample(args.name );
|
|
77
80
|
case 'validate_yaml':return validateYaml(args.yaml );
|
|
78
81
|
case 'explain_error':return explainError(args.error );
|
|
82
|
+
case 'get_extension_artifacts':return describeArtifactCache(project);
|
|
83
|
+
case 'get_extension_artifact':return readArtifactMember(project,args.name ,args.path );
|
|
79
84
|
case 'get_extensions':return describeExtensions(project,host.extensions??[]);
|
|
80
|
-
case 'get_context':return
|
|
85
|
+
case 'get_context':return typeof args.task==='string'
|
|
86
|
+
?buildTaskContext(project,args.task,{...(typeof args.budget==='number'?{budget:args.budget}:{})})
|
|
87
|
+
:buildContext(project,{projectFlag:'.',...(typeof args.target==='string'?{target:args.target}:{}),...(typeof args.budget==='number'?{budget:args.budget}:{})});
|
|
81
88
|
default:if(authoring)return callAuthoringTool(project,name,args,options.origin);throw new Error('Unknown tool');
|
|
82
89
|
}
|
|
83
90
|
};
|
|
@@ -91,7 +98,7 @@ export async function serveMcp(options ) {
|
|
|
91
98
|
if(message.method==='initialize') {
|
|
92
99
|
if(initialized){await error(id,-32600,'Already initialized');return;}
|
|
93
100
|
if(typeof params.protocolVersion!=='string'||!object(params.capabilities)||!object(params.clientInfo)||typeof params.clientInfo.name!=='string'||typeof params.clientInfo.version!=='string'){await error(id,-32602,'Invalid initialize params');return;}
|
|
94
|
-
initialized=true;await send({jsonrpc:'2.0',id,result:{protocolVersion,capabilities:{tools:{}},serverInfo:{name:'urlcode',version:'0.4.
|
|
101
|
+
initialized=true;await send({jsonrpc:'2.0',id,result:{protocolVersion,capabilities:{tools:{}},serverInfo:{name:'urlcode',version:'0.4.8'}}});return;
|
|
95
102
|
}
|
|
96
103
|
if(message.method==='ping'){await send({jsonrpc:'2.0',id,result:{}});return;}
|
|
97
104
|
if(!ready){await error(id,-32002,'Initialize first');return;}
|
package/dist/site.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { stat, readdir, lstat } from 'node:fs/promises';
|
|
1
|
+
import { stat, readdir, lstat, readFile } from 'node:fs/promises';
|
|
2
2
|
import { join, extname } from 'node:path';
|
|
3
3
|
import { assert, ConfigError } from './errors.js';
|
|
4
4
|
import { safeFile } from './config.js';
|
|
@@ -233,3 +233,21 @@ export async function applySite(loaded , options = {
|
|
|
233
233
|
for (const [path, route] of Object.entries(generated)) loaded.routes[path] = route;
|
|
234
234
|
return generated;
|
|
235
235
|
}
|
|
236
|
+
|
|
237
|
+
// The Worker has no filesystem and no asset binding, so the one not-found page
|
|
238
|
+
// is read here and carried inline as a respond route at /404.html. The page is
|
|
239
|
+
// bounded and static: no templating, no request data. It is text/html; the
|
|
240
|
+
// artifact is JSON, which does the escaping, and a body that is not valid
|
|
241
|
+
// UTF-8 is refused rather than silently altered.
|
|
242
|
+
export const notFoundInlineLimit = 65536;
|
|
243
|
+
export async function inlineNotFound(loaded ) {
|
|
244
|
+
const site = loaded.document.site, path = generatedPaths.notFound;
|
|
245
|
+
const route = loaded.routes[path];
|
|
246
|
+
if (!site?.notFound || route?.generated !== 'site.notFound') return false;
|
|
247
|
+
const file = await safeFile(loaded.root, route.page?.file);
|
|
248
|
+
assert((await stat(file)).size <= notFoundInlineLimit, `site.notFound exceeds ${notFoundInlineLimit} bytes; the Cloudflare Worker carries it inline, so keep the page under 64 KiB`);
|
|
249
|
+
let text ;
|
|
250
|
+
try { text = new TextDecoder('utf-8', { fatal: true }).decode(await readFile(file)); } catch { throw new ConfigError('site.notFound must be valid UTF-8 to be carried inline in the Worker'); }
|
|
251
|
+
loaded.routes[path] = { respond: { text }, response: { headers: { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' } }, description: route.description ?? 'generated by site.notFound', generated: 'site.notFound' };
|
|
252
|
+
return true;
|
|
253
|
+
}
|
package/dist/tooling.js
CHANGED
|
@@ -21,8 +21,8 @@ export {getCapability} from './capability-query.js';
|
|
|
21
21
|
export {getSchemaFragment,schemaPathNames} from './schema-query.js';
|
|
22
22
|
|
|
23
23
|
export {listRecipes,showRecipe,searchRecipes,listExamples,searchExamples};
|
|
24
|
-
export {buildContext,renderContext,estimateTokens,documentationTokens} from './context.js';
|
|
25
|
-
|
|
24
|
+
export {buildContext,renderContext,estimateTokens,documentationTokens,buildTaskContext,renderTaskContext,contextTasks} from './context.js';
|
|
25
|
+
|
|
26
26
|
|
|
27
27
|
/** `extensions` are operator registrations from a host file; explain reports whether each requirement has a provider. Nothing is activated. */
|
|
28
28
|
|
package/dist/types/context.d.ts
CHANGED
|
@@ -64,3 +64,47 @@ export declare function renderContext(context: ProjectContext): string;
|
|
|
64
64
|
export declare function buildContext(project: string, options?: ContextOptions): Promise<ProjectContext>;
|
|
65
65
|
/** Estimated size of the shipped offline documentation bundle, for comparison with an emitted context. */
|
|
66
66
|
export declare function documentationTokens(): Promise<number>;
|
|
67
|
+
/** Tasks `--task` / MCP `get_context` accept. Each is fixed guidance plus the project's own facts for that task. */
|
|
68
|
+
export declare const contextTasks: readonly ["redirects"];
|
|
69
|
+
export type ContextTask = typeof contextTasks[number];
|
|
70
|
+
export interface TaskShape {
|
|
71
|
+
need: string;
|
|
72
|
+
support: 'supported' | 'gap';
|
|
73
|
+
/** Exact YAML to merge into urlcode.yaml (`routes` entries or `site`); absent for a gap. */
|
|
74
|
+
yaml?: Record<string, unknown>;
|
|
75
|
+
/** The rule that applies, or the exact validation error a gap produces. */
|
|
76
|
+
note?: string;
|
|
77
|
+
/** For a gap: the tested declarative alternative. */
|
|
78
|
+
workaround?: string;
|
|
79
|
+
}
|
|
80
|
+
/** Established by running `urlcode validate` and `urlcode test` on each shape; test/context.test.ts compiles every `yaml` entry so this cannot drift from the runtime. */
|
|
81
|
+
export declare const redirectShapes: TaskShape[];
|
|
82
|
+
export interface TaskContext {
|
|
83
|
+
urlcode: string;
|
|
84
|
+
schema: '1';
|
|
85
|
+
task: ContextTask;
|
|
86
|
+
shapes?: TaskShape[];
|
|
87
|
+
project?: {
|
|
88
|
+
entry: string;
|
|
89
|
+
routes: number;
|
|
90
|
+
redirects: {
|
|
91
|
+
path: string;
|
|
92
|
+
status: number;
|
|
93
|
+
url: string;
|
|
94
|
+
}[];
|
|
95
|
+
site: string[];
|
|
96
|
+
};
|
|
97
|
+
recipe?: string;
|
|
98
|
+
commands?: Record<string, string>;
|
|
99
|
+
omitted?: string[];
|
|
100
|
+
}
|
|
101
|
+
export declare function renderTaskContext(context: TaskContext): string;
|
|
102
|
+
/**
|
|
103
|
+
* One bounded call for a task: fixed guidance plus this project's facts for that task. Same compiler as buildContext;
|
|
104
|
+
* a directory without urlcode.yaml still gets the guidance, any other load failure propagates.
|
|
105
|
+
*/
|
|
106
|
+
export declare function buildTaskContext(project: string, task: string, options?: {
|
|
107
|
+
budget?: number | undefined;
|
|
108
|
+
hostFile?: string | undefined;
|
|
109
|
+
projectFlag?: string | undefined;
|
|
110
|
+
}): Promise<TaskContext>;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/** Offline, declarative extension bundles. These are deliberately not Node packages. */
|
|
2
|
+
export declare const ARTIFACT_REPOSITORY = "jimhoyd-com/urlcode";
|
|
3
|
+
export declare const ARTIFACT_WORKFLOW = "jimhoyd-com/urlcode/.github/workflows/extension-artifacts.yml";
|
|
4
|
+
export interface ArtifactEntry {
|
|
5
|
+
name: string;
|
|
6
|
+
version: string;
|
|
7
|
+
asset: string;
|
|
8
|
+
sha256: string;
|
|
9
|
+
kind: 'declarative';
|
|
10
|
+
}
|
|
11
|
+
export interface Catalog {
|
|
12
|
+
format: 1;
|
|
13
|
+
tag: string;
|
|
14
|
+
commit: string;
|
|
15
|
+
artifacts: ArtifactEntry[];
|
|
16
|
+
revoked: {
|
|
17
|
+
sha256: string;
|
|
18
|
+
reason: string;
|
|
19
|
+
}[];
|
|
20
|
+
}
|
|
21
|
+
export interface LockedArtifact extends ArtifactEntry {
|
|
22
|
+
catalog: {
|
|
23
|
+
tag: string;
|
|
24
|
+
commit: string;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export interface ExtensionLock {
|
|
28
|
+
format: 1;
|
|
29
|
+
artifacts: LockedArtifact[];
|
|
30
|
+
}
|
|
31
|
+
/** Parse an untrusted catalog only after its GitHub attestation was verified by the caller. */
|
|
32
|
+
export declare function parseCatalog(bytes: Uint8Array, requestedTag: string): Catalog;
|
|
33
|
+
export declare function extractArtifact(bytes: Uint8Array, entry: ArtifactEntry, destination: string): Promise<void>;
|
|
34
|
+
export declare function readLock(project: string): Promise<ExtensionLock>;
|
|
35
|
+
export declare function writeLock(project: string, lock: ExtensionLock): Promise<void>;
|
|
36
|
+
export declare function cachePath(project: string, sha256: string): string;
|
|
37
|
+
export interface ReleaseAsset {
|
|
38
|
+
name: string;
|
|
39
|
+
url: string;
|
|
40
|
+
}
|
|
41
|
+
export interface ArtifactTransport {
|
|
42
|
+
release(tag: string): Promise<ReleaseAsset[]>;
|
|
43
|
+
download(url: string): Promise<Uint8Array>;
|
|
44
|
+
attest(path: string, release: string): Promise<void>;
|
|
45
|
+
}
|
|
46
|
+
/** The default transport accepts only GitHub Release asset URLs and verifies every downloaded subject. */
|
|
47
|
+
export declare const githubTransport: ArtifactTransport;
|
|
48
|
+
export declare function resolveCatalog(release: string, transport?: ArtifactTransport): Promise<{
|
|
49
|
+
catalog: Catalog;
|
|
50
|
+
assets: ReleaseAsset[];
|
|
51
|
+
}>;
|
|
52
|
+
export declare function installArtifact(project: string, release: string, artifactName: string, transport?: ArtifactTransport): Promise<ExtensionLock>;
|
|
53
|
+
export declare function inspectArtifacts(project: string): Promise<{
|
|
54
|
+
lock: ExtensionLock;
|
|
55
|
+
cached: string[];
|
|
56
|
+
missing: string[];
|
|
57
|
+
invalid: string[];
|
|
58
|
+
}>;
|
|
59
|
+
/** Read-only inventory for authoring tools. Paths come from the verified archive, never from an arbitrary filesystem argument. */
|
|
60
|
+
export declare function describeArtifactCache(project: string): Promise<{
|
|
61
|
+
format: 1;
|
|
62
|
+
artifacts: (LockedArtifact & {
|
|
63
|
+
status: 'cached' | 'missing' | 'invalid';
|
|
64
|
+
files: string[];
|
|
65
|
+
})[];
|
|
66
|
+
}>;
|
|
67
|
+
/** Return one bounded text/JSON member from a verified cached artifact for MCP/agent consumers. */
|
|
68
|
+
export declare function readArtifactMember(project: string, artifactName: string, path: string): Promise<{
|
|
69
|
+
format: 1;
|
|
70
|
+
artifact: LockedArtifact;
|
|
71
|
+
path: string;
|
|
72
|
+
mediaType: 'application/json' | 'text/markdown';
|
|
73
|
+
content: unknown;
|
|
74
|
+
}>;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -18,8 +18,8 @@ export { buildTypeScriptProject } from './typescript-authoring.ts';
|
|
|
18
18
|
export type { TypeScriptBuildReport } from './typescript-authoring.ts';
|
|
19
19
|
export { importBulkProject } from './bulk.ts';
|
|
20
20
|
export type { BulkFormat, BulkFilePlan, BulkImportReport } from './bulk.ts';
|
|
21
|
-
export { inspectProject, validateProject, explainRoute, explainProject, previewImport, previewExport, getCapability, getSchemaFragment, schemaPathNames, inspectExtensions, describeExtensions, buildContext, renderContext, estimateTokens, documentationTokens } from './tooling.ts';
|
|
22
|
-
export type { InspectOptions, RouteExplanation, RouteMiss, ExplainedHandler, ExplainedCache, ExplainedExtensionRequirement, ExtensionProvider, TargetSupport, CapabilityEntry, CapabilityUsage, SchemaFragment, ExtensionInspection, ContextOptions, ProjectContext, ContextSection } from './tooling.ts';
|
|
21
|
+
export { inspectProject, validateProject, explainRoute, explainProject, previewImport, previewExport, getCapability, getSchemaFragment, schemaPathNames, inspectExtensions, describeExtensions, buildContext, renderContext, estimateTokens, documentationTokens, buildTaskContext, renderTaskContext, contextTasks } from './tooling.ts';
|
|
22
|
+
export type { InspectOptions, RouteExplanation, RouteMiss, ExplainedHandler, ExplainedCache, ExplainedExtensionRequirement, ExtensionProvider, TargetSupport, CapabilityEntry, CapabilityUsage, SchemaFragment, ExtensionInspection, ContextOptions, ProjectContext, ContextSection, ContextTask, TaskContext, TaskShape } from './tooling.ts';
|
|
23
23
|
export { buildManifest, renderManifest, MANIFEST_SCHEMA_VERSION } from './manifest.ts';
|
|
24
24
|
export type { Manifest, ManifestRoute, ManifestModule, RecipeProvenance } from './manifest.ts';
|
|
25
25
|
export { serveMcp } from './mcp.ts';
|
package/dist/types/site.d.ts
CHANGED
|
@@ -8,4 +8,6 @@ type SiteKey = keyof SiteConfig;
|
|
|
8
8
|
export declare const generatedPaths: Readonly<Record<SiteKey, string>>;
|
|
9
9
|
export declare function expandSite(document: ProjectDocument, root: string, { origin, log, routes }?: SiteOptions): Promise<Record<string, RouteConfig>>;
|
|
10
10
|
export declare function applySite(loaded: LoadedDocument, options?: SiteOptions): Promise<Record<string, RouteConfig>>;
|
|
11
|
+
export declare const notFoundInlineLimit = 65536;
|
|
12
|
+
export declare function inlineNotFound(loaded: LoadedDocument): Promise<boolean>;
|
|
11
13
|
export {};
|
package/dist/types/tooling.d.ts
CHANGED
|
@@ -10,8 +10,8 @@ export type { CapabilityEntry, CapabilityUsage } from './capability-query.ts';
|
|
|
10
10
|
export { getSchemaFragment, schemaPathNames } from './schema-query.ts';
|
|
11
11
|
export type { SchemaFragment } from './schema-query.ts';
|
|
12
12
|
export { listRecipes, showRecipe, searchRecipes, listExamples, searchExamples };
|
|
13
|
-
export { buildContext, renderContext, estimateTokens, documentationTokens } from './context.ts';
|
|
14
|
-
export type { ContextOptions, ProjectContext, ContextSection } from './context.ts';
|
|
13
|
+
export { buildContext, renderContext, estimateTokens, documentationTokens, buildTaskContext, renderTaskContext, contextTasks } from './context.ts';
|
|
14
|
+
export type { ContextOptions, ProjectContext, ContextSection, ContextTask, TaskContext, TaskShape } from './context.ts';
|
|
15
15
|
export type { RouteExplanation, ExplainedHandler, ExplainedCache, ExplainedExtensionRequirement, ExtensionProvider, TargetSupport } from './explain.ts';
|
|
16
16
|
/** `extensions` are operator registrations from a host file; explain reports whether each requirement has a provider. Nothing is activated. */
|
|
17
17
|
export interface InspectOptions {
|
package/llms-full.txt
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<!-- Generated by scripts/build-llms-full.ts (npm run docs:llms). Do not edit; edit the source documents. -->
|
|
2
|
-
<!-- Consolidated URLCode authoring reference: 16 documents, about
|
|
2
|
+
<!-- Consolidated URLCode authoring reference: 16 documents, about 59,492 tokens (estimate: characters / 4). -->
|
|
3
3
|
|
|
4
4
|
<!-- urlcode-current-version:start -->
|
|
5
5
|
# URLCode
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
> A portable runtime for programmable URL behavior, and the framework that grows
|
|
8
8
|
> from it: routes in YAML, functions and middleware, then accounts, administration and
|
|
9
9
|
> stored links as operator-installed extensions. Stable project format
|
|
10
|
-
> `version: "1"`. Core is Apache-2.0; this revision is `0.4.
|
|
10
|
+
> `version: "1"`. Core is Apache-2.0; this revision is `0.4.8`. `function`/`middleware`
|
|
11
11
|
> routes are trusted by default with `sandbox: true` as the per-route opt-in. The
|
|
12
12
|
> auth, admin and ui extension packages in this repository are versioned at the same
|
|
13
13
|
> revision; confirm what is published with `npm run release:status`.
|
|
@@ -199,6 +199,12 @@ and the MCP `get_extensions` tool report those surfaces and their fast checks,
|
|
|
199
199
|
so people and agents can discover the supported path instead of replacing
|
|
200
200
|
package behavior.
|
|
201
201
|
|
|
202
|
+
A signed declarative artifact is a separate, optional authoring input, not a
|
|
203
|
+
fifth way to compose executable behavior. A project may lock an attested
|
|
204
|
+
schema/example bundle and expose it through MCP `get_extension_artifacts` and
|
|
205
|
+
`get_extension_artifact`; the npm package and operator host remain the only
|
|
206
|
+
executable extension path. See [signed declarative artifacts](https://github.com/jimhoyd-com/urlcode/blob/main/docs/EXTENSIONS.md#signed-declarative-artifacts).
|
|
207
|
+
|
|
202
208
|
```sh
|
|
203
209
|
urlcode serve --project /absolute/site --host-file /absolute/operator/host.mjs --origin https://site.example
|
|
204
210
|
```
|
|
@@ -301,7 +307,10 @@ Make the first retrieval one bounded query: the MCP tool `get_context` when the
|
|
|
301
307
|
`urlcode capabilities NAME` (MCP `get_capability`) for one capability's limits,
|
|
302
308
|
`get_schema` for one YAML fragment, `urlcode recipes search TEXT`
|
|
303
309
|
(`search_recipes`), `explain` for a route's effective behavior and, when the
|
|
304
|
-
operator supplies a host file, `get_extensions`.
|
|
310
|
+
operator supplies a host file, `get_extensions`. If the project has a committed
|
|
311
|
+
`urlcode.extensions.lock.json`, use `get_extension_artifacts` to verify and
|
|
312
|
+
inventory its inert data and `get_extension_artifact` to retrieve only the
|
|
313
|
+
needed schema, example or README. Context is a summary with the
|
|
305
314
|
constraints and exact commands, not a schema dump, and it never hides a
|
|
306
315
|
capability limit: ask `capabilities NAME` before promising a feature.
|
|
307
316
|
|
|
@@ -356,7 +365,15 @@ teaches how to retrieve the minimum reference through `urlcode capabilities`,
|
|
|
356
365
|
the documentation whole. For a host-composed application, `get_extensions`
|
|
357
366
|
adds each extension's schemas, hooks, supported authoring surfaces and fast
|
|
358
367
|
checks. Agents should use those surfaces before generating replacement package
|
|
359
|
-
behavior.
|
|
368
|
+
behavior. A committed artifact lock is a separate offline authoring input:
|
|
369
|
+
`get_extension_artifacts` validates its cache and lists allowlisted files;
|
|
370
|
+
`get_extension_artifact` reads one bounded JSON or Markdown member from its
|
|
371
|
+
verified archive. The CLI fallback is `urlcode extension-artifacts inspect
|
|
372
|
+
--project DIR --json`. An artifact never installs or activates an npm package,
|
|
373
|
+
registers a host extension or grants authority. Agents must not fetch or update
|
|
374
|
+
one unless the user explicitly requests that project change and names the
|
|
375
|
+
immutable `extensions@v…` release. Neither guide nor artifact replaces the
|
|
376
|
+
runtime schema; all defer to the pinned implementation.
|
|
360
377
|
|
|
361
378
|
Treat core, installed extensions and product UI as one application with
|
|
362
379
|
different owners. Keep auth/admin security and workflow behavior package-owned;
|
|
@@ -482,8 +499,21 @@ Before writing a function, check whether a declarative feature already covers th
|
|
|
482
499
|
need. Security headers are the usual miss: a project that declares nothing sends
|
|
483
500
|
only the runtime's defaults (`nosniff`, `no-store`, a request ID).
|
|
484
501
|
|
|
502
|
+
Building only redirects? `urlcode context --project DIR --task redirects` (MCP
|
|
503
|
+
`get_context {"task":"redirects"}`) is a bounded, redirect-only call: every row
|
|
504
|
+
below with the exact YAML, the two gaps with their exact validation error, and
|
|
505
|
+
this project's own redirects — cheaper than this table or the recipe catalog.
|
|
506
|
+
|
|
485
507
|
| I need | Declare | Reference |
|
|
486
508
|
|---|---|---|
|
|
509
|
+
| Fixed redirect (301/302/303/307/308, 302 default) | `redirect: {url, status}` | [redirects](https://github.com/jimhoyd-com/urlcode/blob/main/docs/yaml/redirects.md) |
|
|
510
|
+
| Parameterized path redirect (`/users/{id}` to `/profiles/{id}`) | `{name}` placeholder in `redirect.url` naming a declared path parameter | [redirects](https://github.com/jimhoyd-com/urlcode/blob/main/docs/yaml/redirects.md) |
|
|
511
|
+
| Fixed-depth suffix redirect (`/legacy/a/b` to `/modern/a/b`) | one route per depth, one placeholder per segment | [redirects](https://github.com/jimhoyd-com/urlcode/blob/main/docs/yaml/redirects.md) |
|
|
512
|
+
| Redirect that preserves query keys | `redirect.query.pass` (explicit allowlist) or `query.map` | [redirects](https://github.com/jimhoyd-com/urlcode/blob/main/docs/yaml/redirects.md) |
|
|
513
|
+
| Redirect that keeps the method/body (POST) | `methods` plus `status: 307` or `308` | [redirects](https://github.com/jimhoyd-com/urlcode/blob/main/docs/yaml/redirects.md) |
|
|
514
|
+
| 404 for unmatched paths | `site.notFound` (a project-relative `.html` file) | [site](https://github.com/jimhoyd-com/urlcode/blob/main/docs/SITE.md) |
|
|
515
|
+
| Wildcard/suffix redirect (`/legacy/*` to `/modern/*`, any depth) — **gap** | not expressible; terminal `/*` and `{rest...}` are refused on `redirect` | [open decision](https://github.com/jimhoyd-com/urlcode/blob/main/docs/OPEN-DECISIONS.md) |
|
|
516
|
+
| Host-based, scheme-based or relative-URL redirect — **gap** | not expressible; destination must be a literal absolute `https://host/path` | [open decision](https://github.com/jimhoyd-com/urlcode/blob/main/docs/OPEN-DECISIONS.md) |
|
|
487
517
|
| Security headers (CSP, HSTS, frame and referrer policy) | `policies.security: {headers: oshp}` or `policies.profile: hardened` | [security](https://github.com/jimhoyd-com/urlcode/blob/main/docs/policies/security.md) |
|
|
488
518
|
| Cache headers on a page, download or static mount | `cacheControl`: `no-cache` (default), `no-store`, `public, max-age=3600` or `public, max-age=31536000, immutable`; nothing else validates | [assets](https://github.com/jimhoyd-com/urlcode/blob/main/docs/yaml/assets.md) |
|
|
489
519
|
| A cache strategy on any route | `policies.cache` | [cache](https://github.com/jimhoyd-com/urlcode/blob/main/docs/policies/cache.md) |
|
|
@@ -727,7 +757,7 @@ programmatic compatibility analysis and provider verification limits.
|
|
|
727
757
|
Source: https://github.com/jimhoyd-com/urlcode/blob/main/docs/YAML-GUIDE.md
|
|
728
758
|
|
|
729
759
|
<!-- urlcode-current-version:start -->
|
|
730
|
-
This guide targets URLCode 0.4.
|
|
760
|
+
This guide targets URLCode 0.4.8. Start with the function example below,
|
|
731
761
|
then add only the fields your route needs. The authoritative machine-readable
|
|
732
762
|
shape is [JSON Schema](https://github.com/jimhoyd-com/urlcode/blob/main/schemas/urlcode.schema.json); semantic rules are in the
|
|
733
763
|
[specification](https://github.com/jimhoyd-com/urlcode/blob/main/docs/SPECIFICATION.md). Unsupported fields fail validation.
|
|
@@ -2551,15 +2581,19 @@ as the not-found page instead. The page answers every unknown URL, so it cannot
|
|
|
2551
2581
|
|
|
2552
2582
|
`urlcode build --target static` writes it as the object `404.html` (see
|
|
2553
2583
|
[static hosting](https://github.com/jimhoyd-com/urlcode/blob/main/docs/STATIC.md)); point the host's error document at that key.
|
|
2554
|
-
Cloudflare
|
|
2555
|
-
|
|
2584
|
+
Cloudflare inlines it: the page is small, singular and static, so the build
|
|
2585
|
+
reads it (64 KiB cap, must decode as UTF-8) and carries it in the artifact as a
|
|
2586
|
+
`respond` route at `/404.html`, answering the same status, headers and method
|
|
2587
|
+
rules as every other target. See [`docs/CLOUDFLARE.md`](https://github.com/jimhoyd-com/urlcode/blob/main/docs/CLOUDFLARE.md#site-notfound-is-inlined).
|
|
2588
|
+
`favicon` and `llms` stay refused there like any other `page` route (no asset
|
|
2589
|
+
binding).
|
|
2556
2590
|
|
|
2557
2591
|
### Per-target support
|
|
2558
2592
|
|
|
2559
|
-
| Target | `robots`, `sitemap`, `securityTxt` (`respond`) | `favicon`, `llms
|
|
2560
|
-
| --- | --- | --- |
|
|
2561
|
-
| self-hosted, Vercel, AWS | served | served |
|
|
2562
|
-
| Cloudflare | compiled into the artifact (`build --origin` for absolute URLs) | refused at build time like any `page` route: the target has no asset binding; serve them from the platform's static assets |
|
|
2593
|
+
| Target | `robots`, `sitemap`, `securityTxt` (`respond`) | `favicon`, `llms` (`page`) | `notFound` |
|
|
2594
|
+
| --- | --- | --- | --- |
|
|
2595
|
+
| self-hosted, Vercel, AWS | served | served | served |
|
|
2596
|
+
| Cloudflare | compiled into the artifact (`build --origin` for absolute URLs) | refused at build time like any `page` route: the target has no asset binding; serve them from the platform's static assets | inlined into the artifact (64 KiB cap, UTF-8) |
|
|
2563
2597
|
|
|
2564
2598
|
### Not in this release
|
|
2565
2599
|
|
|
@@ -3218,6 +3252,64 @@ Serving the result is the usual explicit host binding:
|
|
|
3218
3252
|
urlcode validate --project app --host-file "$PWD/host.mjs" --origin https://site.example
|
|
3219
3253
|
```
|
|
3220
3254
|
|
|
3255
|
+
### Signed declarative artifacts
|
|
3256
|
+
|
|
3257
|
+
Core remains the npm-distributed runtime. The workspace extensions are not an
|
|
3258
|
+
alternate npm channel. A release can additionally carry a small, **data-only**
|
|
3259
|
+
extension artifact for tooling that understands its declared format. It is not
|
|
3260
|
+
a Node module and cannot activate an extension, run a hook, replace a trusted
|
|
3261
|
+
operator host, or grant a route any authority.
|
|
3262
|
+
|
|
3263
|
+
Install an artifact only from its immutable `extensions@v…` GitHub Release.
|
|
3264
|
+
For example, after the first release is published, its inert store configuration
|
|
3265
|
+
schema snapshot can be installed with:
|
|
3266
|
+
|
|
3267
|
+
```sh
|
|
3268
|
+
urlcode extension-artifacts install store-schema --artifact-release extensions@v1.0.0 --project app
|
|
3269
|
+
urlcode extension-artifacts update store-schema --artifact-release extensions@v1.1.0 --project app
|
|
3270
|
+
urlcode extension-artifacts inspect --project app
|
|
3271
|
+
```
|
|
3272
|
+
|
|
3273
|
+
The command downloads the signed `extensions-catalog.json`, verifies its
|
|
3274
|
+
GitHub attestation against the dedicated artifact workflow in
|
|
3275
|
+
`jimhoyd-com/urlcode` and the exact requested tag ref, then verifies the selected
|
|
3276
|
+
`.tgz` the same way. Self-hosted-runner attestations are refused. The catalog
|
|
3277
|
+
pins its release tag, source commit, filename and SHA-256; a catalog revocation
|
|
3278
|
+
refuses installation. `gh` with support for attestation source-ref verification
|
|
3279
|
+
is therefore a required local dependency for this command.
|
|
3280
|
+
|
|
3281
|
+
The resulting `urlcode.extensions.lock.json` is the reproducibility boundary:
|
|
3282
|
+
commit it with the project. Every locked artifact records its own catalog tag
|
|
3283
|
+
and source commit, so updating one artifact cannot silently relabel another as
|
|
3284
|
+
coming from a newer release. Extracted files live under
|
|
3285
|
+
`app/.urlcode/extensions/<sha256>/` and are checked before extraction. Archives
|
|
3286
|
+
are size- and file-count-bounded, reject links and traversal, and may contain
|
|
3287
|
+
only `extension.json`, JSON configuration/schema data, and an optional README.
|
|
3288
|
+
Any JavaScript, package manifest, install hook, native module, or unknown file
|
|
3289
|
+
causes refusal. `inspect` re-hashes the cached archive and every extracted file,
|
|
3290
|
+
reporting an entry as missing or invalid rather than trusting its directory
|
|
3291
|
+
name. Updates are never automatic: review a newer immutable release and run
|
|
3292
|
+
`update` explicitly.
|
|
3293
|
+
|
|
3294
|
+
Agent tooling can consume a committed lock without gaining write or execution
|
|
3295
|
+
authority. MCP `get_extension_artifacts` validates the lock and cache and lists
|
|
3296
|
+
the signed member paths; `get_extension_artifact {name, path}` returns one
|
|
3297
|
+
verified, bounded JSON or Markdown member directly from the cached archive.
|
|
3298
|
+
The CLI fallback is `urlcode extension-artifacts inspect --project app --json`.
|
|
3299
|
+
Neither MCP tool performs a network request, installs or updates an artifact,
|
|
3300
|
+
loads a host file, or activates code. A missing or modified cache is reported
|
|
3301
|
+
as missing/invalid and its contents are not returned.
|
|
3302
|
+
|
|
3303
|
+
This does not relax the existing host boundary. `--host-file` is still the only
|
|
3304
|
+
way to load trusted operator extension code, and artifact files are never
|
|
3305
|
+
imported by `serve`, `validate`, `init --with`, or the runtime.
|
|
3306
|
+
|
|
3307
|
+
Artifact versions are independent from npm package versions. The initial
|
|
3308
|
+
`store-schema` artifact is a reviewed configuration-schema snapshot and example,
|
|
3309
|
+
not the `@jimhoyd/urlcode-store` implementation. Installing it does not install
|
|
3310
|
+
or activate that package. Its README names the separate executable and operator
|
|
3311
|
+
requirements.
|
|
3312
|
+
|
|
3221
3313
|
---
|
|
3222
3314
|
|
|
3223
3315
|
# Composing a site from ui, auth and admin
|
package/llms.txt
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
> A portable runtime for programmable URL behavior, and the framework that grows
|
|
5
5
|
> from it: routes in YAML, functions and middleware, then accounts, administration and
|
|
6
6
|
> stored links as operator-installed extensions. Stable project format
|
|
7
|
-
> `version: "1"`. Core is Apache-2.0; this revision is `0.4.
|
|
7
|
+
> `version: "1"`. Core is Apache-2.0; this revision is `0.4.8`. `function`/`middleware`
|
|
8
8
|
> routes are trusted by default with `sandbox: true` as the per-route opt-in. The
|
|
9
9
|
> auth, admin and ui extension packages in this repository are versioned at the same
|
|
10
10
|
> revision; confirm what is published with `npm run release:status`.
|
|
@@ -18,6 +18,25 @@ grants. Unsupported features fail with the route named; nothing degrades silentl
|
|
|
18
18
|
Agents that explicitly want the complete consolidated reference in one fetch should read
|
|
19
19
|
[llms-full.txt](llms-full.txt), generated from the documents below (about 50k tokens, estimated).
|
|
20
20
|
|
|
21
|
+
## Building only redirects? Start here
|
|
22
|
+
|
|
23
|
+
Run `urlcode context --project DIR --task redirects` (MCP `get_context {"task":"redirects"}`)
|
|
24
|
+
first: one bounded call returning every supported shape below with exact YAML, the
|
|
25
|
+
gaps with their exact validation error, and this project's own redirects — cheaper
|
|
26
|
+
than reading this file or the recipe catalog. `urlcode recipes search redirect` and
|
|
27
|
+
`urlcode recipes show redirect` give a runnable starting project.
|
|
28
|
+
|
|
29
|
+
| Shape | Supported? | Use |
|
|
30
|
+
|---|---|---|
|
|
31
|
+
| Fixed redirect, any of 301/302/303/307/308 (302 default) | Yes | `redirect: {url, status}` — [redirects](docs/yaml/redirects.md) |
|
|
32
|
+
| Parameterized path (`/users/{id}` to `/profiles/{id}`) | Yes | `{name}` placeholder in `redirect.url`, naming a declared path parameter |
|
|
33
|
+
| Fixed-depth suffix (`/legacy/a/b` to `/modern/a/b`) | Yes, one route per depth | same as above with one placeholder per segment |
|
|
34
|
+
| Query-string preservation | Yes, opt-in only | `redirect.query.pass` (explicit allowlist) / `query.map` |
|
|
35
|
+
| Method-preserving redirect (keep POST body) | Yes | `methods` plus `status: 307` or `308` |
|
|
36
|
+
| 404 for unmatched paths | Yes | `site.notFound` (a project-relative `.html` file) |
|
|
37
|
+
| Wildcard/suffix redirect (`/legacy/*` to `/modern/*`, any depth) | No | terminal `/*` and `{rest...}` are refused on `redirect`; report the gap ([open decision](docs/OPEN-DECISIONS.md)) |
|
|
38
|
+
| Host-based, scheme-based or relative-URL redirect | No | destination must be a literal absolute `https://host/path`; report the gap |
|
|
39
|
+
|
|
21
40
|
## Declarative-first default
|
|
22
41
|
|
|
23
42
|
> Use URLCode's highest-level declarative features whenever possible. Generate custom code only when the framework cannot express the requirement.
|
|
@@ -105,6 +124,7 @@ publish or comment on an issue without the user's explicit approval.
|
|
|
105
124
|
|
|
106
125
|
## Extensions (accounts, administration, presentation)
|
|
107
126
|
- [Extensions](docs/EXTENSIONS.md): `extensions.<name>` blocks, mounts, policies, operator registration, the shared trusted hook primitive and its machine-readable hook contracts. UI/auth/admin project hooks run trusted in-process; contract v1 rejects `sandbox: true`.
|
|
127
|
+
- [Signed declarative artifacts](docs/EXTENSIONS.md#signed-declarative-artifacts): project-pinned, attested JSON/Markdown data for offline tooling. MCP `get_extension_artifacts` verifies/inventories a committed lock and `get_extension_artifact` reads one bounded member; neither installs or activates an npm extension.
|
|
108
128
|
- [Composing a site](docs/COMPOSING-A-SITE.md): what `urlcode init site --with ui,auth,admin` wires, which `--with` combinations are supported, the presentation override paths under `ui/`, and per-package lifecycle hook input, verdict, timing and failure semantics. Separates declarative configuration, project functions and the TypeScript a new extension needs.
|
|
109
129
|
- [urlcode-auth](packages/auth): npm: @jimhoyd/urlcode-auth; accounts, sessions, MFA, roles, account page; its own llms.txt. Lives in this repository as a workspace package.
|
|
110
130
|
- [urlcode-admin](packages/admin): npm: @jimhoyd/urlcode-admin; users, sessions, roles, audit, cases; its own llms.txt. Lives in this repository as a workspace package.
|
|
@@ -120,7 +140,7 @@ publish or comment on an issue without the user's explicit approval.
|
|
|
120
140
|
|
|
121
141
|
## Operations
|
|
122
142
|
- [Operations](docs/OPERATIONS.md), [install](docs/INSTALL.md), [deployment checks](docs/DEPLOYMENT-CHECKS.md), [CI action](docs/CI.md).
|
|
123
|
-
- [Readiness](docs/READINESS.md), [capacity](docs/CAPACITY.md), [
|
|
143
|
+
- [Readiness](docs/READINESS.md), [capacity](docs/CAPACITY.md), [resilience](docs/RESILIENCE.md), [observability](docs/OBSERVABILITY.md).
|
|
124
144
|
- [Vercel](docs/VERCEL.md), [AWS](docs/AWS.md), [Cloudflare](docs/CLOUDFLARE.md): adapters with local conformance tests; no provider deployment verified yet.
|
|
125
145
|
- [Static hosting](docs/STATIC.md): `urlcode build --target static` compiles redirects/pages/static/downloads to S3 + CloudFront objects and redirect metadata; no server, so function/middleware/extension/proxy/signals/conditions/parameters/bindings/policies are all refused; GitHub Pages is explicitly out of scope.
|
|
126
146
|
- [Release readiness](docs/RELEASE-READINESS.md), [roadmap](ROADMAP.md): what is proven, what is planned.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jimhoyd/urlcode",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.8",
|
|
4
4
|
"description": "Portable runtime for programmable URL behavior",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"workspaces": [
|
|
@@ -89,12 +89,12 @@
|
|
|
89
89
|
"test": "node --conditions=development --test test/*.test.ts",
|
|
90
90
|
"lint": "eslint .",
|
|
91
91
|
"check": "npm run check:code && npm run check:docs",
|
|
92
|
-
"check:code": "node scripts/check.ts && node scripts/check-release-tags.ts && node scripts/check-workspace-links.ts && npm run release:check",
|
|
92
|
+
"check:code": "node scripts/check.ts && node scripts/check-release-tags.ts && node scripts/check-workspace-links.ts && node scripts/check-issue-labels.ts && npm run release:check",
|
|
93
|
+
"test:workerd": "node scripts/workerd-parity.ts",
|
|
93
94
|
"verify": "npm run lint && npm run typecheck && npm run check && npm run build && npm test && npm run verify:workspaces",
|
|
94
95
|
"workspace:styles": "npm run styles --workspace @jimhoyd/urlcode-ui",
|
|
95
96
|
"verify:workspaces": "npm run verify --workspace @jimhoyd/urlcode-ui && npm run verify --workspace @jimhoyd/urlcode-auth && npm run verify --workspace @jimhoyd/urlcode-admin && npm run verify --workspace @jimhoyd/urlcode-store && npm run audit:packages && npm run test:workspace-integration",
|
|
96
97
|
"audit:packages": "node scripts/package-audit.ts --all",
|
|
97
|
-
"benchmark": "node benchmarks/routing.ts",
|
|
98
98
|
"test:package": "npm run build && npm run test:package:built",
|
|
99
99
|
"dev": "node src/cli.ts dev --project starters/default",
|
|
100
100
|
"serve": "node src/cli.ts serve --project starters/default",
|
|
@@ -109,10 +109,8 @@
|
|
|
109
109
|
"docs:plugin": "node scripts/generate-claude-plugin.ts",
|
|
110
110
|
"docs:llms": "node scripts/build-llms-full.ts",
|
|
111
111
|
"docs:cookbook-index": "node scripts/build-cookbook-index.ts",
|
|
112
|
+
"artifacts:prepare": "node --disable-warning=ExperimentalWarning scripts/prepare-extension-artifacts.ts",
|
|
112
113
|
"check:downstream-skills": "node scripts/check-downstream-skill-drift.ts",
|
|
113
|
-
"benchmark:bulk": "node benchmarks/bulk.ts",
|
|
114
|
-
"benchmark:sandbox-vs-trusted": "node benchmarks/sandbox-vs-trusted.ts",
|
|
115
|
-
"benchmark:agent": "node benchmarks/agent/run.ts",
|
|
116
114
|
"sync:agents": "node scripts/sync-agent-lists.ts",
|
|
117
115
|
"check:docs": "node scripts/check-trust-model-prose.ts && node scripts/check-version-statements.ts && node scripts/check-local-links.ts && node scripts/check-guidance-claims.ts && node scripts/generate-yaml-reference.ts --check && node scripts/build-llms-full.ts --check && node scripts/build-cookbook-index.ts --check && node scripts/generate-claude-plugin.ts --check",
|
|
118
116
|
"ci:plan": "node scripts/ci-plan.ts",
|
|
@@ -123,6 +121,7 @@
|
|
|
123
121
|
"release:run": "node scripts/release-run.ts",
|
|
124
122
|
"release:peers": "node scripts/release.ts peers",
|
|
125
123
|
"release:check": "node scripts/release.ts check && node scripts/release-prepare.ts --check",
|
|
124
|
+
"rehearse:release": "node --conditions=development --test test/release-rehearsal.test.ts",
|
|
126
125
|
"ci:history": "node scripts/ci-history.ts",
|
|
127
126
|
"ci:report": "node scripts/ci-report.ts",
|
|
128
127
|
"test:workspace-integration": "node --test test/workspace-scaffold.integration.ts",
|
|
@@ -1,7 +1,21 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Redirects: fixed, parameterized, query-preserving and 404
|
|
2
2
|
|
|
3
3
|
Run `urlcode validate --local --project .` and `urlcode serve --project .`.
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
|
|
5
|
+
- `/docs?campaign=launch&private=discarded` redirects (301) to
|
|
6
|
+
`https://example.com/documentation?campaign=launch`. Only the declared
|
|
7
|
+
`campaign` key is forwarded; everything else, including `private`, is
|
|
8
|
+
dropped. Replace the example destination before deploying.
|
|
9
|
+
- `/users/{id}` redirects (308, method- and body-preserving) to
|
|
10
|
+
`https://example.com/profiles/{id}`, substituting the declared path
|
|
11
|
+
parameter into the destination.
|
|
12
|
+
- Any other path, such as `/missing` or `/users/42/extra`, answers the
|
|
13
|
+
runtime's default 404. `DELETE /docs` answers 405 (only GET/HEAD match by
|
|
14
|
+
default). Add top-level `site: {notFound: 404.html}` for a custom 404 page
|
|
15
|
+
instead of the plain default.
|
|
16
|
+
|
|
17
|
+
For the shapes this recipe does not cover — a wildcard/suffix redirect that
|
|
18
|
+
matches any depth (`/legacy/*`), or a host/scheme-based redirect — run
|
|
19
|
+
`urlcode context --project . --task redirects` (MCP `get_context
|
|
20
|
+
{"task":"redirects"}`) for the exact supported alternative and the exact
|
|
21
|
+
validation error, or see [docs/OPEN-DECISIONS.md](../../docs/OPEN-DECISIONS.md).
|
|
@@ -1,25 +1,27 @@
|
|
|
1
1
|
id: redirect
|
|
2
|
-
description:
|
|
3
|
-
tags: [redirect, permanent, "301", query, passthrough, documentation, native]
|
|
2
|
+
description: Fixed, parameterized and query-preserving redirects, and the default 404 for everything else.
|
|
3
|
+
tags: [redirect, permanent, "301", "308", parameterized, query, passthrough, "404", documentation, native]
|
|
4
4
|
complexity: starter
|
|
5
|
-
capabilities: [enabled, methods, redirect]
|
|
6
|
-
targets: {self-hosted: compatible, aws: compatible, vercel: compatible, cloudflare: compatible, static:
|
|
7
|
-
routes:
|
|
5
|
+
capabilities: [enabled, methods, parameters, redirect]
|
|
6
|
+
targets: {self-hosted: compatible, aws: compatible, vercel: compatible, cloudflare: compatible, static: refused}
|
|
7
|
+
routes: 2
|
|
8
8
|
inputs:
|
|
9
9
|
- name: destination
|
|
10
10
|
file: urlcode.yaml
|
|
11
|
-
description: Replace https://example.com/documentation with the real
|
|
11
|
+
description: Replace https://example.com/documentation and https://example.com/profiles with the real targets.
|
|
12
12
|
- name: query.pass
|
|
13
13
|
file: urlcode.yaml
|
|
14
|
-
description: The query keys forwarded to the destination; everything else is dropped.
|
|
14
|
+
description: The query keys forwarded to the /docs destination; everything else is dropped.
|
|
15
15
|
files: [urlcode.yaml, tests/requests.json, README.md]
|
|
16
16
|
tests:
|
|
17
17
|
fixtures: tests/requests.json
|
|
18
18
|
commands:
|
|
19
19
|
- urlcode validate --local --project .
|
|
20
20
|
- urlcode test --project .
|
|
21
|
-
- urlcode audit --project . --expect-routes
|
|
21
|
+
- urlcode audit --project . --expect-routes 2
|
|
22
22
|
behavior:
|
|
23
23
|
- GET /docs?campaign=launch&private=x answers 301 to https://example.com/documentation?campaign=launch
|
|
24
|
-
- HEAD answers the same status with an empty body
|
|
25
|
-
-
|
|
24
|
+
- HEAD /docs answers the same status with an empty body
|
|
25
|
+
- DELETE /docs answers 405
|
|
26
|
+
- GET /users/42 answers 308 (method- and body-preserving) to https://example.com/profiles/42
|
|
27
|
+
- unknown paths, including /users/42/extra and /missing, answer the runtime's default 404
|