@jimhoyd/urlcode 0.4.0-alpha.1 → 0.4.0-alpha.3

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 (232) hide show
  1. package/.claude/skills/urlcode-authoring/SKILL.md +36 -22
  2. package/.claude/skills/urlcode-operations/SKILL.md +16 -22
  3. package/.claude-plugin/marketplace.json +1 -1
  4. package/CONTRIBUTING.md +20 -16
  5. package/README.md +83 -80
  6. package/ROADMAP.md +61 -331
  7. package/SECURITY.md +23 -8
  8. package/dist/BUILD-MANIFEST.json +42 -47
  9. package/dist/agents-guide.js +18 -18
  10. package/dist/build-static.js +136 -0
  11. package/dist/capabilities.js +85 -31
  12. package/dist/capability-query.js +0 -1
  13. package/dist/cli.js +24 -32
  14. package/dist/compliance-rules/baseline.js +2 -10
  15. package/dist/compliance-rules/privacy.js +5 -16
  16. package/dist/compliance-rules/shared.js +0 -2
  17. package/dist/compliance.js +6 -8
  18. package/dist/config.js +15 -8
  19. package/dist/context.js +9 -10
  20. package/dist/examples.js +2 -2
  21. package/dist/explain-cli.js +7 -5
  22. package/dist/explain.js +10 -5
  23. package/dist/extensions.js +61 -2
  24. package/dist/function-sources.js +34 -2
  25. package/dist/function-worker.js +3 -1
  26. package/dist/functions.js +84 -13
  27. package/dist/guest-api.js +29 -3
  28. package/dist/index.js +3 -6
  29. package/dist/manifest.js +11 -7
  30. package/dist/mcp-authoring.js +2 -2
  31. package/dist/mcp.js +1 -1
  32. package/dist/observability.js +1 -21
  33. package/dist/policies/cache.js +8 -3
  34. package/dist/policies.js +3 -1
  35. package/dist/policy.js +28 -9
  36. package/dist/prerender.js +4 -0
  37. package/dist/project-tests.js +3 -3
  38. package/dist/readiness.js +35 -9
  39. package/dist/route-diff.js +12 -5
  40. package/dist/router.js +5 -7
  41. package/dist/runtime.js +77 -58
  42. package/dist/sandbox.js +48 -0
  43. package/dist/scaffold.js +0 -0
  44. package/dist/scripts/operational-drills.js +12 -54
  45. package/dist/server.js +3 -29
  46. package/dist/tooling.js +1 -1
  47. package/dist/trusted-functions.js +210 -0
  48. package/dist/types/build-static.d.ts +43 -0
  49. package/dist/types/capabilities.d.ts +14 -5
  50. package/dist/types/compliance-rules/shared.d.ts +0 -2
  51. package/dist/types/compliance.d.ts +0 -3
  52. package/dist/types/config.d.ts +2 -1
  53. package/dist/types/context.d.ts +2 -1
  54. package/dist/types/examples.d.ts +1 -1
  55. package/dist/types/explain.d.ts +6 -0
  56. package/dist/types/extensions.d.ts +55 -0
  57. package/dist/types/function-sources.d.ts +4 -0
  58. package/dist/types/functions.d.ts +48 -5
  59. package/dist/types/guest-api.d.ts +1 -0
  60. package/dist/types/index.d.ts +3 -6
  61. package/dist/types/manifest.d.ts +5 -3
  62. package/dist/types/observability.d.ts +1 -14
  63. package/dist/types/project-tests.d.ts +1 -2
  64. package/dist/types/readiness.d.ts +12 -3
  65. package/dist/types/router.d.ts +2 -1
  66. package/dist/types/runtime.d.ts +0 -27
  67. package/dist/types/sandbox.d.ts +12 -0
  68. package/dist/types/scaffold.d.ts +0 -2
  69. package/dist/types/server.d.ts +1 -4
  70. package/dist/types/tooling.d.ts +3 -3
  71. package/dist/types/trusted-functions.d.ts +29 -0
  72. package/dist/types/types.d.ts +22 -8
  73. package/dist/types/verify-deployment.d.ts +2 -2
  74. package/dist/types.js +25 -5
  75. package/dist/typescript-authoring.js +67 -18
  76. package/dist/verify-deployment.js +3 -3
  77. package/docs/AI-AUTHORING.md +109 -10
  78. package/docs/ASSETS.md +2 -1
  79. package/docs/AUTH-BACKUP.md +32 -0
  80. package/docs/AWS.md +4 -4
  81. package/docs/BEST-PRACTICES.md +17 -8
  82. package/docs/CAPABILITIES.md +30 -17
  83. package/docs/CAPACITY.md +128 -31
  84. package/docs/CI-FOLLOWUP-2026-09-19.md +97 -0
  85. package/docs/CI-RELEASE-AUDIT-2026-09-19.md +322 -0
  86. package/docs/CI.md +8 -3
  87. package/docs/CLOUDFLARE.md +1 -2
  88. package/docs/CODEBASE-AUDIT-2026-09-20.md +278 -0
  89. package/docs/COMPLIANCE.md +6 -9
  90. package/docs/DEPLOYMENT-CHECKS.md +1 -1
  91. package/docs/DEVELOPMENT-PIPELINE.md +181 -0
  92. package/docs/EXTENSIONS.md +193 -11
  93. package/docs/FRAMEWORK.md +53 -33
  94. package/docs/FUNCTION-SECURITY.md +173 -32
  95. package/docs/INSTALL.md +0 -5
  96. package/docs/LOAD-TESTING.md +4 -4
  97. package/docs/LOCAL-DEVELOPMENT.md +3 -0
  98. package/docs/MIDDLEWARE.md +27 -16
  99. package/docs/MONITORING.md +2 -19
  100. package/docs/OBSERVABILITY.md +7 -16
  101. package/docs/OPEN-DECISIONS.md +184 -0
  102. package/docs/OPERATIONAL-PROOF.md +26 -30
  103. package/docs/OPERATIONS.md +23 -32
  104. package/docs/POLICIES.md +27 -8
  105. package/docs/PRERENDER.md +25 -13
  106. package/docs/PROJECT-DIRECTION.md +35 -10
  107. package/docs/READINESS.md +6 -2
  108. package/docs/README.md +20 -41
  109. package/docs/RELEASE-0.4.0-alpha.3.md +50 -0
  110. package/docs/RELEASE-READINESS.md +50 -30
  111. package/docs/RELEASE-SECURITY.md +72 -86
  112. package/docs/RESILIENCE.md +16 -15
  113. package/docs/ROUTING.md +7 -10
  114. package/docs/SANDBOX-REVIEW.md +19 -6
  115. package/docs/SCAFFOLDING.md +0 -2
  116. package/docs/SECURITY-AUDIT.md +42 -2
  117. package/docs/SPECIFICATION.md +63 -27
  118. package/docs/SPIKE-AI-FRAMEWORK-BENCHMARK.md +288 -0
  119. package/docs/SPIKE-BUSINESS-SUITE.md +16 -0
  120. package/docs/SPIKE-CORE-LAYERING.md +368 -0
  121. package/docs/SPIKE-DEFAULT-TRUST-MODEL.md +211 -0
  122. package/docs/SPIKE-LAMBDA-COMPILE.md +12 -2
  123. package/docs/STARTERS.md +1 -1
  124. package/docs/STATIC.md +105 -0
  125. package/docs/TOOLING.md +17 -12
  126. package/docs/TUNNELS.md +0 -3
  127. package/docs/TYPESCRIPT-AUTHORING.md +32 -12
  128. package/docs/TYPESCRIPT.md +25 -4
  129. package/docs/VERCEL.md +4 -5
  130. package/docs/VERSION-ALIGNMENT.md +46 -0
  131. package/docs/YAML-GUIDE.md +0 -3
  132. package/docs/YAML-REFERENCE.md +16 -6
  133. package/docs/archive/2026-09-19/EXTENSION-IMPLEMENTATION.md +68 -0
  134. package/docs/{MANAGEMENT-SECURITY.md → archive/2026-09-19/MANAGEMENT-SECURITY.md} +20 -0
  135. package/docs/{NEXT-PHASE-PLAN.md → archive/2026-09-19/NEXT-PHASE-PLAN.md} +26 -8
  136. package/docs/{NEXT-STEPS.md → archive/2026-09-19/NEXT-STEPS.md} +274 -43
  137. package/docs/archive/2026-09-19/OPEN-DECISIONS.md +277 -0
  138. package/docs/archive/2026-09-19/RELEASE-SECURITY.md +186 -0
  139. package/docs/archive/2026-09-19/ROADMAP.md +386 -0
  140. package/docs/{SPIKE-EXTENSION-MODEL.md → archive/2026-09-19/SPIKE-EXTENSION-MODEL.md} +12 -2
  141. package/docs/{SPIKE-EXTENSIONS.md → archive/2026-09-19/SPIKE-EXTENSIONS.md} +28 -12
  142. package/docs/archive/2026-09-19/SPIKE-MONOREPO.md +776 -0
  143. package/docs/{USABILITY-REVIEW.md → archive/2026-09-19/USABILITY-REVIEW.md} +18 -2
  144. package/docs/archive/README.md +27 -0
  145. package/docs/policies/agents.md +1 -1
  146. package/docs/policies/cache.md +13 -0
  147. package/docs/policies/compression.md +3 -2
  148. package/docs/policies/security.md +3 -2
  149. package/examples/assets/example.yaml +2 -2
  150. package/examples/aws/example.yaml +1 -1
  151. package/examples/cloudflare/example.yaml +1 -1
  152. package/examples/conditions/example.yaml +1 -1
  153. package/examples/cookbook/example.yaml +1 -1
  154. package/examples/cookbook/middleware/bucket.mjs +12 -2
  155. package/examples/cookbook/middleware/locale.mjs +7 -3
  156. package/examples/cookbook/route-index.json +1 -1
  157. package/examples/cookbook/routes/middleware.yaml +1 -1
  158. package/examples/egress/example.yaml +1 -1
  159. package/examples/extensions/example.yaml +1 -1
  160. package/examples/prerender/README.md +14 -6
  161. package/examples/prerender/example.yaml +1 -1
  162. package/examples/prerender/functions/page.mjs +4 -2
  163. package/examples/prerender/middleware/template.mjs +1 -1
  164. package/examples/prerender/prerender.mjs +1 -1
  165. package/examples/prerender/urlcode.yaml +8 -4
  166. package/examples/provider-conformance/example.yaml +1 -1
  167. package/examples/vercel/example.yaml +1 -1
  168. package/llms-full.txt +686 -239
  169. package/llms.txt +27 -15
  170. package/package.json +32 -5
  171. package/packaging/claude-plugin/.claude-plugin/plugin.json +2 -2
  172. package/packaging/claude-plugin/skills/urlcode-authoring/SKILL.md +36 -22
  173. package/packaging/claude-plugin/skills/urlcode-operations/SKILL.md +16 -22
  174. package/recipes/authenticated-json-api/README.md +4 -3
  175. package/recipes/authenticated-json-api/functions/profile.mjs +2 -1
  176. package/recipes/authenticated-json-api/recipe.yaml +2 -2
  177. package/recipes/contact-form/functions/contact.mjs +2 -1
  178. package/recipes/contact-form/recipe.yaml +3 -3
  179. package/recipes/cors-api/README.md +2 -2
  180. package/recipes/cors-api/recipe.yaml +2 -2
  181. package/recipes/health-page/README.md +1 -1
  182. package/recipes/health-page/recipe.yaml +1 -1
  183. package/recipes/json-api/README.md +1 -1
  184. package/recipes/json-api/recipe.yaml +4 -4
  185. package/recipes/middleware/README.md +8 -4
  186. package/recipes/middleware/middleware/bucket.mjs +12 -2
  187. package/recipes/middleware/middleware/locale.mjs +7 -3
  188. package/recipes/middleware/recipe.yaml +1 -1
  189. package/recipes/protected-download/README.md +1 -1
  190. package/recipes/protected-download/recipe.yaml +2 -2
  191. package/recipes/redirect/recipe.yaml +1 -1
  192. package/recipes/static-plus-api/README.md +2 -2
  193. package/recipes/static-plus-api/public/index.html +1 -1
  194. package/recipes/static-plus-api/recipe.yaml +2 -2
  195. package/recipes/static-plus-api/urlcode.yaml +1 -1
  196. package/recipes/typescript/README.md +2 -1
  197. package/recipes/typescript/recipe.yaml +5 -5
  198. package/recipes/webhook-receiver/README.md +5 -1
  199. package/recipes/webhook-receiver/recipe.yaml +2 -1
  200. package/recipes/webhook-receiver/urlcode.yaml +7 -0
  201. package/schemas/recipe.schema.json +4 -3
  202. package/schemas/urlcode.schema.json +14 -41
  203. package/skills/urlcode/SKILL.md +32 -11
  204. package/starters/default/AGENTS.md +18 -18
  205. package/starters/default/urlcode.yaml +0 -1
  206. package/dist/link-api.js +0 -136
  207. package/dist/link-cli.js +0 -141
  208. package/dist/link-events.js +0 -76
  209. package/dist/link-records.js +0 -31
  210. package/dist/link-store-worker.js +0 -150
  211. package/dist/link-store.js +0 -250
  212. package/dist/management-policy.js +0 -40
  213. package/dist/sqlite-version.js +0 -6
  214. package/dist/types/link-api.d.ts +0 -30
  215. package/dist/types/link-cli.d.ts +0 -37
  216. package/dist/types/link-events.d.ts +0 -27
  217. package/dist/types/link-records.d.ts +0 -11
  218. package/dist/types/link-store-worker.d.ts +0 -1
  219. package/dist/types/link-store.d.ts +0 -130
  220. package/dist/types/management-policy.d.ts +0 -8
  221. package/dist/types/sqlite-version.d.ts +0 -1
  222. package/docs/DYNAMIC-LINKS.md +0 -61
  223. package/docs/links/cli.md +0 -110
  224. package/docs/links/limits.md +0 -175
  225. package/docs/links/management-api.md +0 -80
  226. package/docs/links/pools.md +0 -75
  227. package/docs/links/setup.md +0 -135
  228. package/docs/yaml/links.md +0 -30
  229. package/examples/live-links/README.md +0 -11
  230. package/examples/live-links/example.yaml +0 -21
  231. package/examples/live-links/tests/requests.json +0 -6
  232. package/examples/live-links/urlcode.yaml +0 -16
