@jimhoyd/urlcode 0.3.0 → 0.4.0-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (380) hide show
  1. package/.claude/skills/urlcode-authoring/SKILL.md +122 -0
  2. package/.claude/skills/urlcode-operations/SKILL.md +108 -0
  3. package/.claude-plugin/marketplace.json +18 -0
  4. package/CONTRIBUTING.md +30 -2
  5. package/README.md +195 -255
  6. package/ROADMAP.md +143 -15
  7. package/SECURITY.md +31 -9
  8. package/dist/BUILD-MANIFEST.json +72 -47
  9. package/dist/adapters.js +4 -23
  10. package/dist/agent-lists.js +1 -1
  11. package/dist/agents-guide.js +113 -0
  12. package/dist/authoring-files.js +60 -0
  13. package/dist/authoring.js +11 -1
  14. package/dist/aws.js +4 -3
  15. package/dist/build-cloudflare.js +11 -24
  16. package/dist/build-static.js +134 -0
  17. package/dist/bulk.js +37 -0
  18. package/dist/capabilities.js +262 -0
  19. package/dist/capability-query.js +71 -0
  20. package/dist/catalog.js +105 -0
  21. package/dist/cli.js +165 -34
  22. package/dist/client-address.js +1 -1
  23. package/dist/compliance-rules/baseline.js +9 -17
  24. package/dist/compliance-rules/privacy.js +7 -18
  25. package/dist/compliance-rules/shared.js +0 -2
  26. package/dist/compliance-rules/strict.js +5 -5
  27. package/dist/compliance.js +6 -8
  28. package/dist/conditions.js +88 -0
  29. package/dist/config.js +69 -6
  30. package/dist/context.js +155 -0
  31. package/dist/ecosystem-cli.js +88 -0
  32. package/dist/egress.js +98 -0
  33. package/dist/examples.js +92 -0
  34. package/dist/explain-cli.js +64 -0
  35. package/dist/explain.js +131 -0
  36. package/dist/extensions.js +231 -0
  37. package/dist/function-sources.js +49 -5
  38. package/dist/function-worker.js +3 -1
  39. package/dist/functions.js +84 -13
  40. package/dist/guest-api.js +29 -3
  41. package/dist/index.js +40 -6
  42. package/dist/init-with.js +165 -0
  43. package/dist/interchange-cli.js +42 -0
  44. package/dist/interchange.js +189 -0
  45. package/dist/manifest.js +109 -0
  46. package/dist/match.js +2 -2
  47. package/dist/mcp-authoring.js +147 -0
  48. package/dist/mcp.js +97 -0
  49. package/dist/observability.js +7 -21
  50. package/dist/operator-host.js +29 -0
  51. package/dist/plugins.js +12 -0
  52. package/dist/policies/agents.js +2 -2
  53. package/dist/policies/cache.js +8 -3
  54. package/dist/policies/compression.js +2 -1
  55. package/dist/policies/security.js +0 -0
  56. package/dist/policies.js +1 -1
  57. package/dist/policy.js +56 -15
  58. package/dist/prerender.js +100 -41
  59. package/dist/project-tests.js +3 -3
  60. package/dist/provider-verification.js +92 -0
  61. package/dist/proxy.js +44 -0
  62. package/dist/readiness.js +34 -11
  63. package/dist/recipes.js +41 -0
  64. package/dist/route-diff.js +106 -0
  65. package/dist/router.js +45 -7
  66. package/dist/runtime.js +164 -64
  67. package/dist/sandbox.js +48 -0
  68. package/dist/scaffold.js +0 -0
  69. package/dist/schema-query.js +62 -0
  70. package/dist/scripts/operational-drills.js +12 -54
  71. package/dist/server.js +3 -29
  72. package/dist/signals.js +24 -0
  73. package/dist/site.js +0 -0
  74. package/dist/tooling.js +96 -0
  75. package/dist/trusted-functions.js +210 -0
  76. package/dist/types/adapters.d.ts +7 -4
  77. package/dist/types/agent-lists.d.ts +0 -1
  78. package/dist/types/agents-guide.d.ts +17 -0
  79. package/dist/types/authoring-files.d.ts +10 -0
  80. package/dist/types/aws.d.ts +3 -1
  81. package/dist/types/build-cloudflare.d.ts +1 -0
  82. package/dist/types/build-static.d.ts +43 -0
  83. package/dist/types/bulk.d.ts +27 -0
  84. package/dist/types/capabilities.d.ts +64 -0
  85. package/dist/types/capability-query.d.ts +24 -0
  86. package/dist/types/catalog.d.ts +65 -0
  87. package/dist/types/client-address.d.ts +0 -1
  88. package/dist/types/compliance-rules/baseline.d.ts +1 -9
  89. package/dist/types/compliance-rules/privacy.d.ts +1 -4
  90. package/dist/types/compliance-rules/shared.d.ts +0 -2
  91. package/dist/types/compliance-rules/strict.d.ts +0 -5
  92. package/dist/types/compliance.d.ts +0 -3
  93. package/dist/types/conditions.d.ts +19 -0
  94. package/dist/types/config.d.ts +21 -2
  95. package/dist/types/context.d.ts +66 -0
  96. package/dist/types/ecosystem-cli.d.ts +17 -0
  97. package/dist/types/egress.d.ts +46 -0
  98. package/dist/types/examples.d.ts +50 -0
  99. package/dist/types/explain-cli.d.ts +11 -0
  100. package/dist/types/explain.d.ts +95 -0
  101. package/dist/types/extensions.d.ts +177 -0
  102. package/dist/types/function-sources.d.ts +9 -0
  103. package/dist/types/functions.d.ts +48 -5
  104. package/dist/types/guest-api.d.ts +1 -0
  105. package/dist/types/index.d.ts +36 -6
  106. package/dist/types/init-with.d.ts +30 -0
  107. package/dist/types/interchange-cli.d.ts +16 -0
  108. package/dist/types/interchange.d.ts +42 -0
  109. package/dist/types/manifest.d.ts +79 -0
  110. package/dist/types/match.d.ts +1 -0
  111. package/dist/types/mcp-authoring.d.ts +92 -0
  112. package/dist/types/mcp.d.ts +12 -0
  113. package/dist/types/observability.d.ts +3 -14
  114. package/dist/types/operator-host.d.ts +8 -0
  115. package/dist/types/plugins.d.ts +2 -0
  116. package/dist/types/policies/agents.d.ts +0 -2
  117. package/dist/types/policies/compression.d.ts +2 -0
  118. package/dist/types/policies/security.d.ts +0 -1
  119. package/dist/types/policy.d.ts +15 -4
  120. package/dist/types/project-tests.d.ts +3 -2
  121. package/dist/types/provider-verification.d.ts +53 -0
  122. package/dist/types/proxy.d.ts +21 -0
  123. package/dist/types/readiness.d.ts +10 -3
  124. package/dist/types/recipes.d.ts +30 -0
  125. package/dist/types/route-diff.d.ts +27 -0
  126. package/dist/types/router.d.ts +2 -1
  127. package/dist/types/runtime.d.ts +11 -27
  128. package/dist/types/sandbox.d.ts +12 -0
  129. package/dist/types/scaffold.d.ts +0 -2
  130. package/dist/types/schema-query.d.ts +12 -0
  131. package/dist/types/server.d.ts +1 -4
  132. package/dist/types/signals.d.ts +25 -0
  133. package/dist/types/site.d.ts +0 -1
  134. package/dist/types/tooling.d.ts +115 -0
  135. package/dist/types/trusted-functions.d.ts +29 -0
  136. package/dist/types/types.d.ts +71 -7
  137. package/dist/types/typescript-authoring.d.ts +12 -0
  138. package/dist/types/vercel.d.ts +3 -1
  139. package/dist/types/verify-deployment.d.ts +47 -0
  140. package/dist/types.js +37 -5
  141. package/dist/typescript-authoring.js +142 -0
  142. package/dist/vercel.js +4 -3
  143. package/dist/verify-deployment.js +270 -0
  144. package/docs/AI-AUTHORING.md +232 -15
  145. package/docs/AWS.md +4 -4
  146. package/docs/BEST-PRACTICES.md +3 -2
  147. package/docs/BULK.md +79 -0
  148. package/docs/CAPABILITIES.md +192 -0
  149. package/docs/CAPACITY.md +129 -32
  150. package/docs/CI.md +142 -0
  151. package/docs/CLOUDFLARE.md +1 -2
  152. package/docs/COMPLIANCE.md +6 -9
  153. package/docs/CONDITIONS.md +74 -0
  154. package/docs/DEPLOYMENT-CHECKS.md +108 -0
  155. package/docs/EGRESS.md +125 -0
  156. package/docs/EXTENSIONS.md +398 -0
  157. package/docs/FRAMEWORK.md +198 -0
  158. package/docs/FUNCTION-SECURITY.md +129 -32
  159. package/docs/INSTALL.md +45 -12
  160. package/docs/INTERCHANGE.md +134 -0
  161. package/docs/LOAD-TESTING.md +4 -4
  162. package/docs/MIDDLEWARE-EXAMPLES.md +75 -0
  163. package/docs/MIDDLEWARE.md +29 -16
  164. package/docs/MONITORING.md +2 -19
  165. package/docs/NEXT-PHASE-PLAN.md +98 -0
  166. package/docs/NEXT-STEPS.md +634 -0
  167. package/docs/OBSERVABILITY.md +11 -18
  168. package/docs/OPEN-DECISIONS.md +212 -0
  169. package/docs/OPERATIONAL-PROOF.md +30 -31
  170. package/docs/OPERATIONS.md +29 -35
  171. package/docs/PLUGINS.md +37 -0
  172. package/docs/POLICIES.md +23 -309
  173. package/docs/PRERENDER.md +41 -1
  174. package/docs/PROJECT-DIRECTION.md +75 -8
  175. package/docs/PROVIDER-VERIFICATION.md +84 -0
  176. package/docs/READINESS.md +21 -1
  177. package/docs/README.md +87 -34
  178. package/docs/RECIPES.md +99 -0
  179. package/docs/RELEASE-READINESS.md +57 -35
  180. package/docs/RELEASE-SECURITY.md +116 -7
  181. package/docs/RESILIENCE.md +16 -15
  182. package/docs/ROUTING.md +8 -10
  183. package/docs/SANDBOX-REVIEW.md +19 -6
  184. package/docs/SCAFFOLDING.md +0 -2
  185. package/docs/SECURITY-AUDIT.md +41 -1
  186. package/docs/SPECIFICATION.md +150 -29
  187. package/docs/SPIKE-AI-FRAMEWORK-BENCHMARK.md +287 -0
  188. package/docs/SPIKE-BUSINESS-SUITE.md +1021 -0
  189. package/docs/SPIKE-CORE-LAYERING.md +337 -0
  190. package/docs/SPIKE-DEFAULT-TRUST-MODEL.md +209 -0
  191. package/docs/SPIKE-EXTENSION-MODEL.md +419 -0
  192. package/docs/SPIKE-EXTENSIONS.md +6 -0
  193. package/docs/SPIKE-LAMBDA-COMPILE.md +201 -0
  194. package/docs/SPIKE-MONOREPO.md +322 -0
  195. package/docs/STANDARDS.md +150 -142
  196. package/docs/STARTERS.md +21 -1
  197. package/docs/STATIC.md +94 -0
  198. package/docs/TOOLING.md +295 -0
  199. package/docs/TUNNELS.md +0 -3
  200. package/docs/TYPESCRIPT-AUTHORING.md +82 -0
  201. package/docs/TYPESCRIPT.md +25 -4
  202. package/docs/USABILITY-REVIEW.md +129 -0
  203. package/docs/VERCEL.md +4 -5
  204. package/docs/VERSION-ALIGNMENT.md +205 -0
  205. package/docs/YAML-GUIDE.md +15 -479
  206. package/docs/YAML-REFERENCE.md +143 -22
  207. package/docs/policies/agents.md +1 -1
  208. package/docs/policies/cache.md +13 -0
  209. package/docs/policies/contract.md +52 -0
  210. package/docs/policies/hardened.md +56 -0
  211. package/docs/policies/interoperability.md +169 -0
  212. package/docs/policies/operations.md +45 -0
  213. package/docs/yaml/assets.md +36 -0
  214. package/docs/yaml/conditions.md +20 -0
  215. package/docs/yaml/functions.md +160 -0
  216. package/docs/yaml/middleware.md +29 -0
  217. package/docs/yaml/organization.md +74 -0
  218. package/docs/yaml/policies.md +37 -0
  219. package/docs/yaml/redirects.md +64 -0
  220. package/docs/yaml/responses.md +57 -0
  221. package/docs/yaml/site.md +24 -0
  222. package/examples/assets/example.yaml +17 -0
  223. package/examples/aws/example.yaml +20 -0
  224. package/examples/cloudflare/example.yaml +19 -0
  225. package/examples/compliance/example.yaml +11 -0
  226. package/examples/conditions/README.md +12 -0
  227. package/examples/conditions/example.yaml +19 -0
  228. package/examples/conditions/tests/requests.json +13 -0
  229. package/examples/conditions/urlcode.yaml +24 -0
  230. package/examples/cookbook/README.md +8 -4
  231. package/examples/cookbook/example.yaml +17 -0
  232. package/examples/cookbook/functions/catalog.mjs +3 -0
  233. package/examples/cookbook/functions/fail.mjs +4 -0
  234. package/examples/cookbook/functions/items.mjs +3 -0
  235. package/examples/cookbook/functions/profile.mjs +3 -0
  236. package/examples/cookbook/functions/resource.mjs +3 -0
  237. package/examples/cookbook/functions/status.mjs +3 -0
  238. package/examples/cookbook/middleware/auth.mjs +48 -0
  239. package/examples/cookbook/middleware/body.mjs +15 -0
  240. package/examples/cookbook/middleware/bucket.mjs +29 -0
  241. package/examples/cookbook/middleware/cors.mjs +21 -0
  242. package/examples/cookbook/middleware/debug.mjs +13 -0
  243. package/examples/cookbook/middleware/envelope.mjs +11 -0
  244. package/examples/cookbook/middleware/errors.mjs +11 -0
  245. package/examples/cookbook/middleware/etag.mjs +18 -0
  246. package/examples/cookbook/middleware/locale.mjs +20 -0
  247. package/examples/cookbook/middleware/maintenance.mjs +10 -0
  248. package/examples/cookbook/middleware/methods.mjs +15 -0
  249. package/examples/cookbook/middleware/negotiate.mjs +20 -0
  250. package/examples/cookbook/middleware/referer.mjs +12 -0
  251. package/examples/cookbook/middleware/request-id.mjs +16 -0
  252. package/examples/cookbook/route-index.json +676 -0
  253. package/examples/cookbook/routes/middleware.yaml +126 -0
  254. package/examples/cookbook/tests/requests.json +526 -0
  255. package/examples/cookbook/urlcode.yaml +1 -0
  256. package/examples/egress/README.md +22 -0
  257. package/examples/egress/example.yaml +19 -0
  258. package/examples/egress/urlcode.yaml +19 -0
  259. package/examples/extensions/README.md +7 -0
  260. package/examples/extensions/example.yaml +21 -0
  261. package/examples/extensions/urlcode.yaml +25 -0
  262. package/examples/monitoring/example.yaml +8 -0
  263. package/examples/prerender/README.md +2 -2
  264. package/examples/prerender/example.yaml +16 -0
  265. package/examples/provider-conformance/README.md +12 -0
  266. package/examples/provider-conformance/example.yaml +14 -0
  267. package/examples/provider-conformance/urlcode.yaml +34 -0
  268. package/examples/tunnel/example.yaml +8 -0
  269. package/examples/vercel/example.yaml +19 -0
  270. package/llms-full.txt +3084 -0
  271. package/llms.txt +61 -21
  272. package/package.json +36 -7
  273. package/packaging/claude-plugin/.claude-plugin/plugin.json +19 -0
  274. package/packaging/claude-plugin/skills/urlcode-authoring/SKILL.md +122 -0
  275. package/packaging/claude-plugin/skills/urlcode-operations/SKILL.md +108 -0
  276. package/recipes/authenticated-json-api/README.md +51 -0
  277. package/recipes/authenticated-json-api/functions/profile.mjs +5 -0
  278. package/recipes/authenticated-json-api/recipe.yaml +34 -0
  279. package/recipes/authenticated-json-api/tests/requests.json +39 -0
  280. package/recipes/authenticated-json-api/urlcode.yaml +12 -0
  281. package/recipes/contact-form/README.md +25 -0
  282. package/recipes/contact-form/functions/contact.mjs +17 -0
  283. package/recipes/contact-form/recipe.yaml +33 -0
  284. package/recipes/contact-form/tests/requests.json +47 -0
  285. package/recipes/contact-form/urlcode.yaml +18 -0
  286. package/recipes/cors-api/README.md +16 -0
  287. package/recipes/cors-api/functions/items.mjs +3 -0
  288. package/recipes/cors-api/middleware/cors.mjs +21 -0
  289. package/recipes/cors-api/recipe.yaml +26 -0
  290. package/recipes/cors-api/tests/requests.json +65 -0
  291. package/recipes/cors-api/urlcode.yaml +12 -0
  292. package/recipes/health-page/README.md +13 -0
  293. package/recipes/health-page/recipe.yaml +23 -0
  294. package/recipes/health-page/tests/requests.json +36 -0
  295. package/recipes/health-page/urlcode.yaml +19 -0
  296. package/recipes/json-api/README.md +6 -0
  297. package/recipes/json-api/functions/echo.mjs +3 -0
  298. package/recipes/json-api/recipe.yaml +25 -0
  299. package/recipes/json-api/tests/requests.json +34 -0
  300. package/recipes/json-api/urlcode.yaml +12 -0
  301. package/recipes/middleware/README.md +34 -0
  302. package/recipes/middleware/functions/catalog.mjs +3 -0
  303. package/recipes/middleware/functions/fail.mjs +4 -0
  304. package/recipes/middleware/functions/items.mjs +3 -0
  305. package/recipes/middleware/functions/profile.mjs +3 -0
  306. package/recipes/middleware/functions/resource.mjs +3 -0
  307. package/recipes/middleware/functions/status.mjs +3 -0
  308. package/recipes/middleware/middleware/auth.mjs +48 -0
  309. package/recipes/middleware/middleware/body.mjs +15 -0
  310. package/recipes/middleware/middleware/bucket.mjs +29 -0
  311. package/recipes/middleware/middleware/cors.mjs +21 -0
  312. package/recipes/middleware/middleware/debug.mjs +13 -0
  313. package/recipes/middleware/middleware/envelope.mjs +11 -0
  314. package/recipes/middleware/middleware/errors.mjs +11 -0
  315. package/recipes/middleware/middleware/etag.mjs +18 -0
  316. package/recipes/middleware/middleware/locale.mjs +20 -0
  317. package/recipes/middleware/middleware/maintenance.mjs +10 -0
  318. package/recipes/middleware/middleware/methods.mjs +15 -0
  319. package/recipes/middleware/middleware/negotiate.mjs +20 -0
  320. package/recipes/middleware/middleware/referer.mjs +12 -0
  321. package/recipes/middleware/middleware/request-id.mjs +16 -0
  322. package/recipes/middleware/public/guide.txt +1 -0
  323. package/recipes/middleware/recipe.yaml +50 -0
  324. package/recipes/middleware/tests/requests.json +528 -0
  325. package/recipes/middleware/urlcode.yaml +127 -0
  326. package/recipes/protected-download/README.md +22 -0
  327. package/recipes/protected-download/files/report.txt +1 -0
  328. package/recipes/protected-download/recipe.yaml +31 -0
  329. package/recipes/protected-download/tests/requests.json +32 -0
  330. package/recipes/protected-download/urlcode.yaml +15 -0
  331. package/recipes/redirect/README.md +7 -0
  332. package/recipes/redirect/recipe.yaml +25 -0
  333. package/recipes/redirect/tests/requests.json +19 -0
  334. package/recipes/redirect/urlcode.yaml +9 -0
  335. package/recipes/static-plus-api/README.md +15 -0
  336. package/recipes/static-plus-api/functions/info.mjs +3 -0
  337. package/recipes/static-plus-api/public/assets/index.html +3 -0
  338. package/recipes/static-plus-api/public/assets/site.css +1 -0
  339. package/recipes/static-plus-api/public/index.html +8 -0
  340. package/recipes/static-plus-api/recipe.yaml +29 -0
  341. package/recipes/static-plus-api/tests/requests.json +56 -0
  342. package/recipes/static-plus-api/urlcode.yaml +23 -0
  343. package/recipes/typescript/README.md +8 -0
  344. package/recipes/typescript/functions/hello.ts +5 -0
  345. package/recipes/typescript/recipe.yaml +23 -0
  346. package/recipes/typescript/tests/requests.json +18 -0
  347. package/recipes/typescript/urlcode.yaml +5 -0
  348. package/recipes/webhook-receiver/README.md +20 -0
  349. package/recipes/webhook-receiver/functions/receive.mjs +16 -0
  350. package/recipes/webhook-receiver/recipe.yaml +27 -0
  351. package/recipes/webhook-receiver/tests/requests.json +59 -0
  352. package/recipes/webhook-receiver/urlcode.yaml +23 -0
  353. package/schemas/recipe.schema.json +139 -0
  354. package/schemas/urlcode.schema.json +659 -110
  355. package/skills/urlcode/SKILL.md +119 -0
  356. package/starters/default/.github/workflows/urlcode.yml +23 -0
  357. package/starters/default/.mcp.json +12 -0
  358. package/starters/default/AGENTS.md +79 -0
  359. package/starters/default/urlcode.yaml +0 -1
  360. package/dist/link-api.js +0 -136
  361. package/dist/link-cli.js +0 -141
  362. package/dist/link-events.js +0 -76
  363. package/dist/link-records.js +0 -31
  364. package/dist/link-store-worker.js +0 -150
  365. package/dist/link-store.js +0 -250
  366. package/dist/management-policy.js +0 -41
  367. package/dist/sqlite-version.js +0 -6
  368. package/dist/types/link-api.d.ts +0 -30
  369. package/dist/types/link-cli.d.ts +0 -36
  370. package/dist/types/link-events.d.ts +0 -27
  371. package/dist/types/link-records.d.ts +0 -11
  372. package/dist/types/link-store-worker.d.ts +0 -1
  373. package/dist/types/link-store.d.ts +0 -130
  374. package/dist/types/management-policy.d.ts +0 -9
  375. package/dist/types/sqlite-version.d.ts +0 -1
  376. package/docs/DYNAMIC-LINKS.md +0 -561
  377. package/docs/MANAGEMENT-SECURITY.md +0 -82
  378. package/examples/live-links/README.md +0 -11
  379. package/examples/live-links/tests/requests.json +0 -6
  380. package/examples/live-links/urlcode.yaml +0 -16
