@segmentstream-cloud/ontology 0.12.0 → 0.14.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 (64) hide show
  1. package/README.md +84 -17
  2. package/dist/aggregate-routes.d.ts +95 -0
  3. package/dist/aggregate-routes.js +332 -0
  4. package/dist/analyze.js +83 -16
  5. package/dist/entity.d.ts +31 -15
  6. package/dist/index.d.ts +6 -3
  7. package/dist/index.js +5 -2
  8. package/dist/manifest.d.ts +57 -23
  9. package/dist/manifest.js +77 -30
  10. package/dist/measure.d.ts +11 -2
  11. package/dist/measure.js +14 -1
  12. package/dist/migrate/apply.js +2 -1
  13. package/dist/migrate/plan.d.ts +1 -1
  14. package/dist/migrate/plan.js +59 -22
  15. package/dist/migrate/storage.d.ts +15 -2
  16. package/dist/migrate/storage.js +63 -5
  17. package/dist/names.d.ts +4 -2
  18. package/dist/names.js +5 -1
  19. package/dist/query.d.ts +5 -2
  20. package/dist/query.js +6 -2
  21. package/dist/relationship.d.ts +36 -5
  22. package/dist/relationship.js +5 -3
  23. package/dist/render.d.ts +47 -3
  24. package/dist/render.js +117 -11
  25. package/dist/replication/model.d.ts +1 -1
  26. package/dist/replication/model.js +11 -1
  27. package/dist/runtime/agents.d.ts +9 -0
  28. package/dist/runtime/agents.js +145 -0
  29. package/dist/runtime/service-binding.js +2 -0
  30. package/dist/service/agent/protocol.d.ts +47 -0
  31. package/dist/service/agent/protocol.js +59 -0
  32. package/dist/service/agent/server.d.ts +27 -0
  33. package/dist/service/agent/server.js +235 -0
  34. package/dist/service/aggregate-platform.d.ts +60 -0
  35. package/dist/service/aggregate-platform.js +189 -0
  36. package/dist/service/aggregate.d.ts +42 -0
  37. package/dist/service/aggregate.js +145 -0
  38. package/dist/service/api.d.ts +5 -3
  39. package/dist/service/api.js +52 -6
  40. package/dist/service/authentication.d.ts +16 -5
  41. package/dist/service/authentication.js +51 -6
  42. package/dist/service/catalog.d.ts +10 -1
  43. package/dist/service/catalog.js +12 -3
  44. package/dist/service/config.d.ts +30 -1
  45. package/dist/service/config.js +53 -1
  46. package/dist/service/errors.d.ts +38 -0
  47. package/dist/service/errors.js +118 -0
  48. package/dist/service/handlers.d.ts +2 -1
  49. package/dist/service/handlers.js +66 -28
  50. package/dist/service/mcp/server.d.ts +6 -0
  51. package/dist/service/mcp/server.js +74 -17
  52. package/dist/service/mcp/tools.d.ts +13 -2
  53. package/dist/service/mcp/tools.js +165 -6
  54. package/dist/service/plan.js +7 -1
  55. package/dist/service/runtime-contract.d.ts +68 -0
  56. package/dist/service/runtime-identity.d.ts +59 -0
  57. package/dist/service/runtime-identity.js +102 -0
  58. package/dist/service/schemas.d.ts +99 -1
  59. package/dist/service/schemas.js +150 -2
  60. package/dist/service/service.d.ts +2 -2
  61. package/dist/service/service.js +24 -6
  62. package/dist/user.d.ts +1 -1
  63. package/dist/warehouse-backed.js +222 -121
  64. package/package.json +1 -1
