@continuonai/rcan-ts 0.1.2 → 0.3.0

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/README.md CHANGED
@@ -9,6 +9,8 @@ Official TypeScript SDK for the **RCAN v1.2** Robot Communication and Accountabi
9
9
  npm install @continuonai/rcan-ts
10
10
  ```
11
11
 
12
+ > **v0.2.0** — RCAN v1.2 spec compliance including §17 Distributed Registry Node Protocol
13
+
12
14
  ### CDN / Browser (no build step)
13
15
 
14
16
  ```html
@@ -139,6 +141,89 @@ result.warnings.forEach((w) => console.warn("⚠️", w));
139
141
  | `validateURI` | Validate a Robot URI string |
140
142
  | `validateMessage` | Validate a RCAN message object |
141
143
  | `validateConfig` | L1/L2/L3 conformance check for a robot RCAN config |
144
+ | `NodeClient` | Resolve RRNs from federated registry nodes (§17) |
145
+ | `fetchCanonicalSchema` | Fetch the canonical JSON schema from rcan.dev |
146
+ | `validateConfigAgainstSchema` | Validate a config object against the live JSON schema |
147
+ | `validateNodeAgainstSchema` | Validate a node manifest against the node schema |
148
+
149
+ ---
150
+
151
+ ## Distributed Registry Nodes (§17)
152
+
153
+ RCAN v1.2 §17 introduces a federated registry network. `NodeClient` resolves RRNs from any node — root or delegated authoritative.
154
+
155
+ ```typescript
156
+ import { NodeClient } from '@continuonai/rcan-ts';
157
+
158
+ const client = new NodeClient();
159
+
160
+ // Resolve an RRN across the federation
161
+ const result = await client.resolve('RRN-BD-000000000001');
162
+ console.log(`Resolved by: ${result.resolved_by}`);
163
+ console.log(`Robot: ${result.record.name}`);
164
+
165
+ // Discover the authoritative node for a namespace
166
+ const node = await client.discover('RRN-BD-000000000001');
167
+ console.log(`Authoritative: ${node.operator} at ${node.api_base}`);
168
+
169
+ // List all known registry nodes
170
+ const nodes = await client.listNodes();
171
+ nodes.forEach(n => console.log(`${n.operator}: ${n.namespace_prefix}`));
172
+
173
+ // Verify a node manifest
174
+ const manifest = await client.getNodeManifest('https://registry.example.com');
175
+ const isValid = client.verifyNode(manifest);
176
+
177
+ // Error handling
178
+ import { RCANNodeNotFoundError, RCANNodeTrustError } from '@continuonai/rcan-ts';
179
+ try {
180
+ const result = await client.resolve('RRN-UNKNOWN-000000000001');
181
+ } catch (e) {
182
+ if (e instanceof RCANNodeNotFoundError) {
183
+ console.log(`Not found: ${e.rrn}`);
184
+ } else if (e instanceof RCANNodeTrustError) {
185
+ console.log(`Trust failure: ${e.reason}`);
186
+ }
187
+ }
188
+ ```
189
+
190
+ ### RRN Format
191
+
192
+ ```
193
+ Root namespace: RRN-000000000001 (12-digit recommended, 8-digit still valid)
194
+ Delegated: RRN-BD-000000000001 (prefix 2-8 alphanumeric chars)
195
+ Legacy (valid): RRN-00000001 (8-digit, backward compatible)
196
+ ```
197
+
198
+ ## Schema Validation
199
+
200
+ Validate configs against the canonical JSON schema published at rcan.dev:
201
+
202
+ ```typescript
203
+ import { validateConfigAgainstSchema, validateNodeAgainstSchema } from '@continuonai/rcan-ts';
204
+
205
+ // Validate a RCAN config against the canonical schema from rcan.dev
206
+ const result = await validateConfigAgainstSchema(myConfig);
207
+ if (!result.valid) {
208
+ console.error('Config invalid:', result.errors);
209
+ } else if (result.skipped) {
210
+ console.warn('Schema validation skipped (rcan.dev unreachable)');
211
+ }
212
+
213
+ // Validate a node manifest
214
+ const nodeResult = await validateNodeAgainstSchema(manifest);
215
+ ```
216
+
217
+ ### CDN / Browser Usage
218
+
219
+ ```html
220
+ <script src="https://unpkg.com/@continuonai/rcan-ts/dist/rcan.iife.js"></script>
221
+ <script>
222
+ const { validateConfig, NodeClient } = window.RCAN;
223
+ const client = new NodeClient();
224
+ client.resolve('RRN-000000000001').then(r => console.log(r));
225
+ </script>
226
+ ```
142
227
 
