@north-light/cli 0.3.446 → 0.3.448

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 (71) hide show
  1. package/dist/api/contract.d.ts +1 -1
  2. package/dist/api/contract.js +1 -1
  3. package/dist/api/dto/nodes.d.ts +5 -0
  4. package/dist/api/dto/objects.d.ts +33 -2
  5. package/dist/api/error-codes.d.ts +15 -0
  6. package/dist/api/error-codes.js +1 -1
  7. package/dist/clients/attach/viewer.js +578 -577
  8. package/dist/commands/canvas/list.js +3 -3
  9. package/dist/commands/canvas/read.js +4 -4
  10. package/dist/commands/node/inspect.js +8 -8
  11. package/dist/commands/sys/sync-shared.js +1 -1
  12. package/dist/commands/sys/update.js +1 -1
  13. package/dist/core/canvas/migrations.js +65 -31
  14. package/dist/core/feed/inbox.js +1 -1
  15. package/dist/core/graph/events.js +8 -8
  16. package/dist/core/graph/kinds/node.js +5 -5
  17. package/dist/core/graph/objects.js +8 -8
  18. package/dist/core/graph/reader.js +3 -3
  19. package/dist/core/graph/receivers.d.ts +2 -1
  20. package/dist/core/graph/receivers.js +5 -5
  21. package/dist/core/graph/spaces/app.js +1 -1
  22. package/dist/core/graph/types.d.ts +9 -1
  23. package/dist/core/peer/client.d.ts +2 -0
  24. package/dist/core/peer/client.js +1 -1
  25. package/dist/core/peer/directory.js +1 -1
  26. package/dist/core/peer/home.d.ts +40 -0
  27. package/dist/core/peer/home.js +2 -0
  28. package/dist/core/peer/ingest.d.ts +26 -0
  29. package/dist/core/peer/ingest.js +1 -0
  30. package/dist/core/peer/names.d.ts +3 -0
  31. package/dist/core/peer/names.js +1 -0
  32. package/dist/core/peer/outbox-store.d.ts +70 -0
  33. package/dist/core/peer/outbox-store.js +4 -0
  34. package/dist/core/peer/receiver.d.ts +8 -0
  35. package/dist/core/peer/receiver.js +1 -0
  36. package/dist/core/peer/stubs.d.ts +53 -0
  37. package/dist/core/peer/stubs.js +1 -0
  38. package/dist/core/peer/version.d.ts +2 -2
  39. package/dist/core/peer/version.js +1 -1
  40. package/dist/core/runtime/broker/inbox.js +1 -1
  41. package/dist/core/scoped-state/settings.js +1 -1
  42. package/dist/daemon/api/app-listener.js +9 -9
  43. package/dist/daemon/api/handlers/messages.js +2 -2
  44. package/dist/daemon/api/handlers/nodes.js +1 -1
  45. package/dist/daemon/api/handlers/objects.d.ts +4 -0
  46. package/dist/daemon/api/handlers/objects.js +2 -2
  47. package/dist/daemon/api/principal.d.ts +4 -4
  48. package/dist/daemon/api/principal.js +1 -1
  49. package/dist/daemon/api/sync-notice.d.ts +17 -0
  50. package/dist/daemon/api/sync-notice.js +1 -0
  51. package/dist/daemon/crtrd.js +8 -8
  52. package/dist/daemon/peer/forward.d.ts +69 -0
  53. package/dist/daemon/peer/forward.js +1 -0
  54. package/dist/daemon/peer/handlers.d.ts +6 -1
  55. package/dist/daemon/peer/handlers.js +1 -1
  56. package/dist/daemon/peer/local-ingress.d.ts +8 -0
  57. package/dist/daemon/peer/local-ingress.js +1 -0
  58. package/dist/daemon/peer/outbox.d.ts +38 -0
  59. package/dist/daemon/peer/outbox.js +1 -0
  60. package/dist/daemon/peer/relay-client.d.ts +35 -0
  61. package/dist/daemon/peer/relay-client.js +1 -0
  62. package/dist/daemon/peer/relay-protocol.d.ts +52 -0
  63. package/dist/daemon/peer/relay-protocol.js +1 -0
  64. package/dist/daemon/peer/relay-service.d.ts +26 -0
  65. package/dist/daemon/peer/relay-service.js +1 -0
  66. package/dist/shared/generated-context.d.ts +8 -0
  67. package/dist/shared/generated-context.js +4 -4
  68. package/dist/shared/inbox-entry-body.js +5 -5
  69. package/package.json +2 -2
  70. package/packages/crouter-identity/package.json +1 -1
  71. package/runtime.lock.json +12 -12
@@ -1,3 +1,3 @@
1
- var N=Object.defineProperty;var s=(o,e)=>N(o,"name",{value:e,configurable:!0});import{prepared as f}from"../sqlite-statements.js";import{coveredByAny as w}from"../../shared/env.js";import{parseScope as $}from"@north-light/identity";import{activeGrant as R,grantFor as F}from"../grants/mirror.js";import{prepared as S}from"../canvas/db.js";import{ApiError as b}from"../../api/errors.js";import{readConfig as x}from"../config.js";import{SCOPE_VOCABULARY as E}from"../scopes.js";import{loadExactProfileManifest as A}from"../profiles/manifest.js";import{ensureOwner as g,getByRowid as C,nodeObject as L}from"./objects.js";function v(o){return{repos:o.map(e=>e.repo),checkouts:o}}s(v,"inRepos");function z(o,e,a){if(e.class==="anonymous")throw new Error("a publish token cannot read as a principal");if(e.class==="person")return{class:"person",person:j(o)};if(e.class==="app")return{class:"app",app:_(o,e.app),scopes:e.scopes};if(e.class==="machine")throw new Error("a machine token cannot read as a principal");if(e.class==="forwarded"){if(e.actor.kind==="person")return{class:"person",person:j(o)};const c=F(e.actor.app,e.sub);if(!c)throw new b(403,"app_not_granted",`${e.actor.app} holds no active grant for ${e.sub} on this machine`);const p=e.actor.node_scopes;return{class:"app",app:_(o,e.actor.app),place:e.actor.place,scopes:p===null?c.resolved:p.filter(i=>w(c.resolved,i))}}if(e.class==="cron"){const c=e.grant?R(e.grant)?.resolved??[]:E,p=e.scopes;return{class:"app",app:_(o,e.app),scopes:p===null?c:p.filter(i=>w(c,i)),place:{run:e.origin_run??"",isolated:e.isolated}}}const n=d(o),r=L(o,e.node),t=r.owner===null?null:n.object(r.owner);if(!t)throw new Error(`node ${e.node} has no owner`);const l=f(o,"SELECT cwd, profile_id FROM nodes WHERE node_id = ?").get(e.node);return{class:"node",node:r,app:t,scopes:y(e.app,e.sub,e.grant,e.scopes,n),cwd:l.cwd,...v(a(l.cwd)),profile:l.profile_id===null?null:n.profile(l.profile_id,t)}}s(z,"readerFor");function M(o,e,a,n=d(o),r=!1){const t=S(o,"SELECT o.* FROM objects o WHERE o.id = ? AND o.type = 'node'").get(e),l=S(o,`SELECT (SELECT external_id FROM identities WHERE object = ?) AS grantee,
2
- sub, grant_id, scopes_json, cwd, profile_id FROM nodes WHERE node_id = ?`).get(t?.owner??null,e);if(!t||!l)throw new Error(`no node ${e}`);const{grantee:c,sub:p,grant_id:i,scopes_json:u,cwd:O,profile_id:h}=l,m=t.owner===null?null:n.object(t.owner);if(!m)throw new Error(`node ${e} has no owner`);const H=u===null?null:JSON.parse(u);return{class:"node",node:t,app:m,scopes:y(c,p??"",i,H,n,r),cwd:O,...v(a(O)),profile:h===null?null:n.profile(h,m)}}s(M,"readerForNode");function K(o,e,a=()=>[],n=d(o)){if(e.type==="node")return M(o,e.id,a,n,!0);const r=e.owner===null?null:n.object(e.owner);if(!r)throw new Error(`watcher ${e.id} has no owner`);const l=(r.type==="app"?r:r.type==="profile"&&r.owner!==null?n.object(r.owner):null)??f(o,"SELECT o.* FROM identities i JOIN objects o ON o.rowid = i.object WHERE i.external_id = 'app:terminal' AND i.scheme = 'app'").get()??f(o,"SELECT * FROM objects WHERE type = 'person' AND name = 'user'").get();if(!l)throw new Error("no terminal app or person object");const c=f(o,"SELECT external_id FROM identities WHERE object = ? AND scheme = 'app'").get(l.rowid)?.external_id??"app:terminal",p=f(o,"SELECT resolved_scopes_json FROM grants WHERE grantee = ? AND status = 'active'").all(c),i=r.type==="person"?null:p.length===0?c==="app:terminal"&&!n.terminalHasIssuer()?E:null:p.length===1?JSON.parse(p[0].resolved_scopes_json).filter(u=>u.mode==="always").map(u=>$(u.scope)):null;return{class:"object",object:e,node:e,owner:r,app:l,scopes:i,repos:r.type==="repo"?[r]:[]}}s(K,"readerForObject");function d(o){return{grantById:R,grantFor:F,terminalHasIssuer:s(()=>!!x("user").runtime?.issuer,"terminalHasIssuer"),object:s(e=>C(o,e),"object"),profile:s((e,a)=>T(o,e,a),"profile")}}s(d,"liveLookups");function Q(o){const e=d(o),a=new Map,n=s((r,t)=>(a.has(r)||a.set(r,t()),a.get(r)),"memo");return{grantById:s(r=>n(`g\0${r}`,()=>e.grantById(r)),"grantById"),grantFor:s((r,t)=>n(`a\0${r}\0${t}`,()=>e.grantFor(r,t)),"grantFor"),terminalHasIssuer:s(()=>n("issuer",()=>e.terminalHasIssuer()),"terminalHasIssuer"),object:s(r=>n(`o\0${r}`,()=>e.object(r)),"object"),profile:s((r,t)=>n(`p\0${r}\0${t.rowid}`,()=>e.profile(r,t)),"profile")}}s(Q,"publishLookups");function y(o,e,a,n,r,t=!1){const l=a?r.grantById(a):r.grantFor(o,e),c=l&&l.grantee===o?l.resolved:o==="app:terminal"&&!r.terminalHasIssuer()?E:null;if(c===null){if(t)return null;throw new Error(`the grant of ${o} is not active`)}return n===null?c:n.filter(p=>w(c,p))}s(y,"nodeScopes");function j(o){return g(o,"person","local-user","user:local","user")}s(j,"personObject");function _(o,e){return e==="user:local"?j(o):g(o,"app","app",e,e.replace(/^app:/,""))}s(_,"appObject");function T(o,e,a){return f(o,`SELECT o.* FROM identities i JOIN objects o ON o.rowid = i.object
3
- WHERE i.scheme = 'profile' AND i.external_id = ?`).get(e)??g(o,"profile","profile",e,A(e).manifest.name,a)}s(T,"profileObject");export{d as liveLookups,T as profileObject,Q as publishLookups,z as readerFor,M as readerForNode,K as readerForObject};
1
+ var N=Object.defineProperty;var l=(o,e)=>N(o,"name",{value:e,configurable:!0});import{prepared as f}from"../sqlite-statements.js";import{coveredByAny as E}from"../../shared/env.js";import{parseScope as C}from"@north-light/identity";import{activeGrant as h,grantFor as v}from"../grants/mirror.js";import{prepared as F}from"../canvas/db.js";import{ApiError as L}from"../../api/errors.js";import{readConfig as $}from"../config.js";import{SCOPE_VOCABULARY as _}from"../scopes.js";import{loadExactProfileManifest as x}from"../profiles/manifest.js";import{ensureOwner as O,getByRowid as A,nodeObject as M}from"./objects.js";function S(o){return{repos:o.map(e=>e.repo),checkouts:o}}l(S,"inRepos");function K(o,e,s){if(e.class==="anonymous")throw new Error("a publish token cannot read as a principal");if(e.class==="person")return{class:"person",person:m(o)};if(e.class==="app")return{class:"app",app:g(o,e.app),scopes:e.scopes};if(e.class==="machine")throw new Error("a machine token cannot read as a principal");if(e.class==="forwarded"){if(e.actor.kind==="person")return{class:"person",person:m(o)};const a=v(e.actor.app,e.sub);if(!a)throw new L(403,"app_not_granted",`${e.actor.app} holds no active grant for ${e.sub} on this machine`);const c=e.actor.node_scopes;return{class:"app",app:g(o,e.actor.app),place:e.actor.place,scopes:c===null?a.resolved:c.filter(u=>E(a.resolved,u))}}if(e.class==="cron"){const a=e.grant?h(e.grant)?.resolved??[]:_,c=e.scopes;return{class:"app",app:g(o,e.app),scopes:c===null?a:c.filter(u=>E(a,u)),place:{run:e.origin_run??"",isolated:e.isolated}}}const t=w(o),r=M(o,e.node),n=r.owner===null?null:t.object(r.owner);if(!n)throw new Error(`node ${e.node} has no owner`);const p=f(o,"SELECT cwd, profile_id FROM nodes WHERE node_id = ?").get(e.node);return{class:"node",node:r,app:n,scopes:H(e.app,e.sub,e.grant,e.scopes,t),cwd:p.cwd,...S(s(p.cwd)),profile:p.profile_id===null?null:t.profile(p.profile_id,n)}}l(K,"readerFor");function T(o,e,s,t=w(o),r=!1){const n=F(o,"SELECT o.* FROM objects o WHERE o.id = ? AND o.type = 'node'").get(e),p=F(o,`SELECT (SELECT external_id FROM identities WHERE object = ?) AS grantee,
2
+ sub, grant_id, scopes_json, cwd, profile_id FROM nodes WHERE node_id = ?`).get(n?.owner??null,e);if(!n||!p)throw new Error(`no node ${e}`);const{grantee:a,sub:c,grant_id:u,scopes_json:d,cwd:i,profile_id:R}=p,j=n.owner===null?null:t.object(n.owner);if(!j)throw new Error(`node ${e} has no owner`);const b=d===null?null:JSON.parse(d);return{class:"node",node:n,app:j,scopes:H(a,c??"",u,b,t,r),cwd:i,...S(s(i)),profile:R===null?null:t.profile(R,j)}}l(T,"readerForNode");function Q(o,e,s=()=>[],t=w(o)){const r=f(o,"SELECT envelope_json FROM peer_stubs WHERE object = ?").get(e.rowid);if(r!==void 0)return W(o,e,r.envelope_json===null?null:JSON.parse(r.envelope_json),t);if(e.type==="node")return T(o,e.id,s,t,!0);const n=e.owner===null?null:t.object(e.owner);if(!n)throw new Error(`watcher ${e.id} has no owner`);const a=(n.type==="app"?n:n.type==="profile"&&n.owner!==null?t.object(n.owner):null)??f(o,"SELECT o.* FROM identities i JOIN objects o ON o.rowid = i.object WHERE i.external_id = 'app:terminal' AND i.scheme = 'app'").get()??f(o,"SELECT * FROM objects WHERE type = 'person' AND name = 'user'").get();if(!a)throw new Error("no terminal app or person object");const c=f(o,"SELECT external_id FROM identities WHERE object = ? AND scheme = 'app'").get(a.rowid)?.external_id??"app:terminal",u=f(o,"SELECT resolved_scopes_json FROM grants WHERE grantee = ? AND status = 'active'").all(c),d=n.type==="person"?null:u.length===0?c==="app:terminal"&&!t.terminalHasIssuer()?_:null:u.length===1?JSON.parse(u[0].resolved_scopes_json).filter(i=>i.mode==="always").map(i=>C(i.scope)):null;return{class:"object",object:e,node:e,owner:n,app:a,scopes:d,repos:n.type==="repo"?[n]:[]}}l(Q,"readerForObject");function W(o,e,s,t){const r=m(o);if(s!==null&&s.app===null)return{class:"object",object:e,node:e,owner:r,app:r,scopes:null,repos:[]};const n=s?.app==null?null:g(o,s.app),p=s?.app==null?null:t.grantFor(s.app,s.sub),a=s===null||p===null?null:s.node_scopes===null?p.resolved:s.node_scopes.filter(u=>E(p.resolved,u)),c=n??(e.owner===null?r:t.object(e.owner)??r);return{class:"object",object:e,node:e,owner:c,app:c,scopes:a,repos:[],...s?.place==null?{}:{place:s.place}}}l(W,"readerForStub");function w(o){return{grantById:h,grantFor:v,terminalHasIssuer:l(()=>!!$("user").runtime?.issuer,"terminalHasIssuer"),object:l(e=>A(o,e),"object"),profile:l((e,s)=>B(o,e,s),"profile")}}l(w,"liveLookups");function X(o){const e=w(o),s=new Map,t=l((r,n)=>(s.has(r)||s.set(r,n()),s.get(r)),"memo");return{grantById:l(r=>t(`g\0${r}`,()=>e.grantById(r)),"grantById"),grantFor:l((r,n)=>t(`a\0${r}\0${n}`,()=>e.grantFor(r,n)),"grantFor"),terminalHasIssuer:l(()=>t("issuer",()=>e.terminalHasIssuer()),"terminalHasIssuer"),object:l(r=>t(`o\0${r}`,()=>e.object(r)),"object"),profile:l((r,n)=>t(`p\0${r}\0${n.rowid}`,()=>e.profile(r,n)),"profile")}}l(X,"publishLookups");function H(o,e,s,t,r,n=!1){const p=s?r.grantById(s):r.grantFor(o,e),a=p&&p.grantee===o?p.resolved:o==="app:terminal"&&!r.terminalHasIssuer()?_:null;if(a===null){if(n)return null;throw new Error(`the grant of ${o} is not active`)}return t===null?a:t.filter(c=>E(a,c))}l(H,"nodeScopes");function m(o){return O(o,"person","local-user","user:local","user")}l(m,"personObject");function g(o,e){return e==="user:local"?m(o):O(o,"app","app",e,e.replace(/^app:/,""))}l(g,"appObject");function B(o,e,s){return f(o,`SELECT o.* FROM identities i JOIN objects o ON o.rowid = i.object
3
+ WHERE i.scheme = 'profile' AND i.external_id = ?`).get(e)??O(o,"profile","profile",e,x(e).manifest.name,s)}l(B,"profileObject");export{w as liveLookups,B as profileObject,X as publishLookups,K as readerFor,T as readerForNode,Q as readerForObject};
@@ -11,12 +11,13 @@ export interface ReceivedMessage {
11
11
  level: Level;
12
12
  senderNodeId: string | null;
13
13
  elevatesTier: boolean;
14
+ idempotencyKey?: string;
14
15
  }