@@ -1,250 +0,0 @@
1
- import {Worker} from 'node:worker_threads';
2
- import {realpath,lstat,open} from 'node:fs/promises';
3
- import {dirname,basename,join,relative,isAbsolute,sep} from 'node:path';
4
- import {ConfigError,HttpError,assert} from './errors.js';
5
- import {supportsConcurrentWal} from './sqlite-version.js';
6
-
7
-
8
- /** A stored link as the worker returns it. */
9
-
10
-
11
-
12
-
13
-
14
-
15
-
16
-
17
-
18
-
19
-
20
- // The worker protocol: one command per operation, one reply per command. The
21
- // worker validates every argument again; these types name the contract.
22
-
23
-
24
-
25
-
26
-
27
-
28
-
29
-
30
-
31
-
32
-
33
- /** One worker-backed SQLite connection. */
34
-
35
-
36
-
37
-
38
-
39
-
40
-
41
-
42
-
43
-
44
-
45
-
46
- /** The pooled store openLinkStore returns. */
47
-
48
-
49
-
50
-
51
-
52
-
53
-
54
-
55
-
56
-
57
-
58
-
59
-
60
-
61
- export async function outsideProject(file ,project ) {
62
- assert(typeof file==='string' && isAbsolute(file),'Operator file must use an absolute path');
63
- const parent=await realpath(dirname(file));const actual=join(parent,basename(file));
64
- const root=await realpath(project);const rel=relative(root,actual);
65
- assert(isAbsolute(rel)||rel==='..'||rel.startsWith('..'+sep),'Operator file must be outside the application project');
66
- return actual;
67
- }
68
- // A backstop against a worker that will never answer, not a performance budget:
69
- // a bad build or an unreadable file reports itself in milliseconds, while a cold,
70
- // heavily loaded machine can legitimately take seconds to boot a worker thread
71
- // and open SQLite. Set well clear of that, because refusing to start a store the
72
- // machine would have opened is the worse failure.
73
- const startupMs=15000;
74
-
75
- const isCode=(error ,code ) =>error instanceof Error && 'code' in error && error.code===code;
76
- async function openConnection({file,project='.',readOnly=false,log=()=>{}} ) {
77
- assert(supportsConcurrentWal(process.versions.sqlite),`Live links require a Node build with patched SQLite (3.51.3+, 3.50.7 or 3.44.6); this build has ${process.versions.sqlite}. Upgrade Node`);
78
- const path=await outsideProject(file,project);
79
- if(!readOnly){try{const handle=await open(path,'wx',0o600);await handle.close();}catch(e){if(!isCode(e,'EEXIST'))throw e;}}
80
- const info=await lstat(path);
81
- assert(info.isFile() && !info.isSymbolicLink() && info.nlink===1,'Link store must be a regular operator-owned file');
82
- const pending=new Map ();
83
- let worker ,sequence=0,healthy=false,closed=false,closing ,attempts=0,respawnTimer ;
84
- const report=(status ,extra ={})=>{try{log({event:'link_store_worker',status,readOnly,...extra});}catch{/* Logging cannot fail the store. */}};
85
- const fail=()=>{healthy=false;for(const {reject,timer} of pending.values()){clearTimeout(timer);reject(new HttpError(503,'Link store unavailable'));}pending.clear();};
86
- // A dead connection must not latch the store off: one slow query or an abrupt
87
- // worker exit is recoverable, and the records themselves live on disk.
88
- function scheduleRespawn() {
89
- if(closed)return;
90
- attempts++;
91
- const delayMs=Math.min(30000,250*2**Math.min(attempts-1,7));
92
- report('restarting',{attempt:attempts,delayMs});
93
- respawnTimer=setTimeout(()=>{respawnTimer=undefined;if(closed)return;void launch().catch(()=>scheduleRespawn());},delayMs);
94
- respawnTimer.unref();
95
- }
96
- async function launch() {
97
- const workerData ={file:path,readOnly};
98
- const instance=new Worker(new URL('./link-store-worker.js',import.meta.url),{workerData,env:{},execArgv:[],stdout:true,stderr:true,resourceLimits:{maxOldGenerationSizeMb:64}});
99
- worker=instance;instance.stdout.resume();instance.stderr.resume();
100
- try {
101
- await new Promise ((resolve,reject)=>{
102
- let started=false,settled=false;
103
- const settle=(error )=>{if(settled)return;settled=true;if(error)reject(error);else resolve();};
104
- const timer=setTimeout(()=>settle(new ConfigError(`Link store initialization failed: no ready signal within ${startupMs}ms`)),startupMs);
105
- instance.on('message',(message )=>{ // trust boundary: the worker's own protocol
106
- if('ready' in message){if(!started){started=true;healthy=true;clearTimeout(timer);report('started');settle();}return;}
107
- if('failed' in message){clearTimeout(timer);healthy=false;settle(new ConfigError('Link store initialization failed: the worker could not open the store'));return;}
108
- const request=pending.get(message.id);if(!request)return;
109
- // An answered operation, success or rejection, proves this connection is
110
- // serving again; a worker that starts cleanly but dies on every operation
111
- // must keep backing off rather than restarting in a tight loop.
112
- attempts=0;clearTimeout(request.timer);pending.delete(message.id);
113
- if('error' in message)request.reject(new HttpError(message.error.status,message.error.message));else request.resolve(message.value);
114
- });
115
- const down=()=>{
116
- clearTimeout(timer);
117
- const wasStarted=started;started=false;
118
- if(worker===instance)fail();
119
- settle(new ConfigError('Link store initialization failed: the worker exited or errored during start'));
120
- // Replace only a connection that had been serving; a failed activation
121
- // is reported to the caller instead of retried behind its back.
122
- if(wasStarted&&!closed&&worker===instance)scheduleRespawn();
123
- };
124
- instance.on('error',down);instance.on('exit',down);
125
- });
126
- } catch(error) {
127
- // An initialization error must not escape while its worker still owns the DB.
128
- await instance.terminate();
129
- throw error;
130
- }
131
- }
132
- await launch();
133
- function call (operation ,args ={},internal=false) {
134
- const instance=worker;
135
- if(!healthy||!instance||(!internal&&(closed||pending.size>=32)))return Promise.reject(new HttpError(503,'Link store capacity unavailable'));
136
- const id=++sequence;
137
- return new Promise ((resolve,reject)=>{
138
- const timer=setTimeout(()=>{if(worker===instance){fail();void instance.terminate();}},5000);
139
- pending.set(id,{resolve:value=>resolve(value ),reject,timer}); // trust boundary: the worker answers each operation with its own value type
140
- const command ={id,operation,args};
141
- try{instance.postMessage(command);}
142
- catch{clearTimeout(timer);pending.delete(id);reject(new HttpError(400,'Invalid store arguments'));}
143
- });
144
- }
145
- return {
146
- get healthy(){return healthy&&!closed;},
147
- get:(collection,code)=>call('get',{collection,code}),
148
- list:(collection,options={})=>call('list',{collection,...options}),
149
- exportBegin:(options={})=>call('exportBegin',options),
150
- exportPage:(options={})=>call('exportPage',options),
151
- exportEnd:()=>call('exportEnd',{}),
152
- create:(collection,data,code,audit)=>call('create',{collection,data,code,audit}),
153
- update:(collection,code,data,expectedVersion,audit)=>call('update',{collection,code,data,expectedVersion,audit}),
154
- delete:(collection,code,expectedVersion,audit)=>call('delete',{collection,code,expectedVersion,audit}),
155
- close(){
156
- if(closing)return closing;
157
- closed=true;
158
- if(respawnTimer){clearTimeout(respawnTimer);respawnTimer=undefined;}
159
- // Reserve shutdown admission and enqueue it after all accepted operations.
160
- closing=(async()=>{const instance=worker;try{if(healthy)await call('close',{},true);}finally{fail();await instance?.terminate();}})();
161
- return closing;
162
- },
163
- };
164
- }
165
-
166
- // SQLite permits concurrent readers but serializes writes. Keep independent
167
- // admission budgets so management work cannot consume redirect read capacity.
168
- export async function openLinkStore({file,project='.',readOnly=false,readers=2,maxReads=32,maxWrites=32,log=()=>{}} ={}) {
169
- assert(Number.isInteger(readers)&&readers>=1&&readers<=8,'Link readers must be 1–8');
170
- for(const value of [maxReads,maxWrites])assert(Number.isInteger(value)&&value>=1&&value<=32,'Link pool limits must be 1–32');
171
- const connections =[];let writer ;
172
- try {
173
- // Initialize before opening read-only connections on a new database.
174
- if(!readOnly){writer=await openConnection({file,project,log});connections.push(writer);}
175
- const read =[];
176
- for(let i=0;i<readers;i++){const connection=await openConnection({file,project,readOnly:true,log});connections.push(connection);read.push(connection);}
177
- return pooledStore(read,writer,maxReads,maxWrites);
178
- }catch(error){await Promise.allSettled(connections.map(connection=>connection.close()));throw error;}
179
- }
180
-
181
-
182
- function pooledStore(read ,writer ,maxReads ,maxWrites ) {
183
- const group=(connections ,limit ) =>({connections:connections.map(connection=>({connection,inFlight:0})),limit,inFlight:0,completed:0,failed:0,rejected:0,durationMs:0});
184
- const reads=group(read,maxReads),writes=group(writer?[writer]:[],maxWrites);
185
- let closed=false,closing ,exporting=false;
186
- const healthy=(pool )=>!closed&&pool.connections.length>0&&pool.connections.every(slot=>slot.connection.healthy);
187
- async function run (pool ,operation ) {
188
- if(closed){pool.rejected++;throw new HttpError(503,'Link store unavailable');}
189
- if(!pool.connections.length){pool.rejected++;throw new HttpError(403,'Store is read-only');}
190
- const slot=pool.connections.filter(slot=>slot.connection.healthy).sort((a,b)=>a.inFlight-b.inFlight)[0];
191
- if(!slot || pool.inFlight>=pool.limit){pool.rejected++;throw new HttpError(503,'Link pool capacity unavailable');}
192
- pool.inFlight++;slot.inFlight++;const started=performance.now();
193
- try{const value=await operation(slot.connection);pool.completed++;return value;}
194
- catch(error){pool.failed++;throw error;}
195
- finally{pool.inFlight--;slot.inFlight--;pool.durationMs+=performance.now()-started;}
196
- }
197
- // One bounded, point-in-time export at a time, pinned to a single reader and
198
- // holding that reader's admission for its whole life so it cannot outgrow the
199
- // pool's budget. Writers keep committing; this view does not see them.
200
- async function exportSnapshot({collection,pageSize=100,deadlineMs=60000} ={},{onHeader=()=>{},onRecords=()=>{}} ={}) {
201
- assert(Number.isInteger(pageSize)&&pageSize>=1&&pageSize<=100,'Export page size must be 1–100');
202
- assert(Number.isInteger(deadlineMs)&&deadlineMs>=1000&&deadlineMs<=600000,'Export deadline must be 1000–600000 ms');
203
- assert(typeof onHeader==='function' && typeof onRecords==='function','Export handlers must be functions');
204
- if(closed){reads.rejected++;throw new HttpError(503,'Link store unavailable');}
205
- if(exporting){reads.rejected++;throw new HttpError(409,'An export is already in progress');}
206
- const slot=reads.connections.filter(slot=>slot.connection.healthy).sort((a,b)=>a.inFlight-b.inFlight)[0];
207
- if(!slot||reads.inFlight>=reads.limit){reads.rejected++;throw new HttpError(503,'Link pool capacity unavailable');}
208
- exporting=true;reads.inFlight++;slot.inFlight++;
209
- const started=performance.now(),expires=Date.now()+deadlineMs;
210
- try {
211
- const header=await slot.connection.exportBegin(collection===undefined?{}:{collection});
212
- let exported=0,afterCollection='',afterCode='';
213
- try {
214
- // Identity and the snapshot's revision are handed over before any record,
215
- // so a truncated stream is recognizable rather than silently short.
216
- await onHeader(header);
217
- for(;;){
218
- if(Date.now()>expires)throw new HttpError(503,'Export deadline exceeded');
219
- const records=await slot.connection.exportPage({afterCollection,afterCode,limit:pageSize});
220
- const last=records.at(-1);
221
- if(!last)break;
222
- await onRecords(records);
223
- exported+=records.length;
224
- afterCollection=last.collection;afterCode=last.code;
225
- }
226
- } finally { await slot.connection.exportEnd().catch(()=>{}); }
227
- reads.completed++;
228
- return {...header,exported};
229
- } catch(error){reads.failed++;throw error;}
230
- finally{exporting=false;reads.inFlight--;slot.inFlight--;reads.durationMs+=performance.now()-started;}
231
- }
232
- const stats=(pool ) =>({connections:pool.connections.length,healthyConnections:pool.connections.filter(s=>s.connection.healthy).length,limit:pool.limit,inFlight:pool.inFlight,completed:pool.completed,failed:pool.failed,rejected:pool.rejected,durationMs:pool.durationMs});
233
- return {
234
- get readHealthy(){return healthy(reads);},
235
- get writeHealthy(){return healthy(writes);},
236
- get healthy(){return healthy(reads)&&(!writer||healthy(writes));},
237
- atomicAudit:true,
238
- stats:()=>({closed,exporting,read:stats(reads),write:stats(writes)}),
239
- exportSnapshot,
240
- get:(collection,code)=>run(reads,c=>c.get(collection,code)),list:(collection,options)=>run(reads,c=>c.list(collection,options)),
241
- create:(collection,data,code,audit)=>run(writes,c=>c.create(collection,data,code,audit)),update:(collection,code,data,expectedVersion,audit)=>run(writes,c=>c.update(collection,code,data,expectedVersion,audit)),delete:(collection,code,expectedVersion,audit)=>run(writes,c=>c.delete(collection,code,expectedVersion,audit)),
242
- close(){
243
- if(closing)return closing;closed=true;
244
- closing=(async()=>{
245
- const results=await Promise.allSettled([...read,...(writer?[writer]:[])].map(connection=>connection.close()));
246
- const failed=results.find((result) =>result.status==='rejected');if(failed)throw failed.reason;
247
- })();return closing;
248
- },
249
- };
250
- }
@@ -1,40 +0,0 @@
1
- import {constants} from 'node:fs';
2
- import {open} from 'node:fs/promises';
3
- import {createHash,timingSafeEqual} from 'node:crypto';
4
- import {outsideProject} from './link-store.js';
5
- import {assert} from './errors.js';
6
- /** A credential from the operator's management policy file, once it authenticated a request. */
7
-
8
-
9
-
10
- const actions =['get','list','create','update','delete'];
11
- const isRecord=(value ) =>value!==null && typeof value==='object' && !Array.isArray(value);
12
- export async function managementPolicy(file ,project ) {
13
- const path=await outsideProject(file,project);
14
- return async token=>{
15
- const handle=await open(path,constants.O_RDONLY | (constants.O_NOFOLLOW||0));
16
- let document ;
17
- try {
18
- const info=await handle.stat();
19
- assert(info.isFile() && info.nlink===1 && info.size<=65536 && (process.platform==='win32'||(info.mode&0o077)===0),'Invalid private management policy');
20
- const buffer=Buffer.alloc(65537);let offset=0;
21
- while(offset<buffer.length){const {bytesRead}=await handle.read(buffer,offset,buffer.length-offset,null);if(!bytesRead)break;offset+=bytesRead;}
22
- assert(offset<=65536,'Management policy exceeds 64 KiB');
23
- document=JSON.parse(new TextDecoder('utf-8',{fatal:true}).decode(buffer.subarray(0,offset)));
24
- } finally {await handle.close();}
25
- assert(isRecord(document) && document.version===1 && Object.keys(document).every(k=>['version','credentials'].includes(k)) && Array.isArray(document.credentials) && document.credentials.length<=128,'Invalid management policy');
26
- const ids=new Set (),hashes=new Set ();let principal ;
27
- const digest=createHash('sha256').update(token).digest();
28
- for(const entry of document.credentials ){
29
- assert(isRecord(entry) && Object.keys(entry).every(k=>['id','sha256','expires','revoked','collections','actions'].includes(k)) && typeof entry.id==='string' && /^[A-Za-z0-9_-]{1,64}$/.test(entry.id) && !ids.has(entry.id),'Invalid credential identity');ids.add(entry.id);
30
- assert(typeof entry.sha256==='string' && /^[a-f0-9]{64}$/.test(entry.sha256) && !hashes.has(entry.sha256),'Invalid credential hash');hashes.add(entry.sha256);
31
- assert(typeof entry.expires==='string' && /^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\dZ$/.test(entry.expires) && Number.isFinite(Date.parse(entry.expires)) && new Date(entry.expires).toISOString()===entry.expires.replace('Z','.000Z'),'Credential expiry required');
32
- assert(entry.revoked===undefined || typeof entry.revoked==='boolean','Invalid revocation');
33
- assert(Array.isArray(entry.collections) && entry.collections.length>0 && entry.collections.length<=100 && entry.collections.every(v=>typeof v==='string' && /^[A-Za-z][A-Za-z0-9_-]{0,63}$/.test(v)),'Invalid collection scope');
34
- assert(Array.isArray(entry.actions) && entry.actions.length>0 && entry.actions.every(a=>actions.includes(a)),'Invalid action scope');
35
- const c=entry ; // validated field by field above
36
- if(timingSafeEqual(digest,Buffer.from(c.sha256,'hex')) && !c.revoked && Date.parse(c.expires)>Date.now())principal=c;
37
- }
38
- return principal;
39
- };
40
- }
@@ -1,6 +0,0 @@
1
- // https://www.sqlite.org/wal.html#the_wal_reset_bug
2
- export function supportsConcurrentWal(version ) {
3
- const [major=NaN,minor=NaN,patch=NaN]=String(version).split('.').map(Number);
4
- if(![major,minor,patch].every(Number.isInteger))return false;
5
- return major>3 || major===3 && (minor>51 || minor===51&&patch>=3 || minor===50&&patch>=7 || minor===44&&patch>=6);
6
- }
@@ -1,30 +0,0 @@
1
- import type { AddressInfo } from 'node:net';
2
- import type { AuditIdentity, LinkRow, ListOptions } from './link-store.ts';
3
- import type { ManagementAuthorizer } from './management-policy.ts';
4
- import type { LogFn } from './types.ts';
5
- /** The store surface management needs: the pooled link store, or anything with the same contract. */
6
- export interface ManagementStore {
7
- readonly atomicAudit?: boolean;
8
- get(collection: string, code: string): Promise<LinkRow | null>;
9
- list(collection: string, options?: ListOptions): Promise<LinkRow[]>;
10
- create(collection: string, data: unknown, code?: unknown, audit?: AuditIdentity | undefined): Promise<LinkRow>;
11
- update(collection: string, code: string, data: unknown, expectedVersion: unknown, audit?: AuditIdentity | undefined): Promise<LinkRow>;
12
- delete(collection: string, code: string, expectedVersion: unknown, audit?: AuditIdentity | undefined): Promise<boolean>;
13
- }
14
- export interface LinkApiOptions {
15
- store: ManagementStore;
16
- collection: string;
17
- token?: string | undefined;
18
- authorize?: ManagementAuthorizer | undefined;
19
- host?: string | undefined;
20
- port?: number | undefined;
21
- maxInFlightRequests?: number | undefined;
22
- socketTimeoutMs?: number | undefined;
23
- log?: LogFn | undefined;
24
- }
25
- export interface LinkApi {
26
- address: AddressInfo;
27
- close(): Promise<void>;
28
- }
29
- export declare function loadLinkToken(file: unknown, project: string): Promise<string>;
30
- export declare function startLinkApi({ store, collection, token, authorize, host, port, maxInFlightRequests, socketTimeoutMs, log }: LinkApiOptions): Promise<LinkApi>;
@@ -1,37 +0,0 @@
1
- /** The parsed command-line values the links commands read; every flag is a string or absent. */
2
- interface LinkCommandValues {
3
- project?: string | undefined;
4
- store?: string | undefined;
5
- collection?: string | undefined;
6
- code?: string | undefined;
7
- destination?: string | undefined;
8
- status?: string | undefined;
9
- enabled?: string | undefined;
10
- expires?: string | undefined;
11
- 'if-version'?: string | undefined;
12
- limit?: string | undefined;
13
- after?: string | undefined;
14
- input?: string | undefined;
15
- 'page-size'?: string | undefined;
16
- 'auth-file'?: string | undefined;
17
- 'token-file'?: string | undefined;
18
- port?: string | undefined;
19
- host?: string | undefined;
20
- 'link-readers'?: string | undefined;
21
- 'link-read-limit'?: string | undefined;
22
- 'link-write-limit'?: string | undefined;
23
- }
24
- export type Print = (value: unknown) => unknown;
25
- export interface LinkPoolOptions {
26
- readers?: number;
27
- maxReads?: number;
28
- maxWrites?: number;
29
- }
30
- export interface LinkBinding extends LinkPoolOptions {
31
- collection: string;
32
- file: string;
33
- }
34
- export declare function linkPoolOptions(values: LinkCommandValues): LinkPoolOptions;
35
- export declare function parseLinkBinding(value: string | undefined, options?: LinkPoolOptions): LinkBinding | undefined;
36
- export declare function runLinkCommand(action: string | undefined, values: LinkCommandValues, print: Print): Promise<void>;
37
- export {};
@@ -1,27 +0,0 @@
1
- export interface LinkEvent {
2
- outcome: string;
3
- code?: string | null;
4
- [field: string]: unknown;
5
- }
6
- export interface LinkObserverOptions {
7
- observe: (event: LinkEvent) => unknown;
8
- includeCode?: boolean;
9
- maxQueue?: number;
10
- timeoutMs?: number;
11
- }
12
- export interface LinkObserverStats {
13
- queued: number;
14
- delivered: number;
15
- dropped: number;
16
- failed: number;
17
- timedOut: number;
18
- closed: boolean;
19
- }
20
- export interface LinkObserver {
21
- emit(event: LinkEvent): void;
22
- stats(): LinkObserverStats;
23
- close(): Promise<LinkObserverStats>;
24
- }
25
- type Log = (event: Record<string, unknown>) => void;
26
- export declare function createLinkObserver(options: unknown, log?: Log): LinkObserver | undefined;
27
- export {};
@@ -1,11 +0,0 @@
1
- export interface LinkRecord {
2
- url: string;
3
- status: number;
4
- enabled: boolean;
5
- expires: string | null;
6
- }
7
- export declare function linkCollection(value: unknown): string;
8
- export declare function linkCode(value: unknown): string;
9
- export declare function linkVersion(value: unknown): number;
10
- export declare function linkData(value: unknown): LinkRecord;
11
- export declare const randomLinkCode: () => string;
@@ -1 +0,0 @@
1
- export {};
@@ -1,130 +0,0 @@
1
- import type { LinkRecord } from './link-records.ts';
2
- import type { LogFn } from './types.ts';
3
- /** A stored link as the worker returns it. */
4
- export interface LinkRow extends LinkRecord {
5
- collection: string;
6
- code: string;
7
- version: number;
8
- }
9
- export interface ListOptions {
10
- limit?: number | undefined;
11
- after?: string | undefined;
12
- }
13
- export interface AuditIdentity {
14
- actor?: string | undefined;
15
- requestId?: string | undefined;
16
- }
17
- export interface ExportHeader {
18
- format: string;
19
- schemaVersion: number;
20
- applicationId: number;
21
- collection: string | null;
22
- revision: number;
23
- records: number;
24
- generatedAt: string;
25
- }
26
- export interface ExportPageOptions {
27
- afterCollection?: string | undefined;
28
- afterCode?: string | undefined;
29
- limit?: number | undefined;
30
- }
31
- export interface ExportOptions {
32
- collection?: string | undefined;
33
- pageSize?: number | undefined;
34
- deadlineMs?: number | undefined;
35
- }
36
- export interface ExportHandlers {
37
- onHeader?: ((header: ExportHeader) => unknown) | undefined;
38
- onRecords?: ((records: LinkRow[]) => unknown) | undefined;
39
- }
40
- export interface ExportSummary extends ExportHeader {
41
- exported: number;
42
- }
43
- export interface PoolStats {
44
- connections: number;
45
- healthyConnections: number;
46
- limit: number;
47
- inFlight: number;
48
- completed: number;
49
- failed: number;
50
- rejected: number;
51
- durationMs: number;
52
- }
53
- export interface LinkStoreStats {
54
- closed: boolean;
55
- exporting: boolean;
56
- read: PoolStats;
57
- write: PoolStats;
58
- }
59
- export interface LinkStoreWorkerData {
60
- file: string;
61
- readOnly: boolean;
62
- }
63
- export type LinkStoreOperation = 'get' | 'list' | 'create' | 'update' | 'delete' | 'exportBegin' | 'exportPage' | 'exportEnd' | 'close';
64
- export interface LinkStoreArgs extends ListOptions, ExportPageOptions {
65
- collection?: string | undefined;
66
- code?: unknown;
67
- data?: unknown;
68
- expectedVersion?: unknown;
69
- audit?: AuditIdentity | undefined;
70
- }
71
- export interface LinkStoreCommand {
72
- id: number;
73
- operation: LinkStoreOperation;
74
- args: LinkStoreArgs;
75
- }
76
- export type LinkStoreReply = {
77
- ready: true;
78
- } | {
79
- failed: true;
80
- } | {
81
- id: number;
82
- value: unknown;
83
- } | {
84
- id: number;
85
- error: {
86
- status: number;
87
- message: string;
88
- };
89
- };
90
- /** One worker-backed SQLite connection. */
91
- export interface LinkConnection {
92
- readonly healthy: boolean;
93
- get(collection: string, code: string): Promise<LinkRow | null>;
94
- list(collection: string, options?: ListOptions): Promise<LinkRow[]>;
95
- exportBegin(options?: {
96
- collection?: string | undefined;
97
- }): Promise<ExportHeader>;
98
- exportPage(options?: ExportPageOptions): Promise<LinkRow[]>;
99
- exportEnd(): Promise<boolean>;
100
- create(collection: string, data: unknown, code?: unknown, audit?: AuditIdentity | undefined): Promise<LinkRow>;
101
- update(collection: string, code: string, data: unknown, expectedVersion: unknown, audit?: AuditIdentity | undefined): Promise<LinkRow>;
102
- delete(collection: string, code: string, expectedVersion: unknown, audit?: AuditIdentity | undefined): Promise<boolean>;
103
- close(): Promise<void>;
104
- }
105
- /** The pooled store openLinkStore returns. */
106
- export interface LinkStore {
107
- readonly readHealthy: boolean;
108
- readonly writeHealthy: boolean;
109
- readonly healthy: boolean;
110
- readonly atomicAudit: true;
111
- stats(): LinkStoreStats;
112
- exportSnapshot(options?: ExportOptions, handlers?: ExportHandlers): Promise<ExportSummary>;
113
- get(collection: string, code: string): Promise<LinkRow | null>;
114
- list(collection: string, options?: ListOptions): Promise<LinkRow[]>;
115
- create(collection: string, data: unknown, code?: unknown, audit?: AuditIdentity | undefined): Promise<LinkRow>;
116
- update(collection: string, code: string, data: unknown, expectedVersion: unknown, audit?: AuditIdentity | undefined): Promise<LinkRow>;
117
- delete(collection: string, code: string, expectedVersion: unknown, audit?: AuditIdentity | undefined): Promise<boolean>;
118
- close(): Promise<void>;
119
- }
120
- export interface LinkStoreOptions {
121
- file?: string | undefined;
122
- project?: string | undefined;
123
- readOnly?: boolean | undefined;
124
- readers?: number | undefined;
125
- maxReads?: number | undefined;
126
- maxWrites?: number | undefined;
127
- log?: LogFn | undefined;
128
- }
129
- export declare function outsideProject(file: unknown, project: string): Promise<string>;
130
- export declare function openLinkStore({ file, project, readOnly, readers, maxReads, maxWrites, log }?: LinkStoreOptions): Promise<LinkStore>;
@@ -1,8 +0,0 @@
1
- /** A credential from the operator's management policy file, once it authenticated a request. */
2
- export interface ManagementPrincipal {
3
- id: string;
4
- collections: string[];
5
- actions: string[];
6
- }
7
- export type ManagementAuthorizer = (token: string) => Promise<ManagementPrincipal | undefined>;
8
- export declare function managementPolicy(file: unknown, project: string): Promise<ManagementAuthorizer>;
@@ -1 +0,0 @@
1
- export declare function supportsConcurrentWal(version: unknown): boolean;
@@ -1,61 +0,0 @@
1
- # Dynamic short links without reloads
2
-
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.
6
-
7
- ## Node build requirement
8
-
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).
10
-
11
- ## Behavior in YAML, data outside Git
12
-
13
- The `link` handler and the operator store binding. Read [setup](links/setup.md#behavior-in-yaml-data-outside-git).
14
-
15
- ## Run the included example
16
-
17
- `links init`, `links create` and `serve --link-store` against a private data directory. Read [setup](links/setup.md#run-the-included-example).
18
-
19
- ## Update, disable, expire, list and delete
20
-
21
- `links get/list/update/delete` and the optimistic version rules. Read [cli](links/cli.md#update-disable-expire-list-and-delete).
22
-
23
- ## Consistent operator export and restore
24
-
25
- `links export` holds one read transaction; `links import` refuses occupied collections. Read [cli](links/cli.md#consistent-operator-export-and-restore).
26
-
27
- ## A separate authenticated management API
28
-
29
- `links api`: bearer token, endpoints, status codes and scope. Read [management-api](links/management-api.md#a-separate-authenticated-management-api).
30
-
31
- ## Persistence, bounds and recovery
32
-
33
- Pools, deadlines, worker replacement with backoff, record caps and backups. Read [limits](links/limits.md#persistence-bounds-and-recovery).
34
-
35
- ## Middleware, sandbox and tests
36
-
37
- What guest code can and cannot see; fixtures for `test` and `audit`. Read [setup](links/setup.md#middleware-sandbox-and-tests).
38
-
39
- ## Opt-in completed-redirect events
40
-
41
- The `linkEvents` observer: outcomes, redaction, bounded delivery. Read [limits](links/limits.md#opt-in-completed-redirect-events).
42
-
43
- ## Shutdown and management defaults
44
-
45
- Loopback defaults, drain on close, unknown outcomes after a timeout. Read [pools](links/pools.md#shutdown-and-management-defaults).
46
-
47
- ## Explicit project opt-in
48
-
49
- `dynamicLinks: true` in the entry file only; what enabling changes. Read [setup](links/setup.md#explicit-project-opt-in).
50
-
51
- ## Separate reader and writer pools
52
-
53
- `--link-readers`, `--link-read-limit`, `--link-write-limit` and `stats()`. Read [pools](links/pools.md#separate-reader-and-writer-pools).
54
-
55
- ## Management HTTP and audit safeguards
56
-
57
- Admission, socket timeout and `management_request` events. Read [management-api](links/management-api.md#management-http-and-audit-safeguards).
58
-
59
- ## Management hardening baseline
60
-
61
- Loopback only, `--auth-file` credentials, durable audit rows. Read [management-api](links/management-api.md#management-hardening-baseline).