@amodalai/runtime 0.2.8 → 0.2.10
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/src/__fixtures__/smoke.test.js +6 -0
- package/dist/src/__fixtures__/smoke.test.js.map +1 -1
- package/dist/src/agent/agent-types.d.ts +6 -0
- package/dist/src/agent/local-server.js +24 -2
- package/dist/src/agent/local-server.js.map +1 -1
- package/dist/src/agent/local-server.test.js +42 -0
- package/dist/src/agent/local-server.test.js.map +1 -1
- package/dist/src/agent/routes/files.d.ts +7 -0
- package/dist/src/agent/routes/files.js +151 -2
- package/dist/src/agent/routes/files.js.map +1 -1
- package/dist/src/agent/routes/files.test.d.ts +6 -0
- package/dist/src/agent/routes/files.test.js +249 -0
- package/dist/src/agent/routes/files.test.js.map +1 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +2 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/role-provider.d.ts +90 -0
- package/dist/src/role-provider.js +127 -0
- package/dist/src/role-provider.js.map +1 -0
- package/dist/src/role-provider.test.d.ts +6 -0
- package/dist/src/role-provider.test.js +179 -0
- package/dist/src/role-provider.test.js.map +1 -0
- package/dist/src/routes/route-helpers.js +1 -1
- package/dist/src/routes/route-helpers.js.map +1 -1
- package/dist/src/server.d.ts +16 -0
- package/dist/src/server.js +21 -0
- package/dist/src/server.js.map +1 -1
- package/dist/src/server.test.js +84 -0
- package/dist/src/server.test.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Amodal Labs, Inc.
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* RoleProvider — minimal user role abstraction for role-gated routes.
|
|
8
|
+
*
|
|
9
|
+
* The OSS runtime defines this interface; hosting layers (cloud, self-hosted,
|
|
10
|
+
* `amodal dev`) provide their own implementations:
|
|
11
|
+
* - `amodal dev`: returns `ops` for everyone (the developer is the only user)
|
|
12
|
+
* - cloud: parses platform JWT claims → ops/admin/user
|
|
13
|
+
* - self-hosted: customer plugs in their own auth (OIDC, custom JWT, etc.)
|
|
14
|
+
*
|
|
15
|
+
* The runtime gates admin/ops API routes through `requireRole` middleware
|
|
16
|
+
* which calls into the configured RoleProvider on each request.
|
|
17
|
+
*/
|
|
18
|
+
import type { Request, RequestHandler } from 'express';
|
|
19
|
+
/**
|
|
20
|
+
* The three roles the runtime understands. Roles are ordered by privilege:
|
|
21
|
+
* `user` < `admin` < `ops`.
|
|
22
|
+
*
|
|
23
|
+
* - `user` — end-user. Can chat with the agent and see their own sessions.
|
|
24
|
+
* - `admin` — agent admin (Sally). Can edit content (skills, knowledge, prompts)
|
|
25
|
+
* and view operational state (sessions, automations, stores).
|
|
26
|
+
* Cannot edit infrastructure (connections, model, tools).
|
|
27
|
+
* - `ops` — developer / platform admin. Can edit everything including
|
|
28
|
+
* connections, model config, and custom tools. Can run evals and
|
|
29
|
+
* inspect runtime internals.
|
|
30
|
+
*/
|
|
31
|
+
export type RuntimeRole = 'user' | 'admin' | 'ops';
|
|
32
|
+
export interface RuntimeUser {
|
|
33
|
+
/** Stable identifier for the user (email, sub claim, or 'local-dev'). */
|
|
34
|
+
id: string;
|
|
35
|
+
/** The user's role. Determines which routes they can access. */
|
|
36
|
+
role: RuntimeRole;
|
|
37
|
+
}
|
|
38
|
+
export interface RoleProvider {
|
|
39
|
+
/**
|
|
40
|
+
* Resolve the current user from the HTTP request, or `null` if the request
|
|
41
|
+
* is unauthenticated.
|
|
42
|
+
*
|
|
43
|
+
* Implementations should be stateless and cheap to call — `requireRole`
|
|
44
|
+
* invokes them on every protected request.
|
|
45
|
+
*/
|
|
46
|
+
resolveUser(req: Request): Promise<RuntimeUser | null>;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Default RoleProvider used when none is configured.
|
|
50
|
+
*
|
|
51
|
+
* Returns `ops` for every request — appropriate for `amodal dev` (where the
|
|
52
|
+
* developer is the only user) and for backwards compatibility with existing
|
|
53
|
+
* deployments that don't yet provide a RoleProvider.
|
|
54
|
+
*
|
|
55
|
+
* Hosting layers MUST replace this with their own implementation in production.
|
|
56
|
+
*/
|
|
57
|
+
export declare const defaultRoleProvider: RoleProvider;
|
|
58
|
+
/**
|
|
59
|
+
* Express middleware factory that gates a route on a minimum role.
|
|
60
|
+
*
|
|
61
|
+
* Usage:
|
|
62
|
+
* ```
|
|
63
|
+
* app.put('/api/files/*', requireRole(roleProvider, 'admin'), handler);
|
|
64
|
+
* app.put('/api/connections/*', requireRole(roleProvider, 'ops'), handler);
|
|
65
|
+
* ```
|
|
66
|
+
*
|
|
67
|
+
* Behavior:
|
|
68
|
+
* - 401 if `resolveUser` returns null (unauthenticated)
|
|
69
|
+
* - 403 if the user's role is below the minimum
|
|
70
|
+
* - Otherwise sets `res.locals.user` and calls `next()`
|
|
71
|
+
*
|
|
72
|
+
* The resolved user is attached to `res.locals.user` so handlers can read it
|
|
73
|
+
* without re-resolving.
|
|
74
|
+
*/
|
|
75
|
+
export declare function requireRole(roleProvider: RoleProvider, minRole: RuntimeRole): RequestHandler;
|
|
76
|
+
/**
|
|
77
|
+
* Typed error thrown when the RoleProvider itself fails (not when a user
|
|
78
|
+
* is unauthenticated or forbidden — those are handled directly with HTTP
|
|
79
|
+
* status codes).
|
|
80
|
+
*/
|
|
81
|
+
export declare class RoleProviderError extends Error {
|
|
82
|
+
readonly code: string;
|
|
83
|
+
readonly cause: unknown;
|
|
84
|
+
constructor(code: string, message: string, cause?: unknown);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Compare two roles. Returns true if `a` is at least as privileged as `b`.
|
|
88
|
+
* Useful for inline checks inside route handlers.
|
|
89
|
+
*/
|
|
90
|
+
export declare function hasRole(user: RuntimeUser | null, minRole: RuntimeRole): boolean;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Amodal Labs, Inc.
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
import { asyncHandler } from './routes/route-helpers.js';
|
|
7
|
+
import { createLogger } from './logger.js';
|
|
8
|
+
const log = createLogger({ component: 'role-provider' });
|
|
9
|
+
/**
|
|
10
|
+
* Numeric privilege level for each role. Used by `requireRole` to compare
|
|
11
|
+
* the resolved role against the route's minimum requirement.
|
|
12
|
+
*/
|
|
13
|
+
const ROLE_LEVEL = {
|
|
14
|
+
user: 0,
|
|
15
|
+
admin: 1,
|
|
16
|
+
ops: 2,
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Default RoleProvider used when none is configured.
|
|
20
|
+
*
|
|
21
|
+
* Returns `ops` for every request — appropriate for `amodal dev` (where the
|
|
22
|
+
* developer is the only user) and for backwards compatibility with existing
|
|
23
|
+
* deployments that don't yet provide a RoleProvider.
|
|
24
|
+
*
|
|
25
|
+
* Hosting layers MUST replace this with their own implementation in production.
|
|
26
|
+
*/
|
|
27
|
+
export const defaultRoleProvider = {
|
|
28
|
+
async resolveUser() {
|
|
29
|
+
return { id: 'local-dev', role: 'ops' };
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Express middleware factory that gates a route on a minimum role.
|
|
34
|
+
*
|
|
35
|
+
* Usage:
|
|
36
|
+
* ```
|
|
37
|
+
* app.put('/api/files/*', requireRole(roleProvider, 'admin'), handler);
|
|
38
|
+
* app.put('/api/connections/*', requireRole(roleProvider, 'ops'), handler);
|
|
39
|
+
* ```
|
|
40
|
+
*
|
|
41
|
+
* Behavior:
|
|
42
|
+
* - 401 if `resolveUser` returns null (unauthenticated)
|
|
43
|
+
* - 403 if the user's role is below the minimum
|
|
44
|
+
* - Otherwise sets `res.locals.user` and calls `next()`
|
|
45
|
+
*
|
|
46
|
+
* The resolved user is attached to `res.locals.user` so handlers can read it
|
|
47
|
+
* without re-resolving.
|
|
48
|
+
*/
|
|
49
|
+
export function requireRole(roleProvider, minRole) {
|
|
50
|
+
const minLevel = ROLE_LEVEL[minRole];
|
|
51
|
+
return asyncHandler(async (req, res, next) => {
|
|
52
|
+
let user;
|
|
53
|
+
try {
|
|
54
|
+
user = await roleProvider.resolveUser(req);
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
// RoleProvider failures are infrastructure errors. Log with context and
|
|
58
|
+
// forward as a typed error to the central error handler, which will
|
|
59
|
+
// sanitize the response (we deliberately don't expose the underlying
|
|
60
|
+
// error message to the client here).
|
|
61
|
+
log.error('role_provider_failed', {
|
|
62
|
+
path: req.path,
|
|
63
|
+
method: req.method,
|
|
64
|
+
required_role: minRole,
|
|
65
|
+
error: err instanceof Error ? err.message : String(err),
|
|
66
|
+
});
|
|
67
|
+
next(new RoleProviderError('role_provider_failed', 'Failed to resolve user role', err));
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (!user) {
|
|
71
|
+
log.warn('role_check_unauthenticated', {
|
|
72
|
+
path: req.path,
|
|
73
|
+
method: req.method,
|
|
74
|
+
required_role: minRole,
|
|
75
|
+
});
|
|
76
|
+
res.status(401).json({
|
|
77
|
+
error: { code: 'unauthenticated', message: 'Authentication required' },
|
|
78
|
+
});
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (ROLE_LEVEL[user.role] < minLevel) {
|
|
82
|
+
log.warn('role_check_forbidden', {
|
|
83
|
+
path: req.path,
|
|
84
|
+
method: req.method,
|
|
85
|
+
user_id: user.id,
|
|
86
|
+
current_role: user.role,
|
|
87
|
+
required_role: minRole,
|
|
88
|
+
});
|
|
89
|
+
res.status(403).json({
|
|
90
|
+
error: {
|
|
91
|
+
code: 'forbidden',
|
|
92
|
+
message: `Requires ${minRole} role`,
|
|
93
|
+
required_role: minRole,
|
|
94
|
+
current_role: user.role,
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
res.locals['user'] = user;
|
|
100
|
+
next();
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Typed error thrown when the RoleProvider itself fails (not when a user
|
|
105
|
+
* is unauthenticated or forbidden — those are handled directly with HTTP
|
|
106
|
+
* status codes).
|
|
107
|
+
*/
|
|
108
|
+
export class RoleProviderError extends Error {
|
|
109
|
+
code;
|
|
110
|
+
cause;
|
|
111
|
+
constructor(code, message, cause) {
|
|
112
|
+
super(message);
|
|
113
|
+
this.name = 'RoleProviderError';
|
|
114
|
+
this.code = code;
|
|
115
|
+
this.cause = cause;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Compare two roles. Returns true if `a` is at least as privileged as `b`.
|
|
120
|
+
* Useful for inline checks inside route handlers.
|
|
121
|
+
*/
|
|
122
|
+
export function hasRole(user, minRole) {
|
|
123
|
+
if (!user)
|
|
124
|
+
return false;
|
|
125
|
+
return ROLE_LEVEL[user.role] >= ROLE_LEVEL[minRole];
|
|
126
|
+
}
|
|
127
|
+
//# sourceMappingURL=role-provider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"role-provider.js","sourceRoot":"","sources":["../../src/role-provider.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Amodal Labs, Inc.
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
7
|
+
import { defaultRoleProvider, requireRole, hasRole, RoleProviderError, } from './role-provider.js';
|
|
8
|
+
function createMockRes() {
|
|
9
|
+
return {
|
|
10
|
+
status: vi.fn().mockReturnThis(),
|
|
11
|
+
json: vi.fn().mockReturnThis(),
|
|
12
|
+
locals: {},
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
function makeProvider(user) {
|
|
16
|
+
return {
|
|
17
|
+
async resolveUser() {
|
|
18
|
+
return user;
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Run an Express middleware and wait for it to settle by racing two signals:
|
|
24
|
+
* either `next()` was called, or `res.json()` was called. This avoids the
|
|
25
|
+
* brittle `setTimeout(0)` microtask-flush hack.
|
|
26
|
+
*/
|
|
27
|
+
async function runMiddleware(middleware, res) {
|
|
28
|
+
const nextCalls = [];
|
|
29
|
+
const settled = new Promise((resolve) => {
|
|
30
|
+
const onSettle = () => { resolve(); };
|
|
31
|
+
res.json.mockImplementation(() => { onSettle(); return res; });
|
|
32
|
+
const next = (...args) => {
|
|
33
|
+
nextCalls.push(args);
|
|
34
|
+
onSettle();
|
|
35
|
+
};
|
|
36
|
+
middleware({}, res, next);
|
|
37
|
+
});
|
|
38
|
+
await settled;
|
|
39
|
+
return { nextCalls };
|
|
40
|
+
}
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// defaultRoleProvider
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
describe('defaultRoleProvider', () => {
|
|
45
|
+
it('returns ops for any request', async () => {
|
|
46
|
+
const user = await defaultRoleProvider.resolveUser({});
|
|
47
|
+
expect(user).toEqual({ id: 'local-dev', role: 'ops' });
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
// hasRole
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
describe('hasRole', () => {
|
|
54
|
+
const user = { id: 'u1', role: 'user' };
|
|
55
|
+
const admin = { id: 'a1', role: 'admin' };
|
|
56
|
+
const ops = { id: 'o1', role: 'ops' };
|
|
57
|
+
it('user does not satisfy admin', () => {
|
|
58
|
+
expect(hasRole(user, 'admin')).toBe(false);
|
|
59
|
+
});
|
|
60
|
+
it('user does not satisfy ops', () => {
|
|
61
|
+
expect(hasRole(user, 'ops')).toBe(false);
|
|
62
|
+
});
|
|
63
|
+
it('user satisfies user', () => {
|
|
64
|
+
expect(hasRole(user, 'user')).toBe(true);
|
|
65
|
+
});
|
|
66
|
+
it('admin satisfies admin and user', () => {
|
|
67
|
+
expect(hasRole(admin, 'admin')).toBe(true);
|
|
68
|
+
expect(hasRole(admin, 'user')).toBe(true);
|
|
69
|
+
});
|
|
70
|
+
it('admin does not satisfy ops', () => {
|
|
71
|
+
expect(hasRole(admin, 'ops')).toBe(false);
|
|
72
|
+
});
|
|
73
|
+
it('ops satisfies all roles', () => {
|
|
74
|
+
expect(hasRole(ops, 'ops')).toBe(true);
|
|
75
|
+
expect(hasRole(ops, 'admin')).toBe(true);
|
|
76
|
+
expect(hasRole(ops, 'user')).toBe(true);
|
|
77
|
+
});
|
|
78
|
+
it('null user does not satisfy any role', () => {
|
|
79
|
+
expect(hasRole(null, 'user')).toBe(false);
|
|
80
|
+
expect(hasRole(null, 'admin')).toBe(false);
|
|
81
|
+
expect(hasRole(null, 'ops')).toBe(false);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
// requireRole
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
describe('requireRole', () => {
|
|
88
|
+
it('returns 401 when resolveUser returns null', async () => {
|
|
89
|
+
const res = createMockRes();
|
|
90
|
+
const middleware = requireRole(makeProvider(null), 'admin');
|
|
91
|
+
const { nextCalls } = await runMiddleware(middleware, res);
|
|
92
|
+
expect(res.status).toHaveBeenCalledWith(401);
|
|
93
|
+
expect(res.json).toHaveBeenCalledWith({
|
|
94
|
+
error: { code: 'unauthenticated', message: 'Authentication required' },
|
|
95
|
+
});
|
|
96
|
+
expect(nextCalls).toHaveLength(0);
|
|
97
|
+
});
|
|
98
|
+
it('returns 403 when user role is below minimum', async () => {
|
|
99
|
+
const res = createMockRes();
|
|
100
|
+
const middleware = requireRole(makeProvider({ id: 'u1', role: 'user' }), 'admin');
|
|
101
|
+
const { nextCalls } = await runMiddleware(middleware, res);
|
|
102
|
+
expect(res.status).toHaveBeenCalledWith(403);
|
|
103
|
+
expect(res.json).toHaveBeenCalledWith({
|
|
104
|
+
error: {
|
|
105
|
+
code: 'forbidden',
|
|
106
|
+
message: 'Requires admin role',
|
|
107
|
+
required_role: 'admin',
|
|
108
|
+
current_role: 'user',
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
expect(nextCalls).toHaveLength(0);
|
|
112
|
+
});
|
|
113
|
+
it('returns 403 when admin tries to access ops route', async () => {
|
|
114
|
+
const res = createMockRes();
|
|
115
|
+
const middleware = requireRole(makeProvider({ id: 'a1', role: 'admin' }), 'ops');
|
|
116
|
+
const { nextCalls } = await runMiddleware(middleware, res);
|
|
117
|
+
expect(res.status).toHaveBeenCalledWith(403);
|
|
118
|
+
expect(nextCalls).toHaveLength(0);
|
|
119
|
+
});
|
|
120
|
+
it('calls next() and sets res.locals.user when admin accesses admin route', async () => {
|
|
121
|
+
const user = { id: 'a1', role: 'admin' };
|
|
122
|
+
const res = createMockRes();
|
|
123
|
+
const middleware = requireRole(makeProvider(user), 'admin');
|
|
124
|
+
const { nextCalls } = await runMiddleware(middleware, res);
|
|
125
|
+
expect(res.status).not.toHaveBeenCalled();
|
|
126
|
+
expect(nextCalls).toHaveLength(1);
|
|
127
|
+
expect(nextCalls[0]).toEqual([]); // no error arg
|
|
128
|
+
expect(res.locals['user']).toEqual(user);
|
|
129
|
+
});
|
|
130
|
+
it('calls next() when ops accesses admin route', async () => {
|
|
131
|
+
const user = { id: 'o1', role: 'ops' };
|
|
132
|
+
const res = createMockRes();
|
|
133
|
+
const middleware = requireRole(makeProvider(user), 'admin');
|
|
134
|
+
const { nextCalls } = await runMiddleware(middleware, res);
|
|
135
|
+
expect(nextCalls).toHaveLength(1);
|
|
136
|
+
expect(res.locals['user']).toEqual(user);
|
|
137
|
+
});
|
|
138
|
+
it('calls next() when user accesses user-level route', async () => {
|
|
139
|
+
const user = { id: 'u1', role: 'user' };
|
|
140
|
+
const res = createMockRes();
|
|
141
|
+
const middleware = requireRole(makeProvider(user), 'user');
|
|
142
|
+
const { nextCalls } = await runMiddleware(middleware, res);
|
|
143
|
+
expect(nextCalls).toHaveLength(1);
|
|
144
|
+
expect(res.locals['user']).toEqual(user);
|
|
145
|
+
});
|
|
146
|
+
it('forwards RoleProviderError to error handler when resolveUser throws', async () => {
|
|
147
|
+
const provider = {
|
|
148
|
+
async resolveUser() {
|
|
149
|
+
throw new Error('database connection failed');
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
const res = createMockRes();
|
|
153
|
+
const middleware = requireRole(provider, 'admin');
|
|
154
|
+
const { nextCalls } = await runMiddleware(middleware, res);
|
|
155
|
+
expect(nextCalls).toHaveLength(1);
|
|
156
|
+
expect(nextCalls[0]).toHaveLength(1);
|
|
157
|
+
const err = nextCalls[0]?.[0];
|
|
158
|
+
expect(err).toBeInstanceOf(RoleProviderError);
|
|
159
|
+
expect(err.code).toBe('role_provider_failed');
|
|
160
|
+
// The original error is preserved as the cause for diagnostics
|
|
161
|
+
expect(err.cause).toBeInstanceOf(Error);
|
|
162
|
+
expect(err.cause.message).toBe('database connection failed');
|
|
163
|
+
});
|
|
164
|
+
it('does not write a response body when resolveUser throws', async () => {
|
|
165
|
+
// The middleware passes the error to next() instead of writing a response
|
|
166
|
+
// directly. The actual error sanitization happens in errorHandler.
|
|
167
|
+
const provider = {
|
|
168
|
+
async resolveUser() {
|
|
169
|
+
throw new Error('SECRET INTERNAL DETAIL');
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
const res = createMockRes();
|
|
173
|
+
const middleware = requireRole(provider, 'admin');
|
|
174
|
+
await runMiddleware(middleware, res);
|
|
175
|
+
expect(res.status).not.toHaveBeenCalled();
|
|
176
|
+
expect(res.json).not.toHaveBeenCalled();
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
//# sourceMappingURL=role-provider.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"role-provider.test.js","sourceRoot":"","sources":["../../src/role-provider.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -71,7 +71,7 @@ export async function fireDrainHooks(sessionManager, hooks, ctx) {
|
|
|
71
71
|
});
|
|
72
72
|
}
|
|
73
73
|
if (hooks?.onSessionPersist) {
|
|
74
|
-
hooks.onSessionPersist(session.id,
|
|
74
|
+
hooks.onSessionPersist(session.id, session.messages, 'completed', {
|
|
75
75
|
model: session.model,
|
|
76
76
|
provider: session.providerName,
|
|
77
77
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"route-helpers.js","sourceRoot":"","sources":["../../../src/routes/route-helpers.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAcH,8EAA8E;AAC9E,wBAAwB;AACxB,8EAA8E;AAE9E;;;;;;GAMG;AACH,MAAM,UAAU,YAAY,CAC1B,EAA0F;IAE1F,OAAO,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;QACxB,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAClD,CAAC,CAAC;AACJ,CAAC;AAED,8EAA8E;AAC9E,YAAY;AACZ,8EAA8E;AAE9E,wEAAwE;AACxE,MAAM,CAAC,MAAM,iBAAiB,GAAG,SAAS,CAAC;AAE3C,0DAA0D;AAC1D,MAAM,CAAC,MAAM,uBAAuB,GAAG,mBAAmB,CAAC;AAW3D;;;GAGG;AACH,MAAM,UAAU,YAAY,CAC1B,KAA8B,EAC9B,OAAgB;IAEhB,IAAI,CAAC,KAAK,EAAE,aAAa;QAAE,OAAO,SAAS,CAAC;IAC5C,OAAO,CAAC,KAAgB,EAAE,EAAE;QAC1B,KAAK,CAAC,aAAa,EAAE,CAAC;YACpB,KAAK,EAAE,OAAO,CAAC,KAAK;YACpB,aAAa,EAAE,CAAC;YAChB,MAAM,EAAE;gBACN,WAAW,EAAE,KAAK,CAAC,WAAW;gBAC9B,YAAY,EAAE,KAAK,CAAC,YAAY;gBAChC,YAAY,EAAE,KAAK,CAAC,iBAAiB;aACtC;SACF,CAAC,CAAC;IACL,CAAC,CAAC;AACJ,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,cAAwC,EACxC,KAA8B,EAC9B,GAAiB;IAEjB,MAAM,EAAC,OAAO,EAAE,SAAS,EAAC,GAAG,GAAG,CAAC;IAEjC,2EAA2E;IAC3E,2EAA2E;IAC3E,4EAA4E;IAC5E,MAAM,cAAc,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;IAEtC,IAAI,KAAK,EAAE,UAAU,EAAE,CAAC;QACtB,KAAK,CAAC,UAAU,CAAC;YACf,KAAK,EAAE,uBAAuB;YAC9B,aAAa,EAAE,OAAO,CAAC,EAAE;YACzB,OAAO,EAAE;gBACP,UAAU,EAAE,OAAO,CAAC,EAAE;gBACtB,UAAU,EAAE,SAAS;gBACrB,KAAK,EAAE,OAAO,CAAC,KAAK;gBACpB,QAAQ,EAAE,OAAO,CAAC,YAAY;aAC/B;SACF,CAAC,CAAC;IACL,CAAC;IAED,IAAI,KAAK,EAAE,gBAAgB,EAAE,CAAC;QAC5B,KAAK,CAAC,gBAAgB,CAAC,OAAO,CAAC,EAAE,EAAE,
|
|
1
|
+
{"version":3,"file":"route-helpers.js","sourceRoot":"","sources":["../../../src/routes/route-helpers.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAcH,8EAA8E;AAC9E,wBAAwB;AACxB,8EAA8E;AAE9E;;;;;;GAMG;AACH,MAAM,UAAU,YAAY,CAC1B,EAA0F;IAE1F,OAAO,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;QACxB,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAClD,CAAC,CAAC;AACJ,CAAC;AAED,8EAA8E;AAC9E,YAAY;AACZ,8EAA8E;AAE9E,wEAAwE;AACxE,MAAM,CAAC,MAAM,iBAAiB,GAAG,SAAS,CAAC;AAE3C,0DAA0D;AAC1D,MAAM,CAAC,MAAM,uBAAuB,GAAG,mBAAmB,CAAC;AAW3D;;;GAGG;AACH,MAAM,UAAU,YAAY,CAC1B,KAA8B,EAC9B,OAAgB;IAEhB,IAAI,CAAC,KAAK,EAAE,aAAa;QAAE,OAAO,SAAS,CAAC;IAC5C,OAAO,CAAC,KAAgB,EAAE,EAAE;QAC1B,KAAK,CAAC,aAAa,EAAE,CAAC;YACpB,KAAK,EAAE,OAAO,CAAC,KAAK;YACpB,aAAa,EAAE,CAAC;YAChB,MAAM,EAAE;gBACN,WAAW,EAAE,KAAK,CAAC,WAAW;gBAC9B,YAAY,EAAE,KAAK,CAAC,YAAY;gBAChC,YAAY,EAAE,KAAK,CAAC,iBAAiB;aACtC;SACF,CAAC,CAAC;IACL,CAAC,CAAC;AACJ,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,cAAwC,EACxC,KAA8B,EAC9B,GAAiB;IAEjB,MAAM,EAAC,OAAO,EAAE,SAAS,EAAC,GAAG,GAAG,CAAC;IAEjC,2EAA2E;IAC3E,2EAA2E;IAC3E,4EAA4E;IAC5E,MAAM,cAAc,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;IAEtC,IAAI,KAAK,EAAE,UAAU,EAAE,CAAC;QACtB,KAAK,CAAC,UAAU,CAAC;YACf,KAAK,EAAE,uBAAuB;YAC9B,aAAa,EAAE,OAAO,CAAC,EAAE;YACzB,OAAO,EAAE;gBACP,UAAU,EAAE,OAAO,CAAC,EAAE;gBACtB,UAAU,EAAE,SAAS;gBACrB,KAAK,EAAE,OAAO,CAAC,KAAK;gBACpB,QAAQ,EAAE,OAAO,CAAC,YAAY;aAC/B;SACF,CAAC,CAAC;IACL,CAAC;IAED,IAAI,KAAK,EAAE,gBAAgB,EAAE,CAAC;QAC5B,KAAK,CAAC,gBAAgB,CAAC,OAAO,CAAC,EAAE,EAAE,OAAO,CAAC,QAAQ,EAAE,WAAW,EAAE;YAChE,KAAK,EAAE,OAAO,CAAC,KAAK;YACpB,QAAQ,EAAE,OAAO,CAAC,YAAY;SAC/B,CAAC,CAAC;IACL,CAAC;AACH,CAAC"}
|
package/dist/src/server.d.ts
CHANGED
|
@@ -19,6 +19,7 @@ import type { AuthContext } from './middleware/auth.js';
|
|
|
19
19
|
import type { SessionComponents } from './session/session-builder.js';
|
|
20
20
|
import type { ServerConfig } from './types.js';
|
|
21
21
|
import { RuntimeEventBus } from './events/event-bus.js';
|
|
22
|
+
import { type RoleProvider } from './role-provider.js';
|
|
22
23
|
export interface ServerInstance {
|
|
23
24
|
app: Express;
|
|
24
25
|
start: () => Promise<http.Server>;
|
|
@@ -35,6 +36,19 @@ export interface CreateServerOptions {
|
|
|
35
36
|
fallbackMiddleware?: express.RequestHandler;
|
|
36
37
|
/** Auth middleware for protected routes (injected by hosting layer) */
|
|
37
38
|
authMiddleware?: express.RequestHandler;
|
|
39
|
+
/**
|
|
40
|
+
* RoleProvider for role-gated routes (config editing, admin actions, etc).
|
|
41
|
+
*
|
|
42
|
+
* Defaults to `defaultRoleProvider` which returns `ops` for all requests —
|
|
43
|
+
* appropriate for `amodal dev` where the developer is the only user.
|
|
44
|
+
*
|
|
45
|
+
* Hosting layers (cloud, self-hosted) should provide their own implementation
|
|
46
|
+
* that maps the request's auth context to a `user`/`admin`/`ops` role.
|
|
47
|
+
*
|
|
48
|
+
* The runtime exposes the resolved role at `GET /api/me` and uses it to
|
|
49
|
+
* gate config-editing routes via the `requireRole` middleware.
|
|
50
|
+
*/
|
|
51
|
+
roleProvider?: RoleProvider;
|
|
38
52
|
/** Additional routers to mount (e.g., session history proxy) */
|
|
39
53
|
additionalRouters?: express.Router[];
|
|
40
54
|
/** Factory that builds per-request stream hooks from the auth context */
|
|
@@ -75,6 +89,8 @@ export interface CreateServerOptions {
|
|
|
75
89
|
};
|
|
76
90
|
/** Event bus for channel lifecycle events. If omitted, a minimal one is created. */
|
|
77
91
|
channelEventBus?: RuntimeEventBus;
|
|
92
|
+
/** Optional session store for persisting sessions across restarts. */
|
|
93
|
+
sessionStore?: import('./session/store.js').SessionStore;
|
|
78
94
|
}
|
|
79
95
|
/**
|
|
80
96
|
* Create the Express server with all routes, session management, and graceful shutdown.
|
package/dist/src/server.js
CHANGED
|
@@ -19,6 +19,8 @@ import { RuntimeEventBus } from './events/event-bus.js';
|
|
|
19
19
|
import { createChannelsRouter } from './channels/routes.js';
|
|
20
20
|
import { MessageDedupCache } from './channels/dedup-cache.js';
|
|
21
21
|
import { log, createLogger } from './logger.js';
|
|
22
|
+
import { defaultRoleProvider } from './role-provider.js';
|
|
23
|
+
import { asyncHandler } from './routes/route-helpers.js';
|
|
22
24
|
/**
|
|
23
25
|
* Create the Express server with all routes, session management, and graceful shutdown.
|
|
24
26
|
*/
|
|
@@ -30,6 +32,7 @@ export function createServer(options) {
|
|
|
30
32
|
const sessionManager = new StandaloneSessionManager({
|
|
31
33
|
logger: sessionLogger,
|
|
32
34
|
ttlMs: config.sessionTtlMs,
|
|
35
|
+
...(options.sessionStore ? { store: options.sessionStore } : {}),
|
|
33
36
|
});
|
|
34
37
|
sessionManager.start();
|
|
35
38
|
const shared = {
|
|
@@ -66,6 +69,24 @@ export function createServer(options) {
|
|
|
66
69
|
active_sessions: sessionManager.size,
|
|
67
70
|
});
|
|
68
71
|
});
|
|
72
|
+
// RoleProvider — defaults to "everyone is ops" for amodal dev / backwards compat.
|
|
73
|
+
// Hosting layers inject their own provider via createServer options.
|
|
74
|
+
const roleProvider = options.roleProvider ?? defaultRoleProvider;
|
|
75
|
+
// GET /api/me — returns the current user's role.
|
|
76
|
+
// Used by the runtime-app frontend to decide which nav items / pages to show.
|
|
77
|
+
// Returns 401 if unauthenticated, otherwise { id, role }.
|
|
78
|
+
app.get('/api/me', asyncHandler(async (req, res) => {
|
|
79
|
+
const user = await roleProvider.resolveUser(req);
|
|
80
|
+
if (!user) {
|
|
81
|
+
log.warn('api_me_unauthenticated', { path: req.path });
|
|
82
|
+
res.status(401).json({
|
|
83
|
+
error: { code: 'unauthenticated', message: 'Authentication required' },
|
|
84
|
+
});
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
log.debug('api_me_resolved', { user_id: user.id, role: user.role });
|
|
88
|
+
res.json(user);
|
|
89
|
+
}));
|
|
69
90
|
// Auth middleware for protected routes (injected by hosting layer)
|
|
70
91
|
if (options.authMiddleware) {
|
|
71
92
|
app.use('/chat', options.authMiddleware);
|
package/dist/src/server.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/server.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH;;;;;;GAMG;AAEH,OAAO,OAAO,MAAM,SAAS,CAAC;AAI9B,OAAO,EAAC,YAAY,EAAC,MAAM,+BAA+B,CAAC;AAC3D,OAAO,EAAC,sBAAsB,EAAC,MAAM,yBAAyB,CAAC;AAC/D,OAAO,EAAC,oBAAoB,EAAC,MAAM,uBAAuB,CAAC;AAC3D,OAAO,EAAC,wBAAwB,EAAC,MAAM,sBAAsB,CAAC;AAK9D,OAAO,EAAC,eAAe,EAAC,MAAM,uBAAuB,CAAC;AACtD,OAAO,EAAC,oBAAoB,EAAC,MAAM,sBAAsB,CAAC;AAC1D,OAAO,EAAC,iBAAiB,EAAC,MAAM,2BAA2B,CAAC;AAC5D,OAAO,EAAC,GAAG,EAAE,YAAY,EAAC,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/server.ts"],"names":[],"mappings":"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"}
|
package/dist/src/server.test.js
CHANGED
|
@@ -6,6 +6,19 @@
|
|
|
6
6
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
7
7
|
import request from 'supertest';
|
|
8
8
|
import { createServer } from './server.js';
|
|
9
|
+
const ROLE_TEST_CONFIG = {
|
|
10
|
+
port: 0,
|
|
11
|
+
host: '127.0.0.1',
|
|
12
|
+
sessionTtlMs: 30_000,
|
|
13
|
+
automations: [],
|
|
14
|
+
};
|
|
15
|
+
function makeRoleProvider(user) {
|
|
16
|
+
return {
|
|
17
|
+
async resolveUser() {
|
|
18
|
+
return user;
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
}
|
|
9
22
|
describe('createServer', () => {
|
|
10
23
|
let serverInstance;
|
|
11
24
|
beforeEach(() => {
|
|
@@ -47,4 +60,75 @@ describe('createServer', () => {
|
|
|
47
60
|
expect(res.status).toBe(404);
|
|
48
61
|
});
|
|
49
62
|
});
|
|
63
|
+
describe('createServer - /api/me', () => {
|
|
64
|
+
let serverInstance;
|
|
65
|
+
afterEach(async () => {
|
|
66
|
+
if (serverInstance)
|
|
67
|
+
await serverInstance.stop();
|
|
68
|
+
});
|
|
69
|
+
it('returns ops user by default (no roleProvider configured)', async () => {
|
|
70
|
+
serverInstance = createServer({
|
|
71
|
+
config: ROLE_TEST_CONFIG,
|
|
72
|
+
version: 'test',
|
|
73
|
+
});
|
|
74
|
+
const res = await request(serverInstance.app).get('/api/me');
|
|
75
|
+
expect(res.status).toBe(200);
|
|
76
|
+
expect(res.body).toEqual({ id: 'local-dev', role: 'ops' });
|
|
77
|
+
});
|
|
78
|
+
it('returns user from configured RoleProvider', async () => {
|
|
79
|
+
serverInstance = createServer({
|
|
80
|
+
config: ROLE_TEST_CONFIG,
|
|
81
|
+
roleProvider: makeRoleProvider({ id: 'sally@acme.com', role: 'admin' }),
|
|
82
|
+
});
|
|
83
|
+
const res = await request(serverInstance.app).get('/api/me');
|
|
84
|
+
expect(res.status).toBe(200);
|
|
85
|
+
expect(res.body).toEqual({ id: 'sally@acme.com', role: 'admin' });
|
|
86
|
+
});
|
|
87
|
+
it('returns 401 when RoleProvider returns null', async () => {
|
|
88
|
+
serverInstance = createServer({
|
|
89
|
+
config: ROLE_TEST_CONFIG,
|
|
90
|
+
roleProvider: makeRoleProvider(null),
|
|
91
|
+
});
|
|
92
|
+
const res = await request(serverInstance.app).get('/api/me');
|
|
93
|
+
expect(res.status).toBe(401);
|
|
94
|
+
expect(res.body).toEqual({
|
|
95
|
+
error: { code: 'unauthenticated', message: 'Authentication required' },
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
it('returns 500 when RoleProvider throws', async () => {
|
|
99
|
+
serverInstance = createServer({
|
|
100
|
+
config: ROLE_TEST_CONFIG,
|
|
101
|
+
roleProvider: {
|
|
102
|
+
async resolveUser() {
|
|
103
|
+
throw new Error('database connection failed');
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
const res = await request(serverInstance.app).get('/api/me');
|
|
108
|
+
expect(res.status).toBe(500);
|
|
109
|
+
expect(res.body).toHaveProperty('error');
|
|
110
|
+
});
|
|
111
|
+
it('passes the request through to the provider', async () => {
|
|
112
|
+
let capturedHeader;
|
|
113
|
+
serverInstance = createServer({
|
|
114
|
+
config: ROLE_TEST_CONFIG,
|
|
115
|
+
roleProvider: {
|
|
116
|
+
async resolveUser(req) {
|
|
117
|
+
const auth = req.headers['authorization'];
|
|
118
|
+
capturedHeader = typeof auth === 'string' ? auth : undefined;
|
|
119
|
+
if (capturedHeader === 'Bearer test-token') {
|
|
120
|
+
return { id: 'test-user', role: 'ops' };
|
|
121
|
+
}
|
|
122
|
+
return null;
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
const res = await request(serverInstance.app)
|
|
127
|
+
.get('/api/me')
|
|
128
|
+
.set('Authorization', 'Bearer test-token');
|
|
129
|
+
expect(res.status).toBe(200);
|
|
130
|
+
expect(capturedHeader).toBe('Bearer test-token');
|
|
131
|
+
expect(res.body).toEqual({ id: 'test-user', role: 'ops' });
|
|
132
|
+
});
|
|
133
|
+
});
|
|
50
134
|
//# sourceMappingURL=server.test.js.map
|