@segmentstream-cloud/ontology 0.9.0 → 0.11.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/dist/action.d.ts +19 -0
- package/dist/action.js +1 -0
- package/dist/analyze.d.ts +3 -1
- package/dist/analyze.js +97 -2
- package/dist/bin/migrate.js +3 -2
- package/dist/blob.d.ts +39 -0
- package/dist/blob.js +49 -0
- package/dist/diagnostics.d.ts +3 -3
- package/dist/diagnostics.js +1 -0
- package/dist/entity.d.ts +5 -3
- package/dist/index.d.ts +9 -4
- package/dist/index.js +6 -1
- package/dist/manifest.d.ts +71 -7
- package/dist/manifest.js +18 -1
- package/dist/migrate/apply.d.ts +7 -0
- package/dist/migrate/apply.js +18 -1
- package/dist/migrate/index.d.ts +1 -1
- package/dist/migrate/index.js +1 -1
- package/dist/migrate/model.d.ts +15 -2
- package/dist/migrate/plan.d.ts +6 -0
- package/dist/migrate/plan.js +35 -3
- package/dist/migrate/storage.d.ts +14 -0
- package/dist/migrate/storage.js +95 -0
- package/dist/names.d.ts +9 -1
- package/dist/names.js +11 -0
- package/dist/nitro.d.ts +13 -0
- package/dist/nitro.js +18 -0
- package/dist/property.d.ts +10 -1
- package/dist/property.js +12 -1
- package/dist/query.js +6 -1
- package/dist/runtime/generated-actions.d.ts +3 -0
- package/dist/runtime/generated-actions.js +10 -5
- package/dist/runtime/history.d.ts +3 -0
- package/dist/runtime/oauth.d.ts +9 -0
- package/dist/runtime/oauth.js +165 -0
- package/dist/runtime/pipeline.d.ts +5 -1
- package/dist/runtime/pipeline.js +87 -3
- package/dist/runtime/principals.d.ts +5 -0
- package/dist/runtime/principals.js +15 -0
- package/dist/runtime/records.js +9 -0
- package/dist/runtime/service-binding.d.ts +2 -1
- package/dist/runtime/service-binding.js +15 -2
- package/dist/runtime/writes.js +17 -3
- package/dist/service/api.js +19 -10
- package/dist/service/authentication.d.ts +27 -1
- package/dist/service/authentication.js +48 -26
- package/dist/service/config.d.ts +8 -0
- package/dist/service/config.js +18 -0
- package/dist/service/descriptions.d.ts +8 -0
- package/dist/service/descriptions.js +15 -0
- package/dist/service/errors.d.ts +5 -1
- package/dist/service/errors.js +6 -2
- package/dist/service/handlers.d.ts +1 -4
- package/dist/service/handlers.js +59 -203
- package/dist/service/mcp/server.d.ts +24 -0
- package/dist/service/mcp/server.js +370 -0
- package/dist/service/mcp/tools.d.ts +47 -0
- package/dist/service/mcp/tools.js +218 -0
- package/dist/service/oauth/protocol.d.ts +52 -0
- package/dist/service/oauth/protocol.js +110 -0
- package/dist/service/oauth/server.d.ts +27 -0
- package/dist/service/oauth/server.js +576 -0
- package/dist/service/operations.d.ts +114 -0
- package/dist/service/operations.js +218 -0
- package/dist/service/runtime-contract.d.ts +98 -0
- package/dist/service/schemas.d.ts +18 -2
- package/dist/service/schemas.js +18 -2
- package/dist/service/service.d.ts +3 -3
- package/dist/service/service.js +27 -7
- package/dist/storage/api.d.ts +4 -0
- package/dist/storage/api.js +39 -0
- package/dist/storage/authorization.d.ts +8 -0
- package/dist/storage/authorization.js +30 -0
- package/dist/storage/maintenance-auth.d.ts +20 -0
- package/dist/storage/maintenance-auth.js +24 -0
- package/dist/storage/maintenance.d.ts +7 -0
- package/dist/storage/maintenance.js +50 -0
- package/dist/storage/neon.d.ts +3 -0
- package/dist/storage/neon.js +212 -0
- package/dist/storage/operator.d.ts +10 -0
- package/dist/storage/operator.js +30 -0
- package/dist/storage/provider.d.ts +80 -0
- package/dist/storage/provider.js +20 -0
- package/dist/storage/sessions.d.ts +87 -0
- package/dist/storage/sessions.js +341 -0
- package/dist/storage/tables.d.ts +2 -0
- package/dist/storage/tables.js +55 -0
- package/dist/storage/writes.d.ts +7 -0
- package/dist/storage/writes.js +88 -0
- package/package.json +8 -1
package/dist/service/errors.js
CHANGED
|
@@ -50,11 +50,15 @@ const REQUEST_PARTS = {
|
|
|
50
50
|
function isJsonParseFailure(issue) {
|
|
51
51
|
return (issue._tag === "InvalidValue" && issue.annotations?.message === "Expected a valid JSON body");
|
|
52
52
|
}
|
|
53
|
-
|
|
53
|
+
/**
|
|
54
|
+
* `challenge` is the `WWW-Authenticate` value; the MCP server's names its OAuth metadata, which is
|
|
55
|
+
* how a client learns where members authorize it.
|
|
56
|
+
*/
|
|
57
|
+
export function unauthorized(requestId, detail, challenge = 'Bearer realm="ontology"') {
|
|
54
58
|
return {
|
|
55
59
|
status: 401,
|
|
56
60
|
body: { error: "unauthorized", detail, requestId },
|
|
57
|
-
headers: { "www-authenticate":
|
|
61
|
+
headers: { "www-authenticate": challenge },
|
|
58
62
|
};
|
|
59
63
|
}
|
|
60
64
|
/**
|
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
import { Layer } from "effect";
|
|
2
2
|
import { type HttpApiGroup } from "effect/unstable/httpapi";
|
|
3
|
-
import type { Query } from "../query.js";
|
|
4
3
|
import { type BuiltApi } from "./api.js";
|
|
5
4
|
import type { AfterCommitHook } from "./config.js";
|
|
6
|
-
import {
|
|
5
|
+
import type { ConnectionTransportFactory } from "./connections.js";
|
|
7
6
|
import { type RateLimit } from "./rate-limit.js";
|
|
8
7
|
import { OntologyBackend } from "./runtime-contract.js";
|
|
9
|
-
/** The query the runtime receives: every key present only when set, and no empty `include`. */
|
|
10
|
-
export declare function toQuery(decoded: Query): Query;
|
|
11
8
|
/**
|
|
12
9
|
* Sign-in checks one client may make per minute. A person checks once per sign-in, a few times at
|
|
13
10
|
* most; the limit slows down anyone testing addresses to learn who the app's members are.
|
package/dist/service/handlers.js
CHANGED
|
@@ -1,19 +1,15 @@
|
|
|
1
1
|
import { HttpServerResponse } from "effect/unstable/http";
|
|
2
2
|
import { Effect, Layer, Schema } from "effect";
|
|
3
3
|
import { HttpApiBuilder, OpenApi } from "effect/unstable/httpapi";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { principalFromWire } from "../authoring.js";
|
|
7
|
-
import { toCamelCase } from "../names.js";
|
|
8
|
-
import { PermissionDenied } from "../errors.js";
|
|
9
|
-
import { missingPermission, permissionOf, READ_VERB } from "../permissions.js";
|
|
4
|
+
import { STORAGE_GROUP } from "../storage/api.js";
|
|
5
|
+
import { Conflict } from "../errors.js";
|
|
10
6
|
import { EMAIL_PATTERN } from "../property.js";
|
|
11
7
|
import { renderCatalog } from "../render.js";
|
|
12
8
|
import { SERVICE_GROUP } from "./api.js";
|
|
13
9
|
import { CurrentPrincipal } from "./authentication.js";
|
|
14
10
|
import { catalogFor, graphOf } from "./catalog.js";
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
11
|
+
import { InvalidRequestBody, SignInCheckLimited, schemaIssues } from "./errors.js";
|
|
12
|
+
import { identityToken, makeOperations, requireRead, toQuery, } from "./operations.js";
|
|
17
13
|
import { clientAddress, fixedWindowLimiter, } from "./rate-limit.js";
|
|
18
14
|
import { OntologyBackend } from "./runtime-contract.js";
|
|
19
15
|
import { ActionEnvelope } from "./schemas.js";
|
|
@@ -21,102 +17,6 @@ function dynamicGroup(api, identifier, build) {
|
|
|
21
17
|
const group = HttpApiBuilder.group;
|
|
22
18
|
return group(api, identifier, build);
|
|
23
19
|
}
|
|
24
|
-
/** The query the runtime receives: every key present only when set, and no empty `include`. */
|
|
25
|
-
export function toQuery(decoded) {
|
|
26
|
-
const { include, ...rest } = decoded;
|
|
27
|
-
return include !== undefined && Object.keys(include).length > 0 ? { ...rest, include } : rest;
|
|
28
|
-
}
|
|
29
|
-
function page(result) {
|
|
30
|
-
return { items: result.items, nextCursor: result.nextCursor ?? null };
|
|
31
|
-
}
|
|
32
|
-
function relationshipTargets(plan) {
|
|
33
|
-
return new Map(plan.entities.map((planned) => [
|
|
34
|
-
planned.name,
|
|
35
|
-
new Map(planned.traversals.map((traversal) => [traversal.name, traversal.target])),
|
|
36
|
-
]));
|
|
37
|
-
}
|
|
38
|
-
/** Adds the entities a `where` looks into through relationship filters, at any combinator depth. */
|
|
39
|
-
function filterReads(where, targets, reads) {
|
|
40
|
-
for (const [key, condition] of Object.entries(where ?? {})) {
|
|
41
|
-
if (key === "and" || key === "or") {
|
|
42
|
-
for (const part of condition) {
|
|
43
|
-
filterReads(part, targets, reads);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
else if (key === "not") {
|
|
47
|
-
filterReads(condition, targets, reads);
|
|
48
|
-
}
|
|
49
|
-
else {
|
|
50
|
-
const target = targets.get(key);
|
|
51
|
-
if (target !== undefined) {
|
|
52
|
-
reads.add(target);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
/**
|
|
58
|
-
* The entities a query on `entity` reads: the entity, the entity of every relationship it includes,
|
|
59
|
-
* and the entity of every relationship its filter looks into.
|
|
60
|
-
*/
|
|
61
|
-
function queryReads(entity, query, targets) {
|
|
62
|
-
const own = targets.get(entity) ?? new Map();
|
|
63
|
-
const reads = new Set([entity]);
|
|
64
|
-
for (const name of Object.keys(query.include ?? {})) {
|
|
65
|
-
const target = own.get(name);
|
|
66
|
-
if (target !== undefined) {
|
|
67
|
-
reads.add(target);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
filterReads(query.where, own, reads);
|
|
71
|
-
return [...reads];
|
|
72
|
-
}
|
|
73
|
-
/**
|
|
74
|
-
* Requires `<entity>:read` on every entity a request reads. A missing one answers 403 for the whole
|
|
75
|
-
* request, before anything is read: nothing is silently left out.
|
|
76
|
-
*/
|
|
77
|
-
function requireRead(entities) {
|
|
78
|
-
return Effect.gen(function* () {
|
|
79
|
-
const principal = yield* CurrentPrincipal;
|
|
80
|
-
const missing = missingPermission(entities.map((entity) => permissionOf(entity, READ_VERB)), principal);
|
|
81
|
-
if (missing !== undefined) {
|
|
82
|
-
return yield* new PermissionDenied({
|
|
83
|
-
permission: missing,
|
|
84
|
-
message: `Reading requires ${missing}, which the principal's roles do not grant.`,
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
/**
|
|
90
|
-
* Follows a relationship from one record with the query language: the record's own
|
|
91
|
-
* `<relationship>_id` for a `rel.one`, and for everything to-many a relationship path from the
|
|
92
|
-
* target back to the record (`lead.notes` is notes whose `lead` is the lead, `lead.tags` is tags
|
|
93
|
-
* some of whose `leads` is the lead), combined with the caller's query and paged like any find.
|
|
94
|
-
*/
|
|
95
|
-
function traverse(planned, traversal, request, targets) {
|
|
96
|
-
return Effect.gen(function* () {
|
|
97
|
-
const backend = yield* OntologyBackend;
|
|
98
|
-
const { id } = request.params;
|
|
99
|
-
const query = toQuery(request.query);
|
|
100
|
-
yield* requireRead([planned.name, ...queryReads(traversal.target, query, targets)]);
|
|
101
|
-
const source = yield* backend.get({ entity: planned.name, id });
|
|
102
|
-
if (traversal.kind === "one") {
|
|
103
|
-
const targetId = source[`${traversal.name}_id`];
|
|
104
|
-
if (typeof targetId !== "string") {
|
|
105
|
-
return { items: [], nextCursor: null };
|
|
106
|
-
}
|
|
107
|
-
return page(yield* backend.find({
|
|
108
|
-
entity: traversal.target,
|
|
109
|
-
query: { ...query, where: { id: { eq: targetId } }, limit: 1 },
|
|
110
|
-
}));
|
|
111
|
-
}
|
|
112
|
-
const match = { id: { eq: id } };
|
|
113
|
-
const related = traversal.kind === "inverseOfOne"
|
|
114
|
-
? { [traversal.opposite]: match }
|
|
115
|
-
: { [traversal.opposite]: { some: match } };
|
|
116
|
-
const where = query.where === undefined ? related : { and: [related, query.where] };
|
|
117
|
-
return page(yield* backend.find({ entity: traversal.target, query: { ...query, where } }));
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
20
|
function isJsonMediaType(contentType) {
|
|
121
21
|
if (contentType === undefined || contentType.trim() === "") {
|
|
122
22
|
return true;
|
|
@@ -141,68 +41,7 @@ function readEnvelope(request) {
|
|
|
141
41
|
return yield* Schema.decodeUnknownEffect(ActionEnvelope)(body, { errors: "all" }).pipe(Effect.mapError((error) => new InvalidRequestBody({ reason: "invalid_request", issues: schemaIssues(error.issue) })));
|
|
142
42
|
});
|
|
143
43
|
}
|
|
144
|
-
|
|
145
|
-
* The configured hook as `runAction` runs it after the commit. A failure is logged with the request
|
|
146
|
-
* id here, so the action's caller still gets the committed result.
|
|
147
|
-
*/
|
|
148
|
-
function afterCommitOf(hook, request, ontology) {
|
|
149
|
-
return (committed) => {
|
|
150
|
-
const definition = ontology.config.entities.find((entity) => nameToWire(entity.name) === committed.entity)?.definition;
|
|
151
|
-
return Effect.tryPromise(async () => hook({
|
|
152
|
-
entity: toCamelCase(committed.entity),
|
|
153
|
-
action: toCamelCase(committed.action),
|
|
154
|
-
principal: principalFromWire(committed.principal),
|
|
155
|
-
input: request.generated ? recordFromWire(committed.input, definition) : committed.input,
|
|
156
|
-
result: request.generated
|
|
157
|
-
? generatedResultFromWire(committed.result, committed.action, definition)
|
|
158
|
-
: committed.result,
|
|
159
|
-
edits: editsFromWire(committed.edits, ontology.config.entities),
|
|
160
|
-
requestId: request.requestId,
|
|
161
|
-
})).pipe(Effect.catchCause((cause) => Effect.logError("ontology.after_commit.failed", cause).pipe(Effect.annotateLogs({
|
|
162
|
-
requestId: request.requestId,
|
|
163
|
-
action: `${committed.entity}.${committed.action}`,
|
|
164
|
-
}))));
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
/** The deployment's OIDC token: Vercel sets it on each request, and in the environment of builds. */
|
|
168
|
-
function identityToken(request) {
|
|
169
|
-
const header = request.headers[OIDC_TOKEN_HEADER];
|
|
170
|
-
return typeof header === "string" && header !== "" ? header : process.env.VERCEL_OIDC_TOKEN;
|
|
171
|
-
}
|
|
172
|
-
function runAction(planned, { name, generated }, ontology, hook, connections) {
|
|
173
|
-
return ({ request }) => Effect.gen(function* () {
|
|
174
|
-
const backend = yield* OntologyBackend;
|
|
175
|
-
const principal = yield* CurrentPrincipal;
|
|
176
|
-
const requestId = yield* RequestId;
|
|
177
|
-
const envelope = yield* readEnvelope(request);
|
|
178
|
-
const validateOnly = envelope.options?.validateOnly === true;
|
|
179
|
-
const outcome = yield* backend.runAction({
|
|
180
|
-
entity: planned.name,
|
|
181
|
-
action: name,
|
|
182
|
-
input: envelope.input,
|
|
183
|
-
principal,
|
|
184
|
-
options: { validateOnly },
|
|
185
|
-
requestId,
|
|
186
|
-
connections: connections({
|
|
187
|
-
identityToken: identityToken(request),
|
|
188
|
-
audit: { action: `${planned.name}.${name}`, principalId: principal.id, requestId },
|
|
189
|
-
}),
|
|
190
|
-
...(hook === undefined
|
|
191
|
-
? {}
|
|
192
|
-
: { afterCommit: afterCommitOf(hook, { requestId, generated }, ontology) }),
|
|
193
|
-
});
|
|
194
|
-
const { issues } = outcome.validation;
|
|
195
|
-
if (!outcome.validation.ok && !validateOnly) {
|
|
196
|
-
return yield* new InvalidActionInput({ issues });
|
|
197
|
-
}
|
|
198
|
-
return {
|
|
199
|
-
result: outcome.result ?? null,
|
|
200
|
-
validation: { ok: outcome.validation.ok, issues },
|
|
201
|
-
edits: outcome.edits,
|
|
202
|
-
};
|
|
203
|
-
});
|
|
204
|
-
}
|
|
205
|
-
function entityHandlers(built, planned, hooks, targets, connections) {
|
|
44
|
+
function entityHandlers(built, planned, operationsOf) {
|
|
206
45
|
return dynamicGroup(built.api, planned.name, (handlers) => Effect.gen(function* () {
|
|
207
46
|
if (planned.resolved.entity.definition.source !== undefined) {
|
|
208
47
|
return handlers.handleRaw("sourceCollection", () => requireRead([planned.name]).pipe(Effect.andThen(() => HttpServerResponse.json({
|
|
@@ -211,46 +50,38 @@ function entityHandlers(built, planned, hooks, targets, connections) {
|
|
|
211
50
|
}, { status: 404 }))));
|
|
212
51
|
}
|
|
213
52
|
const backend = yield* OntologyBackend;
|
|
214
|
-
|
|
215
|
-
const find = (query) => requireRead(queryReads(planned.name, query, targets)).pipe(Effect.andThen(() => backend.find({ entity: planned.name, query })), Effect.map(page));
|
|
216
|
-
const readOwn = requireRead([planned.name]);
|
|
53
|
+
const operations = operationsOf(backend);
|
|
217
54
|
let registered = handlers
|
|
218
|
-
.handle("find", (request) => find(toQuery(request.query)))
|
|
219
|
-
.handle("query", (request) => find(toQuery(request.payload)))
|
|
220
|
-
.handle("get", (request) =>
|
|
221
|
-
.handle("history", (request) =>
|
|
55
|
+
.handle("find", (request) => operations.find(planned, toQuery(request.query)))
|
|
56
|
+
.handle("query", (request) => operations.find(planned, toQuery(request.payload)))
|
|
57
|
+
.handle("get", (request) => operations.get(planned, request.params.id))
|
|
58
|
+
.handle("history", (request) => operations.history(planned, request.params.id, request.query));
|
|
59
|
+
for (const property of planned.resolved.properties.filter((property) => property.definition.type === "blob")) {
|
|
60
|
+
registered = registered.handle(`blobs.${property.name}`, (request) => Effect.gen(function* () {
|
|
61
|
+
const principal = yield* CurrentPrincipal;
|
|
62
|
+
if (!backend.blobs) {
|
|
63
|
+
return yield* new Conflict({
|
|
64
|
+
entity: planned.name,
|
|
65
|
+
message: "File storage is not configured.",
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
return yield* backend.blobs.download(planned.name, request.params.id, property.name, request.payload, principal);
|
|
69
|
+
}));
|
|
70
|
+
}
|
|
222
71
|
for (const traversal of planned.traversals) {
|
|
223
|
-
registered = registered.handle(`related.${traversal.name}`, (request) => traverse(planned, traversal, request,
|
|
72
|
+
registered = registered.handle(`related.${traversal.name}`, (request) => operations.traverse(planned, traversal, request.params.id, toQuery(request.query)));
|
|
224
73
|
}
|
|
225
74
|
for (const action of planned.actions) {
|
|
226
|
-
|
|
227
|
-
|
|
75
|
+
registered = registered.handleRaw(`actions.${action.name}`, ({ request }) => readEnvelope(request).pipe(Effect.flatMap((envelope) => operations.runAction(planned, action, {
|
|
76
|
+
input: envelope.input,
|
|
77
|
+
validateOnly: envelope.options?.validateOnly === true,
|
|
78
|
+
...(envelope.options === undefined ? {} : { options: envelope.options }),
|
|
79
|
+
identityToken: identityToken(request),
|
|
80
|
+
}))));
|
|
228
81
|
}
|
|
229
82
|
return registered;
|
|
230
83
|
}));
|
|
231
84
|
}
|
|
232
|
-
/**
|
|
233
|
-
* The caller, as `GET /api/ontology/me` answers: their user record and the roles and permissions
|
|
234
|
-
* authentication resolved. It needs no permission: anyone may learn who they are and what they may do.
|
|
235
|
-
*/
|
|
236
|
-
function callerOf(backend) {
|
|
237
|
-
return Effect.gen(function* () {
|
|
238
|
-
const principal = yield* CurrentPrincipal;
|
|
239
|
-
// Authentication resolved the principal from this user a moment ago; its absence is a defect.
|
|
240
|
-
const user = yield* backend.get({ entity: "user", id: principal.id }).pipe(Effect.orDie);
|
|
241
|
-
return {
|
|
242
|
-
user: {
|
|
243
|
-
id: principal.id,
|
|
244
|
-
name: String(user.name),
|
|
245
|
-
email: typeof user.email === "string" ? user.email : null,
|
|
246
|
-
kind: principal.kind,
|
|
247
|
-
active: user.active === true,
|
|
248
|
-
},
|
|
249
|
-
roles: principal.roles,
|
|
250
|
-
permissions: principal.permissions,
|
|
251
|
-
};
|
|
252
|
-
});
|
|
253
|
-
}
|
|
254
85
|
/**
|
|
255
86
|
* Sign-in checks one client may make per minute. A person checks once per sign-in, a few times at
|
|
256
87
|
* most; the limit slows down anyone testing addresses to learn who the app's members are.
|
|
@@ -277,7 +108,7 @@ function checkSignIn(backend, limiter) {
|
|
|
277
108
|
return yield* backend.checkSignIn(email);
|
|
278
109
|
});
|
|
279
110
|
}
|
|
280
|
-
function serviceHandlers(built) {
|
|
111
|
+
function serviceHandlers(built, operationsOf) {
|
|
281
112
|
const { manifest } = built.plan.ontology;
|
|
282
113
|
const graph = graphOf(manifest);
|
|
283
114
|
const signInLimiter = fixedWindowLimiter(SIGN_IN_CHECK_LIMIT);
|
|
@@ -295,7 +126,7 @@ function serviceHandlers(built) {
|
|
|
295
126
|
return handlers
|
|
296
127
|
.handle("catalog", (request) => Effect.map(permissionsFor(request.query.as), (permissions) => catalogFor(manifest, permissions)))
|
|
297
128
|
.handle("graph", () => Effect.succeed(graph))
|
|
298
|
-
.handle("me", () =>
|
|
129
|
+
.handle("me", () => operationsOf(backend).caller)
|
|
299
130
|
.handle("openapi", () => Effect.sync(() => {
|
|
300
131
|
if (openApi === undefined) {
|
|
301
132
|
openApi = OpenApi.fromApi(built.api);
|
|
@@ -324,12 +155,37 @@ function serviceHandlers(built) {
|
|
|
324
155
|
.handle("signInCheck", checkSignIn(backend, signInLimiter));
|
|
325
156
|
}));
|
|
326
157
|
}
|
|
158
|
+
function storageHandlers(built) {
|
|
159
|
+
return dynamicGroup(built.api, STORAGE_GROUP, (handlers) => Effect.gen(function* () {
|
|
160
|
+
const backend = yield* OntologyBackend;
|
|
161
|
+
const configured = () => backend.blobs
|
|
162
|
+
? Effect.succeed(backend.blobs)
|
|
163
|
+
: Effect.fail(new Conflict({ entity: "upload", message: "File storage is not configured." }));
|
|
164
|
+
let registered = handlers.handle("begin", (request) => Effect.gen(function* () {
|
|
165
|
+
const principal = yield* CurrentPrincipal;
|
|
166
|
+
const blobs = yield* configured();
|
|
167
|
+
return yield* blobs.begin(request.payload, principal);
|
|
168
|
+
}));
|
|
169
|
+
for (const method of ["status", "part", "complete", "cancel"]) {
|
|
170
|
+
registered = registered.handle(method, (request) => Effect.gen(function* () {
|
|
171
|
+
const principal = yield* CurrentPrincipal;
|
|
172
|
+
const blobs = yield* configured();
|
|
173
|
+
return yield* blobs[method](request.params.id, principal);
|
|
174
|
+
}));
|
|
175
|
+
}
|
|
176
|
+
return registered.handle("maintain", () => backend.maintainBlobs ??
|
|
177
|
+
Effect.fail(new Conflict({ entity: "upload", message: "File storage is not configured." })));
|
|
178
|
+
}));
|
|
179
|
+
}
|
|
327
180
|
/** The handlers of every group of the API; they require {@link OntologyBackend}. */
|
|
328
181
|
export function handlersLayer(built, hooks, connections) {
|
|
329
|
-
const
|
|
182
|
+
const operationsOf = (backend) => makeOperations(backend, built.plan, hooks, connections);
|
|
330
183
|
const layers = [
|
|
331
|
-
serviceHandlers(built),
|
|
332
|
-
...built.plan.entities.
|
|
184
|
+
serviceHandlers(built, operationsOf),
|
|
185
|
+
...(built.plan.ontology.manifest.entities.some((entity) => entity.properties.some((property) => property.type === "blob"))
|
|
186
|
+
? [storageHandlers(built)]
|
|
187
|
+
: []),
|
|
188
|
+
...built.plan.entities.map((planned) => entityHandlers(built, planned, operationsOf)),
|
|
333
189
|
];
|
|
334
190
|
const [first, ...rest] = layers;
|
|
335
191
|
return Layer.mergeAll(first, ...rest);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Layer } from "effect";
|
|
2
|
+
import { HttpRouter } from "effect/unstable/http";
|
|
3
|
+
import type { BuiltApi } from "../api.js";
|
|
4
|
+
import { type TokenVerifier } from "../authentication.js";
|
|
5
|
+
import type { AfterCommitHook } from "../config.js";
|
|
6
|
+
import type { ConnectionTransportFactory } from "../connections.js";
|
|
7
|
+
import { RequestId } from "../errors.js";
|
|
8
|
+
import { OntologyBackend } from "../runtime-contract.js";
|
|
9
|
+
/** The URL path of the ontology's MCP server, outside `/api/ontology`: one per app. */
|
|
10
|
+
export declare const MCP_PATH = "/mcp";
|
|
11
|
+
/** The MCP revisions the server speaks, newest first. */
|
|
12
|
+
export declare const MCP_PROTOCOL_VERSIONS: readonly ["2025-11-25", "2025-06-18"];
|
|
13
|
+
export interface McpServerOptions {
|
|
14
|
+
readonly built: BuiltApi;
|
|
15
|
+
readonly hooks: ReadonlyMap<string, AfterCommitHook>;
|
|
16
|
+
readonly connections: ConnectionTransportFactory;
|
|
17
|
+
readonly verifyToken: TokenVerifier;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The ontology's MCP server at `POST /mcp`: Streamable HTTP without sessions, so any instance can
|
|
21
|
+
* answer any request. Its tools run the same operations as the HTTP routes, behind the same
|
|
22
|
+
* authentication. `GET` and `DELETE`, which open a server stream and end a session, answer 405.
|
|
23
|
+
*/
|
|
24
|
+
export declare function mcpLayer(options: McpServerOptions): Layer.Layer<never, never, OntologyBackend | HttpRouter.HttpRouter | HttpRouter.Request<"Requires", RequestId>>;
|