143
228
  ---
144
229
 
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env node
2
+ import { NodeClient, validateConfig, VERSION, SPEC_VERSION } from '../dist/index.js';
3
+ import { readFileSync } from 'fs';
4
+ import { parseArgs } from 'util';
5
+
6
+ const { values, positionals } = parseArgs({
7
+ args: process.argv.slice(2),
8
+ options: {
9
+ version: { type: 'boolean', short: 'v' },
10
+ file: { type: 'string', short: 'f' },
11
+ },
12
+ allowPositionals: true,
13
+ });
14
+
15
+ if (values.version) {
16
+ console.log(`rcan-validate (ts) ${VERSION} (RCAN spec ${SPEC_VERSION})`);
17
+ process.exit(0);
18
+ }
19
+
20
+ const [subcommand, target] = positionals;
21
+
22
+ if (subcommand === 'node') {
23
+ if (!target) {
24
+ console.error('Usage: rcan-validate node <url>');
25
+ process.exit(1);
26
+ }
27
+
28
+ const client = new NodeClient();
29
+ console.log(`Validating node manifest: ${target}`);
30
+
31
+ try {
32
+ const manifest = await client.getNodeManifest(target);
33
+ const valid = client.verifyNode(manifest);
34
+
35
+ const checks = [
36
+ ['node_type', manifest.node_type, ['root','authoritative','resolver','cache'].includes(manifest.node_type)],
37
+ ['operator', manifest.operator, !!manifest.operator],
38
+ ['namespace_prefix', manifest.namespace_prefix, !!manifest.namespace_prefix],
39
+ ['public_key', manifest.public_key?.substring(0, 20) + '...', manifest.public_key?.startsWith('ed25519:')],
40
+ ['api_base', manifest.api_base, manifest.api_base?.startsWith('https://')],
41
+ ];
42
+
43
+ for (const [name, value, pass] of checks) {
44
+ console.log(` ${pass ? '✓' : '✗'} ${name}: ${value}`);
45
+ }
46
+
47
+ const passed = checks.filter(([,,p]) => p).length;
48
+ console.log(`\n${valid ? 'PASS' : 'FAIL'} (${passed}/${checks.length} checks)`);
49
+ process.exit(valid ? 0 : 1);
50
+ } catch (e) {
51
+ console.error(`❌ Error: ${e.message}`);
52
+ process.exit(1);
53
+ }
54
+ }
55
+
56
+ if (subcommand === 'config') {
57
+ const filePath = target || values.file;
58
+ if (!filePath) {
59
+ console.error('Usage: rcan-validate config <file>');
60
+ process.exit(1);
61
+ }
62
+
63
+ try {
64
+ const raw = readFileSync(filePath, 'utf-8');
65
+ const config = JSON.parse(raw);
66
+ const result = validateConfig(config);
67
+ if (result.ok) {
68
+ console.log('✓ Config valid');
69
+ if (result.warnings?.length) {
70
+ result.warnings.forEach(w => console.warn(` ⚠️ ${w}`));
71
+ }
72
+ process.exit(0);
73
+ } else {
74
+ console.error('✗ Config invalid:');
75
+ result.issues?.forEach(i => console.error(` ❌ ${i}`));
76
+ process.exit(1);
77
+ }
78
+ } catch (e) {
79
+ console.error(`❌ Error: ${e.message}`);
80
+ process.exit(1);
81
+ }
82
+ }
83
+
84
+ console.error(`Unknown subcommand: ${subcommand}`);
85
+ console.error('Usage: rcan-validate node <url> | rcan-validate config <file> | rcan-validate --version');
86
+ process.exit(1);
@@ -201,48 +201,101 @@ declare class AuditChain {
201
201
  }
