@jimhoyd/urlcode 0.4.8 → 0.5.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +18 -18
  2. package/dist/BUILD-MANIFEST.json +27 -23
  3. package/dist/agents-guide.js +8 -4
  4. package/dist/authoring.js +81 -7
  5. package/dist/build-cloudflare.js +1 -0
  6. package/dist/build-static.js +1 -0
  7. package/dist/cli.js +39 -14
  8. package/dist/config.js +7 -1
  9. package/dist/context.js +32 -4
  10. package/dist/ecosystem-cli.js +6 -0
  11. package/dist/explain-cli.js +1 -1
  12. package/dist/explain.js +2 -2
  13. package/dist/extension-artifacts.js +28 -34
  14. package/dist/extension-bundles.js +62 -0
  15. package/dist/extension-transport.js +41 -0
  16. package/dist/extensions.js +4 -0
  17. package/dist/feature-plan.js +99 -0
  18. package/dist/functions.js +2 -1
  19. package/dist/index.js +4 -2
  20. package/dist/init-with.js +55 -23
  21. package/dist/interchange.js +1 -1
  22. package/dist/match.js +23 -5
  23. package/dist/mcp.js +6 -2
  24. package/dist/readiness.js +2 -2
  25. package/dist/review.js +206 -0
  26. package/dist/router.js +35 -10
  27. package/dist/runtime.js +1 -0
  28. package/dist/tooling.js +4 -0
  29. package/dist/types/agents-guide.d.ts +6 -1
  30. package/dist/types/authoring.d.ts +3 -1
  31. package/dist/types/context.d.ts +12 -0
  32. package/dist/types/explain.d.ts +3 -0
  33. package/dist/types/extension-artifacts.d.ts +15 -4
  34. package/dist/types/extension-bundles.d.ts +49 -0
  35. package/dist/types/extension-transport.d.ts +31 -0
  36. package/dist/types/extensions.d.ts +4 -0
  37. package/dist/types/feature-plan.d.ts +67 -0
  38. package/dist/types/functions.d.ts +4 -0
  39. package/dist/types/index.d.ts +4 -2
  40. package/dist/types/init-with.d.ts +6 -1
  41. package/dist/types/match.d.ts +1 -0
  42. package/dist/types/review.d.ts +30 -0
  43. package/dist/types/tooling.d.ts +4 -0
  44. package/dist/types/types.d.ts +10 -1
  45. package/dist/types.js +10 -3
  46. package/docs/AI-AUTHORING.md +466 -0
  47. package/docs/FUNCTION-SECURITY.md +251 -0
  48. package/docs/README.md +96 -0
  49. package/docs/TOOLING.md +422 -0
  50. package/docs/YAML-REFERENCE.md +473 -0
  51. package/llms-full.txt +190 -83
  52. package/llms.txt +73 -128
  53. package/package.json +15 -4
  54. package/recipes/redirect/README.md +2 -2
  55. package/recipes/store-crud/README.md +9 -10
  56. package/recipes/store-crud/recipe.yaml +1 -1
  57. package/schemas/urlcode.schema.json +3 -0
  58. package/starters/default/AGENTS.md +2 -2
