@north-light/cli 0.3.442 → 0.3.445

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/dist/api/contract.d.ts +1 -1
  2. package/dist/api/contract.js +1 -1
  3. package/dist/builtin-memory/crouter-sdk/release-notes.md +6 -0
  4. package/dist/clients/attach/viewer.js +570 -570
  5. package/dist/core/audit.d.ts +23 -0
  6. package/dist/core/audit.js +3 -0
  7. package/dist/core/canvas/migrations.js +12 -12
  8. package/dist/core/directory/join.d.ts +2 -0
  9. package/dist/core/directory/join.js +1 -1
  10. package/dist/core/grants/mirror.d.ts +11 -0
  11. package/dist/core/grants/mirror.js +2 -2
  12. package/dist/core/grants/on-sync.js +1 -1
  13. package/dist/core/grants/remove.js +1 -1
  14. package/dist/core/grants/sync.js +1 -1
  15. package/dist/core/graph/reader.js +3 -3
  16. package/dist/core/peer/client.d.ts +57 -0
  17. package/dist/core/peer/client.js +1 -0
  18. package/dist/core/peer/directory.d.ts +65 -0
  19. package/dist/core/peer/directory.js +1 -0
  20. package/dist/core/peer/envelope.d.ts +40 -0
  21. package/dist/core/peer/envelope.js +1 -0
  22. package/dist/core/peer/events.d.ts +9 -0
  23. package/dist/core/peer/events.js +1 -0
  24. package/dist/core/peer/token.d.ts +15 -0
  25. package/dist/core/peer/token.js +2 -0
  26. package/dist/core/peer/version.d.ts +16 -0
  27. package/dist/core/peer/version.js +1 -0
  28. package/dist/core/providers/bundles.js +1 -1
  29. package/dist/daemon/api/app-listener.js +12 -12
  30. package/dist/daemon/api/handlers/bash.d.ts +1 -1
  31. package/dist/daemon/api/handlers/bash.js +1 -1
  32. package/dist/daemon/api/handlers/crons.js +1 -1
  33. package/dist/daemon/api/handlers/files.js +1 -1
  34. package/dist/daemon/api/handlers/human-requests.d.ts +1 -1
  35. package/dist/daemon/api/handlers/human-requests.js +1 -1
  36. package/dist/daemon/api/handlers/messages.js +1 -1
  37. package/dist/daemon/api/handlers/profiles.js +1 -1
  38. package/dist/daemon/api/handlers/providers.js +1 -1
  39. package/dist/daemon/api/operations.d.ts +1 -1
  40. package/dist/daemon/api/operations.js +1 -1
  41. package/dist/daemon/api/principal.d.ts +3 -1
  42. package/dist/daemon/api/principal.js +1 -1
  43. package/dist/daemon/api/router.d.ts +29 -1
  44. package/dist/daemon/api/router.js +1 -1
  45. package/dist/daemon/api/server.js +1 -1
  46. package/dist/daemon/crtrd.js +7 -7
  47. package/dist/daemon/peer/handlers.d.ts +2 -0
  48. package/dist/daemon/peer/handlers.js +1 -0
  49. package/dist/daemon/peer/ingress.d.ts +34 -0
  50. package/dist/daemon/peer/ingress.js +1 -0
  51. package/docs/sdk/release-notes.md +6 -0
  52. package/package.json +2 -2
  53. package/packages/crouter-identity/package.json +1 -1
  54. package/runtime.lock.json +12 -12