202
202
 
203
203
  /**
204
- * RCAN Validationvalidate messages, configs, and URIs.
204
+ * rcan-ts typesTypeScript interfaces for RCAN config and message shapes.
205
205
  *
206
- * Each function returns a ValidationResult with ok, issues, warnings, info.
206
+ * These provide parity with the rcan-py TypedDicts and enable
207
+ * static type checking with tsc/ts-jest/pyright.
207
208
  */
208
- interface ValidationResult {
209
- ok: boolean;
210
- issues: string[];
211
- warnings: string[];
212
- info: string[];
209
+ interface RCANMetadata {
210
+ manufacturer?: string;
211
+ model?: string;
212
+ version?: string;
213
+ device_id?: string;
214
+ robot_name?: string;
215
+ rrn?: string;
216
+ rcan_uri?: string;
213
217
  }
214
- declare function validateURI(uri: string): ValidationResult;
215
- declare function validateMessage(data: unknown): ValidationResult;
216
- interface RCANConfig {
217
- rcan_version?: string;
218
- metadata?: {
219
- manufacturer?: string;
220
- model?: string;
221
- version?: string;
222
- rrn?: string;
223
- rcan_uri?: string;
218
+ interface RCANAgentConfig {
219
+ provider?: string;
220
+ model?: string;
221
+ temperature?: number;
222
+ confidence_gates?: Array<{
223
+ threshold?: number;
224
+ [key: string]: unknown;
225
+ }>;
226
+ hitl_gates?: Array<Record<string, unknown>>;
227
+ commitment_chain?: {
228
+ enabled?: boolean;
229
+ [key: string]: unknown;
224
230
  };
225
- agent?: {
226
- provider?: string;
227
- model?: string;
228
- confidence_gates?: Array<{
229
- threshold?: number;
230
- }>;
231
- hitl_gates?: Array<Record<string, unknown>>;
232
- commitment_chain?: {
233
- enabled?: boolean;
234
- };
235
- signing?: {
236
- enabled?: boolean;
237
- };
231
+ signing?: {
232
+ enabled?: boolean;
233
+ [key: string]: unknown;
238
234
  };
235
+ }
236
+ interface RCANConfig {
237
+ rcan_version?: string;
238
+ metadata?: RCANMetadata;
239
+ agent?: RCANAgentConfig;
240
+ channels?: Record<string, unknown>;
239
241
  rcan_protocol?: {
240
242
  jwt_auth?: {
241
243
  enabled?: boolean;
242
244
  };
245
+ [key: string]: unknown;
243
246
  };
244
247
  [key: string]: unknown;
245
248
  }
249
+ interface RCANMessageEnvelope {
250
+ cmd: string;
251
+ target: string;
252
+ rcan_version?: string;
253
+ confidence?: number;
254
+ timestamp_ns?: number;
255
+ params?: Record<string, unknown>;
256
+ signature?: string;
257
+ [key: string]: unknown;
258
+ }
259
+ interface RCANRegistryNode {
260
+ rcan_node_version: string;
261
+ node_type: "root" | "authoritative" | "resolver" | "cache";
262
+ operator: string;
263
+ namespace_prefix: string;
264
+ public_key: string;
265
+ api_base: string;
266
+ registry_ui?: string;
267
+ spec_version?: string;
268
+ capabilities?: string[];
269
+ sync_endpoint?: string;
270
+ last_sync?: string;
271
+ ttl_seconds?: number;
272
+ contact?: string;
273
+ governance?: string;
274
+ federation_protocol?: string;
275
+ }
276
+ interface RCANResolveResult {
277
+ rrn: string;
278
+ resolved_by: string;
279
+ namespace: string;
280
+ record: Record<string, unknown>;
281
+ cache_status: "HIT" | "MISS" | "STALE";
282
+ resolved_at: string;
283
+ }
284
+
285
+ /**
286
+ * RCAN Validation — validate messages, configs, and URIs.
287
+ *
288
+ * Each function returns a ValidationResult with ok, issues, warnings, info.
289
+ */
290
+
291
+ interface ValidationResult {
292
+ ok: boolean;
293
+ issues: string[];
294
+ warnings: string[];
295
+ info: string[];
296
+ }
297
+ declare function validateURI(uri: string): ValidationResult;
298
+ declare function validateMessage(data: unknown): ValidationResult;
246
299
  declare function validateConfig(config: RCANConfig): ValidationResult;
247
300
 
248
301
  /**
@@ -270,6 +323,22 @@ declare class RCANSignatureError extends RCANError {
270
323
  declare class RCANRegistryError extends RCANError {
271
324
  constructor(message: string);
272
325
  }
326
+ declare class RCANNodeError extends RCANError {
327
+ readonly nodeUrl?: string | undefined;
328
+ constructor(message: string, nodeUrl?: string | undefined);
329
+ }
330
+ declare class RCANNodeNotFoundError extends RCANNodeError {
331
+ readonly rrn: string;
332
+ constructor(rrn: string, nodeUrl?: string);
333
+ }
334
+ declare class RCANNodeSyncError extends RCANNodeError {
335
+ readonly cause?: Error | undefined;
336
+ constructor(message: string, nodeUrl?: string, cause?: Error | undefined);
337
+ }
338
+ declare class RCANNodeTrustError extends RCANNodeError {
339
+ readonly reason: "invalid_signature" | "expired_cert" | "unknown_issuer" | "missing_pubkey";
340
+ constructor(reason: RCANNodeTrustError["reason"], nodeUrl?: string);
341
+ }
273
342
 
274
343
  /**
275
344
  * rcan-ts — RegistryClient
@@ -339,6 +408,68 @@ declare class RegistryClient {
339
408
  }): Promise<Robot[]>;
340
409
  }
341
410
 
411
+ /**
412
+ * rcan-ts — NodeClient
413
+ * Federation-aware node discovery and RRN resolution.
414
+ *
415
+ * Zero runtime dependencies — uses globalThis.fetch (Node 18+, browsers, CF Workers).
416
+ *
417
+ * @see https://rcan.dev/spec#section-federation
418
+ */
419
+
420
+ declare class NodeClient {
421
+ private readonly rootUrl;
422
+ private readonly timeoutMs;
423
+ constructor(rootUrl?: string, timeoutMs?: number);
424
+ private _fetch;
425
+ /**
426
+ * Fetch /.well-known/rcan-node.json from any node URL.
427
+ * Throws RCANNodeTrustError if the manifest is malformed.
428
+ * Throws RCANNodeSyncError on network failure.
429
+ */
430
+ getNodeManifest(nodeUrl: string): Promise<RCANRegistryNode>;
431
+ /**
432
+ * Get list of known registry nodes from root /api/v1/nodes.
433
+ * Optionally filter by namespace prefix (e.g. "BD").
434
+ */
435
+ listNodes(prefix?: string): Promise<RCANRegistryNode[]>;
436
+ /**
437
+ * Find the authoritative node for an RRN.
438
+ * - Delegated RRN (RRN-BD-00000001): GET /api/v1/nodes?prefix=BD, return first match.
439
+ * - Root RRN (RRN-00000042): return root node manifest.
440
+ * - Unknown format: throws RCANNodeNotFoundError.
441
+ */
442
+ discover(rrn: string): Promise<RCANRegistryNode>;
443
+ /**
444
+ * Resolve an RRN across the federation.
445
+ * First tries {rootUrl}/api/v1/resolve/{rrn}.
446
+ * On 404, discovers the authoritative node and tries {node.api_base}/robots/{rrn}.
447
+ */
448
+ resolve(rrn: string): Promise<RCANResolveResult>;
449
+ /**
450
+ * Verify a node manifest is well-formed.
451
+ * Checks required fields, ed25519: public_key prefix, valid node_type, https:// api_base.
452
+ */
453
+ verifyNode(manifest: unknown): manifest is RCANRegistryNode;
454
+ }
455
+
456
+ declare function fetchCanonicalSchema(schemaName: string): Promise<object | null>;
457
+ /**
458
+ * Validate a config object against the canonical rcan-config schema from rcan.dev.
459
+ * Returns { valid: true } or { valid: false, errors: string[] }.
460
+ * Gracefully returns { valid: true, skipped: true } if schema unreachable.
461
+ */
462
+ declare function validateConfigAgainstSchema(config: unknown): Promise<{
463
+ valid: boolean;
464
+ errors?: string[];
465
+ skipped?: boolean;
466
+ }>;
467
+ declare function validateNodeAgainstSchema(manifest: unknown): Promise<{
468
+ valid: boolean;
469
+ errors?: string[];
470
+ skipped?: boolean;
471
+ }>;
472
+
342
473
  /**
343
474
  * rcan-ts — Official TypeScript SDK for RCAN v1.2
344
475
  * Robot Communication and Accountability Network
@@ -347,7 +478,9 @@ declare class RegistryClient {
347
478
  * @see https://github.com/continuonai/rcan-ts
348
479
  */
349
480
 
350
- declare const VERSION = "0.1.0";
481
+ declare const VERSION = "0.3.0";
482
+ declare const SPEC_VERSION = "1.2";
483
+ /** @deprecated Use SPEC_VERSION instead */
351
484
  declare const RCAN_VERSION = "1.2";
352
485
 
353
- export { type ApprovalStatus, AuditChain, AuditError, type ChainVerifyResult, CommitmentRecord, type CommitmentRecordData, type CommitmentRecordJSON, ConfidenceGate, GateError, HiTLGate, type ListResult, type PendingApproval, RCANAddressError, type RCANConfig, RCANError, RCANGateError, RCANMessage, type RCANMessageData, RCANMessageError, RCANRegistryError, RCANSignatureError, RCANValidationError, RCAN_VERSION, type RegistrationResult, RegistryClient, type Robot, type RobotRegistration, RobotURI, RobotURIError, type RobotURIOptions, type SignatureBlock, VERSION, type ValidationResult, validateConfig, validateMessage, validateURI };
486
+ export { type ApprovalStatus, AuditChain, AuditError, type ChainVerifyResult, CommitmentRecord, type CommitmentRecordData, type CommitmentRecordJSON, ConfidenceGate, GateError, HiTLGate, type ListResult, NodeClient, type PendingApproval, RCANAddressError, type RCANAgentConfig, type RCANConfig, RCANError, RCANGateError, RCANMessage, type RCANMessageData, type RCANMessageEnvelope, RCANMessageError, type RCANMetadata, RCANNodeError, RCANNodeNotFoundError, RCANNodeSyncError, RCANNodeTrustError, RCANRegistryError, type RCANRegistryNode, type RCANResolveResult, RCANSignatureError, RCANValidationError, RCAN_VERSION, type RegistrationResult, RegistryClient, type Robot, type RobotRegistration, RobotURI, RobotURIError, type RobotURIOptions, SPEC_VERSION, type SignatureBlock, VERSION, type ValidationResult, fetchCanonicalSchema, validateConfig, validateConfigAgainstSchema, validateMessage, validateNodeAgainstSchema, validateURI };