@@ -1,561 +0,0 @@
1
- # Dynamic short links without reloads
2
-
3
- ## Node build requirement
4
-
5
- Live links need a Node build carrying the patched SQLite WAL fix: SQLite 3.51.3
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.
11
-
12
- Available in URLCode 0.1.0. Define a stable route once; create, update and delete
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.
15
-
16
- This first adapter uses optional SQLite storage on one host. Multiple processes
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).
21
-
22
- ## Behavior in YAML, data outside Git
23
-
24
- ```yaml
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.
58
-
59
- ## Run the included example
60
-
61
- From the runtime checkout, choose a private data directory **outside the app**.
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.
85
-
86
- ## Update, disable, expire, list and delete
87
-
88
- ```sh
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.
114
-
115
- ## Consistent operator export and restore
116
-
117
- Listing pages one after another is not a snapshot: inserts, updates and deletes
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.
192
-
193
- ## A separate authenticated management API
194
-
195
- For a web product, your trusted backend calls the management API after applying
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.
243
-
244
- ## Persistence, bounds and recovery
245
-
246
- SQLite operations use separate reader and writer pools, outside the HTTP event
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.
327
-
328
- ## Middleware, sandbox and tests
329
-
330
- Middleware may wrap a successful link response; the usual native metadata/body
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.
354
-
355
- ## Opt-in completed-redirect events
356
-
357
- Default request logs stay minimal: they carry status and timing, and with
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.
442
-
443
- ## Shutdown and management defaults
444
-
445
- `links api` defaults to private address `127.0.0.1:3001`; public serving defaults
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.
451
-
452
- ## Explicit project opt-in
453
-
454
- Only the entry `urlcode.yaml` may set `dynamicLinks: true`. It defaults to false;
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.
473
-
474
- ## Separate reader and writer pools
475
-
476
- Pool sizes are operator infrastructure settings, not portable behavior YAML.
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.
536
-
537
- ## Management HTTP and audit safeguards
538
-
539
- The private API admits up to 32 in-flight HTTP requests through response finish
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.
553
-
554
- ## Management hardening baseline
555
-
556
- Management is now restricted to literal loopback addresses. Prefer `--auth-file`
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.
@@ -1,82 +0,0 @@
1
- # Private management and durable mutation audit
2
-
3
- Management binds only `127.0.0.1` or `::1`. Use an authenticated private tunnel
4
- (e.g. SSH/VPN with loopback forwarding); never publish its port through a public
5
- proxy or container port mapping. This is operator management, not an end-user
6
- account service. Browser Origin requests remain rejected.
7
-
8
- ## Individual credentials
9
-
10
- Use `urlcode links api --project ./my-links --store /operator/links.sqlite
11
- --auth-file /operator/management.json --host 127.0.0.1 --port 3001` (one line).
12
- The policy is operator-owned, outside the application, at most 64 KiB and mode
13
- 600 on POSIX. Protect its parent directory and apply equivalent Windows ACLs.
14
- It is not YAML and must never be placed in the application or Git.
15
-
16
- Generate each token with `node -e "console.log(require('node:crypto').randomBytes(32).toString('base64url'))"`
17
- on a trusted operator machine. Deliver it through your secret manager. Hash the
18
- raw token using SHA-256 without a newline; the server expects `Authorization:
19
- Bearer <token>`. Tokens must contain 43–256 base64url characters. Never use a
20
- human password as a token. This synthetic policy illustrates the shape; replace
21
- the hash with the real token hash and choose a short operational expiry:
22
-
23
- ```json
24
- {
25
- "version": 1,
26
- "credentials": [{
27
- "id": "operator-alice",
28
- "sha256": "0000000000000000000000000000000000000000000000000000000000000000",
29
- "expires": "2026-10-01T00:00:00Z",
30
- "collections": ["links"],
31
- "actions": ["get", "list", "create", "update", "delete"]
32
- }]
33
- }
34
- ```
35
-
36
- At most 128 credentials; unique IDs and hashes; explicit collection and action
37
- allowlists with no wildcards. Readers should receive only `get` and `list`.
38
- An expired, removed or `"revoked": true` credential gets 401; a valid credential
39
- outside its scope gets 403. The entire policy is validated on every authenticated
40
- request. Malformed/unreadable policy fails closed with 503, without retaining an
41
- old permissive copy. Write a replacement file with mode 600 and atomically rename
42
- it over the policy. No restart is needed. Requests already authorized may finish;
43
- revocation does not cancel an in-flight transaction. Credential administration is
44
- an operator filesystem action, never exposed through this HTTP API.
45
-
46
- Legacy `--token-file` remains available for local compatibility, with full access
47
- to that listener's collection and actor `legacy-shared`. Prefer `--auth-file` for
48
- attribution, expiry and revocation. They are mutually exclusive. These bearer
49
- credentials do not establish verified human identity, MFA, SSO or session login.
50
- Host administrators and custom embedding code remain trusted operators.
51
-
52
- ## Audit durability and recovery
53
-
54
- The built-in SQLite writer creates an additive `urlcode_link_audit` table when
55
- opening a store. Every successful create/update/delete (including CLI and embedded
56
- store calls) records revision, UTC timestamp, actor, request ID, collection,
57
- action and SHA-256 of the short code in the **same transaction** as the mutation.
58
- The default local CLI actor is `local-operator`. Scoped HTTP management requires
59
- an adapter explicitly declaring atomic audit support; the built-in store provides it.
60
-
61
- An audit insert failure rolls back both mutation and revision. Conditional-write
62
- conflicts and rejected requests produce no successful-mutation row. HTTP request
63
- logs still describe failed/aborted requests and remain best effort. A client timeout
64
- may follow a committed transaction: reconcile the revision and request ID before
65
- retrying. A lost HTTP reply is not evidence that the transaction was rolled back.
66
-
67
- Inspect the journal with an operator-only SQLite client, for example:
68
-
69
- ```sql
70
- SELECT revision, timestamp, actor, request_id, collection, action, code_sha256
71
- FROM urlcode_link_audit WHERE revision > 0 ORDER BY revision LIMIT 100;
72
- ```
73
-
74
- No token, URL, request body or raw short code is stored in the journal. Code hashes
75
- are correlatable and guessable for short codes; restrict access accordingly.
76
- This is a durable local transaction journal, **not tamper-evident external storage**.
77
- An administrator with database access can change it. Export ordered revisions to
78
- your protected collector/backup system and monitor lag, gaps and disk/WAL growth.
79
- There is no automatic pruning: define retention and verify archival before any
80
- operator purge. Keep audit records in backups. User_version remains 1 because
81
- this table is additive; older readers work, but older writers do not emit audit
82
- rows. Never roll a management writer back to an unaudited revision.
@@ -1,11 +0,0 @@
1
- # Live links without route reloads
2
-
3
- Follow [the dynamic-link guide](../../docs/DYNAMIC-LINKS.md) to create an external
4
- SQLite store and seed `demo -> https://example.com/demo`. Then start this project
5
- with `--link-store links=/absolute/path/links.sqlite`. Its fixtures expect that
6
- seed and an unused `not-created` code; use a disposable test store, not production.
7
-
8
- Create/update/delete records through the CLI or separate authenticated API while
9
- the public server keeps serving. YAML, code, route counts and health version do
10
- not change per link. This example deliberately requires explicit operator storage
11
- binding; the default starter still runs without a database.
@@ -1,6 +0,0 @@
1
- [
2
- {"path":"/r/demo","status":302,"expectHeaders":{"location":"https://example.com/demo","cache-control":"no-store"}},
3
- {"path":"/r/demo","method":"HEAD","status":302,"expectBody":"","expectHeaders":{"location":"https://example.com/demo"}},
4
- {"path":"/r/not-created","status":404},
5
- {"path":"/r/demo","method":"POST","status":405,"expectHeaders":{"allow":"GET, HEAD"}}
6
- ]