@north-light/api 0.3.491 → 0.3.492

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.
@@ -1,3 +1,4 @@
1
1
  export * from './schema.js';
2
2
  export * from './manifest.js';
3
3
  export * from './result.js';
4
+ export * from './object-kinds.js';
@@ -1,3 +1,4 @@
1
1
  export * from './schema.js';
2
2
  export * from './manifest.js';
3
3
  export * from './result.js';
4
+ export * from './object-kinds.js';
@@ -0,0 +1,20 @@
1
+ export declare const KIND: RegExp;
2
+ export declare const LOCAL: RegExp;
3
+ /** A kind's first segment may not be one of the built-in object types. */
4
+ export declare const RESERVED_KIND_ROOTS: readonly string[];
5
+ export declare const MAX_KIND_EVENTS = 64;
6
+ export declare const MIN_EVENT_LEVEL = 1;
7
+ export declare const MAX_EVENT_LEVEL = 10;
8
+ /** Reasons a catalog (`events` map) is invalid; shared with `registerCustomObject`. */
9
+ export declare function eventCatalogReasons(events: unknown, where: string): string[];
10
+ /** The JSON shape of both blocks of a raw manifest record, independent of the plugin's name and commands. */
11
+ export declare function objectBlocksShapeReasons(record: Record<string, unknown>): string[];
12
+ /**
13
+ * Reasons a kind's `read.command` does not name a non-streaming leaf of plugin `name`. `leafPaths` holds the space-joined path of
14
+ * every leaf below the plugin's root command; `streamingLeafPaths` holds those declared streaming.
15
+ */
16
+ export declare function objectKindReadReasons(kind: string, decl: {
17
+ read?: {
18
+ command: readonly string[];
19
+ };
20
+ }, name: string, leafPaths: ReadonlySet<string>, streamingLeafPaths: ReadonlySet<string>): string[];
@@ -0,0 +1,98 @@
1
+ // The shape rules for a plugin's `object_kinds` block, shared by crouter's install paths and the plugin kit
2
+ // (`definePlugin({objectKinds})`) so both refuse exactly the same declarations. Zero dependencies.
3
+ export const KIND = /^[a-z][a-z0-9-]*\.[a-z][a-z0-9-]*$/;
4
+ export const LOCAL = /^[a-z][a-z0-9-]*$/;
5
+ /** A kind's first segment may not be one of the built-in object types. */
6
+ export const RESERVED_KIND_ROOTS = ['report', 'notice', 'node', 'job', 'document', 'person', 'app', 'repo', 'profile', 'action'];
7
+ export const MAX_KIND_EVENTS = 64;
8
+ export const MIN_EVENT_LEVEL = 1;
9
+ export const MAX_EVENT_LEVEL = 10;
10
+ const KIND_KEYS = new Set(['description', 'events', 'read']);
11
+ const EVENT_KEYS = new Set(['level', 'supersedes']);
12
+ function isRecord(value) {
13
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
14
+ }
15
+ /** Reasons a catalog (`events` map) is invalid; shared with `registerCustomObject`. */
16
+ export function eventCatalogReasons(events, where) {
17
+ if (!isRecord(events))
18
+ return [`${where} events must be an object`];
19
+ const reasons = [];
20
+ const entries = Object.entries(events);
21
+ if (entries.length > MAX_KIND_EVENTS)
22
+ reasons.push(`${where} declares more than ${MAX_KIND_EVENTS} events`);
23
+ for (const [local, spec] of entries) {
24
+ const at = `${where} event ${JSON.stringify(local)}`;
25
+ if (!LOCAL.test(local))
26
+ reasons.push(`${at} is not a valid event name`);
27
+ if (!isRecord(spec)) {
28
+ reasons.push(`${at} must be an object { level, supersedes? }`);
29
+ continue;
30
+ }
31
+ for (const key of Object.keys(spec))
32
+ if (!EVENT_KEYS.has(key))
33
+ reasons.push(`${at} has unknown key ${JSON.stringify(key)}`);
34
+ const level = spec['level'];
35
+ if (typeof level !== 'number' || !Number.isInteger(level) || level < MIN_EVENT_LEVEL || level > MAX_EVENT_LEVEL)
36
+ reasons.push(`${at} level must be an integer ${MIN_EVENT_LEVEL}-${MAX_EVENT_LEVEL}`);
37
+ if (spec['supersedes'] !== undefined && typeof spec['supersedes'] !== 'boolean')
38
+ reasons.push(`${at} supersedes must be a boolean`);
39
+ }
40
+ return reasons;
41
+ }
42
+ /** The JSON shape of both blocks of a raw manifest record, independent of the plugin's name and commands. */
43
+ export function objectBlocksShapeReasons(record) {
44
+ const reasons = [];
45
+ const kinds = record['object_kinds'];
46
+ if (kinds !== undefined) {
47
+ if (!isRecord(kinds))
48
+ reasons.push('object_kinds must be an object keyed by kind name');
49
+ else {
50
+ for (const [kind, decl] of Object.entries(kinds)) {
51
+ const where = `object_kinds ${JSON.stringify(kind)}`;
52
+ if (!KIND.test(kind))
53
+ reasons.push(`${where} is not a valid kind name (<plugin>.<name>, lowercase)`);
54
+ else if (RESERVED_KIND_ROOTS.includes(kind.split('.')[0]))
55
+ reasons.push(`${where} uses a reserved first segment`);
56
+ if (!isRecord(decl)) {
57
+ reasons.push(`${where} must be an object { description, events, read? }`);
58
+ continue;
59
+ }
60
+ for (const key of Object.keys(decl))
61
+ if (!KIND_KEYS.has(key))
62
+ reasons.push(`${where} has unknown key ${JSON.stringify(key)}`);
63
+ if (typeof decl['description'] !== 'string' || decl['description'].trim() === '')
64
+ reasons.push(`${where} description must be non-empty text`);
65
+ reasons.push(...eventCatalogReasons(decl['events'], where));
66
+ const read = decl['read'];
67
+ if (read !== undefined) {
68
+ const command = isRecord(read) ? read['command'] : undefined;
69
+ if (!isRecord(read) || Object.keys(read).some((key) => key !== 'command'))
70
+ reasons.push(`${where} read must be { command: [leaf path] }`);
71
+ else if (!Array.isArray(command) || command.length === 0 || command.some((part) => typeof part !== 'string' || part === ''))
72
+ reasons.push(`${where} read.command must be a non-empty array of command names`);
73
+ }
74
+ }
75
+ }
76
+ }
77
+ const scopes = record['scopes'];
78
+ if (scopes !== undefined) {
79
+ if (!Array.isArray(scopes) || scopes.some((scope) => typeof scope !== 'string'))
80
+ reasons.push('scopes must be an array of scope strings');
81
+ }
82
+ return reasons;
83
+ }
84
+ /**
85
+ * Reasons a kind's `read.command` does not name a non-streaming leaf of plugin `name`. `leafPaths` holds the space-joined path of
86
+ * every leaf below the plugin's root command; `streamingLeafPaths` holds those declared streaming.
87
+ */
88
+ export function objectKindReadReasons(kind, decl, name, leafPaths, streamingLeafPaths) {
89
+ const command = decl.read?.command;
90
+ if (command === undefined)
91
+ return [];
92
+ const path = command.join(' ');
93
+ if (!leafPaths.has(path))
94
+ return [`object_kinds ${JSON.stringify(kind)} read.command "${path}" is not a leaf of plugin "${name}"`];
95
+ if (streamingLeafPaths.has(path))
96
+ return [`object_kinds ${JSON.stringify(kind)} read.command "${path}" streams; a read is one {markdown} answer`];
97
+ return [];
98
+ }
@@ -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.12";
2
+ export declare const CONTRACT_LEVEL = "1.13";
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.12';
15
+ export const CONTRACT_LEVEL = '1.13';
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,21 +1,7 @@
1
1
  import type { DeclLeaf, DeclNode } from '../api/command-manifest/schema.js';
