@north-light/cli 0.3.548 → 0.3.550

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 (160) hide show
  1. package/dist/api/contract.d.ts +1 -1
  2. package/dist/api/contract.js +1 -1
  3. package/dist/api/dto/custom-objects.d.ts +1 -1
  4. package/dist/api/dto/docs.d.ts +12 -0
  5. package/dist/api/dto/objects.d.ts +17 -1
  6. package/dist/api/dto/objects.js +1 -1
  7. package/dist/api/dto/permissions.d.ts +76 -0
  8. package/dist/api/dto/permissions.js +1 -0
  9. package/dist/api/index.d.ts +3 -2
  10. package/dist/api/index.js +1 -1
  11. package/dist/api/routes.d.ts +4 -0
  12. package/dist/api/routes.js +1 -1
  13. package/dist/builtin-memory/crouter-concepts/documents.md +1 -1
  14. package/dist/builtin-memory/crouter-concepts/scopes-and-trust.md +2 -2
  15. package/dist/builtin-memory/crouter-sdk/release-notes.md +11 -0
  16. package/dist/builtin-memory/internal/INDEX.md +2 -1
  17. package/dist/builtin-memory/internal/agent-shaping.md +1 -1
  18. package/dist/builtin-memory/internal/storage-tiers.md +1 -1
  19. package/dist/builtin-memory/northlight-apps/guides/files-and-documents.md +3 -3
  20. package/dist/builtin-memory/northlight-apps/guides/many-users.md +1 -1
  21. package/dist/builtin-memory/northlight-apps/guides/test-your-app.md +1 -1
  22. package/dist/clients/attach/viewer.js +9 -9
  23. package/dist/commands/canvas/list.js +2 -2
  24. package/dist/commands/canvas/share.d.ts +2 -0
  25. package/dist/commands/canvas/share.js +2 -0
  26. package/dist/commands/canvas.js +1 -1
  27. package/dist/commands/doc/edit.js +1 -1
  28. package/dist/commands/doc/shared.js +1 -1
  29. package/dist/commands/doc/write.js +1 -1
  30. package/dist/commands/sys/sync-skills.js +2 -2
  31. package/dist/core/canvas/canvas.js +20 -20
  32. package/dist/core/canvas/db.js +4 -4
  33. package/dist/core/canvas/export-sqlite.js +2 -2
  34. package/dist/core/canvas/import-sqlite.js +5 -5
  35. package/dist/core/canvas/outcome-actions.js +3 -3
  36. package/dist/core/canvas/person.js +11 -9
  37. package/dist/core/canvas/schema-pg.sql +49 -5
  38. package/dist/core/environments.js +1 -1
  39. package/dist/core/feed/feed.js +1 -1
  40. package/dist/core/grants/mirror.d.ts +9 -1
  41. package/dist/core/grants/mirror.js +2 -2
  42. package/dist/core/grants/sync.js +1 -1
  43. package/dist/core/graph/access.d.ts +8 -49
  44. package/dist/core/graph/access.js +4 -7
  45. package/dist/core/graph/custom.d.ts +5 -2
  46. package/dist/core/graph/custom.js +9 -9
  47. package/dist/core/graph/deletion.js +5 -4
  48. package/dist/core/graph/documents.d.ts +12 -3
  49. package/dist/core/graph/documents.js +13 -7
  50. package/dist/core/graph/edges.js +3 -3
  51. package/dist/core/graph/events.js +12 -8
  52. package/dist/core/graph/kinds/document.js +4 -4
  53. package/dist/core/graph/kinds/kind.js +1 -1
  54. package/dist/core/graph/kinds/node.js +5 -5
  55. package/dist/core/graph/names.d.ts +16 -1
  56. package/dist/core/graph/names.js +11 -3
  57. package/dist/core/graph/objects.d.ts +19 -2
  58. package/dist/core/graph/objects.js +16 -10
  59. package/dist/core/graph/package-docs.js +5 -5
  60. package/dist/core/graph/package-files.js +2 -2
  61. package/dist/core/graph/reader.js +3 -3
  62. package/dist/core/graph/repo-sync/exchange.js +2 -2
  63. package/dist/core/graph/rules.d.ts +7 -5
  64. package/dist/core/graph/rules.js +3 -3
  65. package/dist/core/graph/types.d.ts +12 -1
  66. package/dist/core/graph/types.js +1 -1
  67. package/dist/core/graph/watches.d.ts +1 -1
  68. package/dist/core/graph/watches.js +9 -8
  69. package/dist/core/peer/envelope.d.ts +1 -1
  70. package/dist/core/peer/envelope.js +1 -1
  71. package/dist/core/peer/stubs.d.ts +1 -1
  72. package/dist/core/peer/stubs.js +1 -1
  73. package/dist/core/permissions/boot.d.ts +17 -0
  74. package/dist/core/permissions/boot.js +18 -0
  75. package/dist/core/permissions/builtin.d.ts +8 -0
  76. package/dist/core/permissions/builtin.js +20 -0
  77. package/dist/core/permissions/ceiling.d.ts +57 -0
  78. package/dist/core/permissions/ceiling.js +22 -0
  79. package/dist/core/permissions/compile.d.ts +14 -0
  80. package/dist/core/permissions/compile.js +1 -0
  81. package/dist/core/permissions/errors.d.ts +7 -0
  82. package/dist/core/permissions/errors.js +1 -0
  83. package/dist/core/permissions/evaluator.d.ts +28 -0
  84. package/dist/core/permissions/evaluator.js +6 -0
  85. package/dist/core/permissions/facts.d.ts +56 -0
  86. package/dist/core/permissions/facts.js +2 -0
  87. package/dist/core/permissions/schema-parser.d.ts +29 -0
  88. package/dist/core/permissions/schema-parser.js +2 -0
  89. package/dist/core/permissions/schemas.d.ts +21 -0
  90. package/dist/core/permissions/schemas.js +2 -0
  91. package/dist/core/permissions/sql.d.ts +16 -0
  92. package/dist/core/permissions/sql.js +1 -0
  93. package/dist/core/runtime/broker/frame-client.d.ts +1 -1
  94. package/dist/core/runtime/broker/frame-client.js +1 -1
  95. package/dist/core/runtime/broker/frame-dispatch.d.ts +1 -1
  96. package/dist/core/runtime/broker/frame-dispatch.js +1 -1
  97. package/dist/core/runtime/broker/frame-read.d.ts +4 -3
  98. package/dist/core/runtime/broker/frame-read.js +1 -1
  99. package/dist/core/runtime/broker-protocol.d.ts +1 -1
  100. package/dist/core/runtime/host.js +3 -3
  101. package/dist/core/runtime/placement.js +1 -1
  102. package/dist/core/runtime/roadmap.js +1 -1
  103. package/dist/core/substrate/delivery/corpus.d.ts +2 -2
  104. package/dist/core/substrate/delivery/corpus.js +3 -3
  105. package/dist/core/substrate/delivery/deliver.js +2 -2
  106. package/dist/core/substrate/delivery/render-event.js +6 -6
  107. package/dist/core/substrate/subject-fields.d.ts +2 -0
  108. package/dist/core/substrate/subject.js +1 -1
  109. package/dist/daemon/api/app-listener.js +12 -12
  110. package/dist/daemon/api/bridge.d.ts +1 -1
  111. package/dist/daemon/api/bridge.js +4 -4
  112. package/dist/daemon/api/handlers/attach.d.ts +4 -1
  113. package/dist/daemon/api/handlers/attach.js +1 -1
  114. package/dist/daemon/api/handlers/canvas.js +5 -5
  115. package/dist/daemon/api/handlers/check.d.ts +2 -0
  116. package/dist/daemon/api/handlers/check.js +1 -0
  117. package/dist/daemon/api/handlers/custom-objects.js +2 -2
  118. package/dist/daemon/api/handlers/docs.js +1 -1
  119. package/dist/daemon/api/handlers/files.js +1 -1
  120. package/dist/daemon/api/handlers/memory.js +7 -6
  121. package/dist/daemon/api/handlers/nodes.js +1 -1
  122. package/dist/daemon/api/handlers/objects.js +3 -3
  123. package/dist/daemon/api/handlers/relations.d.ts +2 -0
  124. package/dist/daemon/api/handlers/relations.js +1 -0
  125. package/dist/daemon/api/handlers/schemas.d.ts +12 -0
  126. package/dist/daemon/api/handlers/schemas.js +1 -0
  127. package/dist/daemon/api/handlers/shares.d.ts +2 -0
  128. package/dist/daemon/api/handlers/shares.js +1 -0
  129. package/dist/daemon/api/operations.js +1 -1
  130. package/dist/daemon/api/router.d.ts +10 -6
  131. package/dist/daemon/api/router.js +1 -1
  132. package/dist/daemon/api/server.js +1 -1
  133. package/dist/daemon/api/vendor-signin.js +21 -24
  134. package/dist/daemon/crtrd.js +12 -12
  135. package/dist/daemon/reconcilers/delivery-lane.js +2 -2
  136. package/dist/migrations/004-canvas-documents/fields.d.ts +1 -1
  137. package/dist/migrations/004-canvas-documents/fields.js +2 -2
  138. package/docs/apps/guides/files-and-documents.md +3 -3
  139. package/docs/apps/guides/many-users.md +1 -1
  140. package/docs/apps/guides/test-your-app.md +1 -1
  141. package/docs/cli/documents-and-preferences.md +1 -1
  142. package/docs/concepts/documents.md +1 -1
  143. package/docs/concepts/scopes-and-trust.md +2 -2
  144. package/docs/sdk/release-notes.md +11 -0
  145. package/package.json +2 -2
  146. package/packages/crouter-identity/dist/index.js +14 -1
  147. package/packages/crouter-identity/package.json +1 -1
  148. package/runtime.lock.json +13 -13
  149. package/dist/core/graph/spaces/app.d.ts +0 -3
  150. package/dist/core/graph/spaces/app.js +0 -1
  151. package/dist/core/graph/spaces/index.d.ts +0 -18
  152. package/dist/core/graph/spaces/index.js +0 -1
  153. package/dist/core/graph/spaces/own-memory.d.ts +0 -18
  154. package/dist/core/graph/spaces/own-memory.js +0 -1
  155. package/dist/core/graph/spaces/person.d.ts +0 -5
  156. package/dist/core/graph/spaces/person.js +0 -3
  157. package/dist/core/graph/spaces/repo.d.ts +0 -14
  158. package/dist/core/graph/spaces/repo.js +0 -1
  159. package/dist/core/runs/confinement.d.ts +0 -25
  160. package/dist/core/runs/confinement.js +0 -2
