@north-light/cli 0.3.482 → 0.3.484
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/client.d.ts +6 -0
- package/dist/api/client.js +3 -3
- package/dist/api/contract.d.ts +1 -1
- package/dist/api/contract.js +1 -1
- package/dist/api/dto/custom-objects.d.ts +5 -1
- package/dist/api/dto/docs.d.ts +26 -0
- package/dist/api/dto/grants.d.ts +6 -4
- package/dist/api/dto/objects.d.ts +21 -1
- package/dist/api/dto/plugins.d.ts +22 -0
- package/dist/api/dto/plugins.js +0 -0
- package/dist/api/index.d.ts +1 -0
- package/dist/api/index.js +1 -1
- package/dist/api/routes.d.ts +2 -0
- package/dist/api/routes.js +1 -1
- package/dist/builtin-memory/crouter-plugin/deploying.md +107 -0
- package/dist/builtin-memory/crouter-sdk/release-notes.md +79 -0
- package/dist/builtin-memory/northlight-apps/concepts/providers.md +4 -3
- package/dist/builtin-memory/northlight-apps/guides/grant-changes.md +11 -9
- package/dist/clients/attach/viewer.js +579 -578
- package/dist/commands/pkg/plugin-manage.d.ts +4 -0
- package/dist/commands/pkg/plugin-manage.js +18 -16
- package/dist/core/canvas/migrations.js +27 -18
- package/dist/core/command-plugins/bundle.d.ts +4 -1
- package/dist/core/command-plugins/bundle.js +1 -1
- package/dist/core/command-plugins/caller-header.d.ts +10 -0
- package/dist/core/command-plugins/caller-header.js +1 -0
- package/dist/core/command-plugins/compose.js +1 -1
- package/dist/core/command-plugins/endpoint.d.ts +4 -0
- package/dist/core/command-plugins/endpoint.js +1 -1
- package/dist/core/command-plugins/transport/exec-invoke-async.d.ts +11 -0
- package/dist/core/command-plugins/transport/exec-invoke-async.js +1 -0
- package/dist/core/command-plugins/transport/exec-invoke.d.ts +14 -0
- package/dist/core/command-plugins/transport/exec-invoke.js +1 -1
- package/dist/core/command-plugins/transport/http-invoke.d.ts +9 -2
- package/dist/core/command-plugins/transport/http-invoke.js +3 -3
- package/dist/core/command-plugins/transport/http-plan.js +1 -1
- package/dist/core/command-plugins/transport/sign.d.ts +55 -0
- package/dist/core/command-plugins/transport/sign.js +3 -0
- package/dist/core/grants/mirror.d.ts +1 -1
- package/dist/core/grants/mirror.js +2 -2
- package/dist/core/graph/access.d.ts +19 -0
- package/dist/core/graph/access.js +3 -3
- package/dist/core/graph/custom.d.ts +29 -9
- package/dist/core/graph/custom.js +9 -8
- package/dist/core/graph/events.js +8 -8
- package/dist/core/graph/package-kinds.d.ts +25 -0
- package/dist/core/graph/package-kinds.js +1 -0
- package/dist/core/graph/package-reconcile.d.ts +20 -0
- package/dist/core/graph/package-reconcile.js +1 -0
- package/dist/core/graph/reader.d.ts +5 -0
- package/dist/core/graph/reader.js +3 -3
- package/dist/core/graph/receivers.d.ts +2 -1
- package/dist/core/graph/types.d.ts +2 -0
- package/dist/core/graph/watches.d.ts +4 -0
- package/dist/core/graph/watches.js +9 -8
- package/dist/core/peer/directory.d.ts +2 -1
- package/dist/core/peer/directory.js +1 -1
- package/dist/core/peer/ingest.d.ts +6 -1
- package/dist/core/peer/ingest.js +1 -1
- package/dist/core/peer/names.d.ts +2 -0
- package/dist/core/peer/names.js +1 -1
- package/dist/core/peer/outbox-store.d.ts +2 -0
- package/dist/core/peer/receiver.js +1 -1
- package/dist/core/peer/stubs.d.ts +26 -1
- package/dist/core/peer/stubs.js +1 -1
- package/dist/core/plugin-app.d.ts +31 -0
- package/dist/core/plugin-app.js +1 -0
- package/dist/core/plugin-objects-manifest.d.ts +35 -0
- package/dist/core/plugin-objects-manifest.js +1 -0
- package/dist/core/providers/bundles.js +2 -2
- package/dist/core/providers/call.d.ts +3 -0
- package/dist/core/providers/call.js +1 -1
- package/dist/core/secrets.d.ts +11 -0
- package/dist/core/secrets.js +1 -1
- package/dist/daemon/api/app-listener.js +13 -13
- package/dist/daemon/api/handlers/custom-objects.js +3 -3
- package/dist/daemon/api/handlers/objects.js +3 -2
- package/dist/daemon/api/handlers/package-docs.js +1 -1
- package/dist/daemon/api/handlers/plugin-app-token.d.ts +6 -0
- package/dist/daemon/api/handlers/plugin-app-token.js +1 -0
- package/dist/daemon/api/handlers/providers.d.ts +7 -0
- package/dist/daemon/api/handlers/providers.js +1 -1
- package/dist/daemon/api/operations.js +1 -1
- package/dist/daemon/api/server.js +1 -1
- package/dist/daemon/crtrd.js +7 -5
- package/dist/daemon/peer/forward.d.ts +4 -1
- package/dist/daemon/peer/forward.js +1 -1
- package/dist/daemon/plugin-objects/read-leaf.d.ts +20 -0
- package/dist/daemon/plugin-objects/read-leaf.js +2 -0
- package/dist/types.d.ts +16 -0
- package/docs/apps/concepts/providers.md +4 -3
- package/docs/apps/guides/grant-changes.md +11 -9
- package/docs/plugin/deploying.md +107 -0
- package/docs/sdk/release-notes.md +79 -0
- package/package.json +2 -2
- package/packages/crouter-identity/dist/index.d.ts +1 -1
- package/packages/crouter-identity/dist/index.js +6 -2
- package/packages/crouter-identity/package.json +1 -1
- package/runtime.lock.json +12 -12
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { DatabaseSync } from 'node:sqlite';
|
|
2
|
-
import type { Level, ObjectRow, Reader } from './types.js';
|
|
3
|
-
import { mayActAs } from './access.js';
|
|
2
|
+
import type { Level, ObjectRow, Privacy, Reader } from './types.js';
|
|
3
|
+
import { customReachable, mayActAs } from './access.js';
|
|
4
4
|
import { onQueueSignal } from './receivers.js';
|
|
5
|
-
export { mayActAs, onQueueSignal };
|
|
5
|
+
export { customReachable, mayActAs, onQueueSignal };
|
|
6
6
|
export type CustomReceiver = 'queue' | {
|
|
7
7
|
url: string;
|
|
8
8
|
} | {
|
|
@@ -39,15 +39,40 @@ export declare function registerCustomObject(db: DatabaseSync, reader: Reader, i
|
|
|
39
39
|
name: string;
|
|
40
40
|
owner: ObjectRow;
|
|
41
41
|
description: string;
|
|
42
|
-
|
|
42
|
+
/** Omitted: create or return only the instance of a kind an installed plugin (or a peer) declares; the catalog is untouched. */
|
|
43
|
+
events?: CustomEvents;
|
|
43
44
|
receiver?: CustomReceiver | null;
|
|
44
45
|
rotateToken?: boolean;
|
|
46
|
+
privacy?: Privacy;
|
|
45
47
|
}): {
|
|
46
48
|
object: ObjectRow;
|
|
47
49
|
token?: string;
|
|
48
50
|
created: boolean;
|
|
49
51
|
changed: string[];
|
|
50
52
|
};
|
|
53
|
+
export type KindSource = 'register' | 'manifest' | 'peer';
|
|
54
|
+
export type KindChange = 'events' | 'description' | 'read';
|
|
55
|
+
/**
|
|
56
|
+
* The only writer of `object_kinds`. `source` says who is writing: an app's `register` call, an installed plugin's
|
|
57
|
+
* manifest, or a declaration that crossed from another machine (`peer`). Precedence: `manifest` and `register` rows
|
|
58
|
+
* are never overwritten by `peer`; a `manifest` write takes over a row it already owns and replaces a `peer` row;
|
|
59
|
+
* a `register` write leaves a `manifest` row alone (`skipped`) and replaces a `peer` row; another owner's
|
|
60
|
+
* `manifest` or `register` row is `kind_owned`. `read_json` is written only by `manifest`. A catalog is replaced,
|
|
61
|
+
* never merged. Runs in the caller's write transaction.
|
|
62
|
+
*/
|
|
63
|
+
export declare function upsertKind(db: DatabaseSync, input: {
|
|
64
|
+
name: string;
|
|
65
|
+
owner: ObjectRow;
|
|
66
|
+
description: string;
|
|
67
|
+
events: CustomEvents;
|
|
68
|
+
read: {
|
|
69
|
+
command: string[];
|
|
70
|
+
} | null;
|
|
71
|
+
source: KindSource;
|
|
72
|
+
}): {
|
|
73
|
+
outcome: 'inserted' | 'updated' | 'unchanged' | 'skipped';
|
|
74
|
+
changed: KindChange[];
|
|
75
|
+
};
|
|
51
76
|
export declare function publishCustom(db: DatabaseSync, object: ObjectRow, actor: ObjectRow | null, input: {
|
|
52
77
|
event: string;
|
|
53
78
|
summary: string;
|
|
@@ -67,8 +92,3 @@ export declare function acknowledge(db: DatabaseSync, object: ObjectRow, deliver
|
|
|
67
92
|
unknown?: string[];
|
|
68
93
|
};
|
|
69
94
|
export declare function verifyPublishToken(db: DatabaseSync, object: ObjectRow, token: string): boolean;
|
|
70
|
-
/** Run confinement for a node acting on a custom object of its app (Silas, 2026-09-30):
|
|
71
|
-
* a node reaches an object registered in its own run, or when neither run is isolated.
|
|
72
|
-
* An object the person or the app's server registered is reached by nodes outside
|
|
73
|
-
* isolation only. Any other reader is unchanged. */
|
|
74
|
-
export declare function customReachable(db: DatabaseSync, reader: Reader, object: ObjectRow): boolean;
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
var
|
|
2
|
-
VALUES (?, ?, ?, ?)`).run(
|
|
3
|
-
WHERE destination_id = ? AND state = 'pending' AND canceled_at IS NULL`).run(
|
|
4
|
-
WHERE destination_id = ? AND state = 'pending' AND canceled_at IS NULL`).run(
|
|
5
|
-
|
|
6
|
-
`)
|
|
1
|
+
var x=Object.defineProperty;var m=(e,t)=>x(e,"name",{value:t,configurable:!0});import{prepared as a}from"../sqlite-statements.js";import{createHash as J,randomBytes as P,timingSafeEqual as q}from"node:crypto";import{CrtrError as j,usage as p}from"../errors.js";import{ExitCode as O}from"../../types.js";import{createObject as F,liveDocumentNamed as M}from"./objects.js";import{invalidateCustomKind as D}from"./kinds/registry.js";import{customReachable as k,mayActAs as L}from"./access.js";import{publish as N,HOP_LIMIT as B,HopLimitError as K}from"./events.js";import{checkoutsForCwd as T}from"./repo-sync/repos.js";import{onQueueSignal as V,signalQueue as R}from"./receivers.js";import{receiverEndpoint as G,isPublicReceiverAddress as Q}from"./receiver-url.js";import{isIP as Y}from"node:net";import{KIND as H,LOCAL as I,MAX_KIND_EVENTS as z,RESERVED_KIND_ROOTS as X,eventCatalogReasons as Z}from"../plugin-objects-manifest.js";const ee=["public","friends","private"];function w(e,t){throw new j(e,t,O.GENERAL)}m(w,"conflict");function A(e){return J("sha256").update(e).digest("hex")}m(A,"hash");function W(){return`crtr_pub_${P(32).toString("base64url")}`}m(W,"issueToken");function te(e){if(e==null)return null;if(e==="queue")return JSON.stringify(e);if(typeof e!="object"||Array.isArray(e))throw p("invalid custom receiver; expected queue, an https url, or argv and absolute cwd");if("url"in e&&typeof e.url=="string")try{const s=G(e.url).hostname.replace(/^\[|\]$/g,"");if(Y(s)&&!Q(s))throw new Error("non-public IP address");return JSON.stringify({url:e.url})}catch(t){throw p(`invalid custom receiver: ${String(t)}`)}if("argv"in e&&Array.isArray(e.argv)&&e.argv.length>0&&e.argv.every(t=>typeof t=="string")&&typeof e.cwd=="string"&&e.cwd.startsWith("/"))return JSON.stringify(e);throw p("invalid custom receiver; url must be https and argv requires a command and absolute cwd")}m(te,"receiverJson");function ge(e,t,s){const{kind:n,name:i,owner:o,description:d,events:l,receiver:c,rotateToken:h,privacy:f}=s;if(f!==void 0&&!ee.includes(f))throw p(`invalid privacy ${JSON.stringify(f)}`);if(L(e,t,o)||w("forbidden",`cannot act as ${o.id}`),!H.test(n)||X.includes(n.split(".")[0]))throw p(`invalid or reserved kind ${n}`);if(!i.trim()||i!==i.trim()||i.includes("\0"))throw p(`invalid object name ${JSON.stringify(i)}`);const E=te(c);if(c!=="queue"&&c!==null&&c!==void 0&&"argv"in c&&t.class!=="person"&&t.class!=="node"&&w("argv_forbidden","argv receiver requires a person or node principal"),l===void 0){const u=a(e,"SELECT source FROM object_kinds WHERE name = ?").get(n);if(u?.source!=="manifest"&&u?.source!=="peer")throw p("events is required unless the kind is declared by an installed plugin")}else if(Object.keys(l).length>z||Object.entries(l).some(([u,g])=>!I.test(u)||!Number.isInteger(g?.level)||g.level<1||g.level>10||g.supersedes!==void 0&&typeof g.supersedes!="boolean"))throw p("invalid custom event catalog");const r=M(e,o.rowid,i);if(r&&!k(e,t,r)&&w("forbidden",`cannot act as ${o.id}`),r&&t.class!=="person"&&t.class!=="node"){const u=a(e,"SELECT receiver_json FROM custom_objects WHERE object = ?").get(r.rowid)?.receiver_json;u&&typeof JSON.parse(u)=="object"&&"argv"in JSON.parse(u)&&w("argv_forbidden","argv receiver requires a person or node principal")}r&&r.type!==n&&w("name_taken",`${o.name}/${i} is already held by ${r.id}`);const v=l===void 0?null:ne(e,{name:n,owner:o,description:d,events:l,read:null,source:"register"});if(!r){const u=F(e,{type:n,kind:null,owner:o.rowid,privacy:f??"public",name:i,preview:""}),g=W();return a(e,`INSERT INTO custom_objects (object, description, receiver_json, token_hash)
|
|
2
|
+
VALUES (?, ?, ?, ?)`).run(u.rowid,d,E,A(g)),t.class==="node"&&a(e,"UPDATE custom_objects SET registered_by = ? WHERE object = ?").run(t.node.id,u.rowid),E!==null&&a(e,"UPDATE objects SET reception = 2 WHERE rowid = ?").run(u.rowid),N(e,{event:"created",object:u,actor:t.class==="object"?t.object:t.class==="person"?t.person:t.class==="app"?t.app:t.node,attrs:{}},$=>T(e,$)),{object:u,token:g,created:!0,changed:[]}}const y=a(e,"SELECT description, receiver_json FROM custom_objects WHERE object = ?").get(r.rowid);y||w("name_taken",`${o.name}/${i} is not a custom object`);const _=[];if(y.description!==d&&_.push("description"),y.receiver_json!==E&&_.push("receiver"),v?.changed.includes("events")&&_.push("events"),v?.changed.includes("description")&&!_.includes("description")&&_.push("description"),f!==void 0&&r.privacy!==f&&_.push("privacy"),h&&_.push("token"),_.length===0)return{object:r,created:!1,changed:_};const S=h?W():void 0,b=_.includes("privacy")?{...r,privacy:f}:r;if(b!==r&&a(e,"UPDATE objects SET privacy = ? WHERE rowid = ?").run(f,r.rowid),a(e,"UPDATE custom_objects SET description = ?, receiver_json = ?, token_hash = COALESCE(?, token_hash) WHERE object = ?").run(d,E,S===void 0?null:A(S),r.rowid),a(e,"UPDATE objects SET reception = ? WHERE rowid = ?").run(E===null?0:2,r.rowid),y.receiver_json!==E){const u=Date.now();if(E===null){const g=a(e,`UPDATE deliveries SET canceled_at = ?, updated_at = ?
|
|
3
|
+
WHERE destination_id = ? AND state = 'pending' AND canceled_at IS NULL`).run(u,u,r.id);a(e,"UPDATE custom_objects SET dropped = dropped + ? WHERE object = ?").run(g.changes,r.rowid)}else a(e,`UPDATE deliveries SET next_attempt_at = ?, updated_at = ?
|
|
4
|
+
WHERE destination_id = ? AND state = 'pending' AND canceled_at IS NULL`).run(E==='"queue"'?null:u,u,r.id);R(r.id)}return h&&R(r.id),N(e,{event:"configured",object:b,actor:t.class==="object"?t.object:t.class==="person"?t.person:t.class==="app"?t.app:t.node,attrs:{changed:_.join(",")}},u=>T(e,u)),{object:b,...S===void 0?{}:{token:S},created:!1,changed:_}}m(ge,"registerCustomObject");function ne(e,t){const{name:s,owner:n,description:i,events:o,read:d,source:l}=t;if(!H.test(s))throw p(`invalid kind ${s}`);if(Z(o,s).length>0)throw p("invalid custom event catalog");const c=a(e,"SELECT owner, description, events_json, read_json, source, declared FROM object_kinds WHERE name = ?").get(s),h=U(o),f=l==="manifest"&&d!==null?JSON.stringify({command:d.command}):null,E=new Date().toISOString();if(!c)return a(e,`INSERT INTO object_kinds (name, owner, description, events_json, created_at, updated_at, read_json, source, declared)
|
|
5
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 1)`).run(s,n.rowid,i,h,E,E,f,l),D(e,s),{outcome:"inserted",changed:[]};const r=c.owner===n.rowid;if(l==="peer"){if(c.source!=="peer")return{outcome:"skipped",changed:[]}}else if(c.source!=="peer"&&(r||w("kind_owned",`${s} belongs to another owner`),l==="register"&&c.source==="manifest"))return{outcome:"skipped",changed:[]};const v=[];return U(JSON.parse(c.events_json))!==h&&v.push("events"),c.description!==i&&v.push("description"),(c.read_json??null)!==f&&v.push("read"),v.length===0&&r&&c.source===l&&c.declared===1?{outcome:"unchanged",changed:v}:(a(e,"UPDATE object_kinds SET owner = ?, description = ?, events_json = ?, read_json = ?, source = ?, declared = 1, updated_at = ? WHERE name = ?").run(n.rowid,i,h,f,l,E,s),D(e,s),{outcome:"updated",changed:v})}m(ne,"upsertKind");function he(e,t,s,n){const i=a(e,"SELECT events_json FROM object_kinds WHERE name = ?").get(t.type);if(!i||t.deleted_at!==null)throw p(`${t.id} is not a live custom object`);const o=JSON.parse(i.events_json);if(!I.test(n.event)||!Object.hasOwn(o,n.event))throw new j("unknown_event",`${t.type} declares ${Object.keys(o).join(", ")}, not ${n.event}`,O.USAGE,{declared:Object.keys(o),event:n.event});if(typeof n.summary!="string")throw p("summary must be text");const d=n.state===void 0?null:JSON.stringify(n.state);if(n.state!==void 0&&d===void 0)throw p("state must be JSON-serializable");if(Buffer.byteLength(JSON.stringify({summary:n.summary,...n.state===void 0?{}:{state:n.state}}))>64*1024)throw new j("body_too_large","custom event summary and state exceed 64 KiB",O.USAGE);if(d!==null&&Buffer.byteLength(d)>64*1024)throw p("state exceeds 64 KiB");const l=n.cause??null,c=l===null?null:a(e,`SELECT depth FROM messages WHERE message_id = ?
|
|
6
|
+
UNION ALL SELECT depth FROM message_causes WHERE message_id = ? LIMIT 1`).get(l,l);if(l!==null&&c===void 0)throw new j("unknown_cause",`no message ${l}`,O.USAGE);if(c&&c.depth>=B)throw new K(c.depth+1);const h=new Date().toISOString();return a(e,"UPDATE custom_objects SET summary = ?, state_json = ?, published_at = ? WHERE object = ?").run(n.summary,d,h,t.rowid),a(e,"UPDATE objects SET preview = ? WHERE rowid = ?").run(n.summary.split(`
|
|
7
|
+
`)[0],t.rowid),N(e,{event:`${t.type}.${n.event}`,object:t,actor:s,attrs:{},messageBody:{summary:n.summary,...n.state===void 0?{}:{state:n.state}},...n.cause===void 0?{}:{cause:n.cause}},f=>T(e,f))}m(he,"publishCustom");function ve(e,t,s){if(t.deleted_at!==null)return;N(e,{event:"deleted",object:t,actor:s,attrs:{}},i=>T(e,i));const n=new Date().toISOString();a(e,"UPDATE objects SET deleted_at = ?, reception = 0 WHERE rowid = ?").run(n,t.rowid),a(e,"UPDATE deliveries SET canceled_at = ?, updated_at = ? WHERE destination_id = ? AND state = 'pending' AND canceled_at IS NULL").run(Date.now(),Date.now(),t.id),R(t.id)}m(ve,"unregisterCustomObject");function U(e){return JSON.stringify(Object.fromEntries(Object.entries(e).sort(([t],[s])=>t.localeCompare(s)).map(([t,s])=>[t,{level:s.level,...s.supersedes===void 0?{}:{supersedes:s.supersedes}}])))}m(U,"canonicalEvents");function we(e,t,s=0){return C(e,t.id,s,"pending")}m(we,"pendingFor");function ye(e,t){const s=a(e,"SELECT destination_id FROM deliveries WHERE delivery_id = ?").get(t);return s?C(e,s.destination_id,-1,"handed_off").find(n=>n.delivery_id===t)??null:null}m(ye,"pushLineFor");function C(e,t,s,n){return a(e,`SELECT d.delivery_id, m.message_id, m.seq, m.event, m.level, m.object_id, m.actor_id, m.body_json, m.body_ref, m.depth, m.created_at,
|
|
7
8
|
o.type, o.name, owner.id AS owner_id
|
|
8
9
|
FROM deliveries d JOIN messages m ON m.message_id = d.message_id JOIN objects o ON o.id = m.object_id
|
|
9
10
|
LEFT JOIN objects owner ON owner.rowid = o.owner
|
|
10
|
-
WHERE d.destination_id = ? AND d.state = ? AND d.canceled_at IS NULL AND m.seq > ? ORDER BY m.seq`).all(t,n,s).map(o=>{const
|
|
11
|
-
WHERE delivery_id = ? AND destination_id = ? AND state = 'pending' AND canceled_at IS NULL`).run(n,n,
|
|
11
|
+
WHERE d.destination_id = ? AND d.state = ? AND d.canceled_at IS NULL AND m.seq > ? ORDER BY m.seq`).all(t,n,s).map(o=>{const d=o.body_json?JSON.parse(o.body_json):{};return{delivery_id:o.delivery_id,message_id:o.message_id,seq:o.seq,event:o.event,level:o.level,object:{id:o.object_id,type:o.type,name:o.name,owner:o.owner_id},actor:o.actor_id,attrs:d.push?.attrs??{},...d.summary===void 0?{}:{summary:d.summary},...d.state===void 0?{}:{state:d.state},...d.push?.pointer===void 0?{}:{pointer:d.push.pointer},depth:o.depth,created_at:o.created_at}})}m(C,"queueLines");function Se(e,t,s){const n=Date.now(),i=[];let o=0;for(const d of s){const l=a(e,`UPDATE deliveries SET state = 'acknowledged', acknowledged_at = ?, updated_at = ?
|
|
12
|
+
WHERE delivery_id = ? AND destination_id = ? AND state = 'pending' AND canceled_at IS NULL`).run(n,n,d,t.id);Number(l.changes)?o++:i.push(d)}return{acknowledged:o,...i.length?{unknown:i}:{}}}m(Se,"acknowledge");function je(e,t,s){const n=a(e,"SELECT token_hash FROM custom_objects WHERE object = ?").get(t.rowid),i=Buffer.from(n?.token_hash??"".padEnd(64,"0"),"hex"),o=Buffer.from(A(s),"hex");return s.startsWith("crtr_pub_")&&t.deleted_at===null&&i.length===o.length&&q(i,o)&&n?.token_hash!=null}m(je,"verifyPublishToken");export{Se as acknowledge,k as customReachable,L as mayActAs,V as onQueueSignal,we as pendingFor,he as publishCustom,ye as pushLineFor,ge as registerCustomObject,ve as unregisterCustomObject,ne as upsertKind,je as verifyPublishToken};
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
var
|
|
2
|
-
AND name = ? AND rowid != ? LIMIT 1`).get(e.name,e.rowid)?`${e.type}:${e.name}`:e.name}
|
|
3
|
-
`)[0]:`${e.event} ${e.object.type} ${e.object.name}`),...
|
|
1
|
+
var W=Object.defineProperty;var d=(n,e)=>W(n,"name",{value:e,configurable:!0});import{prepared as h}from"../sqlite-statements.js";import{evalCondition as $}from"../predicate.js";import{finalizeNodeMail as x,insertMessage as B}from"../feed/messages.js";import{linkAuthorizes as J,ownerChain as K,readCheck as U}from"./access.js";import{getByRowid as G}from"./objects.js";import{appScopesNow as q,publishLookups as z,readerForObject as X}from"./reader.js";import{configFromJson as k}from"./watches.js";import{CrtrError as v}from"../errors.js";import{ExitCode as L}from"../../types.js";import{eventSpec as Y,kindOf as C}from"./kinds/registry.js";import{machineNameOf as M}from"../peer/names.js";import{RECEIVERS as D}from"./receivers.js";import{DEFAULT_LISTEN as F}from"./types.js";function P(n,e){if(n.level===void 0||n.level===e.level||e.levels?.includes(n.level))return n.level??e.level;const o=[e.level,...e.levels??[]].filter((s,t,i)=>i.indexOf(s)===t).sort((s,t)=>s-t);throw new v("invalid_level",`${n.object.type} ${n.event} publishes at level ${o.join(", ")}, not ${n.level}`,L.USAGE,{kind:n.object.type,event:n.event,level:n.level,allowed:o})}d(P,"resolveLevel");function V(n,e){const o=n.split("."),s=e.split(".");for(let t=0;t<o.length;t++){if(o[t]===">")return t===o.length-1&&s.length>t;if(t>=s.length||o[t]!=="*"&&o[t]!==s[t])return!1}return o.length===s.length}d(V,"matchSubject");const H={"next-turn":1,"after-turn":2,steer:3,interrupt:4};function Q(n,e){let o=null;for(const s of n){let t=null;for(const i of s)i.threshold<=e&&(!t||i.threshold>t.threshold)&&(t=i);t&&(!o||H[t.delivery]>H[o])&&(o=t.delivery)}return o}d(Q,"resolveDelivery");function _e(n,e,o){if(e.config)return e.config;const s=h(n,"SELECT listen_json FROM objects WHERE rowid = ?").get(o.rowid);return s?.listen_json?k(s.listen_json):F}d(_e,"listenConfigFor");function Z(n,e){return e.inboxKind??(n.level>=7?"urgent":"update")}d(Z,"inboxKind");function ee(n,e){return e.type==="node"?e.id:h(n,`SELECT 1 FROM objects WHERE type IN ('person','app','repo','profile')
|
|
2
|
+
AND name = ? AND rowid != ? LIMIT 1`).get(e.name,e.rowid)?`${e.type}:${e.name}`:e.name}d(ee,"ownerHandleOf");function te(n,e,o){const s=e.object.owner===null?null:h(n,"SELECT * FROM objects WHERE rowid = ?").get(e.object.owner),t=s?.id??null,i=s?ee(n,s):null;return{event:e.event,level:e.level,object:{id:e.object.id,type:e.object.type,kind:e.object.kind,owner:t,owner_handle:i,name:e.object.name,...e.object.home_machine?{machine:M(e.object.home_machine),machine_id:e.object.home_machine}:{}},actor:e.actor?.id??null,...e.actor?.home_machine?{actor_machine:M(e.actor.home_machine),actor_machine_id:e.actor.home_machine}:{},attrs:o,...e.pointer===void 0?{}:{pointer:e.pointer}}}d(te,"pushFields");function ne(n,e,o,s){const t=te(n,e,s),i=e.messageBody!==void 0&&o.carriesBody===!0,f=i?typeof e.messageBody=="object"&&e.messageBody!==null&&!Array.isArray(e.messageBody)?{...e.messageBody,push:t}:{body:e.messageBody,push:t}:{push:t},l=i?e.messageBody:null,a=l?.bodyRef===void 0?null:l;return{kind:Z(e,o),label:a?.label??(typeof l?.label=="string"?l.label:e.object.type.includes(".")&&typeof e.messageBody?.summary=="string"?e.messageBody.summary.split(`
|
|
3
|
+
`)[0]:`${e.event} ${e.object.type} ${e.object.name}`),...a?.bodyRef===void 0?{}:{bodyRef:a.bodyRef},...a?.disposition===void 0?{}:{disposition:a.disposition},senderNodeId:e.actor?.type==="node"?e.actor.id:null,senderName:e.actor===null||e.actor===void 0?null:e.actor.home_machine?`${e.actor.name} on ${M(e.actor.home_machine)}`:e.actor.name,bodyJson:f}}d(ne,"messageInput");function oe(n,e,o,s,t){if(!o.supersedes)return[{attrs:e.attrs,destinations:[...s]}];const i=h(n,`SELECT d.delivery_id, d.message_id, d.state, m.body_json FROM deliveries d
|
|
4
4
|
JOIN messages m ON m.message_id = d.message_id
|
|
5
5
|
WHERE d.destination_id = ? AND d.state IN ('pending', 'handed_off')
|
|
6
6
|
AND d.canceled_at IS NULL AND m.event = ? AND m.object_id = ?
|
|
7
|
-
ORDER BY m.seq`),
|
|
8
|
-
UNION ALL SELECT depth FROM message_causes WHERE message_id = ? LIMIT 1`).get(
|
|
9
|
-
COALESCE(s.config_json, o.listen_json) AS config_json`,
|
|
10
|
-
JOIN objects o ON o.rowid = w.watcher WHERE w.object = ? AND w.watcher != ? AND o.reception >= 1`:`SELECT ${
|
|
11
|
-
WHERE o.reception = 2 AND w.watcher = o.rowid AND s.watch = w.rowid AND w.object = ? AND w.watcher != ?`).all(t.object.rowid,t.actor?.rowid??-1);if(
|
|
7
|
+
ORDER BY m.seq`),f=h(n,"DELETE FROM deliveries WHERE delivery_id = ?"),l=h(n,"SELECT * FROM objects WHERE id = ?"),a=new Map,_=new Set;for(const m of s){const w=t(m),O=l.get(w),I=O===void 0?void 0:C(n,O.type),j=i.all(w,e.event,e.object.id),b=new Set;for(const c of j)c.state==="handed_off"?I?.supersedeClaimed?.(n,c.delivery_id)&&b.add(c.delivery_id):(f.run(c.delivery_id),_.add(c.message_id));const y=j.filter(c=>c.state==="pending"||b.has(c.delivery_id)).at(-1),R=y?.message_id??null,r=a.get(R)??{prior:y?.body_json?JSON.parse(y.body_json).push?.attrs??null:null,destinations:[]};r.destinations.push(m),a.set(R,r)}const N=h(n,"DELETE FROM messages WHERE message_id = ? AND NOT EXISTS (SELECT 1 FROM deliveries WHERE message_id = ?)");for(const m of _)N.run(m,m);return[...a.values()].map(m=>({attrs:o.merge?o.merge(m.prior,e):e.attrs,destinations:m.destinations}))}d(oe,"supersede");function se(n,e,o,s,t){const i=G(n,s);if(i===null||i.type!=="app")return!1;const f=h(n,"SELECT 1 FROM peer_stubs WHERE object = ?").get(e.rowid)!==void 0,l=f?null:q(n,i,t);return(f||l!==null)&&J(n,i,l,e,o)}d(se,"linkDelivers");const T=8;class ie extends v{static{d(this,"HopLimitError")}constructor(e){super("hop_limit",`event depth ${e} exceeds ${T}`,L.GENERAL,{depth:e,limit:T})}}function je(n,e,o=()=>[]){const s=Y(n,e.object.type,e.event),t={...e,level:P(e,s)},i=e.cause??null,f=i===null?null:h(n,`SELECT depth FROM messages WHERE message_id = ?
|
|
8
|
+
UNION ALL SELECT depth FROM message_causes WHERE message_id = ? LIMIT 1`).get(i,i);if(i!==null&&f===void 0)throw new v("unknown_cause",`no message ${i}`,L.USAGE,{cause:i});const l=f===null?0:f.depth+1;if(l>T)throw new ie(l);const a=K(n,t.object),_=`w.watcher, o.id AS watcher_id, o.type AS watcher_type, o.reception, s.pattern, s.filter_json,
|
|
9
|
+
COALESCE(s.config_json, o.listen_json) AS config_json, w.link_app`,N=n.prepare(s.durable?`SELECT ${_} FROM watches w JOIN watch_subscriptions s ON s.watch = w.rowid
|
|
10
|
+
JOIN objects o ON o.rowid = w.watcher WHERE w.object = ? AND w.watcher != ? AND o.reception >= 1`:`SELECT ${_} FROM objects o INDEXED BY objects_receiving CROSS JOIN watches w CROSS JOIN watch_subscriptions s
|
|
11
|
+
WHERE o.reception = 2 AND w.watcher = o.rowid AND s.watch = w.rowid AND w.object = ? AND w.watcher != ?`).all(t.object.rowid,t.actor?.rowid??-1);if(N.length===0)return{delivered:0,message_id:null,depth:l};const m={event:t.event,level:t.level,id:t.object.id,type:t.object.type,kind:t.object.kind,actor:t.actor?.id??null,...t.attrs},w=new Map;for(const r of N){if(t.onlyWatchers!==void 0&&!t.onlyWatchers.includes(r.watcher_id)||!V(r.pattern,t.event)||r.filter_json!==null&&!$(JSON.parse(r.filter_json),m))continue;const c=w.get(r.watcher)??{id:r.watcher_id,type:r.watcher_type,reception:r.reception,configs:[],linkApp:r.link_app};c.configs.push(r.config_json?k(r.config_json):F),w.set(r.watcher,c)}const O=(a.length?a[a.length-1]:t.object).type==="repo",I=d(()=>[],"noRepos"),j=U(n,t.object),b=z(n),y=[];for(const r of w.values()){const c=h(n,"SELECT * FROM objects WHERE id = ?").get(r.id),u=C(n,c.type),S=u.receiverFor?u.receiverFor(n,c):u.receiver;if(S===void 0)continue;const E=Q(r.configs,t.level);if(E===null)continue;const g=X(n,c,O?o:I,b);if(g.scopes===null&&(g.class==="node"||g.owner.type!=="person"))continue;const p=r.linkApp!==null&&!j(g)&&se(n,t.object,c,r.linkApp,b);!p&&!j(g)||y.push({id:r.id,reception:r.reception,delivery:E,receiver:S,...p?{linked:p}:{}})}if(y.length===0)return{delivered:0,message_id:null,depth:l};let R=null;for(const r of oe(n,t,s,y,c=>c.id)){const c=new Map;for(const p of r.destinations)c.set(p.receiver,[...c.get(p.receiver)??[],p]);const u=ne(n,t,s,r.attrs),S=[...c].map(([p,A])=>D[p].bodyOwner(A)).find(p=>p!==null)??null,E=x({...u,urgency:"normal"},S),g=B(n,{operationId:E.operationId,createdAt:E.createdAt,senderNodeId:u.senderNodeId,senderName:u.senderName,kind:u.kind,label:u.label,bodyRef:E.bodyRef,bodyJson:E.bodyJson,disposition:u.disposition??null,event:{event:t.event,objectId:t.object.id,level:t.level,actorId:t.actor?.id??null},causeMessageId:i,depth:l});R??=g;for(const[p,A]of c)D[p].enqueue(n,{messageId:g,event:t.event,level:t.level,senderNodeId:u.senderNodeId,elevatesTier:s.elevatesTier===!0,...t.idempotencyKey===void 0?{}:{idempotencyKey:t.idempotencyKey}},A)}return{delivered:y.length,message_id:R,depth:l}}d(je,"publish");export{T as HOP_LIMIT,ie as HopLimitError,_e as listenConfigFor,V as matchSubject,je as publish,Q as resolveDelivery,P as resolveLevel,oe as supersede};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** Installed plugins' `object_kinds` blocks → `object_kinds` rows. Runs in the caller's write transaction.
|
|
2
|
+
* A kind is never deleted and its catalog is never altered when undeclared: only `declared` flips, so the
|
|
3
|
+
* instances of a dropped kind go inert instead of vanishing. */
|
|
4
|
+
import type { DatabaseSync } from 'node:sqlite';
|
|
5
|
+
export interface KindsReport {
|
|
6
|
+
/** every kind a valid plugin declares that now has its row, and what upserting it did */
|
|
7
|
+
applied: Array<{
|
|
8
|
+
plugin: string;
|
|
9
|
+
kind: string;
|
|
10
|
+
outcome: 'inserted' | 'updated' | 'unchanged';
|
|
11
|
+
}>;
|
|
12
|
+
/** kinds another owner holds; the plugin's other kinds still applied */
|
|
13
|
+
refused: Array<{
|
|
14
|
+
plugin: string;
|
|
15
|
+
kind: string;
|
|
16
|
+
code: 'kind_owned';
|
|
17
|
+
message: string;
|
|
18
|
+
}>;
|
|
19
|
+
/** enabled plugins whose manifest or commands are invalid (no kind update, no token); a provider bundle's kinds reconcile without a token, and plugins whose app handle another owner holds */
|
|
20
|
+
invalid: Array<{
|
|
21
|
+
plugin: string;
|
|
22
|
+
error: string;
|
|
23
|
+
}>;
|
|
24
|
+
}
|
|
25
|
+
export declare function reconcilePackageKinds(db: DatabaseSync): KindsReport;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var f=Object.defineProperty;var d=(o,i)=>f(o,"name",{value:i,configurable:!0});import{CrtrError as p}from"../errors.js";import{invalidAppPlugins as l,kindPlugins as g}from"../plugin-app.js";import{prepared as m}from"../sqlite-statements.js";import{upsertKind as h}from"./custom.js";import{ensureOwner as k}from"./objects.js";function S(o){const i={applied:[],refused:[],invalid:l().map(e=>({plugin:e.plugin.name,error:e.error}))},a=new Set;for(const e of g()){const c=Object.entries(e.manifest.object_kinds??{});if(c.length===0)continue;let s;try{s=k(o,"app","app",`app:${e.name}`,e.name)}catch(n){if(!(n instanceof p)||n.code!=="conflict")throw n;i.invalid.push({plugin:e.name,error:n.message});continue}for(const[n,t]of c)try{const r=h(o,{name:n,owner:s,description:t.description,events:t.events,read:t.read===void 0?null:{command:t.read.command},source:"manifest"});if(r.outcome==="skipped")continue;a.add(n),i.applied.push({plugin:e.name,kind:n,outcome:r.outcome})}catch(r){if(!(r instanceof p)||r.code!=="kind_owned")throw r;i.refused.push({plugin:e.name,kind:n,code:"kind_owned",message:r.message})}}const u=m(o,"SELECT name FROM object_kinds WHERE source = 'manifest' AND declared = 1").all();for(const{name:e}of u)a.has(e)||m(o,"UPDATE object_kinds SET declared = 0 WHERE name = ?").run(e);return i}d(S,"reconcilePackageKinds");export{S as reconcilePackageKinds};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** The one package reconcile pass: documents, then kinds, then plugin app tokens. Runs in the caller's write
|
|
2
|
+
* transaction. The token writes go to the secrets store and are idempotent, so a rolled-back pass re-converges
|
|
3
|
+
* on the next one. */
|
|
4
|
+
import type { DatabaseSync } from 'node:sqlite';
|
|
5
|
+
import { type KindsReport } from './package-kinds.js';
|
|
6
|
+
import { type PackageReconcileReport } from './package-docs.js';
|
|
7
|
+
export interface TokensReport {
|
|
8
|
+
/** plugins holding a `crtr_app_` token, with the manifest scopes it carries */
|
|
9
|
+
granted: Array<{
|
|
10
|
+
plugin: string;
|
|
11
|
+
scopes: string[];
|
|
12
|
+
}>;
|
|
13
|
+
/** plugins whose `crtr_app_` token was removed because they no longer qualify */
|
|
14
|
+
revoked: string[];
|
|
15
|
+
}
|
|
16
|
+
export interface PackagesReport extends PackageReconcileReport {
|
|
17
|
+
kinds: KindsReport;
|
|
18
|
+
tokens: TokensReport;
|
|
19
|
+
}
|
|
20
|
+
export declare function reconcilePackages(db: DatabaseSync): PackagesReport;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var a=Object.defineProperty;var p=(o,t)=>a(o,"name",{value:t,configurable:!0});import{appPlugins as u}from"../plugin-app.js";import{ensurePluginAppToken as f,listScopedApiTokens as g,revokePluginAppTokens as l}from"../secrets.js";import{reconcilePackageKinds as m}from"./package-kinds.js";import{reconcilePackageDocuments as d}from"./package-docs.js";function S(o){const t=d(o),i=m(o),s={granted:[],revoked:[]},r=new Set;for(const e of u()){const n=e.manifest.scopes??[];f(e.name,n),r.add(e.name),s.granted.push({plugin:e.name,scopes:[...n]})}const c=new Set;for(const e of g("user")){if(!e.token.startsWith("crtr_app_")||!e.grantee.startsWith("app:"))continue;const n=e.grantee.slice(4);r.has(n)||c.add(n)}for(const e of c)l(e)&&s.revoked.push(e);return{...t,kinds:i,tokens:s}}p(S,"reconcilePackages");export{S as reconcilePackages};
|
|
@@ -20,6 +20,9 @@ export declare function readerForNode(db: DatabaseSync, nodeId: string, reposFn:
|
|
|
20
20
|
export declare function readerForObject(db: DatabaseSync, watcher: ObjectRow, reposFn?: ReposFn, lookups?: ReaderLookups): Extract<Reader, {
|
|
21
21
|
class: 'node' | 'object';
|
|
22
22
|
}>;
|
|
23
|
+
/** The scopes `app` (an app object) holds now: its one active grant's always-scopes, or a local plugin's `crtr_app_` token scopes when it has no
|
|
24
|
+
* grant; null for several grants or none that applies. The same rule as an app-owned object's reader (`readerForObject`). */
|
|
25
|
+
export declare function appScopesNow(db: DatabaseSync, app: ObjectRow, lookups: ReaderLookups): readonly string[] | null;
|
|
23
26
|
/** What building a node reader looks up besides the node's own row. `liveLookups` reads each
|
|
24
27
|
* one per call; `publishLookups` memoizes them for one publish inside its transaction, so the
|
|
25
28
|
* readers of one event share a single config read and one lookup per grant, owner and
|
|
@@ -28,6 +31,8 @@ export interface ReaderLookups {
|
|
|
28
31
|
grantById(grantId: string): ActiveGrant | null;
|
|
29
32
|
grantFor(app: string, sub: string): ActiveGrant | null;
|
|
30
33
|
terminalHasIssuer(): boolean;
|
|
34
|
+
/** The scopes of the `crtr_app_` token a local plugin's app (`app:<plugin>`) holds, or null when it holds none. */
|
|
35
|
+
pluginScopes(grantee: string): readonly string[] | null;
|
|
31
36
|
object(rowid: number): ObjectRow | null;
|
|
32
37
|
profile(profileId: string, app: ObjectRow): ObjectRow;
|
|
33
38
|
/** The profile's project paths in manifest order; none in a sandboxed node, whose profile store is unreadable. */
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
var C=Object.defineProperty;var
|
|
2
|
-
sub, grant_id, scopes_json, cwd, profile_id FROM nodes WHERE node_id = ?`).get(n?.owner??null,e);if(!n||!
|
|
3
|
-
WHERE i.scheme = 'profile' AND i.external_id = ?`).get(e)??
|
|
1
|
+
var C=Object.defineProperty;var p=(o,e)=>C(o,"name",{value:e,configurable:!0});import{prepared as f}from"../sqlite-statements.js";import{coveredByAny as m,isSandboxedNode as b}from"../../shared/env.js";import{existsSync as A}from"node:fs";import{CrtrError as L}from"../errors.js";import{getStoredProfile as $}from"../scoped-state/profiles.js";import{userScopeRoot as M}from"../scope.js";import{parseScope as O}from"@north-light/identity";import{activeGrant as v,grantFor as F}from"../grants/mirror.js";import{prepared as N}from"../canvas/db.js";import{ApiError as T}from"../../api/errors.js";import{readConfig as W}from"../config.js";import{pluginAppToken as B}from"../secrets.js";import{SCOPE_VOCABULARY as E}from"../scopes.js";import{loadExactProfileManifest as D}from"../profiles/manifest.js";import{ensureOwner as S,getByRowid as P,nodeObject as J}from"./objects.js";function x(o,e,t,s){const r=e===null?[]:s.projectPaths(e),n=t(o),c=new Set(n.map(a=>a.repo.rowid)),l=[];for(const a of r){if(!A(a))continue;let u;try{u=t(a)}catch(i){if(i instanceof L&&i.code==="git-lookup-failed")continue;throw i}for(const i of u)c.has(i.repo.rowid)||(c.add(i.repo.rowid),l.push(i))}return{repos:n.map(a=>a.repo),checkouts:n,projectRepos:l.map(a=>a.repo),projectCheckouts:l}}p(x,"viewRepos");function ae(o,e,t){if(e.class==="anonymous")throw new Error("a publish token cannot read as a principal");if(e.class==="person")return{class:"person",person:g(o)};if(e.class==="app")return{class:"app",app:j(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:g(o)};const l=F(e.actor.app,e.sub);if(!l)throw new T(403,"app_not_granted",`${e.actor.app} holds no active grant for ${e.sub} on this machine`);const a=e.actor.node_scopes;return{class:"app",app:j(o,e.actor.app),place:e.actor.place,launched:a,scopes:a===null?l.resolved:a.filter(u=>m(l.resolved,u))}}if(e.class==="cron"){const l=e.grant?v(e.grant)?.resolved??[]:E,a=e.scopes;return{class:"app",app:j(o,e.app),scopes:a===null?l:a.filter(u=>m(l,u)),launched:a,place:{run:e.origin_run??"",isolated:e.isolated}}}const s=w(o),r=J(o,e.node),n=r.owner===null?null:s.object(r.owner);if(!n)throw new Error(`node ${e.node} has no owner`);const c=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,s),launched:e.scopes,cwd:c.cwd,...x(c.cwd,c.profile_id,t,s),profile:c.profile_id===null?null:s.profile(c.profile_id,n)}}p(ae,"readerFor");function y(o,e,t,s=w(o),r=!1){const n=N(o,"SELECT o.* FROM objects o WHERE o.id = ? AND o.type = 'node'").get(e),c=N(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||!c)throw new Error(`no node ${e}`);const{grantee:l,sub:a,grant_id:u,scopes_json:i,cwd:d,profile_id:h}=c,_=n.owner===null?null:s.object(n.owner);if(!_)throw new Error(`node ${e} has no owner`);const R=i===null?null:JSON.parse(i);return{class:"node",node:n,app:_,scopes:H(l,a??"",u,R,s,r),launched:R,cwd:d,...x(d,h,t,s),profile:h===null?null:s.profile(h,_)}}p(y,"readerForNode");function pe(o,e,t=()=>[],s=w(o)){const r=f(o,"SELECT envelope_json FROM peer_stubs WHERE object = ?").get(e.rowid);if(r!==void 0)return I(o,e,r.envelope_json===null?null:JSON.parse(r.envelope_json),s);if(e.type==="node")return y(o,e.id,t,s,!0);const n=e.owner===null?null:s.object(e.owner);if(!n)throw new Error(`watcher ${e.id} has no owner`);const l=(n.type==="app"?n:n.type==="profile"&&n.owner!==null?s.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(!l)throw new Error("no terminal app or person object");const a=f(o,"SELECT external_id FROM identities WHERE object = ? AND scheme = 'app'").get(l.rowid)?.external_id??"app:terminal",u=f(o,"SELECT resolved_scopes_json FROM grants WHERE grantee = ? AND status = 'active'").all(a),i=n.type==="person"?null:u.length===0?a==="app:terminal"&&!s.terminalHasIssuer()?E:s.pluginScopes(a):u.length===1?JSON.parse(u[0].resolved_scopes_json).filter(d=>d.mode==="always").map(d=>O(d.scope)):null;return{class:"object",object:e,node:e,owner:n,app:l,scopes:i,repos:n.type==="repo"?[n]:[]}}p(pe,"readerForObject");function le(o,e,t){const s=f(o,"SELECT external_id FROM identities WHERE object = ? AND scheme = 'app'").get(e.rowid)?.external_id;if(s===void 0)return null;const r=f(o,"SELECT resolved_scopes_json FROM grants WHERE grantee = ? AND status = 'active'").all(s);return r.length===0?s==="app:terminal"&&!t.terminalHasIssuer()?E:t.pluginScopes(s):r.length>1?null:JSON.parse(r[0].resolved_scopes_json).filter(n=>n.mode==="always").map(n=>O(n.scope))}p(le,"appScopesNow");function I(o,e,t,s){const r=g(o);if(t!==null&&t.app===null)return{class:"object",object:e,node:e,owner:r,app:r,scopes:null,repos:[]};const n=t?.app==null?null:j(o,t.app),c=t?.app==null?null:s.grantFor(t.app,t.sub),l=t===null||c===null?null:t.node_scopes===null?c.resolved:t.node_scopes.filter(u=>m(c.resolved,u)),a=n??(e.owner===null?r:s.object(e.owner)??r);return{class:"object",object:e,node:e,owner:a,app:a,scopes:l,launched:t?.node_scopes??null,repos:[],...t?.place==null?{}:{place:t.place}}}p(I,"readerForStub");function w(o){return{grantById:v,grantFor:F,terminalHasIssuer:p(()=>!!W("user").runtime?.issuer,"terminalHasIssuer"),pluginScopes:p(e=>B(e.replace(/^app:/,""))?.scopes??null,"pluginScopes"),object:p(e=>P(o,e),"object"),profile:p((e,t)=>G(o,e,t),"profile"),projectPaths:p(e=>b()?[]:$(M(),e)?.manifest.projects.map(t=>t.path)??[],"projectPaths")}}p(w,"liveLookups");function ce(o){const e=w(o),t=new Map,s=p((r,n)=>(t.has(r)||t.set(r,n()),t.get(r)),"memo");return{grantById:p(r=>s(`g\0${r}`,()=>e.grantById(r)),"grantById"),grantFor:p((r,n)=>s(`a\0${r}\0${n}`,()=>e.grantFor(r,n)),"grantFor"),terminalHasIssuer:p(()=>s("issuer",()=>e.terminalHasIssuer()),"terminalHasIssuer"),pluginScopes:p(r=>s(`t\0${r}`,()=>e.pluginScopes(r)),"pluginScopes"),object:p(r=>s(`o\0${r}`,()=>e.object(r)),"object"),profile:p((r,n)=>s(`p\0${r}\0${n.rowid}`,()=>e.profile(r,n)),"profile"),projectPaths:p(r=>s(`j\0${r}`,()=>e.projectPaths(r)),"projectPaths")}}p(ce,"publishLookups");function H(o,e,t,s,r,n=!1){const c=t?r.grantById(t):r.grantFor(o,e),l=c&&c.grantee===o?c.resolved:o==="app:terminal"&&!r.terminalHasIssuer()?E:null;if(l===null){if(n)return null;throw new Error(`the grant of ${o} is not active`)}return s===null?l:s.filter(a=>m(l,a))}p(H,"nodeScopes");function g(o){return S(o,"person","local-user","user:local","user")}p(g,"personObject");function j(o,e){return e==="user:local"?g(o):S(o,"app","app",e,e.replace(/^app:/,""))}p(j,"appObject");function G(o,e,t){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)??S(o,"profile","profile",e,D(e).manifest.name,t)}p(G,"profileObject");export{le as appScopesNow,w as liveLookups,G as profileObject,ce as publishLookups,ae as readerFor,y as readerForNode,pe as readerForObject,x as viewRepos};
|
|
@@ -3,7 +3,8 @@ import type { ObjectRow, Level, ListenDelivery } from './types.js';
|
|
|
3
3
|
export interface ReceiverDestination {
|
|
4
4
|
id: string;
|
|
5
5
|
reception: 1 | 2;
|
|
6
|
-
delivery: ListenDelivery;
|
|
6
|
+
delivery: ListenDelivery; /** Delivered on the strength of an app's `link` rather than the destination's own read access. */
|
|
7
|
+
linked?: boolean;
|
|
7
8
|
}
|
|
8
9
|
export interface ReceivedMessage {
|
|
9
10
|
messageId: string;
|
|
@@ -42,6 +42,8 @@ export interface CanvasEvent {
|
|
|
42
42
|
cause?: string;
|
|
43
43
|
/** Set by `peer.events` ingestion: each delivery this publish creates carries the unique key `<key>:<destination>`, so a replay adds nothing. */
|
|
44
44
|
idempotencyKey?: string;
|
|
45
|
+
/** Deliver only to these watchers (object ids): every other candidate is dropped before grouping, every other check applies unchanged. */
|
|
46
|
+
onlyWatchers?: readonly string[];
|
|
45
47
|
}
|
|
46
48
|
/** A repo and the directory it is checked out at. */
|
|
47
49
|
export interface Checkout {
|
|
@@ -28,6 +28,8 @@ export interface RecordWatchOpts {
|
|
|
28
28
|
/** message subscriptions only: 'final' (the default) or 'result:<request message_id>' */
|
|
29
29
|
endsOn?: string;
|
|
30
30
|
readHeadsKey?: string;
|
|
31
|
+
/** The app that made this watch through `link`: `publish` then delivers the watched object's events past the watcher's read gate (see `linkAuthorizes`). */
|
|
32
|
+
linkApp?: ObjectRow;
|
|
31
33
|
}
|
|
32
34
|
export declare function configToJson(config: ListenConfig): string;
|
|
33
35
|
export declare function configFromJson(json: string): ListenConfig;
|
|
@@ -35,6 +37,8 @@ export declare function configFromJson(json: string): ListenConfig;
|
|
|
35
37
|
* a pattern already held without an end covers any new one, except that a tree start
|
|
36
38
|
* still adds its own subscription when only non-tree ones hold the pattern. */
|
|
37
39
|
export declare function recordWatch(db: DatabaseSync, watcher: ObjectRow, object: ObjectRow, start: WatchStart, opts?: RecordWatchOpts): void;
|
|
40
|
+
/** Mark the existing watch of `object` by `watcher` as made through `link` by `app`, changing nothing else about it. */
|
|
41
|
+
export declare function markLinkWatch(db: DatabaseSync, watcher: ObjectRow, object: ObjectRow, app: ObjectRow): void;
|
|
38
42
|
/** Remove the whole watch, the subscriptions with one pattern, or (tree-only form) the
|
|
39
43
|
* subscriptions one start made. A watch left with no subscription is deleted. */
|
|
40
44
|
export declare function unwatch(db: DatabaseSync, watcher: ObjectRow, object: ObjectRow, which?: string | {
|
|
@@ -1,18 +1,19 @@
|
|
|
1
|
-
var D=Object.defineProperty;var c=(e,t)=>D(e,"name",{value:t,configurable:!0});import{prepared as i}from"../sqlite-statements.js";import{canRead as
|
|
2
|
-
ON CONFLICT (watcher, object) DO UPDATE SET read_heads_key = COALESCE(excluded.read_heads_key, read_heads_key)
|
|
3
|
-
|
|
1
|
+
var D=Object.defineProperty;var c=(e,t)=>D(e,"name",{value:t,configurable:!0});import{prepared as i}from"../sqlite-statements.js";import{canRead as O}from"./access.js";import{receiverForObject as j}from"./receivers.js";import{getByRowid as S}from"./objects.js";const p=["spawn","job-start","ask"],y=["report.final","report.result","notice.>"];function N(e){return JSON.stringify(Object.fromEntries(e.map(t=>[String(t.threshold),t.delivery])))}c(N,"configToJson");function I(e){const t=JSON.parse(e);return Object.entries(t).map(([o,r])=>({threshold:Number(o),delivery:r})).sort((o,r)=>r.threshold-o.threshold)}c(I,"configFromJson");function _(e,t,o){return i(e,"SELECT * FROM watches WHERE watcher = ? AND object = ?").get(t.rowid,o.rowid)}c(_,"watchRow");function d(e,t){return i(e,"SELECT * FROM watch_subscriptions WHERE watch = ? ORDER BY rowid").all(t)}c(d,"subsOf");function h(e,t){const o=d(e,t);if(o.length===0){i(e,"DELETE FROM watches WHERE rowid = ?").run(t);return}const r=o.some(n=>p.includes(n.started_by))?1:0;i(e,"UPDATE watches SET in_tree = ? WHERE rowid = ? AND in_tree != ?").run(r,t,r)}c(h,"settle");function g(e,t){return e==="message"?y:t.patterns?.length?t.patterns:[">"]}c(g,"defaultPatterns");function F(e,t,o,r,n={}){if(r==="fork")throw new Error("recordWatch: fork watches are made by copyWatchesForFork");if(t.type.includes(".")&&!j(e,t))throw new Error(`custom object ${t.id} has no receiver`);const a=new Date().toISOString();i(e,`INSERT INTO watches (watcher, object, in_tree, read_heads_key, created_at, link_app) VALUES (?, ?, 0, ?, ?, ?)
|
|
2
|
+
ON CONFLICT (watcher, object) DO UPDATE SET read_heads_key = COALESCE(excluded.read_heads_key, read_heads_key),
|
|
3
|
+
link_app = COALESCE(excluded.link_app, link_app)`).run(t.rowid,o.rowid,n.readHeadsKey??null,a,n.linkApp?.rowid??null);const s=_(e,t,o),E=r==="message"?n.endsOn??"final":null,f=p.includes(r),w=d(e,s.rowid),u=i(e,`INSERT INTO watch_subscriptions (watch, pattern, filter_json, config_json, started_by, ends_on, created_at)
|
|
4
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`);for(const R of g(r,n))w.some(l=>l.pattern===R&&(l.ends_on===null||l.ends_on===E)&&(!f||p.includes(l.started_by)))||u.run(s.rowid,R,n.filter===void 0?null:JSON.stringify(n.filter),n.config?N(n.config):null,r,E,a);h(e,s.rowid)}c(F,"recordWatch");function H(e,t,o,r){i(e,"UPDATE watches SET link_app = ? WHERE watcher = ? AND object = ?").run(r.rowid,t.rowid,o.rowid)}c(H,"markLinkWatch");function M(e,t,o,r){const n=_(e,t,o);n&&(r===void 0?i(e,"DELETE FROM watch_subscriptions WHERE watch = ?").run(n.rowid):typeof r=="string"?i(e,"DELETE FROM watch_subscriptions WHERE watch = ? AND pattern = ?").run(n.rowid,r):i(e,"DELETE FROM watch_subscriptions WHERE watch = ? AND started_by = ?").run(n.rowid,r.startedBy),h(e,n.rowid))}c(M,"unwatch");function x(e,t,o,r,n){const a=_(e,t,o),s=a?i(e,"UPDATE watch_subscriptions SET config_json = ? WHERE watch = ? AND pattern = ?").run(n?N(n):null,a.rowid,r).changes:0;if(Number(s)===0)throw new Error(`${t.id} has no subscription ${r} on ${o.id}`)}c(x,"setSubscriptionConfig");function U(e,t,o){const r=typeof o=="string"?i(e,`DELETE FROM watch_subscriptions WHERE started_by = 'message'
|
|
4
5
|
AND watch IN (SELECT rowid FROM watches WHERE object = ?) RETURNING watch`).all(t.rowid):i(e,`DELETE FROM watch_subscriptions WHERE started_by = 'message' AND ends_on = ?
|
|
5
|
-
AND watch IN (SELECT rowid FROM watches WHERE object = ?) RETURNING watch`).all(`result:${o.result}`,t.rowid);for(const n of new Set(r.map(a=>a.watch)))h(e,n)}c(
|
|
6
|
+
AND watch IN (SELECT rowid FROM watches WHERE object = ?) RETURNING watch`).all(`result:${o.result}`,t.rowid);for(const n of new Set(r.map(a=>a.watch)))h(e,n)}c(U,"endMessageSubscriptions");function k(e,t){const o=S(e,t.watcher),r=S(e,t.object);return!o||!r?null:{rowid:t.rowid,watcher:o,object:r,in_tree:t.in_tree===1,read_heads_key:t.read_heads_key,created_at:t.created_at,subscriptions:d(e,t.rowid).map(n=>({rowid:n.rowid,pattern:n.pattern,started_by:n.started_by,ends_on:n.ends_on,created_at:n.created_at,filter:n.filter_json===null?null:JSON.parse(n.filter_json),config:n.config_json===null?null:I(n.config_json)}))}}c(k,"hydrate");function T(e,t,o){return o.map(r=>k(e,r)).filter(r=>r!==null&&O(e,t,r.watcher)&&O(e,t,r.object))}c(T,"visible");function J(e,t,o){return T(e,t,i(e,"SELECT * FROM watches WHERE watcher = ? ORDER BY rowid").all(o.rowid))}c(J,"watchesOf");function B(e,t,o){return T(e,t,i(e,"SELECT * FROM watches WHERE object = ? ORDER BY rowid").all(o.rowid))}c(B,"watchersOf");function v(e,t){return i(e,`WITH RECURSIVE walk(rowid) AS (
|
|
6
7
|
SELECT ?
|
|
7
8
|
UNION
|
|
8
9
|
SELECT w.object FROM walk CROSS JOIN watches w INDEXED BY watches_tree_down ON w.watcher = walk.rowid AND w.in_tree = 1
|
|
9
10
|
) SELECT o.* FROM walk CROSS JOIN objects o ON o.rowid = walk.rowid
|
|
10
|
-
WHERE o.type = 'node' AND o.rowid != ?`).all(t.rowid,t.rowid)}c(
|
|
11
|
+
WHERE o.type = 'node' AND o.rowid != ?`).all(t.rowid,t.rowid)}c(v,"descendants");function Y(e,t){return i(e,`WITH RECURSIVE walk(rowid) AS (
|
|
11
12
|
SELECT ?
|
|
12
13
|
UNION
|
|
13
14
|
SELECT w.watcher FROM walk CROSS JOIN watches w INDEXED BY watches_tree_up ON w.object = walk.rowid AND w.in_tree = 1
|
|
14
15
|
) SELECT o.* FROM walk CROSS JOIN objects o ON o.rowid = walk.rowid
|
|
15
|
-
WHERE o.type = 'node' AND o.rowid != ?`).all(t.rowid,t.rowid)}c(
|
|
16
|
-
WHERE w.object = ? AND w.in_tree = 1 ORDER BY w.rowid`).all(t.rowid)}c(
|
|
16
|
+
WHERE o.type = 'node' AND o.rowid != ?`).all(t.rowid,t.rowid)}c(Y,"ancestors");function P(e,t){return i(e,`SELECT o.* FROM watches w INDEXED BY watches_tree_up CROSS JOIN objects o ON o.rowid = w.watcher
|
|
17
|
+
WHERE w.object = ? AND w.in_tree = 1 ORDER BY w.rowid`).all(t.rowid)}c(P,"treeWatchersOf");function V(e,t,o){const r=new Date().toISOString(),n=i(e,"SELECT * FROM watches WHERE watcher = ?").all(t.rowid),a=i(e,`INSERT INTO watch_subscriptions (watch, pattern, filter_json, config_json, started_by, ends_on, created_at)
|
|
17
18
|
VALUES (?, ?, ?, ?, 'fork', ?, ?)`);for(const s of n){if(s.object===o.rowid)continue;i(e,`INSERT INTO watches (watcher, object, in_tree, read_heads_key, created_at) VALUES (?, ?, 0, ?, ?)
|
|
18
|
-
ON CONFLICT (watcher, object) DO NOTHING`).run(o.rowid,s.object,s.read_heads_key,r);const E=_(e,o,{rowid:s.object}),f=d(e,E.rowid);for(const w of d(e,s.rowid))f.some(u=>u.pattern===w.pattern&&u.ends_on===w.ends_on)||a.run(E.rowid,w.pattern,w.filter_json,w.config_json,w.ends_on,r);h(e,E.rowid)}}c(V,"copyWatchesForFork");export{
|
|
19
|
+
ON CONFLICT (watcher, object) DO NOTHING`).run(o.rowid,s.object,s.read_heads_key,r);const E=_(e,o,{rowid:s.object}),f=d(e,E.rowid);for(const w of d(e,s.rowid))f.some(u=>u.pattern===w.pattern&&u.ends_on===w.ends_on)||a.run(E.rowid,w.pattern,w.filter_json,w.config_json,w.ends_on,r);h(e,E.rowid)}}c(V,"copyWatchesForFork");export{Y as ancestors,I as configFromJson,N as configToJson,V as copyWatchesForFork,v as descendants,U as endMessageSubscriptions,H as markLinkWatch,F as recordWatch,x as setSubscriptionConfig,P as treeWatchersOf,M as unwatch,B as watchersOf,J as watchesOf};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { normalizeMachineId } from './names.js';
|
|
1
2
|
import { type TokenCache } from './token.js';
|
|
2
3
|
/** How long a machine's reach answer is trusted. */
|
|
3
4
|
export declare const REACH_TTL_MS: number;
|
|
@@ -56,7 +57,7 @@ export interface DirectoryClient {
|
|
|
56
57
|
/** Forget one cached reach answer, or all. */
|
|
57
58
|
invalidate(machineId?: string): void;
|
|
58
59
|
}
|
|
59
|
-
export
|
|
60
|
+
export { normalizeMachineId };
|
|
60
61
|
export declare function createDirectoryClient(deps: DirectoryDeps, ownId: () => string): DirectoryClient;
|
|
61
62
|
/** This machine's id as the directory names it (`runtime:<id>`). */
|
|
62
63
|
export declare function ownMachineId(): string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
var
|
|
1
|
+
var $=Object.defineProperty;var o=(t,e)=>$(t,"name",{value:e,configurable:!0});import{normalizeMachineId as m,rememberMachineName as y}from"./names.js";import{prepared as _}from"../sqlite-statements.js";import{openDb as C,withCanvasWrite as M}from"../canvas/db.js";import{readConfig as S}from"../config.js";import{runtimeClient as g}from"../grants/sync.js";import{crtrVersion as I}from"../../shared/crtr-version.js";import{installedPluginIds as R}from"../directory/join.js";import{peerTokens as A}from"./token.js";const b=5*6e4,O=1e4;class c extends Error{static{o(this,"DirectoryError")}status;constructor(e,i){super(i),this.status=e,this.name="DirectoryError"}}function E(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}o(E,"asReach");function T(t){const e=t,i=E(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,kind:e.kind==="device"?"device":"runtime",plugins:Array.isArray(e.plugins)?e.plugins.filter(u=>typeof u=="string"):[],...typeof e.is_default=="boolean"?{is_default:e.is_default}:{}}}o(T,"asMachine");function U(t,e){const i=new Map;let u=null;async function d(s,a,r){const n=t.issuer();if(n===null)throw new c(0,"this daemon has no directory");const f=n.replace(/\/$/,"");for(let w=0;;w++){let l;try{const h=await t.tokens.get(f);l=await t.fetch(`${f}${a}`,{method:s,headers:{authorization:`Bearer ${h}`,...r===void 0?{}:{"content-type":"application/json"}},...r===void 0?{}:{body:JSON.stringify(r)},signal:AbortSignal.timeout(O)})}catch(h){throw new c(0,`the directory could not be reached: ${h instanceof Error?h.message:String(h)}`)}if(l.status===401&&w===0){t.tokens.drop(f);continue}if(!l.ok)throw new c(l.status,`directory ${s} ${a} answered ${l.status}`);try{return await l.json()}catch{throw new c(502,`directory ${s} ${a} answered a body that is not JSON`)}}}return o(d,"call"),{async list(){const s=await d("GET","/v1/machines");if(!Array.isArray(s))throw new c(502,"the directory answered a machine list that is not an array");const a=s.map(T),r=t.now();for(const n of a)i.set(n.machine_id,{machine:n,at:r}),y(n.machine_id,n.name);return u={machines:a,at:r},a},lastList(){return u===null?null:u.machines},async reach(s){const a=m(s),r=i.get(a);if(r&&t.now()-r.at<b)return r.machine;try{const n=T(await d("GET",`/v1/machines/${encodeURIComponent(a)}/reach`));return i.set(a,{machine:n,at:t.now()}),y(n.machine_id,n.name),n}catch(n){if(n instanceof c&&n.status===404)return i.delete(a),null;if(r&&n instanceof c&&n.status===0)return r.machine;throw n}},async status(s){const a=e(),r=await d("PUT",`/v1/machines/${encodeURIComponent(a)}/status`,s);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:E(r.reach)}},invalidate(s){s===void 0?(i.clear(),u=null):i.delete(m(s))}}}o(U,"createDirectoryClient");function k(){return m(g().runtime)}o(k,"ownMachineId");function v(){return S("user").runtime?.issuer??null}o(v,"directoryIssuer");const p=U({issuer:v,tokens:A,fetch:o((t,e)=>fetch(t,e),"fetch"),now:Date.now},k),V=o(()=>p.list(),"listMachines"),W=o(t=>p.reach(t),"machineReach"),x="machine_url";function Y(){return _(C(),"SELECT value FROM runtime_state WHERE key = ?").get(x)?.value??null}o(Y,"machineUrl");function D(t){M(e=>{_(e,"INSERT INTO runtime_state (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value").run(x,t)})}o(D,"storeMachineUrl");async function B(t=p){if(v()===null)return null;try{g()}catch{return null}const e=await t.status({plugins:R(),crtr_version:I()});if(e.machine_id!==k())throw new c(502,"the directory answered a status for another machine");return D(e.url),e}o(B,"reportMachineStatus");export{O as DIRECTORY_CALL_TIMEOUT_MS,c as DirectoryError,b as REACH_TTL_MS,U as createDirectoryClient,p as directory,v as directoryIssuer,V as listMachines,W as machineReach,Y as machineUrl,m as normalizeMachineId,k as ownMachineId,B as reportMachineStatus};
|
|
@@ -15,7 +15,12 @@ export interface IngestResult {
|
|
|
15
15
|
}
|
|
16
16
|
/** The delivery key prefix of one sender's message; a delivery's full key adds `:<destination>`. */
|
|
17
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.
|
|
18
|
+
/** Apply a batch from machine `via`. One event's failure rejects only that event. Runs in the caller's write transaction.
|
|
19
|
+
*
|
|
20
|
+
* The home sends one row per destination while the receipt is per message, so the destination's half is handled for every row:
|
|
21
|
+
* an app-made watch of a remote node's object is recorded here as the destination's own watch of the stub (start `act`, all events,
|
|
22
|
+
* never changing a watch that exists), and a row whose message was already applied for another destination is published once more to
|
|
23
|
+
* that destination alone. The delivery key `peer:<via>:<message_id>:<destination>` keeps each destination to one delivery. */
|
|
19
24
|
export declare function ingestPeerEvents(db: DatabaseSync, via: string, events: readonly PeerEventPayload[]): IngestResult;
|
|
20
25
|
/** Whether `object` is a stub of `machine` (a sender may only describe what it homes). */
|
|
21
26
|
export declare const isStubOf: (db: DatabaseSync, object: ObjectRow, machine: string) => boolean;
|
package/dist/core/peer/ingest.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var
|
|
1
|
+
var g=Object.defineProperty;var c=(e,t)=>g(e,"name",{value:t,configurable:!0});import{prepared as d}from"../sqlite-statements.js";import{CrtrError as k}from"../errors.js";import{getById as S,getByRowid as L}from"../graph/objects.js";import{publish as O}from"../graph/events.js";import{markLinkWatch as h,recordWatch as x}from"../graph/watches.js";import{ackRows as A,claimRows as T}from"./outbox-store.js";import{ensureStub as E,stubOf as f}from"./stubs.js";const V=100,y=c((e,t)=>`peer:${e}:${t}`,"ingestKey");function $(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}c($,"applied");const m=c((e,t,n)=>{d(e,"INSERT OR IGNORE INTO peer_ingested_destinations (key, ingested_at) VALUES (?, ?)").run(`${t}:${n}`,Date.now())},"markDestination");function w(e,t,n){return d(e,"SELECT 1 FROM peer_ingested_destinations WHERE key = ?").get(`${t}:${n}`)!==void 0||d(e,"SELECT 1 FROM deliveries WHERE idempotency_key = ?").get(`${t}:${n}`)!==void 0}c(w,"destinationDone");function N(e,t,n){return E(e,t.id,t.type,n,void 0,t)}c(N,"actorFor");function _(e,t,n,r,i){const s=n.actor===null?null:N(e,n.actor,t),u=n.body_text,o=n.body===void 0&&u===void 0?void 0:{...n.body??{},...u===void 0?{}:{body:u}};O(e,{event:n.event,object:r,actor:s,level:n.level,attrs:n.attrs,...o===void 0?{}:{messageBody:o},idempotencyKey:y(t,n.message_id),...i===void 0?{}:{onlyWatchers:i}})}c(_,"apply");function I(e,t){const n=`ev:${t.message_id}:`;if(!t.key.startsWith(n)||t.key.length===n.length)return null;const r=S(e,t.key.slice(n.length));return r!==null&&r.type==="node"&&r.deleted_at===null&&f(e,r)===null?r:null}c(I,"localDestination");function K(e,t,n){const r={accepted:[],rejected:[]};for(const i of n){const s=y(t,i.message_id),u=$(e,s);e.exec("SAVEPOINT peer_event");try{const o=I(e,i);if(u&&o===null){e.exec("RELEASE peer_event"),r.accepted.push(i.key);continue}const l=E(e,i.object.id,i.object.type,t,void 0,i.object),a=i.linked===!0&&o!==null&&l.owner!==null?L(e,l.owner):null,p=a!==null&&a.type==="app"?a:null;if(o!==null&&d(e,"SELECT 1 FROM watches WHERE watcher = ? AND object = ?").get(o.rowid,l.rowid)===void 0?x(e,o,l,"act",{patterns:[">"],...p===null?{}:{linkApp:p}}):o!==null&&p!==null&&h(e,o,l,p),u)w(e,s,o.id)||_(e,t,i,l,[o.id]);else{_(e,t,i,l),d(e,"INSERT OR IGNORE INTO peer_ingested (key, ingested_at) VALUES (?, ?)").run(s,Date.now());for(const R of d(e,"SELECT destination_id FROM deliveries WHERE idempotency_key >= ? AND idempotency_key < ?").all(`${s}:`,`${s};`))m(e,s,R.destination_id)}o!==null&&m(e,s,o.id),e.exec("RELEASE peer_event"),r.accepted.push(i.key)}catch(o){e.exec("ROLLBACK TO peer_event"),e.exec("RELEASE peer_event");const l=o instanceof k?o.code:"invalid_event";r.rejected.push({key:i.key,code:l,message:o instanceof Error?o.message:String(o)})}}return r}c(K,"ingestPeerEvents");const P=c((e,t,n)=>f(e,t)?.home_machine===n,"isStubOf"),v=c((e,t,n)=>T(e,t,n),"claimFor"),G=c((e,t,n)=>A(e,t,n),"ackFor");export{V as MAX_EVENTS_PER_CALL,G as ackFor,v as claimFor,y as ingestKey,K as ingestPeerEvents,P as isStubOf};
|
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
export declare const rememberMachineName: (machineId: string, name: string) => void;
|
|
2
2
|
/** What to call a machine in text a person reads: its directory name when this daemon has seen it, else its id. */
|
|
3
3
|
export declare const machineNameOf: (machineId: string) => string;
|
|
4
|
+
/** A machine id as the directory spells it: `runtime:<id>`. Here, not in the directory client, so graph code can use it without importing the client. */
|
|
5
|
+
export declare const normalizeMachineId: (id: string) => string;
|
package/dist/core/peer/names.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var
|
|
1
|
+
var a=Object.defineProperty;var t=(e,n)=>a(e,"name",{value:n,configurable:!0});const r=new Map,o=t((e,n)=>{r.set(e,n)},"rememberMachineName"),s=t(e=>r.get(e)??e,"machineNameOf"),c=t(e=>e.startsWith("runtime:")?e:`runtime:${e}`,"normalizeMachineId");export{s as machineNameOf,c as normalizeMachineId,o as rememberMachineName};
|
|
@@ -15,6 +15,8 @@ export interface PeerEventPayload {
|
|
|
15
15
|
body?: Record<string, unknown>;
|
|
16
16
|
/** The text of the report document the event points at, inlined because the receiver cannot read the sender's documents. */
|
|
17
17
|
body_text?: string;
|
|
18
|
+
/** The home delivered this to the destination on the strength of an app's `link` (not the destination's read access): the receiver records the same link on its half of the watch. */
|
|
19
|
+
linked?: true;
|
|
18
20
|
}
|
|
19
21
|
export interface OutboxRow {
|
|
20
22
|
id: number;
|
|
@@ -1 +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
|
|
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 $={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),...e.linked?{linked:!0}:{}}})}}};export{g as eventKey,v as payloadFor,$ as peerReceiver};
|