@@ -0,0 +1,102 @@
1
+ import { Effect } from "effect";
2
+ import { createRemoteJWKSet, decodeJwt, errors, jwtVerify } from "jose";
3
+ /**
4
+ * The header in which a runtime caller names the member it acts for: the member's user id. The
5
+ * service honours it only with a verified runtime token, so nobody else can claim a member with it.
6
+ */
7
+ export const ACT_AS_HEADER = "x-segmentstream-act-as";
8
+ const CLOCK_TOLERANCE_SECONDS = 5;
9
+ const JWKS_TIMEOUT_MS = 5_000;
10
+ /** The environment that a Vercel function token must name: channels run only in production. */
11
+ const PRODUCTION = "production";
12
+ /** The issuer without a trailing slash, as Vercel writes it in `iss` and as its JWKS URL starts. */
13
+ function normalizedIssuer(issuer) {
14
+ let end = issuer.length;
15
+ while (end > 0 && issuer[end - 1] === "/") {
16
+ end -= 1;
17
+ }
18
+ return issuer.slice(0, end);
19
+ }
20
+ /** The issuer's key set, at `<issuer>/.well-known/jwks` as Vercel publishes it. */
21
+ export function runtimeKeySet(issuer) {
22
+ return createRemoteJWKSet(new URL(`${normalizedIssuer(issuer)}/.well-known/jwks`), {
23
+ timeoutDuration: JWKS_TIMEOUT_MS,
24
+ });
25
+ }
26
+ /** The key set could not be read: not the token's fault. Every other jose error is about the token. */
27
+ function keySetUnavailable(error) {
28
+ if (!(error instanceof errors.JOSEError)) {
29
+ return true;
30
+ }
31
+ return (error instanceof errors.JWKSTimeout ||
32
+ error instanceof errors.JWKSInvalid ||
33
+ error.code === errors.JOSEError.code);
34
+ }
35
+ /** What about a token failed, for the operator's log: a jose code or a claim name, never a value. */
36
+ function failureReason(error) {
37
+ if (error instanceof errors.JWTClaimValidationFailed || error instanceof errors.JWTExpired) {
38
+ return `claim:${error.claim}`;
39
+ }
40
+ return error instanceof errors.JOSEError ? error.code : "invalid";
41
+ }
42
+ function claimedIssuer(token) {
43
+ try {
44
+ const { iss } = decodeJwt(token);
45
+ return typeof iss === "string" ? iss : undefined;
46
+ }
47
+ catch {
48
+ return undefined;
49
+ }
50
+ }
51
+ /**
52
+ * Verifies the Vercel OIDC token of a function of this deployment's project, as backend-next
53
+ * verifies the ontology service's own: RS256 from the team's issuer, for the team's audience,
54
+ * unexpired, with `project_id` equal to the configured project and `environment` `production`.
55
+ * The issuer and the audience name the team, so no further owner check is needed. A token that
56
+ * claims another issuer is `other`, without a key-set fetch.
57
+ */
58
+ export function makeRuntimeTokenVerifier(config, keySet) {
59
+ const issuer = normalizedIssuer(config.issuer);
60
+ return async function verifyRuntimeToken(token) {
61
+ if (claimedIssuer(token) !== issuer) {
62
+ return { status: "other" };
63
+ }
64
+ try {
65
+ const { payload } = await jwtVerify(token, keySet, {
66
+ algorithms: ["RS256"],
67
+ issuer,
68
+ audience: config.audience,
69
+ clockTolerance: CLOCK_TOLERANCE_SECONDS,
70
+ requiredClaims: ["exp", "iat", "iss", "aud", "project_id", "environment"],
71
+ });
72
+ if (payload.project_id !== config.projectId) {
73
+ return { status: "invalid", reason: "claim:project_id" };
74
+ }
75
+ if (payload.environment !== PRODUCTION) {
76
+ return { status: "invalid", reason: "claim:environment" };
77
+ }
78
+ return { status: "valid" };
79
+ }
80
+ catch (error) {
81
+ return keySetUnavailable(error)
82
+ ? { status: "unavailable" }
83
+ : { status: "invalid", reason: failureReason(error) };
84
+ }
85
+ };
86
+ }
87
+ /** The runtime verifier on the issuer's remote key set, which jose caches between requests. */
88
+ export function runtimeTokenVerifier(config) {
89
+ return makeRuntimeTokenVerifier(config, runtimeKeySet(config.issuer));
90
+ }
91
+ /** A runtime token that failed verification: the reason is logged, the token never. */
92
+ export function logRuntimeRejection(reason) {
93
+ return Effect.logWarning("ontology.runtime_identity.rejected").pipe(Effect.annotateLogs({ reason }));
94
+ }
95
+ /** The request's runtime credential, or `undefined` while runtime authentication is disabled. */
96
+ export function runtimeCredential(verify, request) {
97
+ if (verify === undefined) {
98
+ return undefined;
99
+ }
100
+ const actAs = request.headers[ACT_AS_HEADER]?.trim();
101
+ return { verify, actAs: actAs === undefined || actAs === "" ? undefined : actAs };
102
+ }
@@ -4,7 +4,7 @@ import type { ApiPlan } from "./plan.js";
4
4
  export declare const IdParam: Schema.String;
