streamby-core 0.36.0 → 0.37.0
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/CHANGELOG.md +34 -0
- package/README.md +80 -44
- package/dist/adapters/database/connectionManager.js +24 -11
- package/dist/index.d.ts +2 -1
- package/dist/index.js +4 -1
- package/dist/mcp/server.d.ts +4 -3
- package/dist/mcp/server.js +13 -10
- package/dist/mcp/tools.d.ts +5 -2
- package/dist/mcp/tools.js +83 -24
- package/dist/middleware/routes/apiToken.js +77 -14
- package/dist/middleware/routes/member.js +8 -0
- package/dist/middleware/routes/project.js +8 -2
- package/dist/middleware/routes/subscription.js +3 -0
- package/dist/services/apiTokenPermissions.d.ts +16 -0
- package/dist/services/apiTokenPermissions.js +98 -0
- package/dist/services/apiTokens.d.ts +38 -14
- package/dist/services/apiTokens.js +134 -40
- package/dist/services/auth.d.ts +4 -1
- package/dist/services/auth.js +24 -18
- package/dist/types/index.d.ts +5 -2
- package/dist/utils/auth.d.ts +12 -1
- package/dist/utils/auth.js +23 -2
- package/package.json +1 -1
package/dist/services/auth.d.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { Request, Response, NextFunction } from 'express';
|
|
2
2
|
import { Auth, StreamByConfig } from '../types';
|
|
3
|
+
import { ApiTokenPermission } from './apiTokenPermissions';
|
|
3
4
|
export declare const isPublicRoute: (method: string, path: string) => boolean;
|
|
4
|
-
/** Whether a token
|
|
5
|
+
/** Whether a token reaches a project. */
|
|
5
6
|
export declare const scopeReachesProject: (scope: Auth["apiToken"], projectId: string) => boolean;
|
|
7
|
+
/** Whether a token has a permission (a request without a token: always). */
|
|
8
|
+
export declare const tokenAllows: (scope: Auth["apiToken"], permission: ApiTokenPermission) => boolean;
|
|
6
9
|
export declare const enforceApiTokenScope: (req: Request, res: Response, next: NextFunction) => void | Response<any, Record<string, any>>;
|
|
7
10
|
export declare const authenticate: (config: StreamByConfig) => (req: Request, res: Response, next: NextFunction) => Promise<void | Response<any, Record<string, any>>>;
|
package/dist/services/auth.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.authenticate = exports.enforceApiTokenScope = exports.scopeReachesProject = exports.isPublicRoute = void 0;
|
|
3
|
+
exports.authenticate = exports.enforceApiTokenScope = exports.tokenAllows = exports.scopeReachesProject = exports.isPublicRoute = void 0;
|
|
4
|
+
const auth_1 = require("../utils/auth");
|
|
5
|
+
const apiTokenPermissions_1 = require("./apiTokenPermissions");
|
|
4
6
|
// The one route open without a session: a project's public export, read
|
|
5
7
|
// (GET /:projectId/export/:exportName, see routes/export). Exactly that path -- not any path
|
|
6
8
|
// that has an "export" segment somewhere.
|
|
@@ -9,10 +11,11 @@ const READ_METHODS = ['GET', 'HEAD', 'OPTIONS'];
|
|
|
9
11
|
const isPublicRoute = (method, path) => READ_METHODS.includes(method.toUpperCase()) && PUBLIC_EXPORT.test(path);
|
|
10
12
|
exports.isPublicRoute = isPublicRoute;
|
|
11
13
|
// ── API tokens ──
|
|
12
|
-
// A request made with an API token reaches only what the token
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
14
|
+
// A request made with an API token reaches only what the token does: its route's permission
|
|
15
|
+
// must be among the token's (see apiTokenPermissions), and a project outside its projects is as
|
|
16
|
+
// if it didn't exist. A user's token is also limited by its user's role in each project (the
|
|
17
|
+
// routes' own checks); a project's token acts in its project as an admin, so its permissions
|
|
18
|
+
// are all it has.
|
|
16
19
|
// The project a route acts on, by its path.
|
|
17
20
|
const PROJECT_PATHS = [/^\/projects\/([^/]+)/, /^\/(?:upload|delete)-project-image-url\/([^/]+)/];
|
|
18
21
|
const NOT_A_PROJECT = new Set(['create', 'explore']);
|
|
@@ -24,11 +27,12 @@ const projectIdOf = (path) => {
|
|
|
24
27
|
}
|
|
25
28
|
return undefined;
|
|
26
29
|
};
|
|
27
|
-
|
|
28
|
-
const OUTSIDE_A_PROJECT = [/^\/auth\/?$/, /^\/projects\/?$/, /^\/jobs\/[^/]+\/?$/, /^\/user\/subscription\/?$/, /^\/users\/search\/?$/];
|
|
29
|
-
/** Whether a token's scope reaches a project. */
|
|
30
|
+
/** Whether a token reaches a project. */
|
|
30
31
|
const scopeReachesProject = (scope, projectId) => !scope || scope.projectIds === '*' || scope.projectIds.includes(projectId);
|
|
31
32
|
exports.scopeReachesProject = scopeReachesProject;
|
|
33
|
+
/** Whether a token has a permission (a request without a token: always). */
|
|
34
|
+
const tokenAllows = (scope, permission) => !scope || scope.permissions.includes(permission);
|
|
35
|
+
exports.tokenAllows = tokenAllows;
|
|
32
36
|
const enforceApiTokenScope = (req, res, next) => {
|
|
33
37
|
const scope = req.auth?.apiToken;
|
|
34
38
|
if (!scope)
|
|
@@ -36,19 +40,17 @@ const enforceApiTokenScope = (req, res, next) => {
|
|
|
36
40
|
// The MCP server handles a token itself: its tools call the routes here, each one checked.
|
|
37
41
|
if (req.path === '/mcp')
|
|
38
42
|
return next();
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
if (!READ_METHODS.includes(req.method.toUpperCase()) && scope.access !== 'write') {
|
|
43
|
-
return res.status(403).json({ error: 'This API token can only read' });
|
|
43
|
+
const rule = (0, apiTokenPermissions_1.routeRuleFor)(req.method, req.path);
|
|
44
|
+
if (!rule || (rule.userTokenOnly && scope.kind !== 'user')) {
|
|
45
|
+
return res.status(403).json({ error: 'Not available with an API token' });
|
|
44
46
|
}
|
|
45
47
|
const projectId = projectIdOf(req.path);
|
|
46
|
-
if (projectId)
|
|
47
|
-
return
|
|
48
|
+
if (projectId && !(0, exports.scopeReachesProject)(scope, projectId))
|
|
49
|
+
return res.status(404).json({ message: 'Project not found' });
|
|
50
|
+
if (rule.permission && !(0, exports.tokenAllows)(scope, rule.permission)) {
|
|
51
|
+
return res.status(403).json({ error: `This API token doesn't have the '${rule.permission}' permission` });
|
|
48
52
|
}
|
|
49
|
-
|
|
50
|
-
return next();
|
|
51
|
-
return res.status(403).json({ error: 'Not available with an API token' });
|
|
53
|
+
next();
|
|
52
54
|
};
|
|
53
55
|
exports.enforceApiTokenScope = enforceApiTokenScope;
|
|
54
56
|
const authenticate = (config) => async (req, res, next) => {
|
|
@@ -60,6 +62,10 @@ const authenticate = (config) => async (req, res, next) => {
|
|
|
60
62
|
if (!auth || !auth.userId || !auth.role) {
|
|
61
63
|
return res.status(401).json({ error: 'Unauthorized' });
|
|
62
64
|
}
|
|
65
|
+
// A project token's principal is only ever a project's token (see utils/auth).
|
|
66
|
+
if ((0, auth_1.isProjectTokenPrincipal)(auth.userId) && auth.apiToken?.kind !== 'project') {
|
|
67
|
+
return res.status(401).json({ error: 'Unauthorized' });
|
|
68
|
+
}
|
|
63
69
|
req.auth = auth;
|
|
64
70
|
next();
|
|
65
71
|
}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ApiTokenPermission, ProjectPermission, AccountPermission } from '../services/apiTokenPermissions';
|
|
1
2
|
import { Request } from 'express';
|
|
2
3
|
import type { WebSocketServer } from 'ws';
|
|
3
4
|
export interface S3Config {
|
|
@@ -167,10 +168,12 @@ export interface Auth {
|
|
|
167
168
|
role: 'viewer' | 'editor' | 'admin';
|
|
168
169
|
apiToken?: ApiTokenScope;
|
|
169
170
|
}
|
|
170
|
-
export type
|
|
171
|
+
export type { ApiTokenPermission, ProjectPermission, AccountPermission };
|
|
172
|
+
export type ApiTokenKind = 'user' | 'project';
|
|
171
173
|
export interface ApiTokenScope {
|
|
172
174
|
tokenId: string;
|
|
173
|
-
|
|
175
|
+
kind: ApiTokenKind;
|
|
176
|
+
permissions: ApiTokenPermission[];
|
|
174
177
|
projectIds: string[] | '*';
|
|
175
178
|
}
|
|
176
179
|
export type AuthProvider = (req: Request) => Promise<Auth>;
|
package/dist/utils/auth.d.ts
CHANGED
|
@@ -1,10 +1,21 @@
|
|
|
1
1
|
import { ProjectInfo } from '../types';
|
|
2
|
-
type ProjectMembers = Pick<ProjectInfo, 'members'>
|
|
2
|
+
type ProjectMembers = (Pick<ProjectInfo, 'members'> & {
|
|
3
|
+
_id?: unknown;
|
|
4
|
+
id?: unknown;
|
|
5
|
+
}) | null | undefined;
|
|
6
|
+
export declare const projectTokenPrincipal: (tokenId: string, projectId: string) => string;
|
|
7
|
+
export declare const isProjectTokenPrincipal: (userId: unknown) => boolean;
|
|
3
8
|
export declare function activeMemberOf(project: ProjectMembers, userId: string | undefined): {
|
|
4
9
|
role: "viewer" | "editor" | "admin";
|
|
5
10
|
userId: string;
|
|
6
11
|
archived?: boolean;
|
|
7
12
|
status?: "active" | "pending";
|
|
13
|
+
} | {
|
|
14
|
+
userId: string;
|
|
15
|
+
username: string;
|
|
16
|
+
role: "admin";
|
|
17
|
+
status: "active";
|
|
18
|
+
archived: boolean;
|
|
8
19
|
} | undefined;
|
|
9
20
|
export declare const projectRoleOf: (project: ProjectMembers, userId: string | undefined) => "viewer" | "editor" | "admin" | undefined;
|
|
10
21
|
/** An active member, whatever their role: can see the project. */
|
package/dist/utils/auth.js
CHANGED
|
@@ -1,11 +1,32 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.isProjectAdmin = exports.canEditProject = exports.isProjectMember = exports.projectRoleOf = void 0;
|
|
3
|
+
exports.isProjectAdmin = exports.canEditProject = exports.isProjectMember = exports.projectRoleOf = exports.isProjectTokenPrincipal = exports.projectTokenPrincipal = void 0;
|
|
4
4
|
exports.activeMemberOf = activeMemberOf;
|
|
5
|
+
// A project's API token acts on its project as itself, not as a user: its request's userId is
|
|
6
|
+
// this principal, naming the token and its project. In that project it's an admin -- what it
|
|
7
|
+
// may do is only its permissions (see enforceApiTokenScope) -- and in any other, nobody.
|
|
8
|
+
const PROJECT_TOKEN_PRINCIPAL = 'project-token:';
|
|
9
|
+
const projectTokenPrincipal = (tokenId, projectId) => `${PROJECT_TOKEN_PRINCIPAL}${tokenId}:${projectId}`;
|
|
10
|
+
exports.projectTokenPrincipal = projectTokenPrincipal;
|
|
11
|
+
const isProjectTokenPrincipal = (userId) => typeof userId === 'string' && userId.startsWith(PROJECT_TOKEN_PRINCIPAL);
|
|
12
|
+
exports.isProjectTokenPrincipal = isProjectTokenPrincipal;
|
|
13
|
+
const projectOfPrincipal = (userId) => {
|
|
14
|
+
if (!(0, exports.isProjectTokenPrincipal)(userId))
|
|
15
|
+
return undefined;
|
|
16
|
+
const rest = userId.slice(PROJECT_TOKEN_PRINCIPAL.length);
|
|
17
|
+
return rest.slice(rest.indexOf(':') + 1) || undefined;
|
|
18
|
+
};
|
|
5
19
|
// A project's member who's in it now: invited and accepted (status 'active'). (A member's
|
|
6
20
|
// `archived` is only that member having archived the project in their own list.)
|
|
7
21
|
function activeMemberOf(project, userId) {
|
|
8
|
-
if (!project
|
|
22
|
+
if (!project || !userId)
|
|
23
|
+
return undefined;
|
|
24
|
+
const tokenProject = projectOfPrincipal(userId);
|
|
25
|
+
if (tokenProject !== undefined) {
|
|
26
|
+
const id = String(project._id ?? project.id ?? '');
|
|
27
|
+
return id && id === tokenProject ? { userId, username: 'API token', role: 'admin', status: 'active', archived: false } : undefined;
|
|
28
|
+
}
|
|
29
|
+
if (!project.members)
|
|
9
30
|
return undefined;
|
|
10
31
|
return project.members.find(member => member.userId?.toString() === userId && member.status === 'active');
|
|
11
32
|
}
|