@kelpie/server 0.5.0 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +13 -0
- package/dist/app.js.map +1 -1
- package/dist/modules/workspace/capabilities.d.ts +10 -1
- package/dist/modules/workspace/capabilities.d.ts.map +1 -1
- package/dist/modules/workspace/capabilities.js +13 -0
- package/dist/modules/workspace/capabilities.js.map +1 -1
- package/dist/modules/workspace/index.d.ts.map +1 -1
- package/dist/modules/workspace/index.js +2 -1
- package/dist/modules/workspace/index.js.map +1 -1
- package/dist/modules/workspace/workspaceAccessMiddleware.d.ts +8 -0
- package/dist/modules/workspace/workspaceAccessMiddleware.d.ts.map +1 -0
- package/dist/modules/workspace/workspaceAccessMiddleware.js +66 -0
- package/dist/modules/workspace/workspaceAccessMiddleware.js.map +1 -0
- package/package.json +2 -2
- package/src/app.ts +16 -0
- package/src/modules/workspace/capabilities.ts +15 -1
- package/src/modules/workspace/index.ts +2 -1
- package/src/modules/workspace/workspaceAccessMiddleware.ts +85 -0
package/dist/app.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAC3B,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AAGnC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AAItD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AAC7C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAA;AAC7C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAA;AAEzD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,+BAA+B,CAAA;
|
|
1
|
+
{"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAC3B,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AAGnC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AAItD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AAC7C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAA;AAC7C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAA;AAEzD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,+BAA+B,CAAA;AAS3E,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAA;AAEhE;;;GAGG;AAEH,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,aAAa,EAAE,MAAM,OAAO,CAAC,aAAa,CAAC,CAAA;IACpD,oEAAoE;IACpE,QAAQ,CAAC,aAAa,EAAE,mBAAmB,CAAA;IAC3C;;;;;OAKG;IACH,QAAQ,CAAC,WAAW,EAAE,sBAAsB,CAAA;IAC5C,4DAA4D;IAC5D,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,MAAM,CAAA;IACzC,qFAAqF;IACrF,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,CAAA;IAC7B,QAAQ,CAAC,SAAS,EAAE,eAAe,CAAA;IACnC;;;;;;OAMG;IACH,QAAQ,CAAC,eAAe,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,MAAM,CAAA;CACvD;AAED,sEAAsE;AACtE,MAAM,WAAW,WAAW;IAC1B,SAAS,EAAE;QACT,SAAS,EAAE,MAAM,CAAA;QACjB,MAAM,EAAE,MAAM,CAAA;KACf,CAAA;CACF;AAqBD,wBAAgB,SAAS,CAAC,YAAY,EAAE,eAAe,GAAG,IAAI,CAAC,WAAW,CAAC,CAwJ1E"}
|
package/dist/app.js
CHANGED
|
@@ -8,6 +8,7 @@ import { MCP_INSTRUCTIONS, MCP_ROUTE_PREFIX, MCP_SERVER_INFO } from './modules/m
|
|
|
8
8
|
import { createMcpEndpoint } from './modules/mcp/router.js';
|
|
9
9
|
import { createAuthAndApiRateLimitMiddleware, createFormSubmitRateLimitMiddleware, } from './modules/rate-limit/middleware.js';
|
|
10
10
|
import { createIdempotencyMiddleware } from './modules/workspace/idempotencyMiddleware.js';
|
|
11
|
+
import { createWorkspaceAccessMiddleware } from './modules/workspace/workspaceAccessMiddleware.js';
|
|
11
12
|
const REQUEST_ID_HEADER = 'X-Request-Id';
|
|
12
13
|
/**
|
|
13
14
|
* Any origin, because a public endpoint exists to be called from a customer's
|
|
@@ -58,6 +59,15 @@ export function createApp(dependencies) {
|
|
|
58
59
|
// itself; `createFormSubmitRateLimitMiddleware` below covers it, mounted
|
|
59
60
|
// where the public CORS middleware requires.
|
|
60
61
|
app.use('/v1/*', createAuthAndApiRateLimitMiddleware(rateLimitDependencies));
|
|
62
|
+
// Ahead of idempotency too, for the same reason: a request this blocks
|
|
63
|
+
// must never reserve a key it will not be allowed to spend. Inert without
|
|
64
|
+
// a module granting or denying `workspace.access` (`capabilities.ts`).
|
|
65
|
+
const workspaceAccessMiddleware = createWorkspaceAccessMiddleware({
|
|
66
|
+
db: dependencies.credentials.db,
|
|
67
|
+
now: dependencies.credentials.now,
|
|
68
|
+
entitlements: dependencies.contributions.entitlements,
|
|
69
|
+
});
|
|
70
|
+
app.use('/v1/*', workspaceAccessMiddleware);
|
|
61
71
|
// Every module's `POST` gets this the same way, decided once here rather than
|
|
62
72
|
// per route (`api.md`). It skips `/v1/public/*` itself — a public request has
|
|
63
73
|
// no `Actor` to scope a key to — so it is mounted ahead of the public CORS
|
|
@@ -97,6 +107,9 @@ export function createApp(dependencies) {
|
|
|
97
107
|
// comes through. No CORS layer to mind the order of here, unlike the forms
|
|
98
108
|
// budget — the transport checks its own `Origin` inside the handler.
|
|
99
109
|
app.use(MCP_ROUTE_PREFIX, createAuthAndApiRateLimitMiddleware(rateLimitDependencies));
|
|
110
|
+
// MCP mirrors `/v1` one-to-one, so a workspace this gate blocks must not
|
|
111
|
+
// still be reachable through agent tools.
|
|
112
|
+
app.use(MCP_ROUTE_PREFIX, workspaceAccessMiddleware);
|
|
100
113
|
app.route(MCP_ROUTE_PREFIX, mcp.transport);
|
|
101
114
|
app.route('/v1', mcp.catalog);
|
|
102
115
|
// Module declarations on the app itself, outside /v1 (`runtime/module.ts`).
|
package/dist/app.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app.js","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAE3B,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAGhC,OAAO,EAAE,QAAQ,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAA;AAC1E,OAAO,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAA;AACnD,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAA;AAI9C,OAAO,EAAE,yBAAyB,EAAE,MAAM,0BAA0B,CAAA;AAEpE,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AAC5F,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAA;AAC3D,OAAO,EACL,mCAAmC,EACnC,mCAAmC,GACpC,MAAM,oCAAoC,CAAA;AAC3C,OAAO,EAAE,2BAA2B,EAAE,MAAM,8CAA8C,CAAA;
|
|
1
|
+
{"version":3,"file":"app.js","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { LimitCapability } from '../../runtime/entitlements.ts';
|
|
1
|
+
import type { FlagCapability, LimitCapability } from '../../runtime/entitlements.ts';
|
|
2
2
|
/**
|
|
3
3
|
* Capabilities the workspace module declares.
|
|
4
4
|
*
|
|
@@ -6,4 +6,13 @@ import type { LimitCapability } from '../../runtime/entitlements.ts';
|
|
|
6
6
|
* these constants rather than the string, so a typo is a compile error.
|
|
7
7
|
*/
|
|
8
8
|
export declare const SEATS_LIMIT: LimitCapability;
|
|
9
|
+
/**
|
|
10
|
+
* Whether a workspace's own members may use it at all. Checked once per
|
|
11
|
+
* request by a blanket `/v1` and `/mcp` middleware (`app.ts`), not by any
|
|
12
|
+
* service — this is deliberately coarser than every other capability here.
|
|
13
|
+
* Granted by default: no provider answers it in a self-hosted assembly, so
|
|
14
|
+
* `EntitlementRegistry`'s open-source default (`entitlements.ts`) applies
|
|
15
|
+
* and nothing changes for anyone without a module that supplies one.
|
|
16
|
+
*/
|
|
17
|
+
export declare const WORKSPACE_ACCESS: FlagCapability;
|
|
9
18
|
//# sourceMappingURL=capabilities.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"capabilities.d.ts","sourceRoot":"","sources":["../../../src/modules/workspace/capabilities.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAA;
|
|
1
|
+
{"version":3,"file":"capabilities.d.ts","sourceRoot":"","sources":["../../../src/modules/workspace/capabilities.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAA;AAEpF;;;;;GAKG;AAEH,eAAO,MAAM,WAAW,EAAE,eAIzB,CAAA;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,gBAAgB,EAAE,cAI9B,CAAA"}
|
|
@@ -9,4 +9,17 @@ export const SEATS_LIMIT = {
|
|
|
9
9
|
kind: 'limit',
|
|
10
10
|
description: 'How many people can belong to one workspace. Unlimited in open source.',
|
|
11
11
|
};
|
|
12
|
+
/**
|
|
13
|
+
* Whether a workspace's own members may use it at all. Checked once per
|
|
14
|
+
* request by a blanket `/v1` and `/mcp` middleware (`app.ts`), not by any
|
|
15
|
+
* service — this is deliberately coarser than every other capability here.
|
|
16
|
+
* Granted by default: no provider answers it in a self-hosted assembly, so
|
|
17
|
+
* `EntitlementRegistry`'s open-source default (`entitlements.ts`) applies
|
|
18
|
+
* and nothing changes for anyone without a module that supplies one.
|
|
19
|
+
*/
|
|
20
|
+
export const WORKSPACE_ACCESS = {
|
|
21
|
+
name: 'workspace.access',
|
|
22
|
+
kind: 'flag',
|
|
23
|
+
description: 'Whether this workspace may be used at all. A deployment-wide override, not a plan feature.',
|
|
24
|
+
};
|
|
12
25
|
//# sourceMappingURL=capabilities.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"capabilities.js","sourceRoot":"","sources":["../../../src/modules/workspace/capabilities.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AAEH,MAAM,CAAC,MAAM,WAAW,GAAoB;IAC1C,IAAI,EAAE,aAAa;IACnB,IAAI,EAAE,OAAO;IACb,WAAW,EAAE,wEAAwE;CACtF,CAAA"}
|
|
1
|
+
{"version":3,"file":"capabilities.js","sourceRoot":"","sources":["../../../src/modules/workspace/capabilities.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AAEH,MAAM,CAAC,MAAM,WAAW,GAAoB;IAC1C,IAAI,EAAE,aAAa;IACnB,IAAI,EAAE,OAAO;IACb,WAAW,EAAE,wEAAwE;CACtF,CAAA;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAmB;IAC9C,IAAI,EAAE,kBAAkB;IACxB,IAAI,EAAE,MAAM;IACZ,WAAW,EAAE,4FAA4F;CAC1G,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/modules/workspace/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAA;AAS3D;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,mBAAmB,EAAE,MAAM,GAAG,YAAY,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/modules/workspace/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAA;AAS3D;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,mBAAmB,EAAE,MAAM,GAAG,YAAY,CAoD/E"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { appUrlConfigSchema } from '../../lib/appUrl.js';
|
|
2
2
|
import { parseModuleCapability } from '../../runtime/moduleConfig.js';
|
|
3
|
-
import { SEATS_LIMIT } from './capabilities.js';
|
|
3
|
+
import { SEATS_LIMIT, WORKSPACE_ACCESS } from './capabilities.js';
|
|
4
4
|
import * as repository from './repository.js';
|
|
5
5
|
import { mountWorkspaceRoutes } from './routes.js';
|
|
6
6
|
import * as schema from './schema.js';
|
|
@@ -20,6 +20,7 @@ export function createWorkspaceModule(migrationsDirectory) {
|
|
|
20
20
|
register(context) {
|
|
21
21
|
const config = context.config(appUrlConfigSchema);
|
|
22
22
|
context.entitlements.declare(SEATS_LIMIT);
|
|
23
|
+
context.entitlements.declare(WORKSPACE_ACCESS);
|
|
23
24
|
// Answers `module.<id>` for whatever a config override left undecided
|
|
24
25
|
// (`runtime/registry.ts` registers that provider first, ahead of this
|
|
25
26
|
// one, so a locked value never reaches this query).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/modules/workspace/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAA;AAExD,OAAO,EAAE,qBAAqB,EAAE,MAAM,+BAA+B,CAAA;AACrE,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/modules/workspace/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAA;AAExD,OAAO,EAAE,qBAAqB,EAAE,MAAM,+BAA+B,CAAA;AACrE,OAAO,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAA;AACjE,OAAO,KAAK,UAAU,MAAM,iBAAiB,CAAA;AAC7C,OAAO,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAA;AAClD,OAAO,KAAK,MAAM,MAAM,aAAa,CAAA;AACrC,OAAO,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAA;AACrD,OAAO,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAA;AAEnD;;;;;GAKG;AACH,MAAM,UAAU,qBAAqB,CAAC,mBAA2B;IAC/D,OAAO;QACL,EAAE,EAAE,WAAW;QACf,QAAQ,EAAE,CAAC,MAAM,CAAC;QAClB,UAAU,EAAE,IAAI;QAEhB,QAAQ,CAAC,OAAO;YACd,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,kBAAkB,CAAC,CAAA;YAEjD,OAAO,CAAC,YAAY,CAAC,OAAO,CAAC,WAAW,CAAC,CAAA;YACzC,OAAO,CAAC,YAAY,CAAC,OAAO,CAAC,gBAAgB,CAAC,CAAA;YAE9C,sEAAsE;YACtE,sEAAsE;YACtE,oDAAoD;YACpD,OAAO,CAAC,YAAY,CAAC,OAAO,CAAC,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,EAAE;gBAC7D,MAAM,QAAQ,GAAG,qBAAqB,CAAC,UAAU,CAAC,IAAI,CAAC,CAAA;gBAEvD,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;oBAC3B,OAAO,SAAS,CAAA;gBAClB,CAAC;gBAED,MAAM,OAAO,GAAG,MAAM,UAAU,CAAC,iBAAiB,CAAC,OAAO,CAAC,EAAE,EAAE,WAAW,EAAE,QAAQ,CAAC,CAAA;gBAErF,OAAO,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE,CAAA;YACvF,CAAC,CAAC,CAAA;YAEF,MAAM,OAAO,GAAG,sBAAsB,CAAC;gBACrC,EAAE,EAAE,OAAO,CAAC,EAAE;gBACd,WAAW,EAAE,OAAO,CAAC,WAAW;gBAChC,KAAK,EAAE,OAAO,CAAC,KAAK;gBACpB,QAAQ,EAAE,OAAO,CAAC,QAAQ;gBAC1B,GAAG,EAAE,OAAO,CAAC,GAAG;gBAChB,YAAY,EAAE,OAAO,CAAC,YAAY;gBAClC,UAAU,EAAE,MAAM,CAAC,YAAY;gBAC/B,mBAAmB,EAAE,OAAO,CAAC,aAAa;qBACvC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,UAAU,CAAC;qBACpC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC;gBAC3B,qBAAqB,EAAE,OAAO,CAAC,YAAY;aAC5C,CAAC,CAAA;YAEF,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAA;YAE3C,OAAO,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE;gBACxB,oBAAoB,CAAC,MAAM,EAAE,EAAE,EAAE,EAAE,OAAO,CAAC,EAAE,EAAE,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE,OAAO,EAAE,CAAC,CAAA;YAC7E,CAAC,CAAC,CAAA;YAEF,sBAAsB,CAAC,OAAO,CAAC,GAAG,EAAE,OAAO,CAAC,CAAA;YAE5C,OAAO,OAAO,CAAC,OAAO,EAAE,CAAA;QAC1B,CAAC;KACF,CAAA;AACH,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { MiddlewareHandler } from 'hono';
|
|
2
|
+
import type { EntitlementRegistry } from '../../runtime/entitlements.ts';
|
|
3
|
+
import type { CredentialDependencies } from '../auth/credentials.ts';
|
|
4
|
+
export interface WorkspaceAccessMiddlewareDependencies extends CredentialDependencies {
|
|
5
|
+
readonly entitlements: EntitlementRegistry;
|
|
6
|
+
}
|
|
7
|
+
export declare function createWorkspaceAccessMiddleware(dependencies: WorkspaceAccessMiddlewareDependencies): MiddlewareHandler;
|
|
8
|
+
//# sourceMappingURL=workspaceAccessMiddleware.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workspaceAccessMiddleware.d.ts","sourceRoot":"","sources":["../../../src/modules/workspace/workspaceAccessMiddleware.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAW,iBAAiB,EAAE,MAAM,MAAM,CAAA;AAGtD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,+BAA+B,CAAA;AAGxE,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,wBAAwB,CAAA;AAqCpE,MAAM,WAAW,qCAAsC,SAAQ,sBAAsB;IACnF,QAAQ,CAAC,YAAY,EAAE,mBAAmB,CAAA;CAC3C;AAED,wBAAgB,+BAA+B,CAC7C,YAAY,EAAE,qCAAqC,GAClD,iBAAiB,CAmCnB"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { AppError } from '../../lib/errors.js';
|
|
2
|
+
import { actorWorkspaceId } from '../auth/actor.js';
|
|
3
|
+
import { resolveActorFrom } from '../auth/credentials.js';
|
|
4
|
+
import { WORKSPACE_ACCESS } from './capabilities.js';
|
|
5
|
+
/**
|
|
6
|
+
* `POST /v1/*` and every `/mcp` call carry a fixed workspace for the whole
|
|
7
|
+
* request (`api.md`): a session or key resolves to one `Actor.workspaceId`,
|
|
8
|
+
* and every query it makes is scoped to that workspace server-side. This
|
|
9
|
+
* blocks the request outright when that workspace's `workspace.access`
|
|
10
|
+
* entitlement is not granted — inert in a self-hosted assembly, since
|
|
11
|
+
* nothing registers a `GrantProvider` that answers it there
|
|
12
|
+
* (`capabilities.ts`).
|
|
13
|
+
*
|
|
14
|
+
* Mounted the same way as `createIdempotencyMiddleware`: tolerates the
|
|
15
|
+
* no-credential case, since an unauthenticated `/v1/auth/*` call has
|
|
16
|
+
* nothing to check, and ahead of idempotency in `app.ts` so a blocked
|
|
17
|
+
* request never reserves a key it will not be allowed to spend.
|
|
18
|
+
*
|
|
19
|
+
* Exempts `/v1/auth/*` and `/v1/account*`: they manage the *account*, not
|
|
20
|
+
* the workspace, and a member of a workspace this gate has locked out
|
|
21
|
+
* still needs to sign in, sign out, and see their own account to have any
|
|
22
|
+
* chance of understanding why. There is no `/v1` endpoint to switch a
|
|
23
|
+
* session to a different workspace membership — a session is bound to one
|
|
24
|
+
* at login — so there is nothing further to exempt for that case.
|
|
25
|
+
*
|
|
26
|
+
* Also exempts `DELETE /v1/workspaces/:id` itself, and only that exact
|
|
27
|
+
* path: a suspension must never trap an owner into a workspace they can no
|
|
28
|
+
* longer read or write but also cannot leave by deleting. Every other verb
|
|
29
|
+
* and every sub-resource under `/v1/workspaces/:id` (members, invites,
|
|
30
|
+
* modules) stays gated.
|
|
31
|
+
*/
|
|
32
|
+
const WORKSPACE_DELETE_PATH = /^\/v1\/workspaces\/[^/]+$/u;
|
|
33
|
+
function isExempt(method, path) {
|
|
34
|
+
return path.startsWith('/v1/auth/') || path.startsWith('/v1/account') ||
|
|
35
|
+
(method === 'DELETE' && WORKSPACE_DELETE_PATH.test(path));
|
|
36
|
+
}
|
|
37
|
+
export function createWorkspaceAccessMiddleware(dependencies) {
|
|
38
|
+
return async (context, next) => {
|
|
39
|
+
if (isExempt(context.req.method, context.req.path)) {
|
|
40
|
+
await next();
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const actor = await resolveActorFrom(dependencies, context).catch((error) => {
|
|
44
|
+
if (error instanceof AppError && error.code === 'unauthorized') {
|
|
45
|
+
// No credential to check a workspace against. The handler this
|
|
46
|
+
// reaches answers its own 401; there is nothing this gate can add.
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
throw error;
|
|
50
|
+
});
|
|
51
|
+
const workspaceId = actor === undefined ? null : actorWorkspaceId(actor);
|
|
52
|
+
// Between signup and a first workspace there is nothing to gate
|
|
53
|
+
// (`POST /v1/workspaces`, `POST /v1/invites/accept`): the same case
|
|
54
|
+
// `createIdempotencyMiddleware` lets through unscoped.
|
|
55
|
+
if (workspaceId === null) {
|
|
56
|
+
await next();
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const entitlement = await dependencies.entitlements.check(workspaceId, WORKSPACE_ACCESS.name);
|
|
60
|
+
if (entitlement.kind === 'flag' && !entitlement.granted) {
|
|
61
|
+
throw new AppError('entitlement_required', 'This workspace does not have access');
|
|
62
|
+
}
|
|
63
|
+
await next();
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=workspaceAccessMiddleware.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workspaceAccessMiddleware.js","sourceRoot":"","sources":["../../../src/modules/workspace/workspaceAccessMiddleware.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAA;AAE9C,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAA;AACnD,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAA;AAEzD,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAA;AAEpD;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,MAAM,qBAAqB,GAAG,4BAA4B,CAAA;AAE1D,SAAS,QAAQ,CAAC,MAAc,EAAE,IAAY;IAC5C,OAAO,IAAI,CAAC,UAAU,CAAC,WAAW,CAAC,IAAI,IAAI,CAAC,UAAU,CAAC,aAAa,CAAC;QACnE,CAAC,MAAM,KAAK,QAAQ,IAAI,qBAAqB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAA;AAC7D,CAAC;AAMD,MAAM,UAAU,+BAA+B,CAC7C,YAAmD;IAEnD,OAAO,KAAK,EAAE,OAAgB,EAAE,IAAI,EAAE,EAAE;QACtC,IAAI,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;YACnD,MAAM,IAAI,EAAE,CAAA;YACZ,OAAM;QACR,CAAC;QAED,MAAM,KAAK,GAAG,MAAM,gBAAgB,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;YACnF,IAAI,KAAK,YAAY,QAAQ,IAAI,KAAK,CAAC,IAAI,KAAK,cAAc,EAAE,CAAC;gBAC/D,+DAA+D;gBAC/D,mEAAmE;gBACnE,OAAO,SAAS,CAAA;YAClB,CAAC;YAED,MAAM,KAAK,CAAA;QACb,CAAC,CAAC,CAAA;QAEF,MAAM,WAAW,GAAG,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,gBAAgB,CAAC,KAAK,CAAC,CAAA;QAExE,gEAAgE;QAChE,oEAAoE;QACpE,uDAAuD;QACvD,IAAI,WAAW,KAAK,IAAI,EAAE,CAAC;YACzB,MAAM,IAAI,EAAE,CAAA;YACZ,OAAM;QACR,CAAC;QAED,MAAM,WAAW,GAAG,MAAM,YAAY,CAAC,YAAY,CAAC,KAAK,CAAC,WAAW,EAAE,gBAAgB,CAAC,IAAI,CAAC,CAAA;QAE7F,IAAI,WAAW,CAAC,IAAI,KAAK,MAAM,IAAI,CAAC,WAAW,CAAC,OAAO,EAAE,CAAC;YACxD,MAAM,IAAI,QAAQ,CAAC,sBAAsB,EAAE,qCAAqC,CAAC,CAAA;QACnF,CAAC;QAED,MAAM,IAAI,EAAE,CAAA;IACd,CAAC,CAAA;AACH,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kelpie/server",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.2",
|
|
4
4
|
"description": "The Kelpie service as a library: module runtime, core CRM modules, REST API, MCP surface, and the shared migration pipeline.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"kelpie",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
},
|
|
53
53
|
"dependencies": {
|
|
54
54
|
"@hono/node-server": "^2.0.12",
|
|
55
|
-
"@kelpie/schemas": "^0.5.
|
|
55
|
+
"@kelpie/schemas": "^0.5.2",
|
|
56
56
|
"@node-rs/argon2": "^2.0.2",
|
|
57
57
|
"drizzle-orm": "^0.45.2",
|
|
58
58
|
"hono": "^4.12.32",
|
package/src/app.ts
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
createFormSubmitRateLimitMiddleware,
|
|
19
19
|
} from './modules/rate-limit/middleware.ts'
|
|
20
20
|
import { createIdempotencyMiddleware } from './modules/workspace/idempotencyMiddleware.ts'
|
|
21
|
+
import { createWorkspaceAccessMiddleware } from './modules/workspace/workspaceAccessMiddleware.ts'
|
|
21
22
|
import type { ModuleContributions } from './runtime/registry.ts'
|
|
22
23
|
|
|
23
24
|
/**
|
|
@@ -120,6 +121,17 @@ export function createApp(dependencies: AppDependencies): Hono<AppBindings> {
|
|
|
120
121
|
// where the public CORS middleware requires.
|
|
121
122
|
app.use('/v1/*', createAuthAndApiRateLimitMiddleware(rateLimitDependencies))
|
|
122
123
|
|
|
124
|
+
// Ahead of idempotency too, for the same reason: a request this blocks
|
|
125
|
+
// must never reserve a key it will not be allowed to spend. Inert without
|
|
126
|
+
// a module granting or denying `workspace.access` (`capabilities.ts`).
|
|
127
|
+
const workspaceAccessMiddleware = createWorkspaceAccessMiddleware({
|
|
128
|
+
db: dependencies.credentials.db,
|
|
129
|
+
now: dependencies.credentials.now,
|
|
130
|
+
entitlements: dependencies.contributions.entitlements,
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
app.use('/v1/*', workspaceAccessMiddleware)
|
|
134
|
+
|
|
123
135
|
// Every module's `POST` gets this the same way, decided once here rather than
|
|
124
136
|
// per route (`api.md`). It skips `/v1/public/*` itself — a public request has
|
|
125
137
|
// no `Actor` to scope a key to — so it is mounted ahead of the public CORS
|
|
@@ -169,6 +181,10 @@ export function createApp(dependencies: AppDependencies): Hono<AppBindings> {
|
|
|
169
181
|
// budget — the transport checks its own `Origin` inside the handler.
|
|
170
182
|
app.use(MCP_ROUTE_PREFIX, createAuthAndApiRateLimitMiddleware(rateLimitDependencies))
|
|
171
183
|
|
|
184
|
+
// MCP mirrors `/v1` one-to-one, so a workspace this gate blocks must not
|
|
185
|
+
// still be reachable through agent tools.
|
|
186
|
+
app.use(MCP_ROUTE_PREFIX, workspaceAccessMiddleware)
|
|
187
|
+
|
|
172
188
|
app.route(MCP_ROUTE_PREFIX, mcp.transport)
|
|
173
189
|
app.route('/v1', mcp.catalog)
|
|
174
190
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { LimitCapability } from '../../runtime/entitlements.ts'
|
|
1
|
+
import type { FlagCapability, LimitCapability } from '../../runtime/entitlements.ts'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Capabilities the workspace module declares.
|
|
@@ -12,3 +12,17 @@ export const SEATS_LIMIT: LimitCapability = {
|
|
|
12
12
|
kind: 'limit',
|
|
13
13
|
description: 'How many people can belong to one workspace. Unlimited in open source.',
|
|
14
14
|
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Whether a workspace's own members may use it at all. Checked once per
|
|
18
|
+
* request by a blanket `/v1` and `/mcp` middleware (`app.ts`), not by any
|
|
19
|
+
* service — this is deliberately coarser than every other capability here.
|
|
20
|
+
* Granted by default: no provider answers it in a self-hosted assembly, so
|
|
21
|
+
* `EntitlementRegistry`'s open-source default (`entitlements.ts`) applies
|
|
22
|
+
* and nothing changes for anyone without a module that supplies one.
|
|
23
|
+
*/
|
|
24
|
+
export const WORKSPACE_ACCESS: FlagCapability = {
|
|
25
|
+
name: 'workspace.access',
|
|
26
|
+
kind: 'flag',
|
|
27
|
+
description: 'Whether this workspace may be used at all. A deployment-wide override, not a plan feature.',
|
|
28
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { appUrlConfigSchema } from '../../lib/appUrl.ts'
|
|
2
2
|
import type { KelpieModule } from '../../runtime/module.ts'
|
|
3
3
|
import { parseModuleCapability } from '../../runtime/moduleConfig.ts'
|
|
4
|
-
import { SEATS_LIMIT } from './capabilities.ts'
|
|
4
|
+
import { SEATS_LIMIT, WORKSPACE_ACCESS } from './capabilities.ts'
|
|
5
5
|
import * as repository from './repository.ts'
|
|
6
6
|
import { mountWorkspaceRoutes } from './routes.ts'
|
|
7
7
|
import * as schema from './schema.ts'
|
|
@@ -24,6 +24,7 @@ export function createWorkspaceModule(migrationsDirectory: string): KelpieModule
|
|
|
24
24
|
const config = context.config(appUrlConfigSchema)
|
|
25
25
|
|
|
26
26
|
context.entitlements.declare(SEATS_LIMIT)
|
|
27
|
+
context.entitlements.declare(WORKSPACE_ACCESS)
|
|
27
28
|
|
|
28
29
|
// Answers `module.<id>` for whatever a config override left undecided
|
|
29
30
|
// (`runtime/registry.ts` registers that provider first, ahead of this
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { Context, MiddlewareHandler } from 'hono'
|
|
2
|
+
|
|
3
|
+
import { AppError } from '../../lib/errors.ts'
|
|
4
|
+
import type { EntitlementRegistry } from '../../runtime/entitlements.ts'
|
|
5
|
+
import { actorWorkspaceId } from '../auth/actor.ts'
|
|
6
|
+
import { resolveActorFrom } from '../auth/credentials.ts'
|
|
7
|
+
import type { CredentialDependencies } from '../auth/credentials.ts'
|
|
8
|
+
import { WORKSPACE_ACCESS } from './capabilities.ts'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* `POST /v1/*` and every `/mcp` call carry a fixed workspace for the whole
|
|
12
|
+
* request (`api.md`): a session or key resolves to one `Actor.workspaceId`,
|
|
13
|
+
* and every query it makes is scoped to that workspace server-side. This
|
|
14
|
+
* blocks the request outright when that workspace's `workspace.access`
|
|
15
|
+
* entitlement is not granted — inert in a self-hosted assembly, since
|
|
16
|
+
* nothing registers a `GrantProvider` that answers it there
|
|
17
|
+
* (`capabilities.ts`).
|
|
18
|
+
*
|
|
19
|
+
* Mounted the same way as `createIdempotencyMiddleware`: tolerates the
|
|
20
|
+
* no-credential case, since an unauthenticated `/v1/auth/*` call has
|
|
21
|
+
* nothing to check, and ahead of idempotency in `app.ts` so a blocked
|
|
22
|
+
* request never reserves a key it will not be allowed to spend.
|
|
23
|
+
*
|
|
24
|
+
* Exempts `/v1/auth/*` and `/v1/account*`: they manage the *account*, not
|
|
25
|
+
* the workspace, and a member of a workspace this gate has locked out
|
|
26
|
+
* still needs to sign in, sign out, and see their own account to have any
|
|
27
|
+
* chance of understanding why. There is no `/v1` endpoint to switch a
|
|
28
|
+
* session to a different workspace membership — a session is bound to one
|
|
29
|
+
* at login — so there is nothing further to exempt for that case.
|
|
30
|
+
*
|
|
31
|
+
* Also exempts `DELETE /v1/workspaces/:id` itself, and only that exact
|
|
32
|
+
* path: a suspension must never trap an owner into a workspace they can no
|
|
33
|
+
* longer read or write but also cannot leave by deleting. Every other verb
|
|
34
|
+
* and every sub-resource under `/v1/workspaces/:id` (members, invites,
|
|
35
|
+
* modules) stays gated.
|
|
36
|
+
*/
|
|
37
|
+
const WORKSPACE_DELETE_PATH = /^\/v1\/workspaces\/[^/]+$/u
|
|
38
|
+
|
|
39
|
+
function isExempt(method: string, path: string): boolean {
|
|
40
|
+
return path.startsWith('/v1/auth/') || path.startsWith('/v1/account') ||
|
|
41
|
+
(method === 'DELETE' && WORKSPACE_DELETE_PATH.test(path))
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface WorkspaceAccessMiddlewareDependencies extends CredentialDependencies {
|
|
45
|
+
readonly entitlements: EntitlementRegistry
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function createWorkspaceAccessMiddleware(
|
|
49
|
+
dependencies: WorkspaceAccessMiddlewareDependencies,
|
|
50
|
+
): MiddlewareHandler {
|
|
51
|
+
return async (context: Context, next) => {
|
|
52
|
+
if (isExempt(context.req.method, context.req.path)) {
|
|
53
|
+
await next()
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const actor = await resolveActorFrom(dependencies, context).catch((error: unknown) => {
|
|
58
|
+
if (error instanceof AppError && error.code === 'unauthorized') {
|
|
59
|
+
// No credential to check a workspace against. The handler this
|
|
60
|
+
// reaches answers its own 401; there is nothing this gate can add.
|
|
61
|
+
return undefined
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
throw error
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
const workspaceId = actor === undefined ? null : actorWorkspaceId(actor)
|
|
68
|
+
|
|
69
|
+
// Between signup and a first workspace there is nothing to gate
|
|
70
|
+
// (`POST /v1/workspaces`, `POST /v1/invites/accept`): the same case
|
|
71
|
+
// `createIdempotencyMiddleware` lets through unscoped.
|
|
72
|
+
if (workspaceId === null) {
|
|
73
|
+
await next()
|
|
74
|
+
return
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const entitlement = await dependencies.entitlements.check(workspaceId, WORKSPACE_ACCESS.name)
|
|
78
|
+
|
|
79
|
+
if (entitlement.kind === 'flag' && !entitlement.granted) {
|
|
80
|
+
throw new AppError('entitlement_required', 'This workspace does not have access')
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
await next()
|
|
84
|
+
}
|
|
85
|
+
}
|