5
5
  /** The schemas of one entity's records and queries. */
6
6
  export interface EntitySchemas {
7
- /** System fields, properties and `<relationship>_id` per `rel.one`. */
7
+ /** System fields, properties and `<relationship>_id` per stored `rel.one`. */
8
8
  readonly record: Schema.Top;
9
9
  /** A record with the relationships a query may include. */
10
10
  readonly result: Schema.Top;
@@ -392,6 +392,13 @@ export declare const Graph: Schema.Struct<{
392
392
  readonly to: Schema.String;
393
393
  readonly inverse: Schema.String;
394
394
  readonly cardinality: Schema.Literals<readonly ["one", "many"]>;
395
+ /** The property pair of a relationship matched by value: one with a warehouse-backed end. */
396
+ readonly on: Schema.optionalKey<Schema.encodeKeys<Schema.Struct<{
397
+ readonly property: Schema.String;
398
+ readonly targetProperty: Schema.String;
399
+ }>, {
400
+ readonly targetProperty: "target_property";
401
+ }>>;
395
402
  readonly optional: Schema.Boolean;
396
403
  readonly phrase: Schema.Tuple<readonly [Schema.String, Schema.String]>;
397
404
  readonly forward: Schema.String;
@@ -414,6 +421,92 @@ export declare const SIGN_IN_CHECK_MAX_EMAIL_LENGTH = 320;
414
421
  export declare const SignInCheckRequest: Schema.Struct<{
415
422
  readonly email: Schema.String;
416
423
  }>;
424
+ /** The bounds of a model-level aggregate (docs/ontology-aggregates-spec.md, "Request"). */
425
+ export declare const AGGREGATE_LIMITS: {
426
+ readonly measures: 30;
427
+ readonly dimensions: 20;
428
+ readonly filters: 50;
429
+ readonly values: 100;
430
+ readonly orderBy: 50;
431
+ readonly rows: 1000;
432
+ };
433
+ export declare const AGGREGATE_FILTER_OPERATORS: readonly ["equals", "in", "isNull", "isNotNull", "between"];
434
+ /** The operators of the per-entity shorthand, which bounds dates with `dateRange` instead. */
435
+ export declare const ENTITY_AGGREGATE_FILTER_OPERATORS: readonly ["equals", "in", "isNull", "isNotNull"];
436
+ export declare const AGGREGATE_GRANULARITIES: readonly ["day", "week", "month"];
437
+ type AggregateValue = string | number | boolean;
438
+ interface AggregateFilter<Operator extends string> {
439
+ readonly dimension: string;
440
+ readonly operator: Operator;
441
+ readonly value?: AggregateValue;
442
+ readonly values?: ReadonlyArray<AggregateValue>;
443
+ }
444
+ declare const AggregateValue: Schema.Union<readonly [Schema.String, Schema.Number, Schema.Boolean]>;
445
+ interface AggregateOrdering {
446
+ readonly measure?: string;
447
+ readonly dimension?: string;
448
+ readonly direction: "asc" | "desc";
449
+ }
450
+ /**
451
+ * `POST /api/ontology/aggregate`. The service checks the shape and the bounds, resolves the names
452
+ * against its manifest and forwards the body unchanged; SegmentStream checks everything else.
453
+ */
454
+ export declare const AggregateRequest: Schema.Codec<AggregateRequest>;
455
+ export interface AggregateRequest {
456
+ readonly measures: ReadonlyArray<string>;
457
+ readonly dimensions?: ReadonlyArray<string>;
458
+ readonly filters?: ReadonlyArray<AggregateFilter<(typeof AGGREGATE_FILTER_OPERATORS)[number]>>;
459
+ readonly granularity?: (typeof AGGREGATE_GRANULARITIES)[number];
460
+ readonly granularityOf?: string;
461
+ readonly orderBy?: ReadonlyArray<AggregateOrdering>;
462
+ readonly limit?: number;
463
+ }
464
+ /**
465
+ * `POST /api/ontology/<entity>/aggregate`: SegmentStream's per-entity request, unchanged. Names are
466
+ * relative to the entity (`revenue`, `product.brand`), and `dateRange` bounds a partitioned entity.
467
+ */
468
+ export declare const EntityAggregateRequest: Schema.Codec<EntityAggregateRequest>;
469
+ export interface EntityAggregateRequest {
470
+ readonly dimensions: ReadonlyArray<string>;
471
+ readonly measures: ReadonlyArray<string>;
472
+ readonly dateRange?: {
473
+ readonly from: string;
474
+ readonly to: string;
475
+ };
476
+ readonly filters?: ReadonlyArray<AggregateFilter<(typeof ENTITY_AGGREGATE_FILTER_OPERATORS)[number]>>;
477
+ readonly granularity?: (typeof AGGREGATE_GRANULARITIES)[number];
478
+ readonly granularityOf?: string;
479
+ readonly orderBy?: ReadonlyArray<AggregateOrdering>;
480
+ readonly limit?: number;
481
+ }
482
+ /** SegmentStream's answer to an aggregate, which the service forwards unchanged. */
483
+ export declare const AggregateResult: Schema.Struct<{
484
+ readonly fields: Schema.$Array<Schema.Struct<{
485
+ readonly name: Schema.String;
486
+ readonly label: Schema.String;
487
+ readonly type: Schema.String;
488
+ readonly unit: Schema.optionalKey<Schema.String>;
489
+ readonly format: Schema.optionalKey<Schema.String>;
490
+ readonly scope: Schema.optionalKey<Schema.$Array<Schema.String>>;
491
+ readonly implicit: Schema.optionalKey<Schema.Boolean>;
492
+ readonly additive: Schema.optionalKey<Schema.Boolean>;
493
+ readonly filters: Schema.optionalKey<Schema.$Array<Schema.String>>;
494
+ }>>;
495
+ readonly rows: Schema.$Array<Schema.$Record<Schema.String, Schema.NullOr<Schema.Union<readonly [Schema.String, Schema.Boolean]>>>>;
496
+ readonly truncated: Schema.Boolean;
497
+ readonly execution: Schema.Struct<{
498
+ readonly elapsedMs: Schema.Number;
499
+ readonly bytesProcessed: Schema.optionalKey<Schema.String>;
500
+ readonly bytesBilled: Schema.optionalKey<Schema.String>;
501
+ readonly warnings: Schema.optionalKey<Schema.$Array<Schema.String>>;
502
+ readonly sources: Schema.optionalKey<Schema.$Array<Schema.Struct<{
503
+ readonly entity: Schema.String;
504
+ readonly asOf: Schema.String;
505
+ }>>>;
506
+ }>;
507
+ readonly deploymentId: Schema.NullOr<Schema.String>;
508
+ readonly definitionDigest: Schema.String;
509
+ }>;
417
510
  /** Whether the address may sign in, and with a code or a link. */
418
511
  export declare const SignInCheckResult: Schema.Union<readonly [Schema.Struct<{
419
512
  readonly allowed: Schema.Literal<false>;
@@ -448,7 +541,12 @@ export declare const ErrorSchemas: {
448
541
  rateLimited: Schema.Top;
449
542
  internal: Schema.Top;
450
543
  unavailable: Schema.Top;
544
+ invalidAggregateQuery: Schema.Top;
545
+ readForbidden: Schema.Top;
546
+ aggregateUnavailable: Schema.Top;
547
+ aggregateUpstreamUnavailable: Schema.Top;
451
548
  connectionRequired: Schema.Top;
452
549
  connectionResponseInvalid: Schema.Top;
453
550
  connectionUnavailable: Schema.Top;
454
551
  };
552
+ export {};
@@ -1,7 +1,7 @@
1
1
  import { BlobValue } from "../blob.js";
2
2
  import { Schema, SchemaTransformation } from "effect";
3
3
  import { wireStruct } from "../wire.js";
4
- import { OntologyManifestWire } from "../manifest.js";
4
+ import { isValueMatched, OntologyManifestWire } from "../manifest.js";
5
5
  import { entityQuerySchemas, queryLinks } from "../query.js";
6
6
  import { ValidationIssue as RuntimeValidationIssue } from "../runtime/errors.js";
7
7
  import { Edit, HistoryEntry as RuntimeHistoryEntry } from "../runtime/history.js";
@@ -96,7 +96,8 @@ function recordFields(entity) {
96
96
  fields[name] = definition.optional ? Schema.NullOr(value) : value;
97
97
  }
98
98
  for (const { name, relationship } of entity.relationships) {
99
- if (relationship.cardinality === "one") {
99
+ // A value-matched relationship's reference is the property its `on` names.
100
+ if (relationship.cardinality === "one" && !isValueMatched(relationship)) {
100
101
  const key = Id.annotate({ description: relationship.description });
101
102
  fields[`${name}_id`] = relationship.optional ? Schema.NullOr(key) : key;
102
103
  }
@@ -276,6 +277,8 @@ const GraphRelationship = Schema.Struct({
276
277
  to: Schema.String,
277
278
  inverse: Schema.String,
278
279
  cardinality: Schema.Literals(["one", "many"]),
280
+ /** The property pair of a relationship matched by value: one with a warehouse-backed end. */
281
+ on: Schema.optionalKey(Schema.Struct({ property: Schema.String, targetProperty: Schema.String }).pipe(Schema.encodeKeys({ targetProperty: "target_property" }))),
279
282
  optional: Schema.Boolean,
280
283
  phrase: Schema.Tuple([Schema.String, Schema.String]),
281
284
  forward: Schema.String.annotate({ examples: ["Lead is owned by one User"] }),
@@ -304,6 +307,143 @@ export const SignInCheckRequest = Schema.Struct({
304
307
  description: "The address the person typed, in any case.",
305
308
  }),
306
309
  }).annotate({ identifier: "SignInCheckRequest" });
310
+ /** The bounds of a model-level aggregate (docs/ontology-aggregates-spec.md, "Request"). */
311
+ export const AGGREGATE_LIMITS = {
312
+ measures: 30,
313
+ dimensions: 20,
314
+ filters: 50,
315
+ values: 100,
316
+ orderBy: 50,
317
+ rows: 1000,
318
+ };
319
+ export const AGGREGATE_FILTER_OPERATORS = [
320
+ "equals",
321
+ "in",
322
+ "isNull",
323
+ "isNotNull",
324
+ "between",
325
+ ];
326
+ /** The operators of the per-entity shorthand, which bounds dates with `dateRange` instead. */
327
+ export const ENTITY_AGGREGATE_FILTER_OPERATORS = ["equals", "in", "isNull", "isNotNull"];
328
+ export const AGGREGATE_GRANULARITIES = ["day", "week", "month"];
329
+ const AggregateValue = Schema.Union([Schema.String, Schema.Number, Schema.Boolean]);
330
+ /** Which values each operator takes, with SegmentStream's own messages (`planner.ts`). */
331
+ function filterValuesIssue(filter) {
332
+ const { operator, value, values } = filter;
333
+ switch (operator) {
334
+ case "equals":
335
+ return value === undefined || values !== undefined
336
+ ? "Equality filters require one value."
337
+ : undefined;
338
+ case "in":
339
+ return values === undefined ||
340
+ values.length === 0 ||
341
+ values.length > AGGREGATE_LIMITS.values ||
342
+ value !== undefined
343
+ ? "Membership filters require between 1 and 100 values."
344
+ : undefined;
345
+ case "between":
346
+ return value !== undefined || values?.length !== 2
347
+ ? "between needs two values in order."
348
+ : undefined;
349
+ default:
350
+ return value !== undefined || values !== undefined
351
+ ? "Null filters do not accept values."
352
+ : undefined;
353
+ }
354
+ }
355
+ function aggregateFilter(operators) {
356
+ const filter = closedStruct({
357
+ dimension: Schema.String.annotate({
358
+ description: "A property, or a path of to-one relationships ending in one.",
359
+ }),
360
+ operator: Schema.Literals(operators),
361
+ value: Schema.optionalKey(AggregateValue).annotate({ description: "The value of equals." }),
362
+ values: Schema.optionalKey(Schema.Array(AggregateValue).check(Schema.isMaxLength(AGGREGATE_LIMITS.values))).annotate({ description: "The values of in, or the two inclusive bounds of between." }),
363
+ }, "Unknown key; use dimension, operator, value and values");
364
+ return filter.check(Schema.makeFilter((candidate) => filterValuesIssue(candidate) ?? true));
365
+ }
366
+ const AggregateOrder = closedStruct({
367
+ measure: Schema.optionalKey(Schema.String),
368
+ dimension: Schema.optionalKey(Schema.String),
369
+ direction: Schema.Literals(["asc", "desc"]),
370
+ }, "Unknown key; use measure or dimension, and direction");
371
+ /** The fields both aggregate requests share, with their bounds. */
372
+ const aggregateFields = {
373
+ granularity: Schema.optionalKey(Schema.Literals(AGGREGATE_GRANULARITIES)),
374
+ granularityOf: Schema.optionalKey(Schema.String).annotate({
375
+ description: "The requested date or timestamp dimension that granularity buckets.",
376
+ }),
377
+ orderBy: Schema.optionalKey(Schema.Array(AggregateOrder).check(Schema.isMaxLength(AGGREGATE_LIMITS.orderBy))),
378
+ limit: Schema.optionalKey(Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: AGGREGATE_LIMITS.rows }))).annotate({ description: "At most 1000 rows; 100 by default." }),
379
+ };
380
+ /**
381
+ * `POST /api/ontology/aggregate`. The service checks the shape and the bounds, resolves the names
382
+ * against its manifest and forwards the body unchanged; SegmentStream checks everything else.
383
+ */
384
+ export const AggregateRequest = closedStruct({
385
+ measures: Schema.Array(Schema.String)
386
+ .check(Schema.isMaxLength(AGGREGATE_LIMITS.measures))
387
+ .annotate({ description: "<entity>.<measure>, at least one." }),
388
+ dimensions: Schema.optionalKey(Schema.Array(Schema.String).check(Schema.isMaxLength(AGGREGATE_LIMITS.dimensions))).annotate({
389
+ description: "<entity>.<property>, or a route from a measure's entity: " +
390
+ "<entity>.<relationship>[.<relationship>].<property>.",
391
+ }),
392
+ filters: Schema.optionalKey(Schema.Array(aggregateFilter(AGGREGATE_FILTER_OPERATORS)).check(Schema.isMaxLength(AGGREGATE_LIMITS.filters))).annotate({ description: "All must hold." }),
393
+ ...aggregateFields,
394
+ }, "Unknown key; use measures, dimensions, filters, granularity, granularityOf, orderBy and limit").annotate({ identifier: "AggregateRequest" });
395
+ /**
396
+ * `POST /api/ontology/<entity>/aggregate`: SegmentStream's per-entity request, unchanged. Names are
397
+ * relative to the entity (`revenue`, `product.brand`), and `dateRange` bounds a partitioned entity.
398
+ */
399
+ export const EntityAggregateRequest = closedStruct({
400
+ dimensions: Schema.Array(Schema.String)
401
+ .check(Schema.isMaxLength(AGGREGATE_LIMITS.dimensions))
402
+ .annotate({
403
+ description: "A property, or a path of to-one relationships: product.brand.",
404
+ }),
405
+ measures: Schema.Array(Schema.String)
406
+ .check(Schema.isMaxLength(AGGREGATE_LIMITS.measures))
407
+ .annotate({ description: "The entity's measures, at least one." }),
408
+ dateRange: Schema.optionalKey(closedStruct({
409
+ from: Schema.String.annotate({ format: "date" }),
410
+ to: Schema.String.annotate({ format: "date" }),
411
+ }, "Unknown key; use from and to")).annotate({ description: "Inclusive days of the entity's partition property." }),
412
+ filters: Schema.optionalKey(Schema.Array(aggregateFilter(ENTITY_AGGREGATE_FILTER_OPERATORS)).check(Schema.isMaxLength(AGGREGATE_LIMITS.filters))).annotate({ description: "All must hold." }),
413
+ ...aggregateFields,
414
+ }, "Unknown key; use dimensions, measures, dateRange, filters, granularity, granularityOf, " +
415
+ "orderBy and limit").annotate({ identifier: "EntityAggregateRequest" });
416
+ /** SegmentStream's answer to an aggregate, which the service forwards unchanged. */
417
+ export const AggregateResult = Schema.Struct({
418
+ fields: Schema.Array(Schema.Struct({
419
+ name: Schema.String.annotate({ description: "A dimension identity or <entity>.<measure>." }),
420
+ label: Schema.String,
421
+ type: Schema.String,
422
+ unit: Schema.optionalKey(Schema.String),
423
+ format: Schema.optionalKey(Schema.String),
424
+ scope: Schema.optionalKey(Schema.Array(Schema.String)),
425
+ implicit: Schema.optionalKey(Schema.Boolean),
426
+ additive: Schema.optionalKey(Schema.Boolean).annotate({
427
+ description: "false when the measure's rows do not add up to a total.",
428
+ }),
429
+ filters: Schema.optionalKey(Schema.Array(Schema.String)).annotate({
430
+ description: "The filters the measure applied.",
431
+ }),
432
+ })),
433
+ rows: Schema.Array(Schema.Record(Schema.String, Schema.NullOr(Schema.Union([Schema.String, Schema.Boolean])))),
434
+ truncated: Schema.Boolean,
435
+ execution: Schema.Struct({
436
+ elapsedMs: Schema.Number,
437
+ bytesProcessed: Schema.optionalKey(Schema.String),
438
+ bytesBilled: Schema.optionalKey(Schema.String),
439
+ warnings: Schema.optionalKey(Schema.Array(Schema.String)).annotate({
440
+ description: "For example, a filter that did not apply to some measure's entity.",
441
+ }),
442
+ sources: Schema.optionalKey(Schema.Array(Schema.Struct({ entity: Schema.String, asOf: Schema.String }))).annotate({ description: "How fresh the replica of each stored entity read is." }),
443
+ }),
444
+ deploymentId: Schema.NullOr(Schema.String),
445
+ definitionDigest: Schema.String,
446
+ }).annotate({ identifier: "AggregateResult" });
307
447
  /** Whether the address may sign in, and with a code or a link. */
308
448
  export const SignInCheckResult = Schema.Union([
309
449
  Schema.Struct({ allowed: Schema.Literal(false) }),
@@ -370,6 +510,14 @@ export const ErrorSchemas = {
370
510
  rateLimited: errorBody("RateLimited", 429, ["rate_limited"]),
371
511
  internal: errorBody("InternalError", 500, ["internal_error"]),
372
512
  unavailable: errorBody("ServiceUnavailable", 503, ["service_unavailable"]),
513
+ invalidAggregateQuery: errorBody("InvalidAggregateQuery", 400, ["invalid_aggregate_query"]),
514
+ readForbidden: errorBody("ReadForbidden", 403, ["forbidden"], {
515
+ permissions: Schema.optionalKey(Schema.Array(Schema.String)),
516
+ }),
517
+ aggregateUnavailable: errorBody("AggregateUnavailable", 503, ["aggregate_unavailable"], {
518
+ retryable: Schema.Boolean,
519
+ }),
520
+ aggregateUpstreamUnavailable: errorBody("AggregateUpstreamUnavailable", 502, ["aggregate_upstream_unavailable"], { retryable: Schema.Boolean }),
373
521
  connectionRequired: errorBody("ConnectionRequired", 409, ["connection_authorization_required", "connections_not_deployed"], { connection: Schema.String }),
374
522
  connectionResponseInvalid: errorBody("ConnectionResponseInvalid", 502, [
375
523
  "connection_response_invalid",
@@ -13,8 +13,8 @@ export interface OntologyService {
13
13
  readonly dispose: () => Promise<void>;
14
14
  }
15
15
  /**
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`
16
+ * The ontology service: the derived HttpApi, the MCP server at `/mcp` and the agent's pairing routes,
17
+ * wired to the runtime, behind Neon Auth, app-token, API-key and runtime-token authentication. Every response carries `x-ontology-hash`
18
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;
@@ -4,6 +4,9 @@ import { HttpApiBuilder } from "effect/unstable/httpapi";
4
4
  import { storageMaintenanceAuthenticationLayer } from "../storage/maintenance-auth.js";
5
5
  import { nameToWire } from "../authoring-names.js";
6
6
  import { sqlServiceRuntime } from "../runtime/service-binding.js";
7
+ import { agentLayer } from "./agent/server.js";
8
+ import { makeAggregates } from "./aggregate.js";
9
+ import { platformAggregates } from "./aggregate-platform.js";
7
10
  import { ErrorResponsesLive, buildApi } from "./api.js";
8
11
  import { authenticationLayer, neonAuthSessionExchange, tokenVerifier } from "./authentication.js";
9
12
  import { OntologyServiceConfigError, } from "./config.js";
@@ -15,6 +18,7 @@ import { deployedReplication } from "./replication.js";
15
18
  import { mcpLayer } from "./mcp/server.js";
16
19
  import { oauthLayer } from "./oauth/server.js";
17
20
  import { planApi } from "./plan.js";
21
+ import { runtimeTokenVerifier } from "./runtime-identity.js";
18
22
  /** The header every response carries: the hash of the manifest the service serves. */
19
23
  export const ONTOLOGY_HASH_HEADER = "x-ontology-hash";
20
24
  /** The header every response carries: the correlation id quoted in error bodies and logs. */
@@ -57,8 +61,8 @@ function decorate(response, requestId, hash) {
57
61
  });
58
62
  }
59
63
  /**
60
- * The ontology service: the derived HttpApi and the MCP server at `/mcp`, wired to the runtime,
61
- * behind Neon Auth, app-token and API-key authentication. Every response carries `x-ontology-hash`
64
+ * The ontology service: the derived HttpApi, the MCP server at `/mcp` and the agent's pairing routes,
65
+ * wired to the runtime, behind Neon Auth, app-token, API-key and runtime-token authentication. Every response carries `x-ontology-hash`
62
66
  * and `x-request-id`; failures answer with a JSON error and never a stack or SQL.
63
67
  */
64
68
  export function createOntologyService(ontology, config) {
@@ -74,6 +78,7 @@ export function createOntologyService(ontology, config) {
74
78
  const hooks = afterCommitHooks(plan, config.afterCommit);
75
79
  const verifyToken = tokenVerifier(config);
76
80
  const exchangeSession = config.neonAuth === undefined ? undefined : neonAuthSessionExchange(config.neonAuth);
81
+ const verifyRuntime = config.runtimeIdentity === undefined ? undefined : runtimeTokenVerifier(config.runtimeIdentity);
77
82
  const backend = runtime.connect({
78
83
  ontology,
79
84
  databaseUrl: config.databaseUrl,
@@ -84,15 +89,28 @@ export function createOntologyService(ontology, config) {
84
89
  ? deployedConnections(config)
85
90
  : localConnections(config.localConnections));
86
91
  const replicate = deployedReplication(config);
87
- const routes = HttpApiBuilder.layer(built.api).pipe(Layer.provide(handlersLayer(built, hooks, connections, replicate)), Layer.provide([
88
- authenticationLayer(verifyToken, exchangeSession),
92
+ const aggregates = makeAggregates({
93
+ manifest: ontology.manifest,
94
+ transport: platformAggregates({ ...config, manifestHash: ontology.manifest.hash }),
95
+ });
96
+ const routes = HttpApiBuilder.layer(built.api).pipe(Layer.provide(handlersLayer(built, hooks, connections, replicate, aggregates)), Layer.provide([
97
+ authenticationLayer(verifyToken, exchangeSession, verifyRuntime),
89
98
  ErrorResponsesLive,
90
99
  storageMaintenanceAuthenticationLayer(config.storageMaintenanceKey),
91
100
  ]));
92
101
  const mcp = config.mcp === false
93
102
  ? Layer.empty
94
- : Layer.mergeAll(mcpLayer({ built, hooks, connections, verifyToken, replicate }), oauthLayer({ verifyToken }));
95
- const application = Layer.mergeAll(routes, mcp, DocsRoute, UnmatchedRoutes).pipe(Layer.provide(backend), Layer.provide(HttpServer.layerServices));
103
+ : Layer.mergeAll(mcpLayer({
104
+ built,
105
+ hooks,
106
+ connections,
107
+ verifyToken,
108
+ verifyRuntime,
109
+ replicate,
110
+ aggregates,
111
+ }), oauthLayer({ verifyToken }));
112
+ const agent = agentLayer({ verifyToken, verifyRuntime, exchangeSession });
113
+ const application = Layer.mergeAll(routes, mcp, agent, DocsRoute, UnmatchedRoutes).pipe(Layer.provide(backend), Layer.provide(HttpServer.layerServices));
96
114
  const web = HttpRouter.toWebHandler(application, { disableLogger: true });
97
115
  const hash = ontology.manifest.hash;
98
116
  return {
package/dist/user.d.ts CHANGED
@@ -16,5 +16,5 @@ export declare const User: import("./entity.js").DraftEntity<"user", {
16
16
  readonly identityProvider: import("effect/Schema").optionalKey<import("./property.js").Property<string, false, false>>;
17
17
  readonly identitySubject: import("effect/Schema").optionalKey<import("./property.js").Property<string, false, false>>;
18
18
  readonly lastSignInAt: import("effect/Schema").optionalKey<import("./property.js").Property<string, false, false>>;
19
- }, import("./entity.js").Relationships>;
19
+ }, import("./entity.js").Relationships, Readonly<Record<never, never>>>;
20
20
  export type User = typeof User.Type;