@north-light/api 0.3.482 → 0.3.484

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -22,6 +22,7 @@ import type { FileEncoding, FileListDTO, FilePeekDTO, FileWriteDTO } from './dto
22
22
  import type { BashRunDTO, BashRunParams } from './dto/bash.js';
23
23
  import type { EdgesDTO, EdgesQuery, ObjectListDTO, ObjectListRequest, ObjectReadDTO, ObjectReadQuery, ObjectSearchDTO, ObjectSearchRequest, UnwatchQuery, UnwatchResultDTO, WatchRequest, WatchResultDTO } from './dto/objects.js';
24
24
  import type { DocDeletedDTO, DocEditRequest, DocEditedDTO, DocHistoryDTO, DocHistoryQuery, DocLintDTO, DocLintRequest, DocMoveRequest, DocMovedDTO, DocWriteRequest, DocWrittenDTO, ReconcilePackagesDTO } from './dto/docs.js';
25
+ import type { PluginAppTokenDTO, PluginCallHeadersDTO, PluginCallHeadersRequestDTO } from './dto/plugins.js';
25
26
  import type { BackgroundedJobDTO, BackgroundedJobRequest, DeliveryDryRunRequest, DeliveryRequest, DeliveryResponse } from './dto/delivery.js';
26
27
  import type { ChatInventoryDTO, ProspectiveChatInventoryDTO, ProspectiveChatInventoryQuery } from './dto/chat-inventory.js';
27
28
  import type { CredentialRemovalResultDTO, CredentialResultDTO, InstallCredentialRequest, CompleteModelAuthFlowRequest, ModelAuthFlowCompletedDTO, ModelAuthFlowDTO, ModelAuthFlowStartedDTO, ModelAuthListDTO, ModelAuthReadinessDTO, ModelAuthReadinessQuery, StartModelAuthFlowRequest } from './dto/modelauth.js';
