@siduri-x/api 1.0.0 → 1.0.1

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/app.js CHANGED
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.createApp = createApp;
7
7
  const express_1 = __importDefault(require("express"));
8
8
  const cors_1 = __importDefault(require("cors"));
9
+ const cors_2 = require("./cors");
9
10
  const runtime_1 = require("./runtime");
10
11
  const brain_1 = require("@siduri-x/brain");
11
12
  const memory_1 = require("@siduri-x/memory");
@@ -20,7 +21,7 @@ const auth_1 = require("./auth");
20
21
  const context_mapper_1 = require("./context-mapper");
21
22
  function createApp(runtimes = new Map()) {
22
23
  const app = (0, express_1.default)();
23
- app.use((0, cors_1.default)());
24
+ app.use((0, cors_1.default)((0, cors_2.createCorsOptions)()));
24
25
  app.use(express_1.default.json());
25
26
  let observationOrgan;
26
27
  function createBrain(config) {
@@ -42,7 +43,7 @@ function createApp(runtimes = new Map()) {
42
43
  function createVoice(config) {
43
44
  return isDisabled(config)
44
45
  ? undefined
45
- : new voice_1.VoicevoxAdapter({ baseUrl: process.env.VOICEVOX_URL || 'http://localhost:50021', speakerId: config.speakerId || 1 });
46
+ : new voice_1.VoiceAdapter({ provider: config.provider || 'voicevox', baseUrl: process.env.VOICEVOX_URL || 'http://localhost:50021', speakerId: config.speakerId || 1 });
46
47
  }
47
48
  function createKnowledge(config) {
48
49
  return isDisabled(config) ? undefined : new knowledge_1.EKnowledgeAdapter(config);
@@ -96,11 +97,19 @@ function createApp(runtimes = new Map()) {
96
97
  });
97
98
  // STATUS / HEALTH ENDPOINTS
98
99
  app.get('/health', (req, res) => res.json({ status: "ok" }));
99
- app.get('/version', (req, res) => res.json({ name: "siduri-y-api", version: "0.2.0-y" }));
100
- app.get('/ready', (req, res) => res.json({ status: "ready", dependencies: {} }));
101
- app.get('/voice/health', (req, res) => res.json({ provider: "voicevox", healthy: true }));
102
- app.get('/obs/health', (req, res) => res.json({ connected: true }));
103
- app.get('/platforms/status', (req, res) => res.json({ platforms: {} }));
100
+ app.get('/version', (req, res) => res.json({ name: "siduri-x-api", version: "0.2.0-x" }));
101
+ app.get('/ready', (req, res) => {
102
+ const isReady = runtimes.size > 0;
103
+ res.status(isReady ? 200 : 503).json({
104
+ status: isReady ? "ready" : "not_ready",
105
+ companionCount: runtimes.size,
106
+ });
107
+ });
108
+ app.get('/voice/health', (req, res) => {
109
+ const hasVoice = Array.from(runtimes.values()).some((r) => Boolean(r.voice));
110
+ res.json({ provider: "siduri-voice", configured: hasVoice });
111
+ });
112
+ app.get('/obs/health', (req, res) => res.json({ connected: Boolean(observationOrgan) }));
104
113
  app.get('/me', auth_1.attachIdentity, (req, res) => {
105
114
  const identity = req.identity;
106
115
  res.json({
@@ -114,11 +123,18 @@ function createApp(runtimes = new Map()) {
114
123
  app.post('/chat', auth_1.attachIdentity, async (req, res) => {
115
124
  const { id, message, history } = req.body;
116
125
  const identity = req.identity;
126
+ // Single-owner companion model: /chat defaults to OWNER identity.
127
+ // Explicit non-owner role in request body or context (e.g. adversarial test suites) is respected.
128
+ const effectiveRole = req.body?.role === 'VIEWER' || req.body?.context?.actor?.authorizationRole === 'viewer'
129
+ ? 'VIEWER'
130
+ : (identity?.role === 'OPERATOR' ? 'OPERATOR' : 'OWNER');
131
+ const isOwner = effectiveRole === 'OWNER';
117
132
  // Call context mapper at the API boundary
118
133
  const mappingResult = (0, context_mapper_1.mapRequestContext)({
119
134
  ...req.body,
120
135
  id: id || req.body.companionId,
121
- role: req.body.role || identity?.role,
136
+ role: effectiveRole,
137
+ authenticated: isOwner,
122
138
  generateCorrelationId: true,
123
139
  }, {
124
140
  endpointPolicy: 'public',
@@ -135,13 +151,13 @@ function createApp(runtimes = new Map()) {
135
151
  if (!runtime)
136
152
  return res.status(404).json({ error: "Companion not found" });
137
153
  try {
138
- // Map authorization role to legacy memory scope for backwards-compatible runtime call
139
- const legacyScope = mappingResult.context.actor.authorizationRole === 'administrator'
140
- ? 'OWNER'
141
- : mappingResult.context.actor.authorizationRole === 'operator'
142
- ? 'OPERATOR'
143
- : 'VIEWER';
144
- const response = await runtime.handleUserMessage(message, legacyScope, history);
154
+ const response = await (0, runtime_1.dispatchCompanionChat)(runtime, {
155
+ id: companionId,
156
+ companionId,
157
+ message,
158
+ context: mappingResult.context,
159
+ history,
160
+ });
145
161
  res.json(response);
146
162
  }
147
163
  catch (e) {
@@ -210,7 +226,26 @@ function createApp(runtimes = new Map()) {
210
226
  }
211
227
  });
212
228
  // MEMORY MUTATIONS - PROPOSALS
213
- app.post('/memory/proposals/update', (0, auth_1.requireRole)(['OWNER', 'OPERATOR']), async (req, res) => res.json({ success: true }));
229
+ app.post('/memory/proposals/update', (0, auth_1.requireRole)(['OWNER', 'OPERATOR']), async (req, res) => {
230
+ const id = req.body.companionId || Array.from(runtimes.keys())[0];
231
+ const runtime = runtimes.get(id);
232
+ if (!runtime)
233
+ return res.status(404).json({ error: "Companion not found" });
234
+ if (!runtime.memory || typeof runtime.memory.updateClaim !== 'function') {
235
+ return res.status(400).json({ error: "Memory organ does not support updating claims" });
236
+ }
237
+ try {
238
+ const claimId = req.body.id || req.body.claimId;
239
+ if (!claimId) {
240
+ return res.status(400).json({ error: "Missing required claim id" });
241
+ }
242
+ const updated = await runtime.memory.updateClaim(claimId, req.body.updates || req.body);
243
+ res.json({ success: true, claim: updated });
244
+ }
245
+ catch (e) {
246
+ res.status(500).json({ error: e.message });
247
+ }
248
+ });
214
249
  app.post('/memory/proposals/approve', (0, auth_1.requireRole)(['OWNER', 'OPERATOR']), async (req, res) => {
215
250
  const id = req.body.companionId || Array.from(runtimes.keys())[0];
216
251
  const runtime = runtimes.get(id);
@@ -302,135 +337,144 @@ function createApp(runtimes = new Map()) {
302
337
  res.status(500).json({ error: e.message });
303
338
  }
304
339
  });
305
- app.post('/dev/memory/reset', (0, auth_1.requireRole)(['OWNER']), async (req, res) => res.json({ reset: true }));
306
- // MOCKS / DEV / EVIDENCE / PLATFORMS
307
- app.get('/platforms/events', (req, res) => res.json({ events: [] }));
308
- app.get('/platforms/actions', (req, res) => res.json({ actions: [] }));
309
- app.get('/evidence', (req, res) => res.json({ results: [] }));
310
- app.get('/observations', (req, res) => res.json({ observations: observationOrgan?.current() ?? [] }));
311
- app.post('/dev/mock-response', async (req, res) => {
312
- const companionId = req.body?.companionId || Array.from(runtimes.keys())[0] || 'default';
313
- const runtime = runtimes.get(companionId);
314
- if (!runtime)
315
- return res.status(404).json({ accepted: false, error: 'Companion not found' });
316
- const staged = runtime.gating.stageResponse({
317
- requestContext: {
318
- companionId,
319
- actor: {
320
- actorId: 'operator-a',
321
- sessionId: 'sess-op',
322
- authorizationRole: 'operator',
323
- capabilities: ['chat:public', 'memory:approve'],
324
- authenticated: true,
325
- },
326
- conversation: {
327
- channel: 'public',
328
- audienceId: 'audience-public',
329
- correlationId: req.body?.correlation_id || `corr-${Date.now()}`,
330
- },
331
- },
332
- candidateSpeech: req.body?.speech || 'Mocked staged response for review',
333
- candidateLanguage: req.body?.language || 'en',
334
- requiresApproval: req.body?.requiresApproval ?? true,
340
+ const isDevMode = process.env.NODE_ENV !== 'production' || process.env.SIDURI_DEV_MODE === 'true';
341
+ if (isDevMode) {
342
+ app.post('/dev/memory/reset', (0, auth_1.requireRole)(['OWNER']), async (req, res) => {
343
+ const id = req.body.companionId || Array.from(runtimes.keys())[0];
344
+ const runtime = runtimes.get(id);
345
+ if (!runtime)
346
+ return res.status(404).json({ error: "Companion not found" });
347
+ if (!runtime.memory || typeof runtime.memory.resetMemory !== 'function') {
348
+ return res.status(400).json({ error: "Memory organ does not support reset" });
349
+ }
350
+ try {
351
+ await runtime.memory.resetMemory();
352
+ res.json({ reset: true });
353
+ }
354
+ catch (e) {
355
+ res.status(500).json({ error: e.message });
356
+ }
335
357
  });
336
- res.json({
337
- accepted: true,
338
- staged: true,
339
- status: staged.status,
340
- response_id: staged.responseId,
341
- correlation_id: staged.correlationId,
342
- speech: staged.speech,
358
+ app.post('/dev/mock-response', async (req, res) => {
359
+ const companionId = req.body?.companionId || Array.from(runtimes.keys())[0] || 'default';
360
+ const runtime = runtimes.get(companionId);
361
+ if (!runtime)
362
+ return res.status(404).json({ accepted: false, error: 'Companion not found' });
363
+ const staged = runtime.gating.stageResponse({
364
+ requestContext: {
365
+ companionId,
366
+ actor: {
367
+ actorId: 'operator-a',
368
+ sessionId: 'sess-op',
369
+ authorizationRole: 'operator',
370
+ capabilities: ['chat:public', 'memory:approve'],
371
+ authenticated: true,
372
+ },
373
+ conversation: {
374
+ channel: 'public',
375
+ audienceId: 'audience-public',
376
+ correlationId: req.body?.correlation_id || `corr-${Date.now()}`,
377
+ },
378
+ },
379
+ candidateSpeech: req.body?.speech || 'Mocked staged response for review',
380
+ candidateLanguage: req.body?.language || 'en',
381
+ requiresApproval: req.body?.requiresApproval ?? true,
382
+ });
383
+ res.json({
384
+ accepted: true,
385
+ staged: true,
386
+ status: staged.status,
387
+ response_id: staged.responseId,
388
+ correlation_id: staged.correlationId,
389
+ speech: staged.speech,
390
+ });
343
391
  });
344
- });
345
- app.post('/dev/approve-response', async (req, res) => {
346
- const companionId = req.body?.companionId || Array.from(runtimes.keys())[0] || 'default';
347
- const runtime = runtimes.get(companionId);
348
- if (!runtime)
349
- return res.status(404).json({ approved: false, error: 'Companion not found' });
350
- let responseId = req.body?.responseId;
351
- let correlationId = req.body?.correlation_id;
352
- if (!responseId && correlationId) {
353
- const found = runtime.gating.findStagedPlanByCorrelation(companionId, correlationId);
354
- if (found) {
355
- responseId = found.responseId;
392
+ app.post('/dev/approve-response', async (req, res) => {
393
+ const companionId = req.body?.companionId || Array.from(runtimes.keys())[0] || 'default';
394
+ const runtime = runtimes.get(companionId);
395
+ if (!runtime)
396
+ return res.status(404).json({ approved: false, error: 'Companion not found' });
397
+ let responseId = req.body?.responseId;
398
+ let correlationId = req.body?.correlation_id;
399
+ if (!responseId && correlationId) {
400
+ const found = runtime.gating.findStagedPlanByCorrelation(companionId, correlationId);
401
+ if (found) {
402
+ responseId = found.responseId;
403
+ }
356
404
  }
357
- }
358
- else if (responseId && !correlationId) {
359
- const found = runtime.gating.getStagedPlan(responseId);
360
- if (found) {
361
- correlationId = found.correlationId;
405
+ else if (responseId && !correlationId) {
406
+ const found = runtime.gating.getStagedPlan(responseId);
407
+ if (found) {
408
+ correlationId = found.correlationId;
409
+ }
362
410
  }
363
- }
364
- if (!responseId) {
365
- return res.status(400).json({ approved: false, error: 'UNKNOWN_APPROVAL_ID' });
366
- }
367
- const result = runtime.gating.approveResponse({
368
- responseId,
369
- companionId,
370
- correlationId: correlationId || '',
371
- audienceId: req.body?.audienceId,
372
- });
373
- if (!result.success) {
374
- return res.status(400).json({ approved: false, error: result.reason });
375
- }
376
- // Now evaluated as approved
377
- const evaluation = runtime.gating.evaluateGate(result.plan);
378
- res.json({
379
- approved: true,
380
- status: evaluation.disposition,
381
- response_id: result.plan.responseId,
382
- speech: result.plan.speech,
383
- language: result.plan.language,
411
+ if (!responseId) {
412
+ return res.status(400).json({ approved: false, error: 'UNKNOWN_APPROVAL_ID' });
413
+ }
414
+ const result = runtime.gating.approveResponse({
415
+ responseId,
416
+ companionId,
417
+ correlationId: correlationId || '',
418
+ audienceId: req.body?.audienceId,
419
+ });
420
+ if (!result.success) {
421
+ return res.status(400).json({ approved: false, error: result.reason });
422
+ }
423
+ // Now evaluated as approved
424
+ const evaluation = runtime.gating.evaluateGate(result.plan);
425
+ res.json({
426
+ approved: true,
427
+ status: evaluation.disposition,
428
+ response_id: result.plan.responseId,
429
+ speech: result.plan.speech,
430
+ language: result.plan.language,
431
+ });
384
432
  });
385
- });
386
- app.post('/dev/reject-response', async (req, res) => {
387
- const companionId = req.body?.companionId || Array.from(runtimes.keys())[0] || 'default';
388
- const runtime = runtimes.get(companionId);
389
- if (!runtime)
390
- return res.status(404).json({ rejected: false, error: 'Companion not found' });
391
- let responseId = req.body?.responseId;
392
- let correlationId = req.body?.correlation_id;
393
- if (!responseId && correlationId) {
394
- const found = runtime.gating.findStagedPlanByCorrelation(companionId, correlationId);
395
- if (found) {
396
- responseId = found.responseId;
433
+ app.post('/dev/reject-response', async (req, res) => {
434
+ const companionId = req.body?.companionId || Array.from(runtimes.keys())[0] || 'default';
435
+ const runtime = runtimes.get(companionId);
436
+ if (!runtime)
437
+ return res.status(404).json({ rejected: false, error: 'Companion not found' });
438
+ let responseId = req.body?.responseId;
439
+ let correlationId = req.body?.correlation_id;
440
+ if (!responseId && correlationId) {
441
+ const found = runtime.gating.findStagedPlanByCorrelation(companionId, correlationId);
442
+ if (found) {
443
+ responseId = found.responseId;
444
+ }
397
445
  }
398
- }
399
- else if (responseId && !correlationId) {
400
- const found = runtime.gating.getStagedPlan(responseId);
401
- if (found) {
402
- correlationId = found.correlationId;
446
+ else if (responseId && !correlationId) {
447
+ const found = runtime.gating.getStagedPlan(responseId);
448
+ if (found) {
449
+ correlationId = found.correlationId;
450
+ }
403
451
  }
404
- }
405
- if (!responseId) {
406
- return res.status(400).json({ rejected: false, error: 'UNKNOWN_APPROVAL_ID' });
407
- }
408
- const result = runtime.gating.rejectResponse({
409
- responseId,
410
- companionId,
411
- correlationId: correlationId || '',
412
- reason: req.body?.reason,
452
+ if (!responseId) {
453
+ return res.status(400).json({ rejected: false, error: 'UNKNOWN_APPROVAL_ID' });
454
+ }
455
+ const result = runtime.gating.rejectResponse({
456
+ responseId,
457
+ companionId,
458
+ correlationId: correlationId || '',
459
+ reason: req.body?.reason,
460
+ });
461
+ if (!result.success) {
462
+ return res.status(400).json({ rejected: false, error: result.reason });
463
+ }
464
+ res.json({
465
+ rejected: true,
466
+ status: result.plan.status,
467
+ response_id: result.plan.responseId,
468
+ });
413
469
  });
414
- if (!result.success) {
415
- return res.status(400).json({ rejected: false, error: result.reason });
416
- }
417
- res.json({
418
- rejected: true,
419
- status: result.plan.status,
420
- response_id: result.plan.responseId,
470
+ app.post('/dev/mock-observation', async (req, res) => {
471
+ if (!observationOrgan)
472
+ return res.status(503).json({ accepted: false, reason: 'observation_unavailable' });
473
+ const result = await observationOrgan.ingest(new Uint8Array([115, 121, 110, 116, 104, 101, 116, 105, 99]), 'fixture-observation', 'configured-vision');
474
+ if (!result.observation)
475
+ return res.status(result.duplicate ? 200 : 409).json({ accepted: false, ...result });
476
+ res.status(202).json({ accepted: true, observation: result.observation });
421
477
  });
422
- });
423
- app.post('/dev/mock-observation', async (req, res) => {
424
- if (!observationOrgan)
425
- return res.status(503).json({ accepted: false, reason: 'observation_unavailable' });
426
- const result = await observationOrgan.ingest(new Uint8Array([115, 121, 110, 116, 104, 101, 116, 105, 99]), 'fixture-observation', 'configured-vision');
427
- if (!result.observation)
428
- return res.status(result.duplicate ? 200 : 409).json({ accepted: false, ...result });
429
- res.status(202).json({ accepted: true, observation: result.observation });
430
- });
431
- app.post('/platforms/actions/suggest', (req, res) => res.json({ suggested: true }));
432
- app.post('/platforms/actions/approve', (req, res) => res.json({ approved: true }));
433
- app.post('/platforms/actions/reject', (req, res) => res.json({ rejected: true }));
434
- app.post('/platforms/actions/send', (req, res) => res.json({ sent: true }));
478
+ }
435
479
  return { app, runtimes, setObservationOrgan: (org) => { observationOrgan = org; } };
436
480
  }
@@ -79,7 +79,7 @@ describe('T0 B0 & B6 Runtime Proof Suite', () => {
79
79
  expect(mockKnowledge.search).not.toHaveBeenCalled();
80
80
  expect(mockBrain.generatePlan).toHaveBeenCalledWith(expect.objectContaining({
81
81
  contextPrompt: '',
82
- recipient: 'VIEWER',
82
+ recipient: 'OWNER',
83
83
  }));
84
84
  });
85
85
  });
package/dist/cors.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ import cors from 'cors';
2
+ export declare function getAllowedOrigins(): Set<string>;
3
+ export declare function createCorsOptions(): cors.CorsOptions;
package/dist/cors.js ADDED
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getAllowedOrigins = getAllowedOrigins;
4
+ exports.createCorsOptions = createCorsOptions;
5
+ function getAllowedOrigins() {
6
+ const allowed = new Set([
7
+ 'http://localhost:3000',
8
+ 'http://127.0.0.1:3000',
9
+ 'http://localhost:3001',
10
+ 'http://127.0.0.1:3001',
11
+ ]);
12
+ if (process.env.PORT) {
13
+ allowed.add(`http://localhost:${process.env.PORT}`);
14
+ allowed.add(`http://127.0.0.1:${process.env.PORT}`);
15
+ }
16
+ const envOrigins = process.env.ALLOWED_ORIGINS;
17
+ if (envOrigins) {
18
+ for (const origin of envOrigins.split(',')) {
19
+ const trimmed = origin.trim();
20
+ if (trimmed) {
21
+ allowed.add(trimmed);
22
+ }
23
+ }
24
+ }
25
+ return allowed;
26
+ }
27
+ function createCorsOptions() {
28
+ return {
29
+ origin: (origin, callback) => {
30
+ // Allow non-browser requests with no origin header (e.g., native tools, curl)
31
+ if (!origin) {
32
+ return callback(null, true);
33
+ }
34
+ const allowedOrigins = getAllowedOrigins();
35
+ if (allowedOrigins.has(origin)) {
36
+ return callback(null, true);
37
+ }
38
+ return callback(null, false);
39
+ },
40
+ credentials: true,
41
+ };
42
+ }
package/dist/index.js CHANGED
@@ -55,7 +55,7 @@ function isDisabled(config) {
55
55
  function createVoice(config) {
56
56
  return isDisabled(config)
57
57
  ? undefined
58
- : new voice_1.VoicevoxAdapter({ baseUrl: process.env.VOICEVOX_URL || 'http://localhost:50021', speakerId: config.speakerId || 1 });
58
+ : new voice_1.VoiceAdapter({ provider: config.provider || 'voicevox', baseUrl: process.env.VOICEVOX_URL || 'http://localhost:50021', speakerId: config.speakerId || 1 });
59
59
  }
60
60
  function createKnowledge(config) {
61
61
  if (isDisabled(config))
@@ -157,8 +157,8 @@ async function bootDefaultCompanion() {
157
157
  }
158
158
  if (process.env.NODE_ENV !== 'test') {
159
159
  bootDefaultCompanion().then(() => {
160
- exports.app.listen(PORT, () => {
161
- console.log(`Siduri-Y API running on port ${PORT}`);
160
+ exports.app.listen(Number(PORT), '127.0.0.1', () => {
161
+ console.log(`Siduri-X API running on port ${PORT} (127.0.0.1)`);
162
162
  });
163
163
  }).catch(e => {
164
164
  console.error("Failed to boot default companion:", e);
@@ -29,7 +29,7 @@ describe('API Boundary Context Validation (P2 Route Integration)', () => {
29
29
  history: [],
30
30
  });
31
31
  expect(res.status).toBe(200);
32
- expect(fakeRuntime.handleUserMessage).toHaveBeenCalledWith('Hello neutral world', 'VIEWER', []);
32
+ expect(fakeRuntime.handleUserMessage).toHaveBeenCalledWith('Hello neutral world', 'OWNER', []);
33
33
  });
34
34
  test('accepts neutral context chat envelope at /chat route', async () => {
35
35
  const res = await (0, supertest_1.default)(app)