@north-light/cli 0.3.441 → 0.3.444
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/clients/attach/viewer.js +527 -527
- package/dist/core/directory/join.d.ts +2 -0
- package/dist/core/directory/join.js +1 -1
- package/dist/core/fs-utils.d.ts +7 -0
- package/dist/core/fs-utils.js +3 -3
- package/dist/core/grants/mirror.d.ts +11 -0
- package/dist/core/grants/mirror.js +2 -2
- package/dist/core/grants/on-sync.js +1 -1
- package/dist/core/grants/remove.js +1 -1
- package/dist/core/grants/sync.js +1 -1
- package/dist/core/layout-migrate/index.js +4 -4
- package/dist/core/layout.d.ts +8 -0
- package/dist/core/layout.js +2 -2
- package/dist/core/locked-file.js +1 -1
- package/dist/core/peer/client.d.ts +57 -0
- package/dist/core/peer/client.js +1 -0
- package/dist/core/peer/directory.d.ts +65 -0
- package/dist/core/peer/directory.js +1 -0
- package/dist/core/peer/envelope.d.ts +40 -0
- package/dist/core/peer/envelope.js +1 -0
- package/dist/core/peer/events.d.ts +9 -0
- package/dist/core/peer/events.js +1 -0
- package/dist/core/peer/token.d.ts +15 -0
- package/dist/core/peer/token.js +2 -0
- package/dist/core/peer/version.d.ts +16 -0
- package/dist/core/peer/version.js +1 -0
- package/dist/core/providers/bundles.js +1 -1
- package/dist/core/scoped-state/db.js +2 -2
- package/dist/core/secrets.d.ts +3 -4
- package/dist/core/secrets.js +1 -2
- package/dist/daemon/api/handlers/providers.js +1 -1
- package/dist/daemon/crtrd.js +7 -7
- package/package.json +2 -2
- package/packages/crouter-identity/package.json +1 -1
- package/runtime.lock.json +12 -12
|
@@ -36,6 +36,8 @@ export interface JoinDaemonControl {
|
|
|
36
36
|
/** Whether this process runs inside a node, whose broker the stop kills. */
|
|
37
37
|
insideNode(): boolean;
|
|
38
38
|
}
|
|
39
|
+
/** Provider plugin ids installed on this machine (reported for display only). */
|
|
40
|
+
export declare function installedPluginIds(): string[];
|
|
39
41
|
/** The owner a successful join recorded, or null when `crtr sys join` never
|
|
40
42
|
* wrote the client file (no file, or one provisioned without join, which has
|
|
41
43
|
* no `token_sha256`). The daemon applies OWN-11 to canvas.db from it at every start. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
var J=Object.defineProperty;var s=(r,t)=>J(r,"name",{value:t,configurable:!0});import{createHash as N,generateKeyPairSync as R,randomUUID as U}from"node:crypto";import{existsSync as I,mkdirSync as D,readdirSync as L,rmSync as y}from"node:fs";import{hostname as O}from"node:os";import{join as S}from"node:path";import{InputError as p}from"../io.js";import{network as k}from"../errors.js";import{secretsDir as j}from"../layout.js";import{apiSocketPath as C}from"../canvas/paths.js";import{daemonAnswers as K}from"../layout-migrate/index.js";import{atomicWriteJson as _,readJsonIfExists as x}from"../fs-utils.js";import{updateConfig as T}from"../config.js";import{launcherPresent as A}from"../grants/sandboxes.js";import{crtrVersion as W}from"../../shared/crtr-version.js";import{pluginsDir as q}from"../scope.js";import{userScopeRoot as z}from"../scope.js";import{rewriteLocalProfileGrantees as B}from"../scoped-state/profiles.js";import{withJoinLock as F}from"./join-lock.js";function l(){return S(j(),"runtime-client.json")}s(l,"clientPath");function b(r){if(typeof r!="string")return!1;try{const{protocol:t}=new URL(r);return t==="http:"||t==="https:"}catch{return!1}}s(b,"isUrl");function $(r){const t=r??{},{runtime:u,client_id:e,issuer:a,owner:n,url:o}=t;if(typeof u!="string"||!/^runtime:.+/.test(u)||typeof e!="string"||e===""||!b(a)||typeof n!="string"||!/^user:.+/.test(n)||!b(o))throw k("directory registration answer is missing runtime, client_id, issuer, owner, or url");const{machine_id:i,is_default:c}=t;return{runtime:u,client_id:e,issuer:a,owner:n,url:o,...typeof i=="string"?{machine_id:i}:{},...typeof c=="boolean"?{is_default:c}:{}}}s($,"registration");function w(){return S(j(),"runtime-client.pending.json")}s(w,"pendingPath");function G(){try{const r=q("user");return!r||!I(r)?[]:L(r,{withFileTypes:!0}).filter(t=>t.isDirectory()&&!t.name.startsWith(".")).map(t=>t.name).sort()}catch{return[]}}s(G,"installedPluginIds");async function V(r,t,u,e){const a=`${r.replace(/\/$/,"")}/v1/runtimes/registration`;let n;try{n=await fetch(a,{method:"PUT",headers:{authorization:`Bearer ${t}`,"content-type":"application/json"},body:JSON.stringify({jwk:u,name:e,platform:`${process.platform}-${process.arch}`,crtr_version:W(),plugins:G(),sandboxed:A()}),signal:AbortSignal.timeout(3e4)})}catch(i){throw k(`directory registration request to ${a} failed: ${i instanceof Error?i.message:String(i)}`)}const o=await n.text();if(!n.ok)throw n.status<500&&y(w(),{force:!0}),new p({error:"join_refused",message:`directory refused the registration: ${n.status} ${o.slice(0,500)}`,next:`Create a new join token at ${r.replace(/\/$/,"")}/account/machines (tokens are single-use and expire after 10 minutes), and run \`crtr sys join\` again.`});try{return $(JSON.parse(o))}catch(i){throw i instanceof SyntaxError?k("directory registration answer is not JSON"):i}}s(V,"register");function P(r){T("user",t=>{t.runtime=r.is_default===!1?{issuer:r.issuer,audience:r.url,unsandboxed:!0}:{...t.runtime,issuer:r.issuer,audience:r.url}}),B(z())}s(P,"commit");function mr(){const r=x(l());if(r?.token_sha256===void 0)return null;const{owner:t}=r;if(typeof t!="string"||!/^user:.+/.test(t))throw new Error(`${l()} was written by join but has a missing or invalid owner`);return t}s(mr,"joinedOwner");function v(r){return new p({error:"daemon_running",message:`cannot join while a daemon answers on ${r}`,next:"This daemon is managed outside crtr, so join cannot restart it: stop the daemon that owns this volume, then run `crtr sys join` again."})}s(v,"daemonRunningError");async function E(r,t,u,e){const a=x(l());if(a?.token_sha256===e)return{joined:$(a),registered:!1};D(j(),{recursive:!0,mode:448});let n=x(w());if(n?.token_sha256!==e||n.directory!==r||!n.jwk||!n.public_jwk){const{privateKey:h,publicKey:f}=R("ec",{namedCurve:"P-256"}),d=U();n={token_sha256:e,directory:r,jwk:{...h.export({format:"jwk"}),kid:d,alg:"ES256",use:"sig"},public_jwk:{...f.export({format:"jwk"}),kid:d,alg:"ES256",use:"sig"}},_(w(),n,{mode:384})}const{jwk:o,public_jwk:i}=n,c=await V(r,t,i,u);return{joined:c,registered:!0,client:{jwk:o,kid:o.kid,alg:"ES256",...c,token_sha256:e}}}s(E,"obtainRegistration");async function pr(r,t,u=O(),e){if(!b(r))throw new p({error:"invalid_directory",message:"directory must be an http or https URL",received:r,field:"directory",next:"Pass the directory base URL, for example https://directory.example."});if(t==="")throw new p({error:"invalid_token",message:"token is empty",field:"token",next:"Pass the enrollment token provisioning supplied."});const a=C(),n=N("sha256").update(t).digest("hex");if(e===void 0){if(await K(a))throw v(a);const{joined:o,registered:i,client:c}=await E(r,t,u,n);return c&&(_(l(),c,{mode:384}),y(w(),{force:!0})),P(o),{...o,registered:i,daemon:"not_running"}}return F(async()=>{const o=s(()=>new p({error:"join_inside_node",message:"a daemon is running and this command is inside a node: stopping the daemon would kill this node's broker and this command with it",next:"Run `crtr sys join` from a plain terminal outside any crtr node. It stops the daemon, joins, and starts it again for you."}),"refuseInsideNode");if(await e.running()&&e.insideNode())throw o();const{joined:i,registered:c,client:h}=await E(r,t,u,n);let f=!1,d;try{if(await e.running()){if(e.insideNode())throw o();if(f=!0,await e.stop(),await e.running())throw v(a)}if(h&&(_(l(),h,{mode:384}),y(w(),{force:!0})),P(i),await e.running()){if(e.insideNode())throw o();f=!0,await e.stop()}}catch(m){d=m}let g={daemon:"not_running"};if(f)try{await e.start(),g={daemon:"restarted"}}catch(m){g={daemon:"restart_failed",daemon_error:m instanceof Error?m.message:String(m)}}if(d!==void 0)throw d;return{...i,registered:c,...g}})}s(pr,"joinDirectory");export{pr as joinDirectory,mr as joinedOwner};
|
|
1
|
+
var J=Object.defineProperty;var s=(r,t)=>J(r,"name",{value:t,configurable:!0});import{createHash as N,generateKeyPairSync as R,randomUUID as U}from"node:crypto";import{existsSync as I,mkdirSync as D,readdirSync as L,rmSync as y}from"node:fs";import{hostname as O}from"node:os";import{join as S}from"node:path";import{InputError as p}from"../io.js";import{network as k}from"../errors.js";import{secretsDir as j}from"../layout.js";import{apiSocketPath as C}from"../canvas/paths.js";import{daemonAnswers as K}from"../layout-migrate/index.js";import{atomicWriteJson as _,readJsonIfExists as x}from"../fs-utils.js";import{updateConfig as T}from"../config.js";import{launcherPresent as A}from"../grants/sandboxes.js";import{crtrVersion as W}from"../../shared/crtr-version.js";import{pluginsDir as q}from"../scope.js";import{userScopeRoot as z}from"../scope.js";import{rewriteLocalProfileGrantees as B}from"../scoped-state/profiles.js";import{withJoinLock as F}from"./join-lock.js";function l(){return S(j(),"runtime-client.json")}s(l,"clientPath");function b(r){if(typeof r!="string")return!1;try{const{protocol:t}=new URL(r);return t==="http:"||t==="https:"}catch{return!1}}s(b,"isUrl");function $(r){const t=r??{},{runtime:u,client_id:e,issuer:a,owner:n,url:o}=t;if(typeof u!="string"||!/^runtime:.+/.test(u)||typeof e!="string"||e===""||!b(a)||typeof n!="string"||!/^user:.+/.test(n)||!b(o))throw k("directory registration answer is missing runtime, client_id, issuer, owner, or url");const{machine_id:i,is_default:c}=t;return{runtime:u,client_id:e,issuer:a,owner:n,url:o,...typeof i=="string"?{machine_id:i}:{},...typeof c=="boolean"?{is_default:c}:{}}}s($,"registration");function w(){return S(j(),"runtime-client.pending.json")}s(w,"pendingPath");function G(){try{const r=q("user");return!r||!I(r)?[]:L(r,{withFileTypes:!0}).filter(t=>t.isDirectory()&&!t.name.startsWith(".")).map(t=>t.name).sort()}catch{return[]}}s(G,"installedPluginIds");async function V(r,t,u,e){const a=`${r.replace(/\/$/,"")}/v1/runtimes/registration`;let n;try{n=await fetch(a,{method:"PUT",headers:{authorization:`Bearer ${t}`,"content-type":"application/json"},body:JSON.stringify({jwk:u,name:e,platform:`${process.platform}-${process.arch}`,crtr_version:W(),plugins:G(),sandboxed:A()}),signal:AbortSignal.timeout(3e4)})}catch(i){throw k(`directory registration request to ${a} failed: ${i instanceof Error?i.message:String(i)}`)}const o=await n.text();if(!n.ok)throw n.status<500&&y(w(),{force:!0}),new p({error:"join_refused",message:`directory refused the registration: ${n.status} ${o.slice(0,500)}`,next:`Create a new join token at ${r.replace(/\/$/,"")}/account/machines (tokens are single-use and expire after 10 minutes), and run \`crtr sys join\` again.`});try{return $(JSON.parse(o))}catch(i){throw i instanceof SyntaxError?k("directory registration answer is not JSON"):i}}s(V,"register");function P(r){T("user",t=>{t.runtime=r.is_default===!1?{issuer:r.issuer,audience:r.url,unsandboxed:!0}:{...t.runtime,issuer:r.issuer,audience:r.url}}),B(z())}s(P,"commit");function mr(){const r=x(l());if(r?.token_sha256===void 0)return null;const{owner:t}=r;if(typeof t!="string"||!/^user:.+/.test(t))throw new Error(`${l()} was written by join but has a missing or invalid owner`);return t}s(mr,"joinedOwner");function v(r){return new p({error:"daemon_running",message:`cannot join while a daemon answers on ${r}`,next:"This daemon is managed outside crtr, so join cannot restart it: stop the daemon that owns this volume, then run `crtr sys join` again."})}s(v,"daemonRunningError");async function E(r,t,u,e){const a=x(l());if(a?.token_sha256===e)return{joined:$(a),registered:!1};D(j(),{recursive:!0,mode:448});let n=x(w());if(n?.token_sha256!==e||n.directory!==r||!n.jwk||!n.public_jwk){const{privateKey:h,publicKey:f}=R("ec",{namedCurve:"P-256"}),d=U();n={token_sha256:e,directory:r,jwk:{...h.export({format:"jwk"}),kid:d,alg:"ES256",use:"sig"},public_jwk:{...f.export({format:"jwk"}),kid:d,alg:"ES256",use:"sig"}},_(w(),n,{mode:384})}const{jwk:o,public_jwk:i}=n,c=await V(r,t,i,u);return{joined:c,registered:!0,client:{jwk:o,kid:o.kid,alg:"ES256",...c,token_sha256:e}}}s(E,"obtainRegistration");async function pr(r,t,u=O(),e){if(!b(r))throw new p({error:"invalid_directory",message:"directory must be an http or https URL",received:r,field:"directory",next:"Pass the directory base URL, for example https://directory.example."});if(t==="")throw new p({error:"invalid_token",message:"token is empty",field:"token",next:"Pass the enrollment token provisioning supplied."});const a=C(),n=N("sha256").update(t).digest("hex");if(e===void 0){if(await K(a))throw v(a);const{joined:o,registered:i,client:c}=await E(r,t,u,n);return c&&(_(l(),c,{mode:384}),y(w(),{force:!0})),P(o),{...o,registered:i,daemon:"not_running"}}return F(async()=>{const o=s(()=>new p({error:"join_inside_node",message:"a daemon is running and this command is inside a node: stopping the daemon would kill this node's broker and this command with it",next:"Run `crtr sys join` from a plain terminal outside any crtr node. It stops the daemon, joins, and starts it again for you."}),"refuseInsideNode");if(await e.running()&&e.insideNode())throw o();const{joined:i,registered:c,client:h}=await E(r,t,u,n);let f=!1,d;try{if(await e.running()){if(e.insideNode())throw o();if(f=!0,await e.stop(),await e.running())throw v(a)}if(h&&(_(l(),h,{mode:384}),y(w(),{force:!0})),P(i),await e.running()){if(e.insideNode())throw o();f=!0,await e.stop()}}catch(m){d=m}let g={daemon:"not_running"};if(f)try{await e.start(),g={daemon:"restarted"}}catch(m){g={daemon:"restart_failed",daemon_error:m instanceof Error?m.message:String(m)}}if(d!==void 0)throw d;return{...i,registered:c,...g}})}s(pr,"joinDirectory");export{G as installedPluginIds,pr as joinDirectory,mr as joinedOwner};
|
package/dist/core/fs-utils.d.ts
CHANGED
|
@@ -27,6 +27,13 @@ export declare function syncDirectory(path: string): void;
|
|
|
27
27
|
* tmux inbox watcher, or crtrd serving a ticket read — sees either the old
|
|
28
28
|
* bytes or the new ones and never a truncated file mid-write. */
|
|
29
29
|
export declare function atomicWriteText(path: string, content: string | Uint8Array, opts?: AtomicWriteOptions): void;
|
|
30
|
+
/** Publish `path` with `content` so it is never observable empty or partial, and
|
|
31
|
+
* never replaces an existing file. The bytes reach stable storage under a
|
|
32
|
+
* temporary name first; a hard link then makes them appear at `path` in one
|
|
33
|
+
* step and fails with EEXIST if another initializer won. The containing
|
|
34
|
+
* directory is fsynced so the new entry survives a volume handoff or power loss.
|
|
35
|
+
* Returns false when `path` already existed. */
|
|
36
|
+
export declare function publishFileExclusive(path: string, content: string, mode: number): boolean;
|
|
30
37
|
/** Fsync every file and directory under `root` (and `root` itself), so a tree about to be renamed into place
|
|
31
38
|
* reaches stable storage before the rename can. */
|
|
32
39
|
export declare function syncTree(root: string): void;
|
package/dist/core/fs-utils.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
var
|
|
2
|
-
`,"utf8")}e(
|
|
3
|
-
`,
|
|
1
|
+
var D=Object.defineProperty;var e=(r,t)=>D(r,"name",{value:t,configurable:!0});import{existsSync as u,lstatSync as N,mkdirSync as $,readdirSync as y,readFileSync as a,rmSync as F,statSync as k,symlinkSync as b,writeFileSync as m,cpSync as E,readlinkSync as W,renameSync as I,chmodSync as L,realpathSync as M,closeSync as p,fsyncSync as S,openSync as d,linkSync as j,unlinkSync as B,writeSync as C}from"node:fs";import{dirname as f,join as w,relative as X,sep as q}from"node:path";import{homedir as J,platform as O}from"node:os";function g(r){try{return M(r)}catch{return r}}e(g,"realpathOrSelf");function Z(r,t=J()){return r==="~"?t:r.startsWith("~/")||O()==="win32"&&r.startsWith("~\\")?w(t,r.slice(2)):r}e(Z,"expandTilde");function _(r,t=J()){return r===t?"~":r.startsWith(t+q)?"~"+r.slice(t.length):r}e(_,"tildify");function x(r){$(r,{recursive:!0})}e(x,"ensureDir");function P(r,t){x(f(r)),m(r,JSON.stringify(t,null,2)+`
|
|
2
|
+
`,"utf8")}e(P,"writeJson");function rr(r,t,i){z(r,JSON.stringify(t,null,2)+`
|
|
3
|
+
`,i)}e(rr,"atomicWriteJson");function v(r){const t=d(r,"r");try{S(t)}finally{p(t)}}e(v,"syncFile");function h(r){const t=d(r,"r");try{S(t)}finally{p(t)}}e(h,"syncDirectory");function z(r,t,i){i?.createParent!==!1&&x(f(r));const n=`${r}.${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}.tmp`,o=i?.mode??(u(r)?k(r).mode&511:384);try{m(n,t,{mode:o}),L(n,o),i?.durable===!0&&v(n),I(n,r),i?.durable===!0&&h(f(r))}finally{u(n)&&F(n,{force:!0})}}e(z,"atomicWriteText");function tr(r,t,i){const n=`${r}.${process.pid}.${Math.random().toString(16).slice(2)}.tmp`,o=d(n,"wx",i);try{try{const c=Buffer.from(t,"utf8");for(let s=0;s<c.length;)s+=C(o,c,s,c.length-s);S(o)}finally{p(o)}try{j(n,r)}catch(c){if(c.code==="EEXIST")return!1;throw c}return h(f(r)),!0}finally{try{B(n)}catch{}}}e(tr,"publishFileExclusive");function A(r){for(const t of y(r,{withFileTypes:!0})){const i=w(r,t.name);t.isDirectory()?A(i):t.isFile()&&v(i)}h(r)}e(A,"syncTree");function G(r){return JSON.parse(a(r,"utf8"))}e(G,"readJson");function er(r){try{return G(r)}catch(t){if(t.code==="ENOENT")return null;throw t}}e(er,"readJsonIfExists");function nr(r){try{return JSON.parse(a(r,"utf8"))}catch{return null}}e(nr,"readJsonOrNull");function ir(r){return u(r)?a(r,"utf8"):null}e(ir,"readTextIfExists");function or(r){return a(r,"utf8")}e(or,"readText");function cr(r,t){x(f(r)),m(r,t,"utf8")}e(cr,"writeText");function H(r){try{return k(r).isDirectory()}catch{return!1}}e(H,"isDir");function K(r){try{return N(r).isSymbolicLink()}catch{return!1}}e(K,"isSymlink");function ur(r){return u(r)}e(ur,"pathExists");function sr(r){return u(r)?y(r,{withFileTypes:!0}).filter(t=>t.isDirectory()||t.isSymbolicLink()).map(t=>t.name):[]}e(sr,"listDirs");function fr(r){return u(r)?y(r):[]}e(fr,"listEntries");function Q(r){!u(r)&&!K(r)||F(r,{recursive:!0,force:!0})}e(Q,"removePath");function lr(r,t,i={}){x(f(t)),Q(t);const n=O()==="win32";if(!i.noSymlink&&!n){const o=X(g(f(t)),g(r));try{return b(o,t,H(r)?"dir":"file"),"symlink"}catch{return E(r,t,{recursive:!0}),"copy"}}return E(r,t,{recursive:!0}),"copy"}e(lr,"linkOrCopy");function yr(r){try{return W(r)}catch{return null}}e(yr,"readSymlinkTarget");function ar(r,t=()=>!0,i=()=>!1){const n=[];if(!u(r))return n;const o=[r];for(;o.length;){const c=o.pop();let s;try{s=y(c,{withFileTypes:!0})}catch{continue}for(const l of s){const T=w(c,l.name);l.isDirectory()?i(l.name)||o.push(T):l.isFile()&&t(l.name)&&n.push(T)}}return n}e(ar,"walkFiles");function xr(){return new Date().toISOString()}e(xr,"nowIso");export{rr as atomicWriteJson,z as atomicWriteText,x as ensureDir,Z as expandTilde,H as isDir,K as isSymlink,lr as linkOrCopy,sr as listDirs,fr as listEntries,xr as nowIso,ur as pathExists,tr as publishFileExclusive,G as readJson,er as readJsonIfExists,nr as readJsonOrNull,yr as readSymlinkTarget,or as readText,ir as readTextIfExists,g as realpathOrSelf,Q as removePath,h as syncDirectory,v as syncFile,A as syncTree,_ as tildify,ar as walkFiles,P as writeJson,cr as writeText};
|
|
@@ -34,6 +34,14 @@ export interface ActiveGrant extends GrantRecord {
|
|
|
34
34
|
* authorization): absent or non-object reads as `{}`, and a malformed entry
|
|
35
35
|
* (see `dependencyOf`) is dropped. A directory older than the Context stage sends none. */
|
|
36
36
|
export declare function dependenciesOf(value: unknown): GrantDependencies;
|
|
37
|
+
/** A grant this machine only mirrors. An unsandboxed machine runs only
|
|
38
|
+
* `app:terminal`; the directory also fans the account's hosted-runtime app
|
|
39
|
+
* grants out to it, read-only, so a forwarded app caller can be authorized
|
|
40
|
+
* (`grantFor`, `activeGrant`). They drive nothing here: no sandbox stop, removal
|
|
41
|
+
* cascade, provider install, or run notice. */
|
|
42
|
+
export declare function mirrorOnly(grant: Pick<GrantRecord, 'grantee'>): boolean;
|
|
43
|
+
/** Every mirrored grant this machine acts on: `allGrants()` without the mirror-only ones. */
|
|
44
|
+
export declare function localGrants(): GrantRecord[];
|
|
37
45
|
/** Every mirrored grant, whatever its status. */
|
|
38
46
|
export declare function allGrants(): GrantRecord[];
|
|
39
47
|
/** The active form of a grant record read earlier, or null when it is not active or unparsable. */
|
|
@@ -58,6 +66,9 @@ export declare function grantSyncStateFor(grantee: string, sub: string): GrantSy
|
|
|
58
66
|
export declare function grantRecord(grantId: string): GrantRecord | null;
|
|
59
67
|
export declare function activeGrant(grantId: string): ActiveGrant | null;
|
|
60
68
|
export declare function grantFor(grantee: string, sub: string): ActiveGrant | null;
|
|
69
|
+
/** The app's own active grant on this machine. Null for a mirror-only grant: an
|
|
70
|
+
* unsandboxed machine runs no app, so a fanned-out hosted grant is never "the
|
|
71
|
+
* app's grant" for a local app caller, sandbox or install. */
|
|
61
72
|
export declare function activeGrantForApp(grantee: string): ActiveGrant | null;
|
|
62
73
|
/** Where a run's notice goes: its grant's registered `run_notice_url`, with the grant's `sub` and `grantee`. Null when the run has no active grant or the grantee registered no URL. */
|
|
63
74
|
export declare function runNoticeTarget(grantId: string): {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
var
|
|
1
|
+
var y=Object.defineProperty;var t=(e,r)=>y(e,"name",{value:r,configurable:!0});import{createHash as _}from"node:crypto";import{prepared as n}from"../sqlite-statements.js";import{parseScope as S}from"@north-light/identity";import{openDb as i,withCanvasWrite as v}from"../canvas/db.js";import{isUnsandboxedMachine as T}from"../runtime/unsandboxed.js";const m=["not_started","pending","ready","failed"];function c(e){if(!Array.isArray(e))throw new TypeError("grant scopes must be a list");return e.map(r=>{if(!r||typeof r!="object"||typeof r.scope!="string"||!["always","ask"].includes(r.mode))throw new TypeError("invalid grant scope entry");return{scope:r.scope,mode:r.mode}})}t(c,"entries");function O(e){if(!e||typeof e!="object"||Array.isArray(e))return null;const{state:r,message:a,next:u}=e;return typeof r!="string"||!m.includes(r)||a!==void 0&&typeof a!="string"||typeof u!="string"||!u?null:{state:r,...a===void 0?{}:{message:a},next:u}}t(O,"dependencyOf");function f(e){if(!e||typeof e!="object"||Array.isArray(e))return{};const r={};for(const[a,u]of Object.entries(e)){const s=O(u);s&&(r[a]=s)}return r}t(f,"dependenciesOf");function l(e){return{id:e.id,runtime:e.runtime,sub:e.sub,grantee:e.grantee,rev:e.rev,scopes:c(JSON.parse(e.scopes_json)),resolved_scopes:c(JSON.parse(e.resolved_scopes_json)),spend_cap:e.spend_cap,expires_at:e.expires_at,status:e.status,revoked_at:e.revoked_at,approved_at:e.approved_at,payer:e.payer,dependencies:f(JSON.parse(e.dependencies_json)),run_notice_url:e.run_notice_url??null,synced_at:e.synced_at}}t(l,"record");function p(e){if(!e||e.status!=="active")return null;try{return x(l(e))}catch(r){if(r instanceof TypeError||r instanceof SyntaxError)return null;throw r}}t(p,"active");function d(e){return e.grantee!=="app:terminal"&&T()}t(d,"mirrorOnly");function M(){return R().filter(e=>!d(e))}t(M,"localGrants");function R(){return n(i(),"SELECT * FROM grants").all().flatMap(e=>{try{return[l(e)]}catch(r){if(r instanceof TypeError||r instanceof SyntaxError)return[];throw r}})}t(R,"allGrants");function x(e){if(e.status!=="active")return null;try{const r=e.resolved_scopes.map(({scope:a})=>S(a));return{...e,resolved:e.resolved_scopes.flatMap(({mode:a},u)=>a==="always"?[r[u]]:[])}}catch(r){if(r instanceof TypeError||r instanceof SyntaxError)return null;throw r}}t(x,"activeRecord");function E(e){return Array.isArray(e)?`[${e.map(E).join(",")}]`:e&&typeof e=="object"?`{${Object.keys(e).sort().filter(r=>e[r]!==void 0).map(r=>`${JSON.stringify(r)}:${E(e[r])}`).join(",")}}`:JSON.stringify(e)??"null"}t(E,"canonicalJson");function b(e){const r={rev:e.rev,resolved_scopes:e.resolved_scopes,spend_cap:e.spend_cap,payer:e.payer,dependencies:e.dependencies??{}};return _("sha256").update(E(r)).digest("base64url").slice(0,22)}t(b,"grantVersion");function k(e){return!!n(i(),"SELECT 1 FROM grants WHERE id = ?").get(e)}t(k,"hasGrant");function H(e){return n(i(),"SELECT rev, status FROM grants WHERE id = ?").get(e)??null}t(H,"grantSyncState");function W(e,r){return n(i(),"SELECT rev, status FROM grants WHERE grantee = ? AND sub = ?").get(e,r)??null}t(W,"grantSyncStateFor");function j(e){const r=n(i(),"SELECT * FROM grants WHERE id = ?").get(e);if(!r)return null;try{return l(r)}catch(a){if(a instanceof TypeError||a instanceof SyntaxError)return null;throw a}}t(j,"grantRecord");function D(e){return p(n(i(),"SELECT * FROM grants WHERE id = ?").get(e))}t(D,"activeGrant");function G(e,r){return p(n(i(),"SELECT * FROM grants WHERE grantee = ? AND sub = ?").get(e,r))}t(G,"grantFor");function I(e){if(d({grantee:e}))return null;const r=n(i(),"SELECT * FROM grants WHERE grantee = ? AND status = 'active'").all(e);if(r.length>1)throw new Error(`multiple active grants for ${e}`);return p(r[0])}t(I,"activeGrantForApp");function J(e){const r=n(i(),"SELECT run_notice_url AS url, sub, grantee FROM grants WHERE id = ? AND status = 'active'").get(e);return r&&r.url&&!d(r)?{url:r.url,sub:r.sub,grantee:r.grantee}:null}t(J,"runNoticeTarget");function U(){const e=n(i(),"SELECT * FROM grants WHERE grantee = 'app:terminal' AND status = 'active'").all();if(e.length>1)throw new Error("multiple active terminal grants");return p(e[0])}t(U,"terminalGrant");function w(){return n(i(),"SELECT value FROM runtime_state WHERE key = 'grants_source'").get()?.value??null}t(w,"grantsSource");function $(e){return n(i(),"SELECT value FROM runtime_state WHERE key = 'grants_source'").get()?.value===e}t($,"hasSyncedGrants");function V(e){return n(e,"SELECT value FROM runtime_state WHERE key = 'grants_source'").get()?.value!=="local"?0:(n(e,"DELETE FROM runtime_state WHERE key = 'grants_source'").run(),Number(n(e,"DELETE FROM grants").run().changes))}t(V,"dropLocalGrantMirror");function P(e,r,a){const u=new Set;for(const s of e){if(u.has(s.id)||!s.id||!s.runtime||!s.sub||!s.grantee||!Number.isInteger(s.rev)||!["active","revoked","removed"].includes(s.status))throw new TypeError("invalid grant sync response");u.add(s.id),c(s.scopes),c(s.resolved_scopes)}v(s=>{s.exec("DELETE FROM grants");const g=n(s,`INSERT INTO grants
|
|
2
2
|
(id, runtime, sub, grantee, rev, scopes_json, resolved_scopes_json, spend_cap, expires_at,
|
|
3
3
|
status, revoked_at, approved_at, payer, dependencies_json, run_notice_url, synced_at)
|
|
4
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);for(const o of e)
|
|
4
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);for(const o of e)g.run(o.id,o.runtime,o.sub,o.grantee,o.rev,JSON.stringify(o.scopes),JSON.stringify(o.resolved_scopes),o.spend_cap,o.expires_at,o.status,o.revoked_at,o.approved_at,o.payer,JSON.stringify(f(o.dependencies)),o.run_notice_url??null,o.synced_at);n(s,"INSERT INTO runtime_state (key, value) VALUES ('grants_source', ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value").run(a),n(s,"INSERT INTO runtime_state (key, value) VALUES ('grants_generation', '1') ON CONFLICT(key) DO UPDATE SET value = CAST(CAST(value AS INTEGER) + 1 AS TEXT)").run()})}t(P,"replaceGrants");function X(){const e=n(i(),"SELECT value FROM runtime_state WHERE key = 'grants_generation'").get();return e?Number(e.value):0}t(X,"grantsGeneration");export{D as activeGrant,I as activeGrantForApp,x as activeRecord,R as allGrants,f as dependenciesOf,V as dropLocalGrantMirror,G as grantFor,j as grantRecord,H as grantSyncState,W as grantSyncStateFor,b as grantVersion,X as grantsGeneration,w as grantsSource,k as hasGrant,$ as hasSyncedGrants,M as localGrants,d as mirrorOnly,P as replaceGrants,J as runNoticeTarget,U as terminalGrant};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
var w=Object.defineProperty;var c=(n,r)=>w(n,"name",{value:r,configurable:!0});import{prepared as v}from"../sqlite-statements.js";import{relative as b,sep as g}from"node:path";import{activeBackgroundBashJobs as x,readJobPgid as y}from"../bash-jobs.js";import{appSpace as S}from"../layout.js";import{getNode as l,recordSettledTurn as k}from"../canvas/index.js";import{transition as E}from"../runtime/lifecycle.js";import{openDb as _}from"../canvas/db.js";import{contextDir as R}from"../canvas/node-agent-paths.js";import{emitEvent as p}from"../events/emit.js";import{closeGrantStreams as C}from"../runs/events.js";import{grantedMounts as G,isGrantedSource as I,mountAllowed as P}from"../spaces/mounts.js";import{permissionsForRun as m}from"../spaces/permissions.js";import{stopNodeSandboxes as
|
|
1
|
+
var w=Object.defineProperty;var c=(n,r)=>w(n,"name",{value:r,configurable:!0});import{prepared as v}from"../sqlite-statements.js";import{relative as b,sep as g}from"node:path";import{activeBackgroundBashJobs as x,readJobPgid as y}from"../bash-jobs.js";import{appSpace as S}from"../layout.js";import{getNode as l,recordSettledTurn as k}from"../canvas/index.js";import{transition as E}from"../runtime/lifecycle.js";import{openDb as _}from"../canvas/db.js";import{contextDir as R}from"../canvas/node-agent-paths.js";import{emitEvent as p}from"../events/emit.js";import{closeGrantStreams as C}from"../runs/events.js";import{grantedMounts as G,isGrantedSource as I,mountAllowed as P}from"../spaces/mounts.js";import{permissionsForRun as m}from"../spaces/permissions.js";import{stopNodeSandboxes as O}from"../spaces/stop.js";import{discardChangedGrantTokens as T}from"../providers/token-cache.js";import{activeGrantForApp as F,allGrants as M,mirrorOnly as A}from"./mirror.js";import{runRemovalCascades as D}from"./remove.js";import{notifyGrantChanges as N}from"./watch.js";import{launcherPresent as W,listSandboxes as j,stopSandboxPid as B}from"./sandboxes.js";import{checkCronFire as J}from"../../daemon/cron/authority.js";import{cronExecutionFor as L}from"../../daemon/cron/live-runs.js";import{cronExecutionsReady as U}from"../../daemon/cron/run-socket.js";function h(){return new Map(v(_(),"SELECT uid, app, status FROM app_users").all().map(n=>[n.uid,{app:n.app,status:n.status}]))}c(h,"appUserOwners");function $(n,r=h()){const o=r.get(n.uid),i=n.nodeId.startsWith("cron-"),e=i?L(n.nodeId.slice(5)):void 0;if(i&&(e===void 0||!o))return"permission_revoked";if(e!==void 0&&o.status==="active"){const s=J({grantee:e.grantee,sub:e.sub,grant_id:e.grantId,scopes:e.scopes});if(!s.allowed)return e.refusal??={reason:s.reason,message:s.message},"permission_revoked"}if(!o||o.app==="app:terminal")return null;if(o.status!=="active")return"app_removed";const t=l(n.nodeId);try{let s;if(e!==void 0)s=m(e.grantee,e.sub,e.grantId,e.scopes);else if(t&&t.grantee===o.app)s=m(o.app,t.sub??"user:local",t.grant_id??null,t.scopes??null);else{const d=F(o.app);if(!d)return"permission_revoked";s=m(o.app,d.sub,d.id,null)}const u=G(s),f=S(o.app).root;return n.mounts.every(d=>{const a=b(f,d.source);return a===""||a!==".."&&!a.startsWith(`..${g}`)&&!a.startsWith(g)||!I(o.app,d.source)||P(d,u)})?null:"permission_revoked"}catch{return"permission_revoked"}}c($,"exceeds");async function H(n){const r=l(n);if(!r)return;(r.status==="active"||r.status==="idle")&&E(n,"release");const o=new Date().toISOString();k(n,o),r.run_id&&r.run_id!==n&&v(_(),"UPDATE nodes SET last_settled_at = COALESCE(last_settled_at, ?) WHERE node_id = ?").run(o,r.run_id)}c(H,"afterRevocationStop");function q(n){const r=new Set,o=l(n);o?.pi_pid!=null&&r.add(o.pi_pid);try{const i=R(n,o?.grantee);for(const e of x(i,!0)){const t=y(i,e.jobId);t!==void 0&&r.add(t)}}catch{}return r}c(q,"nodePids");async function z(){if(!W())return{completion:Promise.resolve()};await U();const n=await j(),r=h(),o=n.flatMap(t=>{const s=$(t,r);return s?[{sandbox:t,reason:s}]:[]}),i=[],e=new Map;for(const t of o){p({level:"info",event:"daemon.grants.sandbox_stop",fields:{sandbox_node:t.sandbox.nodeId,pid:t.sandbox.pid,grantee:r.get(t.sandbox.uid).app,reason:t.reason}});const s=e.get(t.sandbox.nodeId)??[];s.push(t),e.set(t.sandbox.nodeId,s)}for(const[t,s]of e){const u=l(t),f=u?q(t):new Set,d=u!==null&&s.some(({sandbox:a})=>f.has(a.pid));i.push((async()=>{if(d){const a=await O(t,"permission_revoked",{holdRecovery:!0,after:c(()=>H(t),"after")});if(!a.confirmed)throw a.error??new Error(`stop of ${t} was not confirmed`)}for(const{sandbox:a}of s)(!d||!f.has(a.pid))&&await B(a.pid)})().catch(a=>{p({level:"error",event:"daemon.grants.sandbox_stop_failed",...u?{node_id:t}:{fields:{sandbox_node:t}},error:a})}))}return{completion:Promise.all(i).then(()=>{})}}c(z,"sweepSandboxes");function K(n,r){try{const{pairs:o,tokens:i}=T(n,r);o&&p({level:"info",event:"daemon.grants.outbound_tokens_discarded",fields:{grants:o,tokens:i}})}catch(o){p({level:"error",event:"daemon.grants.outbound_tokens_discard_failed",error:o})}}c(K,"discardOutboundTokens");async function he(n,r=o=>{}){const o=M();try{N(n,o)}catch(e){p({level:"error",event:"daemon.grants.notify_failed",error:e})}K(n,o);for(const e of n){if(e.status!=="active"||o.find(u=>u.id===e.id)?.status==="active")continue;const s=C(e.id);s&&p({level:"info",event:"daemon.grants.streams_closed",fields:{grant:e.id,grantee:e.grantee,streams:s}})}try{const{completion:e}=await z();r(e)}catch(e){p({level:"error",event:"daemon.grants.sweep_failed",error:e})}const i=new Set(o.filter(e=>!A(e)&&e.status==="removed"&&n.find(t=>t.id===e.id)?.status!=="removed").map(e=>e.grantee));r(D(i).catch(()=>{})),r(import("../providers/bundles.js").then(({syncProviderBundles:e})=>e()).catch(e=>{p({level:"error",event:"daemon.providers.bundle_sync_failed",error:e})}))}c(he,"onGrantSync");export{$ as exceeds,he as onGrantSync,z as sweepSandboxes};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
var S=Object.defineProperty;var m=(r,i)=>S(r,"name",{value:i,configurable:!0});import{prepared as d}from"../sqlite-statements.js";import{rmSync as h}from"node:fs";import{join as w}from"node:path";import{deleteNodes as O}from"../canvas/canvas.js";import{openDb as c,withCanvasWrite as E}from"../canvas/db.js";import{cancelCron as T}from"../canvas/crons.js";import{emitEvent as l}from"../events/emit.js";import{appSpace as C,appsRoot as u,secretsDir as D}from"../layout.js";import{notifyRunDeleted as L}from"../runs/events.js";import{cancelOpenQuestions as W}from"../runs/questions.js";import{removeTreeBeneath as F}from"../spaces/open-beneath.js";import{markGranteeUploadsDeletePending as H,releaseRunUploads as N,turnSharesOffFor as M}from"../storage/tables.js";import{stopNodeSandboxes as b}from"../spaces/stop.js";import{activeAppUser as p,markAppUserRemoved as I}from"../spaces/users.js";import{launcherRequest as y}from"../../launcher/client.js";import{LaunchRefused as A}from"../../launcher/spec.js";import{
|
|
1
|
+
var S=Object.defineProperty;var m=(r,i)=>S(r,"name",{value:i,configurable:!0});import{prepared as d}from"../sqlite-statements.js";import{rmSync as h}from"node:fs";import{join as w}from"node:path";import{deleteNodes as O}from"../canvas/canvas.js";import{openDb as c,withCanvasWrite as E}from"../canvas/db.js";import{cancelCron as T}from"../canvas/crons.js";import{emitEvent as l}from"../events/emit.js";import{appSpace as C,appsRoot as u,secretsDir as D}from"../layout.js";import{notifyRunDeleted as L}from"../runs/events.js";import{cancelOpenQuestions as W}from"../runs/questions.js";import{removeTreeBeneath as F}from"../spaces/open-beneath.js";import{markGranteeUploadsDeletePending as H,releaseRunUploads as N,turnSharesOffFor as M}from"../storage/tables.js";import{stopNodeSandboxes as b}from"../spaces/stop.js";import{activeAppUser as p,markAppUserRemoved as I}from"../spaces/users.js";import{launcherRequest as y}from"../../launcher/client.js";import{LaunchRefused as A}from"../../launcher/spec.js";import{localGrants as P}from"./mirror.js";import{launcherPresent as g,listSandboxes as U,stopSandboxPid as $}from"./sandboxes.js";let v=Promise.resolve();function ae(r=new Set){const i=v.then(async()=>{const t=P(),o=new Set(t.filter(n=>n.status==="removed").map(n=>n.grantee));for(const n of o)if(!t.some(a=>a.grantee===n&&a.status==="active")&&!(p(n)===null&&!r.has(n)))try{await x(n)}catch(a){l({level:"error",event:"daemon.grants.removal_failed",error:a,fields:{grantee:n}})}});return v=i.catch(()=>{}),i}m(ae,"runRemovalCascades");async function x(r){if(r==="app:terminal")throw new Error("app:terminal cannot be removed");const i=C(r),t=p(r);l({level:"info",event:"daemon.grants.removal_started",fields:{grantee:r,uid:t?.uid??null}}),await W({grantee:r},"the app was removed");const o=E(e=>(d(e,"UPDATE nodes SET deleting_at = COALESCE(deleting_at, ?) WHERE node_id IN (SELECT node_id FROM node_rows WHERE grantee = ?)").run(new Date().toISOString(),r),d(e,"SELECT node_id FROM node_rows WHERE grantee = ? ORDER BY created DESC").all(r).map(s=>s.node_id)));for(const{cron_id:e}of d(c(),"SELECT cron_id FROM crons WHERE grantee = ?").all(r))T(e);const a=(await Promise.all(o.map(e=>b(e,"app_removed",{deleting:!0})))).filter(e=>!e.confirmed);if(a.length)throw new Error(`sandbox teardown could not be confirmed for ${a.length} node(s) of ${r}`);if(t&&g())for(const e of await U())e.uid===t.uid&&await $(e.pid);const R=o.filter(e=>d(c(),"SELECT run_id FROM nodes WHERE node_id = ?").get(e)?.run_id===e);E(e=>{if(M(r,e),N(o,e),H(r,e),!o.length)return;const s=o.map(()=>"?").join(","),_=e.prepare(`SELECT message_id FROM messages WHERE sender_node_id IN (${s}) UNION SELECT message_id FROM deliveries WHERE destination_id IN (${s})`).all(...o,...o);e.prepare(`DELETE FROM deliveries WHERE destination_id IN (${s})`).run(...o);for(const{message_id:f}of _)d(e,"DELETE FROM messages WHERE message_id = ? AND NOT EXISTS (SELECT 1 FROM deliveries WHERE message_id = ?)").run(f,f)}),O(o);for(const e of R)L(e);if(h(w(D(),"env",i.root.slice(u().length+1)),{recursive:!0,force:!0}),F(u(),i.root),t&&g())try{await y({operation:"user.delete",uid:t.uid})}catch(e){if(!(e instanceof A&&e.refusal==="invalid_uid"))throw e}t&&I(t.uid),l({level:"info",event:"daemon.grants.removal_completed",fields:{grantee:r,uid:t?.uid??null,nodes:o.length}})}m(x,"removeApp");export{x as removeApp,ae as runRemovalCascades};
|
package/dist/core/grants/sync.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var
|
|
1
|
+
var w=Object.defineProperty;var n=(e,r)=>w(e,"name",{value:r,configurable:!0});import{createPrivateKey as v,randomUUID as S,sign as b}from"node:crypto";import{readFileSync as E}from"node:fs";import{join as j}from"node:path";import{crtrHome as x}from"../canvas/paths.js";import{isUnsandboxedMachine as g}from"../runtime/unsandboxed.js";import{emitEvent as f}from"../events/emit.js";import{allGrants as $,dependenciesOf as T,replaceGrants as A}from"./mirror.js";import{onGrantSync as N}from"./on-sync.js";const z=300;function O(e,r){const t=Math.floor(Date.now()/1e3),s=Buffer.from(JSON.stringify({alg:"ES256",typ:"JWT",kid:e.kid})).toString("base64url"),c=Buffer.from(JSON.stringify({iss:e.client_id,sub:e.client_id,aud:r,iat:t,exp:t+300,jti:S()})).toString("base64url"),o=`${s}.${c}`,i=b("sha256",Buffer.from(o),{key:v({key:e.jwk,format:"jwk"}),dsaEncoding:"ieee-p1363"});return`${o}.${i.toString("base64url")}`}n(O,"assertion");function y(){const e=JSON.parse(E(j(x(),"secrets","runtime-client.json"),"utf8"));if(!e.jwk||e.alg!=="ES256"||!e.kid||!e.client_id||!e.runtime||e.jwk.kid!==e.kid||e.jwk.kty!=="EC"||e.jwk.crv!=="P-256"||!e.jwk.d)throw new TypeError("invalid runtime client key");return e}n(y,"runtimeClient");function H(e=y()){return e.runtime.startsWith("runtime:")?e.runtime.slice(8):e.runtime}n(H,"runtimeId");function G(e){return`This machine was removed from your Northlight account. To use it again, create a join token at ${e.replace(/\/$/,"")}/account/machines, and run \`crtr sys join\` from a plain terminal.`}n(G,"machineRevokedMessage");class h extends Error{static{n(this,"MachineRevokedError")}constructor(r){super(G(r)),this.name="MachineRevokedError"}}async function R(e,r){const t=y(),s=`${e.replace(/\/$/,"")}/token`,c=new URLSearchParams({grant_type:"client_credentials",client_id:t.client_id,client_assertion_type:"urn:ietf:params:oauth:client-assertion-type:jwt-bearer",client_assertion:O(t,s),...r===void 0?{}:{audience:r}}),o=await fetch(s,{method:"POST",body:c,signal:AbortSignal.timeout(1e4)});if(!o.ok)throw o.status===401&&g()&&await B(o)?new h(e):new Error(`directory token request failed: ${o.status}`);const i=await o.json();if(typeof i.access_token!="string")throw new TypeError("directory token response has no access_token");return{access_token:i.access_token,expires_at:D(i.access_token,i.expires_in)}}n(R,"mintDirectoryToken");async function B(e){try{return(await e.json()).error==="invalid_client"}catch{return!1}}n(B,"isInvalidClient");function D(e,r){if(typeof r=="number"&&Number.isFinite(r)&&r>0)return Date.now()+r*1e3;try{const t=JSON.parse(Buffer.from(e.split(".")[1]??"","base64url").toString("utf8"));if(typeof t.exp=="number")return t.exp*1e3}catch{}return Date.now()+3e5}n(D,"tokenExpiry");function J(e,r){if(!e||typeof e!="object")throw new TypeError("invalid grant sync response");const t=e,s=r.runtime.startsWith("runtime:")?r.runtime:`runtime:${r.runtime}`;if(!(t.runtime===s||g()&&typeof t.runtime=="string"&&t.runtime.startsWith("runtime:")&&t.grantee!=="app:terminal")||typeof t.id!="string"||typeof t.sub!="string"||typeof t.grantee!="string"||!Number.isInteger(t.rev)||!Array.isArray(t.scopes)||!Array.isArray(t.resolved_scopes)||!["active","revoked","removed"].includes(t.status??"")||typeof t.synced_at!="string")throw new TypeError("invalid grant sync response");const i=e.revisions?.find(d=>d.rev===t.rev)?.approved_at;if(typeof i!="string")throw new TypeError("grant revision has no approval time");return{...t,spend_cap:t.spend_cap??null,expires_at:t.expires_at??null,revoked_at:t.revoked_at??null,approved_at:i,payer:"user",dependencies:T(t.dependencies),run_notice_url:typeof t.run_notice_url=="string"&&t.run_notice_url!==""?t.run_notice_url:null}}n(J,"responseGrant");let _;function K(e){return _?_(e):Promise.resolve()}n(K,"requestGrantSync");function V(e,r=t=>{}){let t;const s=e.replace(/\/$/,""),c=n(async()=>{const a=y(),u=await R(s),l=await fetch(`${s}/v1/runtime/grants`,{headers:{Authorization:`Bearer ${u.access_token}`},signal:AbortSignal.timeout(1e4)});if(!l.ok)throw new Error(`directory grant sync failed: ${l.status}`);const p=await l.json();if(!Array.isArray(p.grants)||typeof p.synced_at!="string")throw new TypeError("invalid grant sync response");const k=$();A(p.grants.map(m=>J(m,a)),p.synced_at,s),f({level:"info",event:"daemon.grants.synced",fields:{grants:p.grants.length,synced_at:p.synced_at}});try{await N(k,r)}catch(m){f({level:"error",event:"daemon.grants.on_sync_failed",error:m})}},"run");let o;const i=n(()=>{const a=c().catch(u=>{throw u instanceof h?f({level:"error",event:"daemon.grants.machine_revoked",error:u}):f({level:"error",event:"daemon.grants.sync_failed",error:u}),u}).finally(()=>{t===a&&(t=void 0)});return t=a,a},"start"),d=n((a={})=>t?a.fresh?(o??=t.catch(()=>{}).then(()=>(o=void 0,t??i())),o):t:i(),"sync");return _=d,d}n(V,"createGrantSync");export{z as GRANT_SYNC_INTERVAL_S,h as MachineRevokedError,O as assertion,V as createGrantSync,G as machineRevokedMessage,R as mintDirectoryToken,K as requestGrantSync,y as runtimeClient,H as runtimeId};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
var
|
|
2
|
-
`).find(s=>s.startsWith("crtrd:")),n=
|
|
3
|
-
`).find(s=>s.startsWith("crtr-data:"));if(!e||!n)throw new Error("layout migration requires crtrd user and crtr-data group");const
|
|
4
|
-
`),R(
|
|
1
|
+
var Y=Object.defineProperty;var l=(t,e)=>Y(t,"name",{value:e,configurable:!0});import{DatabaseSync as M}from"node:sqlite";import{randomUUID as D}from"node:crypto";import{symlinkSync as Z,linkSync as ee,chmodSync as _,chownSync as j,closeSync as g,cpSync as te,existsSync as d,fsyncSync as R,lchownSync as ne,lstatSync as f,mkdirSync as v,openSync as S,readFileSync as w,readlinkSync as L,readSync as U,readdirSync as h,renameSync as k,rmSync as T,rmdirSync as b,unlinkSync as $,writeSync as q}from"node:fs";import{request as re}from"node:http";import{basename as A,dirname as y,isAbsolute as oe,join as c,relative as E,resolve as se}from"node:path";import{LAYOUT_VERSION as ie,assertSupportedLayoutMarker as C}from"../layout.js";import{InputError as I}from"../io.js";import{syncDirectory as ce}from"../fs-utils.js";import{oldToNewPathMap as ae}from"./paths.js";import{conversations as ue,memoryAndProfiles as le,previewStores as de,profileFolders as fe,repairMovedRepositories as pe,worktrees as he}from"./stores.js";import{storedPaths as ye}from"./stored-paths.js";import{OldToNewPathMap as ze,oldToNewPathMap as Be}from"./paths.js";const me=new Set([".DS_Store","Thumbs.db","desktop.ini"]);function ve(t){return me.has(t)||t.startsWith("._")}l(ve,"isJunkFile");function F(t,e,n){const r=c(t.map.old.root,"runtime","set-aside",e);let o=c(r,n);for(let i=2;d(o);i++)o=c(r,`${n}.${i}`);return o}l(F,"setAsidePath");function O(t,e,n,r=A(t)){const o=F(n,e,r);return v(y(o),{recursive:!0}),k(t,o),n.moved++,n.changes.push(`${t} -> ${o} (set aside)`),o}l(O,"setAside");function we(t){const e=f(t);return e.isDirectory()?h(t).length===0:e.isFile()&&e.size===0}l(we,"empty");function W(t,e,n,r=!1){const o=f(t),i=f(e);if(o.isDirectory()&&i.isDirectory()&&!r)return{action:"merge"};if(o.isFile()&&i.isFile()&&Q(t,e))return{action:"drop-source"};if(we(e))return{action:"remove-destination",line:`stray-destination: removed empty ${i.isDirectory()?"folder":"file"} ${e} that predates the migration`};const s=F(n,"replaced",E(n.map.old.root,e));return{action:"set-aside-destination",line:`stray-destination: ${e} predates the migration; moved it to ${s} and put ${t} in its place`}}l(W,"claim");function x(t,e,n,r=!1){if(d(t)&&t!==e){if(d(e)){const o=W(t,e,n,r);if(o.line&&n.resolutions.push(o.line),o.action==="merge"){for(const i of h(t))x(c(t,i),c(e,i),n);b(t);return}if(o.action==="drop-source"){T(t,{recursive:!0}),n.changes.push(`${t} (identical to ${e}; removed)`);return}o.action==="remove-destination"?(T(e,{recursive:!0}),n.changes.push(`${e} (removed)`)):O(e,"replaced",n,E(n.map.old.root,e))}v(y(e),{recursive:!0}),k(t,e),n.moved++,n.changes.push(`${t} -> ${e}`)}}l(x,"movePath");function z(t,e,n,r=!1){if(!d(t)||t===e||!d(e))return;const o=W(t,e,n,r);if(o.line&&n.resolutions.push(o.line),o.action==="merge")for(const i of h(t))z(c(t,i),c(e,i),n)}l(z,"previewMove");function B(t){const{map:e}=t,n=c(e.old.canvas,"nodes"),r=c(e.old.canvas,"canvas.db"),o=c(e.old.root,"runtime","canvas.db"),i=d(r)?r:d(o)&&f(o).size>0?o:"";if(!i){if(d(n))throw new Error("old node records exist without canvas.db");return null}const s=new M(i,{readOnly:t.dryRun||i!==r});try{if(i===r&&!t.dryRun&&s.prepare("PRAGMA wal_checkpoint(TRUNCATE)").get().busy!==0)throw new Error("old canvas WAL is busy; stop every database user before migrating");return new Map(s.prepare("SELECT node_id, grantee FROM nodes").all().map(a=>{const u=a.grantee==="user:local"?"terminal":a.grantee.startsWith("app:")?a.grantee.slice(4):"";if(!u)throw new Error(`node ${a.node_id} has no app assignment`);return[a.node_id,u]}))}finally{s.close()}}l(B,"canvasNodeApps");function J(t,e){const n=c(t.map.old.canvas,"nodes");if(!d(n))return[];const r=[];for(const o of h(n)){const i=c(n,o),s=f(i).isDirectory();e.has(o)&&s||r.push({path:i,remove:!s&&ve(o)})}return r}l(J,"orphanNodeEntries");function G(t,e){return t.remove?`node-leftover: deleted ${t.path} (not a node record)`:`node-leftover: ${t.path} has no node row; moved to ${F(e,"nodes",A(t.path))}`}l(G,"orphanLine");function be(t){const{map:e}=t,n=c(e.old.canvas,"nodes"),r=B(t);if(r){for(const[o,i]of r)e.node(o,i);if(d(n)){for(const o of J(t,r)){if(t.resolutions.push(G(o,t)),o.remove){$(o.path),t.changes.push(`${o.path} (removed)`);continue}const i=O(o.path,"nodes",t),s=c(e.old.targetRoot,E(e.old.root,i));for(const a of new Set([e.old.storedCanvas,e.old.storedScope,c(e.old.storedScope,"canvas")]))e.add(c(a,"nodes",A(o.path)),s)}for(const[o,i]of r){const s=c(n,o);if(d(s)){for(const a of["context","reports","attachments"])x(c(s,a),c(e.old.root,"apps",i,"nodes",o,a),t);for(const a of h(s))["worktree","view.sock","broker-launch.json","system-prompt.md","situational-context.md","injected-docs.json"].includes(a)||x(c(s,a),c(e.old.root,"runtime","nodes",o,a),t);h(s).length===0&&b(s)}}h(n).length===0&&b(n)}}}l(be,"records");const H=["auth.json","anthropic-credentials.json","openai-codex-credentials.json","openai-credentials.json","crouter-operations.db"];function ge(t,e,n){if(!d(t))return;if(!f(t).isFile())throw new Error(`credential source is not a regular file: ${t}`);const r=w(t);if(d(e)){(!f(e).isFile()||!w(e).equals(r))&&n.resolutions.push(`credential: kept the existing ${e}; the older ${t} differs and stays where it is`);return}v(y(e),{recursive:!0});const o=c(y(e),`.${D()}.tmp`);try{const i=S(o,"wx",384);try{for(let s=0;s<r.length;)s+=q(i,r,s,r.length-s);R(i)}finally{g(i)}if(k(o,e),!w(e).equals(r))throw new Error(`credential verification failed: ${e}`)}finally{d(o)&&$(o)}n.moved++,n.changes.push(`${t} -> ${e} (source retained)`)}l(ge,"copyCredential");function K(t){if(!d(t))return;const e=new M(t);try{if(e.prepare("PRAGMA wal_checkpoint(TRUNCATE)").get().busy!==0)throw new Error(`SQLite WAL is busy: ${t}`)}finally{e.close()}}l(K,"checkpointDb");function Q(t,e){if(f(t).size!==f(e).size)return!1;const n=S(t,"r"),r=S(e,"r"),o=Buffer.allocUnsafe(64*1024),i=Buffer.allocUnsafe(o.length);try{for(;;){const s=U(n,o,0,o.length,null);if(s===0)return!0;let a=0;for(;a<s;){const u=U(r,i,a,s-a,null);if(u===0)return!1;a+=u}if(!o.subarray(0,s).equals(i.subarray(0,s)))return!1}}finally{g(n),g(r)}}l(Q,"sameFile");function P(t,e,n=!1){let r;try{r=f(e)}catch(u){if(u.code==="ENOENT")return!1;throw u}const o=f(t);if(o.isSymbolicLink())return r.isSymbolicLink()&&L(t)===L(e);if(o.isFile())return r.isFile()&&Q(t,e);if(!o.isDirectory()||!r.isDirectory())return!1;const i=h(t).filter(u=>!n||u!==".git"),s=h(e).filter(u=>!n||u!==".git"),a=new Set(s);return i.length===s.length&&i.every(u=>a.has(u)&&P(c(t,u),c(e,u),n))}l(P,"sameTree");function Se(t,e,n){if(!d(t))return;if(!f(t).isDirectory())throw new Error(`hosted work is not a directory: ${t}`);if(d(e)){if(P(t,e,!0))return;if(!f(e).isDirectory()||h(e).length!==0){n.resolutions.push(`hosted-work: kept the existing ${e}; the old ${t} differs and stays where it is`);return}b(e)}v(y(e),{recursive:!0});const r=c(y(e),`.work-${D()}.migrating`);try{if(te(t,r,{recursive:!0,verbatimSymlinks:!0,preserveTimestamps:!0}),!P(t,r))throw new Error(`hosted work copy verification failed: ${t}`);k(r,e),n.moved++,n.changes.push(`${t} -> ${e} (source retained)`)}finally{d(r)&&T(r,{recursive:!0,force:!0})}}l(Se,"copyHostedWork");function V(t){const{old:e}=t.map,n=c(e.root,"runtime"),r=c(n,"scope"),o=[];for(const i of["canvas.db","canvas.db-wal","canvas.db-shm","on-disk-migrations.json","requests","reviews","plugins","marketplaces","crtrd.log","crtrd.err","cache"])o.push([c(e.canvas,i),c(n,i)]);for(const i of["prompts","personas","config.json","profile-defaults.json","operations.db","operations.db-wal","operations.db-shm"])o.push([c(e.scope,i),c(r,i)]);for(const i of["support","prompt-reviews","plugins","marketplaces"])o.push([c(e.scope,i),c(n,i)]);return o.push([c(e.canvas,"secrets"),c(n,"secrets")]),o}l(V,"topLevelMoves");function ke(t){const{old:e}=t.map,n=c(e.root,"runtime");K(c(e.scope,"operations.db"));for(const[r,o]of V(t)){const i=$e(r);x(r,o,t,!0),Ee(i,r,o,t)}K(c(e.agent,"crouter-operations.db"));for(const r of H)ge(c(e.agent,r),c(n,"secrets",r),t);process.platform==="linux"&&Se(e.work,c(e.root,"user","files"),t)}l(ke,"topLevel");function $e(t){if(!d(t)||!f(t).isDirectory())return[];const e=[];for(const n of h(t)){const r=c(t,n);if(!f(r).isSymbolicLink())continue;const o=L(r);oe(o)||e.push({name:n,target:o})}return e}l($e,"relativeLinks");function Ee(t,e,n,r){if(!(t.length===0||!d(n)))for(const{name:o,target:i}of t){const s=c(n,o);if(!xe(s))continue;const a=r.map.mapPath(se(e,i)),u=E(n,a);u!==i&&($(s),Z(u,s),r.changes.push(`${s}: link ${i} -> ${u}`))}}l(Ee,"retargetLinks");function xe(t){try{return f(t),!0}catch{return!1}}l(xe,"lstatExists");function Ne(t){if(process.platform!=="linux")return;if(process.getuid?.()!==0)throw new Error("Linux layout migration requires root for the ownership pass");const e=w("/etc/passwd","utf8").split(`
|
|
2
|
+
`).find(s=>s.startsWith("crtrd:")),n=w("/etc/group","utf8").split(`
|
|
3
|
+
`).find(s=>s.startsWith("crtr-data:"));if(!e||!n)throw new Error("layout migration requires crtrd user and crtr-data group");const r=Number(e.split(":")[2]),o=Number(n.split(":")[2]);if(!Number.isInteger(r)||!Number.isInteger(o))throw new Error("invalid migration user or group");const i=l((s,a)=>{if(!d(s))return;const u=f(s);if(u.isSymbolicLink()){ne(s,r,a?o:u.gid);return}if(j(s,r,a?o:u.gid),u.isDirectory()){_(s,a?1528:448);for(const m of h(s))i(c(s,m),a)}else a&&_(s,u.mode&73?504:432)},"walk");i(c(t,"runtime"),!1),i(c(t,"user"),!0),i(c(t,"apps"),!0)}l(Ne,"ownership");function De(t){return new Promise(e=>{const n=re({socketPath:t,path:"/healthz",method:"GET",timeout:1e3},r=>{r.resume(),e(!0)});n.on("timeout",()=>n.destroy(new Error("daemon probe timed out"))),n.on("error",r=>{r.code==="ENOENT"||r.code==="ECONNREFUSED"?e(!1):e(!0)}),n.end()})}l(De,"daemonAnswers");async function Ie(t){const e=ae(),n=e.old,r=c(n.root,"runtime","layout");if(d(r))return C(r),null;const o=new Set([c(n.canvas,"crtrd.sock"),c(n.scope,"crtrd.sock"),process.platform==="linux"?"/run/crtr/api.sock":c(n.root,"runtime","api.sock")]);for(const p of o)if(await De(p))throw new I({error:"daemon_running",message:`cannot migrate while a daemon answers on ${p}`,next:"Stop the daemon that owns this volume, then run `crtr sys migrate` again."});if(process.platform==="linux"&&!d(n.agent))throw new I({error:"credential_volume_missing",message:`the old credential volume is not mounted at ${n.agent}`,next:`Mount the old provider-auth volume at ${n.agent}, then run \`crtr sys migrate\` again.`});const i=H.map(p=>c(n.agent,p)).filter(p=>!d(p)),s={map:e,moved:0,changes:[],resolutions:[],bodyPaths:[],absentCredentials:i,dryRun:t};if(t){const p=B(s);if(p)for(const N of J(s,p))s.resolutions.push(G(N,s));for(const[N,X]of V(s))z(N,X,s,!0);return de(s),s}fe(s);const a=[be,he,ke,pe,le,ue,ye];for(const p of a)p(s);n.canvas!==n.root&&d(n.canvas)&&h(n.canvas).length===0&&b(n.canvas),Ne(n.root),v(y(r),{recursive:!0});const u=`${r}.${D()}.tmp`,m=S(u,"wx",384);try{try{if(process.platform==="linux"){const p=f(y(r));j(u,p.uid,p.gid)}q(m,`${ie}
|
|
4
|
+
`),R(m)}finally{g(m)}try{ee(u,r)}catch(p){if(p.code==="EEXIST")C(r);else throw p}}finally{$(u)}return ce(y(r)),s}l(Ie,"migrateLayout");export{ze as OldToNewPathMap,W as claim,De as daemonAnswers,ve as isJunkFile,Ie as migrateLayout,x as movePath,Be as oldToNewPathMap,z as previewMove,Q as sameFile,O as setAside,F as setAsidePath};
|
package/dist/core/layout.d.ts
CHANGED
|
@@ -31,6 +31,14 @@ export declare function cronFolder(app: string, cronId: string): string;
|
|
|
31
31
|
* and the terminal wherever a sandbox exists, keeps its own app home. Callers
|
|
32
32
|
* keep `PI_CODING_AGENT_DIR` in the app home either way. */
|
|
33
33
|
export declare function appProcessHome(app: string): string;
|
|
34
|
+
/** The marker's byte length and an escaped, truncated preview, safe to print: a
|
|
35
|
+
* corrupt marker (empty, truncated, NUL-filled by a lost write) is otherwise
|
|
36
|
+
* indistinguishable from an intentional newer layout in a crash log. */
|
|
37
|
+
export declare function describeLayoutMarker(bytes: Uint8Array): string;
|
|
38
|
+
/** Refuse any marker other than the supported version. A non-matching marker is
|
|
39
|
+
* never rewritten: an empty or damaged one means the volume lost a write, and
|
|
40
|
+
* stamping the current version over it would assert a layout nobody verified. */
|
|
41
|
+
export declare function assertSupportedLayoutMarker(path: string): void;
|
|
34
42
|
/** A missing version on an occupied volume is never initialized by daemon startup. */
|
|
35
43
|
export declare function layoutNeedsMigration(): boolean;
|
|
36
44
|
export declare function initializeEmptyLayout(): void;
|
package/dist/core/layout.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
var
|
|
2
|
-
`,
|
|
1
|
+
var $=Object.defineProperty;var n=(r,o)=>$(r,"name",{value:o,configurable:!0});import{existsSync as i,mkdirSync as d,readFileSync as S,statSync as R}from"node:fs";import{homedir as _,userInfo as g}from"node:os";import{basename as x,dirname as f,isAbsolute as A,join as t,resolve as p}from"node:path";import{envHomeOverride as T}from"../shared/env.js";import{readConfig as O}from"./config.js";import{publishFileExclusive as z,syncDirectory as h}from"./fs-utils.js";const m="1";function s(){const r=p(T()||(process.platform==="linux"?"/":t(_(),".crouter")));if(C()&&r===L())throw new Error(`refusing to use the account's real crouter root ${r} from a node --test process; point CRTR_HOME (not only HOME) at a temporary directory`);return r}n(s,"layoutRoot");function C(){return process.env.NODE_TEST_CONTEXT!==void 0||process.execArgv.includes("--test")}n(C,"runningUnderNodeTest");let c;function L(){if(c===void 0)try{c=p(g().homedir,".crouter")}catch{c=null}return c}n(L,"accountLayoutRoot");function u(){return t(s(),"runtime")}n(u,"runtimeRoot");function v(){return t(s(),"user")}n(v,"userRoot");function y(){return t(s(),"apps")}n(y,"appsRoot");function w(){return t(u(),"secrets")}n(w,"secretsDir");function Y(r,o){if(!/^[a-z0-9]+(?:-[a-z0-9]+)*-[0-9a-f]{8}$/.test(o))throw new Error(`invalid profile id: ${o}`);const e=l(r);return{memory:t(e.memory,"profiles",o),env:t(w(),"env",x(e.root),o),home:e.home}}n(Y,"profileSpace");function E(){return process.platform==="linux"?"/run/crtr":u()}n(E,"socketDir");function a(){return t(u(),"layout")}n(a,"layoutVersionFile");function k(){const r=O("user").paths?.user_files??(process.platform==="linux"?t(v(),"files"):f(s()));if(!A(r))throw new Error("paths.user_files must be absolute");return p(r)}n(k,"userFilesRoot");function l(r){const o=r.startsWith("app:")?r.slice(4):r;if(!/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(o)||o==="."||o==="..")throw new Error(`invalid app id: ${r}`);const e=t(y(),o);return{root:e,memory:t(e,"memory"),conversations:t(e,"conversations"),nodes:t(e,"nodes"),files:t(e,"files"),home:t(e,"home"),cache:t(e,"cache")}}n(l,"appSpace");function q(r,o){if(!/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(o))throw new Error(`invalid cron id: ${o}`);return t(l(r).root,"crons",o)}n(q,"cronFolder");function P(r){const o=l(r);return process.platform==="linux"?o.home:x(o.root)==="terminal"?k():o.home}n(P,"appProcessHome");function F(r){const o=Array.from(r.subarray(0,32),e=>e===92?"\\\\":e===34?'\\"':e===10?"\\n":e===13?"\\r":e===9?"\\t":e>=32&&e<127?String.fromCharCode(e):`\\x${e.toString(16).padStart(2,"0")}`).join("");return`${r.length} bytes, preview "${o}"${r.length>32?"\u2026":""}`}n(F,"describeLayoutMarker");function M(r){const o=S(r);if(o.toString("utf8").trim()!==m)throw new Error(`unsupported runtime layout version: ${r} holds ${F(o)}; expected "${m}"`)}n(M,"assertSupportedLayoutMarker");function N(){if(process.env.CRTR_LAYOUT_REFUSE==="1")return!0;if(i(a()))return M(a()),!1;const r=s(),o=process.platform==="linux"&&r==="/"&&i("/volume")?"/volume":r;return i(t(o,"canvas"))||i(t(o,"canvas.db"))?!0:i(t(u(),"canvas.db"))}n(N,"layoutNeedsMigration");function W(){if(N())throw new Error("layout_migration_required");for(const r of[u(),v(),y(),w()]){const o=d(r,{recursive:!0,mode:448});if(o!==void 0){for(let e=r;h(e),e!==o;e=f(e));h(f(o))}}if(d(E(),{recursive:!0,mode:493}),process.platform==="linux"&&(R(E()).mode&511)!==493)throw new Error("launcher socket directory must be mode 0755");i(a())||z(a(),`${m}
|
|
2
|
+
`,384)}n(W,"initializeEmptyLayout");export{m as LAYOUT_VERSION,P as appProcessHome,l as appSpace,y as appsRoot,M as assertSupportedLayoutMarker,q as cronFolder,F as describeLayoutMarker,W as initializeEmptyLayout,N as layoutNeedsMigration,s as layoutRoot,a as layoutVersionFile,Y as profileSpace,u as runtimeRoot,w as secretsDir,E as socketDir,k as userFilesRoot,v as userRoot};
|
package/dist/core/locked-file.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var
|
|
1
|
+
var k=Object.defineProperty;var o=(c,t)=>k(c,"name",{value:t,configurable:!0});import{chmodSync as f,existsSync as a,fsyncSync as u,mkdirSync as C,openSync as m,readFileSync as y,renameSync as F,writeFileSync as T,closeSync as S}from"node:fs";import{dirname as w}from"node:path";import x from"proper-lockfile";import{publishFileExclusive as E}from"./fs-utils.js";const L={encoding:"utf-8",mode:384},O=3e4,$=5e3;class M{static{o(this,"LockedJsonFile")}path;durable;initialContent;constructor(t,i=!1,e="{}"){this.path=t,this.durable=i,this.initialContent=e}readUnlocked(){return a(this.path)?y(this.path,"utf-8"):void 0}ensureFile(){const t=w(this.path);a(t)||C(t,{recursive:!0,mode:448}),a(this.path)||(E(this.path,this.initialContent,384),f(this.path,384))}async withLockAsync(t){this.ensureFile();let i,e;const s=o(()=>{if(e)throw e},"throwIfCompromised");try{i=await x.lock(this.path,{realpath:!1,retries:{retries:10,factor:2,minTimeout:100,maxTimeout:1e4,randomize:!0},stale:O,update:$,onCompromised:o(h=>{e=h},"onCompromised")}),s();const b=a(this.path)?y(this.path,"utf-8"):void 0;let l=!1;const p=o(h=>{if(l)throw new Error(`locked file was published twice: ${this.path}`);s();const r=`${this.path}.${process.pid}.${Date.now()}.tmp`;if(T(r,h,L),f(r,384),this.durable){const n=m(r,"r");try{u(n)}finally{S(n)}}if(F(r,this.path),this.durable){const n=m(w(this.path),"r");try{u(n)}finally{S(n)}}l=!0},"publish"),{result:_,next:d}=await t(b,s,p);return s(),d!==void 0&&p(d),s(),_}finally{if(i)try{await i()}catch{}}}}export{M as LockedJsonFile};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { type DirectoryClient } from './directory.js';
|
|
2
|
+
import { type Envelope } from './envelope.js';
|
|
3
|
+
import { type TokenCache } from './token.js';
|
|
4
|
+
/** One attempt's budget. The hosting router gives an envelope-bearing request a 120 s header deadline; a cold wake must finish inside it. */
|
|
5
|
+
export declare const PEER_CALL_TIMEOUT_MS = 120000;
|
|
6
|
+
/** How long a `503 runtime_starting` is retried before the machine counts as unreachable. */
|
|
7
|
+
export declare const RUNTIME_STARTING_RETRY_BUDGET_MS = 60000;
|
|
8
|
+
/** First and longest wait between `runtime_starting` retries (doubling). */
|
|
9
|
+
export declare const RUNTIME_STARTING_BACKOFF_MS: {
|
|
10
|
+
readonly first: 1000;
|
|
11
|
+
readonly max: 8000;
|
|
12
|
+
};
|
|
13
|
+
/** Largest response body read. */
|
|
14
|
+
export declare const MAX_PEER_RESPONSE_BYTES: number;
|
|
15
|
+
export interface PeerRequest {
|
|
16
|
+
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
17
|
+
/** Absolute path on the callee, e.g. `/v1/nodes/<id>/messages`. */
|
|
18
|
+
path: string;
|
|
19
|
+
query?: Record<string, string | number | boolean | undefined> | URLSearchParams;
|
|
20
|
+
/** Serialized as JSON. */
|
|
21
|
+
body?: unknown;
|
|
22
|
+
}
|
|
23
|
+
export interface PeerOptions {
|
|
24
|
+
/** On whose behalf the call is made. Without one the call is the machine's own (class `machine` rows). */
|
|
25
|
+
envelope?: Envelope;
|
|
26
|
+
}
|
|
27
|
+
export interface PeerResponse {
|
|
28
|
+
status: number;
|
|
29
|
+
headers: Record<string, string>;
|
|
30
|
+
body: unknown;
|
|
31
|
+
}
|
|
32
|
+
export type UnreachableReason = 'no_directory' | 'unknown_machine' | 'directory_unreachable' | 'network' | 'timeout' | 'machine_offline' | 'runtime_starting';
|
|
33
|
+
export interface PeerUnreachable {
|
|
34
|
+
reason: UnreachableReason;
|
|
35
|
+
message: string;
|
|
36
|
+
}
|
|
37
|
+
export type PeerResult = PeerResponse | {
|
|
38
|
+
unreachable: PeerUnreachable;
|
|
39
|
+
};
|
|
40
|
+
export declare const isUnreachable: (result: PeerResult) => result is {
|
|
41
|
+
unreachable: PeerUnreachable;
|
|
42
|
+
};
|
|
43
|
+
export interface PeerDeps {
|
|
44
|
+
issuer(): string | null;
|
|
45
|
+
directory: DirectoryClient;
|
|
46
|
+
tokens: TokenCache;
|
|
47
|
+
fetch: typeof fetch;
|
|
48
|
+
sleep(ms: number): Promise<void>;
|
|
49
|
+
now(): number;
|
|
50
|
+
version(): string;
|
|
51
|
+
timeoutMs: number;
|
|
52
|
+
startingBudgetMs: number;
|
|
53
|
+
}
|
|
54
|
+
export declare function createPeerCall(deps: PeerDeps): (machineId: string, request: PeerRequest, options?: PeerOptions) => Promise<PeerResult>;
|
|
55
|
+
/** Call a machine of this account. Resolves reach through the directory (cached 5 min), authenticates with a machine token for the callee's URL,
|
|
56
|
+
* and retries `503 runtime_starting`. See the module header for the two kinds of result. */
|
|
57
|
+
export declare const peerCall: (machineId: string, request: PeerRequest, options?: PeerOptions) => Promise<PeerResult>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var S=Object.defineProperty;var i=(e,r)=>S(e,"name",{value:r,configurable:!0});import{crtrVersion as M}from"../../shared/crtr-version.js";import{directory as $,directoryIssuer as v,normalizeMachineId as T}from"./directory.js";import{ENVELOPE_HEADER as N,encodeEnvelope as O}from"./envelope.js";import{peerTokens as x}from"./token.js";import{PEER_VERSION_FLOOR as h,PEER_VERSION_HEADER as P,peerVersionRefusal as k}from"./version.js";const U=12e4,A=6e4,w={first:1e3,max:8e3},p=8*1024*1024,H=i(e=>"unreachable"in e,"isUnreachable"),s=i((e,r)=>({unreachable:{reason:e,message:r}}),"unreachable");function B(e){const r=e?.error;if(typeof r=="string")return r;const o=r?.code;return typeof o=="string"?o:null}i(B,"errorCode");async function I(e){if(Number(e.headers.get("content-length")??"0")>p)throw new RangeError("the peer answer is too large");const o=await e.text();if(o.length>p)throw new RangeError("the peer answer is too large");if(o==="")return null;try{return JSON.parse(o)}catch{return o}}i(I,"readBody");function L(e,r){const o=new URL(`${e.url}${r.path.startsWith("/")?"":"/"}${r.path}`),c=r.query instanceof URLSearchParams?r.query:Object.entries(r.query??{});for(const[l,u]of c instanceof URLSearchParams?c.entries():c)u!==void 0&&o.searchParams.append(l,String(u));return o.toString()}i(L,"targetUrl");function C(e){return i(async function(o,c,l={}){const u=e.issuer();if(u===null)return s("no_directory","this machine is not joined to a Northlight account, so it has no peers");let t;try{t=await e.directory.reach(o)}catch(a){return s("directory_unreachable",a instanceof Error?a.message:String(a))}if(t===null)return s("unknown_machine",`No machine ${T(o)} on your account.`);const d=t.crtr_version===null?null:k(t.crtr_version,h,e.version());if(d!==null)return{status:426,headers:{},body:{error:{code:d.code,message:`${t.name} runs crtr ${t.crtr_version}; peers below ${h} cannot take part. Update it with \`crtr sys update\`.`,details:{peer_version:t.crtr_version,floor:h,receiver_version:e.version()}}}};const R=L(t,c),_=e.now();let f=w.first,E=!1,g=!1;for(;;){let a;try{const n=await e.tokens.get(u,t.url);a=await e.fetch(R,{method:c.method,redirect:"manual",headers:{authorization:`Bearer ${n}`,[P]:e.version(),...l.envelope?{[N]:O(l.envelope)}:{},...c.body===void 0?{}:{"content-type":"application/json"}},...c.body===void 0?{}:{body:JSON.stringify(c.body)},signal:AbortSignal.timeout(g?Math.max(1e3,Math.min(e.timeoutMs,e.startingBudgetMs-(e.now()-_))):e.timeoutMs)})}catch(n){return n instanceof Error&&(n.name==="TimeoutError"||n.name==="AbortError")?s("timeout",`${t.name} did not answer within ${Math.round(e.timeoutMs/1e3)} s.`):n instanceof Error&&/directory/.test(n.message)?s("directory_unreachable",n.message):s("network",`${t.name} could not be reached: ${n instanceof Error?n.message:String(n)}`)}let m;try{m=await I(a)}catch(n){return s("network",`${t.name}'s answer could not be read: ${n instanceof Error?n.message:String(n)}`)}const y=B(m),b=Object.fromEntries(a.headers.entries());if(a.status===401&&!E){E=!0,e.tokens.drop(u,t.url);continue}if(a.status===503&&y==="machine_offline")return s("machine_offline",`${t.name} is offline.`);if(a.status===504)return s("timeout",`${t.name} did not answer in time.`);if(a.status===503&&y==="runtime_starting"){if(e.now()-_+f>e.startingBudgetMs)return s("runtime_starting",`${t.name} is still starting.`);await e.sleep(f),f=Math.min(f*2,w.max),g=!0;continue}return{status:a.status,headers:b,body:m}}},"peerCall")}i(C,"createPeerCall");const J=C({issuer:v,directory:$,tokens:x,fetch:i((e,r)=>fetch(e,r),"fetch"),sleep:i(e=>new Promise(r=>setTimeout(r,e)),"sleep"),now:Date.now,version:M,timeoutMs:U,startingBudgetMs:A});export{p as MAX_PEER_RESPONSE_BYTES,U as PEER_CALL_TIMEOUT_MS,w as RUNTIME_STARTING_BACKOFF_MS,A as RUNTIME_STARTING_RETRY_BUDGET_MS,C as createPeerCall,H as isUnreachable,J as peerCall};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { type TokenCache } from './token.js';
|
|
2
|
+
/** How long a machine's reach answer is trusted. */
|
|
3
|
+
export declare const REACH_TTL_MS: number;
|
|
4
|
+
/** Budget for one directory call. */
|
|
5
|
+
export declare const DIRECTORY_CALL_TIMEOUT_MS = 10000;
|
|
6
|
+
export type Reach = {
|
|
7
|
+
kind: 'stable';
|
|
8
|
+
} | {
|
|
9
|
+
kind: 'relay';
|
|
10
|
+
relay: string;
|
|
11
|
+
};
|
|
12
|
+
export interface Machine {
|
|
13
|
+
machine_id: string;
|
|
14
|
+
name: string;
|
|
15
|
+
/** The machine's callable address, also the audience of its machine tokens. */
|
|
16
|
+
url: string;
|
|
17
|
+
reach: Reach;
|
|
18
|
+
crtr_version: string | null;
|
|
19
|
+
last_seen: string | null;
|
|
20
|
+
is_default?: boolean;
|
|
21
|
+
}
|
|
22
|
+
export interface MachineStatus {
|
|
23
|
+
machine_id: string;
|
|
24
|
+
url: string;
|
|
25
|
+
reach: Reach | null;
|
|
26
|
+
}
|
|
27
|
+
/** The directory refused or failed a call. `status` is 0 when it could not be reached at all. */
|
|
28
|
+
export declare class DirectoryError extends Error {
|
|
29
|
+
readonly status: number;
|
|
30
|
+
constructor(status: number, message: string);
|
|
31
|
+
}
|
|
32
|
+
export interface DirectoryDeps {
|
|
33
|
+
/** The directory base URL, or null when this daemon has no directory. */
|
|
34
|
+
issuer(): string | null;
|
|
35
|
+
tokens: TokenCache;
|
|
36
|
+
fetch: typeof fetch;
|
|
37
|
+
now(): number;
|
|
38
|
+
}
|
|
39
|
+
export interface DirectoryClient {
|
|
40
|
+
/** Every live machine of the account. Refreshes the reach cache. */
|
|
41
|
+
list(): Promise<Machine[]>;
|
|
42
|
+
/** One machine's reach, cached; null when the directory does not know it (not_found, revoked). */
|
|
43
|
+
reach(machineId: string): Promise<Machine | null>;
|
|
44
|
+
/** `PUT /v1/machines/<own id>/status`. */
|
|
45
|
+
status(body: {
|
|
46
|
+
plugins?: string[];
|
|
47
|
+
crtr_version?: string;
|
|
48
|
+
}): Promise<MachineStatus>;
|
|
49
|
+
/** Forget one cached reach answer, or all. */
|
|
50
|
+
invalidate(machineId?: string): void;
|
|
51
|
+
}
|
|
52
|
+
export declare const normalizeMachineId: (id: string) => string;
|
|
53
|
+
export declare function createDirectoryClient(deps: DirectoryDeps, ownId: () => string): DirectoryClient;
|
|
54
|
+
/** This machine's id as the directory names it (`runtime:<id>`). */
|
|
55
|
+
export declare function ownMachineId(): string;
|
|
56
|
+
/** The directory this daemon belongs to, or null when it has none (an unjoined runtime). */
|
|
57
|
+
export declare function directoryIssuer(): string | null;
|
|
58
|
+
export declare const directory: DirectoryClient;
|
|
59
|
+
export declare const listMachines: () => Promise<Machine[]>;
|
|
60
|
+
export declare const machineReach: (machineId: string) => Promise<Machine | null>;
|
|
61
|
+
/** The URL peers call this machine at, as the directory last reported it from a status call; null before the first one. */
|
|
62
|
+
export declare function machineUrl(): string | null;
|
|
63
|
+
/** Tell the directory this machine is up (its plugins and crtr version) and store the URL it answers with as `machine_url`.
|
|
64
|
+
* Returns the stored URL, or null when this daemon has no directory identity. Throws `DirectoryError` when the directory fails. */
|
|
65
|
+
export declare function reportMachineStatus(client?: DirectoryClient): Promise<MachineStatus | null>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var x=Object.defineProperty;var n=(t,e)=>x(t,"name",{value:e,configurable:!0});import{prepared as y}from"../sqlite-statements.js";import{openDb as C,withCanvasWrite as k}from"../canvas/db.js";import{readConfig as v}from"../config.js";import{runtimeClient as p}from"../grants/sync.js";import{crtrVersion as M}from"../../shared/crtr-version.js";import{installedPluginIds as S}from"../directory/join.js";import{peerTokens as I}from"./token.js";const R=5*6e4,O=1e4;class c extends Error{static{n(this,"DirectoryError")}status;constructor(e,i){super(i),this.status=e,this.name="DirectoryError"}}const m=n(t=>t.startsWith("runtime:")?t:`runtime:${t}`,"normalizeMachineId");function _(t){if(!t||typeof t!="object")return null;const{kind:e,relay:i}=t;return e==="stable"?{kind:"stable"}:e==="relay"&&typeof i=="string"?{kind:"relay",relay:i}:null}n(_,"asReach");function g(t){const e=t,i=_(e?.reach);if(!e||typeof e.machine_id!="string"||typeof e.url!="string"||i===null)throw new c(502,"the directory answered a machine record with a missing machine_id, url or reach");return{machine_id:e.machine_id,name:typeof e.name=="string"?e.name:e.machine_id,url:e.url.replace(/\/$/,""),reach:i,crtr_version:typeof e.crtr_version=="string"?e.crtr_version:null,last_seen:typeof e.last_seen=="string"?e.last_seen:null,...typeof e.is_default=="boolean"?{is_default:e.is_default}:{}}}n(g,"asMachine");function U(t,e){const i=new Map;async function h(o,s,r){const a=t.issuer();if(a===null)throw new c(0,"this daemon has no directory");const f=a.replace(/\/$/,"");for(let w=0;;w++){let u;try{const l=await t.tokens.get(f);u=await t.fetch(`${f}${s}`,{method:o,headers:{authorization:`Bearer ${l}`,...r===void 0?{}:{"content-type":"application/json"}},...r===void 0?{}:{body:JSON.stringify(r)},signal:AbortSignal.timeout(O)})}catch(l){throw new c(0,`the directory could not be reached: ${l instanceof Error?l.message:String(l)}`)}if(u.status===401&&w===0){t.tokens.drop(f);continue}if(!u.ok)throw new c(u.status,`directory ${o} ${s} answered ${u.status}`);try{return await u.json()}catch{throw new c(502,`directory ${o} ${s} answered a body that is not JSON`)}}}return n(h,"call"),{async list(){const o=await h("GET","/v1/machines");if(!Array.isArray(o))throw new c(502,"the directory answered a machine list that is not an array");const s=o.map(g),r=t.now();for(const a of s)i.set(a.machine_id,{machine:a,at:r});return s},async reach(o){const s=m(o),r=i.get(s);if(r&&t.now()-r.at<R)return r.machine;try{const a=g(await h("GET",`/v1/machines/${encodeURIComponent(s)}/reach`));return i.set(s,{machine:a,at:t.now()}),a}catch(a){if(a instanceof c&&a.status===404)return i.delete(s),null;if(r&&a instanceof c&&a.status===0)return r.machine;throw a}},async status(o){const s=e(),r=await h("PUT",`/v1/machines/${encodeURIComponent(s)}/status`,o);if(!r||typeof r.machine_id!="string"||typeof r.url!="string")throw new c(502,"the directory answered a status record with a missing machine_id or url");return{machine_id:r.machine_id,url:r.url.replace(/\/$/,""),reach:_(r.reach)}},invalidate(o){o===void 0?i.clear():i.delete(m(o))}}}n(U,"createDirectoryClient");function E(){return m(p().runtime)}n(E,"ownMachineId");function T(){return v("user").runtime?.issuer??null}n(T,"directoryIssuer");const d=U({issuer:T,tokens:I,fetch:n((t,e)=>fetch(t,e),"fetch"),now:Date.now},E),z=n(()=>d.list(),"listMachines"),F=n(t=>d.reach(t),"machineReach"),$="machine_url";function G(){return y(C(),"SELECT value FROM runtime_state WHERE key = ?").get($)?.value??null}n(G,"machineUrl");function b(t){k(e=>{y(e,"INSERT INTO runtime_state (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value").run($,t)})}n(b,"storeMachineUrl");async function J(t=d){if(T()===null)return null;try{p()}catch{return null}const e=await t.status({plugins:S(),crtr_version:M()});if(e.machine_id!==E())throw new c(502,"the directory answered a status for another machine");return b(e.url),e}n(J,"reportMachineStatus");export{O as DIRECTORY_CALL_TIMEOUT_MS,c as DirectoryError,R as REACH_TTL_MS,U as createDirectoryClient,d as directory,T as directoryIssuer,z as listMachines,F as machineReach,G as machineUrl,m as normalizeMachineId,E as ownMachineId,J as reportMachineStatus};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { RequestPrincipal } from '../../daemon/api/router.js';
|
|
2
|
+
import { type RunPlace } from '../runs/confinement.js';
|
|
3
|
+
export declare const ENVELOPE_HEADER = "Crtr-Envelope";
|
|
4
|
+
/** A header larger than this is refused unread. */
|
|
5
|
+
export declare const MAX_ENVELOPE_BYTES: number;
|
|
6
|
+
/** The app (and its node) a call is made for. `node` is null for a cron with no anchor node. */
|
|
7
|
+
export interface EnvelopeApp {
|
|
8
|
+
sub: string;
|
|
9
|
+
node: string | null;
|
|
10
|
+
node_scopes: string[] | null;
|
|
11
|
+
place: RunPlace | null;
|
|
12
|
+
}
|
|
13
|
+
export interface Envelope {
|
|
14
|
+
sub: string;
|
|
15
|
+
act: {
|
|
16
|
+
sub: string;
|
|
17
|
+
act?: EnvelopeApp;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/** The caller kinds that originate a peer call. A forwarded caller never re-forwards, so it is not here. */
|
|
21
|
+
export type EnvelopeCaller = Extract<RequestPrincipal, {
|
|
22
|
+
class: 'person' | 'broker' | 'cron';
|
|
23
|
+
}>;
|
|
24
|
+
export declare class EnvelopeError extends Error {
|
|
25
|
+
constructor(message: string);
|
|
26
|
+
}
|
|
27
|
+
export interface EnvelopeContext {
|
|
28
|
+
/** The account owner (`user:<id>`) the call is made for. */
|
|
29
|
+
owner?: string;
|
|
30
|
+
/** This machine's id (`runtime:<id>`). */
|
|
31
|
+
via?: string;
|
|
32
|
+
/** A node's run place. */
|
|
33
|
+
placeOf?: (nodeId: string) => RunPlace | null;
|
|
34
|
+
}
|
|
35
|
+
/** The envelope for a call made by `principal` on this machine. Broker → its app, node, stored scopes and run place; cron → its app, anchor node
|
|
36
|
+
* (null when unanchored), stored scopes and origin run; person → no inner `act`. */
|
|
37
|
+
export declare function envelopeFor(principal: EnvelopeCaller, context?: EnvelopeContext): Envelope;
|
|
38
|
+
export declare function encodeEnvelope(envelope: Envelope): string;
|
|
39
|
+
/** Parse and validate a `Crtr-Envelope` header value. Throws `EnvelopeError` for anything that is not the documented shape. */
|
|
40
|
+
export declare function decodeEnvelope(header: string): Envelope;
|