15
16
  export interface Receiver {
16
17
  bodyOwner(destinations: readonly ReceiverDestination[]): string | null;
17
18
  enqueue(db: DatabaseSync, message: ReceivedMessage, destinations: readonly ReceiverDestination[]): void;
18
19
  }
19
- export type ReceiverName = 'inbox' | 'argv' | 'url' | 'queue';
20
+ export type ReceiverName = 'inbox' | 'argv' | 'url' | 'queue' | 'peer';
20
21
  /** The receiver is per instance, not per kind. An action's historical argv JSON is unchanged. */
21
22
  export declare function receiverForObject(db: DatabaseSync, object: ObjectRow): ReceiverName | undefined;
22
23
  export declare const BACKLOG_CAP = 1000;
@@ -1,7 +1,7 @@
1
- var f=Object.defineProperty;var i=(e,t)=>f(e,"name",{value:t,configurable:!0});import{prepared as o}from"../sqlite-statements.js";import{randomBytes as g}from"node:crypto";import{nodeInbox as _}from"./kinds/node.js";function L(e,t){const n=o(e,"SELECT receiver_json FROM custom_objects WHERE object = ?").get(t.rowid);if(!n?.receiver_json)return;const r=JSON.parse(n.receiver_json);if(r==="queue")return"queue";if(r&&typeof r=="object"&&!Array.isArray(r)){if("url"in r)return"url";if("argv"in r)return"argv"}}i(L,"receiverForObject");const E=1e3,a=new Map;function A(e,t){const n=a.get(e)??new Set;return n.add(t),a.set(e,n),()=>{n.delete(t),n.size===0&&a.delete(e)}}i(A,"onQueueSignal");function p(e){setImmediate(()=>{for(const t of a.get(e)??[])t()})}i(p,"signalQueue");function m(e,t){const n=o(e,`SELECT COUNT(*) AS n FROM deliveries WHERE destination_id = ?
2
- AND state = 'pending' AND canceled_at IS NULL`).get(t).n;if(n<=E)return;const r=o(e,`SELECT delivery_id, message_id FROM deliveries
1
+ var f=Object.defineProperty;var i=(e,t)=>f(e,"name",{value:t,configurable:!0});import{prepared as o}from"../sqlite-statements.js";import{randomBytes as p}from"node:crypto";import{nodeInbox as g}from"./kinds/node.js";import{peerReceiver as _}from"../peer/receiver.js";function N(e,t){const r=o(e,"SELECT receiver_json FROM custom_objects WHERE object = ?").get(t.rowid);if(!r?.receiver_json)return;const n=JSON.parse(r.receiver_json);if(n==="queue")return"queue";if(n&&typeof n=="object"&&!Array.isArray(n)){if("url"in n)return"url";if("argv"in n)return"argv"}}i(N,"receiverForObject");const E=1e3,a=new Map;function w(e,t){const r=a.get(e)??new Set;return r.add(t),a.set(e,r),()=>{r.delete(t),r.size===0&&a.delete(e)}}i(w,"onQueueSignal");function m(e){setImmediate(()=>{for(const t of a.get(e)??[])t()})}i(m,"signalQueue");function R(e,t){const r=o(e,`SELECT COUNT(*) AS n FROM deliveries WHERE destination_id = ?
2
+ AND state = 'pending' AND canceled_at IS NULL`).get(t).n;if(r<=E)return;const n=o(e,`SELECT delivery_id, message_id FROM deliveries
3
3
  WHERE destination_id = ? AND state = 'pending' AND canceled_at IS NULL
4
- ORDER BY created_at, rowid LIMIT ?`).all(t,n-E);for(const s of r)o(e,"DELETE FROM deliveries WHERE delivery_id = ?").run(s.delivery_id),o(e,"DELETE FROM messages WHERE message_id = ? AND NOT EXISTS (SELECT 1 FROM deliveries WHERE message_id = ?)").run(s.message_id,s.message_id);r.length&&o(e,`UPDATE custom_objects SET dropped = dropped + ? WHERE object =
5
- (SELECT rowid FROM objects WHERE id = ?)`).run(r.length,t)}i(m,"cap");function c(e,t){return{bodyOwner:i(()=>null,"bodyOwner"),enqueue(n,r,s){const d=Date.now(),l=o(n,`INSERT INTO deliveries
4
+ ORDER BY created_at, rowid LIMIT ?`).all(t,r-E);for(const s of n)o(e,"DELETE FROM deliveries WHERE delivery_id = ?").run(s.delivery_id),o(e,"DELETE FROM messages WHERE message_id = ? AND NOT EXISTS (SELECT 1 FROM deliveries WHERE message_id = ?)").run(s.message_id,s.message_id);n.length&&o(e,`UPDATE custom_objects SET dropped = dropped + ? WHERE object =
5
+ (SELECT rowid FROM objects WHERE id = ?)`).run(n.length,t)}i(R,"cap");function c(e,t){return{bodyOwner:i(()=>null,"bodyOwner"),enqueue(r,n,s){const d=Date.now(),l=o(r,`INSERT INTO deliveries
6
6
  (delivery_id, message_id, destination_id, urgency, wake, state, next_attempt_at, created_at, updated_at)
7
- VALUES (?, ?, ?, 'normal', 0, 'pending', ?, ?, ?)`);for(const u of s)l.run(g(16).toString("hex"),r.messageId,u.id,e?d:null,d,d),m(n,u.id),t&&p(u.id)}}}i(c,"pushReceiver");const R=c(!0,!1),N={inbox:_,argv:R,url:c(!0,!1),queue:c(!1,!0)};export{E as BACKLOG_CAP,N as RECEIVERS,R as argvReceiver,A as onQueueSignal,L as receiverForObject,p as signalQueue};
7
+ VALUES (?, ?, ?, 'normal', 0, 'pending', ?, ?, ?)`);for(const u of s)l.run(p(16).toString("hex"),n.messageId,u.id,e?d:null,d,d),R(r,u.id),t&&m(u.id)}}}i(c,"pushReceiver");const v=c(!0,!1),x={inbox:g,argv:v,url:c(!0,!1),queue:c(!1,!0),peer:_};export{E as BACKLOG_CAP,x as RECEIVERS,v as argvReceiver,w as onQueueSignal,N as receiverForObject,m as signalQueue};
@@ -1 +1 @@
1
- var g=Object.defineProperty;var s=(t,n)=>g(t,"name",{value:n,configurable:!0});import{covers as u}from"@north-light/identity";import{identityOf as c}from"./index.js";import{objectPlace as $,runPlace as p,runsSeeEachOther as a}from"../../runs/confinement.js";const j={identity:c,sync:"runtime",canRead(t,n,o,e,r){if(n.class==="person"||e.length===0)return!0;if(n.app.rowid===o.rowid)return m(t,n,e);const i=c(t,o).external_id,l=s(d=>n.scopes!==null&&n.scopes.some(w=>u(w,d)),"held");return e[0].type==="node"?l(`crtr:objects:read:${i}`)||r.type==="document"&&r.kind==="report"&&l(`crtr:conversations:read:${i}`):l(y(t,i,"read",e))},canChange(t,n,o,e){if(n.class==="person")return!0;if(n.app.rowid===o.rowid)return e.length===0||m(t,n,e);if(e.length===0||e[0].type==="node")return!1;const r=y(t,c(t,o).external_id,"rw",e);return n.scopes!==null&&n.scopes.some(i=>u(i,r))}},f=new WeakMap;function m(t,n,o){if(n.class==="person"||o.length===0||o[0].type!=="node"||n.class==="app"&&n.place===void 0)return!0;let e=f.get(n);if(e===void 0&&(e=n.class==="app"?n.place:n.class==="node"?p(n.node.id,t):$(n.object.rowid,t),f.set(n,e)),e===null)return!0;const r=p(o[0].id,t);return r===null?!e.isolated:a(e,r)}s(m,"runConfined");function y(t,n,o,e){const r=e[0];return r.type!=="profile"?`crtr:memory:${o}:${n}`:`crtr:memory:${o}:${n}:profile:${c(t,r).external_id}`}s(y,"memoryScope");export{j as appSpace};
1
+ var g=Object.defineProperty;var s=(t,n)=>g(t,"name",{value:n,configurable:!0});import{covers as u}from"@north-light/identity";import{identityOf as c}from"./index.js";import{objectPlace as $,runPlace as p,runsSeeEachOther as a}from"../../runs/confinement.js";const j={identity:c,sync:"runtime",canRead(t,n,o,e,r){if(n.class==="person"||e.length===0)return!0;if(n.app.rowid===o.rowid)return m(t,n,e);const i=c(t,o).external_id,l=s(w=>n.scopes!==null&&n.scopes.some(d=>u(d,w)),"held");return e[0].type==="node"?l(`crtr:objects:read:${i}`)||r.type==="document"&&r.kind==="report"&&l(`crtr:conversations:read:${i}`):l(y(t,i,"read",e))},canChange(t,n,o,e){if(n.class==="person")return!0;if(n.app.rowid===o.rowid)return e.length===0||m(t,n,e);if(e.length===0||e[0].type==="node")return!1;const r=y(t,c(t,o).external_id,"rw",e);return n.scopes!==null&&n.scopes.some(i=>u(i,r))}},f=new WeakMap;function m(t,n,o){if(n.class==="person"||o.length===0||o[0].type!=="node"||n.class==="app"&&n.place===void 0)return!0;let e=f.get(n);if(e===void 0&&(e=n.class==="app"?n.place:n.class==="node"?p(n.node.id,t):n.place??$(n.object.rowid,t),f.set(n,e)),e===null)return!0;const r=p(o[0].id,t);return r===null?!e.isolated:a(e,r)}s(m,"runConfined");function y(t,n,o,e){const r=e[0];return r.type!=="profile"?`crtr:memory:${o}:${n}`:`crtr:memory:${o}:${n}:profile:${c(t,r).external_id}`}s(y,"memoryScope");export{j as appSpace};
@@ -12,6 +12,8 @@ export interface ObjectRow {
12
12
  preview: string;
13
13
  created_at: string;
14
14
  deleted_at: string | null;
15
+ /** Set on a stub (a remote object's local representative): the machine (`runtime:<id>`) that owns the real object. */
16
+ home_machine?: string | null;
15
17
  }
