@north-light/crouter-api 0.3.205 → 0.3.207

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,4 +1,14 @@
1
1
  import type { InboxTierDTO, IsoTime, NodeIdDTO } from './common.js';
2
+ /** One runtime card supplied by a caller: the runtime renders and escapes it
3
+ * exactly once, so a caller cannot emit a malformed card or smuggle markup
4
+ * into one. `kind` must be namespaced (contain `:`) — bare kinds are
5
+ * crouter's own closed vocabulary. */
6
+ export interface RuntimeCardRequest {
7
+ kind: string;
8
+ facts?: Record<string, string | number>;
9
+ /** Data, not markup. */
10
+ body?: string;
11
+ }
2
12
  /** `POST /v1/nodes/{id}/messages` body. */
3
13
  export interface SendMessageRequest {
4
14
  body: string;
@@ -12,10 +22,19 @@ export interface SendMessageRequest {
12
22
  /** Clear a latched target's finalization latch before an immediate delivery
13
23
  * or `--fresh` revive (`--reopen`). Immediate only. */
14
24
  reopen?: boolean;
15
- /** Hidden ambient context upserted onto the target's sidecar and delivered as
16
- * a `<situational-context>` block, never visible chat (`--situational-context`).
17
- * Immediate only; valid alone (no body). */
25
+ /** Hidden ambient context upserted onto the target's sidecar as a
26
+ * `situational` card, never visible chat (`--situational-context`).
27
+ * Immediate only; valid alone (no body). Prose only — its body is NOT
28
+ * escaped; a caller with untrusted text uses `situational_card`. */
18
29
  situational_context?: string;
30
+ /** A runtime card that REPLACES the target's situational sidecar, delivered
31
+ * ahead of the body in the same turn and re-stated by the session-start
32
+ * bearings after a context refresh. Mutually exclusive with
33
+ * `situational_context`; valid alone (no body) on the durable path. */
34
+ situational_card?: RuntimeCardRequest;
35
+ /** One-shot runtime cards for this turn only — never persisted. Delivered in
36
+ * array order, after `situational_card` and ahead of the body. */
37
+ context_cards?: RuntimeCardRequest[];
19
38
  /** Raw JSON-schema string granting a one-off `submit` tool before delivery
20
39
  * (`--output-schema`). Immediate only. */
21
40
  output_schema?: string;
@@ -24,7 +43,10 @@ export interface SendMessageRequest {
24
43
  * instead of the durable inbox; a dormant or mid-revive target falls back to
25
44
  * the durable inbox + revive (watcher delivers post-boot). Plain immediate
26
45
  * body only — rejected with fresh/reopen/situational_context/
27
- * output_schema or tier 'deferred'. Absent durable inbox (unchanged). */
46
+ * output_schema or tier 'deferred'. Runtime cards ARE accepted: a
47
+ * card-bearing send is an ordinary human send that happens to carry context,
48
+ * and the live deliver frame places the cards ahead of the body in one turn.
49
+ * Absent → durable inbox (unchanged). */
28
50
  delivery?: 'interactive';
29
51
  }
30
52
  /** Result of an immediate message send. */
@@ -71,10 +71,21 @@ export interface CardKindDefinition {
71
71
  }
72
72
  /** The closed crouter vocabulary, plus custom-role aliases for the same cards. */
73
73
  export declare const KIND_TABLE: Readonly<Record<string, CardKindDefinition>>;
74
+ /** Contribute presentation for namespaced kinds this process will render.
75
+ * Idempotent and last-write-wins, so a re-imported module cannot fail a boot.
76
+ * A process that never registers is not degraded: an unregistered kind still
77
+ * parses and only falls back to the generic label and summary. */
78
+ export declare function registerCardKinds(kinds: Readonly<Record<string, CardKindDefinition>>): void;
74
79
  /** Plain text from a generated message's string or text-block content. */
75
80
  export declare function generatedContextText(message: GeneratedContextMessageLike): string;
76
81
  /** Wrap a user-role runtime message in its whole-message card envelope. */
77
82
  export declare function formatCard(kind: string, facts: Record<string, string | number | boolean | undefined>, body: string): string;
83
+ /** Wrap a card whose body is DATA, not markup — the only escape on the API
84
+ * path, so a caller can neither emit a malformed card nor smuggle markup into
85
+ * one. `formatCard` keeps its raw body for the trusted crouter producers that
86
+ * deliberately nest markup (bearings blocks, the inbox `<from>`/`<entry>`
87
+ * grammar `parseInboxBody` depends on). */
88
+ export declare function formatDataCard(kind: string, facts: Record<string, string | number | boolean | undefined>, body: string): string;
78
89
  /** Parse a runtime envelope, with pre-envelope customType compatibility. */
79
90
  export declare function parseCard(message: GeneratedContextMessageLike): GeneratedCard | null;
80
91
  /** Format a complete inbox card from report bodies resolved by the caller. */
@@ -114,6 +114,21 @@ const GENERIC_CARD = {
114
114
  summary: () => 'runtime context',
115
115
  expandable: true,
116
116
  };
117
+ /** Kinds contributed by a product or plugin, in whatever process renders them. */
118
+ const KIND_REGISTRY = {};
119
+ /** Contribute presentation for namespaced kinds this process will render.
120
+ * Idempotent and last-write-wins, so a re-imported module cannot fail a boot.
121
+ * A process that never registers is not degraded: an unregistered kind still
122
+ * parses and only falls back to the generic label and summary. */
123
+ export function registerCardKinds(kinds) {
124
+ for (const [kind, definition] of Object.entries(kinds)) {
125
+ // Bare kinds are crouter core's closed vocabulary. A product claiming one
126
+ // is a programming error at boot, not a runtime condition to tolerate.
127
+ if (!kind.includes(':'))
128
+ throw new Error(`Runtime card kind must be namespaced (contain ':'): ${kind}`);
129
+ KIND_REGISTRY[kind] = definition;
130
+ }
131
+ }
117
132
  const XML_ATTRIBUTE_NAME = /^[A-Za-z_:][A-Za-z0-9_:.-]*$/;
118
133
  const ATTRIBUTE_RE = /\s+([A-Za-z_:][A-Za-z0-9_:.-]*)\s*=\s*(?:"([^"]*)"|'([^']*)')/gy;
119
134
  function escapeXmlAttribute(value) {
@@ -187,6 +202,14 @@ export function formatCard(kind, facts, body) {
187
202
  throw new Error('Runtime card facts cannot replace kind');
188
203
  return `<runtime${formatAttributes({ kind, ...facts })}>${body}</runtime>`;
189
204
  }
205
+ /** Wrap a card whose body is DATA, not markup — the only escape on the API
206
+ * path, so a caller can neither emit a malformed card nor smuggle markup into
207
+ * one. `formatCard` keeps its raw body for the trusted crouter producers that
208
+ * deliberately nest markup (bearings blocks, the inbox `<from>`/`<entry>`
209
+ * grammar `parseInboxBody` depends on). */
210
+ export function formatDataCard(kind, facts, body) {
211
+ return formatCard(kind, facts, escapeXmlText(body));
212
+ }
190
213
  function parseInboxBody(body) {
191
214
  const senders = [];
192
215
  const fromRe = /<from\b([^>]*)>([\s\S]*?)<\/from>/g;
@@ -225,7 +248,7 @@ function parseInboxBody(body) {
225
248
  return senders;
226
249
  }
227
250
  function cardFor(kind, facts, body, senders) {
228
- const definition = KIND_TABLE[kind] ?? GENERIC_CARD;
251
+ const definition = KIND_TABLE[kind] ?? KIND_REGISTRY[kind] ?? GENERIC_CARD;
229
252
  return {
230
253
  kind,
231
254
  facts,
@@ -244,9 +267,13 @@ function parseEnvelope(text) {
244
267
  const attributes = parseAttributes(opening[1] ?? '');
245
268
  if (attributes?.kind === undefined)
246
269
  return null;
247
- const body = text.slice(opening[0].length, -'</runtime>'.length);
270
+ const raw = text.slice(opening[0].length, -'</runtime>'.length);
248
271
  const { kind, ...factValues } = attributes;
249
272
  const facts = Object.freeze(factValues);
273
+ // A namespaced kind can only have been produced through the API path, whose
274
+ // renderer is `formatDataCard`, so its body is escaped by construction and
275
+ // decodes here. A bare kind's body is raw markup written by a core producer.
276
+ const body = kind.includes(':') ? unescapeXmlAttribute(raw) : raw;
250
277
  return cardFor(kind, facts, body, kind === 'inbox' ? parseInboxBody(body) : []);
251
278
  }
252
279
  /** Parse a runtime envelope, with pre-envelope customType compatibility. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@north-light/crouter-api",
3
- "version": "0.3.205",
3
+ "version": "0.3.207",
4
4
  "description": "Typed crtrd /v1 API contract — DTOs, route builders, the error contract, and the CrtrClient. Zero runtime dependencies.",
5
5
  "type": "module",
6
6
  "main": "./dist/api/index.js",
@@ -11,6 +11,12 @@
11
11
  "import": "./dist/api/index.js",
12
12
  "require": "./dist/api/index.js",
13
13
  "default": "./dist/api/index.js"
14
+ },
15
+ "./cards": {
16
+ "types": "./dist/shared/generated-context.d.ts",
17
+ "import": "./dist/shared/generated-context.js",
18
+ "require": "./dist/shared/generated-context.js",
19
+ "default": "./dist/shared/generated-context.js"
14
20
  }
15
21
  },
16
22
  "files": [