streamby-core 0.40.0 → 0.42.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 CHANGED
@@ -1,5 +1,65 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.42.0
4
+
5
+ ### Added
6
+
7
+ - **Project documents**: markdown a project's team reads and edits in the UI, and agents read and
8
+ keep up to date through the MCP server, with every save a version (who saved it, and when). The
9
+ README's Documents section documents them.
10
+ - **New tables: `documents` and `document_versions`.** They're created at startup in the main
11
+ database (collections in MongoDB; in memory without one).
12
+ - Routes under `/projects/:projectId/documents`: the tree, a document, creating, a new version
13
+ (`PATCH` with `baseVersion`; 409 with the document as it is now when it changed since), a
14
+ section's (`PATCH .../sections`), moving and marking exportable, the history
15
+ (`.../versions[/:version]`), publishing a version (`POST .../publish`) and deleting (a folder
16
+ with everything in it). Deleting a project deletes its documents.
17
+ - Permissions `docs:read`, `docs:write`, `docs:publish` and `docs:delete`. Publishing (a
18
+ version for the exports, or whether they deliver it: `POST .../publish`, `PATCH` with
19
+ `exportable`) needs `docs:publish`, not `docs:write`. The OAuth scopes grant reading
20
+ (`streamby:read`) and writing (`streamby:write`); an app publishes and deletes none, whatever
21
+ its scopes: what it edits stays a draft.
22
+ - In MongoDB, a save writes the document and its version in a transaction when the server has
23
+ them (a replica set or a sharded cluster); on a standalone server, two writes.
24
+ - MCP tools `list_documents`, `get_document`, `list_document_versions`, `create_document`,
25
+ `update_document` and `update_document_section`.
26
+ - An export's `documentsNode` delivers the published version of the project's exportable
27
+ documents (all, one folder's, or some), never a draft.
28
+ - A `documentEvent` to the project's active members on each change.
29
+ - Types `ProjectDocument`, `DocumentVersion`, `DocumentAuthor` and `DocumentKind`.
30
+
31
+ ## 0.41.0
32
+
33
+ ### Added
34
+
35
+ - **OAuth access tokens**: apps the user signs in to with their account (claude.ai's
36
+ connector) work with StreamBy and its MCP server, as a protected resource (RFC 9728). The
37
+ README documents the setup, and how to add the connector in claude.ai.
38
+ - `createOAuthTokenVerifier({ issuer, audiences, jwks })` checks a token with the
39
+ authorization server's keys (RS256, from its JWKS URL or a function), its issuer, its
40
+ audience (one of StreamBy's resources, RFC 8707) and its expiry. The host's `authProvider`
41
+ returns its user with `apiToken: verified.scope` and `oauth: { clientId, clientName? }`.
42
+ - Scopes map to permissions: `streamby:read` to every `:read` one, `streamby:write` to those
43
+ and making and changing; none to deleting nor managing members (`OAUTH_SCOPE_PERMISSIONS`,
44
+ `permissionsForOAuthScopes`). The user's role in each project still applies.
45
+ - An app deletes nothing: its `DELETE` requests answer 403 whatever their route's permission
46
+ (`project:update`, `workflow:write` and `exports:write` reach some that delete), but taking
47
+ a deliverable's mark off. API tokens are unchanged.
48
+ - The router's `oauth` config (`{ issuer, resource, resourceMetadataUrl?, resourceName? }`)
49
+ makes a request without a valid token answer 401 with
50
+ `WWW-Authenticate: Bearer resource_metadata="..."`, and serves the metadata at
51
+ `GET /.well-known/oauth-protected-resource`; `protectedResourceMetadata` builds it for the
52
+ host to serve at its root too.
53
+ - `looksLikeOAuthAccessToken` tells an access token from a host's session tokens.
54
+ - `Auth.oauth`: the app a request comes from. A request with it and without `apiToken` is
55
+ refused (an app never gets all of its user's access).
56
+
57
+ ### Changed
58
+
59
+ - The MCP server takes an OAuth access token too. An app's requests are limited per user it acts
60
+ for, and its tool calls are recorded in `mcp_audit` by its client (`oauthClientId`, a new
61
+ column added at startup) with no `tokenId` (now nullable).
62
+
3
63
  ## 0.40.0
4
64
 
5
65
  ### ⚠️ Breaking changes
package/README.md CHANGED
@@ -134,6 +134,7 @@ hash is stored, with a short prefix to tell it apart.
134
134
  | Exports | `exports:read`, `exports:write` | The project's exports. |
135
135
  | Rights | `rights:read`, `rights:write` | Assets' rights. |
136
136
  | Jobs | `jobs:read`, `jobs:run` | Following jobs; starting them (transcoding, rendering, AI, publishing a deliverable). |
137
+ | Documents | `docs:read`, `docs:write`, `docs:publish`, `docs:delete` | The project's documents: reading them and their history; creating, editing and moving them; publishing them (the version its exports deliver, and whether they deliver it); deleting them. |
137
138
  | Account | `projects:create` | Creating projects. A user's token only; the token reaches what it creates. |
138
139
 
139
140
  `PROJECT_PERMISSIONS` and `ACCOUNT_PERMISSIONS` are exported. A route no permission covers
@@ -173,19 +174,107 @@ list shows only the token's projects.
173
174
 
174
175
  ---
175
176
 
177
+ ## 🔐 OAuth: apps the user signs in to
178
+
179
+ An app the user signs in to with their account, such as claude.ai's connector, works with
180
+ StreamBy through an **OAuth access token**, not an API token. StreamBy is the protected resource
181
+ (RFC 9728); the tokens are issued by the host's authorization server (for Nhexa, Nhexa-API).
182
+
183
+ A token is a JWT the server signs with RS256 for one resource (its audience, RFC 8707), checked
184
+ with the server's published keys (JWKS) alone: no secret is shared. It reaches what the user
185
+ approved, and never more than the user can: their role in each project still applies.
186
+
187
+ | Scope | Permissions |
188
+ |---|---|
189
+ | `streamby:read` | Every `:read` permission: `project:read`, `members:read`, `assets:read`, `production:read`, `workflow:read`, `exports:read`, `rights:read`, `jobs:read`, `docs:read`. |
190
+ | `streamby:write` | Those, and making and changing: `project:update`, `assets:write`, `production:write`, `workflow:write`, `exports:write`, `rights:write`, `jobs:run`, `projects:create`, `docs:write`. |
191
+
192
+ No scope grants deleting (`*:delete`, `project:delete`) nor managing members. And since some
193
+ write permissions also reach routes that delete (`project:update` a connection, a worker or an
194
+ integration; `workflow:write` a pipeline; `exports:write` an export), **an app's `DELETE` is
195
+ refused (403) whatever its route**, but taking a deliverable's mark off, which only forgets the
196
+ mark (the asset stays). Nor does an app publish a document (`POST .../publish`, or a `PATCH`
197
+ changing `exportable`), whatever its scopes: what it edits stays a draft until someone publishes
198
+ it. API tokens aren't affected. Other scopes (`profile`, `email`) grant nothing in StreamBy. `OAUTH_SCOPE_PERMISSIONS` is exported.
199
+
200
+ **Telling clients where to sign in**
201
+
202
+ Set `oauth` in the router's config. A request without a valid token then gets a 401 with
203
+ `WWW-Authenticate: Bearer resource_metadata="..."`, and the metadata says which authorization
204
+ server to use. The router serves it at `GET /streamby/.well-known/oauth-protected-resource`;
205
+ serve it at your root too, where RFC 9728 puts it (`resourceMetadataUrl` defaults to that):
206
+
207
+ ```ts
208
+ import express from 'express';
209
+ import { createStreamByRouter, protectedResourceMetadata } from 'streamby-core';
210
+
211
+ const oauth = { issuer: 'https://api.example.com', resource: 'https://api.example.com/streamby/mcp' };
212
+ app.get('/.well-known/oauth-protected-resource/streamby/mcp', (_req, res) => res.json(protectedResourceMetadata(oauth)));
213
+ app.use('/streamby', createStreamByRouter({ /* ... */ oauth }));
214
+ ```
215
+
216
+ **Validating a token (your `authProvider`)**
217
+
218
+ Your `authProvider` checks the token with `createOAuthTokenVerifier` and returns the user, with
219
+ the token's scope in `apiToken` and the app in `oauth`. A request with `oauth` but no `apiToken`
220
+ is refused, so an app never gets all of its user's access. `looksLikeOAuthAccessToken` tells the
221
+ token from your own session tokens.
222
+
223
+ ```ts
224
+ import { createOAuthTokenVerifier, looksLikeOAuthAccessToken, API_TOKEN_PREFIX } from 'streamby-core';
225
+
226
+ const verifyOAuthToken = createOAuthTokenVerifier({
227
+ issuer: 'https://api.example.com',
228
+ // What a token may be for: StreamBy's resources.
229
+ audiences: ['https://api.example.com/streamby', 'https://api.example.com/streamby/mcp'],
230
+ // The server's JWKS URL, or a function giving its keys (when the host is the server itself).
231
+ jwks: 'https://api.example.com/.well-known/jwks.json',
232
+ });
233
+
234
+ createStreamByRouter({
235
+ // ...
236
+ authProvider: async (req) => {
237
+ const bearer = req.headers.authorization?.replace(/^Bearer /, '');
238
+ if (bearer?.startsWith(API_TOKEN_PREFIX)) { /* an API token, as above */ }
239
+ if (looksLikeOAuthAccessToken(bearer)) {
240
+ const verified = await verifyOAuthToken(bearer); // null: forged, expired, or for another resource
241
+ if (!verified) throw new Error('Invalid OAuth access token');
242
+ // Optional but recommended: has the user taken the app's access away? (your own records)
243
+ if (!(await stillConnected(verified.userId, verified.clientId))) throw new Error('Access revoked');
244
+ const user = await findUser(verified.userId);
245
+ return {
246
+ userId: user.id, username: user.username, role: user.role,
247
+ apiToken: verified.scope,
248
+ oauth: { clientId: verified.clientId, clientName: 'Claude' }, // the name, if you know it
249
+ };
250
+ }
251
+ // ...your session-based authentication
252
+ },
253
+ });
254
+ ```
255
+
256
+ The verifier keeps the keys for 5 minutes (`jwksCacheMs`) and fetches them again for a key id it
257
+ doesn't know (a rotation), at most once a minute. It doesn't know whether the user has since
258
+ taken the app's access away: without that check, a revoked app keeps its access until its access
259
+ token expires (Nhexa's last an hour). Nhexa-API checks the user's consent on every request, so
260
+ taking the access away in accounts.nhexa.cl ends it at once.
261
+
262
+ ---
263
+
176
264
  ## 🤖 MCP server
177
265
 
178
266
  StreamBy serves an [MCP](https://modelcontextprotocol.io) server at `POST /streamby/mcp`
179
267
  (Streamable HTTP, stateless). Agents such as Claude Code use it to read and work on StreamBy
180
- projects with an API token (see above). It takes only an API token; a browser session gets 401.
268
+ projects with an API token, and apps the user signed in to (claude.ai's connector) with an OAuth
269
+ access token (see above). A browser session gets 401.
181
270
 
182
271
  Each tool calls StreamBy's own REST API with the caller's token. So it reaches exactly what the
183
- token allows (and, for a user's token, what its user's role in each project allows), and a
272
+ token allows (and, for a user's token or an app, what the user's role in each project allows), and a
184
273
  change is saved just like one made from the UI, which shows it when it loads the project.
185
274
 
186
275
  **Tools**
187
276
 
188
- A token sees only the tools whose permissions it has.
277
+ A token sees only the tools whose permissions it has (an app: those its scopes grant).
189
278
 
190
279
  | Tools | Permissions |
191
280
  |---|---|
@@ -210,6 +299,8 @@ A token sees only the tools whose permissions it has.
210
299
  | `delete_sequence`, `delete_shot`, `delete_task` | `production:delete` |
211
300
  | `list_asset_versions`, `get_asset_download_url`, `list_deliverables` | `assets:read` |
212
301
  | `request_asset_upload`, `mark_deliverable`, `remove_deliverable` | `assets:write` |
302
+ | `list_documents`, `get_document`, `list_document_versions` | `docs:read` |
303
+ | `create_document`, `update_document`, `update_document_section` | `docs:write` |
213
304
 
214
305
  The tools take the path a user does in the UI, and each one's description says where its
215
306
  result shows there. Production data lives under a production node of the project's workflow:
@@ -273,13 +364,14 @@ code there that downloads the URL and imports the file (in Blender,
273
364
 
274
365
  **Limits and audit**
275
366
 
276
- - Each token can make at most 120 requests a minute (a sliding window). Past that, StreamBy
277
- answers 429 with `Retry-After`. Each instance counts its own requests. Change the limit with
278
- `mcp.rateLimit`.
367
+ - Each token can make at most 120 requests a minute (a sliding window); an app, per user it acts
368
+ for. Past that, StreamBy answers 429 with `Retry-After`. Each instance counts its own
369
+ requests. Change the limit with `mcp.rateLimit`.
279
370
  - Every tool call is recorded in the `mcp_audit` table, created at startup in the main database.
280
- Each row has the user (a project's token: the token itself), the token's id, the project, the
281
- tool, whether it went through, the status, the error and the duration. A row never includes
282
- the arguments or the token.
371
+ Each row has the user (a project's token: the token itself), the token's id (an app's call:
372
+ its OAuth client, in `oauthClientId`, and no token), the project, the tool, whether it went
373
+ through, the status, the error and the duration. A row never includes the arguments or the
374
+ token.
283
375
 
284
376
  ```ts
285
377
  createStreamByRouter({
@@ -337,7 +429,67 @@ and an expiry:
337
429
  }
338
430
  ```
339
431
 
340
- The claude.ai connector needs OAuth, which isn't available yet: it's for API tokens only for now.
432
+ ### Connecting from claude.ai
433
+
434
+ claude.ai connects through a custom connector, which signs in with OAuth (see above): nobody on
435
+ the team sets up a token or a file. Each person signs in with their own account and approves what
436
+ Claude may do, and Claude reaches only that, and only what their role in each project allows.
437
+
438
+ - **For yourself**: in claude.ai, Settings → Connectors → Add custom connector. Give it a name
439
+ (StreamBy) and the server's URL, `https://your-host/streamby/mcp`, and add it. Then Connect:
440
+ claude.ai opens the sign-in page, and after you sign in, the consent screen, with what it asks
441
+ for (`streamby:read`, and `streamby:write` to make and change things). Approve it.
442
+ - **For your organization** (Team and Enterprise plans): an owner adds it once in Organization
443
+ settings → Connectors, with the same URL; each member then connects it with their own account
444
+ from their Settings → Connectors.
445
+
446
+ claude.ai registers itself with the authorization server (dynamic client registration or a
447
+ client ID metadata document), so there's nothing to set up there. To take its access away, the
448
+ person removes it in their account's connected apps (in Nhexa, accounts.nhexa.cl → Aplicaciones
449
+ → Quitar acceso), or disconnects it in claude.ai.
450
+
451
+ ---
452
+
453
+ ## 📄 Documents
454
+
455
+ Each project keeps its documents: markdown its team reads and edits (its concept, its
456
+ architecture, its decisions), and agents read and keep up to date through the MCP server. They're
457
+ kept in the main database (the `documents` and `document_versions` tables, created at startup,
458
+ or collections), each one in a single project, in folders.
459
+
460
+ - **Every save is a version**, with who saved it and when: a user, an API token, or an app on a
461
+ user's behalf (`{ userId, username, via: 'session' | 'apiToken' | 'oauth', tokenId?, clientId?, clientName? }`).
462
+ - **Nothing is overwritten without notice.** A change says the version it was made on
463
+ (`baseVersion`); if someone saved another one since, it's refused with a 409 and the document
464
+ as it is now, to read again.
465
+ - **A section** is what's under a heading (`## Architecture`), up to the next one of the same
466
+ level or above: it can be replaced alone, the rest untouched.
467
+ - **Drafts and published versions.** A document marked exportable delivers, through the project's
468
+ exports, only the version published from its history; editing it changes nothing exported until
469
+ another version is published. An export's `documentsNode` (on the StreamBy node's data input)
470
+ delivers them, `[{ id, title, parentId, position, version, content, publishedAt }]`: all the
471
+ project's, those in one folder (`data.folderId`), or some (`data.documentIds`).
472
+ - **In real time**, a `documentEvent` reaches the project's active members on each change:
473
+ `{ projectId, action: 'created' | 'saved' | 'changed' | 'deleted', documentIds, version?, title?, author }`
474
+ (`saved` is a new version; `changed`, a move or a publication).
475
+
476
+ | Route | Permission |
477
+ |---|---|
478
+ | `GET /projects/:projectId/documents`: the tree, without contents | `docs:read` |
479
+ | `POST /projects/:projectId/documents` `{ title, content?, kind?: 'document' \| 'folder', parentId?, position? }` | `docs:write` |
480
+ | `GET /projects/:projectId/documents/:docId` | `docs:read` |
481
+ | `PATCH /projects/:projectId/documents/:docId` `{ baseVersion, title?, content? }` (a new version) and/or `{ parentId?, position? }` | `docs:write` |
482
+ | `PATCH /projects/:projectId/documents/:docId` `{ exportable }` | `docs:write` and `docs:publish` |
483
+ | `PATCH /projects/:projectId/documents/:docId/sections` `{ baseVersion, heading, content }` | `docs:write` |
484
+ | `GET /projects/:projectId/documents/:docId/versions` and `/versions/:version` | `docs:read` |
485
+ | `POST /projects/:projectId/documents/:docId/publish` `{ version }` (`null`: none) | `docs:publish` |
486
+ | `DELETE /projects/:projectId/documents/:docId` (a folder, with everything in it) | `docs:delete` |
487
+
488
+ Editors and admins create, edit, publish and delete them; viewers read them. An app connected
489
+ with OAuth reads and edits them, but publishes and deletes none: what it edits stays a draft.
490
+
491
+ In MongoDB, a save writes the document and its version in a transaction when the server has them
492
+ (a replica set or a sharded cluster). A standalone server doesn't: there they're two writes.
341
493
 
342
494
  ---
343
495
 
@@ -455,7 +607,7 @@ What a user can do in a project depends on their role in that project, as an act
455
607
  | Role | Can |
456
608
  |---|---|
457
609
  | `viewer` | See the project and its content, and archive the project in their own list. |
458
- | `editor` | Everything a viewer can, plus change the project: its details, connections, exports, files and folders, workflow, pipelines, production, reviews, rights, and start jobs. |
610
+ | `editor` | Everything a viewer can, plus change the project: its details, connections, exports, files and folders, workflow, pipelines, production, reviews, rights, documents, and start jobs. |
459
611
  | `admin` | Everything an editor can, plus manage the project's members and delete the project. |
460
612
 
461
613
  A project's public exports (`GET /streamby/:projectId/export/:exportName`) are read without a
@@ -249,8 +249,43 @@ const createTables = async (pool) => {
249
249
  "createdAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW()
250
250
  );
251
251
  CREATE INDEX IF NOT EXISTS mcp_audit_token_idx ON streamby.mcp_audit ("tokenId", "createdAt");
252
+ -- An app's calls (an OAuth access token) are recorded by its client, with no token.
253
+ ALTER TABLE streamby.mcp_audit ADD COLUMN IF NOT EXISTS "oauthClientId" VARCHAR(255);
254
+ ALTER TABLE streamby.mcp_audit ALTER COLUMN "tokenId" DROP NOT NULL;
252
255
  `);
253
256
  console.log('✅ "mcp_audit" table ensured to exist.');
257
+ // Projects' documents and their versions (see services/documents). Main db only.
258
+ await pool.query(`
259
+ CREATE TABLE IF NOT EXISTS streamby.documents (
260
+ id TEXT PRIMARY KEY,
261
+ "projectId" VARCHAR(255) NOT NULL,
262
+ kind VARCHAR(20) NOT NULL DEFAULT 'document',
263
+ title TEXT NOT NULL,
264
+ "parentId" TEXT,
265
+ position INTEGER NOT NULL DEFAULT 0,
266
+ content TEXT NOT NULL DEFAULT '',
267
+ version INTEGER NOT NULL,
268
+ exportable BOOLEAN NOT NULL DEFAULT FALSE,
269
+ "publishedVersion" INTEGER,
270
+ "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
271
+ "createdBy" JSONB NOT NULL,
272
+ "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
273
+ "updatedBy" JSONB NOT NULL
274
+ );
275
+ CREATE INDEX IF NOT EXISTS documents_project_idx ON streamby.documents ("projectId");
276
+ CREATE TABLE IF NOT EXISTS streamby.document_versions (
277
+ "documentId" TEXT NOT NULL,
278
+ "projectId" VARCHAR(255) NOT NULL,
279
+ version INTEGER NOT NULL,
280
+ title TEXT NOT NULL,
281
+ content TEXT NOT NULL,
282
+ author JSONB NOT NULL,
283
+ "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
284
+ PRIMARY KEY ("documentId", version)
285
+ );
286
+ CREATE INDEX IF NOT EXISTS document_versions_project_idx ON streamby.document_versions ("projectId");
287
+ `);
288
+ console.log('✅ "documents" and "document_versions" tables ensured to exist.');
254
289
  }
255
290
  catch (error) {
256
291
  console.error('❌ Error ensuring tables exist:', error);
package/dist/index.d.ts CHANGED
@@ -1,7 +1,9 @@
1
1
  export { createStreamByRouter } from './middleware/createRouter';
2
2
  export { emitToUser } from './services/wsHub';
3
3
  export { createNotification } from './services/notification';
4
- export type { UserPlan, UserSubscription, Auth, ApiTokenScope, ApiTokenKind, ApiTokenPermission, ProjectPermission, AccountPermission } from './types';
4
+ export type { UserPlan, UserSubscription, Auth, ApiTokenScope, ApiTokenKind, ApiTokenPermission, ProjectPermission, AccountPermission, OAuthClientInfo, OAuthResourceConfig, ProjectDocument, DocumentVersion, DocumentAuthor, DocumentKind } from './types';
5
5
  export { resolveApiToken, API_TOKEN_PREFIX } from './services/apiTokens';
6
6
  export { PROJECT_PERMISSIONS, ACCOUNT_PERMISSIONS } from './services/apiTokenPermissions';
7
7
  export type { ResolvedApiToken } from './services/apiTokens';
8
+ export { createOAuthTokenVerifier, looksLikeOAuthAccessToken, protectedResourceMetadata, defaultResourceMetadataUrl, permissionsForOAuthScopes, OAUTH_SCOPES, OAUTH_SCOPE_PERMISSIONS, } from './services/oauthTokens';
9
+ export type { VerifiedOAuthToken, OAuthTokenVerifierOptions, Jwks, Jwk, OAuthScope } from './services/oauthTokens';
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.ACCOUNT_PERMISSIONS = exports.PROJECT_PERMISSIONS = exports.API_TOKEN_PREFIX = exports.resolveApiToken = exports.createNotification = exports.emitToUser = exports.createStreamByRouter = void 0;
3
+ exports.OAUTH_SCOPE_PERMISSIONS = exports.OAUTH_SCOPES = exports.permissionsForOAuthScopes = exports.defaultResourceMetadataUrl = exports.protectedResourceMetadata = exports.looksLikeOAuthAccessToken = exports.createOAuthTokenVerifier = exports.ACCOUNT_PERMISSIONS = exports.PROJECT_PERMISSIONS = exports.API_TOKEN_PREFIX = exports.resolveApiToken = exports.createNotification = exports.emitToUser = exports.createStreamByRouter = void 0;
4
4
  var createRouter_1 = require("./middleware/createRouter");
5
5
  Object.defineProperty(exports, "createStreamByRouter", { enumerable: true, get: function () { return createRouter_1.createStreamByRouter; } });
6
6
  var wsHub_1 = require("./services/wsHub");
@@ -13,3 +13,11 @@ Object.defineProperty(exports, "API_TOKEN_PREFIX", { enumerable: true, get: func
13
13
  var apiTokenPermissions_1 = require("./services/apiTokenPermissions");
14
14
  Object.defineProperty(exports, "PROJECT_PERMISSIONS", { enumerable: true, get: function () { return apiTokenPermissions_1.PROJECT_PERMISSIONS; } });
15
15
  Object.defineProperty(exports, "ACCOUNT_PERMISSIONS", { enumerable: true, get: function () { return apiTokenPermissions_1.ACCOUNT_PERMISSIONS; } });
16
+ var oauthTokens_1 = require("./services/oauthTokens");
17
+ Object.defineProperty(exports, "createOAuthTokenVerifier", { enumerable: true, get: function () { return oauthTokens_1.createOAuthTokenVerifier; } });
18
+ Object.defineProperty(exports, "looksLikeOAuthAccessToken", { enumerable: true, get: function () { return oauthTokens_1.looksLikeOAuthAccessToken; } });
19
+ Object.defineProperty(exports, "protectedResourceMetadata", { enumerable: true, get: function () { return oauthTokens_1.protectedResourceMetadata; } });
20
+ Object.defineProperty(exports, "defaultResourceMetadataUrl", { enumerable: true, get: function () { return oauthTokens_1.defaultResourceMetadataUrl; } });
21
+ Object.defineProperty(exports, "permissionsForOAuthScopes", { enumerable: true, get: function () { return oauthTokens_1.permissionsForOAuthScopes; } });
22
+ Object.defineProperty(exports, "OAUTH_SCOPES", { enumerable: true, get: function () { return oauthTokens_1.OAUTH_SCOPES; } });
23
+ Object.defineProperty(exports, "OAUTH_SCOPE_PERMISSIONS", { enumerable: true, get: function () { return oauthTokens_1.OAUTH_SCOPE_PERMISSIONS; } });
@@ -1,7 +1,8 @@
1
- import { StreamByConfig } from '../types';
1
+ import { Auth, StreamByConfig } from '../types';
2
2
  export interface McpAuditEntry {
3
3
  userId: string;
4
- tokenId: string;
4
+ tokenId: string | null;
5
+ oauthClientId: string | null;
5
6
  projectId: string | null;
6
7
  tool: string;
7
8
  ok: boolean;
@@ -9,4 +10,6 @@ export interface McpAuditEntry {
9
10
  error: string | null;
10
11
  durationMs: number;
11
12
  }
13
+ /** Who made an MCP request, as the audit log keeps it. */
14
+ export declare const auditCallerOf: (auth: Auth) => Pick<McpAuditEntry, "userId" | "tokenId" | "oauthClientId">;
12
15
  export declare function recordMcpCall(entry: McpAuditEntry, config: StreamByConfig): Promise<void>;
package/dist/mcp/audit.js CHANGED
@@ -3,9 +3,17 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.auditCallerOf = void 0;
6
7
  exports.recordMcpCall = recordMcpCall;
7
8
  const crypto_1 = __importDefault(require("crypto"));
8
9
  const manager_1 = require("../models/manager");
10
+ /** Who made an MCP request, as the audit log keeps it. */
11
+ const auditCallerOf = (auth) => ({
12
+ userId: auth.userId,
13
+ tokenId: auth.oauth ? null : auth.apiToken?.tokenId ?? null,
14
+ oauthClientId: auth.oauth?.clientId ?? null,
15
+ });
16
+ exports.auditCallerOf = auditCallerOf;
9
17
  async function recordMcpCall(entry, config) {
10
18
  try {
11
19
  const dbType = config.databases?.find(db => db.main)?.type ?? 'nosql';
@@ -40,7 +40,7 @@ function createMcpServer(auth, api, config, version) {
40
40
  const refused = await (0, workflowNodes_1.ensureWorkflowNode)(tool.workflowNode, tool.projectOf?.(args), auth, api);
41
41
  if (refused) {
42
42
  await (0, audit_1.recordMcpCall)({
43
- userId: auth.userId, tokenId: auth.apiToken.tokenId, projectId, tool: tool.name,
43
+ ...(0, audit_1.auditCallerOf)(auth), projectId, tool: tool.name,
44
44
  ok: false, status: null, error: refused, durationMs: Date.now() - started,
45
45
  }, config);
46
46
  return { isError: true, content: [{ type: 'text', text: refused }] };
@@ -50,7 +50,7 @@ function createMcpServer(auth, api, config, version) {
50
50
  status = answer.status;
51
51
  const ok = status >= 200 && status < 300;
52
52
  await (0, audit_1.recordMcpCall)({
53
- userId: auth.userId, tokenId: auth.apiToken.tokenId, projectId, tool: tool.name,
53
+ ...(0, audit_1.auditCallerOf)(auth), projectId, tool: tool.name,
54
54
  ok, status, error: ok ? null : errorText(answer.body, status), durationMs: Date.now() - started,
55
55
  }, config);
56
56
  if (!ok)
@@ -61,7 +61,7 @@ function createMcpServer(auth, api, config, version) {
61
61
  catch (err) {
62
62
  const message = err instanceof Error ? err.message : String(err);
63
63
  await (0, audit_1.recordMcpCall)({
64
- userId: auth.userId, tokenId: auth.apiToken.tokenId, projectId, tool: tool.name,
64
+ ...(0, audit_1.auditCallerOf)(auth), projectId, tool: tool.name,
65
65
  ok: false, status, error: message, durationMs: Date.now() - started,
66
66
  }, config);
67
67
  return { isError: true, content: [{ type: 'text', text: `The call failed: ${message}` }] };
package/dist/mcp/tools.js CHANGED
@@ -358,6 +358,74 @@ exports.TOOLS = [
358
358
  call: ({ projectId, jobId }) => ({ method: 'POST', path: `/projects/${enc(projectId)}/jobs/${enc(jobId)}/retry` }),
359
359
  pick: (data) => data?.job,
360
360
  }),
361
+ // ── Documents ──
362
+ tool({
363
+ name: 'list_documents', title: 'List documents', permissions: ['docs:read'],
364
+ description: "A project's documents (its concept, architecture, decisions...), as the tree of its Documents section shows them: each one's id, title, folder (parentId; a folder's kind is 'folder'), position, current version and the version published through its exports, without their content (read one with get_document).",
365
+ input: { projectId },
366
+ projectOf: byProject,
367
+ call: ({ projectId }) => ({ method: 'GET', path: `/projects/${enc(projectId)}/documents` }),
368
+ pick: (data) => data?.documents ?? [],
369
+ }),
370
+ tool({
371
+ name: 'get_document', title: 'Read a document', permissions: ['docs:read'],
372
+ description: "A document's markdown and its current version, the one an update is made on. With `version`, that past version instead (see list_document_versions).",
373
+ input: { projectId, documentId: id('document'), version: zod_1.z.number().int().min(1).optional().describe('A past version to read; the current one if not given') },
374
+ projectOf: byProject,
375
+ call: ({ projectId, documentId, version }) => ({
376
+ method: 'GET',
377
+ path: `/projects/${enc(projectId)}/documents/${enc(documentId)}${version ? `/versions/${version}` : ''}`,
378
+ }),
379
+ pick: (data) => data?.document ?? data?.version,
380
+ }),
381
+ tool({
382
+ name: 'list_document_versions', title: "List a document's versions", permissions: ['docs:read'],
383
+ description: "A document's history, the latest first: each version's number, title, who saved it (a user, an API token, or an app on a user's behalf) and when.",
384
+ input: { projectId, documentId: id('document') },
385
+ projectOf: byProject,
386
+ call: ({ projectId, documentId }) => ({ method: 'GET', path: `/projects/${enc(projectId)}/documents/${enc(documentId)}/versions` }),
387
+ pick: (data) => data?.versions ?? [],
388
+ }),
389
+ tool({
390
+ name: 'create_document', title: 'Create a document', permissions: ['docs:write'], write: true,
391
+ description: "Creates a markdown document (or, with kind 'folder', a folder to group them) in a project's Documents section, at the end of its folder. It's a draft: nothing is exported until someone publishes a version of it in the UI.",
392
+ input: {
393
+ projectId,
394
+ title: zod_1.z.string().min(1),
395
+ content: zod_1.z.string().optional().describe('Its markdown'),
396
+ kind: zod_1.z.enum(['document', 'folder']).optional(),
397
+ parentId: zod_1.z.string().optional().describe("The folder it goes in (a folder's id); at the top if not given"),
398
+ },
399
+ projectOf: byProject,
400
+ call: ({ projectId, ...fields }) => ({ method: 'POST', path: `/projects/${enc(projectId)}/documents`, body: defined(fields) }),
401
+ pick: (data) => data?.document,
402
+ }),
403
+ tool({
404
+ name: 'update_document', title: 'Update a document', permissions: ['docs:write'], write: true,
405
+ description: "Saves a new version of a document: its whole markdown, its title, or both. `baseVersion` is the version it was read at (get_document): if someone saved another one since, nothing is saved and the answer says so; read it again and make the change on the new version. To change one part, update_document_section rewrites less.",
406
+ input: {
407
+ projectId, documentId: id('document'),
408
+ baseVersion: zod_1.z.number().int().min(1).describe('The version the change was made on'),
409
+ title: zod_1.z.string().min(1).optional(),
410
+ content: zod_1.z.string().optional().describe('The whole new markdown'),
411
+ },
412
+ projectOf: byProject,
413
+ call: ({ projectId, documentId, ...fields }) => ({ method: 'PATCH', path: `/projects/${enc(projectId)}/documents/${enc(documentId)}`, body: defined(fields) }),
414
+ pick: (data) => data?.document,
415
+ }),
416
+ tool({
417
+ name: 'update_document_section', title: "Update a document's section", permissions: ['docs:write'], write: true,
418
+ description: "Saves a new version of a document with only what's under one of its headings replaced (up to the next heading of the same level or above), the rest untouched. `heading` is the heading as written (\"## Architecture\") or its title alone. `baseVersion` is the version it was read at: if the document changed since, nothing is saved; read it again.",
419
+ input: {
420
+ projectId, documentId: id('document'),
421
+ baseVersion: zod_1.z.number().int().min(1).describe('The version the change was made on'),
422
+ heading: zod_1.z.string().min(1).describe('The section\'s heading, e.g. "## Architecture"'),
423
+ content: zod_1.z.string().describe("The section's new markdown, without its heading"),
424
+ },
425
+ projectOf: byProject,
426
+ call: ({ projectId, documentId, ...fields }) => ({ method: 'PATCH', path: `/projects/${enc(projectId)}/documents/${enc(documentId)}/sections`, body: fields }),
427
+ pick: (data) => data?.document,
428
+ }),
361
429
  // ── Projects ──
362
430
  tool({
363
431
  name: 'create_project', title: 'Create a project', permissions: ['projects:create'], write: true,
@@ -28,6 +28,7 @@ const dbConnection_1 = require("./routes/dbConnection");
28
28
  const storageConnection_1 = require("./routes/storageConnection");
29
29
  const userIntegration_1 = require("./routes/userIntegration");
30
30
  const oauth_1 = require("./routes/oauth");
31
+ const oauthResource_1 = require("./routes/oauthResource");
31
32
  const notification_1 = require("./routes/notification");
32
33
  const subscription_1 = require("./routes/subscription");
33
34
  const media_1 = require("./routes/media");
@@ -43,11 +44,14 @@ const worker_1 = require("./routes/worker");
43
44
  const workerCallback_1 = require("./routes/workerCallback");
44
45
  const workerJobs_1 = require("../services/workerJobs");
45
46
  const deliverable_1 = require("./routes/deliverable");
47
+ const document_1 = require("./routes/document");
48
+ const documents_1 = require("../services/documents");
46
49
  function createStreamByRouter(config) {
47
50
  const router = express_1.default.Router();
48
51
  if (config.encrypt)
49
52
  (0, encryption_1.setEncryptionKey)(config.encrypt, config.encryptPreviousKeys);
50
53
  (0, jobStore_1.setJobStoreDatabase)(config);
54
+ (0, documents_1.setDocumentStoreDatabase)(config);
51
55
  // Establish DB connections asynchronously (Postgres/Mongo + schema migrations).
52
56
  // Until they're ready, HOLD incoming requests instead of letting them through —
53
57
  // route handlers resolve projects/exports (e.g. the /menu-list export) against a
@@ -112,6 +116,8 @@ function createStreamByRouter(config) {
112
116
  if (config.websocket?.server) {
113
117
  (0, wsHub_1.initWsHub)(config.websocket.server, config);
114
118
  }
119
+ // Where to sign in for a token (RFC 9728): open, before anything asks for one.
120
+ router.use((0, oauthResource_1.oauthResourceRouter)(config));
115
121
  // /auth answers whether there's a session (it reads it itself); everything after needs one,
116
122
  // but for the public export route (see authenticate).
117
123
  router.use((0, auth_2.authRouter)(config));
@@ -146,5 +152,6 @@ function createStreamByRouter(config) {
146
152
  router.use((0, pipeline_1.pipelineRouter)(config));
147
153
  router.use((0, worker_1.workerRouter)({ ...config, adapter }));
148
154
  router.use((0, deliverable_1.deliverableRouter)({ ...config, adapter }));
155
+ router.use((0, document_1.documentRouter)(config));
149
156
  return router;
150
157
  }
@@ -0,0 +1,3 @@
1
+ import { Router } from 'express';
2
+ import { StreamByConfig } from '../../types';
3
+ export declare function documentRouter(_config: StreamByConfig): Router;