@segmentstream-cloud/ontology 0.13.0 → 0.15.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 (51) hide show
  1. package/README.md +65 -3
  2. package/dist/agent-chat.d.ts +75 -0
  3. package/dist/agent-chat.js +176 -0
  4. package/dist/analyze.d.ts +6 -2
  5. package/dist/analyze.js +29 -18
  6. package/dist/authoring.d.ts +6 -3
  7. package/dist/authoring.js +6 -3
  8. package/dist/index.d.ts +7 -2
  9. package/dist/index.js +5 -1
  10. package/dist/migrate/apply.js +2 -1
  11. package/dist/migrate/plan.js +21 -9
  12. package/dist/migrate/storage.d.ts +12 -0
  13. package/dist/migrate/storage.js +48 -0
  14. package/dist/names.d.ts +1 -1
  15. package/dist/names.js +1 -0
  16. package/dist/permissions.d.ts +11 -2
  17. package/dist/permissions.js +15 -3
  18. package/dist/replication/baseline.js +1 -1
  19. package/dist/replication/model.js +2 -2
  20. package/dist/replication/schema.js +1 -1
  21. package/dist/runtime/agents.d.ts +10 -0
  22. package/dist/runtime/agents.js +305 -0
  23. package/dist/runtime/generated-actions.d.ts +2 -1
  24. package/dist/runtime/generated-actions.js +18 -13
  25. package/dist/runtime/service-binding.js +2 -0
  26. package/dist/service/agent/channel.d.ts +16 -0
  27. package/dist/service/agent/channel.js +88 -0
  28. package/dist/service/agent/protocol.d.ts +61 -0
  29. package/dist/service/agent/protocol.js +85 -0
  30. package/dist/service/agent/server.d.ts +28 -0
  31. package/dist/service/agent/server.js +257 -0
  32. package/dist/service/aggregate-platform.d.ts +5 -0
  33. package/dist/service/aggregate-platform.js +1 -1
  34. package/dist/service/authentication.d.ts +16 -5
  35. package/dist/service/authentication.js +51 -6
  36. package/dist/service/config.d.ts +29 -2
  37. package/dist/service/config.js +52 -2
  38. package/dist/service/errors.d.ts +6 -0
  39. package/dist/service/errors.js +16 -0
  40. package/dist/service/handlers.d.ts +2 -1
  41. package/dist/service/handlers.js +2 -2
  42. package/dist/service/mcp/server.d.ts +6 -0
  43. package/dist/service/mcp/server.js +15 -10
  44. package/dist/service/operations.d.ts +6 -2
  45. package/dist/service/operations.js +13 -5
  46. package/dist/service/runtime-contract.d.ts +94 -0
  47. package/dist/service/runtime-identity.d.ts +59 -0
  48. package/dist/service/runtime-identity.js +102 -0
  49. package/dist/service/service.d.ts +2 -2
  50. package/dist/service/service.js +24 -6
  51. package/package.json +1 -1
