@north-light/api 0.3.483 → 0.3.484
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/client.d.ts +6 -0
- package/dist/api/client.js +5 -0
- package/dist/api/contract.d.ts +1 -1
- package/dist/api/contract.js +1 -1
- package/dist/api/dto/custom-objects.d.ts +5 -1
- package/dist/api/dto/docs.d.ts +26 -0
- package/dist/api/dto/objects.d.ts +21 -1
- package/dist/api/dto/plugins.d.ts +22 -0
- package/dist/api/dto/plugins.js +5 -0
- package/dist/api/index.d.ts +1 -0
- package/dist/api/index.js +1 -0
- package/dist/api/routes.d.ts +2 -0
- package/dist/api/routes.js +2 -0
- package/dist/core/command-plugins/bundle.d.ts +4 -1
- package/dist/core/command-plugins/bundle.js +19 -2
- package/dist/core/command-plugins/endpoint.d.ts +4 -0
- package/dist/core/command-plugins/endpoint.js +1 -1
- package/dist/core/plugin-objects-manifest.d.ts +35 -0
- package/dist/core/plugin-objects-manifest.js +159 -0
- package/dist/types.d.ts +16 -0
- package/package.json +1 -1
package/dist/api/client.d.ts
CHANGED
|
@@ -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. */
|
package/dist/api/client.js
CHANGED
|
@@ -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);
|
package/dist/api/contract.d.ts
CHANGED
|
@@ -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.
|
|
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`. */
|
package/dist/api/contract.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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;
|
package/dist/api/dto/docs.d.ts
CHANGED
|
@@ -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
|
}
|
|
@@ -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
|
+
}
|
package/dist/api/index.d.ts
CHANGED
|
@@ -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
package/dist/api/routes.d.ts
CHANGED
|
@@ -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
|
};
|
package/dist/api/routes.js
CHANGED
|
@@ -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
|
|
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.
|
|
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",
|