@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.
- package/package.json +14 -13
- package/src/app.ts +445 -181
- package/src/b0-b6.test.ts +1 -1
- package/src/cors.ts +44 -0
- package/src/index.test.ts +54 -1
- package/src/index.ts +32 -23
- package/src/runtime.test.ts +55 -1
- package/src/t6-security.test.ts +210 -1
- package/dist/app.d.ts +0 -9
- package/dist/app.js +0 -436
- package/dist/auth.d.ts +0 -8
- package/dist/auth.js +0 -40
- package/dist/auth.test.d.ts +0 -1
- package/dist/auth.test.js +0 -48
- package/dist/b0-b6.test.d.ts +0 -1
- package/dist/b0-b6.test.js +0 -121
- package/dist/context-mapper.d.ts +0 -15
- package/dist/context-mapper.js +0 -287
- package/dist/context-mapper.test.d.ts +0 -1
- package/dist/context-mapper.test.js +0 -233
- package/dist/index.d.ts +0 -6
- package/dist/index.js +0 -167
- package/dist/index.test.d.ts +0 -1
- package/dist/index.test.js +0 -115
- package/dist/runtime.d.ts +0 -1
- package/dist/runtime.js +0 -17
- package/dist/runtime.test.d.ts +0 -1
- package/dist/runtime.test.js +0 -240
- package/dist/smoke.test.d.ts +0 -0
- package/dist/smoke.test.js +0 -6
- package/dist/t4-gating.test.d.ts +0 -1
- package/dist/t4-gating.test.js +0 -193
- package/dist/t5-experience.test.d.ts +0 -1
- package/dist/t5-experience.test.js +0 -156
- package/dist/t6-security.test.d.ts +0 -1
- package/dist/t6-security.test.js +0 -234
- package/dist/t7-release.test.d.ts +0 -1
- package/dist/t7-release.test.js +0 -119
package/dist/context-mapper.js
DELETED
|
@@ -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
|
-
});
|