@north-light/cli 0.3.442 → 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 +570 -570
- package/dist/core/directory/join.d.ts +2 -0
- package/dist/core/directory/join.js +1 -1
- 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/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/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};
|
|
@@ -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};
|
|
@@ -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;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var i=Object.defineProperty;var l=(e,o)=>i(e,"name",{value:o,configurable:!0});import{joinedOwner as d}from"../directory/join.js";import{runPlace as h}from"../runs/confinement.js";import{ownMachineId as p}from"./directory.js";const g="Crtr-Envelope",b=16*1024;class n extends Error{static{l(this,"EnvelopeError")}constructor(o){super(o),this.name="EnvelopeError"}}function y(e,o={}){const s=o.via??p(),t=e.class==="person"?"":e.sub,r=o.owner??(t.startsWith("user:")?t:d());if(r==null)throw new n("this machine has no account owner to make a peer call for");if(e.class==="person")return{sub:r,act:{sub:s}};if(e.class==="broker"){const a=(o.placeOf??(u=>h(u)))(e.node);return{sub:r,act:{sub:s,act:{sub:e.app,node:e.node,node_scopes:e.scopes===null?null:[...e.scopes],place:a}}}}return{sub:r,act:{sub:s,act:{sub:e.app,node:e.anchor,node_scopes:e.scopes===null?null:[...e.scopes],place:{run:e.origin_run??"",isolated:e.isolated}}}}}l(y,"envelopeFor");function _(e){return Buffer.from(JSON.stringify(e),"utf8").toString("base64url")}l(_,"encodeEnvelope");const c=l(e=>typeof e=="string"&&e!=="","isString"),f=l(e=>typeof e=="object"&&e!==null&&!Array.isArray(e),"isRecord");function A(e){if(e.length>b)throw new n("the envelope is too large");if(!/^[A-Za-z0-9_-]+$/.test(e))throw new n("the envelope is not base64url");let o;try{o=JSON.parse(Buffer.from(e,"base64url").toString("utf8"))}catch{throw new n("the envelope is not JSON")}if(!f(o)||!c(o.sub)||!o.sub.startsWith("user:"))throw new n("the envelope names no user");const s=o.act;if(!f(s)||!c(s.sub)||!s.sub.startsWith("runtime:"))throw new n("the envelope names no calling machine");const t=s.act;if(t===void 0)return{sub:o.sub,act:{sub:s.sub}};if(!f(t)||!c(t.sub)||!t.sub.startsWith("app:"))throw new n("the envelope names no calling app");const{node:r,node_scopes:a,place:u}=t;if(r!==null&&!c(r))throw new n("the envelope node is not a node id");if(a!==null&&!(Array.isArray(a)&&a.every(c)))throw new n("the envelope node_scopes is not a list of scopes");if(u!==null&&!(f(u)&&typeof u.run=="string"&&typeof u.isolated=="boolean"))throw new n("the envelope place is malformed");return{sub:o.sub,act:{sub:s.sub,act:{sub:t.sub,node:r,node_scopes:a,place:u===null?null:{run:u.run,isolated:u.isolated}}}}}l(A,"decodeEnvelope");export{g as ENVELOPE_HEADER,n as EnvelopeError,b as MAX_ENVELOPE_BYTES,A as decodeEnvelope,_ as encodeEnvelope,y as envelopeFor};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface PeerEvent {
|
|
2
|
+
/** Subscribe; returns the function that unsubscribes. */
|
|
3
|
+
on(listener: () => void): () => void;
|
|
4
|
+
/** Call every listener once. A throwing listener never stops the others. */
|
|
5
|
+
emit(): void;
|
|
6
|
+
listenerCount(): number;
|
|
7
|
+
}
|
|
8
|
+
/** Emitted on every successful connect of this daemon's WebSocket to the account relay. */
|
|
9
|
+
export declare const relayConnected: PeerEvent;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var r=Object.defineProperty;var n=(e,t)=>r(e,"name",{value:t,configurable:!0});function o(){const e=new Set;return{on(t){return e.add(t),()=>{e.delete(t)}},emit(){for(const t of[...e])try{t()}catch{}},listenerCount:n(()=>e.size,"listenerCount")}}n(o,"createPeerEvent");const s=o();export{s as relayConnected};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type DirectoryToken } from '../grants/sync.js';
|
|
2
|
+
export declare const REFRESH_BEFORE_EXPIRY_MS = 30000;
|
|
3
|
+
export type Mint = (issuer: string, audience?: string) => Promise<DirectoryToken>;
|
|
4
|
+
export interface TokenCache {
|
|
5
|
+
/** A token for `audience` (a peer machine URL or the relay origin), or for the directory's own `/v1` when omitted. */
|
|
6
|
+
get(issuer: string, audience?: string): Promise<string>;
|
|
7
|
+
/** Forget a token the far side refused (revoked key, clock skew); the next `get` mints a new one. */
|
|
8
|
+
drop(issuer: string, audience?: string): void;
|
|
9
|
+
clear(): void;
|
|
10
|
+
}
|
|
11
|
+
export declare function createTokenCache(mint?: Mint, now?: () => number): TokenCache;
|
|
12
|
+
/** The daemon's process-wide cache. */
|
|
13
|
+
export declare const peerTokens: TokenCache;
|
|
14
|
+
/** A machine token whose `aud` is `audience` (a peer machine's URL, or `https://relay.northlight.ai`). */
|
|
15
|
+
export declare function machineToken(issuer: string, audience: string): Promise<string>;
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
var k=Object.defineProperty;var i=(n,r)=>k(n,"name",{value:r,configurable:!0});import{mintDirectoryToken as _}from"../grants/sync.js";const g=3e4;function h(n=_,r=Date.now){const c=new Map,a=new Map,p=i((e,t)=>`${e.replace(/\/$/,"")}
|
|
2
|
+
${t??""}`,"keyOf");return{async get(e,t){const o=p(e,t),l=c.get(o);if(l&&l.expires_at-g>r())return l.access_token;let s=a.get(o);return s||(s=n(e,t).then(f=>(c.set(o,f),f)).finally(()=>{a.delete(o)}),a.set(o,s)),(await s).access_token},drop(e,t){c.delete(p(e,t))},clear(){c.clear(),a.clear()}}}i(h,"createTokenCache");const u=h();function E(n,r){return u.get(n,r)}i(E,"machineToken");export{g as REFRESH_BEFORE_EXPIRY_MS,h as createTokenCache,E as machineToken,u as peerTokens};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Request header naming the sending machine's crtr release. */
|
|
2
|
+
export declare const PEER_VERSION_HEADER = "Crtr-Version";
|
|
3
|
+
/** The oldest crtr release that speaks the peer protocol. L1 sets this to the release that ships C1–C6. */
|
|
4
|
+
export declare const PEER_VERSION_FLOOR = "0.3.442";
|
|
5
|
+
export interface PeerVersionRefusal {
|
|
6
|
+
status: 426;
|
|
7
|
+
code: 'peer_version_too_old';
|
|
8
|
+
message: string;
|
|
9
|
+
details: {
|
|
10
|
+
peer_version: string | null;
|
|
11
|
+
floor: string;
|
|
12
|
+
receiver_version: string;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
/** The refusal for a peer announcing `peerVersion` (the `Crtr-Version` header, or a reach answer's `crtr_version`), or null when it is at or above the floor. */
|
|
16
|
+
export declare function peerVersionRefusal(peerVersion: string | null | undefined, floor?: string, own?: string): PeerVersionRefusal | null;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var n=Object.defineProperty;var i=(r,e)=>n(r,"name",{value:e,configurable:!0});import{compareReleaseVersions as a}from"../../api/runtime-version.js";import{crtrVersion as o}from"../../shared/crtr-version.js";const l="Crtr-Version",c="0.3.442";function d(r,e=c,s=o()){const t=r?.trim()?r.trim():null;return t!==null&&a(t,e)>=0?null:{status:426,code:"peer_version_too_old",message:t===null?`The peer did not name its crtr version; machines below ${e} cannot take part (this machine is ${s}). Update it with \`crtr sys update\`.`:`The peer runs crtr ${t}; machines below ${e} cannot take part (this machine is ${s}). Update it with \`crtr sys update\`.`,details:{peer_version:t,floor:e,receiver_version:s}}}i(d,"peerVersionRefusal");export{c as PEER_VERSION_FLOOR,l as PEER_VERSION_HEADER,d as peerVersionRefusal};
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
var X=Object.defineProperty;var i=(e,n)=>X(e,"name",{value:n,configurable:!0});import{existsSync as h,readdirSync as H,readFileSync as y,rmSync as b,statSync as x}from"node:fs";import{join as u}from"node:path";import{validateCommandManifest as Y}from"../../api/command-manifest/manifest.js";import{crtrHome as O}from"../canvas/paths.js";import{withCanvasWrite as ee}from"../canvas/db.js";import{bundleVersion as ne,isInstalledGeneration as re,recordCheckedGeneration as te}from"../command-plugins/bundle.js";import{readConfig as U,readState as oe}from"../config.js";import{emitEvent as g}from"../events/emit.js";import{withExclusiveDirectoryLockAsync as ie}from"../exclusive-lock.js";import{atomicWriteText as M,ensureDir as I,pathExists as ae}from"../fs-utils.js";import{reconcilePackageDocuments as se}from"../graph/package-docs.js";import{readPluginManifest as W}from"../manifest.js";import{bundleSwapLockPath as de}from"../plugin-swap-lock.js";import{pluginsDir as ue,userScopeRoot as ce}from"../scope.js";import{
|
|
1
|
+
var X=Object.defineProperty;var i=(e,n)=>X(e,"name",{value:n,configurable:!0});import{existsSync as h,readdirSync as H,readFileSync as y,rmSync as b,statSync as x}from"node:fs";import{join as u}from"node:path";import{validateCommandManifest as Y}from"../../api/command-manifest/manifest.js";import{crtrHome as O}from"../canvas/paths.js";import{withCanvasWrite as ee}from"../canvas/db.js";import{bundleVersion as ne,isInstalledGeneration as re,recordCheckedGeneration as te}from"../command-plugins/bundle.js";import{readConfig as U,readState as oe}from"../config.js";import{emitEvent as g}from"../events/emit.js";import{withExclusiveDirectoryLockAsync as ie}from"../exclusive-lock.js";import{atomicWriteText as M,ensureDir as I,pathExists as ae}from"../fs-utils.js";import{reconcilePackageDocuments as se}from"../graph/package-docs.js";import{readPluginManifest as W}from"../manifest.js";import{bundleSwapLockPath as de}from"../plugin-swap-lock.js";import{pluginsDir as ue,userScopeRoot as ce}from"../scope.js";import{localGrants as fe}from"../grants/mirror.js";import{ProviderError as c}from"./errors.js";const T="[A-Za-z0-9_-]{1,64}",le=new RegExp(`^(${T}):(${T})$`),z=new RegExp(`^${T}$`),pe=3e4,me=3e4;function ve(e){const n=new Set;for(const r of e)if(r.status==="active")for(const{scope:t}of r.resolved_scopes){const o=le.exec(t);o&&o[1]!=="crtr"&&n.add(o[1])}return[...n].sort()}i(ve,"providerIdsFromGrants");function w(e){return u(ue("user"),e)}i(w,"pluginRoot");function F(){return u(O(),"providers")}i(F,"providersRoot");function G(e,n){return u(F(),e,`v${n}`)}i(G,"majorDir");function j(e){if(!z.test(e))throw new c("not_found",`"${e}" is not a provider id`,{param:"provider"})}i(j,"assertId");function A(){const e=U("user").runtime?.issuer;if(!e)throw new c("not_found","provider bundles need a directory-connected runtime");return e.replace(/\/+$/,"")}i(A,"directoryBase");async function q(e,n){const r=`${A()}/providers/${encodeURIComponent(e)}/plugin${n.version!==void 0?`?version=${n.version}`:""}`;let t;try{t=await fetch(r,{headers:{Accept:"application/x-tar",...n.ifNoneMatch!==void 0?{"If-None-Match":n.ifNoneMatch}:{}},signal:AbortSignal.timeout(pe)})}catch(s){return{status:"failed",message:`could not reach ${r}: ${s instanceof Error?s.message:String(s)}`}}if(t.status===304)return n.ifNoneMatch===void 0?{status:"failed",message:`HTTP 304 from ${r} without a conditional request`}:{status:304};if(t.status===404||t.status===410)return await t.body?.cancel().catch(()=>{}),{status:t.status===404?404:410,message:`HTTP ${t.status} from ${r}`};if(t.status<200||t.status>=300)return await t.body?.cancel().catch(()=>{}),{status:"failed",message:`HTTP ${t.status} from ${r}`};let o;try{o=new Uint8Array(await t.arrayBuffer())}catch(s){return{status:"failed",message:`reading ${r} failed: ${s instanceof Error?s.message:String(s)}`}}const a=t.headers.get("etag");return{status:200,raw:o,...a!==null?{etag:a}:{}}}i(q,"fetchProviderBundle");const _=new WeakSet;class S extends Error{static{i(this,"BundleInvalid")}issues;constructor(n,r){super(n),this.issues=r}}function V(e,n,r){if(r.provider===void 0)return"the archive carries no provider block";if(r.provider.id!==e)return`the archive is provider "${r.provider.id}", not "${e}"`;if(n!==void 0&&r.provider.major!==n)return`the archive is major ${r.provider.major}, not ${n}`;const t=["hooks","kinds","page_components","memory_extensions"].filter(o=>r[o]!==void 0);if(t.length>0)return`a provider bundle may not carry ${t.join(", ")}`}i(V,"wrongBundle");function C(e,n,r){g({level:"error",event:"daemon.providers.bundle_invalid",fields:{provider:e,...n!==void 0?{major:n}:{},...r instanceof S?{issues:r.issues}:{}},error:r})}i(C,"logInvalid");function Z(e,n,r){g({level:"warn",event:"daemon.providers.bundle_fetch_failed",fields:{provider:e,...n!==void 0?{major:n}:{},message:r}})}i(Z,"logFailure");async function L(){return import("../../commands/pkg/plugin-manage.js")}i(L,"pluginManage");const P=new Map,E=i(e=>`${O()}\0${e}`,"installKey");function he(e){const n=e,r=n?.code??n?.cause?.code;return typeof r=="string"&&/^[A-Z][A-Z0-9_]{1,30}$/.test(r)?`${r} on provider install directory`:e instanceof c&&e.message.includes("HTTP 404")?"directory returned HTTP 404":e instanceof c&&e.message.includes("HTTP 410")?"directory returned HTTP 410":"provider bundle could not be installed"}i(he,"publicInstallCause");function ge(e){if(m(e)===void 0)return P.get(E(e))??{status:"installing"}}i(ge,"providerInstallState");function we(e){return new c("provider_unavailable",`provider "${e}" unavailable: ${ge(e)?.error??"bundle installation pending"}`)}i(we,"unavailableProviderError");function m(e){const n=W(w(e));if(!(n?.provider===void 0||n.transport?.kind!=="http")&&ae(u(w(e),"commands.json"))){try{if(x(u(w(e),"commands.json")).size===0)return}catch{return}return{provider:n.provider,transport:{kind:"http",endpoint:n.transport.endpoint}}}}i(m,"installedManifest");async function k(e,n,r=!1){const t=ce();m(e)===void 0&&P.set(E(e),{status:"installing"});try{return I(u(t,"tmp")),await ie(de(t,e),async()=>{const o=w(e),a=W(o);if(a!==null&&a.provider===void 0)throw new c("provider_unavailable",`plugin "${e}" is installed and is not the directory's provider bundle`);const s=m(e)!==void 0;if(s&&r)return"unchanged";const f=s&&n?oe("user").plugins[e]?.etag:void 0,d=await q(e,f!==void 0?{ifNoneMatch:f}:{});if(d.status===304)return"unchanged";if(d.status===404||d.status===410)throw new c("not_found",`the directory serves no provider "${e}" (${d.message})`,{param:"provider"});if(d.status==="failed")throw new c("provider_unavailable",`provider "${e}" bundle fetch failed: ${d.message}`);const l=ne(d.raw);if(s&&re(e,"user",o,l))return te("user",e,d.etag,{swapped:!1}),"unchanged";const{installPluginArchive:v}=await L();try{await v(e,d.raw,{scope:"user",scopeRootPath:t,root:o,version:l,etag:d.etag,enable:!s,bundle:{endpoint:`${A()}/providers/${encodeURIComponent(e)}/plugin`},transport:{kind:"http",endpoint:A()},accept:i(p=>V(e,void 0,p),"accept")})}catch(p){C(e,void 0,p);const N=new c("not_found",`provider "${e}" served an invalid bundle: ${p instanceof Error?p.message:String(p)}`,{param:"provider"});throw _.add(N),N}try{ee(p=>se(p))}catch(p){g({level:"error",event:"daemon.providers.docs_reconcile_failed",fields:{provider:e},error:p})}return g({level:"info",event:"daemon.providers.bundle_installed",fields:{provider:e,version:l,replaced:s}}),P.delete(E(e)),s?"updated":"installed"},{timeoutMs:me,timeoutError:i(()=>new c("provider_unavailable",`timed out waiting for another process replacing plugin "${e}"`),"timeoutError")})}catch(o){throw m(e)===void 0&&P.set(E(e),{status:"install_failed",error:he(o)}),o instanceof c?o:(g({level:"error",event:"daemon.providers.bundle_install_failed",fields:{provider:e},error:o}),we(e))}}i(k,"refreshInstalled");async function We(e){j(e),m(e)===void 0&&await k(e,!1,!0)}i(We,"ensureProviderBundle");async function ze(e,n){return j(e),n.version!==void 0&&m(e)?.provider.major!==n.version?R(e,n.version,n.conditional):k(e,n.conditional)}i(ze,"refreshProviderBundle");async function J(e,n,r){const{validatePluginArchive:t}=await L(),o=await t(r);if(o.bundle===void 0)throw new S(`provider "${e}" major ${n} bundle is invalid: ${o.issues.map(s=>s.message).join("; ")}`,o.issues);const a=V(e,n,o.bundle);if(a!==void 0)throw new S(`provider "${e}" major ${n} bundle is invalid: ${a}`,[]);if(o.bundle.transport===void 0)throw new S(`provider "${e}" major ${n} bundle declares no transport`,[]);return o.bundle}i(J,"validateArchive");function $e(e){I(e),M(u(e,"retired"),`${new Date().toISOString()}
|
|
2
2
|
`),b(u(e,"bundle.tar"),{force:!0}),b(u(e,"etag"),{force:!0})}i($e,"retire");function K(e,n){return new c("provider_version_retired",`provider "${e}" major ${n} is retired`,{param:"version"})}i(K,"retiredError");async function R(e,n,r){const t=G(e,n),o=u(t,"bundle.tar"),a=h(o),s=a&&r&&h(u(t,"etag"))?y(u(t,"etag"),"utf8"):void 0,f=await q(e,{version:n,...s!==void 0?{ifNoneMatch:s}:{}});if(f.status===304)return"unchanged";if(f.status===410)throw $e(t),g({level:"info",event:"daemon.providers.major_retired",fields:{provider:e,major:n}}),K(e,n);if(f.status===404)throw new c("not_found",`provider "${e}" never served major ${n}`,{param:"version"});if(f.status==="failed")throw new c("provider_unavailable",`provider "${e}" major ${n} bundle fetch failed: ${f.message}`);try{await J(e,n,f.raw)}catch(l){C(e,n,l);const v=new c(a?"provider_unavailable":"not_found",l instanceof Error?l.message:String(l),{param:"version"});throw _.add(v),v}I(t),b(u(t,"retired"),{force:!0});const d=a&&Buffer.from(f.raw).equals(y(o));return d||M(o,f.raw),f.etag!==void 0?M(u(t,"etag"),f.etag):b(u(t,"etag"),{force:!0}),d?"unchanged":a?"updated":"installed"}i(R,"refreshPinned");function ye(){const e=[],n=F();if(!h(n))return e;for(const r of H(n)){if(!z.test(r))continue;let t;try{t=H(u(n,r))}catch{continue}for(const o of t){const a=/^v([1-9][0-9]*)$/.exec(o);a&&h(u(n,r,o,"bundle.tar"))&&e.push([r,Number(a[1])])}}return e}i(ye,"cachedMajors");function be(e){const n=Y(e,{transport:"http",reservedCoreNames:new Set});if(n.manifest===void 0)throw new c("not_found","the provider bundle's commands.json no longer validates");return n.manifest}i(be,"readCommands");const B=new Map,Q=new Map;function _e(e,n){const r=x(n),t=`${r.ino}:${r.mtimeMs}:${r.size}`,o=Q.get(e);if(o?.stamp===t)return o.commands;const a=be(JSON.parse(y(n,"utf8")));return Q.set(e,{stamp:t,commands:a}),a}i(_e,"installedCommands");async function Fe(e,n){j(e);const r=m(e);if(n===void 0||r?.provider.major===n){if(r===void 0)throw new c("not_found",`provider "${e}" has no installed bundle`,{param:"provider"});const l=w(e),v=_e(e,u(l,"commands.json"));return{id:e,major:r.provider.major,provider:r.provider,commands:v,transport:r.transport,source:"installed",root:l}}if(!Number.isInteger(n)||n<1)throw new c("invalid_request","version must be an integer of at least 1",{param:"version"});const t=G(e,n);if(h(u(t,"retired")))throw K(e,n);const o=u(t,"bundle.tar");h(o)||await R(e,n,!1);const a=`${e}/${n}`,s=x(o),f=`${s.mtimeMs}:${s.size}`;let d=B.get(a)?.stamp===f?B.get(a).bundle:void 0;if(d===void 0){try{d=await J(e,n,new Uint8Array(y(o)))}catch(l){throw C(e,n,l),new c("not_found",`provider "${e}" has no valid bundle for major ${n}`,{param:"version"})}B.set(a,{stamp:f,bundle:d})}return{id:e,major:n,provider:d.provider,commands:d.commands,transport:{kind:"http",endpoint:d.transport.endpoint},source:"pinned"}}i(Fe,"resolveProviderBundle");let $=null,D=!1;async function Se(){if(U("user").runtime?.issuer){for(const e of ve(fe()))try{await k(e,!0)}catch(n){n instanceof Object&&_.has(n)||Z(e,void 0,n instanceof Error?n.message:String(n))}for(const[e,n]of ye())try{await R(e,n,!0)}catch(r){if(r instanceof c&&r.code==="provider_version_retired"||r instanceof Object&&_.has(r))continue;Z(e,n,r instanceof Error?r.message:String(r))}}}i(Se,"syncOnce");function Ge(){return $!==null?(D=!0,$):($=(async()=>{try{do D=!1,await Se();while(D)}finally{$=null}})(),$)}i(Ge,"syncProviderBundles");export{We as ensureProviderBundle,ve as providerIdsFromGrants,ge as providerInstallState,ze as refreshProviderBundle,Fe as resolveProviderBundle,Ge as syncProviderBundles,we as unavailableProviderError};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
var E=Object.defineProperty;var s=(e,r)=>E(e,"name",{value:r,configurable:!0});import{randomUUID as P}from"node:crypto";import{readConfig as I}from"../../../core/config.js";import{activeGrantForApp as R,
|
|
1
|
+
var E=Object.defineProperty;var s=(e,r)=>E(e,"name",{value:r,configurable:!0});import{randomUUID as P}from"node:crypto";import{readConfig as I}from"../../../core/config.js";import{activeGrantForApp as R,localGrants as _}from"../../../core/grants/mirror.js";import{getNode as O}from"../../../core/canvas/canvas.js";import{scopeAllowed as $}from"../../../core/scopes.js";import{ProviderError as u,providerErrorBody as q,scopeMissing as x}from"../../../core/providers/errors.js";import{callProvider as G,providerTools as N}from"../../../core/providers/call.js";import{providerIdsFromGrants as T,providerInstallState as F}from"../../../core/providers/bundles.js";import{listInstalledPlugins as j}from"../../../core/resolver.js";import{listAudit as C,writeProviderAudit as D}from"../../../core/providers/audit.js";import{emitEvent as g}from"../../../core/events/emit.js";import{resolvedScopes as h}from"../router.js";const fe=new Set(["provider.tools","provider.call","audit.list"]);function M(e){if(e.class==="person")return{class:"person",sub:null,grantee:null,client_id:null,resolved:null,nodeScopes:null,run_id:null,node_id:null};if(e.class==="app"){const r=R(e.app);if(!r)throw new u("grant_revoked",`app ${e.app} has no active grant`);return{class:"app",sub:r.sub,grantee:e.app,client_id:null,resolved:r.resolved,nodeScopes:e.scopes,run_id:null,node_id:null}}return e.class==="cron"?{class:"app",sub:e.sub,grantee:e.app,client_id:null,resolved:h(e),nodeScopes:e.scopes,run_id:e.origin_run,node_id:null}:{class:"broker",sub:e.sub,grantee:e.app,client_id:e.client_id,resolved:h(e),nodeScopes:e.scopes,run_id:e.run??O(e.node)?.run_id??null,node_id:e.node}}s(M,"callerFromRequest");function ce(e){return{class:"app",sub:e.sub,grantee:e.grantee,client_id:e.client_id,resolved:e.resolved,nodeScopes:null,run_id:null,node_id:null}}s(ce,"callerFromApp");const U=/^([A-Za-z0-9_-]{1,64}):([A-Za-z0-9_-]{1,64})$/;function k(e){const r=U.exec(e);return r!==null&&r[1]!=="crtr"}s(k,"isProviderScope");const w=new WeakMap;function z(e){let r=w.get(e);return r===void 0&&(r=new Set(e),w.set(e,r)),r}s(z,"heldSet");function y(e,r){return e===null?!0:k(r)?z(e).has(r):$(e,r)}s(y,"holds");function S(e,r){return e.class==="person"?!0:y(e.resolved??[],r)&&y(e.nodeScopes,r)}s(S,"callerAllows");function pe(e,r,t){const n=`${r}:${t}`;if(!S(e,n))throw x(n)}s(pe,"requireGroup");function L(){return!!I("user").runtime?.issuer}s(L,"providerOperationsEnabled");const V=/^[A-Za-z0-9_-]{1,64}$/;function p(e,r){return new u("invalid_request",e,{param:r})}s(p,"invalid");function A(e){if(typeof e!="string"||!V.test(e)||e==="crtr")throw p("provider must be a provider id","provider");return e}s(A,"parseProvider");function Z(e){if(!e||typeof e!="object"||Array.isArray(e))throw new u("invalid_request","a JSON object is required");const r=e,t=A(r.provider),n=r.tool;if(typeof n!="string"||!n.trim())throw p("tool must be a non-empty string","tool");const i=r.arguments??{};if(!i||typeof i!="object"||Array.isArray(i))throw p("arguments must be an object","arguments");const a=r.version;if(a!==void 0&&(typeof a!="number"||!Number.isInteger(a)||a<1))throw p("version must be an integer >= 1","version");const o=r.approval;if(o!==void 0&&(typeof o!="string"||!o))throw p("approval must be a question id","approval");return{provider:t,tool:n.trim(),arguments:i,...a===void 0?{}:{version:a},...o===void 0?{}:{approval:o}}}s(Z,"parseCallInput");function B(e,r=new URLSearchParams){const t=s((o,f,c)=>{const l=r.get(o);if(l===null)return f;if(!/^\d+$/.test(l)||Number(l)>c||Number(l)<1)throw p(`invalid ${o}`,o);return Number(l)},"numeric"),n=r.has("after")?t("after",0,Number.MAX_SAFE_INTEGER):void 0,i=t("limit",100,1e3),a=Object.fromEntries(["operation","outcome","run_id","since","until"].filter(o=>r.has(o)).map(o=>[o,r.get(o)]));return{status:200,body:C(e.class==="person"?null:e.grantee,{after:n,limit:i,...a})}}s(B,"auditList");async function H(e,r,t){const n=performance.now(),i=new Set(j("user").filter(d=>d.enabled&&d.manifest.provider?.id===d.name).map(d=>d.name)),a=T(_()),o=[...new Set([...i,...a])].sort();if(r!==void 0&&!o.includes(r))throw new u("not_found",`provider "${r}" is not available`);const f=r===void 0?o:[r],c=e.class==="person"?_().flatMap(d=>d.status==="active"?d.resolved_scopes.map(({scope:v})=>v):[]):e.resolved??[],l=await Promise.all(f.map(async d=>{if(!c.some(m=>m.startsWith(`${d}:`)&&S(e,m)))return null;if(i.has(d)){const m=await N(e,d);return m.groups.length>0?m:null}const b=F(d);return{provider:d,display_name:d,status:b.status,...b.error?{error:b.error}:{},groups:[],commands:[]}})).then(d=>d.filter(v=>v!==null));if(g({level:"debug",event:"api.provider.tools",duration_ms:performance.now()-n,fields:{caller:e.class,provider:r??null,providers:l.length}}),r!==void 0){if(l.length===0)throw new u("not_found",`provider "${r}" is not available`);return{status:200,body:l[0]}}return{status:200,body:{providers:l}}}s(H,"providerTools");async function W(e,r,t){return G(e,r,t)}s(W,"providerCall");async function J(e,r,t,n){if(!L())throw new u("not_found","provider operations need a directory-connected runtime");if(e==="audit.list")return B(r,t.query);if(e==="provider.tools")return H(r,t.params.provider===void 0?void 0:A(t.params.provider),n);if(e==="provider.call"){let i;try{i=Z(t.body)}catch(a){const o=t.body&&typeof t.body=="object"&&!Array.isArray(t.body)?t.body:{};throw D({request_id:n.requestId,caller:r,provider:typeof o.provider=="string"?o.provider:"",tool:typeof o.tool=="string"?o.tool:"",outcome:`refused:${a instanceof u?a.code:"invalid_request"}`}),a}return W(r,i,n)}throw new u("not_found","unknown operation")}s(J,"handleProviderOperation");function X(e){const r=new AbortController,t=s(()=>{e.writableFinished||r.abort()},"abort");return e.on("close",t),{signal:r.signal,release:s(()=>e.off("close",t),"release")}}s(X,"abortOnClose");function K(e,r,t,n){const i=q(e,r,t,n??void 0);return i.status>=500&&!(e instanceof u)&&g({event:"api.provider.failed",level:"error",error:e instanceof Error?e:new Error(String(e)),fields:{request_id:r}}),i}s(K,"providerFailure");const me=[{method:"GET",pattern:"/v1/audit",name:"audit.list"},{method:"GET",pattern:"/v1/providers/tools",name:"provider.tools"},{method:"GET",pattern:"/v1/providers/:provider/tools",name:"provider.tools"},{method:"POST",pattern:"/v1/provider/call",name:"provider.call"}].map(({method:e,pattern:r,name:t})=>({method:e,pattern:r,handler:s(async n=>{const i=P(),{signal:a,release:o}=X(n.res);let f=null;try{return f=M(n.principal),{...await J(t,f,{params:n.params,body:n.body,query:new URL(n.req.url??"/","http://runtime.local").searchParams},{requestId:i,signal:a}),headers:{"Request-Id":i}}}catch(c){return K(c,i,!1,f?.run_id)}finally{o()}},"handler")}));export{fe as PROVIDER_OPERATIONS,X as abortOnClose,S as callerAllows,ce as callerFromApp,M as callerFromRequest,J as handleProviderOperation,Z as parseCallInput,A as parseProvider,K as providerFailure,L as providerOperationsEnabled,me as providerRoutes,pe as requireGroup};
|