@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.
- package/README.md +18 -18
- package/dist/BUILD-MANIFEST.json +27 -23
- package/dist/agents-guide.js +8 -4
- package/dist/authoring.js +81 -7
- package/dist/build-cloudflare.js +1 -0
- package/dist/build-static.js +1 -0
- package/dist/cli.js +39 -14
- package/dist/config.js +7 -1
- package/dist/context.js +32 -4
- package/dist/ecosystem-cli.js +6 -0
- package/dist/explain-cli.js +1 -1
- package/dist/explain.js +2 -2
- package/dist/extension-artifacts.js +28 -34
- package/dist/extension-bundles.js +62 -0
- package/dist/extension-transport.js +41 -0
- package/dist/extensions.js +4 -0
- package/dist/feature-plan.js +99 -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 +6 -2
- package/dist/readiness.js +2 -2
- package/dist/review.js +206 -0
- package/dist/router.js +35 -10
- package/dist/runtime.js +1 -0
- package/dist/tooling.js +4 -0
- package/dist/types/agents-guide.d.ts +6 -1
- package/dist/types/authoring.d.ts +3 -1
- package/dist/types/context.d.ts +12 -0
- package/dist/types/explain.d.ts +3 -0
- package/dist/types/extension-artifacts.d.ts +15 -4
- package/dist/types/extension-bundles.d.ts +49 -0
- package/dist/types/extension-transport.d.ts +31 -0
- package/dist/types/extensions.d.ts +4 -0
- package/dist/types/feature-plan.d.ts +67 -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/review.d.ts +30 -0
- package/dist/types/tooling.d.ts +4 -0
- package/dist/types/types.d.ts +10 -1
- package/dist/types.js +10 -3
- package/docs/AI-AUTHORING.md +466 -0
- package/docs/FUNCTION-SECURITY.md +251 -0
- package/docs/README.md +96 -0
- package/docs/TOOLING.md +422 -0
- package/docs/YAML-REFERENCE.md +473 -0
- package/llms-full.txt +190 -83
- package/llms.txt +73 -128
- package/package.json +15 -4
- package/recipes/redirect/README.md +2 -2
- package/recipes/store-crud/README.md +9 -10
- package/recipes/store-crud/recipe.yaml +1 -1
- package/schemas/urlcode.schema.json +3 -0
- 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
|
-
|
|
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
|
-
|
|
141
|
-
|
|
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(
|
|
173
|
-
|
|
174
|
-
|
|
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(
|
|
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 (
|
|
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
|
|
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>;
|
package/dist/types/context.d.ts
CHANGED
|
@@ -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;
|
package/dist/types/explain.d.ts
CHANGED
|
@@ -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
|
|
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;
|
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, 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>;
|
package/dist/types/match.d.ts
CHANGED
|
@@ -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>;
|
package/dist/types/tooling.d.ts
CHANGED
|
@@ -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;
|
package/dist/types/types.d.ts
CHANGED
|
@@ -19,10 +19,18 @@ export interface ParameterConfig {
|
|
|
19
19
|
required?: boolean;
|
|
20
20
|
schema: ParameterSchema;
|
|
21
21
|
}
|
|
22
|
-
/**
|
|
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
|
-
/**
|
|
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
|
|