@siduri-x/api 1.0.0 → 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.
@@ -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 {};
@@ -1,233 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- const context_mapper_1 = require("./context-mapper");
4
- describe('API Request Context Mapper (P2 & T1-01 to T1-10)', () => {
5
- // T1-01: Anonymous public chat, no audience
6
- test('T1-01: Anonymous public chat with no audience resolves to configured public audience and no subject', () => {
7
- const input = {
8
- companionId: 'companion-a',
9
- context: {
10
- actor: {
11
- actorId: 'anonymous-session-a',
12
- sessionId: 'session-a',
13
- authorizationRole: 'viewer',
14
- capabilities: ['chat:public'],
15
- authenticated: false,
16
- },
17
- conversation: {
18
- channel: 'public',
19
- correlationId: 'corr-public-a',
20
- },
21
- },
22
- message: 'Hello.',
23
- history: [],
24
- };
25
- const result = (0, context_mapper_1.mapRequestContext)(input, {
26
- endpointPolicy: 'public',
27
- defaultPublicAudience: 'audience-public',
28
- });
29
- expect(result.accepted).toBe(true);
30
- expect(result.context?.companionId).toBe('companion-a');
31
- expect(result.context?.conversation.channel).toBe('public');
32
- expect(result.context?.conversation.audienceId).toBe('audience-public');
33
- expect(result.context?.subject).toBeUndefined();
34
- expect(result.diagnostics).toContain('audience_defaulted_by_public_policy');
35
- });
36
- // T1-02: OWNER public chat
37
- test('T1-02: OWNER public chat maps authorization metadata only with no relationship or private routing', () => {
38
- const input = {
39
- id: 'companion-a',
40
- role: 'OWNER',
41
- correlationId: 'corr-owner-public',
42
- message: 'Public broadcast message',
43
- };
44
- const result = (0, context_mapper_1.mapRequestContext)(input, { endpointPolicy: 'public' });
45
- expect(result.accepted).toBe(true);
46
- expect(result.context?.actor.authorizationRole).toBe('administrator');
47
- expect(result.context?.conversation.channel).toBe('public');
48
- expect(result.context?.conversation.audienceId).toBe('audience-public');
49
- expect(result.context?.subject).toBeUndefined();
50
- expect(result.diagnostics).toContain('legacy_role_mapped_to_authorization');
51
- });
52
- // T1-03: Private chat without capability
53
- test('T1-03: Private chat without capability is rejected before organ invocation', () => {
54
- const input = {
55
- companionId: 'companion-a',
56
- context: {
57
- actor: {
58
- actorId: 'actor-a',
59
- sessionId: 'session-a',
60
- authorizationRole: 'viewer',
61
- capabilities: ['chat:public'], // Missing 'chat:private'
62
- authenticated: true,
63
- },
64
- conversation: {
65
- channel: 'private',
66
- audienceId: 'audience-private-a',
67
- correlationId: 'corr-private-a',
68
- },
69
- },
70
- message: 'Show private data',
71
- };
72
- const result = (0, context_mapper_1.mapRequestContext)(input, { endpointPolicy: 'private' });
73
- expect(result.accepted).toBe(false);
74
- expect(result.error?.code).toBe('UNAUTHORIZED_CHANNEL_OR_CAPABILITY');
75
- expect(result.error?.fields).toContain('actor.capabilities');
76
- });
77
- // T1-04: Operator request without operator context
78
- test('T1-04: Operator request without operator context is rejected with diagnostic', () => {
79
- const input = {
80
- id: 'companion-a',
81
- role: 'OPERATOR',
82
- correlationId: 'corr-op-missing',
83
- };
84
- const result = (0, context_mapper_1.mapRequestContext)(input, { endpointPolicy: 'operator' });
85
- expect(result.accepted).toBe(false);
86
- expect(result.error?.code).toBe('MISSING_CONTEXT');
87
- expect(result.error?.fields).toEqual(expect.arrayContaining(['conversation.channel', 'conversation.audienceId', 'actor.capabilities']));
88
- });
89
- // T1-05: Global primary_user input
90
- test('T1-05: Global primary_user input is rejected/quarantined at mapper', () => {
91
- const input = {
92
- companionId: 'companion-a',
93
- context: {
94
- actor: {
95
- actorId: 'actor-a',
96
- sessionId: 'session-a',
97
- authorizationRole: 'viewer',
98
- capabilities: ['chat:public'],
99
- authenticated: true,
100
- },
101
- conversation: {
102
- channel: 'public',
103
- audienceId: 'audience-public',
104
- correlationId: 'corr-primary-user',
105
- },
106
- subject: {
107
- subjectId: 'primary_user',
108
- kind: 'actor',
109
- },
110
- },
111
- };
112
- const result = (0, context_mapper_1.mapRequestContext)(input);
113
- expect(result.accepted).toBe(false);
114
- expect(result.error?.code).toBe('FORBIDDEN_CONTEXT');
115
- expect(result.error?.field).toBe('subject.subjectId');
116
- });
117
- // T1-06: MASTER_PRIVATE in public mode
118
- test('T1-06: MASTER_PRIVATE in public mode is rejected with no fallback', () => {
119
- const input = {
120
- companionId: 'companion-a',
121
- context: {
122
- actor: {
123
- actorId: 'actor-a',
124
- sessionId: 'session-a',
125
- authorizationRole: 'viewer',
126
- capabilities: ['chat:public'],
127
- authenticated: false,
128
- },
129
- conversation: {
130
- channel: 'public',
131
- audienceId: 'MASTER_PRIVATE',
132
- correlationId: 'corr-master-private',
133
- },
134
- },
135
- };
136
- const result = (0, context_mapper_1.mapRequestContext)(input, { endpointPolicy: 'public' });
137
- expect(result.accepted).toBe(false);
138
- expect(result.error?.code).toBe('LEGACY_PERSONAL_AUDIENCE');
139
- expect(result.error?.field).toBe('audienceId');
140
- });
141
- // T1-07: Missing actor on explicit teaching
142
- test('T1-07: Missing actor on explicit teaching produces no durable profile and pending/anonymous context only', () => {
143
- const input = {
144
- companionId: 'companion-a',
145
- context: {
146
- actor: {
147
- actorId: '',
148
- sessionId: 'session-a',
149
- authorizationRole: 'viewer',
150
- capabilities: ['chat:public'],
151
- authenticated: false,
152
- },
153
- conversation: {
154
- channel: 'public',
155
- audienceId: 'audience-public',
156
- correlationId: 'corr-teach-no-actor',
157
- },
158
- },
159
- };
160
- const result = (0, context_mapper_1.mapRequestContext)(input);
161
- expect(result.accepted).toBe(false);
162
- expect(result.error?.code).toBe('MISSING_CONTEXT');
163
- expect(result.error?.fields).toContain('actor.actorId');
164
- });
165
- // T1-08: Two companion IDs, same actor remain isolated
166
- test('T1-08: Two companion IDs with same actor remain isolated under context mapper', () => {
167
- const actor = {
168
- actorId: 'actor-a',
169
- sessionId: 'session-a',
170
- authorizationRole: 'viewer',
171
- capabilities: ['chat:public'],
172
- authenticated: true,
173
- };
174
- const conv = {
175
- channel: 'public',
176
- audienceId: 'audience-public',
177
- correlationId: 'corr-iso-1',
178
- };
179
- const reqA = { companionId: 'companion-a', context: { actor, conversation: conv } };
180
- const reqB = { companionId: 'companion-b', context: { actor, conversation: conv } };
181
- const resA = (0, context_mapper_1.mapRequestContext)(reqA);
182
- const resB = (0, context_mapper_1.mapRequestContext)(reqB);
183
- expect(resA.accepted).toBe(true);
184
- expect(resB.accepted).toBe(true);
185
- expect(resA.context?.companionId).toBe('companion-a');
186
- expect(resB.context?.companionId).toBe('companion-b');
187
- });
188
- // T1-09: Missing correlation ID
189
- test('T1-09: Missing correlation ID is rejected before stateful operation', () => {
190
- const input = {
191
- companionId: 'companion-a',
192
- context: {
193
- actor: {
194
- actorId: 'actor-a',
195
- sessionId: 'session-a',
196
- authorizationRole: 'viewer',
197
- capabilities: ['chat:public'],
198
- authenticated: true,
199
- },
200
- conversation: {
201
- channel: 'public',
202
- audienceId: 'audience-public',
203
- correlationId: '',
204
- },
205
- },
206
- };
207
- const result = (0, context_mapper_1.mapRequestContext)(input);
208
- expect(result.accepted).toBe(false);
209
- expect(result.error?.code).toBe('MISSING_CONTEXT');
210
- expect(result.error?.fields).toContain('conversation.correlationId');
211
- });
212
- // T1-10: Ambiguous legacy request
213
- test('T1-10: Ambiguous legacy request emits structured diagnostic and rejects ambiguous mapping', () => {
214
- const input = {
215
- companionId: 'companion-a',
216
- role: 'VIEWER',
217
- context: {
218
- actor: {
219
- actorId: 'actor-a',
220
- sessionId: 'session-a',
221
- authorizationRole: 'viewer',
222
- capabilities: ['chat:public'],
223
- authenticated: true,
224
- },
225
- // Role supplied at top level without channel or audience
226
- },
227
- };
228
- const result = (0, context_mapper_1.mapRequestContext)(input);
229
- expect(result.accepted).toBe(false);
230
- expect(result.error?.code).toBe('AMBIGUOUS_CONTEXT');
231
- expect(result.error?.conflicts).toEqual(expect.arrayContaining(['role_does_not_select_audience', 'role_does_not_select_subject']));
232
- });
233
- });
package/dist/index.d.ts DELETED
@@ -1,6 +0,0 @@
1
- import { Express } from 'express';
2
- import { createApp, AppInstance } from './app';
3
- export { createApp, AppInstance };
4
- export * from './context-mapper';
5
- export declare const app: Express;
6
- export default app;