@jimhoyd/urlcode 0.4.7 → 0.5.0
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 +9 -3
- package/dist/BUILD-MANIFEST.json +23 -21
- package/dist/agents-guide.js +9 -5
- package/dist/authoring.js +36 -3
- package/dist/build-cloudflare.js +5 -2
- package/dist/build-static.js +1 -0
- package/dist/cli.js +47 -15
- package/dist/cloudflare.js +6 -3
- package/dist/config.js +7 -1
- package/dist/context.js +99 -1
- package/dist/extension-artifacts.js +147 -0
- package/dist/extension-bundles.js +70 -0
- package/dist/extensions.js +4 -0
- package/dist/functions.js +2 -1
- package/dist/index.js +4 -2
- package/dist/init-with.js +55 -23
- package/dist/interchange.js +1 -1
- package/dist/match.js +23 -5
- package/dist/mcp.js +11 -4
- package/dist/readiness.js +2 -2
- package/dist/router.js +20 -8
- package/dist/runtime.js +1 -0
- package/dist/site.js +19 -1
- package/dist/tooling.js +2 -2
- package/dist/types/agents-guide.d.ts +6 -1
- package/dist/types/authoring.d.ts +1 -1
- package/dist/types/cloudflare.d.ts +1 -0
- package/dist/types/context.d.ts +56 -0
- package/dist/types/extension-artifacts.d.ts +87 -0
- package/dist/types/extension-bundles.d.ts +49 -0
- package/dist/types/extensions.d.ts +4 -0
- package/dist/types/functions.d.ts +4 -0
- package/dist/types/index.d.ts +4 -2
- package/dist/types/init-with.d.ts +6 -1
- package/dist/types/match.d.ts +1 -0
- package/dist/types/site.d.ts +2 -0
- package/dist/types/tooling.d.ts +2 -2
- package/dist/types/types.d.ts +1 -0
- package/dist/types.js +1 -1
- package/llms-full.txt +188 -24
- package/llms.txt +63 -107
- package/package.json +11 -3
- 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 +3 -3
|
@@ -0,0 +1,87 @@
|
|
|
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 interface TarFile {
|
|
34
|
+
path: string;
|
|
35
|
+
bytes: Uint8Array;
|
|
36
|
+
}
|
|
37
|
+
export interface ArchiveLimits {
|
|
38
|
+
archive: number;
|
|
39
|
+
expanded: number;
|
|
40
|
+
files: number;
|
|
41
|
+
file: number;
|
|
42
|
+
label: string;
|
|
43
|
+
}
|
|
44
|
+
/** A minimal tar reader: only regular files are accepted, before any write occurs. */
|
|
45
|
+
export declare function readBoundedTgz(source: Uint8Array, limits: ArchiveLimits): TarFile[];
|
|
46
|
+
export declare function extractArtifact(bytes: Uint8Array, entry: ArtifactEntry, destination: string): Promise<void>;
|
|
47
|
+
export declare function readLock(project: string): Promise<ExtensionLock>;
|
|
48
|
+
export declare function writeLock(project: string, lock: ExtensionLock): Promise<void>;
|
|
49
|
+
export declare function cachePath(project: string, sha256: string): string;
|
|
50
|
+
export interface ReleaseAsset {
|
|
51
|
+
name: string;
|
|
52
|
+
url: string;
|
|
53
|
+
}
|
|
54
|
+
export interface ArtifactTransport {
|
|
55
|
+
release(tag: string): Promise<ReleaseAsset[]>;
|
|
56
|
+
download(url: string): Promise<Uint8Array>;
|
|
57
|
+
attest(path: string, release: string): Promise<void>;
|
|
58
|
+
}
|
|
59
|
+
/** The default transport accepts only GitHub Release asset URLs and verifies every downloaded subject. */
|
|
60
|
+
export declare const githubTransport: ArtifactTransport;
|
|
61
|
+
export declare function resolveCatalog(release: string, transport?: ArtifactTransport): Promise<{
|
|
62
|
+
catalog: Catalog;
|
|
63
|
+
assets: ReleaseAsset[];
|
|
64
|
+
}>;
|
|
65
|
+
export declare function installArtifact(project: string, release: string, artifactName: string, transport?: ArtifactTransport): Promise<ExtensionLock>;
|
|
66
|
+
export declare function inspectArtifacts(project: string): Promise<{
|
|
67
|
+
lock: ExtensionLock;
|
|
68
|
+
cached: string[];
|
|
69
|
+
missing: string[];
|
|
70
|
+
invalid: string[];
|
|
71
|
+
}>;
|
|
72
|
+
/** Read-only inventory for authoring tools. Paths come from the verified archive, never from an arbitrary filesystem argument. */
|
|
73
|
+
export declare function describeArtifactCache(project: string): Promise<{
|
|
74
|
+
format: 1;
|
|
75
|
+
artifacts: (LockedArtifact & {
|
|
76
|
+
status: 'cached' | 'missing' | 'invalid';
|
|
77
|
+
files: string[];
|
|
78
|
+
})[];
|
|
79
|
+
}>;
|
|
80
|
+
/** Return one bounded text/JSON member from a verified cached artifact for MCP/agent consumers. */
|
|
81
|
+
export declare function readArtifactMember(project: string, artifactName: string, path: string): Promise<{
|
|
82
|
+
format: 1;
|
|
83
|
+
artifact: LockedArtifact;
|
|
84
|
+
path: string;
|
|
85
|
+
mediaType: 'application/json' | 'text/markdown';
|
|
86
|
+
content: unknown;
|
|
87
|
+
}>;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/** Verified, executable first-party bundles. Unlike extension artifacts, these are trusted operator code. */
|
|
2
|
+
export declare const BUNDLE_REPOSITORY = "jimhoyd-com/urlcode";
|
|
3
|
+
export declare const BUNDLE_WORKFLOW = "jimhoyd-com/urlcode/.github/workflows/extension-bundles.yml";
|
|
4
|
+
export interface BundleEntry {
|
|
5
|
+
name: string;
|
|
6
|
+
version: string;
|
|
7
|
+
asset: string;
|
|
8
|
+
sha256: string;
|
|
9
|
+
entry: string;
|
|
10
|
+
}
|
|
11
|
+
export interface BundleCatalog {
|
|
12
|
+
format: 1;
|
|
13
|
+
tag: string;
|
|
14
|
+
commit: string;
|
|
15
|
+
coreVersion: string;
|
|
16
|
+
bundles: BundleEntry[];
|
|
17
|
+
revoked: {
|
|
18
|
+
sha256: string;
|
|
19
|
+
reason: string;
|
|
20
|
+
}[];
|
|
21
|
+
}
|
|
22
|
+
export interface LockedBundle extends BundleEntry {
|
|
23
|
+
catalog: {
|
|
24
|
+
tag: string;
|
|
25
|
+
commit: string;
|
|
26
|
+
};
|
|
27
|
+
coreVersion: string;
|
|
28
|
+
}
|
|
29
|
+
export interface BundleLock {
|
|
30
|
+
format: 1;
|
|
31
|
+
bundles: LockedBundle[];
|
|
32
|
+
}
|
|
33
|
+
export interface BundleTransport {
|
|
34
|
+
release(tag: string): Promise<{
|
|
35
|
+
name: string;
|
|
36
|
+
url: string;
|
|
37
|
+
}[]>;
|
|
38
|
+
download(url: string): Promise<Uint8Array>;
|
|
39
|
+
attest(path: string, release: string): Promise<void>;
|
|
40
|
+
}
|
|
41
|
+
/** Parse only a catalog whose attestation was already verified against the requested immutable tag. */
|
|
42
|
+
export declare function parseBundleCatalog(bytes: Uint8Array, requested: string): BundleCatalog;
|
|
43
|
+
export declare function extractBundle(bytes: Uint8Array, item: Pick<LockedBundle, 'name' | 'version' | 'entry' | 'sha256' | 'coreVersion'>, destination: string): Promise<void>;
|
|
44
|
+
export declare function bundleCachePath(project: string, sha256: string): string;
|
|
45
|
+
export declare function readBundleLock(project: string): Promise<BundleLock>;
|
|
46
|
+
export declare const githubBundleTransport: BundleTransport;
|
|
47
|
+
export declare function installBundle(project: string, release: string, bundleName: string, transport?: BundleTransport): Promise<BundleLock>;
|
|
48
|
+
/** Explicit host-only loader. It never reads project YAML, downloads, updates, or discovers code. */
|
|
49
|
+
export declare function loadExtensionBundle(project: string, bundleName: string): Promise<Record<string, unknown>>;
|
|
@@ -185,6 +185,8 @@ export interface ScaffoldRequest {
|
|
|
185
185
|
hostFile: string;
|
|
186
186
|
/** Every extension name being scaffolded together, including this one, in a canonical (sorted) order that is independent of the `--with` spelling. */
|
|
187
187
|
names: readonly string[];
|
|
188
|
+
/** `npm` resolves extension packages from the operator's install; `bundle` resolves only already-verified, locked release bundles. */
|
|
189
|
+
distribution?: 'npm' | 'bundle';
|
|
188
190
|
/**
|
|
189
191
|
* Operator acknowledgements from repeated `--ack <extension>:<id>` flags, sorted and de-duplicated; empty when none. Core treats
|
|
190
192
|
* them as opaque strings and never invents one. An extension reads only the ones qualified with its own name. To require one, throw
|
|
@@ -224,6 +226,8 @@ export interface ScaffoldResult {
|
|
|
224
226
|
hostSetup: string[];
|
|
225
227
|
hostEntries: string[];
|
|
226
228
|
hostClose?: string[];
|
|
229
|
+
/** Named exports core may bind from this extension's already-verified executable bundle. Required for bundle distribution; never a project-controlled module reference. */
|
|
230
|
+
hostBundleExports?: string[];
|
|
227
231
|
/** Files written relative to `directory` with their modes; never inside the project, never overwriting. */
|
|
228
232
|
files: ScaffoldFile[];
|
|
229
233
|
/** Markdown appended to README.md under a heading core adds; the numbered steps merged in the resolved order. */
|
|
@@ -36,8 +36,12 @@ export interface FunctionWorkerData {
|
|
|
36
36
|
dependencies: Record<string, string[]>;
|
|
37
37
|
entries: [string, string][];
|
|
38
38
|
}
|
|
39
|
+
/** `route.pattern` is the route key that matched, so one module can serve several routes without reading `request.url`. */
|
|
39
40
|
export type FunctionContext = RequestContext & {
|
|
40
41
|
args?: Record<string, ParameterValue>;
|
|
42
|
+
route?: {
|
|
43
|
+
pattern: string;
|
|
44
|
+
};
|
|
41
45
|
};
|
|
42
46
|
export interface FunctionWorkerRequest {
|
|
43
47
|
id: string;
|
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';
|
|
@@ -40,3 +40,5 @@ export { initProject, addRedirect } from './authoring.ts';
|
|
|
40
40
|
export { initProjectWith } from './init-with.ts';
|
|
41
41
|
export { collectDependencySet, renderPackageManifest, installSteps } from './project-dependencies.ts';
|
|
42
42
|
export type { ScaffoldRequest, ScaffoldResult, ScaffoldFile } from './extensions.ts';
|
|
43
|
+
export { installBundle, loadExtensionBundle, readBundleLock, parseBundleCatalog } from './extension-bundles.ts';
|
|
44
|
+
export type { BundleCatalog, BundleEntry, BundleLock, LockedBundle, BundleTransport } from './extension-bundles.ts';
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ScaffoldResult } from './extensions.ts';
|
|
2
2
|
import type { DependencyPin } from './project-dependencies.ts';
|
|
3
|
+
import { type BundleTransport } from './extension-bundles.ts';
|
|
3
4
|
export interface InitWithOptions {
|
|
4
5
|
cwd?: string | undefined;
|
|
5
6
|
/** Default true: record exact pins for core, the named extensions and their declared peers. */
|
|
@@ -8,6 +9,10 @@ export interface InitWithOptions {
|
|
|
8
9
|
pins?: ReadonlyMap<string, string> | undefined;
|
|
9
10
|
/** `--ack <extension>:<id>`, repeatable: opaque qualified acknowledgements handed to every scaffold. Core refuses one that no scaffold consumed. */
|
|
10
11
|
acknowledgements?: readonly string[] | undefined;
|
|
12
|
+
/** Immutable signed release used instead of resolving executable extension packages from npm. */
|
|
13
|
+
bundleRelease?: string | undefined;
|
|
14
|
+
/** Test-only transport injection; production uses GitHub attestation verification. */
|
|
15
|
+
bundleTransport?: BundleTransport | undefined;
|
|
11
16
|
}
|
|
12
17
|
export interface InitWithResult {
|
|
13
18
|
directory: string;
|
|
@@ -30,4 +35,4 @@ export declare function orderScaffolds(results: readonly ScaffoldResult[]): Scaf
|
|
|
30
35
|
* `urlcode.yaml`, one `host.mjs`, one `README.md` and the extensions' own files. All packages are resolved and
|
|
31
36
|
* their scaffolds computed before anything is written, so a refusal leaves no directory behind.
|
|
32
37
|
*/
|
|
33
|
-
export declare function initProjectWith(destination: string, requested: readonly string[], { cwd, manifest, pins, acknowledgements }?: InitWithOptions): Promise<InitWithResult>;
|
|
38
|
+
export declare function initProjectWith(destination: string, requested: readonly string[], { cwd, manifest, pins, acknowledgements, bundleRelease, bundleTransport }?: InitWithOptions): Promise<InitWithResult>;
|
package/dist/types/match.d.ts
CHANGED
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/dist/types/types.d.ts
CHANGED
|
@@ -264,6 +264,7 @@ export interface CompiledRoute extends Omit<RouteConfig, 'methods' | 'parameters
|
|
|
264
264
|
reply?: Reply;
|
|
265
265
|
expiresAt?: number;
|
|
266
266
|
prefix?: string;
|
|
267
|
+
wildcard?: boolean;
|
|
267
268
|
middleware: CompiledMiddleware[];
|
|
268
269
|
function?: CompiledFunction;
|
|
269
270
|
respond?: RespondSpec;
|
package/dist/types.js
CHANGED
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 60,557 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.
|
|
10
|
+
> `version: "1"`. Core is Apache-2.0; this revision is `0.5.0`. `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`.
|
|
@@ -18,9 +18,6 @@ or fetch inside a `sandbox: true` function, regex routes, database access,
|
|
|
18
18
|
global middleware, YAML interpolation, or packages named in YAML. Secrets need external revision-pinned
|
|
19
19
|
grants. Unsupported features fail with the route named; nothing degrades silently.
|
|
20
20
|
|
|
21
|
-
Agents that explicitly want the complete consolidated reference in one fetch should read
|
|
22
|
-
[llms-full.txt](https://github.com/jimhoyd-com/urlcode/blob/main/llms-full.txt), generated from the documents below (about 50k tokens, estimated).
|
|
23
|
-
|
|
24
21
|
## Contents
|
|
25
22
|
|
|
26
23
|
1. [The URLCode framework](#the-urlcode-framework)
|
|
@@ -199,15 +196,23 @@ and the MCP `get_extensions` tool report those surfaces and their fast checks,
|
|
|
199
196
|
so people and agents can discover the supported path instead of replacing
|
|
200
197
|
package behavior.
|
|
201
198
|
|
|
199
|
+
A signed declarative artifact is a separate, optional authoring input, not a
|
|
200
|
+
fifth way to compose executable behavior. A project may lock an attested
|
|
201
|
+
schema/example bundle and expose it through MCP `get_extension_artifacts` and
|
|
202
|
+
`get_extension_artifact`; the npm package and operator host remain the only
|
|
203
|
+
executable extension path. See [signed declarative artifacts](https://github.com/jimhoyd-com/urlcode/blob/main/docs/EXTENSIONS.md#signed-declarative-artifacts).
|
|
204
|
+
|
|
202
205
|
```sh
|
|
203
206
|
urlcode serve --project /absolute/site --host-file /absolute/operator/host.mjs --origin https://site.example
|
|
204
207
|
```
|
|
205
208
|
|
|
206
|
-
`urlcode init <dir> --with ui,auth,admin` writes this layout in one step:
|
|
207
|
-
resolves each installed `@jimhoyd/urlcode-<name>` from the current
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
209
|
+
`urlcode init <dir> --with ui,auth,admin` writes this layout in one step: by
|
|
210
|
+
default it resolves each installed `@jimhoyd/urlcode-<name>` from the current
|
|
211
|
+
directory; with `--bundle-release extension-bundles@v…` it verifies and locks
|
|
212
|
+
the named GitHub Release bundles instead. In either mode it calls the verified
|
|
213
|
+
module's `scaffold` export and merges fragments into `app/urlcode.yaml`, one
|
|
214
|
+
explicit `host.mjs` and one `README.md`, refusing before writing a site when a
|
|
215
|
+
package/bundle is missing or two fragments collide (the contract is documented under
|
|
211
216
|
[scaffolding](https://github.com/jimhoyd-com/urlcode/blob/main/docs/EXTENSIONS.md#scaffolding-with-init---with)). `urlcode-auth init`
|
|
212
217
|
and `urlcode-admin init` write the same layout for a single package; `urlcode-auth bootstrap` creates the first
|
|
213
218
|
administrator from JSON on stdin. `inspectExtensionRevision(project)` prints
|
|
@@ -301,7 +306,10 @@ Make the first retrieval one bounded query: the MCP tool `get_context` when the
|
|
|
301
306
|
`urlcode capabilities NAME` (MCP `get_capability`) for one capability's limits,
|
|
302
307
|
`get_schema` for one YAML fragment, `urlcode recipes search TEXT`
|
|
303
308
|
(`search_recipes`), `explain` for a route's effective behavior and, when the
|
|
304
|
-
operator supplies a host file, `get_extensions`.
|
|
309
|
+
operator supplies a host file, `get_extensions`. If the project has a committed
|
|
310
|
+
`urlcode.extensions.lock.json`, use `get_extension_artifacts` to verify and
|
|
311
|
+
inventory its inert data and `get_extension_artifact` to retrieve only the
|
|
312
|
+
needed schema, example or README. Context is a summary with the
|
|
305
313
|
constraints and exact commands, not a schema dump, and it never hides a
|
|
306
314
|
capability limit: ask `capabilities NAME` before promising a feature.
|
|
307
315
|
|
|
@@ -356,7 +364,15 @@ teaches how to retrieve the minimum reference through `urlcode capabilities`,
|
|
|
356
364
|
the documentation whole. For a host-composed application, `get_extensions`
|
|
357
365
|
adds each extension's schemas, hooks, supported authoring surfaces and fast
|
|
358
366
|
checks. Agents should use those surfaces before generating replacement package
|
|
359
|
-
behavior.
|
|
367
|
+
behavior. A committed artifact lock is a separate offline authoring input:
|
|
368
|
+
`get_extension_artifacts` validates its cache and lists allowlisted files;
|
|
369
|
+
`get_extension_artifact` reads one bounded JSON or Markdown member from its
|
|
370
|
+
verified archive. The CLI fallback is `urlcode extension-artifacts inspect
|
|
371
|
+
--project DIR --json`. An artifact never installs or activates an npm package,
|
|
372
|
+
registers a host extension or grants authority. Agents must not fetch or update
|
|
373
|
+
one unless the user explicitly requests that project change and names the
|
|
374
|
+
immutable `extensions@v…` release. Neither guide nor artifact replaces the
|
|
375
|
+
runtime schema; all defer to the pinned implementation.
|
|
360
376
|
|
|
361
377
|
Treat core, installed extensions and product UI as one application with
|
|
362
378
|
different owners. Keep auth/admin security and workflow behavior package-owned;
|
|
@@ -482,8 +498,21 @@ Before writing a function, check whether a declarative feature already covers th
|
|
|
482
498
|
need. Security headers are the usual miss: a project that declares nothing sends
|
|
483
499
|
only the runtime's defaults (`nosniff`, `no-store`, a request ID).
|
|
484
500
|
|
|
501
|
+
Building only redirects? `urlcode context --project DIR --task redirects` (MCP
|
|
502
|
+
`get_context {"task":"redirects"}`) is a bounded, redirect-only call: every row
|
|
503
|
+
below with the exact YAML, the two gaps with their exact validation error, and
|
|
504
|
+
this project's own redirects — cheaper than this table or the recipe catalog.
|
|
505
|
+
|
|
485
506
|
| I need | Declare | Reference |
|
|
486
507
|
|---|---|---|
|
|
508
|
+
| 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) |
|
|
509
|
+
| 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) |
|
|
510
|
+
| Root-relative redirect (`/users/{id}` to `/profiles/{id}`) | `redirect.url: /profiles/{id}`: one leading slash, path only, `{name}` placeholders | [redirects](https://github.com/jimhoyd-com/urlcode/blob/main/docs/yaml/redirects.md) |
|
|
511
|
+
| Wildcard/suffix redirect (`/legacy/**` to `/modern/{**}`, any depth) | terminal `/**` route key with a literal prefix, `{**}` in the destination path; redirect only, not static or Cloudflare | [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
|
+
| Host-based or scheme-based redirect — **gap** | not expressible; destination is a literal absolute `https://host/path` or a root-relative path | [open decision](https://github.com/jimhoyd-com/urlcode/blob/main/docs/OPEN-DECISIONS.md) |
|
|
487
516
|
| 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
517
|
| 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
518
|
| A cache strategy on any route | `policies.cache` | [cache](https://github.com/jimhoyd-com/urlcode/blob/main/docs/policies/cache.md) |
|
|
@@ -507,8 +536,9 @@ Which handler serves the response:
|
|
|
507
536
|
Data persistence has no native handler. The operator-installed `store` extension
|
|
508
537
|
serves declared collections as a CRUD API, and `urlcode recipes search "crud store
|
|
509
538
|
persist"` finds the `store-crud` recipe. It needs the operator to install
|
|
510
|
-
`@jimhoyd/urlcode-store`
|
|
511
|
-
ui,auth,store
|
|
539
|
+
`@jimhoyd/urlcode-store` package or an attested executable bundle and a host
|
|
540
|
+
file. `init --with ui,auth,store --bundle-release extension-bundles@v…`
|
|
541
|
+
scaffolds the npm-free bundle form; a no-auth
|
|
512
542
|
`--with store` needs `--ack store:public-write`, which only a core release after the
|
|
513
543
|
store's first publication has, so say so rather than promising it. Report anything beyond that recipe (filtering, sorting, per-record
|
|
514
544
|
ownership, a database) as a gap. `urlcode context` lists the same built-ins so
|
|
@@ -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.
|
|
760
|
+
This guide targets URLCode 0.5.0. 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.
|
|
@@ -1297,7 +1327,8 @@ Keys are absolute case-sensitive paths. Trailing slashes are significant.
|
|
|
1297
1327
|
Parameters occupy whole segments, e.g. `/p/{id}`, with distinct identifier names.
|
|
1298
1328
|
Each parameter matches exactly one nonempty segment, never across `/`; it is not
|
|
1299
1329
|
greedy. No regex paths, client-controlled host dispatch or dot segments. Only static directory mounts
|
|
1300
|
-
support a terminal `/*` wildcard with an otherwise literal path
|
|
1330
|
+
support a terminal `/*` wildcard with an otherwise literal path; a `redirect` alone supports a
|
|
1331
|
+
terminal `/**` (one or more remaining segments, at least a one-segment literal prefix, see [route matching](https://github.com/jimhoyd-com/urlcode/blob/main/docs/ROUTING.md)). Route keys cannot contain
|
|
1301
1332
|
percent encoding, spaces, backslashes or query strings. Path length is limited
|
|
1302
1333
|
to 2,048 characters and 32 segments. `/_urlcode` is reserved.
|
|
1303
1334
|
|
|
@@ -1436,7 +1467,9 @@ Unknown query keys are ignored unless explicitly passed by a redirect.
|
|
|
1436
1467
|
### Redirects
|
|
1437
1468
|
|
|
1438
1469
|
`redirect.url` is an absolute HTTP(S) URL with literal scheme/host and no embedded
|
|
1439
|
-
credentials or whitespace/control characters
|
|
1470
|
+
credentials or whitespace/control characters, or a root-relative path (one leading `/`, never
|
|
1471
|
+
`//`, no dot segments) that answers a path-only `Location`. On a `/**` route `{**}` is the captured
|
|
1472
|
+
suffix, once, each segment encoded. `{pathInput}` placeholders are
|
|
1440
1473
|
allowed only in the destination pathname and encoded as single components.
|
|
1441
1474
|
No environment/secret interpolation. Status defaults to 302; allowed values are
|
|
1442
1475
|
301, 302, 303, 307 and 308.
|
|
@@ -1679,6 +1712,7 @@ not part of the route key.
|
|
|
1679
1712
|
| `/go` | `/go`, `/go?campaign=spring` | `/Go`, `/go/`, `/go/extra` |
|
|
1680
1713
|
| `/r/{code}` | `/r/abc`, `/r/123` | `/r/`, `/r/abc/extra` |
|
|
1681
1714
|
| `/r/{code}/details` | `/r/abc/details` | `/r/abc/other/details` |
|
|
1715
|
+
| `/legacy/**` with a `redirect` handler | `/legacy/a`, `/legacy/a/b/c` | `/legacy`, `/legacy/`, `/legacy/a//b` |
|
|
1682
1716
|
| `/assets/*` with a `static` handler | Files under `/assets/`, including `/assets/css/site.css` | `/assets`, `/assets-other/site.css` |
|
|
1683
1717
|
|
|
1684
1718
|
A `{parameter}` captures exactly one nonempty path segment. It is **not greedy**:
|
|
@@ -1691,8 +1725,16 @@ path. It covers the remaining nested file path; it is not a named capture or a
|
|
|
1691
1725
|
regex operator. Matching a mount does not guarantee a response file exists:
|
|
1692
1726
|
missing files return 404. It is not a catch-all for functions or redirects.
|
|
1693
1727
|
|
|
1728
|
+
A `redirect` handler alone supports a terminal `/**` after a literal prefix (never bare `/**`, never
|
|
1729
|
+
with a `{parameter}`). It matches one or more remaining segments, and `{**}` in `redirect.url` is
|
|
1730
|
+
those segments, each percent-encoded and joined by `/`, usable once and only in the destination
|
|
1731
|
+
path. Empty segments, `.`/`..`, encoded slashes and captures over 1,024 characters do not match.
|
|
1732
|
+
Exact and `{parameter}` routes always win over it, so `/legacy/keep/{id}` can carve an exception out
|
|
1733
|
+
of `/legacy/**`. It is refused on static hosting (S3 redirects match one path) and on Cloudflare
|
|
1734
|
+
until the Worker table supports suffix matching, and it cannot share a prefix with a `static` mount.
|
|
1735
|
+
|
|
1694
1736
|
No regex routes, greedy parameters, optional segments, partial-segment parameters,
|
|
1695
|
-
`**` globs, or regex constraints inside `{code}` are implemented. Characters such
|
|
1737
|
+
other `**` globs, or regex constraints inside `{code}` are implemented. Characters such
|
|
1696
1738
|
as `.` and `+` have no regex meaning in a literal path. Do not paste a regex into
|
|
1697
1739
|
a route key: some regex-looking text is legal literal text, while unsupported
|
|
1698
1740
|
syntax may fail validation. Parameter-schema `pattern` is also unsupported.
|
|
@@ -2551,15 +2593,19 @@ as the not-found page instead. The page answers every unknown URL, so it cannot
|
|
|
2551
2593
|
|
|
2552
2594
|
`urlcode build --target static` writes it as the object `404.html` (see
|
|
2553
2595
|
[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
|
-
|
|
2596
|
+
Cloudflare inlines it: the page is small, singular and static, so the build
|
|
2597
|
+
reads it (64 KiB cap, must decode as UTF-8) and carries it in the artifact as a
|
|
2598
|
+
`respond` route at `/404.html`, answering the same status, headers and method
|
|
2599
|
+
rules as every other target. See [`docs/CLOUDFLARE.md`](https://github.com/jimhoyd-com/urlcode/blob/main/docs/CLOUDFLARE.md#site-notfound-is-inlined).
|
|
2600
|
+
`favicon` and `llms` stay refused there like any other `page` route (no asset
|
|
2601
|
+
binding).
|
|
2556
2602
|
|
|
2557
2603
|
### Per-target support
|
|
2558
2604
|
|
|
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 |
|
|
2605
|
+
| Target | `robots`, `sitemap`, `securityTxt` (`respond`) | `favicon`, `llms` (`page`) | `notFound` |
|
|
2606
|
+
| --- | --- | --- | --- |
|
|
2607
|
+
| self-hosted, Vercel, AWS | served | served | served |
|
|
2608
|
+
| 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
2609
|
|
|
2564
2610
|
### Not in this release
|
|
2565
2611
|
|
|
@@ -3218,6 +3264,124 @@ Serving the result is the usual explicit host binding:
|
|
|
3218
3264
|
urlcode validate --project app --host-file "$PWD/host.mjs" --origin https://site.example
|
|
3219
3265
|
```
|
|
3220
3266
|
|
|
3267
|
+
### Signed declarative artifacts
|
|
3268
|
+
|
|
3269
|
+
Core remains the npm-distributed runtime. The workspace extensions are not an
|
|
3270
|
+
alternate npm channel. A release can additionally carry a small, **data-only**
|
|
3271
|
+
extension artifact for tooling that understands its declared format. It is not
|
|
3272
|
+
a Node module and cannot activate an extension, run a hook, replace a trusted
|
|
3273
|
+
operator host, or grant a route any authority.
|
|
3274
|
+
|
|
3275
|
+
Install an artifact only from its immutable `extensions@v…` GitHub Release. The
|
|
3276
|
+
published inert store configuration schema snapshot can be installed with:
|
|
3277
|
+
|
|
3278
|
+
```sh
|
|
3279
|
+
urlcode extension-artifacts install store-schema --artifact-release extensions@v1.0.0 --project app
|
|
3280
|
+
urlcode extension-artifacts update store-schema --artifact-release extensions@v1.1.0 --project app
|
|
3281
|
+
urlcode extension-artifacts inspect --project app
|
|
3282
|
+
```
|
|
3283
|
+
|
|
3284
|
+
The command downloads the signed `extensions-catalog.json`, verifies its
|
|
3285
|
+
GitHub attestation against the dedicated artifact workflow in
|
|
3286
|
+
`jimhoyd-com/urlcode` and the exact requested tag ref, then verifies the selected
|
|
3287
|
+
`.tgz` the same way. Self-hosted-runner attestations are refused. The catalog
|
|
3288
|
+
pins its release tag, source commit, filename and SHA-256; a catalog revocation
|
|
3289
|
+
refuses installation. `gh` with support for attestation source-ref verification
|
|
3290
|
+
is therefore a required local dependency for this command.
|
|
3291
|
+
|
|
3292
|
+
The resulting `urlcode.extensions.lock.json` is the reproducibility boundary:
|
|
3293
|
+
commit it with the project. Every locked artifact records its own catalog tag
|
|
3294
|
+
and source commit, so updating one artifact cannot silently relabel another as
|
|
3295
|
+
coming from a newer release. Extracted files live under
|
|
3296
|
+
`app/.urlcode/extensions/<sha256>/` and are checked before extraction. Archives
|
|
3297
|
+
are size- and file-count-bounded, reject links and traversal, and may contain
|
|
3298
|
+
only `extension.json`, JSON configuration/schema data, and an optional README.
|
|
3299
|
+
Any JavaScript, package manifest, install hook, native module, or unknown file
|
|
3300
|
+
causes refusal. `inspect` re-hashes the cached archive and every extracted file,
|
|
3301
|
+
reporting an entry as missing or invalid rather than trusting its directory
|
|
3302
|
+
name. Updates are never automatic: review a newer immutable release and run
|
|
3303
|
+
`update` explicitly.
|
|
3304
|
+
|
|
3305
|
+
Agent tooling can consume a committed lock without gaining write or execution
|
|
3306
|
+
authority. MCP `get_extension_artifacts` validates the lock and cache and lists
|
|
3307
|
+
the signed member paths; `get_extension_artifact {name, path}` returns one
|
|
3308
|
+
verified, bounded JSON or Markdown member directly from the cached archive.
|
|
3309
|
+
The CLI fallback is `urlcode extension-artifacts inspect --project app --json`.
|
|
3310
|
+
Neither MCP tool performs a network request, installs or updates an artifact,
|
|
3311
|
+
loads a host file, or activates code. A missing or modified cache is reported
|
|
3312
|
+
as missing/invalid and its contents are not returned.
|
|
3313
|
+
|
|
3314
|
+
This does not relax the existing host boundary. `--host-file` is still the only
|
|
3315
|
+
way to load trusted operator extension code, and artifact files are never
|
|
3316
|
+
imported by `serve`, `validate`, `init --with`, or the runtime.
|
|
3317
|
+
|
|
3318
|
+
Artifact versions are independent from npm package versions. The initial
|
|
3319
|
+
`store-schema` artifact is a reviewed configuration-schema snapshot and example,
|
|
3320
|
+
not the `@jimhoyd/urlcode-store` implementation. Installing it does not install
|
|
3321
|
+
or activate that package. Its README names the separate executable and operator
|
|
3322
|
+
requirements.
|
|
3323
|
+
|
|
3324
|
+
### Signed executable extension bundles
|
|
3325
|
+
|
|
3326
|
+
Official executable extensions are migrating away from consumer npm installs.
|
|
3327
|
+
They use a separate, immutable `extension-bundles@v…` GitHub Release namespace;
|
|
3328
|
+
it is intentionally disjoint from the permanently data-only `extensions@v…`
|
|
3329
|
+
artifact channel above. A bundle is a bounded, frozen Node module tree produced
|
|
3330
|
+
from reviewed first-party source, not a general extension marketplace and not
|
|
3331
|
+
a project dependency resolver.
|
|
3332
|
+
|
|
3333
|
+
An operator explicitly installs one named bundle from an immutable release:
|
|
3334
|
+
|
|
3335
|
+
```sh
|
|
3336
|
+
urlcode extension-bundles install store \
|
|
3337
|
+
--bundle-release extension-bundles@v1.0.0 --project app
|
|
3338
|
+
```
|
|
3339
|
+
|
|
3340
|
+
For a new composed site, `init --with` can perform that verified installation
|
|
3341
|
+
before it writes the route project. This is the npm-free extension path: the
|
|
3342
|
+
generated `package.json`, when requested, pins URLCode core only; the generated
|
|
3343
|
+
host loads only the names recorded in the bundle lockfile.
|
|
3344
|
+
|
|
3345
|
+
```sh
|
|
3346
|
+
urlcode init site --with ui,auth,admin \
|
|
3347
|
+
--bundle-release extension-bundles@v1.0.0
|
|
3348
|
+
```
|
|
3349
|
+
|
|
3350
|
+
`init` verifies each requested bundle in a temporary operator staging root,
|
|
3351
|
+
obtains each scaffold from that verified module tree, then writes the cache and
|
|
3352
|
+
`urlcode.extension-bundles.lock.json` into the new site. It never resolves an
|
|
3353
|
+
extension package from npm in this mode. A failed verification or scaffold
|
|
3354
|
+
refusal leaves no site directory behind. The release tag is still an explicit
|
|
3355
|
+
operator choice; YAML cannot supply it.
|
|
3356
|
+
|
|
3357
|
+
The command verifies attestations for both the catalog and selected archive
|
|
3358
|
+
against the requested tag and dedicated workflow, rejects self-hosted runners,
|
|
3359
|
+
checks the catalog's commit, filename and SHA-256, and extracts only regular
|
|
3360
|
+
files in the signed module tree. It writes
|
|
3361
|
+
`urlcode.extension-bundles.lock.json` and keeps the frozen bytes under
|
|
3362
|
+
`app/.urlcode/extension-bundles/<sha256>/`. There is no automatic discovery,
|
|
3363
|
+
installation, update, or fallback to npm. `inspect` reads the committed lock;
|
|
3364
|
+
a modified cache or an incompatible core version refuses before import.
|
|
3365
|
+
|
|
3366
|
+
Executable bundles are **trusted operator code**, exactly like a hand-written
|
|
3367
|
+
operator host module. Project YAML cannot choose a bundle, name a release,
|
|
3368
|
+
trigger a download, or grant a bundle authority. An operator host explicitly
|
|
3369
|
+
loads a locked entry by name, then chooses which returned registration to pass
|
|
3370
|
+
to `createRuntime`:
|
|
3371
|
+
|
|
3372
|
+
```js
|
|
3373
|
+
import { loadExtensionBundle } from '@jimhoyd/urlcode/extension-bundles';
|
|
3374
|
+
|
|
3375
|
+
const { storeExtension } = await loadExtensionBundle('/absolute/site/app', 'store');
|
|
3376
|
+
export default { extensions: [storeExtension({ directory: '/srv/site-data', projectSha256: process.env.PROJECT_SHA256 })] };
|
|
3377
|
+
```
|
|
3378
|
+
|
|
3379
|
+
This does not make bundle code sandboxed and does not alter a route that
|
|
3380
|
+
declares `sandbox: true`; those remain distinct execution modes. npm packages
|
|
3381
|
+
remain the migration fallback until the first signed bundle release and the
|
|
3382
|
+
fresh composed consumer flow have been released and proven. Do not unpublish a
|
|
3383
|
+
package merely because its data-only artifact exists.
|
|
3384
|
+
|
|
3221
3385
|
---
|
|
3222
3386
|
|
|
3223
3387
|
# Composing a site from ui, auth and admin
|