@jimhoyd/urlcode 0.3.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/CONTRIBUTING.md +80 -0
- package/LICENSE +202 -0
- package/NOTICE +20 -0
- package/README.md +342 -0
- package/ROADMAP.md +248 -0
- package/SECURITY.md +40 -0
- package/data/agents/LICENSES/ai-robots-txt.txt +24 -0
- package/data/agents/LICENSES/crawler-user-agents.txt +23 -0
- package/data/agents/ai-crawlers.json +1240 -0
- package/data/agents/crawlers.json +10515 -0
- package/data/agents/index.d.ts +3 -0
- package/data/agents/index.js +2453 -0
- package/data/agents/monitoring.json +1520 -0
- package/data/agents/seo.json +3865 -0
- package/dist/BUILD-MANIFEST.json +60 -0
- package/dist/adapters.js +59 -0
- package/dist/agent-lists.js +59 -0
- package/dist/assets.js +129 -0
- package/dist/authoring.js +68 -0
- package/dist/aws.js +110 -0
- package/dist/build-cloudflare.js +164 -0
- package/dist/cli.js +185 -0
- package/dist/client-address.js +85 -0
- package/dist/cloudflare.js +165 -0
- package/dist/compliance-rules/baseline.js +98 -0
- package/dist/compliance-rules/privacy.js +38 -0
- package/dist/compliance-rules/shared.js +62 -0
- package/dist/compliance-rules/strict.js +61 -0
- package/dist/compliance.js +219 -0
- package/dist/config-worker.js +9 -0
- package/dist/config.js +172 -0
- package/dist/errors.js +8 -0
- package/dist/function-sources.js +54 -0
- package/dist/function-worker.js +105 -0
- package/dist/functions.js +174 -0
- package/dist/guest-api.js +114 -0
- package/dist/header-validation.js +18 -0
- package/dist/http-policy.js +74 -0
- package/dist/http-response.js +84 -0
- package/dist/index.js +11 -0
- package/dist/link-api.js +136 -0
- package/dist/link-cli.js +141 -0
- package/dist/link-events.js +76 -0
- package/dist/link-records.js +31 -0
- package/dist/link-store-worker.js +150 -0
- package/dist/link-store.js +250 -0
- package/dist/logging.js +22 -0
- package/dist/management-policy.js +41 -0
- package/dist/match.js +124 -0
- package/dist/observability.js +242 -0
- package/dist/plugins.js +74 -0
- package/dist/policies/agents.js +248 -0
- package/dist/policies/cache.js +297 -0
- package/dist/policies/compression.js +187 -0
- package/dist/policies/security.js +0 -0
- package/dist/policies/throttle.js +131 -0
- package/dist/policies.js +142 -0
- package/dist/policy.js +55 -0
- package/dist/prerender.js +165 -0
- package/dist/project-tests.js +23 -0
- package/dist/readiness.js +216 -0
- package/dist/router.js +166 -0
- package/dist/runtime.js +244 -0
- package/dist/scaffold.js +0 -0
- package/dist/scripts/operational-drills.js +75 -0
- package/dist/server.js +270 -0
- package/dist/site.js +0 -0
- package/dist/sqlite-version.js +6 -0
- package/dist/types/adapters.d.ts +12 -0
- package/dist/types/agent-lists.d.ts +5 -0
- package/dist/types/assets.d.ts +13 -0
- package/dist/types/authoring.d.ts +2 -0
- package/dist/types/aws.d.ts +35 -0
- package/dist/types/build-cloudflare.d.ts +14 -0
- package/dist/types/cli.d.ts +2 -0
- package/dist/types/client-address.d.ts +9 -0
- package/dist/types/cloudflare.d.ts +75 -0
- package/dist/types/compliance-rules/baseline.d.ts +12 -0
- package/dist/types/compliance-rules/privacy.d.ts +6 -0
- package/dist/types/compliance-rules/shared.d.ts +24 -0
- package/dist/types/compliance-rules/strict.d.ts +9 -0
- package/dist/types/compliance.d.ts +119 -0
- package/dist/types/config-worker.d.ts +1 -0
- package/dist/types/config.d.ts +21 -0
- package/dist/types/errors.d.ts +7 -0
- package/dist/types/function-sources.d.ts +21 -0
- package/dist/types/function-worker.d.ts +1 -0
- package/dist/types/functions.d.ts +92 -0
- package/dist/types/guest-api.d.ts +16 -0
- package/dist/types/header-validation.d.ts +2 -0
- package/dist/types/http-policy.d.ts +39 -0
- package/dist/types/http-response.d.ts +43 -0
- package/dist/types/index.d.ts +11 -0
- package/dist/types/link-api.d.ts +30 -0
- package/dist/types/link-cli.d.ts +36 -0
- package/dist/types/link-events.d.ts +27 -0
- package/dist/types/link-records.d.ts +11 -0
- package/dist/types/link-store-worker.d.ts +1 -0
- package/dist/types/link-store.d.ts +130 -0
- package/dist/types/logging.d.ts +9 -0
- package/dist/types/management-policy.d.ts +9 -0
- package/dist/types/match.d.ts +81 -0
- package/dist/types/observability.d.ts +104 -0
- package/dist/types/plugins.d.ts +27 -0
- package/dist/types/policies/agents.d.ts +86 -0
- package/dist/types/policies/cache.d.ts +82 -0
- package/dist/types/policies/compression.d.ts +41 -0
- package/dist/types/policies/security.d.ts +37 -0
- package/dist/types/policies/throttle.d.ts +55 -0
- package/dist/types/policies.d.ts +36 -0
- package/dist/types/policy.d.ts +21 -0
- package/dist/types/prerender.d.ts +31 -0
- package/dist/types/project-tests.d.ts +13 -0
- package/dist/types/readiness.d.ts +121 -0
- package/dist/types/router.d.ts +11 -0
- package/dist/types/runtime.d.ts +91 -0
- package/dist/types/scaffold.d.ts +17 -0
- package/dist/types/server.d.ts +37 -0
- package/dist/types/site.d.ts +12 -0
- package/dist/types/sqlite-version.d.ts +1 -0
- package/dist/types/types.d.ts +332 -0
- package/dist/types/vercel.d.ts +12 -0
- package/dist/types.js +177 -0
- package/dist/vercel.js +65 -0
- package/docs/AI-AUTHORING.md +113 -0
- package/docs/ASSETS.md +106 -0
- package/docs/AWS.md +77 -0
- package/docs/BEST-PRACTICES.md +267 -0
- package/docs/CAPACITY.md +208 -0
- package/docs/CLOUDFLARE.md +110 -0
- package/docs/COMPLIANCE.md +242 -0
- package/docs/DYNAMIC-LINKS.md +561 -0
- package/docs/FUNCTION-SECURITY.md +113 -0
- package/docs/HTTP.md +129 -0
- package/docs/INSTALL.md +90 -0
- package/docs/LOAD-TESTING.md +91 -0
- package/docs/LOCAL-DEVELOPMENT.md +99 -0
- package/docs/MANAGEMENT-SECURITY.md +82 -0
- package/docs/MIDDLEWARE.md +83 -0
- package/docs/MONITORING.md +132 -0
- package/docs/OBSERVABILITY.md +229 -0
- package/docs/OPERATIONAL-PROOF.md +42 -0
- package/docs/OPERATIONS.md +207 -0
- package/docs/ORGANIZATION.md +135 -0
- package/docs/PERFORMANCE.md +72 -0
- package/docs/PLUGINS.md +234 -0
- package/docs/POLICIES.md +489 -0
- package/docs/PRERENDER.md +193 -0
- package/docs/PROJECT-DIRECTION.md +51 -0
- package/docs/READINESS.md +125 -0
- package/docs/README.md +58 -0
- package/docs/RELEASE-READINESS.md +95 -0
- package/docs/RELEASE-SECURITY.md +72 -0
- package/docs/RESILIENCE.md +160 -0
- package/docs/ROUTING.md +95 -0
- package/docs/SANDBOX-REVIEW.md +59 -0
- package/docs/SCAFFOLDING.md +72 -0
- package/docs/SECURITY-AUDIT.md +124 -0
- package/docs/SITE.md +150 -0
- package/docs/SPECIFICATION.md +232 -0
- package/docs/SPIKE-EXTENSIONS.md +475 -0
- package/docs/STANDARDS.md +303 -0
- package/docs/STARTERS.md +51 -0
- package/docs/TUNNELS.md +75 -0
- package/docs/TYPESCRIPT.md +102 -0
- package/docs/VERCEL.md +107 -0
- package/docs/YAML-GUIDE.md +521 -0
- package/docs/YAML-REFERENCE.md +328 -0
- package/docs/policies/agents.md +182 -0
- package/docs/policies/cache.md +139 -0
- package/docs/policies/compression.md +168 -0
- package/docs/policies/security.md +160 -0
- package/docs/policies/throttle.md +103 -0
- package/examples/assets/.env.example +1 -0
- package/examples/assets/.gitattributes +2 -0
- package/examples/assets/Makefile +30 -0
- package/examples/assets/README.md +18 -0
- package/examples/assets/functions/hello.mjs +3 -0
- package/examples/assets/gitignore.template +7 -0
- package/examples/assets/public/about.html +2 -0
- package/examples/assets/public/assets/example.txt +1 -0
- package/examples/assets/public/guide.txt +1 -0
- package/examples/assets/tests/requests.json +44 -0
- package/examples/assets/urlcode.yaml +27 -0
- package/examples/aws/README.md +13 -0
- package/examples/aws/handler.mjs +5 -0
- package/examples/aws/public/index.html +3 -0
- package/examples/aws/public/notes.txt +1 -0
- package/examples/aws/template.yaml +33 -0
- package/examples/aws/tests/requests.json +9 -0
- package/examples/aws/urlcode.yaml +19 -0
- package/examples/cloudflare/README.md +18 -0
- package/examples/cloudflare/package.json +14 -0
- package/examples/cloudflare/tests/requests.json +9 -0
- package/examples/cloudflare/urlcode.yaml +24 -0
- package/examples/cloudflare/wrangler.toml +5 -0
- package/examples/compliance/README.md +20 -0
- package/examples/compliance/rules.mjs +65 -0
- package/examples/cookbook/README.md +21 -0
- package/examples/cookbook/functions/choice.mjs +4 -0
- package/examples/cookbook/functions/echo.mjs +3 -0
- package/examples/cookbook/functions/hello.mjs +3 -0
- package/examples/cookbook/functions/text.mjs +3 -0
- package/examples/cookbook/middleware/headers.mjs +6 -0
- package/examples/cookbook/public/about.html +1 -0
- package/examples/cookbook/public/assets/index.html +1 -0
- package/examples/cookbook/public/assets/site.css +1 -0
- package/examples/cookbook/public/favicon.svg +1 -0
- package/examples/cookbook/public/guide.txt +1 -0
- package/examples/cookbook/public/llms.txt +6 -0
- package/examples/cookbook/routes/code.yaml +55 -0
- package/examples/cookbook/routes/files.yaml +17 -0
- package/examples/cookbook/routes/policies.yaml +28 -0
- package/examples/cookbook/routes/redirects.yaml +38 -0
- package/examples/cookbook/routes/responses.yaml +26 -0
- package/examples/cookbook/tests/requests.json +254 -0
- package/examples/cookbook/urlcode.yaml +22 -0
- package/examples/live-links/README.md +11 -0
- package/examples/live-links/tests/requests.json +6 -0
- package/examples/live-links/urlcode.yaml +16 -0
- package/examples/monitoring/blackbox-jobs.yaml +37 -0
- package/examples/monitoring/prometheus-rules.yaml +88 -0
- package/examples/monitoring/prometheus-scrape.yaml +20 -0
- package/examples/monitoring/vector.toml +65 -0
- package/examples/prerender/README.md +48 -0
- package/examples/prerender/functions/page.mjs +6 -0
- package/examples/prerender/middleware/template.mjs +29 -0
- package/examples/prerender/prerender.d.mts +4 -0
- package/examples/prerender/prerender.mjs +63 -0
- package/examples/prerender/tests/requests.json +39 -0
- package/examples/prerender/urlcode.yaml +50 -0
- package/examples/tunnel/dev-with-ngrok.sh +61 -0
- package/examples/vercel/README.md +13 -0
- package/examples/vercel/api/index.js +5 -0
- package/examples/vercel/package.json +8 -0
- package/examples/vercel/public/index.html +3 -0
- package/examples/vercel/public/notes.txt +1 -0
- package/examples/vercel/tests/requests.json +48 -0
- package/examples/vercel/urlcode.yaml +19 -0
- package/examples/vercel/vercel.json +10 -0
- package/llms.txt +46 -0
- package/package.json +123 -0
- package/schemas/urlcode.schema.json +1230 -0
- package/starters/default/.gitattributes +1 -0
- package/starters/default/Makefile +30 -0
- package/starters/default/README.md +34 -0
- package/starters/default/functions/hello.mjs +3 -0
- package/starters/default/gitignore.template +8 -0
- package/starters/default/middleware/headers.mjs +6 -0
- package/starters/default/routes/functions.yaml +20 -0
- package/starters/default/routes/marketing/links.yaml +7 -0
- package/starters/default/starter.json +5 -0
- package/starters/default/tests/requests.json +56 -0
- package/starters/default/urlcode.yaml +8 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Prerender recipe: render this project's function/middleware routes once, at
|
|
3
|
+
// build time, into a native page project that serves the same bytes with no
|
|
4
|
+
// guest execution on the request path.
|
|
5
|
+
//
|
|
6
|
+
// The orchestration that is easy to get wrong — runtime lifecycle, response
|
|
7
|
+
// validation, budgets, byte fidelity and output-path safety — lives in the
|
|
8
|
+
// runtime's build helper. What is left here is the part every site does
|
|
9
|
+
// differently: assembling a project from the rendered pages. In your own
|
|
10
|
+
// project, import the helper from the package:
|
|
11
|
+
//
|
|
12
|
+
// import {prerenderPages, assertNativeProject} from '@jimhoyd/urlcode/prerender';
|
|
13
|
+
//
|
|
14
|
+
import {mkdir, writeFile, rm} from 'node:fs/promises';
|
|
15
|
+
import {realpathSync} from 'node:fs';
|
|
16
|
+
import {join, resolve} from 'node:path';
|
|
17
|
+
import {fileURLToPath, pathToFileURL} from 'node:url';
|
|
18
|
+
import {stringify} from 'yaml';
|
|
19
|
+
import {prerenderPages, assertNativeProject} from '@jimhoyd/urlcode/prerender';
|
|
20
|
+
|
|
21
|
+
export async function prerender(project, output, {log = () => {}} = {}) {
|
|
22
|
+
const out = resolve(output);
|
|
23
|
+
// Pages are rendered and written first; nothing below runs unless every one
|
|
24
|
+
// of them produced a complete HTML response.
|
|
25
|
+
const rendered = await prerenderPages(project, join(out, 'public'), {log});
|
|
26
|
+
|
|
27
|
+
// Assemble the serving project. A larger site does this differently: it adds
|
|
28
|
+
// static and download routes, response security headers and its own entry
|
|
29
|
+
// point, and may write a generated include instead of a whole project.
|
|
30
|
+
await writeFile(join(out, 'urlcode.yaml'), stringify({
|
|
31
|
+
version: '1',
|
|
32
|
+
routes: Object.fromEntries(rendered.pages.map(page =>
|
|
33
|
+
[page.path, {page: {file: `public/${page.file}`, cacheControl: 'no-cache'}}])),
|
|
34
|
+
}));
|
|
35
|
+
await mkdir(join(out, 'tests'), {recursive: true});
|
|
36
|
+
await writeFile(join(out, 'tests/requests.json'), JSON.stringify(rendered.fixtures, null, 2) + '\n');
|
|
37
|
+
|
|
38
|
+
// Prove the artifact is inert before anyone deploys it.
|
|
39
|
+
await assertNativeProject(out, {allow: ['page'], log});
|
|
40
|
+
return {pages: rendered.count, bytes: rendered.bytes, output: out};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Node resolves a module's own URL through symlinks, so comparing it to a raw
|
|
44
|
+
// argv[1] misses when this file is reached through one — as it is under macOS's
|
|
45
|
+
// /var -> /private/var temporary directories, where the script would otherwise
|
|
46
|
+
// exit 0 having silently done nothing.
|
|
47
|
+
const invokedDirectly = () => {
|
|
48
|
+
try { return import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href; }
|
|
49
|
+
catch { return false; }
|
|
50
|
+
};
|
|
51
|
+
if (process.argv[1] && invokedDirectly()) {
|
|
52
|
+
const [project = fileURLToPath(new URL('.', import.meta.url)), output = 'dist'] = process.argv.slice(2);
|
|
53
|
+
const print = value => process.stdout.write(JSON.stringify(value) + '\n');
|
|
54
|
+
try {
|
|
55
|
+
// The helper refuses to write into an existing directory, so a repeated
|
|
56
|
+
// build clears its own output rather than merging into a stale one.
|
|
57
|
+
await rm(resolve(output), {recursive: true, force: true});
|
|
58
|
+
print({event: 'prerendered-project', ...await prerender(project, output, {log: print})});
|
|
59
|
+
} catch (error) {
|
|
60
|
+
process.stderr.write(JSON.stringify({event: 'error', message: error.message}) + '\n');
|
|
61
|
+
process.exitCode = 1;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"path": "/",
|
|
4
|
+
"status": 200,
|
|
5
|
+
"expectHeaders": {"content-type": "text/html; charset=utf-8"}
|
|
6
|
+
},
|
|
7
|
+
{
|
|
8
|
+
"path": "/",
|
|
9
|
+
"method": "HEAD",
|
|
10
|
+
"status": 200,
|
|
11
|
+
"expectBody": ""
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"path": "/guide",
|
|
15
|
+
"status": 200,
|
|
16
|
+
"expectHeaders": {"content-type": "text/html; charset=utf-8"}
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"path": "/guide",
|
|
20
|
+
"method": "HEAD",
|
|
21
|
+
"status": 200,
|
|
22
|
+
"expectBody": ""
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"path": "/about",
|
|
26
|
+
"status": 200,
|
|
27
|
+
"expectHeaders": {"content-type": "text/html; charset=utf-8"}
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"path": "/about",
|
|
31
|
+
"method": "HEAD",
|
|
32
|
+
"status": 200,
|
|
33
|
+
"expectBody": ""
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"path": "/missing",
|
|
37
|
+
"status": 404
|
|
38
|
+
}
|
|
39
|
+
]
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
version: "1"
|
|
2
|
+
# The dynamic source of the site. Every page is a function response wrapped by
|
|
3
|
+
# one shared template middleware, so `dev` serves it live while you edit. The
|
|
4
|
+
# prerender recipe renders these routes once and emits a native page project.
|
|
5
|
+
routes:
|
|
6
|
+
/:
|
|
7
|
+
description: Home page rendered by a function and the shared template
|
|
8
|
+
middleware:
|
|
9
|
+
- source: middleware/template.mjs
|
|
10
|
+
export: template
|
|
11
|
+
function:
|
|
12
|
+
source: functions/page.mjs
|
|
13
|
+
export: page
|
|
14
|
+
args:
|
|
15
|
+
title: URLCode prerender
|
|
16
|
+
body: |
|
|
17
|
+
<p>This page is generated by a function and wrapped by shared template
|
|
18
|
+
middleware. Running the prerender recipe turns it into a native page
|
|
19
|
+
route that serves the same bytes with no sandbox on the request path.</p>
|
|
20
|
+
<ul>
|
|
21
|
+
<li><a href="/guide">Guide</a></li>
|
|
22
|
+
<li><a href="/about">About</a></li>
|
|
23
|
+
</ul>
|
|
24
|
+
/guide:
|
|
25
|
+
description: A second page reusing the same function and template
|
|
26
|
+
middleware:
|
|
27
|
+
- source: middleware/template.mjs
|
|
28
|
+
export: template
|
|
29
|
+
function:
|
|
30
|
+
source: functions/page.mjs
|
|
31
|
+
export: page
|
|
32
|
+
args:
|
|
33
|
+
title: Guide
|
|
34
|
+
body: |
|
|
35
|
+
<p>Content is reviewed literal input in YAML. Guest code never reads the
|
|
36
|
+
filesystem, so a larger site generates this project from host-prepared
|
|
37
|
+
content before rendering.</p>
|
|
38
|
+
/about:
|
|
39
|
+
description: A third page proving the template is shared, not copied
|
|
40
|
+
middleware:
|
|
41
|
+
- source: middleware/template.mjs
|
|
42
|
+
export: template
|
|
43
|
+
function:
|
|
44
|
+
source: functions/page.mjs
|
|
45
|
+
export: page
|
|
46
|
+
args:
|
|
47
|
+
title: About & "quoting"
|
|
48
|
+
body: |
|
|
49
|
+
<p>The title above is escaped by the template, so page data cannot inject
|
|
50
|
+
markup into the surrounding document.</p>
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Start `urlcode dev` with the public origin of an ngrok tunnel that is already
|
|
3
|
+
# running, so functions, redirects and absolute URLs see the address a visitor
|
|
4
|
+
# actually used rather than 127.0.0.1.
|
|
5
|
+
#
|
|
6
|
+
# Terminal 1: ngrok http 3000
|
|
7
|
+
# Terminal 2: PROJECT=. ./dev-with-ngrok.sh
|
|
8
|
+
#
|
|
9
|
+
# This script discovers a tunnel; it does not start, stop or configure ngrok.
|
|
10
|
+
# That keeps it honest about what it controls and leaves the tunnel's own
|
|
11
|
+
# authentication and access policy where it belongs: in ngrok.
|
|
12
|
+
set -eu
|
|
13
|
+
|
|
14
|
+
PROJECT=${PROJECT:-.}
|
|
15
|
+
PORT=${PORT:-3000}
|
|
16
|
+
URLCODE=${URLCODE:-urlcode}
|
|
17
|
+
# The ngrok agent's local inspection API. Overridable for a different agent port
|
|
18
|
+
# or for this repository's test, which serves a recorded response.
|
|
19
|
+
NGROK_API=${URLCODE_NGROK_API:-http://127.0.0.1:4040/api/tunnels}
|
|
20
|
+
PRINT_ONLY=0
|
|
21
|
+
|
|
22
|
+
while [ $# -gt 0 ]; do
|
|
23
|
+
case "$1" in
|
|
24
|
+
--print) PRINT_ONLY=1; shift ;;
|
|
25
|
+
--help|-h) sed -n '2,11p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
|
|
26
|
+
*) echo "dev-with-ngrok: unknown option $1" >&2; exit 2 ;;
|
|
27
|
+
esac
|
|
28
|
+
done
|
|
29
|
+
|
|
30
|
+
command -v node >/dev/null 2>&1 || { echo "dev-with-ngrok: node is required" >&2; exit 1; }
|
|
31
|
+
|
|
32
|
+
# Select the HTTPS tunnel that forwards to this port. Anything else is a
|
|
33
|
+
# different tunnel on the same agent and must not be guessed at.
|
|
34
|
+
ORIGIN=$(NGROK_API="$NGROK_API" PORT="$PORT" node -e '
|
|
35
|
+
const api = process.env.NGROK_API, port = process.env.PORT;
|
|
36
|
+
fetch(api, {headers:{accept:"application/json"}})
|
|
37
|
+
.then(r => r.ok ? r.json() : Promise.reject(new Error("HTTP " + r.status)))
|
|
38
|
+
.then(body => {
|
|
39
|
+
const tunnels = Array.isArray(body?.tunnels) ? body.tunnels : [];
|
|
40
|
+
const match = tunnels.find(t =>
|
|
41
|
+
typeof t?.public_url === "string" && t.public_url.startsWith("https://") &&
|
|
42
|
+
typeof t?.config?.addr === "string" && new RegExp(`(^|[^0-9])${port}$`).test(t.config.addr));
|
|
43
|
+
if (!match) throw new Error(`no https tunnel forwarding to port ${port}`);
|
|
44
|
+
const url = new URL(match.public_url);
|
|
45
|
+
if (url.origin !== match.public_url.replace(/\/$/,"")) throw new Error("tunnel URL is not a bare origin");
|
|
46
|
+
process.stdout.write(url.origin);
|
|
47
|
+
})
|
|
48
|
+
.catch(e => { console.error("dev-with-ngrok: " + e.message); process.exit(1); });
|
|
49
|
+
') || {
|
|
50
|
+
echo "dev-with-ngrok: could not read a tunnel from $NGROK_API" >&2
|
|
51
|
+
echo " start one first, e.g. ngrok http $PORT" >&2
|
|
52
|
+
exit 1
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
echo "dev-with-ngrok: public origin $ORIGIN"
|
|
56
|
+
if [ "$PRINT_ONLY" -eq 1 ]; then exit 0; fi
|
|
57
|
+
|
|
58
|
+
# dev loads .env.local and watches files. A tunnel makes that reachable from the
|
|
59
|
+
# internet: expose a development project only deliberately, and never tunnel the
|
|
60
|
+
# separate link-management API.
|
|
61
|
+
exec "$URLCODE" dev --project "$PROJECT" --port "$PORT" --origin "$ORIGIN"
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# URLCode on Vercel
|
|
2
|
+
|
|
3
|
+
A native-handler project deployed as a Vercel Node function. See
|
|
4
|
+
[the adapter guide](../../docs/VERCEL.md) for what is and is not supported.
|
|
5
|
+
|
|
6
|
+
```sh
|
|
7
|
+
urlcode test --project . # the same assertions run locally
|
|
8
|
+
vercel deploy
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
`vercel.json` rewrites every path to `api/index.js`, which serves the project
|
|
12
|
+
with `createVercelHandler`. `includeFiles` must list every file the project
|
|
13
|
+
reads; add to it when you add a page, download or static directory.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Served by the static handler through the Vercel adapter.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"path": "/go",
|
|
4
|
+
"status": 302,
|
|
5
|
+
"expectHeaders": {
|
|
6
|
+
"location": "https://example.com/target"
|
|
7
|
+
}
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"path": "/u/42",
|
|
11
|
+
"status": 302,
|
|
12
|
+
"expectHeaders": {
|
|
13
|
+
"location": "https://example.com/users/42"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"path": "/health",
|
|
18
|
+
"status": 200,
|
|
19
|
+
"expectBody": "{\"ok\":true}"
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"path": "/",
|
|
23
|
+
"status": 200,
|
|
24
|
+
"expectHeaders": {
|
|
25
|
+
"content-type": "text/html; charset=utf-8"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"path": "/files/notes.txt",
|
|
30
|
+
"status": 200,
|
|
31
|
+
"expectHeaders": {
|
|
32
|
+
"content-type": "text/plain; charset=utf-8"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"path": "/nothing-here",
|
|
37
|
+
"status": 404
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"path": "/u/42",
|
|
41
|
+
"method": "HEAD",
|
|
42
|
+
"status": 302,
|
|
43
|
+
"expectHeaders": {
|
|
44
|
+
"location": "https://example.com/users/42"
|
|
45
|
+
},
|
|
46
|
+
"expectBody": ""
|
|
47
|
+
}
|
|
48
|
+
]
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
version: "1"
|
|
2
|
+
routes:
|
|
3
|
+
/go:
|
|
4
|
+
redirect:
|
|
5
|
+
url: https://example.com/target
|
|
6
|
+
/u/{id}:
|
|
7
|
+
parameters:
|
|
8
|
+
- {name: id, in: path, required: true, schema: {type: string}}
|
|
9
|
+
redirect:
|
|
10
|
+
url: https://example.com/users/{id}
|
|
11
|
+
/health:
|
|
12
|
+
respond:
|
|
13
|
+
json: {ok: true}
|
|
14
|
+
/:
|
|
15
|
+
page:
|
|
16
|
+
file: public/index.html
|
|
17
|
+
/files/*:
|
|
18
|
+
static:
|
|
19
|
+
directory: public
|
package/llms.txt
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# URLCode
|
|
2
|
+
|
|
3
|
+
> Portable YAML-defined URLs that run code. Current contract: 0.1.0,
|
|
4
|
+
> stable project format version "1". Local/self-hosted first; provider adapters are future.
|
|
5
|
+
|
|
6
|
+
Use the schema and docs from the same runtime revision. Do not assume Node/fetch,
|
|
7
|
+
regex routes, database access, global middleware or arbitrary YAML interpolation.
|
|
8
|
+
The runtime is licensed under Apache-2.0. Secrets require external revision-pinned grants.
|
|
9
|
+
|
|
10
|
+
## Authoring
|
|
11
|
+
- [AI authoring contract](docs/AI-AUTHORING.md): workflow, capability matrix, checks.
|
|
12
|
+
- [YAML guide](docs/YAML-GUIDE.md): recipes for all handlers and common options.
|
|
13
|
+
- [JSON Schema](schemas/urlcode.schema.json): accepted fields and types.
|
|
14
|
+
- [Field reference](docs/YAML-REFERENCE.md): generated exhaustive field inventory.
|
|
15
|
+
- [Semantics](docs/SPECIFICATION.md): validation, defaults and sandbox API.
|
|
16
|
+
- [Runnable cookbook](examples/cookbook/README.md): 25 routes with HTTP fixtures.
|
|
17
|
+
- [Dynamic links](docs/DYNAMIC-LINKS.md): optional SQLite, live mutations, management API and limits.
|
|
18
|
+
- [Route matching](docs/ROUTING.md): precedence, non-greedy parameters, updates.
|
|
19
|
+
- [Middleware](docs/MIDDLEWARE.md): next(), state, ordering and native body limits.
|
|
20
|
+
- [Policies](docs/POLICIES.md): optional host-enforced `policies`/`profiles` keys, all off by default: `throttle`, `agents`, `security`, `compression`, `cache`; merge rules and per-target support.
|
|
21
|
+
- [Plugins](docs/PLUGINS.md): host hook API operators pass in code; never named in YAML.
|
|
22
|
+
- [TypeScript](docs/TYPESCRIPT.md): the package ships declarations for every export (`urlcode`, `@jimhoyd/urlcode/plugins`, `@jimhoyd/urlcode/policies`, `@jimhoyd/urlcode/observability`, `@jimhoyd/urlcode/compliance`, `@jimhoyd/urlcode/prerender`, `@jimhoyd/urlcode/aws`, `@jimhoyd/urlcode/vercel`, `@jimhoyd/urlcode/cloudflare`); the runtime source is TypeScript, `dist/` is its stripped JavaScript.
|
|
23
|
+
- [HTTP](docs/HTTP.md): methods, request bodies and response headers.
|
|
24
|
+
- [Assets](docs/ASSETS.md): pages, MIME, downloads, cache and ranges.
|
|
25
|
+
- [Site conventions](docs/SITE.md): optional top-level `site` key, all off by default: `robots`, `sitemap`, `favicon`, `securityTxt`, `llms` generate native routes; declared routes win; absolute URLs need `--origin`.
|
|
26
|
+
- [Prerendering](docs/PRERENDER.md): `@jimhoyd/urlcode/prerender` build helper and recipe; render function/middleware routes once into native page routes, no request-time guest code.
|
|
27
|
+
- [Organization](docs/ORGANIZATION.md): entry point and included files.
|
|
28
|
+
|
|
29
|
+
- [Best practices](docs/BEST-PRACTICES.md): layouts, readable YAML/code, testing and refactoring.
|
|
30
|
+
|
|
31
|
+
## Operations
|
|
32
|
+
- [Security](docs/FUNCTION-SECURITY.md): untrusted code and external binding policy.
|
|
33
|
+
- [Readiness](docs/READINESS.md): fixture coverage and count checks.
|
|
34
|
+
- [Capacity](docs/CAPACITY.md): concurrency, limits and theoretical sizing.
|
|
35
|
+
- [Resilience](docs/RESILIENCE.md): DDoS, overload, incident and recovery plans.
|
|
36
|
+
- [Operations](docs/OPERATIONS.md): deploy, observe, rotate and roll back.
|
|
37
|
+
- [Performance](docs/PERFORMANCE.md): measured results and their limitations.
|
|
38
|
+
- [Roadmap](ROADMAP.md): clearly separates implemented and planned features.
|
|
39
|
+
|
|
40
|
+
- [Release readiness](docs/RELEASE-READINESS.md): verified safeguards, open gates and supported deployment scope.
|
|
41
|
+
|
|
42
|
+
- [Scaffolding](docs/SCAFFOLDING.md): generate missing placeholders from YAML, preserve existing files, fail-closed code stubs.
|
|
43
|
+
|
|
44
|
+
Security hardening: docs/MANAGEMENT-SECURITY.md, docs/SANDBOX-REVIEW.md,
|
|
45
|
+
docs/OPERATIONAL-PROOF.md and docs/RELEASE-SECURITY.md describe private operator
|
|
46
|
+
management, resource limits, acceptance evidence and signed alpha candidates.
|
package/package.json
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@jimhoyd/urlcode",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Portable runtime for programmable URL behavior",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "Apache-2.0",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=22.13.0"
|
|
9
|
+
},
|
|
10
|
+
"devEngines": {
|
|
11
|
+
"runtime": {
|
|
12
|
+
"name": "node",
|
|
13
|
+
"version": ">=22.18.0",
|
|
14
|
+
"onFail": "error"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"bin": {
|
|
18
|
+
"urlcode": "./dist/cli.js"
|
|
19
|
+
},
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"types": "./dist/types/index.d.ts",
|
|
23
|
+
"development": "./src/index.ts",
|
|
24
|
+
"default": "./dist/index.js"
|
|
25
|
+
},
|
|
26
|
+
"./aws": {
|
|
27
|
+
"types": "./dist/types/aws.d.ts",
|
|
28
|
+
"development": "./src/aws.ts",
|
|
29
|
+
"default": "./dist/aws.js"
|
|
30
|
+
},
|
|
31
|
+
"./cloudflare": {
|
|
32
|
+
"types": "./dist/types/cloudflare.d.ts",
|
|
33
|
+
"development": "./src/cloudflare.ts",
|
|
34
|
+
"default": "./dist/cloudflare.js"
|
|
35
|
+
},
|
|
36
|
+
"./prerender": {
|
|
37
|
+
"types": "./dist/types/prerender.d.ts",
|
|
38
|
+
"development": "./src/prerender.ts",
|
|
39
|
+
"default": "./dist/prerender.js"
|
|
40
|
+
},
|
|
41
|
+
"./schema": "./schemas/urlcode.schema.json",
|
|
42
|
+
"./vercel": {
|
|
43
|
+
"types": "./dist/types/vercel.d.ts",
|
|
44
|
+
"development": "./src/vercel.ts",
|
|
45
|
+
"default": "./dist/vercel.js"
|
|
46
|
+
},
|
|
47
|
+
"./plugins": {
|
|
48
|
+
"types": "./dist/types/plugins.d.ts",
|
|
49
|
+
"development": "./src/plugins.ts",
|
|
50
|
+
"default": "./dist/plugins.js"
|
|
51
|
+
},
|
|
52
|
+
"./policies": {
|
|
53
|
+
"types": "./dist/types/policies.d.ts",
|
|
54
|
+
"development": "./src/policies.ts",
|
|
55
|
+
"default": "./dist/policies.js"
|
|
56
|
+
},
|
|
57
|
+
"./compliance": {
|
|
58
|
+
"types": "./dist/types/compliance.d.ts",
|
|
59
|
+
"development": "./src/compliance.ts",
|
|
60
|
+
"default": "./dist/compliance.js"
|
|
61
|
+
},
|
|
62
|
+
"./observability": {
|
|
63
|
+
"types": "./dist/types/observability.d.ts",
|
|
64
|
+
"development": "./src/observability.ts",
|
|
65
|
+
"default": "./dist/observability.js"
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
"files": [
|
|
69
|
+
"dist",
|
|
70
|
+
"schemas",
|
|
71
|
+
"data",
|
|
72
|
+
"starters",
|
|
73
|
+
"README.md",
|
|
74
|
+
"docs",
|
|
75
|
+
"SECURITY.md",
|
|
76
|
+
"ROADMAP.md",
|
|
77
|
+
"CONTRIBUTING.md",
|
|
78
|
+
"examples",
|
|
79
|
+
"llms.txt",
|
|
80
|
+
"NOTICE"
|
|
81
|
+
],
|
|
82
|
+
"scripts": {
|
|
83
|
+
"build": "node --disable-warning=ExperimentalWarning scripts/build.ts",
|
|
84
|
+
"typecheck": "tsc -p tsconfig.json",
|
|
85
|
+
"test": "node --conditions=development --test test/*.test.ts",
|
|
86
|
+
"lint": "eslint .",
|
|
87
|
+
"check": "node scripts/check.ts && node scripts/generate-yaml-reference.ts --check",
|
|
88
|
+
"verify": "npm run lint && npm run typecheck && npm run check && npm run build && npm test",
|
|
89
|
+
"benchmark": "node benchmarks/routing.ts",
|
|
90
|
+
"test:package": "npm run build && node scripts/package-smoke.ts",
|
|
91
|
+
"dev": "node src/cli.ts dev --project starters/default",
|
|
92
|
+
"serve": "node src/cli.ts serve --project starters/default",
|
|
93
|
+
"validate": "node src/cli.ts validate --local --project starters/default",
|
|
94
|
+
"test:project": "node src/cli.ts test --project starters/default",
|
|
95
|
+
"init": "node src/cli.ts init",
|
|
96
|
+
"doctor": "node src/cli.ts doctor",
|
|
97
|
+
"routes": "node src/cli.ts routes --project starters/default",
|
|
98
|
+
"audit:routes": "node src/cli.ts audit --project starters/default",
|
|
99
|
+
"benchmark:project": "node src/cli.ts benchmark --project starters/default",
|
|
100
|
+
"docs:reference": "node scripts/generate-yaml-reference.ts"
|
|
101
|
+
},
|
|
102
|
+
"repository": {
|
|
103
|
+
"type": "git",
|
|
104
|
+
"url": "https://github.com/jimhoyd-com/urlcode.git"
|
|
105
|
+
},
|
|
106
|
+
"dependencies": {
|
|
107
|
+
"ajv": "8.20.0",
|
|
108
|
+
"content-disposition": "3.0.0",
|
|
109
|
+
"es-module-lexer": "3.0.2",
|
|
110
|
+
"mime-types": "3.0.2",
|
|
111
|
+
"quickjs-emscripten": "0.32.0",
|
|
112
|
+
"yaml": "2.9.1"
|
|
113
|
+
},
|
|
114
|
+
"devDependencies": {
|
|
115
|
+
"@eslint/js": "10.0.1",
|
|
116
|
+
"@types/mime-types": "3.0.1",
|
|
117
|
+
"@types/node": "22.20.3",
|
|
118
|
+
"eslint": "10.10.0",
|
|
119
|
+
"globals": "17.12.0",
|
|
120
|
+
"typescript": "5.9.3",
|
|
121
|
+
"typescript-eslint": "8.70.0"
|
|
122
|
+
}
|
|
123
|
+
}
|