@jimhoyd/urlcode 0.4.0-alpha.3 → 0.4.2
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 +10 -0
- package/.claude-plugin/marketplace.json +1 -1
- package/CONTRIBUTING.md +36 -0
- package/README.md +20 -15
- package/ROADMAP.md +25 -16
- package/dist/BUILD-MANIFEST.json +20 -19
- package/dist/authoring.js +15 -1
- package/dist/capability-query.js +0 -1
- package/dist/catalog.js +0 -1
- package/dist/cli.js +24 -7
- package/dist/config.js +1 -1
- package/dist/explain.js +1 -1
- package/dist/extensions.js +78 -1
- package/dist/http-response.js +1 -1
- package/dist/index.js +1 -0
- package/dist/init-with.js +36 -11
- package/dist/manifest.js +1 -1
- package/dist/mcp-authoring.js +2 -2
- package/dist/mcp.js +1 -1
- package/dist/policies/cache.js +2 -2
- package/dist/policy.js +16 -0
- package/dist/project-dependencies.js +305 -0
- package/dist/readiness.js +5 -1
- package/dist/runtime.js +1 -1
- package/dist/tooling.js +2 -1
- package/dist/trusted-functions.js +4 -5
- package/dist/types/authoring.d.ts +9 -1
- package/dist/types/capability-query.d.ts +0 -1
- package/dist/types/catalog.d.ts +0 -4
- package/dist/types/config.d.ts +1 -9
- package/dist/types/explain.d.ts +0 -1
- package/dist/types/extensions.d.ts +58 -0
- package/dist/types/http-response.d.ts +0 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/init-with.d.ts +7 -13
- package/dist/types/manifest.d.ts +0 -1
- package/dist/types/project-dependencies.d.ts +78 -0
- package/dist/types/readiness.d.ts +3 -0
- package/dist/types/tooling.d.ts +1 -0
- package/dist/types/trusted-functions.d.ts +1 -4
- package/docs/AI-AUTHORING.md +10 -5
- package/docs/AWS.md +9 -0
- package/docs/CI-FOLLOWUP-2026-09-19.md +1 -1
- package/docs/CODEBASE-AUDIT-2026-09-20.md +6 -0
- package/docs/COMPOSING-A-SITE.md +287 -0
- package/docs/CONTAINER-PROMOTION.md +74 -0
- package/docs/DEVELOPMENT-PIPELINE.md +242 -119
- package/docs/EXTENSIONS.md +88 -93
- package/docs/FRAMEWORK.md +45 -30
- package/docs/FUNCTION-SECURITY.md +5 -8
- package/docs/INSTALL.md +13 -8
- package/docs/MIDDLEWARE.md +10 -4
- package/docs/OPEN-DECISIONS.md +64 -99
- package/docs/READINESS.md +8 -4
- package/docs/README.md +18 -13
- package/docs/RELEASE-0.4.1.md +73 -0
- package/docs/RELEASE-0.4.2.md +30 -0
- package/docs/RELEASE-READINESS.md +40 -11
- package/docs/RELEASE-SECURITY.md +33 -14
- package/docs/SPECIFICATION.md +5 -1
- package/docs/SPIKE-CORE-LAYERING.md +1 -1
- package/docs/SPIKE-DEFAULT-TRUST-MODEL.md +9 -13
- package/docs/STARTERS.md +17 -5
- package/docs/TOOLING.md +7 -5
- package/docs/VERCEL.md +10 -2
- package/docs/VERSION-ALIGNMENT.md +50 -8
- package/docs/archive/2026-09-19/ROADMAP.md +1 -0
- package/docs/archive/2026-09-19/SPIKE-EXTENSION-MODEL.md +1 -0
- package/docs/{SPIKE-LAMBDA-COMPILE.md → archive/2026-09-19/SPIKE-LAMBDA-COMPILE.md} +168 -12
- package/docs/archive/2026-09-19/SPIKE-MONOREPO.md +2 -0
- package/docs/archive/2026-09-20/OPEN-DECISIONS-COMPLETED.md +116 -0
- package/docs/archive/README.md +2 -0
- package/docs/yaml/functions.md +10 -2
- package/docs/yaml/middleware.md +5 -3
- package/examples/cookbook/middleware/envelope.mjs +4 -2
- package/llms-full.txt +458 -143
- package/llms.txt +2 -1
- package/package.json +8 -5
- package/packaging/claude-plugin/.claude-plugin/plugin.json +1 -1
- package/packaging/claude-plugin/skills/urlcode-authoring/SKILL.md +10 -0
- package/recipes/middleware/middleware/envelope.mjs +4 -2
- package/skills/urlcode/SKILL.md +8 -1
package/dist/types/config.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { LoadedDocument, ProjectDocument, RouteConfig } from './types.ts';
|
|
2
2
|
/** What config-worker.ts posts back: the loaded document, or the ConfigError message. */
|
|
3
3
|
export type ConfigWorkerResult = {
|
|
4
4
|
value: LoadedDocument;
|
|
@@ -17,14 +17,6 @@ export declare const SHORT_FORM_PATH_SCHEMA: {
|
|
|
17
17
|
readonly minLength: 1;
|
|
18
18
|
readonly maxLength: 128;
|
|
19
19
|
};
|
|
20
|
-
/**
|
|
21
|
-
* Expands the YAML short forms into the canonical long form. `function: functions/x.mjs`
|
|
22
|
-
* becomes `{source, args}` with an argument per `{param}` in the path, declaring any
|
|
23
|
-
* parameter the route does not declare itself; a string middleware entry becomes `{source}`;
|
|
24
|
-
* a route-level `cache` becomes `policies.cache` (refused alongside a direct `policies.cache`).
|
|
25
|
-
* Everything downstream (routes, audit, the compiled table) sees only the long form.
|
|
26
|
-
*/
|
|
27
|
-
export declare function normalizeRoute(pattern: string, route: AuthoredRouteConfig | RouteConfig): RouteConfig;
|
|
28
20
|
export declare function safeFile(root: string, file: unknown): Promise<string>;
|
|
29
21
|
export declare function loadDocument(project: string, { timeoutMs }?: {
|
|
30
22
|
timeoutMs?: number;
|
package/dist/types/explain.d.ts
CHANGED
|
@@ -95,7 +95,6 @@ export interface ExplainOptions {
|
|
|
95
95
|
projectSha256?: string | undefined;
|
|
96
96
|
now?: number | undefined;
|
|
97
97
|
}
|
|
98
|
-
export declare function routeState(route: CompiledRoute, now: number): RouteState;
|
|
99
98
|
/** Describe one compiled route. `chain` is the policy chain compiled for it, when the project declares policies. */
|
|
100
99
|
export declare function explainCompiledRoute(loaded: LoadedDocument, route: CompiledRoute, chain: PolicyChain | undefined, options?: ExplainOptions): RouteExplanation;
|
|
101
100
|
export declare function nearestRoutes(target: string, patterns: Iterable<string>, limit?: number): string[];
|
|
@@ -66,6 +66,62 @@ export interface ExtensionInstance {
|
|
|
66
66
|
export interface ExtensionImmutableAssets {
|
|
67
67
|
prefix: string;
|
|
68
68
|
}
|
|
69
|
+
export interface ExtensionHookReference {
|
|
70
|
+
source: string;
|
|
71
|
+
export?: string;
|
|
72
|
+
sandbox?: boolean;
|
|
73
|
+
sandboxReason?: string;
|
|
74
|
+
}
|
|
75
|
+
export type ExtensionHookConfig = string | ExtensionHookReference;
|
|
76
|
+
export type ExtensionHookKind = 'filter' | 'action';
|
|
77
|
+
/** Machine-readable contract for one project hook an extension exposes. */
|
|
78
|
+
export interface ExtensionHookContract {
|
|
79
|
+
name: string;
|
|
80
|
+
kind: ExtensionHookKind;
|
|
81
|
+
description: string;
|
|
82
|
+
inputSchema: object;
|
|
83
|
+
outputSchema?: object;
|
|
84
|
+
}
|
|
85
|
+
export type LoadedExtensionHooks<T extends string = string> = Partial<Record<T, (input: unknown) => unknown>>;
|
|
86
|
+
/** Shared schema for project hook references. Omission means trusted execution. */
|
|
87
|
+
export declare const extensionHookReferenceSchema: {
|
|
88
|
+
readonly oneOf: readonly [{
|
|
89
|
+
readonly type: "string";
|
|
90
|
+
readonly minLength: 1;
|
|
91
|
+
readonly maxLength: 1024;
|
|
92
|
+
}, {
|
|
93
|
+
readonly type: "object";
|
|
94
|
+
readonly additionalProperties: false;
|
|
95
|
+
readonly required: readonly ["source"];
|
|
96
|
+
readonly properties: {
|
|
97
|
+
readonly source: {
|
|
98
|
+
readonly type: "string";
|
|
99
|
+
readonly minLength: 1;
|
|
100
|
+
readonly maxLength: 1024;
|
|
101
|
+
};
|
|
102
|
+
readonly export: {
|
|
103
|
+
readonly type: "string";
|
|
104
|
+
readonly pattern: "^[A-Za-z_][A-Za-z0-9_]*$";
|
|
105
|
+
};
|
|
106
|
+
readonly sandbox: {
|
|
107
|
+
readonly type: "boolean";
|
|
108
|
+
};
|
|
109
|
+
readonly sandboxReason: {
|
|
110
|
+
readonly type: "string";
|
|
111
|
+
readonly minLength: 1;
|
|
112
|
+
readonly maxLength: 512;
|
|
113
|
+
};
|
|
114
|
+
};
|
|
115
|
+
}];
|
|
116
|
+
};
|
|
117
|
+
/** Builds the strict `config.hooks` schema from an extension's declared hook names. */
|
|
118
|
+
export declare function extensionHooksSchema(contracts: readonly ExtensionHookContract[]): object;
|
|
119
|
+
/**
|
|
120
|
+
* Loads project hooks once per activation. Project hooks are trusted first-party
|
|
121
|
+
* code by default, matching function/middleware routes. Sandboxed arbitrary-value
|
|
122
|
+
* hooks are not part of contract v1 and are refused rather than run trusted.
|
|
123
|
+
*/
|
|
124
|
+
export declare function loadExtensionHooks<T extends string>(config: Readonly<Record<string, unknown>> | undefined, contracts: readonly ExtensionHookContract[], context: Pick<ExtensionActivation, 'root'>): Promise<LoadedExtensionHooks<T>>;
|
|
69
125
|
export interface RuntimeExtension {
|
|
70
126
|
name: string;
|
|
71
127
|
version: '1';
|
|
@@ -75,6 +131,8 @@ export interface RuntimeExtension {
|
|
|
75
131
|
policySchema?: object;
|
|
76
132
|
credentialHeaders?: string[];
|
|
77
133
|
immutableAssets?: ExtensionImmutableAssets;
|
|
134
|
+
/** Project customization points, exposed by CLI/MCP for authors and agents. */
|
|
135
|
+
hooks?: readonly ExtensionHookContract[];
|
|
78
136
|
/**
|
|
79
137
|
* Reviewed, operator-declared cache sensitivity for `policies.extensions.<name>`
|
|
80
138
|
* routes (never for an `extension:` mount, which is always treated as
|
|
@@ -32,7 +32,6 @@ export interface ResponseWriter {
|
|
|
32
32
|
end(body?: ResponseBody): unknown;
|
|
33
33
|
destroy(): unknown;
|
|
34
34
|
}
|
|
35
|
-
export declare const forbiddenHeaders: Set<string>;
|
|
36
35
|
export declare function prepareResponse(result: HandlerResult, { requestId, method }: ResponseOptions): PreparedResponse;
|
|
37
36
|
export declare function writeResponse(res: ResponseWriter, result: HandlerResult, options: ResponseOptions): number;
|
|
38
37
|
export declare function errorResponse(error: unknown, { requestId, method, headers }: ResponseOptions & {
|
package/dist/types/index.d.ts
CHANGED
|
@@ -38,4 +38,5 @@ export { scaffoldProject } from './scaffold.ts';
|
|
|
38
38
|
export type { ScaffoldReport, Unresolved as ScaffoldUnresolved } from './scaffold.ts';
|
|
39
39
|
export { initProject, addRedirect } from './authoring.ts';
|
|
40
40
|
export { initProjectWith } from './init-with.ts';
|
|
41
|
+
export { collectDependencySet, renderPackageManifest, installSteps } from './project-dependencies.ts';
|
|
41
42
|
export type { ScaffoldRequest, ScaffoldResult, ScaffoldFile } from './extensions.ts';
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
/** Directory names inside the generated site. The route project lives under `app/`; everything else is operator-owned. */
|
|
3
|
-
export declare const PROJECT_DIRECTORY = "app", HOST_FILE = "host.mjs", ROUTES_FILE = "routes/extensions.yaml";
|
|
1
|
+
import type { DependencyPin } from './project-dependencies.ts';
|
|
4
2
|
export interface InitWithOptions {
|
|
5
3
|
cwd?: string | undefined;
|
|
4
|
+
/** Default true: record exact pins for core, the named extensions and their declared peers. */
|
|
5
|
+
manifest?: boolean | undefined;
|
|
6
|
+
/** `--pin <package>=<specifier>` overrides, for local tarballs, checkouts and mirrors. */
|
|
7
|
+
pins?: ReadonlyMap<string, string> | undefined;
|
|
6
8
|
}
|
|
7
9
|
export interface InitWithResult {
|
|
8
10
|
directory: string;
|
|
@@ -11,20 +13,12 @@ export interface InitWithResult {
|
|
|
11
13
|
extensions: string[];
|
|
12
14
|
projectSha256: string;
|
|
13
15
|
nextSteps: string[];
|
|
16
|
+
dependencies: DependencyPin[];
|
|
14
17
|
}
|
|
15
18
|
export declare function parseWithNames(value: string): string[];
|
|
16
|
-
export declare const packageName: (name: string) => string;
|
|
17
|
-
/**
|
|
18
|
-
* Resolves the extension package from the invoking directory (Node's package resolution with the default
|
|
19
|
-
* conditions), imports it, and calls its `scaffold` export. Nothing is bundled; core never imports these packages
|
|
20
|
-
* at build time. Refuses a missing package or a package without `scaffold` before anything is written.
|
|
21
|
-
*/
|
|
22
|
-
export declare function loadScaffold(name: string, request: ScaffoldRequest, cwd: string): Promise<ScaffoldResult>;
|
|
23
|
-
export declare function renderHost(names: readonly string[], results: readonly ScaffoldResult[]): string;
|
|
24
|
-
export declare function renderReadme(directory: string, names: readonly string[], results: readonly ScaffoldResult[], starter: string, env: Record<string, string>, projectSha256: string): string;
|
|
25
19
|
/**
|
|
26
20
|
* `urlcode init <directory> --with a,b`: the starter under `app/`, every extension's fragments merged into one
|
|
27
21
|
* `urlcode.yaml`, one `host.mjs`, one `README.md` and the extensions' own files. All packages are resolved and
|
|
28
22
|
* their scaffolds computed before anything is written, so a refusal leaves no directory behind.
|
|
29
23
|
*/
|
|
30
|
-
export declare function initProjectWith(destination: string, names: readonly string[], { cwd }?: InitWithOptions): Promise<InitWithResult>;
|
|
24
|
+
export declare function initProjectWith(destination: string, names: readonly string[], { cwd, manifest, pins }?: InitWithOptions): Promise<InitWithResult>;
|
package/dist/types/manifest.d.ts
CHANGED
|
@@ -79,5 +79,4 @@ export interface Manifest {
|
|
|
79
79
|
export declare function buildManifest(project: string, options?: InspectOptions): Promise<Manifest>;
|
|
80
80
|
/** The manifest as `build` writes it: two-space JSON with a trailing newline. */
|
|
81
81
|
export declare function renderManifest(manifest: Manifest): string;
|
|
82
|
-
export declare const manifestFileName = "manifest.json";
|
|
83
82
|
export declare function manifestPath(out: string): string;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Exact dependency pins for a generated application.
|
|
3
|
+
*
|
|
4
|
+
* `urlcode init --with` resolves whatever `@jimhoyd/urlcode-<name>` packages are already installed beside the
|
|
5
|
+
* invoking directory. Without a manifest the generated site records nothing about which versions it was built
|
|
6
|
+
* against, so a later `npm install @jimhoyd/urlcode-auth` in that site can resolve a different set (#212). This
|
|
7
|
+
* module reads the versions that were actually resolved, validates the whole set against the packages' own
|
|
8
|
+
* declared `peerDependencies`, and renders a `package.json` pinning every one of them exactly.
|
|
9
|
+
*
|
|
10
|
+
* It never runs a package manager: generating a lockfile stays an explicit `npm install` the operator runs after
|
|
11
|
+
* reviewing the manifest. It also never imports an extension implementation -- only package metadata is read, by
|
|
12
|
+
* a name the caller supplied -- so core's generic extension boundary is unchanged.
|
|
13
|
+
*/
|
|
14
|
+
export declare const CORE_PACKAGE = "@jimhoyd/urlcode";
|
|
15
|
+
interface Version {
|
|
16
|
+
major: number;
|
|
17
|
+
minor: number;
|
|
18
|
+
patch: number;
|
|
19
|
+
pre: readonly (string | number)[];
|
|
20
|
+
}
|
|
21
|
+
export declare function parseVersion(value: string): Version | null;
|
|
22
|
+
export declare function compareVersions(a: Version, b: Version): number;
|
|
23
|
+
export declare function satisfiesRange(version: string, range: string, context?: string): boolean;
|
|
24
|
+
export interface InstalledPackage {
|
|
25
|
+
name: string;
|
|
26
|
+
version: string;
|
|
27
|
+
directory: string;
|
|
28
|
+
peers: Record<string, string>;
|
|
29
|
+
optionalPeers: ReadonlySet<string>;
|
|
30
|
+
node: string | undefined;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Walks `node_modules` upwards from the invoking directory, exactly like Node's own resolution but reading the
|
|
34
|
+
* package's manifest rather than its entry point. Reading the manifest directly (instead of resolving the entry)
|
|
35
|
+
* means a package whose `exports` does not expose `./package.json` is still inspectable, and nothing in the
|
|
36
|
+
* package is loaded or executed.
|
|
37
|
+
*/
|
|
38
|
+
export declare function findInstalledPackage(name: string, from: string): Promise<InstalledPackage | null>;
|
|
39
|
+
/** The version of the runtime executing this command; that is the version a generated site is pinned to. */
|
|
40
|
+
export declare function runningCore(): Promise<InstalledPackage>;
|
|
41
|
+
export interface DependencyPin {
|
|
42
|
+
name: string;
|
|
43
|
+
version: string;
|
|
44
|
+
specifier: string;
|
|
45
|
+
/** True when the specifier is a local path or tarball rather than a registry version. */
|
|
46
|
+
local: boolean;
|
|
47
|
+
/** `runtime` is core, `extension` was named in --with, `peer` was pulled in by a package's peerDependencies. */
|
|
48
|
+
role: 'runtime' | 'extension' | 'peer';
|
|
49
|
+
}
|
|
50
|
+
export interface DependencySet {
|
|
51
|
+
pins: DependencyPin[];
|
|
52
|
+
/** Exactly what goes into the generated `dependencies` block, sorted by name. */
|
|
53
|
+
dependencies: Record<string, string>;
|
|
54
|
+
/** Highest recognized `engines.node` floor across the set, or undefined when none was expressed as `>=x.y.z`. */
|
|
55
|
+
node: string | undefined;
|
|
56
|
+
/** True when any pin points at a local path or tarball. */
|
|
57
|
+
local: boolean;
|
|
58
|
+
}
|
|
59
|
+
export interface DependencyOptions {
|
|
60
|
+
cwd?: string | undefined;
|
|
61
|
+
/** `--pin <package>=<specifier>`: an operator-chosen specifier, for local tarballs and mirrors. */
|
|
62
|
+
overrides?: ReadonlyMap<string, string> | undefined;
|
|
63
|
+
}
|
|
64
|
+
export declare function parsePin(value: string): [string, string];
|
|
65
|
+
/**
|
|
66
|
+
* Resolves core plus every named extension and their declared peers, then validates the whole set against every
|
|
67
|
+
* declared peer range before returning. Compatibility is judged as a set: a mismatch anywhere refuses, listing
|
|
68
|
+
* every mismatch rather than the first.
|
|
69
|
+
*/
|
|
70
|
+
export declare function collectDependencySet(names: readonly string[], packageNames: readonly string[], { cwd, overrides }?: DependencyOptions): Promise<DependencySet>;
|
|
71
|
+
/** The generated manifest: private, module type, exact pins, and nothing that runs a package manager. */
|
|
72
|
+
export declare function renderPackageManifest(directory: string, set: DependencySet): string;
|
|
73
|
+
/**
|
|
74
|
+
* The install step is printed, never run: generating `package-lock.json` executes a package manager, which
|
|
75
|
+
* resolves and downloads code, so it stays the operator's explicit action after reviewing the manifest.
|
|
76
|
+
*/
|
|
77
|
+
export declare function installSteps(directory: string, set: DependencySet): string[];
|
|
78
|
+
export {};
|
|
@@ -57,6 +57,9 @@ export interface AuditOptions {
|
|
|
57
57
|
export interface AuditReport {
|
|
58
58
|
elapsedMs: number;
|
|
59
59
|
ready: boolean;
|
|
60
|
+
/** Empty when `ready`; otherwise one stable code per failed condition:
|
|
61
|
+
* `no-active-routes`, `route-count-mismatch`, `failed-checks`, `uncovered-route-methods`. */
|
|
62
|
+
notReadyReasons: string[];
|
|
60
63
|
counts: {
|
|
61
64
|
configured: number;
|
|
62
65
|
active: number;
|
package/dist/types/tooling.d.ts
CHANGED
|
@@ -2,11 +2,9 @@ import type { FunctionRoute } from './function-sources.ts';
|
|
|
2
2
|
import type { FunctionContext, FunctionResult } from './functions.ts';
|
|
3
3
|
import type { GuestRequestPayload } from './guest-api.ts';
|
|
4
4
|
import type { HandlerResult } from './http-response.ts';
|
|
5
|
-
import type { LogFn } from './types.ts';
|
|
6
5
|
export interface TrustedFunctionsOptions {
|
|
7
6
|
timeoutMs?: number | undefined;
|
|
8
7
|
maxBytes?: number | undefined;
|
|
9
|
-
log?: LogFn | undefined;
|
|
10
8
|
}
|
|
11
9
|
interface TrustedDefinition {
|
|
12
10
|
source: string;
|
|
@@ -16,9 +14,8 @@ export type TrustedRoute = FunctionRoute<TrustedDefinition>;
|
|
|
16
14
|
export declare class TrustedFunctions {
|
|
17
15
|
timeoutMs: number;
|
|
18
16
|
maxBytes: number;
|
|
19
|
-
log: LogFn;
|
|
20
17
|
private readonly epoch;
|
|
21
|
-
constructor({ timeoutMs, maxBytes
|
|
18
|
+
constructor({ timeoutMs, maxBytes }?: TrustedFunctionsOptions);
|
|
22
19
|
start(routes: TrustedRoute[]): Promise<this>;
|
|
23
20
|
private loadExport;
|
|
24
21
|
execute(route: TrustedRoute, request: GuestRequestPayload, context: FunctionContext, native: HandlerResult | undefined): Promise<FunctionResult>;
|
package/docs/AI-AUTHORING.md
CHANGED
|
@@ -25,7 +25,11 @@ fields or bypass target limits or operator grants. See [the design principle](PR
|
|
|
25
25
|
6. [Readiness](READINESS.md), [capacity](CAPACITY.md), [DDoS/recovery](RESILIENCE.md).
|
|
26
26
|
7. [The framework](FRAMEWORK.md) for accounts, administration and presentation:
|
|
27
27
|
`extensions.<name>` blocks and `extension` mounts are the only YAML those
|
|
28
|
-
packages need
|
|
28
|
+
packages need. [Composing a site](COMPOSING-A-SITE.md) is the map of what a
|
|
29
|
+
consumer may then change: the `config` each package accepts, the
|
|
30
|
+
presentation overrides under `ui/`, the project functions its lifecycle
|
|
31
|
+
hooks call, and when a requirement instead needs a new extension in
|
|
32
|
+
TypeScript.
|
|
29
33
|
|
|
30
34
|
The root [llms.txt](../llms.txt) is a compact discovery index; the generated
|
|
31
35
|
[llms-full.txt](../llms-full.txt) concatenates the authoring documents above in
|
|
@@ -269,9 +273,10 @@ either `true` or `false`) or `sandbox: true` is enough to silence it.
|
|
|
269
273
|
|
|
270
274
|
The same judgment call applies to a project-level lifecycle hook an
|
|
271
275
|
extension invokes (`onSignUp`, `beforeRegister` and the like) — it is
|
|
272
|
-
first-party project code with the same trusted-by-default rule
|
|
273
|
-
`
|
|
274
|
-
|
|
276
|
+
first-party project code with the same trusted-by-default rule as a
|
|
277
|
+
`function`/`middleware` route. Extension hook contract v1 is trusted-only;
|
|
278
|
+
`sandbox: true` is rejected rather than silently ignored. See
|
|
279
|
+
[EXTENSIONS.md](EXTENSIONS.md#project-level-lifecycle-hooks).
|
|
275
280
|
|
|
276
281
|
Guest TypeScript needs `build-typescript --project SOURCE --out NEW_DIRECTORY`
|
|
277
282
|
before serving. Only the emitted `.js`/`.mjs` executes, in QuickJS for a
|
|
@@ -290,7 +295,7 @@ origin grants pinned to the project revision. These are self-hosted features;
|
|
|
290
295
|
providers refuse them. Signals are bounded best effort with drops, no retries
|
|
291
296
|
or persistence. Never turn a user request into an implicit network grant.
|
|
292
297
|
|
|
293
|
-
Before using a feature, ask `urlcode capabilities <name>` for its constraints, grants and target support and `urlcode schema <path>` for only that YAML fragment (MCP: `get_capability`, `get_schema`), instead of guessing.
|
|
298
|
+
Before using a feature, ask `urlcode capabilities <name>` for its constraints, grants and target support and `urlcode schema <path>` for only that YAML fragment (MCP: `get_capability`, `get_schema`), instead of guessing. For an installed extension, use `urlcode extensions --host-file ... --json` or MCP `get_extensions`; its hook contracts include the accepted names, purpose and input/output schemas. Prefer extension configuration and UI copy/templates/theme/CSS, then a declared project hook, and only then a new extension or fork.
|
|
294
299
|
The [tooling SDK and stdio MCP](TOOLING.md) inspect, validate, explain and preview
|
|
295
300
|
without guest execution, environment reads or writes. Run `urlcode explain /route`
|
|
296
301
|
to check effective methods, policies and cache outcome, and `urlcode manifest`
|
package/docs/AWS.md
CHANGED
|
@@ -11,6 +11,15 @@ self-hosted Node lifecycle, and a `sandbox: true` route would pay worker and
|
|
|
11
11
|
WASM startup on every cold start. Both are refused at activation with the route
|
|
12
12
|
named, never per request, trusted or sandboxed alike.
|
|
13
13
|
|
|
14
|
+
That refusal is a settled position, not a gap awaiting an adapter: per-route
|
|
15
|
+
Lambda compilation was considered and declined
|
|
16
|
+
([the decision](OPEN-DECISIONS.md#accepted-one-node-deployment-per-project),
|
|
17
|
+
[the analysis behind it](archive/2026-09-19/SPIKE-LAMBDA-COMPILE.md)). A project that uses
|
|
18
|
+
`function` or `middleware` deploys instead as one trusted Node process — a
|
|
19
|
+
container or a VM running the project as it runs locally — which supports every
|
|
20
|
+
route type today. That process can run on AWS: ECS, EC2 and App Runner all
|
|
21
|
+
serve it. The decision is about the execution model, not about avoiding AWS.
|
|
22
|
+
|
|
14
23
|
A working project is in [`examples/aws/`](../examples/aws/).
|
|
15
24
|
|
|
16
25
|
## Set it up
|
|
@@ -68,7 +68,7 @@ extra shards would add setup and runner pressure.
|
|
|
68
68
|
quality check or claim measured savings from a workflow with no prior runs.
|
|
69
69
|
- The manual signed candidate builds all four tarballs and installs them together
|
|
70
70
|
in a temporary consumer outside the workspace. It verifies the peer dependency
|
|
71
|
-
tree, installed versions, public imports, and `init --with auth,admin
|
|
71
|
+
tree, installed versions, public imports, and `init --with ui,auth,admin`.
|
|
72
72
|
`train.json` records package SHA-512 integrity and the source commit; the
|
|
73
73
|
candidate manifest/checksums and provenance include the extension archives.
|
|
74
74
|
Failure stops the candidate before attestation/upload. Nothing is published.
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
<!-- trust-model-prose: historical-file -->
|
|
2
|
+
<!-- This report quotes the defective wording it is reporting -- including the
|
|
3
|
+
pre-trusted-default claims in finding 4 -- so the prose gate would read the
|
|
4
|
+
quotations as the guidance itself. The file is a dated review of one
|
|
5
|
+
commit and is not edited as the defects are fixed. -->
|
|
6
|
+
|
|
1
7
|
# Codebase, tooling and documentation audit — 2026-09-20
|
|
2
8
|
|
|
3
9
|
Reviewed commit: `bca8ac7` (core 0.4.0-alpha.2, auth 0.1.0-alpha.5,
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
# Composing a site from ui, auth and admin
|
|
2
|
+
|
|
3
|
+
One command produces a site that already has accounts, an administration
|
|
4
|
+
console and a presentation kit wired together:
|
|
5
|
+
|
|
6
|
+
```sh
|
|
7
|
+
npm install @jimhoyd/urlcode @jimhoyd/urlcode-ui @jimhoyd/urlcode-auth @jimhoyd/urlcode-admin
|
|
8
|
+
urlcode init site --with ui,auth,admin
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
This page is the map of what you may then change, and with which tool. It
|
|
12
|
+
covers three different activities that are easy to confuse:
|
|
13
|
+
|
|
14
|
+
| You want to | Use | Code? |
|
|
15
|
+
|---|---|---|
|
|
16
|
+
| Change what an extension is configured to do | the `config` block in `app/urlcode.yaml` | none |
|
|
17
|
+
| Change how its screens look or read | files under `<site>/ui/` | none (templates are data) |
|
|
18
|
+
| Run your own logic at one of its lifecycle points | a project function named from that `config` | first-party project JavaScript |
|
|
19
|
+
| Add a capability none of the three provides | a new extension package | TypeScript against the runtime contract |
|
|
20
|
+
|
|
21
|
+
Work down that list, not up. The [declarative-first
|
|
22
|
+
principle](PROJECT-DIRECTION.md#design-principle-declarative-first) applies
|
|
23
|
+
here as much as it does to routes: reach for the next row only when the one
|
|
24
|
+
above it cannot express the requirement.
|
|
25
|
+
|
|
26
|
+
## What `--with ui,auth,admin` generates
|
|
27
|
+
|
|
28
|
+
- `site/app/` — the route project: `urlcode.yaml` with an `extensions` block
|
|
29
|
+
per package, and `routes/extensions.yaml` holding `/assets/ui/*`,
|
|
30
|
+
`/account/*`, `/private` and `/admin/*`.
|
|
31
|
+
- `site/host.mjs` — the operator host module, the one place that holds code.
|
|
32
|
+
It builds the kit with `createUiExtension`, passes the returned object into
|
|
33
|
+
`authExtension` and `adminExtension`, and lists `ui.registration` first.
|
|
34
|
+
- `site/ui/` — `copy/`, `templates/` and `extra.css`, the project's
|
|
35
|
+
presentation overrides, beside the host and **outside** `app/`.
|
|
36
|
+
- `site/operator-service.mjs`, `site/data/` — auth's operator service and its
|
|
37
|
+
private key material, mode `0600`.
|
|
38
|
+
- `site/README.md` — the merged next steps, environment table and the project
|
|
39
|
+
revision to review and pin.
|
|
40
|
+
|
|
41
|
+
Nothing about that wiring is manual any more. The generated host registers
|
|
42
|
+
`authCatalogue` as a copy source and both `authUiTemplates` and
|
|
43
|
+
`adminUiTemplates` as template namespaces, because auth and admin render only
|
|
44
|
+
through the kit and refuse to activate without their own templates present.
|
|
45
|
+
|
|
46
|
+
### Supported combinations
|
|
47
|
+
|
|
48
|
+
`--with` order is the activation order, and the kit must be active before
|
|
49
|
+
anything that renders through it.
|
|
50
|
+
|
|
51
|
+
| `--with` | Result |
|
|
52
|
+
|---|---|
|
|
53
|
+
| `ui` | Kit only; the host wires no peer catalogue or templates. |
|
|
54
|
+
| `ui,auth` | Accounts on `/account/*`, rendered through the kit. |
|
|
55
|
+
| `ui,auth,admin` | The full composition above. |
|
|
56
|
+
| `auth` or `auth,admin` | Refused: the scaffold names the missing `ui`. |
|
|
57
|
+
| `auth,admin,ui` | Refused: `ui` must come before the extensions it renders. |
|
|
58
|
+
| `admin` without `auth` | Refused: admin reuses auth's service, CSRF key and revision. |
|
|
59
|
+
|
|
60
|
+
Every refusal happens before anything is written, and leaves no directory
|
|
61
|
+
behind. There is no auth-without-ui or admin-without-ui configuration in this
|
|
62
|
+
revision: the UI primitive fallback was retired, so the kit is the only render
|
|
63
|
+
path (see [OPEN-DECISIONS.md](OPEN-DECISIONS.md)).
|
|
64
|
+
|
|
65
|
+
## Declarative configuration
|
|
66
|
+
|
|
67
|
+
Each package owns one `extensions.<name>` block. The block itself is core
|
|
68
|
+
schema (`version` plus `config`); what may go inside `config` is the
|
|
69
|
+
package's own JSON Schema, which you can print rather than guess:
|
|
70
|
+
|
|
71
|
+
```sh
|
|
72
|
+
urlcode extensions --project ./site/app --host-file "$PWD/site/host.mjs" --json
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The same report is the MCP tool `get_extensions`, and it is the authoritative
|
|
76
|
+
answer for both the configuration schema and the per-route policy schema. The
|
|
77
|
+
generated site starts from something like this:
|
|
78
|
+
|
|
79
|
+
```yaml
|
|
80
|
+
version: "1"
|
|
81
|
+
extensions:
|
|
82
|
+
ui:
|
|
83
|
+
version: "1"
|
|
84
|
+
config:
|
|
85
|
+
theme:
|
|
86
|
+
name: My Site
|
|
87
|
+
languages: [en]
|
|
88
|
+
copy: ui/copy
|
|
89
|
+
templates: ui/templates
|
|
90
|
+
stylesheet: ui/extra.css
|
|
91
|
+
auth:
|
|
92
|
+
version: "1"
|
|
93
|
+
config:
|
|
94
|
+
registration: "off"
|
|
95
|
+
admin:
|
|
96
|
+
version: "1"
|
|
97
|
+
config: {}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Routes mount an extension, and policies require one:
|
|
101
|
+
|
|
102
|
+
```yaml
|
|
103
|
+
routes:
|
|
104
|
+
/assets/ui/*:
|
|
105
|
+
extension: ui
|
|
106
|
+
methods: [GET, HEAD]
|
|
107
|
+
/account/*:
|
|
108
|
+
extension: auth
|
|
109
|
+
methods: [GET, HEAD, POST]
|
|
110
|
+
/admin/*:
|
|
111
|
+
extension: admin
|
|
112
|
+
methods: [GET, HEAD, POST]
|
|
113
|
+
/private:
|
|
114
|
+
respond:
|
|
115
|
+
text: Signed in
|
|
116
|
+
policies:
|
|
117
|
+
extensions:
|
|
118
|
+
auth: {}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
See [EXTENSIONS.md](EXTENSIONS.md) for the `auth` route short form, extension
|
|
122
|
+
middleware, and the host-file trust boundary.
|
|
123
|
+
|
|
124
|
+
## Presentation overrides
|
|
125
|
+
|
|
126
|
+
The `ui` config's `copy`, `templates` and `stylesheet` paths point at the
|
|
127
|
+
project's own directories. Nothing here forks a package.
|
|
128
|
+
|
|
129
|
+
| Override | File | Effect |
|
|
130
|
+
|---|---|---|
|
|
131
|
+
| Wording and translation | `ui/copy/<locale>.json` | Replaces catalogue ids, including ids the auth and admin packages own. Listed in `languages`. |
|
|
132
|
+
| A whole screen | `ui/templates/<name>.html` | Shadows a kit or extension template of that name, for example `ui/templates/auth/sign-in.html` or `ui/templates/admin/dashboard.html`. |
|
|
133
|
+
| Styling | `ui/extra.css` | Appended after the kit stylesheet; `{file, replace: true}` replaces it instead. |
|
|
134
|
+
| Colours, logo, favicon, radius, font | the `theme` block | Declarative; no file needed. |
|
|
135
|
+
|
|
136
|
+
A template is data in the kit's own language. It cannot add a script, change
|
|
137
|
+
what a form validates, or change what a page sends in headers — so an override
|
|
138
|
+
cannot weaken the screen it restyles. Stylesheets containing `@import`,
|
|
139
|
+
`script`, `javascript:` or `expression(` are refused.
|
|
140
|
+
|
|
141
|
+
Names, coverage and what the runtime will actually load:
|
|
142
|
+
|
|
143
|
+
```sh
|
|
144
|
+
npx urlcode-ui list --project ./site --extensions @jimhoyd/urlcode-auth,@jimhoyd/urlcode-admin
|
|
145
|
+
npx urlcode-ui doctor --project ./site --extensions @jimhoyd/urlcode-auth,@jimhoyd/urlcode-admin --copy ui/copy --templates ui/templates --stylesheet ui/extra.css
|
|
146
|
+
npx urlcode-ui eject auth/sign-in --out ./site/ui/templates --project ./site --extensions @jimhoyd/urlcode-auth
|
|
147
|
+
npx urlcode-ui copy --missing fr --project ./site --extensions @jimhoyd/urlcode-auth --copy ui/copy --languages en,fr
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
`eject` copies the shipped source so an override starts from what ships and
|
|
151
|
+
never overwrites an existing file. `ui/` lives outside `app/`, so editing copy
|
|
152
|
+
or templates does **not** change the project revision and does not require
|
|
153
|
+
re-pinning `PROJECT_SHA256`.
|
|
154
|
+
|
|
155
|
+
**Name the packages that ship the other namespaces.** `urlcode-ui` is this
|
|
156
|
+
kit alone until `--extensions` names them. Each package is resolved from
|
|
157
|
+
`--project` with Node package resolution and imported for the namespace it
|
|
158
|
+
exports; one that is not installed there is skipped with a note, so the
|
|
159
|
+
command still runs. The site's `host.mjs` is never read: it builds services
|
|
160
|
+
and reads secrets at its top level, and a read-only `list` or `doctor` must
|
|
161
|
+
not run it. With the packages named:
|
|
162
|
+
|
|
163
|
+
- `list` shows the `auth/*` and `admin/*` names beside the kit's own, each
|
|
164
|
+
with its origin, and `eject auth/sign-in` copies the shipped source.
|
|
165
|
+
- `doctor` reports an `expected` view model for an extension template, so its
|
|
166
|
+
`behind` flag tells you when an override of one has fallen behind what
|
|
167
|
+
ships. Its `extensions` field names the namespaces the report covers, so a
|
|
168
|
+
report built without a peer is visible as such.
|
|
169
|
+
- `preview auth/sign-in` renders the extension's own sample view model.
|
|
170
|
+
- `copy --missing` skeletons cover the auth ids the account screens use.
|
|
171
|
+
Admin-owned `adminUi.*` ids are deliberately not offered: admin composes
|
|
172
|
+
its catalogue onto the kit's presentation rather than registering it there,
|
|
173
|
+
and those translations do not currently reach the console
|
|
174
|
+
([#227](https://github.com/jimhoyd-com/urlcode/issues/227)).
|
|
175
|
+
|
|
176
|
+
`urlcode init <directory> --with ui,auth,admin` writes these commands into the
|
|
177
|
+
generated README with the flag already set. `@jimhoyd/urlcode-ui` depends on
|
|
178
|
+
neither peer; the operator names them.
|
|
179
|
+
|
|
180
|
+
Overrides of extension templates and of extension-owned catalogue ids reach
|
|
181
|
+
the rendered screens, which is what the regression test below asserts.
|
|
182
|
+
|
|
183
|
+
## Project functions: lifecycle hooks
|
|
184
|
+
|
|
185
|
+
A hook is your own function, named from the extension's `config`, that the
|
|
186
|
+
extension calls at a lifecycle point it defines. It uses the same source shape
|
|
187
|
+
a `function` route uses — a bare path, or `{source, export}` — resolved
|
|
188
|
+
relative to the route project.
|
|
189
|
+
|
|
190
|
+
```yaml
|
|
191
|
+
extensions:
|
|
192
|
+
auth:
|
|
193
|
+
version: "1"
|
|
194
|
+
config:
|
|
195
|
+
registration: open
|
|
196
|
+
hooks:
|
|
197
|
+
beforeRegister:
|
|
198
|
+
source: ./hooks/registration-rule.mjs
|
|
199
|
+
export: default
|
|
200
|
+
onSignUp: ./hooks/on-signup.mjs
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
`hooks` remains each package's own config, but core supplies the reference
|
|
204
|
+
schema, trusted loader and machine-readable hook contract. `get_extensions`
|
|
205
|
+
reports those contracts, so an agent can discover accepted names, purpose and
|
|
206
|
+
input/output shapes without guessing from prose.
|
|
207
|
+
|
|
208
|
+
Hooks are first-party project code and run **trusted and in-process**, the
|
|
209
|
+
same default `function` and `middleware` routes have
|
|
210
|
+
([FUNCTION-SECURITY.md](FUNCTION-SECURITY.md)). Extension hook contract v1 is
|
|
211
|
+
trusted-only; `sandbox: true` is rejected loudly at activation.
|
|
212
|
+
|
|
213
|
+
### `@jimhoyd/urlcode-ui`
|
|
214
|
+
|
|
215
|
+
| Hook | Input | Returns | Called |
|
|
216
|
+
|---|---|---|---|
|
|
217
|
+
| `transformView` | `{template, view}` | the view object to render | Synchronously before each public `ui.kit.render()` or `ui.kit.page()` call. Use it only when theme, copy, templates and CSS cannot express the change. |
|
|
218
|
+
|
|
219
|
+
For example, `transformView: ./hooks/ui-view.mjs` can add project-computed
|
|
220
|
+
navigation or labels to an auth/admin view without editing either package.
|
|
221
|
+
|
|
222
|
+
### `@jimhoyd/urlcode-auth`
|
|
223
|
+
|
|
224
|
+
| Hook | Input | Returns | Called |
|
|
225
|
+
|---|---|---|---|
|
|
226
|
+
| `beforeRegister` | `{email, profile?}` | `{allow: boolean, reason?}` | Before an account is created, on `POST /account/register` and on `POST /account/signup/begin`. |
|
|
227
|
+
| `onSignUp` | `{accountId, email}` | ignored | After a genuinely new account is created — on `/account/register`, and on `/account/signup/complete` only when that completion created an account rather than signing an existing one in. |
|
|
228
|
+
| `onDelete` | `{accountId, email}` | ignored | After the account owner's own deletion is scheduled. Not on an administrator-initiated deletion, and not on the background purge when the grace period ends. |
|
|
229
|
+
|
|
230
|
+
### `@jimhoyd/urlcode-admin`
|
|
231
|
+
|
|
232
|
+
| Hook | Input | Returns | Called |
|
|
233
|
+
|---|---|---|---|
|
|
234
|
+
| `beforeRoleChange` | `{accountId, currentRoles, requestedRoles, actorId, reason}` | `{allow: boolean, reason?}` | Before roles are applied, after the administrator's permission check. A veto means the auth service is never asked. |
|
|
235
|
+
| `onRegistrationApproved` | `{requestId, accountId, email, actorId, reason}` | ignored | After a registration request is approved. |
|
|
236
|
+
| `onAccountStatusChanged` | `{accountId, status, actorId, reason}` | ignored | After an account is locked or unlocked. |
|
|
237
|
+
|
|
238
|
+
### Verdicts and failure
|
|
239
|
+
|
|
240
|
+
- **A veto is explicit.** A pre-action hook allows only by returning
|
|
241
|
+
`allow: true`. `allow: false`, or no verdict at all, rejects the operation
|
|
242
|
+
with `403` and the hook's own `reason`, or a generic message when it gave
|
|
243
|
+
none. Nothing is written. A hook that *throws* has not returned a verdict:
|
|
244
|
+
the operation is still refused, but as a generic `500`, so return a verdict
|
|
245
|
+
rather than throwing when you mean to deny.
|
|
246
|
+
- **Broken hooks fail at activation, not at the first request.** A missing
|
|
247
|
+
module, a source path escaping the project, an export that is not a
|
|
248
|
+
function, or `sandbox: true` all throw while the extension activates, naming
|
|
249
|
+
the hook. The site does not start.
|
|
250
|
+
- **A post-action hook cannot undo anything.** `onSignUp`,
|
|
251
|
+
`onDelete`, `onRegistrationApproved` and `onAccountStatusChanged` run after
|
|
252
|
+
the operation has committed. Throwing from one replaces the success response
|
|
253
|
+
with a `500` while the account, approval or status change stands. There is
|
|
254
|
+
no retry and no rollback. Keep them non-throwing: catch your own errors and
|
|
255
|
+
queue the work instead of failing the request.
|
|
256
|
+
- **A hook's message is not a channel to the browser.** Only a pre-action
|
|
257
|
+
`reason` is shown. An uncaught error surfaces as a generic failure.
|
|
258
|
+
- **An edited hook needs a restart.** Activation re-imports the hook's entry
|
|
259
|
+
module, so a reload picks up an edit to that file — but modules it imports
|
|
260
|
+
stay on Node's module cache, exactly as for trusted route functions.
|
|
261
|
+
|
|
262
|
+
## TypeScript: implementing a new extension
|
|
263
|
+
|
|
264
|
+
Only write an extension when a capability is genuinely absent — not to
|
|
265
|
+
customize one of the three above. An extension is an operator-installed
|
|
266
|
+
package whose host object core activates; it is named in `host.mjs`, never
|
|
267
|
+
in YAML. The contract, the activation inputs, `ExtensionActivation.root`,
|
|
268
|
+
credential headers and the `projectSha256` pin are in
|
|
269
|
+
[EXTENSIONS.md](EXTENSIONS.md) and [TYPESCRIPT.md](TYPESCRIPT.md); the
|
|
270
|
+
`scaffold` export that makes a package work with `init --with` is in
|
|
271
|
+
[EXTENSIONS.md](EXTENSIONS.md#scaffolding-with-init---with).
|
|
272
|
+
|
|
273
|
+
If its screens should be themeable the same way auth's and admin's are, it
|
|
274
|
+
also exports a template namespace (and, if it ships English wording, a
|
|
275
|
+
catalogue) for a host to pass to `createUiExtension`. That is what makes
|
|
276
|
+
`ui/templates/<yourname>/<screen>.html` work in a consumer project without a
|
|
277
|
+
fork.
|
|
278
|
+
|
|
279
|
+
## What this page does not claim
|
|
280
|
+
|
|
281
|
+
The composition, the refusals and the override path are exercised by
|
|
282
|
+
`test/workspace-scaffold.integration.ts`, which runs `init --with` against the
|
|
283
|
+
built packages, drops a template and a copy catalogue into the generated
|
|
284
|
+
`ui/` directory and asserts both reach a rendered auth screen and a rendered
|
|
285
|
+
admin screen. That runs in-process against the generated host: no HTTP
|
|
286
|
+
listener, TLS proxy, browser or deployed site is exercised, and no published
|
|
287
|
+
npm tarball is checked against this checkout.
|