@segmentstream-cloud/ontology 0.23.0 → 0.24.1
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/README.md +16 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/manifest.d.ts +5 -0
- package/dist/manifest.js +2 -0
- package/dist/migrate/apply.js +2 -1
- package/dist/migrate/model.d.ts +2 -0
- package/dist/migrate/plan.js +9 -1
- package/dist/migrate/storage.d.ts +3 -0
- package/dist/migrate/storage.js +10 -0
- package/dist/permissions.d.ts +3 -0
- package/dist/permissions.js +8 -1
- package/dist/runtime/history.d.ts +22 -0
- package/dist/runtime/history.js +2 -0
- package/dist/runtime/pipeline.js +6 -2
- package/dist/runtime/principals.d.ts +4 -1
- package/dist/runtime/principals.js +141 -63
- package/dist/runtime/reads.js +9 -1
- package/dist/runtime/service-binding.js +2 -1
- package/dist/runtime/sql-runtime.d.ts +12 -2
- package/dist/runtime/sql-runtime.js +33 -0
- package/dist/runtime/workload.d.ts +23 -0
- package/dist/runtime/workload.js +22 -0
- package/dist/runtime/writes.js +7 -1
- package/dist/runtime.d.ts +3 -0
- package/dist/service/authentication.d.ts +11 -0
- package/dist/service/authentication.js +114 -5
- package/dist/service/config.d.ts +4 -0
- package/dist/service/config.js +11 -0
- package/dist/service/handlers.js +2 -1
- package/dist/service/mcp/server.js +5 -4
- package/dist/service/operations.d.ts +25 -5
- package/dist/service/runtime-contract.d.ts +1 -0
- package/dist/service/schemas.d.ts +122 -0
- package/dist/storage/attachments.js +4 -4
- package/dist/storage/authorization.d.ts +1 -1
- package/dist/storage/authorization.js +9 -4
- package/dist/storage/sessions.js +3 -3
- package/dist/storage/writes.js +1 -1
- package/package.json +2 -1
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { Effect } from "effect";
|
|
2
2
|
import type { SqlClient } from "effect/unstable/sql";
|
|
3
|
-
import { type NotFound } from "../errors.js";
|
|
3
|
+
import { Conflict, type NotFound } from "../errors.js";
|
|
4
4
|
import type { Ontology } from "../ontology.js";
|
|
5
5
|
import type { Query, QueryResult } from "../query.js";
|
|
6
|
-
import type { OntologyRuntimeService, StoredRecord } from "../runtime.js";
|
|
6
|
+
import type { OntologyRuntimeService, StoredRecord, StoredValues } from "../runtime.js";
|
|
7
7
|
import { InvalidQuery } from "./errors.js";
|
|
8
8
|
import type { Edit, HistoryEntry, HistoryScope } from "./history.js";
|
|
9
9
|
/**
|
|
@@ -39,6 +39,16 @@ export interface SqlOntologyRuntime extends OntologyRuntimeService {
|
|
|
39
39
|
readonly limit?: number;
|
|
40
40
|
}) => Effect.Effect<QueryResult<HistoryEntry>, InvalidQuery>;
|
|
41
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* Platform identity bookkeeping touches only User properties and their history. User is excluded
|
|
44
|
+
* from business replication; keep this internal capability separate from the general runtime so
|
|
45
|
+
* deleting users, cascading relationships or editing business records still takes its fence.
|
|
46
|
+
*/
|
|
47
|
+
export declare function makePrincipalUserWriter(ontology: Ontology, sql: SqlClient.SqlClient, scope: Pick<HistoryScope, "action" | "actionId">): {
|
|
48
|
+
recordsSignIn: boolean;
|
|
49
|
+
create: (values: StoredValues) => Effect.Effect<Readonly<Record<string, unknown>>, Conflict | NotFound, never>;
|
|
50
|
+
update: (id: string, values: StoredValues) => Effect.Effect<Readonly<Record<string, unknown>>, Conflict | NotFound, never>;
|
|
51
|
+
};
|
|
42
52
|
/**
|
|
43
53
|
* The runtime over one SQL client, normally `@effect/sql-pg`'s. Statements are parameterised; the
|
|
44
54
|
* client runs them on the transaction of the calling fiber, so a runtime made for an action shares
|
|
@@ -2,6 +2,8 @@ import { Effect } from "effect";
|
|
|
2
2
|
import { beginReplicationWrite } from "../replication/transaction.js";
|
|
3
3
|
import { isSqlError } from "effect/unstable/sql/SqlError";
|
|
4
4
|
import { Conflict } from "../errors.js";
|
|
5
|
+
import { toSnakeCase } from "../names.js";
|
|
6
|
+
import { User } from "../user.js";
|
|
5
7
|
import { InvalidQuery, WriteOutsideAction } from "./errors.js";
|
|
6
8
|
import { entityModel, ontologyModel } from "./model.js";
|
|
7
9
|
import { loadRecord, notFound, readHistory, relatedCondition, runQuery, validateQuery, } from "./reads.js";
|
|
@@ -41,6 +43,37 @@ function uniqueConflict(entity, error) {
|
|
|
41
43
|
message: `A ${label} with this ${field.name} already exists.`,
|
|
42
44
|
}));
|
|
43
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* Platform identity bookkeeping touches only User properties and their history. User is excluded
|
|
48
|
+
* from business replication; keep this internal capability separate from the general runtime so
|
|
49
|
+
* deleting users, cascading relationships or editing business records still takes its fence.
|
|
50
|
+
*/
|
|
51
|
+
export function makePrincipalUserWriter(ontology, sql, scope) {
|
|
52
|
+
const model = ontologyModel(ontology);
|
|
53
|
+
const user = entityModel(model, User.name);
|
|
54
|
+
const allowed = new Set(Object.keys(User.definition.properties).map(toSnakeCase));
|
|
55
|
+
const client = sql.withoutTransforms();
|
|
56
|
+
const context = () => ({
|
|
57
|
+
model,
|
|
58
|
+
scope: { action: scope.action, actionId: scope.actionId, principalId: null },
|
|
59
|
+
edits: [],
|
|
60
|
+
run: (text, params) => client.unsafe(text, params.values),
|
|
61
|
+
});
|
|
62
|
+
const write = (values, operation) => Effect.suspend(() => {
|
|
63
|
+
if (!user.manifest.builtIn ||
|
|
64
|
+
Object.keys(values).some((name) => !allowed.has(name) || user.fieldsByName.get(name)?.source !== "property")) {
|
|
65
|
+
return Effect.die(new Error("Principal writes accept only built-in User properties."));
|
|
66
|
+
}
|
|
67
|
+
return sql
|
|
68
|
+
.withTransaction(operation())
|
|
69
|
+
.pipe(Effect.catchIf(isSqlError, (error) => uniqueConflict(user, error)));
|
|
70
|
+
});
|
|
71
|
+
return {
|
|
72
|
+
recordsSignIn: user.fieldsByName.has("last_sign_in_at"),
|
|
73
|
+
create: (values) => write(values, () => createRecord(context(), user, values)),
|
|
74
|
+
update: (id, values) => write(values, () => updateRecord(context(), user, id, values)),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
44
77
|
/**
|
|
45
78
|
* The runtime over one SQL client, normally `@effect/sql-pg`'s. Statements are parameterised; the
|
|
46
79
|
* client runs them on the transaction of the calling fiber, so a runtime made for an action shares
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
/** Verified platform claims retained with each write; never supplied by request headers. */
|
|
3
|
+
export declare const WorkloadExecution: Schema.Struct<{
|
|
4
|
+
readonly issuer: Schema.String;
|
|
5
|
+
readonly audience: Schema.String;
|
|
6
|
+
readonly organizationId: Schema.String;
|
|
7
|
+
readonly workspaceId: Schema.String;
|
|
8
|
+
readonly subject: Schema.String;
|
|
9
|
+
readonly deploymentId: Schema.String;
|
|
10
|
+
readonly ontologyDeploymentId: Schema.String;
|
|
11
|
+
readonly servingOntologyDeploymentId: Schema.String;
|
|
12
|
+
readonly ontologyOrigin: Schema.String;
|
|
13
|
+
readonly environment: Schema.Literal<"production">;
|
|
14
|
+
readonly jobName: Schema.String;
|
|
15
|
+
readonly runId: Schema.String;
|
|
16
|
+
readonly executionId: Schema.NullOr<Schema.String>;
|
|
17
|
+
readonly attemptId: Schema.String;
|
|
18
|
+
readonly generation: Schema.Number;
|
|
19
|
+
readonly tokenId: Schema.String;
|
|
20
|
+
readonly issuedAt: Schema.Number;
|
|
21
|
+
readonly expiresAt: Schema.Number;
|
|
22
|
+
}>;
|
|
23
|
+
export type WorkloadExecution = typeof WorkloadExecution.Type;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
/** Verified platform claims retained with each write; never supplied by request headers. */
|
|
3
|
+
export const WorkloadExecution = Schema.Struct({
|
|
4
|
+
issuer: Schema.String,
|
|
5
|
+
audience: Schema.String,
|
|
6
|
+
organizationId: Schema.String,
|
|
7
|
+
workspaceId: Schema.String,
|
|
8
|
+
subject: Schema.String,
|
|
9
|
+
deploymentId: Schema.String,
|
|
10
|
+
ontologyDeploymentId: Schema.String,
|
|
11
|
+
servingOntologyDeploymentId: Schema.String,
|
|
12
|
+
ontologyOrigin: Schema.String,
|
|
13
|
+
environment: Schema.Literal("production"),
|
|
14
|
+
jobName: Schema.String,
|
|
15
|
+
runId: Schema.String,
|
|
16
|
+
executionId: Schema.NullOr(Schema.String),
|
|
17
|
+
attemptId: Schema.String,
|
|
18
|
+
generation: Schema.Number,
|
|
19
|
+
tokenId: Schema.String,
|
|
20
|
+
issuedAt: Schema.Number,
|
|
21
|
+
expiresAt: Schema.Number,
|
|
22
|
+
});
|
package/dist/runtime/writes.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Effect } from "effect";
|
|
2
2
|
import { resolveBlobWrites } from "../storage/writes.js";
|
|
3
3
|
import { Conflict } from "../errors.js";
|
|
4
|
-
import { quoteIdentifier } from "../migrate/storage.js";
|
|
4
|
+
import { ACTION_ATTRIBUTION_TABLE, quoteIdentifier } from "../migrate/storage.js";
|
|
5
5
|
import { InvalidValues } from "./errors.js";
|
|
6
6
|
import { uuidv7 } from "./ids.js";
|
|
7
7
|
import { diffRecords, insertHistory } from "./history.js";
|
|
@@ -13,6 +13,12 @@ import { columnOf, Params } from "./statement.js";
|
|
|
13
13
|
function recordEdit(context, entity, edit) {
|
|
14
14
|
return Effect.gen(function* () {
|
|
15
15
|
const params = new Params();
|
|
16
|
+
if (context.scope.execution !== undefined) {
|
|
17
|
+
const attribution = new Params();
|
|
18
|
+
yield* context.run(`INSERT INTO "${ACTION_ATTRIBUTION_TABLE}" (action_id, execution) VALUES (` +
|
|
19
|
+
`${attribution.add(context.scope.actionId, "uuid")}, ` +
|
|
20
|
+
`${attribution.add(JSON.stringify(context.scope.execution), "jsonb")}) ON CONFLICT DO NOTHING`, attribution);
|
|
21
|
+
}
|
|
16
22
|
yield* context.run(insertHistory(context.scope, { entity, edit }, params), params);
|
|
17
23
|
context.edits.push(edit);
|
|
18
24
|
});
|
package/dist/runtime.d.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { Context, Effect } from "effect";
|
|
2
2
|
import type { Conflict, NotFound } from "./errors.js";
|
|
3
3
|
import type { Where } from "./query.js";
|
|
4
|
+
import type { WorkloadExecution } from "./runtime/workload.js";
|
|
4
5
|
/** Who runs an action: an app member or a service principal, both `user` records. */
|
|
5
6
|
export interface Principal {
|
|
7
|
+
/** Present only for a verified managed pipeline worker. */
|
|
8
|
+
readonly execution?: WorkloadExecution;
|
|
6
9
|
/** The id of the principal's `user` record. */
|
|
7
10
|
readonly id: string;
|
|
8
11
|
readonly kind: "member" | "service";
|
|
@@ -2,6 +2,7 @@ import { Context, Effect, Layer, Redacted } from "effect";
|
|
|
2
2
|
import { HttpServerRequest, type HttpServerResponse } from "effect/unstable/http";
|
|
3
3
|
import { HttpApiMiddleware, HttpApiSecurity } from "effect/unstable/httpapi";
|
|
4
4
|
import { type JWTVerifyGetKey } from "jose";
|
|
5
|
+
import type { WorkloadExecution } from "../runtime/workload.js";
|
|
5
6
|
import type { Principal } from "../runtime.js";
|
|
6
7
|
import type { NeonAuthEnvironment } from "./config.js";
|
|
7
8
|
import { RequestId } from "./errors.js";
|
|
@@ -32,6 +33,10 @@ declare const Authentication_base: HttpApiMiddleware.ServiceClass<Authentication
|
|
|
32
33
|
export declare class Authentication extends Authentication_base {
|
|
33
34
|
}
|
|
34
35
|
export interface TokenVerifierConfig {
|
|
36
|
+
/** Production workload activation and the deployment actually serving the request. */
|
|
37
|
+
readonly workloadTarget?: {
|
|
38
|
+
readonly deploymentId: string;
|
|
39
|
+
};
|
|
35
40
|
/** The control plane: the issuer of app tokens and the host of their JWKS. */
|
|
36
41
|
readonly segmentstreamUrl: string;
|
|
37
42
|
readonly applicationId: string;
|
|
@@ -39,6 +44,9 @@ export interface TokenVerifierConfig {
|
|
|
39
44
|
readonly organizationId: string;
|
|
40
45
|
}
|
|
41
46
|
export type TokenVerification = {
|
|
47
|
+
readonly status: "workload";
|
|
48
|
+
readonly execution: WorkloadExecution;
|
|
49
|
+
} | {
|
|
42
50
|
readonly status: "valid";
|
|
43
51
|
readonly identity: SignInIdentity;
|
|
44
52
|
} | {
|
|
@@ -56,6 +64,9 @@ export declare function neonAuthKeySet(jwksUrl: string): JWTVerifyGetKey;
|
|
|
56
64
|
* `SEGMENTSTREAM_URL` for this application (`aud: app:<application id>`), organization and workspace.
|
|
57
65
|
*/
|
|
58
66
|
export declare function makeTokenVerifier(config: TokenVerifierConfig, keySet: JWTVerifyGetKey): TokenVerifier;
|
|
67
|
+
export declare const WORKLOAD_TOKEN_TYPE = "segmentstream-ontology-workload+jwt";
|
|
68
|
+
/** A separate verifier: a workload token can never resolve as a human or act as an agent. */
|
|
69
|
+
export declare function makeWorkloadTokenVerifier(config: TokenVerifierConfig, keySet: JWTVerifyGetKey): TokenVerifier;
|
|
59
70
|
/**
|
|
60
71
|
* Neon Auth's issuer is the origin of its Auth URL: `https://ep-….neon.tech/ontology/auth` issues
|
|
61
72
|
* tokens with `iss: https://ep-….neon.tech`.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Cause, Context, Effect, Layer, Redacted } from "effect";
|
|
2
2
|
import { HttpServerRequest } from "effect/unstable/http";
|
|
3
3
|
import { HttpApiMiddleware, HttpApiSecurity, OpenApi } from "effect/unstable/httpapi";
|
|
4
|
-
import { createRemoteJWKSet, decodeJwt, errors, jwtVerify, } from "jose";
|
|
4
|
+
import { createRemoteJWKSet, decodeJwt, decodeProtectedHeader, errors, jwtVerify, } from "jose";
|
|
5
5
|
import { API_KEY_PREFIX } from "../runtime/principals.js";
|
|
6
6
|
import { RequestId, crossOriginSession, forbidden, internalError, notInvited, toServerResponse, unauthorized, unavailable, } from "./errors.js";
|
|
7
7
|
import { ACCESS_TOKEN_PREFIX, principalOfAccessToken } from "./oauth/protocol.js";
|
|
@@ -76,6 +76,7 @@ export function makeTokenVerifier(config, keySet) {
|
|
|
76
76
|
try {
|
|
77
77
|
const verified = await jwtVerify(token, keySet, {
|
|
78
78
|
algorithms: ["ES256"],
|
|
79
|
+
typ: "JWT",
|
|
79
80
|
issuer,
|
|
80
81
|
audience,
|
|
81
82
|
clockTolerance: CLOCK_TOLERANCE_SECONDS,
|
|
@@ -90,6 +91,96 @@ export function makeTokenVerifier(config, keySet) {
|
|
|
90
91
|
return identity === undefined ? { status: "invalid" } : { status: "valid", identity };
|
|
91
92
|
};
|
|
92
93
|
}
|
|
94
|
+
export const WORKLOAD_TOKEN_TYPE = "segmentstream-ontology-workload+jwt";
|
|
95
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
96
|
+
/** A separate verifier: a workload token can never resolve as a human or act as an agent. */
|
|
97
|
+
export function makeWorkloadTokenVerifier(config, keySet) {
|
|
98
|
+
return async (token) => {
|
|
99
|
+
if (config.workloadTarget === undefined) {
|
|
100
|
+
return { status: "invalid" };
|
|
101
|
+
}
|
|
102
|
+
let payload;
|
|
103
|
+
try {
|
|
104
|
+
const verified = await jwtVerify(token, keySet, {
|
|
105
|
+
algorithms: ["ES256"],
|
|
106
|
+
typ: WORKLOAD_TOKEN_TYPE,
|
|
107
|
+
issuer: new URL(config.segmentstreamUrl).origin,
|
|
108
|
+
audience: `ontology:${config.applicationId}`,
|
|
109
|
+
clockTolerance: CLOCK_TOLERANCE_SECONDS,
|
|
110
|
+
requiredClaims: [
|
|
111
|
+
"sub",
|
|
112
|
+
"exp",
|
|
113
|
+
"iat",
|
|
114
|
+
"jti",
|
|
115
|
+
"org",
|
|
116
|
+
"wsp",
|
|
117
|
+
"deploymentId",
|
|
118
|
+
"ontologyDeploymentId",
|
|
119
|
+
"ontologyOrigin",
|
|
120
|
+
"environment",
|
|
121
|
+
"jobName",
|
|
122
|
+
"runId",
|
|
123
|
+
"executionId",
|
|
124
|
+
"attemptId",
|
|
125
|
+
"generation",
|
|
126
|
+
],
|
|
127
|
+
});
|
|
128
|
+
payload = verified.payload;
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
return keySetUnavailable(error) ? { status: "unavailable" } : { status: "invalid" };
|
|
132
|
+
}
|
|
133
|
+
const strings = ["jti", "jobName"];
|
|
134
|
+
const ids = ["deploymentId", "ontologyDeploymentId", "runId", "attemptId"];
|
|
135
|
+
if (payload.org !== config.organizationId ||
|
|
136
|
+
payload.wsp !== config.workspaceId ||
|
|
137
|
+
payload.sub !== `pipeline-workers:${config.workspaceId}` ||
|
|
138
|
+
payload.environment !== "production" ||
|
|
139
|
+
payload.aud !== `ontology:${config.applicationId}` ||
|
|
140
|
+
strings.some((name) => typeof payload[name] !== "string" || payload[name].trim() === "") ||
|
|
141
|
+
ids.some((name) => typeof payload[name] !== "string" || !UUID.test(payload[name])) ||
|
|
142
|
+
(payload.executionId !== null &&
|
|
143
|
+
(typeof payload.executionId !== "string" || !UUID.test(payload.executionId))) ||
|
|
144
|
+
!Number.isSafeInteger(payload.generation) ||
|
|
145
|
+
payload.generation < 1 ||
|
|
146
|
+
!Number.isSafeInteger(payload.iat) ||
|
|
147
|
+
!Number.isSafeInteger(payload.exp) ||
|
|
148
|
+
payload.iat > Math.floor(Date.now() / 1000) + CLOCK_TOLERANCE_SECONDS ||
|
|
149
|
+
payload.exp <= payload.iat ||
|
|
150
|
+
payload.exp - payload.iat > 300 ||
|
|
151
|
+
typeof payload.ontologyOrigin !== "string" ||
|
|
152
|
+
!URL.canParse(payload.ontologyOrigin)) {
|
|
153
|
+
return { status: "invalid" };
|
|
154
|
+
}
|
|
155
|
+
const origin = new URL(payload.ontologyOrigin);
|
|
156
|
+
if (origin.protocol !== "https:" || origin.origin !== payload.ontologyOrigin) {
|
|
157
|
+
return { status: "invalid" };
|
|
158
|
+
}
|
|
159
|
+
return {
|
|
160
|
+
status: "workload",
|
|
161
|
+
execution: {
|
|
162
|
+
issuer: payload.iss,
|
|
163
|
+
audience: payload.aud,
|
|
164
|
+
organizationId: config.organizationId,
|
|
165
|
+
workspaceId: config.workspaceId,
|
|
166
|
+
subject: payload.sub,
|
|
167
|
+
deploymentId: payload.deploymentId,
|
|
168
|
+
ontologyDeploymentId: payload.ontologyDeploymentId,
|
|
169
|
+
servingOntologyDeploymentId: config.workloadTarget.deploymentId,
|
|
170
|
+
ontologyOrigin: payload.ontologyOrigin,
|
|
171
|
+
environment: "production",
|
|
172
|
+
jobName: payload.jobName,
|
|
173
|
+
runId: payload.runId,
|
|
174
|
+
executionId: payload.executionId,
|
|
175
|
+
attemptId: payload.attemptId,
|
|
176
|
+
generation: payload.generation,
|
|
177
|
+
tokenId: payload.jti,
|
|
178
|
+
issuedAt: payload.iat,
|
|
179
|
+
expiresAt: payload.exp,
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
};
|
|
183
|
+
}
|
|
93
184
|
/**
|
|
94
185
|
* Neon Auth's issuer is the origin of its Auth URL: `https://ep-….neon.tech/ontology/auth` issues
|
|
95
186
|
* tokens with `iss: https://ep-….neon.tech`.
|
|
@@ -166,11 +257,21 @@ export function verifyByIssuer(verifiers) {
|
|
|
166
257
|
* once it has Neon Auth. Both are accepted while workspaces move to Neon Auth.
|
|
167
258
|
*/
|
|
168
259
|
export function tokenVerifier(config) {
|
|
260
|
+
const platformKeys = remoteKeySet(config.segmentstreamUrl);
|
|
261
|
+
const human = makeTokenVerifier(config, platformKeys);
|
|
262
|
+
const workload = makeWorkloadTokenVerifier(config, platformKeys);
|
|
263
|
+
const platform = (token) => {
|
|
264
|
+
try {
|
|
265
|
+
return decodeProtectedHeader(token).typ === WORKLOAD_TOKEN_TYPE
|
|
266
|
+
? workload(token)
|
|
267
|
+
: human(token);
|
|
268
|
+
}
|
|
269
|
+
catch {
|
|
270
|
+
return Promise.resolve({ status: "invalid" });
|
|
271
|
+
}
|
|
272
|
+
};
|
|
169
273
|
const verifiers = new Map([
|
|
170
|
-
[
|
|
171
|
-
new URL(config.segmentstreamUrl).origin,
|
|
172
|
-
makeTokenVerifier(config, remoteKeySet(config.segmentstreamUrl)),
|
|
173
|
-
],
|
|
274
|
+
[new URL(config.segmentstreamUrl).origin, platform],
|
|
174
275
|
]);
|
|
175
276
|
if (config.neonAuth !== undefined) {
|
|
176
277
|
verifiers.set(neonAuthIssuer(config.neonAuth.baseUrl), makeNeonAuthTokenVerifier(config.neonAuth, neonAuthKeySet(config.neonAuth.jwksUrl)));
|
|
@@ -337,6 +438,14 @@ export function authenticate(credential, verifyToken, options = {}) {
|
|
|
337
438
|
}
|
|
338
439
|
}
|
|
339
440
|
const verification = yield* Effect.promise(() => verifyToken(token));
|
|
441
|
+
if (verification.status === "workload") {
|
|
442
|
+
const principal = backend.resolvePipelineWorker === undefined
|
|
443
|
+
? undefined
|
|
444
|
+
: yield* backend.resolvePipelineWorker(verification.execution);
|
|
445
|
+
return principal === undefined
|
|
446
|
+
? { status: "rejected" }
|
|
447
|
+
: { status: "authenticated", principal };
|
|
448
|
+
}
|
|
340
449
|
if (verification.status !== "valid") {
|
|
341
450
|
return verification.status === "invalid"
|
|
342
451
|
? { status: "rejected" }
|
package/dist/service/config.d.ts
CHANGED
|
@@ -66,6 +66,10 @@ export interface OntologyEnvironment {
|
|
|
66
66
|
* `segmentstreamUrl` for `workspaceId`; previews and local development have none.
|
|
67
67
|
*/
|
|
68
68
|
readonly appCredential?: Redacted.Redacted<string>;
|
|
69
|
+
/** Production deployment identity; absent in local, preview and earlier deployments. */
|
|
70
|
+
readonly workloadTarget?: {
|
|
71
|
+
readonly deploymentId: string;
|
|
72
|
+
};
|
|
69
73
|
/** Present once the workspace has Neon Auth: members then sign in with its tokens. */
|
|
70
74
|
readonly neonAuth?: NeonAuthEnvironment;
|
|
71
75
|
/**
|
package/dist/service/config.js
CHANGED
|
@@ -134,6 +134,13 @@ export function configFromEnv(environment = process.env) {
|
|
|
134
134
|
message: `The ontology service cannot start: ${list} ${verb} not set.`,
|
|
135
135
|
});
|
|
136
136
|
}
|
|
137
|
+
const deploymentId = read("SEGMENTSTREAM_ONTOLOGY_DEPLOYMENT_ID");
|
|
138
|
+
if (deploymentId !== "" &&
|
|
139
|
+
!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(deploymentId)) {
|
|
140
|
+
throw new OntologyServiceConfigError({
|
|
141
|
+
message: "SEGMENTSTREAM_ONTOLOGY_DEPLOYMENT_ID must be a UUID.",
|
|
142
|
+
});
|
|
143
|
+
}
|
|
137
144
|
const neonAuth = neonAuthFromEnv(read);
|
|
138
145
|
const runtimeIdentity = runtimeIdentityFromEnv(read);
|
|
139
146
|
const localConnections = localConnectionsFromEnv(environment);
|
|
@@ -163,6 +170,10 @@ export function configFromEnv(environment = process.env) {
|
|
|
163
170
|
...(read(APP_CREDENTIAL_VARIABLE)
|
|
164
171
|
? { appCredential: Redacted.make(read(APP_CREDENTIAL_VARIABLE)) }
|
|
165
172
|
: {}),
|
|
173
|
+
...(environment.VERCEL_ENV === "production" &&
|
|
174
|
+
read("SEGMENTSTREAM_ONTOLOGY_DEPLOYMENT_ID") !== ""
|
|
175
|
+
? { workloadTarget: { deploymentId: read("SEGMENTSTREAM_ONTOLOGY_DEPLOYMENT_ID") } }
|
|
176
|
+
: {}),
|
|
166
177
|
...(neonAuth === undefined ? {} : { neonAuth }),
|
|
167
178
|
...(runtimeIdentity === undefined ? {} : { runtimeIdentity }),
|
|
168
179
|
...(runtimeIdentity === undefined || read(AGENT_HOST_VARIABLE) === ""
|
package/dist/service/handlers.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { effectivePermissions } from "../permissions.js";
|
|
1
2
|
import { HttpServerResponse } from "effect/unstable/http";
|
|
2
3
|
import { Effect, Layer, Schema } from "effect";
|
|
3
4
|
import { HttpApiBuilder, OpenApi } from "effect/unstable/httpapi";
|
|
@@ -145,7 +146,7 @@ function serviceHandlers(built, operationsOf, aggregate) {
|
|
|
145
146
|
return undefined;
|
|
146
147
|
}
|
|
147
148
|
const principal = yield* CurrentPrincipal;
|
|
148
|
-
return principal
|
|
149
|
+
return effectivePermissions(principal);
|
|
149
150
|
});
|
|
150
151
|
return handlers
|
|
151
152
|
.handle("catalog", (request) => Effect.map(permissionsFor(request.query.as), (permissions) => catalogFor(manifest, permissions)))
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { effectivePermissions } from "../../permissions.js";
|
|
1
2
|
import { Cause, Data, Effect, Layer, Option, Schema, SchemaTransformation } from "effect";
|
|
2
3
|
import { HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/unstable/http";
|
|
3
4
|
import { HttpApiBuilder, HttpApiSecurity } from "effect/unstable/httpapi";
|
|
@@ -175,7 +176,7 @@ function runTool({ built, operations, aggregate, conversations }, tool, args, re
|
|
|
175
176
|
case "describe": {
|
|
176
177
|
const principal = yield* CurrentPrincipal;
|
|
177
178
|
return {
|
|
178
|
-
text: describeCatalog(built.plan.ontology.manifest, principal
|
|
179
|
+
text: describeCatalog(built.plan.ontology.manifest, effectivePermissions(principal)),
|
|
179
180
|
};
|
|
180
181
|
}
|
|
181
182
|
case "aggregate": {
|
|
@@ -285,7 +286,7 @@ function readResource({ built }, uri) {
|
|
|
285
286
|
{
|
|
286
287
|
uri,
|
|
287
288
|
mimeType: "application/json",
|
|
288
|
-
text: JSON.stringify(encodeManifest(catalogFor(manifest, principal
|
|
289
|
+
text: JSON.stringify(encodeManifest(catalogFor(manifest, effectivePermissions(principal)))),
|
|
289
290
|
},
|
|
290
291
|
];
|
|
291
292
|
case DESCRIBE_URI:
|
|
@@ -293,7 +294,7 @@ function readResource({ built }, uri) {
|
|
|
293
294
|
{
|
|
294
295
|
uri,
|
|
295
296
|
mimeType: "text/plain",
|
|
296
|
-
text: describeCatalog(manifest, principal
|
|
297
|
+
text: describeCatalog(manifest, effectivePermissions(principal)),
|
|
297
298
|
},
|
|
298
299
|
];
|
|
299
300
|
default:
|
|
@@ -354,7 +355,7 @@ function dispatch(server, message, request) {
|
|
|
354
355
|
title: "Ontology",
|
|
355
356
|
version: server.built.plan.ontology.manifest.hash,
|
|
356
357
|
},
|
|
357
|
-
instructions: instructions(server, principal
|
|
358
|
+
instructions: instructions(server, effectivePermissions(principal)),
|
|
358
359
|
});
|
|
359
360
|
case "ping":
|
|
360
361
|
return rpcResult(id, {});
|
|
@@ -74,19 +74,39 @@ export declare function makeOperations(backend: Backend, plan: ApiPlan, hooks: R
|
|
|
74
74
|
readonly after?: string;
|
|
75
75
|
readonly limit?: number;
|
|
76
76
|
}) => Effect.Effect<QueryResult<{
|
|
77
|
+
readonly at: string;
|
|
77
78
|
readonly id: string;
|
|
78
|
-
readonly recordId: string;
|
|
79
|
-
readonly action: string;
|
|
80
|
-
readonly actionId: string;
|
|
81
79
|
readonly kind: "create" | "update" | "delete" | "link" | "unlink";
|
|
82
|
-
readonly
|
|
83
|
-
readonly at: string;
|
|
80
|
+
readonly action: string;
|
|
84
81
|
readonly changes: {
|
|
85
82
|
readonly [x: string]: {
|
|
86
83
|
readonly before: import("effect/Schema").Json;
|
|
87
84
|
readonly after: import("effect/Schema").Json;
|
|
88
85
|
};
|
|
89
86
|
};
|
|
87
|
+
readonly principalId: string | null;
|
|
88
|
+
readonly recordId: string;
|
|
89
|
+
readonly actionId: string;
|
|
90
|
+
readonly execution?: {
|
|
91
|
+
readonly issuer: string;
|
|
92
|
+
readonly audience: string;
|
|
93
|
+
readonly organizationId: string;
|
|
94
|
+
readonly workspaceId: string;
|
|
95
|
+
readonly subject: string;
|
|
96
|
+
readonly deploymentId: string;
|
|
97
|
+
readonly ontologyDeploymentId: string;
|
|
98
|
+
readonly servingOntologyDeploymentId: string;
|
|
99
|
+
readonly ontologyOrigin: string;
|
|
100
|
+
readonly environment: "production";
|
|
101
|
+
readonly jobName: string;
|
|
102
|
+
readonly runId: string;
|
|
103
|
+
readonly executionId: string | null;
|
|
104
|
+
readonly attemptId: string;
|
|
105
|
+
readonly generation: number;
|
|
106
|
+
readonly tokenId: string;
|
|
107
|
+
readonly issuedAt: number;
|
|
108
|
+
readonly expiresAt: number;
|
|
109
|
+
} | undefined;
|
|
90
110
|
}>, PermissionDenied | import("../errors.js").NotFound | import("../index.js").InvalidQuery, CurrentPrincipal>;
|
|
91
111
|
/** The records a relationship reaches from one record, as a page of the target entity. */
|
|
92
112
|
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>;
|
|
@@ -274,6 +274,7 @@ declare const OntologyBackend_base: Context.ServiceClass<OntologyBackend, "@segm
|
|
|
274
274
|
* Finds, links or creates the member's user. `undefined` when the user may not sign in, and
|
|
275
275
|
* {@link NotInvited} when the sign-in policy turns away a person nobody invited.
|
|
276
276
|
*/
|
|
277
|
+
readonly resolvePipelineWorker?: (execution: import("../runtime/workload.js").WorkloadExecution) => Effect.Effect<Principal | undefined>;
|
|
277
278
|
readonly resolvePrincipal: (identity: SignInIdentity) => Effect.Effect<Principal | undefined, NotInvited>;
|
|
278
279
|
/**
|
|
279
280
|
* Whether a person may sign in with this address, and how, read before the app emails them.
|