@north-light/sdk 0.3.549 → 0.3.551

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/client.d.ts CHANGED
@@ -1,8 +1,10 @@
1
- import { type HealthDTO, type MessageResultDTO, type NodeDetailDTO, type ListNodesQuery, type NodeOutcomeDTO, type NodeOutcomeResponseDTO, type NodeSummaryDTO, type ProfileDTO, type ReportDTO, type ReportsQuery, type StatusDTO } from '@north-light/api';
1
+ import { type CheckDTO, type HealthDTO, type MessageResultDTO, type NodeDetailDTO, type ListNodesQuery, type NodeOutcomeDTO, type NodeOutcomeResponseDTO, type NodeSummaryDTO, type ProfileDTO, type ReportDTO, type ReportsQuery, type StatusDTO } from '@north-light/api';
2
2
  import { Bash } from './resources/bash.js';
3
3
  import { Canvas } from './resources/canvas.js';
4
4
  import { Crons } from './resources/crons.js';
5
5
  import { Docs } from './resources/docs.js';
6
+ import { Relations } from './resources/relations.js';
7
+ import { Schemas } from './resources/schemas.js';
6
8
  import { CustomObjects } from './resources/custom-objects.js';
7
9
  import { Attachments } from './resources/attachments.js';
8
10
  import { Files } from './resources/files.js';
