@north-light/cli 0.3.549 → 0.3.550
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/contract.d.ts +1 -1
- package/dist/api/contract.js +1 -1
- package/dist/api/dto/custom-objects.d.ts +1 -1
- package/dist/api/dto/docs.d.ts +12 -0
- package/dist/api/dto/objects.d.ts +17 -1
- package/dist/api/dto/objects.js +1 -1
- package/dist/api/dto/permissions.d.ts +76 -0
- package/dist/api/dto/permissions.js +1 -0
- package/dist/api/index.d.ts +3 -2
- package/dist/api/index.js +1 -1
- package/dist/api/routes.d.ts +4 -0
- package/dist/api/routes.js +1 -1
- package/dist/builtin-memory/crouter-concepts/documents.md +1 -1
- package/dist/builtin-memory/crouter-concepts/scopes-and-trust.md +2 -2
- package/dist/builtin-memory/crouter-sdk/release-notes.md +11 -0
- package/dist/builtin-memory/internal/INDEX.md +2 -1
- package/dist/builtin-memory/internal/agent-shaping.md +1 -1
- package/dist/builtin-memory/internal/storage-tiers.md +1 -1
- package/dist/builtin-memory/northlight-apps/guides/files-and-documents.md +3 -3
- package/dist/builtin-memory/northlight-apps/guides/many-users.md +1 -1
- package/dist/builtin-memory/northlight-apps/guides/test-your-app.md +1 -1
- package/dist/clients/attach/viewer.js +9 -9
- package/dist/commands/canvas/list.js +2 -2
- package/dist/commands/canvas/share.d.ts +2 -0
- package/dist/commands/canvas/share.js +2 -0
- package/dist/commands/canvas.js +1 -1
- package/dist/commands/doc/edit.js +1 -1
- package/dist/commands/doc/shared.js +1 -1
- package/dist/commands/doc/write.js +1 -1
- package/dist/commands/sys/sync-skills.js +2 -2
- package/dist/core/canvas/canvas.js +20 -20
- package/dist/core/canvas/db.js +4 -4
- package/dist/core/canvas/export-sqlite.js +2 -2
- package/dist/core/canvas/import-sqlite.js +5 -5
- package/dist/core/canvas/outcome-actions.js +3 -3
- package/dist/core/canvas/person.js +11 -9
- package/dist/core/canvas/schema-pg.sql +49 -5
- package/dist/core/environments.js +1 -1
- package/dist/core/feed/feed.js +1 -1
- package/dist/core/grants/mirror.d.ts +9 -1
- package/dist/core/grants/mirror.js +2 -2
- package/dist/core/grants/sync.js +1 -1
- package/dist/core/graph/access.d.ts +8 -49
- package/dist/core/graph/access.js +4 -7
- package/dist/core/graph/custom.d.ts +5 -2
- package/dist/core/graph/custom.js +9 -9
- package/dist/core/graph/deletion.js +5 -4
- package/dist/core/graph/documents.d.ts +12 -3
- package/dist/core/graph/documents.js +13 -7
- package/dist/core/graph/edges.js +3 -3
- package/dist/core/graph/events.js +12 -8
- package/dist/core/graph/kinds/document.js +4 -4
- package/dist/core/graph/kinds/kind.js +1 -1
- package/dist/core/graph/kinds/node.js +5 -5
- package/dist/core/graph/names.d.ts +16 -1
- package/dist/core/graph/names.js +11 -3
- package/dist/core/graph/objects.d.ts +19 -2
- package/dist/core/graph/objects.js +16 -10
- package/dist/core/graph/package-docs.js +5 -5
- package/dist/core/graph/package-files.js +2 -2
- package/dist/core/graph/reader.js +3 -3
- package/dist/core/graph/repo-sync/exchange.js +2 -2
- package/dist/core/graph/rules.d.ts +7 -5
- package/dist/core/graph/rules.js +3 -3
- package/dist/core/graph/types.d.ts +12 -1
- package/dist/core/graph/types.js +1 -1
- package/dist/core/graph/watches.d.ts +1 -1
- package/dist/core/graph/watches.js +9 -8
- package/dist/core/peer/envelope.d.ts +1 -1
- package/dist/core/peer/envelope.js +1 -1
- package/dist/core/peer/stubs.d.ts +1 -1
- package/dist/core/peer/stubs.js +1 -1
- package/dist/core/permissions/boot.d.ts +17 -0
- package/dist/core/permissions/boot.js +18 -0
- package/dist/core/permissions/builtin.d.ts +8 -0
- package/dist/core/permissions/builtin.js +20 -0
- package/dist/core/permissions/ceiling.d.ts +57 -0
- package/dist/core/permissions/ceiling.js +22 -0
- package/dist/core/permissions/compile.d.ts +14 -0
- package/dist/core/permissions/compile.js +1 -0
- package/dist/core/permissions/errors.d.ts +7 -0
- package/dist/core/permissions/errors.js +1 -0
- package/dist/core/permissions/evaluator.d.ts +28 -0
- package/dist/core/permissions/evaluator.js +6 -0
- package/dist/core/permissions/facts.d.ts +56 -0
- package/dist/core/permissions/facts.js +2 -0
- package/dist/core/permissions/schema-parser.d.ts +29 -0
- package/dist/core/permissions/schema-parser.js +2 -0
- package/dist/core/permissions/schemas.d.ts +21 -0
- package/dist/core/permissions/schemas.js +2 -0
- package/dist/core/permissions/sql.d.ts +16 -0
- package/dist/core/permissions/sql.js +1 -0
- package/dist/core/runtime/broker/frame-client.d.ts +1 -1
- package/dist/core/runtime/broker/frame-client.js +1 -1
- package/dist/core/runtime/broker/frame-dispatch.d.ts +1 -1
- package/dist/core/runtime/broker/frame-dispatch.js +1 -1
- package/dist/core/runtime/broker/frame-read.d.ts +4 -3
- package/dist/core/runtime/broker/frame-read.js +1 -1
- package/dist/core/runtime/broker-protocol.d.ts +1 -1
- package/dist/core/runtime/host.js +3 -3
- package/dist/core/runtime/placement.js +1 -1
- package/dist/core/runtime/roadmap.js +1 -1
- package/dist/core/substrate/delivery/corpus.d.ts +2 -2
- package/dist/core/substrate/delivery/corpus.js +3 -3
- package/dist/core/substrate/delivery/deliver.js +2 -2
- package/dist/core/substrate/delivery/render-event.js +6 -6
- package/dist/core/substrate/subject-fields.d.ts +2 -0
- package/dist/core/substrate/subject.js +1 -1
- package/dist/daemon/api/app-listener.js +12 -12
- package/dist/daemon/api/bridge.d.ts +1 -1
- package/dist/daemon/api/bridge.js +4 -4
- package/dist/daemon/api/handlers/attach.d.ts +4 -1
- package/dist/daemon/api/handlers/attach.js +1 -1
- package/dist/daemon/api/handlers/canvas.js +5 -5
- package/dist/daemon/api/handlers/check.d.ts +2 -0
- package/dist/daemon/api/handlers/check.js +1 -0
- package/dist/daemon/api/handlers/custom-objects.js +2 -2
- package/dist/daemon/api/handlers/docs.js +1 -1
- package/dist/daemon/api/handlers/files.js +1 -1
- package/dist/daemon/api/handlers/memory.js +7 -6
- package/dist/daemon/api/handlers/nodes.js +1 -1
- package/dist/daemon/api/handlers/objects.js +3 -3
- package/dist/daemon/api/handlers/relations.d.ts +2 -0
- package/dist/daemon/api/handlers/relations.js +1 -0
- package/dist/daemon/api/handlers/schemas.d.ts +12 -0
- package/dist/daemon/api/handlers/schemas.js +1 -0
- package/dist/daemon/api/handlers/shares.d.ts +2 -0
- package/dist/daemon/api/handlers/shares.js +1 -0
- package/dist/daemon/api/operations.js +1 -1
- package/dist/daemon/api/router.d.ts +10 -6
- package/dist/daemon/api/router.js +1 -1
- package/dist/daemon/api/server.js +1 -1
- package/dist/daemon/crtrd.js +12 -12
- package/dist/daemon/reconcilers/delivery-lane.js +2 -2
- package/dist/migrations/004-canvas-documents/fields.d.ts +1 -1
- package/dist/migrations/004-canvas-documents/fields.js +2 -2
- package/docs/apps/guides/files-and-documents.md +3 -3
- package/docs/apps/guides/many-users.md +1 -1
- package/docs/apps/guides/test-your-app.md +1 -1
- package/docs/cli/documents-and-preferences.md +1 -1
- package/docs/concepts/documents.md +1 -1
- package/docs/concepts/scopes-and-trust.md +2 -2
- package/docs/sdk/release-notes.md +11 -0
- package/package.json +2 -2
- package/packages/crouter-identity/dist/index.js +14 -1
- package/packages/crouter-identity/package.json +1 -1
- package/runtime.lock.json +13 -13
- package/dist/core/graph/spaces/app.d.ts +0 -3
- package/dist/core/graph/spaces/app.js +0 -1
- package/dist/core/graph/spaces/index.d.ts +0 -18
- package/dist/core/graph/spaces/index.js +0 -1
- package/dist/core/graph/spaces/own-memory.d.ts +0 -18
- package/dist/core/graph/spaces/own-memory.js +0 -1
- package/dist/core/graph/spaces/person.d.ts +0 -5
- package/dist/core/graph/spaces/person.js +0 -3
- package/dist/core/graph/spaces/repo.d.ts +0 -14
- package/dist/core/graph/spaces/repo.js +0 -1
- package/dist/core/runs/confinement.d.ts +0 -25
- package/dist/core/runs/confinement.js +0 -2
package/dist/core/peer/stubs.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var T=Object.defineProperty;var a=(n,e)=>T(n,"name",{value:e,configurable:!0});import{createObject as O,getById as c,ownerForGrantee as
|
|
1
|
+
var T=Object.defineProperty;var a=(n,e)=>T(n,"name",{value:e,configurable:!0});import{createObject as O,getById as c,ownerForGrantee as _}from"../graph/objects.js";import{readLegacyPrivacy as x}from"../graph/types.js";import{upsertKind as A}from"../graph/custom.js";import{CrtrError as w}from"../errors.js";import{ExitCode as d}from"../../types.js";import{normalizeMachineId as b}from"./names.js";import{personId as g,personIdAsync as H}from"../canvas/person.js";import{writeActsFor as N}from"../permissions/facts.js";const W=["public","private"],D=a(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");async function h(n,e){const i=typeof e=="string"?await c(n,e):e;if(i===null)return null;const t=await n.prepare("SELECT home_machine, envelope_json FROM peer_stubs WHERE object = ?").get(i.id);return t?{object:i,home_machine:t.home_machine,identity:t.envelope_json===null?null:JSON.parse(t.envelope_json)}:null}a(h,"stubOf");const G=a(async(n,e)=>await n.prepare("SELECT 1 FROM peer_stubs WHERE object = ?").get(e.id)!==void 0,"isStub");async function E(n){return await _(n,g())}a(E,"localPerson");async function f(n,e){return await _(n,e)}a(f,"localApp");async function K(n,e){return e.type==="app"?await f(n,`app:${e.name}`):e.type==="person"?await E(n):null}a(K,"localOwnerActor");async function v(n,e,i){if(e!==null)return e.app===null?await E(n):await f(n,e.app);const t=i?.owner;return t?.type==="app"?await f(n,t.external_id??`app:${t.handle}`):await E(n)}a(v,"ownerFor");async function P(n,e,i,t,m,r){const p=m===void 0?null:D(m),s=x(r?.privacy??"public");if(!W.includes(s))throw new w("invalid_event",`invalid privacy ${JSON.stringify(s)}`,d.GENERAL,{id:e});const o=await c(n,e);if(o!==null){const l=await h(n,o);if(l===null)throw new w("conflict",`${e} is a local ${o.type}, not a stub of ${t}`,d.GENERAL,{id:e});if(l.home_machine!==t)throw new w("conflict",`${e} is already a stub of ${l.home_machine}, not ${t}`,d.GENERAL,{id:e,home_machine:l.home_machine});return p!==null&&(await n.prepare("UPDATE peer_stubs SET envelope_json = ? WHERE object = ?").run(JSON.stringify(p),o.id),await n.prepare("UPDATE objects SET reception = 2 WHERE id = ?").run(o.id)),r!==void 0&&(await S(n,i,r),r.privacy!==void 0&&o.privacy!==s)?(await n.prepare("UPDATE objects SET privacy = ? WHERE id = ?").run(s,o.id),await c(n,o.id)):o}const y=await v(n,p,r);await S(n,i,r);const u=await O(n,{id:e,type:i,kind:r?.kind??null,owner:y.id,privacy:s,name:r?.name??e,preview:r?.preview??"",home_machine:t});return await n.prepare("INSERT INTO peer_stubs (object, home_machine, envelope_json, created_at) VALUES (?, ?, ?, ?)").run(u.id,t,p===null?null:JSON.stringify(p),new Date().toISOString()),y.type==="app"&&await N(n,u.id,await H()),p!==null&&await n.prepare("UPDATE objects SET reception = 2 WHERE id = ?").run(u.id),await c(n,u.id)}a(P,"ensureStub");async function S(n,e,i){const t=i?.kind_decl;t===void 0||t.name!==e||await A(n,{name:t.name,owner:await v(n,null,{...i,owner:t.owner}),description:t.description,events:t.events,read:null,source:"peer"})}a(S,"holdKind");async function V(n,e,i){const t=await P(n,e,"node",b(i));return await n.prepare("UPDATE objects SET reception = 2 WHERE id = ?").run(t.id),await c(n,t.id)}a(V,"ensureNodeStub");async function z(n,e,i){const t=await h(n,e);t===null||t.home_machine===i||(await n.prepare("UPDATE peer_stubs SET home_machine = ? WHERE object = ?").run(i,t.object.id),await n.prepare("UPDATE objects SET home_machine = ? WHERE id = ?").run(i,t.object.id))}a(z,"rehomeStub");async function B(n,e){const i=await j(n,e.owner===null?null:await c(n,e.owner)),t=await R(n,e.type);return{id:e.id,type:e.type,kind:e.kind,name:e.name,preview:e.preview,owner:i,privacy:e.privacy,...t===null?{}:{kind_decl:t}}}a(B,"describeObject");async function j(n,e){if(e===null||e.type!=="person"&&e.type!=="app"&&e.type!=="repo"&&e.type!=="profile")return null;const i=await n.prepare("SELECT scheme, external_id FROM identities WHERE object = ?").get(e.id);return{id:e.id,type:e.type,scheme:i?.scheme??null,external_id:i?.external_id??null,handle:e.name}}a(j,"ownerRefOf");async function R(n,e){const i=await n.prepare("SELECT owner, description, events_json FROM object_kinds WHERE name = ?").get(e),t=i===void 0?null:await j(n,await c(n,i.owner));return i===void 0||t===null?null:{name:e,description:i.description,events:JSON.parse(i.events_json),owner:t}}a(R,"kindDeclarationOf");async function Y(n,e,i){const t=await R(n,e.type);return t===null?i:{...i,kind_decl:t}}a(Y,"withKindDecl");export{B as describeObject,V as ensureNodeStub,P as ensureStub,D as identityOfEnvelope,G as isStub,R as kindDeclarationOf,K as localOwnerActor,z as rehomeStub,h as stubOf,Y as withKindDecl};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type CanvasDb } from '../canvas/db.js';
|
|
2
|
+
export interface PermissionsBootCounts {
|
|
3
|
+
nodesApp: number;
|
|
4
|
+
reparented: number;
|
|
5
|
+
personActsFor: number;
|
|
6
|
+
actsFor: number;
|
|
7
|
+
reapedNodes: number;
|
|
8
|
+
shippedReaders: number;
|
|
9
|
+
seeded: boolean;
|
|
10
|
+
}
|
|
11
|
+
/** crtrd calls this once at boot, after the install's person is bound. */
|
|
12
|
+
export declare function runPermissionsBoot(): Promise<PermissionsBootCounts>;
|
|
13
|
+
/** nodes.app = the node's owner where null and the owner is an app. crtrd runs it before package reconciliation, whose publishes read every
|
|
14
|
+
* watching node's app, and before the person is bound; step 2 below reparents nodes under profiles, so it must run first. */
|
|
15
|
+
export declare function backfillNodeApps(db: CanvasDb): Promise<number>;
|
|
16
|
+
/** The steps inside the caller's transaction. `personPrincipal` is the install's person (`user:<id>`), the one every pre-existing object was made for. */
|
|
17
|
+
export declare function permissionsBoot(db: CanvasDb, personPrincipal: string): Promise<PermissionsBootCounts>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
var E=Object.defineProperty;var r=(e,o)=>E(e,"name",{value:o,configurable:!0});import{withCanvasWrite as O}from"../canvas/db.js";import{personIdAsync as u}from"../canvas/person.js";async function b(){return await O(async e=>await I(e,await u()))}r(b,"runPermissionsBoot");async function _(e){return(await e.prepare(`UPDATE nodes n SET app = o.owner FROM objects o JOIN objects a ON a.id = o.owner AND a.type = 'app'
|
|
2
|
+
WHERE o.id = n.node_id AND n.app IS NULL`).run()).changes}r(_,"backfillNodeApps");async function I(e,o){const t=new Date().toISOString(),s=o.replace(/^user:/,""),c=await _(e),N=(await e.prepare(`UPDATE objects o SET owner = p.id FROM nodes n
|
|
3
|
+
JOIN identities i ON i.scheme = 'profile' AND i.external_id = n.profile_id
|
|
4
|
+
JOIN objects p ON p.id = i.object AND p.type = 'profile' AND p.deleted_at IS NULL
|
|
5
|
+
WHERE o.id = n.node_id AND o.owner IS DISTINCT FROM p.id`).run()).changes,d=(await e.prepare(`INSERT INTO relations (resource_type, resource_id, relation, subject_type, subject_id, subject_relation, written_by, created_at)
|
|
6
|
+
SELECT 'crtr/object', o.id, 'acts_for', 'user', regexp_replace(i.external_id, '^user:', ''), '', 'boot', ?
|
|
7
|
+
FROM objects o JOIN identities i ON i.object = o.id AND i.scheme = 'user' WHERE o.type = 'person' ON CONFLICT DO NOTHING`).run(t)).changes;let a=0,i=0;const n=await e.prepare("SELECT value FROM canvas_meta WHERE key = 'permissions_seeded'").get();n===void 0&&(a=(await e.prepare(`INSERT INTO relations (resource_type, resource_id, relation, subject_type, subject_id, subject_relation, written_by, created_at)
|
|
8
|
+
SELECT 'crtr/object', o.id, 'acts_for', 'user', ?, '', 'boot', ? FROM objects o
|
|
9
|
+
WHERE o.deleted_at IS NULL AND ((o.type = 'repo' AND o.owner IS NULL) OR (o.owner IN (SELECT id FROM objects WHERE type = 'app')
|
|
10
|
+
AND NOT EXISTS (SELECT 1 FROM documents d WHERE d.object = o.id AND d.source_path IS NOT NULL)))
|
|
11
|
+
ON CONFLICT DO NOTHING`).run(s,t)).changes,i=(await e.prepare(`INSERT INTO relations (resource_type, resource_id, relation, subject_type, subject_id, subject_relation, written_by, created_at)
|
|
12
|
+
SELECT 'crtr/object', o.id, 'reader', 'user', '*', '', 'package', ? FROM objects o JOIN documents d ON d.object = o.id
|
|
13
|
+
WHERE o.deleted_at IS NULL AND d.source_path IS NOT NULL ON CONFLICT DO NOTHING`).run(t)).changes,await e.prepare("INSERT INTO canvas_meta (key, value) VALUES ('permissions_seeded', ?) ON CONFLICT (key) DO NOTHING").run(t));const p=(await e.prepare(`INSERT INTO relations (resource_type, resource_id, relation, subject_type, subject_id, subject_relation, written_by, created_at)
|
|
14
|
+
SELECT 'crtr/object', o.id, 'acts_for', 'user', ?, '', 'boot', ? FROM objects o
|
|
15
|
+
WHERE o.type = 'node' AND o.deleted_at IS NOT NULL AND o.owner IN (SELECT id FROM objects WHERE type = 'app')
|
|
16
|
+
AND EXISTS (SELECT 1 FROM objects d WHERE d.owner = o.id AND d.deleted_at IS NULL)
|
|
17
|
+
AND NOT EXISTS (SELECT 1 FROM relations r WHERE r.resource_type = 'crtr/object' AND r.resource_id = o.id AND r.relation = 'acts_for')
|
|
18
|
+
ON CONFLICT DO NOTHING`).run(s,t)).changes;return{nodesApp:c,reparented:N,personActsFor:d,actsFor:a,reapedNodes:p,shippedReaders:i,seeded:n===void 0}}r(I,"permissionsBoot");export{_ as backfillNodeApps,I as permissionsBoot,b as runPermissionsBoot};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type Definition } from './schema-parser.js';
|
|
2
|
+
/** The built-in definitions: every object's `crtr/object` rules, teams, and the subject types. `crtr` is a reserved namespace. */
|
|
3
|
+
export declare const BUILTIN_SOURCE = "\ndefinition user {}\ndefinition app {}\ndefinition crtr/group {\n relation member: user | crtr/group#member\n}\ndefinition crtr/object {\n relation parent: crtr/object\n relation acts_for: user\n relation reader: user | user:* | crtr/group#member\n relation editor: user | crtr/group#member\n relation messenger: user | crtr/group#member\n relation user_of: user | crtr/group#member\n permission read = reader + editor + messenger + user_of + acts_for + parent->read\n permission edit = editor + acts_for + parent->edit\n permission message = messenger + acts_for + parent->message\n permission use = user_of + acts_for + parent->use\n permission share = acts_for + parent->share\n}\n";
|
|
4
|
+
export declare const BUILTIN_DEFINITIONS: Definition[];
|
|
5
|
+
export declare const OBJECT_TYPE = "crtr/object";
|
|
6
|
+
export declare const GROUP_TYPE = "crtr/group";
|
|
7
|
+
/** Verb on the CLI → relation on `crtr/object`. */
|
|
8
|
+
export declare const VERB_RELATION: Record<string, string>;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import{parseSchema as r}from"./schema-parser.js";const e=`
|
|
2
|
+
definition user {}
|
|
3
|
+
definition app {}
|
|
4
|
+
definition crtr/group {
|
|
5
|
+
relation member: user | crtr/group#member
|
|
6
|
+
}
|
|
7
|
+
definition crtr/object {
|
|
8
|
+
relation parent: crtr/object
|
|
9
|
+
relation acts_for: user
|
|
10
|
+
relation reader: user | user:* | crtr/group#member
|
|
11
|
+
relation editor: user | crtr/group#member
|
|
12
|
+
relation messenger: user | crtr/group#member
|
|
13
|
+
relation user_of: user | crtr/group#member
|
|
14
|
+
permission read = reader + editor + messenger + user_of + acts_for + parent->read
|
|
15
|
+
permission edit = editor + acts_for + parent->edit
|
|
16
|
+
permission message = messenger + acts_for + parent->message
|
|
17
|
+
permission use = user_of + acts_for + parent->use
|
|
18
|
+
permission share = acts_for + parent->share
|
|
19
|
+
}
|
|
20
|
+
`,s=r(e),o="crtr/object",i="crtr/group",n={read:"reader",edit:"editor",message:"messenger",use:"user_of"};export{s as BUILTIN_DEFINITIONS,e as BUILTIN_SOURCE,i as GROUP_TYPE,o as OBJECT_TYPE,n as VERB_RELATION};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { CanvasDb } from '../canvas/db.js';
|
|
2
|
+
import type { Reader } from '../graph/types.js';
|
|
3
|
+
import { type Frag } from './sql.js';
|
|
4
|
+
/** How a candidate must sit under a root to pass through it. `depth` is the distance from the candidate up to the root (0 = the root itself). */
|
|
5
|
+
export type Match = 'any' | 'below' | 'direct-doc' | 'no-node' | 'node-tree' | 'objects-app' | 'node-report' | 'run-node' | 'custom' | 'shipped' | 'non-document' | 'run-report' | 'self' | `authored:${string}`;
|
|
6
|
+
export interface CeilingRoot {
|
|
7
|
+
id: string;
|
|
8
|
+
private: boolean;
|
|
9
|
+
match: Match;
|
|
10
|
+
}
|
|
11
|
+
export interface Ceiling {
|
|
12
|
+
/** The reader's own app object: run confinement applies to every object below it, whichever alternative lets the object through. */
|
|
13
|
+
ownApp: string;
|
|
14
|
+
roots: CeilingRoot[];
|
|
15
|
+
/** Schema namespaces the reader's app owns: a document whose `schema_type` is in one passes the ceiling anywhere, at any privacy. */
|
|
16
|
+
namespaces: string[];
|
|
17
|
+
/** The reader's run place; null for the app's server and for readers outside a run. */
|
|
18
|
+
place: {
|
|
19
|
+
run: string;
|
|
20
|
+
isolated: boolean;
|
|
21
|
+
} | null;
|
|
22
|
+
predicates: {
|
|
23
|
+
/** `crtr:objects:<verb>:node`: every public node passes. */
|
|
24
|
+
allNodes: boolean;
|
|
25
|
+
/** The space objects themselves (apps, persons, repos with no owner) pass: a granted reader sees that they exist. */
|
|
26
|
+
spaces: boolean;
|
|
27
|
+
};
|
|
28
|
+
/** App object ids for the `authored:` match (first-revision author rule): the caller's app and every first-party app, one whose grant holds
|
|
29
|
+
* `crtr:memory:manage:user` always. A revoked first-party grant still counts: revocation stops its calls, not who wrote the document. */
|
|
30
|
+
authorApps: string[];
|
|
31
|
+
}
|
|
32
|
+
export type Access = 'read' | 'write' | 'message';
|
|
33
|
+
export interface CeilingOptions {
|
|
34
|
+
verb?: 'watch' | 'link';
|
|
35
|
+
}
|
|
36
|
+
/** Every live local person object. The ceiling reaches a person's tree through the scope the reader holds; which person's documents the reader's user may
|
|
37
|
+
* actually read is the schema's decision (`acts_for` on the reader's own person, a share on anyone else's), so a document shared with the reader is in reach. */
|
|
38
|
+
export declare function personObjectIds(db: CanvasDb): Promise<string[]>;
|
|
39
|
+
/** Whether `scopes` hold a provider-group scope `<provider>:<group>` of the app whose external id is `appExt`: what lets a run read that app's private objects. */
|
|
40
|
+
export declare function holdsProviderGroup(scopes: readonly string[], appExt: string): boolean;
|
|
41
|
+
/** The ceiling of `reader` for `access`; null for a person reader (no ceiling). */
|
|
42
|
+
export declare function resolveCeiling(db: CanvasDb, reader: Reader, access: Access, options?: CeilingOptions): Promise<Ceiling | null>;
|
|
43
|
+
export interface RunPlace {
|
|
44
|
+
run: string;
|
|
45
|
+
isolated: boolean;
|
|
46
|
+
}
|
|
47
|
+
/** A node's run and whether it is isolated (its own flag: isolation is sticky). A deleted node of an isolated run keeps its run in `deleted_isolated_nodes`. */
|
|
48
|
+
export declare function nodePlace(db: CanvasDb, nodeId: string): Promise<RunPlace | null>;
|
|
49
|
+
/** Record, before a node's row is deleted, the run of a node of an isolated run. */
|
|
50
|
+
export declare function rememberDeletedIsolated(nodeId: string, db: CanvasDb): Promise<void>;
|
|
51
|
+
/** The reader with its launched list lifted, for the runtime's own writes inside a narrowed run (a `crtr push` report, a promoted node's scaffolded roadmap). */
|
|
52
|
+
export declare function asRuntimeWriter<R extends Reader>(reader: R): R;
|
|
53
|
+
/** The own-app memory scope a narrowed run lacks to `read` or `rw` the document `objectId` (or a document about to be created under `objectId` when `creating`), or null.
|
|
54
|
+
* A run with no launched list, the app's server and a person are never narrowed. Only documents are gated. */
|
|
55
|
+
export declare function missingMemoryScope(db: CanvasDb, reader: Reader, objectId: string, access: 'read' | 'rw', creating?: boolean): Promise<string | null>;
|
|
56
|
+
/** The ceiling as a condition on the candidate `o` (an alias of `objects`). */
|
|
57
|
+
export declare function ceilingFrag(c: Ceiling, o: string): Frag;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
var j=Object.defineProperty;var s=(t,e)=>j(t,"name",{value:e,configurable:!0});import{covers as O}from"@north-light/identity";import{fileRoot as v}from"../spaces/file-root.js";import{FALSE as b,TRUE as F,and as g,f as w,or as T,raw as l}from"./sql.js";const x=s(t=>({ownApp:t,roots:[],namespaces:[],place:null,predicates:{allNodes:!1,spaces:!1},authorApps:[]}),"empty");async function A(t,e){return(await t.prepare("SELECT external_id FROM identities WHERE object = ? ORDER BY scheme LIMIT 1").get(e))?.external_id??null}s(A,"externalId");async function _(t,e,n){return(await t.prepare("SELECT o.id FROM identities i JOIN objects o ON o.id = i.object WHERE i.scheme = ? AND i.external_id = ? AND o.deleted_at IS NULL").get(e,n))?.id??null}s(_,"objectOf");async function M(t){return(await t.prepare("SELECT id FROM objects WHERE type = 'person' AND home_machine IS NULL AND deleted_at IS NULL ORDER BY created_at").all()).map(n=>n.id)}s(M,"personObjectIds");async function U(t){return(await t.prepare(`SELECT DISTINCT i.object AS id FROM grants g JOIN identities i ON i.external_id = g.grantee AND i.scheme = 'app'
|
|
2
|
+
WHERE g.resolved_scopes_json::jsonb @> '[{"scope":"crtr:memory:manage:user","mode":"always"}]'::jsonb`).all()).map(n=>n.id)}s(U,"firstPartyApps");function D(t,e){return J(t).has(e.replace(/^app:/,""))}s(D,"holdsProviderGroup");const I=new WeakMap;function J(t){let e=I.get(t);if(e===void 0){e=new Set;for(const n of t){const d=n.indexOf(":");d>=0&&!n.includes(":",d+1)&&e.add(n.slice(0,d))}I.set(t,e)}return e}s(J,"providerGroupHolders");async function k(t,e,n){if(e.class==="object")return e.repos.map(o=>o.id);if(e.class!=="node")return[];const d=await A(t,e.app.id);if(d===null)return[];const p={app:d,resolved:e.scopes??[],launched:null},c=new Set;for(const o of[...e.checkouts,...e.projectCheckouts])"root"in v(p,o.root,n)&&c.add(o.repo.id);return[...c]}s(k,"repoRoots");async function G(t,e,n,d={}){if(e.class==="person")return null;const p=x(e.app.id),c=e.scopes,o=s(a=>c!==null&&c.some(r=>O(r,a)),"has"),f=await A(t,e.app.id),u=e.class==="node"?await L(t,e.node.id):e.place??null;p.place=u,p.authorApps=[e.app.id];const h=n==="write"?"rw":"read",i=e.launched,N=s((a,r,E=!0)=>{p.roots.push({id:a,private:E,match:r})},"own");if(n==="message"||i===void 0||i===null||f===null||i.some(a=>O(a,`crtr:memory:${h}:${f}`)))N(e.app.id,"any"),f!==null&&(p.namespaces=await X(t,f));else{N(e.app.id,"non-document"),n==="read"&&u!==null&&N(e.app.id,"run-report");for(const a of await t.prepare("SELECT o.id, i.external_id FROM objects o JOIN identities i ON i.object = o.id AND i.scheme = 'profile' WHERE o.owner = ? AND o.type = 'profile' AND o.deleted_at IS NULL").all(e.app.id))i.some(r=>O(r,`crtr:memory:${h}:${f}:profile:${a.external_id}`))&&N(a.id,"no-node")}if(c===null)return p;p.predicates.spaces=!0;const C=await M(t),R=o("crtr:memory:manage:user"),$=R||o("crtr:memory:read_private:user"),m=s((a,r)=>{for(const E of C)E!==e.app.id&&p.roots.push({id:E,private:r,match:a})},"addPerson");if(n==="write")if(R)m("any",!0);else{o("crtr:memory:rw:user")&&(m("self",!0),m("authored:*",!0));for(const a of c){const r=/^crtr:memory:rw:user:(.+)$/.exec(a);r!==null&&m(`authored:${r[1]}`,!0)}o("crtr:objects:write:user")&&m("any",!0),p.roots.some(a=>a.match.startsWith("authored:"))&&p.authorApps.push(...await U(t))}else n==="read"&&(R?m("any",!0):m("direct-doc",$),o(`crtr:objects:${d.verb??"read"}:user`)&&m("any",$));const H=n==="write"?"write":d.verb??"read",W=e.class==="object"&&e.object.type.includes("."),S=s(async(a,r)=>{const E=await _(t,"app",a);E===null||E===e.app.id||p.roots.push({id:E,private:D(c,a),match:r})},"appRoot");for(const a of c){if(!a.startsWith("crtr:"))continue;let r=/^crtr:memory:(read|rw):app:([A-Za-z0-9_-]{1,64})(?::profile:([A-Za-z0-9_-]{1,64}))?$/.exec(a);if(r!==null&&(n==="read"||n==="write"&&r[1]==="rw")){if(r[3]===void 0)await S(`app:${r[2]}`,"no-node");else{const E=await _(t,"profile",r[3]);E!==null&&p.roots.push({id:E,private:!1,match:"no-node"})}continue}if(r=/^crtr:objects:(read|watch|link|write):(user|node|profile:[A-Za-z0-9_-]+|app:[A-Za-z0-9_-]+)$/.exec(a),r!==null&&n!=="message"&&(r[1]===H||n==="read"&&W&&r[1]==="watch")){const E=r[2];if(E==="node")p.predicates.allNodes=!0;else if(E.startsWith("profile:")){const y=await _(t,"profile",E.slice(8));y!==null&&p.roots.push({id:y,private:!1,match:"below"})}else E.startsWith("app:")&&await S(E,"objects-app");continue}if(r=/^crtr:conversations:read:(app:[A-Za-z0-9_-]+)$/.exec(a),r!==null&&n==="read"){await S(r[1],"node-report"),await S(r[1],"run-node");continue}r=/^crtr:objects:message:(app:[A-Za-z0-9_-]+)$/.exec(a),r!==null&&n==="message"&&await S(r[1],"node-tree")}if(n==="read"){const a=await t.prepare(`SELECT a.id FROM objects a WHERE a.type = 'app' AND a.deleted_at IS NULL AND EXISTS (
|
|
3
|
+
SELECT 1 FROM objects o JOIN documents d ON d.object = o.id WHERE o.owner = a.id AND o.type = 'document' AND o.deleted_at IS NULL AND d.source_path IS NOT NULL)`).all();for(const r of a)r.id!==e.app.id&&p.roots.push({id:r.id,private:!1,match:"shipped"});for(const r of await t.prepare("SELECT o.id, i.external_id FROM objects o JOIN identities i ON i.object = o.id AND i.scheme = 'app' WHERE o.type = 'app' AND o.deleted_at IS NULL").all())r.id!==e.app.id&&p.roots.push({id:r.id,private:D(c,r.external_id),match:"custom"})}if(n!=="message")for(const a of await k(t,e,n==="write"?"rw":"read"))p.roots.push({id:a,private:n==="read",match:"any"});return p}s(G,"resolveCeiling");async function X(t,e){return(await t.prepare("SELECT namespace FROM schemas WHERE app = ?").all(e)).map(n=>n.namespace)}s(X,"ownNamespaces");async function L(t,e){const n=await t.prepare("SELECT run_id, isolated FROM nodes WHERE node_id = ?").get(e);if(n!==void 0)return{run:n.run_id??e,isolated:n.isolated===1};const d=await t.prepare("SELECT run_id FROM deleted_isolated_nodes WHERE node_id = ?").get(e);return d?{run:d.run_id,isolated:!0}:null}s(L,"nodePlace");async function K(t,e){const n=await L(e,t);n?.isolated&&await e.prepare("INSERT INTO deleted_isolated_nodes (node_id, run_id) VALUES (?, ?) ON CONFLICT (node_id) DO UPDATE SET run_id = excluded.run_id").run(t,n.run)}s(K,"rememberDeletedIsolated");function q(t){return t.class==="person"?t:{...t,launched:null}}s(q,"asRuntimeWriter");async function Q(t,e,n,d,p=!1){if(e.class==="person"||e.launched===void 0||e.launched===null)return null;const c=await t.prepare(`WITH RECURSIVE c(id, owner, type, kind, depth) AS (
|
|
4
|
+
SELECT id, owner, type, kind, 0 FROM objects WHERE id = ? UNION ALL
|
|
5
|
+
SELECT p.id, p.owner, p.type, p.kind, c.depth + 1 FROM c JOIN objects p ON p.id = c.owner WHERE c.depth < 12)
|
|
6
|
+
SELECT c.id, c.type, c.kind FROM c ORDER BY c.depth`).all(n);if(c[c.length-1]?.id!==e.app.id)return null;const o=await A(t,e.app.id);if(o===null)return null;if(c.length===1)return p?e.launched.some(i=>O(i,`crtr:memory:${d}:${o}`))?null:`crtr:memory:${d}:${o}`:null;if(!p&&c[0].type!=="document")return null;const f=c.slice(0,-1).reverse();if(d==="read"&&!p&&c[0].kind==="report"&&f[0].type==="node"&&e.class!=="object"){const i=e.class==="node"?await L(t,e.node.id):e.place??null,N=await L(t,f[0].id);if(i!==null&&N!==null&&i.run===N.run)return null}const u=f[0],h=u.type==="profile"?`crtr:memory:${d}:${o}:profile:${await A(t,u.id)}`:`crtr:memory:${d}:${o}`;return e.launched.some(i=>O(i,h))?null:h}s(Q,"missingMemoryScope");function ee(t,e){const n=[];return t.roots.length>0&&n.push(P(t,e)),t.namespaces.length>0&&n.push(w`split_part(${l(e)}.schema_type, '/', 1) = ANY(${t.namespaces}::text[])`),t.predicates.allNodes&&n.push(l(`(${e}.type = 'node' AND ${e}.privacy = 'public')`)),t.predicates.spaces&&n.push(l(`(${e}.owner IS NULL AND ${e}.type IN ('app','person','repo'))`)),g([T(n),z(t,e)])}s(ee,"ceilingFrag");function z(t,e){if(t.place===null)return F;const n=t.place.isolated?1:0;return w`NOT EXISTS (WITH RECURSIVE ca(id, owner, depth, near_node) AS (
|
|
7
|
+
SELECT ${l(e)}.id, ${l(e)}.owner, 0, CASE WHEN ${l(e)}.type = 'node' THEN ${l(e)}.id END
|
|
8
|
+
UNION ALL
|
|
9
|
+
SELECT p.id, p.owner, ca.depth + 1, COALESCE(ca.near_node, CASE WHEN p.type = 'node' THEN p.id END)
|
|
10
|
+
FROM ca JOIN objects p ON p.id = ca.owner WHERE ca.depth < 12)
|
|
11
|
+
SELECT 1 FROM ca LEFT JOIN nodes cn ON cn.node_id = ca.near_node LEFT JOIN deleted_isolated_nodes cd ON cd.node_id = ca.near_node
|
|
12
|
+
WHERE ca.id = ${t.ownApp} AND ca.depth > 0 AND ca.near_node IS NOT NULL
|
|
13
|
+
AND NOT (CASE WHEN cn.node_id IS NOT NULL THEN COALESCE(cn.run_id, cn.node_id) = ${t.place.run} OR (cn.isolated = 0 AND ${n} = 0)
|
|
14
|
+
WHEN cd.node_id IS NOT NULL THEN cd.run_id = ${t.place.run} ELSE ${n} = 0 END))`}s(z,"placeFrag");function P(t,e){const n=t.roots.map(i=>i.id),d=t.roots.map(i=>i.private?1:0),p=t.roots.map(i=>i.match),c=new Set(t.roots.map(i=>i.match)),o=s(i=>`${e}.${i}`,"O"),f=[],u=s((i,N)=>{c.has(i)&&f.push(w`(r.match = ${i} AND ${typeof N=="string"?l(N):N})`)},"t");u("any","TRUE"),u("below","ca.depth > 0"),u("direct-doc",`(ca.depth = 0 OR (ca.depth = 1 AND ${o("type")} = 'document'))`),u("no-node","NOT ca.node_below"),u("node-tree","ca.node_below"),u("objects-app",`(ca.node_below OR ${o("type")} LIKE '%.%')`),u("node-report",`(ca.node_below AND ${o("type")} = 'document' AND ${o("kind")} = 'report')`),u("run-node",`(${o("type")} = 'node' AND EXISTS (SELECT 1 FROM nodes rn WHERE rn.node_id = ${o("id")} AND rn.run_id IS NOT NULL))`),u("custom",`${o("type")} LIKE '%.%'`),u("shipped",`(ca.depth > 0 AND EXISTS (SELECT 1 FROM documents sd WHERE sd.object = ${o("id")} AND sd.source_path IS NOT NULL))`),u("non-document",`(${o("type")} <> 'document')`),u("self","ca.depth = 0"),c.has("run-report")&&f.push(t.place===null?b:w`(r.match = 'run-report' AND ${l(o("type"))} = 'document' AND ${l(o("kind"))} = 'report' AND EXISTS (SELECT 1 FROM nodes rn WHERE rn.node_id = ca.near_node AND COALESCE(rn.run_id, rn.node_id) = ${t.place.run}))`);for(const i of c)if(i.startsWith("authored:")){const N=i.slice(9);f.push(w`(r.match = ${i} AND ca.depth = 1 AND ${l(o("type"))} = 'document' AND ${N==="*"?l("TRUE"):w`${l(o("name"))} = ${N}`} AND EXISTS (SELECT 1 FROM revisions rv JOIN objects au ON au.id = rv.author
|
|
15
|
+
WHERE rv.document = ${l(o("id"))} AND rv.number = (SELECT min(number) FROM revisions WHERE document = ${l(o("id"))})
|
|
16
|
+
AND (au.type = 'person' OR COALESCE((SELECT app FROM nodes WHERE node_id = au.id), au.owner) = ANY(${t.authorApps}::text[]))))`)}const h=T(f);return w`EXISTS (WITH RECURSIVE ca(id, owner, type, depth, node_below, near_node) AS (
|
|
17
|
+
SELECT ${l(o("id"))}, ${l(o("owner"))}, ${l(o("type"))}, 0, FALSE, CASE WHEN ${l(o("type"))} = 'node' THEN ${l(o("id"))} END
|
|
18
|
+
UNION ALL
|
|
19
|
+
SELECT p.id, p.owner, p.type, ca.depth + 1, ca.node_below OR ca.type = 'node', COALESCE(ca.near_node, CASE WHEN p.type = 'node' THEN p.id END)
|
|
20
|
+
FROM ca JOIN objects p ON p.id = ca.owner WHERE ca.depth < 12)
|
|
21
|
+
SELECT 1 FROM ca JOIN unnest(${n}::text[], ${d}::int[], ${p}::text[]) AS r(id, priv, match) ON r.id = ca.id
|
|
22
|
+
WHERE (${l(o("privacy"))} = 'public' OR r.priv = 1) AND ${h})`}s(P,"rootsFrag");export{q as asRuntimeWriter,ee as ceilingFrag,D as holdsProviderGroup,Q as missingMemoryScope,L as nodePlace,M as personObjectIds,K as rememberDeletedIsolated,G as resolveCeiling};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type Definition } from './schema-parser.js';
|
|
2
|
+
import { type Frag } from './sql.js';
|
|
3
|
+
/** The subjects a reader stands for: its user ids (including `*`) and every group it reaches, directly or through nested groups. */
|
|
4
|
+
export interface SubjectSet {
|
|
5
|
+
userIds: string[];
|
|
6
|
+
groupIds: string[];
|
|
7
|
+
}
|
|
8
|
+
export type Defs = ReadonlyMap<string, Definition>;
|
|
9
|
+
/** Refuse a schema whose evaluation would recurse past the closure: a permission that reaches itself other than through top-level self-arrows
|
|
10
|
+
* (inside `&`/`-`, through another permission, or through a `type#relation` subject). Such a rule would be cut off at MAX_DEPTH, so its meaning
|
|
11
|
+
* would depend on a hidden depth. `defs` is every definition the schema can reach (the built-ins and the new ones); the error names the line. */
|
|
12
|
+
export declare function assertNoUnsupportedRecursion(defs: readonly Definition[], own: readonly Definition[]): void;
|
|
13
|
+
/** SQL true when the subjects hold `name` on the resource of type `typeName` whose id is the expression `id`. */
|
|
14
|
+
export declare function compilePermission(defs: Defs, subjects: SubjectSet, typeName: string, name: string, id: Frag): Frag;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var S=Object.defineProperty;var m=(f,s)=>S(f,"name",{value:s,configurable:!0});import{SchemaParseError as g}from"./schema-parser.js";import{FALSE as h,TRUE as N,and as _,f as E,or as A,raw as u}from"./sql.js";const w=8;class x{static{m(this,"Compiler")}defs;subjects;n=0;constructor(s,e){this.defs=s,this.subjects=e}alias(s){return`${s}${++this.n}`}permission(s,e,i,r=0){const t=this.defs.get(s);if(t===void 0||r>w)return h;if(e in t.relations)return this.relation(t,e,i,r);const n=t.permissions[e];return n===void 0?h:this.closure(t,e,n,i,r)??this.expr(t,n,i,r)}closure(s,e,i,r,t){const{terms:n,selfs:c}=d(s,e,i);if(c.length===0)return null;const $=[...new Set(c.map(l=>l.left))],p=n.filter(l=>!c.includes(l)).map(l=>({t:l,skip:void 0})).concat(c.map(l=>({t:l,skip:s.name}))),a=this.alias("anc"),o=this.alias("a"),b=this.alias("rs"),y=A(p.map(({t:l,skip:j})=>l.op==="arrow"&&j!==void 0?this.arrow(s,l.left,l.right,u(`${o}.id`),t,j):this.expr(s,l,u(`${o}.id`),t)));return E`EXISTS (WITH RECURSIVE ${u(a)}(id) AS (SELECT ${r}::text UNION SELECT ${u(b)}.subject_id FROM ${u(a)}, LATERAL (SELECT s.subject_id FROM relation_source s WHERE s.resource_type = ${s.name} AND s.resource_id = ${u(a)}.id AND s.relation = ANY(${$}::text[]) AND s.subject_type = ${s.name} AND s.subject_relation = '' OFFSET 0) ${u(b)}) SELECT 1 FROM ${u(a)} ${u(o)} WHERE ${y})`}expr(s,e,i,r){switch(e.op){case"ref":return this.permission(s.name,e.name,i,r+1);case"arrow":return this.arrow(s,e.left,e.right,i,r);case"union":return A([this.expr(s,e.l,i,r),this.expr(s,e.r,i,r)]);case"inter":return _([this.expr(s,e.l,i,r),this.expr(s,e.r,i,r)]);case"excl":return _([this.expr(s,e.l,i,r),D(this.expr(s,e.r,i,r))])}}subjectMatch(s,e,i){const r=m(n=>u(`${s}.${n}`),"col");if(e.type==="user")return e.relation!==void 0?h:E`(${r("subject_type")} = 'user' AND ${r("subject_id")} = ANY(${this.subjects.userIds}::text[]))`;if(e.relation===void 0)return h;if(e.type==="crtr/group"&&e.relation==="member")return E`(${r("subject_type")} = 'crtr/group' AND ${r("subject_relation")} = 'member' AND ${r("subject_id")} = ANY(${this.subjects.groupIds}::text[]))`;const t=this.permission(e.type,e.relation,r("subject_id"),i+1);return _([E`${r("subject_type")} = ${e.type}`,E`${r("subject_relation")} = ${e.relation}`,t])}relation(s,e,i,r){const t=this.alias("rs"),n=A(s.relations[e].map(c=>this.subjectMatch(t,c,r)));return n===h?h:E`EXISTS (SELECT 1 FROM relation_source ${u(t)} WHERE ${u(t)}.resource_type = ${s.name} AND ${u(t)}.resource_id = ${i} AND ${u(t)}.relation = ${e} AND ${n})`}arrow(s,e,i,r,t,n){const c=s.relations[e];if(c===void 0)return h;const $=[];for(const p of c){if(p.type==="user"||p.relation!==void 0||p.type===n)continue;const a=this.defs.get(p.type);if(a===void 0||!(i in a.permissions||i in a.relations))continue;const o=this.alias("rs"),b=this.permission(p.type,i,u(`${o}.subject_id`),t+1);$.push(E`EXISTS (SELECT 1 FROM relation_source ${u(o)} WHERE ${u(o)}.resource_type = ${s.name} AND ${u(o)}.resource_id = ${r} AND ${u(o)}.relation = ${e} AND ${u(o)}.subject_type = ${p.type} AND ${b})`)}return A($)}}function d(f,s,e){const i=[],r=m(n=>{n.op==="union"?(r(n.l),r(n.r)):i.push(n)},"flatten");r(e);const t=i.filter(n=>n.op==="arrow"&&n.right===s&&(f.relations[n.left]?.some(c=>c.type===f.name&&c.relation===void 0)??!1));return{terms:i,selfs:t}}m(d,"selfArrows");function F(f,s){const e=new Map(f.map(t=>[t.name,t])),i=m((t,n)=>`${t}\0${n}`,"key"),r=m((t,n)=>{const c=[];if(n in t.relations){for(const o of t.relations[n])o.relation!==void 0&&o.type!=="user"&&!(o.type==="crtr/group"&&o.relation==="member")&&c.push([o.type,o.relation]);return c}const $=t.permissions[n];if($===void 0)return c;const{selfs:p}=d(t,n,$),a=m(o=>{if(o.op==="ref")c.push([t.name,o.name]);else if(o.op==="arrow"){const b=p.includes(o);for(const y of t.relations[o.left]??[]){if(y.type==="user"||y.relation!==void 0||b&&y.type===t.name)continue;const l=e.get(y.type);l!==void 0&&(o.right in l.permissions||o.right in l.relations)&&c.push([y.type,o.right])}}else a(o.l),a(o.r)},"walk");return a($),c},"edges");for(const t of s)for(const n of[...Object.keys(t.relations),...Object.keys(t.permissions)]){const c=new Set,$=r(t,n);for(;$.length>0;){const[p,a]=$.pop();if(p===t.name&&a===n)throw new g(t.lines?.[n]??1,`${t.name}#${n} reaches itself; only top-level \`relation->${n}\` terms of a permission may recurse`);if(c.has(i(p,a)))continue;c.add(i(p,a));const o=e.get(p);o!==void 0&&$.push(...r(o,a))}}}m(F,"assertNoUnsupportedRecursion");function D(f){return f===N?h:f===h?N:{sql:`NOT (${f.sql})`,params:f.params}}m(D,"notFrag");function L(f,s,e,i,r){return new x(f,s).permission(e,i,r)}m(L,"compilePermission");export{F as assertNoUnsupportedRecursion,L as compilePermission};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** A refusal the daemon maps to its HTTP status and error code. */
|
|
2
|
+
export declare class PermissionError extends Error {
|
|
3
|
+
readonly status: number;
|
|
4
|
+
readonly code: string;
|
|
5
|
+
readonly details?: Record<string, unknown> | undefined;
|
|
6
|
+
constructor(status: number, code: string, message: string, details?: Record<string, unknown> | undefined);
|
|
7
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var a=Object.defineProperty;var t=(r,s)=>a(r,"name",{value:s,configurable:!0});class d extends Error{static{t(this,"PermissionError")}status;code;details;constructor(s,e,i,o){super(i),this.status=s,this.code=e,this.details=o,this.name="PermissionError"}}export{d as PermissionError};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { CanvasDb } from '../canvas/db.js';
|
|
2
|
+
import type { Reader } from '../graph/types.js';
|
|
3
|
+
import { type CeilingOptions } from './ceiling.js';
|
|
4
|
+
import { type SubjectSet } from './compile.js';
|
|
5
|
+
import { type Frag } from './sql.js';
|
|
6
|
+
export type Permission = 'read' | 'edit' | 'message' | 'use' | 'share';
|
|
7
|
+
export declare const PERMISSIONS: readonly Permission[];
|
|
8
|
+
/** A reader with its user and subject set resolved: build once, pass to many `permits`/`check` calls. */
|
|
9
|
+
export interface EvalReader {
|
|
10
|
+
reader: Reader;
|
|
11
|
+
userId: string;
|
|
12
|
+
subjects: SubjectSet;
|
|
13
|
+
}
|
|
14
|
+
/** The user a reader acts for: a person's own identity; an app call's `sub`; a node's `sub`; otherwise the install's person. */
|
|
15
|
+
export declare function userOf(db: CanvasDb, reader: Reader): Promise<string>;
|
|
16
|
+
/** The user ids a subject stands for (the user and `*`) and every group it reaches, directly or through nested groups. */
|
|
17
|
+
export declare function subjectSet(db: CanvasDb, userId: string): Promise<SubjectSet>;
|
|
18
|
+
export declare function resolveReader(db: CanvasDb, reader: Reader): Promise<EvalReader>;
|
|
19
|
+
/** SQL true when `reader` holds `permission` on the object `alias` (a row of `objects` in the caller's SELECT): schema within the ceiling.
|
|
20
|
+
* Bare `?` placeholders in textual order: embed `sql` and append `params` at the same position in the caller's parameter list. */
|
|
21
|
+
export declare function permits(db: CanvasDb, reader: Reader | EvalReader, permission: Permission, alias?: string, options?: CeilingOptions): Promise<Frag>;
|
|
22
|
+
/** May `reader` do `permission` on the object `objectId`? A missing object is false. */
|
|
23
|
+
export declare function check(db: CanvasDb, reader: Reader | EvalReader, permission: Permission, objectId: string, options?: CeilingOptions): Promise<boolean>;
|
|
24
|
+
/** `check` for a subject that is not a Reader: a person's user id with no ceiling (the SDK's `POST /v1/check`). */
|
|
25
|
+
export declare function checkUser(db: CanvasDb, userId: string, permission: Permission | string, resource: {
|
|
26
|
+
type: string;
|
|
27
|
+
id: string;
|
|
28
|
+
}): Promise<boolean>;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
var L=Object.defineProperty;var a=(e,t)=>L(e,"name",{value:t,configurable:!0});import{personIdAsync as l}from"../canvas/person.js";import{OBJECT_TYPE as O,GROUP_TYPE as w}from"./builtin.js";import{ceilingFrag as b,resolveCeiling as C}from"./ceiling.js";import{compilePermission as f}from"./compile.js";import{PermissionError as I}from"./errors.js";import{loadDefinitions as g}from"./schemas.js";import{TRUE as $,and as H,f as E,or as x,raw as p}from"./sql.js";const D=["read","edit","message","use","share"],U={read:"read",use:"read",edit:"write",share:"write",message:"message"},m=a(e=>e.replace(/^user:/,""),"bareUser");async function W(e,t){if(t.class==="person"){const c=await e.prepare("SELECT external_id FROM identities WHERE object = ? AND scheme = 'user'").get(t.person.id);return m(c?.external_id??await l())}if(t.class==="app")return m(t.sub??await l());const s=t.class==="node"?t.node.id:t.object.id,r=await e.prepare("SELECT sub FROM nodes WHERE node_id = ?").get(s);return m(r?.sub??await l())}a(W,"userOf");async function y(e,t){const s=[t,"*"],r=await e.prepareUncached(`WITH RECURSIVE g(id) AS (
|
|
2
|
+
SELECT resource_id FROM relations WHERE resource_type = ? AND relation = 'member' AND subject_type = 'user' AND subject_id = ANY(?::text[])
|
|
3
|
+
UNION
|
|
4
|
+
SELECT r.resource_id FROM relations r JOIN g ON r.subject_type = ? AND r.subject_id = g.id AND r.subject_relation = 'member'
|
|
5
|
+
WHERE r.resource_type = ? AND r.relation = 'member')
|
|
6
|
+
SELECT id FROM g`).all(w,s,w,w);return{userIds:s,groupIds:r.map(c=>c.id)}}a(y,"subjectSet");async function F(e,t){const s=await W(e,t);return{reader:t,userId:s,subjects:await y(e,s)}}a(F,"resolveReader");const M=a(e=>"subjects"in e,"isEval");async function k(e,t,s,r="o",c={}){if(!D.includes(s))throw new I(400,"unknown_permission",`unknown permission ${s}`);const n=M(t)?t:await F(e,t),i=await g(e),d=p(`${r}.id`),N=f(i,n.subjects,O,s,d),S=s==="read"?x([N,p(`${r}.type = 'app'`)]):N,h=n.reader.class==="person"?null:n.reader.app.id,T=n.reader.class==="app"&&n.reader.place===void 0&&h!==null?await P(e,h):new Set,u=[];for(const[o,_]of i){if(o.startsWith("crtr/")||o==="user"||o==="app"||!(s in _.permissions)&&!(s in _.relations))continue;const j=o.split("/")[0];u.push(E`WHEN ${p(r)}.schema_type = ${o} THEN ${T.has(j)?$:f(i,n.subjects,o,s,d)}`)}const A=u.length===0?E`(CASE WHEN ${p(r)}.schema_type IS NULL THEN ${S} ELSE FALSE END)`:E`(CASE WHEN ${p(r)}.schema_type IS NULL THEN ${S} ${{sql:u.map(o=>o.sql).join(" "),params:u.flatMap(o=>o.params)}} ELSE FALSE END)`,R=await C(e,n.reader,U[s],c);return R===null?A:H([b(R,r),A])}a(k,"permits");async function P(e,t){const s=await e.prepare("SELECT s.namespace FROM schemas s JOIN identities i ON i.external_id = s.app AND i.scheme = 'app' WHERE i.object = ?").all(t);return new Set(s.map(r=>r.namespace))}a(P,"ownNamespaceTypes");async function Q(e,t,s,r,c={}){const n=await k(e,t,s,"o",c);return(await e.prepareUncached(`SELECT ${n.sql} AS ok FROM objects o WHERE o.id = ?`).get(...n.params,r))?.ok===!0}a(Q,"check");async function X(e,t,s,r){const c=await y(e,m(t)),n=await g(e),i=f(n,c,r.type,s,E`${r.id}::text`);return(await e.prepareUncached(`SELECT ${i.sql} AS ok`).get(...i.params))?.ok===!0}a(X,"checkUser");export{D as PERMISSIONS,Q as check,X as checkUser,k as permits,F as resolveReader,y as subjectSet,W as userOf};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { CanvasDb } from '../canvas/db.js';
|
|
2
|
+
export interface FactSubject {
|
|
3
|
+
type: string;
|
|
4
|
+
id: string;
|
|
5
|
+
relation: string;
|
|
6
|
+
}
|
|
7
|
+
export interface Fact {
|
|
8
|
+
resourceType: string;
|
|
9
|
+
resourceId: string;
|
|
10
|
+
relation: string;
|
|
11
|
+
subject: FactSubject;
|
|
12
|
+
writtenBy: string;
|
|
13
|
+
}
|
|
14
|
+
export interface FactRow {
|
|
15
|
+
resource_type: string;
|
|
16
|
+
resource_id: string;
|
|
17
|
+
relation: string;
|
|
18
|
+
subject_type: string;
|
|
19
|
+
subject_id: string;
|
|
20
|
+
subject_relation: string;
|
|
21
|
+
written_by: string;
|
|
22
|
+
created_at: string;
|
|
23
|
+
}
|
|
24
|
+
/** `user:<id>` | `user:*` | `<type>:<id>` | `<type>:<id>#<relation>`; the type may contain `/`. The text of a person's principal (`user:abc`) is the subject user `abc`. */
|
|
25
|
+
export declare function parseSubject(text: string): FactSubject;
|
|
26
|
+
export declare function formatSubject(s: FactSubject): string;
|
|
27
|
+
/** `<type>:<id>` of a resource in an app namespace, or a bare crouter object id. */
|
|
28
|
+
export declare function parseResource(text: string): {
|
|
29
|
+
type: string;
|
|
30
|
+
id: string;
|
|
31
|
+
};
|
|
32
|
+
export declare function validateFact(db: CanvasDb, fact: Omit<Fact, 'writtenBy'>): Promise<void>;
|
|
33
|
+
export declare function writeFact(db: CanvasDb, fact: Fact): Promise<boolean>;
|
|
34
|
+
export declare function deleteFact(db: CanvasDb, fact: Omit<Fact, 'writtenBy'>): Promise<boolean>;
|
|
35
|
+
/** Make the facts `(resource, relation, *)` exactly `subjects`, in the caller's transaction. */
|
|
36
|
+
export declare function replaceSet(db: CanvasDb, resourceType: string, resourceId: string, relation: string, subjects: FactSubject[], writtenBy: string): Promise<{
|
|
37
|
+
written: number;
|
|
38
|
+
deleted: number;
|
|
39
|
+
}>;
|
|
40
|
+
export declare function listFacts(db: CanvasDb, resourceType: string, resourceId: string): Promise<FactRow[]>;
|
|
41
|
+
/** Replace every fact of `relation` on `resourceType` written by `writtenBy` with `facts` (grant sync's team members: `crtr/group` `member`, `directory`). */
|
|
42
|
+
export declare function replaceWrittenBy(db: CanvasDb, writtenBy: string, resourceType: string, relation: string, facts: Array<{
|
|
43
|
+
resourceId: string;
|
|
44
|
+
subject: FactSubject;
|
|
45
|
+
}>): Promise<{
|
|
46
|
+
written: number;
|
|
47
|
+
deleted: number;
|
|
48
|
+
}>;
|
|
49
|
+
/** The user subject of a person principal (`user:abc` → user `abc`). */
|
|
50
|
+
export declare const userSubject: (principal: string) => FactSubject;
|
|
51
|
+
export declare const EVERYONE: FactSubject;
|
|
52
|
+
/** `acts_for @ user:<userId>` on a crouter object: the person it is kept for. Idempotent. */
|
|
53
|
+
export declare function writeActsFor(db: CanvasDb, objectId: string, userId: string, writtenBy?: string): Promise<boolean>;
|
|
54
|
+
/** The shipped-document fact: `reader @ user:*`, `written_by = 'package'`; the package reconciler writes it with the document and removes it with it. */
|
|
55
|
+
export declare function writeShippedReader(db: CanvasDb, objectId: string): Promise<boolean>;
|
|
56
|
+
export declare function deleteShippedReader(db: CanvasDb, objectId: string): Promise<boolean>;
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
var h=Object.defineProperty;var o=(t,e)=>h(t,"name",{value:e,configurable:!0});import{OBJECT_TYPE as w}from"./builtin.js";import{PermissionError as j}from"./errors.js";import{loadDefinitions as A}from"./schemas.js";function m(t){const e=/^([A-Za-z][A-Za-z0-9_/]*):([^#\s]+)(?:#([A-Za-z_][A-Za-z0-9_]*))?$/.exec(t);if(e===null)throw new j(400,"subject_invalid",`${JSON.stringify(t)} is not a subject (<type>:<id>[#<relation>])`);return{type:e[1],id:e[2],relation:e[3]??""}}o(m,"parseSubject");function l(t){return`${t.type}:${t.id}${t.relation===""?"":`#${t.relation}`}`}o(l,"formatSubject");function O(t){const e=/^([A-Za-z][A-Za-z0-9_/]*):(\S+)$/.exec(t);return e===null?{type:w,id:t}:{type:e[1],id:e[2]}}o(O,"parseResource");async function N(t,e){const i=(await A(t)).get(e.resourceType);if(i===void 0)throw new j(400,"unknown_type",`no schema defines ${e.resourceType}`);if(e.resourceType===w&&e.relation==="parent")throw new j(400,"unknown_relation","parent is the object owner, never stored");const u=i.relations[e.relation];if(u===void 0)throw new j(400,"unknown_relation",`${e.resourceType} has no relation ${e.relation}`,{relation:e.relation});const a=e.subject;if(!u.some(c=>c.type===a.type&&(a.id==="*"?c.wildcard===!0:a.relation===""?c.relation===void 0&&c.wildcard!==!0:c.relation===a.relation)))throw new j(400,"subject_not_allowed",`${e.resourceType}#${e.relation} does not accept ${l(a)}`,{relation:e.relation,subject:l(a)})}o(N,"validateFact");async function b(t,e){await N(t,e);const n=e.subject;return(await t.prepare(`INSERT INTO relations (resource_type, resource_id, relation, subject_type, subject_id, subject_relation, written_by, created_at)
|
|
2
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT DO NOTHING`).run(e.resourceType,e.resourceId,e.relation,n.type,n.id,n.relation,e.writtenBy,new Date().toISOString())).changes>0}o(b,"writeFact");async function E(t,e){const n=e.subject;return(await t.prepare("DELETE FROM relations WHERE resource_type = ? AND resource_id = ? AND relation = ? AND subject_type = ? AND subject_id = ? AND subject_relation = ?").run(e.resourceType,e.resourceId,e.relation,n.type,n.id,n.relation)).changes>0}o(E,"deleteFact");async function x(t,e,n,i,u,a){const p=new Map(u.map(r=>[l(r),r])),c=await t.prepare("SELECT subject_type, subject_id, subject_relation FROM relations WHERE resource_type = ? AND resource_id = ? AND relation = ?").all(e,n,i);let d=0,y=0;const _=new Set;for(const r of c){const s={type:r.subject_type,id:r.subject_id,relation:r.subject_relation};_.add(l(s)),!p.has(l(s))&&await E(t,{resourceType:e,resourceId:n,relation:i,subject:s})&&d++}for(const[r,s]of p)!_.has(r)&&await b(t,{resourceType:e,resourceId:n,relation:i,subject:s,writtenBy:a})&&y++;return{written:y,deleted:d}}o(x,"replaceSet");async function I(t,e,n){return await t.prepare("SELECT * FROM relations WHERE resource_type = ? AND resource_id = ? ORDER BY relation, subject_type, subject_id").all(e,n)}o(I,"listFacts");async function F(t,e,n,i,u){const a=o((r,s)=>`${r}\0${l(s)}`,"key"),p=new Map(u.map(r=>[a(r.resourceId,r.subject),r])),c=await t.prepare("SELECT resource_id, subject_type, subject_id, subject_relation FROM relations WHERE written_by = ? AND resource_type = ? AND relation = ?").all(e,n,i);let d=0,y=0;const _=new Set;for(const r of c){const s={type:r.subject_type,id:r.subject_id,relation:r.subject_relation};_.add(a(r.resource_id,s)),!p.has(a(r.resource_id,s))&&await E(t,{resourceType:n,resourceId:r.resource_id,relation:i,subject:s})&&d++}for(const[r,s]of p)!_.has(r)&&await b(t,{resourceType:n,resourceId:s.resourceId,relation:i,subject:s.subject,writtenBy:e})&&y++;return{written:y,deleted:d}}o(F,"replaceWrittenBy");const S=o(t=>({type:"user",id:t.replace(/^user:/,""),relation:""}),"userSubject"),f={type:"user",id:"*",relation:""};async function k(t,e,n,i="crtr"){return await b(t,{resourceType:w,resourceId:e,relation:"acts_for",subject:S(n),writtenBy:i})}o(k,"writeActsFor");async function v(t,e){return await b(t,{resourceType:w,resourceId:e,relation:"reader",subject:f,writtenBy:"package"})}o(v,"writeShippedReader");async function g(t,e){return await E(t,{resourceType:w,resourceId:e,relation:"reader",subject:f})}o(g,"deleteShippedReader");export{f as EVERYONE,E as deleteFact,g as deleteShippedReader,l as formatSubject,I as listFacts,O as parseResource,m as parseSubject,x as replaceSet,F as replaceWrittenBy,S as userSubject,N as validateFact,k as writeActsFor,b as writeFact,v as writeShippedReader};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export interface SubjectType {
|
|
2
|
+
type: string;
|
|
3
|
+
relation?: string;
|
|
4
|
+
wildcard?: boolean;
|
|
5
|
+
}
|
|
6
|
+
export type Expr = {
|
|
7
|
+
op: 'ref';
|
|
8
|
+
name: string;
|
|
9
|
+
} | {
|
|
10
|
+
op: 'arrow';
|
|
11
|
+
left: string;
|
|
12
|
+
right: string;
|
|
13
|
+
} | {
|
|
14
|
+
op: 'union' | 'inter' | 'excl';
|
|
15
|
+
l: Expr;
|
|
16
|
+
r: Expr;
|
|
17
|
+
};
|
|
18
|
+
export interface Definition {
|
|
19
|
+
name: string;
|
|
20
|
+
relations: Record<string, SubjectType[]>;
|
|
21
|
+
permissions: Record<string, Expr>;
|
|
22
|
+
/** Source line of each relation and permission name (set by `parseSchema`; absent in a definition stored by an older build). */
|
|
23
|
+
lines?: Record<string, number>;
|
|
24
|
+
}
|
|
25
|
+
export declare class SchemaParseError extends Error {
|
|
26
|
+
readonly line: number;
|
|
27
|
+
constructor(line: number, message: string);
|
|
28
|
+
}
|
|
29
|
+
export declare function parseSchema(source: string): Definition[];
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
var k=Object.defineProperty;var a=(p,o)=>k(p,"name",{value:o,configurable:!0});class c extends Error{static{a(this,"SchemaParseError")}line;constructor(o,m){super(`line ${o}: ${m}`),this.line=o,this.name="SchemaParseError"}}function g(p){const o=[],m=p.split(`
|
|
2
|
+
`),h=/\s*(->|[{}():|+&\-=#*]|[A-Za-z_][A-Za-z0-9_/]*)/y;for(let r=0;r<m.length;r++){const s=m[r].replace(/\/\/.*$/,"").replace(/\/\*.*?\*\//g,"");h.lastIndex=0;let l=0;for(;l<s.length&&!/^\s*$/.test(s.slice(l));){h.lastIndex=l;const f=h.exec(s);if(f===null)throw new c(r+1,`unexpected character ${JSON.stringify(s.slice(l).trim()[0])}`);o.push({text:f[1],line:r+1}),l=h.lastIndex}}return o}a(g,"tokenize");const b=/^[A-Za-z_][A-Za-z0-9_/]*$/;function z(p){const o=g(p);let m=0;const h=a(()=>o[Math.min(m,o.length-1)]?.line??1,"lastLine"),r=a(()=>o[m]?.text,"peek"),s=a(()=>{const n=o[m++];if(n===void 0)throw new c(h(),"unexpected end of schema");return n},"next"),l=a(n=>{const e=s();if(e.text!==n)throw new c(e.line,`expected "${n}", found "${e.text}"`)},"expect"),f=a(n=>{const e=s();if(!b.test(e.text))throw new c(e.line,`expected ${n}, found "${e.text}"`);return e.text},"ident");function x(){if(r()==="("){s();const e=$();return l(")"),e}const n=f("a relation or permission name");return r()==="->"?(s(),{op:"arrow",left:n,right:f("a permission name")}):{op:"ref",name:n}}a(x,"primary");function $(){let n=x();for(;;){const e=r(),t=e==="+"?"union":e==="&"?"inter":e==="-"?"excl":null;if(t===null)return n;s(),n={op:t,l:n,r:x()}}}a($,"expression");const d=[];for(;m<o.length;){const n=s();if(n.text!=="definition")throw new c(n.line,`expected "definition", found "${n.text}"`);const e={name:f("a definition name"),relations:{},permissions:{},lines:{}};if(d.some(t=>t.name===e.name))throw new c(n.line,`definition ${e.name} is declared twice`);for(l("{");r()!=="}";){const t=s();if(t.text==="relation"){const i=f("a relation name");if(i in e.relations||i in e.permissions)throw new c(t.line,`${i} is declared twice in ${e.name}`);l(":");const u=[];for(;;){const w=f("a subject type");if(r()===":"?(s(),l("*"),u.push({type:w,wildcard:!0})):r()==="#"?(s(),u.push({type:w,relation:f("a relation name")})):u.push({type:w}),r()!=="|")break;s()}e.relations[i]=u,e.lines[i]=t.line}else if(t.text==="permission"){const i=f("a permission name");if(i in e.relations||i in e.permissions)throw new c(t.line,`${i} is declared twice in ${e.name}`);l("="),e.permissions[i]=$(),e.lines[i]=t.line}else throw new c(t.line,`expected "relation", "permission" or "}", found "${t.text}"`)}s(),d.push(e)}for(const n of d){const e=a((t,i)=>{if(t.op==="ref"){if(!(t.name in n.relations)&&!(t.name in n.permissions))throw new c(i,`${n.name} has no relation or permission "${t.name}"`)}else if(t.op==="arrow"){if(!(t.left in n.relations))throw new c(i,`${n.name} has no relation "${t.left}" to follow with ->`)}else e(t.l,i),e(t.r,i)},"check");for(const t of Object.values(n.permissions))e(t,o.find(i=>i.text===n.name)?.line??1)}return d}a(z,"parseSchema");export{c as SchemaParseError,z as parseSchema};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { CanvasDb } from '../canvas/db.js';
|
|
2
|
+
import { type Definition } from './schema-parser.js';
|
|
3
|
+
export interface Registered {
|
|
4
|
+
namespace: string;
|
|
5
|
+
version: number;
|
|
6
|
+
changed: boolean;
|
|
7
|
+
}
|
|
8
|
+
/** Built-ins plus every registered app definition. */
|
|
9
|
+
export declare function loadDefinitions(db: CanvasDb): Promise<Map<string, Definition>>;
|
|
10
|
+
export interface SchemaRow {
|
|
11
|
+
namespace: string;
|
|
12
|
+
app: string;
|
|
13
|
+
version: number;
|
|
14
|
+
source: string;
|
|
15
|
+
}
|
|
16
|
+
export declare function schemaOf(db: CanvasDb, namespace: string): Promise<SchemaRow | null>;
|
|
17
|
+
/** Validate `source` as the schema of `namespace`: parse it, require every definition to live in the namespace and every subject type to be declared. */
|
|
18
|
+
export declare function validateSchema(namespace: string, source: string): Definition[];
|
|
19
|
+
/** Register or change an app's schema. Same source: no-op. A new source bumps the version; removing a relation that still holds facts is refused
|
|
20
|
+
* `409 relation_in_use`. A namespace belongs to the app that registered it first (`403 namespace_taken`). Facts are never rewritten. */
|
|
21
|
+
export declare function registerSchema(db: CanvasDb, app: string, namespace: string, source: string): Promise<Registered>;
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
var w=Object.defineProperty;var l=(s,o)=>w(s,"name",{value:o,configurable:!0});import{BUILTIN_DEFINITIONS as d}from"./builtin.js";import{PermissionError as a}from"./errors.js";import{assertNoUnsupportedRecursion as g}from"./compile.js";import{parseSchema as u,SchemaParseError as h}from"./schema-parser.js";const $=/^[a-z][a-z0-9_-]{0,63}$/;async function j(s){const o=new Map(d.map(t=>[t.name,t]));for(const t of await s.prepareUncached("SELECT compiled_json FROM schemas").all())for(const r of JSON.parse(t.compiled_json))o.set(r.name,r);return o}l(j,"loadDefinitions");async function _(s,o){return await s.prepare("SELECT namespace, app, version, source FROM schemas WHERE namespace = ?").get(o)??null}l(_,"schemaOf");function E(s,o){if(!$.test(s)||s==="crtr")throw new a(403,"namespace_taken",`namespace ${JSON.stringify(s)} is reserved or malformed`);let t;try{t=u(o)}catch(e){throw e instanceof h?new a(400,"schema_invalid",e.message,{line:e.line,message:e.message}):e}const r=[...d,...t];try{g(r,t)}catch(e){throw e instanceof h?new a(400,"schema_invalid",e.message,{line:e.line,message:e.message}):e}const f=new Set(r.map(e=>e.name));for(const e of t){if(!e.name.startsWith(`${s}/`))throw new a(400,"schema_invalid",`definition ${e.name} must be named ${s}/<name>`,{line:1,message:`definition ${e.name} must be named ${s}/<name>`});for(const[c,p]of Object.entries(e.relations))for(const n of p){if(!f.has(n.type))throw new a(400,"schema_invalid",`${e.name}#${c}: unknown subject type ${n.type}`,{line:1,message:`unknown subject type ${n.type}`});if(n.relation!==void 0){const i=r.find(m=>m.name===n.type);if(!(n.relation in i.relations)&&!(n.relation in i.permissions))throw new a(400,"schema_invalid",`${e.name}#${c}: ${n.type} has no ${n.relation}`,{line:1,message:`${n.type} has no ${n.relation}`})}}}return t}l(E,"validateSchema");async function x(s,o,t,r){const f=E(t,r),e=await _(s,t);if(e!==null&&e.app!==o)throw new a(403,"namespace_taken",`namespace ${t} belongs to ${e.app}`);if(e!==null&&e.source===r)return{namespace:t,version:e.version,changed:!1};if(e!==null){const p=new Map(f.map(n=>[n.name,n]));for(const n of u(e.source))for(const i of Object.keys(n.relations)){if(p.get(n.name)?.relations[i]!==void 0)continue;const m=await s.prepare("SELECT count(*) AS n FROM relations WHERE resource_type = ? AND relation = ?").get(n.name,i);if(m.n>0)throw new a(409,"relation_in_use",`${n.name}#${i} still holds ${m.n} facts; delete them first`,{relation:`${n.name}#${i}`,facts:m.n})}}const c=(e?.version??0)+1;return await s.prepare(`INSERT INTO schemas (namespace, app, version, source, compiled_json, registered_at) VALUES (?, ?, ?, ?, ?, ?)
|
|
2
|
+
ON CONFLICT (namespace) DO UPDATE SET version = excluded.version, source = excluded.source, compiled_json = excluded.compiled_json, registered_at = excluded.registered_at`).run(t,o,c,r,JSON.stringify(f),new Date().toISOString()),{namespace:t,version:c,changed:!0}}l(x,"registerSchema");export{j as loadDefinitions,x as registerSchema,_ as schemaOf,E as validateSchema};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { CanvasValue } from '../canvas/db.js';
|
|
2
|
+
/** A SQL fragment with bare `?` placeholders and its parameters in textual order: concatenating fragments in order keeps them aligned. */
|
|
3
|
+
export interface Frag {
|
|
4
|
+
sql: string;
|
|
5
|
+
params: CanvasValue[];
|
|
6
|
+
}
|
|
7
|
+
export declare const TRUE: Frag;
|
|
8
|
+
export declare const FALSE: Frag;
|
|
9
|
+
/** Tagged template: a `Frag` value is inlined with its parameters, any other value becomes one `?` parameter (arrays included). */
|
|
10
|
+
export declare function f(strings: TemplateStringsArray, ...values: unknown[]): Frag;
|
|
11
|
+
export declare const raw: (sql: string) => Frag;
|
|
12
|
+
export declare const isFrag: (v: unknown) => v is Frag;
|
|
13
|
+
/** Join fragments with a separator, e.g. ` OR `. An empty list is `empty`. */
|
|
14
|
+
export declare function joinFrags(parts: Frag[], sep: string, empty: Frag): Frag;
|
|
15
|
+
export declare const or: (parts: Frag[]) => Frag;
|
|
16
|
+
export declare const and: (parts: Frag[]) => Frag;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var p=Object.defineProperty;var t=(r,s)=>p(r,"name",{value:s,configurable:!0});const i={sql:"TRUE",params:[]},c={sql:"FALSE",params:[]};function q(r,...s){let o=r[0];const a=[];return s.forEach((n,l)=>{f(n)?(o+=n.sql,a.push(...n.params)):(o+="?",a.push(n)),o+=r[l+1]}),{sql:o,params:a}}t(q,"f");const u=t(r=>({sql:r,params:[]}),"raw"),f=t(r=>typeof r=="object"&&r!==null&&"sql"in r&&"params"in r&&Array.isArray(r.params),"isFrag");function e(r,s,o){return r.length===0?o:r.length===1?r[0]:{sql:`(${r.map(a=>a.sql).join(s)})`,params:r.flatMap(a=>a.params)}}t(e,"joinFrags");const x=t(r=>e(r.filter(s=>s.sql!=="FALSE")," OR ",c),"or"),E=t(r=>e(r.filter(s=>s.sql!=="TRUE")," AND ",i),"and");export{c as FALSE,i as TRUE,E as and,q as f,f as isFrag,e as joinFrags,x as or,u as raw};
|
|
@@ -23,7 +23,7 @@ export type ClientFrameHandlers = {
|
|
|
23
23
|
type: 'extension_ui_response';
|
|
24
24
|
}>): void;
|
|
25
25
|
bye(client: BrokerClient): void;
|
|
26
|
-
shutdown(): void;
|
|
26
|
+
shutdown(client: BrokerClient): void;
|
|
27
27
|
};
|
|
28
28
|
export declare function createClientHandlers(deps: {
|
|
29
29
|
registry: BrokerClientRegistry;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
var E=Object.defineProperty;var
|
|
1
|
+
var E=Object.defineProperty;var p=(s,d)=>E(s,"name",{value:d,configurable:!0});import{SITUATIONAL_CONTEXT_CUSTOM_TYPE as S}from"../../../shared/generated-context.js";import{emitEvent as g}from"../../events/emit.js";import{normalizeSnapshotTail as b}from"../broker-protocol.js";function O(s){const{registry:d,replies:f,session:c,driveEngine:h,turnAcceptance:l,pendingDialogs:T,sendWelcome:m,replayExtraPendingDialogsTo:y,disposeAndExit:A}=s,{notWritable:a,ackTo:u}=f;return{hello(e,r){const t=!e.helloed;e.id=r.client_id,e.snapshotTail=b(r.snapshot_tail),t&&(e.helloed=!0,e.role=r.role==="controller"?"controller":"observer",e.attends=r.attends!==!1),m(e),t&&d.persistAttachState(),e.role==="controller"&&y(e)},deliver(e,r){if(a(e,"drive the engine",r.id))return;const t=c().isStreaming?"steer":"prompt";for(const i of r.cards??[])c().sendCustomMessage({customType:S,content:i,display:!0},t==="steer"?{deliverAs:"steer"}:void 0).catch(_=>g({level:"error",event:"broker.deliver.card_failed",error:_}));let n=!1;const o=p(()=>{n||(n=!0,u(e,"deliver",!0,t,r.id))},"ackAccepted");t==="prompt"&&l.waitFor(o);const v=h(e,{type:"prompt",text:r.text,...r.images!==void 0?{images:r.images}:{},delivery:{...r.origin===void 0?{}:{origin:r.origin}}});if(t==="steer"||v===void 0){l.cancel(o),o();return}v.then(i=>{if(l.cancel(o),i===void 0){o();return}g({level:"error",event:"broker.prompt.deliver_failed",error:i}),!n&&(n=!0,u(e,"deliver",!1,String(i),r.id))})},dialog(e,r){a(e,"answer dialogs")||T.get(r.id)?.resolve(r)},bye(e){e.socket.end()},shutdown(e){a(e,"shut the node down")||A("shutdown-frame")}}}p(O,"createClientHandlers");export{O as createClientHandlers};
|
|
@@ -53,7 +53,7 @@ export declare function createFrameDispatchContext(deps: FrameDispatchDeps): {
|
|
|
53
53
|
session: (client: BrokerClient, frame: import("./frame-session.js").SessionFrame) => void;
|
|
54
54
|
authentication: (client: BrokerClient) => void;
|
|
55
55
|
bye: (client: BrokerClient) => void;
|
|
56
|
-
shutdown: () => void;
|
|
56
|
+
shutdown: (client: BrokerClient) => void;
|
|
57
57
|
};
|
|
58
58
|
kickoff: (prompt: string, messageIds?: string[]) => void;
|
|
59
59
|
notifyTurnAccepted: () => void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
var j=Object.defineProperty;var o=(r,e)=>j(r,"name",{value:e,configurable:!0});import{createFrameReplies as q}from"./frame-replies.js";import{createEngineDriver as N}from"./engine-drive.js";import{announceMessageOrigin as x}from"../stamp/channel.js";import{createClientHandlers as z}from"./frame-client.js";import{createMemoryRefGuidance as B}from"./frame-memory-refs.js";import{createEngineControlHandler as J}from"./frame-engine-control.js";import{createReadHandler as L}from"./frame-read.js";import{createSessionHandler as Q}from"./frame-session.js";import{createKickoffDriver as U,createPromptDispatchTracker as V,createTurnAcceptanceGate as X}from"./turn-ignition.js";import{prependRunDeferredMail as y}from"./run-deferred-mail.js";function ce(r){const{nodeId:e,registry:s,toolGroups:v,projection:k,rebind:d,boundaryReviewId:m,pendingDialogs:w,sendWelcome:D,replayExtraPendingDialogsTo:b,reWelcomeAll:A,disposeAndExit:C,persistModelChoice:F,broadcastModelChanged:M,registryOf:E,formatModelSpec:R,formatExactModelSpec:S,turnAdmission:u,runNode:h,memoryInventory:H,onKickoffAdmitted:T}=r,t=o(()=>d.session(),"currentSession"),P=o(()=>d.services(),"currentServices"),a=q(s),{notWritable:G}=a,l=B(t,e,H),p=V(t),_=X(),g=U({session:t,dispatches:p,turnAdmission:u,prependDeferred:h?n=>y(e,n):void 0,onAdmitted:T}),f=N({nodeId:e,registry:s,replies:a,session:t,dispatches:p,memoryRefs:l,rebind:d,projection:k,toolGroups:v,turnAdmission:u,prependDeferred:h?n=>y(e,n):void 0}),I=o((n,i)=>{G(n,"drive the engine")||((i.type==="prompt"||i.type==="steer"||i.type==="follow_up")&&x(i.text,{type:"human",channel:{type:"viewer"}}),f(n,i))},"handleDriveFrame"),c=z({registry:s,replies:a,session:t,driveEngine:f,turnAcceptance:_,pendingDialogs:w,sendWelcome:D,replayExtraPendingDialogsTo:b,disposeAndExit:C}),W=J({registry:s,replies:a,session:t,services:P,registryOf:E,persistModelChoice:F,broadcastModelChanged:o(async(...n)=>{await M(...n),g.drivePending()},"broadcastModelChanged"),formatModelSpec:R}),K=L({registry:s,replies:a,session:t,boundaryReviewId:m,memoryRefs:l,formatExactModelSpec:S}),O=Q({registry:s,replies:a,rebind:d,session:t,boundaryReviewId:m,memoryRefs:l,reWelcomeAll:A});return{handlers:{hello:c.hello,drive:I,deliver:c.deliver,dialog:c.dialog,engineControl:W,read:K,session:O,authentication:o(n=>{a.ackTo(n,"reload_auth")},"authentication"),bye:c.bye,shutdown:c.shutdown},kickoff:o((n,i)=>g.kickoff(n,i),"kickoff"),notifyTurnAccepted:_.notifyAccepted,pendingPromptDispatches:p.pending,resetForSession:o(()=>{},"resetForSession")}}o(ce,"createFrameDispatchContext");function de(r,e,s){switch(s.type){case"hello":r.handlers.hello(e,s);return;case"prompt":case"steer":case"follow_up":case"abort":case"bash":r.handlers.drive(e,s);return;case"deliver":r.handlers.deliver(e,s);return;case"extension_ui_response":r.handlers.dialog(e,s);return;case"set_model":case"deliver_custom_message":case"cycle_model":case"cycle_ladder":case"cycle_thinking":case"dequeue":case"set_thinking_level":case"set_auto_retry":case"set_auto_compaction":case"compact":case"set_session_name":r.handlers.engineControl(e,s);return;case"get_commands":case"list_models":case"list_scoped_models":case"list_memory_refs":case"get_tree":case"get_settings":case"list_sessions":case"get_human_fork_coordinates":r.handlers.read(e,s);return;case"navigate_tree":case"reload":case"export":case"new_session":case"switch_session":case"fork":case"clone":case"share":r.handlers.session(e,s);return;case"reload_auth":r.handlers.authentication(e);return;case"bye":r.handlers.bye(e);return;case"shutdown":r.handlers.shutdown();return}}o(de,"handleFrame");export{ce as createFrameDispatchContext,de as handleFrame};
|
|
1
|
+
var j=Object.defineProperty;var o=(r,e)=>j(r,"name",{value:e,configurable:!0});import{createFrameReplies as q}from"./frame-replies.js";import{createEngineDriver as N}from"./engine-drive.js";import{announceMessageOrigin as x}from"../stamp/channel.js";import{createClientHandlers as z}from"./frame-client.js";import{createMemoryRefGuidance as B}from"./frame-memory-refs.js";import{createEngineControlHandler as J}from"./frame-engine-control.js";import{createReadHandler as L}from"./frame-read.js";import{createSessionHandler as Q}from"./frame-session.js";import{createKickoffDriver as U,createPromptDispatchTracker as V,createTurnAcceptanceGate as X}from"./turn-ignition.js";import{prependRunDeferredMail as y}from"./run-deferred-mail.js";function ce(r){const{nodeId:e,registry:s,toolGroups:v,projection:k,rebind:d,boundaryReviewId:m,pendingDialogs:w,sendWelcome:D,replayExtraPendingDialogsTo:b,reWelcomeAll:A,disposeAndExit:C,persistModelChoice:F,broadcastModelChanged:M,registryOf:E,formatModelSpec:R,formatExactModelSpec:S,turnAdmission:u,runNode:h,memoryInventory:H,onKickoffAdmitted:T}=r,t=o(()=>d.session(),"currentSession"),P=o(()=>d.services(),"currentServices"),a=q(s),{notWritable:G}=a,l=B(t,e,H),p=V(t),_=X(),g=U({session:t,dispatches:p,turnAdmission:u,prependDeferred:h?n=>y(e,n):void 0,onAdmitted:T}),f=N({nodeId:e,registry:s,replies:a,session:t,dispatches:p,memoryRefs:l,rebind:d,projection:k,toolGroups:v,turnAdmission:u,prependDeferred:h?n=>y(e,n):void 0}),I=o((n,i)=>{G(n,"drive the engine")||((i.type==="prompt"||i.type==="steer"||i.type==="follow_up")&&x(i.text,{type:"human",channel:{type:"viewer"}}),f(n,i))},"handleDriveFrame"),c=z({registry:s,replies:a,session:t,driveEngine:f,turnAcceptance:_,pendingDialogs:w,sendWelcome:D,replayExtraPendingDialogsTo:b,disposeAndExit:C}),W=J({registry:s,replies:a,session:t,services:P,registryOf:E,persistModelChoice:F,broadcastModelChanged:o(async(...n)=>{await M(...n),g.drivePending()},"broadcastModelChanged"),formatModelSpec:R}),K=L({registry:s,replies:a,session:t,boundaryReviewId:m,memoryRefs:l,formatExactModelSpec:S}),O=Q({registry:s,replies:a,rebind:d,session:t,boundaryReviewId:m,memoryRefs:l,reWelcomeAll:A});return{handlers:{hello:c.hello,drive:I,deliver:c.deliver,dialog:c.dialog,engineControl:W,read:K,session:O,authentication:o(n=>{a.ackTo(n,"reload_auth")},"authentication"),bye:c.bye,shutdown:c.shutdown},kickoff:o((n,i)=>g.kickoff(n,i),"kickoff"),notifyTurnAccepted:_.notifyAccepted,pendingPromptDispatches:p.pending,resetForSession:o(()=>{},"resetForSession")}}o(ce,"createFrameDispatchContext");function de(r,e,s){switch(s.type){case"hello":r.handlers.hello(e,s);return;case"prompt":case"steer":case"follow_up":case"abort":case"bash":r.handlers.drive(e,s);return;case"deliver":r.handlers.deliver(e,s);return;case"extension_ui_response":r.handlers.dialog(e,s);return;case"set_model":case"deliver_custom_message":case"cycle_model":case"cycle_ladder":case"cycle_thinking":case"dequeue":case"set_thinking_level":case"set_auto_retry":case"set_auto_compaction":case"compact":case"set_session_name":r.handlers.engineControl(e,s);return;case"get_commands":case"list_models":case"list_scoped_models":case"list_memory_refs":case"get_tree":case"get_settings":case"list_sessions":case"get_human_fork_coordinates":r.handlers.read(e,s);return;case"navigate_tree":case"reload":case"export":case"new_session":case"switch_session":case"fork":case"clone":case"share":r.handlers.session(e,s);return;case"reload_auth":r.handlers.authentication(e);return;case"bye":r.handlers.bye(e);return;case"shutdown":r.handlers.shutdown(e);return}}o(de,"handleFrame");export{ce as createFrameDispatchContext,de as handleFrame};
|
|
@@ -3,9 +3,10 @@ import type { BrokerClient, BrokerClientRegistry } from './client-registry.js';
|
|
|
3
3
|
import type { FrameReplies } from './frame-replies.js';
|
|
4
4
|
import type { MemoryRefGuidance } from './frame-memory-refs.js';
|
|
5
5
|
import { type BrokerSession } from './read-ops.js';
|
|
6
|
-
/** The read ops —
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
/** The read ops — pure reads that drive nothing in the engine, so terminal
|
|
7
|
+
* observer connections populate their palettes and pickers through these too.
|
|
8
|
+
* `list_sessions` is the exception: it lists conversations beyond this node, so
|
|
9
|
+
* only a controller (one that may message the node) gets it. */
|
|
9
10
|
export type ReadFrame = Extract<ClientToBroker, {
|
|
10
11
|
type: 'get_commands' | 'list_models' | 'list_scoped_models' | 'list_memory_refs' | 'get_tree' | 'get_settings' | 'list_sessions' | 'get_human_fork_coordinates';
|
|
11
12
|
}>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
var h=Object.defineProperty;var c=(t,d)=>h(t,"name",{value:d,configurable:!0});import{SessionListCache as k}from"../session-list-cache.js";import{sessionListCachePath as y}from"../../canvas/paths.js";import{engineLeadingCommandTokens as _}from"./engine-routing.js";import{buildCommandList as p,buildGetTreeData as
|
|
1
|
+
var h=Object.defineProperty;var c=(t,d)=>h(t,"name",{value:d,configurable:!0});import{SessionListCache as k}from"../session-list-cache.js";import{sessionListCachePath as y}from"../../canvas/paths.js";import{engineLeadingCommandTokens as _}from"./engine-routing.js";import{buildCommandList as p,buildGetTreeData as l,buildListMemoryRefsData as T,buildListModelsData as E,buildScopedModelsData as S,buildSessionsData as F,buildSettingsData as L}from"./read-ops.js";function R(t){const{registry:d,replies:o,session:i,boundaryReviewId:g,memoryRefs:b,formatExactModelSpec:m}=t,u=new k(y());return(s,r)=>{switch(r.type){case"get_commands":{try{o.ackTo(s,"get_commands",!0,JSON.stringify(p(i())))}catch(e){o.engineErrorTo(s)(e)}break}case"list_models":{try{d.sendFrame(s,E(r.id,i()))}catch(e){o.engineErrorTo(s,r.id)(e)}break}case"list_scoped_models":{try{d.sendFrame(s,S(r.id,i()))}catch(e){o.engineErrorTo(s,r.id)(e)}break}case"list_memory_refs":{try{d.sendFrame(s,T(r.id,b.refs(),_(i())))}catch(e){o.engineErrorTo(s,r.id)(e)}break}case"get_tree":{try{d.sendFrame(s,l(r.id,i(),g))}catch(e){o.engineErrorTo(s,r.id)(e)}break}case"get_settings":{try{d.sendFrame(s,L(r.id,i()))}catch(e){o.engineErrorTo(s,r.id)(e)}break}case"list_sessions":{if(o.notWritable(s,"list sessions",r.id))break;F(r.id,r.scope??"cwd",i(),u).then(e=>d.sendFrame(s,e)).catch(o.engineErrorTo(s,r.id));break}case"get_human_fork_coordinates":{try{const e=i().sessionManager,n=e.getSessionFile(),a=e.getLeafId();if(n===void 0||n===""||a===null){o.engineErrorTo(s,r.id)(new Error("no persisted source session leaf"));break}d.sendFrame(s,{type:"data",id:r.id,kind:"human_fork_coordinates",sourceFile:n,sourceSessionId:e.getSessionId(),leafId:a,modelSpec:m(i().model,i().thinkingLevel)})}catch(e){o.engineErrorTo(s,r.id)(e)}break}}}}c(R,"createReadHandler");export{R as createReadHandler};
|
|
@@ -319,7 +319,7 @@ export interface ListModelsFrame {
|
|
|
319
319
|
type: 'list_models';
|
|
320
320
|
id: string;
|
|
321
321
|
}
|
|
322
|
-
/** Session-resume data (`/resume`): the session list. `scope` selects the
|
|
322
|
+
/** Session-resume data (`/resume`): the session list. Controller only. `scope` selects the
|
|
323
323
|
* cwd-local list (default) or the cross-project list — `SessionSelectorComponent`
|
|
324
324
|
* uses BOTH loaders, so the viewer issues one request per scope. */
|
|
325
325
|
export interface ListSessionsFrame {
|