@@ -299,6 +300,11 @@ export declare class CrtrClient {
299
300
  readonly documents: {
300
301
  reconcilePackages: () => Promise<ReconcilePackagesDTO>;
301
302
  };
303
+ /** A plugin's `crtr_app_` credential and the headers crtrd adds to its local HTTP calls. */
304
+ readonly plugins: {
305
+ appToken: (name: string) => Promise<PluginAppTokenDTO>;
306
+ callHeaders: (name: string, request: PluginCallHeadersRequestDTO) => Promise<PluginCallHeadersDTO>;
307
+ };
302
308
  /** The broker's delivery of one node activity. */
303
309
  nodeDelivery(id: string, request: DeliveryRequest): Promise<DeliveryResponse>;
304
310
  /** A person's dry run of one delivery activity, for a live or hypothetical node. Writes nothing. */
@@ -529,6 +529,11 @@ export class CrtrClient {
529
529
  documents = {
530
530
  reconcilePackages: () => this.request('POST', routes.documentsReconcilePackages()),
531
531
  };
532
+ /** A plugin's `crtr_app_` credential and the headers crtrd adds to its local HTTP calls. */
533
+ plugins = {
534
+ appToken: (name) => this.request('GET', routes.pluginAppToken(name)),
535
+ callHeaders: (name, request) => this.request('POST', routes.pluginCallHeaders(name), request),
536
+ };
532
537
  /** The broker's delivery of one node activity. */
533
538
  nodeDelivery(id, request) {
534
539
  return this.request('POST', routes.nodeDelivery(this.nodePath(id)), request);
@@ -1,5 +1,5 @@
1
1
  /** The contract level this build serves and requires. Written by `scripts/contract.mjs release`; never edit by hand. */
2
- export declare const CONTRACT_LEVEL = "1.8";
2
+ export declare const CONTRACT_LEVEL = "1.10";
3
3
  /** Request header naming the one contract level the caller needs, e.g. `1.3`. Optional: absent means no check. */
4
4
  export declare const REQUIRED_CONTRACT_HEADER = "Crouter-Required-Contract";
5
5
  /** Response header listing the contract levels the server serves, comma-separated, one per major, e.g. `1.3`. */
@@ -12,7 +12,7 @@
12
12
  //
13
13
  // PURITY (spec §3.1): Node built-ins + `src/api/*` only.
14
14
  /** The contract level this build serves and requires. Written by `scripts/contract.mjs release`; never edit by hand. */
15
- export const CONTRACT_LEVEL = '1.8';
15
+ export const CONTRACT_LEVEL = '1.10';
16
16
  /** Request header naming the one contract level the caller needs, e.g. `1.3`. Optional: absent means no check. */
17
17
  export const REQUIRED_CONTRACT_HEADER = 'Crouter-Required-Contract';
18
18
  /** Response header listing the contract levels the server serves, comma-separated, one per major, e.g. `1.3`. */
@@ -1,3 +1,4 @@
1
+ import type { PrivacyName } from './objects.js';
1
2
  export interface CustomEventSpecDTO {
2
3
  level: number;
3
4
  supersedes?: boolean;
@@ -13,9 +14,12 @@ export interface RegisterCustomObjectRequestDTO {
13
14
  name: string;
14
15
  owner?: string;
15
16
  description: string;
16
- events: Record<string, CustomEventSpecDTO>;
17
+ /** Required unless an installed plugin declares the kind; then a register without it creates only the instance. */
18
+ events?: Record<string, CustomEventSpecDTO>;
17
19
  receiver?: CustomReceiverDTO;
18
20
  rotate_token?: boolean;
21
+ /** Who can read the instance: `public` (default), `friends` or `private`. Setting it on an existing instance changes it. */
22
+ privacy?: PrivacyName;
19
23
  }
20
24
  export interface RegisterCustomObjectResponseDTO {
21
25
  id: string;
@@ -139,4 +139,30 @@ export interface ReconcilePackagesDTO {
139
139
  reason: string;
140
140
  movedTo?: string;
141
141
  }>;
142
+ /** `object_kinds` the installed plugins declared: what applied, what another owner holds, which plugins are invalid */
143
+ kinds?: {
144
+ applied: Array<{
145
+ plugin: string;
146
+ kind: string;
147
+ outcome: 'inserted' | 'updated' | 'unchanged';
148
+ }>;
149
+ refused: Array<{
150
+ plugin: string;
151
+ kind: string;
152
+ code: 'kind_owned';
153
+ message: string;
154
+ }>;
155
+ invalid: Array<{
156
+ plugin: string;
157
+ error: string;
158
+ }>;
159
+ };
160
+ /** the `crtr_app_` tokens after this pass: who holds one (with its scopes) and whose was removed */
161
+ tokens?: {
162
+ granted: Array<{
163
+ plugin: string;
164
+ scopes: string[];
165
+ }>;
166
+ revoked: string[];
167
+ };
142
168
  }
@@ -1,10 +1,12 @@
1
- /** A provider's setup state for the account (directory spec, "Provider setup state"). */
2
- export type SetupState = 'not_started' | 'pending' | 'ready' | 'failed';
1
+ /** A provider's setup state for the account (directory spec, "Provider setup state"). `unavailable` means the provider does not offer this setup to the account; it is not `failed`, and there is nothing for the person to do. */
2
+ export type SetupState = 'not_started' | 'pending' | 'ready' | 'failed' | 'unavailable';
3
3
  /** One provider the grantee requires, as the directory's grant sync carried it. */
4
4
  export interface ProviderDependency {
5
5
  state: SetupState;
6
- /** The provider's own sentence for `pending` and `failed` (its `message_pending` / `message_failed`); absent for `not_started` and `ready`. */
6
+ /** The provider's own sentence for `pending`, `failed` and `unavailable` (its `message_pending` / `message_failed` / `message_unavailable`); absent for `not_started` and `ready`. */
7
7
  message?: string;
8
+ /** Where in its journey the provider is, when it reports one (for example `researching`, or `downloaded` on `device`). The values belong to the provider and may grow, so treat an unknown one as no step. Absent when the provider reports none. Changes `version` like any other field. */
9
+ step?: string;
8
10
  /** Absolute URL of the directory's setup page for this provider and this app (`<issuer>/connect/setup/<provider>?client_id=<grantee>`); the app may append `return_to` (its own registered redirect origin) so Continue brings the person back. */
9
11
  next: string;
10
12
  }
@@ -21,7 +23,7 @@ export interface GrantGetDTO {
21
23
  }[];
22
24
  spend_cap: number | null;
23
25
  payer: 'user';
24
- /** `{ <provider id>: ProviderDependency }` as the directory's grant sync carried it; `{}` when the grantee has none. The daemon performs no `requires` check: an app follows this with the SDK grant watch (`oauth.watchGrants`, or `grant.get` with `wait`/`since`) and refuses its own operation with `provider_setup_pending`. When an entry's `state` is not `ready`, show its `message` (the provider's own sentence, when it has one) and send the person to its `next` (the directory's setup page) — never copy the provider's prose into the app. */
26
+ /** `{ <setup id>: ProviderDependency }` (a setup id is a provider id such as `context`, or a dotted level such as `context.deep`) as the directory's grant sync carried it; `{}` when the grantee has none. The daemon performs no `requires` check: an app follows this with the SDK grant watch (`oauth.watchGrants`, or `grant.get` with `wait`/`since`) and refuses its own operation with `provider_setup_pending`. When an entry's `state` is not `ready`, show its `message` (the provider's own sentence, when it has one) and send the person to its `next` (the directory's setup page; not for `unavailable`, where there is nothing for them to do) — never copy the provider's prose into the app. */
25
27
  dependencies: Record<string, ProviderDependency>;
26
28
  /** Opaque; changes exactly when `rev`, `resolved_scopes`, `spend_cap`, `payer` or `dependencies` changes (never on a periodic sync alone or a new token). Pass it back as `since` with `wait` to hold the read until it changes. */
27
29
  version: string;
@@ -6,6 +6,22 @@ export interface ObjectOwnerDTO {
6
6
  type: OwnerTypeName;
7
7
  handle: string;
8
8
  }
9
+ /** A custom kind's declaration, carried with an object so another machine can hold a stub of it. It grants nothing. */
10
+ export interface ObjectKindDeclDTO {
11
+ name: string;
12
+ description: string;
13
+ events: Record<string, {
14
+ level: number;
15
+ supersedes?: boolean;
16
+ }>;
17
+ owner: {
18
+ id: string;
19
+ type: 'person' | 'app' | 'repo' | 'profile';
20
+ scheme: string | null;
21
+ external_id: string | null;
22
+ handle: string;
23
+ };
24
+ }
9
25
  export interface ObjectDTO {
10
26
  id: string;
11
27
  /** The object's kind name: `document`, `node`, `job`, `person`, `app`, `repo`, `profile`, or a later kind. */
@@ -17,6 +33,8 @@ export interface ObjectDTO {
17
33
  preview: string;
18
34
  created_at: string;
19
35
  deleted_at: string | null;
36
+ /** Present on an instance of a custom kind. */
37
+ kind_decl?: ObjectKindDeclDTO;
20
38
  }
21
39
  export interface DocHeadDTO {
22
40
  rev_id: string;
@@ -171,11 +189,13 @@ export interface WatchRequest {
171
189
  patterns?: string[];
172
190
  config?: ListenConfigDTO;
173
191
  filter?: Record<string, unknown>;
174
- watcher?: string;
192
+ watcher?: string; /** With `watcher`: the machine of the account that homes that node (a node not on this machine). */
193
+ watcher_machine?: string;
175
194
  }
176
195
  export interface UnwatchQuery {
177
196
  pattern?: string;
178
197
  watcher?: string;
198
+ watcher_machine?: string;
179
199
  }
180
200
  export interface WatchResultDTO {
181
201
  object: ObjectDTO;
@@ -0,0 +1,22 @@
1
+ /** `GET /v1/plugins/:name/app-token`. */
2
+ export interface PluginAppTokenDTO {
3
+ /** The plugin's `crtr_app_` bearer for crtrd's `/v1` API. */
4
+ token: string;
5
+ /** The crtrd unix socket to present it on. */
6
+ socket: string;
7
+ }
8
+ /** `POST /v1/plugins/:name/call-headers`. Digests only: neither the body nor any header value reaches crtrd. */
9
+ export interface PluginCallHeadersRequestDTO {
10
+ method: string;
11
+ url: string;
12
+ /** Lowercase hex sha256 of the exact body bytes sent (of "" when none). */
13
+ body_sha256: string;
14
+ /** Lowercase names of the headers the signature covers, sorted. */
15
+ header_names: string[];
16
+ /** Lowercase hex sha256 over `<name>:<value>\n` for each name in `header_names`. */
17
+ headers_sha256: string;
18
+ }
19
+ export interface PluginCallHeadersDTO {
20
+ /** `Crtr-Caller`, and `Crtr-Signature` when the call qualifies for signing. */
21
+ headers: Record<string, string>;
22
+ }
@@ -0,0 +1,5 @@
1
+ // Plugin credential routes: the app token a plugin's own service or exec leaf uses, and the headers crtrd adds to a
2
+ // local HTTP plugin call.
3
+ //
4
+ // PURITY (spec §3.1): Node built-ins + `src/api/*` only.
5
+ export {};
@@ -48,4 +48,5 @@ export * from './dto/review-comments.js';
48
48
  export * from './dto/chat-inventory.js';
49
49
  export * from './dto/analytics.js';
50
50
  export * from './dto/custom-objects.js';
51
+ export * from './dto/plugins.js';
51
52
  export type { WatchRequest, WatchResultDTO, UnwatchQuery, UnwatchResultDTO } from './dto/objects.js';
package/dist/api/index.js CHANGED
@@ -45,3 +45,4 @@ export * from './dto/review-comments.js';
45
45
  export * from './dto/chat-inventory.js';
46
46
  export * from './dto/analytics.js';
47
47
  export * from './dto/custom-objects.js';
48
+ export * from './dto/plugins.js';
@@ -160,5 +160,7 @@ export declare const routes: {
160
160
  readonly docMove: (ref: string) => string;
161
161
  readonly docHistory: (ref: string) => string;
162
162
  readonly docsLint: () => string;
163
+ readonly pluginAppToken: (name: string) => string;
164
+ readonly pluginCallHeaders: (name: string) => string;
163
165
  readonly documentsReconcilePackages: () => string;
164
166
  };
@@ -193,5 +193,7 @@ export const routes = {
193
193
  docMove: (ref) => `${V}/docs/${encodeURIComponent(ref)}/move`,
194
194
  docHistory: (ref) => `${V}/docs/${encodeURIComponent(ref)}/history`,
195
195
  docsLint: () => `${V}/docs/lint`,
196
+ pluginAppToken: (name) => `${V}/plugins/${encodeURIComponent(name)}/app-token`,
197
+ pluginCallHeaders: (name) => `${V}/plugins/${encodeURIComponent(name)}/call-headers`,
196
198
  documentsReconcilePackages: () => `${V}/documents/reconcile-packages`,
197
199
  };
@@ -1,5 +1,5 @@
1
1
  import { type ValidatedCommandManifest } from '../../api/command-manifest/manifest.js';
2
- import type { KindConfig, MemoryExtensionDeclarations, PageComponentRegistration, PluginBundle, PluginManifest, Scope } from '../../types.js';
2
+ import type { KindConfig, MemoryExtensionDeclarations, ObjectKindDeclaration, PageComponentRegistration, PluginBundle, PluginManifest, Scope } from '../../types.js';
3
3
  import { type BundleTransport } from './endpoint.js';
4
4
  /** Who may call a capability provider's tool group (contract 3). */
5
5
  export type ProviderGroupWho = 'own' | 'user' | 'allowlist' | 'any';
@@ -51,6 +51,9 @@ export interface ValidatedBundle {
51
51
  /** Plugin-owned memory metadata declarations copied into the synthesized
52
52
  * installed manifest alongside the archive's memory documents. */
53
53
  memory_extensions?: MemoryExtensionDeclarations;
54
+ /** Canvas object kinds and scopes declared in `bundle.json` (optional), copied into the synthesized manifest. */
55
+ object_kinds?: Record<string, ObjectKindDeclaration>;
56
+ scopes?: string[];
54
57
  commands: ValidatedCommandManifest;
55
58
  commandsBytes: Uint8Array;
56
59
  directories: ReadonlyArray<{
@@ -8,13 +8,14 @@ import { general } from '../errors.js';
8
8
  import { atomicWriteJson, atomicWriteText, ensureDir, nowIso, pathExists, removePath, syncDirectory, syncTree } from '../fs-utils.js';
9
9
  import { invalidPageComponentsReasons, normalizePageComponents } from '../human/page-catalog.js';
10
10
  import { validateMemoryExtensionDeclarations } from '../plugin-extensions.js';
11
+ import { objectBlocksShapeReasons } from '../plugin-objects-manifest.js';
11
12
  import { validateBundleTransport } from './endpoint.js';
12
13
  /** A capability provider id or tool group id: contract 1's `<id>` grammar. */
13
14
  const PROVIDER_ID = /^[A-Za-z0-9_-]{1,64}$/;
14
15
  const PROVIDER_KEYS = new Set(['id', 'major', 'groups']);
15
16
  const PROVIDER_GROUP_KEYS = new Set(['group', 'consent_line', 'rate', 'who', 'allowlist']);
16
17
  const PROVIDER_WHO = new Set(['own', 'user', 'allowlist', 'any']);
17
- const BUNDLE_KEYS = new Set(['bundleVersion', 'kinds', 'page_components', 'memory_extensions', 'version', 'transport', 'provider']);
18
+ const BUNDLE_KEYS = new Set(['bundleVersion', 'kinds', 'page_components', 'memory_extensions', 'version', 'transport', 'provider', 'object_kinds', 'scopes']);
18
19
  function issue(code, message, received, expected, next, path) {
19
20
  return { code, message, received, expected, next, ...(path !== undefined ? { path } : {}) };
20
21
  }
@@ -275,7 +276,7 @@ function parseBundleMetadata(bytes) {
275
276
  const unknownKeys = Object.keys(record).filter((key) => !BUNDLE_KEYS.has(key));
276
277
  if (record['bundleVersion'] !== 1 || unknownKeys.length > 0) {
277
278
  return {
278
- issue: bundleInvalid('bundle.json must contain bundleVersion 1 and at most version, transport, provider, kinds, page_components, and memory_extensions', JSON.stringify(raw), 'an object with bundleVersion set to 1 and optional version, transport, provider, kinds, page_components, and memory_extensions', 'Set bundle.json to {"bundleVersion":1} plus optional contribution blocks.', 'bundle.json'),
279
+ issue: bundleInvalid('bundle.json must contain bundleVersion 1 and at most version, transport, provider, kinds, page_components, memory_extensions, object_kinds, and scopes', JSON.stringify(raw), 'an object with bundleVersion set to 1 and optional version, transport, provider, kinds, page_components, memory_extensions, object_kinds, and scopes', 'Set bundle.json to {"bundleVersion":1} plus optional contribution blocks.', 'bundle.json'),
279
280
  };
280
281
  }
281
282
  // Install-time strictness: a bad block fails the install loudly here, because
@@ -297,6 +298,15 @@ function parseBundleMetadata(bytes) {
297
298
  };
298
299
  }
299
300
  }
301
+ const objectReasons = objectBlocksShapeReasons(record);
302
+ if (record['provider'] !== undefined && record['scopes'] !== undefined) {
303
+ objectReasons.push('scopes is for local plugins; a provider gets its scopes from directory consent');
304
+ }
305
+ if (objectReasons.length > 0) {
306
+ return {
307
+ issue: bundleInvalid('bundle.json object_kinds or scopes block is invalid', objectReasons.join('; '), 'object_kinds keyed by kind name with description, events and optional read.command; scopes an array of crtr:objects:<verb>:<target> strings (local plugins only)', 'Fix the object_kinds and scopes blocks in bundle.json.', 'bundle.json'),
308
+ };
309
+ }
300
310
  let memoryExtensions;
301
311
  if (record['memory_extensions'] !== undefined) {
302
312
  try {
@@ -342,6 +352,8 @@ function parseBundleMetadata(bytes) {
342
352
  ...(record['kinds'] !== undefined ? { kinds: record['kinds'] } : {}),
343
353
  ...(record['page_components'] !== undefined ? { page_components: normalizePageComponents(record['page_components']) } : {}),
344
354
  ...(memoryExtensions !== undefined ? { memory_extensions: memoryExtensions } : {}),
355
+ ...(record['object_kinds'] !== undefined ? { object_kinds: record['object_kinds'] } : {}),
356
+ ...(record['scopes'] !== undefined ? { scopes: record['scopes'] } : {}),
345
357
  };
346
358
  }
347
359
  function parseCommands(bytes, reservedCoreNames, coreCommandPaths, extensibleCoreBranches = new Set()) {
@@ -421,6 +433,8 @@ export async function validatePluginBundle(archive, options) {
421
433
  ...(metadata.kinds !== undefined ? { kinds: metadata.kinds } : {}),
422
434
  ...(metadata.page_components !== undefined ? { page_components: metadata.page_components } : {}),
423
435
  ...(metadata.memory_extensions !== undefined ? { memory_extensions: metadata.memory_extensions } : {}),
436
+ ...(metadata.object_kinds !== undefined ? { object_kinds: metadata.object_kinds } : {}),
437
+ ...(metadata.scopes !== undefined ? { scopes: metadata.scopes } : {}),
424
438
  commands: commandValidation.commands,
425
439
  commandsBytes: commands.bytes,
426
440
  directories: parsed.members
@@ -534,6 +548,9 @@ export async function installBundleArchive(name, raw, options) {
534
548
  // later archive that no longer declares them is what retires them.
535
549
  ...(validated.page_components !== undefined ? { page_components: validated.page_components } : {}),
536
550
  ...(validated.memory_extensions !== undefined ? { memory_extensions: validated.memory_extensions } : {}),
551
+ // Canvas object kinds and scopes, copied like page_components: a later archive that drops a block drops it here.
552
+ ...(validated.object_kinds !== undefined ? { object_kinds: validated.object_kinds } : {}),
553
+ ...(validated.scopes !== undefined ? { scopes: validated.scopes } : {}),
537
554
  ...(validated.hooks !== undefined ? { hooks: 'hooks.json', hookExecutable: 'hooks/dispatch' } : {}),
538
555
  // A capability provider's block, read by provider composition and calls.
539
556
  ...(validated.provider !== undefined ? { provider: validated.provider } : {}),
@@ -12,6 +12,10 @@ export interface EndpointValidation {
12
12
  isValid: boolean;
13
13
  errors: string[];
14
14
  }
15
+ /** Hosts `http:` is allowed for: loopback, Docker's host alias, and any
16
+ * `*.localhost` name (RFC 6761 reserves it for local use; the local
17
+ * directory and provider stacks serve `http://<svc>.localhost:<port>`). */
18
+ export declare function isLoopbackHostname(hostname: string): boolean;
15
19
  export declare function validateEndpoint(endpoint: unknown): EndpointValidation;
16
20
  export declare function normalizeEndpoint(endpoint: string): string;
17
21
  export declare function validateHttpPluginTransport(raw: unknown): {
@@ -2,7 +2,7 @@ import { validateEnvVarName } from '../env-name.js';
2
2
  /** Hosts `http:` is allowed for: loopback, Docker's host alias, and any
3
3
  * `*.localhost` name (RFC 6761 reserves it for local use; the local
4
4
  * directory and provider stacks serve `http://<svc>.localhost:<port>`). */
5
- function isLoopbackHostname(hostname) {
5
+ export function isLoopbackHostname(hostname) {
6
6
  return hostname === 'localhost' || hostname.endsWith('.localhost') || hostname === '127.0.0.1' || hostname === '[::1]' || hostname === '::1' || hostname === 'host.docker.internal';
7
7
  }
8
8
  export function validateEndpoint(endpoint) {
@@ -0,0 +1,35 @@
1
+ import type { DeclLeaf, DeclNode } from '../api/command-manifest/schema.js';
2
+ import type { PluginManifest } from '../types.js';
3
+ /**
4
+ * The `object_kinds` and `scopes` blocks of a plugin manifest (`plugin.json`, `bundle.json`) and the one
5
+ * validator every install, update, enable, provider-bundle and reconcile path shares. Canvas-free by
6
+ * design: the CLI and the bundle parser import it, so it must not reach `node:sqlite` or the canvas.
7
+ */
8
+ export declare const KIND: RegExp;
9
+ export declare const LOCAL: RegExp;
10
+ /** A kind's first segment may not be one of the built-in object types. */
11
+ export declare const RESERVED_KIND_ROOTS: readonly string[];
12
+ export declare const MAX_KIND_EVENTS = 64;
13
+ export declare const MIN_EVENT_LEVEL = 1;
14
+ export declare const MAX_EVENT_LEVEL = 10;
15
+ /** Reasons a catalog (`events` map) is invalid; shared with `registerCustomObject`. */
16
+ export declare function eventCatalogReasons(events: unknown, where: string): string[];
17
+ /** The JSON shape of both blocks of a raw manifest record, independent of the plugin's name and commands. */
18
+ export declare function objectBlocksShapeReasons(record: Record<string, unknown>): string[];
19
+ /**
20
+ * The full check. `leafPaths` holds the space-joined path of every leaf below the plugin's root command
21
+ * (`page read` for plugin `search`'s leaf `search page read`); `streamingLeafPaths` holds those declared `rest.streaming: true`.
22
+ */
23
+ export declare function objectBlocksReasons(manifest: Pick<PluginManifest, 'object_kinds' | 'scopes' | 'provider'>, context: {
24
+ name: string;
25
+ leafPaths: ReadonlySet<string>;
26
+ streamingLeafPaths: ReadonlySet<string>;
27
+ }): string[];
28
+ /** The manifest carries an `object_kinds` or `scopes` block, whether or not it is valid or the plugin holds a token. */
29
+ export declare function declaresObjectBlocks(manifest: Pick<PluginManifest, 'object_kinds' | 'scopes'>): boolean;
30
+ /** A user plugin holds a `crtr_app_` token when it is not a provider and declares kinds or scopes, or serves over HTTP. */
31
+ export declare function needsAppToken(manifest: Pick<PluginManifest, 'object_kinds' | 'scopes' | 'provider' | 'transport'>): boolean;
32
+ /** The space-joined path of every leaf below the plugin's root command (`page read` for plugin `search`'s leaf `search page read`). */
33
+ export declare function leafPathsBelowRoot(name: string, roots: ReadonlyArray<DeclNode<DeclLeaf>>): Set<string>;
34
+ /** The leaves of `leafPathsBelowRoot` that declare `rest.streaming: true`. */
35
+ export declare function streamingLeafPathsBelowRoot(name: string, roots: ReadonlyArray<DeclNode<DeclLeaf>>): Set<string>;
@@ -0,0 +1,159 @@
1
+ import { parseScope } from '@north-light/identity';
2
+ /**
3
+ * The `object_kinds` and `scopes` blocks of a plugin manifest (`plugin.json`, `bundle.json`) and the one
4
+ * validator every install, update, enable, provider-bundle and reconcile path shares. Canvas-free by
5
+ * design: the CLI and the bundle parser import it, so it must not reach `node:sqlite` or the canvas.
6
+ */
7
+ export const KIND = /^[a-z][a-z0-9-]*\.[a-z][a-z0-9-]*$/;
8
+ export const LOCAL = /^[a-z][a-z0-9-]*$/;
9
+ /** A kind's first segment may not be one of the built-in object types. */
10
+ export const RESERVED_KIND_ROOTS = ['report', 'notice', 'node', 'job', 'document', 'person', 'app', 'repo', 'profile', 'action'];
11
+ export const MAX_KIND_EVENTS = 64;
12
+ export const MIN_EVENT_LEVEL = 1;
13
+ export const MAX_EVENT_LEVEL = 10;
14
+ const KIND_KEYS = new Set(['description', 'events', 'read']);
15
+ const EVENT_KEYS = new Set(['level', 'supersedes']);
16
+ const SCOPE_VERBS = /^crtr:objects:(?:read|watch|link|write):/;
17
+ function isRecord(value) {
18
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
19
+ }
20
+ /** Reasons a catalog (`events` map) is invalid; shared with `registerCustomObject`. */
21
+ export function eventCatalogReasons(events, where) {
22
+ if (!isRecord(events))
23
+ return [`${where} events must be an object`];
24
+ const reasons = [];
25
+ const entries = Object.entries(events);
26
+ if (entries.length > MAX_KIND_EVENTS)
27
+ reasons.push(`${where} declares more than ${MAX_KIND_EVENTS} events`);
28
+ for (const [local, spec] of entries) {
29
+ const at = `${where} event ${JSON.stringify(local)}`;
30
+ if (!LOCAL.test(local))
31
+ reasons.push(`${at} is not a valid event name`);
32
+ if (!isRecord(spec)) {
33
+ reasons.push(`${at} must be an object { level, supersedes? }`);
34
+ continue;
35
+ }
36
+ for (const key of Object.keys(spec))
37
+ if (!EVENT_KEYS.has(key))
38
+ reasons.push(`${at} has unknown key ${JSON.stringify(key)}`);
39
+ const level = spec['level'];
40
+ if (typeof level !== 'number' || !Number.isInteger(level) || level < MIN_EVENT_LEVEL || level > MAX_EVENT_LEVEL)
41
+ reasons.push(`${at} level must be an integer ${MIN_EVENT_LEVEL}-${MAX_EVENT_LEVEL}`);
42
+ if (spec['supersedes'] !== undefined && typeof spec['supersedes'] !== 'boolean')
43
+ reasons.push(`${at} supersedes must be a boolean`);
44
+ }
45
+ return reasons;
46
+ }
47
+ /** The JSON shape of both blocks of a raw manifest record, independent of the plugin's name and commands. */
48
+ export function objectBlocksShapeReasons(record) {
49
+ const reasons = [];
50
+ const kinds = record['object_kinds'];
51
+ if (kinds !== undefined) {
52
+ if (!isRecord(kinds))
53
+ reasons.push('object_kinds must be an object keyed by kind name');
54
+ else {
55
+ for (const [kind, decl] of Object.entries(kinds)) {
56
+ const where = `object_kinds ${JSON.stringify(kind)}`;
57
+ if (!KIND.test(kind))
58
+ reasons.push(`${where} is not a valid kind name (<plugin>.<name>, lowercase)`);
59
+ else if (RESERVED_KIND_ROOTS.includes(kind.split('.')[0]))
60
+ reasons.push(`${where} uses a reserved first segment`);
61
+ if (!isRecord(decl)) {
62
+ reasons.push(`${where} must be an object { description, events, read? }`);
63
+ continue;
64
+ }
65
+ for (const key of Object.keys(decl))
66
+ if (!KIND_KEYS.has(key))
67
+ reasons.push(`${where} has unknown key ${JSON.stringify(key)}`);
68
+ if (typeof decl['description'] !== 'string' || decl['description'].trim() === '')
69
+ reasons.push(`${where} description must be non-empty text`);
70
+ reasons.push(...eventCatalogReasons(decl['events'], where));
71
+ const read = decl['read'];
72
+ if (read !== undefined) {
73
+ const command = isRecord(read) ? read['command'] : undefined;
74
+ if (!isRecord(read) || Object.keys(read).some((key) => key !== 'command'))
75
+ reasons.push(`${where} read must be { command: [leaf path] }`);
76
+ else if (!Array.isArray(command) || command.length === 0 || command.some((part) => typeof part !== 'string' || part === ''))
77
+ reasons.push(`${where} read.command must be a non-empty array of command names`);
78
+ }
79
+ }
80
+ }
81
+ }
82
+ const scopes = record['scopes'];
83
+ if (scopes !== undefined) {
84
+ if (!Array.isArray(scopes) || scopes.some((scope) => typeof scope !== 'string'))
85
+ reasons.push('scopes must be an array of scope strings');
86
+ }
87
+ return reasons;
88
+ }
89
+ /**
90
+ * The full check. `leafPaths` holds the space-joined path of every leaf below the plugin's root command
91
+ * (`page read` for plugin `search`'s leaf `search page read`); `streamingLeafPaths` holds those declared `rest.streaming: true`.
92
+ */
93
+ export function objectBlocksReasons(manifest, context) {
94
+ const reasons = objectBlocksShapeReasons(manifest);
95
+ if (reasons.length > 0)
96
+ return reasons;
97
+ const { name, leafPaths, streamingLeafPaths } = context;
98
+ for (const [kind, decl] of Object.entries(manifest.object_kinds ?? {})) {
99
+ if (kind.split('.')[0] !== name)
100
+ reasons.push(`object_kinds ${JSON.stringify(kind)} must start with the plugin name "${name}."`);
101
+ const command = decl.read?.command;
102
+ if (command !== undefined && !leafPaths.has(command.join(' ')))
103
+ reasons.push(`object_kinds ${JSON.stringify(kind)} read.command "${command.join(' ')}" is not a leaf of plugin "${name}"`);
104
+ else if (command !== undefined && streamingLeafPaths.has(command.join(' ')))
105
+ reasons.push(`object_kinds ${JSON.stringify(kind)} read.command "${command.join(' ')}" streams; a read is one {markdown} answer`);
106
+ }
107
+ const scopes = manifest.scopes ?? [];
108
+ if (manifest.scopes !== undefined && manifest.provider !== undefined)
109
+ reasons.push('scopes is for local plugins; a provider gets its scopes from directory consent');
110
+ for (const scope of scopes) {
111
+ try {
112
+ parseScope(scope);
113
+ }
114
+ catch {
115
+ reasons.push(`scope ${JSON.stringify(scope)} is not a recognized scope`);
116
+ continue;
117
+ }
118
+ if (!SCOPE_VERBS.test(scope))
119
+ reasons.push(`scope ${JSON.stringify(scope)} must be crtr:objects:<read|watch|link|write>:<target>`);
120
+ else if (scope.endsWith(`:app:${name}`))
121
+ reasons.push(`scope ${JSON.stringify(scope)} names the plugin's own space; an app needs no scope for its own objects`);
122
+ }
123
+ return reasons;
124
+ }
125
+ /** The manifest carries an `object_kinds` or `scopes` block, whether or not it is valid or the plugin holds a token. */
126
+ export function declaresObjectBlocks(manifest) {
127
+ return manifest.object_kinds !== undefined || manifest.scopes !== undefined;
128
+ }
129
+ /** A user plugin holds a `crtr_app_` token when it is not a provider and declares kinds or scopes, or serves over HTTP. */
130
+ export function needsAppToken(manifest) {
131
+ if (manifest.provider !== undefined)
132
+ return false;
133
+ return Object.keys(manifest.object_kinds ?? {}).length > 0 || (manifest.scopes?.length ?? 0) > 0 || manifest.transport?.kind === 'http';
134
+ }
135
+ /** The space-joined path of every leaf below the plugin's root command (`page read` for plugin `search`'s leaf `search page read`). */
136
+ export function leafPathsBelowRoot(name, roots) {
137
+ return collectLeafPaths(name, roots, () => true);
138
+ }
139
+ /** The leaves of `leafPathsBelowRoot` that declare `rest.streaming: true`. */
140
+ export function streamingLeafPathsBelowRoot(name, roots) {
141
+ return collectLeafPaths(name, roots, (leaf) => 'rest' in leaf && leaf.rest.streaming === true);
142
+ }
143
+ function collectLeafPaths(name, roots, include) {
144
+ const leafPaths = new Set();
145
+ const walk = (node, prefix) => {
146
+ if (node.kind === 'leaf') {
147
+ if (include(node))
148
+ leafPaths.add([...prefix, node.name].join(' '));
149
+ }
150
+ else
151
+ for (const child of node.children)
152
+ walk(child, [...prefix, node.name]);
153
+ };
154
+ for (const root of roots)
155
+ if (root.name === name && root.kind === 'branch')
156
+ for (const child of root.children)
157
+ walk(child, []);
158
+ return leafPaths;
159
+ }
package/dist/types.d.ts CHANGED
@@ -60,6 +60,18 @@ export type MemoryExtensionDeclaration = BooleanMemoryExtensionDeclaration | Str
60
60
  /** Field declarations owned by one plugin manifest. The plugin's `name` is
61
61
  * the namespace; keys here are its lowercase kebab-case local field names. */
62
62
  export type MemoryExtensionDeclarations = Record<string, MemoryExtensionDeclaration>;
63
+ /** One canvas object kind a plugin declares in `object_kinds`. */
64
+ export interface ObjectKindDeclaration {
65
+ description: string;
66
+ events: Record<string, {
67
+ level: number;
68
+ supersedes?: boolean;
69
+ }>;
70
+ /** A leaf path of the plugin's own commands, run to render an instance as markdown. */
71
+ read?: {
72
+ command: string[];
73
+ };
74
+ }
63
75
  export interface PluginManifest {
64
76
  name: string;
65
77
  version?: string;
@@ -124,6 +136,10 @@ export interface PluginManifest {
124
136
  /** App-scoped env variables required by this plugin, collected during install. */
125
137
  env?: Record<string, import('./core/plugin-env.js').PluginEnvDeclaration>;
126
138
  transport?: PluginTransport;
139
+ /** Canvas object kinds this plugin declares, keyed by kind name (`<plugin>.<name>`). */
140
+ object_kinds?: Record<string, ObjectKindDeclaration>;
141
+ /** Canvas scopes (`crtr:objects:<verb>:<target>`) a local plugin's app credential holds. */
142
+ scopes?: string[];
127
143
  /** Archive source for a crouter-materialized HTTP plugin. Distinct from the invocation transport. */
128
144
  bundle?: PluginBundle;
129
145
  /** A capability provider's `bundle.json` `provider` block, copied by the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@north-light/api",
3
- "version": "0.3.482",
3
+ "version": "0.3.484",
4
4
  "description": "Typed crtrd /v1 API contract — DTOs, route builders, the error contract, the CrtrClient, and the command-plugin manifest format. Zero runtime dependencies.",
5
5
  "type": "module",
6
6
  "main": "./dist/api/index.js",