@@ -0,0 +1,23 @@
1
+ export interface PeerAuditRow {
2
+ request_id: string;
3
+ class: 'forwarded' | 'machine';
4
+ /** The app the call is made for (`app:<x>`), or the person (`user:<id>`) when no app is named, or the account owner for a machine-class call. */
5
+ grantee: string | null;
6
+ /** The account owner, `user:<id>`; null when the token never verified. */
7
+ sub: string | null;
8
+ /** The calling machine, `runtime:<id>`; `unauthenticated` when the token never verified (an unverified claim is never recorded). */
9
+ via: string;
10
+ /** The operation name (`messages.post.58`), or `METHOD /path` when no route matched. */
11
+ operation: string;
12
+ /** `ok`, or `status:<http status>` for any other answer. */
13
+ outcome: string;
14
+ /** The node the call was made for (the envelope's node), or the one it targets when the caller names none. */
15
+ node_id?: string | null;
16
+ run_id?: string | null;
17
+ /** What was touched: the request path. */
18
+ resource?: string | null;
19
+ }
20
+ /** Write one audit row. A failure to write is logged by the caller's wrapper and never changes the answer. */
21
+ export declare function writePeerAudit(row: PeerAuditRow): void;
22
+ /** The outcome string for an HTTP status. */
23
+ export declare const auditOutcome: (status: number) => string;
@@ -0,0 +1,3 @@
1
+ var n=Object.defineProperty;var i=(e,t)=>n(e,"name",{value:t,configurable:!0});import{prepared as u}from"./sqlite-statements.js";import{withCanvasWrite as r}from"./canvas/db.js";function s(e){r(t=>{u(t,`INSERT INTO audit_rows
2
+ (request_id, time, grantee, sub, class, operation, outcome, node_id, run_id, resource, via)
3
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(e.request_id,new Date().toISOString(),e.grantee,e.sub,e.class,e.operation,e.outcome,e.node_id??null,e.run_id??null,e.resource??null,e.via)})}i(s,"writePeerAudit");const c=i(e=>e>=200&&e<300?"ok":`status:${e}`,"auditOutcome");export{c as auditOutcome,s as writePeerAudit};
@@ -23,7 +23,7 @@ CREATE TABLE IF NOT EXISTS edges (
23
23
  CREATE INDEX IF NOT EXISTS idx_edges_to ON edges(type, to_id);
24
24
  CREATE INDEX IF NOT EXISTS idx_edges_from ON edges(type, from_id);
25
25
  CREATE INDEX IF NOT EXISTS idx_nodes_status ON nodes(status);
26
- `)}o(Ue,"baselineSchema");function b(e,t){return new Set(e.prepare(`PRAGMA table_info(${t})`).all().map(n=>n.name))}o(b,"tableColumns");function $(e,t,n,E){t.has(n)||(e.exec(E),t.add(n))}o($,"addColumnIfMissing");function m(e){return b(e,"nodes")}o(m,"nodeColumns");const N=$;function Ce(e){const t=m(e);N(e,t,"intent","ALTER TABLE nodes ADD COLUMN intent TEXT;"),N(e,t,"pi_pid","ALTER TABLE nodes ADD COLUMN pi_pid INTEGER;"),N(e,t,"window","ALTER TABLE nodes ADD COLUMN window TEXT;"),N(e,t,"tmux_session","ALTER TABLE nodes ADD COLUMN tmux_session TEXT;")}o(Ce,"addRuntimeColumns");function Se(e){const t=h();if(!U(t))return;const n=e.prepare('UPDATE nodes SET intent = ?, pi_pid = ?, "window" = ?, tmux_session = ? WHERE node_id = ?');for(const E of k(t)){const a=x(E);if(!U(a))continue;let i;try{i=JSON.parse(M(a,"utf8"))}catch{continue}n.run(i.intent??null,i.pi_pid??null,i.window??null,i.tmux_session??null,E)}}o(Se,"backfillRuntime");function ge(e){B(e,3,()=>{if(!(e.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'edges'").get()!==void 0)){e.exec(`
26
+ `)}o(Ue,"baselineSchema");function b(e,t){return new Set(e.prepare(`PRAGMA table_info(${t})`).all().map(n=>n.name))}o(b,"tableColumns");function $(e,t,n,E){t.has(n)||(e.exec(E),t.add(n))}o($,"addColumnIfMissing");function m(e){return b(e,"nodes")}o(m,"nodeColumns");const N=$;function Ce(e){const t=m(e);N(e,t,"intent","ALTER TABLE nodes ADD COLUMN intent TEXT;"),N(e,t,"pi_pid","ALTER TABLE nodes ADD COLUMN pi_pid INTEGER;"),N(e,t,"window","ALTER TABLE nodes ADD COLUMN window TEXT;"),N(e,t,"tmux_session","ALTER TABLE nodes ADD COLUMN tmux_session TEXT;")}o(Ce,"addRuntimeColumns");function Se(e){const t=h();if(!U(t))return;const n=e.prepare('UPDATE nodes SET intent = ?, pi_pid = ?, "window" = ?, tmux_session = ? WHERE node_id = ?');for(const E of k(t)){const a=x(E);if(!U(a))continue;let i;try{i=JSON.parse(M(a,"utf8"))}catch{continue}n.run(i.intent??null,i.pi_pid??null,i.window??null,i.tmux_session??null,E)}}o(Se,"backfillRuntime");function De(e){B(e,3,()=>{if(!(e.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'edges'").get()!==void 0)){e.exec(`
27
27
  CREATE TABLE edges (
28
28
  type TEXT NOT NULL,
29
29
  from_id TEXT NOT NULL,
@@ -57,7 +57,7 @@ DROP TABLE edges;
57
57
  ALTER TABLE edges_new RENAME TO edges;
58
58
  CREATE INDEX IF NOT EXISTS idx_edges_to ON edges(type, to_id);
59
59
  CREATE INDEX IF NOT EXISTS idx_edges_from ON edges(type, from_id);
60
- `);const E=e.prepare("SELECT COUNT(*) AS n FROM edges").get().n;if(E!==n)throw new Error(`edges FK rebuild lost rows: expected ${n} non-orphan edge(s), got ${E}`);const a=e.prepare("PRAGMA foreign_key_check").all();if(a.length>0)throw new Error(`edges FK rebuild left ${a.length} foreign-key violation(s)`)})}o(ge,"edgesForeignKeyCascade");function De(e){const t=m(e);N(e,t,"pane","ALTER TABLE nodes ADD COLUMN pane TEXT;")}o(De,"addPaneColumn");function ve(e){e.exec(`
60
+ `);const E=e.prepare("SELECT COUNT(*) AS n FROM edges").get().n;if(E!==n)throw new Error(`edges FK rebuild lost rows: expected ${n} non-orphan edge(s), got ${E}`);const a=e.prepare("PRAGMA foreign_key_check").all();if(a.length>0)throw new Error(`edges FK rebuild left ${a.length} foreign-key violation(s)`)})}o(De,"edgesForeignKeyCascade");function ge(e){const t=m(e);N(e,t,"pane","ALTER TABLE nodes ADD COLUMN pane TEXT;")}o(ge,"addPaneColumn");function ve(e){e.exec(`
61
61
  CREATE TABLE IF NOT EXISTS focuses (
62
62
  focus_id TEXT PRIMARY KEY, -- stable internal id for the viewport
63
63
  pane TEXT, -- the durable %pane_id realizing the focus
@@ -213,10 +213,10 @@ CREATE TABLE IF NOT EXISTS cron_runs ( -- bounded ring, last 20 p
213
213
  FOREIGN KEY (cron_id) REFERENCES crons(cron_id) ON DELETE CASCADE
214
214
  );
215
215
  CREATE INDEX IF NOT EXISTS idx_cron_runs_cron ON cron_runs(cron_id, started);
216
- `)}o(We,"addCronsTables");function D(e){return`'${e.replace(/'/g,`'"'"'`)}'`}o(D,"shellSingleQuote");function Pe(e,t){let n=0;for(;;){const E=`${t}_${n}`;if(!e.split(`
216
+ `)}o(We,"addCronsTables");function g(e){return`'${e.replace(/'/g,`'"'"'`)}'`}o(g,"shellSingleQuote");function Pe(e,t){let n=0;for(;;){const E=`${t}_${n}`;if(!e.split(`
217
217
  `).includes(E))return E;n++}}o(Pe,"uniqueHeredocDelimiter");function Q(e,t,n){const E=Pe(t,n);return`${e} <<'${E}'
218
218
  ${t}
219
- ${E}`}o(Q,"heredocCommand");function Ye(e){if(typeof e!="string")return{};try{const t=JSON.parse(e);return typeof t!="object"||t===null||Array.isArray(t)?{}:t}catch{return{}}}o(Ye,"migrationPayload");function X(e,t,n){const E=e[t];if(typeof E!="string"||E.trim()==="")throw new Error(`trigger ${n} payload requires non-empty ${t}`);return E}o(X,"requiredString");function H(e,t){const n=e[t];return typeof n=="string"&&n.trim()!==""?n:void 0}o(H,"optionalString");function v(e,t){const n=e[t];return typeof n=="string"&&n.trim()!==""?n:void 0}o(v,"optionalColumn");function z(e,t,n,E){return Q(`crtr node message send --to ${D(e)} --tier ${D(t)}`,n,`CRTR_MIGRATED_${E.replace(/[^A-Za-z0-9]/g,"_")}`)}o(z,"migratedMessageCommand");function Ke(e,t){return`printf '%s\\n' "$CRTR_GATE_STDOUT" | crtr node message send --to ${D(e)} --tier ${D(t)}`}o(Ke,"migratedGateStdoutMessageCommand");function qe(e,t){const n=[`crtr node new --kind ${D(X(e,"kind",t))}`],E=[["mode","mode"],["cwd","cwd"],["name","name"],["parent","parent"],["worktreeBase","worktree"],["forkFrom","fork-from"],["model","model"],["profile","profile"],["situationalContext","situational-context"]];for(const[i,T]of E){const c=H(e,i);c!==void 0&&n.push(`--${T} ${D(c)}`)}e.root===!0&&n.push("--root");const a=X(e,"prompt",t);return Q(n.join(" "),a,`CRTR_MIGRATED_${t.replace(/[^A-Za-z0-9]/g,"_")}`)}o(qe,"migratedSpawnCommand");const V="migration.v22.skipped";function $e(e){const t=e.prepare("SELECT cwd FROM nodes WHERE parent IS NULL ORDER BY created LIMIT 1").get();return(t!==void 0?v(t,"cwd"):void 0)??process.cwd()}o($e,"ambientMigrationCwd");function Je(e){let t;try{t=e.prepare("SELECT eval_pid FROM triggers WHERE eval_state = 'running' AND eval_pid IS NOT NULL").all()}catch{return}for(const n of t){const E=n.eval_pid;if(!(typeof E!="number"||!Number.isInteger(E)||E<=1))try{process.kill(-E,"SIGTERM")}catch{}}}o(Je,"reapRunningPredicateEvaluations");function Qe(e){const t=[],n=e.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'triggers'").get()!==void 0;n&&Je(e);let E=[];if(n)try{E=e.prepare("SELECT * FROM triggers ORDER BY created, trigger_id").all()}catch(T){t.push({trigger_id:"<unreadable table>",reason:`triggers table could not be read: ${T instanceof Error?T.message:String(T)}`})}const a=$e(e),i=e.prepare(`
219
+ ${E}`}o(Q,"heredocCommand");function Ye(e){if(typeof e!="string")return{};try{const t=JSON.parse(e);return typeof t!="object"||t===null||Array.isArray(t)?{}:t}catch{return{}}}o(Ye,"migrationPayload");function X(e,t,n){const E=e[t];if(typeof E!="string"||E.trim()==="")throw new Error(`trigger ${n} payload requires non-empty ${t}`);return E}o(X,"requiredString");function H(e,t){const n=e[t];return typeof n=="string"&&n.trim()!==""?n:void 0}o(H,"optionalString");function v(e,t){const n=e[t];return typeof n=="string"&&n.trim()!==""?n:void 0}o(v,"optionalColumn");function z(e,t,n,E){return Q(`crtr node message send --to ${g(e)} --tier ${g(t)}`,n,`CRTR_MIGRATED_${E.replace(/[^A-Za-z0-9]/g,"_")}`)}o(z,"migratedMessageCommand");function Ke(e,t){return`printf '%s\\n' "$CRTR_GATE_STDOUT" | crtr node message send --to ${g(e)} --tier ${g(t)}`}o(Ke,"migratedGateStdoutMessageCommand");function qe(e,t){const n=[`crtr node new --kind ${g(X(e,"kind",t))}`],E=[["mode","mode"],["cwd","cwd"],["name","name"],["parent","parent"],["worktreeBase","worktree"],["forkFrom","fork-from"],["model","model"],["profile","profile"],["situationalContext","situational-context"]];for(const[i,T]of E){const c=H(e,i);c!==void 0&&n.push(`--${T} ${g(c)}`)}e.root===!0&&n.push("--root");const a=X(e,"prompt",t);return Q(n.join(" "),a,`CRTR_MIGRATED_${t.replace(/[^A-Za-z0-9]/g,"_")}`)}o(qe,"migratedSpawnCommand");const V="migration.v22.skipped";function $e(e){const t=e.prepare("SELECT cwd FROM nodes WHERE parent IS NULL ORDER BY created LIMIT 1").get();return(t!==void 0?v(t,"cwd"):void 0)??process.cwd()}o($e,"ambientMigrationCwd");function Je(e){let t;try{t=e.prepare("SELECT eval_pid FROM triggers WHERE eval_state = 'running' AND eval_pid IS NOT NULL").all()}catch{return}for(const n of t){const E=n.eval_pid;if(!(typeof E!="number"||!Number.isInteger(E)||E<=1))try{process.kill(-E,"SIGTERM")}catch{}}}o(Je,"reapRunningPredicateEvaluations");function Qe(e){const t=[],n=e.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'triggers'").get()!==void 0;n&&Je(e);let E=[];if(n)try{E=e.prepare("SELECT * FROM triggers ORDER BY created, trigger_id").all()}catch(T){t.push({trigger_id:"<unreadable table>",reason:`triggers table could not be read: ${T instanceof Error?T.message:String(T)}`})}const a=$e(e),i=e.prepare(`
220
220
  INSERT OR REPLACE INTO crons (
221
221
  cron_id, name, created_by, command, fire_at, recur, tz, expires_at,
222
222
  anchor_node, cancel_on_wake, cwd, env_json, profile, scope,
@@ -228,7 +228,7 @@ DROP INDEX IF EXISTS idx_triggers_owner;
228
228
  DROP INDEX IF EXISTS idx_triggers_eval;
229
229
  DROP INDEX IF EXISTS idx_triggers_deadline;
230
230
  DROP TABLE IF EXISTS triggers;
231
- `)}o(Qe,"migrateTriggersToCrons");function ze(e,t,n,E,a){const i=X(n,"owner_id",E),T=e.prepare("SELECT cwd, profile_id FROM nodes WHERE node_id = ?").get(i),c=(T===void 0?void 0:v(T,"cwd"))??a,L=T===void 0?null:v(T,"profile_id")??null,l=v(n,"node_id")??null,r=X(n,"action",E),_=X(n,"action_mode",E),d=X(n,"trigger_type",E),u=Ye(n.payload),p=v(n,"created")??new Date().toISOString(),O=_==="deadline_message"?"urgent":H(u,"tier")??"normal",R=H(u,"label");let s,A,I=!1;if(r==="msg"&&(_==="inbox_message"||_==="deadline_message")){if(l===null)throw new Error("has no message target");const f=H(u,"body");f!==void 0?s=z(l,O,f,E):d==="when"?(I=!0,s=Ke(l,O)):s=z(l,O,R??"Scheduled message (no body was recorded when this schedule was armed).",E),A=R??(_==="deadline_message"?"deadline":"scheduled message")}else if(r==="msg"&&_==="fresh_revive"){if(l===null)throw new Error("has no revive target");s=`crtr node lifecycle revive ${D(l)} --fresh`,A=R??"scheduled fresh revive"}else if(r==="new"&&_==="node_birth")s=qe(u,E),A=H(u,"name")??`scheduled ${X(u,"kind",E)} node`;else throw new Error(`has unsupported action ${r}/${_}`);let g,w,G=null;if(d==="when"){g=X(n,"next_eval_at",E),w=X(n,"eval_every",E),G=v(n,"timeout_at")??null;const f=X(n,"when_script",E);s=`${I?`if ! CRTR_GATE_STDOUT="$(
231
+ `)}o(Qe,"migrateTriggersToCrons");function ze(e,t,n,E,a){const i=X(n,"owner_id",E),T=e.prepare("SELECT cwd, profile_id FROM nodes WHERE node_id = ?").get(i),c=(T===void 0?void 0:v(T,"cwd"))??a,L=T===void 0?null:v(T,"profile_id")??null,l=v(n,"node_id")??null,r=X(n,"action",E),_=X(n,"action_mode",E),d=X(n,"trigger_type",E),u=Ye(n.payload),p=v(n,"created")??new Date().toISOString(),O=_==="deadline_message"?"urgent":H(u,"tier")??"normal",R=H(u,"label");let s,A,I=!1;if(r==="msg"&&(_==="inbox_message"||_==="deadline_message")){if(l===null)throw new Error("has no message target");const f=H(u,"body");f!==void 0?s=z(l,O,f,E):d==="when"?(I=!0,s=Ke(l,O)):s=z(l,O,R??"Scheduled message (no body was recorded when this schedule was armed).",E),A=R??(_==="deadline_message"?"deadline":"scheduled message")}else if(r==="msg"&&_==="fresh_revive"){if(l===null)throw new Error("has no revive target");s=`crtr node lifecycle revive ${g(l)} --fresh`,A=R??"scheduled fresh revive"}else if(r==="new"&&_==="node_birth")s=qe(u,E),A=H(u,"name")??`scheduled ${X(u,"kind",E)} node`;else throw new Error(`has unsupported action ${r}/${_}`);let D,w,G=null;if(d==="when"){D=X(n,"next_eval_at",E),w=X(n,"eval_every",E),G=v(n,"timeout_at")??null;const f=X(n,"when_script",E);s=`${I?`if ! CRTR_GATE_STDOUT="$(
232
232
  ${f}
233
233
  )"; then`:`if ! (
234
234
  ${f}
@@ -237,7 +237,7 @@ ${f}
237
237
  fi
238
238
  {
239
239
  ${s}
240
- } && crtr cron cancel "$CRTR_CRON_ID"`}else if(d==="at"||d==="every"||d==="until")g=X(n,"fire_at",E),w=d==="every"?X(n,"recur",E):null;else throw new Error(`has unknown timing type ${d}`);if(_==="deadline_message"&&l!==null){const f=e.prepare("SELECT cron_id FROM crons WHERE anchor_node = ? AND cancel_on_wake = 1 AND cron_id <> ?").get(l,E);if(f!==void 0)throw new Error(`node ${l} already has a migrated deadline (${String(f.cron_id)}); only one is allowed`)}t.run(E,A,i,s,g,w,G,l,_==="deadline_message"?1:0,c,L,300,O,p,p)}o(ze,"convertTriggerRow");function Ve(e){const t=b(e,"crons");t.size===0||t.has("run_pid_identity")||e.exec("ALTER TABLE crons ADD COLUMN run_pid_identity TEXT;")}o(Ve,"addCronRunPidIdentityColumn");function Ze(e){const t=b(e,"crons");t.size!==0&&(t.has("run_out_of_band")||e.exec("ALTER TABLE crons ADD COLUMN run_out_of_band INTEGER NOT NULL DEFAULT 0;"),t.has("queued")&&e.exec("ALTER TABLE crons DROP COLUMN queued;"))}o(Ze,"addCronOutOfBandColumn");function et(e){e.exec(`
240
+ } && crtr cron cancel "$CRTR_CRON_ID"`}else if(d==="at"||d==="every"||d==="until")D=X(n,"fire_at",E),w=d==="every"?X(n,"recur",E):null;else throw new Error(`has unknown timing type ${d}`);if(_==="deadline_message"&&l!==null){const f=e.prepare("SELECT cron_id FROM crons WHERE anchor_node = ? AND cancel_on_wake = 1 AND cron_id <> ?").get(l,E);if(f!==void 0)throw new Error(`node ${l} already has a migrated deadline (${String(f.cron_id)}); only one is allowed`)}t.run(E,A,i,s,D,w,G,l,_==="deadline_message"?1:0,c,L,300,O,p,p)}o(ze,"convertTriggerRow");function Ve(e){const t=b(e,"crons");t.size===0||t.has("run_pid_identity")||e.exec("ALTER TABLE crons ADD COLUMN run_pid_identity TEXT;")}o(Ve,"addCronRunPidIdentityColumn");function Ze(e){const t=b(e,"crons");t.size!==0&&(t.has("run_out_of_band")||e.exec("ALTER TABLE crons ADD COLUMN run_out_of_band INTEGER NOT NULL DEFAULT 0;"),t.has("queued")&&e.exec("ALTER TABLE crons DROP COLUMN queued;"))}o(Ze,"addCronOutOfBandColumn");function et(e){e.exec(`
241
241
  CREATE TABLE IF NOT EXISTS warm_pool (
242
242
  node_id TEXT PRIMARY KEY REFERENCES nodes(node_id) ON DELETE CASCADE,
243
243
  recipe_key TEXT NOT NULL, -- frozen launch tuple this spare was minted for
@@ -659,7 +659,7 @@ CREATE INDEX idx_structured_requests_effects ON structured_requests(effects_done
659
659
  WHERE NOT EXISTS (SELECT 1 FROM identities i WHERE i.external_id = n.grantee AND i.scheme IN ('app', 'local-user'))`).all();for(const{grantee:i}of a){let T;if(i==="user:local")T={type:"person",scheme:"local-user",handle:"user"};else if(/^app:[a-z0-9][a-z0-9._-]*$/.test(i))T={type:"app",scheme:"app",handle:i.slice(4)};else throw new Error(`canvas objects migration: node grantee ${JSON.stringify(i)} is neither app:<name> nor user:local`);const{rowid:c}=n.get(`${Date.now().toString(36)}-${P(4).toString("hex")}`,T.type,T.handle,t);E.run(c,T.scheme,i)}e.exec(`INSERT INTO objects (id, type, kind, owner, privacy, name, preview, created_at)
660
660
  SELECT n.node_id, 'node', n.kind, i.object, 'public', n.name, '', n.created
661
661
  FROM nodes n JOIN identities i ON i.external_id = n.grantee AND i.scheme IN ('app', 'local-user')
662
- WHERE NOT EXISTS (SELECT 1 FROM objects o WHERE o.id = n.node_id)`)}o(ie,"copyNodesIntoObjects");function gt(e){if(Ee(e),!S(e,"nodes","name"))return;ie(e);const t=e.prepare("SELECT COUNT(*) AS n FROM nodes WHERE node_id NOT IN (SELECT id FROM objects)").get();if(t.n>0)throw new Error(`canvasObjectsCut: ${t.n} node(s) have no objects row`);e.exec(`
662
+ WHERE NOT EXISTS (SELECT 1 FROM objects o WHERE o.id = n.node_id)`)}o(ie,"copyNodesIntoObjects");function Dt(e){if(Ee(e),!S(e,"nodes","name"))return;ie(e);const t=e.prepare("SELECT COUNT(*) AS n FROM nodes WHERE node_id NOT IN (SELECT id FROM objects)").get();if(t.n>0)throw new Error(`canvasObjectsCut: ${t.n} node(s) have no objects row`);e.exec(`
663
663
  ALTER TABLE edges RENAME TO legacy_edges;
664
664
  CREATE TABLE edges (
665
665
  type TEXT NOT NULL CHECK (type IN ('acted-on','links-to')),
@@ -707,7 +707,7 @@ CREATE INDEX idx_structured_requests_effects ON structured_requests(effects_done
707
707
  LEFT JOIN sessions s ON s.node_id = n.node_id AND s.unbound_at IS NULL
708
708
  LEFT JOIN node_faults f ON f.node_id = n.node_id AND f.active = 1;
709
709
  ANALYZE;
710
- `)}o(gt,"canvasObjectsCut");function Dt(e){e.exec(`CREATE TABLE IF NOT EXISTS uploads (
710
+ `)}o(Dt,"canvasObjectsCut");function gt(e){e.exec(`CREATE TABLE IF NOT EXISTS uploads (
711
711
  upload_id TEXT PRIMARY KEY, grantee TEXT NOT NULL, name TEXT NOT NULL, media_type TEXT NOT NULL,
712
712
  size INTEGER NOT NULL, created_at TEXT NOT NULL, delete_pending_since TEXT
713
713
  );
@@ -723,7 +723,7 @@ CREATE INDEX idx_structured_requests_effects ON structured_requests(effects_done
723
723
  url TEXT NOT NULL, created_by TEXT NOT NULL, created_at TEXT NOT NULL, expires_at TEXT,
724
724
  status TEXT NOT NULL CHECK (status IN ('on', 'off')), object_deleted_at TEXT
725
725
  );
726
- CREATE INDEX IF NOT EXISTS idx_shares_created_by ON shares(created_by);`)}o(Dt,"addFileTransfer");function vt(e){e.exec(`CREATE TABLE IF NOT EXISTS daemon_actions (
726
+ CREATE INDEX IF NOT EXISTS idx_shares_created_by ON shares(created_by);`)}o(gt,"addFileTransfer");function vt(e){e.exec(`CREATE TABLE IF NOT EXISTS daemon_actions (
727
727
  action_id TEXT PRIMARY KEY,
728
728
  adapter_json TEXT NOT NULL,
729
729
  body_json TEXT,
@@ -881,7 +881,7 @@ CREATE INDEX idx_structured_requests_effects ON structured_requests(effects_done
881
881
  VALUES (?, ?, 1, NULL, ?) RETURNING rowid`),l=e.prepare(`INSERT INTO watch_subscriptions
882
882
  (watch, pattern, filter_json, config_json, started_by, ends_on, created_at)
883
883
  VALUES (?, 'outcome', NULL, '{"1":"after-turn"}', 'spawn', NULL, ?)`),r=e.prepare("UPDATE deliveries SET destination_id = ?, idempotency_key = NULL WHERE delivery_id = ?"),_=e.prepare("SELECT body_json FROM messages WHERE message_id = ?"),d=e.prepare(`UPDATE messages SET body_json = ?, event = 'outcome', object_id = ?, level = 1
884
- WHERE message_id = ?`);for(const s of i){if(s.owner===null)throw new Error(`v67 outcome node ${s.node_id} has no owner`);const A=JSON.parse(s.adapter_json);if(A.adapter!=="subprocess"||!Array.isArray(A.argv)||typeof A.cwd!="string"||typeof A.config_origin!="string")throw new Error(`v67 invalid outcome adapter on ${s.delivery_id}`);const I=P(16).toString("hex"),g=new Date(s.created_at).toISOString(),w=T.get(I,s.owner,s.privacy,`outcome/${s.node_id}`,g);c.run(w.rowid,JSON.stringify({argv:A.argv,cwd:A.cwd,config_origin:A.config_origin,name:s.destination_id}),JSON.stringify(A.payload??null));const G=L.get(w.rowid,e.prepare("SELECT rowid FROM objects WHERE id = ?").get(s.node_id).rowid,g);if(l.run(G.rowid,g),s.message_id===null){if(s.state!=="armed")throw new Error(`v67 delivery ${s.delivery_id} has no message but is ${s.state}`);continue}const f=_.get(s.message_id);if(!f||f.body_json===null)throw new Error(`v67 delivery ${s.delivery_id} has no frozen outcome document`);if(e.prepare("SELECT COUNT(*) AS n FROM deliveries WHERE message_id = ?").get(s.message_id).n!==1)throw new Error(`v67 outcome message ${s.message_id} is shared`);const y=JSON.parse(f.body_json);if(y===null||Array.isArray(y)||typeof y!="object")throw new Error(`v67 invalid outcome body on ${s.delivery_id}`);delete y.action,delete y.delivery,d.run(JSON.stringify(y),s.node_id,s.message_id),r.run(I,s.delivery_id)}const u=e.prepare("SELECT COUNT(*) AS n FROM deliveries WHERE state = 'armed'").get();if(u.n!==i.filter(s=>s.state==="armed").length)throw new Error(`v67 found ${u.n} armed deliveries but only ${i.length} mapped outcome rows`);const p=e.prepare("SELECT COUNT(*) AS n FROM deliveries").get().n-u.n,O=e.prepare("SELECT name, sql FROM sqlite_master WHERE type = 'view' AND sql LIKE '%deliveries%'").all();for(const s of O)e.exec(`DROP VIEW "${s.name.replaceAll('"','""')}"`);e.exec(`
884
+ WHERE message_id = ?`);for(const s of i){if(s.owner===null)throw new Error(`v67 outcome node ${s.node_id} has no owner`);const A=JSON.parse(s.adapter_json);if(A.adapter!=="subprocess"||!Array.isArray(A.argv)||typeof A.cwd!="string"||typeof A.config_origin!="string")throw new Error(`v67 invalid outcome adapter on ${s.delivery_id}`);const I=P(16).toString("hex"),D=new Date(s.created_at).toISOString(),w=T.get(I,s.owner,s.privacy,`outcome/${s.node_id}`,D);c.run(w.rowid,JSON.stringify({argv:A.argv,cwd:A.cwd,config_origin:A.config_origin,name:s.destination_id}),JSON.stringify(A.payload??null));const G=L.get(w.rowid,e.prepare("SELECT rowid FROM objects WHERE id = ?").get(s.node_id).rowid,D);if(l.run(G.rowid,D),s.message_id===null){if(s.state!=="armed")throw new Error(`v67 delivery ${s.delivery_id} has no message but is ${s.state}`);continue}const f=_.get(s.message_id);if(!f||f.body_json===null)throw new Error(`v67 delivery ${s.delivery_id} has no frozen outcome document`);if(e.prepare("SELECT COUNT(*) AS n FROM deliveries WHERE message_id = ?").get(s.message_id).n!==1)throw new Error(`v67 outcome message ${s.message_id} is shared`);const y=JSON.parse(f.body_json);if(y===null||Array.isArray(y)||typeof y!="object")throw new Error(`v67 invalid outcome body on ${s.delivery_id}`);delete y.action,delete y.delivery,d.run(JSON.stringify(y),s.node_id,s.message_id),r.run(I,s.delivery_id)}const u=e.prepare("SELECT COUNT(*) AS n FROM deliveries WHERE state = 'armed'").get();if(u.n!==i.filter(s=>s.state==="armed").length)throw new Error(`v67 found ${u.n} armed deliveries but only ${i.length} mapped outcome rows`);const p=e.prepare("SELECT COUNT(*) AS n FROM deliveries").get().n-u.n,O=e.prepare("SELECT name, sql FROM sqlite_master WHERE type = 'view' AND sql LIKE '%deliveries%'").all();for(const s of O)e.exec(`DROP VIEW "${s.name.replaceAll('"','""')}"`);e.exec(`
885
885
  CREATE TABLE deliveries_new (
886
886
  delivery_id TEXT PRIMARY KEY,
887
887
  message_id TEXT NOT NULL REFERENCES messages(message_id),
@@ -924,7 +924,7 @@ CREATE INDEX idx_structured_requests_effects ON structured_requests(effects_done
924
924
  state TEXT NOT NULL DEFAULT 'reserved' CHECK (state IN ('reserved', 'warm_claimed', 'complete'))
925
925
  );
926
926
  CREATE INDEX IF NOT EXISTS idx_create_requests_expires ON create_requests(expires_at);
927
- `)}o(Ht,"addCreateRequests");function Gt(e){for(const[r,_]of[["sub","TEXT"],["grant_id","TEXT"],["scopes_json","TEXT"],["origin_run_id","TEXT"],["run_execution_id","TEXT"]])S(e,"crons",r)||e.exec(`ALTER TABLE crons ADD COLUMN ${r} ${_}`);const t=[],n=o(r=>e.prepare("SELECT id, sub, resolved_scopes_json FROM grants WHERE grantee = ? ORDER BY CASE status WHEN 'active' THEN 0 ELSE 1 END, rev DESC LIMIT 1").get(r),"grantRow"),E=o(r=>e.prepare("SELECT grantee, sub, resolved_scopes_json FROM grants WHERE id = ?").get(r),"grantById"),a=o(r=>{try{const _=JSON.parse(r);return Array.isArray(_)?_.flatMap(d=>d&&d.mode==="always"&&typeof d.scope=="string"?[d.scope]:[]):[]}catch{return[]}},"snapshot"),i=o((r,_)=>{if(_===null)return null;const d=e.prepare("SELECT grantee, sub, grant_id, scopes_json, run_id FROM node_rows WHERE node_id = ?").get(_);if(d===void 0||d.grantee!==r)return null;const u=r==="app:terminal";let p=null;if(d.scopes_json!==null){let A;try{A=JSON.parse(d.scopes_json)}catch{return{pause:"bad_scopes"}}if(!Array.isArray(A)||!A.every(I=>typeof I=="string"))return{pause:"bad_scopes"};p=A}const O=u?n(r):void 0;let R=d.grant_id??O?.id??null;R===null&&!u&&d.sub!==null&&(R=e.prepare("SELECT id FROM grants WHERE grantee = ? AND sub = ? ORDER BY CASE status WHEN 'active' THEN 0 ELSE 1 END, rev DESC LIMIT 1").get(r,d.sub)?.id??null);const s=R===null?void 0:E(R);return R!==null&&(s===void 0||s.grantee!==r||d.sub!==null&&s.sub!==d.sub)?{pause:"no_grant"}:R===null&&!u?{pause:"no_grant"}:(p===null&&!u&&(p=a(s.resolved_scopes_json)),{principal:{sub:d.sub??s?.sub??O?.sub??"user:local",grant_id:R,scopes:p,origin_run_id:d.run_id}})},"fromCreator"),T=o(r=>{const _=n(r);return r==="app:terminal"?{sub:_?.sub??"user:local",grant_id:_?.id??null,scopes:null,origin_run_id:null}:_===void 0?null:{sub:_.sub,grant_id:_.id,scopes:a(_.resolved_scopes_json),origin_run_id:null}},"fromGrantee"),c=e.prepare("SELECT cron_id, grantee, created_by, isolated, cwd FROM crons WHERE sub IS NULL").all(),L=e.prepare("UPDATE crons SET sub = ?, grant_id = ?, scopes_json = ?, origin_run_id = ? WHERE cron_id = ?"),l=new Map;for(const r of c){const _=i(r.grantee,r.created_by),d=_===null?T(r.grantee):"principal"in _?_.principal:null;if(d===null){const u=_!==null&&"pause"in _?_.pause:"no_grant";L.run("user:local",null,null,null,r.cron_id),e.prepare("UPDATE crons SET state = 'paused' WHERE cron_id = ?").run(r.cron_id),t.push({event:`cron.migrated.${u}`,fields:{cron_id:r.cron_id,grantee:r.grantee}})}else l.set(r.cron_id,d),L.run(d.sub,d.grant_id,d.scopes===null?null:JSON.stringify(d.scopes),d.origin_run_id,r.cron_id);if(r.isolated!==0){const u=j(Re(r.grantee).root,"crons",r.cron_id);e.prepare("UPDATE crons SET cwd = ? WHERE cron_id = ?").run(u,r.cron_id),t.push({event:"cron.migrated.isolated_cwd",fields:{cron_id:r.cron_id,old_cwd:r.cwd,cwd:u}})}}if(C(e,"daemon_actions")){const r=e.prepare("SELECT action_id, adapter_json FROM daemon_actions WHERE state IN ('pending', 'handed_off') AND json_extract(adapter_json, '$.adapter') = 'cron_spawn' AND json_extract(adapter_json, '$.principal') IS NULL").all();for(const _ of r){const d=JSON.parse(_.adapter_json),u=typeof d.cronId=="string"?d.cronId:null,p=u===null?void 0:e.prepare("SELECT grantee, sub, grant_id, scopes_json, origin_run_id, isolated, created_by FROM crons WHERE cron_id = ?").get(u);let O=null;if(p!==void 0&&!(p.grant_id===null&&p.grantee!=="app:terminal"))O={cron_id:u,grantee:p.grantee,sub:p.sub,grant_id:p.grant_id,scopes:p.scopes_json===null?null:JSON.parse(p.scopes_json),origin_run_id:p.origin_run_id,isolated:p.isolated!==0,creator:p.created_by};else if(p===void 0){const R=typeof d.parent=="string"?d.parent:null,s=R===null?void 0:e.prepare("SELECT grantee FROM node_rows WHERE node_id = ?").get(R),A=s===void 0?null:i(s.grantee,R),I=A===null||!("principal"in A)?null:A.principal;s!==void 0&&I!==null&&(O={cron_id:u,grantee:s.grantee,sub:I.sub,grant_id:I.grant_id,scopes:I.scopes,origin_run_id:I.origin_run_id,isolated:d.isolated===!0,creator:R})}if(O!==null)e.prepare("UPDATE daemon_actions SET adapter_json = ? WHERE action_id = ?").run(JSON.stringify({...d,principal:O}),_.action_id);else{const R=Date.now();e.prepare("UPDATE daemon_actions SET state = 'permanent_failed', failed_at = ?, updated_at = ?, claimed_at = NULL, claim_owner = NULL, next_attempt_at = NULL, last_failure_json = ? WHERE action_id = ?").run(R,R,JSON.stringify({kind:"grant_refused",message:"the cron has no derivable principal to authorize this birth"}),_.action_id),t.push({event:"cron.birth.refused",fields:{cron_id:u,action_id:_.action_id,reason:"grant_missing"}})}}}for(const{event:r,fields:_}of t)Ae({level:"warn",event:r,fields:_})}o(Gt,"addCronPrincipal");const F=[Ue,Ce,Se,ge,De,ve,we,he,ye,xe,Fe,Me,ke,J,je,J,He,Ge,be,Be,We,Qe,Ve,Ze,et,tt,nt,ot,Et,it,st,rt,at,Tt,ct,dt,_t,Nt,Lt,e=>{de(e),ce(e),Ne(e),Le(e)},ut,pt,lt,Rt,At,Ot,It,Ut,ft,Xt,ee,te,ne,oe,Ct,St,Ee,gt,me,Dt,vt,wt,yt,kt,Mt,Ft,jt,xt,e=>e.exec(`
927
+ `)}o(Ht,"addCreateRequests");function Gt(e){for(const[r,_]of[["sub","TEXT"],["grant_id","TEXT"],["scopes_json","TEXT"],["origin_run_id","TEXT"],["run_execution_id","TEXT"]])S(e,"crons",r)||e.exec(`ALTER TABLE crons ADD COLUMN ${r} ${_}`);const t=[],n=o(r=>e.prepare("SELECT id, sub, resolved_scopes_json FROM grants WHERE grantee = ? ORDER BY CASE status WHEN 'active' THEN 0 ELSE 1 END, rev DESC LIMIT 1").get(r),"grantRow"),E=o(r=>e.prepare("SELECT grantee, sub, resolved_scopes_json FROM grants WHERE id = ?").get(r),"grantById"),a=o(r=>{try{const _=JSON.parse(r);return Array.isArray(_)?_.flatMap(d=>d&&d.mode==="always"&&typeof d.scope=="string"?[d.scope]:[]):[]}catch{return[]}},"snapshot"),i=o((r,_)=>{if(_===null)return null;const d=e.prepare("SELECT grantee, sub, grant_id, scopes_json, run_id FROM node_rows WHERE node_id = ?").get(_);if(d===void 0||d.grantee!==r)return null;const u=r==="app:terminal";let p=null;if(d.scopes_json!==null){let A;try{A=JSON.parse(d.scopes_json)}catch{return{pause:"bad_scopes"}}if(!Array.isArray(A)||!A.every(I=>typeof I=="string"))return{pause:"bad_scopes"};p=A}const O=u?n(r):void 0;let R=d.grant_id??O?.id??null;R===null&&!u&&d.sub!==null&&(R=e.prepare("SELECT id FROM grants WHERE grantee = ? AND sub = ? ORDER BY CASE status WHEN 'active' THEN 0 ELSE 1 END, rev DESC LIMIT 1").get(r,d.sub)?.id??null);const s=R===null?void 0:E(R);return R!==null&&(s===void 0||s.grantee!==r||d.sub!==null&&s.sub!==d.sub)?{pause:"no_grant"}:R===null&&!u?{pause:"no_grant"}:(p===null&&!u&&(p=a(s.resolved_scopes_json)),{principal:{sub:d.sub??s?.sub??O?.sub??"user:local",grant_id:R,scopes:p,origin_run_id:d.run_id}})},"fromCreator"),T=o(r=>{const _=n(r);return r==="app:terminal"?{sub:_?.sub??"user:local",grant_id:_?.id??null,scopes:null,origin_run_id:null}:_===void 0?null:{sub:_.sub,grant_id:_.id,scopes:a(_.resolved_scopes_json),origin_run_id:null}},"fromGrantee"),c=e.prepare("SELECT cron_id, grantee, created_by, isolated, cwd FROM crons WHERE sub IS NULL").all(),L=e.prepare("UPDATE crons SET sub = ?, grant_id = ?, scopes_json = ?, origin_run_id = ? WHERE cron_id = ?"),l=new Map;for(const r of c){const _=i(r.grantee,r.created_by),d=_===null?T(r.grantee):"principal"in _?_.principal:null;if(d===null){const u=_!==null&&"pause"in _?_.pause:"no_grant";L.run("user:local",null,null,null,r.cron_id),e.prepare("UPDATE crons SET state = 'paused' WHERE cron_id = ?").run(r.cron_id),t.push({event:`cron.migrated.${u}`,fields:{cron_id:r.cron_id,grantee:r.grantee}})}else l.set(r.cron_id,d),L.run(d.sub,d.grant_id,d.scopes===null?null:JSON.stringify(d.scopes),d.origin_run_id,r.cron_id);if(r.isolated!==0){const u=j(Re(r.grantee).root,"crons",r.cron_id);e.prepare("UPDATE crons SET cwd = ? WHERE cron_id = ?").run(u,r.cron_id),t.push({event:"cron.migrated.isolated_cwd",fields:{cron_id:r.cron_id,old_cwd:r.cwd,cwd:u}})}}if(C(e,"daemon_actions")){const r=e.prepare("SELECT action_id, adapter_json FROM daemon_actions WHERE state IN ('pending', 'handed_off') AND json_extract(adapter_json, '$.adapter') = 'cron_spawn' AND json_extract(adapter_json, '$.principal') IS NULL").all();for(const _ of r){const d=JSON.parse(_.adapter_json),u=typeof d.cronId=="string"?d.cronId:null,p=u===null?void 0:e.prepare("SELECT grantee, sub, grant_id, scopes_json, origin_run_id, isolated, created_by FROM crons WHERE cron_id = ?").get(u);let O=null;if(p!==void 0&&!(p.grant_id===null&&p.grantee!=="app:terminal"))O={cron_id:u,grantee:p.grantee,sub:p.sub,grant_id:p.grant_id,scopes:p.scopes_json===null?null:JSON.parse(p.scopes_json),origin_run_id:p.origin_run_id,isolated:p.isolated!==0,creator:p.created_by};else if(p===void 0){const R=typeof d.parent=="string"?d.parent:null,s=R===null?void 0:e.prepare("SELECT grantee FROM node_rows WHERE node_id = ?").get(R),A=s===void 0?null:i(s.grantee,R),I=A===null||!("principal"in A)?null:A.principal;s!==void 0&&I!==null&&(O={cron_id:u,grantee:s.grantee,sub:I.sub,grant_id:I.grant_id,scopes:I.scopes,origin_run_id:I.origin_run_id,isolated:d.isolated===!0,creator:R})}if(O!==null)e.prepare("UPDATE daemon_actions SET adapter_json = ? WHERE action_id = ?").run(JSON.stringify({...d,principal:O}),_.action_id);else{const R=Date.now();e.prepare("UPDATE daemon_actions SET state = 'permanent_failed', failed_at = ?, updated_at = ?, claimed_at = NULL, claim_owner = NULL, next_attempt_at = NULL, last_failure_json = ? WHERE action_id = ?").run(R,R,JSON.stringify({kind:"grant_refused",message:"the cron has no derivable principal to authorize this birth"}),_.action_id),t.push({event:"cron.birth.refused",fields:{cron_id:u,action_id:_.action_id,reason:"grant_missing"}})}}}for(const{event:r,fields:_}of t)Ae({level:"warn",event:r,fields:_})}o(Gt,"addCronPrincipal");const F=[Ue,Ce,Se,De,ge,ve,we,he,ye,xe,Fe,Me,ke,J,je,J,He,Ge,be,Be,We,Qe,Ve,Ze,et,tt,nt,ot,Et,it,st,rt,at,Tt,ct,dt,_t,Nt,Lt,e=>{de(e),ce(e),Ne(e),Le(e)},ut,pt,lt,Rt,At,Ot,It,Ut,ft,Xt,ee,te,ne,oe,Ct,St,Ee,Dt,me,gt,vt,wt,yt,kt,Mt,Ft,jt,xt,e=>e.exec(`
928
928
  CREATE TABLE audit_rows (
929
929
  id INTEGER PRIMARY KEY, request_id TEXT NOT NULL, time TEXT NOT NULL, grantee TEXT, sub TEXT,
930
930
  client_id TEXT, class TEXT, operation TEXT NOT NULL, scope_checked TEXT,
@@ -993,4 +993,4 @@ CREATE INDEX idx_structured_requests_effects ON structured_requests(effects_done
993
993
  usd_equivalent REAL,
994
994
  PRIMARY KEY (grantee, month, payer, source, served_model)
995
995
  );
996
- `),Gt];function B(e,t,n){const E=e.prepare("PRAGMA foreign_keys").get().foreign_keys;e.exec("PRAGMA foreign_keys = OFF;");try{e.exec("BEGIN IMMEDIATE");try{W(e)===t&&(n(),e.exec(`PRAGMA user_version = ${t+1}`)),e.exec("COMMIT")}catch(a){try{e.exec("ROLLBACK")}catch{}throw a}}finally{e.exec(`PRAGMA foreign_keys = ${E};`)}}o(B,"selfTransactionalMigration");const bt=new Set([3,61,63,66]);function W(e){return e.prepare("PRAGMA user_version").get().user_version}o(W,"currentUserVersion");function se(e,t){F[t](e),e.exec(`PRAGMA user_version = ${t+1}`)}o(se,"applyMigration");function En(e){const t=W(e);if(t>F.length)throw new Error(`unsupported canvas schema version ${t} (latest supported ${F.length})`);if(t===F.length)return;const n=e.prepare("PRAGMA busy_timeout").get().timeout;e.exec("PRAGMA busy_timeout = 60000;");try{Bt(e)}finally{e.exec(`PRAGMA busy_timeout = ${n};`)}}o(En,"migrate");function Bt(e){for(;;){const t=W(e);if(t>=F.length)return;if(bt.has(t)){if(e.isTransaction)throw new Error(`canvas migration v${t+1} requires autocommit`);F[t](e);continue}if(e.isTransaction){se(e,t);continue}e.exec("BEGIN IMMEDIATE");try{W(e)===t&&se(e,t),e.exec("COMMIT")}catch(n){try{e.exec("ROLLBACK")}catch{}throw n}}}o(Bt,"runPendingMigrations");export{F as MIGRATIONS,En as migrate};
996
+ `),Gt,e=>e.exec("ALTER TABLE audit_rows ADD COLUMN via TEXT")];function B(e,t,n){const E=e.prepare("PRAGMA foreign_keys").get().foreign_keys;e.exec("PRAGMA foreign_keys = OFF;");try{e.exec("BEGIN IMMEDIATE");try{W(e)===t&&(n(),e.exec(`PRAGMA user_version = ${t+1}`)),e.exec("COMMIT")}catch(a){try{e.exec("ROLLBACK")}catch{}throw a}}finally{e.exec(`PRAGMA foreign_keys = ${E};`)}}o(B,"selfTransactionalMigration");const bt=new Set([3,61,63,66]);function W(e){return e.prepare("PRAGMA user_version").get().user_version}o(W,"currentUserVersion");function se(e,t){F[t](e),e.exec(`PRAGMA user_version = ${t+1}`)}o(se,"applyMigration");function En(e){const t=W(e);if(t>F.length)throw new Error(`unsupported canvas schema version ${t} (latest supported ${F.length})`);if(t===F.length)return;const n=e.prepare("PRAGMA busy_timeout").get().timeout;e.exec("PRAGMA busy_timeout = 60000;");try{Bt(e)}finally{e.exec(`PRAGMA busy_timeout = ${n};`)}}o(En,"migrate");function Bt(e){for(;;){const t=W(e);if(t>=F.length)return;if(bt.has(t)){if(e.isTransaction)throw new Error(`canvas migration v${t+1} requires autocommit`);F[t](e);continue}if(e.isTransaction){se(e,t);continue}e.exec("BEGIN IMMEDIATE");try{W(e)===t&&se(e,t),e.exec("COMMIT")}catch(n){try{e.exec("ROLLBACK")}catch{}throw n}}}o(Bt,"runPendingMigrations");export{F as MIGRATIONS,En as migrate};
@@ -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 g=Object.defineProperty;var r=(e,t)=>g(e,"name",{value:t,configurable:!0});import{createHash as _}from"node:crypto";import{prepared as n}from"../sqlite-statements.js";import{parseScope as y}from"@north-light/identity";import{openDb as i,withCanvasWrite as S}from"../canvas/db.js";const v=["not_started","pending","ready","failed"];function c(e){if(!Array.isArray(e))throw new TypeError("grant scopes must be a list");return e.map(t=>{if(!t||typeof t!="object"||typeof t.scope!="string"||!["always","ask"].includes(t.mode))throw new TypeError("invalid grant scope entry");return{scope:t.scope,mode:t.mode}})}r(c,"entries");function T(e){if(!e||typeof e!="object"||Array.isArray(e))return null;const{state:t,message:a,next:u}=e;return typeof t!="string"||!v.includes(t)||a!==void 0&&typeof a!="string"||typeof u!="string"||!u?null:{state:t,...a===void 0?{}:{message:a},next:u}}r(T,"dependencyOf");function l(e){if(!e||typeof e!="object"||Array.isArray(e))return{};const t={};for(const[a,u]of Object.entries(e)){const s=T(u);s&&(t[a]=s)}return t}r(l,"dependenciesOf");function E(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:l(JSON.parse(e.dependencies_json)),run_notice_url:e.run_notice_url??null,synced_at:e.synced_at}}r(E,"record");function p(e){if(!e||e.status!=="active")return null;try{return m(E(e))}catch(t){if(t instanceof TypeError||t instanceof SyntaxError)return null;throw t}}r(p,"active");function h(){return n(i(),"SELECT * FROM grants").all().flatMap(e=>{try{return[E(e)]}catch(t){if(t instanceof TypeError||t instanceof SyntaxError)return[];throw t}})}r(h,"allGrants");function m(e){if(e.status!=="active")return null;try{const t=e.resolved_scopes.map(({scope:a})=>y(a));return{...e,resolved:e.resolved_scopes.flatMap(({mode:a},u)=>a==="always"?[t[u]]:[])}}catch(t){if(t instanceof TypeError||t instanceof SyntaxError)return null;throw t}}r(m,"activeRecord");function d(e){return Array.isArray(e)?`[${e.map(d).join(",")}]`:e&&typeof e=="object"?`{${Object.keys(e).sort().filter(t=>e[t]!==void 0).map(t=>`${JSON.stringify(t)}:${d(e[t])}`).join(",")}}`:JSON.stringify(e)??"null"}r(d,"canonicalJson");function L(e){const t={rev:e.rev,resolved_scopes:e.resolved_scopes,spend_cap:e.spend_cap,payer:e.payer,dependencies:e.dependencies??{}};return _("sha256").update(d(t)).digest("base64url").slice(0,22)}r(L,"grantVersion");function F(e){return!!n(i(),"SELECT 1 FROM grants WHERE id = ?").get(e)}r(F,"hasGrant");function C(e){return n(i(),"SELECT rev, status FROM grants WHERE id = ?").get(e)??null}r(C,"grantSyncState");function M(e,t){return n(i(),"SELECT rev, status FROM grants WHERE grantee = ? AND sub = ?").get(e,t)??null}r(M,"grantSyncStateFor");function b(e){const t=n(i(),"SELECT * FROM grants WHERE id = ?").get(e);if(!t)return null;try{return E(t)}catch(a){if(a instanceof TypeError||a instanceof SyntaxError)return null;throw a}}r(b,"grantRecord");function k(e){return p(n(i(),"SELECT * FROM grants WHERE id = ?").get(e))}r(k,"activeGrant");function H(e,t){return p(n(i(),"SELECT * FROM grants WHERE grantee = ? AND sub = ?").get(e,t))}r(H,"grantFor");function W(e){const t=n(i(),"SELECT * FROM grants WHERE grantee = ? AND status = 'active'").all(e);if(t.length>1)throw new Error(`multiple active grants for ${e}`);return p(t[0])}r(W,"activeGrantForApp");function j(e){const t=n(i(),"SELECT run_notice_url AS url, sub, grantee FROM grants WHERE id = ? AND status = 'active'").get(e);return t&&t.url?{url:t.url,sub:t.sub,grantee:t.grantee}:null}r(j,"runNoticeTarget");function D(){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])}r(D,"terminalGrant");function G(){return n(i(),"SELECT value FROM runtime_state WHERE key = 'grants_source'").get()?.value??null}r(G,"grantsSource");function I(e){return n(i(),"SELECT value FROM runtime_state WHERE key = 'grants_source'").get()?.value===e}r(I,"hasSyncedGrants");function J(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))}r(J,"dropLocalGrantMirror");function U(e,t,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)}S(s=>{s.exec("DELETE FROM grants");const f=n(s,`INSERT INTO grants
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)f.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(l(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()})}r(U,"replaceGrants");function w(){const e=n(i(),"SELECT value FROM runtime_state WHERE key = 'grants_generation'").get();return e?Number(e.value):0}r(w,"grantsGeneration");export{k as activeGrant,W as activeGrantForApp,m as activeRecord,h as allGrants,l as dependenciesOf,J as dropLocalGrantMirror,H as grantFor,b as grantRecord,C as grantSyncState,M as grantSyncStateFor,L as grantVersion,w as grantsGeneration,G as grantsSource,F as hasGrant,I as hasSyncedGrants,U as replaceGrants,j as runNoticeTarget,D as terminalGrant};
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 T}from"../spaces/stop.js";import{discardChangedGrantTokens as F}from"../providers/token-cache.js";import{activeGrantForApp as M,allGrants as O}from"./mirror.js";import{runRemovalCascades as A}from"./remove.js";import{notifyGrantChanges as D}from"./watch.js";import{launcherPresent as N,listSandboxes as W,stopSandboxPid as j}from"./sandboxes.js";import{checkCronFire as B}from"../../daemon/cron/authority.js";import{cronExecutionFor as J}from"../../daemon/cron/live-runs.js";import{cronExecutionsReady as L}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 U(n,r=h()){const o=r.get(n.uid),i=n.nodeId.startsWith("cron-"),e=i?J(n.nodeId.slice(5)):void 0;if(i&&(e===void 0||!o))return"permission_revoked";if(e!==void 0&&o.status==="active"){const s=B({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=M(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(U,"exceeds");async function $(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($,"afterRevocationStop");function H(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(H,"nodePids");async function q(){if(!N())return{completion:Promise.resolve()};await L();const n=await W(),r=h(),o=n.flatMap(t=>{const s=U(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?H(t):new Set,d=u!==null&&s.some(({sandbox:a})=>f.has(a.pid));i.push((async()=>{if(d){const a=await T(t,"permission_revoked",{holdRecovery:!0,after:c(()=>$(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 j(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(q,"sweepSandboxes");function z(n,r){try{const{pairs:o,tokens:i}=F(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(z,"discardOutboundTokens");async function _e(n,r=o=>{}){const o=O();try{D(n,o)}catch(e){p({level:"error",event:"daemon.grants.notify_failed",error:e})}z(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 q();r(e)}catch(e){p({level:"error",event:"daemon.grants.sweep_failed",error:e})}const i=new Set(o.filter(e=>e.status==="removed"&&n.find(t=>t.id===e.id)?.status!=="removed").map(e=>e.grantee));r(A(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(_e,"onGrantSync");export{U as exceeds,_e as onGrantSync,q as sweepSandboxes};
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{allGrants 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};
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};
@@ -1 +1 @@
1
- var k=Object.defineProperty;var n=(e,r)=>k(e,"name",{value:r,configurable:!0});import{createPrivateKey as w,randomUUID as v,sign as S}from"node:crypto";import{readFileSync as b}from"node:fs";import{join as E}from"node:path";import{crtrHome as j}from"../canvas/paths.js";import{isUnsandboxedMachine as x}from"../runtime/unsandboxed.js";import{emitEvent as p}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 G(e,r){const t=Math.floor(Date.now()/1e3),i=Buffer.from(JSON.stringify({alg:"ES256",typ:"JWT",kid:e.kid})).toString("base64url"),a=Buffer.from(JSON.stringify({iss:e.client_id,sub:e.client_id,aud:r,iat:t,exp:t+300,jti:v()})).toString("base64url"),o=`${i}.${a}`,s=S("sha256",Buffer.from(o),{key:w({key:e.jwk,format:"jwk"}),dsaEncoding:"ieee-p1363"});return`${o}.${s.toString("base64url")}`}n(G,"assertion");function y(){const e=JSON.parse(b(E(j(),"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 R(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(R,"machineRevokedMessage");class g extends Error{static{n(this,"MachineRevokedError")}constructor(r){super(R(r)),this.name="MachineRevokedError"}}async function O(e,r){const t=y(),i=`${e.replace(/\/$/,"")}/token`,a=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:G(t,i),...r===void 0?{}:{audience:r}}),o=await fetch(i,{method:"POST",body:a,signal:AbortSignal.timeout(1e4)});if(!o.ok)throw o.status===401&&x()&&await B(o)?new g(e):new Error(`directory token request failed: ${o.status}`);const s=await o.json();if(typeof s.access_token!="string")throw new TypeError("directory token response has no access_token");return{access_token:s.access_token,expires_at:D(s.access_token,s.expires_in)}}n(O,"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;if(t.runtime!==(r.runtime.startsWith("runtime:")?r.runtime:`runtime:${r.runtime}`)||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 a=e.revisions?.find(o=>o.rev===t.rev)?.approved_at;if(typeof a!="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:a,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 m;function K(e){return m?m(e):Promise.resolve()}n(K,"requestGrantSync");function V(e,r=t=>{}){let t;const i=e.replace(/\/$/,""),a=n(async()=>{const c=y(),u=await O(i),f=await fetch(`${i}/v1/runtime/grants`,{headers:{Authorization:`Bearer ${u.access_token}`},signal:AbortSignal.timeout(1e4)});if(!f.ok)throw new Error(`directory grant sync failed: ${f.status}`);const d=await f.json();if(!Array.isArray(d.grants)||typeof d.synced_at!="string")throw new TypeError("invalid grant sync response");const h=$();A(d.grants.map(l=>J(l,c)),d.synced_at,i),p({level:"info",event:"daemon.grants.synced",fields:{grants:d.grants.length,synced_at:d.synced_at}});try{await N(h,r)}catch(l){p({level:"error",event:"daemon.grants.on_sync_failed",error:l})}},"run");let o;const s=n(()=>{const c=a().catch(u=>{throw u instanceof g?p({level:"error",event:"daemon.grants.machine_revoked",error:u}):p({level:"error",event:"daemon.grants.sync_failed",error:u}),u}).finally(()=>{t===c&&(t=void 0)});return t=c,c},"start"),_=n((c={})=>t?c.fresh?(o??=t.catch(()=>{}).then(()=>(o=void 0,t??s())),o):t:s(),"sync");return m=_,_}n(V,"createGrantSync");export{z as GRANT_SYNC_INTERVAL_S,g as MachineRevokedError,G as assertion,V as createGrantSync,R as machineRevokedMessage,O as mintDirectoryToken,K as requestGrantSync,y as runtimeClient,H as runtimeId};
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,3 +1,3 @@
1
- var N=Object.defineProperty;var s=(o,e)=>N(o,"name",{value:e,configurable:!0});import{prepared as u}from"../sqlite-statements.js";import{coveredByAny as O}from"../../shared/env.js";import{parseScope as b}from"@north-light/identity";import{activeGrant as _,grantFor as x}from"../grants/mirror.js";import{prepared as R}from"../canvas/db.js";import{readConfig as C}from"../config.js";import{SCOPE_VOCABULARY as m}from"../scopes.js";import{loadExactProfileManifest as L}from"../profiles/manifest.js";import{ensureOwner as w,getByRowid as v,nodeObject as A}from"./objects.js";function F(o){return{repos:o.map(e=>e.repo),checkouts:o}}s(F,"inRepos");function Y(o,e,a){if(e.class==="anonymous")throw new Error("a publish token cannot read as a principal");if(e.class==="person")return{class:"person",person:h(o)};if(e.class==="app")return{class:"app",app:y(o,e.app),scopes:e.scopes};if(e.class==="cron"){const i=e.grant?_(e.grant)?.resolved??[]:m,p=e.scopes;return{class:"app",app:y(o,e.app),scopes:p===null?i:p.filter(f=>O(i,f)),place:{run:e.origin_run??"",isolated:e.isolated}}}const n=d(o),r=A(o,e.node),t=r.owner===null?null:n.object(r.owner);if(!t)throw new Error(`node ${e.node} has no owner`);const l=u(o,"SELECT cwd, profile_id FROM nodes WHERE node_id = ?").get(e.node);return{class:"node",node:r,app:t,scopes:S(e.app,e.sub,e.grant,e.scopes,n),cwd:l.cwd,...F(a(l.cwd)),profile:l.profile_id===null?null:n.profile(l.profile_id,t)}}s(Y,"readerFor");function M(o,e,a,n=d(o),r=!1){const t=R(o,"SELECT o.* FROM objects o WHERE o.id = ? AND o.type = 'node'").get(e),l=R(o,`SELECT (SELECT external_id FROM identities WHERE object = ?) AS grantee,
2
- sub, grant_id, scopes_json, cwd, profile_id FROM nodes WHERE node_id = ?`).get(t?.owner??null,e);if(!t||!l)throw new Error(`no node ${e}`);const{grantee:i,sub:p,grant_id:f,scopes_json:c,cwd:g,profile_id:j}=l,E=t.owner===null?null:n.object(t.owner);if(!E)throw new Error(`node ${e} has no owner`);const H=c===null?null:JSON.parse(c);return{class:"node",node:t,app:E,scopes:S(i,p??"",f,H,n,r),cwd:g,...F(a(g)),profile:j===null?null:n.profile(j,E)}}s(M,"readerForNode");function q(o,e,a=()=>[],n=d(o)){if(e.type==="node")return M(o,e.id,a,n,!0);const r=e.owner===null?null:n.object(e.owner);if(!r)throw new Error(`watcher ${e.id} has no owner`);const l=(r.type==="app"?r:r.type==="profile"&&r.owner!==null?n.object(r.owner):null)??u(o,"SELECT o.* FROM identities i JOIN objects o ON o.rowid = i.object WHERE i.external_id = 'app:terminal' AND i.scheme = 'app'").get()??u(o,"SELECT * FROM objects WHERE type = 'person' AND name = 'user'").get();if(!l)throw new Error("no terminal app or person object");const i=u(o,"SELECT external_id FROM identities WHERE object = ? AND scheme = 'app'").get(l.rowid)?.external_id??"app:terminal",p=u(o,"SELECT resolved_scopes_json FROM grants WHERE grantee = ? AND status = 'active'").all(i),f=r.type==="person"?null:p.length===0?i==="app:terminal"&&!n.terminalHasIssuer()?m:null:p.length===1?JSON.parse(p[0].resolved_scopes_json).filter(c=>c.mode==="always").map(c=>b(c.scope)):null;return{class:"object",object:e,node:e,owner:r,app:l,scopes:f,repos:r.type==="repo"?[r]:[]}}s(q,"readerForObject");function d(o){return{grantById:_,grantFor:x,terminalHasIssuer:s(()=>!!C("user").runtime?.issuer,"terminalHasIssuer"),object:s(e=>v(o,e),"object"),profile:s((e,a)=>$(o,e,a),"profile")}}s(d,"liveLookups");function z(o){const e=d(o),a=new Map,n=s((r,t)=>(a.has(r)||a.set(r,t()),a.get(r)),"memo");return{grantById:s(r=>n(`g\0${r}`,()=>e.grantById(r)),"grantById"),grantFor:s((r,t)=>n(`a\0${r}\0${t}`,()=>e.grantFor(r,t)),"grantFor"),terminalHasIssuer:s(()=>n("issuer",()=>e.terminalHasIssuer()),"terminalHasIssuer"),object:s(r=>n(`o\0${r}`,()=>e.object(r)),"object"),profile:s((r,t)=>n(`p\0${r}\0${t.rowid}`,()=>e.profile(r,t)),"profile")}}s(z,"publishLookups");function S(o,e,a,n,r,t=!1){const l=a?r.grantById(a):r.grantFor(o,e),i=l&&l.grantee===o?l.resolved:o==="app:terminal"&&!r.terminalHasIssuer()?m:null;if(i===null){if(t)return null;throw new Error(`the grant of ${o} is not active`)}return n===null?i:n.filter(p=>O(i,p))}s(S,"nodeScopes");function h(o){return w(o,"person","local-user","user:local","user")}s(h,"personObject");function y(o,e){return e==="user:local"?h(o):w(o,"app","app",e,e.replace(/^app:/,""))}s(y,"appObject");function $(o,e,a){return u(o,`SELECT o.* FROM identities i JOIN objects o ON o.rowid = i.object
3
- WHERE i.scheme = 'profile' AND i.external_id = ?`).get(e)??w(o,"profile","profile",e,L(e).manifest.name,a)}s($,"profileObject");export{d as liveLookups,$ as profileObject,z as publishLookups,Y as readerFor,M as readerForNode,q as readerForObject};
1
+ var N=Object.defineProperty;var s=(o,e)=>N(o,"name",{value:e,configurable:!0});import{prepared as f}from"../sqlite-statements.js";import{coveredByAny as w}from"../../shared/env.js";import{parseScope as $}from"@north-light/identity";import{activeGrant as R,grantFor as F}from"../grants/mirror.js";import{prepared as S}from"../canvas/db.js";import{ApiError as b}from"../../api/errors.js";import{readConfig as x}from"../config.js";import{SCOPE_VOCABULARY as E}from"../scopes.js";import{loadExactProfileManifest as A}from"../profiles/manifest.js";import{ensureOwner as g,getByRowid as C,nodeObject as L}from"./objects.js";function v(o){return{repos:o.map(e=>e.repo),checkouts:o}}s(v,"inRepos");function z(o,e,a){if(e.class==="anonymous")throw new Error("a publish token cannot read as a principal");if(e.class==="person")return{class:"person",person:j(o)};if(e.class==="app")return{class:"app",app:_(o,e.app),scopes:e.scopes};if(e.class==="machine")throw new Error("a machine token cannot read as a principal");if(e.class==="forwarded"){if(e.actor.kind==="person")return{class:"person",person:j(o)};const c=F(e.actor.app,e.sub);if(!c)throw new b(403,"app_not_granted",`${e.actor.app} holds no active grant for ${e.sub} on this machine`);const p=e.actor.node_scopes;return{class:"app",app:_(o,e.actor.app),place:e.actor.place,scopes:p===null?c.resolved:p.filter(i=>w(c.resolved,i))}}if(e.class==="cron"){const c=e.grant?R(e.grant)?.resolved??[]:E,p=e.scopes;return{class:"app",app:_(o,e.app),scopes:p===null?c:p.filter(i=>w(c,i)),place:{run:e.origin_run??"",isolated:e.isolated}}}const n=d(o),r=L(o,e.node),t=r.owner===null?null:n.object(r.owner);if(!t)throw new Error(`node ${e.node} has no owner`);const l=f(o,"SELECT cwd, profile_id FROM nodes WHERE node_id = ?").get(e.node);return{class:"node",node:r,app:t,scopes:y(e.app,e.sub,e.grant,e.scopes,n),cwd:l.cwd,...v(a(l.cwd)),profile:l.profile_id===null?null:n.profile(l.profile_id,t)}}s(z,"readerFor");function M(o,e,a,n=d(o),r=!1){const t=S(o,"SELECT o.* FROM objects o WHERE o.id = ? AND o.type = 'node'").get(e),l=S(o,`SELECT (SELECT external_id FROM identities WHERE object = ?) AS grantee,
2
+ sub, grant_id, scopes_json, cwd, profile_id FROM nodes WHERE node_id = ?`).get(t?.owner??null,e);if(!t||!l)throw new Error(`no node ${e}`);const{grantee:c,sub:p,grant_id:i,scopes_json:u,cwd:O,profile_id:h}=l,m=t.owner===null?null:n.object(t.owner);if(!m)throw new Error(`node ${e} has no owner`);const H=u===null?null:JSON.parse(u);return{class:"node",node:t,app:m,scopes:y(c,p??"",i,H,n,r),cwd:O,...v(a(O)),profile:h===null?null:n.profile(h,m)}}s(M,"readerForNode");function K(o,e,a=()=>[],n=d(o)){if(e.type==="node")return M(o,e.id,a,n,!0);const r=e.owner===null?null:n.object(e.owner);if(!r)throw new Error(`watcher ${e.id} has no owner`);const l=(r.type==="app"?r:r.type==="profile"&&r.owner!==null?n.object(r.owner):null)??f(o,"SELECT o.* FROM identities i JOIN objects o ON o.rowid = i.object WHERE i.external_id = 'app:terminal' AND i.scheme = 'app'").get()??f(o,"SELECT * FROM objects WHERE type = 'person' AND name = 'user'").get();if(!l)throw new Error("no terminal app or person object");const c=f(o,"SELECT external_id FROM identities WHERE object = ? AND scheme = 'app'").get(l.rowid)?.external_id??"app:terminal",p=f(o,"SELECT resolved_scopes_json FROM grants WHERE grantee = ? AND status = 'active'").all(c),i=r.type==="person"?null:p.length===0?c==="app:terminal"&&!n.terminalHasIssuer()?E:null:p.length===1?JSON.parse(p[0].resolved_scopes_json).filter(u=>u.mode==="always").map(u=>$(u.scope)):null;return{class:"object",object:e,node:e,owner:r,app:l,scopes:i,repos:r.type==="repo"?[r]:[]}}s(K,"readerForObject");function d(o){return{grantById:R,grantFor:F,terminalHasIssuer:s(()=>!!x("user").runtime?.issuer,"terminalHasIssuer"),object:s(e=>C(o,e),"object"),profile:s((e,a)=>T(o,e,a),"profile")}}s(d,"liveLookups");function Q(o){const e=d(o),a=new Map,n=s((r,t)=>(a.has(r)||a.set(r,t()),a.get(r)),"memo");return{grantById:s(r=>n(`g\0${r}`,()=>e.grantById(r)),"grantById"),grantFor:s((r,t)=>n(`a\0${r}\0${t}`,()=>e.grantFor(r,t)),"grantFor"),terminalHasIssuer:s(()=>n("issuer",()=>e.terminalHasIssuer()),"terminalHasIssuer"),object:s(r=>n(`o\0${r}`,()=>e.object(r)),"object"),profile:s((r,t)=>n(`p\0${r}\0${t.rowid}`,()=>e.profile(r,t)),"profile")}}s(Q,"publishLookups");function y(o,e,a,n,r,t=!1){const l=a?r.grantById(a):r.grantFor(o,e),c=l&&l.grantee===o?l.resolved:o==="app:terminal"&&!r.terminalHasIssuer()?E:null;if(c===null){if(t)return null;throw new Error(`the grant of ${o} is not active`)}return n===null?c:n.filter(p=>w(c,p))}s(y,"nodeScopes");function j(o){return g(o,"person","local-user","user:local","user")}s(j,"personObject");function _(o,e){return e==="user:local"?j(o):g(o,"app","app",e,e.replace(/^app:/,""))}s(_,"appObject");function T(o,e,a){return f(o,`SELECT o.* FROM identities i JOIN objects o ON o.rowid = i.object
3
+ WHERE i.scheme = 'profile' AND i.external_id = ?`).get(e)??g(o,"profile","profile",e,A(e).manifest.name,a)}s(T,"profileObject");export{d as liveLookups,T as profileObject,Q as publishLookups,z as readerFor,M as readerForNode,K as readerForObject};
@@ -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};