streamby-core 0.36.0 → 0.38.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 +97 -0
- package/README.md +141 -44
- package/dist/adapters/database/connectionManager.js +24 -11
- package/dist/adapters/storage/s3.d.ts +3 -2
- package/dist/adapters/storage/s3.js +18 -5
- 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 +25 -10
- package/dist/mcp/tools.d.ts +7 -2
- package/dist/mcp/tools.js +187 -35
- package/dist/mcp/workflowNodes.d.ts +24 -0
- package/dist/mcp/workflowNodes.js +48 -0
- package/dist/middleware/createRouter.js +2 -0
- package/dist/middleware/routes/apiToken.js +77 -14
- package/dist/middleware/routes/deliverable.d.ts +3 -0
- package/dist/middleware/routes/deliverable.js +131 -0
- package/dist/middleware/routes/export.js +2 -0
- package/dist/middleware/routes/member.js +8 -0
- package/dist/middleware/routes/production.js +60 -67
- package/dist/middleware/routes/project.js +15 -3
- package/dist/middleware/routes/storage.js +115 -24
- package/dist/middleware/routes/subscription.js +3 -0
- package/dist/services/apiTokenPermissions.d.ts +16 -0
- package/dist/services/apiTokenPermissions.js +102 -0
- package/dist/services/apiTokens.d.ts +38 -14
- package/dist/services/apiTokens.js +134 -40
- package/dist/services/assetVersions.d.ts +48 -0
- package/dist/services/assetVersions.js +84 -0
- package/dist/services/auth.d.ts +4 -1
- package/dist/services/auth.js +24 -18
- package/dist/services/deliverables.d.ts +26 -0
- package/dist/services/deliverables.js +55 -0
- package/dist/services/mediaProcessor.js +3 -1
- package/dist/services/production.d.ts +2 -0
- package/dist/services/production.js +13 -0
- package/dist/services/productionFields.d.ts +16 -0
- package/dist/services/productionFields.js +80 -0
- package/dist/services/signedUrls.d.ts +6 -0
- package/dist/services/signedUrls.js +9 -0
- package/dist/types/index.d.ts +12 -4
- package/dist/utils/auth.d.ts +12 -1
- package/dist/utils/auth.js +23 -2
- package/dist/utils/nosqlDb.d.ts +2 -0
- package/dist/utils/nosqlDb.js +11 -0
- package/package.json +1 -1
|
@@ -5,17 +5,27 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.ApiTokenError = exports.API_TOKEN_EXPIRY_DAYS = exports.API_TOKEN_PREFIX = void 0;
|
|
7
7
|
exports.createApiToken = createApiToken;
|
|
8
|
-
exports.
|
|
9
|
-
exports.
|
|
8
|
+
exports.listUserApiTokens = listUserApiTokens;
|
|
9
|
+
exports.listProjectApiTokens = listProjectApiTokens;
|
|
10
|
+
exports.revokeUserApiToken = revokeUserApiToken;
|
|
11
|
+
exports.revokeProjectApiToken = revokeProjectApiToken;
|
|
12
|
+
exports.projectApiTokensMadeBy = projectApiTokensMadeBy;
|
|
13
|
+
exports.deleteProjectApiTokens = deleteProjectApiTokens;
|
|
14
|
+
exports.addProjectToApiToken = addProjectToApiToken;
|
|
10
15
|
exports.resolveApiToken = resolveApiToken;
|
|
11
16
|
const crypto_1 = __importDefault(require("crypto"));
|
|
12
17
|
const manager_1 = require("../models/manager");
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
18
|
+
const auth_1 = require("../utils/auth");
|
|
19
|
+
const apiTokenPermissions_1 = require("./apiTokenPermissions");
|
|
20
|
+
// API tokens: how an agent or a service works with StreamBy without a browser session. A token
|
|
21
|
+
// is "sbt_" and 32 random bytes; only its hash is stored, with a prefix to tell it apart in a
|
|
22
|
+
// list. It's sent as `Authorization: Bearer sbt_...` and the host's authProvider resolves it
|
|
23
|
+
// (resolveApiToken). It does only what its permissions allow (see apiTokenPermissions), and:
|
|
24
|
+
//
|
|
25
|
+
// - a user's token acts as its user, on the projects it reaches: never more than its user can,
|
|
26
|
+
// since their role in each project still decides;
|
|
27
|
+
// - a project's token acts on its project alone, as the project's, not as whoever made it: it
|
|
28
|
+
// keeps working after they leave the project. Only the project's admins manage it.
|
|
19
29
|
exports.API_TOKEN_PREFIX = 'sbt_';
|
|
20
30
|
const PREFIX_SHOWN = exports.API_TOKEN_PREFIX.length + 8;
|
|
21
31
|
exports.API_TOKEN_EXPIRY_DAYS = [7, 30, 90, 365];
|
|
@@ -28,35 +38,72 @@ class ApiTokenError extends Error {
|
|
|
28
38
|
}
|
|
29
39
|
exports.ApiTokenError = ApiTokenError;
|
|
30
40
|
const hashOf = (token) => crypto_1.default.createHash('sha256').update(token).digest('hex');
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
projectIds: parseProjectIds(info.projectIds),
|
|
34
|
-
});
|
|
35
|
-
// As stored (a list, read back from SQL JSON as a list or its text): ['*'] is every project.
|
|
36
|
-
const parseProjectIds = (value) => {
|
|
41
|
+
// A list, as stored in SQL JSON: read back as a list or as its text.
|
|
42
|
+
const listOf = (value) => {
|
|
37
43
|
if (typeof value === 'string') {
|
|
38
44
|
try {
|
|
39
|
-
return
|
|
45
|
+
return listOf(JSON.parse(value));
|
|
40
46
|
}
|
|
41
47
|
catch {
|
|
42
48
|
return [];
|
|
43
49
|
}
|
|
44
50
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
51
|
+
return Array.isArray(value) ? value.map(String) : [];
|
|
52
|
+
};
|
|
53
|
+
const parseProjectIds = (value) => {
|
|
54
|
+
const ids = listOf(value);
|
|
48
55
|
return ids.includes(ALL_PROJECTS) ? '*' : ids;
|
|
49
56
|
};
|
|
57
|
+
// Only the permissions its kind may have: a stored one that isn't known anymore is dropped.
|
|
58
|
+
const parsePermissions = (value, kind) => listOf(value).filter((p) => (0, apiTokenPermissions_1.isProjectPermission)(p) || (kind === 'user' && (0, apiTokenPermissions_1.isAccountPermission)(p)));
|
|
59
|
+
// Tokens made before they had a kind and permissions don't work anymore, nor are listed.
|
|
60
|
+
const isCurrent = (record) => !!record && (record.kind === 'user' || record.kind === 'project');
|
|
61
|
+
const infoOf = (record) => {
|
|
62
|
+
const { hash: _hash, userId, createdByName, projectIds, permissions, ...info } = record;
|
|
63
|
+
return {
|
|
64
|
+
...info,
|
|
65
|
+
permissions: parsePermissions(permissions, record.kind),
|
|
66
|
+
projectIds: parseProjectIds(projectIds),
|
|
67
|
+
createdBy: { userId, username: createdByName ?? null },
|
|
68
|
+
};
|
|
69
|
+
};
|
|
50
70
|
const mainDbType = (config) => config.databases?.find(db => db.main)?.type ?? 'nosql';
|
|
51
|
-
|
|
52
|
-
|
|
71
|
+
const validPermissions = (value, kind) => {
|
|
72
|
+
if (!Array.isArray(value) || !value.length)
|
|
73
|
+
throw new ApiTokenError('A token needs at least one permission.');
|
|
74
|
+
for (const p of value) {
|
|
75
|
+
if ((0, apiTokenPermissions_1.isAccountPermission)(p) && kind === 'project')
|
|
76
|
+
throw new ApiTokenError(`A project's token can't have '${p}'.`);
|
|
77
|
+
if (!(0, apiTokenPermissions_1.isProjectPermission)(p) && !(0, apiTokenPermissions_1.isAccountPermission)(p))
|
|
78
|
+
throw new ApiTokenError(`Unknown permission '${String(p)}'.`);
|
|
79
|
+
}
|
|
80
|
+
return [...new Set(value)];
|
|
81
|
+
};
|
|
82
|
+
/** A new token: the token itself, shown only now, and what's kept of it. */
|
|
83
|
+
async function createApiToken(owner, input, config) {
|
|
53
84
|
const name = typeof input.name === 'string' ? input.name.trim() : '';
|
|
54
85
|
if (!name)
|
|
55
86
|
throw new ApiTokenError('A token needs a name.');
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if (
|
|
59
|
-
|
|
87
|
+
const permissions = validPermissions(input.permissions, owner.kind);
|
|
88
|
+
let projectIds;
|
|
89
|
+
if (owner.kind === 'project') {
|
|
90
|
+
if (!owner.projectId)
|
|
91
|
+
throw new ApiTokenError("A project's token needs its project.");
|
|
92
|
+
projectIds = [owner.projectId];
|
|
93
|
+
}
|
|
94
|
+
else if (input.projectIds === '*') {
|
|
95
|
+
projectIds = [ALL_PROJECTS];
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
const ids = input.projectIds;
|
|
99
|
+
if (!Array.isArray(ids) || !ids.every(id => typeof id === 'string' && id && id !== ALL_PROJECTS)) {
|
|
100
|
+
throw new ApiTokenError("projectIds must be '*' or a list of project ids.");
|
|
101
|
+
}
|
|
102
|
+
// No project yet only for a token that makes them: it reaches the ones it makes.
|
|
103
|
+
if (!ids.length && !permissions.includes('projects:create')) {
|
|
104
|
+
throw new ApiTokenError('A token needs at least one project, or to be able to create them.');
|
|
105
|
+
}
|
|
106
|
+
projectIds = [...new Set(ids)];
|
|
60
107
|
}
|
|
61
108
|
if (input.expiresInDays !== null && !exports.API_TOKEN_EXPIRY_DAYS.includes(input.expiresInDays)) {
|
|
62
109
|
throw new ApiTokenError(`expiresInDays must be one of ${exports.API_TOKEN_EXPIRY_DAYS.join(', ')}, or null.`);
|
|
@@ -65,12 +112,14 @@ async function createApiToken(userId, input, config) {
|
|
|
65
112
|
const now = new Date();
|
|
66
113
|
const record = {
|
|
67
114
|
id: crypto_1.default.randomUUID(),
|
|
68
|
-
|
|
115
|
+
kind: owner.kind,
|
|
116
|
+
userId: owner.userId,
|
|
117
|
+
createdByName: owner.username ?? null,
|
|
69
118
|
name,
|
|
70
119
|
prefix: token.slice(0, PREFIX_SHOWN),
|
|
71
120
|
hash: hashOf(token),
|
|
72
|
-
|
|
73
|
-
projectIds
|
|
121
|
+
permissions,
|
|
122
|
+
projectIds,
|
|
74
123
|
createdAt: now,
|
|
75
124
|
expiresAt: input.expiresInDays === null ? null : new Date(now.getTime() + input.expiresInDays * 86400000),
|
|
76
125
|
lastUsedAt: null,
|
|
@@ -78,33 +127,78 @@ async function createApiToken(userId, input, config) {
|
|
|
78
127
|
await (0, manager_1.getModel)('api_tokens').create({ ...record, dbType: mainDbType(config) });
|
|
79
128
|
return { token, info: infoOf(record) };
|
|
80
129
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
130
|
+
const newestFirst = (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
|
|
131
|
+
/** A user's own tokens. */
|
|
132
|
+
async function listUserApiTokens(userId) {
|
|
133
|
+
const records = (await (0, manager_1.getModel)('api_tokens').find({ userId, kind: 'user' }));
|
|
134
|
+
return records.filter(isCurrent).map(infoOf).sort(newestFirst);
|
|
135
|
+
}
|
|
136
|
+
/** A project's tokens. */
|
|
137
|
+
async function listProjectApiTokens(projectId) {
|
|
138
|
+
const records = (await (0, manager_1.getModel)('api_tokens').find({ kind: 'project' }));
|
|
84
139
|
return records
|
|
140
|
+
.filter(r => isCurrent(r) && listOf(r.projectIds)[0] === projectId)
|
|
85
141
|
.map(infoOf)
|
|
86
|
-
.sort(
|
|
142
|
+
.sort(newestFirst);
|
|
143
|
+
}
|
|
144
|
+
/** Revokes a user's own token: it stops working at once. Whether there was one. */
|
|
145
|
+
async function revokeUserApiToken(userId, tokenId) {
|
|
146
|
+
return (await (0, manager_1.getModel)('api_tokens').delete({ id: tokenId, userId, kind: 'user' })) > 0;
|
|
147
|
+
}
|
|
148
|
+
/** Revokes a project's token. Whether there was one. */
|
|
149
|
+
async function revokeProjectApiToken(projectId, tokenId) {
|
|
150
|
+
const ApiTokens = (0, manager_1.getModel)('api_tokens');
|
|
151
|
+
const record = (await ApiTokens.findOne({ id: tokenId, kind: 'project' }));
|
|
152
|
+
if (!record || listOf(record.projectIds)[0] !== projectId)
|
|
153
|
+
return false;
|
|
154
|
+
return (await ApiTokens.delete({ id: tokenId })) > 0;
|
|
155
|
+
}
|
|
156
|
+
/** A project's tokens that `userId` made (to tell its admins they keep working once they leave). */
|
|
157
|
+
async function projectApiTokensMadeBy(projectId, userId) {
|
|
158
|
+
return (await listProjectApiTokens(projectId)).filter(t => t.createdBy.userId === userId);
|
|
159
|
+
}
|
|
160
|
+
/** Ends a deleted project's tokens. */
|
|
161
|
+
async function deleteProjectApiTokens(projectId) {
|
|
162
|
+
const ApiTokens = (0, manager_1.getModel)('api_tokens');
|
|
163
|
+
for (const token of await listProjectApiTokens(projectId))
|
|
164
|
+
await ApiTokens.delete({ id: token.id });
|
|
87
165
|
}
|
|
88
|
-
/**
|
|
89
|
-
async function
|
|
90
|
-
|
|
166
|
+
/** A project a user's token made: it reaches it from now on (one reaching every project already does). */
|
|
167
|
+
async function addProjectToApiToken(tokenId, projectId) {
|
|
168
|
+
const ApiTokens = (0, manager_1.getModel)('api_tokens');
|
|
169
|
+
const record = (await ApiTokens.findOne({ id: tokenId, kind: 'user' }));
|
|
170
|
+
if (!record)
|
|
171
|
+
return;
|
|
172
|
+
const ids = listOf(record.projectIds);
|
|
173
|
+
if (ids.includes(ALL_PROJECTS) || ids.includes(projectId))
|
|
174
|
+
return;
|
|
175
|
+
await ApiTokens.update({ id: tokenId }, { projectIds: [...ids, projectId] });
|
|
91
176
|
}
|
|
92
177
|
/**
|
|
93
|
-
* The token's
|
|
94
|
-
* known (revoked), or has expired. Marks when it was last used.
|
|
178
|
+
* The token's request: who it is and what it may do, for the host's authProvider; null when
|
|
179
|
+
* it isn't a token, isn't known (revoked), or has expired. Marks when it was last used.
|
|
95
180
|
*/
|
|
96
181
|
async function resolveApiToken(token) {
|
|
97
182
|
if (typeof token !== 'string' || !token.startsWith(exports.API_TOKEN_PREFIX))
|
|
98
183
|
return null;
|
|
99
184
|
const ApiTokens = (0, manager_1.getModel)('api_tokens');
|
|
100
185
|
const record = (await ApiTokens.findOne({ hash: hashOf(token) }));
|
|
101
|
-
if (!record)
|
|
186
|
+
if (!isCurrent(record))
|
|
102
187
|
return null;
|
|
103
188
|
if (record.expiresAt && new Date(record.expiresAt).getTime() <= Date.now())
|
|
104
189
|
return null;
|
|
105
190
|
ApiTokens.update({ id: record.id }, { lastUsedAt: new Date() }).catch(() => { });
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
191
|
+
const scope = {
|
|
192
|
+
tokenId: record.id,
|
|
193
|
+
kind: record.kind,
|
|
194
|
+
permissions: parsePermissions(record.permissions, record.kind),
|
|
195
|
+
projectIds: parseProjectIds(record.projectIds),
|
|
109
196
|
};
|
|
197
|
+
if (record.kind === 'project') {
|
|
198
|
+
const projectId = listOf(record.projectIds)[0];
|
|
199
|
+
if (!projectId)
|
|
200
|
+
return null;
|
|
201
|
+
return { userId: (0, auth_1.projectTokenPrincipal)(record.id, projectId), name: record.name, scope: { ...scope, projectIds: [projectId] } };
|
|
202
|
+
}
|
|
203
|
+
return { userId: record.userId, name: record.name, scope };
|
|
110
204
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export interface AssetVersion {
|
|
2
|
+
versionId: string;
|
|
3
|
+
assetId: string;
|
|
4
|
+
projectId: string;
|
|
5
|
+
version: number;
|
|
6
|
+
storageKey: string;
|
|
7
|
+
contentType: string | null;
|
|
8
|
+
fileName: string | null;
|
|
9
|
+
createdBy: string | null;
|
|
10
|
+
createdAt: Date;
|
|
11
|
+
}
|
|
12
|
+
export declare const versionsCollection: () => import("mongodb").Collection<import("bson").Document> | null;
|
|
13
|
+
/** The key an asset's version `n` is archived in, next to the asset's own key. */
|
|
14
|
+
export declare const archivedVersionKey: (assetKey: string, version: number) => string;
|
|
15
|
+
/** Records the first version of an asset just made (or one made before versions were kept). */
|
|
16
|
+
export declare function recordFirstVersion(file: {
|
|
17
|
+
fileId: string;
|
|
18
|
+
projectId: string;
|
|
19
|
+
storageKey: string;
|
|
20
|
+
contentType?: string;
|
|
21
|
+
displayName?: string;
|
|
22
|
+
uploadedBy?: string;
|
|
23
|
+
createdAt?: Date;
|
|
24
|
+
}): Promise<void>;
|
|
25
|
+
/**
|
|
26
|
+
* Makes room for a new version of an asset, to be uploaded to its own key: its current version
|
|
27
|
+
* is copied to an archive key first (`copyFile`) and recorded there. If the copy fails nothing
|
|
28
|
+
* changes, and it throws: the current version is never lost.
|
|
29
|
+
*/
|
|
30
|
+
export declare function archiveCurrentVersion(file: {
|
|
31
|
+
fileId: string;
|
|
32
|
+
projectId: string;
|
|
33
|
+
storageKey: string;
|
|
34
|
+
contentType?: string;
|
|
35
|
+
displayName?: string;
|
|
36
|
+
uploadedBy?: string;
|
|
37
|
+
createdAt?: Date;
|
|
38
|
+
}, upload: {
|
|
39
|
+
fileName: string;
|
|
40
|
+
contentType: string;
|
|
41
|
+
userId: string;
|
|
42
|
+
}, copyFile: (from: string, to: string) => Promise<void>): Promise<AssetVersion>;
|
|
43
|
+
/** One of an asset's versions, or null. */
|
|
44
|
+
export declare function findVersion(projectId: string, assetId: string, versionId: string): Promise<AssetVersion | null>;
|
|
45
|
+
/** Every key an asset's versions are stored in (its current one included). */
|
|
46
|
+
export declare function versionStorageKeys(projectId: string, assetId: string, currentKey?: string): Promise<string[]>;
|
|
47
|
+
/** Forgets an asset's versions (once their files are deleted). */
|
|
48
|
+
export declare function deleteVersions(projectId: string, assetId: string): Promise<void>;
|
|
@@ -0,0 +1,84 @@
|
|
|
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
|
+
exports.archivedVersionKey = exports.versionsCollection = void 0;
|
|
7
|
+
exports.recordFirstVersion = recordFirstVersion;
|
|
8
|
+
exports.archiveCurrentVersion = archiveCurrentVersion;
|
|
9
|
+
exports.findVersion = findVersion;
|
|
10
|
+
exports.versionStorageKeys = versionStorageKeys;
|
|
11
|
+
exports.deleteVersions = deleteVersions;
|
|
12
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
13
|
+
const mongodb_1 = require("mongodb");
|
|
14
|
+
const nosqlDb_1 = require("../utils/nosqlDb");
|
|
15
|
+
const versionsCollection = () => (0, nosqlDb_1.nosqlDb)()?.collection('asset_versions') ?? null;
|
|
16
|
+
exports.versionsCollection = versionsCollection;
|
|
17
|
+
/** The key an asset's version `n` is archived in, next to the asset's own key. */
|
|
18
|
+
const archivedVersionKey = (assetKey, version) => {
|
|
19
|
+
const extension = /(\.[^./]*)$/.exec(assetKey)?.[1] ?? '';
|
|
20
|
+
return `${assetKey.slice(0, assetKey.length - extension.length)}.v${version}${extension}`;
|
|
21
|
+
};
|
|
22
|
+
exports.archivedVersionKey = archivedVersionKey;
|
|
23
|
+
/** Records the first version of an asset just made (or one made before versions were kept). */
|
|
24
|
+
async function recordFirstVersion(file) {
|
|
25
|
+
const col = (0, exports.versionsCollection)();
|
|
26
|
+
if (!col)
|
|
27
|
+
return;
|
|
28
|
+
if (await col.findOne({ assetId: file.fileId, projectId: file.projectId }))
|
|
29
|
+
return;
|
|
30
|
+
await col.insertOne({
|
|
31
|
+
_id: new mongodb_1.ObjectId(),
|
|
32
|
+
versionId: crypto_1.default.randomUUID(),
|
|
33
|
+
assetId: file.fileId,
|
|
34
|
+
projectId: file.projectId,
|
|
35
|
+
version: 1,
|
|
36
|
+
storageKey: file.storageKey,
|
|
37
|
+
contentType: file.contentType ?? null,
|
|
38
|
+
fileName: file.displayName ?? null,
|
|
39
|
+
createdBy: file.uploadedBy ?? null,
|
|
40
|
+
createdAt: file.createdAt ?? new Date(),
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Makes room for a new version of an asset, to be uploaded to its own key: its current version
|
|
45
|
+
* is copied to an archive key first (`copyFile`) and recorded there. If the copy fails nothing
|
|
46
|
+
* changes, and it throws: the current version is never lost.
|
|
47
|
+
*/
|
|
48
|
+
async function archiveCurrentVersion(file, upload, copyFile) {
|
|
49
|
+
const col = (0, exports.versionsCollection)();
|
|
50
|
+
if (!col)
|
|
51
|
+
throw new Error('Database not available');
|
|
52
|
+
await recordFirstVersion(file);
|
|
53
|
+
const [current] = await col.find({ assetId: file.fileId, projectId: file.projectId }).sort({ version: -1 }).limit(1).toArray();
|
|
54
|
+
const currentVersion = current?.version ?? 1;
|
|
55
|
+
const archiveKey = (0, exports.archivedVersionKey)(file.storageKey, currentVersion);
|
|
56
|
+
await copyFile(file.storageKey, archiveKey);
|
|
57
|
+
await col.updateOne({ assetId: file.fileId, projectId: file.projectId, version: currentVersion }, { $set: { storageKey: archiveKey } });
|
|
58
|
+
const record = {
|
|
59
|
+
versionId: crypto_1.default.randomUUID(),
|
|
60
|
+
assetId: file.fileId,
|
|
61
|
+
projectId: file.projectId,
|
|
62
|
+
version: currentVersion + 1,
|
|
63
|
+
storageKey: file.storageKey,
|
|
64
|
+
contentType: upload.contentType,
|
|
65
|
+
fileName: upload.fileName,
|
|
66
|
+
createdBy: upload.userId,
|
|
67
|
+
createdAt: new Date(),
|
|
68
|
+
};
|
|
69
|
+
await col.insertOne({ _id: new mongodb_1.ObjectId(), ...record });
|
|
70
|
+
return record;
|
|
71
|
+
}
|
|
72
|
+
/** One of an asset's versions, or null. */
|
|
73
|
+
async function findVersion(projectId, assetId, versionId) {
|
|
74
|
+
return (await (0, exports.versionsCollection)()?.findOne({ projectId, assetId, versionId })) ?? null;
|
|
75
|
+
}
|
|
76
|
+
/** Every key an asset's versions are stored in (its current one included). */
|
|
77
|
+
async function versionStorageKeys(projectId, assetId, currentKey) {
|
|
78
|
+
const versions = ((await (0, exports.versionsCollection)()?.find({ projectId, assetId }).toArray()) ?? []);
|
|
79
|
+
return [...new Set([...(currentKey ? [currentKey] : []), ...versions.map(v => v.storageKey)])];
|
|
80
|
+
}
|
|
81
|
+
/** Forgets an asset's versions (once their files are deleted). */
|
|
82
|
+
async function deleteVersions(projectId, assetId) {
|
|
83
|
+
await (0, exports.versionsCollection)()?.deleteMany({ projectId, assetId });
|
|
84
|
+
}
|
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
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export interface Deliverable {
|
|
2
|
+
id: string;
|
|
3
|
+
projectId: string;
|
|
4
|
+
assetId: string;
|
|
5
|
+
versionId: string | null;
|
|
6
|
+
shotId: string | null;
|
|
7
|
+
exportId: string | null;
|
|
8
|
+
note: string | null;
|
|
9
|
+
createdBy: string;
|
|
10
|
+
createdAt: Date;
|
|
11
|
+
}
|
|
12
|
+
export declare const deliverablesCollection: () => import("mongodb").Collection<import("bson").Document> | null;
|
|
13
|
+
export declare function createDeliverable(fields: Omit<Deliverable, 'id' | 'createdAt'>): Promise<Deliverable>;
|
|
14
|
+
/** A project's deliverables, of one shot or export if given, newest first. */
|
|
15
|
+
export declare function listDeliverables(projectId: string, of?: {
|
|
16
|
+
shotId?: string;
|
|
17
|
+
exportId?: string;
|
|
18
|
+
}): Promise<Deliverable[]>;
|
|
19
|
+
/** Removes a deliverable (never its asset). Whether there was one. */
|
|
20
|
+
export declare function deleteDeliverable(projectId: string, id: string): Promise<boolean>;
|
|
21
|
+
/** Forgets the deliverables of what's gone: an asset, shots, or an export. */
|
|
22
|
+
export declare function deleteDeliverablesOf(projectId: string, of: {
|
|
23
|
+
assetId?: string;
|
|
24
|
+
shotIds?: string[];
|
|
25
|
+
exportId?: string;
|
|
26
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,55 @@
|
|
|
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
|
+
exports.deliverablesCollection = void 0;
|
|
7
|
+
exports.createDeliverable = createDeliverable;
|
|
8
|
+
exports.listDeliverables = listDeliverables;
|
|
9
|
+
exports.deleteDeliverable = deleteDeliverable;
|
|
10
|
+
exports.deleteDeliverablesOf = deleteDeliverablesOf;
|
|
11
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
12
|
+
const mongodb_1 = require("mongodb");
|
|
13
|
+
const nosqlDb_1 = require("../utils/nosqlDb");
|
|
14
|
+
const deliverablesCollection = () => (0, nosqlDb_1.nosqlDb)()?.collection('deliverables') ?? null;
|
|
15
|
+
exports.deliverablesCollection = deliverablesCollection;
|
|
16
|
+
const strip = ({ _id, ...deliverable }) => deliverable;
|
|
17
|
+
async function createDeliverable(fields) {
|
|
18
|
+
const col = (0, exports.deliverablesCollection)();
|
|
19
|
+
if (!col)
|
|
20
|
+
throw new Error('Database not available');
|
|
21
|
+
const deliverable = { id: crypto_1.default.randomUUID(), ...fields, createdAt: new Date() };
|
|
22
|
+
await col.insertOne({ _id: new mongodb_1.ObjectId(), ...deliverable });
|
|
23
|
+
return deliverable;
|
|
24
|
+
}
|
|
25
|
+
/** A project's deliverables, of one shot or export if given, newest first. */
|
|
26
|
+
async function listDeliverables(projectId, of = {}) {
|
|
27
|
+
const col = (0, exports.deliverablesCollection)();
|
|
28
|
+
if (!col)
|
|
29
|
+
throw new Error('Database not available');
|
|
30
|
+
const filter = { projectId };
|
|
31
|
+
if (of.shotId)
|
|
32
|
+
filter.shotId = of.shotId;
|
|
33
|
+
if (of.exportId)
|
|
34
|
+
filter.exportId = of.exportId;
|
|
35
|
+
return (await col.find(filter).sort({ createdAt: -1 }).toArray()).map(strip);
|
|
36
|
+
}
|
|
37
|
+
/** Removes a deliverable (never its asset). Whether there was one. */
|
|
38
|
+
async function deleteDeliverable(projectId, id) {
|
|
39
|
+
const col = (0, exports.deliverablesCollection)();
|
|
40
|
+
if (!col)
|
|
41
|
+
throw new Error('Database not available');
|
|
42
|
+
return (await col.deleteOne({ projectId, id })).deletedCount > 0;
|
|
43
|
+
}
|
|
44
|
+
/** Forgets the deliverables of what's gone: an asset, shots, or an export. */
|
|
45
|
+
async function deleteDeliverablesOf(projectId, of) {
|
|
46
|
+
const col = (0, exports.deliverablesCollection)();
|
|
47
|
+
if (!col)
|
|
48
|
+
return;
|
|
49
|
+
if (of.assetId)
|
|
50
|
+
await col.deleteMany({ projectId, assetId: of.assetId });
|
|
51
|
+
if (of.shotIds?.length)
|
|
52
|
+
await col.deleteMany({ projectId, shotId: { $in: of.shotIds } });
|
|
53
|
+
if (of.exportId)
|
|
54
|
+
await col.deleteMany({ projectId, exportId: of.exportId });
|
|
55
|
+
}
|
|
@@ -69,7 +69,9 @@ async function runIngestJob(jobId, fileId, projectId) {
|
|
|
69
69
|
}
|
|
70
70
|
(0, jobQueue_1.updateJob)(jobId, { stage: 'creating-version', progress: 70 });
|
|
71
71
|
const versionsCol = getVersionsCollection();
|
|
72
|
-
|
|
72
|
+
// Its upload already recorded the version it's in; only an asset from before versions
|
|
73
|
+
// were kept gets one here.
|
|
74
|
+
if (versionsCol && !(await versionsCol.findOne({ assetId: fileId, projectId, storageKey: fileDoc.storageKey }))) {
|
|
73
75
|
const existing = await versionsCol.countDocuments({ assetId: fileId, projectId });
|
|
74
76
|
await versionsCol.insertOne({
|
|
75
77
|
_id: new mongodb_1.ObjectId(),
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.deleteProductionData = deleteProductionData;
|
|
4
|
+
const nosqlDb_1 = require("../utils/nosqlDb");
|
|
5
|
+
/** A deleted project's sequences, shots, tasks and deliverables: only that project's. */
|
|
6
|
+
async function deleteProductionData(projectId) {
|
|
7
|
+
const db = (0, nosqlDb_1.nosqlDb)();
|
|
8
|
+
if (!db || !projectId)
|
|
9
|
+
return;
|
|
10
|
+
for (const name of ['production_tasks', 'production_shots', 'production_sequences', 'deliverables']) {
|
|
11
|
+
await db.collection(name).deleteMany({ projectId });
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export declare const PRODUCTION_STATUSES: readonly ["todo", "in_progress", "review", "done"];
|
|
2
|
+
export declare const TASK_PRIORITIES: readonly ["low", "medium", "high"];
|
|
3
|
+
type Field = 'name' | 'description' | 'notes' | 'status' | 'priority' | 'assignees' | 'assignee' | 'dueDate' | 'order' | 'assetId' | 'exportId';
|
|
4
|
+
export type FieldsResult = {
|
|
5
|
+
values: Record<string, any>;
|
|
6
|
+
} | {
|
|
7
|
+
error: string;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* The fields of `body` that `allowed` lists, checked and ready to save (`assignees` and
|
|
11
|
+
* `assignee` are saved as `assignedTo`). With `creating`, a name is required.
|
|
12
|
+
*/
|
|
13
|
+
export declare function checkProductionFields(body: any, allowed: readonly Field[], project: any, { creating }?: {
|
|
14
|
+
creating?: boolean | undefined;
|
|
15
|
+
}): FieldsResult;
|
|
16
|
+
export {};
|