@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
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import type { HttpServerRequest } from "effect/unstable/http";
|
|
3
|
+
import { PermissionDenied } from "../errors.js";
|
|
4
|
+
import type { Query, QueryResult } from "../query.js";
|
|
5
|
+
import { CurrentPrincipal } from "./authentication.js";
|
|
6
|
+
import type { AfterCommitHook } from "./config.js";
|
|
7
|
+
import { type ConnectionTransportFactory } from "./connections.js";
|
|
8
|
+
import { InvalidActionInput, RequestId } from "./errors.js";
|
|
9
|
+
import type { ApiPlan, PlannedAction, PlannedEntity, Traversal } from "./plan.js";
|
|
10
|
+
import type { OntologyBackend } from "./runtime-contract.js";
|
|
11
|
+
/**
|
|
12
|
+
* What the service does for a caller, whatever the transport: the reads and actions of every entity
|
|
13
|
+
* and the caller's own account. The HTTP routes and the MCP tools both call these, so a permission
|
|
14
|
+
* check, a validation or a history row never depends on how the request arrived. Each requires the
|
|
15
|
+
* caller, as {@link CurrentPrincipal}, and the request id.
|
|
16
|
+
*/
|
|
17
|
+
/** The query the runtime receives: every key present only when set, and no empty `include`. */
|
|
18
|
+
export declare function toQuery(decoded: Query): Query;
|
|
19
|
+
/**
|
|
20
|
+
* Requires `<entity>:read` on every entity a request reads. A missing one fails the whole request,
|
|
21
|
+
* before anything is read: nothing is silently left out.
|
|
22
|
+
*/
|
|
23
|
+
export declare function requireRead(entities: ReadonlyArray<string>): Effect.Effect<undefined, PermissionDenied, CurrentPrincipal>;
|
|
24
|
+
/** The deployment's OIDC token: Vercel sets it on each request, and in the environment of builds. */
|
|
25
|
+
export declare function identityToken(request: HttpServerRequest.HttpServerRequest): string | undefined;
|
|
26
|
+
/** An action call, as a transport decoded it. */
|
|
27
|
+
export interface ActionCall {
|
|
28
|
+
/** The input as the caller sent it; the runtime validates it against the action's input schema. */
|
|
29
|
+
readonly input: unknown;
|
|
30
|
+
readonly validateOnly: boolean;
|
|
31
|
+
/** The request's other action options: the idempotency key and create intent of an upload. */
|
|
32
|
+
readonly options?: {
|
|
33
|
+
readonly idempotencyKey?: string;
|
|
34
|
+
readonly createIntent?: string;
|
|
35
|
+
};
|
|
36
|
+
/** The deployment's OIDC token, which authorizes the action's connection calls. */
|
|
37
|
+
readonly identityToken: string | undefined;
|
|
38
|
+
}
|
|
39
|
+
/** What an action answers: `{ result, validation, edits }`. */
|
|
40
|
+
export interface ActionAnswer {
|
|
41
|
+
readonly result: unknown;
|
|
42
|
+
readonly validation: {
|
|
43
|
+
readonly ok: boolean;
|
|
44
|
+
readonly issues: ReadonlyArray<unknown>;
|
|
45
|
+
};
|
|
46
|
+
readonly edits: ReadonlyArray<unknown>;
|
|
47
|
+
}
|
|
48
|
+
/** The caller, as `GET /api/ontology/me` answers. */
|
|
49
|
+
export interface Caller {
|
|
50
|
+
readonly user: {
|
|
51
|
+
readonly id: string;
|
|
52
|
+
readonly name: string;
|
|
53
|
+
readonly email: string | null;
|
|
54
|
+
readonly kind: string;
|
|
55
|
+
readonly active: boolean;
|
|
56
|
+
};
|
|
57
|
+
readonly roles: ReadonlyArray<string>;
|
|
58
|
+
readonly permissions: ReadonlyArray<string>;
|
|
59
|
+
}
|
|
60
|
+
type Backend = OntologyBackend["Service"];
|
|
61
|
+
/** The operations of a planned ontology, bound to its backend, after-commit hooks and connections. */
|
|
62
|
+
export declare function makeOperations(backend: Backend, plan: ApiPlan, hooks: ReadonlyMap<string, AfterCommitHook>, connections: ConnectionTransportFactory): {
|
|
63
|
+
/** A page of records. Requires `read` on the entity and on every entity the query reaches. */
|
|
64
|
+
find: (planned: PlannedEntity, query: Query) => Effect.Effect<QueryResult<Readonly<Record<string, unknown>>>, PermissionDenied | import("../index.js").InvalidQuery, CurrentPrincipal>;
|
|
65
|
+
/** One record. Requires `read` on the entity. */
|
|
66
|
+
get: (planned: PlannedEntity, id: string) => Effect.Effect<Readonly<Record<string, unknown>>, PermissionDenied | import("../errors.js").NotFound, CurrentPrincipal>;
|
|
67
|
+
/** A page of a record's history, newest first. Requires `read` on the entity. */
|
|
68
|
+
history: (planned: PlannedEntity, id: string, params: {
|
|
69
|
+
readonly after?: string;
|
|
70
|
+
readonly limit?: number;
|
|
71
|
+
}) => Effect.Effect<QueryResult<{
|
|
72
|
+
readonly id: string;
|
|
73
|
+
readonly recordId: string;
|
|
74
|
+
readonly action: string;
|
|
75
|
+
readonly actionId: string;
|
|
76
|
+
readonly kind: "create" | "update" | "delete" | "link" | "unlink";
|
|
77
|
+
readonly principalId: string | null;
|
|
78
|
+
readonly at: string;
|
|
79
|
+
readonly changes: {
|
|
80
|
+
readonly [x: string]: {
|
|
81
|
+
readonly before: import("effect/Schema").Json;
|
|
82
|
+
readonly after: import("effect/Schema").Json;
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
}>, PermissionDenied | import("../errors.js").NotFound | import("../index.js").InvalidQuery, CurrentPrincipal>;
|
|
86
|
+
/** The records a relationship reaches from one record, as a page of the target entity. */
|
|
87
|
+
traverse: (planned: PlannedEntity, traversal: Traversal, id: string, query: Query) => Effect.Effect<QueryResult<Readonly<Record<string, unknown>>>, PermissionDenied | import("../errors.js").NotFound | import("../index.js").InvalidQuery, CurrentPrincipal>;
|
|
88
|
+
/** Runs an action as the caller, with the action's after-commit hook and connections. */
|
|
89
|
+
runAction: (planned: PlannedEntity, { name, generated }: PlannedAction, call: ActionCall) => Effect.Effect<{
|
|
90
|
+
result: {} | null;
|
|
91
|
+
validation: {
|
|
92
|
+
ok: boolean;
|
|
93
|
+
issues: readonly {
|
|
94
|
+
readonly path: readonly (string | number)[];
|
|
95
|
+
readonly message: string;
|
|
96
|
+
}[];
|
|
97
|
+
};
|
|
98
|
+
edits: readonly {
|
|
99
|
+
readonly entity: string;
|
|
100
|
+
readonly id: string;
|
|
101
|
+
readonly kind: "create" | "update" | "delete" | "link" | "unlink";
|
|
102
|
+
readonly changes: {
|
|
103
|
+
readonly [x: string]: {
|
|
104
|
+
readonly before: import("effect/Schema").Json;
|
|
105
|
+
readonly after: import("effect/Schema").Json;
|
|
106
|
+
};
|
|
107
|
+
};
|
|
108
|
+
}[];
|
|
109
|
+
}, import("../connection.js").ConnectionError | import("../errors.js").ActionRejected | PermissionDenied | import("../errors.js").Conflict | import("../errors.js").NotFound | InvalidActionInput, RequestId | CurrentPrincipal>;
|
|
110
|
+
/** The caller's user, roles and permissions. It needs no permission. */
|
|
111
|
+
caller: Effect.Effect<Caller, never, CurrentPrincipal>;
|
|
112
|
+
};
|
|
113
|
+
export type Operations = ReturnType<typeof makeOperations>;
|
|
114
|
+
export {};
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { editsFromWire, generatedResultFromWire, recordFromWire } from "../authoring-records.js";
|
|
3
|
+
import { nameToWire } from "../authoring-names.js";
|
|
4
|
+
import { principalFromWire } from "../authoring.js";
|
|
5
|
+
import { PermissionDenied } from "../errors.js";
|
|
6
|
+
import { toCamelCase } from "../names.js";
|
|
7
|
+
import { missingPermission, permissionOf, READ_VERB } from "../permissions.js";
|
|
8
|
+
import { CurrentPrincipal } from "./authentication.js";
|
|
9
|
+
import { OIDC_TOKEN_HEADER } from "./connections.js";
|
|
10
|
+
import { InvalidActionInput, RequestId } from "./errors.js";
|
|
11
|
+
/**
|
|
12
|
+
* What the service does for a caller, whatever the transport: the reads and actions of every entity
|
|
13
|
+
* and the caller's own account. The HTTP routes and the MCP tools both call these, so a permission
|
|
14
|
+
* check, a validation or a history row never depends on how the request arrived. Each requires the
|
|
15
|
+
* caller, as {@link CurrentPrincipal}, and the request id.
|
|
16
|
+
*/
|
|
17
|
+
/** The query the runtime receives: every key present only when set, and no empty `include`. */
|
|
18
|
+
export function toQuery(decoded) {
|
|
19
|
+
const { include, ...rest } = decoded;
|
|
20
|
+
return include !== undefined && Object.keys(include).length > 0 ? { ...rest, include } : rest;
|
|
21
|
+
}
|
|
22
|
+
function page(result) {
|
|
23
|
+
return { items: result.items, nextCursor: result.nextCursor ?? null };
|
|
24
|
+
}
|
|
25
|
+
function relationshipTargets(plan) {
|
|
26
|
+
return new Map(plan.entities.map((planned) => [
|
|
27
|
+
planned.name,
|
|
28
|
+
new Map(planned.traversals.map((traversal) => [traversal.name, traversal.target])),
|
|
29
|
+
]));
|
|
30
|
+
}
|
|
31
|
+
/** Adds the entities a `where` looks into through relationship filters, at any combinator depth. */
|
|
32
|
+
function filterReads(where, targets, reads) {
|
|
33
|
+
for (const [key, condition] of Object.entries(where ?? {})) {
|
|
34
|
+
if (key === "and" || key === "or") {
|
|
35
|
+
for (const part of condition) {
|
|
36
|
+
filterReads(part, targets, reads);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
else if (key === "not") {
|
|
40
|
+
filterReads(condition, targets, reads);
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
const target = targets.get(key);
|
|
44
|
+
if (target !== undefined) {
|
|
45
|
+
reads.add(target);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The entities a query on `entity` reads: the entity, the entity of every relationship it includes,
|
|
52
|
+
* and the entity of every relationship its filter looks into.
|
|
53
|
+
*/
|
|
54
|
+
function queryReads(entity, query, targets) {
|
|
55
|
+
const own = targets.get(entity) ?? new Map();
|
|
56
|
+
const reads = new Set([entity]);
|
|
57
|
+
for (const name of Object.keys(query.include ?? {})) {
|
|
58
|
+
const target = own.get(name);
|
|
59
|
+
if (target !== undefined) {
|
|
60
|
+
reads.add(target);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
filterReads(query.where, own, reads);
|
|
64
|
+
return [...reads];
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Requires `<entity>:read` on every entity a request reads. A missing one fails the whole request,
|
|
68
|
+
* before anything is read: nothing is silently left out.
|
|
69
|
+
*/
|
|
70
|
+
export function requireRead(entities) {
|
|
71
|
+
return Effect.gen(function* () {
|
|
72
|
+
const principal = yield* CurrentPrincipal;
|
|
73
|
+
const missing = missingPermission(entities.map((entity) => permissionOf(entity, READ_VERB)), principal);
|
|
74
|
+
if (missing !== undefined) {
|
|
75
|
+
return yield* new PermissionDenied({
|
|
76
|
+
permission: missing,
|
|
77
|
+
message: `Reading requires ${missing}, which the principal's roles do not grant.`,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* The configured hook as `runAction` runs it after the commit. A failure is logged with the request
|
|
84
|
+
* id here, so the action's caller still gets the committed result.
|
|
85
|
+
*/
|
|
86
|
+
function afterCommitOf(hook, request, ontology) {
|
|
87
|
+
return (committed) => {
|
|
88
|
+
const definition = ontology.config.entities.find((entity) => nameToWire(entity.name) === committed.entity)?.definition;
|
|
89
|
+
return Effect.tryPromise(async () => hook({
|
|
90
|
+
entity: toCamelCase(committed.entity),
|
|
91
|
+
action: toCamelCase(committed.action),
|
|
92
|
+
principal: principalFromWire(committed.principal),
|
|
93
|
+
input: request.generated ? recordFromWire(committed.input, definition) : committed.input,
|
|
94
|
+
result: request.generated
|
|
95
|
+
? generatedResultFromWire(committed.result, committed.action, definition)
|
|
96
|
+
: committed.result,
|
|
97
|
+
edits: editsFromWire(committed.edits, ontology.config.entities),
|
|
98
|
+
requestId: request.requestId,
|
|
99
|
+
})).pipe(Effect.catchCause((cause) => Effect.logError("ontology.after_commit.failed", cause).pipe(Effect.annotateLogs({
|
|
100
|
+
requestId: request.requestId,
|
|
101
|
+
action: `${committed.entity}.${committed.action}`,
|
|
102
|
+
}))));
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
/** The deployment's OIDC token: Vercel sets it on each request, and in the environment of builds. */
|
|
106
|
+
export function identityToken(request) {
|
|
107
|
+
const header = request.headers[OIDC_TOKEN_HEADER];
|
|
108
|
+
return typeof header === "string" && header !== "" ? header : process.env.VERCEL_OIDC_TOKEN;
|
|
109
|
+
}
|
|
110
|
+
function findOf(backend, targets, entity, query) {
|
|
111
|
+
// Every read checks the caller's permissions first; the backend is called only afterwards.
|
|
112
|
+
return requireRead(queryReads(entity, query, targets)).pipe(Effect.andThen(() => backend.find({ entity, query })), Effect.map(page));
|
|
113
|
+
}
|
|
114
|
+
function getOf(backend, entity, id) {
|
|
115
|
+
return requireRead([entity]).pipe(Effect.andThen(() => backend.get({ entity, id })));
|
|
116
|
+
}
|
|
117
|
+
function historyOf(backend, entity, id, params) {
|
|
118
|
+
return requireRead([entity]).pipe(Effect.andThen(() => backend.history({ entity, id, ...params })), Effect.map(page));
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Follows a relationship from one record with the query language: the record's own
|
|
122
|
+
* `<relationship>_id` for a `rel.one`, and for everything to-many a relationship path from the
|
|
123
|
+
* target back to the record (`lead.notes` is notes whose `lead` is the lead, `lead.tags` is tags
|
|
124
|
+
* some of whose `leads` is the lead), combined with the caller's query and paged like any find.
|
|
125
|
+
*/
|
|
126
|
+
function traverseOf(backend, targets, planned, traversal, id, query) {
|
|
127
|
+
return Effect.gen(function* () {
|
|
128
|
+
yield* requireRead([planned.name, ...queryReads(traversal.target, query, targets)]);
|
|
129
|
+
const source = yield* backend.get({ entity: planned.name, id });
|
|
130
|
+
if (traversal.kind === "one") {
|
|
131
|
+
const targetId = source[`${traversal.name}_id`];
|
|
132
|
+
if (typeof targetId !== "string") {
|
|
133
|
+
return { items: [], nextCursor: null };
|
|
134
|
+
}
|
|
135
|
+
return page(yield* backend.find({
|
|
136
|
+
entity: traversal.target,
|
|
137
|
+
query: { ...query, where: { id: { eq: targetId } }, limit: 1 },
|
|
138
|
+
}));
|
|
139
|
+
}
|
|
140
|
+
const match = { id: { eq: id } };
|
|
141
|
+
const related = traversal.kind === "inverseOfOne"
|
|
142
|
+
? { [traversal.opposite]: match }
|
|
143
|
+
: { [traversal.opposite]: { some: match } };
|
|
144
|
+
const where = query.where === undefined ? related : { and: [related, query.where] };
|
|
145
|
+
return page(yield* backend.find({ entity: traversal.target, query: { ...query, where } }));
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
function runActionOf(backend, ontology, hooks, connections) {
|
|
149
|
+
return (planned, { name, generated }, call) => Effect.gen(function* () {
|
|
150
|
+
const principal = yield* CurrentPrincipal;
|
|
151
|
+
const requestId = yield* RequestId;
|
|
152
|
+
const hook = hooks.get(`${planned.name}.${name}`);
|
|
153
|
+
const outcome = yield* backend.runAction({
|
|
154
|
+
entity: planned.name,
|
|
155
|
+
action: name,
|
|
156
|
+
input: call.input,
|
|
157
|
+
principal,
|
|
158
|
+
options: { ...call.options, validateOnly: call.validateOnly },
|
|
159
|
+
requestId,
|
|
160
|
+
connections: connections({
|
|
161
|
+
identityToken: call.identityToken,
|
|
162
|
+
audit: { action: `${planned.name}.${name}`, principalId: principal.id, requestId },
|
|
163
|
+
}),
|
|
164
|
+
...(hook === undefined
|
|
165
|
+
? {}
|
|
166
|
+
: { afterCommit: afterCommitOf(hook, { requestId, generated }, ontology) }),
|
|
167
|
+
});
|
|
168
|
+
const { issues } = outcome.validation;
|
|
169
|
+
if (!outcome.validation.ok && !call.validateOnly) {
|
|
170
|
+
return yield* new InvalidActionInput({ issues });
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
result: outcome.result ?? null,
|
|
174
|
+
validation: { ok: outcome.validation.ok, issues },
|
|
175
|
+
edits: outcome.edits,
|
|
176
|
+
};
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* The caller: their user record and the roles and permissions authentication resolved. Anyone may
|
|
181
|
+
* learn who they are and what they may do.
|
|
182
|
+
*/
|
|
183
|
+
function callerOf(backend) {
|
|
184
|
+
return Effect.gen(function* () {
|
|
185
|
+
const principal = yield* CurrentPrincipal;
|
|
186
|
+
// Authentication resolved the principal from this user a moment ago; its absence is a defect.
|
|
187
|
+
const user = yield* backend.get({ entity: "user", id: principal.id }).pipe(Effect.orDie);
|
|
188
|
+
return {
|
|
189
|
+
user: {
|
|
190
|
+
id: principal.id,
|
|
191
|
+
name: String(user.name),
|
|
192
|
+
email: typeof user.email === "string" ? user.email : null,
|
|
193
|
+
kind: principal.kind,
|
|
194
|
+
active: user.active === true,
|
|
195
|
+
},
|
|
196
|
+
roles: principal.roles,
|
|
197
|
+
permissions: principal.permissions,
|
|
198
|
+
};
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
/** The operations of a planned ontology, bound to its backend, after-commit hooks and connections. */
|
|
202
|
+
export function makeOperations(backend, plan, hooks, connections) {
|
|
203
|
+
const targets = relationshipTargets(plan);
|
|
204
|
+
return {
|
|
205
|
+
/** A page of records. Requires `read` on the entity and on every entity the query reaches. */
|
|
206
|
+
find: (planned, query) => findOf(backend, targets, planned.name, query),
|
|
207
|
+
/** One record. Requires `read` on the entity. */
|
|
208
|
+
get: (planned, id) => getOf(backend, planned.name, id),
|
|
209
|
+
/** A page of a record's history, newest first. Requires `read` on the entity. */
|
|
210
|
+
history: (planned, id, params) => historyOf(backend, planned.name, id, params),
|
|
211
|
+
/** The records a relationship reaches from one record, as a page of the target entity. */
|
|
212
|
+
traverse: (planned, traversal, id, query) => traverseOf(backend, targets, planned, traversal, id, query),
|
|
213
|
+
/** Runs an action as the caller, with the action's after-commit hook and connections. */
|
|
214
|
+
runAction: runActionOf(backend, plan.ontology, hooks, connections),
|
|
215
|
+
/** The caller's user, roles and permissions. It needs no permission. */
|
|
216
|
+
caller: callerOf(backend),
|
|
217
|
+
};
|
|
218
|
+
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { Context, Schema, type Effect, type Layer, type Redacted } from "effect";
|
|
2
|
+
import type { BlobRuntime } from "../storage/sessions.js";
|
|
3
|
+
import type { BlobStorageBinding } from "../storage/provider.js";
|
|
2
4
|
import type { AnyAction } from "../action.js";
|
|
3
5
|
import type { ResolvedEntity } from "../analyze.js";
|
|
4
6
|
import type { ConnectionError, ConnectionTransport } from "../connection.js";
|
|
@@ -23,6 +25,8 @@ export interface RunActionInput {
|
|
|
23
25
|
readonly principal: Principal;
|
|
24
26
|
readonly options: {
|
|
25
27
|
readonly validateOnly?: boolean;
|
|
28
|
+
readonly idempotencyKey?: string;
|
|
29
|
+
readonly createIntent?: string;
|
|
26
30
|
};
|
|
27
31
|
/**
|
|
28
32
|
* Runs once the action has committed, outside its transaction, before `runAction` returns. It
|
|
@@ -62,7 +66,98 @@ declare const NotInvited_base: Schema.Class<NotInvited, Schema.TaggedStruct<"Not
|
|
|
62
66
|
*/
|
|
63
67
|
export declare class NotInvited extends NotInvited_base {
|
|
64
68
|
}
|
|
69
|
+
/** How a client proves itself at the token endpoint; `none` for a public client, with PKCE only. */
|
|
70
|
+
export type OAuthClientAuthMethod = "none" | "client_secret_post" | "client_secret_basic";
|
|
71
|
+
/** A client that registered itself to connect an agent (RFC 7591 dynamic registration). */
|
|
72
|
+
export interface OAuthClient {
|
|
73
|
+
readonly id: string;
|
|
74
|
+
readonly name: string;
|
|
75
|
+
readonly redirectUris: ReadonlyArray<string>;
|
|
76
|
+
readonly authMethod: OAuthClientAuthMethod;
|
|
77
|
+
/** SHA-256 of a confidential client's secret; null for a public client. */
|
|
78
|
+
readonly secretHash: string | null;
|
|
79
|
+
readonly clientUri: string | null;
|
|
80
|
+
}
|
|
81
|
+
/** An authorization request waiting, for a few minutes, for a member's consent. */
|
|
82
|
+
export interface OAuthRequest {
|
|
83
|
+
readonly id: string;
|
|
84
|
+
readonly clientId: string;
|
|
85
|
+
readonly redirectUri: string;
|
|
86
|
+
readonly state: string | null;
|
|
87
|
+
/** The PKCE S256 challenge the token request must answer. */
|
|
88
|
+
readonly codeChallenge: string;
|
|
89
|
+
readonly scope: string;
|
|
90
|
+
/** The MCP server the tokens are for (RFC 8707). */
|
|
91
|
+
readonly resource: string;
|
|
92
|
+
readonly expiresAt: Date;
|
|
93
|
+
}
|
|
94
|
+
/** What a member allowed a client to do. Its tokens stop working once it is revoked. */
|
|
95
|
+
export interface OAuthGrant {
|
|
96
|
+
readonly id: string;
|
|
97
|
+
readonly clientId: string;
|
|
98
|
+
readonly userId: string;
|
|
99
|
+
readonly scope: string;
|
|
100
|
+
readonly resource: string;
|
|
101
|
+
}
|
|
102
|
+
export type OAuthTokenKind = "code" | "access" | "refresh";
|
|
103
|
+
/** A code or token of a grant, stored only as the SHA-256 of its value. */
|
|
104
|
+
export interface OAuthToken {
|
|
105
|
+
readonly hash: string;
|
|
106
|
+
readonly grantId: string;
|
|
107
|
+
readonly kind: OAuthTokenKind;
|
|
108
|
+
readonly expiresAt: Date;
|
|
109
|
+
/** For a code: the redirect URI it was sent to, which the token request must repeat. */
|
|
110
|
+
readonly redirectUri?: string;
|
|
111
|
+
/** For a code: the PKCE challenge the token request's verifier must answer. */
|
|
112
|
+
readonly codeChallenge?: string;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* A code or refresh token presented once more: `reused` when it was used before, which means it
|
|
116
|
+
* leaked, and the caller revokes its grant.
|
|
117
|
+
*/
|
|
118
|
+
export type UsedOAuthToken = {
|
|
119
|
+
readonly status: "valid";
|
|
120
|
+
readonly token: OAuthToken;
|
|
121
|
+
readonly grant: OAuthGrant;
|
|
122
|
+
} | {
|
|
123
|
+
readonly status: "reused";
|
|
124
|
+
readonly grant: OAuthGrant;
|
|
125
|
+
} | {
|
|
126
|
+
readonly status: "invalid";
|
|
127
|
+
};
|
|
128
|
+
/**
|
|
129
|
+
* Where the MCP server's authorization server keeps clients, pending requests, grants and tokens:
|
|
130
|
+
* the runtime's OAuth tables in the workspace database. Expiry is checked against the database's
|
|
131
|
+
* clock; failures are defects.
|
|
132
|
+
*/
|
|
133
|
+
export interface OAuthStore {
|
|
134
|
+
readonly saveClient: (client: OAuthClient) => Effect.Effect<void>;
|
|
135
|
+
readonly findClient: (id: string) => Effect.Effect<OAuthClient | undefined>;
|
|
136
|
+
/** Stores a request, and forgets the expired ones. */
|
|
137
|
+
readonly saveRequest: (request: OAuthRequest) => Effect.Effect<void>;
|
|
138
|
+
/** A request that has not expired. */
|
|
139
|
+
readonly findRequest: (id: string) => Effect.Effect<OAuthRequest | undefined>;
|
|
140
|
+
/** Removes a request that has not expired and returns it: a request is decided once. */
|
|
141
|
+
readonly takeRequest: (id: string) => Effect.Effect<OAuthRequest | undefined>;
|
|
142
|
+
/** Records a grant with its authorization code. */
|
|
143
|
+
readonly saveGrant: (grant: OAuthGrant, code: OAuthToken) => Effect.Effect<void>;
|
|
144
|
+
/** Uses a code or refresh token, once, before it expires and while its grant stands. */
|
|
145
|
+
readonly useToken: (hash: string, kind: "code" | "refresh") => Effect.Effect<UsedOAuthToken>;
|
|
146
|
+
/** Stores new tokens, and forgets the expired ones. */
|
|
147
|
+
readonly saveTokens: (tokens: ReadonlyArray<OAuthToken>) => Effect.Effect<void>;
|
|
148
|
+
/** The grant of an access token that has not expired, unless the grant was revoked. */
|
|
149
|
+
readonly findAccess: (hash: string) => Effect.Effect<OAuthGrant | undefined>;
|
|
150
|
+
/** The grant of a token of any kind, revoked or not. */
|
|
151
|
+
readonly findGrantOfToken: (hash: string) => Effect.Effect<OAuthGrant | undefined>;
|
|
152
|
+
readonly revokeGrant: (id: string) => Effect.Effect<void>;
|
|
153
|
+
/** The member a grant acts for, with the roles they hold now; undefined once they may not sign in. */
|
|
154
|
+
readonly memberPrincipal: (userId: string) => Effect.Effect<Principal | undefined>;
|
|
155
|
+
}
|
|
65
156
|
declare const OntologyBackend_base: Context.ServiceClass<OntologyBackend, "@segmentstream-cloud/ontology/OntologyBackend", {
|
|
157
|
+
readonly blobs?: BlobRuntime;
|
|
158
|
+
readonly maintainBlobs?: Effect.Effect<{
|
|
159
|
+
readonly removed: number;
|
|
160
|
+
}, unknown>;
|
|
66
161
|
readonly runAction: (input: RunActionInput) => Effect.Effect<ActionOutcome, PermissionDenied | ActionRejected | Conflict | NotFound | ConnectionError>;
|
|
67
162
|
readonly get: (input: {
|
|
68
163
|
readonly entity: string;
|
|
@@ -94,6 +189,8 @@ declare const OntologyBackend_base: Context.ServiceClass<OntologyBackend, "@segm
|
|
|
94
189
|
readonly resolveApiKey: (key: Redacted.Redacted<string>) => Effect.Effect<Principal | undefined>;
|
|
95
190
|
/** The manifest hash recorded in the `_ontology` catalog table, or `undefined` before the first. */
|
|
96
191
|
readonly appliedHash: Effect.Effect<string | undefined>;
|
|
192
|
+
/** The OAuth tables, for members who connect agents; without it, agents use API keys. */
|
|
193
|
+
readonly oauth?: OAuthStore;
|
|
97
194
|
}>;
|
|
98
195
|
/**
|
|
99
196
|
* The runtime as the HTTP service sees it, with its database and ontology already bound. Failures
|
|
@@ -111,6 +208,7 @@ export interface OntologyServiceRuntime {
|
|
|
111
208
|
readonly connect: (input: {
|
|
112
209
|
readonly ontology: Ontology;
|
|
113
210
|
readonly databaseUrl: Redacted.Redacted<string>;
|
|
211
|
+
readonly blobStorage?: Redacted.Redacted<BlobStorageBinding>;
|
|
114
212
|
}) => Layer.Layer<OntologyBackend, unknown>;
|
|
115
213
|
}
|
|
116
214
|
export {};
|
|
@@ -124,6 +124,8 @@ export declare const ActionEnvelope: Schema.Codec<{
|
|
|
124
124
|
readonly input: unknown;
|
|
125
125
|
readonly options?: {
|
|
126
126
|
readonly validateOnly?: boolean;
|
|
127
|
+
readonly idempotencyKey?: string;
|
|
128
|
+
readonly createIntent?: string;
|
|
127
129
|
};
|
|
128
130
|
}>;
|
|
129
131
|
export declare const ActionResponse: Schema.Struct<{
|
|
@@ -151,7 +153,7 @@ export declare const Catalog: Schema.decodeTo<Schema.Struct<{
|
|
|
151
153
|
readonly column: Schema.optionalKey<Schema.String>;
|
|
152
154
|
readonly scope: Schema.optionalKey<Schema.$Array<Schema.String>>;
|
|
153
155
|
readonly name: Schema.String;
|
|
154
|
-
readonly type: Schema.Literals<readonly ["text", "email", "url", "integer", "number", "boolean", "date", "timestamp", "select", "json"]>;
|
|
156
|
+
readonly type: Schema.Literals<readonly ["text", "email", "url", "integer", "number", "boolean", "date", "timestamp", "select", "json", "blob"]>;
|
|
155
157
|
readonly label: Schema.String;
|
|
156
158
|
readonly description: Schema.optionalKey<Schema.String>;
|
|
157
159
|
readonly optional: Schema.Boolean;
|
|
@@ -163,6 +165,8 @@ export declare const Catalog: Schema.decodeTo<Schema.Struct<{
|
|
|
163
165
|
readonly min: Schema.optionalKey<Schema.Number>;
|
|
164
166
|
readonly max: Schema.optionalKey<Schema.Number>;
|
|
165
167
|
readonly maxLength: Schema.optionalKey<Schema.Number>;
|
|
168
|
+
readonly accept: Schema.optionalKey<Schema.$Array<Schema.String>>;
|
|
169
|
+
readonly maxBytes: Schema.optionalKey<Schema.Number>;
|
|
166
170
|
readonly options: Schema.optionalKey<Schema.$Array<Schema.Struct<{
|
|
167
171
|
readonly value: Schema.String;
|
|
168
172
|
readonly label: Schema.String;
|
|
@@ -171,6 +175,7 @@ export declare const Catalog: Schema.decodeTo<Schema.Struct<{
|
|
|
171
175
|
}>, {
|
|
172
176
|
readonly renamedFrom: "renamed_from";
|
|
173
177
|
readonly maxLength: "max_length";
|
|
178
|
+
readonly maxBytes: "max_bytes";
|
|
174
179
|
}>>;
|
|
175
180
|
readonly relationships: Schema.$Array<Schema.encodeKeys<Schema.Struct<{
|
|
176
181
|
readonly on: Schema.optionalKey<Schema.encodeKeys<Schema.Struct<{
|
|
@@ -223,6 +228,10 @@ export declare const Catalog: Schema.decodeTo<Schema.Struct<{
|
|
|
223
228
|
readonly permission: Schema.optionalKey<Schema.String>;
|
|
224
229
|
readonly input: Schema.$Record<Schema.String, Schema.Codec<Schema.Json, Schema.Json, never, never>>;
|
|
225
230
|
readonly connections: Schema.optionalKey<Schema.$Array<Schema.String>>;
|
|
231
|
+
readonly hints: Schema.optionalKey<Schema.Struct<{
|
|
232
|
+
readonly destructive: Schema.optionalKey<Schema.Boolean>;
|
|
233
|
+
readonly idempotent: Schema.optionalKey<Schema.Boolean>;
|
|
234
|
+
}>>;
|
|
226
235
|
}>>;
|
|
227
236
|
readonly permissions: Schema.optionalKey<Schema.Struct<{
|
|
228
237
|
readonly read: Schema.String;
|
|
@@ -259,7 +268,7 @@ export declare const Catalog: Schema.decodeTo<Schema.Struct<{
|
|
|
259
268
|
readonly column: Schema.optionalKey<Schema.String>;
|
|
260
269
|
readonly scope: Schema.optionalKey<Schema.$Array<Schema.String>>;
|
|
261
270
|
readonly name: Schema.String;
|
|
262
|
-
readonly type: Schema.Literals<readonly ["text", "email", "url", "integer", "number", "boolean", "date", "timestamp", "select", "json"]>;
|
|
271
|
+
readonly type: Schema.Literals<readonly ["text", "email", "url", "integer", "number", "boolean", "date", "timestamp", "select", "json", "blob"]>;
|
|
263
272
|
readonly label: Schema.String;
|
|
264
273
|
readonly description: Schema.optionalKey<Schema.String>;
|
|
265
274
|
readonly optional: Schema.Boolean;
|
|
@@ -271,6 +280,8 @@ export declare const Catalog: Schema.decodeTo<Schema.Struct<{
|
|
|
271
280
|
readonly min: Schema.optionalKey<Schema.Number>;
|
|
272
281
|
readonly max: Schema.optionalKey<Schema.Number>;
|
|
273
282
|
readonly maxLength: Schema.optionalKey<Schema.Number>;
|
|
283
|
+
readonly accept: Schema.optionalKey<Schema.$Array<Schema.String>>;
|
|
284
|
+
readonly maxBytes: Schema.optionalKey<Schema.Number>;
|
|
274
285
|
readonly options: Schema.optionalKey<Schema.$Array<Schema.Struct<{
|
|
275
286
|
readonly value: Schema.String;
|
|
276
287
|
readonly label: Schema.String;
|
|
@@ -279,6 +290,7 @@ export declare const Catalog: Schema.decodeTo<Schema.Struct<{
|
|
|
279
290
|
}>, {
|
|
280
291
|
readonly renamedFrom: "renamed_from";
|
|
281
292
|
readonly maxLength: "max_length";
|
|
293
|
+
readonly maxBytes: "max_bytes";
|
|
282
294
|
}>>;
|
|
283
295
|
readonly relationships: Schema.$Array<Schema.encodeKeys<Schema.Struct<{
|
|
284
296
|
readonly on: Schema.optionalKey<Schema.encodeKeys<Schema.Struct<{
|
|
@@ -331,6 +343,10 @@ export declare const Catalog: Schema.decodeTo<Schema.Struct<{
|
|
|
331
343
|
readonly permission: Schema.optionalKey<Schema.String>;
|
|
332
344
|
readonly input: Schema.$Record<Schema.String, Schema.Codec<Schema.Json, Schema.Json, never, never>>;
|
|
333
345
|
readonly connections: Schema.optionalKey<Schema.$Array<Schema.String>>;
|
|
346
|
+
readonly hints: Schema.optionalKey<Schema.Struct<{
|
|
347
|
+
readonly destructive: Schema.optionalKey<Schema.Boolean>;
|
|
348
|
+
readonly idempotent: Schema.optionalKey<Schema.Boolean>;
|
|
349
|
+
}>>;
|
|
334
350
|
}>>;
|
|
335
351
|
readonly permissions: Schema.optionalKey<Schema.Struct<{
|
|
336
352
|
readonly read: Schema.String;
|
package/dist/service/schemas.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { BlobValue } from "../blob.js";
|
|
1
2
|
import { Schema, SchemaTransformation } from "effect";
|
|
2
3
|
import { wireStruct } from "../wire.js";
|
|
3
4
|
import { OntologyManifestWire } from "../manifest.js";
|
|
@@ -68,6 +69,8 @@ function propertyValue(definition) {
|
|
|
68
69
|
}
|
|
69
70
|
case "json":
|
|
70
71
|
return Schema.Json.annotate(documentation);
|
|
72
|
+
case "blob":
|
|
73
|
+
return BlobValue.annotate(documentation);
|
|
71
74
|
}
|
|
72
75
|
}
|
|
73
76
|
function recordFields(entity) {
|
|
@@ -226,11 +229,20 @@ export const HistoryPage = Schema.Struct({
|
|
|
226
229
|
})
|
|
227
230
|
.pipe(Schema.encodeKeys({ nextCursor: "next_cursor" }))
|
|
228
231
|
.annotate({ identifier: "HistoryPage" });
|
|
232
|
+
const uploadOptions = {
|
|
233
|
+
idempotencyKey: Schema.optionalKey(Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(128))),
|
|
234
|
+
createIntent: Schema.optionalKey(Schema.String.check(Schema.isUUID())),
|
|
235
|
+
};
|
|
229
236
|
const ActionOptions = wireStruct({
|
|
237
|
+
...uploadOptions,
|
|
230
238
|
validateOnly: Schema.optionalKey(Schema.Boolean.annotate({
|
|
231
239
|
description: "Validate the input and report issues without a write.",
|
|
232
240
|
})),
|
|
233
|
-
}, {
|
|
241
|
+
}, {
|
|
242
|
+
validateOnly: "validate_only",
|
|
243
|
+
idempotencyKey: "idempotency_key",
|
|
244
|
+
createIntent: "create_intent",
|
|
245
|
+
});
|
|
234
246
|
/** The body of `POST /<entity>/actions/<action>`, as documented. */
|
|
235
247
|
export function actionRequest(input) {
|
|
236
248
|
return Schema.Struct({ input, options: Schema.optionalKey(ActionOptions) });
|
|
@@ -238,7 +250,11 @@ export function actionRequest(input) {
|
|
|
238
250
|
/** The body of `POST /<entity>/actions/<action>`, as decoded: `runAction` validates the input. */
|
|
239
251
|
export const ActionEnvelope = closedStruct({
|
|
240
252
|
input: Schema.Unknown,
|
|
241
|
-
options: Schema.optionalKey(wireStruct({ validateOnly: Schema.optionalKey(Schema.Boolean) }, {
|
|
253
|
+
options: Schema.optionalKey(wireStruct({ validateOnly: Schema.optionalKey(Schema.Boolean), ...uploadOptions }, {
|
|
254
|
+
validateOnly: "validate_only",
|
|
255
|
+
idempotencyKey: "idempotency_key",
|
|
256
|
+
createIntent: "create_intent",
|
|
257
|
+
})),
|
|
242
258
|
}, "Unknown key; use input and options");
|
|
243
259
|
export const ActionResponse = Schema.Struct({
|
|
244
260
|
result: Schema.Unknown.annotate({ description: "What the action returned." }),
|
|
@@ -13,8 +13,8 @@ export interface OntologyService {
|
|
|
13
13
|
readonly dispose: () => Promise<void>;
|
|
14
14
|
}
|
|
15
15
|
/**
|
|
16
|
-
* The ontology service: the derived HttpApi
|
|
17
|
-
* API-key authentication. Every response carries `x-ontology-hash`
|
|
18
|
-
* answer with a JSON error and never a stack or SQL.
|
|
16
|
+
* The ontology service: the derived HttpApi and the MCP server at `/mcp`, wired to the runtime,
|
|
17
|
+
* behind Neon Auth, app-token and API-key authentication. Every response carries `x-ontology-hash`
|
|
18
|
+
* and `x-request-id`; failures answer with a JSON error and never a stack or SQL.
|
|
19
19
|
*/
|
|
20
20
|
export declare function createOntologyService(ontology: Ontology, config: OntologyServiceConfig): OntologyService;
|