2
+ import { KIND, LOCAL, RESERVED_KIND_ROOTS, MAX_KIND_EVENTS, MIN_EVENT_LEVEL, MAX_EVENT_LEVEL, eventCatalogReasons, objectBlocksShapeReasons, objectKindReadReasons } from '../api/command-manifest/object-kinds.js';
2
3
  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[];
4
+ export { KIND, LOCAL, RESERVED_KIND_ROOTS, MAX_KIND_EVENTS, MIN_EVENT_LEVEL, MAX_EVENT_LEVEL, eventCatalogReasons, objectBlocksShapeReasons, objectKindReadReasons };
19
5
  /**
20
6
  * The full check. `leafPaths` holds the space-joined path of every leaf below the plugin's root command
21
7
  * (`page read` for plugin `search`'s leaf `search page read`); `streamingLeafPaths` holds those declared `rest.streaming: true`.
@@ -1,91 +1,12 @@
1
1
  import { parseScope } from '@north-light/identity';
2
+ import { KIND, LOCAL, RESERVED_KIND_ROOTS, MAX_KIND_EVENTS, MIN_EVENT_LEVEL, MAX_EVENT_LEVEL, eventCatalogReasons, objectBlocksShapeReasons, objectKindReadReasons, } from '../api/command-manifest/object-kinds.js';
3
+ export { KIND, LOCAL, RESERVED_KIND_ROOTS, MAX_KIND_EVENTS, MIN_EVENT_LEVEL, MAX_EVENT_LEVEL, eventCatalogReasons, objectBlocksShapeReasons, objectKindReadReasons };
2
4
  /**
3
5
  * The `object_kinds` and `scopes` blocks of a plugin manifest (`plugin.json`, `bundle.json`) and the one
4
6
  * validator every install, update, enable, provider-bundle and reconcile path shares. Canvas-free by
5
7
  * design: the CLI and the bundle parser import it, so it must not reach `node:sqlite` or the canvas.
6
8
  */
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
9
  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
10
  /**
90
11
  * The full check. `leafPaths` holds the space-joined path of every leaf below the plugin's root command
91
12
  * (`page read` for plugin `search`'s leaf `search page read`); `streamingLeafPaths` holds those declared `rest.streaming: true`.
@@ -98,11 +19,7 @@ export function objectBlocksReasons(manifest, context) {
98
19
  for (const [kind, decl] of Object.entries(manifest.object_kinds ?? {})) {
99
20
  if (kind.split('.')[0] !== name)
100
21
  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`);
22
+ reasons.push(...objectKindReadReasons(kind, decl, name, leafPaths, streamingLeafPaths));
106
23
  }
107
24
  const scopes = manifest.scopes ?? [];
108
25
  if (manifest.scopes !== undefined && manifest.provider !== undefined)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@north-light/api",
3
- "version": "0.3.491",
3
+ "version": "0.3.492",
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",