@@ -1,3 +1,3 @@
1
- var V=Object.defineProperty;var f=(i,a)=>V(i,"name",{value:a,configurable:!0});import{DatabaseSync as W}from"node:sqlite";import{existsSync as Y,mkdirSync as k,mkdtempSync as J,renameSync as X,rmSync as P}from"node:fs";import{tmpdir as Q}from"node:os";import{join as A}from"node:path";import j from"pg";import{migrate as z,MIGRATIONS as I}from"./migrations.js";import{canvasDbPath as Z}from"./paths.js";import{EXPORT_MARK_KEY as F,IMPORT_MARK_KEY as ee,OBJECT_COLUMNS as te,pgColumns as ne,quote as c,TABLE_ORDER as C}from"./import-sqlite.js";const re=5e3,se=new Set(["environments"]),oe={exposures:"node, object, channel",rules:"object, position",edges:"type, src, dst",unresolved_links:"src, name",revision_parents:"child, parent",canvas_meta:"key",runtime_state:"key",message_causes:"message_id",peer_ingested:"key",peer_ingested_destinations:"key",message_sequence:"id",messages:"seq",review_comment_events:"seq",audit_rows:"id",peer_outbox:"id",app_users:"uid"},ae={objects:"id",revisions:"rev_id",watches:"watch_id"},H=new Set(["daemon_owner","schema_version",ee,F]);function ie(i){const a=J(A(Q(),"crtr-export-")),r=process.env.CRTR_HOME;process.env.CRTR_HOME=a,k(A(a,"runtime","nodes"),{recursive:!0});try{const s=new W(i);return s.exec("PRAGMA journal_mode = DELETE;"),s.exec("PRAGMA foreign_keys = ON;"),z(s),s.exec("DELETE FROM canvas_meta; DELETE FROM message_sequence;"),s}finally{r===void 0?delete process.env.CRTR_HOME:process.env.CRTR_HOME=r,P(a,{recursive:!0,force:!0})}}f(ie,"createEmptyDatabase");async function he(i,a=()=>{}){const r=Z();if(Y(r))throw new Error(`${r} already exists; refusing to overwrite it`);k(A(r,".."),{recursive:!0,mode:448});const s=`${r}.exporting-${process.pid}`,u=Date.now(),t=new j.Client({connectionString:i,application_name:`crtr-export-${process.pid}`,types:{getTypeParser:f(((l,o)=>l===20?d=>Number(d):j.types.getTypeParser(l,o)),"getTypeParser")}});await t.connect();let m;try{if((await t.query("SELECT value FROM canvas_meta WHERE key = 'schema_version'").catch(()=>({rows:[]}))).rows.length===0)throw new Error("the Postgres database has no canvas schema; nothing to export");m=ie(s);const o=m;o.exec("PRAGMA foreign_keys = OFF;");const d=await ne(t),L=[...d.keys()].filter(e=>!se.has(e)&&!C.includes(e));if(L.length>0)throw new Error(`export: Postgres tables with no export rule: ${L.join(", ")}`);await t.query("BEGIN READ ONLY ISOLATION LEVEL REPEATABLE READ");const _={};for(const[e,O]of Object.entries(ae)){const h=new Map;for(const E of(await t.query(`SELECT ${O} AS id, seq FROM ${e}`)).rows)h.set(E.id,Number(E.seq));_[e]=h}const B=_.objects;let M=0;o.exec("BEGIN");for(const e of C){const O=Date.now(),h=new Set(o.prepare(`PRAGMA table_info(${c(e)})`).all().map(n=>n.name)),E=d.get(e);if(E===void 0)throw new Error(`export: Postgres has no table ${e}`);const G=E.some(n=>n.name==="seq"),$=E.filter(n=>h.has(n.name)),K=new Set(te[e]??[]),R=e==="objects"||e==="revisions"||e==="watches"||e==="watch_subscriptions",v=$.map(n=>n.type==="jsonb"?`${c(n.name)}::text AS ${c(n.name)}`:c(n.name));e==="watches"&&v.push("watch_id AS _watch_id"),R&&v.push("seq AS _seq");const y=$.map(n=>c(n.name)),U=o.prepare(`INSERT INTO ${c(e)} (${R?["rowid",...y]:y}) VALUES (${(R?["?",...y]:y).map(()=>"?").join(", ")})`),q=G?"seq":oe[e],T=`export_${e}`;await t.query(`DECLARE ${T} NO SCROLL CURSOR FOR SELECT ${v.join(", ")} FROM ${c(e)}${e==="canvas_meta"?` WHERE key NOT IN (${[...H].map(n=>`'${n}'`).join(", ")})`:""}${q===void 0?"":` ORDER BY ${q}`}`);let b=0;for(;;){const n=await t.query(`FETCH ${re} FROM ${T}`);if(n.rows.length===0)break;for(const N of n.rows){const D=R?[Number(N._seq)]:[];for(const p of $){let w=N[p.name];e==="revision_parents"&&(p.name==="child"||p.name==="parent")?w=x(_.revisions,"revision",e,p.name,w):e==="watch_subscriptions"&&p.name==="watch"?w=x(_.watches,"watch",e,p.name,w):K.has(p.name)&&(w=x(B,"object",e,p.name,w)),D.push(w??null)}U.run(...D)}b+=n.rows.length}await t.query(`CLOSE ${T}`),M+=b,a(`export ${e}: ${b} rows in ${Date.now()-O} ms`)}o.exec("COMMIT"),await t.query("COMMIT");const g=o.prepare("PRAGMA foreign_key_check").all();if(g.length>0)throw new Error(`export: ${g.length} foreign key violation(s) in the exported canvas.db, first: ${JSON.stringify(g[0])}`);await ce(t,o);const S=o.prepare("PRAGMA user_version").get().user_version;if(S!==I.length)throw new Error(`export: exported canvas.db is at user_version ${S}, expected ${I.length}`);return o.close(),m=void 0,await t.query("INSERT INTO canvas_meta (key, value) VALUES ($1, $2) ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value",[F,JSON.stringify({at:new Date().toISOString(),path:r})]),X(s,r),{path:r,rows:M,ms:Date.now()-u,userVersion:S}}catch(l){await t.query("ROLLBACK").catch(()=>{});try{m?.close()}catch{}for(const o of["","-wal","-shm","-journal"])P(`${s}${o}`,{force:!0});throw l}finally{await t.end().catch(()=>{})}}f(he,"exportPostgresToSqlite");function x(i,a,r,s,u){if(u==null)return null;const t=i.get(u);if(t===void 0)throw new Error(`export: ${r}.${s} references ${a} ${String(u)}, which was not exported (`);return t}f(x,"mapId");async function ce(i,a){const r=[];for(const s of C){const u=Number((await i.query(`SELECT count(*) AS n FROM ${c(s)}`)).rows[0].n);let t=Number(a.prepare(`SELECT count(*) AS n FROM ${c(s)}`).get().n),m=u;s==="canvas_meta"&&(m=Number((await i.query("SELECT count(*) AS n FROM canvas_meta WHERE key <> ALL($1)",[[...H]])).rows[0].n),t=Number(a.prepare("SELECT count(*) AS n FROM canvas_meta").get().n)),m!==t&&r.push(`${s}: Postgres ${m} rows, SQLite ${t}`)}if(r.length>0)throw new Error(`Postgres \u2192 SQLite export verification failed:
1
+ var V=Object.defineProperty;var f=(i,a)=>V(i,"name",{value:a,configurable:!0});import{DatabaseSync as W}from"node:sqlite";import{existsSync as Y,mkdirSync as k,mkdtempSync as J,renameSync as X,rmSync as P}from"node:fs";import{tmpdir as Q}from"node:os";import{join as A}from"node:path";import j from"pg";import{migrate as z,MIGRATIONS as I}from"./migrations.js";import{canvasDbPath as Z}from"./paths.js";import{EXPORT_MARK_KEY as F,IMPORT_MARK_KEY as ee,OBJECT_COLUMNS as te,pgColumns as ne,quote as c,TABLE_ORDER as C}from"./import-sqlite.js";const re=5e3,se=new Set(["environments","relations","schemas"]),oe={exposures:"node, object, channel",rules:"object, position",edges:"type, src, dst",unresolved_links:"src, name",revision_parents:"child, parent",canvas_meta:"key",runtime_state:"key",message_causes:"message_id",peer_ingested:"key",peer_ingested_destinations:"key",message_sequence:"id",messages:"seq",review_comment_events:"seq",audit_rows:"id",peer_outbox:"id",app_users:"uid"},ae={objects:"id",revisions:"rev_id",watches:"watch_id"},H=new Set(["daemon_owner","schema_version",ee,F]);function ie(i){const a=J(A(Q(),"crtr-export-")),r=process.env.CRTR_HOME;process.env.CRTR_HOME=a,k(A(a,"runtime","nodes"),{recursive:!0});try{const s=new W(i);return s.exec("PRAGMA journal_mode = DELETE;"),s.exec("PRAGMA foreign_keys = ON;"),z(s),s.exec("DELETE FROM canvas_meta; DELETE FROM message_sequence;"),s}finally{r===void 0?delete process.env.CRTR_HOME:process.env.CRTR_HOME=r,P(a,{recursive:!0,force:!0})}}f(ie,"createEmptyDatabase");async function _e(i,a=()=>{}){const r=Z();if(Y(r))throw new Error(`${r} already exists; refusing to overwrite it`);k(A(r,".."),{recursive:!0,mode:448});const s=`${r}.exporting-${process.pid}`,u=Date.now(),t=new j.Client({connectionString:i,application_name:`crtr-export-${process.pid}`,types:{getTypeParser:f(((l,o)=>l===20?d=>Number(d):j.types.getTypeParser(l,o)),"getTypeParser")}});await t.connect();let m;try{if((await t.query("SELECT value FROM canvas_meta WHERE key = 'schema_version'").catch(()=>({rows:[]}))).rows.length===0)throw new Error("the Postgres database has no canvas schema; nothing to export");m=ie(s);const o=m;o.exec("PRAGMA foreign_keys = OFF;");const d=await ne(t),L=[...d.keys()].filter(e=>!se.has(e)&&!C.includes(e));if(L.length>0)throw new Error(`export: Postgres tables with no export rule: ${L.join(", ")}`);await t.query("BEGIN READ ONLY ISOLATION LEVEL REPEATABLE READ");const h={};for(const[e,O]of Object.entries(ae)){const _=new Map;for(const E of(await t.query(`SELECT ${O} AS id, seq FROM ${e}`)).rows)_.set(E.id,Number(E.seq));h[e]=_}const B=h.objects;let M=0;o.exec("BEGIN");for(const e of C){const O=Date.now(),_=new Set(o.prepare(`PRAGMA table_info(${c(e)})`).all().map(n=>n.name)),E=d.get(e);if(E===void 0)throw new Error(`export: Postgres has no table ${e}`);const G=E.some(n=>n.name==="seq"),$=E.filter(n=>_.has(n.name)),K=new Set(te[e]??[]),R=e==="objects"||e==="revisions"||e==="watches"||e==="watch_subscriptions",v=$.map(n=>n.type==="jsonb"?`${c(n.name)}::text AS ${c(n.name)}`:c(n.name));e==="watches"&&v.push("watch_id AS _watch_id"),R&&v.push("seq AS _seq");const y=$.map(n=>c(n.name)),U=o.prepare(`INSERT INTO ${c(e)} (${R?["rowid",...y]:y}) VALUES (${(R?["?",...y]:y).map(()=>"?").join(", ")})`),q=G?"seq":oe[e],T=`export_${e}`;await t.query(`DECLARE ${T} NO SCROLL CURSOR FOR SELECT ${v.join(", ")} FROM ${c(e)}${e==="canvas_meta"?` WHERE key NOT IN (${[...H].map(n=>`'${n}'`).join(", ")})`:""}${q===void 0?"":` ORDER BY ${q}`}`);let b=0;for(;;){const n=await t.query(`FETCH ${re} FROM ${T}`);if(n.rows.length===0)break;for(const N of n.rows){const D=R?[Number(N._seq)]:[];for(const p of $){let w=N[p.name];e==="revision_parents"&&(p.name==="child"||p.name==="parent")?w=x(h.revisions,"revision",e,p.name,w):e==="watch_subscriptions"&&p.name==="watch"?w=x(h.watches,"watch",e,p.name,w):K.has(p.name)&&(w=x(B,"object",e,p.name,w)),D.push(w??null)}U.run(...D)}b+=n.rows.length}await t.query(`CLOSE ${T}`),M+=b,a(`export ${e}: ${b} rows in ${Date.now()-O} ms`)}o.exec("COMMIT"),await t.query("COMMIT");const g=o.prepare("PRAGMA foreign_key_check").all();if(g.length>0)throw new Error(`export: ${g.length} foreign key violation(s) in the exported canvas.db, first: ${JSON.stringify(g[0])}`);await ce(t,o);const S=o.prepare("PRAGMA user_version").get().user_version;if(S!==I.length)throw new Error(`export: exported canvas.db is at user_version ${S}, expected ${I.length}`);return o.close(),m=void 0,await t.query("INSERT INTO canvas_meta (key, value) VALUES ($1, $2) ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value",[F,JSON.stringify({at:new Date().toISOString(),path:r})]),X(s,r),{path:r,rows:M,ms:Date.now()-u,userVersion:S}}catch(l){await t.query("ROLLBACK").catch(()=>{});try{m?.close()}catch{}for(const o of["","-wal","-shm","-journal"])P(`${s}${o}`,{force:!0});throw l}finally{await t.end().catch(()=>{})}}f(_e,"exportPostgresToSqlite");function x(i,a,r,s,u){if(u==null)return null;const t=i.get(u);if(t===void 0)throw new Error(`export: ${r}.${s} references ${a} ${String(u)}, which was not exported (`);return t}f(x,"mapId");async function ce(i,a){const r=[];for(const s of C){const u=Number((await i.query(`SELECT count(*) AS n FROM ${c(s)}`)).rows[0].n);let t=Number(a.prepare(`SELECT count(*) AS n FROM ${c(s)}`).get().n),m=u;s==="canvas_meta"&&(m=Number((await i.query("SELECT count(*) AS n FROM canvas_meta WHERE key <> ALL($1)",[[...H]])).rows[0].n),t=Number(a.prepare("SELECT count(*) AS n FROM canvas_meta").get().n)),m!==t&&r.push(`${s}: Postgres ${m} rows, SQLite ${t}`)}if(r.length>0)throw new Error(`Postgres \u2192 SQLite export verification failed:
2
2
  ${r.join(`
3
- `)}`)}f(ce,"verify");export{he as exportPostgresToSqlite};
3
+ `)}`)}f(ce,"verify");export{_e as exportPostgresToSqlite};
@@ -1,7 +1,7 @@
1
- var J=Object.defineProperty;var p=(t,r)=>J(t,"name",{value:r,configurable:!0});import{DatabaseSync as Q}from"node:sqlite";import{createHash as X,randomBytes as z}from"node:crypto";import{existsSync as T,mkdirSync as Z,renameSync as P}from"node:fs";import{dirname as ee}from"node:path";import te from"pg";import{withExclusiveDirectoryLockAsync as re}from"../exclusive-lock.js";import{DAEMON_STARTUP_WINDOW_MS as oe}from"../../daemon/startup-policy.js";import{migrate as se}from"./migrations.js";import{canvasDbPath as F,onDiskMigrationLockPath as ne}from"./paths.js";const q="imported_from_sqlite",B="exported_to_sqlite",C=1e4,L=["objects","documents","revisions","revision_parents","jobs","identities","custom_objects","context_sessions","peer_stubs","object_kinds","watches","watch_subscriptions","exposures","rules","edges","unresolved_links","nodes","focuses","canvas_meta","runtime_state","warm_pool","sessions","executions","worktrees","node_faults","node_tokens","node_outcome_deliveries","deleted_isolated_nodes","create_requests","waits","messages","message_sequence","message_causes","deliveries","structured_requests","daemon_actions","human_requests","human_action_deliveries","review_comments","review_comment_events","run_events","idempotency_keys","grants","memory_reads","app_users","uploads","upload_uses","shares","audit_rows","grantee_usage","provider_receipts","provider_approvals","usage","crons","cron_runs","peer_outbox","peer_locations","peer_ingested","peer_ingested_destinations"],ae=new Set(["sqlite_sequence","sqlite_stat1","sqlite_stat4","reviews"]),ie={objects:["owner"],documents:["object"],revisions:["document","author"],jobs:["object"],identities:["object"],custom_objects:["object"],context_sessions:["node"],peer_stubs:["object"],object_kinds:["owner"],watches:["watcher","object","link_app"],exposures:["node","object"],rules:["object","owner"],edges:["src","dst"],unresolved_links:["src"]},U=new Set(["daemon_owner","schema_version"]),ce={canvas_meta:"key",message_sequence:"id"};function W(){return z(16).toString("hex")}p(W,"newId");function m(t){return`"${t.replace(/"/g,'""')}"`}p(m,"quote");function ue(t){switch(t){case"bigint":return"bigint[]";case"double precision":return"float8[]";case"bytea":return"bytea[]";case"text":case"jsonb":return"text[]";default:throw new Error(`import: no array type for Postgres column type ${t}`)}}p(ue,"arrayType");async function de(t){const r=await t.query(`SELECT c.relname AS tbl, a.attname AS col, format_type(a.atttypid, a.atttypmod) AS type, a.attidentity <> '' AS identity
1
+ var Q=Object.defineProperty;var m=(t,o)=>Q(t,"name",{value:o,configurable:!0});import{DatabaseSync as X}from"node:sqlite";import{createHash as z,randomBytes as Z}from"node:crypto";import{existsSync as O,mkdirSync as ee,renameSync as F}from"node:fs";import{dirname as te}from"node:path";import oe from"pg";import{withExclusiveDirectoryLockAsync as se}from"../exclusive-lock.js";import{DAEMON_STARTUP_WINDOW_MS as re}from"../../daemon/startup-policy.js";import{migrate as ne}from"./migrations.js";import{readLegacyPrivacy as ae}from"../graph/types.js";import{canvasDbPath as B,onDiskMigrationLockPath as ie}from"./paths.js";const q="imported_from_sqlite",U="exported_to_sqlite",M=1e4,L=["objects","documents","revisions","revision_parents","jobs","identities","custom_objects","context_sessions","peer_stubs","object_kinds","watches","watch_subscriptions","exposures","rules","edges","unresolved_links","nodes","focuses","canvas_meta","runtime_state","warm_pool","sessions","executions","worktrees","node_faults","node_tokens","node_outcome_deliveries","deleted_isolated_nodes","create_requests","waits","messages","message_sequence","message_causes","deliveries","structured_requests","daemon_actions","human_requests","human_action_deliveries","review_comments","review_comment_events","run_events","idempotency_keys","grants","memory_reads","app_users","uploads","upload_uses","shares","audit_rows","grantee_usage","provider_receipts","provider_approvals","usage","crons","cron_runs","peer_outbox","peer_locations","peer_ingested","peer_ingested_destinations"],ce=new Set(["sqlite_sequence","sqlite_stat1","sqlite_stat4","reviews"]),ue={objects:["owner"],documents:["object"],revisions:["document","author"],jobs:["object"],identities:["object"],custom_objects:["object"],context_sessions:["node"],peer_stubs:["object"],object_kinds:["owner"],watches:["watcher","object","link_app"],exposures:["node","object"],rules:["object","owner"],edges:["src","dst"],unresolved_links:["src"]},pe={objects:["schema_type"],nodes:["app"],revisions:["acting_app","acting_for"]},W=new Set(["daemon_owner","schema_version"]),de={canvas_meta:"key",message_sequence:"id"};function H(){return Z(16).toString("hex")}m(H,"newId");function f(t){return`"${t.replace(/"/g,'""')}"`}m(f,"quote");function me(t){switch(t){case"bigint":return"bigint[]";case"double precision":return"float8[]";case"bytea":return"bytea[]";case"text":case"jsonb":return"text[]";default:throw new Error(`import: no array type for Postgres column type ${t}`)}}m(me,"arrayType");async function fe(t){const o=await t.query(`SELECT c.relname AS tbl, a.attname AS col, format_type(a.atttypid, a.atttypmod) AS type, a.attidentity <> '' AS identity
2
2
  FROM pg_attribute a JOIN pg_class c ON c.oid = a.attrelid
3
3
  WHERE c.relnamespace = current_schema()::regnamespace AND c.relkind = 'r' AND a.attnum > 0 AND NOT a.attisdropped
4
- ORDER BY c.relname, a.attnum`),e=new Map;for(const o of r.rows){const a=e.get(o.tbl)??[];a.push({name:o.col,type:o.type,identity:o.identity}),e.set(o.tbl,a)}return e}p(de,"pgColumns");const pe="\uFFFD";function me(t){if(!t.includes("\\u0000"))return{text:t,replaced:0};let r;try{r=JSON.parse(t)}catch{return{text:t,replaced:0}}let e=0;const o=p(u=>{const i=u.split("\0");return e+=i.length-1,i.join(pe)},"clean"),a=p(u=>typeof u=="string"?o(u):Array.isArray(u)?u.map(a):u!==null&&typeof u=="object"?Object.fromEntries(Object.entries(u).map(([i,_])=>[o(i),a(_)])):u,"walk"),w=a(r);return e===0?{text:t,replaced:e}:{text:JSON.stringify(w),replaced:e}}p(me,"scrubNul");async function fe(t,r,e,o,a,w,u){for(let i=0;i<o.length;i++)for(const _ of e){const s=a[w.indexOf(_)][i];if(s!=null){await t.query("SAVEPOINT import_probe");try{await t.query("SELECT $1::jsonb",[s]),await t.query("RELEASE SAVEPOINT import_probe")}catch(c){return await t.query("ROLLBACK TO SAVEPOINT import_probe"),new Error(`import: ${r}.${_.name} at ${u(o[i])} is not a value jsonb accepts: ${c.message}`,{cause:c})}}}}p(fe,"describeJsonbFailure");function j(t,r,e,o,a){if(a==null)return null;const w=t.get(Number(a));if(w===void 0)throw new Error(`import: ${e}.${o} references ${r} rowid ${String(a)}, which does not exist`);return w}p(j,"lookup");async function we(t,r,e,o,a,w){const u=t.prepare(`PRAGMA table_info(${m(e)})`).all(),i=new Set(u.map(n=>n.name)),s=!t.prepare("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?").get(e).sql.toUpperCase().includes("WITHOUT ROWID"),c=new Set(ie[e]??[]),l=[];for(const n of o)if(n.name==="seq"&&!i.has("seq")){if(!s)throw new Error(`import: ${e} needs seq from rowid but has none`);l.push("rowid AS seq")}else if(n.name==="watch_id"||n.name==="subscription_id")l.push("NULL AS "+m(n.name));else if(i.has(n.name))l.push(m(n.name));else throw new Error(`import: SQLite table ${e} has no column ${n.name}`);s&&l.push("rowid AS _rowid");const $=e==="canvas_meta"?` WHERE key NOT IN (${[...U].map(n=>`'${n}'`).join(",")})`:"",S=t.prepare(`SELECT ${l.join(", ")} FROM ${m(e)}${$}${s?" ORDER BY rowid":""}`),y=o.map(n=>m(n.name)),h=o.map((n,d)=>`$${d+1}::${ue(n.type)}`),g=o.map((n,d)=>`c${d}`),A=o.map((n,d)=>n.type==="jsonb"?`c${d}::jsonb`:`c${d}`);let R=`INSERT INTO ${m(e)} (${y.join(", ")}) SELECT ${A.join(", ")} FROM unnest(${h.join(", ")}) AS u(${g.join(", ")})`;const v=ce[e];if(v!==void 0){const n=o.filter(d=>d.name!==v).map(d=>`${m(d.name)} = EXCLUDED.${m(d.name)}`);R+=` ON CONFLICT (${m(v)}) DO UPDATE SET ${n.join(", ")}`}const M=e==="objects",I=o.filter(n=>n.type==="jsonb"),D=s?"_rowid":o[0].name,K=p(n=>`${s?"rowid":D} ${String(n[D])}`,"key");let k=0,b=[];const x=p(async()=>{if(b.length===0)return;const n=o.map(()=>new Array(b.length));if(b.forEach((d,O)=>{o.forEach((f,V)=>{let E=d[f.name];if(f.name==="watch_id"||f.name==="subscription_id"?E=W():e==="revision_parents"&&(f.name==="child"||f.name==="parent")?E=j(a.revisions,"revision",e,f.name,E):e==="watch_subscriptions"&&f.name==="watch"?E=j(a.watches,"watch",e,f.name,E):c.has(f.name)&&(E=M?null:j(a.objects,"object",e,f.name,E)),f.type==="bytea"&&E instanceof Uint8Array&&!Buffer.isBuffer(E)&&(E=Buffer.from(E.buffer,E.byteOffset,E.byteLength)),f.type==="jsonb"&&typeof E=="string"){const N=me(E);N.replaced>0&&(E=N.text,w.set(`${e}.${f.name}`,(w.get(`${e}.${f.name}`)??0)+N.replaced))}n[V][O]=E??null}),e==="watches"&&a.watches.set(Number(d._rowid),n[o.findIndex(f=>f.name==="watch_id")][O])}),I.length===0)await r.query(R,n);else{await r.query("SAVEPOINT import_batch");try{await r.query(R,n)}catch(d){throw await r.query("ROLLBACK TO SAVEPOINT import_batch"),await fe(r,e,I,b,n,o,K)??d}await r.query("RELEASE SAVEPOINT import_batch")}k+=b.length,b=[]},"flush");for(const n of S.iterate())e==="objects"&&a.objects.set(Number(n._rowid),n.id),e==="revisions"&&a.revisions.set(Number(n._rowid),n.rev_id),b.push(n),b.length>=C&&await x();if(await x(),M){const n=t.prepare("SELECT id, owner FROM objects WHERE owner IS NOT NULL").all();for(let d=0;d<n.length;d+=C){const O=n.slice(d,d+C);await r.query("UPDATE objects o SET owner = v.owner FROM unnest($1::text[], $2::text[]) AS v(id, owner) WHERE o.id = v.id",[O.map(f=>f.id),O.map(f=>j(a.objects,"object","objects","owner",f.owner))])}}return k}p(we,"copyTable");async function Ee(t,r){for(const[e,o]of r)for(const a of o)a.identity&&await t.query(`SELECT setval(pg_get_serial_sequence($1, $2), COALESCE((SELECT max(${m(a.name)}) FROM ${m(e)}), 1), (SELECT max(${m(a.name)}) IS NOT NULL FROM ${m(e)}))`,[m(e),a.name])}p(Ee,"advanceIdentities");function H(t,r,e=""){return Number(t.prepare(`SELECT count(*) AS n FROM ${m(r)}${e}`).get().n)}p(H,"sqliteCount");async function le(t,r,e){const o=[];for(const s of L){if(s==="canvas_meta")continue;const c=H(t,s),l=Number((await r.query(`SELECT count(*) AS n FROM ${m(s)}`)).rows[0].n);c!==l&&o.push(`${s}: SQLite ${c} rows, Postgres ${l}`)}for(const s of t.prepare(`SELECT key, value FROM canvas_meta WHERE key NOT IN (${[...U].map(c=>`'${c}'`).join(",")})`).all())(await r.query("SELECT value FROM canvas_meta WHERE key = $1",[s.key])).rows[0]?.value!==s.value&&o.push(`canvas_meta ${s.key}: value differs`);const a=p(s=>{const c=X("sha256");for(const l of s)c.update(l).update(`
5
- `);return c.digest("hex")},"hash"),w=t.prepare("SELECT id FROM objects ORDER BY rowid").all().map(s=>s.id),u=(await r.query("SELECT id FROM objects ORDER BY seq")).rows.map(s=>s.id);a(w)!==a(u)&&o.push("objects: ids in insertion order differ");const i=t.prepare("SELECT o.id AS id, p.id AS owner FROM objects o JOIN objects p ON p.rowid = o.owner ORDER BY o.rowid").all(),_=(await r.query("SELECT id, owner FROM objects WHERE owner IS NOT NULL ORDER BY seq")).rows;a(i.map(s=>`${s.id}>${s.owner}`))!==a(_.map(s=>`${s.id}>${s.owner}`))&&o.push("objects: owner links differ");for(const[s,c]of[["run_events","body_json"],["messages","body_json"],["deliveries","last_failure_json"]]){const l=Number(t.prepare(`SELECT COALESCE(sum(length(CAST(${m(c)} AS TEXT))), 0) AS n FROM ${m(s)}`).get().n),$=e.get(s).find(h=>h.name===c).type,S=$==="jsonb"?`${m(c)}::text`:m(c);if($==="jsonb")continue;const y=Number((await r.query(`SELECT COALESCE(sum(length(${S})), 0)::bigint AS n FROM ${m(s)}`)).rows[0].n);l!==y&&o.push(`${s}.${c}: total length SQLite ${l}, Postgres ${y}`)}if(o.length>0)throw new Error(`SQLite \u2192 Postgres import verification failed (rolled back; canvas.db untouched):
6
- ${o.join(`
7
- `)}`)}p(le,"verify");async function ye(t,r,e={}){const o=e.log??(()=>{}),a=Date.now(),w=await de(r),u=t.prepare("SELECT name FROM sqlite_master WHERE type = 'table'").all().map(y=>y.name),i=u.filter(y=>!ae.has(y)&&!L.includes(y));if(i.length>0)throw new Error(`import: SQLite tables with no copy rule: ${i.join(", ")}`);const _=L.filter(y=>!w.has(y));if(_.length>0)throw new Error(`import: Postgres tables missing from the schema: ${_.join(", ")}`);const s=u.includes("reviews")?H(t,"reviews"):0;if(s>0)throw new Error(`import: the legacy reviews table still holds ${s} rows after the SQLite migration chain`);const c={objects:new Map,revisions:new Map,watches:new Map},l=[],$=new Map;let S=0;await r.query("BEGIN");try{await r.query("ALTER TABLE nodes DISABLE TRIGGER USER"),await r.query("ALTER TABLE deliveries DISABLE TRIGGER USER");for(const h of L){const g=Date.now(),A=await we(t,r,h,w.get(h),c,$),R=Date.now()-g;l.push({table:h,rows:A,ms:R}),o(`import ${h}: ${A} rows in ${R} ms`)}await r.query("ALTER TABLE nodes ENABLE TRIGGER USER"),await r.query("ALTER TABLE deliveries ENABLE TRIGGER USER"),await Ee(r,w);const y=Date.now();await le(t,r,w),S=Date.now()-y,o(`import verify: ${S} ms`),await r.query("INSERT INTO canvas_meta (key, value) VALUES ($1, $2)",[q,JSON.stringify({at:new Date().toISOString(),rows:l.reduce((h,g)=>h+g.rows,0)})]),await r.query("COMMIT")}catch(y){throw await r.query("ROLLBACK").catch(()=>{}),y}return $.size>0&&o(`import: replaced NUL (\\u0000) with U+FFFD in jsonb values: ${[...$].map(([y,h])=>`${y} ${h}`).join(", ")}`),{tables:l,verifyMs:S,totalMs:Date.now()-a,nulReplaced:Object.fromEntries($)}}p(ye,"copySqliteToPostgres");const G=["","-wal","-shm"];function Y(t=F()){if(T(`${t}.imported`)){const r=`${t}.imported.${Date.now()}`;for(const e of G)T(`${t}.imported${e}`)&&P(`${t}.imported${e}`,`${r}${e}`)}for(const r of G)T(`${t}${r}`)&&P(`${t}${r}`,`${t}.imported${r}`)}p(Y,"renameImportedDatabase");async function Le(t,r=()=>{}){const e=F();if(!T(e))return null;const o=ne();return Z(ee(o),{recursive:!0,mode:448}),re(o,async()=>{if(!T(e))return null;const a=new te.Client({connectionString:t,application_name:`crtr-import-${process.pid}`});await a.connect();try{const w=await a.query("SELECT key FROM canvas_meta WHERE key = ANY($1)",[[q,B]]),u=new Set(w.rows.map(s=>s.key));if(u.has(q)){if(u.has(B))throw new Error(`${e} was written by \`crtr sys db export-sqlite\` and the Postgres database predates it; importing would drop the exported writes. Stop the daemon and delete the Postgres cluster (runtime/pg/data) to import ${e} afresh, or move ${e} aside to keep using Postgres.`);return Y(e),r(`import: finished the rename of ${e} (already imported)`),null}const i=new Q(e);let _;try{i.exec("PRAGMA busy_timeout = 5000;"),i.exec("PRAGMA foreign_keys = ON;"),se(i),_=await ye(i,a,{log:r})}finally{i.close()}return Y(e),r(`import: ${_.tables.reduce((s,c)=>s+c.rows,0)} rows in ${_.totalMs} ms; canvas.db renamed to canvas.db.imported`),_}finally{await a.end().catch(()=>{})}},{timeoutMs:oe,timeoutError:p(()=>new Error(`timed out waiting for the canvas import lock: ${o}`),"timeoutError")})}p(Le,"importSqliteCanvasIfPresent");export{B as EXPORT_MARK_KEY,q as IMPORT_MARK_KEY,ie as OBJECT_COLUMNS,L as TABLE_ORDER,ye as copySqliteToPostgres,Le as importSqliteCanvasIfPresent,de as pgColumns,m as quote,Y as renameImportedDatabase};
4
+ ORDER BY c.relname, a.attnum`),e=new Map;for(const a of o.rows){const r=e.get(a.tbl)??[];r.push({name:a.col,type:a.type,identity:a.identity}),e.set(a.tbl,r)}return e}m(fe,"pgColumns");const we="\uFFFD";function Ee(t){if(!t.includes("\\u0000"))return{text:t,replaced:0};let o;try{o=JSON.parse(t)}catch{return{text:t,replaced:0}}let e=0;const a=m(i=>{const u=i.split("\0");return e+=u.length-1,u.join(we)},"clean"),r=m(i=>typeof i=="string"?a(i):Array.isArray(i)?i.map(r):i!==null&&typeof i=="object"?Object.fromEntries(Object.entries(i).map(([u,l])=>[a(u),r(l)])):i,"walk"),E=r(o);return e===0?{text:t,replaced:e}:{text:JSON.stringify(E),replaced:e}}m(Ee,"scrubNul");async function le(t,o,e,a,r,E,i){for(let u=0;u<a.length;u++)for(const l of e){const n=r[E.indexOf(l)][u];if(n!=null){await t.query("SAVEPOINT import_probe");try{await t.query("SELECT $1::jsonb",[n]),await t.query("RELEASE SAVEPOINT import_probe")}catch(c){return await t.query("ROLLBACK TO SAVEPOINT import_probe"),new Error(`import: ${o}.${l.name} at ${i(a[u])} is not a value jsonb accepts: ${c.message}`,{cause:c})}}}}m(le,"describeJsonbFailure");function j(t,o,e,a,r){if(r==null)return null;const E=t.get(Number(r));if(E===void 0)throw new Error(`import: ${e}.${a} references ${o} rowid ${String(r)}, which does not exist`);return E}m(j,"lookup");async function ye(t,o,e,a,r,E){const i=a.filter(s=>!(pe[e]??[]).includes(s.name)),u=t.prepare(`PRAGMA table_info(${f(e)})`).all(),l=new Set(u.map(s=>s.name)),c=!t.prepare("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?").get(e).sql.toUpperCase().includes("WITHOUT ROWID"),_=new Set(ue[e]??[]),h=[];for(const s of i)if(s.name==="seq"&&!l.has("seq")){if(!c)throw new Error(`import: ${e} needs seq from rowid but has none`);h.push("rowid AS seq")}else if(s.name==="watch_id"||s.name==="subscription_id")h.push("NULL AS "+f(s.name));else if(l.has(s.name))h.push(f(s.name));else throw new Error(`import: SQLite table ${e} has no column ${s.name}`);c&&h.push("rowid AS _rowid");const S=e==="canvas_meta"?` WHERE key NOT IN (${[...W].map(s=>`'${s}'`).join(",")})`:"",y=t.prepare(`SELECT ${h.join(", ")} FROM ${f(e)}${S}${c?" ORDER BY rowid":""}`),$=i.map(s=>f(s.name)),g=i.map((s,p)=>`$${p+1}::${me(s.type)}`),T=i.map((s,p)=>`c${p}`),A=i.map((s,p)=>s.type==="jsonb"?`c${p}::jsonb`:`c${p}`);let v=`INSERT INTO ${f(e)} (${$.join(", ")}) SELECT ${A.join(", ")} FROM unnest(${g.join(", ")}) AS u(${T.join(", ")})`;const N=de[e];if(N!==void 0){const s=i.filter(p=>p.name!==N).map(p=>`${f(p.name)} = EXCLUDED.${f(p.name)}`);v+=` ON CONFLICT (${f(N)}) DO UPDATE SET ${s.join(", ")}`}const I=e==="objects",D=i.filter(s=>s.type==="jsonb"),k=c?"_rowid":i[0].name,V=m(s=>`${c?"rowid":k} ${String(s[k])}`,"key");let x=0,b=[];const P=m(async()=>{if(b.length===0)return;const s=i.map(()=>new Array(b.length));if(b.forEach((p,R)=>{i.forEach((d,J)=>{let w=p[d.name];if(e==="objects"&&d.name==="privacy"?w=ae(w):d.name==="watch_id"||d.name==="subscription_id"?w=H():e==="revision_parents"&&(d.name==="child"||d.name==="parent")?w=j(r.revisions,"revision",e,d.name,w):e==="watch_subscriptions"&&d.name==="watch"?w=j(r.watches,"watch",e,d.name,w):_.has(d.name)&&(w=I?null:j(r.objects,"object",e,d.name,w)),d.type==="bytea"&&w instanceof Uint8Array&&!Buffer.isBuffer(w)&&(w=Buffer.from(w.buffer,w.byteOffset,w.byteLength)),d.type==="jsonb"&&typeof w=="string"){const C=Ee(w);C.replaced>0&&(w=C.text,E.set(`${e}.${d.name}`,(E.get(`${e}.${d.name}`)??0)+C.replaced))}s[J][R]=w??null}),e==="watches"&&r.watches.set(Number(p._rowid),s[i.findIndex(d=>d.name==="watch_id")][R])}),D.length===0)await o.query(v,s);else{await o.query("SAVEPOINT import_batch");try{await o.query(v,s)}catch(p){throw await o.query("ROLLBACK TO SAVEPOINT import_batch"),await le(o,e,D,b,s,i,V)??p}await o.query("RELEASE SAVEPOINT import_batch")}x+=b.length,b=[]},"flush");for(const s of y.iterate())e==="objects"&&r.objects.set(Number(s._rowid),s.id),e==="revisions"&&r.revisions.set(Number(s._rowid),s.rev_id),b.push(s),b.length>=M&&await P();if(await P(),I){const s=t.prepare("SELECT id, owner FROM objects WHERE owner IS NOT NULL").all();for(let p=0;p<s.length;p+=M){const R=s.slice(p,p+M);await o.query("UPDATE objects o SET owner = v.owner FROM unnest($1::text[], $2::text[]) AS v(id, owner) WHERE o.id = v.id",[R.map(d=>d.id),R.map(d=>j(r.objects,"object","objects","owner",d.owner))])}}return x}m(ye,"copyTable");async function _e(t,o){for(const[e,a]of o)for(const r of a)r.identity&&await t.query(`SELECT setval(pg_get_serial_sequence($1, $2), COALESCE((SELECT max(${f(r.name)}) FROM ${f(e)}), 1), (SELECT max(${f(r.name)}) IS NOT NULL FROM ${f(e)}))`,[f(e),r.name])}m(_e,"advanceIdentities");function G(t,o,e=""){return Number(t.prepare(`SELECT count(*) AS n FROM ${f(o)}${e}`).get().n)}m(G,"sqliteCount");async function he(t,o,e){const a=[];for(const n of L){if(n==="canvas_meta")continue;const c=G(t,n),_=Number((await o.query(`SELECT count(*) AS n FROM ${f(n)}`)).rows[0].n);c!==_&&a.push(`${n}: SQLite ${c} rows, Postgres ${_}`)}for(const n of t.prepare(`SELECT key, value FROM canvas_meta WHERE key NOT IN (${[...W].map(c=>`'${c}'`).join(",")})`).all())(await o.query("SELECT value FROM canvas_meta WHERE key = $1",[n.key])).rows[0]?.value!==n.value&&a.push(`canvas_meta ${n.key}: value differs`);const r=m(n=>{const c=z("sha256");for(const _ of n)c.update(_).update(`
5
+ `);return c.digest("hex")},"hash"),E=t.prepare("SELECT id FROM objects ORDER BY rowid").all().map(n=>n.id),i=(await o.query("SELECT id FROM objects ORDER BY seq")).rows.map(n=>n.id);r(E)!==r(i)&&a.push("objects: ids in insertion order differ");const u=t.prepare("SELECT o.id AS id, p.id AS owner FROM objects o JOIN objects p ON p.rowid = o.owner ORDER BY o.rowid").all(),l=(await o.query("SELECT id, owner FROM objects WHERE owner IS NOT NULL ORDER BY seq")).rows;r(u.map(n=>`${n.id}>${n.owner}`))!==r(l.map(n=>`${n.id}>${n.owner}`))&&a.push("objects: owner links differ");for(const[n,c]of[["run_events","body_json"],["messages","body_json"],["deliveries","last_failure_json"]]){const _=Number(t.prepare(`SELECT COALESCE(sum(length(CAST(${f(c)} AS TEXT))), 0) AS n FROM ${f(n)}`).get().n),h=e.get(n).find($=>$.name===c).type,S=h==="jsonb"?`${f(c)}::text`:f(c);if(h==="jsonb")continue;const y=Number((await o.query(`SELECT COALESCE(sum(length(${S})), 0)::bigint AS n FROM ${f(n)}`)).rows[0].n);_!==y&&a.push(`${n}.${c}: total length SQLite ${_}, Postgres ${y}`)}if(a.length>0)throw new Error(`SQLite \u2192 Postgres import verification failed (rolled back; canvas.db untouched):
6
+ ${a.join(`
7
+ `)}`)}m(he,"verify");async function $e(t,o,e={}){const a=e.log??(()=>{}),r=Date.now(),E=await fe(o),i=t.prepare("SELECT name FROM sqlite_master WHERE type = 'table'").all().map(y=>y.name),u=i.filter(y=>!ce.has(y)&&!L.includes(y));if(u.length>0)throw new Error(`import: SQLite tables with no copy rule: ${u.join(", ")}`);const l=L.filter(y=>!E.has(y));if(l.length>0)throw new Error(`import: Postgres tables missing from the schema: ${l.join(", ")}`);const n=i.includes("reviews")?G(t,"reviews"):0;if(n>0)throw new Error(`import: the legacy reviews table still holds ${n} rows after the SQLite migration chain`);const c={objects:new Map,revisions:new Map,watches:new Map},_=[],h=new Map;let S=0;await o.query("BEGIN");try{await o.query("ALTER TABLE nodes DISABLE TRIGGER USER"),await o.query("ALTER TABLE deliveries DISABLE TRIGGER USER");for(const $ of L){const g=Date.now(),T=await ye(t,o,$,E.get($),c,h),A=Date.now()-g;_.push({table:$,rows:T,ms:A}),a(`import ${$}: ${T} rows in ${A} ms`)}await o.query("ALTER TABLE nodes ENABLE TRIGGER USER"),await o.query("ALTER TABLE deliveries ENABLE TRIGGER USER"),await _e(o,E);const y=Date.now();await he(t,o,E),S=Date.now()-y,a(`import verify: ${S} ms`),await o.query("INSERT INTO canvas_meta (key, value) VALUES ($1, $2)",[q,JSON.stringify({at:new Date().toISOString(),rows:_.reduce(($,g)=>$+g.rows,0)})]),await o.query("COMMIT")}catch(y){throw await o.query("ROLLBACK").catch(()=>{}),y}return h.size>0&&a(`import: replaced NUL (\\u0000) with U+FFFD in jsonb values: ${[...h].map(([y,$])=>`${y} ${$}`).join(", ")}`),{tables:_,verifyMs:S,totalMs:Date.now()-r,nulReplaced:Object.fromEntries(h)}}m($e,"copySqliteToPostgres");const Y=["","-wal","-shm"];function K(t=B()){if(O(`${t}.imported`)){const o=`${t}.imported.${Date.now()}`;for(const e of Y)O(`${t}.imported${e}`)&&F(`${t}.imported${e}`,`${o}${e}`)}for(const o of Y)O(`${t}${o}`)&&F(`${t}${o}`,`${t}.imported${o}`)}m(K,"renameImportedDatabase");async function Ce(t,o=()=>{}){const e=B();if(!O(e))return null;const a=ie();return ee(te(a),{recursive:!0,mode:448}),se(a,async()=>{if(!O(e))return null;const r=new oe.Client({connectionString:t,application_name:`crtr-import-${process.pid}`});await r.connect();try{const E=await r.query("SELECT key FROM canvas_meta WHERE key = ANY($1)",[[q,U]]),i=new Set(E.rows.map(n=>n.key));if(i.has(q)){if(i.has(U))throw new Error(`${e} was written by \`crtr sys db export-sqlite\` and the Postgres database predates it; importing would drop the exported writes. Stop the daemon and delete the Postgres cluster (runtime/pg/data) to import ${e} afresh, or move ${e} aside to keep using Postgres.`);return K(e),o(`import: finished the rename of ${e} (already imported)`),null}const u=new X(e);let l;try{u.exec("PRAGMA busy_timeout = 5000;"),u.exec("PRAGMA foreign_keys = ON;"),ne(u),l=await $e(u,r,{log:o})}finally{u.close()}return K(e),o(`import: ${l.tables.reduce((n,c)=>n+c.rows,0)} rows in ${l.totalMs} ms; canvas.db renamed to canvas.db.imported`),l}finally{await r.end().catch(()=>{})}},{timeoutMs:re,timeoutError:m(()=>new Error(`timed out waiting for the canvas import lock: ${a}`),"timeoutError")})}m(Ce,"importSqliteCanvasIfPresent");export{U as EXPORT_MARK_KEY,q as IMPORT_MARK_KEY,ue as OBJECT_COLUMNS,L as TABLE_ORDER,$e as copySqliteToPostgres,Ce as importSqliteCanvasIfPresent,fe as pgColumns,f as quote,K as renameImportedDatabase};
@@ -1,6 +1,6 @@
1
- var p=Object.defineProperty;var i=(t,a)=>p(t,"name",{value:a,configurable:!0});import{createObject as u,nodeObject as l}from"../graph/objects.js";import{recordWatch as d}from"../graph/watches.js";import{argvReceiver as m}from"../graph/receivers.js";import{finalizeNodeMail as v,insertMessage as w}from"../feed/messages.js";import{deliveryFrom as E}from"./deliveries.js";async function s(t,a){const e=await t.prepare(`SELECT a.*, c.receiver_json, c.payload_json FROM objects a
1
+ var u=Object.defineProperty;var n=(e,t)=>u(e,"name",{value:t,configurable:!0});import{writeActsFor as d}from"../permissions/facts.js";import{createObject as m,nodeObject as p}from"../graph/objects.js";import{recordWatch as E}from"../graph/watches.js";import{argvReceiver as w}from"../graph/receivers.js";import{finalizeNodeMail as v,insertMessage as y}from"../feed/messages.js";import{deliveryFrom as f}from"./deliveries.js";async function s(e,t){const a=await e.prepare(`SELECT a.*, c.receiver_json, c.payload_json FROM objects a
2
2
  JOIN objects n ON n.id = ? AND n.type = 'node'
3
3
  JOIN custom_objects c ON c.object = a.id
4
4
  WHERE a.type = 'action' AND a.name = ? AND a.owner = n.owner AND a.deleted_at IS NULL
5
- ORDER BY a.seq DESC LIMIT 1`).get(a,`outcome/${a}`);if(!e)return null;const r=JSON.parse(e.receiver_json),o=await t.prepare("SELECT * FROM deliveries WHERE destination_id = ? ORDER BY created_at DESC, seq DESC LIMIT 1").get(e.id);return{object:e,actionName:r.name,argv:r.argv,cwd:r.cwd,configOrigin:r.config_origin,payload:e.payload_json===null?null:JSON.parse(e.payload_json),delivery:o?E(o):null,updatedAt:r.updated_at??Date.parse(e.created_at)}}i(s,"readOutcomeAction");async function g(t,a,e){const r=await l(t,a),o=await s(t,a);if(o?.delivery)throw new Error(`outcome action ${a} already has a delivery`);const n=o?.object??await u(t,{type:"action",kind:null,owner:r.owner,privacy:"public",name:`outcome/${a}`,preview:e.actionName}),c=JSON.stringify({argv:e.argv,cwd:e.cwd,config_origin:e.configOrigin,name:e.actionName,updated_at:o?Math.max(Date.now(),o.updatedAt+1):Date.parse(n.created_at)});await t.prepare(`INSERT INTO custom_objects (object, receiver_json, payload_json) VALUES (?, ?, ?)
6
- ON CONFLICT(object) DO UPDATE SET receiver_json = excluded.receiver_json, payload_json = excluded.payload_json`).run(n.id,c,JSON.stringify(e.payload??null)),o?await t.prepare("UPDATE objects SET preview = ? WHERE id = ?").run(e.actionName,n.id):(await t.prepare("UPDATE objects SET reception = 2 WHERE id = ?").run(n.id),await d(t,n,r,"spawn",{patterns:["outcome"],config:[{threshold:1,delivery:"after-turn"}]}))}i(g,"registerOutcomeAction");async function D(t,a){const e=await s(t,a);return e?e.delivery?!1:(await t.prepare("DELETE FROM objects WHERE id = ?").run(e.object.id),!0):!0}i(D,"unregisterOutcomeAction");async function S(t,a,e){const r=await s(t,a);if(!r||r.delivery)throw new Error(`outcome action ${a} cannot be delivered again`);const o=await l(t,a),n=v({senderNodeId:a,senderName:o.name,kind:"completed",label:`outcome node ${o.name}`,bodyJson:e,urgency:"normal"},null),c=await w(t,{...n,senderNodeId:a,senderName:o.name,kind:"completed",label:`outcome node ${o.name}`,disposition:null,event:{event:"outcome",objectId:a,level:1,actorId:a}});await m.enqueue(t,{messageId:c,event:"outcome",level:1,senderNodeId:a,elevatesTier:!1},[{id:r.object.id,reception:2,delivery:"after-turn"}])}i(S,"enqueueLateOutcomeAction");export{S as enqueueLateOutcomeAction,s as readOutcomeAction,g as registerOutcomeAction,D as unregisterOutcomeAction};
5
+ ORDER BY a.seq DESC LIMIT 1`).get(t,`outcome/${t}`);if(!a)return null;const r=JSON.parse(a.receiver_json),o=await e.prepare("SELECT * FROM deliveries WHERE destination_id = ? ORDER BY created_at DESC, seq DESC LIMIT 1").get(a.id);return{object:a,actionName:r.name,argv:r.argv,cwd:r.cwd,configOrigin:r.config_origin,payload:a.payload_json===null?null:JSON.parse(a.payload_json),delivery:o?f(o):null,updatedAt:r.updated_at??Date.parse(a.created_at)}}n(s,"readOutcomeAction");async function S(e,t,a){const r=await p(e,t),o=await s(e,t);if(o?.delivery)throw new Error(`outcome action ${t} already has a delivery`);const i=o?.object??await m(e,{type:"action",kind:null,owner:r.owner,privacy:"public",name:`outcome/${t}`,preview:a.actionName}),c=(await e.prepare("SELECT sub FROM nodes WHERE node_id = ?").get(t))?.sub;!o&&c&&await e.prepare("SELECT 1 FROM objects WHERE id = ? AND type = 'app'").get(r.owner)!==void 0&&await d(e,i.id,c);const l=JSON.stringify({argv:a.argv,cwd:a.cwd,config_origin:a.configOrigin,name:a.actionName,updated_at:o?Math.max(Date.now(),o.updatedAt+1):Date.parse(i.created_at)});await e.prepare(`INSERT INTO custom_objects (object, receiver_json, payload_json) VALUES (?, ?, ?)
6
+ ON CONFLICT(object) DO UPDATE SET receiver_json = excluded.receiver_json, payload_json = excluded.payload_json`).run(i.id,l,JSON.stringify(a.payload??null)),o?await e.prepare("UPDATE objects SET preview = ? WHERE id = ?").run(a.actionName,i.id):(await e.prepare("UPDATE objects SET reception = 2 WHERE id = ?").run(i.id),await E(e,i,r,"spawn",{patterns:["outcome"],config:[{threshold:1,delivery:"after-turn"}]}))}n(S,"registerOutcomeAction");async function T(e,t){const a=await s(e,t);return a?a.delivery?!1:(await e.prepare("DELETE FROM objects WHERE id = ?").run(a.object.id),!0):!0}n(T,"unregisterOutcomeAction");async function A(e,t,a){const r=await s(e,t);if(!r||r.delivery)throw new Error(`outcome action ${t} cannot be delivered again`);const o=await p(e,t),i=v({senderNodeId:t,senderName:o.name,kind:"completed",label:`outcome node ${o.name}`,bodyJson:a,urgency:"normal"},null),c=await y(e,{...i,senderNodeId:t,senderName:o.name,kind:"completed",label:`outcome node ${o.name}`,disposition:null,event:{event:"outcome",objectId:t,level:1,actorId:t}});await w.enqueue(e,{messageId:c,event:"outcome",level:1,senderNodeId:t,elevatesTier:!1},[{id:r.object.id,reception:2,delivery:"after-turn"}])}n(A,"enqueueLateOutcomeAction");export{A as enqueueLateOutcomeAction,s as readOutcomeAction,S as registerOutcomeAction,T as unregisterOutcomeAction};
@@ -1,18 +1,20 @@
1
- var j=Object.defineProperty;var s=(n,e)=>j(n,"name",{value:e,configurable:!0});import{randomBytes as k}from"node:crypto";import{CrtrError as N}from"../errors.js";import{readConfig as A}from"../config.js";import{joinedOwner as R}from"../directory/join.js";import{installLocalTerminalGrant as U}from"../grants/local.js";import{dropLocalGrantMirror as P}from"../grants/mirror.js";import{ownerForGrantee as C,ensureOwner as H}from"../graph/objects.js";import{canvasDatabaseUrl as x,CanvasConstraintError as W,withCanvasWrite as O}from"./db.js";import{LEGACY_LOCAL_PERSON as c,LEGACY_LOCAL_TERMINAL_GRANT as $}from"./legacy-person.js";const p="person_id",L="local_person_retired",M=/^user:[A-Za-z0-9._-]+$/,v="abcdefghijklmnopqrstuvwxyz0123456789";class T extends Error{static{s(this,"PersonUnresolvedError")}code="person_unresolved";constructor(){super("the install's person is not resolved yet"),this.name="PersonUnresolvedError"}}let E;const I=new Map;function F(n){E=n}s(F,"setPersonId");function ae(){if(E===void 0)throw new T;return E}s(ae,"personId");async function oe(){if(E!==void 0)return E;const n=await x(),e=I.get(n);if(e!==void 0)return e;const t=await G();if(t===null)throw new T;return I.set(n,t),t}s(oe,"personIdAsync");function b(){const n=k(12);let e="user:";for(let t=0;t<12;t++)e+=v[n[t]%v.length];return e}s(b,"mintPersonId");async function m(n,e){return(await n.prepare("SELECT value FROM canvas_meta WHERE key = ?").get(e))?.value??null}s(m,"readMeta");async function l(n,e,t){await n.prepare("INSERT INTO canvas_meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value").run(e,t)}s(l,"writeMeta");async function q(){return await O(async n=>{const e=await m(n,p);if(e!==null)return{id:e,source:"stored"};const t=R();if(t!==null)return await l(n,p,t),{id:t,source:"joined"};if(A("user").runtime?.issuer){const r=await n.prepare("SELECT DISTINCT sub FROM grants").all();return r.length!==1?null:(await l(n,p,r[0].sub),{id:r[0].sub,source:"grants"})}const a=b();return await l(n,p,a),{id:a,source:"minted"}})}s(q,"resolvePerson");async function G(){return(await q())?.id??null}s(G,"resolvePersonId");const o=s(n=>Number(n.changes),"changes"),J=[["nodes","sub"],["node_tokens","sub"],["grants","sub"],["crons","sub"],["audit_rows","sub"],["provider_approvals","sub"],["audit_rows","grantee"],["run_events","grantee"],["node_tokens","grantee"],["uploads","grantee"],["provider_receipts","grantee"],["provider_approvals","grantee"]];function D(n,e){if(!M.test(e))throw new N("invalid_person",`${n} must be a user:<id> principal, got ${JSON.stringify(e)}`)}s(D,"assertPerson");async function S(n,e,t){if(D("from",e),D("to",t),e===t)throw new N("invalid_person","cannot substitute a person with itself");const a={};try{a.identities=o(await n.prepare("UPDATE identities SET scheme = 'user', external_id = ? WHERE scheme IN ('local-user', 'user') AND external_id = ?").run(t,e))}catch(r){throw r instanceof W?new N("person_conflict",`another person object already holds ${t}; merging two person objects is not supported (${r.message})`,void 0,{from:e,to:t}):r}a["daemon_actions.principal_sub"]=o(await n.prepare(`UPDATE daemon_actions SET adapter_json = jsonb_set(adapter_json::jsonb, '{principal,sub}', to_jsonb(?::text))::text
1
+ var v=Object.defineProperty;var o=(e,n)=>v(e,"name",{value:n,configurable:!0});import{randomBytes as U}from"node:crypto";import{CrtrError as T}from"../errors.js";import{readConfig as D}from"../config.js";import{joinedOwner as f}from"../directory/join.js";import{installLocalTerminalGrant as P}from"../grants/local.js";import{dropLocalGrantMirror as k}from"../grants/mirror.js";import{ownerForGrantee as H,ensureOwner as W}from"../graph/objects.js";import{canvasDatabaseUrl as C,CanvasConstraintError as x,withCanvasWrite as S}from"./db.js";import{LEGACY_LOCAL_PERSON as p,LEGACY_LOCAL_TERMINAL_GRANT as b}from"./legacy-person.js";const E="person_id",h="local_person_retired",M=/^user:[A-Za-z0-9._-]+$/,O="abcdefghijklmnopqrstuvwxyz0123456789";class m extends Error{static{o(this,"PersonUnresolvedError")}code="person_unresolved";constructor(){super("the install's person is not resolved yet"),this.name="PersonUnresolvedError"}}let d;const j=new Map;function $(e){d=e}o($,"setPersonId");function ae(){if(d===void 0)throw new m;return d}o(ae,"personId");async function se(){if(d!==void 0)return d;const e=await C(),n=j.get(e);if(n!==void 0)return n;const t=await G();if(t===null)throw new m;return j.set(e,t),t}o(se,"personIdAsync");function F(){const e=U(12);let n="user:";for(let t=0;t<12;t++)n+=O[e[t]%O.length];return n}o(F,"mintPersonId");async function N(e,n){return(await e.prepare("SELECT value FROM canvas_meta WHERE key = ?").get(n))?.value??null}o(N,"readMeta");async function l(e,n,t){await e.prepare("INSERT INTO canvas_meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value").run(n,t)}o(l,"writeMeta");async function q(){return await S(async e=>{const n=await N(e,E);if(n!==null)return{id:n,source:"stored"};const t=f();if(t!==null)return await l(e,E,t),{id:t,source:"joined"};if(D("user").runtime?.issuer){const a=await e.prepare("SELECT DISTINCT sub FROM grants").all();return a.length!==1?null:(await l(e,E,a[0].sub),{id:a[0].sub,source:"grants"})}const r=F();return await l(e,E,r),{id:r,source:"minted"}})}o(q,"resolvePerson");async function G(){return(await q())?.id??null}o(G,"resolvePersonId");const s=o(e=>Number(e.changes),"changes"),J=[["nodes","sub"],["node_tokens","sub"],["grants","sub"],["crons","sub"],["audit_rows","sub"],["provider_approvals","sub"],["audit_rows","grantee"],["run_events","grantee"],["node_tokens","grantee"],["uploads","grantee"],["provider_receipts","grantee"],["provider_approvals","grantee"]];function I(e,n){if(!M.test(n))throw new T("invalid_person",`${e} must be a user:<id> principal, got ${JSON.stringify(n)}`)}o(I,"assertPerson");async function L(e,n,t){if(I("from",n),I("to",t),n===t)throw new T("invalid_person","cannot substitute a person with itself");const r={};try{r.identities=s(await e.prepare("UPDATE identities SET scheme = 'user', external_id = ? WHERE scheme IN ('local-user', 'user') AND external_id = ?").run(t,n))}catch(i){throw i instanceof x?new T("person_conflict",`another person object already holds ${t}; merging two person objects is not supported (${i.message})`,void 0,{from:n,to:t}):i}r["daemon_actions.principal_sub"]=s(await e.prepare(`UPDATE daemon_actions SET adapter_json = jsonb_set(adapter_json::jsonb, '{principal,sub}', to_jsonb(?::text))::text
2
2
  WHERE state IN ('pending', 'handed_off') AND adapter_json::jsonb ->> 'adapter' = 'cron_spawn'
3
- AND adapter_json::jsonb #>> '{principal,sub}' = ?`).run(t,e));for(const[r,i]of J)a[`${r}.${i}`]=o(await n.prepareUncached(`UPDATE ${r} SET ${i} = ? WHERE ${i} = ?`).run(t,e));return a["idempotency_keys.grantee"]=o(await n.prepare("DELETE FROM idempotency_keys WHERE grantee = ? AND key IN (SELECT key FROM idempotency_keys WHERE grantee = ?)").run(e,t))+o(await n.prepare("UPDATE idempotency_keys SET grantee = ? WHERE grantee = ?").run(t,e)),await n.prepare(`INSERT INTO grantee_usage (grantee, month, usd_equivalent)
3
+ AND adapter_json::jsonb #>> '{principal,sub}' = ?`).run(t,n));for(const[i,c]of J)r[`${i}.${c}`]=s(await e.prepareUncached(`UPDATE ${i} SET ${c} = ? WHERE ${c} = ?`).run(t,n));r["idempotency_keys.grantee"]=s(await e.prepare("DELETE FROM idempotency_keys WHERE grantee = ? AND key IN (SELECT key FROM idempotency_keys WHERE grantee = ?)").run(n,t))+s(await e.prepare("UPDATE idempotency_keys SET grantee = ? WHERE grantee = ?").run(t,n));const a=n.replace(/^user:/,""),u=t.replace(/^user:/,"");return r["relations.subject_id"]=s(await e.prepare(`DELETE FROM relations r WHERE r.subject_type = 'user' AND r.subject_id = ?
4
+ AND EXISTS (SELECT 1 FROM relations n WHERE n.subject_type = 'user' AND n.subject_id = ? AND n.resource_type = r.resource_type
5
+ AND n.resource_id = r.resource_id AND n.relation = r.relation AND n.subject_relation = r.subject_relation)`).run(a,u))+s(await e.prepare("UPDATE relations SET subject_id = ? WHERE subject_type = 'user' AND subject_id = ?").run(u,a)),r["relations.written_by"]=s(await e.prepare("UPDATE relations SET written_by = ? WHERE written_by = ?").run(t,n)),r["revisions.acting_for"]=s(await e.prepare("UPDATE revisions SET acting_for = ? WHERE acting_for = ?").run(t,n)),await e.prepare(`INSERT INTO grantee_usage (grantee, month, usd_equivalent)
4
6
  SELECT ?::text, month, usd_equivalent FROM grantee_usage WHERE grantee = ?
5
- ON CONFLICT(grantee, month) DO UPDATE SET usd_equivalent = grantee_usage.usd_equivalent + excluded.usd_equivalent`).run(t,e),a["grantee_usage.grantee"]=o(await n.prepare("DELETE FROM grantee_usage WHERE grantee = ?").run(e)),await n.prepare(`INSERT INTO usage (grantee, month, payer, source, served_model, input_tokens, output_tokens, cached_tokens, usd_equivalent)
7
+ ON CONFLICT(grantee, month) DO UPDATE SET usd_equivalent = grantee_usage.usd_equivalent + excluded.usd_equivalent`).run(t,n),r["grantee_usage.grantee"]=s(await e.prepare("DELETE FROM grantee_usage WHERE grantee = ?").run(n)),await e.prepare(`INSERT INTO usage (grantee, month, payer, source, served_model, input_tokens, output_tokens, cached_tokens, usd_equivalent)
6
8
  SELECT ?::text, month, payer, source, served_model, input_tokens, output_tokens, cached_tokens, usd_equivalent FROM usage WHERE grantee = ?
7
9
  ON CONFLICT(grantee, month, payer, source, served_model) DO UPDATE SET
8
10
  input_tokens = usage.input_tokens + excluded.input_tokens,
9
11
  output_tokens = usage.output_tokens + excluded.output_tokens,
10
12
  cached_tokens = usage.cached_tokens + excluded.cached_tokens,
11
13
  usd_equivalent = CASE WHEN usage.usd_equivalent IS NULL AND excluded.usd_equivalent IS NULL THEN NULL
12
- ELSE COALESCE(usage.usd_equivalent, 0) + COALESCE(excluded.usd_equivalent, 0) END`).run(t,e),a["usage.grantee"]=o(await n.prepare("DELETE FROM usage WHERE grantee = ?").run(e)),a}s(S,"substitutePerson");async function Y(n){const e=`SELECT o.id FROM objects o JOIN identities i ON i.object = o.owner JOIN nodes n ON n.node_id = o.id
13
- WHERE o.type = 'node' AND i.scheme = 'local-user' AND i.external_id = '${c}'`;return(await n.prepareUncached(`${e} LIMIT 1`).get()!==void 0?o(await n.prepareUncached(`UPDATE objects SET owner = ? WHERE id IN (${e})`).run((await C(n,"app:terminal")).id)):0)+o(await n.prepare("UPDATE node_tokens SET grantee = 'app:terminal' WHERE grantee = ?").run(c))+o(await n.prepare("UPDATE run_events SET grantee = 'app:terminal' WHERE grantee = ?").run(c))+o(await n.prepare("DELETE FROM idempotency_keys WHERE grantee = ? AND key IN (SELECT key FROM idempotency_keys WHERE grantee = 'app:terminal')").run(c))+o(await n.prepare("UPDATE idempotency_keys SET grantee = 'app:terminal' WHERE grantee = ?").run(c))}s(Y,"rewriteLegacyGrantees");async function z(n,e,t){const a=[$,`terminal:${e}`].filter(d=>d!==t),r=a.map(()=>"?").join(", "),i={};i["nodes.grant_id"]=o(await n.prepareUncached(`UPDATE nodes SET grant_id = ? WHERE grant_id IN (${r})`).run(t,...a)),i["node_tokens.grant"]=o(await n.prepareUncached(`UPDATE node_tokens SET "grant" = ? WHERE "grant" IN (${r})`).run(t,...a)),i["crons.grant_id"]=o(await n.prepareUncached(`UPDATE crons SET grant_id = ? WHERE grant_id IN (${r})`).run(t,...a));const u=t===null?"'null'::jsonb":"to_jsonb(?::text)";return i["daemon_actions.principal_grant_id"]=o(await n.prepareUncached(`UPDATE daemon_actions SET adapter_json = jsonb_set(adapter_json::jsonb, '{principal,grant_id}', ${u})::text
14
+ ELSE COALESCE(usage.usd_equivalent, 0) + COALESCE(excluded.usd_equivalent, 0) END`).run(t,n),r["usage.grantee"]=s(await e.prepare("DELETE FROM usage WHERE grantee = ?").run(n)),r}o(L,"substitutePerson");async function Y(e){const n=`SELECT o.id FROM objects o JOIN identities i ON i.object = o.owner JOIN nodes n ON n.node_id = o.id
15
+ WHERE o.type = 'node' AND i.scheme = 'local-user' AND i.external_id = '${p}'`;return(await e.prepareUncached(`${n} LIMIT 1`).get()!==void 0?s(await e.prepareUncached(`UPDATE objects SET owner = ? WHERE id IN (${n})`).run((await H(e,"app:terminal")).id)):0)+s(await e.prepare("UPDATE node_tokens SET grantee = 'app:terminal' WHERE grantee = ?").run(p))+s(await e.prepare("UPDATE run_events SET grantee = 'app:terminal' WHERE grantee = ?").run(p))+s(await e.prepare("DELETE FROM idempotency_keys WHERE grantee = ? AND key IN (SELECT key FROM idempotency_keys WHERE grantee = 'app:terminal')").run(p))+s(await e.prepare("UPDATE idempotency_keys SET grantee = 'app:terminal' WHERE grantee = ?").run(p))}o(Y,"rewriteLegacyGrantees");async function z(e,n,t){const r=[b,`terminal:${n}`].filter(c=>c!==t),a=r.map(()=>"?").join(", "),u={};u["nodes.grant_id"]=s(await e.prepareUncached(`UPDATE nodes SET grant_id = ? WHERE grant_id IN (${a})`).run(t,...r)),u["node_tokens.grant"]=s(await e.prepareUncached(`UPDATE node_tokens SET "grant" = ? WHERE "grant" IN (${a})`).run(t,...r)),u["crons.grant_id"]=s(await e.prepareUncached(`UPDATE crons SET grant_id = ? WHERE grant_id IN (${a})`).run(t,...r));const i=t===null?"'null'::jsonb":"to_jsonb(?::text)";return u["daemon_actions.principal_grant_id"]=s(await e.prepareUncached(`UPDATE daemon_actions SET adapter_json = jsonb_set(adapter_json::jsonb, '{principal,grant_id}', ${i})::text
14
16
  WHERE state IN ('pending', 'handed_off') AND adapter_json::jsonb ->> 'adapter' = 'cron_spawn'
15
- AND adapter_json::jsonb #>> '{principal,grant_id}' IN (${r})`).run(...t===null?[]:[t],...a)),i}s(z,"repointGrantReferences");async function se(){const n=await O(async e=>{const t={},a=s((w,g="")=>{for(const[h,y]of Object.entries(w))y!==0&&(t[g+h]=(t[g+h]??0)+y)},"add");let r=await m(e,p);if(r===null)throw new T;let i=null;const u=R();u!==null&&u!==r&&(a(await S(e,r,u),"join."),await l(e,p,u),i=r,r=u);const d=A("user").runtime?.issuer,_=await m(e,"install_id");if(_===null)throw new Error("retireLocalPerson: the install id is not stored");const f=await m(e,L)!==r;if(f){a({legacy_grantees:await Y(e)}),a(await S(e,c,r));const w=`SELECT o.id FROM objects o JOIN peer_stubs s ON s.object = o.id JOIN identities i ON i.object = o.owner
16
- WHERE o.type = 'node' AND o.reception = 2 AND s.envelope_json ->> 'app' IS NULL AND i.scheme = 'app' AND i.external_id = 'app:terminal'`;if(await e.prepareUncached(`${w} LIMIT 1`).get()!==void 0){const g=await H(e,"person","user",r,"user");a({repaired_stubs:o(await e.prepareUncached(`UPDATE objects SET owner = ? WHERE id IN (${w})`).run(g.id))})}a({"nodes.sub_null_terminal":o(await e.prepare(`UPDATE nodes SET sub = ? WHERE sub IS NULL AND node_id IN (
17
- SELECT o.id FROM objects o JOIN identities i ON i.object = o.owner
18
- WHERE o.type = 'node' AND i.scheme = 'app' AND i.external_id = 'app:terminal')`).run(r))}),a(await z(e,_,d?null:`terminal:${_}`)),d&&a({local_grant_mirror:await P(e)}),await l(e,L,r)}return d||await U(_,r),{person:r,joinedFrom:i,retired:f,columns:t}});return F(n.person),n}s(se,"retireLocalPerson");export{c as LEGACY_LOCAL_PERSON,T as PersonUnresolvedError,ae as personId,oe as personIdAsync,q as resolvePerson,G as resolvePersonId,se as retireLocalPerson,F as setPersonId,S as substitutePerson};
17
+ AND adapter_json::jsonb #>> '{principal,grant_id}' IN (${a})`).run(...t===null?[]:[t],...r)),u}o(z,"repointGrantReferences");async function oe(){const e=await S(async n=>{const t={},r=o((w,g="")=>{for(const[y,R]of Object.entries(w))R!==0&&(t[g+y]=(t[g+y]??0)+R)},"add");let a=await N(n,E);if(a===null)throw new m;let u=null;const i=f();i!==null&&i!==a&&(r(await L(n,a,i),"join."),await l(n,E,i),u=a,a=i);const c=D("user").runtime?.issuer,_=await N(n,"install_id");if(_===null)throw new Error("retireLocalPerson: the install id is not stored");const A=await N(n,h)!==a;if(A){r({legacy_grantees:await Y(n)}),r(await L(n,p,a));const w=`SELECT o.id FROM objects o JOIN peer_stubs s ON s.object = o.id JOIN identities i ON i.object = o.owner
18
+ WHERE o.type = 'node' AND o.reception = 2 AND s.envelope_json ->> 'app' IS NULL AND i.scheme = 'app' AND i.external_id = 'app:terminal'`;if(await n.prepareUncached(`${w} LIMIT 1`).get()!==void 0){const g=await W(n,"person","user",a,"user");r({repaired_stubs:s(await n.prepareUncached(`UPDATE objects SET owner = ? WHERE id IN (${w})`).run(g.id))})}r({"nodes.sub_null_terminal":s(await n.prepare(`UPDATE nodes SET sub = ? WHERE sub IS NULL AND node_id IN (
19
+ SELECT n.node_id FROM nodes n JOIN identities i ON i.object = n.app
20
+ WHERE i.scheme = 'app' AND i.external_id = 'app:terminal')`).run(a))}),r(await z(n,_,c?null:`terminal:${_}`)),c&&r({local_grant_mirror:await k(n)}),await l(n,h,a)}return c||await P(_,a),{person:a,joinedFrom:u,retired:A,columns:t}});return $(e.person),e}o(oe,"retireLocalPerson");export{p as LEGACY_LOCAL_PERSON,m as PersonUnresolvedError,ae as personId,se as personIdAsync,q as resolvePerson,G as resolvePersonId,oe as retireLocalPerson,$ as setPersonId,L as substitutePerson};
@@ -15,7 +15,7 @@ CREATE TABLE objects (
15
15
  type text NOT NULL,
16
16
  kind text,
17
17
  owner text REFERENCES objects(id),
18
- privacy text NOT NULL DEFAULT 'public' CHECK (privacy IN ('public','friends','private')),
18
+ privacy text NOT NULL DEFAULT 'public' CHECK (privacy IN ('public','private')),
19
19
  name text NOT NULL,
20
20
  preview text NOT NULL DEFAULT '',
21
21
  created_at text NOT NULL,
@@ -939,10 +939,53 @@ CREATE TABLE peer_ingested_destinations (
939
939
  ingested_at bigint NOT NULL
940
940
  );
941
941
 
942
- -- ---------------------------------------------------------------- view
942
+ -- ---------------------------------------------------------------- permissions (cycle 3)
943
+
944
+ -- One permission model: relation facts, app schemas, the columns the evaluator and owner tree need, the narrowed privacy
945
+ -- levels, `relation_source` (facts plus each live object's, and each reaped node's, owner presented as its `parent`) and `node_rows` with `grantee` read
946
+ -- from `nodes.app`. Added after schema version 1 shipped: `ensureSchema` runs this block alone against an older database
947
+ -- (idempotent; the version stays 1). The whole block is one multi-statement query, so it applies entirely or not at all.
948
+ -- permissions:begin
949
+ CREATE TABLE IF NOT EXISTS relations (
950
+ resource_type text NOT NULL,
951
+ resource_id text NOT NULL,
952
+ relation text NOT NULL,
953
+ subject_type text NOT NULL,
954
+ subject_id text NOT NULL,
955
+ subject_relation text NOT NULL DEFAULT '',
956
+ written_by text NOT NULL,
957
+ created_at text NOT NULL,
958
+ PRIMARY KEY (resource_type, resource_id, relation, subject_type, subject_id, subject_relation)
959
+ );
960
+ CREATE INDEX IF NOT EXISTS relations_subject ON relations(subject_type, subject_id, relation);
961
+ CREATE INDEX IF NOT EXISTS relations_resource ON relations(resource_id);
962
+
963
+ CREATE TABLE IF NOT EXISTS schemas (
964
+ namespace text PRIMARY KEY,
965
+ app text NOT NULL,
966
+ version bigint NOT NULL,
967
+ source text NOT NULL,
968
+ compiled_json text NOT NULL,
969
+ registered_at text NOT NULL
970
+ );
971
+
972
+ ALTER TABLE nodes ADD COLUMN IF NOT EXISTS app text;
973
+ ALTER TABLE objects ADD COLUMN IF NOT EXISTS schema_type text;
974
+ ALTER TABLE revisions ADD COLUMN IF NOT EXISTS acting_app text;
975
+ ALTER TABLE revisions ADD COLUMN IF NOT EXISTS acting_for text;
976
+ CREATE INDEX IF NOT EXISTS objects_owner_live ON objects(owner) WHERE deleted_at IS NULL;
977
+
978
+ UPDATE objects SET privacy = 'private' WHERE privacy = 'friends';
979
+ ALTER TABLE objects DROP CONSTRAINT IF EXISTS objects_privacy_check,
980
+ ADD CONSTRAINT objects_privacy_check CHECK (privacy IN ('public','private'));
981
+
982
+ CREATE OR REPLACE VIEW relation_source AS
983
+ SELECT resource_type, resource_id, relation, subject_type, subject_id, subject_relation, written_by FROM relations
984
+ UNION ALL
985
+ SELECT 'crtr/object', id, 'parent', 'crtr/object', owner, '', 'owner' FROM objects
986
+ WHERE owner IS NOT NULL AND (deleted_at IS NULL OR type = 'node');
943
987
 
944
- -- Column list is fixed at creation (n.* expands here): a later nodes column needs a delta that
945
- -- recreates this view.
988
+ DROP VIEW IF EXISTS node_rows;
946
989
  CREATE VIEW node_rows AS
947
990
  SELECT n.*,
948
991
  o.name,
@@ -970,10 +1013,11 @@ SELECT n.*,
970
1013
  f.provider_error_json AS fault_provider_error_json
971
1014
  FROM nodes n
972
1015
  JOIN objects o ON o.id = n.node_id
973
- LEFT JOIN identities oi ON oi.object = o.owner
1016
+ LEFT JOIN identities oi ON oi.object = COALESCE(n.app, o.owner)
974
1017
  LEFT JOIN executions e ON e.node_id = n.node_id AND e.state != 'exited'
975
1018
  LEFT JOIN sessions s ON s.node_id = n.node_id AND s.unbound_at IS NULL
976
1019
  LEFT JOIN node_faults f ON f.node_id = n.node_id AND f.active = 1;
1020
+ -- permissions:end
977
1021
 
978
1022
  -- ---------------------------------------------------------------- triggers
979
1023
 
@@ -1 +1 @@
1
- var s=Object.defineProperty;var i=(e,r)=>s(e,"name",{value:r,configurable:!0});import{openDb as p,withCanvasWrite as u}from"./canvas/db.js";import{createObject as E,ownerForGrantee as h}from"./graph/objects.js";let t=null;async function f(e){const r=new Date().toISOString(),a=await u(async n=>{const o=await n.prepare("SELECT object FROM environments WHERE machine_id = ?").get(e);if(o!==void 0)return await n.prepare("UPDATE environments SET last_seen_at = ? WHERE machine_id = ?").run(r,e),{environmentId:o.object,created:!1};const m=await h(n,"app:terminal"),c=await E(n,{type:"environment",kind:"machine",owner:m.id,privacy:"public",name:e,preview:"machine"});return await n.prepare("INSERT INTO environments (object, kind, machine_id, last_seen_at) VALUES (?, 'machine', ?, ?)").run(c.id,e,r),{environmentId:c.id,created:!0}});return t={environmentId:a.environmentId,machineId:e},a}i(f,"registerMachineEnvironment");async function _(e){t!==null&&await(await p()).prepare("UPDATE environments SET last_seen_at = ? WHERE machine_id = ?").run(e,t.machineId)}i(_,"touchMachineEnvironment");function I(){if(t===null)throw new Error("the machine environment is not registered yet");return t.environmentId}i(I,"machineEnvironmentId");export{I as machineEnvironmentId,f as registerMachineEnvironment,_ as touchMachineEnvironment};
1
+ var s=Object.defineProperty;var i=(e,r)=>s(e,"name",{value:r,configurable:!0});import{openDb as p,withCanvasWrite as w}from"./canvas/db.js";import{createObject as u,ownerForGrantee as E}from"./graph/objects.js";import{writeActsFor as h}from"./permissions/facts.js";import{personIdAsync as v}from"./canvas/person.js";let t=null;async function S(e){const r=new Date().toISOString(),o=await w(async n=>{const c=await n.prepare("SELECT object FROM environments WHERE machine_id = ?").get(e);if(c!==void 0)return await n.prepare("UPDATE environments SET last_seen_at = ? WHERE machine_id = ?").run(r,e),{environmentId:c.object,created:!1};const m=await E(n,"app:terminal"),a=await u(n,{type:"environment",kind:"machine",owner:m.id,privacy:"public",name:e,preview:"machine"});return await h(n,a.id,await v()),await n.prepare("INSERT INTO environments (object, kind, machine_id, last_seen_at) VALUES (?, 'machine', ?, ?)").run(a.id,e,r),{environmentId:a.id,created:!0}});return t={environmentId:o.environmentId,machineId:e},o}i(S,"registerMachineEnvironment");async function T(e){t!==null&&await(await p()).prepare("UPDATE environments SET last_seen_at = ? WHERE machine_id = ?").run(e,t.machineId)}i(T,"touchMachineEnvironment");function g(){if(t===null)throw new Error("the machine environment is not registered yet");return t.environmentId}i(g,"machineEnvironmentId");export{g as machineEnvironmentId,S as registerMachineEnvironment,T as touchMachineEnvironment};
@@ -1,4 +1,4 @@
1
- var O=Object.defineProperty;var i=(e,t)=>O(e,"name",{value:t,configurable:!0});import{cancelCronsOnWake as q,getNode as E}from"../canvas/index.js";import{runStructuredRequestEffects as g,settleStructuredRequestIn as R}from"../canvas/structured-requests.js";import{afterCommit as x,openDb as F,withCanvasWrite as k}from"../canvas/db.js";import{brokerRelayGeneration as M,recordBrokerRelease as W}from"../runtime/lifecycle.js";import{randomUUID as z}from"node:crypto";import{composeNodeOutcomeDocument as A}from"../runtime/outcome-document.js";import{closeNode as L}from"../runtime/close.js";import{put as H}from"../graph/bodies.js";import{writeDocument as j}from"../graph/documents.js";import{asRuntimeWriter as B}from"../graph/spaces/own-memory.js";import{publish as T}from"../graph/events.js";import{nodeObject as v}from"../graph/objects.js";import{readerForNode as J}from"../graph/reader.js";import{endMessageSubscriptions as C}from"../graph/watches.js";import{operationIdContext as D}from"../events/operation-id.js";class b extends Error{static{i(this,"StructuredRequestFinalizationError")}state;constructor(t){super(t==="expired"?"structured_request_expired":"structured_request_settled"),this.state=t}}class h extends Error{static{i(this,"FinalizationError")}code;report;constructor(t,r){super(t),this.code=t,this.report=r}}function Y(e){const t=i((r,n=2)=>String(r).padStart(n,"0"),"pad");return`${e.getUTCFullYear()}${t(e.getUTCMonth()+1)}${t(e.getUTCDate())}T${t(e.getUTCHours())}${t(e.getUTCMinutes())}${t(e.getUTCSeconds())}${t(e.getUTCMilliseconds(),3)}`}i(Y,"compactTs");function S(e,t=80){const r=e.split(`
1
+ var O=Object.defineProperty;var i=(e,t)=>O(e,"name",{value:t,configurable:!0});import{cancelCronsOnWake as q,getNode as E}from"../canvas/index.js";import{runStructuredRequestEffects as g,settleStructuredRequestIn as R}from"../canvas/structured-requests.js";import{afterCommit as x,openDb as F,withCanvasWrite as k}from"../canvas/db.js";import{brokerRelayGeneration as M,recordBrokerRelease as W}from"../runtime/lifecycle.js";import{randomUUID as z}from"node:crypto";import{composeNodeOutcomeDocument as A}from"../runtime/outcome-document.js";import{closeNode as L}from"../runtime/close.js";import{put as H}from"../graph/bodies.js";import{writeDocument as j}from"../graph/documents.js";import{asRuntimeWriter as B}from"../permissions/ceiling.js";import{publish as T}from"../graph/events.js";import{nodeObject as v}from"../graph/objects.js";import{readerForNode as J}from"../graph/reader.js";import{endMessageSubscriptions as C}from"../graph/watches.js";import{operationIdContext as D}from"../events/operation-id.js";class b extends Error{static{i(this,"StructuredRequestFinalizationError")}state;constructor(t){super(t==="expired"?"structured_request_expired":"structured_request_settled"),this.state=t}}class h extends Error{static{i(this,"FinalizationError")}code;report;constructor(t,r){super(t),this.code=t,this.report=r}}function Y(e){const t=i((r,n=2)=>String(r).padStart(n,"0"),"pad");return`${e.getUTCFullYear()}${t(e.getUTCMonth()+1)}${t(e.getUTCDate())}T${t(e.getUTCHours())}${t(e.getUTCMinutes())}${t(e.getUTCSeconds())}${t(e.getUTCMilliseconds(),3)}`}i(Y,"compactTs");function S(e,t=80){const r=e.split(`
2
2
  `)[0]??"";return r.length>t?`${r.slice(0,t-1)}\u2026`:r}i(S,"firstLine");function P(e){return e.kind==="final"?{event:"report.final"}:e.result!==void 0?{event:"report.result"}:e.kind==="urgent"?{event:"report.update",level:7}:{event:"report.update",level:e.deliveryTier==="deferred"?3:e.deliveryTier==="urgent"?7:5}}i(P,"eventFor");async function U(e,t,r){const n=r.from??t,o=`reports/${Y(new Date)}-${z().slice(-8)}-${r.kind}`,u=await v(e,t),c=await v(e,n),{object:d}=await j(e,B(await J(e,n,async()=>[])),{owner:u,kind:"report",name:o,preview:S(r.body)||r.kind,privacy:"public"},r.body),s=`${t}/${o}`,{event:p,level:m}=P(r);await T(e,{event:p,object:u,actor:c,...m===void 0?{}:{level:m},attrs:{},pointer:d.id,messageBody:{kind:r.kind,label:S(r.body),bodyRef:s,...r.disposition===void 0?{}:{disposition:r.disposition}}});const l=(await e.prepare(`SELECT d.destination_id FROM deliveries d JOIN messages m ON m.message_id = d.message_id
3
3
  JOIN objects receiver ON receiver.id = d.destination_id
4
4
  WHERE m.body_ref = ? AND receiver.type = 'node' ORDER BY d.destination_id`).all(s)).map(w=>w.destination_id);return r.kind==="final"?await C(e,u,"final"):r.result?.requestMessageId&&await C(e,u,{result:r.result.requestMessageId}),{name:o,ref:s,doc:d,createdAt:d.created_at,deliveredTo:l}}i(U,"writeReport");async function G(e,t){return k(async r=>{const n=await r.prepare("SELECT status, final_report FROM nodes WHERE node_id = ?").get(e);if(n===void 0)throw new h("unknown_node");const o=t.outcome;let u;if(o?.structured!==void 0){const a=await R(r,o.structured.requestId,o.structured.declined===void 0?{intended:"completed",result:o.structured.value}:{intended:"declined",declined:o.structured.declined});if(!a.changed||a.row===null||a.row.state!=="completed"&&a.row.state!=="declined")return{written:void 0,...a.row===null?{}:{structured:a.row},structuredRefused:!0};u=a.row}else{const a=await r.prepareUncached("SELECT request_id FROM structured_requests WHERE node_id = ? AND state = 'pending'").get(e);if(a!==void 0){const f=await R(r,a.request_id,{intended:"failed",failureReason:"finalized"});if(!f.changed||f.row===null||f.row.state!=="failed")return{written:void 0,...f.row===null?{}:{structured:f.row},structuredRefused:!0};u=f.row}}if(n.final_report!==null)throw new h("already_finalized",`${e}/${n.final_report}`);if(n.status!=="active"&&n.status!=="idle"&&n.status!=="done")throw new h("finalization_forbidden");const c=await U(r,e,t),d=await E(e),s=d?.kind!=="human",p=(d?.outcome_revision??0)+1,m=t.outcome?.kind??"result",l=t.outcome?.reason??"finalized",w=t.outcome?.detailJson??null,_=c.createdAt;if(s){const a=d===null?null:await A({node:{...d,status:"done",intent:"done",terminal_reason:l},outcome:{kind:m,reason:l,at:_,revision:p,detailJson:w},finalReport:c.name,...u?.state==="completed"?{structuredResult:u.result}:{}});await r.prepare("UPDATE nodes SET status = 'done', intent = 'done', terminal_reason = ?, final_report = ?, finalized_at = ?, outcome_kind = ?, outcome_reason = ?, outcome_at = ?, outcome_revision = outcome_revision + 1, outcome_detail_json = ? WHERE node_id = ? AND final_report IS NULL").run(l,c.name,_,m,l,_,w,e),a!==null&&await T(r,{event:"outcome",object:await v(r,e),actor:null,attrs:{revision:p},messageBody:a})}else await r.prepare("UPDATE nodes SET status = 'done', intent = 'done', terminal_reason = 'finalized', final_report = ?, finalized_at = ? WHERE node_id = ? AND final_report IS NULL").run(c.name,_,e);const N=M(e);return x(()=>W(e,N)),{written:c,...u===void 0?{}:{structured:u}}})}i(G,"commitCanonicalFinal");async function $(e,t){if(!e)throw new Error(`push: empty nodeId (kind=${t.kind}) \u2014 a report must belong to a node`);if(t.kind==="final"&&t.deliveryTier!==void 0)throw new Error("push final does not accept a delivery tier");if(t.kind!=="final"&&t.outcome!==void 0)throw new Error("push outcome is final-only");if(t.kind!=="update"&&t.result!==void 0)throw new Error("a one-off result is an update report");if(H(t.body),t.kind==="final"){const n=await G(e,t);if(n.structuredRefused===!0)throw n.structured!==void 0&&g(n.structured),new b(n.structured?.state??"failed");n.structured!==void 0&&t.outcome?.structured?.skipEffects!==!0&&g(n.structured);const{written:o}=n;if(await q(e),(await E(e))?.run_id===e){const c=await(await F()).prepare("SELECT node_id FROM nodes WHERE run_id = ? AND node_id != ? ORDER BY created DESC").all(e,e);for(const d of c){const s=await E(d.node_id);s&&s.status!=="done"&&s.status!=="canceled"&&s.status!=="dead"&&await L(d.node_id,{preserveEmpty:!0})}}return{report:o.ref,reportId:o.doc.id,deliveredTo:o.deliveredTo}}const r=await k(async n=>await U(n,e,t));return{report:r.ref,reportId:r.doc.id,deliveredTo:r.deliveredTo}}i($,"pushWithContext");function y(e,t){return D.current()===void 0?D.fresh(()=>$(e,t)):$(e,t)}i(y,"push");async function ce(e,t,r){return y(e,{kind:"update",body:t,...r})}i(ce,"pushUpdate");async function le(e,t,r){return y(e,{kind:"urgent",body:t,...r})}i(le,"pushUrgent");async function fe(e,t,r){return y(e,{kind:"final",body:t,...r})}i(fe,"pushFinal");export{h as FinalizationError,b as StructuredRequestFinalizationError,y as push,fe as pushFinal,ce as pushUpdate,le as pushUrgent};
@@ -101,7 +101,15 @@ export declare function hasSyncedGrants(source: string): Promise<boolean>;
101
101
  * grant exists until grant sync installs the directory's mirror. */
102
102
  export declare function dropLocalGrantMirror(db: CanvasDb): Promise<number>;
103
103
  /** The directory response is complete; commit it or keep the prior snapshot intact. */
104
- export declare function replaceGrants(grants: readonly GrantRecord[], syncedAt: string, source: string): Promise<void>;
104
+ /** A directory team the runtime's owner belongs to: `team` is `team:<id>`, `members` are `user:<id>`. */
105
+ export interface TeamRecord {
106
+ team: string;
107
+ members: string[];
108
+ }
109
+ /** The `teams` a grant sync answer carried; absent (an unjoined install, an older directory) reads as none. */
110
+ export declare function teamsOf(value: unknown): TeamRecord[];
111
+ /** Replace the mirror with `grants` and the `written_by = 'directory'` team `member` facts with `teams`, in one transaction. */
112
+ export declare function replaceGrants(grants: readonly GrantRecord[], syncedAt: string, source: string, teams?: readonly TeamRecord[]): Promise<void>;
105
113
  /** Bumped by every `replaceGrants`. A launch compiled against one generation
106
114
  * and started under another may hold mounts the new mirror no longer grants. */
107
115
  export declare function grantsGeneration(): Promise<number>;
@@ -1,4 +1,4 @@
1
- var y=Object.defineProperty;var t=(e,r)=>y(e,"name",{value:r,configurable:!0});import{createHash as _}from"node:crypto";import{parseScope as g}from"@north-light/identity";import{openDb as o,withCanvasWrite as S}from"../canvas/db.js";import{isUnsandboxedMachine as T}from"../runtime/unsandboxed.js";const v=["not_started","pending","ready","failed","unavailable"],m=["ultra","strong","medium","light"];function p(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(p,"entries");function O(e){if(!e||typeof e!="object"||Array.isArray(e))return null;const{state:r,message:a,next:i,step:n}=e;return typeof r!="string"||!v.includes(r)||a!==void 0&&typeof a!="string"||n!==void 0&&typeof n!="string"||typeof i!="string"||!i?null:{state:r,...a===void 0?{}:{message:a},next:i,...n===void 0?{}:{step:n}}}t(O,"dependencyOf");function E(e){if(!e||typeof e!="object"||Array.isArray(e))return{};const r={};for(const[a,i]of Object.entries(e)){const n=O(i);n&&(r[a]=n)}return r}t(E,"dependenciesOf");function w(e){if(!e||typeof e!="object"||Array.isArray(e))return null;const{base_url:r,rungs:a}=e;if(typeof r!="string"||!r||!a||typeof a!="object"||Array.isArray(a))return null;const i={};for(const n of m){const c=a[n];typeof c=="string"&&c&&(i[n]=c)}return Object.keys(i).length?{base_url:r.replace(/\/+$/,""),rungs:i}:null}t(w,"inferenceEndpointOf");function l(e){return{id:e.id,runtime:e.runtime,sub:e.sub,grantee:e.grantee,rev:e.rev,scopes:p(JSON.parse(e.scopes_json)),resolved_scopes:p(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:E(JSON.parse(e.dependencies_json)),run_notice_url:e.run_notice_url??null,inference_endpoint:e.inference_endpoint_json?w(JSON.parse(e.inference_endpoint_json)):null,synced_at:e.synced_at}}t(l,"record");function u(e){if(!e||e.status!=="active")return null;try{return N(l(e))}catch(r){if(r instanceof TypeError||r instanceof SyntaxError)return null;throw r}}t(u,"active");function f(e){return e.grantee!=="app:terminal"&&T()}t(f,"mirrorOnly");async function M(){return(await x()).filter(e=>!f(e))}t(M,"localGrants");async function x(){return(await(await o()).prepare("SELECT * FROM grants").all()).flatMap(e=>{try{return[l(e)]}catch(r){if(r instanceof TypeError||r instanceof SyntaxError)return[];throw r}})}t(x,"allGrants");function R(e){return e.expires_at!==null&&!(Date.parse(e.expires_at)>Date.now())}t(R,"grantExpired");function N(e){if(e.status!=="active"||R(e))return null;try{const r=e.resolved_scopes.map(({scope:a})=>g(a));return{...e,resolved:e.resolved_scopes.flatMap(({mode:a},i)=>a==="always"?[r[i]]:[])}}catch(r){if(r instanceof TypeError||r instanceof SyntaxError)return null;throw r}}t(N,"activeRecord");function d(e){return Array.isArray(e)?`[${e.map(d).join(",")}]`:e&&typeof e=="object"?`{${Object.keys(e).sort().filter(r=>e[r]!==void 0).map(r=>`${JSON.stringify(r)}:${d(e[r])}`).join(",")}}`:JSON.stringify(e)??"null"}t(d,"canonicalJson");function j(e){const r={rev:e.rev,resolved_scopes:e.resolved_scopes,spend_cap:e.spend_cap,payer:e.payer,dependencies:e.dependencies??{},...e.inference_endpoint?{inference_endpoint:e.inference_endpoint}:{}};return _("sha256").update(d(r)).digest("base64url").slice(0,22)}t(j,"grantVersion");async function k(e){return!!await(await o()).prepare("SELECT 1 FROM grants WHERE id = ?").get(e)}t(k,"hasGrant");async function H(e){return await(await o()).prepare("SELECT rev, status FROM grants WHERE id = ?").get(e)??null}t(H,"grantSyncState");async function D(e,r){return await(await o()).prepare("SELECT rev, status FROM grants WHERE grantee = ? AND sub = ?").get(e,r)??null}t(D,"grantSyncStateFor");async function A(e){const r=await(await o()).prepare("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(A,"grantRecord");async function W(e){return(e?(await A(e))?.payer:null)??"user"}t(W,"payerOfGrant");async function G(e){return u(await(await o()).prepare("SELECT * FROM grants WHERE id = ?").get(e))}t(G,"activeGrant");async function J(e,r){return u(await(await o()).prepare("SELECT * FROM grants WHERE grantee = ? AND sub = ?").get(e,r))}t(J,"grantFor");async function I(e){if(f({grantee:e}))return null;const r=await(await o()).prepare("SELECT * FROM grants WHERE grantee = ? AND status = 'active'").all(e);if(r.length>1)throw new Error(`multiple active grants for ${e}`);return u(r[0])}t(I,"activeGrantForApp");async function U(e){const r=await(await o()).prepare("SELECT run_notice_url AS url, sub, grantee FROM grants WHERE id = ? AND status = 'active'").get(e);return r&&r.url&&!f(r)?{url:r.url,sub:r.sub,grantee:r.grantee}:null}t(U,"runNoticeTarget");async function $(){const e=await(await o()).prepare("SELECT * FROM grants WHERE grantee = 'app:terminal' AND status = 'active'").all();if(e.length>1)throw new Error("multiple active terminal grants");return u(e[0])}t($,"terminalGrant");async function V(){return(await(await o()).prepare("SELECT value FROM runtime_state WHERE key = 'grants_source'").get())?.value??null}t(V,"grantsSource");async function P(e){return(await(await o()).prepare("SELECT value FROM runtime_state WHERE key = 'grants_source'").get())?.value===e}t(P,"hasSyncedGrants");async function X(e){return(await e.prepare("SELECT value FROM runtime_state WHERE key = 'grants_source'").get())?.value!=="local"?0:(await e.prepare("DELETE FROM runtime_state WHERE key = 'grants_source'").run(),Number((await e.prepare("DELETE FROM grants").run()).changes))}t(X,"dropLocalGrantMirror");async function q(e,r,a){const i=new Set;for(const n of e){if(i.has(n.id)||!n.id||!n.runtime||!n.sub||!n.grantee||!Number.isInteger(n.rev)||!["active","revoked","removed"].includes(n.status))throw new TypeError("invalid grant sync response");i.add(n.id),p(n.scopes),p(n.resolved_scopes)}await S(async n=>{await n.exec("DELETE FROM grants");const c=n.prepare(`INSERT INTO grants
1
+ var m=Object.defineProperty;var t=(e,r)=>m(e,"name",{value:r,configurable:!0});import{createHash as S}from"node:crypto";import{parseScope as T}from"@north-light/identity";import{openDb as c,withCanvasWrite as O}from"../canvas/db.js";import{isUnsandboxedMachine as v}from"../runtime/unsandboxed.js";import{replaceWrittenBy as w,userSubject as x}from"../permissions/facts.js";import{GROUP_TYPE as R}from"../permissions/builtin.js";const A=["not_started","pending","ready","failed","unavailable"],N=["ultra","strong","medium","light"];function p(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(p,"entries");function h(e){if(!e||typeof e!="object"||Array.isArray(e))return null;const{state:r,message:n,next:i,step:o}=e;return typeof r!="string"||!A.includes(r)||n!==void 0&&typeof n!="string"||o!==void 0&&typeof o!="string"||typeof i!="string"||!i?null:{state:r,...n===void 0?{}:{message:n},next:i,...o===void 0?{}:{step:o}}}t(h,"dependencyOf");function y(e){if(!e||typeof e!="object"||Array.isArray(e))return{};const r={};for(const[n,i]of Object.entries(e)){const o=h(i);o&&(r[n]=o)}return r}t(y,"dependenciesOf");function b(e){if(!e||typeof e!="object"||Array.isArray(e))return null;const{base_url:r,rungs:n}=e;if(typeof r!="string"||!r||!n||typeof n!="object"||Array.isArray(n))return null;const i={};for(const o of N){const a=n[o];typeof a=="string"&&a&&(i[o]=a)}return Object.keys(i).length?{base_url:r.replace(/\/+$/,""),rungs:i}:null}t(b,"inferenceEndpointOf");function f(e){return{id:e.id,runtime:e.runtime,sub:e.sub,grantee:e.grantee,rev:e.rev,scopes:p(JSON.parse(e.scopes_json)),resolved_scopes:p(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:y(JSON.parse(e.dependencies_json)),run_notice_url:e.run_notice_url??null,inference_endpoint:e.inference_endpoint_json?b(JSON.parse(e.inference_endpoint_json)):null,synced_at:e.synced_at}}t(f,"record");function u(e){if(!e||e.status!=="active")return null;try{return F(f(e))}catch(r){if(r instanceof TypeError||r instanceof SyntaxError)return null;throw r}}t(u,"active");function l(e){return e.grantee!=="app:terminal"&&v()}t(l,"mirrorOnly");async function I(){return(await L()).filter(e=>!l(e))}t(I,"localGrants");async function L(){return(await(await c()).prepare("SELECT * FROM grants").all()).flatMap(e=>{try{return[f(e)]}catch(r){if(r instanceof TypeError||r instanceof SyntaxError)return[];throw r}})}t(L,"allGrants");function j(e){return e.expires_at!==null&&!(Date.parse(e.expires_at)>Date.now())}t(j,"grantExpired");function F(e){if(e.status!=="active"||j(e))return null;try{const r=e.resolved_scopes.map(({scope:n})=>T(n));return{...e,resolved:e.resolved_scopes.flatMap(({mode:n},i)=>n==="always"?[r[i]]:[])}}catch(r){if(r instanceof TypeError||r instanceof SyntaxError)return null;throw r}}t(F,"activeRecord");function d(e){return Array.isArray(e)?`[${e.map(d).join(",")}]`:e&&typeof e=="object"?`{${Object.keys(e).sort().filter(r=>e[r]!==void 0).map(r=>`${JSON.stringify(r)}:${d(e[r])}`).join(",")}}`:JSON.stringify(e)??"null"}t(d,"canonicalJson");function U(e){const r={rev:e.rev,resolved_scopes:e.resolved_scopes,spend_cap:e.spend_cap,payer:e.payer,dependencies:e.dependencies??{},...e.inference_endpoint?{inference_endpoint:e.inference_endpoint}:{}};return S("sha256").update(d(r)).digest("base64url").slice(0,22)}t(U,"grantVersion");async function $(e){return!!await(await c()).prepare("SELECT 1 FROM grants WHERE id = ?").get(e)}t($,"hasGrant");async function P(e){return await(await c()).prepare("SELECT rev, status FROM grants WHERE id = ?").get(e)??null}t(P,"grantSyncState");async function V(e,r){return await(await c()).prepare("SELECT rev, status FROM grants WHERE grantee = ? AND sub = ?").get(e,r)??null}t(V,"grantSyncStateFor");async function M(e){const r=await(await c()).prepare("SELECT * FROM grants WHERE id = ?").get(e);if(!r)return null;try{return f(r)}catch(n){if(n instanceof TypeError||n instanceof SyntaxError)return null;throw n}}t(M,"grantRecord");async function B(e){return(e?(await M(e))?.payer:null)??"user"}t(B,"payerOfGrant");async function X(e){return u(await(await c()).prepare("SELECT * FROM grants WHERE id = ?").get(e))}t(X,"activeGrant");async function Y(e,r){return u(await(await c()).prepare("SELECT * FROM grants WHERE grantee = ? AND sub = ?").get(e,r))}t(Y,"grantFor");async function q(e){if(l({grantee:e}))return null;const r=await(await c()).prepare("SELECT * FROM grants WHERE grantee = ? AND status = 'active'").all(e);if(r.length>1)throw new Error(`multiple active grants for ${e}`);return u(r[0])}t(q,"activeGrantForApp");async function z(e){const r=await(await c()).prepare("SELECT run_notice_url AS url, sub, grantee FROM grants WHERE id = ? AND status = 'active'").get(e);return r&&r.url&&!l(r)?{url:r.url,sub:r.sub,grantee:r.grantee}:null}t(z,"runNoticeTarget");async function K(){const e=await(await c()).prepare("SELECT * FROM grants WHERE grantee = 'app:terminal' AND status = 'active'").all();if(e.length>1)throw new Error("multiple active terminal grants");return u(e[0])}t(K,"terminalGrant");async function Q(){return(await(await c()).prepare("SELECT value FROM runtime_state WHERE key = 'grants_source'").get())?.value??null}t(Q,"grantsSource");async function Z(e){return(await(await c()).prepare("SELECT value FROM runtime_state WHERE key = 'grants_source'").get())?.value===e}t(Z,"hasSyncedGrants");async function ee(e){return(await e.prepare("SELECT value FROM runtime_state WHERE key = 'grants_source'").get())?.value!=="local"?0:(await e.prepare("DELETE FROM runtime_state WHERE key = 'grants_source'").run(),Number((await e.prepare("DELETE FROM grants").run()).changes))}t(ee,"dropLocalGrantMirror");function re(e){if(e===void 0)return[];if(!Array.isArray(e))throw new TypeError("invalid grant sync response");return e.map(r=>{const{team:n,members:i}=r??{};if(typeof n!="string"||!/^team:\S+$/.test(n)||!Array.isArray(i)||i.some(o=>typeof o!="string"||!/^user:[^\s*]+$/.test(o)))throw new TypeError("invalid grant sync response");return{team:n,members:i}})}t(re,"teamsOf");async function te(e,r,n,i=[]){const o=new Set;for(const a of e){if(o.has(a.id)||!a.id||!a.runtime||!a.sub||!a.grantee||!Number.isInteger(a.rev)||!["active","revoked","removed"].includes(a.status))throw new TypeError("invalid grant sync response");o.add(a.id),p(a.scopes),p(a.resolved_scopes)}await O(async a=>{await a.exec("DELETE FROM grants");const E=a.prepare(`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, inference_endpoint_json, synced_at)
4
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);for(const s of e)await c.run(s.id,s.runtime,s.sub,s.grantee,s.rev,JSON.stringify(s.scopes),JSON.stringify(s.resolved_scopes),s.spend_cap,s.expires_at,s.status,s.revoked_at,s.approved_at,s.payer,JSON.stringify(E(s.dependencies)),s.run_notice_url??null,s.inference_endpoint?JSON.stringify(s.inference_endpoint):null,s.synced_at);await n.prepare("INSERT INTO runtime_state (key, value) VALUES ('grants_source', ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value").run(a),await n.prepare("INSERT INTO runtime_state (key, value) VALUES ('grants_generation', '1') ON CONFLICT(key) DO UPDATE SET value = CAST(CAST(runtime_state.value AS INTEGER) + 1 AS TEXT)").run()})}t(q,"replaceGrants");async function z(){const e=await(await o()).prepare("SELECT value FROM runtime_state WHERE key = 'grants_generation'").get();return e?Number(e.value):0}t(z,"grantsGeneration");export{G as activeGrant,I as activeGrantForApp,N as activeRecord,x as allGrants,E as dependenciesOf,X as dropLocalGrantMirror,R as grantExpired,J as grantFor,A as grantRecord,H as grantSyncState,D as grantSyncStateFor,j as grantVersion,z as grantsGeneration,V as grantsSource,k as hasGrant,P as hasSyncedGrants,w as inferenceEndpointOf,M as localGrants,f as mirrorOnly,W as payerOfGrant,q as replaceGrants,U as runNoticeTarget,$ as terminalGrant};
4
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);for(const s of e)await E.run(s.id,s.runtime,s.sub,s.grantee,s.rev,JSON.stringify(s.scopes),JSON.stringify(s.resolved_scopes),s.spend_cap,s.expires_at,s.status,s.revoked_at,s.approved_at,s.payer,JSON.stringify(y(s.dependencies)),s.run_notice_url??null,s.inference_endpoint?JSON.stringify(s.inference_endpoint):null,s.synced_at);await w(a,"directory",R,"member",i.flatMap(({team:s,members:_})=>_.map(g=>({resourceId:s,subject:x(g)})))),await a.prepare("INSERT INTO runtime_state (key, value) VALUES ('grants_source', ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value").run(n),await a.prepare("INSERT INTO runtime_state (key, value) VALUES ('grants_generation', '1') ON CONFLICT(key) DO UPDATE SET value = CAST(CAST(runtime_state.value AS INTEGER) + 1 AS TEXT)").run()})}t(te,"replaceGrants");async function ne(){const e=await(await c()).prepare("SELECT value FROM runtime_state WHERE key = 'grants_generation'").get();return e?Number(e.value):0}t(ne,"grantsGeneration");export{X as activeGrant,q as activeGrantForApp,F as activeRecord,L as allGrants,y as dependenciesOf,ee as dropLocalGrantMirror,j as grantExpired,Y as grantFor,M as grantRecord,P as grantSyncState,V as grantSyncStateFor,U as grantVersion,ne as grantsGeneration,Q as grantsSource,$ as hasGrant,Z as hasSyncedGrants,b as inferenceEndpointOf,I as localGrants,l as mirrorOnly,B as payerOfGrant,te as replaceGrants,z as runNoticeTarget,re as teamsOf,K as terminalGrant};
@@ -1 +1 @@
1
- var S=Object.defineProperty;var r=(e,n)=>S(e,"name",{value:n,configurable:!0});import{createPrivateKey as E,randomUUID as j,sign as x}from"node:crypto";import{readFileSync as $}from"node:fs";import{join as T}from"node:path";import{crtrHome as A}from"../canvas/paths.js";import{isUnsandboxedMachine as g}from"../runtime/unsandboxed.js";import{emitEvent as p}from"../events/emit.js";import{describeOutboundFailure as N,outboundFetch as v,outboundJson as h,outboundRefusal as k}from"../events/outbound.js";import{allGrants as O,dependenciesOf as P,inferenceEndpointOf as R,replaceGrants as G}from"./mirror.js";import{onGrantSync as J}from"./on-sync.js";import{PersonUnresolvedError as B,personId as D,resolvePerson as F,retireLocalPerson as M,setPersonId as U}from"../canvas/person.js";const ie=300;function C(e,n){const t=Math.floor(Date.now()/1e3),o=Buffer.from(JSON.stringify({alg:"ES256",typ:"JWT",kid:e.kid})).toString("base64url"),u=Buffer.from(JSON.stringify({iss:e.client_id,sub:e.client_id,aud:n,iat:t,exp:t+300,jti:j()})).toString("base64url"),i=`${o}.${u}`,s=x("sha256",Buffer.from(i),{key:E({key:e.jwk,format:"jwk"}),dsaEncoding:"ieee-p1363"});return`${i}.${s.toString("base64url")}`}r(C,"assertion");function y(){const e=JSON.parse($(T(A(),"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}r(y,"runtimeClient");function se(e=y()){return e.runtime.startsWith("runtime:")?e.runtime.slice(8):e.runtime}r(se,"runtimeId");function I(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.`}r(I,"machineRevokedMessage");class w extends Error{static{r(this,"MachineRevokedError")}constructor(n){super(I(n)),this.name="MachineRevokedError"}}async function L(e,n){const t=y(),o=`${e.replace(/\/$/,"")}/token`,u=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:C(t,o),...n===void 0?{}:{audience:n}}),i=await v("directory_token",o,{method:"POST",body:u,signal:AbortSignal.timeout(1e4)});if(!i.ok)throw i.status===401&&g()&&await W(i)?new w(e):k("directory_token",o,`directory token request failed: ${i.status}`);const s=await h("directory_token",o,i);if(typeof s.access_token!="string")throw new TypeError("directory token response has no access_token");return{access_token:s.access_token,expires_at:q(s.access_token,s.expires_in)}}r(L,"mintDirectoryToken");async function W(e){try{return(await e.json()).error==="invalid_client"}catch{return!1}}r(W,"isInvalidClient");function q(e,n){if(typeof n=="number"&&Number.isFinite(n)&&n>0)return Date.now()+n*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}r(q,"tokenExpiry");function z(e,n){if(!e||typeof e!="object")throw new TypeError("invalid grant sync response");const t=e,o=n.runtime.startsWith("runtime:")?n.runtime:`runtime:${n.runtime}`;if(!(t.runtime===o||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 s=e.revisions?.find(a=>a.rev===t.rev)?.approved_at;if(typeof s!="string")throw new TypeError("grant revision has no approval time");const l=t.payer==="app"?"app":"user";return{...t,spend_cap:t.spend_cap??null,expires_at:t.expires_at??null,revoked_at:t.revoked_at??null,approved_at:s,payer:l,dependencies:P(t.dependencies),inference_endpoint:l==="app"?R(t.inference_endpoint):null,run_notice_url:typeof t.run_notice_url=="string"&&t.run_notice_url!==""?t.run_notice_url:null}}r(z,"responseGrant");async function H(){try{D();return}catch(t){if(!(t instanceof B))throw t}const e=await F();if(e===null)return;U(e.id);const n=await M();p({level:"info",event:"daemon.person.resolved",fields:{person_id:e.id,source:e.source}}),p({level:"info",event:"daemon.person.retired",fields:{person_id:n.person,retired:n.retired,columns:n.columns}})}r(H,"resolveLatePerson");let _;function ae(e){return _?_(e):Promise.resolve()}r(ae,"requestGrantSync");function ce(e,n=t=>{}){let t;const o=e.replace(/\/$/,""),u=r(async()=>{const a=y(),c=await L(o),f=await v("grant_sync",`${o}/v1/runtime/grants`,{headers:{Authorization:`Bearer ${c.access_token}`},signal:AbortSignal.timeout(1e4)});if(!f.ok)throw k("grant_sync",`${o}/v1/runtime/grants`,`directory grant sync failed: ${f.status}`);const d=await h("grant_sync",`${o}/v1/runtime/grants`,f);if(!Array.isArray(d.grants)||typeof d.synced_at!="string")throw new TypeError("invalid grant sync response");const b=await O();await G(d.grants.map(m=>z(m,a)),d.synced_at,o),p({level:"info",event:"daemon.grants.synced",fields:{grants:d.grants.length,synced_at:d.synced_at}}),await H();try{await J(b,n)}catch(m){p({level:"error",event:"daemon.grants.on_sync_failed",error:m})}},"run");let i;const s=r(()=>{const a=u().catch(c=>{throw c instanceof w?p({level:"error",event:"daemon.grants.machine_revoked",error:c}):p({level:"error",event:"daemon.grants.sync_failed",error:c,fields:N(c)}),c}).finally(()=>{t===a&&(t=void 0)});return t=a,a},"start"),l=r((a={})=>t?a.fresh?(i??=t.catch(()=>{}).then(()=>(i=void 0,t??s())),i):t:s(),"sync");return _=l,l}r(ce,"createGrantSync");export{ie as GRANT_SYNC_INTERVAL_S,w as MachineRevokedError,C as assertion,ce as createGrantSync,I as machineRevokedMessage,L as mintDirectoryToken,ae as requestGrantSync,y as runtimeClient,se as runtimeId};
1
+ var S=Object.defineProperty;var r=(e,n)=>S(e,"name",{value:n,configurable:!0});import{createPrivateKey as E,randomUUID as j,sign as x}from"node:crypto";import{readFileSync as $}from"node:fs";import{join as T}from"node:path";import{crtrHome as A}from"../canvas/paths.js";import{isUnsandboxedMachine as g}from"../runtime/unsandboxed.js";import{emitEvent as p}from"../events/emit.js";import{describeOutboundFailure as O,outboundFetch as v,outboundJson as h,outboundRefusal as k}from"../events/outbound.js";import{allGrants as N,dependenciesOf as P,inferenceEndpointOf as R,replaceGrants as G,teamsOf as J}from"./mirror.js";import{onGrantSync as B}from"./on-sync.js";import{PersonUnresolvedError as D,personId as F,resolvePerson as M,retireLocalPerson as U,setPersonId as C}from"../canvas/person.js";const se=300;function I(e,n){const t=Math.floor(Date.now()/1e3),o=Buffer.from(JSON.stringify({alg:"ES256",typ:"JWT",kid:e.kid})).toString("base64url"),d=Buffer.from(JSON.stringify({iss:e.client_id,sub:e.client_id,aud:n,iat:t,exp:t+300,jti:j()})).toString("base64url"),i=`${o}.${d}`,s=x("sha256",Buffer.from(i),{key:E({key:e.jwk,format:"jwk"}),dsaEncoding:"ieee-p1363"});return`${i}.${s.toString("base64url")}`}r(I,"assertion");function y(){const e=JSON.parse($(T(A(),"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}r(y,"runtimeClient");function ae(e=y()){return e.runtime.startsWith("runtime:")?e.runtime.slice(8):e.runtime}r(ae,"runtimeId");function L(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.`}r(L,"machineRevokedMessage");class w extends Error{static{r(this,"MachineRevokedError")}constructor(n){super(L(n)),this.name="MachineRevokedError"}}async function W(e,n){const t=y(),o=`${e.replace(/\/$/,"")}/token`,d=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:I(t,o),...n===void 0?{}:{audience:n}}),i=await v("directory_token",o,{method:"POST",body:d,signal:AbortSignal.timeout(1e4)});if(!i.ok)throw i.status===401&&g()&&await q(i)?new w(e):k("directory_token",o,`directory token request failed: ${i.status}`);const s=await h("directory_token",o,i);if(typeof s.access_token!="string")throw new TypeError("directory token response has no access_token");return{access_token:s.access_token,expires_at:z(s.access_token,s.expires_in)}}r(W,"mintDirectoryToken");async function q(e){try{return(await e.json()).error==="invalid_client"}catch{return!1}}r(q,"isInvalidClient");function z(e,n){if(typeof n=="number"&&Number.isFinite(n)&&n>0)return Date.now()+n*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}r(z,"tokenExpiry");function H(e,n){if(!e||typeof e!="object")throw new TypeError("invalid grant sync response");const t=e,o=n.runtime.startsWith("runtime:")?n.runtime:`runtime:${n.runtime}`;if(!(t.runtime===o||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 s=e.revisions?.find(a=>a.rev===t.rev)?.approved_at;if(typeof s!="string")throw new TypeError("grant revision has no approval time");const l=t.payer==="app"?"app":"user";return{...t,spend_cap:t.spend_cap??null,expires_at:t.expires_at??null,revoked_at:t.revoked_at??null,approved_at:s,payer:l,dependencies:P(t.dependencies),inference_endpoint:l==="app"?R(t.inference_endpoint):null,run_notice_url:typeof t.run_notice_url=="string"&&t.run_notice_url!==""?t.run_notice_url:null}}r(H,"responseGrant");async function K(){try{F();return}catch(t){if(!(t instanceof D))throw t}const e=await M();if(e===null)return;C(e.id);const n=await U();p({level:"info",event:"daemon.person.resolved",fields:{person_id:e.id,source:e.source}}),p({level:"info",event:"daemon.person.retired",fields:{person_id:n.person,retired:n.retired,columns:n.columns}})}r(K,"resolveLatePerson");let _;function ce(e){return _?_(e):Promise.resolve()}r(ce,"requestGrantSync");function ue(e,n=t=>{}){let t;const o=e.replace(/\/$/,""),d=r(async()=>{const a=y(),c=await W(o),f=await v("grant_sync",`${o}/v1/runtime/grants`,{headers:{Authorization:`Bearer ${c.access_token}`},signal:AbortSignal.timeout(1e4)});if(!f.ok)throw k("grant_sync",`${o}/v1/runtime/grants`,`directory grant sync failed: ${f.status}`);const u=await h("grant_sync",`${o}/v1/runtime/grants`,f);if(!Array.isArray(u.grants)||typeof u.synced_at!="string")throw new TypeError("invalid grant sync response");const b=await N();await G(u.grants.map(m=>H(m,a)),u.synced_at,o,J(u.teams)),p({level:"info",event:"daemon.grants.synced",fields:{grants:u.grants.length,synced_at:u.synced_at}}),await K();try{await B(b,n)}catch(m){p({level:"error",event:"daemon.grants.on_sync_failed",error:m})}},"run");let i;const s=r(()=>{const a=d().catch(c=>{throw c instanceof w?p({level:"error",event:"daemon.grants.machine_revoked",error:c}):p({level:"error",event:"daemon.grants.sync_failed",error:c,fields:O(c)}),c}).finally(()=>{t===a&&(t=void 0)});return t=a,a},"start"),l=r((a={})=>t?a.fresh?(i??=t.catch(()=>{}).then(()=>(i=void 0,t??s())),i):t:s(),"sync");return _=l,l}r(ue,"createGrantSync");export{se as GRANT_SYNC_INTERVAL_S,w as MachineRevokedError,I as assertion,ue as createGrantSync,L as machineRevokedMessage,W as mintDirectoryToken,ce as requestGrantSync,y as runtimeClient,ae as runtimeId};
@@ -1,54 +1,13 @@
1
- import type { ObjectRow, Reader } from './types.js';
2
1
  import type { CanvasDb } from '../canvas/db.js';
3
- /** The object's owners, nearest first, ending at its space; empty when the object is a space.
4
- * CROSS JOIN pins the chain as the outer loop: left to the planner, it scans every
5
- * object and probes the chain, which made each read check cost milliseconds. */
6
- export declare function ownerChain(db: CanvasDb, object: ObjectRow): Promise<ObjectRow[]>;
2
+ import type { ObjectRow, Reader } from './types.js';
3
+ /** The top of the object's owner chain (the object itself when it has no owner): its app, person or repo. */
7
4
  export declare function spaceOf(db: CanvasDb, object: ObjectRow): Promise<ObjectRow>;
8
- /** A shipped document: one a package reconciler owns, read from its package file
9
- * (`documents.source_path` is set). Only the reconciler writes it. */
5
+ /** A shipped document: one a package reconciler owns, read from its package file (`documents.source_path` is set). Only the reconciler writes it. */
10
6
  export declare function isShipped(db: CanvasDb, object: ObjectRow): Promise<boolean>;
11
- /** The apps whose tools a run may use, `crtr` last. On a local runtime (v0) every
12
- * installed, enabled plugin's tools are usable by every run, and an installed
13
- * plugin is exactly an app holding live shipped documents. Hosted grants narrow
14
- * this per reader later; `mayUseTools` is the one question both ask. */
15
- export declare function toolApps(db: CanvasDb, reader: Reader): Promise<ObjectRow[]>;
16
- /** May `reader` use any of `app`'s tool groups? v0: every run may use every installed plugin's tools. */
17
- export declare function mayUseTools(_db: CanvasDb, _reader: Reader, app: ObjectRow): boolean;
18
- /** Whether `scopes` hold a provider-group scope `<provider>:<group>` of `app`: what a run needs to call that app's provider commands
19
- * (`requireGroup`). The provider id is the app's client id. */
20
- export declare function holdsProviderGroup(db: CanvasDb, scopes: readonly string[], app: ObjectRow): Promise<boolean>;
21
- /** Whether this reader may register, publish or watch as an object owned by `owner`. */
22
- export declare function mayActAs(db: CanvasDb, reader: Reader, owner: ObjectRow): Promise<boolean>;
23
- /** Run confinement for a node acting on a custom object of its app (Silas, 2026-09-30):
24
- * a node reaches an object registered in its own run, or when neither run is isolated.
25
- * An object the person or the app's server registered is reached by nodes outside
26
- * isolation only. Any other reader is unchanged. */
27
- export declare function customReachable(db: CanvasDb, reader: Reader, object: ObjectRow): Promise<boolean>;
28
- /** The `crtr:objects:*` targets an object belongs to, from its owner chain (rulings R5):
29
- * person space → `user`; under an app's profile → `profile:<id>`; a node of an app or a custom
30
- * object → `app:<x>`; any node (local, or a stub of one of the account's nodes) → `node`.
31
- * App documents and the profile itself stay under the memory scopes; a repo has no target.
32
- * The app's own `app:<x>` is left out: its own objects need no scope. */
33
- export declare function objectsTargets(db: CanvasDb, object: ObjectRow, appGrantee: string): Promise<string[]>;
34
- /** Whether a non-person reader's scopes hold `crtr:objects:<verb>:<t>` for a target t of `object`.
35
- * False for a person and for a revoked grant (`scopes === null`). `covers` is exact equality here. */
36
- export declare function holdsObjectsScope(db: CanvasDb, reader: Reader, verb: 'read' | 'watch' | 'link' | 'write', object: ObjectRow): Promise<boolean>;
37
- /** Whether the watch `watcher` → `object` that `app` made through `link` still authorizes delivering `object`'s events to `watcher` past the
38
- * watcher's read gate: `app` owns `object` (directly, or through its profile: `mayActAs`) and, when its `scopes` are known, they hold
39
- * `crtr:objects:link:<t>` for the watcher. Null `scopes` (a stub of a remote object: the home already checked the link) skip the scope. It grants
40
- * delivery of this one object's events only; nothing about reading it. */
41
- export declare function linkAuthorizes(db: CanvasDb, app: ObjectRow, scopes: readonly string[] | null, object: ObjectRow, watcher: ObjectRow): Promise<boolean>;
42
- /** Callers turn `false` into `not_found`. A person reader reads everything. An app's
43
- * shipped documents are read by any run that may use that app's tools. */
7
+ /** Callers turn `false` into `not_found`. */
44
8
  export declare function canRead(db: CanvasDb, reader: Reader, object: ObjectRow): Promise<boolean>;
45
- /** `canRead` for many readers of one object: the owner chain is walked once. */
46
- export declare function readCheck(db: CanvasDb, object: ObjectRow): Promise<(reader: Reader) => Promise<boolean>>;
47
- /** Always asked of the space, also for a person: a repo never owns a non-public
48
- * document, whoever asks. A shipped document is changed by nobody, its own app
49
- * included — only the package reconciler writes it, and it asks no reader. */
9
+ /** A shipped document is changed by nobody, its own app included: only the package reconciler writes it, and it asks no reader. A repo owns only
10
+ * public documents, so a non-public one under a repo is never changed, whoever asks. */
50
11
  export declare function canChange(db: CanvasDb, reader: Reader, object: ObjectRow): Promise<boolean>;
51
- /** The own-app memory scope a narrowed run lacks for `access` on a document, or null (see `missingOwnMemoryScope`). */
52
- export declare function missingMemoryScope(db: CanvasDb, reader: Reader, object: ObjectRow, access: 'read' | 'rw'): Promise<string | null>;
53
- /** The same for a document about to be created under `owner`. */
54
- export declare function missingMemoryScopeUnder(db: CanvasDb, reader: Reader, owner: ObjectRow, access: 'read' | 'rw'): Promise<string | null>;
12
+ /** Whether a non-person reader reaches `object` through `crtr:objects:<verb>:<target>`: the ceiling for that verb, then the schema. A person holds no scope. */
13
+ export declare function holdsObjectsScope(db: CanvasDb, reader: Reader, verb: 'read' | 'watch' | 'link' | 'write', object: ObjectRow): Promise<boolean>;