@@ -130,10 +132,14 @@ export declare class Crouter {
130
132
  /** Questions agents in runs ask the user (app listener only). */
131
133
  readonly questions: Questions;
132
134
  /** Read the documents the person owns (`store: 'user'`): an app sees their `public` documents, `private` ones too while the grant holds
133
- * `crtr:memory:read_private:user` (read-only; `friends` never), and every privacy level with a `crtr:memory:manage:user` grant. */
135
+ * `crtr:memory:read_private:user` (read-only), and `crtr:memory:manage:user` covers both. */
134
136
  readonly memory: Memory;
135
137
  /** Write, read and edit documents the calling app owns (`docs.write`, `docs.edit`, `docs.get`): for example one app-owned `preference` with a `boot` rule that carries the app's agent instructions. */
136
138
  readonly docs: Docs;
139
+ /** Register the app's permission schema (app listener only). */
140
+ readonly schemas: Schemas;
141
+ /** Write, replace and list the relation facts of the app's own schema (app listener only). */
142
+ readonly relations: Relations;
137
143
  /** Scope-filtered provider tools and daemon-mediated calls. */
138
144
  readonly providers: Providers;
139
145
  /** The caller's grant. With `wait` (1–25 s) and `since` (a `version` from an earlier answer), the runtime holds the
@@ -169,6 +175,8 @@ export declare class Crouter {
169
175
  private readonly baseURL;
170
176
  private readonly socketPath;
171
177
  constructor(options?: CrouterOptions);
178
+ /** May the person `subject` (`user:<id>`) do `permission` on `resource` (`<type>:<id>` in the app's own namespace, or a crouter object id with `read|edit|message|use`)? App listener only. */
179
+ check(subject: string, permission: string, resource: string, options?: RequestOptions): Promise<CheckDTO>;
172
180
  /** Establish NDJSON through the JSON client's cold-socket/retry transport; leave the live body unread. */
173
181
  private openCustomDeliveries;
174
182
  private openRunEvents;
package/dist/client.js CHANGED
@@ -4,6 +4,8 @@ import { Bash } from './resources/bash.js';
4
4
  import { Canvas } from './resources/canvas.js';
5
5
  import { Crons } from './resources/crons.js';
6
6
  import { Docs } from './resources/docs.js';
7
+ import { Relations } from './resources/relations.js';
8
+ import { Schemas } from './resources/schemas.js';
7
9
  import { CustomObjects } from './resources/custom-objects.js';
8
10
  import { Attachments } from './resources/attachments.js';
9
11
  import { Files } from './resources/files.js';
@@ -65,10 +67,14 @@ export class Crouter {
65
67
  /** Questions agents in runs ask the user (app listener only). */
66
68
  questions;
67
69
  /** Read the documents the person owns (`store: 'user'`): an app sees their `public` documents, `private` ones too while the grant holds
68
- * `crtr:memory:read_private:user` (read-only; `friends` never), and every privacy level with a `crtr:memory:manage:user` grant. */
70
+ * `crtr:memory:read_private:user` (read-only), and `crtr:memory:manage:user` covers both. */
69
71
  memory;
70
72
  /** Write, read and edit documents the calling app owns (`docs.write`, `docs.edit`, `docs.get`): for example one app-owned `preference` with a `boot` rule that carries the app's agent instructions. */
71
73
  docs;
74
+ /** Register the app's permission schema (app listener only). */
75
+ schemas;
76
+ /** Write, replace and list the relation facts of the app's own schema (app listener only). */
77
+ relations;
72
78
  /** Scope-filtered provider tools and daemon-mediated calls. */
73
79
  providers;
74
80
  /** The caller's grant. With `wait` (1–25 s) and `since` (a `version` from an earlier answer), the runtime holds the
@@ -227,6 +233,8 @@ export class Crouter {
227
233
  this.questions = new Questions(request);
228
234
  this.memory = new Memory(request);
229
235
  this.docs = new Docs(request);
236
+ this.schemas = new Schemas(request);
237
+ this.relations = new Relations(request);
230
238
  this.runs = new Runs(request, (id, after, signal) => this.openRunEvents(id, after, signal), this.questions);
231
239
  this.providers = new Providers(request);
232
240
  this.grant = { get: (options = {}) => {
@@ -237,6 +245,10 @@ export class Crouter {
237
245
  this.health = { get: (options) => this.request('GET', '/v1/health', undefined, options) };
238
246
  this.usage = { get: (params = {}, options) => this.request('GET', withQuery('/v1/usage', { month: params.month }), undefined, options) };
239
247
  }
248
+ /** May the person `subject` (`user:<id>`) do `permission` on `resource` (`<type>:<id>` in the app's own namespace, or a crouter object id with `read|edit|message|use`)? App listener only. */
249
+ check(subject, permission, resource, options) {
250
+ return this.request('POST', routes.check(), { subject, permission, resource }, options);
251
+ }
240
252
  /** Establish NDJSON through the JSON client's cold-socket/retry transport; leave the live body unread. */
241
253
  async openCustomDeliveries(path, options) {
242
254
  const response = await this.call(undefined, () => this.client.rawResponse('GET', path, {
package/dist/index.d.ts CHANGED
@@ -12,7 +12,9 @@ export type { FileDownloadOptions } from './resources/files.js';
12
12
  export type { Upload, UploadCreateParams } from './resources/uploads.js';
13
13
  export type { AttachmentLink } from './resources/attachments.js';
14
14
  export type { Share, ShareCreated, ShareCreateParams } from './resources/shares.js';
15
- export type { Doc, DocEditParams, DocWriteParams } from './resources/docs.js';
15
+ export type { Doc, DocEditParams, DocListParams, DocWriteParams } from './resources/docs.js';
16
+ export type { Schemas } from './resources/schemas.js';
17
+ export type { Relations } from './resources/relations.js';
16
18
  export type { MemoryDocument, MemoryGetParams, MemoryListItem, MemoryListPage, MemoryListParams } from './resources/memory.js';
17
19
  export type { Question, QuestionAnswered, QuestionCanceled, QuestionFields, QuestionKind, QuestionResponses, QuestionSlot, QuestionStatus } from './resources/questions.js';
18
20
  export type { ProviderToolEntry } from './resources/providers.js';
@@ -1,4 +1,4 @@
1
- import { type DeliveryRuleDTO, type DocEditedDTO, type DocEditRequest, type DocKindName, type DocWriteRequest, type DocWrittenDTO, type ObjectDTO } from '@north-light/api';
1
+ import { type DeliveryRuleDTO, type DocDeletedDTO, type DocEditedDTO, type DocEditRequest, type DocKindName, type DocWriteRequest, type DocWrittenDTO, type ObjectDTO, type ObjectListDTO, type ObjectListRequest, type ObjectSearchDTO, type ObjectSearchRequest } from '@north-light/api';
2
2
  import type { RequestOptions } from '../types.js';
3
3
  import { type Request } from './request.js';
4
4
  /** `docs.write` input. */
@@ -13,9 +13,15 @@ export interface DocWriteParams extends Omit<DocWriteRequest, 'owner' | 'cwd' |
13
13
  kind?: DocKindName;
14
14
  /** When the document is delivered to a node: `{on: 'boot', deliver: 'content'}` puts the whole body into the context of every node at boot. */
15
15
  rules?: DeliveryRuleDTO[];
16
- /** Whose document it is. Defaults to the calling app (`'app'`), which is the only owner an app server can write as. */
17
- owner?: 'app';
16
+ /** Whose document it is. Defaults to the calling app (`'app'`). `'user'` writes it under the person the call's token names (`sub`),
17
+ * the member the app acts for; the app must hold an active grant from that person. */
18
+ owner?: 'app' | 'user';
18
19
  }
20
+ /** `docs.list` input: the `GET /v1/objects` filters, with `extensions` as an object. */
21
+ export type DocListParams = Omit<ObjectListRequest, 'extensions'> & {
22
+ /** Documents whose `extensions` hold these top-level fields (equality), e.g. `{ firstlight: { workspace_id: 'w1' } }`. Sent JSON-encoded. */
23
+ extensions?: ObjectListRequest['extensions'];
24
+ };
19
25
  /** `docs.edit` input: a new `body` or exact-text `replace` blocks (not both), and/or document fields. */
20
26
  export type DocEditParams = Omit<DocEditRequest, 'base'> & {
21
27
  /** The `base` from the `docs.get` the change was computed from. Absent, the edit is made from this app's last `docs.get` or `docs.write` of the document;
@@ -45,6 +51,10 @@ export interface Doc {
45
51
  base: string;
46
52
  /** The delivery rules the document carries. */
47
53
  rules: DeliveryRuleDTO[];
54
+ /** The plugin-keyed fields the document carries (`{ firstlight: {...} }`); `{}` when none. */
55
+ extensions: Record<string, Record<string, unknown>>;
56
+ /** The app type the document is an instance of (`firstlight/memory`); null when it is governed by `crtr/object`. */
57
+ schema_type: string | null;
48
58
  }
49
59
  /**
50
60
  * Documents the calling app owns (`docs.write` defaults the owner to the app): write one, read it, edit it in place.
@@ -59,6 +69,12 @@ export declare class Docs {
59
69
  /** Change a document in place: a new body or replacements, and/or fields (`preview`, `rules`, `privacy`, …). Returns the new `revision`.
60
70
  * `ref` is the canonical name `docs.write` returned. */
61
71
  edit(ref: string, params: DocEditParams, options?: RequestOptions): Promise<DocEditedDTO>;
72
+ /** Delete a document (`ref` is its canonical name or id) and, when it is schema-typed, its facts. Refused like any write the caller may not make. */
73
+ delete(ref: string, options?: RequestOptions): Promise<DocDeletedDTO>;
74
+ /** List the objects the app may read, newest first, a page at a time; `extensions` and `schema_type` filter inside the query, before the page limit. */
75
+ list(params?: DocListParams, options?: RequestOptions): Promise<ObjectListDTO>;
76
+ /** Search objects by name, `body` text or `grep`; `extensions` and `schema_type` filter inside the query in every mode. */
77
+ search(params: ObjectSearchRequest, options?: RequestOptions): Promise<ObjectSearchDTO>;
62
78
  /** Read a document with its body, revision, edit base and rules. Throws `NotFoundError` when no such document is in the app's view. */
63
79
  get(ref: string, options?: RequestOptions): Promise<Doc>;
64
80
  }
@@ -20,6 +20,19 @@ export class Docs {
20
20
  edit(ref, params, options) {
21
21
  return this.request('PATCH', routes.doc(ref), params, options);
22
22
  }
23
+ /** Delete a document (`ref` is its canonical name or id) and, when it is schema-typed, its facts. Refused like any write the caller may not make. */
24
+ delete(ref, options) {
25
+ return this.request('DELETE', routes.doc(ref), undefined, options);
26
+ }
27
+ /** List the objects the app may read, newest first, a page at a time; `extensions` and `schema_type` filter inside the query, before the page limit. */
28
+ list(params = {}, options) {
29
+ const { extensions, ...rest } = params;
30
+ return this.request('GET', withQuery(routes.objects(), { ...rest, ...(extensions !== undefined ? { extensions: JSON.stringify(extensions) } : {}) }), undefined, options);
31
+ }
32
+ /** Search objects by name, `body` text or `grep`; `extensions` and `schema_type` filter inside the query in every mode. */
33
+ search(params, options) {
34
+ return this.request('POST', routes.objectsSearch(), params, options);
35
+ }
23
36
  /** Read a document with its body, revision, edit base and rules. Throws `NotFoundError` when no such document is in the app's view. */
24
37
  async get(ref, options) {
25
38
  const read = await this.request('GET', withQuery(routes.object(ref), { fields: 1 }), undefined, options);
@@ -42,6 +55,8 @@ export class Docs {
42
55
  ...(match_frontmatter !== undefined ? { match_frontmatter } : {}),
43
56
  ...(condition !== undefined ? { if: condition } : {}),
44
57
  })),
58
+ extensions: data.fields?.extensions ?? {},
59
+ schema_type: read.object.schema_type ?? null,
45
60
  };
46
61
  }
47
62
  }
@@ -26,8 +26,8 @@ export interface MemoryListItem {
26
26
  kind: 'knowledge' | 'preference';
27
27
  preview: string;
28
28
  summary: string | null;
29
- /** An app sees `public`; `private` too while its grant holds `crtr:memory:read_private:user`; `friends` only with `crtr:memory:manage:user`. */
30
- privacy: 'public' | 'friends' | 'private';
29
+ /** An app sees `public`; `private` too while its grant holds `crtr:memory:read_private:user` (or `crtr:memory:manage:user`). */
30
+ privacy: 'public' | 'private';
31
31
  /** The current revision number. */
32
32
  revision: number;
33
33
  /** The time of revision 1. */
@@ -55,8 +55,8 @@ export interface MemoryDocument extends MemoryListItem {
55
55
  /**
56
56
  * Reads the documents the person owns. The daemon filters each one by the caller's
57
57
  * grant: an app reads `public` documents with no scope, `private` ones too while the grant
58
- * holds `crtr:memory:read_private:user` (the person may switch it on and off; `friends` never),
59
- * and every privacy level with `crtr:memory:manage:user`; a document above the grant's level
58
+ * holds `crtr:memory:read_private:user` (the person may switch it on and off),
59
+ * and `manage` covers both; a document above the grant's level
60
60
  * reads exactly as an absent one (`NotFoundError`). A run started with a `scopes` list sees
61
61
  * `private` only if that list covers `crtr:memory:read_private:user` (names it, or `crtr:memory:manage:user`). Reading records nothing.
62
62
  */
@@ -2,8 +2,8 @@ import { withQuery } from './request.js';
2
2
  /**
3
3
  * Reads the documents the person owns. The daemon filters each one by the caller's
4
4
  * grant: an app reads `public` documents with no scope, `private` ones too while the grant
5
- * holds `crtr:memory:read_private:user` (the person may switch it on and off; `friends` never),
6
- * and every privacy level with `crtr:memory:manage:user`; a document above the grant's level
5
+ * holds `crtr:memory:read_private:user` (the person may switch it on and off),
6
+ * and `manage` covers both; a document above the grant's level
7
7
  * reads exactly as an absent one (`NotFoundError`). A run started with a `scopes` list sees
8
8
  * `private` only if that list covers `crtr:memory:read_private:user` (names it, or `crtr:memory:manage:user`). Reading records nothing.
9
9
  */
@@ -0,0 +1,23 @@
1
+ import { type FactDTO, type RelationsChangedDTO, type RelationsListDTO } from '@north-light/api';
2
+ import type { RequestOptions } from '../types.js';
3
+ import { type Request } from './request.js';
4
+ /** The relation facts of the calling app's own schema namespaces (app listener only). A resource is `<type>:<id>`; a subject is `user:<id>`, `<type>:<id>` or `<type>:<id>#<relation>`. */
5
+ export declare class Relations {
6
+ private readonly request;
7
+ constructor(request: Request);
8
+ /** Write and delete facts in one call. Writing a fact that exists, or deleting one that does not, is a no-op. A relation the schema does not declare is refused `400 unknown_relation`; another app's namespace `403 not_schema_owner`. */
9
+ write(params: {
10
+ write?: FactDTO[];
11
+ delete?: FactDTO[];
12
+ }, options?: RequestOptions): Promise<RelationsChangedDTO>;
13
+ /** Make the facts `(resource, relation, *)` exactly `subjects`, in one transaction. */
14
+ replace(params: {
15
+ resource: string;
16
+ relation: string;
17
+ subjects: string[];
18
+ }, options?: RequestOptions): Promise<RelationsChangedDTO>;
19
+ /** The facts about one resource. */
20
+ list(params: {
21
+ resource: string;
22
+ }, options?: RequestOptions): Promise<RelationsListDTO>;
23
+ }
@@ -0,0 +1,21 @@
1
+ import { routes } from '@north-light/api';
2
+ import { withQuery } from './request.js';
3
+ /** The relation facts of the calling app's own schema namespaces (app listener only). A resource is `<type>:<id>`; a subject is `user:<id>`, `<type>:<id>` or `<type>:<id>#<relation>`. */
4
+ export class Relations {
5
+ request;
6
+ constructor(request) {
7
+ this.request = request;
8
+ }
9
+ /** Write and delete facts in one call. Writing a fact that exists, or deleting one that does not, is a no-op. A relation the schema does not declare is refused `400 unknown_relation`; another app's namespace `403 not_schema_owner`. */
10
+ write(params, options) {
11
+ return this.request('POST', routes.relations(), params, options);
12
+ }
13
+ /** Make the facts `(resource, relation, *)` exactly `subjects`, in one transaction. */
14
+ replace(params, options) {
15
+ return this.request('PUT', routes.relations(), params, options);
16
+ }
17
+ /** The facts about one resource. */
18
+ list(params, options) {
19
+ return this.request('GET', withQuery(routes.relations(), params), undefined, options);
20
+ }
21
+ }
@@ -0,0 +1,14 @@
1
+ import { type SchemaRegisterDTO } from '@north-light/api';
2
+ import type { RequestOptions } from '../types.js';
3
+ import type { Request } from './request.js';
4
+ /** The permission schemas an app registers on a person's runtime (app listener only). */
5
+ export declare class Schemas {
6
+ private readonly request;
7
+ constructor(request: Request);
8
+ /** Register or update the calling app's permission schema (SpiceDB syntax) for `namespace`. The first app to register a namespace on a runtime owns it;
9
+ * another app naming it is refused `403 namespace_taken`; a source that does not parse is `400 schema_invalid`, and one that drops a relation facts still use is `409 relation_in_use`. `changed` is false when `source` is the registered one. */
10
+ register(params: {
11
+ namespace: string;
12
+ source: string;
13
+ }, options?: RequestOptions): Promise<SchemaRegisterDTO>;
14
+ }
@@ -0,0 +1,13 @@
1
+ import { routes } from '@north-light/api';
2
+ /** The permission schemas an app registers on a person's runtime (app listener only). */
3
+ export class Schemas {
4
+ request;
5
+ constructor(request) {
6
+ this.request = request;
7
+ }
8
+ /** Register or update the calling app's permission schema (SpiceDB syntax) for `namespace`. The first app to register a namespace on a runtime owns it;
9
+ * another app naming it is refused `403 namespace_taken`; a source that does not parse is `400 schema_invalid`, and one that drops a relation facts still use is `409 relation_in_use`. `changed` is false when `source` is the registered one. */
10
+ register(params, options) {
11
+ return this.request('POST', routes.schemas(), params, options);
12
+ }
13
+ }
@@ -2,7 +2,7 @@
2
2
  name: northlight
3
3
  description: "Use when building, debugging or shipping an app that signs people in with Northlight, starts runs on their Northlight runtime, or calls Northlight providers (sms, search, integrations, context, device) with @north-light/sdk."
4
4
  metadata:
5
- sdk-version: "0.3.549"
5
+ sdk-version: "0.3.551"
6
6
  ---
7
7
 
8
8
  # Building a Northlight app
@@ -27,7 +27,7 @@ Never text a person first: Comms refuses a send to anyone who has not texted you
27
27
 
28
28
  ## First: check this skill matches the SDK
29
29
 
30
- This skill is for SDK version `0.3.549`. If the project has `node_modules/@north-light/sdk/package.json`, compare its `version` with that number. If they differ, run `npx @north-light/sdk skill` in the project, then read the rewritten `.agents/skills/northlight/SKILL.md` (or `.claude/skills/northlight/SKILL.md`) instead of this copy. It needs no account. A project that has no SDK yet starts with `npx @north-light/sdk@latest init <dir>`.
30
+ This skill is for SDK version `0.3.551`. If the project has `node_modules/@north-light/sdk/package.json`, compare its `version` with that number. If they differ, run `npx @north-light/sdk skill` in the project, then read the rewritten `.agents/skills/northlight/SKILL.md` (or `.claude/skills/northlight/SKILL.md`) instead of this copy. It needs no account. A project that has no SDK yet starts with `npx @north-light/sdk@latest init <dir>`.
31
31
 
32
32
  ## Where things run
33
33
 
@@ -3,10 +3,11 @@ import { Crouter } from '../client.js';
3
3
  import { type ConnectionStore } from '../oauth/index.js';
4
4
  import { type FakeNotice, type RunNoticeOptions } from './notices.js';
5
5
  import { FakeRun } from './run.js';
6
+ import { type SpaceOp } from './space.js';
6
7
  import { type FakeRefusalCode, type FakeScope, type FakeUser, type Responder } from './users.js';
7
8
  import { type FakeError } from './wire.js';
8
9
  /** A runtime operation, named as the daemon's `OPERATIONS` table names it. */
9
- export type FakeOp = 'grant.get' | 'run.start' | 'run.list' | 'run.get' | 'run.message' | 'run.events' | 'run.interrupt' | 'run.cancel' | 'run.delete' | 'run.rename' | 'question.get' | 'question.answer' | 'question.cancel' | 'trace.get' | 'memory.list' | 'memory.get' | 'provider.tools' | 'provider.call' | 'profile.list';
10
+ export type FakeOp = 'grant.get' | 'run.start' | 'run.list' | 'run.get' | 'run.message' | 'run.events' | 'run.interrupt' | 'run.cancel' | 'run.delete' | 'run.rename' | 'question.get' | 'question.answer' | 'question.cancel' | 'trace.get' | 'memory.list' | 'memory.get' | 'provider.tools' | 'provider.call' | 'profile.list' | SpaceOp;
10
11
  /** A directory operation. */
11
12
  export type FakeDirectoryOp = 'directory.discovery' | 'directory.token' | 'directory.jwks';
12
13
  /** What `intercept` / `failNext` / `gate` match: a runtime operation, `'*'` (every runtime operation), or a directory operation. */
@@ -298,8 +299,8 @@ export declare class FakeCloud {
298
299
  private runMessage;
299
300
  private question;
300
301
  private memoryStore;
301
- /** `public` always; `private` with `crtr:memory:read_private:user`; every level with `crtr:memory:manage:user`; `friends` never
302
- * below manage. Unlike the real runtime, which narrows a run to the `scopes` it was started with, this reads the user's whole
302
+ /** `public` always; `private` with `crtr:memory:read_private:user` or `crtr:memory:manage:user`.
303
+ * Unlike the real runtime, which narrows a run to the `scopes` it was started with, this reads the user's whole
303
304
  * grant (`user.grant.set`): narrowed runs are tested on the real runtime. */
304
305
  private memoryVisible;
305
306
  private memoryList;
@@ -10,6 +10,7 @@ import { MemoryConnectionStore, OAuth2Client } from '../oauth/index.js';
10
10
  import { SDK_VERSION } from '../version.js';
11
11
  import { RunNotices, SigningKey } from './notices.js';
12
12
  import { FakeRun } from './run.js';
13
+ import { SPACE_OPS } from './space.js';
13
14
  import { UserState, normalizeScopes } from './users.js';
14
15
  import { FakeRefused, decodeUnsigned, envelopeBody, fakeError, integerParam, invalid, isFakeError, jsonAnswer, notFound, objectBody, refuse, requestedScopes, sha256Hex, unsignedJwt, waitSeconds, } from './wire.js';
15
16
  /** The directory's cap on tester entries per app. */
@@ -35,6 +36,17 @@ const ROUTES = [
35
36
  ['GET', /^\/v1\/providers\/([^/]+)\/tools$/, 'provider.tools'],
36
37
  ['POST', /^\/v1\/provider\/call$/, 'provider.call'],
37
38
  ['GET', /^\/v1\/profiles$/, 'profile.list'],
39
+ ['POST', /^\/v1\/schemas$/, 'schema.register'],
40
+ ['POST', /^\/v1\/relations$/, 'relations.write'],
41
+ ['PUT', /^\/v1\/relations$/, 'relations.replace'],
42
+ ['GET', /^\/v1\/relations$/, 'relations.list'],
43
+ ['POST', /^\/v1\/check$/, 'check'],
44
+ ['POST', /^\/v1\/docs$/, 'doc.write'],
45
+ ['PATCH', /^\/v1\/docs\/([^/]+)$/, 'doc.edit'],
46
+ ['DELETE', /^\/v1\/docs\/([^/]+)$/, 'doc.delete'],
47
+ ['GET', /^\/v1\/objects$/, 'object.list'],
48
+ ['POST', /^\/v1\/objects\/search$/, 'object.search'],
49
+ ['GET', /^\/v1\/objects\/([^/]+)$/, 'object.get'],
38
50
  ];
39
51
  const DIRECTORY_OPS = new Set(['directory.discovery', 'directory.token', 'directory.jwks']);
40
52
  const RUN_OPS = new Set(['run.get', 'run.delete', 'run.message', 'run.events', 'run.interrupt', 'run.cancel', 'run.rename', 'trace.get']);
@@ -368,6 +380,7 @@ export class FakeCloud {
368
380
  user.runs.length = 0;
369
381
  user.responder = null;
370
382
  user.memory.clear();
383
+ user.space.clear();
371
384
  user.idempotency.clear();
372
385
  }
373
386
  this.responder = null;
@@ -596,7 +609,7 @@ export class FakeCloud {
596
609
  const id = segment === undefined ? undefined : decodeURIComponent(segment);
597
610
  let body = null;
598
611
  let badJson = false;
599
- if (request.method === 'POST') {
612
+ if (request.method === 'POST' || request.method === 'PUT' || request.method === 'PATCH') {
600
613
  const text = await request.text();
601
614
  if (text) {
602
615
  try {
@@ -808,7 +821,10 @@ export class FakeCloud {
808
821
  case 'provider.tools': return this.providerTools(user, id);
809
822
  case 'provider.call': return this.providerCall(user, body, signal);
810
823
  case 'profile.list': return { status: 200, body: { profiles: [{ profile_id: 'default', name: 'default', grantee: this.host.grantee, default_kind: 'general', default: true }] } };
811
- default: throw notFound('unknown operation');
824
+ default:
825
+ if (SPACE_OPS.has(op))
826
+ return user.space.handle(op, id, query, body);
827
+ throw notFound('unknown operation');
812
828
  }
813
829
  }
814
830
  async grantGet(user, query, signal, claims) {
@@ -976,8 +992,8 @@ export class FakeCloud {
976
992
  if (value !== 'user')
977
993
  throw invalid('store must be "user"', 'store');
978
994
  }
979
- /** `public` always; `private` with `crtr:memory:read_private:user`; every level with `crtr:memory:manage:user`; `friends` never
980
- * below manage. Unlike the real runtime, which narrows a run to the `scopes` it was started with, this reads the user's whole
995
+ /** `public` always; `private` with `crtr:memory:read_private:user` or `crtr:memory:manage:user`.
996
+ * Unlike the real runtime, which narrows a run to the `scopes` it was started with, this reads the user's whole
981
997
  * grant (`user.grant.set`): narrowed runs are tested on the real runtime. */
982
998
  memoryVisible(user, doc) {
983
999
  const scopes = user.scopeStrings();
@@ -0,0 +1,37 @@
1
+ import { type Definition } from './schema-parser.js';
2
+ 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";
3
+ export declare const OBJECT_TYPE = "crtr/object";
4
+ export type Definitions = ReadonlyMap<string, Definition>;
5
+ export interface FactSubject {
6
+ type: string;
7
+ id: string;
8
+ relation: string;
9
+ }
10
+ export interface Fact {
11
+ resourceType: string;
12
+ resourceId: string;
13
+ relation: string;
14
+ subject: FactSubject;
15
+ }
16
+ export declare const formatSubject: (s: FactSubject) => string;
17
+ export declare const factKey: (f: Fact) => string;
18
+ /** `<type>:<id>[#<relation>]`; the type may contain `/`. */
19
+ export declare function parseSubject(text: string): FactSubject;
20
+ /** `<type>:<id>` in an app namespace, or a bare crouter object id. */
21
+ export declare function parseResource(text: string): {
22
+ type: string;
23
+ id: string;
24
+ };
25
+ export declare const builtinDefinitions: () => Map<string, Definition>;
26
+ /** Parse `source` as the schema of `namespace`: every definition must live in the namespace and every subject type must be declared. */
27
+ export declare function validateSchema(namespace: string, source: string): Definition[];
28
+ /** The relations `previous` declares that `next` drops: `{ name: '<definition>#<relation>' }` for each. */
29
+ export declare function droppedRelations(previous: Definition[], next: Definition[]): Array<{
30
+ definition: string;
31
+ relation: string;
32
+ }>;
33
+ /** A fact must name a defined type, a relation that type declares, and a subject the relation lists (so `editor @ user:*` fails like any unlisted subject). */
34
+ export declare function validateFact(defs: Definitions, fact: Fact): void;
35
+ /** Does `userId` hold `permission` on the resource? An unknown type or permission is false, as in the daemon. A person reaches no `crtr/group`:
36
+ * an app cannot write group facts, so no group is ever reached here. */
37
+ export declare function holds(defs: Definitions, facts: Iterable<Fact>, userId: string, resourceType: string, resourceId: string, permission: string): boolean;