@north-light/sdk 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/client.d.ts +10 -2
- package/dist/client.js +13 -1
- package/dist/index.d.ts +3 -1
- package/dist/resources/docs.d.ts +19 -3
- package/dist/resources/docs.js +15 -0
- package/dist/resources/memory.d.ts +4 -4
- package/dist/resources/memory.js +2 -2
- package/dist/resources/relations.d.ts +23 -0
- package/dist/resources/relations.js +21 -0
- package/dist/resources/schemas.d.ts +14 -0
- package/dist/resources/schemas.js +13 -0
- package/dist/skill/northlight/SKILL.md +2 -2
- package/dist/testing/cloud.d.ts +4 -3
- package/dist/testing/cloud.js +20 -4
- package/dist/testing/permissions.d.ts +37 -0
- package/dist/testing/permissions.js +261 -0
- package/dist/testing/schema-parser.d.ts +29 -0
- package/dist/testing/schema-parser.js +153 -0
- package/dist/testing/space.d.ts +42 -0
- package/dist/testing/space.js +265 -0
- package/dist/testing/users.d.ts +4 -1
- package/dist/testing/users.js +4 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +5 -5
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
|
|
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
|
|
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';
|
package/dist/resources/docs.d.ts
CHANGED
|
@@ -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'`)
|
|
17
|
-
|
|
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
|
}
|
package/dist/resources/docs.js
CHANGED
|
@@ -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
|
|
30
|
-
privacy: 'public' | '
|
|
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
|
|
59
|
-
* and
|
|
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
|
*/
|
package/dist/resources/memory.js
CHANGED
|
@@ -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
|
|
6
|
-
* and
|
|
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.
|
|
5
|
+
sdk-version: "0.3.550"
|
|
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.
|
|
30
|
+
This skill is for SDK version `0.3.550`. 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
|
|
package/dist/testing/cloud.d.ts
CHANGED
|
@@ -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
|
|
302
|
-
*
|
|
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;
|
package/dist/testing/cloud.js
CHANGED
|
@@ -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:
|
|
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
|
|
980
|
-
*
|
|
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;
|