package/dist/router.js CHANGED
@@ -73,7 +73,10 @@ export async function compileRoutes(loaded , bindings
73
73
  const parts = segments(pattern);
74
74
  assert(!pattern.startsWith('/_urlcode'), 'The /_urlcode prefix is reserved for runtime operations');
75
75
  const names = parts.map(parameterName).filter((name) => Boolean(name));
76
- assert(!pattern.includes('*') || ((config.static || config.extension) && pattern.endsWith('/*') && parts.filter(p => p.includes('*')).length === 1 && parts.at(-1) === '*' && !names.length), 'Only static or extension routes support a terminal /* wildcard');
76
+ // `/prefix/**` is the redirect-only suffix wildcard: a literal prefix, one terminal `**`, no path parameters.
77
+ const wildcardRedirect = Boolean(config.redirect) && pattern.endsWith('/**');
78
+ if (wildcardRedirect) assert(pattern !== '/**' && pattern.indexOf('*') === pattern.length - 2 && parts.at(-1) === '**' && !names.length && !config.conditional, 'A /** wildcard redirect needs a literal prefix, one terminal **, no path parameters and no conditional');
79
+ assert(wildcardRedirect || !pattern.includes('*') || ((config.static || config.extension) && pattern.endsWith('/*') && parts.filter(p => p.includes('*')).length === 1 && parts.at(-1) === '*' && !names.length), 'Only static or extension routes support a terminal /* wildcard; a redirect uses a terminal /** instead');
77
80
  assert(!config.static || pattern.endsWith('/*'), 'Static routes require a terminal /* wildcard');
78
81
  if (config.page || config.download || config.static) assert((config.methods || methodsDefault).every(m => methodsDefault.includes(m)), 'Asset routes support only GET and HEAD');
79
82
  assert(new Set(names).size === names.length, 'Duplicate path parameter');
@@ -137,8 +140,21 @@ export async function compileRoutes(loaded , bindings
137
140
  }
138
141
  assert(names.every(name => route.parameters.some(p => p.in === 'path' && p.name === name)), 'Every path placeholder requires an input declaration');
139
142
  for (const [alias, ref] of Object.entries(config.env || {})) {
140
- if (ref.env) assert(permissions.projectSha256 === projectSha256 && permissions.routes?.[pattern]?.env?.includes(ref.env), 'Environment binding denied by operator policy');
141
- const value = own(ref, 'value') ? ref.value : bindings[ref.env ];
143
+ // `env` always requires an operator grant for that route/name. A missing grant is not
144
+ // fatal when a `default` is declared: the binding just degrades to its literal default
145
+ // and never reads the host (issue #258). With no `default`, a missing grant still fails
146
+ // route compilation, as before. `value`-only bindings are plain literals and never
147
+ // consult the grant or the host environment.
148
+ let value ;
149
+ if (ref.env) {
150
+ const granted = permissions.projectSha256 === projectSha256 && permissions.routes?.[pattern]?.env?.includes(ref.env);
151
+ assert(granted || ref.default !== undefined, 'Environment binding denied by operator policy');
152
+ const hostValue = bindings[ref.env];
153
+ const hostSet = ref.default !== undefined ? typeof hostValue === 'string' && hostValue.length > 0 : hostValue !== undefined;
154
+ value = granted ? (hostSet ? hostValue : ref.default) : ref.default;
155
+ } else {
156
+ value = ref.value;
157
+ }
142
158
  assert(typeof value === 'string', 'Missing required environment binding');
143
159
  route.env[alias] = value;
144
160
  }
@@ -167,14 +183,21 @@ export async function compileRoutes(loaded , bindings
167
183
  if (declaredRedirect) {
168
184
  const value = declaredRedirect.url;
169
185
  assert(!/[\u0000-\u0020\u007f\\]/u.test(value), 'Redirect URL contains unsafe characters');
186
+ // A root-relative destination (`/profiles/{id}`) stays on this site: a single leading slash, so never `//host`, and no dot segments.
187
+ const relative = value.startsWith('/') && !value.startsWith('//');
170
188
  let dest ;
171
- try { dest = new URL(value); } catch { assert(false, 'Redirect URL must be absolute HTTP(S)'); }
172
- assert(['http:', 'https:'].includes(dest.protocol) && !dest.username && !dest.password, 'Redirect must use HTTP(S) without credentials');
173
- const authority = value.match(/^https?:\/\/([^/?#]+)/i)?.[1];
174
- assert(authority && !/[{}]/.test(authority) && !/[{}]/.test(dest.search + dest.hash), 'Redirect placeholders are allowed only in path segments');
189
+ try { dest = new URL(value, relative ? 'https://relative.invalid' : undefined); } catch { assert(false, 'Redirect URL must be an absolute HTTP(S) URL or a root-relative path'); }
190
+ if (relative) assert(dest.origin === 'https://relative.invalid' && !value.split(/[?#]/, 1)[0] .split('/').some(part => part === '.' || part === '..'), 'Root-relative redirect must be a plain path without dot segments');
191
+ else {
192
+ assert(['http:', 'https:'].includes(dest.protocol) && !dest.username && !dest.password, 'Redirect must use HTTP(S) without credentials');
193
+ const authority = value.match(/^https?:\/\/([^/?#]+)/i)?.[1];
194
+ assert(authority && !/[{}]/.test(authority), 'Redirect placeholders are allowed only in path segments');
195
+ }
196
+ assert(!/[{}]/.test(dest.search + dest.hash), 'Redirect placeholders are allowed only in path segments');
175
197
  const placeholders = [...value.matchAll(/\{([^}]+)\}/g)].map(m => m[1] );
176
- assert(placeholders.every(n => token.test(n) && names.includes(n)), 'Redirect placeholder must reference a declared path input');
177
- assert(!/[{}]/.test(value.replace(/\{[A-Za-z_][A-Za-z0-9_]*\}/g, '')), 'Invalid redirect placeholder');
198
+ assert(placeholders.every(n => (n === '**' && wildcardRedirect) || (token.test(n) && names.includes(n))), 'Redirect placeholder must reference a declared path input');
199
+ assert(placeholders.filter(n => n === '**').length <= 1, '{**} may appear once in a redirect destination');
200
+ assert(!/[{}]/.test(value.replace(/\{(?:[A-Za-z_][A-Za-z0-9_]*|\*\*)\}/g, '')), 'Invalid redirect placeholder');
178
201
  const query = declaredRedirect.query || {};
179
202
  const reserved = new Set(dest.searchParams.keys());
180
203
  for (const [key, ref] of Object.entries(query.map || {})) {
@@ -195,7 +218,8 @@ export async function compileRoutes(loaded , bindings
195
218
  route.function = { ...declaredFunction, source, export: declaredFunction.export || 'default' };
196
219
  for (const ref of Object.values(declaredFunction.args || {})) referenceCheck(ref, route, true);
197
220
  }
198
- if (config.static || config.extension) { route.prefix = pattern.slice(0, -1); mounts.push(route); }
221
+ if (wildcardRedirect) { route.prefix = pattern.slice(0, -2); route.wildcard = true; mounts.push(route); }
222
+ else if (config.static || config.extension) { route.prefix = pattern.slice(0, -1); mounts.push(route); }
199
223
  else if (!names.length) exact.set(pattern, route);
200
224
  else {
201
225
  assert(dynamic.length < 1000, 'Maximum 1000 parameterized routes per snapshot');
@@ -215,6 +239,7 @@ export async function compileRoutes(loaded , bindings
215
239
  byLength.get(route.parts.length) .push(route);
216
240
  }
217
241
  for(const mount of mounts.filter(route=>route.extension)){const base=mount.parts.slice(0,-1);for(const candidate of [...exact.values(),...dynamic,...mounts]){if(candidate===mount)continue;const parts=candidate.parts;const shared=Math.min(base.length,parts.length-(candidate.prefix?1:0));const compatible=base.slice(0,shared).every((part,index)=>part===parts[index]||parameterName(parts[index] ));assert(!compatible||(!candidate.prefix&&parts.length<base.length),'Extension mount overlaps another route');}}
242
+ assert(new Set(mounts.map(mount => mount.prefix)).size === mounts.length, 'A /** wildcard redirect cannot share its prefix with a static or extension mount');
218
243
  mounts.sort((a,b) => b.prefix .length - a.prefix .length);
219
244
  assert(performance.now()<deadline, 'Route compilation deadline exceeded');
220
245
  return { exact, byLength, mounts, modules: [...modules.keys()], count: exact.size + dynamic.length + mounts.length };
package/dist/runtime.js CHANGED
@@ -299,6 +299,7 @@ export async function createRuntime(project , rawOptions
299
299
  }
300
300
  if (native && !route.middleware.length) return await finishResponse(native);
301
301
  context.args = Object.fromEntries(Object.entries(route.function?.args || {}).map(([key, ref]) => [key, resolveValue(ref, context)]));
302
+ context.route = { pattern: route.pattern };
302
303
  // Uniform for `function` and `middleware` alike: a route dispatches
303
304
  // through the sandboxed worker pool only when it declares
304
305
  // `sandbox: true`; every other route runs trusted, in-process
package/dist/tooling.js CHANGED
@@ -23,7 +23,11 @@ export {getSchemaFragment,schemaPathNames} from './schema-query.js';
23
23
  export {listRecipes,showRecipe,searchRecipes,listExamples,searchExamples};
24
24
  export {buildContext,renderContext,estimateTokens,documentationTokens,buildTaskContext,renderTaskContext,contextTasks} from './context.js';
25
25
 
26
+ export {planFeature,featurePlanMaxBytes,featurePlanMaxGoalLength} from './feature-plan.js';
27
+
26
28
 
29
+ export {reviewProject} from './review.js';
30
+
27
31
  /** `extensions` are operator registrations from a host file; explain reports whether each requirement has a provider. Nothing is activated. */
28
32
 
29
33
  function routesOf(table ) {return [...table.exact.values(),...[...table.byLength.values()].flat(),...table.mounts];}
@@ -5,8 +5,13 @@ export declare const mcpConfigFile = ".mcp.json";
5
5
  /**
6
6
  * Renders `.mcp.json` registering the read-only `urlcode mcp` server for the project at `project`, relative
7
7
  * to the file. `--allow-authoring` is deliberately absent: the operator adds it by hand when they want it.
8
+ * `local` is for a project whose package.json pins the runtime: the server is then launched through `npx --no`,
9
+ * which uses the installed copy and refuses to fetch anything (a bare `urlcode` is not on PATH for a local-only install,
10
+ * and `npx urlcode` would resolve an unrelated registry package). Without a pin the bare command is kept for global installs.
8
11
  */
9
- export declare function renderMcpConfig(project?: string): string;
12
+ export declare function renderMcpConfig(project?: string, { local }?: {
13
+ local?: boolean;
14
+ }): string;
10
15
  /**
11
16
  * The application-level AGENTS.md written by `urlcode init`. Built from the
12
17
  * installed runtime's capability catalog, so it names only the handlers,
@@ -6,7 +6,9 @@ export interface InitOptions {
6
6
  */
7
7
  manifest?: DependencySet | undefined;
8
8
  /** `default` (function, middleware, redirect) or `page`: urlcode.yaml, public/index.html, a README and fixtures only. */
9
- template?: 'default' | 'page' | undefined;
9
+ template?: 'default' | 'page' | 'redirects' | undefined;
10
10
  }
11
+ /** Adds what the starter needs to an existing package.json and changes nothing else; a conflicting script is refused, never overwritten. */
12
+ export declare function mergePackageJson(text: string, scripts: Record<string, string>, dependency: string, version: string): string;
11
13
  export declare function initProject(destination: string, { manifest, template }?: InitOptions): Promise<string>;
12
14
  export declare function addRedirect(project: string, destination: string, alias?: string | undefined): Promise<string>;
@@ -79,11 +79,23 @@ export interface TaskShape {
79
79
  }
80
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
81
  export declare const redirectShapes: TaskShape[];
82
+ /** A complete, paste-ready project skeleton: every supported shape merged into one urlcode.yaml, plus the start script. */
83
+ export interface TaskStarter {
84
+ file: string;
85
+ yaml: string;
86
+ /** Files the yaml references that must exist, with minimal content. */
87
+ companions: Record<string, string>;
88
+ packageScripts: Record<string, string>;
89
+ note: string;
90
+ }
91
+ /** Merges every supported shape's YAML; test/context-task.test.ts compiles the result, so it cannot drift from the runtime. */
92
+ export declare function redirectStarter(): TaskStarter;
82
93
  export interface TaskContext {
83
94
  urlcode: string;
84
95
  schema: '1';
85
96
  task: ContextTask;
86
97
  shapes?: TaskShape[];
98
+ starter?: TaskStarter;
87
99
  project?: {
88
100
  entry: string;
89
101
  routes: number;
@@ -76,6 +76,9 @@ export interface RouteExplanation {
76
76
  env: string;
77
77
  } | {
78
78
  literal: true;
79
+ } | {
80
+ env: string;
81
+ default: string;
79
82
  }>;
80
83
  secrets: Record<string, {
81
84
  secret: string;
@@ -1,3 +1,4 @@
1
+ import { type ReleaseAsset } from './extension-transport.ts';
1
2
  /** Offline, declarative extension bundles. These are deliberately not Node packages. */
2
3
  export declare const ARTIFACT_REPOSITORY = "jimhoyd-com/urlcode";
3
4
  export declare const ARTIFACT_WORKFLOW = "jimhoyd-com/urlcode/.github/workflows/extension-artifacts.yml";
@@ -30,14 +31,24 @@ export interface ExtensionLock {
30
31
  }
31
32
  /** Parse an untrusted catalog only after its GitHub attestation was verified by the caller. */
32
33
  export declare function parseCatalog(bytes: Uint8Array, requestedTag: string): Catalog;
34
+ export interface TarFile {
35
+ path: string;
36
+ bytes: Uint8Array;
37
+ }
38
+ export interface ArchiveLimits {
39
+ archive: number;
40
+ expanded: number;
41
+ files: number;
42
+ file: number;
43
+ label: string;
44
+ }
45
+ /** A minimal tar reader: only regular files are accepted, before any write occurs. */
46
+ export declare function readBoundedTgz(source: Uint8Array, limits: ArchiveLimits): TarFile[];
33
47
  export declare function extractArtifact(bytes: Uint8Array, entry: ArtifactEntry, destination: string): Promise<void>;
34
48
  export declare function readLock(project: string): Promise<ExtensionLock>;
35
49
  export declare function writeLock(project: string, lock: ExtensionLock): Promise<void>;
36
50
  export declare function cachePath(project: string, sha256: string): string;
37
- export interface ReleaseAsset {
38
- name: string;
39
- url: string;
40
- }
51
+ export type { ReleaseAsset };
41
52
  export interface ArtifactTransport {
42
53
  release(tag: string): Promise<ReleaseAsset[]>;
43
54
  download(url: string): Promise<Uint8Array>;
@@ -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>>;
@@ -0,0 +1,31 @@
1
+ /** Shared GitHub release/cache/lockfile plumbing for extension-artifacts.ts and extension-bundles.ts. No opinion on what content is allowed or executable; that trust boundary stays local to each caller (#441). */
2
+ export type UnknownRecord = Record<string, unknown>;
3
+ export declare const isRecord: (v: unknown) => v is UnknownRecord;
4
+ export declare const digestHex: (bytes: Uint8Array) => string;
5
+ export declare function textField(value: unknown, what: string): string;
6
+ export declare function exactKeys(value: UnknownRecord, expected: readonly string[], what: string): void;
7
+ /** Sorted, recursive listing of an extension cache directory; refuses links and special files. */
8
+ export declare function listCachedFiles(root: string, itemLabel: string, prefix?: string): Promise<string[]>;
9
+ /** Atomic write-then-rename for a JSON lockfile, refusing to clobber a concurrent writer. */
10
+ export declare function writeLockAtomic(path: string, temporary: string, data: unknown): Promise<void>;
11
+ export interface ReleaseAsset {
12
+ name: string;
13
+ url: string;
14
+ }
15
+ export interface GithubTransport {
16
+ release(tag: string): Promise<ReleaseAsset[]>;
17
+ download(url: string): Promise<Uint8Array>;
18
+ attest(path: string, release: string): Promise<void>;
19
+ }
20
+ export interface GithubTransportConfig {
21
+ repository: string;
22
+ workflow: string;
23
+ tagPattern: RegExp;
24
+ exampleTag: string;
25
+ maxAssetSize: number;
26
+ itemLabel: string;
27
+ }
28
+ /** A transport that accepts only GitHub Release asset URLs and verifies every downloaded subject. */
29
+ export declare function createGithubTransport(config: GithubTransportConfig): GithubTransport;
30
+ /** Downloads one named release asset and has the transport attest it before returning its bytes. */
31
+ export declare function verifiedReleaseAsset(assets: ReleaseAsset[], asset: string, release: string, transport: GithubTransport, itemLabel: string, tempPrefix: string): Promise<Uint8Array>;
@@ -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. */
@@ -0,0 +1,67 @@
1
+ import type { CapabilityName, CapabilityTarget } from './capabilities.ts';
2
+ import type { RuntimeExtension } from './extensions.ts';
3
+ /** The planner is deliberately a small, local projection. It never treats goal
4
+ * text as instructions, opens a host, or reads extension/project source. */
5
+ export declare const featurePlanMaxBytes = 32768;
6
+ export declare const featurePlanMaxGoalLength = 512;
7
+ export interface FeaturePlanOptions {
8
+ target?: string;
9
+ extensions?: readonly RuntimeExtension[] | undefined;
10
+ }
11
+ export interface FeaturePlan {
12
+ format: 1;
13
+ goalTerms: string[];
14
+ target: CapabilityTarget;
15
+ project: {
16
+ routes: number;
17
+ extensions: string[];
18
+ };
19
+ applicable: {
20
+ capabilities: {
21
+ name: CapabilityName;
22
+ support: string;
23
+ reason: string;
24
+ }[];
25
+ recipes: {
26
+ name: string;
27
+ description: string;
28
+ matched: string[];
29
+ }[];
30
+ };
31
+ extensions: {
32
+ required: {
33
+ name: string;
34
+ reason: string;
35
+ declared: boolean;
36
+ registered: boolean;
37
+ target: string;
38
+ artifact: 'none' | 'cached' | 'missing' | 'invalid';
39
+ }[];
40
+ ordering: {
41
+ status: 'operator-resolved';
42
+ names: string[];
43
+ note: string;
44
+ };
45
+ };
46
+ outline: {
47
+ kind: string;
48
+ note: string;
49
+ }[];
50
+ applicationCode: {
51
+ requirement: string;
52
+ reason: string;
53
+ }[];
54
+ unsupported: {
55
+ requirement: string;
56
+ reason: string;
57
+ }[];
58
+ next: string[];
59
+ estimatedTokens: number;
60
+ }
61
+ /**
62
+ * Plans only from the current compiled project, package-owned catalogs, locked
63
+ * inert artifacts, and registrations passed by the already-opened operator
64
+ * session. It intentionally has no filesystem path, host-file, binding, or
65
+ * execution argument.
66
+ */
67
+ export declare function planFeature(project: string, goal: string, options?: FeaturePlanOptions): Promise<FeaturePlan>;
@@ -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;
@@ -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, 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';
21
+ export { inspectProject, validateProject, explainRoute, explainProject, previewImport, previewExport, getCapability, getSchemaFragment, schemaPathNames, inspectExtensions, describeExtensions, buildContext, renderContext, estimateTokens, documentationTokens, buildTaskContext, renderTaskContext, contextTasks, planFeature, featurePlanMaxBytes, featurePlanMaxGoalLength } from './tooling.ts';
22
+ export type { InspectOptions, RouteExplanation, RouteMiss, ExplainedHandler, ExplainedCache, ExplainedExtensionRequirement, ExtensionProvider, TargetSupport, CapabilityEntry, CapabilityUsage, SchemaFragment, ExtensionInspection, ContextOptions, ProjectContext, ContextSection, ContextTask, TaskContext, TaskShape, FeaturePlan, FeaturePlanOptions } 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>;
@@ -37,6 +37,7 @@ export interface MatchableRoute {
37
37
  pattern: string;
38
38
  parts: string[];
39
39
  prefix?: string;
40
+ wildcard?: boolean;
40
41
  extension?: string;
41
42
  parameters: CompiledParameter[];
42
43
  env: Record<string, string>;
@@ -0,0 +1,30 @@
1
+ import type { InspectOptions } from './tooling.ts';
2
+ export type ReviewCategory = 'native-alternative' | 'extension-alternative' | 'gap' | 'manual-review';
3
+ export type ReviewSignal = 'manual-body-validation' | 'manual-cookie-session' | 'global-mutable-state' | 'outbound-network-call' | 'method-dispatch' | 'manual-rate-limit' | 'manual-security-headers';
4
+ export interface ReviewObservation {
5
+ category: ReviewCategory;
6
+ signal: ReviewSignal;
7
+ routes: string[];
8
+ source: string;
9
+ line: number;
10
+ confidence: 'low' | 'medium';
11
+ reason: string;
12
+ excerpt: string;
13
+ capability?: 'request.body' | 'proxy' | 'methods' | 'policies.throttle' | 'policies.security';
14
+ extension?: string;
15
+ note: string;
16
+ /** Set only for an extension-alternative observation when the caller supplied operator registrations (InspectOptions.extensions): whether that extension is actually registered, and, if so, whether the registration is pinned to this project's current revision. Absent when registration state could not be determined (no registrations supplied), in which case `note` stays with the conservative "declared, setup unconfirmed" wording. */
17
+ registered?: boolean;
18
+ revisionPinned?: boolean;
19
+ }
20
+ export interface ProjectReview {
21
+ format: 1;
22
+ projectSha256: string;
23
+ routeCount: number;
24
+ moduleCount: number;
25
+ observations: ReviewObservation[];
26
+ summary: Record<ReviewCategory, number>;
27
+ }
28
+ export declare const reviewModuleByteLimit = 1048576;
29
+ export declare const reviewExcerptLimit = 240;
30
+ export declare function reviewProject(project: string, options?: InspectOptions): Promise<ProjectReview>;
@@ -12,7 +12,11 @@ export type { SchemaFragment } from './schema-query.ts';
12
12
  export { listRecipes, showRecipe, searchRecipes, listExamples, searchExamples };
13
13
  export { buildContext, renderContext, estimateTokens, documentationTokens, buildTaskContext, renderTaskContext, contextTasks } from './context.ts';
14
14
  export type { ContextOptions, ProjectContext, ContextSection, ContextTask, TaskContext, TaskShape } from './context.ts';
15
+ export { planFeature, featurePlanMaxBytes, featurePlanMaxGoalLength } from './feature-plan.ts';
16
+ export type { FeaturePlan, FeaturePlanOptions } from './feature-plan.ts';
15
17
  export type { RouteExplanation, ExplainedHandler, ExplainedCache, ExplainedExtensionRequirement, ExtensionProvider, TargetSupport } from './explain.ts';
18
+ export { reviewProject } from './review.ts';
19
+ export type { ProjectReview, ReviewObservation, ReviewCategory, ReviewSignal } from './review.ts';
16
20
  /** `extensions` are operator registrations from a host file; explain reports whether each requirement has a provider. Nothing is activated. */
17
21
  export interface InspectOptions {
18
22
  origin?: string;
@@ -19,10 +19,18 @@ export interface ParameterConfig {
19
19
  required?: boolean;
20
20
  schema: ParameterSchema;
21
21
  }
22
- /** `env` binding: a literal `value`, or the `env` name to read from the process environment. */
22
+ /**
23
+ * `env` binding: a plain literal `value` (always reviewable, never overridden); or the `env`
24
+ * name to read from the process environment, with an optional `default` used when that
25
+ * variable is unset. `env` always requires an operator grant for that route/name — if the
26
+ * grant is missing, a declared `default` is used with no host read attempted (the binding
27
+ * degrades to its literal default rather than failing); with no `default`, a missing grant
28
+ * fails route compilation (docs/yaml/functions.md, "Host overrides").
29
+ */
23
30
  export interface EnvBinding {
24
31
  value?: string;
25
32
  env?: string;
33
+ default?: string;
26
34
  }
27
35
  /** `secrets` binding: the `secret` name to read from the process environment. */
28
36
  export interface SecretBinding {
@@ -264,6 +272,7 @@ export interface CompiledRoute extends Omit<RouteConfig, 'methods' | 'parameters
264
272
  reply?: Reply;
265
273
  expiresAt?: number;
266
274
  prefix?: string;
275
+ wildcard?: boolean;
267
276
  middleware: CompiledMiddleware[];
268
277
  function?: CompiledFunction;
269
278
  respond?: RespondSpec;
package/dist/types.js CHANGED
@@ -26,8 +26,15 @@
26
26
 
27
27
  /** One declared input: a path placeholder, a query parameter or a request header. */
28
28
 
29
- /** `env` binding: a literal `value`, or the `env` name to read from the process environment. */
30
-
29
+ /**
30
+ * `env` binding: a plain literal `value` (always reviewable, never overridden); or the `env`
31
+ * name to read from the process environment, with an optional `default` used when that
32
+ * variable is unset. `env` always requires an operator grant for that route/name — if the
33
+ * grant is missing, a declared `default` is used with no host read attempted (the binding
34
+ * degrades to its literal default rather than failing); with no `default`, a missing grant
35
+ * fails route compilation (docs/yaml/functions.md, "Host overrides").
36
+ */
37
+
31
38
  /** `secrets` binding: the `secret` name to read from the process environment. */
32
39
 
33
40
 
@@ -127,7 +134,7 @@
127
134
 
128
135
 
129
136
 
130
-
137
+
131
138
 
132
139
 
133
140