@siduri-x/api 1.0.1 → 1.0.2

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/auth.js DELETED
@@ -1,40 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.resolveIdentity = resolveIdentity;
4
- exports.requireRole = requireRole;
5
- exports.attachIdentity = attachIdentity;
6
- function resolveIdentity(req) {
7
- const authHeader = req.headers.authorization;
8
- const token = authHeader?.startsWith('Bearer ') ? authHeader.split(' ')[1] : undefined;
9
- // 1. Explicit token matches
10
- if (process.env.OWNER_TOKEN && token === process.env.OWNER_TOKEN) {
11
- return { role: 'OWNER' };
12
- }
13
- if (process.env.OPERATOR_TOKEN && token === process.env.OPERATOR_TOKEN) {
14
- return { role: 'OPERATOR' };
15
- }
16
- // 2. Development fallback
17
- const isDev = process.env.NODE_ENV !== 'production';
18
- if (isDev && process.env.DEV_LOCAL_AUTH_ROLE) {
19
- const fallbackRole = process.env.DEV_LOCAL_AUTH_ROLE.toUpperCase();
20
- if (['OWNER', 'OPERATOR', 'VIEWER'].includes(fallbackRole)) {
21
- return { role: fallbackRole };
22
- }
23
- }
24
- // Default
25
- return { role: 'VIEWER' };
26
- }
27
- function requireRole(allowedRoles) {
28
- return (req, res, next) => {
29
- const identity = resolveIdentity(req);
30
- req.identity = identity;
31
- if (!allowedRoles.includes(identity.role)) {
32
- return res.status(403).json({ error: `Forbidden: requires one of ${allowedRoles.join(', ')}` });
33
- }
34
- next();
35
- };
36
- }
37
- function attachIdentity(req, res, next) {
38
- req.identity = resolveIdentity(req);
39
- next();
40
- }
@@ -1 +0,0 @@
1
- export {};
package/dist/auth.test.js DELETED
@@ -1,48 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- const auth_1 = require("./auth");
4
- describe('Auth Identity Resolution', () => {
5
- const originalEnv = process.env;
6
- beforeEach(() => {
7
- jest.resetModules();
8
- process.env = { ...originalEnv };
9
- });
10
- afterAll(() => {
11
- process.env = originalEnv;
12
- });
13
- const mockReq = (token) => ({
14
- headers: {
15
- authorization: token ? `Bearer ${token}` : undefined
16
- }
17
- });
18
- test('resolves explicit owner token', () => {
19
- process.env.OWNER_TOKEN = 'owner-secret';
20
- process.env.OPERATOR_TOKEN = 'operator-secret';
21
- process.env.NODE_ENV = 'production';
22
- const ownerId = (0, auth_1.resolveIdentity)(mockReq('owner-secret'));
23
- expect(ownerId.role).toBe('OWNER');
24
- });
25
- test('resolves explicit operator token', () => {
26
- process.env.OWNER_TOKEN = 'owner-secret';
27
- process.env.OPERATOR_TOKEN = 'operator-secret';
28
- process.env.NODE_ENV = 'production';
29
- const opId = (0, auth_1.resolveIdentity)(mockReq('operator-secret'));
30
- expect(opId.role).toBe('OPERATOR');
31
- });
32
- test('defaults to viewer when no token is present in production', () => {
33
- process.env.NODE_ENV = 'production';
34
- const viewerId = (0, auth_1.resolveIdentity)(mockReq());
35
- expect(viewerId.role).toBe('VIEWER');
36
- });
37
- test('defaults to viewer for invalid token in production', () => {
38
- process.env.NODE_ENV = 'production';
39
- const invalidId = (0, auth_1.resolveIdentity)(mockReq('invalid-token'));
40
- expect(invalidId.role).toBe('VIEWER');
41
- });
42
- test('resolves development fallback role', () => {
43
- process.env.NODE_ENV = 'development';
44
- process.env.DEV_LOCAL_AUTH_ROLE = 'OWNER';
45
- const devId = (0, auth_1.resolveIdentity)(mockReq());
46
- expect(devId.role).toBe('OWNER');
47
- });
48
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,121 +0,0 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- const supertest_1 = __importDefault(require("supertest"));
7
- const app_1 = require("./app");
8
- const runtime_1 = require("./runtime");
9
- describe('T0 B0 & B6 Runtime Proof Suite', () => {
10
- let mockBrain;
11
- let mockMemory;
12
- let mockKnowledge;
13
- let mockBehavior;
14
- let runtime;
15
- let app;
16
- beforeEach(async () => {
17
- mockBrain = {
18
- generatePlan: jest.fn().mockImplementation(async (ctx) => {
19
- return {
20
- speech: 'Hello. I am a neutral companion.',
21
- language: 'en',
22
- };
23
- }),
24
- };
25
- mockMemory = {
26
- initialize: jest.fn().mockResolvedValue(undefined),
27
- searchClaims: jest.fn().mockResolvedValue([]),
28
- getClaims: jest.fn().mockResolvedValue([]),
29
- getDirectives: jest.fn().mockResolvedValue([]),
30
- getPendingClaims: jest.fn().mockResolvedValue([]),
31
- proposeClaim: jest.fn().mockResolvedValue({}),
32
- approveClaim: jest.fn().mockResolvedValue(undefined),
33
- rejectClaim: jest.fn().mockResolvedValue(undefined),
34
- };
35
- mockKnowledge = {
36
- search: jest.fn().mockResolvedValue([]),
37
- };
38
- mockBehavior = {
39
- compile: jest.fn().mockResolvedValue(''),
40
- };
41
- const config = {
42
- name: 'NeutralCompanion',
43
- brain: { provider: 'openrouter' },
44
- memory: { provider: 'postgres' },
45
- knowledge: { provider: 'e-knowledge' },
46
- behavior: { provider: 'active-self' },
47
- voice: { provider: 'none' },
48
- vision: { provider: 'none' },
49
- body: { provider: 'none' },
50
- };
51
- runtime = new runtime_1.SiduriRuntime('companion-a', config, {
52
- brain: mockBrain,
53
- memory: mockMemory,
54
- knowledge: mockKnowledge,
55
- behavior: mockBehavior,
56
- });
57
- await runtime.initialize();
58
- const runtimes = new Map([['companion-a', runtime]]);
59
- const created = (0, app_1.createApp)(runtimes);
60
- app = created.app;
61
- });
62
- // B0: Fresh companion is empty (no prior claims, no user relationship, no knowledge search on greeting)
63
- describe('B0 — Fresh companion is empty', () => {
64
- test('initial state has empty memory and empty directives', async () => {
65
- const claims = await runtime.memory?.getClaims();
66
- const directives = await runtime.memory?.getDirectives();
67
- expect(claims).toEqual([]);
68
- expect(directives).toEqual([]);
69
- });
70
- test('greeting does not query knowledge or inject prior personal knowledge', async () => {
71
- const res = await (0, supertest_1.default)(app)
72
- .post('/chat')
73
- .send({
74
- companionId: 'companion-a',
75
- message: 'Hello.',
76
- history: [],
77
- });
78
- expect(res.status).toBe(200);
79
- expect(mockKnowledge.search).not.toHaveBeenCalled();
80
- expect(mockBrain.generatePlan).toHaveBeenCalledWith(expect.objectContaining({
81
- contextPrompt: '',
82
- recipient: 'OWNER',
83
- }));
84
- });
85
- });
86
- // B6: Identity and relationship are learned, not inferred (self identity questions do not query external knowledge)
87
- describe('B6 — Identity and relationship are learned, not inferred', () => {
88
- test('asking "Who are you?" suppresses knowledge query and asserts self identity without external search', async () => {
89
- mockBrain.generatePlan.mockResolvedValueOnce({
90
- speech: 'I am NeutralCompanion.',
91
- language: 'en',
92
- });
93
- const res = await (0, supertest_1.default)(app)
94
- .post('/chat')
95
- .send({
96
- companionId: 'companion-a',
97
- message: 'Who are you?',
98
- history: [],
99
- });
100
- expect(res.status).toBe(200);
101
- // B6 oracle: self identity chat does not query external knowledge
102
- expect(mockKnowledge.search).not.toHaveBeenCalled();
103
- expect(mockMemory.searchClaims).toHaveBeenCalled();
104
- expect(mockBrain.generatePlan).toHaveBeenCalledWith(expect.objectContaining({
105
- contextPrompt: '',
106
- }));
107
- expect(res.body.response.subtitle_en).toBe('I am NeutralCompanion.');
108
- });
109
- test('asking "Tell me about yourself" suppresses knowledge query', async () => {
110
- const res = await (0, supertest_1.default)(app)
111
- .post('/chat')
112
- .send({
113
- companionId: 'companion-a',
114
- message: 'Tell me about yourself',
115
- history: [],
116
- });
117
- expect(res.status).toBe(200);
118
- expect(mockKnowledge.search).not.toHaveBeenCalled();
119
- });
120
- });
121
- });
@@ -1,15 +0,0 @@
1
- import { RequestContext, DiagnosticCode, ContextError } from '@siduri-x/core';
2
- export interface ContextMapperOptions {
3
- endpointPolicy?: 'public' | 'private' | 'operator' | 'direct';
4
- defaultPublicAudience?: string;
5
- defaultPrivateAudience?: string;
6
- defaultOperatorAudience?: string;
7
- allowAnonymousPublicChat?: boolean;
8
- }
9
- export interface MapRequestContextResult {
10
- accepted: boolean;
11
- context?: RequestContext;
12
- diagnostics?: DiagnosticCode[];
13
- error?: ContextError;
14
- }
15
- export declare function mapRequestContext(input: any, options?: ContextMapperOptions): MapRequestContextResult;
@@ -1,287 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.mapRequestContext = mapRequestContext;
4
- const core_1 = require("@siduri-x/core");
5
- function mapRequestContext(input, options = {}) {
6
- const diagnostics = [];
7
- const endpointPolicy = options.endpointPolicy || 'public';
8
- const defaultPublicAudience = options.defaultPublicAudience || 'audience-public';
9
- if (!input || typeof input !== 'object') {
10
- return {
11
- accepted: false,
12
- error: {
13
- code: 'MISSING_CONTEXT',
14
- fields: ['request'],
15
- },
16
- };
17
- }
18
- // Check for legacy MASTER_PRIVATE in any audience field or request
19
- const rawAudience = input?.context?.conversation?.audienceId ??
20
- input?.conversation?.audienceId ??
21
- input?.audienceId ??
22
- input?.audience;
23
- if (rawAudience === 'MASTER_PRIVATE' || input?.scope === 'MASTER_PRIVATE') {
24
- if (endpointPolicy === 'public' || input?.channel === 'public' || input?.context?.conversation?.channel === 'public') {
25
- return {
26
- accepted: false,
27
- error: {
28
- code: 'LEGACY_PERSONAL_AUDIENCE',
29
- field: 'audienceId',
30
- correlationId: input?.context?.conversation?.correlationId || input?.correlationId,
31
- },
32
- };
33
- }
34
- }
35
- // 1. If incoming input already has a full neutral context structure
36
- if (input.context && typeof input.context === 'object') {
37
- const rawCtx = input.context;
38
- const companionId = input.companionId || rawCtx.companionId || input.id;
39
- const correlationId = rawCtx.conversation?.correlationId || input.correlationId;
40
- if (!companionId) {
41
- return {
42
- accepted: false,
43
- error: {
44
- code: 'MISSING_CONTEXT',
45
- fields: ['companionId'],
46
- correlationId,
47
- },
48
- };
49
- }
50
- // Role cannot select audience or subject
51
- const rawRole = input.role || rawCtx.actor?.authorizationRole;
52
- if (input.role && !rawCtx.conversation?.channel && !rawCtx.conversation?.audienceId) {
53
- return {
54
- accepted: false,
55
- error: {
56
- code: 'AMBIGUOUS_CONTEXT',
57
- conflicts: ['role_does_not_select_audience', 'role_does_not_select_subject'],
58
- correlationId,
59
- },
60
- };
61
- }
62
- const channel = rawCtx.conversation?.channel || endpointPolicy;
63
- let audienceId = rawCtx.conversation?.audienceId;
64
- if (!audienceId) {
65
- if (channel === 'public' || endpointPolicy === 'public') {
66
- audienceId = defaultPublicAudience;
67
- diagnostics.push('audience_defaulted_by_public_policy');
68
- }
69
- else {
70
- return {
71
- accepted: false,
72
- error: {
73
- code: 'MISSING_CONTEXT',
74
- fields: ['conversation.audienceId'],
75
- correlationId,
76
- },
77
- };
78
- }
79
- }
80
- if (!correlationId) {
81
- return {
82
- accepted: false,
83
- error: {
84
- code: 'MISSING_CONTEXT',
85
- fields: ['conversation.correlationId'],
86
- },
87
- };
88
- }
89
- const actor = rawCtx.actor;
90
- if (!actor || typeof actor !== 'object') {
91
- return {
92
- accepted: false,
93
- error: {
94
- code: 'MISSING_CONTEXT',
95
- fields: ['actor'],
96
- correlationId,
97
- },
98
- };
99
- }
100
- // Check required capabilities for private/operator/direct channels
101
- const capabilities = Array.isArray(actor.capabilities) ? actor.capabilities : [];
102
- if (channel === 'private' && !capabilities.includes('chat:private')) {
103
- return {
104
- accepted: false,
105
- error: {
106
- code: 'UNAUTHORIZED_CHANNEL_OR_CAPABILITY',
107
- message: 'Private channel requires explicit chat:private capability',
108
- fields: ['actor.capabilities'],
109
- correlationId,
110
- },
111
- };
112
- }
113
- if (channel === 'operator' && !capabilities.includes('memory:inspect') && !capabilities.includes('operator:access')) {
114
- return {
115
- accepted: false,
116
- error: {
117
- code: 'UNAUTHORIZED_CHANNEL_OR_CAPABILITY',
118
- message: 'Operator channel requires explicit operator capability',
119
- fields: ['actor.capabilities'],
120
- correlationId,
121
- },
122
- };
123
- }
124
- // Check subject policy
125
- let subject = rawCtx.subject;
126
- if (subject) {
127
- if (subject.subjectId === 'primary_user' || subject === 'primary_user') {
128
- // Global primary_user is rejected or quarantined
129
- return {
130
- accepted: false,
131
- error: {
132
- code: 'FORBIDDEN_CONTEXT',
133
- message: 'Global primary_user subject is forbidden',
134
- field: 'subject.subjectId',
135
- correlationId,
136
- },
137
- };
138
- }
139
- }
140
- const constructed = {
141
- companionId,
142
- actor: {
143
- actorId: actor.actorId,
144
- sessionId: actor.sessionId,
145
- authorizationRole: actor.authorizationRole,
146
- capabilities,
147
- authenticated: Boolean(actor.authenticated),
148
- },
149
- conversation: {
150
- channel,
151
- audienceId,
152
- isLive: rawCtx.conversation?.isLive,
153
- correlationId,
154
- },
155
- subject,
156
- };
157
- const validated = (0, core_1.validateRequestContext)(constructed);
158
- if (!validated.accepted) {
159
- return validated;
160
- }
161
- return {
162
- accepted: true,
163
- context: validated.context,
164
- diagnostics: diagnostics.length > 0 ? diagnostics : undefined,
165
- };
166
- }
167
- // 2. Legacy compatibility envelope mapping
168
- const companionId = input.companionId || input.id;
169
- const correlationId = input.correlationId || input.conversation?.correlationId;
170
- if (!companionId) {
171
- return {
172
- accepted: false,
173
- error: {
174
- code: 'MISSING_CONTEXT',
175
- fields: ['companionId'],
176
- correlationId,
177
- },
178
- };
179
- }
180
- if (companionId === 'default') {
181
- diagnostics.push('companion_default_mapped_for_bootstrap');
182
- }
183
- // Map legacy role to authorizationRole
184
- const legacyRole = input.role?.toString().toUpperCase();
185
- let authRole = 'viewer';
186
- if (legacyRole === 'OWNER') {
187
- authRole = 'administrator';
188
- diagnostics.push('legacy_role_mapped_to_authorization');
189
- }
190
- else if (legacyRole === 'OPERATOR') {
191
- authRole = 'operator';
192
- diagnostics.push('legacy_role_mapped_to_authorization');
193
- }
194
- else if (legacyRole === 'VIEWER') {
195
- authRole = 'viewer';
196
- diagnostics.push('legacy_role_mapped_to_authorization');
197
- }
198
- else if (input.role) {
199
- return {
200
- accepted: false,
201
- error: {
202
- code: 'INVALID_CONTEXT',
203
- field: 'role',
204
- correlationId,
205
- },
206
- };
207
- }
208
- // Check endpoint policy vs legacy request
209
- if (endpointPolicy === 'private' || endpointPolicy === 'operator' || endpointPolicy === 'direct') {
210
- // Missing explicit channel, audience, or capability on private/operator endpoint is an error
211
- const fields = [];
212
- if (!input.channel)
213
- fields.push('conversation.channel');
214
- if (!input.audienceId)
215
- fields.push('conversation.audienceId');
216
- if (!input.capabilities && !input.actor?.capabilities)
217
- fields.push('actor.capabilities');
218
- if (!correlationId)
219
- fields.push('conversation.correlationId');
220
- return {
221
- accepted: false,
222
- error: {
223
- code: 'MISSING_CONTEXT',
224
- fields: fields.length > 0 ? fields : ['conversation.audienceId', 'actor.capabilities'],
225
- correlationId,
226
- },
227
- };
228
- }
229
- // Ambiguity check: if legacy input specifies role without correlationId or endpoint context for stateful op
230
- if (input.subject === 'primary_user' || input.subjectId === 'primary_user') {
231
- return {
232
- accepted: false,
233
- error: {
234
- code: 'FORBIDDEN_CONTEXT',
235
- message: 'Global primary_user subject is forbidden',
236
- field: 'subject',
237
- correlationId,
238
- },
239
- };
240
- }
241
- // Missing correlationId for stateful requests
242
- const finalCorrelationId = correlationId || (input.generateCorrelationId ? `corr-${Date.now()}` : undefined);
243
- if (!finalCorrelationId) {
244
- return {
245
- accepted: false,
246
- error: {
247
- code: 'MISSING_CONTEXT',
248
- fields: ['conversation.correlationId'],
249
- },
250
- };
251
- }
252
- // Build anonymous public context
253
- const actorId = input.actorId || input.actor?.actorId || 'anonymous-session-a';
254
- const sessionId = input.sessionId || input.actor?.sessionId || 'session-a';
255
- if (!input.actorId && !input.actor?.actorId) {
256
- diagnostics.push('anonymous_session_generated');
257
- }
258
- const channel = 'public';
259
- const audienceId = defaultPublicAudience;
260
- diagnostics.push('audience_defaulted_by_public_policy');
261
- const capabilities = authRole === 'administrator'
262
- ? ['chat:public', 'admin:access']
263
- : authRole === 'operator'
264
- ? ['chat:public', 'operator:access']
265
- : ['chat:public'];
266
- const mappedContext = {
267
- companionId,
268
- actor: {
269
- actorId,
270
- sessionId,
271
- authorizationRole: authRole,
272
- capabilities,
273
- authenticated: Boolean(input.authenticated),
274
- },
275
- conversation: {
276
- channel,
277
- audienceId,
278
- correlationId: finalCorrelationId,
279
- },
280
- subject: undefined, // Anonymous public chat has no subject
281
- };
282
- return {
283
- accepted: true,
284
- context: mappedContext,
285
- diagnostics: diagnostics.length > 0 ? diagnostics : undefined,
286
- };
287
- }
@@ -1 +0,0 @@
1
- export {};