@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.
Files changed (90) hide show
  1. package/dist/action.d.ts +19 -0
  2. package/dist/action.js +1 -0
  3. package/dist/analyze.d.ts +3 -1
  4. package/dist/analyze.js +97 -2
  5. package/dist/bin/migrate.js +3 -2
  6. package/dist/blob.d.ts +39 -0
  7. package/dist/blob.js +49 -0
  8. package/dist/diagnostics.d.ts +3 -3
  9. package/dist/diagnostics.js +1 -0
  10. package/dist/entity.d.ts +5 -3
  11. package/dist/index.d.ts +9 -4
  12. package/dist/index.js +6 -1
  13. package/dist/manifest.d.ts +71 -7
  14. package/dist/manifest.js +18 -1
  15. package/dist/migrate/apply.d.ts +7 -0
  16. package/dist/migrate/apply.js +18 -1
  17. package/dist/migrate/index.d.ts +1 -1
  18. package/dist/migrate/index.js +1 -1
  19. package/dist/migrate/model.d.ts +15 -2
  20. package/dist/migrate/plan.d.ts +6 -0
  21. package/dist/migrate/plan.js +35 -3
  22. package/dist/migrate/storage.d.ts +14 -0
  23. package/dist/migrate/storage.js +95 -0
  24. package/dist/names.d.ts +9 -1
  25. package/dist/names.js +11 -0
  26. package/dist/nitro.d.ts +13 -0
  27. package/dist/nitro.js +18 -0
  28. package/dist/property.d.ts +10 -1
  29. package/dist/property.js +12 -1
  30. package/dist/query.js +6 -1
  31. package/dist/runtime/generated-actions.d.ts +3 -0
  32. package/dist/runtime/generated-actions.js +10 -5
  33. package/dist/runtime/history.d.ts +3 -0
  34. package/dist/runtime/oauth.d.ts +9 -0
  35. package/dist/runtime/oauth.js +165 -0
  36. package/dist/runtime/pipeline.d.ts +5 -1
  37. package/dist/runtime/pipeline.js +87 -3
  38. package/dist/runtime/principals.d.ts +5 -0
  39. package/dist/runtime/principals.js +15 -0
  40. package/dist/runtime/records.js +9 -0
  41. package/dist/runtime/service-binding.d.ts +2 -1
  42. package/dist/runtime/service-binding.js +15 -2
  43. package/dist/runtime/writes.js +17 -3
  44. package/dist/service/api.js +19 -10
  45. package/dist/service/authentication.d.ts +27 -1
  46. package/dist/service/authentication.js +48 -26
  47. package/dist/service/config.d.ts +8 -0
  48. package/dist/service/config.js +18 -0
  49. package/dist/service/descriptions.d.ts +8 -0
  50. package/dist/service/descriptions.js +15 -0
  51. package/dist/service/errors.d.ts +5 -1
  52. package/dist/service/errors.js +6 -2
  53. package/dist/service/handlers.d.ts +1 -4
  54. package/dist/service/handlers.js +59 -203
  55. package/dist/service/mcp/server.d.ts +24 -0
  56. package/dist/service/mcp/server.js +370 -0
  57. package/dist/service/mcp/tools.d.ts +47 -0
  58. package/dist/service/mcp/tools.js +218 -0
  59. package/dist/service/oauth/protocol.d.ts +52 -0
  60. package/dist/service/oauth/protocol.js +110 -0
  61. package/dist/service/oauth/server.d.ts +27 -0
  62. package/dist/service/oauth/server.js +576 -0
  63. package/dist/service/operations.d.ts +114 -0
  64. package/dist/service/operations.js +218 -0
  65. package/dist/service/runtime-contract.d.ts +98 -0
  66. package/dist/service/schemas.d.ts +18 -2
  67. package/dist/service/schemas.js +18 -2
  68. package/dist/service/service.d.ts +3 -3
  69. package/dist/service/service.js +27 -7
  70. package/dist/storage/api.d.ts +4 -0
  71. package/dist/storage/api.js +39 -0
  72. package/dist/storage/authorization.d.ts +8 -0
  73. package/dist/storage/authorization.js +30 -0
  74. package/dist/storage/maintenance-auth.d.ts +20 -0
  75. package/dist/storage/maintenance-auth.js +24 -0
  76. package/dist/storage/maintenance.d.ts +7 -0
  77. package/dist/storage/maintenance.js +50 -0
  78. package/dist/storage/neon.d.ts +3 -0
  79. package/dist/storage/neon.js +212 -0
  80. package/dist/storage/operator.d.ts +10 -0
  81. package/dist/storage/operator.js +30 -0
  82. package/dist/storage/provider.d.ts +80 -0
  83. package/dist/storage/provider.js +20 -0
  84. package/dist/storage/sessions.d.ts +87 -0
  85. package/dist/storage/sessions.js +341 -0
  86. package/dist/storage/tables.d.ts +2 -0
  87. package/dist/storage/tables.js +55 -0
  88. package/dist/storage/writes.d.ts +7 -0
  89. package/dist/storage/writes.js +88 -0
  90. 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;
@@ -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
- }, { validateOnly: "validate_only" });
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) }, { validateOnly: "validate_only" })),
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, wired to the runtime, behind Neon Auth, app-token and
17
- * API-key authentication. Every response carries `x-ontology-hash` and `x-request-id`; failures
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;