@lunora/container 1.0.0-alpha.90 → 1.0.0-alpha.91
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/do/index.mjs +1 -1
- package/package.json +1 -1
package/dist/do/index.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{LunoraError as u}from"@lunora/errors";import{a as A}from"../packem_shared/abort-deadline-jwbW9Ala.mjs";import{isManagedImage as C,resolveContainerEnvVars as I,parseDurationSeconds as L}from"../packem_shared/DURABLE_OBJECT_POLICY_FORBIDDEN_KEYS-gYi8RBUO.mjs";import{D as H,C as O,p as M}from"../packem_shared/exec-BYLW70sR.mjs";import{a as j}from"../packem_shared/jurisdiction-DtE9s70w.mjs";import{RpcTarget as V}from"cloudflare:workers";import{Container as U}from"../packem_shared/ContainerProxy-CfXjS6ZI.mjs";import{ContainerProxy as wt,outboundParams as ft}from"../packem_shared/ContainerProxy-CfXjS6ZI.mjs";const k="lunora",B=(s,t,e,n)=>({container:s,event:e,instance:t,level:e==="error"?"error":"info",message:n,source:k,ts:Date.now(),type:"container"}),p=(s,t,e,n)=>{const r=B(s,t,e,n),a=JSON.stringify(r);return e==="error"?console.error(a):console.log(a),r},F=async(s,t,e,n)=>{const r=t.maxOutputBytes??H,a=await s([t.command,...t.args??[]],{...t.cwd===void 0?{}:{cwd:t.cwd},env:{...e,...t.env},stderr:"pipe",stdout:"pipe"}),o=new AbortController;let i=!1;const c={overflowed:!1,timedOut:!1},v=()=>c.timedOut,m=a.exitCode.finally(()=>{i=!0}),y=()=>{o.abort(),i||(i=!0,a.kill())},$=t.timeoutMs===void 0?void 0:setTimeout(()=>{c.timedOut=!0,y()},t.timeoutMs);let S=0;const R=async l=>{if(l===null)return"";const d=l.getReader(),w=new TextDecoder,h=()=>{d.cancel().catch(()=>{})};let E="";o.signal.addEventListener("abort",h);try{for(;!o.signal.aborted;){const{done:P,value:T}=await d.read();if(P)break;if(S+=T.byteLength,S>r){c.overflowed=!0,y();break}E+=w.decode(T,{stream:!0})}return E+w.decode()}finally{o.signal.removeEventListener("abort",h),d.cancel().catch(()=>{})}};try{const[l,d]=await Promise.allSettled([R(a.stdout),R(a.stderr)]);if(c.overflowed)return new Response(`container "${n}": exec output exceeded ${String(r)} bytes; the process was killed`,{status:413});if(c.timedOut)return new Response(`container "${n}": exec timed out after ${String(t.timeoutMs)}ms`,{status:504});if(l.status==="rejected")throw l.reason;if(d.status==="rejected")throw d.reason;const w=await m;if(v())return new Response(`container "${n}": exec timed out after ${String(t.timeoutMs)}ms`,{status:504});const h=JSON.stringify({code:w,stderr:d.value,stdout:l.value});return new TextEncoder().encode(h).byteLength>r?new Response(`container "${n}": exec output exceeded ${String(r)} bytes once encoded`,{status:413}):new Response(h,{headers:{"content-type":"application/json"}})}finally{clearTimeout($)}},J=async(s,t,e,n)=>{let r;try{r=await t.json()}catch{return new Response(`container "${n}": exec body must be JSON`,{status:400})}return typeof r.command!="string"||r.command.length===0?new Response(`container "${n}": exec requires a non-empty \`command\``,{status:400}):F(s,r,e,n)},z="__lunora_admin__:recordContainerEvent",N="__root__",G=s=>{if(s===null||typeof s!="object")return!1;const t=s;return typeof t.get=="function"&&typeof t.idFromName=="function"},Q=(s,t)=>{const e=j(s,t);return typeof e.getByName=="function"?e.getByName(N):e.get(e.idFromName(N))},W=async(s,t,e)=>{try{const n=s??{},r=n.SHARD;if(!G(r))return;const a=typeof n.LUNORA_ADMIN_TOKEN=="string"?n.LUNORA_ADMIN_TOKEN:void 0;if(!a||a.length===0)return;const o=new Request("https://shard.internal/rpc",{body:JSON.stringify({args:{event:t},functionPath:z}),headers:{authorization:`Bearer ${a}`,"content-type":"application/json"},method:"POST"});await Q(r,e).fetch(o)}catch{}},K=1048576,_=s=>{if(s==null)return null;const{readable:t,writable:e}=new TransformStream(void 0,new ByteLengthQueuingStrategy({highWaterMark:K}));return s.pipeTo(e).catch(()=>{}),t};class Y extends V{#t;#e;constructor(t,e){super(),this.#t=t,this.#e=e}async exitCode(){return this.#e}kill(t){this.#t.kill(t)}resize(t,e){if(this.#t.isPty!==!0||typeof this.#t.resize!="function")throw new TypeError("resize() needs a process spawned with `pty`");this.#t.resize(t,e)}}const X=async(s,t,e,n)=>{const r=t.pty===void 0?void 0:{cols:t.pty.cols??80,rows:t.pty.rows??24};let a;try{a=await s([t.command,...t.args??[]],{...t.cwd===void 0?{}:{cwd:t.cwd},env:{...e,...t.env},...r===void 0?{stderr:"pipe"}:{pty:r},...t.stdin===void 0||t.stdin===!1?{}:{stdin:t.stdin===!0?"pipe":t.stdin},stdout:"pipe"})}catch(c){throw n(),c}const o=A(void 0,t.timeoutMs,()=>new DOMException(`spawn timed out after ${String(t.timeoutMs)}ms`,"TimeoutError"));o.signal?.addEventListener("abort",()=>{a.kill()});const i=a.exitCode.finally(()=>{o.dispose(),n()});return i.catch(()=>{}),{control:new Y(a,i),isPty:a.isPty===!0,pid:a.pid??-1,stderr:_(a.stderr),stdin:t.stdin===!0?a.stdin??null:null,stdout:_(a.stdout)}},f="__lunoraStartOverride",Z=(s,t)=>{if(s===void 0||t===void 0)return s===t;const e=Object.keys(s);return e.length===Object.keys(t).length&&e.every(n=>Object.hasOwn(t,n)&&s[n]===t[n])},q=(s,t)=>Z(s.envVars,t.envVars)&&s.image===t.image&&JSON.stringify(s.instanceType)===JSON.stringify(t.instanceType),tt=(s,t,e)=>{if(C(t))return t;const n=s[t];if(n===void 0){const r=Object.keys(s);throw new u("BAD_REQUEST",`container "${e}": no image named "${t}" — ${r.length===0?"the container declares no images":`declared images: ${r.join(", ")}`}`)}return n},x=500,b=3e4,g="__lunoraHardTimeoutGeneration",D="__lunora",et=/[/\\]/u,nt=s=>{const t=s.indexOf(";");return t===-1?s:s.slice(0,t)},rt=s=>M(s,e=>e.split(et).some(n=>nt(n).toLowerCase()===D)),at=s=>{const t=s instanceof Request?s.url:String(s);return URL.parse(t,"https://container")?.pathname??t},st="cf-container-target-port";class pt extends U{lunoraName;lunoraJurisdiction;lunoraDefaultPort;lunoraHardTimeoutSeconds;lunoraReadiness;lunoraReadyOn;lunoraSecretsStore;lunoraDeclaredEnv;lunoraOverride;lunoraOverrideLoaded=!1;lunoraSecretsStoreResolved;lunoraDurableObjectScheduled;lunoraDefaultSelection;lunoraStops=0;constructor(t,e,n,r,a){const o=I(n,e,r);super(t,e,{defaultPort:n.defaultPort,entrypoint:n.entrypoint?[...n.entrypoint]:void 0,envVars:o,sleepAfter:n.sleepAfter}),this.lunoraDeclaredEnv={...o},n.enableInternet!==void 0&&(this.enableInternet=n.enableInternet),n.requiredPorts!==void 0&&(this.requiredPorts=[...n.requiredPorts]),n.interceptHttps!==void 0&&(this.interceptHttps=n.interceptHttps),n.allowedHosts!==void 0&&(this.allowedHosts=[...n.allowedHosts]),n.deniedHosts!==void 0&&(this.deniedHosts=[...n.deniedHosts]),n.pingEndpoint!==void 0&&(this.pingEndpoint=n.pingEndpoint),n.labels!==void 0&&(this.labels={...n.labels}),this.lunoraName=r??"container",this.lunoraJurisdiction=a,this.lunoraDefaultPort=n.defaultPort,this.lunoraReadyOn=n.readyOn?[...n.readyOn]:[],this.lunoraHardTimeoutSeconds=n.hardTimeout===void 0?void 0:L(n.hardTimeout),this.lunoraSecretsStore=n.secretsStore,this.lunoraDurableObjectScheduled=n.schedulingPolicy==="durable_object",this.lunoraDefaultSelection=n.schedulingPolicy==="durable_object"?{image:n.image,instanceType:n.instanceType}:{}}async lunoraSnapshot(t={}){if(!this.lunoraDurableObjectScheduled)throw new u("BAD_REQUEST",`container "${this.lunoraName}": snapshots need schedulingPolicy "durable_object"`);const{container:e}=this.ctx;if(e?.running!==!0||typeof e.snapshotContainer!="function")throw new u("BAD_REQUEST",`container "${this.lunoraName}": snapshot() needs a running container — start it first`);const{id:n,name:r,size:a}=await e.snapshotContainer({...t.name===void 0?{}:{name:t.name}});return{id:n,size:a,...r===void 0?{}:{name:r}}}async fetch(t){return this.refuseReserved(new URL(t.url).pathname)??super.fetch(t)}async containerFetch(...t){const e=this.refuseReserved(at(t[0]));return e!==void 0?e:(await this.awaitReadinessGate(),super.containerFetch(...t))}async lunoraExec(t){const{pathname:e}=new URL(t.url);if(t.method!=="POST"||e!==O)return new Response(`container "${this.lunoraName}": lunoraExec only serves POST ${O}.`,{status:400});const n=t.headers.get(st),r=n===null?this.defaultPort:Number.parseInt(n,10);if(r===void 0||Number.isNaN(r))return new Response(`container "${this.lunoraName}": exec needs a port — set \`defaultPort\` or exec through \`.port(n)\`.`,{status:400});await this.awaitReadinessGate();const{container:a}=this.ctx;if(typeof a?.exec!="function")return super.containerFetch(t,r);await this.startAndWaitForPorts(r),this.inflightRequests+=1,this.renewActivityTimeout();try{return await J(a.exec.bind(a),t,this.envVars??{},this.lunoraName)}finally{this.decrementInflight()}}async lunoraSpawn(t){const{container:e,release:n}=await this.lunoraAcquire("spawn");return X(e.exec.bind(e),t,this.envVars??{},n)}async startAndWaitForPorts(...t){await this.applyStartEnv(),await this.applyStartSelection(),await this.beforeContainerStart();const e=this.lunoraStops,n=this.beginStart();await super.startAndWaitForPorts(...t),await this.afterContainerStart(n&&this.lunoraStops===e)}async start(t,e){const{image:n,instanceType:r,snapshot:a,...o}=t??{};await this.recordStartOverride({envVars:t?.envVars,image:n,instanceType:r},a),await this.applyStartEnv(),await this.applyStartSelection(a!==void 0),await this.beforeContainerStart();const i=this.lunoraStops,c=this.beginStart();await super.start({...o,...a===void 0?{}:{containerSnapshot:a}},e),await this.afterContainerStart(c&&this.lunoraStops===i)}async destroy(){await super.destroy(),await this.ctx.storage.delete(f),this.lunoraOverride=void 0}async onActivityExpired(){const t=p(this.lunoraName,this.instanceId(),"sleep");this.surfaceInStudioLogs(t),await super.onActivityExpired()}onError(t){const e=p(this.lunoraName,this.instanceId(),"error",t instanceof Error?t.message:String(t));return this.surfaceInStudioLogs(e),super.onError(t)}async onStart(){const t=p(this.lunoraName,this.instanceId(),"start");this.surfaceInStudioLogs(t),await super.onStart()}async onHardTimeoutExpired(t){const e=await this.ctx.storage.get(g);if(t?.generation!==void 0&&t.generation!==e||this.ctx.container?.running!==!0)return;const n=p(this.lunoraName,this.instanceId(),"stop","hard timeout reached");this.surfaceInStudioLogs(n),await this.stop()}async onStop(t){const e=p(this.lunoraName,this.instanceId(),"stop",`${t.reason} (exit ${String(t.exitCode)})`);this.surfaceInStudioLogs(e),this.lunoraReadiness=void 0,this.lunoraStops+=1,await super.onStop(t)}async lunoraAcquire(t){const{container:e}=this.ctx;if(typeof e?.exec!="function")throw new u("NOT_IMPLEMENTED",`container "${this.lunoraName}": ${t}() needs the runtime's native ctx.container.exec(), which this host does not provide`);await this.awaitReadinessGate(),e.running||await this.start(),this.inflightRequests+=1,this.renewActivityTimeout();let n=!1;return{container:e,release:()=>{n||(n=!0,this.decrementInflight())}}}async beforeContainerStart(){}refuseReserved(t){if(rt(t))return new Response(`container "${this.lunoraName}": /${D}/* is reserved for Lunora's own container routes; use exec.`,{status:403})}async afterContainerStart(t){const e=this.lunoraReadiness;if(e!==void 0){await e;return}const n=(async()=>{t||await this.armHardTimeout(),await this.awaitContainerReadiness()})();this.lunoraReadiness=n;try{await n}catch(r){throw this.lunoraReadiness===n&&(this.lunoraReadiness=void 0),r}}beginStart(){const t=this.ctx.container?.running===!0;return t||(this.lunoraReadiness=void 0),t}async awaitReadinessGate(){if(this.lunoraReadyOn.length===0)return;if(this.lunoraReadiness===void 0){const{status:e}=await this.getState();if(e!=="healthy")return;this.lunoraReadiness=this.awaitContainerReadiness()}const t=this.lunoraReadiness;try{await t}catch(e){throw this.lunoraReadiness===t&&(this.lunoraReadiness=void 0),e}}async armHardTimeout(){if(this.lunoraHardTimeoutSeconds===void 0)return;const t=(await this.ctx.storage.get(g)??0)+1;await this.ctx.storage.put(g,t),await this.schedule(this.lunoraHardTimeoutSeconds,"onHardTimeoutExpired",{generation:t})}async resolveSecretsStoreEnv(){const t=this.lunoraSecretsStore;return t===void 0?{}:(this.lunoraSecretsStoreResolved??=(async()=>{const e=this.env,n={};for(const[r,a]of Object.entries(t)){const o=e[a];if(o===void 0||typeof o.get!="function")throw new u("INTERNAL",`container "${this.lunoraName}": secretsStore env "${r}" points at binding "${a}", which is not a Secrets Store binding on the Worker env. Add a \`secrets_store_secrets\` entry binding "${a}".`);const i=await o.get();if(typeof i!="string")throw new TypeError(`container "${this.lunoraName}": Secrets Store binding "${a}" (env "${r}") did not resolve to a string value.`);n[r]=i}return n})().catch(e=>{throw this.lunoraSecretsStoreResolved=void 0,e}),this.lunoraSecretsStoreResolved)}async readOverride(){return this.lunoraOverrideLoaded||(this.lunoraOverride=await this.ctx.storage.get(f),this.lunoraOverrideLoaded=!0),this.lunoraOverride??{}}async recordStartOverride(t,e){const n=t.image!==void 0||t.instanceType!==void 0;if((n||e!==void 0)&&!this.lunoraDurableObjectScheduled)throw new u("BAD_REQUEST",`container "${this.lunoraName}": start({ image | instanceType | snapshot }) needs schedulingPolicy "durable_object" — a default-policy container's image and size are set in lunora/containers.ts`);if(t.image!==void 0&&e!==void 0)throw new u("BAD_REQUEST",`container "${this.lunoraName}": start() takes an image or a snapshot, not both — the snapshot is the filesystem`);t.image!==void 0&&this.resolveImage(t.image);const r=await this.readOverride(),a={...r,...t.envVars===void 0?{}:{envVars:{...t.envVars}},...t.image===void 0?{}:{image:t.image},...t.instanceType===void 0?{}:{instanceType:t.instanceType}};if((this.ctx.container?.running===!0||this.startInFlight!==void 0)&&(e!==void 0||!q({...this.lunoraDefaultSelection,...r},{...this.lunoraDefaultSelection,...a})))throw new u("CONFLICT",`container "${this.lunoraName}": start() on an instance that is already running with a different env, image or size (or a snapshot to restore) — the running container keeps its own. stop() it first (or destroy() it to also drop the override), then start again.`);(n||t.envVars!==void 0)&&(await this.ctx.storage.put(f,a),this.lunoraOverride=a)}async applyStartEnv(){const{envVars:t}=await this.readOverride();this.envVars=t===void 0?{...this.lunoraDeclaredEnv,...await this.resolveSecretsStoreEnv()}:{...t}}async applyStartSelection(t=!1){if(!this.lunoraDurableObjectScheduled)return;const e={...this.lunoraDefaultSelection,...await this.readOverride()};if(e.image===void 0&&!t&&this.ctx.container?.running!==!0)throw new u("BAD_REQUEST",`container "${this.lunoraName}": no image to start — pass start({ image }) or start({ snapshot }), or set a default \`image\` in lunora/containers.ts`);this.instance=e.instanceType,this.image=e.image===void 0?void 0:this.resolveImage(e.image)}resolveImage(t){return tt(this.ctx.container?.images??{},t,this.lunoraName)}async awaitContainerReadiness(){if(this.lunoraReadyOn.length===0)return;const{container:t}=this.ctx;if(t===void 0)return;const e=Date.now()+b;await Promise.all(this.lunoraReadyOn.map(async n=>this.awaitReadinessCheck(t,n,e)))}async awaitReadinessCheck(t,e,n){const r=e.port??this.lunoraDefaultPort;if(r===void 0)throw new u("INTERNAL",`container "${this.lunoraName}": readyOn check "${e.path}" has no port — set the check's \`port\` or the container \`defaultPort\`.`);const a=e.status??200,o=e.path.startsWith("/")?e.path:`/${e.path}`,i=t.getTcpPort(r);for(;;){const c=Math.max(x,n-Date.now()),v=A(void 0,c,()=>new DOMException(`readiness probe timed out after ${String(c)}ms`,"TimeoutError"));try{if((await i.fetch(`http://container${o}`,{signal:v.signal})).status===a)return}catch{}finally{v.dispose()}if(Date.now()>=n)throw new u("INTERNAL",`container "${this.lunoraName}": readiness check "${e.path}" (port ${String(r)}) did not return ${String(a)} within ${String(b)}ms`);await new Promise(m=>{setTimeout(m,x)})}}surfaceInStudioLogs(t){W(this.env,t,this.lunoraJurisdiction).catch(()=>{})}instanceId(){try{const{id:t}=this.ctx;return typeof t?.toString=="function"?t.toString():"unknown"}catch{return"unknown"}}}export{wt as ContainerProxy,pt as LunoraContainer,ft as outboundParams};
|
|
1
|
+
import{LunoraError as u}from"@lunora/errors";import{a as A}from"../packem_shared/abort-deadline-jwbW9Ala.mjs";import{isManagedImage as C,resolveContainerEnvVars as I,parseDurationSeconds as L}from"../packem_shared/DURABLE_OBJECT_POLICY_FORBIDDEN_KEYS-gYi8RBUO.mjs";import{D as H,C as O,p as M}from"../packem_shared/exec-BYLW70sR.mjs";import{a as j}from"../packem_shared/jurisdiction-DtE9s70w.mjs";import{RpcTarget as V}from"cloudflare:workers";import{Container as U}from"../packem_shared/ContainerProxy-CfXjS6ZI.mjs";import{ContainerProxy as wt,outboundParams as ft}from"../packem_shared/ContainerProxy-CfXjS6ZI.mjs";const k="lunora",B=(s,t,e,n)=>({container:s,event:e,instance:t,level:e==="error"?"error":"info",message:n,source:k,ts:Date.now(),type:"container"}),v=(s,t,e,n)=>{const r=B(s,t,e,n),a=JSON.stringify(r);return e==="error"?console.error(a):console.log(a),r},F=async(s,t,e,n)=>{const r=t.maxOutputBytes??H,a=await s([t.command,...t.args??[]],{...t.cwd===void 0?{}:{cwd:t.cwd},env:{...e,...t.env},stderr:"pipe",stdout:"pipe"}),o=new AbortController;let i=!1;const c={overflowed:!1,timedOut:!1},m=()=>c.timedOut,h=a.exitCode.finally(()=>{i=!0});h.catch(()=>{});const y=()=>{o.abort(),i||(i=!0,a.kill())},$=t.timeoutMs===void 0?void 0:setTimeout(()=>{c.timedOut=!0,y()},t.timeoutMs);let S=0;const R=async l=>{if(l===null)return"";const d=l.getReader(),w=new TextDecoder,p=()=>{d.cancel().catch(()=>{})};let E="";o.signal.addEventListener("abort",p);try{for(;!o.signal.aborted;){const{done:P,value:T}=await d.read();if(P)break;if(S+=T.byteLength,S>r){c.overflowed=!0,y();break}E+=w.decode(T,{stream:!0})}return E+w.decode()}finally{o.signal.removeEventListener("abort",p),d.cancel().catch(()=>{})}};try{const[l,d]=await Promise.allSettled([R(a.stdout),R(a.stderr)]);if(c.overflowed)return new Response(`container "${n}": exec output exceeded ${String(r)} bytes; the process was killed`,{status:413});if(c.timedOut)return new Response(`container "${n}": exec timed out after ${String(t.timeoutMs)}ms`,{status:504});if(l.status==="rejected")throw l.reason;if(d.status==="rejected")throw d.reason;const w=await h;if(m())return new Response(`container "${n}": exec timed out after ${String(t.timeoutMs)}ms`,{status:504});const p=JSON.stringify({code:w,stderr:d.value,stdout:l.value});return new TextEncoder().encode(p).byteLength>r?new Response(`container "${n}": exec output exceeded ${String(r)} bytes once encoded`,{status:413}):new Response(p,{headers:{"content-type":"application/json"}})}finally{clearTimeout($)}},J=async(s,t,e,n)=>{let r;try{r=await t.json()}catch{return new Response(`container "${n}": exec body must be JSON`,{status:400})}return typeof r.command!="string"||r.command.length===0?new Response(`container "${n}": exec requires a non-empty \`command\``,{status:400}):F(s,r,e,n)},z="__lunora_admin__:recordContainerEvent",N="__root__",G=s=>{if(s===null||typeof s!="object")return!1;const t=s;return typeof t.get=="function"&&typeof t.idFromName=="function"},Q=(s,t)=>{const e=j(s,t);return typeof e.getByName=="function"?e.getByName(N):e.get(e.idFromName(N))},W=async(s,t,e)=>{try{const n=s??{},r=n.SHARD;if(!G(r))return;const a=typeof n.LUNORA_ADMIN_TOKEN=="string"?n.LUNORA_ADMIN_TOKEN:void 0;if(!a||a.length===0)return;const o=new Request("https://shard.internal/rpc",{body:JSON.stringify({args:{event:t},functionPath:z}),headers:{authorization:`Bearer ${a}`,"content-type":"application/json"},method:"POST"});await Q(r,e).fetch(o)}catch{}},K=1048576,_=s=>{if(s==null)return null;const{readable:t,writable:e}=new TransformStream(void 0,new ByteLengthQueuingStrategy({highWaterMark:K}));return s.pipeTo(e).catch(()=>{}),t};class Y extends V{#t;#e;constructor(t,e){super(),this.#t=t,this.#e=e}async exitCode(){return this.#e}kill(t){this.#t.kill(t)}resize(t,e){if(this.#t.isPty!==!0||typeof this.#t.resize!="function")throw new TypeError("resize() needs a process spawned with `pty`");this.#t.resize(t,e)}}const X=async(s,t,e,n)=>{const r=t.pty===void 0?void 0:{cols:t.pty.cols??80,rows:t.pty.rows??24};let a;try{a=await s([t.command,...t.args??[]],{...t.cwd===void 0?{}:{cwd:t.cwd},env:{...e,...t.env},...r===void 0?{stderr:"pipe"}:{pty:r},...t.stdin===void 0||t.stdin===!1?{}:{stdin:t.stdin===!0?"pipe":t.stdin},stdout:"pipe"})}catch(c){throw n(),c}const o=A(void 0,t.timeoutMs,()=>new DOMException(`spawn timed out after ${String(t.timeoutMs)}ms`,"TimeoutError"));o.signal?.addEventListener("abort",()=>{a.kill()});const i=a.exitCode.finally(()=>{o.dispose(),n()});return i.catch(()=>{}),{control:new Y(a,i),isPty:a.isPty===!0,pid:a.pid??-1,stderr:_(a.stderr),stdin:t.stdin===!0?a.stdin??null:null,stdout:_(a.stdout)}},f="__lunoraStartOverride",Z=(s,t)=>{if(s===void 0||t===void 0)return s===t;const e=Object.keys(s);return e.length===Object.keys(t).length&&e.every(n=>Object.hasOwn(t,n)&&s[n]===t[n])},q=(s,t)=>Z(s.envVars,t.envVars)&&s.image===t.image&&JSON.stringify(s.instanceType)===JSON.stringify(t.instanceType),tt=(s,t,e)=>{if(C(t))return t;const n=s[t];if(n===void 0){const r=Object.keys(s);throw new u("BAD_REQUEST",`container "${e}": no image named "${t}" — ${r.length===0?"the container declares no images":`declared images: ${r.join(", ")}`}`)}return n},x=500,b=3e4,g="__lunoraHardTimeoutGeneration",D="__lunora",et=/[/\\]/u,nt=s=>{const t=s.indexOf(";");return t===-1?s:s.slice(0,t)},rt=s=>M(s,e=>e.split(et).some(n=>nt(n).toLowerCase()===D)),at=s=>{const t=s instanceof Request?s.url:String(s);return URL.parse(t,"https://container")?.pathname??t},st="cf-container-target-port";class pt extends U{lunoraName;lunoraJurisdiction;lunoraDefaultPort;lunoraHardTimeoutSeconds;lunoraReadiness;lunoraReadyOn;lunoraSecretsStore;lunoraDeclaredEnv;lunoraOverride;lunoraOverrideLoaded=!1;lunoraSecretsStoreResolved;lunoraDurableObjectScheduled;lunoraDefaultSelection;lunoraStops=0;constructor(t,e,n,r,a){const o=I(n,e,r);super(t,e,{defaultPort:n.defaultPort,entrypoint:n.entrypoint?[...n.entrypoint]:void 0,envVars:o,sleepAfter:n.sleepAfter}),this.lunoraDeclaredEnv={...o},n.enableInternet!==void 0&&(this.enableInternet=n.enableInternet),n.requiredPorts!==void 0&&(this.requiredPorts=[...n.requiredPorts]),n.interceptHttps!==void 0&&(this.interceptHttps=n.interceptHttps),n.allowedHosts!==void 0&&(this.allowedHosts=[...n.allowedHosts]),n.deniedHosts!==void 0&&(this.deniedHosts=[...n.deniedHosts]),n.pingEndpoint!==void 0&&(this.pingEndpoint=n.pingEndpoint),n.labels!==void 0&&(this.labels={...n.labels}),this.lunoraName=r??"container",this.lunoraJurisdiction=a,this.lunoraDefaultPort=n.defaultPort,this.lunoraReadyOn=n.readyOn?[...n.readyOn]:[],this.lunoraHardTimeoutSeconds=n.hardTimeout===void 0?void 0:L(n.hardTimeout),this.lunoraSecretsStore=n.secretsStore,this.lunoraDurableObjectScheduled=n.schedulingPolicy==="durable_object",this.lunoraDefaultSelection=n.schedulingPolicy==="durable_object"?{image:n.image,instanceType:n.instanceType}:{}}async lunoraSnapshot(t={}){if(!this.lunoraDurableObjectScheduled)throw new u("BAD_REQUEST",`container "${this.lunoraName}": snapshots need schedulingPolicy "durable_object"`);const{container:e}=this.ctx;if(e?.running!==!0||typeof e.snapshotContainer!="function")throw new u("BAD_REQUEST",`container "${this.lunoraName}": snapshot() needs a running container — start it first`);const{id:n,name:r,size:a}=await e.snapshotContainer({...t.name===void 0?{}:{name:t.name}});return{id:n,size:a,...r===void 0?{}:{name:r}}}async fetch(t){return this.refuseReserved(new URL(t.url).pathname)??super.fetch(t)}async containerFetch(...t){const e=this.refuseReserved(at(t[0]));return e!==void 0?e:(await this.awaitReadinessGate(),super.containerFetch(...t))}async lunoraExec(t){const{pathname:e}=new URL(t.url);if(t.method!=="POST"||e!==O)return new Response(`container "${this.lunoraName}": lunoraExec only serves POST ${O}.`,{status:400});const n=t.headers.get(st),r=n===null?this.defaultPort:Number.parseInt(n,10);if(r===void 0||Number.isNaN(r))return new Response(`container "${this.lunoraName}": exec needs a port — set \`defaultPort\` or exec through \`.port(n)\`.`,{status:400});await this.awaitReadinessGate();const{container:a}=this.ctx;if(typeof a?.exec!="function")return super.containerFetch(t,r);await this.startAndWaitForPorts(r),this.inflightRequests+=1,this.renewActivityTimeout();try{return await J(a.exec.bind(a),t,this.envVars??{},this.lunoraName)}finally{this.decrementInflight()}}async lunoraSpawn(t){const{container:e,release:n}=await this.lunoraAcquire("spawn");return X(e.exec.bind(e),t,this.envVars??{},n)}async startAndWaitForPorts(...t){await this.applyStartEnv(),await this.applyStartSelection(),await this.beforeContainerStart();const e=this.lunoraStops,n=this.beginStart();await super.startAndWaitForPorts(...t),await this.afterContainerStart(n&&this.lunoraStops===e)}async start(t,e){const{image:n,instanceType:r,snapshot:a,...o}=t??{};await this.recordStartOverride({envVars:t?.envVars,image:n,instanceType:r},a),await this.applyStartEnv(),await this.applyStartSelection(a!==void 0),await this.beforeContainerStart();const i=this.lunoraStops,c=this.beginStart();await super.start({...o,...a===void 0?{}:{containerSnapshot:a}},e),await this.afterContainerStart(c&&this.lunoraStops===i)}async destroy(){await super.destroy(),await this.ctx.storage.delete(f),this.lunoraOverride=void 0}async onActivityExpired(){const t=v(this.lunoraName,this.instanceId(),"sleep");this.surfaceInStudioLogs(t),await super.onActivityExpired()}onError(t){const e=v(this.lunoraName,this.instanceId(),"error",t instanceof Error?t.message:String(t));return this.surfaceInStudioLogs(e),super.onError(t)}async onStart(){const t=v(this.lunoraName,this.instanceId(),"start");this.surfaceInStudioLogs(t),await super.onStart()}async onHardTimeoutExpired(t){const e=await this.ctx.storage.get(g);if(t?.generation!==void 0&&t.generation!==e||this.ctx.container?.running!==!0)return;const n=v(this.lunoraName,this.instanceId(),"stop","hard timeout reached");this.surfaceInStudioLogs(n),await this.stop()}async onStop(t){const e=v(this.lunoraName,this.instanceId(),"stop",`${t.reason} (exit ${String(t.exitCode)})`);this.surfaceInStudioLogs(e),this.lunoraReadiness=void 0,this.lunoraStops+=1,await super.onStop(t)}async lunoraAcquire(t){const{container:e}=this.ctx;if(typeof e?.exec!="function")throw new u("NOT_IMPLEMENTED",`container "${this.lunoraName}": ${t}() needs the runtime's native ctx.container.exec(), which this host does not provide`);await this.awaitReadinessGate(),e.running||await this.start(),this.inflightRequests+=1,this.renewActivityTimeout();let n=!1;return{container:e,release:()=>{n||(n=!0,this.decrementInflight())}}}async beforeContainerStart(){}refuseReserved(t){if(rt(t))return new Response(`container "${this.lunoraName}": /${D}/* is reserved for Lunora's own container routes; use exec.`,{status:403})}async afterContainerStart(t){const e=this.lunoraReadiness;if(e!==void 0){await e;return}const n=(async()=>{t||await this.armHardTimeout(),await this.awaitContainerReadiness()})();this.lunoraReadiness=n;try{await n}catch(r){throw this.lunoraReadiness===n&&(this.lunoraReadiness=void 0),r}}beginStart(){const t=this.ctx.container?.running===!0;return t||(this.lunoraReadiness=void 0),t}async awaitReadinessGate(){if(this.lunoraReadyOn.length===0)return;if(this.lunoraReadiness===void 0){const{status:e}=await this.getState();if(e!=="healthy")return;this.lunoraReadiness=this.awaitContainerReadiness()}const t=this.lunoraReadiness;try{await t}catch(e){throw this.lunoraReadiness===t&&(this.lunoraReadiness=void 0),e}}async armHardTimeout(){if(this.lunoraHardTimeoutSeconds===void 0)return;const t=(await this.ctx.storage.get(g)??0)+1;await this.ctx.storage.put(g,t),await this.schedule(this.lunoraHardTimeoutSeconds,"onHardTimeoutExpired",{generation:t})}async resolveSecretsStoreEnv(){const t=this.lunoraSecretsStore;return t===void 0?{}:(this.lunoraSecretsStoreResolved??=(async()=>{const e=this.env,n={};for(const[r,a]of Object.entries(t)){const o=e[a];if(o===void 0||typeof o.get!="function")throw new u("INTERNAL",`container "${this.lunoraName}": secretsStore env "${r}" points at binding "${a}", which is not a Secrets Store binding on the Worker env. Add a \`secrets_store_secrets\` entry binding "${a}".`);const i=await o.get();if(typeof i!="string")throw new TypeError(`container "${this.lunoraName}": Secrets Store binding "${a}" (env "${r}") did not resolve to a string value.`);n[r]=i}return n})().catch(e=>{throw this.lunoraSecretsStoreResolved=void 0,e}),this.lunoraSecretsStoreResolved)}async readOverride(){return this.lunoraOverrideLoaded||(this.lunoraOverride=await this.ctx.storage.get(f),this.lunoraOverrideLoaded=!0),this.lunoraOverride??{}}async recordStartOverride(t,e){const n=t.image!==void 0||t.instanceType!==void 0;if((n||e!==void 0)&&!this.lunoraDurableObjectScheduled)throw new u("BAD_REQUEST",`container "${this.lunoraName}": start({ image | instanceType | snapshot }) needs schedulingPolicy "durable_object" — a default-policy container's image and size are set in lunora/containers.ts`);if(t.image!==void 0&&e!==void 0)throw new u("BAD_REQUEST",`container "${this.lunoraName}": start() takes an image or a snapshot, not both — the snapshot is the filesystem`);t.image!==void 0&&this.resolveImage(t.image);const r=await this.readOverride(),a={...r,...t.envVars===void 0?{}:{envVars:{...t.envVars}},...t.image===void 0?{}:{image:t.image},...t.instanceType===void 0?{}:{instanceType:t.instanceType}};if((this.ctx.container?.running===!0||this.startInFlight!==void 0)&&(e!==void 0||!q({...this.lunoraDefaultSelection,...r},{...this.lunoraDefaultSelection,...a})))throw new u("CONFLICT",`container "${this.lunoraName}": start() on an instance that is already running with a different env, image or size (or a snapshot to restore) — the running container keeps its own. stop() it first (or destroy() it to also drop the override), then start again.`);(n||t.envVars!==void 0)&&(await this.ctx.storage.put(f,a),this.lunoraOverride=a)}async applyStartEnv(){const{envVars:t}=await this.readOverride();this.envVars=t===void 0?{...this.lunoraDeclaredEnv,...await this.resolveSecretsStoreEnv()}:{...t}}async applyStartSelection(t=!1){if(!this.lunoraDurableObjectScheduled)return;const e={...this.lunoraDefaultSelection,...await this.readOverride()};if(e.image===void 0&&!t&&this.ctx.container?.running!==!0)throw new u("BAD_REQUEST",`container "${this.lunoraName}": no image to start — pass start({ image }) or start({ snapshot }), or set a default \`image\` in lunora/containers.ts`);this.instance=e.instanceType,this.image=e.image===void 0?void 0:this.resolveImage(e.image)}resolveImage(t){return tt(this.ctx.container?.images??{},t,this.lunoraName)}async awaitContainerReadiness(){if(this.lunoraReadyOn.length===0)return;const{container:t}=this.ctx;if(t===void 0)return;const e=Date.now()+b;await Promise.all(this.lunoraReadyOn.map(async n=>this.awaitReadinessCheck(t,n,e)))}async awaitReadinessCheck(t,e,n){const r=e.port??this.lunoraDefaultPort;if(r===void 0)throw new u("INTERNAL",`container "${this.lunoraName}": readyOn check "${e.path}" has no port — set the check's \`port\` or the container \`defaultPort\`.`);const a=e.status??200,o=e.path.startsWith("/")?e.path:`/${e.path}`,i=t.getTcpPort(r);for(;;){const c=Math.max(x,n-Date.now()),m=A(void 0,c,()=>new DOMException(`readiness probe timed out after ${String(c)}ms`,"TimeoutError"));try{if((await i.fetch(`http://container${o}`,{signal:m.signal})).status===a)return}catch{}finally{m.dispose()}if(Date.now()>=n)throw new u("INTERNAL",`container "${this.lunoraName}": readiness check "${e.path}" (port ${String(r)}) did not return ${String(a)} within ${String(b)}ms`);await new Promise(h=>{setTimeout(h,x)})}}surfaceInStudioLogs(t){W(this.env,t,this.lunoraJurisdiction).catch(()=>{})}instanceId(){try{const{id:t}=this.ctx;return typeof t?.toString=="function"?t.toString():"unknown"}catch{return"unknown"}}}export{wt as ContainerProxy,pt as LunoraContainer,ft as outboundParams};
|
package/package.json
CHANGED