@siduri-x/api 1.0.2 → 1.0.5
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.d.ts +43 -0
- package/dist/app.js +637 -0
- package/dist/auth.d.ts +34 -0
- package/dist/auth.js +84 -0
- package/dist/auth.test.d.ts +1 -0
- package/dist/auth.test.js +74 -0
- package/dist/b0-b6.test.d.ts +1 -0
- package/dist/b0-b6.test.js +121 -0
- package/dist/context-mapper.d.ts +18 -0
- package/dist/context-mapper.js +160 -0
- package/dist/context-mapper.test.d.ts +1 -0
- package/dist/context-mapper.test.js +136 -0
- package/dist/cors.d.ts +3 -0
- package/dist/cors.js +42 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +191 -0
- package/dist/index.test.d.ts +1 -0
- package/dist/index.test.js +137 -0
- package/dist/runtime.d.ts +1 -0
- package/dist/runtime.js +17 -0
- package/dist/runtime.test.d.ts +1 -0
- package/dist/runtime.test.js +282 -0
- package/dist/smoke.test.d.ts +0 -0
- package/dist/smoke.test.js +6 -0
- package/dist/t4-gating.test.d.ts +1 -0
- package/dist/t4-gating.test.js +193 -0
- package/dist/t5-experience.test.d.ts +1 -0
- package/dist/t5-experience.test.js +155 -0
- package/dist/t6-security.test.d.ts +1 -0
- package/dist/t6-security.test.js +423 -0
- package/dist/t7-release.test.d.ts +1 -0
- package/dist/t7-release.test.js +119 -0
- package/package.json +14 -11
- package/src/app.ts +36 -64
- package/src/auth.test.ts +56 -23
- package/src/auth.ts +74 -22
- package/src/context-mapper.test.ts +42 -147
- package/src/context-mapper.ts +44 -179
- package/src/index.test.ts +8 -32
- package/src/index.ts +19 -3
- package/src/runtime.test.ts +3 -5
- package/src/t5-experience.test.ts +0 -1
- package/src/t6-security.test.ts +0 -1
package/src/app.ts
CHANGED
|
@@ -13,7 +13,7 @@ import { FixtureObservationOrgan } from '@siduri-x/observation';
|
|
|
13
13
|
import { DefaultHandsOrgan, DefaultHandsOrganConfig } from '@siduri-x/hands';
|
|
14
14
|
import { DefaultEarOrgan, EarOrganConfig } from '@siduri-x/ear';
|
|
15
15
|
import { DefaultMouthOrgan, DefaultMouthOrganConfig } from '@siduri-x/mouth';
|
|
16
|
-
import { attachIdentity,
|
|
16
|
+
import { attachIdentity, requireAuth, Identity } from './auth';
|
|
17
17
|
import { mapRequestContext } from './context-mapper';
|
|
18
18
|
|
|
19
19
|
export interface AppBrainConfig {
|
|
@@ -130,7 +130,7 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
|
|
|
130
130
|
: new DefaultMouthOrgan({ ...config, voice });
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
app.post('/boot',
|
|
133
|
+
app.post('/boot', requireAuth, async (req, res) => {
|
|
134
134
|
try {
|
|
135
135
|
const { id, config } = req.body;
|
|
136
136
|
if (runtimes.has(id)) {
|
|
@@ -204,39 +204,26 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
|
|
|
204
204
|
app.get('/me', attachIdentity, (req, res) => {
|
|
205
205
|
const identity = (req as any).identity as Identity;
|
|
206
206
|
res.json({
|
|
207
|
-
actorId: identity.
|
|
208
|
-
role: identity.role,
|
|
209
|
-
authenticated: identity.
|
|
207
|
+
actorId: identity.actorId || (identity.authenticated ? 'owner-user' : 'anonymous-session'),
|
|
208
|
+
role: identity.role || (identity.authenticated ? 'OWNER' : 'VIEWER'),
|
|
209
|
+
authenticated: Boolean(identity.authenticated),
|
|
210
210
|
});
|
|
211
211
|
});
|
|
212
|
-
app.put('/me',
|
|
212
|
+
app.put('/me', requireAuth, (req, res) => res.json({ success: true }));
|
|
213
213
|
|
|
214
214
|
// CHAT (API context boundary validation)
|
|
215
215
|
app.post('/chat', attachIdentity, async (req, res) => {
|
|
216
216
|
const { id, message, history } = req.body;
|
|
217
217
|
const identity = (req as any).identity as Identity;
|
|
218
218
|
|
|
219
|
-
// Single-owner companion model:
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
:
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
const mappingResult = mapRequestContext(
|
|
228
|
-
{
|
|
229
|
-
...req.body,
|
|
230
|
-
id: id || req.body.companionId,
|
|
231
|
-
role: effectiveRole,
|
|
232
|
-
authenticated: isOwner,
|
|
233
|
-
generateCorrelationId: true,
|
|
234
|
-
},
|
|
235
|
-
{
|
|
236
|
-
endpointPolicy: 'public',
|
|
237
|
-
defaultPublicAudience: 'audience-public',
|
|
238
|
-
}
|
|
239
|
-
);
|
|
219
|
+
// Single-owner companion model: map request context directly
|
|
220
|
+
const mappingResult = mapRequestContext({
|
|
221
|
+
...req.body,
|
|
222
|
+
id: id || req.body.companionId,
|
|
223
|
+
authenticated: identity?.authenticated ?? true,
|
|
224
|
+
source: identity?.source ?? 'local',
|
|
225
|
+
generateCorrelationId: true,
|
|
226
|
+
});
|
|
240
227
|
|
|
241
228
|
if (!mappingResult.accepted) {
|
|
242
229
|
return res.status(400).json({
|
|
@@ -269,24 +256,13 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
|
|
|
269
256
|
const { id, message, history } = req.body;
|
|
270
257
|
const identity = (req as any).identity as Identity;
|
|
271
258
|
|
|
272
|
-
const
|
|
273
|
-
|
|
274
|
-
:
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
...req.body,
|
|
280
|
-
id: id || req.body.companionId,
|
|
281
|
-
role: effectiveRole,
|
|
282
|
-
authenticated: isOwner,
|
|
283
|
-
generateCorrelationId: true,
|
|
284
|
-
},
|
|
285
|
-
{
|
|
286
|
-
endpointPolicy: 'public',
|
|
287
|
-
defaultPublicAudience: 'audience-public',
|
|
288
|
-
}
|
|
289
|
-
);
|
|
259
|
+
const mappingResult = mapRequestContext({
|
|
260
|
+
...req.body,
|
|
261
|
+
id: id || req.body.companionId,
|
|
262
|
+
authenticated: identity?.authenticated ?? true,
|
|
263
|
+
source: identity?.source ?? 'local',
|
|
264
|
+
generateCorrelationId: true,
|
|
265
|
+
});
|
|
290
266
|
|
|
291
267
|
if (!mappingResult.accepted) {
|
|
292
268
|
return res.status(400).json({
|
|
@@ -406,7 +382,7 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
|
|
|
406
382
|
});
|
|
407
383
|
|
|
408
384
|
// MEMORY GETTERS
|
|
409
|
-
app.get('/memory/proposals',
|
|
385
|
+
app.get('/memory/proposals', requireAuth, async (req, res) => {
|
|
410
386
|
const id = req.query.id as string || Array.from(runtimes.keys())[0];
|
|
411
387
|
const runtime = runtimes.get(id);
|
|
412
388
|
if (!runtime) return res.status(404).json({ error: "Companion not found" });
|
|
@@ -419,7 +395,7 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
|
|
|
419
395
|
}
|
|
420
396
|
});
|
|
421
397
|
|
|
422
|
-
app.get('/memory',
|
|
398
|
+
app.get('/memory', requireAuth, async (req, res) => {
|
|
423
399
|
const id = req.query.id as string || Array.from(runtimes.keys())[0];
|
|
424
400
|
const runtime = runtimes.get(id);
|
|
425
401
|
if (!runtime) return res.status(404).json({ error: "Companion not found" });
|
|
@@ -432,7 +408,7 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
|
|
|
432
408
|
}
|
|
433
409
|
});
|
|
434
410
|
|
|
435
|
-
app.get('/memory/claims',
|
|
411
|
+
app.get('/memory/claims', requireAuth, async (req, res) => {
|
|
436
412
|
const id = req.query.id as string || Array.from(runtimes.keys())[0];
|
|
437
413
|
const runtime = runtimes.get(id);
|
|
438
414
|
if (!runtime) return res.status(404).json({ error: "Companion not found" });
|
|
@@ -445,7 +421,7 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
|
|
|
445
421
|
}
|
|
446
422
|
});
|
|
447
423
|
|
|
448
|
-
app.get('/memory/behavioral',
|
|
424
|
+
app.get('/memory/behavioral', requireAuth, async (req, res) => {
|
|
449
425
|
const id = req.query.id as string || Array.from(runtimes.keys())[0];
|
|
450
426
|
const runtime = runtimes.get(id);
|
|
451
427
|
if (!runtime) return res.status(404).json({ error: "Companion not found" });
|
|
@@ -459,7 +435,7 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
|
|
|
459
435
|
});
|
|
460
436
|
|
|
461
437
|
// MEMORY MUTATIONS - PROPOSALS
|
|
462
|
-
app.post('/memory/proposals/update',
|
|
438
|
+
app.post('/memory/proposals/update', requireAuth, async (req, res) => {
|
|
463
439
|
const id = req.body.companionId as string || Array.from(runtimes.keys())[0];
|
|
464
440
|
const runtime = runtimes.get(id);
|
|
465
441
|
if (!runtime) return res.status(404).json({ error: "Companion not found" });
|
|
@@ -478,7 +454,7 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
|
|
|
478
454
|
}
|
|
479
455
|
});
|
|
480
456
|
|
|
481
|
-
app.post('/memory/proposals/approve',
|
|
457
|
+
app.post('/memory/proposals/approve', requireAuth, async (req, res) => {
|
|
482
458
|
const id = req.body.companionId as string || Array.from(runtimes.keys())[0];
|
|
483
459
|
const runtime = runtimes.get(id);
|
|
484
460
|
if (!runtime) return res.status(404).json({ error: "Companion not found" });
|
|
@@ -491,7 +467,7 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
|
|
|
491
467
|
}
|
|
492
468
|
});
|
|
493
469
|
|
|
494
|
-
app.post('/memory/proposals/reject',
|
|
470
|
+
app.post('/memory/proposals/reject', requireAuth, async (req, res) => {
|
|
495
471
|
const id = req.body.companionId as string || Array.from(runtimes.keys())[0];
|
|
496
472
|
const runtime = runtimes.get(id);
|
|
497
473
|
if (!runtime) return res.status(404).json({ error: "Companion not found" });
|
|
@@ -505,7 +481,7 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
|
|
|
505
481
|
});
|
|
506
482
|
|
|
507
483
|
// MEMORY MUTATIONS - BEHAVIORAL
|
|
508
|
-
app.post('/memory/behavioral/approve',
|
|
484
|
+
app.post('/memory/behavioral/approve', requireAuth, async (req, res) => {
|
|
509
485
|
const id = req.body.companionId as string || Array.from(runtimes.keys())[0];
|
|
510
486
|
const runtime = runtimes.get(id);
|
|
511
487
|
if (!runtime) return res.status(404).json({ error: "Companion not found" });
|
|
@@ -518,7 +494,7 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
|
|
|
518
494
|
}
|
|
519
495
|
});
|
|
520
496
|
|
|
521
|
-
app.post('/memory/behavioral/reject',
|
|
497
|
+
app.post('/memory/behavioral/reject', requireAuth, async (req, res) => {
|
|
522
498
|
const id = req.body.companionId as string || Array.from(runtimes.keys())[0];
|
|
523
499
|
const runtime = runtimes.get(id);
|
|
524
500
|
if (!runtime) return res.status(404).json({ error: "Companion not found" });
|
|
@@ -531,7 +507,7 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
|
|
|
531
507
|
}
|
|
532
508
|
});
|
|
533
509
|
|
|
534
|
-
app.post('/memory/behavioral/revoke',
|
|
510
|
+
app.post('/memory/behavioral/revoke', requireAuth, async (req, res) => {
|
|
535
511
|
const id = req.body.companionId as string || Array.from(runtimes.keys())[0];
|
|
536
512
|
const runtime = runtimes.get(id);
|
|
537
513
|
if (!runtime) return res.status(404).json({ error: "Companion not found" });
|
|
@@ -544,7 +520,7 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
|
|
|
544
520
|
}
|
|
545
521
|
});
|
|
546
522
|
|
|
547
|
-
app.post('/memory/behavioral/disable',
|
|
523
|
+
app.post('/memory/behavioral/disable', requireAuth, async (req, res) => {
|
|
548
524
|
const id = req.body.companionId as string || Array.from(runtimes.keys())[0];
|
|
549
525
|
const runtime = runtimes.get(id);
|
|
550
526
|
if (!runtime) return res.status(404).json({ error: "Companion not found" });
|
|
@@ -559,7 +535,7 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
|
|
|
559
535
|
|
|
560
536
|
const isDevMode = process.env.NODE_ENV !== 'production' || process.env.SIDURI_DEV_MODE === 'true';
|
|
561
537
|
if (isDevMode) {
|
|
562
|
-
app.post('/dev/memory/reset',
|
|
538
|
+
app.post('/dev/memory/reset', requireAuth, async (req, res) => {
|
|
563
539
|
const id = req.body.companionId as string || Array.from(runtimes.keys())[0];
|
|
564
540
|
const runtime = runtimes.get(id);
|
|
565
541
|
if (!runtime) return res.status(404).json({ error: "Companion not found" });
|
|
@@ -582,15 +558,12 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
|
|
|
582
558
|
requestContext: {
|
|
583
559
|
companionId,
|
|
584
560
|
actor: {
|
|
585
|
-
actorId: '
|
|
586
|
-
sessionId: 'sess-
|
|
587
|
-
|
|
588
|
-
capabilities: ['chat:public', 'memory:approve'],
|
|
561
|
+
actorId: 'local-user',
|
|
562
|
+
sessionId: 'sess-local',
|
|
563
|
+
capabilities: ['chat', 'memory:approve'],
|
|
589
564
|
authenticated: true,
|
|
590
565
|
},
|
|
591
566
|
conversation: {
|
|
592
|
-
channel: 'public',
|
|
593
|
-
audienceId: 'audience-public',
|
|
594
567
|
correlationId: req.body?.correlation_id || `corr-${Date.now()}`,
|
|
595
568
|
},
|
|
596
569
|
},
|
|
@@ -636,7 +609,6 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
|
|
|
636
609
|
responseId,
|
|
637
610
|
companionId,
|
|
638
611
|
correlationId: correlationId || '',
|
|
639
|
-
audienceId: req.body?.audienceId,
|
|
640
612
|
});
|
|
641
613
|
|
|
642
614
|
if (!result.success) {
|
package/src/auth.test.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { resolveIdentity } from './auth';
|
|
1
|
+
import { resolveIdentity, requireAuth } from './auth';
|
|
2
2
|
|
|
3
|
-
describe('Auth Identity Resolution', () => {
|
|
3
|
+
describe('Auth Identity Resolution (Single-Owner External Boundary)', () => {
|
|
4
4
|
const originalEnv = process.env;
|
|
5
5
|
|
|
6
6
|
beforeEach(() => {
|
|
@@ -12,46 +12,79 @@ describe('Auth Identity Resolution', () => {
|
|
|
12
12
|
process.env = originalEnv;
|
|
13
13
|
});
|
|
14
14
|
|
|
15
|
-
const mockReq = (token?: string) => ({
|
|
15
|
+
const mockReq = (token?: string, ip: string = '192.168.1.50') => ({
|
|
16
16
|
headers: {
|
|
17
|
-
authorization: token ? `Bearer ${token}` : undefined
|
|
18
|
-
}
|
|
17
|
+
authorization: token ? `Bearer ${token}` : undefined,
|
|
18
|
+
},
|
|
19
|
+
ip,
|
|
20
|
+
socket: { remoteAddress: ip },
|
|
19
21
|
} as any);
|
|
20
22
|
|
|
21
|
-
test('
|
|
22
|
-
process.env.
|
|
23
|
-
process.env.OPERATOR_TOKEN = 'operator-secret';
|
|
23
|
+
test('authenticates external request when token matches configured AUTH_TOKEN', () => {
|
|
24
|
+
process.env.AUTH_TOKEN = 'secret-auth-key';
|
|
24
25
|
process.env.NODE_ENV = 'production';
|
|
25
26
|
|
|
26
|
-
const
|
|
27
|
-
expect(
|
|
27
|
+
const identity = resolveIdentity(mockReq('secret-auth-key'));
|
|
28
|
+
expect(identity.authenticated).toBe(true);
|
|
29
|
+
expect(identity.source).toBe('external');
|
|
28
30
|
});
|
|
29
31
|
|
|
30
|
-
test('
|
|
32
|
+
test('authenticates external request when token matches legacy OWNER_TOKEN', () => {
|
|
31
33
|
process.env.OWNER_TOKEN = 'owner-secret';
|
|
32
|
-
process.env.OPERATOR_TOKEN = 'operator-secret';
|
|
33
34
|
process.env.NODE_ENV = 'production';
|
|
34
35
|
|
|
35
|
-
const
|
|
36
|
-
expect(
|
|
36
|
+
const identity = resolveIdentity(mockReq('owner-secret'));
|
|
37
|
+
expect(identity.authenticated).toBe(true);
|
|
38
|
+
expect(identity.source).toBe('external');
|
|
37
39
|
});
|
|
38
40
|
|
|
39
|
-
test('
|
|
41
|
+
test('rejects external request when token is missing in production with configured token', () => {
|
|
42
|
+
process.env.AUTH_TOKEN = 'secret-auth-key';
|
|
40
43
|
process.env.NODE_ENV = 'production';
|
|
41
|
-
|
|
42
|
-
|
|
44
|
+
|
|
45
|
+
const identity = resolveIdentity(mockReq());
|
|
46
|
+
expect(identity.authenticated).toBe(false);
|
|
43
47
|
});
|
|
44
48
|
|
|
45
|
-
test('
|
|
49
|
+
test('rejects external request for invalid token in production', () => {
|
|
50
|
+
process.env.AUTH_TOKEN = 'secret-auth-key';
|
|
46
51
|
process.env.NODE_ENV = 'production';
|
|
47
|
-
|
|
48
|
-
|
|
52
|
+
|
|
53
|
+
const identity = resolveIdentity(mockReq('invalid-token'));
|
|
54
|
+
expect(identity.authenticated).toBe(false);
|
|
49
55
|
});
|
|
50
56
|
|
|
51
|
-
test('
|
|
57
|
+
test('authenticates local loopback request in development mode', () => {
|
|
52
58
|
process.env.NODE_ENV = 'development';
|
|
53
59
|
process.env.DEV_LOCAL_AUTH_ROLE = 'OWNER';
|
|
54
|
-
|
|
55
|
-
|
|
60
|
+
process.env.AUTH_TOKEN = 'secret-key';
|
|
61
|
+
|
|
62
|
+
const devIdentity = resolveIdentity(mockReq(undefined, '127.0.0.1'));
|
|
63
|
+
expect(devIdentity.authenticated).toBe(true);
|
|
64
|
+
expect(devIdentity.source).toBe('local');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('requireAuth middleware accepts authenticated request and rejects unauthenticated with 401', () => {
|
|
68
|
+
process.env.AUTH_TOKEN = 'secret-key';
|
|
69
|
+
process.env.NODE_ENV = 'production';
|
|
70
|
+
|
|
71
|
+
const nextFn = jest.fn();
|
|
72
|
+
const resUnauthorized = {
|
|
73
|
+
status: jest.fn().mockReturnThis(),
|
|
74
|
+
json: jest.fn().mockReturnThis(),
|
|
75
|
+
} as any;
|
|
76
|
+
|
|
77
|
+
// Unauthorized call
|
|
78
|
+
requireAuth(mockReq('wrong-token'), resUnauthorized, nextFn);
|
|
79
|
+
expect(resUnauthorized.status).toHaveBeenCalledWith(401);
|
|
80
|
+
expect(nextFn).not.toHaveBeenCalled();
|
|
81
|
+
|
|
82
|
+
// Authorized call
|
|
83
|
+
const resAuthorized = {
|
|
84
|
+
status: jest.fn().mockReturnThis(),
|
|
85
|
+
json: jest.fn().mockReturnThis(),
|
|
86
|
+
} as any;
|
|
87
|
+
requireAuth(mockReq('secret-key'), resAuthorized, nextFn);
|
|
88
|
+
expect(nextFn).toHaveBeenCalled();
|
|
56
89
|
});
|
|
57
90
|
});
|
package/src/auth.ts
CHANGED
|
@@ -1,49 +1,101 @@
|
|
|
1
1
|
import { Request, Response, NextFunction } from 'express';
|
|
2
2
|
|
|
3
|
-
export type Role = 'OWNER' | 'OPERATOR' | 'VIEWER';
|
|
3
|
+
export type Role = 'OWNER' | 'OPERATOR' | 'VIEWER' | 'user' | string;
|
|
4
4
|
|
|
5
5
|
export interface Identity {
|
|
6
|
-
|
|
6
|
+
authenticated: boolean;
|
|
7
|
+
source: 'local' | 'external';
|
|
8
|
+
actorId?: string;
|
|
9
|
+
role?: Role;
|
|
10
|
+
[key: string]: unknown;
|
|
7
11
|
}
|
|
8
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Checks whether an incoming request originates from the local machine loopback interface.
|
|
15
|
+
*/
|
|
16
|
+
export function isLocalRequest(req: Request): boolean {
|
|
17
|
+
const ip = req.ip || req.socket?.remoteAddress || '';
|
|
18
|
+
return (
|
|
19
|
+
ip === '127.0.0.1' ||
|
|
20
|
+
ip === '::1' ||
|
|
21
|
+
ip === '::ffff:127.0.0.1' ||
|
|
22
|
+
ip.endsWith('127.0.0.1') ||
|
|
23
|
+
ip === 'localhost'
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Resolves request identity according to the single-owner external machine boundary security model.
|
|
29
|
+
*
|
|
30
|
+
* - Security is enforced at the external machine boundary, NOT internally.
|
|
31
|
+
* - External network requests require a matching token (AUTH_TOKEN / OWNER_TOKEN).
|
|
32
|
+
* - There are no internal viewer/operator/owner role privileges inside the machine.
|
|
33
|
+
*/
|
|
9
34
|
export function resolveIdentity(req: Request): Identity {
|
|
10
|
-
const authHeader = req.headers
|
|
35
|
+
const authHeader = req.headers?.authorization;
|
|
11
36
|
const token = authHeader?.startsWith('Bearer ') ? authHeader.split(' ')[1] : undefined;
|
|
37
|
+
const ownerToken = process.env.AUTH_TOKEN || process.env.API_TOKEN || process.env.OWNER_TOKEN;
|
|
38
|
+
const operatorToken = process.env.OPERATOR_TOKEN;
|
|
39
|
+
const configuredToken = ownerToken || operatorToken;
|
|
40
|
+
|
|
41
|
+
const local = isLocalRequest(req);
|
|
12
42
|
|
|
13
43
|
// 1. Explicit token matches
|
|
14
|
-
if (
|
|
15
|
-
return { role: 'OWNER' };
|
|
44
|
+
if (ownerToken && token === ownerToken) {
|
|
45
|
+
return { authenticated: true, source: local ? 'local' : 'external', role: 'OWNER' };
|
|
16
46
|
}
|
|
17
|
-
if (
|
|
18
|
-
return { role: 'OPERATOR' };
|
|
47
|
+
if (operatorToken && token === operatorToken) {
|
|
48
|
+
return { authenticated: true, source: local ? 'local' : 'external', role: 'OPERATOR' };
|
|
19
49
|
}
|
|
20
50
|
|
|
21
|
-
// 2.
|
|
51
|
+
// 2. Dev local auth role fallback
|
|
22
52
|
const isDev = process.env.NODE_ENV !== 'production';
|
|
23
53
|
if (isDev && process.env.DEV_LOCAL_AUTH_ROLE) {
|
|
24
54
|
const fallbackRole = process.env.DEV_LOCAL_AUTH_ROLE.toUpperCase() as Role;
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
55
|
+
return {
|
|
56
|
+
authenticated: fallbackRole === 'OWNER' || fallbackRole === 'OPERATOR',
|
|
57
|
+
source: 'local',
|
|
58
|
+
role: fallbackRole,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 3. Default unauthenticated / visitor
|
|
63
|
+
return { authenticated: false, source: local ? 'local' : 'external', role: 'VIEWER' };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* External boundary authentication middleware.
|
|
68
|
+
* Verifies that requests crossing the external boundary are authorized.
|
|
69
|
+
*/
|
|
70
|
+
export function requireAuth(req: Request, res: Response, next: NextFunction) {
|
|
71
|
+
const identity = resolveIdentity(req);
|
|
72
|
+
(req as any).identity = identity;
|
|
73
|
+
|
|
74
|
+
if (!identity.authenticated) {
|
|
75
|
+
return res.status(401).json({ error: 'Unauthorized: missing or invalid authentication token' });
|
|
28
76
|
}
|
|
77
|
+
next();
|
|
78
|
+
}
|
|
29
79
|
|
|
30
|
-
|
|
31
|
-
|
|
80
|
+
/**
|
|
81
|
+
* Attaches the resolved machine identity to the request.
|
|
82
|
+
*/
|
|
83
|
+
export function attachIdentity(req: Request, res: Response, next: NextFunction) {
|
|
84
|
+
(req as any).identity = resolveIdentity(req);
|
|
85
|
+
next();
|
|
32
86
|
}
|
|
33
87
|
|
|
34
|
-
|
|
88
|
+
/**
|
|
89
|
+
* Compatibility alias: in single-owner model, all authenticated callers have full access.
|
|
90
|
+
*/
|
|
91
|
+
export function requireRole(allowedRoles?: Role[]) {
|
|
35
92
|
return (req: Request, res: Response, next: NextFunction) => {
|
|
36
93
|
const identity = resolveIdentity(req);
|
|
37
94
|
(req as any).identity = identity;
|
|
38
|
-
|
|
39
|
-
if (!
|
|
40
|
-
return res.status(403).json({ error: `Forbidden: requires one of ${allowedRoles.join(', ')}` });
|
|
95
|
+
|
|
96
|
+
if (!identity.authenticated) {
|
|
97
|
+
return res.status(403).json({ error: `Forbidden: requires one of ${(allowedRoles || []).join(', ')}` });
|
|
41
98
|
}
|
|
42
99
|
next();
|
|
43
100
|
};
|
|
44
101
|
}
|
|
45
|
-
|
|
46
|
-
export function attachIdentity(req: Request, res: Response, next: NextFunction) {
|
|
47
|
-
(req as any).identity = resolveIdentity(req);
|
|
48
|
-
next();
|
|
49
|
-
}
|