@jimhoyd/urlcode 0.3.0 → 0.4.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/urlcode-authoring/SKILL.md +106 -0
- package/.claude/skills/urlcode-operations/SKILL.md +114 -0
- package/.claude-plugin/marketplace.json +18 -0
- package/CONTRIBUTING.md +30 -2
- package/README.md +157 -230
- package/ROADMAP.md +85 -2
- package/SECURITY.md +8 -1
- package/dist/BUILD-MANIFEST.json +62 -32
- package/dist/adapters.js +4 -23
- package/dist/agent-lists.js +1 -1
- package/dist/agents-guide.js +113 -0
- package/dist/authoring-files.js +60 -0
- package/dist/authoring.js +11 -1
- package/dist/aws.js +4 -3
- package/dist/build-cloudflare.js +11 -24
- package/dist/bulk.js +37 -0
- package/dist/capabilities.js +208 -0
- package/dist/capability-query.js +72 -0
- package/dist/catalog.js +105 -0
- package/dist/cli.js +154 -15
- package/dist/client-address.js +1 -1
- package/dist/compliance-rules/baseline.js +8 -8
- package/dist/compliance-rules/privacy.js +3 -3
- package/dist/compliance-rules/strict.js +5 -5
- package/dist/conditions.js +88 -0
- package/dist/config.js +60 -4
- package/dist/context.js +156 -0
- package/dist/ecosystem-cli.js +88 -0
- package/dist/egress.js +98 -0
- package/dist/examples.js +92 -0
- package/dist/explain-cli.js +64 -0
- package/dist/explain.js +132 -0
- package/dist/extensions.js +172 -0
- package/dist/function-sources.js +15 -3
- package/dist/index.js +37 -0
- package/dist/init-with.js +165 -0
- package/dist/interchange-cli.js +42 -0
- package/dist/interchange.js +189 -0
- package/dist/link-cli.js +1 -1
- package/dist/management-policy.js +0 -1
- package/dist/manifest.js +111 -0
- package/dist/match.js +2 -2
- package/dist/mcp-authoring.js +147 -0
- package/dist/mcp.js +97 -0
- package/dist/observability.js +6 -0
- package/dist/operator-host.js +29 -0
- package/dist/plugins.js +12 -0
- package/dist/policies/agents.js +2 -2
- package/dist/policies/compression.js +2 -1
- package/dist/policies/security.js +0 -0
- package/dist/policies.js +1 -1
- package/dist/policy.js +29 -7
- package/dist/prerender.js +100 -41
- package/dist/project-tests.js +3 -3
- package/dist/provider-verification.js +92 -0
- package/dist/proxy.js +44 -0
- package/dist/readiness.js +5 -5
- package/dist/recipes.js +41 -0
- package/dist/route-diff.js +106 -0
- package/dist/router.js +42 -2
- package/dist/runtime.js +95 -14
- package/dist/schema-query.js +62 -0
- package/dist/signals.js +24 -0
- package/dist/site.js +0 -0
- package/dist/tooling.js +96 -0
- package/dist/types/adapters.d.ts +7 -4
- package/dist/types/agent-lists.d.ts +0 -1
- package/dist/types/agents-guide.d.ts +17 -0
- package/dist/types/authoring-files.d.ts +10 -0
- package/dist/types/aws.d.ts +3 -1
- package/dist/types/build-cloudflare.d.ts +1 -0
- package/dist/types/bulk.d.ts +27 -0
- package/dist/types/capabilities.d.ts +55 -0
- package/dist/types/capability-query.d.ts +24 -0
- package/dist/types/catalog.d.ts +65 -0
- package/dist/types/client-address.d.ts +0 -1
- package/dist/types/compliance-rules/baseline.d.ts +1 -9
- package/dist/types/compliance-rules/privacy.d.ts +1 -4
- package/dist/types/compliance-rules/strict.d.ts +0 -5
- package/dist/types/conditions.d.ts +19 -0
- package/dist/types/config.d.ts +20 -2
- package/dist/types/context.d.ts +65 -0
- package/dist/types/ecosystem-cli.d.ts +17 -0
- package/dist/types/egress.d.ts +46 -0
- package/dist/types/examples.d.ts +50 -0
- package/dist/types/explain-cli.d.ts +11 -0
- package/dist/types/explain.d.ts +95 -0
- package/dist/types/extensions.d.ts +122 -0
- package/dist/types/function-sources.d.ts +5 -0
- package/dist/types/index.d.ts +33 -0
- package/dist/types/init-with.d.ts +30 -0
- package/dist/types/interchange-cli.d.ts +16 -0
- package/dist/types/interchange.d.ts +42 -0
- package/dist/types/link-cli.d.ts +2 -1
- package/dist/types/management-policy.d.ts +0 -1
- package/dist/types/manifest.d.ts +81 -0
- package/dist/types/match.d.ts +1 -0
- package/dist/types/mcp-authoring.d.ts +92 -0
- package/dist/types/mcp.d.ts +12 -0
- package/dist/types/observability.d.ts +2 -0
- package/dist/types/operator-host.d.ts +8 -0
- package/dist/types/plugins.d.ts +2 -0
- package/dist/types/policies/agents.d.ts +0 -2
- package/dist/types/policies/compression.d.ts +2 -0
- package/dist/types/policies/security.d.ts +0 -1
- package/dist/types/policy.d.ts +15 -4
- package/dist/types/project-tests.d.ts +3 -1
- package/dist/types/provider-verification.d.ts +53 -0
- package/dist/types/proxy.d.ts +21 -0
- package/dist/types/readiness.d.ts +1 -1
- package/dist/types/recipes.d.ts +30 -0
- package/dist/types/route-diff.d.ts +27 -0
- package/dist/types/runtime.d.ts +11 -0
- package/dist/types/schema-query.d.ts +12 -0
- package/dist/types/signals.d.ts +25 -0
- package/dist/types/site.d.ts +0 -1
- package/dist/types/tooling.d.ts +115 -0
- package/dist/types/types.d.ts +57 -0
- package/dist/types/typescript-authoring.d.ts +12 -0
- package/dist/types/vercel.d.ts +3 -1
- package/dist/types/verify-deployment.d.ts +47 -0
- package/dist/types.js +21 -2
- package/dist/typescript-authoring.js +104 -0
- package/dist/vercel.js +4 -3
- package/dist/verify-deployment.js +270 -0
- package/docs/AI-AUTHORING.md +130 -8
- package/docs/BULK.md +79 -0
- package/docs/CAPABILITIES.md +179 -0
- package/docs/CAPACITY.md +1 -1
- package/docs/CI.md +142 -0
- package/docs/CONDITIONS.md +74 -0
- package/docs/DEPLOYMENT-CHECKS.md +108 -0
- package/docs/DYNAMIC-LINKS.md +18 -518
- package/docs/EGRESS.md +125 -0
- package/docs/EXTENSIONS.md +226 -0
- package/docs/FRAMEWORK.md +182 -0
- package/docs/INSTALL.md +45 -7
- package/docs/INTERCHANGE.md +134 -0
- package/docs/MIDDLEWARE-EXAMPLES.md +75 -0
- package/docs/MIDDLEWARE.md +2 -0
- package/docs/NEXT-PHASE-PLAN.md +90 -0
- package/docs/NEXT-STEPS.md +415 -0
- package/docs/OBSERVABILITY.md +4 -2
- package/docs/OPERATIONAL-PROOF.md +4 -1
- package/docs/OPERATIONS.md +6 -3
- package/docs/PLUGINS.md +37 -0
- package/docs/POLICIES.md +12 -309
- package/docs/PRERENDER.md +40 -0
- package/docs/PROJECT-DIRECTION.md +42 -0
- package/docs/PROVIDER-VERIFICATION.md +84 -0
- package/docs/READINESS.md +21 -1
- package/docs/README.md +82 -31
- package/docs/RECIPES.md +99 -0
- package/docs/RELEASE-READINESS.md +11 -9
- package/docs/RELEASE-SECURITY.md +27 -4
- package/docs/SECURITY-AUDIT.md +1 -1
- package/docs/SPECIFICATION.md +95 -8
- package/docs/SPIKE-BUSINESS-SUITE.md +1013 -0
- package/docs/SPIKE-EXTENSION-MODEL.md +419 -0
- package/docs/SPIKE-EXTENSIONS.md +1 -0
- package/docs/SPIKE-LAMBDA-COMPILE.md +199 -0
- package/docs/STANDARDS.md +150 -142
- package/docs/STARTERS.md +21 -1
- package/docs/TOOLING.md +291 -0
- package/docs/TYPESCRIPT-AUTHORING.md +67 -0
- package/docs/TYPESCRIPT.md +1 -1
- package/docs/USABILITY-REVIEW.md +123 -0
- package/docs/YAML-GUIDE.md +18 -479
- package/docs/YAML-REFERENCE.md +127 -16
- package/docs/links/cli.md +110 -0
- package/docs/links/limits.md +175 -0
- package/docs/links/management-api.md +80 -0
- package/docs/links/pools.md +75 -0
- package/docs/links/setup.md +135 -0
- package/docs/policies/agents.md +1 -1
- package/docs/policies/contract.md +52 -0
- package/docs/policies/hardened.md +56 -0
- package/docs/policies/interoperability.md +169 -0
- package/docs/policies/operations.md +45 -0
- package/docs/yaml/assets.md +36 -0
- package/docs/yaml/conditions.md +20 -0
- package/docs/yaml/functions.md +160 -0
- package/docs/yaml/links.md +30 -0
- package/docs/yaml/middleware.md +29 -0
- package/docs/yaml/organization.md +74 -0
- package/docs/yaml/policies.md +37 -0
- package/docs/yaml/redirects.md +64 -0
- package/docs/yaml/responses.md +57 -0
- package/docs/yaml/site.md +24 -0
- package/examples/assets/example.yaml +17 -0
- package/examples/aws/example.yaml +20 -0
- package/examples/cloudflare/example.yaml +19 -0
- package/examples/compliance/example.yaml +11 -0
- package/examples/conditions/README.md +12 -0
- package/examples/conditions/example.yaml +19 -0
- package/examples/conditions/tests/requests.json +13 -0
- package/examples/conditions/urlcode.yaml +24 -0
- package/examples/cookbook/README.md +8 -4
- package/examples/cookbook/example.yaml +17 -0
- package/examples/cookbook/functions/catalog.mjs +3 -0
- package/examples/cookbook/functions/fail.mjs +4 -0
- package/examples/cookbook/functions/items.mjs +3 -0
- package/examples/cookbook/functions/profile.mjs +3 -0
- package/examples/cookbook/functions/resource.mjs +3 -0
- package/examples/cookbook/functions/status.mjs +3 -0
- package/examples/cookbook/middleware/auth.mjs +48 -0
- package/examples/cookbook/middleware/body.mjs +15 -0
- package/examples/cookbook/middleware/bucket.mjs +19 -0
- package/examples/cookbook/middleware/cors.mjs +21 -0
- package/examples/cookbook/middleware/debug.mjs +13 -0
- package/examples/cookbook/middleware/envelope.mjs +11 -0
- package/examples/cookbook/middleware/errors.mjs +11 -0
- package/examples/cookbook/middleware/etag.mjs +18 -0
- package/examples/cookbook/middleware/locale.mjs +16 -0
- package/examples/cookbook/middleware/maintenance.mjs +10 -0
- package/examples/cookbook/middleware/methods.mjs +15 -0
- package/examples/cookbook/middleware/negotiate.mjs +20 -0
- package/examples/cookbook/middleware/referer.mjs +12 -0
- package/examples/cookbook/middleware/request-id.mjs +16 -0
- package/examples/cookbook/route-index.json +676 -0
- package/examples/cookbook/routes/middleware.yaml +126 -0
- package/examples/cookbook/tests/requests.json +526 -0
- package/examples/cookbook/urlcode.yaml +1 -0
- package/examples/egress/README.md +22 -0
- package/examples/egress/example.yaml +19 -0
- package/examples/egress/urlcode.yaml +19 -0
- package/examples/extensions/README.md +7 -0
- package/examples/extensions/example.yaml +21 -0
- package/examples/extensions/urlcode.yaml +25 -0
- package/examples/live-links/example.yaml +21 -0
- package/examples/monitoring/example.yaml +8 -0
- package/examples/prerender/example.yaml +16 -0
- package/examples/provider-conformance/README.md +12 -0
- package/examples/provider-conformance/example.yaml +14 -0
- package/examples/provider-conformance/urlcode.yaml +34 -0
- package/examples/tunnel/example.yaml +8 -0
- package/examples/vercel/example.yaml +19 -0
- package/llms-full.txt +2709 -0
- package/llms.txt +48 -19
- package/package.json +29 -7
- package/packaging/claude-plugin/.claude-plugin/plugin.json +19 -0
- package/packaging/claude-plugin/skills/urlcode-authoring/SKILL.md +106 -0
- package/packaging/claude-plugin/skills/urlcode-operations/SKILL.md +114 -0
- package/recipes/authenticated-json-api/README.md +51 -0
- package/recipes/authenticated-json-api/functions/profile.mjs +5 -0
- package/recipes/authenticated-json-api/recipe.yaml +34 -0
- package/recipes/authenticated-json-api/tests/requests.json +39 -0
- package/recipes/authenticated-json-api/urlcode.yaml +12 -0
- package/recipes/contact-form/README.md +25 -0
- package/recipes/contact-form/functions/contact.mjs +17 -0
- package/recipes/contact-form/recipe.yaml +33 -0
- package/recipes/contact-form/tests/requests.json +47 -0
- package/recipes/contact-form/urlcode.yaml +18 -0
- package/recipes/cors-api/README.md +16 -0
- package/recipes/cors-api/functions/items.mjs +3 -0
- package/recipes/cors-api/middleware/cors.mjs +21 -0
- package/recipes/cors-api/recipe.yaml +26 -0
- package/recipes/cors-api/tests/requests.json +65 -0
- package/recipes/cors-api/urlcode.yaml +12 -0
- package/recipes/health-page/README.md +13 -0
- package/recipes/health-page/recipe.yaml +23 -0
- package/recipes/health-page/tests/requests.json +36 -0
- package/recipes/health-page/urlcode.yaml +19 -0
- package/recipes/json-api/README.md +6 -0
- package/recipes/json-api/functions/echo.mjs +3 -0
- package/recipes/json-api/recipe.yaml +25 -0
- package/recipes/json-api/tests/requests.json +34 -0
- package/recipes/json-api/urlcode.yaml +12 -0
- package/recipes/middleware/README.md +34 -0
- package/recipes/middleware/functions/catalog.mjs +3 -0
- package/recipes/middleware/functions/fail.mjs +4 -0
- package/recipes/middleware/functions/items.mjs +3 -0
- package/recipes/middleware/functions/profile.mjs +3 -0
- package/recipes/middleware/functions/resource.mjs +3 -0
- package/recipes/middleware/functions/status.mjs +3 -0
- package/recipes/middleware/middleware/auth.mjs +48 -0
- package/recipes/middleware/middleware/body.mjs +15 -0
- package/recipes/middleware/middleware/bucket.mjs +19 -0
- package/recipes/middleware/middleware/cors.mjs +21 -0
- package/recipes/middleware/middleware/debug.mjs +13 -0
- package/recipes/middleware/middleware/envelope.mjs +11 -0
- package/recipes/middleware/middleware/errors.mjs +11 -0
- package/recipes/middleware/middleware/etag.mjs +18 -0
- package/recipes/middleware/middleware/locale.mjs +16 -0
- package/recipes/middleware/middleware/maintenance.mjs +10 -0
- package/recipes/middleware/middleware/methods.mjs +15 -0
- package/recipes/middleware/middleware/negotiate.mjs +20 -0
- package/recipes/middleware/middleware/referer.mjs +12 -0
- package/recipes/middleware/middleware/request-id.mjs +16 -0
- package/recipes/middleware/public/guide.txt +1 -0
- package/recipes/middleware/recipe.yaml +50 -0
- package/recipes/middleware/tests/requests.json +528 -0
- package/recipes/middleware/urlcode.yaml +127 -0
- package/recipes/protected-download/README.md +22 -0
- package/recipes/protected-download/files/report.txt +1 -0
- package/recipes/protected-download/recipe.yaml +31 -0
- package/recipes/protected-download/tests/requests.json +32 -0
- package/recipes/protected-download/urlcode.yaml +15 -0
- package/recipes/redirect/README.md +7 -0
- package/recipes/redirect/recipe.yaml +25 -0
- package/recipes/redirect/tests/requests.json +19 -0
- package/recipes/redirect/urlcode.yaml +9 -0
- package/recipes/static-plus-api/README.md +15 -0
- package/recipes/static-plus-api/functions/info.mjs +3 -0
- package/recipes/static-plus-api/public/assets/index.html +3 -0
- package/recipes/static-plus-api/public/assets/site.css +1 -0
- package/recipes/static-plus-api/public/index.html +8 -0
- package/recipes/static-plus-api/recipe.yaml +29 -0
- package/recipes/static-plus-api/tests/requests.json +56 -0
- package/recipes/static-plus-api/urlcode.yaml +23 -0
- package/recipes/typescript/README.md +7 -0
- package/recipes/typescript/functions/hello.ts +5 -0
- package/recipes/typescript/recipe.yaml +23 -0
- package/recipes/typescript/tests/requests.json +18 -0
- package/recipes/typescript/urlcode.yaml +5 -0
- package/recipes/webhook-receiver/README.md +16 -0
- package/recipes/webhook-receiver/functions/receive.mjs +16 -0
- package/recipes/webhook-receiver/recipe.yaml +26 -0
- package/recipes/webhook-receiver/tests/requests.json +59 -0
- package/recipes/webhook-receiver/urlcode.yaml +16 -0
- package/schemas/recipe.schema.json +138 -0
- package/schemas/urlcode.schema.json +656 -80
- package/skills/urlcode/SKILL.md +98 -0
- package/starters/default/.github/workflows/urlcode.yml +23 -0
- package/starters/default/.mcp.json +12 -0
- package/starters/default/AGENTS.md +79 -0
package/docs/DYNAMIC-LINKS.md
CHANGED
|
@@ -1,561 +1,61 @@
|
|
|
1
1
|
# Dynamic short links without reloads
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
or newer, 3.50.7 or 3.44.6. Node bundles SQLite, so this is a property of the
|
|
7
|
-
build, not something you can install separately, and some current releases on a
|
|
8
|
-
supported Node line ship an unpatched version. Run `urlcode doctor` and check
|
|
9
|
-
`liveLinks`; activation fails closed and names the detected version when the
|
|
10
|
-
build is unsuitable. Everything except live links works on any supported Node.
|
|
3
|
+
Define a stable route once and create, update and delete short-code records
|
|
4
|
+
while `serve` keeps running, from optional SQLite storage on one host. Each
|
|
5
|
+
section below lives on its own page; the headings here keep existing links working.
|
|
11
6
|
|
|
12
|
-
|
|
13
|
-
short-code records while `serve` keeps running. No YAML rewrite, route snapshot
|
|
14
|
-
rebuild, Git commit or public-server restart is needed for record changes.
|
|
7
|
+
## Node build requirement
|
|
15
8
|
|
|
16
|
-
|
|
17
|
-
on that host can share it. No database is required for ordinary YAML-defined
|
|
18
|
-
routes. Network filesystems, multi-host replication and serverless ephemeral disks
|
|
19
|
-
are not supported by this adapter. SQLite WAL requires processes on the same
|
|
20
|
-
host; see [SQLite WAL](https://www.sqlite.org/wal.html).
|
|
9
|
+
SQLite 3.51.3+ (or 3.50.7 / 3.44.6) bundled with Node; `urlcode doctor` reports it. Read [setup](links/setup.md#node-build-requirement).
|
|
21
10
|
|
|
22
11
|
## Behavior in YAML, data outside Git
|
|
23
12
|
|
|
24
|
-
|
|
25
|
-
version: "1"
|
|
26
|
-
dynamicLinks: true
|
|
27
|
-
routes:
|
|
28
|
-
/r/{code}:
|
|
29
|
-
parameters:
|
|
30
|
-
- name: code
|
|
31
|
-
in: path
|
|
32
|
-
required: true
|
|
33
|
-
schema: {type: string, minLength: 1, maxLength: 128}
|
|
34
|
-
link:
|
|
35
|
-
collection: links
|
|
36
|
-
code: {from: path, name: code}
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
`link` is a seventh primary handler, exclusive with `function`, `redirect`,
|
|
40
|
-
`respond`, `page`, `static` and `download`. Its `code` is a declared path-input
|
|
41
|
-
reference. `collection` is a logical name, not a filesystem path. GET and HEAD
|
|
42
|
-
are supported; normal route precedence, lifecycle and input validation apply.
|
|
43
|
-
An exact YAML route can intentionally shadow a stored code's URL.
|
|
44
|
-
|
|
45
|
-
The operator binds `links` to a database outside the project. YAML cannot choose
|
|
46
|
-
a database file or grant itself storage. The built-in public binding opens it
|
|
47
|
-
read-only. It performs an indexed database lookup for each request, with no
|
|
48
|
-
application cache or negative cache. Requests starting after an acknowledged
|
|
49
|
-
mutation can see it immediately; an already-running request can finish with an
|
|
50
|
-
older result. Browser/proxy caches remain outside that guarantee.
|
|
51
|
-
|
|
52
|
-
Missing or disabled record: 404. Expired record: 410. Unavailable/overloaded
|
|
53
|
-
store: 503. Invalid code syntax: 404 after route input validation. Redirects
|
|
54
|
-
default to 302 and no-store. Query strings are not forwarded. A record's URL is
|
|
55
|
-
literal HTTP(S), with no embedded credentials, control characters or interpolation.
|
|
56
|
-
It is a redirect, not a server-side fetch. The store does not verify destination
|
|
57
|
-
ownership, safety or availability; application abuse prevention remains necessary.
|
|
13
|
+
The `link` handler and the operator store binding. Read [setup](links/setup.md#behavior-in-yaml-data-outside-git).
|
|
58
14
|
|
|
59
15
|
## Run the included example
|
|
60
16
|
|
|
61
|
-
|
|
62
|
-
Its parent directory must already exist. The following POSIX-shell example uses
|
|
63
|
-
an operator-created directory; on Windows use an absolute local drive path.
|
|
64
|
-
|
|
65
|
-
```sh
|
|
66
|
-
mkdir -p ../urlcode-data
|
|
67
|
-
URLCODE_DATA="$(cd ../urlcode-data && pwd)"
|
|
68
|
-
node src/cli.ts links init --store "$URLCODE_DATA/links.sqlite"
|
|
69
|
-
node src/cli.ts links create --store "$URLCODE_DATA/links.sqlite" \
|
|
70
|
-
--code demo --destination https://example.com/demo
|
|
71
|
-
node src/cli.ts serve --project examples/live-links \
|
|
72
|
-
--link-store "links=$URLCODE_DATA/links.sqlite" --port 3000
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
Open `/r/demo`. In a second terminal, run another `links create` with an unused
|
|
76
|
-
code against the same absolute file; its URL works immediately. Omit `--code`
|
|
77
|
-
to generate a cryptographically random 16-character base64url code. Codes allow
|
|
78
|
-
1–128 letters, digits, underscores and hyphens; collection names begin with a
|
|
79
|
-
letter and contain at most 64 letters/digits/underscores/hyphens.
|
|
80
|
-
|
|
81
|
-
The CLI defaults to collection `links`; specify `--collection` to manage another.
|
|
82
|
-
Serving currently accepts one `--link-store collection=/absolute/file` binding;
|
|
83
|
-
embedded applications may supply multiple adapters. Commands do not copy records
|
|
84
|
-
into YAML or create route definitions automatically.
|
|
17
|
+
`links init`, `links create` and `serve --link-store` against a private data directory. Read [setup](links/setup.md#run-the-included-example).
|
|
85
18
|
|
|
86
19
|
## Update, disable, expire, list and delete
|
|
87
20
|
|
|
88
|
-
|
|
89
|
-
urlcode links get --store /absolute/links.sqlite --code demo
|
|
90
|
-
urlcode links list --store /absolute/links.sqlite --limit 100
|
|
91
|
-
urlcode links list --store /absolute/links.sqlite --limit 100 --after last-code
|
|
92
|
-
urlcode links update --store /absolute/links.sqlite --code demo \
|
|
93
|
-
--if-version 1 --destination https://example.com/new --enabled false
|
|
94
|
-
urlcode links delete --store /absolute/links.sqlite --code demo --if-version 2
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
Replace illustrative versions with the actual returned `version`. Update is a
|
|
98
|
-
**full replacement**: omitted status becomes 302, enabled becomes true, and expiry
|
|
99
|
-
becomes null. Use `--status 307` or `--expires 2030-01-01T00:00:00Z` as needed.
|
|
100
|
-
Allowed status codes match ordinary redirects. Disabled/expired records remain
|
|
101
|
-
stored and reserve the code until explicitly deleted.
|
|
102
|
-
|
|
103
|
-
Create is insert-only: an existing code returns conflict, never an overwrite.
|
|
104
|
-
Updates/deletes require a matching positive version. Every mutation uses a
|
|
105
|
-
transaction; revisions advance globally within that database, including deletion,
|
|
106
|
-
so a stale edit cannot silently affect a deleted-and-recreated code. Competing
|
|
107
|
-
writers receive a conflict. Read the latest record before making a new decision;
|
|
108
|
-
do not blindly retry a stale update. Listing is ordered by code, at most 100 per
|
|
109
|
-
page, within one collection. Pagination is not a snapshot across concurrent writes.
|
|
110
|
-
|
|
111
|
-
CLI commands print record data intentionally; treat output as operational data.
|
|
112
|
-
Do not store secrets in destinations or capture output into public logs.
|
|
113
|
-
`urlcode add` remains the separate command for adding a Git/YAML-defined redirect.
|
|
21
|
+
`links get/list/update/delete` and the optimistic version rules. Read [cli](links/cli.md#update-disable-expire-list-and-delete).
|
|
114
22
|
|
|
115
23
|
## Consistent operator export and restore
|
|
116
24
|
|
|
117
|
-
|
|
118
|
-
between pages can produce a logically inconsistent copy. `links export` instead
|
|
119
|
-
holds one SQLite read transaction for the whole export, so every record it writes
|
|
120
|
-
comes from a single point in time.
|
|
121
|
-
|
|
122
|
-
```sh
|
|
123
|
-
umask 077
|
|
124
|
-
urlcode links export --store /absolute/links.sqlite > /absolute/backups/links-export.ndjson
|
|
125
|
-
urlcode links import --store /absolute/restored.sqlite --input /absolute/backups/links-export.ndjson
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
Export is an operator command on the operator's own database. It is not reachable
|
|
129
|
-
from the public redirect server, from route YAML, from guest function code or from
|
|
130
|
-
the management HTTP API, and it grants guest code no storage capability. The
|
|
131
|
-
output is operational data: write it somewhere only operators can read, keep it out
|
|
132
|
-
of the project, Git and build artifacts, and treat it like the database itself.
|
|
133
|
-
|
|
134
|
-
**Consistency contract.** The export reflects the database exactly as of the moment
|
|
135
|
-
the snapshot is pinned, which is the first read after the transaction opens. Writers
|
|
136
|
-
are never blocked and keep committing; none of their later commits appear in the
|
|
137
|
-
export, and no record appears twice or is skipped. The header line carries
|
|
138
|
-
`format`, `schemaVersion`, `applicationId`, the store `revision` at that instant and
|
|
139
|
-
`generatedAt`, so a restored copy can be identified and ordered against others. This
|
|
140
|
-
is a consistent logical copy, not a point-in-time recovery system: it has no
|
|
141
|
-
continuous log and cannot reconstruct a moment between two exports.
|
|
142
|
-
|
|
143
|
-
**Contents.** Every record in the store, or in one `--collection`, including
|
|
144
|
-
enabled, disabled and expired records, with `collection`, `code`, `url`, `status`,
|
|
145
|
-
`enabled`, `expires` and `version`. Disabled and expired records are exported
|
|
146
|
-
because they still reserve their codes. The output is NDJSON: a
|
|
147
|
-
`link-export-begin` header line, one `{"record": {...}}` line per record ordered by
|
|
148
|
-
collection and code, and a `link-export-complete` line carrying the record count
|
|
149
|
-
and a SHA-256 digest over every preceding line. A stream without that final line is
|
|
150
|
-
truncated, and `links import` rejects it.
|
|
151
|
-
|
|
152
|
-
**Restore semantics.** `links import` replays the records into the target store and
|
|
153
|
-
refuses to touch a collection that already holds records, so a restore never
|
|
154
|
-
overwrites live data. It verifies the format, schema version, store identity, record
|
|
155
|
-
count and digest before it commits the last record. Codes, destinations, statuses,
|
|
156
|
-
enabled flags and expiries are restored exactly. **Versions are not.** The target
|
|
157
|
-
assigns its own revisions, which is why the report sets `versionsReassigned: true`:
|
|
158
|
-
management ETags taken against the exported database are stale after a restore, so
|
|
159
|
-
discard them and re-read records before the next conditional write, exactly as after
|
|
160
|
-
restoring an older database file.
|
|
161
|
-
|
|
162
|
-
**Audit journal.** The export carries records only. The mutation audit journal stays
|
|
163
|
-
in the source database and is not part of an export, so a store restored from one
|
|
164
|
-
starts a fresh journal covering only mutations made after the restore. Keep the
|
|
165
|
-
journal by backing up the database file itself, as described under *Persistence,
|
|
166
|
-
bounds and recovery*; that file backup, not the export, is the archival copy of who
|
|
167
|
-
changed what. See [management security](MANAGEMENT-SECURITY.md) for retention.
|
|
168
|
-
|
|
169
|
-
**Bounds.** One export runs at a time per store and a second is rejected with 409.
|
|
170
|
-
It pins exactly one reader connection and holds that reader's admission for its whole
|
|
171
|
-
life, so it can never exceed the pool's read budget or starve redirects of every
|
|
172
|
-
reader — run it against a management store or size `--link-readers` accordingly.
|
|
173
|
-
Pages are at most 100 records (`--page-size`), each page carries the usual
|
|
174
|
-
five-second operation deadline, and the export as a whole has a 60-second default
|
|
175
|
-
deadline after which it fails and releases the reader. A consumer that fails or a
|
|
176
|
-
process that stops ends the read transaction rather than leaving it open. Because
|
|
177
|
-
the transaction pins a WAL read mark, a long export delays WAL checkpointing: keep
|
|
178
|
-
exports short and do not leave one running against a busy store.
|
|
179
|
-
|
|
180
|
-
Embedders call the same mechanism directly:
|
|
181
|
-
|
|
182
|
-
```js
|
|
183
|
-
await store.exportSnapshot({collection: 'links', pageSize: 100, deadlineMs: 60000}, {
|
|
184
|
-
onHeader: header => sink.write(header),
|
|
185
|
-
onRecords: records => sink.write(records),
|
|
186
|
-
});
|
|
187
|
-
```
|
|
188
|
-
|
|
189
|
-
`onRecords` is awaited, so a slow sink applies backpressure to the export instead of
|
|
190
|
-
buffering the store in memory; anything it throws aborts the export and releases the
|
|
191
|
-
reader. `stats()` reports `exporting` while one is in flight.
|
|
25
|
+
`links export` holds one read transaction; `links import` refuses occupied collections. Read [cli](links/cli.md#consistent-operator-export-and-restore).
|
|
192
26
|
|
|
193
27
|
## A separate authenticated management API
|
|
194
28
|
|
|
195
|
-
|
|
196
|
-
its own user authentication, authorization, quotas and abuse rules. The API is an
|
|
197
|
-
operator interface, not a public anonymous link-creation endpoint. Never put its
|
|
198
|
-
shared token in browser JavaScript. It is not available on the public route server.
|
|
199
|
-
|
|
200
|
-
Generate a private token file outside the application (POSIX-compatible Node example):
|
|
201
|
-
|
|
202
|
-
```sh
|
|
203
|
-
node --input-type=module -e 'import {writeFileSync} from "node:fs"; import {randomBytes} from "node:crypto"; writeFileSync(process.argv[1],randomBytes(32).toString("base64url"),{mode:0o600,flag:"wx"})' /absolute/link-admin.token
|
|
204
|
-
urlcode links api --store /absolute/links.sqlite --collection links \
|
|
205
|
-
--token-file /absolute/link-admin.token --host 127.0.0.1 --port 3001
|
|
206
|
-
```
|
|
207
|
-
|
|
208
|
-
Use a securely generated token, at least 43 base64url characters. File permissions
|
|
209
|
-
must exclude group/other access on POSIX; protect Windows files with operator ACLs.
|
|
210
|
-
The API reads the token at startup; rotate by replacing it and restarting this
|
|
211
|
-
management process. This does not require restarting public resolution.
|
|
212
|
-
|
|
213
|
-
Every request needs `Authorization: Bearer <token>`. Authenticate before body
|
|
214
|
-
processing. Use application/json for POST/PUT, with no compression; max body is
|
|
215
|
-
16 KiB. Browser Origin-bearing requests are rejected. No CORS, cookies, user
|
|
216
|
-
accounts, per-user scopes, JWT system or automatic rate limiter is provided.
|
|
217
|
-
Keep it on a private network/loopback behind authenticated TLS ingress as needed.
|
|
218
|
-
If changing `--host`, protect it before exposing it; no built-in HTTPS exists.
|
|
219
|
-
|
|
220
|
-
| Request | Result |
|
|
221
|
-
|---|---|
|
|
222
|
-
| `POST /v1/links` with `{ "code": "demo", "url": "https://example.com/demo" }` | 201 with record and ETag; code optional |
|
|
223
|
-
| `GET /v1/links/demo` | 200 with record and ETag, or 404 |
|
|
224
|
-
| `GET /v1/links?limit=100&after=demo` | `{items, nextAfter}`; nextAfter null on a short page |
|
|
225
|
-
| `PUT /v1/links/demo`, `If-Match: "VERSION"`, complete record fields | Replace and return new record/ETag |
|
|
226
|
-
| `DELETE /v1/links/demo`, `If-Match: "VERSION"` | 204 |
|
|
227
|
-
|
|
228
|
-
POST accepts optional `code` plus `url`, optional `status`, `enabled`, `expires`.
|
|
229
|
-
PUT accepts the same record fields except code, which is immutable. `expires`
|
|
230
|
-
may be null. Responses include collection, code, url, status, enabled, expires,
|
|
231
|
-
version. Missing precondition: 428; stale version or duplicate code: 409;
|
|
232
|
-
invalid input: 400; authentication failure: 401; unsupported media: 415;
|
|
233
|
-
record limit: 507; store failure/capacity: 503. Error messages omit credentials,
|
|
234
|
-
submitted URLs and SQL details. A full page can return a cursor even when the
|
|
235
|
-
next page will be empty. No bulk mutation API is implemented, and the management
|
|
236
|
-
API exposes no export: sequential list pages are not a snapshot, so use the
|
|
237
|
-
operator `links export` command above for a consistent copy.
|
|
238
|
-
|
|
239
|
-
A token authorizes its configured collection, not all collections. There are no
|
|
240
|
-
per-end-user permissions: those belong to your backend. The API can also list
|
|
241
|
-
inactive records for management. The public `/r/{code}` route never exposes
|
|
242
|
-
management JSON, token files or a mutation endpoint.
|
|
29
|
+
`links api`: bearer token, endpoints, status codes and scope. Read [management-api](links/management-api.md#a-separate-authenticated-management-api).
|
|
243
30
|
|
|
244
31
|
## Persistence, bounds and recovery
|
|
245
32
|
|
|
246
|
-
|
|
247
|
-
loop and function workers. Default: two read-only worker connections plus one
|
|
248
|
-
writer for writable stores; public serving opens readers only. Reads and writes
|
|
249
|
-
have independent 32-operation admission limits and 5-second deadlines including
|
|
250
|
-
waiting. Lock wait is one second. Excess work returns 503. Failed connections
|
|
251
|
-
are excluded from selection and readiness degrades; surviving readers can still
|
|
252
|
-
serve requests. Plain YAML redirects remain independent.
|
|
253
|
-
|
|
254
|
-
Startup and established-worker failures recover differently.
|
|
255
|
-
|
|
256
|
-
* **Startup failure.** A connection that never reports ready is terminated and its
|
|
257
|
-
error is returned to the caller: `openLinkStore` rejects and activation fails
|
|
258
|
-
closed. Nothing is retried behind the operator's back, so a `serve` or `links api`
|
|
259
|
-
process that cannot open the store does not start, and a reload that cannot open
|
|
260
|
-
it keeps the last-good runtime.
|
|
261
|
-
* **Established-worker failure.** A connection that had been serving and then
|
|
262
|
-
errors, exits or misses an operation deadline is replaced automatically.
|
|
263
|
-
In-flight operations on it reject with 503, the connection is marked unhealthy
|
|
264
|
-
and excluded from selection, and a replacement worker is launched after an
|
|
265
|
-
exponential backoff from 250 ms up to 30 seconds. Each attempt emits a
|
|
266
|
-
`link_store_worker` event with `status: "restarting"`, the attempt number and the
|
|
267
|
-
delay; a replacement that serves an operation resets the backoff, and one that
|
|
268
|
-
starts but dies on every operation keeps backing off instead of spinning. Close
|
|
269
|
-
cancels a pending replacement.
|
|
270
|
-
|
|
271
|
-
While a pool member is down the pool is degraded, not off: `readHealthy`,
|
|
272
|
-
`writeHealthy` and `healthy` report false and readiness degrades, but surviving
|
|
273
|
-
readers keep answering and a recovered writer resumes accepting mutations with no
|
|
274
|
-
operator action. Records live on disk, so a replaced connection loses no committed
|
|
275
|
-
data.
|
|
276
|
-
|
|
277
|
-
Restart the process when recovery cannot help: an unsuitable Node/SQLite build,
|
|
278
|
-
missing or invalid revision metadata, an incompatible schema, a store file that was
|
|
279
|
-
replaced, moved or symlinked under a running connection, or a host-level fault such
|
|
280
|
-
as a full or read-only disk. Those fail activation rather than reconnecting, and
|
|
281
|
-
the replacement worker will keep failing until the underlying cause is fixed.
|
|
282
|
-
|
|
283
|
-
Automatic connection recovery does not make writes idempotent. A mutation whose
|
|
284
|
-
reply was lost to a worker failure or deadline may still have committed, and the
|
|
285
|
-
records it touched carry versions that advance globally. Callers must therefore
|
|
286
|
-
re-read the record and decide again instead of blindly retrying a write; the
|
|
287
|
-
optimistic-version rules under *Update, disable, expire, list and delete* apply
|
|
288
|
-
unchanged. Recovery behavior is covered by the `acknowledged writes survive abrupt
|
|
289
|
-
writer exit and pagination retains records`, `a blocked writer does not occupy read
|
|
290
|
-
connections and recovers after lock release` and `stores with missing revision
|
|
291
|
-
metadata fail activation` cases in `test/links.test.ts`.
|
|
292
|
-
|
|
293
|
-
The initial store has a 100,000-record cap across collections and an 8,192-byte
|
|
294
|
-
normalized destination limit. WAL + FULL synchronous commits provide transactional
|
|
295
|
-
persistence subject to the disk/filesystem's guarantees. The format has an
|
|
296
|
-
application identifier and schema version; incompatible databases fail activation.
|
|
297
|
-
Use trusted local storage and a protected parent directory. Do not replace,
|
|
298
|
-
symlink or move an open database or its WAL/SHM files. The database and token must
|
|
299
|
-
be outside the project; keep them outside public directories, Git and artifacts.
|
|
300
|
-
|
|
301
|
-
An acknowledged mutation is committed. If a caller loses the response or receives
|
|
302
|
-
a timeout, the write may nevertheless have committed: inspect state before retry.
|
|
303
|
-
For retryable creation, choose a stable code and resolve conflicts; automatic
|
|
304
|
-
code generation cannot give exactly-once semantics after a lost response.
|
|
305
|
-
|
|
306
|
-
`links export` gives a consistent logical copy of the records while the store keeps
|
|
307
|
-
serving; it does not replace a file backup, which is what preserves the audit
|
|
308
|
-
journal and the exact record versions. For offline backups, stop management writers and all readers, then copy the
|
|
309
|
-
database together with any remaining WAL file as one consistent stopped set,
|
|
310
|
-
preserving their matching basenames. Restore into a separate private directory
|
|
311
|
-
while no connection is open. Do not discard a WAL just because the app stopped.
|
|
312
|
-
For online backups, use SQLite-aware tooling rather than copying only the live
|
|
313
|
-
main file. SQLite's [WAL documentation](https://www.sqlite.org/wal.html) explains why
|
|
314
|
-
committed state may still be in the WAL. Test restores on a separate closed store.
|
|
315
|
-
Restoring an older database also restores older record versions: discard old
|
|
316
|
-
management ETags and re-read records after restore. This is not a replication or
|
|
317
|
-
point-in-time recovery system. Define retention, RPO/RTO and disk limits yourself.
|
|
318
|
-
|
|
319
|
-
Multiple same-host processes can share the local file; a distributed deployment
|
|
320
|
-
needs another adapter. The trusted embedding API accepts
|
|
321
|
-
`linkStores: {links: adapter}` where `get(collection, code)` resolves to null or a
|
|
322
|
-
record with url/status/enabled/expires. The caller owns adapter shutdown and must
|
|
323
|
-
provide bounded operations, validation and consistency. Optional `healthy=false`
|
|
324
|
-
makes readiness fail. `openLinkStore` provides the built-in implementation plus
|
|
325
|
-
create/update/delete/list/exportSnapshot/close methods. Adapter code is operator code, never
|
|
326
|
-
loaded from route YAML. No remote provider adapter ships in this release.
|
|
33
|
+
Pools, deadlines, worker replacement with backoff, record caps and backups. Read [limits](links/limits.md#persistence-bounds-and-recovery).
|
|
327
34
|
|
|
328
35
|
## Middleware, sandbox and tests
|
|
329
36
|
|
|
330
|
-
|
|
331
|
-
rules apply. Lookup/missing/disabled/expiry errors happen before middleware. The
|
|
332
|
-
handler does not make database objects available to guest code. Functions still
|
|
333
|
-
have no filesystem, SQL, fetch, storage broker or management token capability.
|
|
334
|
-
Link changes do not alter configuration/source digests or invalidate unrelated
|
|
335
|
-
function-binding grants. Changing YAML still does.
|
|
336
|
-
|
|
337
|
-
`routes` and `audit --expect-routes` count definitions, not stored records. Link
|
|
338
|
-
routes require explicit successful GET/HEAD fixtures; there is no assumed fixture
|
|
339
|
-
for live data. Seed a disposable test database, then pass `--link-store` to
|
|
340
|
-
validate/test/audit/benchmark. Do not run mutation tests against production.
|
|
341
|
-
|
|
342
|
-
```sh
|
|
343
|
-
node src/cli.ts test --project examples/live-links --link-store links=/absolute/test-links.sqlite
|
|
344
|
-
node src/cli.ts audit --project examples/live-links --link-store links=/absolute/test-links.sqlite --expect-routes 2
|
|
345
|
-
```
|
|
346
|
-
|
|
347
|
-
The example expects `demo -> https://example.com/demo` and an unused `not-created`
|
|
348
|
-
code. Tests cover create/update/delete visibility without reload, persistence,
|
|
349
|
-
concurrent conflicts, expiry/disabled semantics, scope, public/admin separation,
|
|
350
|
-
body/token boundaries, overload and acknowledged writes after abrupt writer exit.
|
|
351
|
-
Production durability, sustained load and recovery drills still require validation
|
|
352
|
-
on your actual storage. General application state, WebRTC sessions, user-account
|
|
353
|
-
APIs and arbitrary runtime code registration remain separate future work.
|
|
37
|
+
What guest code can and cannot see; fixtures for `test` and `audit`. Read [setup](links/setup.md#middleware-sandbox-and-tests).
|
|
354
38
|
|
|
355
39
|
## Opt-in completed-redirect events
|
|
356
40
|
|
|
357
|
-
|
|
358
|
-
`--request-log detailed` the method and the configured route pattern. They never
|
|
359
|
-
carry a short code or a request target. Counting store lookups is not a substitute
|
|
360
|
-
either, because a lookup cannot tell a completed redirect from a HEAD probe, an
|
|
361
|
-
error or a client that disconnected.
|
|
362
|
-
|
|
363
|
-
A trusted operator embedding the runtime can instead enable a post-response
|
|
364
|
-
observer. It is explicitly enabled in operator code, off by default, and there is no
|
|
365
|
-
`serve` flag and no YAML setting for it: route YAML cannot name a callback, and no
|
|
366
|
-
untrusted code is ever loaded as one.
|
|
367
|
-
|
|
368
|
-
```js
|
|
369
|
-
import {startServer} from '@jimhoyd/urlcode';
|
|
370
|
-
|
|
371
|
-
await startServer({
|
|
372
|
-
project: './links',
|
|
373
|
-
linkStore: {collection: 'links', file: '/absolute/links.sqlite'},
|
|
374
|
-
linkEvents: {
|
|
375
|
-
observe: event => collector.record(event), // operator code, awaited off the request path
|
|
376
|
-
includeCode: false, // set true to disclose the short code to this collector
|
|
377
|
-
maxQueue: 256, // 1–4096 events; excess is dropped and counted
|
|
378
|
-
timeoutMs: 1000, // 1–10000 ms budget per observer call
|
|
379
|
-
},
|
|
380
|
-
});
|
|
381
|
-
```
|
|
382
|
-
|
|
383
|
-
In TypeScript the collector's argument is `LinkEvent` and the option block is
|
|
384
|
-
`LinkObserverOptions`, both exported from `urlcode` beside `LinkStore`,
|
|
385
|
-
`LinkRow`, `LinkStoreOptions`, `LinkApi` and `LinkApiOptions`; the declarations
|
|
386
|
-
ship with the package:
|
|
387
|
-
|
|
388
|
-
```ts
|
|
389
|
-
import { startServer, type LinkEvent, type LinkObserverOptions } from '@jimhoyd/urlcode';
|
|
390
|
-
|
|
391
|
-
const linkEvents: LinkObserverOptions = {
|
|
392
|
-
observe: (event: LinkEvent) => collector.record(event), // event.code is null for an invalid code
|
|
393
|
-
includeCode: false,
|
|
394
|
-
};
|
|
395
|
-
await startServer({ project: './links', linkStore: { collection: 'links', file: '/absolute/links.sqlite' }, linkEvents });
|
|
396
|
-
```
|
|
397
|
-
|
|
398
|
-
Each event is `{event: 'link_request', requestId, collection, route, method, status,
|
|
399
|
-
outcome, durationMs}`, plus `code` only when `includeCode` is true. `route` is the
|
|
400
|
-
configured route pattern, never the request target. Nothing else from the request is
|
|
401
|
-
included: no token, destination URL, query string, headers, body, cookie or client
|
|
402
|
-
IP address, and no stored record. Only stored-link routes produce events; a plain
|
|
403
|
-
YAML redirect never does. Disclosing a short code identifies a link, so treat a
|
|
404
|
-
collector that receives one as holding operational data and keep it off public logs.
|
|
405
|
-
|
|
406
|
-
| `outcome` | Meaning |
|
|
407
|
-
|---|---|
|
|
408
|
-
| `completed` | The redirect response finished. With `method: "GET"` this is the closest thing to a click; `HEAD` is a probe, not a click |
|
|
409
|
-
| `aborted` | A redirect was produced but the response never finished, because the peer disconnected |
|
|
410
|
-
| `missing` | No record for that code |
|
|
411
|
-
| `disabled` | The record exists but is disabled |
|
|
412
|
-
| `expired` | The record exists but its expiry has passed |
|
|
413
|
-
| `invalid_code` | The code failed route input validation |
|
|
414
|
-
| `invalid_record` | The stored record failed validation |
|
|
415
|
-
| `unavailable` | The store was unavailable or over its admission budget |
|
|
416
|
-
|
|
417
|
-
Nothing here is a human click count. Bots, prefetchers, proxies and repeat requests
|
|
418
|
-
all produce `completed` events, the runtime does not deduplicate, and browser and
|
|
419
|
-
proxy caches mean a real navigation may produce no request at all. Do the
|
|
420
|
-
interpretation in your own collector.
|
|
421
|
-
|
|
422
|
-
**The observer cannot affect a redirect.** Events are enqueued after the response
|
|
423
|
-
finishes or the connection closes, never before, so an observer cannot delay,
|
|
424
|
-
rewrite or fail a redirect. Delivery is sequential and bounded: at most `maxQueue`
|
|
425
|
-
events are held, each call gets `timeoutMs` and a slow or hung collector is
|
|
426
|
-
abandoned rather than allowed to pin the queue, and a call that throws is counted
|
|
427
|
-
instead of propagated. Drops and failures are counted, reported through
|
|
428
|
-
`link_observer` events on the normal log and readable at any time through
|
|
429
|
-
`app.linkEventStats()` as `{queued, delivered, dropped, failed, timedOut, closed}`.
|
|
430
|
-
An overloaded collector loses events, by design, instead of growing memory.
|
|
431
|
-
|
|
432
|
-
Shutdown closes the observer after the server's connections are gone, drains what
|
|
433
|
-
was already accepted within one bounded deadline, drops the rest and emits a final
|
|
434
|
-
`link_observer` event with `status: "closed"` and those totals. Events are
|
|
435
|
-
best-effort operational signals, not durable analytics or an audit record: the
|
|
436
|
-
durable, atomic record of mutations remains the store's audit journal.
|
|
437
|
-
|
|
438
|
-
Tests cover GET and HEAD, completed and aborted responses, missing, disabled and
|
|
439
|
-
expired records, default redaction and opt-in code disclosure, a failing collector,
|
|
440
|
-
a hung collector hitting its budget, queue overflow with counted drops, and drain on
|
|
441
|
-
shutdown.
|
|
41
|
+
The `linkEvents` observer: outcomes, redaction, bounded delivery. Read [limits](links/limits.md#opt-in-completed-redirect-events).
|
|
442
42
|
|
|
443
43
|
## Shutdown and management defaults
|
|
444
44
|
|
|
445
|
-
|
|
446
|
-
to port 3000. Endpoint method errors return 405 with an `Allow` header.
|
|
447
|
-
Store shutdown rejects new work, drains accepted operations within each connection and
|
|
448
|
-
then closes SQLite. Repeated `close()` calls share completion. Existing operation
|
|
449
|
-
deadlines still apply: a timeout can leave a mutation outcome unknown, so read
|
|
450
|
-
the record before retrying. Missing/invalid revision metadata rejects startup.
|
|
45
|
+
Loopback defaults, drain on close, unknown outcomes after a timeout. Read [pools](links/pools.md#shutdown-and-management-defaults).
|
|
451
46
|
|
|
452
47
|
## Explicit project opt-in
|
|
453
48
|
|
|
454
|
-
|
|
455
|
-
the starter writes `dynamicLinks: false` explicitly. Included route files cannot
|
|
456
|
-
set or override it. Any `link` handler, including a disabled route, or runtime
|
|
457
|
-
link-store binding requires the opt-in. This flag means live stored-link records,
|
|
458
|
-
not parameterized redirects, custom functions, middleware or development reload.
|
|
459
|
-
|
|
460
|
-
`validate`, `routes`, `audit` and `scaffold` reports expose `dynamicLinks` as a
|
|
461
|
-
boolean. Enabling it grants no storage access to guest code and starts no
|
|
462
|
-
management endpoint. The operator still supplies the external store binding;
|
|
463
|
-
management remains a separate authenticated service. Standalone `links` CRUD/API
|
|
464
|
-
commands operate the operator's store independently of this public-runtime flag.
|
|
465
|
-
|
|
466
|
-
Migration: existing live-link projects must add `dynamicLinks: true` to their
|
|
467
|
-
entry file. Refresh revision-pinned function policies for those projects using
|
|
468
|
-
the normal operator review flow; enabling the capability changes the approval
|
|
469
|
-
digest. Projects that omit it or explicitly set false retain their prior digest.
|
|
470
|
-
To disable, remove `link` declarations and serving-store bindings, then set false
|
|
471
|
-
and validate/redeploy. Editing the flag alone does not stop an already running
|
|
472
|
-
production process or management API. Invalid reloads retain the last-good state.
|
|
49
|
+
`dynamicLinks: true` in the entry file only; what enabling changes. Read [setup](links/setup.md#explicit-project-opt-in).
|
|
473
50
|
|
|
474
51
|
## Separate reader and writer pools
|
|
475
52
|
|
|
476
|
-
|
|
477
|
-
`dynamicLinks: true` and logical collection names stay the same across targets.
|
|
478
|
-
|
|
479
|
-
```sh
|
|
480
|
-
urlcode serve --project ./my-links \
|
|
481
|
-
--link-store links=/absolute/links.sqlite \
|
|
482
|
-
--link-readers 4 --link-read-limit 32
|
|
483
|
-
|
|
484
|
-
urlcode links api --project ./my-links \
|
|
485
|
-
--store /absolute/links.sqlite --token-file /absolute/link-admin.token \
|
|
486
|
-
--link-readers 2 --link-read-limit 16 --link-write-limit 8
|
|
487
|
-
```
|
|
488
|
-
|
|
489
|
-
| Control | Default | Meaning |
|
|
490
|
-
|---|---|---|
|
|
491
|
-
| `--link-readers` / JS `readers` | 2 | 1–8 read-only connections per store instance |
|
|
492
|
-
| `--link-read-limit` / JS `maxReads` | 32 | 1–32 admitted reads across all readers, not per reader |
|
|
493
|
-
| `--link-write-limit` / JS `maxWrites` | 32 | 1–32 admitted mutations across the single writer |
|
|
494
|
-
|
|
495
|
-
`get` and `list` use the least-busy healthy reader. `create`, `update` and `delete`
|
|
496
|
-
use the writer. Each worker serializes its own accepted operations. There is no
|
|
497
|
-
additional unbounded acquisition queue. A read flood cannot take writer admission
|
|
498
|
-
and a mutation backlog cannot take reader admission. CPU, disk, database locks
|
|
499
|
-
and the event loop remain shared resources: separation is not an isolation SLA.
|
|
500
|
-
A write response resolves after commit; a subsequent awaited read sees committed
|
|
501
|
-
data on the same database. Reads started before the commit may see the old value.
|
|
502
|
-
There is no replica lag or application cache in this adapter.
|
|
503
|
-
|
|
504
|
-
`openLinkStore({file, project, readOnly, readers, maxReads, maxWrites})` exposes
|
|
505
|
-
`stats()` with separate read/write connection counts, healthy counts, in-flight
|
|
506
|
-
counts, limits, completed/failed/rejected totals and cumulative durationMs. These
|
|
507
|
-
process-local counters reset at restart and are for trusted operator monitoring;
|
|
508
|
-
there is no public metrics endpoint/exporter yet. `readHealthy` and `writeHealthy`
|
|
509
|
-
are separate. Public runtime readiness uses `readHealthy` when available; management
|
|
510
|
-
operators must monitor writer health separately. A read-only pool has zero writers
|
|
511
|
-
and rejects mutations. Shutdown drains both groups and rejects new admission.
|
|
512
|
-
|
|
513
|
-
Do not multiply connection counts blindly across processes: N public replicas at
|
|
514
|
-
R readers use N×R connections, plus management readers/writers. Each connection
|
|
515
|
-
has its own worker and memory budget. Measure mixed load and lock contention;
|
|
516
|
-
adding readers can reduce performance on an already saturated disk.
|
|
517
|
-
|
|
518
|
-
SQLite WAL supports simultaneous readers and a single active writer across the
|
|
519
|
-
database. Multiple writer connections cannot create parallel write throughput.
|
|
520
|
-
All connections must access the same local database on one host; never mount it
|
|
521
|
-
across hosts over a network filesystem. See [SQLite WAL](https://www.sqlite.org/wal.html).
|
|
522
|
-
We require a Node build with SQLite 3.51.3+ or patched branches 3.50.7+/3.44.6+
|
|
523
|
-
to avoid the documented [WAL-reset concurrency bug](https://www.sqlite.org/wal.html#the_wal_reset_bug).
|
|
524
|
-
Live-link initialization checks this before opening files and verifies WAL mode.
|
|
525
|
-
Use `urlcode doctor` to inspect the bundled SQLite version; upgrade Node when
|
|
526
|
-
rejected. Static YAML projects do not require SQLite.
|
|
527
|
-
|
|
528
|
-
For multi-host scaling, a future server-database adapter must provide distinct
|
|
529
|
-
read/write credentials and pool budgets, connect/acquire/query deadlines,
|
|
530
|
-
transactional version checks, primary-read or explicit replica consistency,
|
|
531
|
-
replica-lag monitoring, migration coordination, backup/restore and failover drills.
|
|
532
|
-
These are adapter acceptance requirements, not implemented PostgreSQL support.
|
|
533
|
-
Keep writes on the primary and avoid automatic retries of ambiguous commits.
|
|
534
|
-
The existing operator adapter boundary remains available, but deploying a custom
|
|
535
|
-
adapter requires its own conformance/load/recovery evidence.
|
|
53
|
+
`--link-readers`, `--link-read-limit`, `--link-write-limit` and `stats()`. Read [pools](links/pools.md#separate-reader-and-writer-pools).
|
|
536
54
|
|
|
537
55
|
## Management HTTP and audit safeguards
|
|
538
56
|
|
|
539
|
-
|
|
540
|
-
or disconnect, returning 503 on overload. A 10-second socket inactivity timeout
|
|
541
|
-
closes stalled peers; this is not a total response deadline. The embedding API
|
|
542
|
-
accepts `maxInFlightRequests` (1–64) and `socketTimeoutMs` (100–60,000). Existing
|
|
543
|
-
connection/body/header and database admission limits still apply. Noncanonical
|
|
544
|
-
dot-segment/backslash path normalization is rejected.
|
|
545
|
-
|
|
546
|
-
JSON `management_request` events go to stdout by default and include timestamp,
|
|
547
|
-
request ID, collection, action, authentication result, status and finish/abort
|
|
548
|
-
outcome. No token, code, destination, URL, headers or body is logged. Embedders
|
|
549
|
-
can supply `log(event)`; failed sinks cannot crash request handling. These are
|
|
550
|
-
best-effort operational events, not durable per-actor audit records. Aborted
|
|
551
|
-
mutations may have committed: re-read state before retrying. See the
|
|
552
|
-
[security review](SECURITY-AUDIT.md) for remaining controls.
|
|
57
|
+
Admission, socket timeout and `management_request` events. Read [management-api](links/management-api.md#management-http-and-audit-safeguards).
|
|
553
58
|
|
|
554
59
|
## Management hardening baseline
|
|
555
60
|
|
|
556
|
-
|
|
557
|
-
for individual expiring, revocable credentials with collection/action scopes.
|
|
558
|
-
Every successful built-in store mutation has an atomic, durable SQLite audit row;
|
|
559
|
-
HTTP request logs remain best effort. See [management security](MANAGEMENT-SECURITY.md)
|
|
560
|
-
for policy examples, compatibility, archival and rollback requirements, and
|
|
561
|
-
[operational proof](OPERATIONAL-PROOF.md) for executable recovery drills.
|
|
61
|
+
Loopback only, `--auth-file` credentials, durable audit rows. Read [management-api](links/management-api.md#management-hardening-baseline).
|