@@ -0,0 +1,59 @@
1
+ import { Effect } from "effect";
2
+ import type { HttpServerRequest } from "effect/unstable/http";
3
+ import { type JWTVerifyGetKey } from "jose";
4
+ /**
5
+ * The header in which a runtime caller names the member it acts for: the member's user id. The
6
+ * service honours it only with a verified runtime token, so nobody else can claim a member with it.
7
+ */
8
+ export declare const ACT_AS_HEADER = "x-segmentstream-act-as";
9
+ /**
10
+ * Whom the service trusts as its runtime caller: Vercel OIDC tokens of the functions of this
11
+ * deployment's own project. The app's agent is a service of the same Vercel project as the ontology
12
+ * service, so `VERCEL_PROJECT_ID` pins exactly the deployment's own functions.
13
+ */
14
+ export interface RuntimeIdentityConfig {
15
+ /** The team's issuer: `https://oidc.vercel.com/<team slug>` in team issuer mode. */
16
+ readonly issuer: string;
17
+ /** The audience Vercel issues function tokens for: `https://vercel.com/<team slug>`. */
18
+ readonly audience: string;
19
+ /** The project the token must name, from `VERCEL_PROJECT_ID`. */
20
+ readonly projectId: string;
21
+ }
22
+ export type RuntimeVerification = {
23
+ readonly status: "valid";
24
+ }
25
+ /** The token claims the runtime issuer but fails verification; `reason` names what failed. */
26
+ | {
27
+ readonly status: "invalid";
28
+ readonly reason: string;
29
+ } | {
30
+ readonly status: "unavailable";
31
+ }
32
+ /** The token does not claim the runtime issuer: it is some other credential. */
33
+ | {
34
+ readonly status: "other";
35
+ };
36
+ /** Verifies a bearer token as the deployment's own runtime token. */
37
+ export type RuntimeTokenVerifier = (token: string) => Promise<RuntimeVerification>;
38
+ /** The issuer's key set, at `<issuer>/.well-known/jwks` as Vercel publishes it. */
39
+ export declare function runtimeKeySet(issuer: string): JWTVerifyGetKey;
40
+ /**
41
+ * Verifies the Vercel OIDC token of a function of this deployment's project, as backend-next
42
+ * verifies the ontology service's own: RS256 from the team's issuer, for the team's audience,
43
+ * unexpired, with `project_id` equal to the configured project and `environment` `production`.
44
+ * The issuer and the audience name the team, so no further owner check is needed. A token that
45
+ * claims another issuer is `other`, without a key-set fetch.
46
+ */
47
+ export declare function makeRuntimeTokenVerifier(config: RuntimeIdentityConfig, keySet: JWTVerifyGetKey): RuntimeTokenVerifier;
48
+ /** The runtime verifier on the issuer's remote key set, which jose caches between requests. */
49
+ export declare function runtimeTokenVerifier(config: RuntimeIdentityConfig): RuntimeTokenVerifier;
50
+ /** A runtime token that failed verification: the reason is logged, the token never. */
51
+ export declare function logRuntimeRejection(reason: string): Effect.Effect<void>;
52
+ /** How a request may authenticate as the runtime: the verifier and the member it names, if any. */
53
+ export interface RuntimeCredential {
54
+ readonly verify: RuntimeTokenVerifier;
55
+ /** The user id in `X-SegmentStream-Act-As`; `undefined` without the header. */
56
+ readonly actAs: string | undefined;
57
+ }
58
+ /** The request's runtime credential, or `undefined` while runtime authentication is disabled. */
59
+ export declare function runtimeCredential(verify: RuntimeTokenVerifier | undefined, request: HttpServerRequest.HttpServerRequest): RuntimeCredential | undefined;
@@ -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
+ }
@@ -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,8 @@ 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 { deployedAgentChannel } from "./agent/channel.js";
8
+ import { agentLayer } from "./agent/server.js";
7
9
  import { makeAggregates } from "./aggregate.js";
8
10
  import { platformAggregates } from "./aggregate-platform.js";
9
11
  import { ErrorResponsesLive, buildApi } from "./api.js";
@@ -17,6 +19,7 @@ import { deployedReplication } from "./replication.js";
17
19
  import { mcpLayer } from "./mcp/server.js";
18
20
  import { oauthLayer } from "./oauth/server.js";
19
21
  import { planApi } from "./plan.js";
22
+ import { runtimeTokenVerifier } from "./runtime-identity.js";
20
23
  /** The header every response carries: the hash of the manifest the service serves. */
21
24
  export const ONTOLOGY_HASH_HEADER = "x-ontology-hash";
22
25
  /** The header every response carries: the correlation id quoted in error bodies and logs. */
@@ -59,8 +62,8 @@ function decorate(response, requestId, hash) {
59
62
  });
60
63
  }
