@jimhoyd/urlcode 0.4.8 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -3
- package/dist/BUILD-MANIFEST.json +20 -19
- package/dist/agents-guide.js +8 -4
- package/dist/authoring.js +36 -3
- package/dist/build-cloudflare.js +1 -0
- package/dist/build-static.js +1 -0
- package/dist/cli.js +21 -9
- package/dist/config.js +7 -1
- package/dist/context.js +32 -4
- package/dist/extension-artifacts.js +18 -11
- package/dist/extension-bundles.js +70 -0
- package/dist/extensions.js +4 -0
- package/dist/functions.js +2 -1
- package/dist/index.js +2 -0
- package/dist/init-with.js +55 -23
- package/dist/interchange.js +1 -1
- package/dist/match.js +23 -5
- package/dist/mcp.js +1 -1
- package/dist/readiness.js +2 -2
- package/dist/router.js +20 -8
- package/dist/runtime.js +1 -0
- package/dist/types/agents-guide.d.ts +6 -1
- package/dist/types/authoring.d.ts +1 -1
- package/dist/types/context.d.ts +12 -0
- package/dist/types/extension-artifacts.d.ts +13 -0
- package/dist/types/extension-bundles.d.ts +49 -0
- package/dist/types/extensions.d.ts +4 -0
- package/dist/types/functions.d.ts +4 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/init-with.d.ts +6 -1
- package/dist/types/match.d.ts +1 -0
- package/dist/types/types.d.ts +1 -0
- package/dist/types.js +1 -1
- package/llms-full.txt +94 -22
- package/llms.txt +62 -126
- package/package.json +7 -2
- package/recipes/redirect/README.md +2 -2
- package/starters/default/AGENTS.md +2 -2
package/dist/extensions.js
CHANGED
package/dist/functions.js
CHANGED
|
@@ -30,7 +30,8 @@ import { assert, ConfigError, HttpError } from './errors.js';
|
|
|
30
30
|
|
|
31
31
|
// The worker protocol. Only JSON-shaped data and byte buffers cross it.
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
/** `route.pattern` is the route key that matched, so one module can serve several routes without reading `request.url`. */
|
|
34
|
+
|
|
34
35
|
|
|
35
36
|
|
|
36
37
|
|
package/dist/index.js
CHANGED
|
@@ -44,3 +44,5 @@ export {initProject, addRedirect} from './authoring.js';
|
|
|
44
44
|
export {initProjectWith} from './init-with.js';
|
|
45
45
|
export {collectDependencySet,renderPackageManifest,installSteps} from './project-dependencies.js';
|
|
46
46
|
|
|
47
|
+
export {installBundle,loadExtensionBundle,readBundleLock,parseBundleCatalog} from './extension-bundles.js';
|
|
48
|
+
|
package/dist/init-with.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mkdir, open, readFile, rm, unlink, lstat } from 'node:fs/promises';
|
|
1
|
+
import { mkdir, mkdtemp, open, readFile, rename, rm, unlink, lstat } from 'node:fs/promises';
|
|
2
2
|
import { createRequire } from 'node:module';
|
|
3
3
|
import { basename, dirname, join, relative, resolve, sep } from 'node:path';
|
|
4
4
|
import { pathToFileURL } from 'node:url';
|
|
@@ -11,6 +11,7 @@ import { inspectExtensionRevision } from './extensions.js';
|
|
|
11
11
|
import { collectDependencySet, installSteps, renderPackageManifest } from './project-dependencies.js';
|
|
12
12
|
|
|
13
13
|
import { ConfigError, assert } from './errors.js';
|
|
14
|
+
import { installBundle, loadExtensionBundle, } from './extension-bundles.js';
|
|
14
15
|
|
|
15
16
|
/** Directory names inside the generated site. The route project lives under `app/`; everything else is operator-owned. */
|
|
16
17
|
const PROJECT_DIRECTORY = 'app', HOST_FILE = 'host.mjs', ROUTES_FILE = 'routes/extensions.yaml';
|
|
@@ -24,6 +25,10 @@ const namePattern = /^[a-z][a-z0-9-]{0,63}$/, capabilityPattern = /^[a-z][a-z0-9
|
|
|
24
25
|
|
|
25
26
|
|
|
26
27
|
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
27
32
|
|
|
28
33
|
|
|
29
34
|
|
|
@@ -79,15 +84,19 @@ export function orderScaffolds(results )
|
|
|
79
84
|
* conditions), imports it, and calls its `scaffold` export. Nothing is bundled; core never imports these packages
|
|
80
85
|
* at build time. Refuses a missing package or a package without `scaffold` before anything is written.
|
|
81
86
|
*/
|
|
82
|
-
async function loadScaffold(name , request , cwd , retry ) {
|
|
87
|
+
async function loadScaffold(name , request , cwd , retry , bundleProject ) {
|
|
83
88
|
const pkg = packageName(name);
|
|
84
|
-
let
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
+
let module ;
|
|
90
|
+
if (bundleProject) module = await loadExtensionBundle(bundleProject, name);
|
|
91
|
+
else {
|
|
92
|
+
let entry ;
|
|
93
|
+
try { entry = createRequire(join(cwd, 'package.json')).resolve(pkg); }
|
|
94
|
+
catch (error) {
|
|
95
|
+
if (isCode(error, 'MODULE_NOT_FOUND')) throw new ConfigError(`Extension package ${pkg} is not installed in ${cwd}; run: npm install ${pkg}`);
|
|
96
|
+
throw error;
|
|
97
|
+
}
|
|
98
|
+
module = await import(pathToFileURL(entry).href) ;
|
|
89
99
|
}
|
|
90
|
-
const module = await import(pathToFileURL(entry).href) ;
|
|
91
100
|
const scaffold = module.scaffold;
|
|
92
101
|
if (typeof scaffold !== 'function') throw new ConfigError(`${pkg} does not export scaffold; upgrade it to a release that supports urlcode init --with, or add ${name} by hand following its README`);
|
|
93
102
|
let result ;
|
|
@@ -102,6 +111,8 @@ async function loadScaffold(name , request , cwd ,
|
|
|
102
111
|
assert(record(result) && result.name === name, `${pkg} scaffold must return a result named ${name}`);
|
|
103
112
|
assert(record(result.extensions) && record(result.routes), `${pkg} scaffold must return extensions and routes objects`);
|
|
104
113
|
assert(strings(result.hostImports) && strings(result.hostSetup) && strings(result.hostEntries) && (result.hostClose === undefined || strings(result.hostClose)), `${pkg} scaffold must return host fragments as string arrays`);
|
|
114
|
+
assert(result.hostBundleExports === undefined || (strings(result.hostBundleExports) && result.hostBundleExports.every(value => /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(value))), `${pkg} scaffold hostBundleExports must be JavaScript identifiers`);
|
|
115
|
+
if (request.distribution === 'bundle') assert(Array.isArray(result.hostBundleExports) && result.hostBundleExports.length > 0, `${pkg} scaffold must declare hostBundleExports for executable bundle distribution`);
|
|
105
116
|
assert(result.acknowledged === undefined || (strings(result.acknowledged) && result.acknowledged.every(id => request.acknowledgements.includes(id) && id.startsWith(`${name}:`))), `${pkg} scaffold acknowledged may only list ${name}:<id> acknowledgements the operator passed`);
|
|
106
117
|
assert(result.routeNotes === undefined || (strings(result.routeNotes) && result.routeNotes.every(note => note.length <= 300 && !/[\r\n]/.test(note))), `${pkg} scaffold routeNotes must be single-line strings`);
|
|
107
118
|
assert(strings(result.nextSteps) && typeof result.readme === 'string', `${pkg} scaffold must return readme text and nextSteps strings`);
|
|
@@ -124,8 +135,17 @@ async function write(target , content , mode = 0o644)
|
|
|
124
135
|
const file = await open(target, 'wx', mode);
|
|
125
136
|
try { await file.writeFile(content); await file.sync(); } finally { await file.close(); }
|
|
126
137
|
}
|
|
127
|
-
function renderHost(names , results ) {
|
|
138
|
+
function renderHost(names , results , distribution ='npm') {
|
|
128
139
|
const lines = [`// Generated by urlcode init --with ${names.join(',')}. Trusted operator code: keep it outside ${PROJECT_DIRECTORY}/ and review before serving.`];
|
|
140
|
+
if (distribution === 'bundle') {
|
|
141
|
+
lines.push("import {loadExtensionBundle} from '@jimhoyd/urlcode/extension-bundles';", "import {fileURLToPath} from 'node:url';", "const extensionBundleDirectory = fileURLToPath(new URL('.', import.meta.url));");
|
|
142
|
+
const exports = new Set ();
|
|
143
|
+
for (const result of results) {
|
|
144
|
+
const names = result.hostBundleExports ;
|
|
145
|
+
for (const name of names) { assert(!exports.has(name), `Bundle host export ${name} is declared by more than one extension`); exports.add(name); }
|
|
146
|
+
lines.push(`const {${names.join(', ')}} = await loadExtensionBundle(extensionBundleDirectory, '${result.name}');`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
129
149
|
// Extensions that need the same module (node:url, for example) each list it; an identical line is written once so the host stays valid ESM.
|
|
130
150
|
for (const result of results) for (const line of result.hostImports) if (!lines.includes(line)) lines.push(line);
|
|
131
151
|
lines.push('');
|
|
@@ -142,23 +162,23 @@ function demote(markdown ) {
|
|
|
142
162
|
let fence = false;
|
|
143
163
|
return markdown.split('\n').map(line => { if (/^\s*(?:```|~~~)/.test(line)) fence = !fence; return !fence && /^#{1,5} /.test(line) ? `#${line}` : line; }).join('\n');
|
|
144
164
|
}
|
|
145
|
-
function renderDependencySection(directory , set ) {
|
|
165
|
+
function renderDependencySection(directory , set , bundle = false) {
|
|
146
166
|
const rows = set.pins.map(pin => `- \`${pin.name}\` ${pin.version} (${pin.role})${pin.specifier === pin.version ? '' : ` installed from \`${pin.specifier}\``}`);
|
|
147
167
|
const lines = ['## Dependencies', '',
|
|
148
|
-
'`package.json` pins the runtime, every extension named in `--with` and their declared peers to the exact versions that were installed when this site was generated. Those versions were checked against each package\'s own `peerDependencies` as one set.', '',
|
|
168
|
+
bundle ? '`package.json` pins only the URLCode runtime. Executable extensions are locked GitHub Release bundles in `urlcode.extension-bundles.lock.json`, not npm dependencies.' : '`package.json` pins the runtime, every extension named in `--with` and their declared peers to the exact versions that were installed when this site was generated. Those versions were checked against each package\'s own `peerDependencies` as one set.', '',
|
|
149
169
|
...rows, '',
|
|
150
170
|
...installSteps(directory, set).flatMap(step => [step, '']),
|
|
151
171
|
set.local ? 'At least one pin is a local path or tarball rather than a registry version: reproducing this install needs that path to exist, so keep it under your control or replace the specifier before publishing the site.' : 'The pins are registry versions; `npm install` resolves them without the network only if your cache or mirror already holds them.', '',
|
|
152
172
|
'There is no upgrade command. Changing a pinned version today means editing `package.json` yourself and re-running `npm install`; review the extension changelogs first.', ''];
|
|
153
173
|
return lines.join('\n');
|
|
154
174
|
}
|
|
155
|
-
function renderReadme(directory , names , results , starter , env , projectSha256 , set ) {
|
|
175
|
+
function renderReadme(directory , names , results , starter , env , projectSha256 , set , distribution ='npm') {
|
|
156
176
|
const steps = [...(set ? installSteps(directory, set) : []), ...results.flatMap(result => result.nextSteps)];
|
|
157
177
|
const parts = [`# ${basename(directory)}`, '',
|
|
158
|
-
`Created with \`urlcode init ${basename(directory)} --with ${names.join(',')}\`. \`${PROJECT_DIRECTORY}/\` is the route project (\`urlcode.yaml\`, functions, tests); \`${HOST_FILE}\` is the trusted operator host that wires the installed extension packages; operator modules and private data stay outside the project. Run every command with \`--project ${PROJECT_DIRECTORY} --host-file "$PWD/${HOST_FILE}"\`.`, '',
|
|
178
|
+
`Created with \`urlcode init ${basename(directory)} --with ${names.join(',')}\`. \`${PROJECT_DIRECTORY}/\` is the route project (\`urlcode.yaml\`, functions, tests); \`${HOST_FILE}\` is the trusted operator host that wires ${distribution === 'bundle' ? 'the explicitly installed, verified extension bundles' : 'the installed extension packages'}; operator modules and private data stay outside the project. Run every command with \`--project ${PROJECT_DIRECTORY} --host-file "$PWD/${HOST_FILE}"\`.`, '',
|
|
159
179
|
'## Starter', '', `The starter files live in \`${PROJECT_DIRECTORY}/\`; add \`--project ${PROJECT_DIRECTORY}\` and the host file to the commands below.`, '', demote(starter).trim(), ''];
|
|
160
180
|
for (const result of results) parts.push(`## Extension: ${result.name}`, '', result.readme.trim(), '');
|
|
161
|
-
if (set) parts.push(renderDependencySection(directory, set));
|
|
181
|
+
if (set) parts.push(renderDependencySection(directory, set, distribution === 'bundle'));
|
|
162
182
|
parts.push('## Next steps', '', ...steps.map((step, index) => `${index + 1}. ${step}`), '');
|
|
163
183
|
if (Object.keys(env).length) parts.push('## Environment', '', ...Object.entries(env).map(([key, text]) => `- \`${key}\`: ${text}`), '');
|
|
164
184
|
parts.push('## Project revision', '', `\`${PROJECT_DIRECTORY}/urlcode.yaml\` currently has revision \`${projectSha256}\` (\`inspectExtensionRevision\`). Review the project, then pin exactly that value where the host expects it; any change to extension YAML, policies or mounts changes it and needs a new explicit review.`, '');
|
|
@@ -170,7 +190,7 @@ function renderReadme(directory , names , results
|
|
|
170
190
|
* `urlcode.yaml`, one `host.mjs`, one `README.md` and the extensions' own files. All packages are resolved and
|
|
171
191
|
* their scaffolds computed before anything is written, so a refusal leaves no directory behind.
|
|
172
192
|
*/
|
|
173
|
-
export async function initProjectWith(destination , requested , { cwd = process.cwd(), manifest = true, pins, acknowledgements = [] } = {}) {
|
|
193
|
+
export async function initProjectWith(destination , requested , { cwd = process.cwd(), manifest = true, pins, acknowledgements = [], bundleRelease, bundleTransport } = {}) {
|
|
174
194
|
assert(requested.length > 0, 'Provide at least one --with name');
|
|
175
195
|
assert(new Set(requested).size === requested.length, 'Duplicate --with names');
|
|
176
196
|
// --with is an unordered set: scaffolds see one canonical name order, and the emitted order comes from their declared requirements.
|
|
@@ -178,13 +198,20 @@ export async function initProjectWith(destination , requested
|
|
|
178
198
|
const directory = resolve(destination), project = join(directory, PROJECT_DIRECTORY), hostFile = join(directory, HOST_FILE);
|
|
179
199
|
assert(acknowledgements.every(id => acknowledgementPattern.test(id)), 'Use --ack <extension>:<id>, for example --ack store:public-write');
|
|
180
200
|
const acked = [...new Set(acknowledgements)].sort();
|
|
181
|
-
const
|
|
201
|
+
const distribution =bundleRelease===undefined?'npm':'bundle';
|
|
202
|
+
const request = { directory, project, hostFile, names: sorted, acknowledgements: acked, distribution };
|
|
182
203
|
const quote = (value ) => /^[\w@%+=:,./-]+$/.test(value) ? value : `'${value.replaceAll("'", `'\\''`)}'`;
|
|
183
|
-
const retry = (id ) => ['urlcode init', quote(destination), '--with', requested.join(','), ...(manifest ? [] : ['--no-manifest']), ...[...(pins ?? [])].flatMap(([pkg, specifier]) => ['--pin', quote(`${pkg}=${specifier}`)]), ...[...acked, id].sort().flatMap(item => ['--ack', item])].join(' ');
|
|
204
|
+
const retry = (id ) => ['urlcode init', quote(destination), '--with', requested.join(','), ...(bundleRelease===undefined?[]:['--bundle-release',bundleRelease]), ...(manifest ? [] : ['--no-manifest']), ...[...(pins ?? [])].flatMap(([pkg, specifier]) => ['--pin', quote(`${pkg}=${specifier}`)]), ...[...acked, id].sort().flatMap(item => ['--ack', item])].join(' ');
|
|
184
205
|
const results = [];
|
|
206
|
+
let bundleRoot ;
|
|
185
207
|
const wipe = () => { for (const result of results) for (const file of result.files) if (file.content instanceof Uint8Array) file.content.fill(0); };
|
|
186
208
|
try {
|
|
187
|
-
|
|
209
|
+
if (bundleRelease) {
|
|
210
|
+
await mkdir(dirname(directory), { recursive: true });
|
|
211
|
+
bundleRoot=await mkdtemp(join(dirname(directory),'.urlcode-bundle-init-'));
|
|
212
|
+
for (const name of sorted) await installBundle(bundleRoot,bundleRelease,name,bundleTransport);
|
|
213
|
+
}
|
|
214
|
+
for (const name of sorted) results.push(await loadScaffold(name, request, cwd, retry, bundleRoot));
|
|
188
215
|
const consumed = new Set(results.flatMap(result => result.acknowledged ?? []));
|
|
189
216
|
const unused = acked.filter(id => !consumed.has(id));
|
|
190
217
|
assert(unused.length === 0, `--ack ${unused.join(', ')} has no effect here: no scaffold in --with (${sorted.join(', ')}) consumed it. Remove it, or check the extension name and id in that extension's documentation`);
|
|
@@ -202,11 +229,16 @@ export async function initProjectWith(destination , requested
|
|
|
202
229
|
}
|
|
203
230
|
// Also resolved before the destination exists: an incompatible or incompletely installed set refuses with
|
|
204
231
|
// nothing written. It runs after the scaffold conflicts so a composition error is still reported as one.
|
|
205
|
-
const dependencies = manifest ? await collectDependencySet(names, names.map(packageName), { cwd, ...(pins === undefined ? {} : { overrides: pins }) }) : undefined;
|
|
232
|
+
const dependencies = manifest ? await collectDependencySet(bundleRelease===undefined?names:[], bundleRelease===undefined?names.map(packageName):[], { cwd, ...(pins === undefined ? {} : { overrides: pins }) }) : undefined;
|
|
206
233
|
await mkdir(dirname(directory), { recursive: true });
|
|
207
234
|
await mkdir(directory, { mode: 0o700 }); // refuses an existing destination
|
|
208
235
|
try {
|
|
209
236
|
await initProject(project);
|
|
237
|
+
if (bundleRoot) {
|
|
238
|
+
await rename(join(bundleRoot,'.urlcode'),join(directory,'.urlcode'));
|
|
239
|
+
await rename(join(bundleRoot,'urlcode.extension-bundles.lock.json'),join(directory,'urlcode.extension-bundles.lock.json'));
|
|
240
|
+
await rm(bundleRoot,{recursive:true,force:true}); bundleRoot=undefined;
|
|
241
|
+
}
|
|
210
242
|
const starter = await readFile(join(project, 'README.md'), 'utf8');
|
|
211
243
|
await unlink(join(project, 'README.md')); // its content moves into the site README
|
|
212
244
|
await unlink(join(project, mcpConfigFile)); // re-registered at the site root, pointing at app/
|
|
@@ -236,17 +268,17 @@ export async function initProjectWith(destination , requested
|
|
|
236
268
|
while (probe !== directory && probe.startsWith(directory)) { try { assert(!(await lstat(probe)).isSymbolicLink(), `Scaffold path passes through a symlink: ${file.path}`); } catch (error) { if (!isCode(error, 'ENOENT')) throw error; } probe = dirname(probe); }
|
|
237
269
|
await write(target, file.content, file.mode ?? 0o644); written.add(target);
|
|
238
270
|
}
|
|
239
|
-
await write(hostFile, renderHost(names, results), 0o600);
|
|
271
|
+
await write(hostFile, renderHost(names, results, distribution), 0o600);
|
|
240
272
|
if (dependencies) await write(join(directory, 'package.json'), renderPackageManifest(directory, dependencies));
|
|
241
|
-
await write(join(directory, 'README.md'), renderReadme(directory, names, results, starter, env, projectSha256, dependencies));
|
|
273
|
+
await write(join(directory, 'README.md'), renderReadme(directory, names, results, starter, env, projectSha256, dependencies, distribution));
|
|
242
274
|
await write(join(directory, '.gitignore'), 'node_modules/\ndata/\n.env\n.env.*\n');
|
|
243
275
|
// The read-only MCP server for agents opened at the site root; --host-file and --allow-authoring stay operator choices.
|
|
244
|
-
await write(join(directory, mcpConfigFile), renderMcpConfig(PROJECT_DIRECTORY));
|
|
276
|
+
await write(join(directory, mcpConfigFile), renderMcpConfig(PROJECT_DIRECTORY, { local: dependencies !== undefined }));
|
|
245
277
|
// AGENTS.md: initProject writes the application-level file into app/ once it produces one (NEXT-STEPS 1.1);
|
|
246
278
|
// nothing here overrides it. A site-level agent note would be assembled beside README.md at this point.
|
|
247
279
|
return { directory, project, hostFile, extensions: [...names], projectSha256,
|
|
248
280
|
nextSteps: [...(dependencies ? installSteps(directory, dependencies) : []), ...results.flatMap(result => result.nextSteps)],
|
|
249
281
|
dependencies: dependencies?.pins ?? [] };
|
|
250
282
|
} catch (error) { await rm(directory, { recursive: true, force: true }); throw error; }
|
|
251
|
-
} finally { wipe(); }
|
|
283
|
+
} finally { if(bundleRoot)await rm(bundleRoot,{recursive:true,force:true}); wipe(); }
|
|
252
284
|
}
|
package/dist/interchange.js
CHANGED
|
@@ -166,7 +166,7 @@ export async function exportRoutes(options )
|
|
|
166
166
|
keys(record(options.document),['version','routes']);validateDocument(options.document);
|
|
167
167
|
if(Object.keys(options.document.routes).length>100000)throw new Error('Input exceeds 100,000 routes');
|
|
168
168
|
for(const [path,config]of Object.entries(options.document.routes)) {
|
|
169
|
-
try{keys(record(config),['redirect']);if(!config.redirect)throw new Error('Only redirects can be exported');keys(record(config.redirect),['url','status']);rows.push(row({path,url:config.redirect.url,status:config.redirect.status??302},rows.length+1));}
|
|
169
|
+
try{keys(record(config),['redirect']);if(!config.redirect)throw new Error('Only redirects can be exported');if(path.includes('*')||config.redirect.url.startsWith('/'))throw new Error('Suffix wildcards and root-relative destinations have no provider equivalent');keys(record(config.redirect),['url','status']);rows.push(row({path,url:config.redirect.url,status:config.redirect.status??302},rows.length+1));}
|
|
170
170
|
catch{diagnostics.push({severity:'error',code:'runtime-required',source,path,message:'Only literal redirects with default methods and no extra behavior can be exported'});}
|
|
171
171
|
}
|
|
172
172
|
rows.sort((a,b)=>a.path<b.path?-1:a.path>b.path?1:0);
|
package/dist/match.js
CHANGED
|
@@ -19,7 +19,7 @@ import { HttpError } from './errors.js';
|
|
|
19
19
|
|
|
20
20
|
/** The part of a compiled route that request-time matching reads. router.ts widens it. */
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
|
|
23
23
|
|
|
24
24
|
|
|
25
25
|
|
|
@@ -52,6 +52,8 @@ export function parseTarget(target ) {
|
|
|
52
52
|
if (parts.length > 32 || parts.some(p => p === '.' || p === '..')) throw new HttpError(400, 'Invalid path');
|
|
53
53
|
return { path, parts, query: new URLSearchParams(query) };
|
|
54
54
|
}
|
|
55
|
+
/** Longest suffix a `/**` redirect captures; a longer one is simply not matched. */
|
|
56
|
+
const wildcardMaxLength = 1024;
|
|
55
57
|
export function matchRoute (compiled , target ) {
|
|
56
58
|
const exact = compiled.exact.get(target.path);
|
|
57
59
|
if (exact) return { route: exact, path: dict() };
|
|
@@ -64,7 +66,15 @@ export function matchRoute (compiled
|
|
|
64
66
|
return p === actual;
|
|
65
67
|
})) return { route, path };
|
|
66
68
|
}
|
|
67
|
-
for (const route of compiled.mounts)
|
|
69
|
+
for (const route of compiled.mounts) {
|
|
70
|
+
if (route.wildcard) {
|
|
71
|
+
// `/prefix/**`: one or more whole segments, no empty segment (so a redirect can never gain a `//`), bounded length.
|
|
72
|
+
const rest = route.prefix !== undefined && target.path.startsWith(route.prefix) ? target.path.slice(route.prefix.length) : '';
|
|
73
|
+
if (rest && rest.length <= wildcardMaxLength && !rest.split('/').includes('')) { const path = dict (); path['**'] = rest; return { route, path }; }
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (route.prefix !== undefined && (target.path.startsWith(route.prefix)||(route.extension&&target.path===route.prefix.slice(0,-1)))) return { route, path: dict() };
|
|
77
|
+
}
|
|
68
78
|
return null;
|
|
69
79
|
}
|
|
70
80
|
function scalar(value , type ) {
|
|
@@ -102,14 +112,21 @@ export function contextFor(route , path ,
|
|
|
102
112
|
if (!p.validate(value)) throw new HttpError(400, 'Invalid parameter');
|
|
103
113
|
(inputs[p.in] )[p.name] = value;
|
|
104
114
|
}
|
|
115
|
+
if (route.wildcard) inputs.path['**'] = path['**'] ?? '';
|
|
105
116
|
return { inputs, env: route.env, secrets: route.secrets };
|
|
106
117
|
}
|
|
107
118
|
// The router accepts a placeholder only for a declared path input, and a path
|
|
108
119
|
// input always matches a segment, so the '' fallback is unreachable through a
|
|
109
120
|
// compiled route; it keeps a direct call with an undeclared name from writing
|
|
110
121
|
// the text "undefined" into the location.
|
|
122
|
+
// A root-relative destination (`/profiles/{id}`) is resolved against this placeholder origin only to normalize and encode it;
|
|
123
|
+
// the origin never appears in the Location.
|
|
124
|
+
const relativeBase = 'https://relative.invalid';
|
|
111
125
|
export function redirectLocation(route , context , query ) {
|
|
112
|
-
const
|
|
126
|
+
const relative = route.redirect.url.startsWith('/');
|
|
127
|
+
// `{**}` is the captured suffix: each segment encoded on its own, joined by the `/` that separated them.
|
|
128
|
+
const encode = (name ) => name === '**' ? (context.inputs.path['**'] ?? '').split('/').map(encodeURIComponent).join('/') : encodeURIComponent(context.inputs.path[name] ?? '');
|
|
129
|
+
const location = new URL(route.redirect.url.replace(/\{([^}]+)\}/g, (_m, name ) => encode(name)), relativeBase);
|
|
113
130
|
function append(key , value ) {
|
|
114
131
|
if (value === undefined) return;
|
|
115
132
|
for (const item of Array.isArray(value) ? value : [value]) location.searchParams.append(key, String(item));
|
|
@@ -119,6 +136,7 @@ export function redirectLocation(route
|
|
|
119
136
|
if (own(context.inputs.query, key)) append(key, context.inputs.query[key]);
|
|
120
137
|
else for (const value of query.getAll(key)) append(key,value);
|
|
121
138
|
}
|
|
122
|
-
|
|
123
|
-
|
|
139
|
+
const result = relative ? location.pathname + location.search + location.hash : location.href;
|
|
140
|
+
if (result.length > 16384) throw new HttpError(400, 'Redirect URL too long');
|
|
141
|
+
return result;
|
|
124
142
|
}
|
package/dist/mcp.js
CHANGED
|
@@ -98,7 +98,7 @@ export async function serveMcp(options ) {
|
|
|
98
98
|
if(message.method==='initialize') {
|
|
99
99
|
if(initialized){await error(id,-32600,'Already initialized');return;}
|
|
100
100
|
if(typeof params.protocolVersion!=='string'||!object(params.capabilities)||!object(params.clientInfo)||typeof params.clientInfo.name!=='string'||typeof params.clientInfo.version!=='string'){await error(id,-32602,'Invalid initialize params');return;}
|
|
101
|
-
initialized=true;await send({jsonrpc:'2.0',id,result:{protocolVersion,capabilities:{tools:{}},serverInfo:{name:'urlcode',version:'0.
|
|
101
|
+
initialized=true;await send({jsonrpc:'2.0',id,result:{protocolVersion,capabilities:{tools:{}},serverInfo:{name:'urlcode',version:'0.5.0'}}});return;
|
|
102
102
|
}
|
|
103
103
|
if(message.method==='ping'){await send({jsonrpc:'2.0',id,result:{}});return;}
|
|
104
104
|
if(!ready){await error(id,-32002,'Initialize first');return;}
|
package/dist/readiness.js
CHANGED
|
@@ -119,11 +119,11 @@ export function projectPlan(compiled )
|
|
|
119
119
|
if (route.extension || route.extensionPolicyNames?.length || route.proxy || route.signals?.length || route.match || route.conditional || route.function || route.middleware?.length || route.names.length) continue;
|
|
120
120
|
// Required inputs need intentional fixtures; never invent business data.
|
|
121
121
|
let context ;
|
|
122
|
-
try { context = contextFor(route,{},new URLSearchParams(),new Headers()); } catch { continue; }
|
|
122
|
+
try { context = contextFor(route,route.wildcard ? {'**':'sample'} : {},new URLSearchParams(),new Headers()); } catch { continue; }
|
|
123
123
|
if (route.request?.body?.required) continue;
|
|
124
124
|
const files = route.asset instanceof Map ? route.asset : undefined;
|
|
125
125
|
const prefix = route.prefix ?? '';
|
|
126
|
-
const paths = route.static && files ? [...files.keys()].map(key => prefix + key.split('/').map(encodeURIComponent).join('/')) : [route.pattern];
|
|
126
|
+
const paths = route.wildcard ? [prefix + 'sample'] : route.static && files ? [...files.keys()].map(key => prefix + key.split('/').map(encodeURIComponent).join('/')) : [route.pattern];
|
|
127
127
|
for (const path of paths) for (const method of route.methods) {
|
|
128
128
|
if (!['GET','HEAD'].includes(method)) continue;
|
|
129
129
|
const test = {path,method,status:(route.redirect ? route.redirect.status || 302 : route.reply?.status || 200)};
|
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');
|
|
@@ -167,14 +170,21 @@ export async function compileRoutes(loaded , bindings
|
|
|
167
170
|
if (declaredRedirect) {
|
|
168
171
|
const value = declaredRedirect.url;
|
|
169
172
|
assert(!/[\u0000-\u0020\u007f\\]/u.test(value), 'Redirect URL contains unsafe characters');
|
|
173
|
+
// A root-relative destination (`/profiles/{id}`) stays on this site: a single leading slash, so never `//host`, and no dot segments.
|
|
174
|
+
const relative = value.startsWith('/') && !value.startsWith('//');
|
|
170
175
|
let dest ;
|
|
171
|
-
try { dest = new URL(value); } catch { assert(false, 'Redirect URL must be absolute HTTP(S)'); }
|
|
172
|
-
assert(
|
|
173
|
-
|
|
174
|
-
|
|
176
|
+
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'); }
|
|
177
|
+
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');
|
|
178
|
+
else {
|
|
179
|
+
assert(['http:', 'https:'].includes(dest.protocol) && !dest.username && !dest.password, 'Redirect must use HTTP(S) without credentials');
|
|
180
|
+
const authority = value.match(/^https?:\/\/([^/?#]+)/i)?.[1];
|
|
181
|
+
assert(authority && !/[{}]/.test(authority), 'Redirect placeholders are allowed only in path segments');
|
|
182
|
+
}
|
|
183
|
+
assert(!/[{}]/.test(dest.search + dest.hash), 'Redirect placeholders are allowed only in path segments');
|
|
175
184
|
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(
|
|
185
|
+
assert(placeholders.every(n => (n === '**' && wildcardRedirect) || (token.test(n) && names.includes(n))), 'Redirect placeholder must reference a declared path input');
|
|
186
|
+
assert(placeholders.filter(n => n === '**').length <= 1, '{**} may appear once in a redirect destination');
|
|
187
|
+
assert(!/[{}]/.test(value.replace(/\{(?:[A-Za-z_][A-Za-z0-9_]*|\*\*)\}/g, '')), 'Invalid redirect placeholder');
|
|
178
188
|
const query = declaredRedirect.query || {};
|
|
179
189
|
const reserved = new Set(dest.searchParams.keys());
|
|
180
190
|
for (const [key, ref] of Object.entries(query.map || {})) {
|
|
@@ -195,7 +205,8 @@ export async function compileRoutes(loaded , bindings
|
|
|
195
205
|
route.function = { ...declaredFunction, source, export: declaredFunction.export || 'default' };
|
|
196
206
|
for (const ref of Object.values(declaredFunction.args || {})) referenceCheck(ref, route, true);
|
|
197
207
|
}
|
|
198
|
-
if (
|
|
208
|
+
if (wildcardRedirect) { route.prefix = pattern.slice(0, -2); route.wildcard = true; mounts.push(route); }
|
|
209
|
+
else if (config.static || config.extension) { route.prefix = pattern.slice(0, -1); mounts.push(route); }
|
|
199
210
|
else if (!names.length) exact.set(pattern, route);
|
|
200
211
|
else {
|
|
201
212
|
assert(dynamic.length < 1000, 'Maximum 1000 parameterized routes per snapshot');
|
|
@@ -215,6 +226,7 @@ export async function compileRoutes(loaded , bindings
|
|
|
215
226
|
byLength.get(route.parts.length) .push(route);
|
|
216
227
|
}
|
|
217
228
|
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');}}
|
|
229
|
+
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
230
|
mounts.sort((a,b) => b.prefix .length - a.prefix .length);
|
|
219
231
|
assert(performance.now()<deadline, 'Route compilation deadline exceeded');
|
|
220
232
|
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
|
|
@@ -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,7 @@ 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
11
|
export declare function initProject(destination: string, { manifest, template }?: InitOptions): Promise<string>;
|
|
12
12
|
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;
|
|
@@ -30,6 +30,19 @@ export interface ExtensionLock {
|
|
|
30
30
|
}
|
|
31
31
|
/** Parse an untrusted catalog only after its GitHub attestation was verified by the caller. */
|
|
32
32
|
export declare function parseCatalog(bytes: Uint8Array, requestedTag: string): Catalog;
|
|
33
|
+
export interface TarFile {
|
|
34
|
+
path: string;
|
|
35
|
+
bytes: Uint8Array;
|
|
36
|
+
}
|
|
37
|
+
export interface ArchiveLimits {
|
|
38
|
+
archive: number;
|
|
39
|
+
expanded: number;
|
|
40
|
+
files: number;
|
|
41
|
+
file: number;
|
|
42
|
+
label: string;
|
|
43
|
+
}
|
|
44
|
+
/** A minimal tar reader: only regular files are accepted, before any write occurs. */
|
|
45
|
+
export declare function readBoundedTgz(source: Uint8Array, limits: ArchiveLimits): TarFile[];
|
|
33
46
|
export declare function extractArtifact(bytes: Uint8Array, entry: ArtifactEntry, destination: string): Promise<void>;
|
|
34
47
|
export declare function readLock(project: string): Promise<ExtensionLock>;
|
|
35
48
|
export declare function writeLock(project: string, lock: ExtensionLock): Promise<void>;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/** Verified, executable first-party bundles. Unlike extension artifacts, these are trusted operator code. */
|
|
2
|
+
export declare const BUNDLE_REPOSITORY = "jimhoyd-com/urlcode";
|
|
3
|
+
export declare const BUNDLE_WORKFLOW = "jimhoyd-com/urlcode/.github/workflows/extension-bundles.yml";
|
|
4
|
+
export interface BundleEntry {
|
|
5
|
+
name: string;
|
|
6
|
+
version: string;
|
|
7
|
+
asset: string;
|
|
8
|
+
sha256: string;
|
|
9
|
+
entry: string;
|
|
10
|
+
}
|
|
11
|
+
export interface BundleCatalog {
|
|
12
|
+
format: 1;
|
|
13
|
+
tag: string;
|
|
14
|
+
commit: string;
|
|
15
|
+
coreVersion: string;
|
|
16
|
+
bundles: BundleEntry[];
|
|
17
|
+
revoked: {
|
|
18
|
+
sha256: string;
|
|
19
|
+
reason: string;
|
|
20
|
+
}[];
|
|
21
|
+
}
|
|
22
|
+
export interface LockedBundle extends BundleEntry {
|
|
23
|
+
catalog: {
|
|
24
|
+
tag: string;
|
|
25
|
+
commit: string;
|
|
26
|
+
};
|
|
27
|
+
coreVersion: string;
|
|
28
|
+
}
|
|
29
|
+
export interface BundleLock {
|
|
30
|
+
format: 1;
|
|
31
|
+
bundles: LockedBundle[];
|
|
32
|
+
}
|
|
33
|
+
export interface BundleTransport {
|
|
34
|
+
release(tag: string): Promise<{
|
|
35
|
+
name: string;
|
|
36
|
+
url: string;
|
|
37
|
+
}[]>;
|
|
38
|
+
download(url: string): Promise<Uint8Array>;
|
|
39
|
+
attest(path: string, release: string): Promise<void>;
|
|
40
|
+
}
|
|
41
|
+
/** Parse only a catalog whose attestation was already verified against the requested immutable tag. */
|
|
42
|
+
export declare function parseBundleCatalog(bytes: Uint8Array, requested: string): BundleCatalog;
|
|
43
|
+
export declare function extractBundle(bytes: Uint8Array, item: Pick<LockedBundle, 'name' | 'version' | 'entry' | 'sha256' | 'coreVersion'>, destination: string): Promise<void>;
|
|
44
|
+
export declare function bundleCachePath(project: string, sha256: string): string;
|
|
45
|
+
export declare function readBundleLock(project: string): Promise<BundleLock>;
|
|
46
|
+
export declare const githubBundleTransport: BundleTransport;
|
|
47
|
+
export declare function installBundle(project: string, release: string, bundleName: string, transport?: BundleTransport): Promise<BundleLock>;
|
|
48
|
+
/** Explicit host-only loader. It never reads project YAML, downloads, updates, or discovers code. */
|
|
49
|
+
export declare function loadExtensionBundle(project: string, bundleName: string): Promise<Record<string, unknown>>;
|
|
@@ -185,6 +185,8 @@ export interface ScaffoldRequest {
|
|
|
185
185
|
hostFile: string;
|
|
186
186
|
/** Every extension name being scaffolded together, including this one, in a canonical (sorted) order that is independent of the `--with` spelling. */
|
|
187
187
|
names: readonly string[];
|
|
188
|
+
/** `npm` resolves extension packages from the operator's install; `bundle` resolves only already-verified, locked release bundles. */
|
|
189
|
+
distribution?: 'npm' | 'bundle';
|
|
188
190
|
/**
|
|
189
191
|
* Operator acknowledgements from repeated `--ack <extension>:<id>` flags, sorted and de-duplicated; empty when none. Core treats
|
|
190
192
|
* them as opaque strings and never invents one. An extension reads only the ones qualified with its own name. To require one, throw
|
|
@@ -224,6 +226,8 @@ export interface ScaffoldResult {
|
|
|
224
226
|
hostSetup: string[];
|
|
225
227
|
hostEntries: string[];
|
|
226
228
|
hostClose?: string[];
|
|
229
|
+
/** Named exports core may bind from this extension's already-verified executable bundle. Required for bundle distribution; never a project-controlled module reference. */
|
|
230
|
+
hostBundleExports?: string[];
|
|
227
231
|
/** Files written relative to `directory` with their modes; never inside the project, never overwriting. */
|
|
228
232
|
files: ScaffoldFile[];
|
|
229
233
|
/** Markdown appended to README.md under a heading core adds; the numbered steps merged in the resolved order. */
|
|
@@ -36,8 +36,12 @@ export interface FunctionWorkerData {
|
|
|
36
36
|
dependencies: Record<string, string[]>;
|
|
37
37
|
entries: [string, string][];
|
|
38
38
|
}
|
|
39
|
+
/** `route.pattern` is the route key that matched, so one module can serve several routes without reading `request.url`. */
|
|
39
40
|
export type FunctionContext = RequestContext & {
|
|
40
41
|
args?: Record<string, ParameterValue>;
|
|
42
|
+
route?: {
|
|
43
|
+
pattern: string;
|
|
44
|
+
};
|
|
41
45
|
};
|
|
42
46
|
export interface FunctionWorkerRequest {
|
|
43
47
|
id: string;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -40,3 +40,5 @@ export { initProject, addRedirect } from './authoring.ts';
|
|
|
40
40
|
export { initProjectWith } from './init-with.ts';
|
|
41
41
|
export { collectDependencySet, renderPackageManifest, installSteps } from './project-dependencies.ts';
|
|
42
42
|
export type { ScaffoldRequest, ScaffoldResult, ScaffoldFile } from './extensions.ts';
|
|
43
|
+
export { installBundle, loadExtensionBundle, readBundleLock, parseBundleCatalog } from './extension-bundles.ts';
|
|
44
|
+
export type { BundleCatalog, BundleEntry, BundleLock, LockedBundle, BundleTransport } from './extension-bundles.ts';
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ScaffoldResult } from './extensions.ts';
|
|
2
2
|
import type { DependencyPin } from './project-dependencies.ts';
|
|
3
|
+
import { type BundleTransport } from './extension-bundles.ts';
|
|
3
4
|
export interface InitWithOptions {
|
|
4
5
|
cwd?: string | undefined;
|
|
5
6
|
/** Default true: record exact pins for core, the named extensions and their declared peers. */
|
|
@@ -8,6 +9,10 @@ export interface InitWithOptions {
|
|
|
8
9
|
pins?: ReadonlyMap<string, string> | undefined;
|
|
9
10
|
/** `--ack <extension>:<id>`, repeatable: opaque qualified acknowledgements handed to every scaffold. Core refuses one that no scaffold consumed. */
|
|
10
11
|
acknowledgements?: readonly string[] | undefined;
|
|
12
|
+
/** Immutable signed release used instead of resolving executable extension packages from npm. */
|
|
13
|
+
bundleRelease?: string | undefined;
|
|
14
|
+
/** Test-only transport injection; production uses GitHub attestation verification. */
|
|
15
|
+
bundleTransport?: BundleTransport | undefined;
|
|
11
16
|
}
|
|
12
17
|
export interface InitWithResult {
|
|
13
18
|
directory: string;
|
|
@@ -30,4 +35,4 @@ export declare function orderScaffolds(results: readonly ScaffoldResult[]): Scaf
|
|
|
30
35
|
* `urlcode.yaml`, one `host.mjs`, one `README.md` and the extensions' own files. All packages are resolved and
|
|
31
36
|
* their scaffolds computed before anything is written, so a refusal leaves no directory behind.
|
|
32
37
|
*/
|
|
33
|
-
export declare function initProjectWith(destination: string, requested: readonly string[], { cwd, manifest, pins, acknowledgements }?: InitWithOptions): Promise<InitWithResult>;
|
|
38
|
+
export declare function initProjectWith(destination: string, requested: readonly string[], { cwd, manifest, pins, acknowledgements, bundleRelease, bundleTransport }?: InitWithOptions): Promise<InitWithResult>;
|
package/dist/types/match.d.ts
CHANGED
package/dist/types/types.d.ts
CHANGED
|
@@ -264,6 +264,7 @@ export interface CompiledRoute extends Omit<RouteConfig, 'methods' | 'parameters
|
|
|
264
264
|
reply?: Reply;
|
|
265
265
|
expiresAt?: number;
|
|
266
266
|
prefix?: string;
|
|
267
|
+
wildcard?: boolean;
|
|
267
268
|
middleware: CompiledMiddleware[];
|
|
268
269
|
function?: CompiledFunction;
|
|
269
270
|
respond?: RespondSpec;
|