16
18
  export type Level = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10;
17
19
  export type ListenDelivery = 'interrupt' | 'steer' | 'after-turn' | 'next-turn';
@@ -38,6 +40,8 @@ export interface CanvasEvent {
38
40
  /** The message whose handling caused this publish, named by the publisher. Its depth + 1 is this
39
41
  * event's depth and is bounded by HOP_LIMIT; omitted, the event starts a new chain at depth 0. */
40
42
  cause?: string;
43
+ /** Set by `peer.events` ingestion: each delivery this publish creates carries the unique key `<key>:<destination>`, so a replay adds nothing. */
44
+ idempotencyKey?: string;
41
45
  }
42
46
  /** A repo and the directory it is checked out at. */
43
47
  export interface Checkout {
@@ -69,7 +73,11 @@ export type Reader = {
69
73
  profile: ObjectRow | null;
70
74
  } | {
71
75
  class: 'object';
72
- object: ObjectRow; /** Identity alias for shared actor access; never a node principal. */
76
+ object: ObjectRow; /** A stub reader's saved run place (from its envelope); run-confines it within its own app. */
77
+ place?: {
78
+ run: string;
79
+ isolated: boolean;
80
+ }; /** Identity alias for shared actor access; never a node principal. */
73
81
  node: ObjectRow;
74
82
  app: ObjectRow;
75
83
  scopes: readonly string[] | null;
@@ -23,6 +23,8 @@ export interface PeerRequest {
23
23
  export interface PeerOptions {
24
24
  /** On whose behalf the call is made. Without one the call is the machine's own (class `machine` rows). */
25
25
  envelope?: Envelope;
26
+ /** Ends the call early (the caller's own budget ran out); the result is then `{unreachable: timeout}`. */
27
+ signal?: AbortSignal;
26
28
  }
27
29
  export interface PeerResponse {
28
30
  status: number;
@@ -1 +1 @@
1
- var S=Object.defineProperty;var i=(e,r)=>S(e,"name",{value:r,configurable:!0});import{crtrVersion as M}from"../../shared/crtr-version.js";import{directory as $,directoryIssuer as v,normalizeMachineId as T}from"./directory.js";import{ENVELOPE_HEADER as N,encodeEnvelope as O}from"./envelope.js";import{peerTokens as x}from"./token.js";import{PEER_VERSION_FLOOR as h,PEER_VERSION_HEADER as P,peerVersionRefusal as k}from"./version.js";const U=12e4,A=6e4,w={first:1e3,max:8e3},p=8*1024*1024,H=i(e=>"unreachable"in e,"isUnreachable"),s=i((e,r)=>({unreachable:{reason:e,message:r}}),"unreachable");function B(e){const r=e?.error;if(typeof r=="string")return r;const o=r?.code;return typeof o=="string"?o:null}i(B,"errorCode");async function I(e){if(Number(e.headers.get("content-length")??"0")>p)throw new RangeError("the peer answer is too large");const o=await e.text();if(o.length>p)throw new RangeError("the peer answer is too large");if(o==="")return null;try{return JSON.parse(o)}catch{return o}}i(I,"readBody");function L(e,r){const o=new URL(`${e.url}${r.path.startsWith("/")?"":"/"}${r.path}`),c=r.query instanceof URLSearchParams?r.query:Object.entries(r.query??{});for(const[l,u]of c instanceof URLSearchParams?c.entries():c)u!==void 0&&o.searchParams.append(l,String(u));return o.toString()}i(L,"targetUrl");function C(e){return i(async function(o,c,l={}){const u=e.issuer();if(u===null)return s("no_directory","this machine is not joined to a Northlight account, so it has no peers");let t;try{t=await e.directory.reach(o)}catch(a){return s("directory_unreachable",a instanceof Error?a.message:String(a))}if(t===null)return s("unknown_machine",`No machine ${T(o)} on your account.`);const d=t.crtr_version===null?null:k(t.crtr_version,h,e.version());if(d!==null)return{status:426,headers:{},body:{error:{code:d.code,message:`${t.name} runs crtr ${t.crtr_version}; peers below ${h} cannot take part. Update it with \`crtr sys update\`.`,details:{peer_version:t.crtr_version,floor:h,receiver_version:e.version()}}}};const R=L(t,c),_=e.now();let f=w.first,E=!1,g=!1;for(;;){let a;try{const n=await e.tokens.get(u,t.url);a=await e.fetch(R,{method:c.method,redirect:"manual",headers:{authorization:`Bearer ${n}`,[P]:e.version(),...l.envelope?{[N]:O(l.envelope)}:{},...c.body===void 0?{}:{"content-type":"application/json"}},...c.body===void 0?{}:{body:JSON.stringify(c.body)},signal:AbortSignal.timeout(g?Math.max(1e3,Math.min(e.timeoutMs,e.startingBudgetMs-(e.now()-_))):e.timeoutMs)})}catch(n){return n instanceof Error&&(n.name==="TimeoutError"||n.name==="AbortError")?s("timeout",`${t.name} did not answer within ${Math.round(e.timeoutMs/1e3)} s.`):n instanceof Error&&/directory/.test(n.message)?s("directory_unreachable",n.message):s("network",`${t.name} could not be reached: ${n instanceof Error?n.message:String(n)}`)}let m;try{m=await I(a)}catch(n){return s("network",`${t.name}'s answer could not be read: ${n instanceof Error?n.message:String(n)}`)}const y=B(m),b=Object.fromEntries(a.headers.entries());if(a.status===401&&!E){E=!0,e.tokens.drop(u,t.url);continue}if(a.status===503&&y==="machine_offline")return s("machine_offline",`${t.name} is offline.`);if(a.status===504)return s("timeout",`${t.name} did not answer in time.`);if(a.status===503&&y==="runtime_starting"){if(e.now()-_+f>e.startingBudgetMs)return s("runtime_starting",`${t.name} is still starting.`);await e.sleep(f),f=Math.min(f*2,w.max),g=!0;continue}return{status:a.status,headers:b,body:m}}},"peerCall")}i(C,"createPeerCall");const J=C({issuer:v,directory:$,tokens:x,fetch:i((e,r)=>fetch(e,r),"fetch"),sleep:i(e=>new Promise(r=>setTimeout(r,e)),"sleep"),now:Date.now,version:M,timeoutMs:U,startingBudgetMs:A});export{p as MAX_PEER_RESPONSE_BYTES,U as PEER_CALL_TIMEOUT_MS,w as RUNTIME_STARTING_BACKOFF_MS,A as RUNTIME_STARTING_RETRY_BUDGET_MS,C as createPeerCall,H as isUnreachable,J as peerCall};
1
+ var S=Object.defineProperty;var i=(e,r)=>S(e,"name",{value:r,configurable:!0});import{crtrVersion as M}from"../../shared/crtr-version.js";import{directory as $,directoryIssuer as v,normalizeMachineId as T}from"./directory.js";import{ENVELOPE_HEADER as N,encodeEnvelope as O}from"./envelope.js";import{peerTokens as x}from"./token.js";import{PEER_VERSION_FLOOR as h,PEER_VERSION_HEADER as P,peerVersionRefusal as k}from"./version.js";const A=12e4,U=6e4,w={first:1e3,max:8e3},R=8*1024*1024,H=i(e=>"unreachable"in e,"isUnreachable"),s=i((e,r)=>({unreachable:{reason:e,message:r}}),"unreachable");function B(e){const r=e?.error;if(typeof r=="string")return r;const o=r?.code;return typeof o=="string"?o:null}i(B,"errorCode");async function I(e){if(Number(e.headers.get("content-length")??"0")>R)throw new RangeError("the peer answer is too large");const o=await e.text();if(o.length>R)throw new RangeError("the peer answer is too large");if(o==="")return null;try{return JSON.parse(o)}catch{return o}}i(I,"readBody");function L(e,r){const o=new URL(`${e.url}${r.path.startsWith("/")?"":"/"}${r.path}`),c=r.query instanceof URLSearchParams?r.query:Object.entries(r.query??{});for(const[u,l]of c instanceof URLSearchParams?c.entries():c)l!==void 0&&o.searchParams.append(u,String(l));return o.toString()}i(L,"targetUrl");function C(e){return i(async function(o,c,u={}){const l=e.issuer();if(l===null)return s("no_directory","this machine is not joined to a Northlight account, so it has no peers");let t;try{t=await e.directory.reach(o)}catch(a){return s("directory_unreachable",a instanceof Error?a.message:String(a))}if(t===null)return s("unknown_machine",`No machine ${T(o)} on your account.`);const d=t.crtr_version===null?null:k(t.crtr_version,h,e.version());if(d!==null)return{status:426,headers:{},body:{error:{code:d.code,message:`${t.name} runs crtr ${t.crtr_version}; peers below ${h} cannot take part. Update it with \`crtr sys update\`.`,details:{peer_version:t.crtr_version,floor:h,receiver_version:e.version()}}}};const b=L(t,c),g=e.now();let f=w.first,_=!1,E=!1;for(;;){let a;try{const n=await e.tokens.get(l,t.url);a=await e.fetch(b,{method:c.method,redirect:"manual",headers:{authorization:`Bearer ${n}`,[P]:e.version(),...u.envelope?{[N]:O(u.envelope)}:{},...c.body===void 0?{}:{"content-type":"application/json"}},...c.body===void 0?{}:{body:JSON.stringify(c.body)},signal:AbortSignal.any([AbortSignal.timeout(E?Math.max(1e3,Math.min(e.timeoutMs,e.startingBudgetMs-(e.now()-g))):e.timeoutMs),...u.signal?[u.signal]:[]])})}catch(n){return n instanceof Error&&(n.name==="TimeoutError"||n.name==="AbortError")?s("timeout",`${t.name} did not answer within ${Math.round(e.timeoutMs/1e3)} s.`):n instanceof Error&&/directory/.test(n.message)?s("directory_unreachable",n.message):s("network",`${t.name} could not be reached: ${n instanceof Error?n.message:String(n)}`)}let m;try{m=await I(a)}catch(n){return s("network",`${t.name}'s answer could not be read: ${n instanceof Error?n.message:String(n)}`)}const y=B(m),p=Object.fromEntries(a.headers.entries());if(a.status===401&&!_){_=!0,e.tokens.drop(l,t.url);continue}if(a.status===503&&y==="machine_offline")return s("machine_offline",`${t.name} is offline.`);if(a.status===504)return s("timeout",`${t.name} did not answer in time.`);if(a.status===503&&y==="runtime_starting"){if(e.now()-g+f>e.startingBudgetMs)return s("runtime_starting",`${t.name} is still starting.`);await e.sleep(f),f=Math.min(f*2,w.max),E=!0;continue}return{status:a.status,headers:p,body:m}}},"peerCall")}i(C,"createPeerCall");const J=C({issuer:v,directory:$,tokens:x,fetch:i((e,r)=>fetch(e,r),"fetch"),sleep:i(e=>new Promise(r=>setTimeout(r,e)),"sleep"),now:Date.now,version:M,timeoutMs:A,startingBudgetMs:U});export{R as MAX_PEER_RESPONSE_BYTES,A as PEER_CALL_TIMEOUT_MS,w as RUNTIME_STARTING_BACKOFF_MS,U as RUNTIME_STARTING_RETRY_BUDGET_MS,C as createPeerCall,H as isUnreachable,J as peerCall};
@@ -1 +1 @@
1
- var x=Object.defineProperty;var n=(t,e)=>x(t,"name",{value:e,configurable:!0});import{prepared as y}from"../sqlite-statements.js";import{openDb as C,withCanvasWrite as k}from"../canvas/db.js";import{readConfig as v}from"../config.js";import{runtimeClient as p}from"../grants/sync.js";import{crtrVersion as M}from"../../shared/crtr-version.js";import{installedPluginIds as S}from"../directory/join.js";import{peerTokens as I}from"./token.js";const R=5*6e4,O=1e4;class c extends Error{static{n(this,"DirectoryError")}status;constructor(e,i){super(i),this.status=e,this.name="DirectoryError"}}const m=n(t=>t.startsWith("runtime:")?t:`runtime:${t}`,"normalizeMachineId");function _(t){if(!t||typeof t!="object")return null;const{kind:e,relay:i}=t;return e==="stable"?{kind:"stable"}:e==="relay"&&typeof i=="string"?{kind:"relay",relay:i}:null}n(_,"asReach");function g(t){const e=t,i=_(e?.reach);if(!e||typeof e.machine_id!="string"||typeof e.url!="string"||i===null)throw new c(502,"the directory answered a machine record with a missing machine_id, url or reach");return{machine_id:e.machine_id,name:typeof e.name=="string"?e.name:e.machine_id,url:e.url.replace(/\/$/,""),reach:i,crtr_version:typeof e.crtr_version=="string"?e.crtr_version:null,last_seen:typeof e.last_seen=="string"?e.last_seen:null,...typeof e.is_default=="boolean"?{is_default:e.is_default}:{}}}n(g,"asMachine");function U(t,e){const i=new Map;async function h(o,s,r){const a=t.issuer();if(a===null)throw new c(0,"this daemon has no directory");const f=a.replace(/\/$/,"");for(let w=0;;w++){let u;try{const l=await t.tokens.get(f);u=await t.fetch(`${f}${s}`,{method:o,headers:{authorization:`Bearer ${l}`,...r===void 0?{}:{"content-type":"application/json"}},...r===void 0?{}:{body:JSON.stringify(r)},signal:AbortSignal.timeout(O)})}catch(l){throw new c(0,`the directory could not be reached: ${l instanceof Error?l.message:String(l)}`)}if(u.status===401&&w===0){t.tokens.drop(f);continue}if(!u.ok)throw new c(u.status,`directory ${o} ${s} answered ${u.status}`);try{return await u.json()}catch{throw new c(502,`directory ${o} ${s} answered a body that is not JSON`)}}}return n(h,"call"),{async list(){const o=await h("GET","/v1/machines");if(!Array.isArray(o))throw new c(502,"the directory answered a machine list that is not an array");const s=o.map(g),r=t.now();for(const a of s)i.set(a.machine_id,{machine:a,at:r});return s},async reach(o){const s=m(o),r=i.get(s);if(r&&t.now()-r.at<R)return r.machine;try{const a=g(await h("GET",`/v1/machines/${encodeURIComponent(s)}/reach`));return i.set(s,{machine:a,at:t.now()}),a}catch(a){if(a instanceof c&&a.status===404)return i.delete(s),null;if(r&&a instanceof c&&a.status===0)return r.machine;throw a}},async status(o){const s=e(),r=await h("PUT",`/v1/machines/${encodeURIComponent(s)}/status`,o);if(!r||typeof r.machine_id!="string"||typeof r.url!="string")throw new c(502,"the directory answered a status record with a missing machine_id or url");return{machine_id:r.machine_id,url:r.url.replace(/\/$/,""),reach:_(r.reach)}},invalidate(o){o===void 0?i.clear():i.delete(m(o))}}}n(U,"createDirectoryClient");function E(){return m(p().runtime)}n(E,"ownMachineId");function T(){return v("user").runtime?.issuer??null}n(T,"directoryIssuer");const d=U({issuer:T,tokens:I,fetch:n((t,e)=>fetch(t,e),"fetch"),now:Date.now},E),z=n(()=>d.list(),"listMachines"),F=n(t=>d.reach(t),"machineReach"),$="machine_url";function G(){return y(C(),"SELECT value FROM runtime_state WHERE key = ?").get($)?.value??null}n(G,"machineUrl");function b(t){k(e=>{y(e,"INSERT INTO runtime_state (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value").run($,t)})}n(b,"storeMachineUrl");async function J(t=d){if(T()===null)return null;try{p()}catch{return null}const e=await t.status({plugins:S(),crtr_version:M()});if(e.machine_id!==E())throw new c(502,"the directory answered a status for another machine");return b(e.url),e}n(J,"reportMachineStatus");export{O as DIRECTORY_CALL_TIMEOUT_MS,c as DirectoryError,R as REACH_TTL_MS,U as createDirectoryClient,d as directory,T as directoryIssuer,z as listMachines,F as machineReach,G as machineUrl,m as normalizeMachineId,E as ownMachineId,J as reportMachineStatus};
1
+ var C=Object.defineProperty;var i=(t,e)=>C(t,"name",{value:e,configurable:!0});import{rememberMachineName as y}from"./names.js";import{prepared as p}from"../sqlite-statements.js";import{openDb as M,withCanvasWrite as k}from"../canvas/db.js";import{readConfig as v}from"../config.js";import{runtimeClient as _}from"../grants/sync.js";import{crtrVersion as S}from"../../shared/crtr-version.js";import{installedPluginIds as I}from"../directory/join.js";import{peerTokens as R}from"./token.js";const b=5*6e4,O=1e4;class c extends Error{static{i(this,"DirectoryError")}status;constructor(e,a){super(a),this.status=e,this.name="DirectoryError"}}const f=i(t=>t.startsWith("runtime:")?t:`runtime:${t}`,"normalizeMachineId");function g(t){if(!t||typeof t!="object")return null;const{kind:e,relay:a}=t;return e==="stable"?{kind:"stable"}:e==="relay"&&typeof a=="string"?{kind:"relay",relay:a}:null}i(g,"asReach");function E(t){const e=t,a=g(e?.reach);if(!e||typeof e.machine_id!="string"||typeof e.url!="string"||a===null)throw new c(502,"the directory answered a machine record with a missing machine_id, url or reach");return{machine_id:e.machine_id,name:typeof e.name=="string"?e.name:e.machine_id,url:e.url.replace(/\/$/,""),reach:a,crtr_version:typeof e.crtr_version=="string"?e.crtr_version:null,last_seen:typeof e.last_seen=="string"?e.last_seen:null,...typeof e.is_default=="boolean"?{is_default:e.is_default}:{}}}i(E,"asMachine");function U(t,e){const a=new Map;async function h(o,s,r){const n=t.issuer();if(n===null)throw new c(0,"this daemon has no directory");const m=n.replace(/\/$/,"");for(let w=0;;w++){let u;try{const l=await t.tokens.get(m);u=await t.fetch(`${m}${s}`,{method:o,headers:{authorization:`Bearer ${l}`,...r===void 0?{}:{"content-type":"application/json"}},...r===void 0?{}:{body:JSON.stringify(r)},signal:AbortSignal.timeout(O)})}catch(l){throw new c(0,`the directory could not be reached: ${l instanceof Error?l.message:String(l)}`)}if(u.status===401&&w===0){t.tokens.drop(m);continue}if(!u.ok)throw new c(u.status,`directory ${o} ${s} answered ${u.status}`);try{return await u.json()}catch{throw new c(502,`directory ${o} ${s} answered a body that is not JSON`)}}}return i(h,"call"),{async list(){const o=await h("GET","/v1/machines");if(!Array.isArray(o))throw new c(502,"the directory answered a machine list that is not an array");const s=o.map(E),r=t.now();for(const n of s)a.set(n.machine_id,{machine:n,at:r}),y(n.machine_id,n.name);return s},async reach(o){const s=f(o),r=a.get(s);if(r&&t.now()-r.at<b)return r.machine;try{const n=E(await h("GET",`/v1/machines/${encodeURIComponent(s)}/reach`));return a.set(s,{machine:n,at:t.now()}),y(n.machine_id,n.name),n}catch(n){if(n instanceof c&&n.status===404)return a.delete(s),null;if(r&&n instanceof c&&n.status===0)return r.machine;throw n}},async status(o){const s=e(),r=await h("PUT",`/v1/machines/${encodeURIComponent(s)}/status`,o);if(!r||typeof r.machine_id!="string"||typeof r.url!="string")throw new c(502,"the directory answered a status record with a missing machine_id or url");return{machine_id:r.machine_id,url:r.url.replace(/\/$/,""),reach:g(r.reach)}},invalidate(o){o===void 0?a.clear():a.delete(f(o))}}}i(U,"createDirectoryClient");function T(){return f(_().runtime)}i(T,"ownMachineId");function $(){return v("user").runtime?.issuer??null}i($,"directoryIssuer");const d=U({issuer:$,tokens:R,fetch:i((t,e)=>fetch(t,e),"fetch"),now:Date.now},T),G=i(()=>d.list(),"listMachines"),J=i(t=>d.reach(t),"machineReach"),x="machine_url";function V(){return p(M(),"SELECT value FROM runtime_state WHERE key = ?").get(x)?.value??null}i(V,"machineUrl");function A(t){k(e=>{p(e,"INSERT INTO runtime_state (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value").run(x,t)})}i(A,"storeMachineUrl");async function Y(t=d){if($()===null)return null;try{_()}catch{return null}const e=await t.status({plugins:I(),crtr_version:S()});if(e.machine_id!==T())throw new c(502,"the directory answered a status for another machine");return A(e.url),e}i(Y,"reportMachineStatus");export{O as DIRECTORY_CALL_TIMEOUT_MS,c as DirectoryError,b as REACH_TTL_MS,U as createDirectoryClient,d as directory,$ as directoryIssuer,G as listMachines,J as machineReach,V as machineUrl,f as normalizeMachineId,T as ownMachineId,Y as reportMachineStatus};
@@ -0,0 +1,40 @@
1
+ import type { DatabaseSync } from 'node:sqlite';
2
+ import { type PeerRequest, type PeerResult } from './client.js';
3
+ import { type Machine } from './directory.js';
4
+ export type HomeAnswer = {
5
+ where: 'here';
6
+ } | {
7
+ where: 'machine';
8
+ machine: string; /** How it was learned: a stub, the request's hint, the cache, or a fresh `peer.locate`. */
9
+ source: 'stub' | 'hint' | 'cache' | 'locate';
10
+ }
11
+ /** No machine answered `here`, and `unreachable` names the ones that could not answer. Never "missing". */
12
+ | {
13
+ where: 'unknown';
14
+ unreachable: string[];
15
+ }
16
+ /** Every other machine answered `no`. The only answer from which a caller may say "missing". */
17
+ | {
18
+ where: 'nowhere';
19
+ };
20
+ export interface HomeDeps {
21
+ call(machine: string, request: PeerRequest): Promise<PeerResult>;
22
+ machines(): Promise<Machine[]>;
23
+ self(): string;
24
+ now(): number;
25
+ /** Whether this daemon belongs to an account at all; an unjoined daemon has no peers, so nothing is ever "elsewhere". */
26
+ joined(): boolean;
27
+ }
28
+ export declare const defaultHomeDeps: HomeDeps;
29
+ export declare function cachedLocation(db: DatabaseSync, id: string): string | null;
30
+ export declare function rememberLocation(db: DatabaseSync, id: string, machine: string, now?: number): void;
31
+ export declare function dropLocation(db: DatabaseSync, id: string): void;
32
+ /** Ask every other machine "do you hold `id`?". First `here`/`moved` wins; unreachable machines are listed. */
33
+ export declare function locate(db: DatabaseSync, id: string, deps?: HomeDeps, skip?: string): Promise<HomeAnswer>;
34
+ /** Where `id` lives. `hint` is the machine the request names (`?machine=`). */
35
+ export declare function homeOf(db: DatabaseSync, id: string, opts?: {
36
+ hint?: string | null;
37
+ deps?: HomeDeps;
38
+ }): Promise<HomeAnswer>;
39
+ /** A cached or hinted machine said `404 not_here`: forget it and ask everyone else. */
40
+ export declare function relocate(db: DatabaseSync, id: string, wrong: string, deps?: HomeDeps): Promise<HomeAnswer>;
@@ -0,0 +1,2 @@
1
+ var E=Object.defineProperty;var a=(n,r)=>E(n,"name",{value:r,configurable:!0});import{prepared as l}from"../sqlite-statements.js";import{getById as y}from"../graph/objects.js";import{isUnreachable as b,peerCall as x}from"./client.js";import{directoryIssuer as O,listMachines as T,normalizeMachineId as w,ownMachineId as I}from"./directory.js";import{stubOf as L}from"./stubs.js";const m={call:a((n,r)=>x(n,r),"call"),machines:T,self:I,now:Date.now,joined:a(()=>O()!==null,"joined")};function S(n,r){return l(n,"SELECT machine_id FROM peer_locations WHERE id = ?").get(r)?.machine_id??null}a(S,"cachedLocation");function g(n,r,t,i=Date.now()){l(n,`INSERT INTO peer_locations (id, machine_id, learned_at) VALUES (?, ?, ?)
2
+ ON CONFLICT(id) DO UPDATE SET machine_id = excluded.machine_id, learned_at = excluded.learned_at`).run(r,t,i)}a(g,"rememberLocation");function D(n,r){l(n,"DELETE FROM peer_locations WHERE id = ?").run(r)}a(D,"dropLocation");async function p(n,r,t=m,i){if(!t.joined())return{where:"nowhere"};let c;try{c=await t.machines()}catch(e){return{where:"unknown",unreachable:[`directory (${e instanceof Error?e.message:String(e)})`]}}const s=t.self(),_=c.filter(e=>e.machine_id!==s&&e.machine_id!==i),d=await Promise.all(_.map(async e=>{const o=await t.call(e.machine_id,{method:"POST",path:"/v1/peer/locate",body:{id:r}});if(b(o))return{m:e,state:"unreachable"};const h=o.body;return o.status===200&&h?.answer==="here"?{m:e,state:"here",machine:e.machine_id}:o.status===200&&h?.answer==="moved"&&typeof h.machine=="string"?{m:e,state:"here",machine:w(h.machine)}:o.status===200&&h?.answer==="no"?{m:e,state:"no"}:{m:e,state:"unreachable"}})),u=d.find(e=>e.state==="here");if(u&&"machine"in u)return g(n,r,u.machine,t.now()),{where:"machine",machine:u.machine,source:"locate"};const f=d.filter(e=>e.state==="unreachable").map(e=>e.m.name||e.m.machine_id);return f.length>0?{where:"unknown",unreachable:f}:{where:"nowhere"}}a(p,"locate");async function H(n,r,t={}){const i=y(n,r);if(i!==null){const s=L(n,i);return s===null?{where:"here"}:{where:"machine",machine:s.home_machine,source:"stub"}}if(!(t.deps??m).joined())return{where:"nowhere"};if(t.hint)return{where:"machine",machine:w(t.hint),source:"hint"};const c=S(n,r);return c!==null?{where:"machine",machine:c,source:"cache"}:p(n,r,t.deps)}a(H,"homeOf");async function P(n,r,t,i=m){return D(n,r),p(n,r,i,t)}a(P,"relocate");export{S as cachedLocation,m as defaultHomeDeps,D as dropLocation,H as homeOf,p as locate,P as relocate,g as rememberLocation};
@@ -0,0 +1,26 @@
1
+ import type { DatabaseSync } from 'node:sqlite';
2
+ import type { ObjectRow } from '../graph/types.js';
3
+ import { type ClaimedRow, type PeerEventPayload } from './outbox-store.js';
4
+ /** Most events one `peer.events` call carries. */
5
+ export declare const MAX_EVENTS_PER_CALL = 100;
6
+ export interface IngestResult {
7
+ /** Keys applied (or already applied): the sender marks them done. */
8
+ accepted: string[];
9
+ /** Keys this machine will never apply (unknown event, an id that is another machine's): the sender gives up on them. */
10
+ rejected: Array<{
11
+ key: string;
12
+ code: string;
13
+ message: string;
14
+ }>;
15
+ }
16
+ /** The delivery key prefix of one sender's message; a delivery's full key adds `:<destination>`. */
17
+ export declare const ingestKey: (via: string, messageId: string) => string;
18
+ /** Apply a batch from machine `via`. One event's failure rejects only that event. Runs in the caller's write transaction. */
19
+ export declare function ingestPeerEvents(db: DatabaseSync, via: string, events: readonly PeerEventPayload[]): IngestResult;
20
+ /** Whether `object` is a stub of `machine` (a sender may only describe what it homes). */
21
+ export declare const isStubOf: (db: DatabaseSync, object: ObjectRow, machine: string) => boolean;
22
+ export declare const claimFor: (db: DatabaseSync, via: string, limit: number) => ClaimedRow[];
23
+ export declare const ackFor: (db: DatabaseSync, via: string, acks: ReadonlyArray<{
24
+ key: string;
25
+ claim_token: string;
26
+ }>) => string[];
@@ -0,0 +1 @@
1
+ var a=Object.defineProperty;var r=(e,t)=>a(e,"name",{value:t,configurable:!0});import{prepared as d}from"../sqlite-statements.js";import{CrtrError as f}from"../errors.js";import{publish as m}from"../graph/events.js";import{ackRows as E,claimRows as y}from"./outbox-store.js";import{ensureStub as u,stubOf as _}from"./stubs.js";const A=100,p=r((e,t)=>`peer:${e}:${t}`,"ingestKey");function l(e,t){return d(e,"SELECT 1 FROM peer_ingested WHERE key = ?").get(t)!==void 0?!0:d(e,"SELECT 1 FROM deliveries WHERE idempotency_key >= ? AND idempotency_key < ? LIMIT 1").get(`${t}:`,`${t};`)!==void 0}r(l,"applied");function g(e,t,o){return u(e,t.id,t.type,o,void 0,t)}r(g,"actorFor");function R(e,t,o){const s=u(e,o.object.id,o.object.type,t,void 0,o.object),c=o.actor===null?null:g(e,o.actor,t),n=o.body_text,i=o.body===void 0&&n===void 0?void 0:{...o.body??{},...n===void 0?{}:{body:n}};m(e,{event:o.event,object:s,actor:c,level:o.level,attrs:o.attrs,...i===void 0?{}:{messageBody:i},idempotencyKey:p(t,o.message_id)}),d(e,"INSERT OR IGNORE INTO peer_ingested (key, ingested_at) VALUES (?, ?)").run(p(t,o.message_id),Date.now())}r(R,"apply");function T(e,t,o){const s={accepted:[],rejected:[]};for(const c of o){if(l(e,p(t,c.message_id))){s.accepted.push(c.key);continue}e.exec("SAVEPOINT peer_event");try{R(e,t,c),e.exec("RELEASE peer_event"),s.accepted.push(c.key)}catch(n){e.exec("ROLLBACK TO peer_event"),e.exec("RELEASE peer_event");const i=n instanceof f?n.code:"invalid_event";s.rejected.push({key:c.key,code:i,message:n instanceof Error?n.message:String(n)})}}return s}r(T,"ingestPeerEvents");const j=r((e,t,o)=>_(e,t)?.home_machine===o,"isStubOf"),I=r((e,t,o)=>y(e,t,o),"claimFor"),N=r((e,t,o)=>E(e,t,o),"ackFor");export{A as MAX_EVENTS_PER_CALL,N as ackFor,I as claimFor,p as ingestKey,T as ingestPeerEvents,j as isStubOf};
@@ -0,0 +1,3 @@
1
+ export declare const rememberMachineName: (machineId: string, name: string) => void;
2
+ /** What to call a machine in text a person reads: its directory name when this daemon has seen it, else its id. */
3
+ export declare const machineNameOf: (machineId: string) => string;
@@ -0,0 +1 @@
1
+ var m=Object.defineProperty;var n=(e,t)=>m(e,"name",{value:t,configurable:!0});const a=new Map,s=n((e,t)=>{a.set(e,t)},"rememberMachineName"),c=n(e=>a.get(e)??e,"machineNameOf");export{c as machineNameOf,s as rememberMachineName};
@@ -0,0 +1,70 @@
1
+ import type { DatabaseSync } from 'node:sqlite';
2
+ import type { ObjectRef } from './stubs.js';
3
+ export type OutboxState = 'pending' | 'waiting' | 'claimed' | 'done' | 'expired';
4
+ /** What one event looks like on the wire, in both directions (`peer.events` body, `peer.outbox.claim` rows). */
5
+ export interface PeerEventPayload {
6
+ key: string;
7
+ /** The sending machine's message id: the receiver's idempotency identity for the event. */
8
+ message_id: string;
9
+ event: string;
10
+ level: number;
11
+ object: ObjectRef;
12
+ actor: ObjectRef | null;
13
+ attrs: Record<string, unknown>;
14
+ /** The message's own fields other than `push` (a report's kind, label and disposition; a notice's summary), minus `bodyRef`. */
15
+ body?: Record<string, unknown>;
16
+ /** The text of the report document the event points at, inlined because the receiver cannot read the sender's documents. */
17
+ body_text?: string;
18
+ }
19
+ export interface OutboxRow {
20
+ id: number;
21
+ peer: string;
22
+ kind: string;
23
+ payload_json: string;
24
+ idempotency_key: string;
25
+ state: OutboxState;
26
+ attempts: number;
27
+ next_at: number | null;
28
+ claimed_by: string | null;
29
+ claimed_at: number | null;
30
+ first_failed_at: number | null;
31
+ created_at: number;
32
+ }
33
+ /** A claim not acked within this long is lost (the caller crashed or its answer never arrived) and may be taken again. */
34
+ export declare const CLAIM_LEASE_MS: number;
35
+ /** A row not delivered within this long is given up on. */
36
+ export declare const OUTBOX_MAX_AGE_MS: number;
37
+ /** Subscribe to enqueues (the delivery lane pushes a relay peer's row at once). Returns the unsubscribe. */
38
+ export declare function onPeerEnqueued(listener: (peer: string) => void): () => void;
39
+ /** Write one row; a key already present adds nothing. Returns whether a row was added. Runs in the caller's transaction. */
40
+ export declare function enqueuePeerRow(db: DatabaseSync, row: {
41
+ peer: string;
42
+ kind: string;
43
+ key: string;
44
+ payload: unknown;
45
+ }, now?: number): boolean;
46
+ /** Rows the lane may push now: pending and due. */
47
+ export declare function dueRows(db: DatabaseSync, now: number, limit?: number): OutboxRow[];
48
+ /** Pending rows to one peer that are new or due (a relay peer's push at enqueue; a row that already failed waits for its backoff). */
49
+ export declare function pendingFor(db: DatabaseSync, peer: string, now: number, limit?: number): OutboxRow[];
50
+ export declare const markDone: (db: DatabaseSync, id: number) => void;
51
+ export declare const markWaiting: (db: DatabaseSync, id: number, attempts: number) => void;
52
+ export declare const markRetry: (db: DatabaseSync, id: number, attempts: number, nextAt: number, firstFailedAt: number) => void;
53
+ export declare const markExpired: (db: DatabaseSync, id: number) => void;
54
+ export interface ClaimedRow {
55
+ key: string;
56
+ claim_token: string;
57
+ payload: PeerEventPayload;
58
+ }
59
+ /** The calling machine's own rows (`peer = via`, never another's), fenced with a fresh token. Takes pending and waiting rows
60
+ * and claims whose lease ran out; a row still under a live lease is left alone. */
61
+ export declare function claimRows(db: DatabaseSync, via: string, limit: number, now?: number): ClaimedRow[];
62
+ /** Mark done the rows the caller still holds: `peer = via`, the same token, still `claimed`. Returns the keys acked. */
63
+ export declare function ackRows(db: DatabaseSync, via: string, acks: ReadonlyArray<{
64
+ key: string;
65
+ claim_token: string;
66
+ }>): string[];
67
+ /** Retire rows done long ago, so the table stays small. */
68
+ export declare function pruneOutbox(db: DatabaseSync, now: number, keepMs?: number): void;
69
+ /** Give up on rows older than `OUTBOX_MAX_AGE_MS` that were never delivered. Returns how many. */
70
+ export declare function expireOld(db: DatabaseSync, now: number): number;
@@ -0,0 +1,4 @@
1
+ var c=Object.defineProperty;var a=(e,t)=>c(e,"name",{value:t,configurable:!0});import{prepared as r}from"../sqlite-statements.js";import{randomBytes as _}from"node:crypto";const x=10*6e4,m=720*60*6e4,d=new Set;function l(e){return d.add(e),()=>{d.delete(e)}}a(l,"onPeerEnqueued");function A(e,t,n=Date.now()){const o=r(e,`INSERT INTO peer_outbox (peer, kind, payload_json, idempotency_key, state, attempts, next_at, created_at)
2
+ VALUES (?, ?, ?, ?, 'pending', 0, ?, ?) ON CONFLICT (idempotency_key) DO NOTHING`).run(t.peer,t.kind,JSON.stringify(t.payload),t.key,n,n);return Number(o.changes)===0?!1:(setImmediate(()=>{for(const i of[...d])try{i(t.peer)}catch{}}),!0)}a(A,"enqueuePeerRow");function L(e,t,n=200){return r(e,"SELECT * FROM peer_outbox WHERE state = 'pending' AND next_at <= ? ORDER BY id LIMIT ?").all(t,n)}a(L,"dueRows");function T(e,t,n,o=50){return r(e,"SELECT * FROM peer_outbox WHERE state = 'pending' AND peer = ? AND (attempts = 0 OR next_at <= ?) ORDER BY id LIMIT ?").all(t,n,o)}a(T,"pendingFor");const y=a((e,t)=>{r(e,"UPDATE peer_outbox SET state = 'done', claimed_by = NULL, next_at = NULL WHERE id = ? AND state IN ('pending', 'waiting')").run(t)},"markDone"),g=a((e,t,n)=>{r(e,"UPDATE peer_outbox SET state = 'waiting', attempts = ?, next_at = NULL WHERE id = ? AND state = 'pending'").run(n,t)},"markWaiting"),O=a((e,t,n,o,i)=>{r(e,"UPDATE peer_outbox SET attempts = ?, next_at = ?, first_failed_at = ? WHERE id = ? AND state = 'pending'").run(n,o,i,t)},"markRetry"),S=a((e,t)=>{r(e,"UPDATE peer_outbox SET state = 'expired', next_at = NULL WHERE id = ? AND state IN ('pending', 'waiting')").run(t)},"markExpired");function b(e,t,n,o=Date.now()){const i=Math.max(1,Math.min(n,100)),E=r(e,`SELECT * FROM peer_outbox WHERE peer = ?
3
+ AND (state IN ('pending', 'waiting') OR (state = 'claimed' AND claimed_at <= ?)) ORDER BY id LIMIT ?`).all(t,o-x,i),s=_(16).toString("hex"),u=r(e,"UPDATE peer_outbox SET state = 'claimed', claimed_by = ?, claimed_at = ?, next_at = NULL WHERE id = ?");return E.map(p=>(u.run(s,o,p.id),{key:p.idempotency_key,claim_token:s,payload:JSON.parse(p.payload_json)}))}a(b,"claimRows");function f(e,t,n){const o=r(e,`UPDATE peer_outbox SET state = 'done', claimed_by = NULL WHERE idempotency_key = ? AND peer = ?
4
+ AND state = 'claimed' AND claimed_by = ?`);return n.filter(i=>Number(o.run(i.key,t,i.claim_token).changes)>0).map(i=>i.key)}a(f,"ackRows");function k(e,t,n=1440*6e4){r(e,"DELETE FROM peer_outbox WHERE state IN ('done', 'expired') AND created_at < ?").run(t-n)}a(k,"pruneOutbox");function I(e,t){return Number(r(e,"UPDATE peer_outbox SET state = 'expired', next_at = NULL WHERE state IN ('pending', 'waiting') AND created_at < ?").run(t-m).changes)}a(I,"expireOld");export{x as CLAIM_LEASE_MS,m as OUTBOX_MAX_AGE_MS,f as ackRows,b as claimRows,L as dueRows,A as enqueuePeerRow,I as expireOld,y as markDone,S as markExpired,O as markRetry,g as markWaiting,l as onPeerEnqueued,T as pendingFor,k as pruneOutbox};
@@ -0,0 +1,8 @@
1
+ import type { DatabaseSync } from 'node:sqlite';
2
+ import type { Receiver } from '../graph/receivers.js';
3
+ import { type PeerEventPayload } from './outbox-store.js';
4
+ /** The outbox key of one event to one destination stub. */
5
+ export declare const eventKey: (messageId: string, destination: string) => string;
6
+ /** The event as it crosses the wire: the object and actor by identity, the attributes, and the body inline. */
7
+ export declare function payloadFor(db: DatabaseSync, messageId: string, key: string): PeerEventPayload;
8
+ export declare const peerReceiver: Receiver;
@@ -0,0 +1 @@
1
+ var p=Object.defineProperty;var s=(o,t)=>p(o,"name",{value:t,configurable:!0});import{prepared as a}from"../sqlite-statements.js";import{heads as _}from"../graph/documents.js";import{bodyOf as y}from"../graph/bodies.js";import{getById as i}from"../graph/objects.js";import{enqueuePeerRow as b}from"./outbox-store.js";import{describeObject as f,stubOf as j}from"./stubs.js";const g=s((o,t)=>`ev:${o}:${t}`,"eventKey");function h(o,t){const d=typeof t?.pointer=="string"?t.pointer:void 0,e=d===void 0?null:i(o,d);if(e===null||e.type!=="document"||e.deleted_at!==null)return;const n=_(o,e)[0];if(n===void 0)return;const r=a(o,"SELECT source_path FROM documents WHERE object = ?").get(e.rowid);return y(n.body_hash,r?.source_path??null)}s(h,"reportText");function v(o,t,d){const e=a(o,"SELECT message_id, event, object_id, level, actor_id, body_ref, body_json FROM messages WHERE message_id = ?").get(t);if(e===void 0)throw new Error(`no message ${t}`);const n=i(o,e.object_id);if(n===null)throw new Error(`no object ${e.object_id}`);const r=e.actor_id===null?null:i(o,e.actor_id),m=e.body_json===null?{}:JSON.parse(e.body_json),{push:c,bodyRef:E,...u}=m,l=e.body_ref===null?void 0:h(o,c);return{key:d,message_id:e.message_id,event:e.event,level:e.level,object:f(o,n),actor:r===null?null:f(o,r),attrs:c?.attrs??{},...Object.keys(u).length>0?{body:u}:{},...l===void 0?{}:{body_text:l}}}s(v,"payloadFor");const k={bodyOwner:s(()=>null,"bodyOwner"),enqueue(o,t,d){for(const e of d){const n=j(o,e.id);if(n===null)continue;const r=g(t.messageId,e.id);b(o,{peer:n.home_machine,kind:"event",key:r,payload:v(o,t.messageId,r)})}}};export{g as eventKey,v as payloadFor,k as peerReceiver};
@@ -0,0 +1,53 @@
1
+ import type { DatabaseSync } from 'node:sqlite';
2
+ import type { ObjectRow } from '../graph/types.js';
3
+ import type { Envelope } from './envelope.js';
4
+ /** The part of an envelope a stub stores: who the remote watcher is, never what it may do. */
5
+ export interface StubIdentity {
6
+ sub: string;
7
+ /** `null` for a person. */
8
+ app: string | null;
9
+ node: string | null;
10
+ node_scopes: string[] | null;
11
+ place: {
12
+ run: string;
13
+ isolated: boolean;
14
+ } | null;
15
+ }
16
+ /** An owner as the sending machine names it, so the receiver can map it onto its own owner object. */
17
+ export interface OwnerRef {
18
+ id: string;
19
+ type: 'person' | 'app' | 'repo' | 'profile';
20
+ scheme: string | null;
21
+ external_id: string | null;
22
+ handle: string;
23
+ }
24
+ /** An object as it crosses the wire: identity only, never content. */
25
+ export interface ObjectRef {
26
+ id: string;
27
+ type: string;
28
+ kind: string | null;
29
+ name: string;
30
+ preview?: string;
31
+ owner: OwnerRef | null;
32
+ }
33
+ export interface StubRecord {
34
+ object: ObjectRow;
35
+ home_machine: string;
36
+ /** Absent on a stub that only stands for a watched object. */
37
+ identity: StubIdentity | null;
38
+ }
39
+ export declare const identityOfEnvelope: (envelope: Envelope) => StubIdentity;
40
+ export declare function stubOf(db: DatabaseSync, object: ObjectRow | string): StubRecord | null;
41
+ export declare const isStub: (db: DatabaseSync, object: ObjectRow) => boolean;
42
+ /**
43
+ * The stub of the object `id` that lives on `machine`, created on first sight. Idempotent: a second
44
+ * call returns the same row and, given an `envelope`, replaces the identity it stores. Refuses
45
+ * (`conflict`) an id that is a real local object or a stub of another machine. The owner is the
46
+ * local app (or person) object of the remote owner, so the stub sits in a space the access rules
47
+ * understand. No `nodes` row is written.
48
+ */
49
+ export declare function ensureStub(db: DatabaseSync, id: string, type: string, machine: string, envelope?: Envelope, ref?: ObjectRef): ObjectRow;
50
+ /** The object moved: the machine that answered for it is now its home. A no-op for a local object or a stub already homed there. */
51
+ export declare function rehomeStub(db: DatabaseSync, id: string, machine: string): void;
52
+ /** How `o` is described to another machine. */
53
+ export declare function describeObject(db: DatabaseSync, o: ObjectRow): ObjectRef;
@@ -0,0 +1 @@
1
+ var j=Object.defineProperty;var r=(n,e)=>j(n,"name",{value:e,configurable:!0});import{prepared as i}from"../sqlite-statements.js";import{createObject as b,ensureOwner as w,getById as m,getByRowid as f}from"../graph/objects.js";import{CrtrError as d}from"../errors.js";import{ExitCode as _}from"../../types.js";const R=r(n=>{const e=n.act.act;return e===void 0?{sub:n.sub,app:null,node:null,node_scopes:null,place:null}:{sub:n.sub,app:e.sub,node:e.node,node_scopes:e.node_scopes,place:e.place}},"identityOfEnvelope");function h(n,e){const t=typeof e=="string"?m(n,e):e;if(t===null)return null;const o=i(n,"SELECT home_machine, envelope_json FROM peer_stubs WHERE object = ?").get(t.rowid);return o?{object:t,home_machine:o.home_machine,identity:o.envelope_json===null?null:JSON.parse(o.envelope_json)}:null}r(h,"stubOf");const g=r((n,e)=>i(n,"SELECT 1 FROM peer_stubs WHERE object = ?").get(e.rowid)!==void 0,"isStub");function E(n){return w(n,"person","local-user","user:local","user")}r(E,"localPerson");function S(n,e){return e==="user:local"?E(n):w(n,"app","app",e,e.replace(/^app:/,""))}r(S,"localApp");function T(n,e,t){if(e!==null)return e.app===null?E(n):S(n,e.app);const o=t?.owner;return o?.type==="app"?S(n,o.external_id??`app:${o.handle}`):E(n)}r(T,"ownerFor");function v(n,e,t,o,p,s){const l=p===void 0?null:R(p),u=m(n,e);if(u!==null){const a=h(n,u);if(a===null)throw new d("conflict",`${e} is a local ${u.type}, not a stub of ${o}`,_.GENERAL,{id:e});if(a.home_machine!==o)throw new d("conflict",`${e} is already a stub of ${a.home_machine}, not ${o}`,_.GENERAL,{id:e,home_machine:a.home_machine});return l!==null&&(i(n,"UPDATE peer_stubs SET envelope_json = ? WHERE object = ?").run(JSON.stringify(l),u.rowid),i(n,"UPDATE objects SET reception = 2 WHERE rowid = ?").run(u.rowid)),u}const y=T(n,l,s),c=b(n,{id:e,type:t,kind:s?.kind??null,owner:y.rowid,privacy:"public",name:s?.name??e,preview:s?.preview??""});return i(n,"UPDATE objects SET home_machine = ? WHERE rowid = ?").run(o,c.rowid),i(n,"INSERT INTO peer_stubs (object, home_machine, envelope_json, created_at) VALUES (?, ?, ?, ?)").run(c.rowid,o,l===null?null:JSON.stringify(l),new Date().toISOString()),l!==null&&i(n,"UPDATE objects SET reception = 2 WHERE rowid = ?").run(c.rowid),f(n,c.rowid)}r(v,"ensureStub");function D(n,e,t){const o=h(n,e);o===null||o.home_machine===t||(i(n,"UPDATE peer_stubs SET home_machine = ? WHERE object = ?").run(t,o.object.rowid),i(n,"UPDATE objects SET home_machine = ? WHERE rowid = ?").run(t,o.object.rowid))}r(D,"rehomeStub");function N(n,e){const t=e.owner===null?null:f(n,e.owner);let o=null;if(t!==null&&(t.type==="person"||t.type==="app"||t.type==="repo"||t.type==="profile")){const p=i(n,"SELECT scheme, external_id FROM identities WHERE object = ?").get(t.rowid);o={id:t.id,type:t.type,scheme:p?.scheme??null,external_id:p?.external_id??null,handle:t.name}}return{id:e.id,type:e.type,kind:e.kind,name:e.name,preview:e.preview,owner:o}}r(N,"describeObject");export{N as describeObject,v as ensureStub,R as identityOfEnvelope,g as isStub,D as rehomeStub,h as stubOf};
@@ -1,7 +1,7 @@
1
1
  /** Request header naming the sending machine's crtr release. */
2
2
  export declare const PEER_VERSION_HEADER = "Crtr-Version";
3
- /** The oldest crtr release that speaks the peer protocol. L1 sets this to the release that ships C1–C6. */
4
- export declare const PEER_VERSION_FLOOR = "0.3.442";
3
+ /** The oldest crtr release that speaks the peer protocol. The release that first ships C1–C6 with the peer relay switched on. */
4
+ export declare const PEER_VERSION_FLOOR = "0.3.448";
5
5
  export interface PeerVersionRefusal {
6
6
  status: 426;
7
7
  code: 'peer_version_too_old';
@@ -1 +1 @@
1
- var n=Object.defineProperty;var i=(r,e)=>n(r,"name",{value:e,configurable:!0});import{compareReleaseVersions as a}from"../../api/runtime-version.js";import{crtrVersion as o}from"../../shared/crtr-version.js";const l="Crtr-Version",c="0.3.442";function d(r,e=c,s=o()){const t=r?.trim()?r.trim():null;return t!==null&&a(t,e)>=0?null:{status:426,code:"peer_version_too_old",message:t===null?`The peer did not name its crtr version; machines below ${e} cannot take part (this machine is ${s}). Update it with \`crtr sys update\`.`:`The peer runs crtr ${t}; machines below ${e} cannot take part (this machine is ${s}). Update it with \`crtr sys update\`.`,details:{peer_version:t,floor:e,receiver_version:s}}}i(d,"peerVersionRefusal");export{c as PEER_VERSION_FLOOR,l as PEER_VERSION_HEADER,d as peerVersionRefusal};
1
+ var n=Object.defineProperty;var i=(r,e)=>n(r,"name",{value:e,configurable:!0});import{compareReleaseVersions as a}from"../../api/runtime-version.js";import{crtrVersion as o}from"../../shared/crtr-version.js";const l="Crtr-Version",c="0.3.448";function d(r,e=c,s=o()){const t=r?.trim()?r.trim():null;return t!==null&&a(t,e)>=0?null:{status:426,code:"peer_version_too_old",message:t===null?`The peer did not name its crtr version; machines below ${e} cannot take part (this machine is ${s}). Update it with \`crtr sys update\`.`:`The peer runs crtr ${t}; machines below ${e} cannot take part (this machine is ${s}). Update it with \`crtr sys update\`.`,details:{peer_version:t,floor:e,receiver_version:s}}}i(d,"peerVersionRefusal");export{c as PEER_VERSION_FLOOR,l as PEER_VERSION_HEADER,d as peerVersionRefusal};
@@ -1 +1 @@
1
- var u=Object.defineProperty;var d=(e,t)=>u(e,"name",{value:t,configurable:!0});import{formatInboxCard as p}from"../../../shared/generated-context.js";import{renderEntryBody as m}from"../../../shared/inbox-entry-body.js";import{reportRefNodeId as I}from"../../../shared/inbox-entry-body.js";function y(e){const t=e.data?.report_body;return m(e,typeof t=="string"?t:null)}d(y,"entryBody");function l(e){return{kind:e.kind,body:y(e),...e.ref===void 0?{}:{ref:e.ref},...e.disposition===void 0?{}:{disposition:e.disposition}}}d(l,"cardEntry");function c(e){const t=e.data?.cards;return Array.isArray(t)?t.filter(s=>typeof s=="string"):[]}d(c,"entryCards");function b(e){const t=[];let s=[];const a=d(()=>{if(s.length===0)return;const r=new Map;for(const n of s){const i=n.from??"system";r.has(i)||r.set(i,[]),r.get(i).push(n)}const o=[...r].map(([n,i])=>({id:n,entries:i.map(f=>l(f))}));t.push({kind:"card",text:p(o),cards:s.flatMap(n=>c(n)),entries:s}),s=[]},"flushRun");for(const r of e){if((r.from??"system")!=="human"){s.push(r);continue}a();const o=r.data?.body;t.push({kind:"human",text:typeof o=="string"?o:r.label,cards:c(r),entries:[r]})}a();for(let r=t.length-2;r>=0;r-=1){const o=t[r],n=t[r+1];o.kind!=="card"||n.kind!=="human"||o.entries.every(i=>i.tier==="deferred")&&(t[r]={...n,cards:[...o.cards,o.text,...n.cards],entries:[...o.entries,...n.entries]},t.splice(r+1,1))}return t}d(b,"coalesceBrokerInbox");export{b as coalesceBrokerInbox,c as entryCards,I as reportRefNodeId};
1
+ var u=Object.defineProperty;var d=(n,e)=>u(n,"name",{value:e,configurable:!0});import{formatInboxCard as p,machineOfEntries as m}from"../../../shared/generated-context.js";import{renderEntryBody as y}from"../../../shared/inbox-entry-body.js";import{reportRefNodeId as I}from"../../../shared/inbox-entry-body.js";function l(n){const e=n.data?.report_body;return y(n,typeof e=="string"?e:null)}d(l,"entryBody");function h(n){return{kind:n.kind,body:l(n),...n.ref===void 0?{}:{ref:n.ref},...n.disposition===void 0?{}:{disposition:n.disposition}}}d(h,"cardEntry");function c(n){const e=n.data?.cards;return Array.isArray(e)?e.filter(s=>typeof s=="string"):[]}d(c,"entryCards");function g(n){const e=[];let s=[];const a=d(()=>{if(s.length===0)return;const r=new Map;for(const t of s){const i=t.from??"system";r.has(i)||r.set(i,[]),r.get(i).push(t)}const o=[...r].map(([t,i])=>({id:t,...m(i),entries:i.map(f=>h(f))}));e.push({kind:"card",text:p(o),cards:s.flatMap(t=>c(t)),entries:s}),s=[]},"flushRun");for(const r of n){if((r.from??"system")!=="human"){s.push(r);continue}a();const o=r.data?.body;e.push({kind:"human",text:typeof o=="string"?o:r.label,cards:c(r),entries:[r]})}a();for(let r=e.length-2;r>=0;r-=1){const o=e[r],t=e[r+1];o.kind!=="card"||t.kind!=="human"||o.entries.every(i=>i.tier==="deferred")&&(e[r]={...t,cards:[...o.cards,o.text,...t.cards],entries:[...o.entries,...t.entries]},e.splice(r+1,1))}return e}d(g,"coalesceBrokerInbox");export{g as coalesceBrokerInbox,c as entryCards,I as reportRefNodeId};
@@ -1 +1 @@
1
- var d=Object.defineProperty;var r=(e,t)=>d(e,"name",{value:t,configurable:!0});import{prepared as a}from"../sqlite-statements.js";import{createHash as E}from"node:crypto";import{openScopedStore as p,withScopedWrite as _}from"./db.js";function l(e,t){const n=a(e,"SELECT kind, root, profile_id FROM scope_owners WHERE owner_key = ?").get(t.key);if(!n)throw new Error(`scoped SQL owner is missing: ${t.key}`);if(n.kind!==t.kind||n.root!==t.root||(t.profileId??null)!==n.profile_id)throw new Error(`scoped SQL owner does not match its root: ${t.key}`)}r(l,"ensureOwner");function u(e){return e===void 0?"absent":E("sha256").update(e.raw_json).digest("hex")}r(u,"revisionFor");function k(e,t){return a(e,"SELECT raw_json FROM scope_settings WHERE owner_key = ?").get(t.key)}r(k,"readRawSettingsRow");class g extends Error{static{r(this,"SettingsRevisionConflictError")}constructor(){super("settings changed since the supplied revision"),this.name="SettingsRevisionConflictError"}}function f(e){const t=p(e.store);l(t,e);const n=k(t,e);return{revision:u(n),settings:n===void 0?null:JSON.parse(n.raw_json)}}r(f,"readRawSettingsWithRevision");function T(e){return f(e).settings}r(T,"readRawSettings");function h(e,t,n,i){return _(e.store,o=>{l(o,e);const s=k(o,e);if(n!==u(s))throw new g;i(t);const c=JSON.stringify(t);return s?.raw_json===c?{revision:u(s),changed:!1}:(a(o,"INSERT INTO scope_settings(owner_key, raw_json) VALUES (?, ?) ON CONFLICT(owner_key) DO UPDATE SET raw_json=excluded.raw_json").run(e.key,c),{revision:u({raw_json:c}),changed:!0})})}r(h,"replaceRawSettings");function m(e,t){_(e.store,n=>{l(n,e),a(n,"INSERT INTO scope_settings(owner_key, raw_json) VALUES (?, ?) ON CONFLICT(owner_key) DO UPDATE SET raw_json=excluded.raw_json").run(e.key,JSON.stringify(t))})}r(m,"writeRawSettings");function v(e,t){return _(e.store,n=>{l(n,e);const i=a(n,"SELECT raw_json FROM scope_settings WHERE owner_key = ?").get(e.key),o=t(i?JSON.parse(i.raw_json):{});return a(n,"INSERT INTO scope_settings(owner_key, raw_json) VALUES (?, ?) ON CONFLICT(owner_key) DO UPDATE SET raw_json=excluded.raw_json").run(e.key,JSON.stringify(o.next)),o.value})}r(v,"mutateRawSettings");function N(e){const t=p(e.store);l(t,e);const n=a(t,"SELECT last_self_check, active_canvas FROM scope_bookkeeping WHERE owner_key = ?").get(e.key),i=Object.fromEntries(a(t,"SELECT name, last_updated FROM scope_marketplaces WHERE owner_key = ?").all(e.key).map(s=>[s.name,s.last_updated===null?{}:{last_updated:s.last_updated}])),o=Object.fromEntries(a(t,"SELECT name, last_updated, etag, checked_at FROM scope_plugins WHERE owner_key = ?").all(e.key).map(s=>[s.name,{...s.last_updated===null?{}:{last_updated:s.last_updated},...s.etag===null?{}:{etag:s.etag},...s.checked_at===null?{}:{checked_at:s.checked_at}}]));return{marketplaces:i,plugins:o,...n?.last_self_check===null||n===void 0?{}:{last_self_check:n.last_self_check},activeCanvas:n?.active_canvas??null}}r(N,"readBookkeeping");function w(e,t){_(e.store,n=>{l(n,e),a(n,"INSERT INTO scope_bookkeeping(owner_key, last_self_check, active_canvas) VALUES (?, ?, ?) ON CONFLICT(owner_key) DO UPDATE SET last_self_check=excluded.last_self_check, active_canvas=excluded.active_canvas").run(e.key,t.last_self_check??null,t.activeCanvas??null),a(n,"DELETE FROM scope_marketplaces WHERE owner_key = ?").run(e.key),a(n,"DELETE FROM scope_plugins WHERE owner_key = ?").run(e.key);const i=a(n,"INSERT INTO scope_marketplaces(owner_key, name, last_updated) VALUES (?, ?, ?)");for(const[s,c]of Object.entries(t.marketplaces))i.run(e.key,s,c.last_updated??null);const o=a(n,"INSERT INTO scope_plugins(owner_key, name, last_updated, etag, checked_at) VALUES (?, ?, ?, ?, ?)");for(const[s,c]of Object.entries(t.plugins))o.run(e.key,s,c.last_updated??null,c.etag??null,c.checked_at??null)})}r(w,"writeBookkeeping");export{g as SettingsRevisionConflictError,v as mutateRawSettings,N as readBookkeeping,T as readRawSettings,f as readRawSettingsWithRevision,h as replaceRawSettings,w as writeBookkeeping,m as writeRawSettings};
1
+ var d=Object.defineProperty;var a=(e,t)=>d(e,"name",{value:t,configurable:!0});import{prepared as r}from"../sqlite-statements.js";import{createHash as E}from"node:crypto";import{openScopedStore as p,withScopedWrite as l}from"./db.js";function _(e,t,n){const o=r(e,"SELECT kind, root, profile_id FROM scope_owners WHERE owner_key = ?").get(t.key);if(!o)throw new Error(`scoped SQL owner is missing: ${t.key}`);if(o.kind!==t.kind||(t.profileId??null)!==o.profile_id)throw new Error(`scoped SQL owner does not match its root: ${t.key}`);if(o.root!==t.root){if(t.kind!=="project"||t.store.kind!=="project"||t.store.root!==t.root)throw new Error(`scoped SQL owner does not match its root: ${t.key}`);n==="write"&&r(e,"UPDATE scope_owners SET root = ? WHERE owner_key = ?").run(t.root,t.key)}}a(_,"ensureOwner");function u(e){return e===void 0?"absent":E("sha256").update(e.raw_json).digest("hex")}a(u,"revisionFor");function k(e,t){return r(e,"SELECT raw_json FROM scope_settings WHERE owner_key = ?").get(t.key)}a(k,"readRawSettingsRow");class f extends Error{static{a(this,"SettingsRevisionConflictError")}constructor(){super("settings changed since the supplied revision"),this.name="SettingsRevisionConflictError"}}function g(e){const t=p(e.store);_(t,e,"read");const n=k(t,e);return{revision:u(n),settings:n===void 0?null:JSON.parse(n.raw_json)}}a(g,"readRawSettingsWithRevision");function R(e){return g(e).settings}a(R,"readRawSettings");function h(e,t,n,o){return l(e.store,c=>{_(c,e,"write");const s=k(c,e);if(n!==u(s))throw new f;o(t);const i=JSON.stringify(t);return s?.raw_json===i?{revision:u(s),changed:!1}:(r(c,"INSERT INTO scope_settings(owner_key, raw_json) VALUES (?, ?) ON CONFLICT(owner_key) DO UPDATE SET raw_json=excluded.raw_json").run(e.key,i),{revision:u({raw_json:i}),changed:!0})})}a(h,"replaceRawSettings");function m(e,t){l(e.store,n=>{_(n,e,"write"),r(n,"INSERT INTO scope_settings(owner_key, raw_json) VALUES (?, ?) ON CONFLICT(owner_key) DO UPDATE SET raw_json=excluded.raw_json").run(e.key,JSON.stringify(t))})}a(m,"writeRawSettings");function w(e,t){return l(e.store,n=>{_(n,e,"write");const o=r(n,"SELECT raw_json FROM scope_settings WHERE owner_key = ?").get(e.key),c=t(o?JSON.parse(o.raw_json):{});return r(n,"INSERT INTO scope_settings(owner_key, raw_json) VALUES (?, ?) ON CONFLICT(owner_key) DO UPDATE SET raw_json=excluded.raw_json").run(e.key,JSON.stringify(c.next)),c.value})}a(w,"mutateRawSettings");function v(e){const t=p(e.store);_(t,e,"read");const n=r(t,"SELECT last_self_check, active_canvas FROM scope_bookkeeping WHERE owner_key = ?").get(e.key),o=Object.fromEntries(r(t,"SELECT name, last_updated FROM scope_marketplaces WHERE owner_key = ?").all(e.key).map(s=>[s.name,s.last_updated===null?{}:{last_updated:s.last_updated}])),c=Object.fromEntries(r(t,"SELECT name, last_updated, etag, checked_at FROM scope_plugins WHERE owner_key = ?").all(e.key).map(s=>[s.name,{...s.last_updated===null?{}:{last_updated:s.last_updated},...s.etag===null?{}:{etag:s.etag},...s.checked_at===null?{}:{checked_at:s.checked_at}}]));return{marketplaces:o,plugins:c,...n?.last_self_check===null||n===void 0?{}:{last_self_check:n.last_self_check},activeCanvas:n?.active_canvas??null}}a(v,"readBookkeeping");function N(e,t){l(e.store,n=>{_(n,e,"write"),r(n,"INSERT INTO scope_bookkeeping(owner_key, last_self_check, active_canvas) VALUES (?, ?, ?) ON CONFLICT(owner_key) DO UPDATE SET last_self_check=excluded.last_self_check, active_canvas=excluded.active_canvas").run(e.key,t.last_self_check??null,t.activeCanvas??null),r(n,"DELETE FROM scope_marketplaces WHERE owner_key = ?").run(e.key),r(n,"DELETE FROM scope_plugins WHERE owner_key = ?").run(e.key);const o=r(n,"INSERT INTO scope_marketplaces(owner_key, name, last_updated) VALUES (?, ?, ?)");for(const[s,i]of Object.entries(t.marketplaces))o.run(e.key,s,i.last_updated??null);const c=r(n,"INSERT INTO scope_plugins(owner_key, name, last_updated, etag, checked_at) VALUES (?, ?, ?, ?, ?)");for(const[s,i]of Object.entries(t.plugins))c.run(e.key,s,i.last_updated??null,i.etag??null,i.checked_at??null)})}a(N,"writeBookkeeping");export{f as SettingsRevisionConflictError,w as mutateRawSettings,v as readBookkeeping,R as readRawSettings,g as readRawSettingsWithRevision,h as replaceRawSettings,N as writeBookkeeping,m as writeRawSettings};