@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/migrate/storage.js
CHANGED
|
@@ -29,6 +29,7 @@ export const COLUMN_TYPES = {
|
|
|
29
29
|
date: "date",
|
|
30
30
|
timestamp: "timestamptz",
|
|
31
31
|
json: "jsonb",
|
|
32
|
+
blob: "uuid",
|
|
32
33
|
};
|
|
33
34
|
const ON_DELETE_ACTIONS = {
|
|
34
35
|
setNull: "SET NULL",
|
|
@@ -198,6 +199,15 @@ function foreignKeyDefinition(input) {
|
|
|
198
199
|
`REFERENCES ${quoteIdentifier(input.references)} (${quoteIdentifier(SYSTEM_COLUMNS.id)})`;
|
|
199
200
|
return input.onDelete === undefined ? reference : `${reference} ON DELETE ${input.onDelete}`;
|
|
200
201
|
}
|
|
202
|
+
export function blobForeignKey(entity, property) {
|
|
203
|
+
const column = columnName(property.name);
|
|
204
|
+
return {
|
|
205
|
+
name: foreignKeyName(entity, column),
|
|
206
|
+
kind: "foreignKey",
|
|
207
|
+
definition: foreignKeyDefinition({ column, references: "_blobs" }),
|
|
208
|
+
references: "_blobs",
|
|
209
|
+
};
|
|
210
|
+
}
|
|
201
211
|
function userReference(table, column) {
|
|
202
212
|
return {
|
|
203
213
|
name: foreignKeyName(table, column),
|
|
@@ -389,6 +399,9 @@ export function entityStorage(entity) {
|
|
|
389
399
|
userReference(table, SYSTEM_COLUMNS.created_by),
|
|
390
400
|
userReference(table, SYSTEM_COLUMNS.updated_by),
|
|
391
401
|
...checks,
|
|
402
|
+
...entity.properties
|
|
403
|
+
.filter((property) => property.type === "blob")
|
|
404
|
+
.map((property) => blobForeignKey(entity.name, property)),
|
|
392
405
|
...ones.map((relationship) => relationshipForeignKey(entity.name, relationship)),
|
|
393
406
|
],
|
|
394
407
|
indexes: entityIndexes(entity),
|
|
@@ -420,6 +433,87 @@ export const USER_ROLES_TABLE = "_user_roles";
|
|
|
420
433
|
* secret only as a salted PBKDF2 hash.
|
|
421
434
|
*/
|
|
422
435
|
export const API_KEYS_TABLE = "_api_keys";
|
|
436
|
+
/** OAuth clients that registered themselves to connect agents to the MCP server. */
|
|
437
|
+
export const OAUTH_CLIENTS_TABLE = "_oauth_clients";
|
|
438
|
+
/** Authorization requests waiting for a member's consent, for a few minutes. */
|
|
439
|
+
export const OAUTH_REQUESTS_TABLE = "_oauth_requests";
|
|
440
|
+
/** What a member allowed a client to do: revoking a grant stops its tokens. */
|
|
441
|
+
export const OAUTH_GRANTS_TABLE = "_oauth_grants";
|
|
442
|
+
/** Authorization codes, access tokens and refresh tokens, stored only as SHA-256 hashes. */
|
|
443
|
+
export const OAUTH_TOKENS_TABLE = "_oauth_tokens";
|
|
444
|
+
/** The OAuth tables, in the order they are created. */
|
|
445
|
+
export const OAUTH_TABLES = [
|
|
446
|
+
OAUTH_CLIENTS_TABLE,
|
|
447
|
+
OAUTH_REQUESTS_TABLE,
|
|
448
|
+
OAUTH_GRANTS_TABLE,
|
|
449
|
+
OAUTH_TOKENS_TABLE,
|
|
450
|
+
];
|
|
451
|
+
/** The OAuth tables of the MCP server's authorization server. Every statement is idempotent. */
|
|
452
|
+
export const OAUTH_TABLES_DDL = [
|
|
453
|
+
[
|
|
454
|
+
`CREATE TABLE IF NOT EXISTS ${quoteIdentifier(OAUTH_CLIENTS_TABLE)} (`,
|
|
455
|
+
` "id" text NOT NULL,`,
|
|
456
|
+
` "name" text NOT NULL,`,
|
|
457
|
+
` "redirect_uris" jsonb NOT NULL,`,
|
|
458
|
+
` "auth_method" text NOT NULL,`,
|
|
459
|
+
` "secret_hash" text,`,
|
|
460
|
+
` "client_uri" text,`,
|
|
461
|
+
` "created_at" timestamptz NOT NULL DEFAULT now(),`,
|
|
462
|
+
` CONSTRAINT "${OAUTH_CLIENTS_TABLE}_pkey" PRIMARY KEY ("id")`,
|
|
463
|
+
`)`,
|
|
464
|
+
].join("\n"),
|
|
465
|
+
[
|
|
466
|
+
`CREATE TABLE IF NOT EXISTS ${quoteIdentifier(OAUTH_REQUESTS_TABLE)} (`,
|
|
467
|
+
` "id" text NOT NULL,`,
|
|
468
|
+
` "client_id" text NOT NULL,`,
|
|
469
|
+
` "redirect_uri" text NOT NULL,`,
|
|
470
|
+
` "state" text,`,
|
|
471
|
+
` "code_challenge" text NOT NULL,`,
|
|
472
|
+
` "scope" text NOT NULL,`,
|
|
473
|
+
` "resource" text NOT NULL,`,
|
|
474
|
+
` "expires_at" timestamptz NOT NULL,`,
|
|
475
|
+
` CONSTRAINT "${OAUTH_REQUESTS_TABLE}_pkey" PRIMARY KEY ("id"),`,
|
|
476
|
+
` CONSTRAINT "${OAUTH_REQUESTS_TABLE}_client_id_fkey" FOREIGN KEY ("client_id") ` +
|
|
477
|
+
`REFERENCES ${quoteIdentifier(OAUTH_CLIENTS_TABLE)} ("id") ON DELETE CASCADE`,
|
|
478
|
+
`)`,
|
|
479
|
+
].join("\n"),
|
|
480
|
+
[
|
|
481
|
+
`CREATE TABLE IF NOT EXISTS ${quoteIdentifier(OAUTH_GRANTS_TABLE)} (`,
|
|
482
|
+
` "id" uuid NOT NULL,`,
|
|
483
|
+
` "client_id" text NOT NULL,`,
|
|
484
|
+
` "user_id" uuid NOT NULL,`,
|
|
485
|
+
` "scope" text NOT NULL,`,
|
|
486
|
+
` "resource" text NOT NULL,`,
|
|
487
|
+
` "created_at" timestamptz NOT NULL DEFAULT now(),`,
|
|
488
|
+
` "revoked_at" timestamptz,`,
|
|
489
|
+
` CONSTRAINT "${OAUTH_GRANTS_TABLE}_pkey" PRIMARY KEY ("id"),`,
|
|
490
|
+
` CONSTRAINT "${OAUTH_GRANTS_TABLE}_client_id_fkey" FOREIGN KEY ("client_id") ` +
|
|
491
|
+
`REFERENCES ${quoteIdentifier(OAUTH_CLIENTS_TABLE)} ("id") ON DELETE CASCADE,`,
|
|
492
|
+
` CONSTRAINT "${OAUTH_GRANTS_TABLE}_user_id_fkey" FOREIGN KEY ("user_id") ` +
|
|
493
|
+
`REFERENCES ${quoteIdentifier(USER_TABLE)} ("id") ON DELETE CASCADE`,
|
|
494
|
+
`)`,
|
|
495
|
+
].join("\n"),
|
|
496
|
+
`CREATE INDEX IF NOT EXISTS "${OAUTH_GRANTS_TABLE}_user_id_idx" ` +
|
|
497
|
+
`ON ${quoteIdentifier(OAUTH_GRANTS_TABLE)} ("user_id")`,
|
|
498
|
+
[
|
|
499
|
+
`CREATE TABLE IF NOT EXISTS ${quoteIdentifier(OAUTH_TOKENS_TABLE)} (`,
|
|
500
|
+
` "hash" text NOT NULL,`,
|
|
501
|
+
` "grant_id" uuid NOT NULL,`,
|
|
502
|
+
` "kind" text NOT NULL,`,
|
|
503
|
+
` "redirect_uri" text,`,
|
|
504
|
+
` "code_challenge" text,`,
|
|
505
|
+
` "expires_at" timestamptz NOT NULL,`,
|
|
506
|
+
` "used_at" timestamptz,`,
|
|
507
|
+
` "created_at" timestamptz NOT NULL DEFAULT now(),`,
|
|
508
|
+
` CONSTRAINT "${OAUTH_TOKENS_TABLE}_pkey" PRIMARY KEY ("hash"),`,
|
|
509
|
+
` CONSTRAINT "${OAUTH_TOKENS_TABLE}_kind_check" CHECK ("kind" IN ('code', 'access', 'refresh')),`,
|
|
510
|
+
` CONSTRAINT "${OAUTH_TOKENS_TABLE}_grant_id_fkey" FOREIGN KEY ("grant_id") ` +
|
|
511
|
+
`REFERENCES ${quoteIdentifier(OAUTH_GRANTS_TABLE)} ("id") ON DELETE CASCADE`,
|
|
512
|
+
`)`,
|
|
513
|
+
].join("\n"),
|
|
514
|
+
`CREATE INDEX IF NOT EXISTS "${OAUTH_TOKENS_TABLE}_grant_id_idx" ` +
|
|
515
|
+
`ON ${quoteIdentifier(OAUTH_TOKENS_TABLE)} ("grant_id")`,
|
|
516
|
+
];
|
|
423
517
|
/**
|
|
424
518
|
* The runtime's own tables beside the catalog. They are created with it, after the entity tables
|
|
425
519
|
* because they refer to the built-in user table, and every statement is idempotent.
|
|
@@ -451,6 +545,7 @@ export const RUNTIME_TABLES_DDL = [
|
|
|
451
545
|
`)`,
|
|
452
546
|
].join("\n"),
|
|
453
547
|
`CREATE INDEX IF NOT EXISTS "${API_KEYS_TABLE}_user_id_idx" ON ${quoteIdentifier(API_KEYS_TABLE)} ("user_id")`,
|
|
548
|
+
...OAUTH_TABLES_DDL,
|
|
454
549
|
];
|
|
455
550
|
export function columnDefinition(column) {
|
|
456
551
|
const parts = [quoteIdentifier(column.name), column.type];
|
package/dist/names.d.ts
CHANGED
|
@@ -21,7 +21,15 @@ export declare const HISTORY_SUFFIX = "_history";
|
|
|
21
21
|
/** The combinators of the query language, which take the place of field names in a `where`. */
|
|
22
22
|
export declare const RESERVED_MEMBER_NAMES: readonly ["and", "or", "not"];
|
|
23
23
|
/** Routes of the ontology service under `/api/ontology/`, which an entity's routes would shadow. */
|
|
24
|
-
export declare const RESERVED_ENTITY_NAMES: readonly ["graph", "status", "describe", "openapi", "docs", "mcp", "me"];
|
|
24
|
+
export declare const RESERVED_ENTITY_NAMES: readonly ["graph", "status", "describe", "openapi", "docs", "mcp", "me", "oauth"];
|
|
25
|
+
/** The MCP tools of every stored entity's reads: `lead_find`, `lead_get` and `lead_history`. */
|
|
26
|
+
export declare const MCP_READ_TOOLS: readonly ["find", "get", "history"];
|
|
27
|
+
/** The MCP tools of the ontology as a whole. No entity tool can take them: they have no underscore. */
|
|
28
|
+
export declare const MCP_GLOBAL_TOOLS: readonly ["describe", "me"];
|
|
29
|
+
/** The longest tool name that Claude and OpenAI accept. */
|
|
30
|
+
export declare const MCP_TOOL_NAME_MAX_LENGTH = 64;
|
|
31
|
+
/** The MCP tool of an entity's read or action: `lead_find`, `lead_change_status`. */
|
|
32
|
+
export declare function mcpToolName(entity: string, operation: string): string;
|
|
25
33
|
/** `/api/ontology/<entity>/{id}/history`, which a relationship's traversal would shadow. */
|
|
26
34
|
export declare const RESERVED_RELATIONSHIP_NAMES: readonly ["history"];
|
|
27
35
|
export declare function isSystemProperty(name: string): boolean;
|
package/dist/names.js
CHANGED
|
@@ -57,7 +57,18 @@ export const RESERVED_ENTITY_NAMES = [
|
|
|
57
57
|
"docs",
|
|
58
58
|
"mcp",
|
|
59
59
|
"me",
|
|
60
|
+
"oauth",
|
|
60
61
|
];
|
|
62
|
+
/** The MCP tools of every stored entity's reads: `lead_find`, `lead_get` and `lead_history`. */
|
|
63
|
+
export const MCP_READ_TOOLS = ["find", "get", "history"];
|
|
64
|
+
/** The MCP tools of the ontology as a whole. No entity tool can take them: they have no underscore. */
|
|
65
|
+
export const MCP_GLOBAL_TOOLS = ["describe", "me"];
|
|
66
|
+
/** The longest tool name that Claude and OpenAI accept. */
|
|
67
|
+
export const MCP_TOOL_NAME_MAX_LENGTH = 64;
|
|
68
|
+
/** The MCP tool of an entity's read or action: `lead_find`, `lead_change_status`. */
|
|
69
|
+
export function mcpToolName(entity, operation) {
|
|
70
|
+
return `${entity}_${operation}`;
|
|
71
|
+
}
|
|
61
72
|
/** `/api/ontology/<entity>/{id}/history`, which a relationship's traversal would shadow. */
|
|
62
73
|
export const RESERVED_RELATIONSHIP_NAMES = ["history"];
|
|
63
74
|
export function isSystemProperty(name) {
|
package/dist/nitro.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The paths the ontology service answers, for the workspace's `ontology/nitro.config.ts`. They come
|
|
3
|
+
* from the SDK, so a path the service adds arrives with an upgrade and the config stays as it is.
|
|
4
|
+
* This entry imports nothing: Nitro loads it while it builds.
|
|
5
|
+
*/
|
|
6
|
+
export declare const ONTOLOGY_SERVICE_PATHS: readonly ["/api/ontology", "/api/ontology/**", "/mcp", "/.well-known/oauth-protected-resource", "/.well-known/oauth-protected-resource/mcp", "/.well-known/oauth-authorization-server"];
|
|
7
|
+
/** A Nitro route to the service's handler, loaded on its first request. */
|
|
8
|
+
export interface OntologyRoute {
|
|
9
|
+
readonly handler: string;
|
|
10
|
+
readonly lazy: true;
|
|
11
|
+
}
|
|
12
|
+
/** The Nitro routes that send every path of the ontology service to one handler. */
|
|
13
|
+
export declare function ontologyRoutes(handler: string): Record<string, OntologyRoute>;
|
package/dist/nitro.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The paths the ontology service answers, for the workspace's `ontology/nitro.config.ts`. They come
|
|
3
|
+
* from the SDK, so a path the service adds arrives with an upgrade and the config stays as it is.
|
|
4
|
+
* This entry imports nothing: Nitro loads it while it builds.
|
|
5
|
+
*/
|
|
6
|
+
export const ONTOLOGY_SERVICE_PATHS = [
|
|
7
|
+
"/api/ontology",
|
|
8
|
+
"/api/ontology/**",
|
|
9
|
+
"/mcp",
|
|
10
|
+
// The MCP server's OAuth metadata, on the app's origin.
|
|
11
|
+
"/.well-known/oauth-protected-resource",
|
|
12
|
+
"/.well-known/oauth-protected-resource/mcp",
|
|
13
|
+
"/.well-known/oauth-authorization-server",
|
|
14
|
+
];
|
|
15
|
+
/** The Nitro routes that send every path of the ontology service to one handler. */
|
|
16
|
+
export function ontologyRoutes(handler) {
|
|
17
|
+
return Object.fromEntries(ONTOLOGY_SERVICE_PATHS.map((path) => [path, { handler, lazy: true }]));
|
|
18
|
+
}
|
package/dist/property.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Schema } from "effect";
|
|
2
|
+
import { BlobValue, type BLOB_CONTENT_TYPES } from "./blob.js";
|
|
2
3
|
/** The closed set of storage types. Each has a known column, JSON Schema, filters and rendering. */
|
|
3
|
-
export declare const PROPERTY_TYPES: readonly ["text", "email", "url", "integer", "number", "boolean", "date", "timestamp", "select", "json"];
|
|
4
|
+
export declare const PROPERTY_TYPES: readonly ["text", "email", "url", "integer", "number", "boolean", "date", "timestamp", "select", "json", "blob"];
|
|
4
5
|
export type PropertyType = (typeof PROPERTY_TYPES)[number];
|
|
5
6
|
/** Schema annotation that carries a property's storage metadata. */
|
|
6
7
|
export declare const PROPERTY_ANNOTATION = "segmentstream/ontology/property";
|
|
@@ -40,6 +41,8 @@ export interface PropertyDefinition {
|
|
|
40
41
|
readonly max?: number;
|
|
41
42
|
readonly maxLength?: number;
|
|
42
43
|
readonly options?: ReadonlyArray<SelectOption>;
|
|
44
|
+
readonly accept?: ReadonlyArray<string>;
|
|
45
|
+
readonly maxBytes?: number;
|
|
43
46
|
}
|
|
44
47
|
/**
|
|
45
48
|
* A property: an Effect Schema of its value, usable in action inputs, with its storage metadata in
|
|
@@ -127,6 +130,11 @@ declare function ref<const O extends ReferenceOptions>(target: string | {
|
|
|
127
130
|
readonly name: string;
|
|
128
131
|
}, options?: O): ReferenceOf<O>;
|
|
129
132
|
/** Property constructors. */
|
|
133
|
+
export interface BlobOptions extends Omit<PropertyOptions<BlobValue>, "default" | "unique"> {
|
|
134
|
+
readonly accept: ReadonlyArray<(typeof BLOB_CONTENT_TYPES)[number]>;
|
|
135
|
+
readonly maxBytes?: number;
|
|
136
|
+
}
|
|
137
|
+
declare function blob<const O extends BlobOptions>(options: O): PropertyOf<BlobValue, O>;
|
|
130
138
|
export declare const p: {
|
|
131
139
|
readonly text: typeof text;
|
|
132
140
|
readonly email: typeof email;
|
|
@@ -138,6 +146,7 @@ export declare const p: {
|
|
|
138
146
|
readonly timestamp: typeof timestamp;
|
|
139
147
|
readonly select: typeof select;
|
|
140
148
|
readonly json: typeof json;
|
|
149
|
+
readonly blob: typeof blob;
|
|
141
150
|
readonly ref: typeof ref;
|
|
142
151
|
};
|
|
143
152
|
/** The storage metadata of a schema built with a `p.*` constructor, or `undefined` for any other value. */
|
package/dist/property.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Schema } from "effect";
|
|
2
2
|
import { humanize } from "./names.js";
|
|
3
|
+
import { BlobValue, MAX_BLOB_BYTES } from "./blob.js";
|
|
3
4
|
import { nameToWire } from "./authoring-names.js";
|
|
4
5
|
/** The closed set of storage types. Each has a known column, JSON Schema, filters and rendering. */
|
|
5
6
|
export const PROPERTY_TYPES = [
|
|
@@ -13,6 +14,7 @@ export const PROPERTY_TYPES = [
|
|
|
13
14
|
"timestamp",
|
|
14
15
|
"select",
|
|
15
16
|
"json",
|
|
17
|
+
"blob",
|
|
16
18
|
];
|
|
17
19
|
/** Schema annotation that carries a property's storage metadata. */
|
|
18
20
|
export const PROPERTY_ANNOTATION = "segmentstream/ontology/property";
|
|
@@ -47,6 +49,8 @@ function construct({ type, base, options, checks = [], format, selectOptions, })
|
|
|
47
49
|
...(options.min !== undefined ? { min: options.min } : {}),
|
|
48
50
|
...(options.max !== undefined ? { max: options.max } : {}),
|
|
49
51
|
...(options.maxLength !== undefined ? { maxLength: options.maxLength } : {}),
|
|
52
|
+
...(options.accept !== undefined ? { accept: options.accept } : {}),
|
|
53
|
+
...(options.maxBytes !== undefined ? { maxBytes: options.maxBytes } : {}),
|
|
50
54
|
...(selectOptions !== undefined ? { options: selectOptions } : {}),
|
|
51
55
|
};
|
|
52
56
|
// Annotations go on the base node, before the checks, so that the JSON Schema keeps them at the top
|
|
@@ -176,7 +180,13 @@ function ref(target, options) {
|
|
|
176
180
|
}).check(Schema.isPattern(UUID));
|
|
177
181
|
return (settings.optional === true ? Schema.optionalKey(schema) : schema);
|
|
178
182
|
}
|
|
179
|
-
|
|
183
|
+
function blob(options) {
|
|
184
|
+
return construct({
|
|
185
|
+
type: "blob",
|
|
186
|
+
base: BlobValue,
|
|
187
|
+
options: { ...options, maxBytes: options.maxBytes ?? MAX_BLOB_BYTES },
|
|
188
|
+
});
|
|
189
|
+
}
|
|
180
190
|
export const p = {
|
|
181
191
|
text,
|
|
182
192
|
email,
|
|
@@ -188,6 +198,7 @@ export const p = {
|
|
|
188
198
|
timestamp,
|
|
189
199
|
select,
|
|
190
200
|
json,
|
|
201
|
+
blob,
|
|
191
202
|
ref,
|
|
192
203
|
};
|
|
193
204
|
function findAnnotation(ast, key) {
|
package/dist/query.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Schema } from "effect";
|
|
2
2
|
import { wireStruct } from "./wire.js";
|
|
3
|
+
import { BlobValue } from "./blob.js";
|
|
3
4
|
/**
|
|
4
5
|
* The query language of `find` and of the HTTP API's list and query endpoints.
|
|
5
6
|
*
|
|
@@ -194,12 +195,13 @@ export function fieldValueSchema(field) {
|
|
|
194
195
|
case "json":
|
|
195
196
|
return JsonValue;
|
|
196
197
|
case "uuid":
|
|
198
|
+
case "blob":
|
|
197
199
|
return UuidValue;
|
|
198
200
|
}
|
|
199
201
|
}
|
|
200
202
|
/** Fields a query can sort on: every field but `json` properties. */
|
|
201
203
|
export function isOrderable(field) {
|
|
202
|
-
return field.storage !== "json";
|
|
204
|
+
return field.storage !== "json" && field.storage !== "blob";
|
|
203
205
|
}
|
|
204
206
|
const FILTER_IDENTIFIERS = {
|
|
205
207
|
text: "TextFilter",
|
|
@@ -212,6 +214,7 @@ const FILTER_IDENTIFIERS = {
|
|
|
212
214
|
timestamp: "TimestampFilter",
|
|
213
215
|
json: "JsonFilter",
|
|
214
216
|
uuid: "IdFilter",
|
|
217
|
+
blob: "IdFilter",
|
|
215
218
|
};
|
|
216
219
|
const FILTER_DESCRIPTIONS = {
|
|
217
220
|
TextFilter: "contains and startsWith ignore case.",
|
|
@@ -413,6 +416,8 @@ function recordValueSchema(field) {
|
|
|
413
416
|
return Schema.Literals(field.options ?? []);
|
|
414
417
|
case "json":
|
|
415
418
|
return Schema.Json;
|
|
419
|
+
case "blob":
|
|
420
|
+
return BlobValue;
|
|
416
421
|
default:
|
|
417
422
|
return Schema.String;
|
|
418
423
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
1
2
|
import { type AnyAction } from "../action.js";
|
|
2
3
|
import type { JsonSchemaObject } from "../analyze.js";
|
|
3
4
|
import type { Ontology } from "../ontology.js";
|
|
@@ -14,6 +15,8 @@ export declare function upsertActionName(property: string): string;
|
|
|
14
15
|
* and services by the app or the CLI.
|
|
15
16
|
*/
|
|
16
17
|
export declare function generatedActions(ontology: Ontology, entityName: string): GeneratedActions;
|
|
18
|
+
/** An action's input as JSON Schema (draft 2020-12), as the manifest describes authored actions. */
|
|
19
|
+
export declare function actionInputJsonSchema(input: Schema.Top): JsonSchemaObject;
|
|
17
20
|
/** A generated action, described as the manifest describes authored ones. */
|
|
18
21
|
export interface GeneratedActionManifest {
|
|
19
22
|
readonly name: string;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Effect, Schema } from "effect";
|
|
2
|
+
import { uploadInput, BLOB_UPLOAD_ANNOTATION } from "../blob.js";
|
|
2
3
|
import { action } from "../action.js";
|
|
3
4
|
import { GENERATED_UPSERT_PREFIX } from "../names.js";
|
|
4
5
|
import { generatedActionPermissions } from "../permissions.js";
|
|
@@ -16,7 +17,10 @@ export function upsertActionName(property) {
|
|
|
16
17
|
return `${GENERATED_UPSERT_PREFIX}${property}`;
|
|
17
18
|
}
|
|
18
19
|
/** A property's value schema, refusing days that do not exist, which PostgreSQL would reject. */
|
|
19
|
-
function propertyInput(field) {
|
|
20
|
+
function propertyInput(field, entity) {
|
|
21
|
+
if (field.storage === "blob") {
|
|
22
|
+
return uploadInput(entity.name, field.name);
|
|
23
|
+
}
|
|
20
24
|
const schema = propertyValueSchema(field.property);
|
|
21
25
|
if (field.storage === "date") {
|
|
22
26
|
return schema.check(Schema.makeFilter((value) => isCalendarDate(value) || "Expected a date that exists"));
|
|
@@ -44,7 +48,7 @@ function valueFields(entity, { mode }) {
|
|
|
44
48
|
if (mode === "update" && definition?.managed === true) {
|
|
45
49
|
continue;
|
|
46
50
|
}
|
|
47
|
-
const value = propertyInput(field);
|
|
51
|
+
const value = propertyInput(field, entity);
|
|
48
52
|
const required = mode === "create" &&
|
|
49
53
|
!field.nullable &&
|
|
50
54
|
!(definition !== undefined && "default" in definition);
|
|
@@ -162,9 +166,10 @@ function buildGeneratedActions(ontology, entityName) {
|
|
|
162
166
|
}
|
|
163
167
|
return actions;
|
|
164
168
|
}
|
|
165
|
-
|
|
169
|
+
/** An action's input as JSON Schema (draft 2020-12), as the manifest describes authored actions. */
|
|
170
|
+
export function actionInputJsonSchema(input) {
|
|
166
171
|
const document = Schema.toJsonSchemaDocument(input, {
|
|
167
|
-
includeAnnotationKey: (key) => key === REFERENCE_ANNOTATION,
|
|
172
|
+
includeAnnotationKey: (key) => key === REFERENCE_ANNOTATION || key === BLOB_UPLOAD_ANNOTATION,
|
|
168
173
|
onExcessProperty: "error",
|
|
169
174
|
});
|
|
170
175
|
const schema = document.schema;
|
|
@@ -178,6 +183,6 @@ export function generatedActionManifests(ontology, entityName) {
|
|
|
178
183
|
name,
|
|
179
184
|
description: generated.description,
|
|
180
185
|
permissions: generatedActionPermissions(entityName, name),
|
|
181
|
-
input:
|
|
186
|
+
input: actionInputJsonSchema(generated.input),
|
|
182
187
|
}));
|
|
183
188
|
}
|
|
@@ -57,6 +57,9 @@ export interface HistoryScope {
|
|
|
57
57
|
readonly action: string;
|
|
58
58
|
readonly actionId: string;
|
|
59
59
|
readonly principalId: string | null;
|
|
60
|
+
readonly branchId?: string;
|
|
61
|
+
readonly blobActionKey?: string;
|
|
62
|
+
readonly createIntent?: string;
|
|
60
63
|
}
|
|
61
64
|
/** `INSERT` of one history row; `at` is the transaction's time, shared by the whole action. */
|
|
62
65
|
export declare function insertHistory(scope: HistoryScope, write: HistoryWrite, params: Params): string;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { SqlClient } from "effect/unstable/sql";
|
|
2
|
+
import type { Ontology } from "../ontology.js";
|
|
3
|
+
import type { OAuthStore } from "../service/runtime-contract.js";
|
|
4
|
+
/**
|
|
5
|
+
* The OAuth store on the runtime's OAuth tables, as `ontology_app`. Expiry is the database's
|
|
6
|
+
* `now()`; codes and refresh tokens are used with one `UPDATE … WHERE "used_at" IS NULL`, so two
|
|
7
|
+
* concurrent uses cannot both succeed.
|
|
8
|
+
*/
|
|
9
|
+
export declare function sqlOAuthStore(ontology: Ontology, sql: SqlClient.SqlClient): OAuthStore;
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { SqlClient } from "effect/unstable/sql";
|
|
3
|
+
import { OAUTH_CLIENTS_TABLE, OAUTH_GRANTS_TABLE, OAUTH_REQUESTS_TABLE, OAUTH_TOKENS_TABLE, } from "../migrate/storage.js";
|
|
4
|
+
import { resolveMemberById } from "./principals.js";
|
|
5
|
+
const CLIENTS = `"${OAUTH_CLIENTS_TABLE}"`;
|
|
6
|
+
const REQUESTS = `"${OAUTH_REQUESTS_TABLE}"`;
|
|
7
|
+
const GRANTS = `"${OAUTH_GRANTS_TABLE}"`;
|
|
8
|
+
const TOKENS = `"${OAUTH_TOKENS_TABLE}"`;
|
|
9
|
+
const CLIENT_COLUMNS = `"id", "name", "redirect_uris"::text AS "redirect_uris", "auth_method", "secret_hash", ` +
|
|
10
|
+
`"client_uri"`;
|
|
11
|
+
/** A timestamp as epoch milliseconds, which every driver returns as a plain number. */
|
|
12
|
+
function epochMs(column) {
|
|
13
|
+
return `(extract(epoch from ${column}) * 1000)::double precision`;
|
|
14
|
+
}
|
|
15
|
+
const REQUEST_COLUMNS = `"id", "client_id", "redirect_uri", "state", "code_challenge", "scope", "resource", ` +
|
|
16
|
+
`${epochMs(`"expires_at"`)} AS "expires_at"`;
|
|
17
|
+
/** The grant's columns, read through the alias `g`. */
|
|
18
|
+
const GRANT_COLUMNS = `"g"."id"::text AS "grant_id", "g"."client_id" AS "client_id", ` +
|
|
19
|
+
`"g"."user_id"::text AS "user_id", "g"."scope" AS "scope", "g"."resource" AS "resource"`;
|
|
20
|
+
function text(value) {
|
|
21
|
+
return String(value);
|
|
22
|
+
}
|
|
23
|
+
function nullableText(value) {
|
|
24
|
+
return value === null || value === undefined ? null : String(value);
|
|
25
|
+
}
|
|
26
|
+
function date(value) {
|
|
27
|
+
return new Date(Number(value));
|
|
28
|
+
}
|
|
29
|
+
function clientOf(row) {
|
|
30
|
+
return {
|
|
31
|
+
id: text(row.id),
|
|
32
|
+
name: text(row.name),
|
|
33
|
+
redirectUris: JSON.parse(text(row.redirect_uris)),
|
|
34
|
+
authMethod: text(row.auth_method),
|
|
35
|
+
secretHash: nullableText(row.secret_hash),
|
|
36
|
+
clientUri: nullableText(row.client_uri),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function requestOf(row) {
|
|
40
|
+
return {
|
|
41
|
+
id: text(row.id),
|
|
42
|
+
clientId: text(row.client_id),
|
|
43
|
+
redirectUri: text(row.redirect_uri),
|
|
44
|
+
state: nullableText(row.state),
|
|
45
|
+
codeChallenge: text(row.code_challenge),
|
|
46
|
+
scope: text(row.scope),
|
|
47
|
+
resource: text(row.resource),
|
|
48
|
+
expiresAt: date(row.expires_at),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function grantOf(row) {
|
|
52
|
+
return {
|
|
53
|
+
id: text(row.grant_id),
|
|
54
|
+
clientId: text(row.client_id),
|
|
55
|
+
userId: text(row.user_id),
|
|
56
|
+
scope: text(row.scope),
|
|
57
|
+
resource: text(row.resource),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function tokenOf(row) {
|
|
61
|
+
return {
|
|
62
|
+
hash: text(row.hash),
|
|
63
|
+
grantId: text(row.grant_id),
|
|
64
|
+
kind: text(row.kind),
|
|
65
|
+
expiresAt: date(row.expires_at),
|
|
66
|
+
...(row.redirect_uri === null ? {} : { redirectUri: text(row.redirect_uri) }),
|
|
67
|
+
...(row.code_challenge === null ? {} : { codeChallenge: text(row.code_challenge) }),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
const INSERT_TOKEN = `INSERT INTO ${TOKENS} ("hash", "grant_id", "kind", "redirect_uri", "code_challenge", ` +
|
|
71
|
+
`"expires_at") VALUES ($1, $2::uuid, $3, $4, $5, $6::timestamptz)`;
|
|
72
|
+
function tokenValues(token) {
|
|
73
|
+
return [
|
|
74
|
+
token.hash,
|
|
75
|
+
token.grantId,
|
|
76
|
+
token.kind,
|
|
77
|
+
token.redirectUri ?? null,
|
|
78
|
+
token.codeChallenge ?? null,
|
|
79
|
+
token.expiresAt.toISOString(),
|
|
80
|
+
];
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* The OAuth store on the runtime's OAuth tables, as `ontology_app`. Expiry is the database's
|
|
84
|
+
* `now()`; codes and refresh tokens are used with one `UPDATE … WHERE "used_at" IS NULL`, so two
|
|
85
|
+
* concurrent uses cannot both succeed.
|
|
86
|
+
*/
|
|
87
|
+
export function sqlOAuthStore(ontology, sql) {
|
|
88
|
+
const rows = (statement, values = []) => sql.unsafe(statement, values).pipe(Effect.orDie);
|
|
89
|
+
return {
|
|
90
|
+
saveClient: (client) => rows(`INSERT INTO ${CLIENTS} ("id", "name", "redirect_uris", "auth_method", "secret_hash", ` +
|
|
91
|
+
`"client_uri") VALUES ($1, $2, $3::jsonb, $4, $5, $6)`, [
|
|
92
|
+
client.id,
|
|
93
|
+
client.name,
|
|
94
|
+
JSON.stringify(client.redirectUris),
|
|
95
|
+
client.authMethod,
|
|
96
|
+
client.secretHash,
|
|
97
|
+
client.clientUri,
|
|
98
|
+
]).pipe(Effect.asVoid),
|
|
99
|
+
findClient: (id) => rows(`SELECT ${CLIENT_COLUMNS} FROM ${CLIENTS} WHERE "id" = $1`, [id]).pipe(Effect.map(([row]) => (row === undefined ? undefined : clientOf(row)))),
|
|
100
|
+
saveRequest: (request) => sql
|
|
101
|
+
.withTransaction(Effect.gen(function* () {
|
|
102
|
+
yield* rows(`DELETE FROM ${REQUESTS} WHERE "expires_at" <= now()`);
|
|
103
|
+
yield* rows(`INSERT INTO ${REQUESTS} ("id", "client_id", "redirect_uri", "state", ` +
|
|
104
|
+
`"code_challenge", "scope", "resource", "expires_at") ` +
|
|
105
|
+
`VALUES ($1, $2, $3, $4, $5, $6, $7, $8::timestamptz)`, [
|
|
106
|
+
request.id,
|
|
107
|
+
request.clientId,
|
|
108
|
+
request.redirectUri,
|
|
109
|
+
request.state,
|
|
110
|
+
request.codeChallenge,
|
|
111
|
+
request.scope,
|
|
112
|
+
request.resource,
|
|
113
|
+
request.expiresAt.toISOString(),
|
|
114
|
+
]);
|
|
115
|
+
}))
|
|
116
|
+
.pipe(Effect.orDie),
|
|
117
|
+
findRequest: (id) => rows(`SELECT ${REQUEST_COLUMNS} FROM ${REQUESTS} WHERE "id" = $1 AND "expires_at" > now()`, [
|
|
118
|
+
id,
|
|
119
|
+
]).pipe(Effect.map(([row]) => (row === undefined ? undefined : requestOf(row)))),
|
|
120
|
+
takeRequest: (id) => rows(`DELETE FROM ${REQUESTS} WHERE "id" = $1 AND "expires_at" > now() ` +
|
|
121
|
+
`RETURNING ${REQUEST_COLUMNS}`, [id]).pipe(Effect.map(([row]) => (row === undefined ? undefined : requestOf(row)))),
|
|
122
|
+
saveGrant: (grant, code) => sql
|
|
123
|
+
.withTransaction(Effect.gen(function* () {
|
|
124
|
+
yield* rows(`INSERT INTO ${GRANTS} ("id", "client_id", "user_id", "scope", "resource") ` +
|
|
125
|
+
`VALUES ($1::uuid, $2, $3::uuid, $4, $5)`, [grant.id, grant.clientId, grant.userId, grant.scope, grant.resource]);
|
|
126
|
+
yield* rows(INSERT_TOKEN, tokenValues(code));
|
|
127
|
+
}))
|
|
128
|
+
.pipe(Effect.orDie),
|
|
129
|
+
useToken: (hash, kind) => Effect.gen(function* () {
|
|
130
|
+
const [used] = yield* rows(`UPDATE ${TOKENS} AS "t" SET "used_at" = now() FROM ${GRANTS} AS "g" ` +
|
|
131
|
+
`WHERE "t"."hash" = $1 AND "t"."kind" = $2 AND "t"."used_at" IS NULL ` +
|
|
132
|
+
`AND "t"."expires_at" > now() AND "g"."id" = "t"."grant_id" ` +
|
|
133
|
+
`AND "g"."revoked_at" IS NULL ` +
|
|
134
|
+
`RETURNING "t"."hash" AS "hash", "t"."kind" AS "kind", ` +
|
|
135
|
+
`"t"."redirect_uri" AS "redirect_uri", "t"."code_challenge" AS "code_challenge", ` +
|
|
136
|
+
`${epochMs(`"t"."expires_at"`)} AS "expires_at", ${GRANT_COLUMNS}`, [hash, kind]);
|
|
137
|
+
if (used !== undefined) {
|
|
138
|
+
return { status: "valid", token: tokenOf(used), grant: grantOf(used) };
|
|
139
|
+
}
|
|
140
|
+
const [reused] = yield* rows(`SELECT ${GRANT_COLUMNS} FROM ${TOKENS} AS "t" ` +
|
|
141
|
+
`JOIN ${GRANTS} AS "g" ON "g"."id" = "t"."grant_id" ` +
|
|
142
|
+
`WHERE "t"."hash" = $1 AND "t"."kind" = $2 AND "t"."used_at" IS NOT NULL ` +
|
|
143
|
+
`AND "t"."expires_at" > now() AND "g"."revoked_at" IS NULL`, [hash, kind]);
|
|
144
|
+
return reused === undefined
|
|
145
|
+
? { status: "invalid" }
|
|
146
|
+
: { status: "reused", grant: grantOf(reused) };
|
|
147
|
+
}),
|
|
148
|
+
saveTokens: (tokens) => sql
|
|
149
|
+
.withTransaction(Effect.gen(function* () {
|
|
150
|
+
yield* rows(`DELETE FROM ${TOKENS} WHERE "expires_at" <= now()`);
|
|
151
|
+
for (const token of tokens) {
|
|
152
|
+
yield* rows(INSERT_TOKEN, tokenValues(token));
|
|
153
|
+
}
|
|
154
|
+
}))
|
|
155
|
+
.pipe(Effect.orDie),
|
|
156
|
+
findAccess: (hash) => rows(`SELECT ${GRANT_COLUMNS} FROM ${TOKENS} AS "t" ` +
|
|
157
|
+
`JOIN ${GRANTS} AS "g" ON "g"."id" = "t"."grant_id" ` +
|
|
158
|
+
`WHERE "t"."hash" = $1 AND "t"."kind" = 'access' AND "t"."expires_at" > now() ` +
|
|
159
|
+
`AND "g"."revoked_at" IS NULL`, [hash]).pipe(Effect.map(([row]) => (row === undefined ? undefined : grantOf(row)))),
|
|
160
|
+
findGrantOfToken: (hash) => rows(`SELECT ${GRANT_COLUMNS} FROM ${TOKENS} AS "t" ` +
|
|
161
|
+
`JOIN ${GRANTS} AS "g" ON "g"."id" = "t"."grant_id" WHERE "t"."hash" = $1`, [hash]).pipe(Effect.map(([row]) => (row === undefined ? undefined : grantOf(row)))),
|
|
162
|
+
revokeGrant: (id) => rows(`UPDATE ${GRANTS} SET "revoked_at" = now() WHERE "id" = $1::uuid AND "revoked_at" IS NULL`, [id]).pipe(Effect.asVoid),
|
|
163
|
+
memberPrincipal: (userId) => resolveMemberById(ontology, userId).pipe(Effect.provideService(SqlClient.SqlClient, sql)),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
@@ -32,7 +32,10 @@ export interface ActionRequest<R = never> {
|
|
|
32
32
|
readonly options?: {
|
|
33
33
|
/** Decode the input and check the permission, without running `prepare` or the handler. */
|
|
34
34
|
readonly validateOnly?: boolean;
|
|
35
|
+
readonly idempotencyKey?: string;
|
|
36
|
+
readonly createIntent?: string;
|
|
35
37
|
};
|
|
38
|
+
readonly branchId?: string;
|
|
36
39
|
readonly afterCommit?: ReadonlyArray<AfterCommit<R>>;
|
|
37
40
|
/** The request's id, which `prepare` and the handler receive for logs; the action id by default. */
|
|
38
41
|
readonly requestId?: string;
|
|
@@ -96,7 +99,8 @@ export declare function findAction(ontology: Ontology, entityName: string, actio
|
|
|
96
99
|
* 1. decode the input with the action's schema; issues are returned, not raised;
|
|
97
100
|
* 2. check that the principal's permissions cover the action's (PermissionDenied);
|
|
98
101
|
* 3. with `validateOnly`, stop;
|
|
99
|
-
* 4.
|
|
102
|
+
* 4. reauthorize and replay a committed blob action, if its idempotency key matches;
|
|
103
|
+
* otherwise run `prepare`, when the action has one, without a transaction: reads and the declared
|
|
100
104
|
* connections only, under {@link PREPARE_TIMEOUT};
|
|
101
105
|
* 5. without a handler, answer with what `prepare` returned;
|
|
102
106
|
* 6. open one transaction and run the handler with what `prepare` returned and the runtime, which
|