@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/package.json +11 -10
- package/src/app.ts +276 -49
- package/src/index.test.ts +53 -0
- package/src/index.ts +30 -21
- package/src/runtime.test.ts +55 -1
- package/dist/app.d.ts +0 -9
- package/dist/app.js +0 -480
- 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/cors.d.ts +0 -3
- package/dist/cors.js +0 -42
- 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 -424
- package/dist/t7-release.test.d.ts +0 -1
- package/dist/t7-release.test.js +0 -119
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
|
-
}
|
package/dist/auth.test.d.ts
DELETED
|
@@ -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
|
-
});
|
package/dist/b0-b6.test.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/dist/b0-b6.test.js
DELETED
|
@@ -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
|
-
});
|
package/dist/context-mapper.d.ts
DELETED
|
@@ -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;
|
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 {};
|