61
64
  /**
62
- * The ontology service: the derived HttpApi and the MCP server at `/mcp`, wired to the runtime,
63
- * behind Neon Auth, app-token and API-key authentication. Every response carries `x-ontology-hash`
65
+ * The ontology service: the derived HttpApi, the MCP server at `/mcp` and the agent's pairing routes,
66
+ * wired to the runtime, behind Neon Auth, app-token, API-key and runtime-token authentication. Every response carries `x-ontology-hash`
64
67
  * and `x-request-id`; failures answer with a JSON error and never a stack or SQL.
65
68
  */
66
69
  export function createOntologyService(ontology, config) {
@@ -76,6 +79,7 @@ export function createOntologyService(ontology, config) {
76
79
  const hooks = afterCommitHooks(plan, config.afterCommit);
77
80
  const verifyToken = tokenVerifier(config);
78
81
  const exchangeSession = config.neonAuth === undefined ? undefined : neonAuthSessionExchange(config.neonAuth);
82
+ const verifyRuntime = config.runtimeIdentity === undefined ? undefined : runtimeTokenVerifier(config.runtimeIdentity);
79
83
  const backend = runtime.connect({
80
84
  ontology,
81
85
  databaseUrl: config.databaseUrl,
@@ -86,19 +90,33 @@ export function createOntologyService(ontology, config) {
86
90
  ? deployedConnections(config)
87
91
  : localConnections(config.localConnections));
88
92
  const replicate = deployedReplication(config);
93
+ // The agent's runtime identity is configured only on deployments that have an agent.
94
+ const agentChannel = config.runtimeIdentity === undefined || config.agentUrl === undefined
95
+ ? undefined
96
+ : deployedAgentChannel(config.agentUrl);
89
97
  const aggregates = makeAggregates({
90
98
  manifest: ontology.manifest,
91
99
  transport: platformAggregates({ ...config, manifestHash: ontology.manifest.hash }),
92
100
  });
93
- const routes = HttpApiBuilder.layer(built.api).pipe(Layer.provide(handlersLayer(built, hooks, connections, replicate, aggregates)), Layer.provide([
94
- authenticationLayer(verifyToken, exchangeSession),
101
+ const routes = HttpApiBuilder.layer(built.api).pipe(Layer.provide(handlersLayer(built, hooks, connections, replicate, aggregates, agentChannel)), Layer.provide([
102
+ authenticationLayer(verifyToken, exchangeSession, verifyRuntime),
95
103
  ErrorResponsesLive,
96
104
  storageMaintenanceAuthenticationLayer(config.storageMaintenanceKey),
97
105
  ]));
98
106
  const mcp = config.mcp === false
99
107
  ? Layer.empty
100
- : Layer.mergeAll(mcpLayer({ built, hooks, connections, verifyToken, replicate, aggregates }), oauthLayer({ verifyToken }));
101
- const application = Layer.mergeAll(routes, mcp, DocsRoute, UnmatchedRoutes).pipe(Layer.provide(backend), Layer.provide(HttpServer.layerServices));
108
+ : Layer.mergeAll(mcpLayer({
109
+ built,
110
+ hooks,
111
+ connections,
112
+ verifyToken,
113
+ verifyRuntime,
114
+ replicate,
115
+ aggregates,
116
+ agentChannel,
117
+ }), oauthLayer({ verifyToken }));
118
+ const agent = agentLayer({ verifyToken, verifyRuntime, exchangeSession });
119
+ const application = Layer.mergeAll(routes, mcp, agent, DocsRoute, UnmatchedRoutes).pipe(Layer.provide(backend), Layer.provide(HttpServer.layerServices));
102
120
  const web = HttpRouter.toWebHandler(application, { disableLogger: true });
103
121
  const hash = ontology.manifest.hash;
104
122
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@segmentstream-cloud/ontology",
3
- "version": "0.13.0",
3
+ "version": "0.15.0",
4
4
  "description": "Define a SegmentStream workspace app's ontology: entities, relationships, roles and actions, written with Effect.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "SegmentStream",