@powerhousedao/reactor-api 6.2.3-dev.3 → 6.2.3-dev.31
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 +7 -2
- package/dist/{adapter-gateway-apollo-Cn9E96GF.mjs → adapter-gateway-apollo-B54G_cXT.mjs} +18 -17
- package/dist/adapter-gateway-apollo-B54G_cXT.mjs.map +1 -0
- package/dist/{adapter-gateway-apollo-C_RdohOv.mjs → adapter-gateway-apollo-D2DNlPY0.mjs} +3 -3
- package/dist/{adapter-gateway-mercurius-CjaUcMgX.mjs → adapter-gateway-mercurius-rbKlwF9m.mjs} +7 -6
- package/dist/adapter-gateway-mercurius-rbKlwF9m.mjs.map +1 -0
- package/dist/{adapter-gateway-stitching-HmWjRMhm.mjs → adapter-gateway-stitching-Dzv10u8D.mjs} +18 -14
- package/dist/adapter-gateway-stitching-Dzv10u8D.mjs.map +1 -0
- package/dist/{adapter-http-express-B39rPhqH.mjs → adapter-http-express-CWqziB1p.mjs} +7 -7
- package/dist/adapter-http-express-CWqziB1p.mjs.map +1 -0
- package/dist/{adapter-http-fastify-BwXO6rqE.mjs → adapter-http-fastify-CYZQ6FFv.mjs} +10 -9
- package/dist/adapter-http-fastify-CYZQ6FFv.mjs.map +1 -0
- package/dist/index.d.mts +865 -453
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1160 -320
- package/dist/index.mjs.map +1 -1
- package/dist/{utils-XWVecaNn.mjs → pieces-WWMZBM0n.mjs} +146 -9
- package/dist/pieces-WWMZBM0n.mjs.map +1 -0
- package/dist/rolldown-runtime-8H4AJuhK.mjs +16 -0
- package/dist/run-node-handler-CQjyjOuN.mjs +52 -0
- package/dist/run-node-handler-CQjyjOuN.mjs.map +1 -0
- package/dist/src/packages/https-hooks.d.mts +4 -4
- package/dist/src/packages/https-hooks.d.mts.map +1 -1
- package/dist/src/packages/https-hooks.mjs +2 -2
- package/dist/src/packages/https-hooks.mjs.map +1 -1
- package/dist/src/packages/vite-loader.d.mts +10 -6
- package/dist/src/packages/vite-loader.d.mts.map +1 -1
- package/dist/src/packages/vite-loader.mjs +71 -9
- package/dist/src/packages/vite-loader.mjs.map +1 -1
- package/dist/{types-DcYyNcHJ.d.mts → types-CuSz_zv6.d.mts} +26 -3
- package/dist/types-CuSz_zv6.d.mts.map +1 -0
- package/dist/{websocket-DncAB9XK.mjs → websocket-_SH5aBcy.mjs} +3 -3
- package/dist/{websocket-DncAB9XK.mjs.map → websocket-_SH5aBcy.mjs.map} +1 -1
- package/package.json +18 -18
- package/dist/adapter-gateway-apollo-Cn9E96GF.mjs.map +0 -1
- package/dist/adapter-gateway-mercurius-CjaUcMgX.mjs.map +0 -1
- package/dist/adapter-gateway-stitching-HmWjRMhm.mjs.map +0 -1
- package/dist/adapter-http-express-B39rPhqH.mjs.map +0 -1
- package/dist/adapter-http-fastify-BwXO6rqE.mjs.map +0 -1
- package/dist/path-normalize-BOQwY9wR.mjs +0 -23
- package/dist/path-normalize-BOQwY9wR.mjs.map +0 -1
- package/dist/types-DcYyNcHJ.d.mts.map +0 -1
- package/dist/utils-XWVecaNn.mjs.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -1,24 +1,26 @@
|
|
|
1
1
|
|
|
2
|
-
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="
|
|
3
|
-
import {
|
|
2
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="5c9c63cc-7da0-5a74-ae18-1418ddd6118f")}catch(e){}}();
|
|
3
|
+
import { t as __exportAll } from "./rolldown-runtime-8H4AJuhK.mjs";
|
|
4
|
+
import { C as AuthorizedDocumentHandle, D as ForbiddenError, E as AuthenticationRequiredError, O as MalformedStoredOperationError, S as AuthorizationPolicy, T as AuthEvaluationUnsupportedError, _ as loadSubgraphs, a as piecesFromCdnList, b as CanonicalDocumentIdResolutionError, c as buildGraphQlDriveDocument, d as isSubgraphClass, f as debounce, g as loadProcessors, h as loadDocumentModels, i as pieceListLocation, l as buildGraphqlOperation, o as piecesFromListModule, p as extractUpgradeManifests, r as PIECE_SOURCE_LIST, s as buildGraphQlDocument, u as buildGraphqlOperations, v as BaseSubgraph, w as createAuthorizationService, x as createCanonicalDocumentIdResolver, y as callerSubject } from "./pieces-WWMZBM0n.mjs";
|
|
4
5
|
import { AnalyticsQueryEngine } from "@powerhousedao/analytics-engine-core";
|
|
5
6
|
import { AnalyticsModel, AnalyticsResolvers, typedefs } from "@powerhousedao/analytics-engine-graphql";
|
|
6
7
|
import { gql } from "graphql-tag";
|
|
7
|
-
import { GraphQLError, Kind, parse, print } from "graphql";
|
|
8
|
-
import { AuthEnforcementDisabledError, DEFAULT_DRIVE_CONTAINER_TYPES, DriveCollectionId, ModelReadGate, PropagationMode as PropagationMode$1, SyncScopeGate, consolidateSyncOperations, createReactorHostModuleBase, driveIdFromUrl, envelopesToSyncOperations, parseDriveUrl, readDecisionModel, syncOperationErrorType } from "@powerhousedao/reactor";
|
|
8
|
+
import { GraphQLError, Kind, Lexer, Source, TokenKind, parse, print, visit } from "graphql";
|
|
9
|
+
import { AuthEnforcementDisabledError, BareReadGate, DEFAULT_DRIVE_CONTAINER_TYPES, DriveCollectionId, JobStatus, ModelReadGate, PropagationMode as PropagationMode$1, SyncScopeGate, consolidateSyncOperations, createReactorHostModuleBase, driveIdFromUrl, envelopesToSyncOperations, parseDriveUrl, readDecisionModel, syncOperationErrorType } from "@powerhousedao/reactor";
|
|
9
10
|
import { ConsoleLogger, childLogger, documentModelDocumentModelModule } from "document-model";
|
|
10
|
-
import path, { join } from "node:path";
|
|
11
|
+
import path, { isAbsolute, join } from "node:path";
|
|
11
12
|
import { match } from "path-to-regexp";
|
|
12
13
|
import posix from "node:path/posix";
|
|
13
14
|
import { Readable } from "node:stream";
|
|
14
|
-
import fs, { readFileSync, watchFile } from "node:fs";
|
|
15
|
+
import fs, { existsSync, readFileSync, watchFile } from "node:fs";
|
|
15
16
|
import { pipeline } from "node:stream/promises";
|
|
16
17
|
import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
18
|
+
import { pathToFileURL } from "node:url";
|
|
17
19
|
import { buildSubgraphSchema } from "@apollo/subgraph";
|
|
18
20
|
import { typeDefs } from "@powerhousedao/document-engineering/graphql";
|
|
19
21
|
import { camelCase, kebabCase, pascalCase } from "change-case";
|
|
20
22
|
import { GraphQLJSON, GraphQLJSONObject } from "graphql-type-json";
|
|
21
|
-
import { deserializeSignature, serializeSignature, setName } from "@powerhousedao/shared/document-model";
|
|
23
|
+
import { deserializeSignature, serializeSignature, setName, withSignaturePolicy } from "@powerhousedao/shared/document-model";
|
|
22
24
|
import * as z$1 from "zod";
|
|
23
25
|
import { z } from "zod";
|
|
24
26
|
import { createHandler } from "graphql-sse/lib/use/fetch";
|
|
@@ -46,19 +48,8 @@ import { AtomicNodeFs } from "@powerhousedao/pglite-fs";
|
|
|
46
48
|
import knex from "knex";
|
|
47
49
|
import ClientPgLite from "knex-pglite";
|
|
48
50
|
import { KyselyKnexDialect, PGColdDialect } from "kysely-knex";
|
|
49
|
-
//#region \0rolldown/runtime.js
|
|
50
|
-
var __defProp = Object.defineProperty;
|
|
51
|
-
var __exportAll = (all, no_symbols) => {
|
|
52
|
-
let target = {};
|
|
53
|
-
for (var name in all) __defProp(target, name, {
|
|
54
|
-
get: all[name],
|
|
55
|
-
enumerable: true
|
|
56
|
-
});
|
|
57
|
-
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
58
|
-
return target;
|
|
59
|
-
};
|
|
60
|
-
//#endregion
|
|
61
51
|
//#region src/graphql/analytics-subgraph.ts
|
|
52
|
+
const analyticsResolvers = AnalyticsResolvers;
|
|
62
53
|
var AnalyticsSubgraph = class extends BaseSubgraph {
|
|
63
54
|
analyticsStore;
|
|
64
55
|
analyticsModel;
|
|
@@ -66,13 +57,33 @@ var AnalyticsSubgraph = class extends BaseSubgraph {
|
|
|
66
57
|
typeDefs = gql`
|
|
67
58
|
${typedefs}
|
|
68
59
|
`;
|
|
69
|
-
|
|
60
|
+
/**
|
|
61
|
+
* Every analytics field hangs off `Query.analytics`, so gating it gates them
|
|
62
|
+
* all. Series are aggregates a processor built from many documents and carry
|
|
63
|
+
* no document to decide on, so only a caller with policy-wide read (anyone
|
|
64
|
+
* under OPEN, an admin otherwise) may read them.
|
|
65
|
+
*/
|
|
66
|
+
resolvers = {
|
|
67
|
+
...analyticsResolvers,
|
|
68
|
+
Query: {
|
|
69
|
+
...analyticsResolvers.Query,
|
|
70
|
+
analytics: (parent, args, ctx) => {
|
|
71
|
+
this.#assertCanReadAnalytics(ctx);
|
|
72
|
+
return analyticsResolvers.Query.analytics(parent, args, ctx);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
};
|
|
70
76
|
constructor(args) {
|
|
71
77
|
super(args);
|
|
72
78
|
this.analyticsStore = args.analyticsStore;
|
|
73
79
|
this.analyticsModel = new AnalyticsModel(new AnalyticsQueryEngine(this.analyticsStore));
|
|
74
80
|
args.graphqlManager.setAdditionalContextFields({ dataSources: { db: { Analytics: this.analyticsModel } } });
|
|
75
81
|
}
|
|
82
|
+
#assertCanReadAnalytics(ctx) {
|
|
83
|
+
if (this.authorizationService.isSupremeAdmin(ctx.user?.address)) return;
|
|
84
|
+
if (ctx.user?.address) throw new ForbiddenError("to read analytics");
|
|
85
|
+
throw new AuthenticationRequiredError("to read analytics");
|
|
86
|
+
}
|
|
76
87
|
};
|
|
77
88
|
//#endregion
|
|
78
89
|
//#region src/graphql/auth/schema.graphql
|
|
@@ -311,15 +322,15 @@ function createAuthFetchMiddleware(authService) {
|
|
|
311
322
|
async function createGatewayAdapter(type, logger) {
|
|
312
323
|
switch (type) {
|
|
313
324
|
case "apollo": {
|
|
314
|
-
const { ApolloGatewayAdapter } = await import("./adapter-gateway-apollo-
|
|
325
|
+
const { ApolloGatewayAdapter } = await import("./adapter-gateway-apollo-D2DNlPY0.mjs");
|
|
315
326
|
return new ApolloGatewayAdapter(logger);
|
|
316
327
|
}
|
|
317
328
|
case "mercurius": {
|
|
318
|
-
const { MercuriusGatewayAdapter } = await import("./adapter-gateway-mercurius-
|
|
329
|
+
const { MercuriusGatewayAdapter } = await import("./adapter-gateway-mercurius-rbKlwF9m.mjs");
|
|
319
330
|
return new MercuriusGatewayAdapter(logger);
|
|
320
331
|
}
|
|
321
332
|
case "stitching": {
|
|
322
|
-
const { StitchingGatewayAdapter } = await import("./adapter-gateway-stitching-
|
|
333
|
+
const { StitchingGatewayAdapter } = await import("./adapter-gateway-stitching-Dzv10u8D.mjs");
|
|
323
334
|
return new StitchingGatewayAdapter(logger);
|
|
324
335
|
}
|
|
325
336
|
}
|
|
@@ -327,16 +338,24 @@ async function createGatewayAdapter(type, logger) {
|
|
|
327
338
|
async function createHttpAdapter(type) {
|
|
328
339
|
switch (type) {
|
|
329
340
|
case "express": {
|
|
330
|
-
const { createExpressHttpAdapter } = await import("./adapter-http-express-
|
|
341
|
+
const { createExpressHttpAdapter } = await import("./adapter-http-express-CWqziB1p.mjs");
|
|
331
342
|
return createExpressHttpAdapter();
|
|
332
343
|
}
|
|
333
344
|
case "fastify": {
|
|
334
|
-
const { createFastifyHttpAdapter } = await import("./adapter-http-fastify-
|
|
345
|
+
const { createFastifyHttpAdapter } = await import("./adapter-http-fastify-CYZQ6FFv.mjs");
|
|
335
346
|
return createFastifyHttpAdapter();
|
|
336
347
|
}
|
|
337
348
|
}
|
|
338
349
|
}
|
|
339
350
|
//#endregion
|
|
351
|
+
//#region src/graphql/gateway/types.ts
|
|
352
|
+
/** No authenticated caller resolved, and REQUIRE_AUTHENTICATED_CALLER wants one. */
|
|
353
|
+
const WS_CLOSE_REASON_AUTHENTICATION_REQUIRED = "authentication-required";
|
|
354
|
+
/** A bearer was sent and could not be verified. */
|
|
355
|
+
const WS_CLOSE_REASON_BEARER_REJECTED = "bearer-rejected";
|
|
356
|
+
/** Every reason a refusal closes with, for a client matching on the set. */
|
|
357
|
+
const WS_AUTH_CLOSE_REASONS = [WS_CLOSE_REASON_AUTHENTICATION_REQUIRED, WS_CLOSE_REASON_BEARER_REJECTED];
|
|
358
|
+
//#endregion
|
|
340
359
|
//#region src/http/namespace.ts
|
|
341
360
|
/**
|
|
342
361
|
* A package's namespace is its npm name, used verbatim as one or two path
|
|
@@ -351,7 +370,7 @@ async function createHttpAdapter(type) {
|
|
|
351
370
|
*/
|
|
352
371
|
const SEGMENT = /^[a-z0-9][-a-z0-9._]*$/i;
|
|
353
372
|
/** Segments that would read as path traversal rather than as a name. */
|
|
354
|
-
const TRAVERSAL = new Set([".", ".."]);
|
|
373
|
+
const TRAVERSAL = /* @__PURE__ */ new Set([".", ".."]);
|
|
355
374
|
var InvalidNamespaceError = class extends Error {};
|
|
356
375
|
/**
|
|
357
376
|
* Turns a package name into the path segments it occupies.
|
|
@@ -837,27 +856,29 @@ var HttpScope = class {
|
|
|
837
856
|
headers: fetchHeaders(req),
|
|
838
857
|
signal: controller.signal
|
|
839
858
|
};
|
|
840
|
-
if (method !== "GET" && method !== "HEAD" && body !== "none")
|
|
841
|
-
|
|
842
|
-
body: Readable.toWeb(req),
|
|
843
|
-
duplex: "half"
|
|
844
|
-
};
|
|
845
|
-
else {
|
|
846
|
-
try {
|
|
847
|
-
raw = await readBody(req, spec.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES$1);
|
|
848
|
-
} catch (error) {
|
|
849
|
-
if (error instanceof BodyTooLargeError) {
|
|
850
|
-
res.setHeader("connection", "close");
|
|
851
|
-
res.once("finish", () => req.destroy());
|
|
852
|
-
writeError(res, 413, "Payload too large");
|
|
853
|
-
return;
|
|
854
|
-
}
|
|
855
|
-
throw error;
|
|
856
|
-
}
|
|
857
|
-
if (raw.length) init = {
|
|
859
|
+
if (method !== "GET" && method !== "HEAD" && body !== "none") {
|
|
860
|
+
if (body === "stream") init = {
|
|
858
861
|
...init,
|
|
859
|
-
body:
|
|
862
|
+
body: Readable.toWeb(req),
|
|
863
|
+
duplex: "half"
|
|
860
864
|
};
|
|
865
|
+
else {
|
|
866
|
+
try {
|
|
867
|
+
raw = await readBody(req, spec.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES$1);
|
|
868
|
+
} catch (error) {
|
|
869
|
+
if (error instanceof BodyTooLargeError) {
|
|
870
|
+
res.setHeader("connection", "close");
|
|
871
|
+
res.once("finish", () => req.destroy());
|
|
872
|
+
writeError(res, 413, "Payload too large");
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
throw error;
|
|
876
|
+
}
|
|
877
|
+
if (raw.length) init = {
|
|
878
|
+
...init,
|
|
879
|
+
body: new Uint8Array(raw.buffer, raw.byteOffset, raw.byteLength)
|
|
880
|
+
};
|
|
881
|
+
}
|
|
861
882
|
}
|
|
862
883
|
await writeResponse(res, await spec.handler(new Request(url, init), {
|
|
863
884
|
...ctx,
|
|
@@ -1034,7 +1055,7 @@ function verifyWebhook(options) {
|
|
|
1034
1055
|
}
|
|
1035
1056
|
/** Headers that would put a credential in a log or run journal. The configured signature
|
|
1036
1057
|
* header joins them: it is an HMAC of the body under a live secret. */
|
|
1037
|
-
const REDACTED_HEADERS = new Set([
|
|
1058
|
+
const REDACTED_HEADERS = /* @__PURE__ */ new Set([
|
|
1038
1059
|
"authorization",
|
|
1039
1060
|
"proxy-authorization",
|
|
1040
1061
|
"cookie",
|
|
@@ -1307,6 +1328,7 @@ function newWebhookToken() {
|
|
|
1307
1328
|
const NAMESPACE = "reactor_webhooks";
|
|
1308
1329
|
/** Relational implementation, over the reactor's own database. */
|
|
1309
1330
|
var RelationalWebhookStore = class {
|
|
1331
|
+
relationalDb;
|
|
1310
1332
|
#db;
|
|
1311
1333
|
constructor(relationalDb) {
|
|
1312
1334
|
this.relationalDb = relationalDb;
|
|
@@ -1321,7 +1343,11 @@ var RelationalWebhookStore = class {
|
|
|
1321
1343
|
"owner_key"
|
|
1322
1344
|
]).unique().execute();
|
|
1323
1345
|
await db.schema.createTable("webhook_deliveries").ifNotExists().addColumn("token", "text", (col) => col.notNull()).addColumn("dedupe_key", "text", (col) => col.notNull()).addColumn("seen_at", "text", (col) => col.notNull()).addColumn("claim_id", "text", (col) => col.notNull()).execute();
|
|
1346
|
+
try {
|
|
1347
|
+
await db.schema.alterTable("webhook_deliveries").addColumn("expires_at", "text").execute();
|
|
1348
|
+
} catch {}
|
|
1324
1349
|
await db.schema.createIndex("webhook_deliveries_key").ifNotExists().on("webhook_deliveries").columns(["token", "dedupe_key"]).unique().execute();
|
|
1350
|
+
await db.schema.createIndex("webhook_deliveries_expires_at").ifNotExists().on("webhook_deliveries").columns(["expires_at"]).execute();
|
|
1325
1351
|
await db.schema.createIndex("webhook_deliveries_seen_at").ifNotExists().on("webhook_deliveries").columns(["seen_at"]).execute();
|
|
1326
1352
|
this.#db = db;
|
|
1327
1353
|
}
|
|
@@ -1362,13 +1388,16 @@ var RelationalWebhookStore = class {
|
|
|
1362
1388
|
}
|
|
1363
1389
|
async seen(token, key, ttlSeconds) {
|
|
1364
1390
|
const now = Date.now();
|
|
1391
|
+
const nowIso = new Date(now).toISOString();
|
|
1365
1392
|
const cutoff = (/* @__PURE__ */ new Date(now - ttlSeconds * 1e3)).toISOString();
|
|
1366
|
-
await this.#handle.deleteFrom("webhook_deliveries").where("seen_at", "<", cutoff).execute();
|
|
1393
|
+
await this.#handle.deleteFrom("webhook_deliveries").where((eb) => eb.or([eb("expires_at", "<", nowIso), eb.and([eb("expires_at", "is", null), eb("seen_at", "<", cutoff)])])).execute();
|
|
1394
|
+
await this.#handle.deleteFrom("webhook_deliveries").where("token", "=", token).where("dedupe_key", "=", key).where("seen_at", "<", cutoff).execute();
|
|
1367
1395
|
const claimId = randomBytes(16).toString("hex");
|
|
1368
1396
|
await this.#handle.insertInto("webhook_deliveries").values({
|
|
1369
1397
|
token,
|
|
1370
1398
|
dedupe_key: key,
|
|
1371
|
-
seen_at:
|
|
1399
|
+
seen_at: nowIso,
|
|
1400
|
+
expires_at: new Date(now + ttlSeconds * 1e3).toISOString(),
|
|
1372
1401
|
claim_id: claimId
|
|
1373
1402
|
}).onConflict((oc) => oc.columns(["token", "dedupe_key"]).doNothing()).execute();
|
|
1374
1403
|
return (await this.#handle.selectFrom("webhook_deliveries").select("claim_id").where("token", "=", token).where("dedupe_key", "=", key).executeTakeFirst())?.claim_id !== claimId;
|
|
@@ -1414,10 +1443,14 @@ var MemoryWebhookStore = class {
|
|
|
1414
1443
|
}
|
|
1415
1444
|
seen(token, key, ttlSeconds) {
|
|
1416
1445
|
const now = Date.now();
|
|
1417
|
-
for (const [k,
|
|
1446
|
+
for (const [k, entry] of this.#deliveries) if (entry.expiresAt < now) this.#deliveries.delete(k);
|
|
1418
1447
|
const composite = `${token}|${key}`;
|
|
1419
|
-
|
|
1420
|
-
|
|
1448
|
+
const existing = this.#deliveries.get(composite);
|
|
1449
|
+
if (existing && existing.seenAt >= now - ttlSeconds * 1e3) return Promise.resolve(true);
|
|
1450
|
+
this.#deliveries.set(composite, {
|
|
1451
|
+
seenAt: now,
|
|
1452
|
+
expiresAt: now + ttlSeconds * 1e3
|
|
1453
|
+
});
|
|
1421
1454
|
return Promise.resolve(false);
|
|
1422
1455
|
}
|
|
1423
1456
|
};
|
|
@@ -1452,7 +1485,7 @@ const stripScalarDefinitions = (doc) => {
|
|
|
1452
1485
|
* whole gateway. We dedupe these by name; field/operation/extension nodes are
|
|
1453
1486
|
* left untouched.
|
|
1454
1487
|
*/
|
|
1455
|
-
const TYPE_DEFINITION_KINDS = new Set([
|
|
1488
|
+
const TYPE_DEFINITION_KINDS = /* @__PURE__ */ new Set([
|
|
1456
1489
|
Kind.OBJECT_TYPE_DEFINITION,
|
|
1457
1490
|
Kind.ENUM_TYPE_DEFINITION,
|
|
1458
1491
|
Kind.INPUT_OBJECT_TYPE_DEFINITION,
|
|
@@ -1488,6 +1521,98 @@ const dedupeTypeDefinitions = (doc) => {
|
|
|
1488
1521
|
definitions
|
|
1489
1522
|
};
|
|
1490
1523
|
};
|
|
1524
|
+
const EMPTY_DOCUMENT = {
|
|
1525
|
+
kind: Kind.DOCUMENT,
|
|
1526
|
+
definitions: []
|
|
1527
|
+
};
|
|
1528
|
+
const OUTPUT_TYPE_KINDS = [
|
|
1529
|
+
Kind.OBJECT_TYPE_DEFINITION,
|
|
1530
|
+
Kind.OBJECT_TYPE_EXTENSION,
|
|
1531
|
+
Kind.ENUM_TYPE_DEFINITION,
|
|
1532
|
+
Kind.ENUM_TYPE_EXTENSION,
|
|
1533
|
+
Kind.UNION_TYPE_DEFINITION,
|
|
1534
|
+
Kind.UNION_TYPE_EXTENSION,
|
|
1535
|
+
Kind.INTERFACE_TYPE_DEFINITION,
|
|
1536
|
+
Kind.INTERFACE_TYPE_EXTENSION
|
|
1537
|
+
];
|
|
1538
|
+
const STATE_PREFIX_KINDS = new Set(OUTPUT_TYPE_KINDS);
|
|
1539
|
+
const ALL_PREFIX_KINDS = /* @__PURE__ */ new Set([
|
|
1540
|
+
...OUTPUT_TYPE_KINDS,
|
|
1541
|
+
Kind.INPUT_OBJECT_TYPE_DEFINITION,
|
|
1542
|
+
Kind.INPUT_OBJECT_TYPE_EXTENSION
|
|
1543
|
+
]);
|
|
1544
|
+
const TYPE_NAME_KINDS = /* @__PURE__ */ new Set([
|
|
1545
|
+
...ALL_PREFIX_KINDS,
|
|
1546
|
+
Kind.SCALAR_TYPE_DEFINITION,
|
|
1547
|
+
Kind.SCALAR_TYPE_EXTENSION,
|
|
1548
|
+
Kind.NAMED_TYPE
|
|
1549
|
+
]);
|
|
1550
|
+
const LOCAL_STATE_STUBS = /* @__PURE__ */ new Set([
|
|
1551
|
+
"AccountSnapshotLocalState",
|
|
1552
|
+
"BudgetStatementLocalState",
|
|
1553
|
+
"ScopeFrameworkLocalState"
|
|
1554
|
+
]);
|
|
1555
|
+
/** Parse SDL; blank or comment-only input yields an empty document. */
|
|
1556
|
+
function parseSdl(sdl) {
|
|
1557
|
+
const source = new Source(sdl);
|
|
1558
|
+
if (new Lexer(source).advance().kind === TokenKind.EOF) return EMPTY_DOCUMENT;
|
|
1559
|
+
try {
|
|
1560
|
+
return parse(source, { noLocation: true });
|
|
1561
|
+
} catch (error) {
|
|
1562
|
+
const withoutSemicolons = sdl.replaceAll(";", "");
|
|
1563
|
+
if (withoutSemicolons === sdl) throw error;
|
|
1564
|
+
return parse(withoutSemicolons, { noLocation: true });
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
function definedTypeNames(doc, kinds) {
|
|
1568
|
+
return doc.definitions.filter((def) => kinds.has(def.kind)).map((def) => def.name.value);
|
|
1569
|
+
}
|
|
1570
|
+
/** Prefix type definition names and type references found in `names`. */
|
|
1571
|
+
function prefixTypeNames(doc, prefix, names) {
|
|
1572
|
+
return visit(doc, { enter(node) {
|
|
1573
|
+
if (!TYPE_NAME_KINDS.has(node.kind)) return void 0;
|
|
1574
|
+
const { name } = node;
|
|
1575
|
+
if (!names.has(name.value)) return void 0;
|
|
1576
|
+
return {
|
|
1577
|
+
...node,
|
|
1578
|
+
name: {
|
|
1579
|
+
...name,
|
|
1580
|
+
value: `${prefix}_${name.value}`
|
|
1581
|
+
}
|
|
1582
|
+
};
|
|
1583
|
+
} });
|
|
1584
|
+
}
|
|
1585
|
+
/** State schema definitions without inputs, `DateTime` or the given object types. */
|
|
1586
|
+
function stateTypeDefinitions(sdl, droppedObjectTypes = /* @__PURE__ */ new Set()) {
|
|
1587
|
+
return parseSdl(sdl).definitions.filter((def) => def.kind !== Kind.INPUT_OBJECT_TYPE_DEFINITION && def.kind !== Kind.INPUT_OBJECT_TYPE_EXTENSION && !(def.kind === Kind.SCALAR_TYPE_DEFINITION && def.name.value === "DateTime") && !(def.kind === Kind.OBJECT_TYPE_DEFINITION && droppedObjectTypes.has(def.name.value)));
|
|
1588
|
+
}
|
|
1589
|
+
/**
|
|
1590
|
+
* Prefixed operation schemas per module. The first definition of each module
|
|
1591
|
+
* carries a "Module: <Name>" description unless it has its own.
|
|
1592
|
+
*/
|
|
1593
|
+
function buildModuleSchemas(specification, documentName, allTypeNames, hasValidSchema) {
|
|
1594
|
+
const names = new Set(allTypeNames);
|
|
1595
|
+
return specification?.modules.map((module) => {
|
|
1596
|
+
const definitions = module.operations.filter((op) => hasValidSchema(op.schema)).flatMap((op) => {
|
|
1597
|
+
const doc = parseSdl(op.schema ?? "");
|
|
1598
|
+
return prefixTypeNames(doc, documentName, /* @__PURE__ */ new Set([...names, ...definedTypeNames(doc, ALL_PREFIX_KINDS)])).definitions;
|
|
1599
|
+
});
|
|
1600
|
+
if (definitions.length === 0) return "";
|
|
1601
|
+
const [first, ...rest] = definitions;
|
|
1602
|
+
const header = "description" in first && !first.description ? {
|
|
1603
|
+
...first,
|
|
1604
|
+
description: {
|
|
1605
|
+
kind: Kind.STRING,
|
|
1606
|
+
value: `Module: ${pascalCase(module.name)}`,
|
|
1607
|
+
block: true
|
|
1608
|
+
}
|
|
1609
|
+
} : first;
|
|
1610
|
+
return print({
|
|
1611
|
+
kind: Kind.DOCUMENT,
|
|
1612
|
+
definitions: [header, ...rest]
|
|
1613
|
+
});
|
|
1614
|
+
}).filter(Boolean).join("\n") ?? "";
|
|
1615
|
+
}
|
|
1491
1616
|
const buildSubgraphSchemaModule = (documentModels, resolvers, typeDefs) => {
|
|
1492
1617
|
const newResolvers = {
|
|
1493
1618
|
...resolvers,
|
|
@@ -1526,20 +1651,13 @@ const getDocumentModelTypeDefs = (documentModels, typeDefs$1) => {
|
|
|
1526
1651
|
const latestSpec = documentModel.global.specifications.at(-1);
|
|
1527
1652
|
const globalSchema = latestSpec?.state.global.schema ?? "";
|
|
1528
1653
|
const localSchema = latestSpec?.state.local.schema ?? "";
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
const typeRegex = new RegExp(`(?<![_A-Za-z0-9])(${f})(?![_A-Za-z0-9])|\\[(${f})\\]|\\[(${f})!\\]|\\[(${f})\\]!|\\[(${f})!\\]!`, "g");
|
|
1537
|
-
tmpDmSchema = tmpDmSchema.replace(typeRegex, (match, p1, p2, p3, p4, p5) => {
|
|
1538
|
-
if (match.startsWith("[")) return match.replace(p2 || p3 || p4 || p5, `${dmSchemaName}_${p2 || p3 || p4 || p5}`);
|
|
1539
|
-
return `${dmSchemaName}_${p1}`;
|
|
1540
|
-
});
|
|
1541
|
-
});
|
|
1542
|
-
dmSchema += tmpDmSchema;
|
|
1654
|
+
const definitions = [...stateTypeDefinitions(globalSchema), ...stateTypeDefinitions(localSchema, LOCAL_STATE_STUBS)];
|
|
1655
|
+
const stateDoc = {
|
|
1656
|
+
kind: Kind.DOCUMENT,
|
|
1657
|
+
definitions
|
|
1658
|
+
};
|
|
1659
|
+
const prefixed = prefixTypeNames(stateDoc, dmSchemaName, new Set(definedTypeNames(stateDoc, STATE_PREFIX_KINDS)));
|
|
1660
|
+
if (definitions.length > 0) dmSchema += `${print(prefixed)}\n`;
|
|
1543
1661
|
dmSchema += `
|
|
1544
1662
|
type ${dmSchemaName} implements IDocument {
|
|
1545
1663
|
id: String!
|
|
@@ -1554,7 +1672,7 @@ const getDocumentModelTypeDefs = (documentModels, typeDefs$1) => {
|
|
|
1554
1672
|
stateJSON: JSONObject
|
|
1555
1673
|
}\n`;
|
|
1556
1674
|
});
|
|
1557
|
-
|
|
1675
|
+
const schema = gql`
|
|
1558
1676
|
scalar JSONObject
|
|
1559
1677
|
scalar AttachmentRef
|
|
1560
1678
|
# Codegen scalars not in the document-engineering set.
|
|
@@ -1605,7 +1723,7 @@ const getDocumentModelTypeDefs = (documentModels, typeDefs$1) => {
|
|
|
1605
1723
|
operations(first: Int, skip: Int): [Operation!]!
|
|
1606
1724
|
stateJSON: JSONObject
|
|
1607
1725
|
}
|
|
1608
|
-
${dmSchema
|
|
1726
|
+
${dmSchema}
|
|
1609
1727
|
|
|
1610
1728
|
type GqlDocument implements IDocument {
|
|
1611
1729
|
id: String!
|
|
@@ -1630,34 +1748,32 @@ const getDocumentModelTypeDefs = (documentModels, typeDefs$1) => {
|
|
|
1630
1748
|
}
|
|
1631
1749
|
|
|
1632
1750
|
${stripScalarDefinitions(typeDefs$1)}
|
|
1633
|
-
|
|
1751
|
+
`;
|
|
1752
|
+
return dedupeTypeDefinitions(schema);
|
|
1634
1753
|
};
|
|
1635
1754
|
/**
|
|
1636
|
-
* Extract
|
|
1637
|
-
*
|
|
1638
|
-
* @returns {string[]} Array of type names
|
|
1755
|
+
* Extract the names of types defined in a GraphQL schema.
|
|
1756
|
+
* Unparseable schemas yield no names.
|
|
1639
1757
|
*/
|
|
1640
1758
|
function extractTypeNames(schema) {
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1759
|
+
try {
|
|
1760
|
+
return definedTypeNames(parseSdl(schema), ALL_PREFIX_KINDS);
|
|
1761
|
+
} catch (error) {
|
|
1762
|
+
logger.debug(`Skipping unparseable schema: ${String(error)}`);
|
|
1763
|
+
return [];
|
|
1764
|
+
}
|
|
1644
1765
|
}
|
|
1645
1766
|
/**
|
|
1646
|
-
*
|
|
1647
|
-
* @param
|
|
1648
|
-
* @param {Set<string>} excludeTypeNames - Type names to exclude from extraction
|
|
1649
|
-
* @returns {string} All input type definitions as a string
|
|
1767
|
+
* Print the input type definitions of a GraphQL schema.
|
|
1768
|
+
* @param excludeTypeNames - Input type names to leave out
|
|
1650
1769
|
*/
|
|
1651
1770
|
function extractInputTypeDefinitions(schema, excludeTypeNames = /* @__PURE__ */ new Set()) {
|
|
1652
|
-
const
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
}
|
|
1659
|
-
if (matches.length === 0) return "";
|
|
1660
|
-
return matches.join("\n\n");
|
|
1771
|
+
const definitions = parseSdl(schema).definitions.filter((def) => def.kind === Kind.INPUT_OBJECT_TYPE_DEFINITION && !excludeTypeNames.has(def.name.value));
|
|
1772
|
+
if (definitions.length === 0) return "";
|
|
1773
|
+
return print({
|
|
1774
|
+
kind: Kind.DOCUMENT,
|
|
1775
|
+
definitions
|
|
1776
|
+
});
|
|
1661
1777
|
}
|
|
1662
1778
|
/**
|
|
1663
1779
|
* Extract the root state type name from a scope's GraphQL schema.
|
|
@@ -1726,9 +1842,7 @@ function generateStateInputTypes(stateSchema, excludeTypeNames) {
|
|
|
1726
1842
|
case Kind.OBJECT_TYPE_DEFINITION:
|
|
1727
1843
|
objectNames.add(def.name.value);
|
|
1728
1844
|
break;
|
|
1729
|
-
case Kind.INPUT_OBJECT_TYPE_DEFINITION:
|
|
1730
|
-
existingInputNames.add(def.name.value);
|
|
1731
|
-
break;
|
|
1845
|
+
case Kind.INPUT_OBJECT_TYPE_DEFINITION: existingInputNames.add(def.name.value);
|
|
1732
1846
|
}
|
|
1733
1847
|
const inputTypes = [];
|
|
1734
1848
|
for (const def of ast.definitions) {
|
|
@@ -1755,18 +1869,10 @@ function generateStateInputTypes(stateSchema, excludeTypeNames) {
|
|
|
1755
1869
|
*/
|
|
1756
1870
|
function applyGraphQLTypePrefixes(schema, prefix, externalTypeNames = []) {
|
|
1757
1871
|
if (!schema || !schema.trim()) return schema;
|
|
1758
|
-
|
|
1759
|
-
const
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
allTypeNames.forEach((typeName) => {
|
|
1763
|
-
const typeRegex = new RegExp(`(?<![_A-Za-z0-9])(${typeName})(?![_A-Za-z0-9])|\\[(${typeName})\\]|\\[(${typeName})!\\]|\\[(${typeName})\\]!|\\[(${typeName})!\\]!`, "g");
|
|
1764
|
-
processedSchema = processedSchema.replace(typeRegex, (match, p1, p2, p3, p4, p5) => {
|
|
1765
|
-
if (match.startsWith("[")) return match.replace(p2 || p3 || p4 || p5, `${prefix}_${p2 || p3 || p4 || p5}`);
|
|
1766
|
-
return `${prefix}_${p1}`;
|
|
1767
|
-
});
|
|
1768
|
-
});
|
|
1769
|
-
return processedSchema;
|
|
1872
|
+
const doc = parseSdl(schema);
|
|
1873
|
+
const names = /* @__PURE__ */ new Set([...definedTypeNames(doc, ALL_PREFIX_KINDS), ...externalTypeNames]);
|
|
1874
|
+
if (names.size === 0) return schema;
|
|
1875
|
+
return print(prefixTypeNames(doc, prefix, names));
|
|
1770
1876
|
}
|
|
1771
1877
|
/**
|
|
1772
1878
|
* Generate a GraphQL schema for a document model.
|
|
@@ -1785,11 +1891,11 @@ function generateDocumentModelSchema(documentModel, options = {}) {
|
|
|
1785
1891
|
const localStateTypeNames = extractTypeNames(localStateSchema ?? "");
|
|
1786
1892
|
const stateTypeNames = [...globalStateTypeNames, ...localStateTypeNames];
|
|
1787
1893
|
const allOperationTypeNames = specification?.modules.flatMap((module) => module.operations.flatMap((op) => extractTypeNames(op.schema ?? ""))) ?? [];
|
|
1788
|
-
const allTypeNames = [
|
|
1789
|
-
const
|
|
1894
|
+
const allTypeNames = [.../* @__PURE__ */ new Set([...stateTypeNames, ...allOperationTypeNames])];
|
|
1895
|
+
const operationInputTypeNames = new Set(allOperationTypeNames);
|
|
1896
|
+
const prefixedStateInputTypes = applyGraphQLTypePrefixes(extractInputTypeDefinitions(globalStateSchema ?? "", operationInputTypeNames), documentName, allTypeNames);
|
|
1790
1897
|
const hasValidSchema = (schema) => !!(schema && /\b(input|type|enum|union|interface)\s+\w+/.test(schema));
|
|
1791
|
-
|
|
1792
|
-
if (useNewApi) return generateNewApiSchema(documentName, specification, stateSchemaTypes, prefixedStateInputTypes, allTypeNames, hasValidSchema);
|
|
1898
|
+
if (useNewApi) return generateNewApiSchema(documentName, specification, prefixedStateInputTypes, allTypeNames, hasValidSchema);
|
|
1793
1899
|
return generateLegacyApiSchema(documentName, specification, prefixedStateInputTypes, allTypeNames, hasValidSchema);
|
|
1794
1900
|
}
|
|
1795
1901
|
/**
|
|
@@ -1799,10 +1905,7 @@ function generateLegacyApiSchema(documentName, specification, prefixedStateInput
|
|
|
1799
1905
|
const createDocumentMutation = `${documentName}_createDocument(name:String!, driveId:String): String`;
|
|
1800
1906
|
const operationMutations = specification?.modules.flatMap((module) => module.operations.filter((op) => op.name && hasValidSchema(op.schema)).map((op) => `${documentName}_${camelCase(op.name)}(
|
|
1801
1907
|
driveId: String, docId: PHID, input: ${documentName}_${pascalCase(op.name)}Input): Int`)).join("\n ") ?? "";
|
|
1802
|
-
const moduleSchemas = specification
|
|
1803
|
-
Module: ${pascalCase(module.name)}
|
|
1804
|
-
"""
|
|
1805
|
-
${module.operations.filter((op) => hasValidSchema(op.schema)).map((op) => applyGraphQLTypePrefixes(op.schema ?? "", documentName, allTypeNames)).join("\n ")}`).join("\n") ?? "";
|
|
1908
|
+
const moduleSchemas = buildModuleSchemas(specification, documentName, allTypeNames, hasValidSchema);
|
|
1806
1909
|
return gql`
|
|
1807
1910
|
"""
|
|
1808
1911
|
Queries: ${documentName} Document
|
|
@@ -1839,7 +1942,7 @@ function generateLegacyApiSchema(documentName, specification, prefixedStateInput
|
|
|
1839
1942
|
* in getDocumentModelTypeDefs() which is used during schema composition.
|
|
1840
1943
|
* Including them here would cause duplicate type definitions.
|
|
1841
1944
|
*/
|
|
1842
|
-
function generateNewApiSchema(documentName, specification,
|
|
1945
|
+
function generateNewApiSchema(documentName, specification, prefixedStateInputTypes, allTypeNames, hasValidSchema) {
|
|
1843
1946
|
const stateType = `${documentName}_FullState!`;
|
|
1844
1947
|
const sharedBaseTypes = `
|
|
1845
1948
|
"""Hash configuration for document state"""
|
|
@@ -1916,7 +2019,6 @@ function generateNewApiSchema(documentName, specification, _stateSchemaTypes, pr
|
|
|
1916
2019
|
"""
|
|
1917
2020
|
type ${documentName}_DocumentResultPage {
|
|
1918
2021
|
items: [${documentName}MutationResult!]!
|
|
1919
|
-
totalCount: Int!
|
|
1920
2022
|
hasNextPage: Boolean!
|
|
1921
2023
|
hasPreviousPage: Boolean!
|
|
1922
2024
|
cursor: String
|
|
@@ -1971,10 +2073,7 @@ function generateNewApiSchema(documentName, specification, _stateSchemaTypes, pr
|
|
|
1971
2073
|
const createDocumentMutation = initialStateInputSchema ? `createDocument(name: String!, parentIdentifier: String, slug: String, preferredEditor: String, initialState: ${documentName}_InitialStateInput): ${documentName}MutationResult!` : `createDocument(name: String!, parentIdentifier: String, preferredEditor: String): ${documentName}MutationResult!`;
|
|
1972
2074
|
const createEmptyDocumentMutation = `createEmptyDocument(parentIdentifier: String): ${documentName}MutationResult!`;
|
|
1973
2075
|
const operationMutations = specification?.modules.flatMap((module) => module.operations.filter((op) => op.name && hasValidSchema(op.schema)).flatMap((op) => [`${camelCase(op.name)}(docId: PHID!, input: ${documentName}_${pascalCase(op.name)}Input!): ${documentName}MutationResult!`, `${camelCase(op.name)}Async(docId: PHID!, input: ${documentName}_${pascalCase(op.name)}Input!): String!`])).join("\n ") ?? "";
|
|
1974
|
-
const moduleSchemas = specification
|
|
1975
|
-
Module: ${pascalCase(module.name)}
|
|
1976
|
-
"""
|
|
1977
|
-
${module.operations.filter((op) => hasValidSchema(op.schema)).map((op) => applyGraphQLTypePrefixes(op.schema ?? "", documentName, allTypeNames)).join("\n ")}`).join("\n") ?? "";
|
|
2076
|
+
const moduleSchemas = buildModuleSchemas(specification, documentName, allTypeNames, hasValidSchema);
|
|
1978
2077
|
return gql`
|
|
1979
2078
|
scalar DateTime
|
|
1980
2079
|
scalar JSONObject
|
|
@@ -2222,6 +2321,16 @@ const PhDocumentFieldsFragmentDoc = gql`
|
|
|
2222
2321
|
lastModifiedAtUtcIso
|
|
2223
2322
|
}
|
|
2224
2323
|
`;
|
|
2324
|
+
const DocumentRelationshipFieldsFragmentDoc = gql`
|
|
2325
|
+
fragment DocumentRelationshipFields on DocumentRelationship {
|
|
2326
|
+
sourceId
|
|
2327
|
+
targetId
|
|
2328
|
+
relationshipType
|
|
2329
|
+
metadata
|
|
2330
|
+
createdAt
|
|
2331
|
+
updatedAt
|
|
2332
|
+
}
|
|
2333
|
+
`;
|
|
2225
2334
|
const GetDocumentModelsDocument = gql`
|
|
2226
2335
|
query GetDocumentModels($namespace: String, $paging: PagingInput) {
|
|
2227
2336
|
documentModels(namespace: $namespace, paging: $paging) {
|
|
@@ -2232,7 +2341,6 @@ const GetDocumentModelsDocument = gql`
|
|
|
2232
2341
|
version
|
|
2233
2342
|
specification
|
|
2234
2343
|
}
|
|
2235
|
-
totalCount
|
|
2236
2344
|
hasNextPage
|
|
2237
2345
|
hasPreviousPage
|
|
2238
2346
|
cursor
|
|
@@ -2291,7 +2399,6 @@ const GetDocumentWithOperationsDocument = gql`
|
|
|
2291
2399
|
}
|
|
2292
2400
|
}
|
|
2293
2401
|
}
|
|
2294
|
-
totalCount
|
|
2295
2402
|
hasNextPage
|
|
2296
2403
|
hasPreviousPage
|
|
2297
2404
|
cursor
|
|
@@ -2318,7 +2425,6 @@ const GetDocumentOutgoingRelationshipsDocument = gql`
|
|
|
2318
2425
|
items {
|
|
2319
2426
|
...PHDocumentFields
|
|
2320
2427
|
}
|
|
2321
|
-
totalCount
|
|
2322
2428
|
hasNextPage
|
|
2323
2429
|
hasPreviousPage
|
|
2324
2430
|
cursor
|
|
@@ -2342,7 +2448,6 @@ const GetDocumentIncomingRelationshipsDocument = gql`
|
|
|
2342
2448
|
items {
|
|
2343
2449
|
...PHDocumentFields
|
|
2344
2450
|
}
|
|
2345
|
-
totalCount
|
|
2346
2451
|
hasNextPage
|
|
2347
2452
|
hasPreviousPage
|
|
2348
2453
|
cursor
|
|
@@ -2350,6 +2455,52 @@ const GetDocumentIncomingRelationshipsDocument = gql`
|
|
|
2350
2455
|
}
|
|
2351
2456
|
${PhDocumentFieldsFragmentDoc}
|
|
2352
2457
|
`;
|
|
2458
|
+
const GetDocumentOutgoingRelationshipEdgesDocument = gql`
|
|
2459
|
+
query GetDocumentOutgoingRelationshipEdges(
|
|
2460
|
+
$sourceIdentifier: String!
|
|
2461
|
+
$relationshipType: String
|
|
2462
|
+
$view: ViewFilterInput
|
|
2463
|
+
$paging: PagingInput
|
|
2464
|
+
) {
|
|
2465
|
+
documentOutgoingRelationshipEdges(
|
|
2466
|
+
sourceIdentifier: $sourceIdentifier
|
|
2467
|
+
relationshipType: $relationshipType
|
|
2468
|
+
view: $view
|
|
2469
|
+
paging: $paging
|
|
2470
|
+
) {
|
|
2471
|
+
items {
|
|
2472
|
+
...DocumentRelationshipFields
|
|
2473
|
+
}
|
|
2474
|
+
hasNextPage
|
|
2475
|
+
hasPreviousPage
|
|
2476
|
+
cursor
|
|
2477
|
+
}
|
|
2478
|
+
}
|
|
2479
|
+
${DocumentRelationshipFieldsFragmentDoc}
|
|
2480
|
+
`;
|
|
2481
|
+
const GetDocumentIncomingRelationshipEdgesDocument = gql`
|
|
2482
|
+
query GetDocumentIncomingRelationshipEdges(
|
|
2483
|
+
$targetIdentifier: String!
|
|
2484
|
+
$relationshipType: String
|
|
2485
|
+
$view: ViewFilterInput
|
|
2486
|
+
$paging: PagingInput
|
|
2487
|
+
) {
|
|
2488
|
+
documentIncomingRelationshipEdges(
|
|
2489
|
+
targetIdentifier: $targetIdentifier
|
|
2490
|
+
relationshipType: $relationshipType
|
|
2491
|
+
view: $view
|
|
2492
|
+
paging: $paging
|
|
2493
|
+
) {
|
|
2494
|
+
items {
|
|
2495
|
+
...DocumentRelationshipFields
|
|
2496
|
+
}
|
|
2497
|
+
hasNextPage
|
|
2498
|
+
hasPreviousPage
|
|
2499
|
+
cursor
|
|
2500
|
+
}
|
|
2501
|
+
}
|
|
2502
|
+
${DocumentRelationshipFieldsFragmentDoc}
|
|
2503
|
+
`;
|
|
2353
2504
|
const FindDocumentsDocument = gql`
|
|
2354
2505
|
query FindDocuments(
|
|
2355
2506
|
$search: SearchFilterInput
|
|
@@ -2360,7 +2511,6 @@ const FindDocumentsDocument = gql`
|
|
|
2360
2511
|
items {
|
|
2361
2512
|
...PHDocumentFields
|
|
2362
2513
|
}
|
|
2363
|
-
totalCount
|
|
2364
2514
|
hasNextPage
|
|
2365
2515
|
hasPreviousPage
|
|
2366
2516
|
cursor
|
|
@@ -2404,7 +2554,6 @@ const GetDocumentOperationsDocument = gql`
|
|
|
2404
2554
|
}
|
|
2405
2555
|
}
|
|
2406
2556
|
}
|
|
2407
|
-
totalCount
|
|
2408
2557
|
hasNextPage
|
|
2409
2558
|
hasPreviousPage
|
|
2410
2559
|
cursor
|
|
@@ -2540,12 +2689,34 @@ const AddRelationshipDocument = gql`
|
|
|
2540
2689
|
$sourceIdentifier: String!
|
|
2541
2690
|
$targetIdentifier: String!
|
|
2542
2691
|
$relationshipType: String!
|
|
2692
|
+
$metadata: JSONObject
|
|
2543
2693
|
$branch: String
|
|
2544
2694
|
) {
|
|
2545
2695
|
addRelationship(
|
|
2546
2696
|
sourceIdentifier: $sourceIdentifier
|
|
2547
2697
|
targetIdentifier: $targetIdentifier
|
|
2548
2698
|
relationshipType: $relationshipType
|
|
2699
|
+
metadata: $metadata
|
|
2700
|
+
branch: $branch
|
|
2701
|
+
) {
|
|
2702
|
+
...PHDocumentFields
|
|
2703
|
+
}
|
|
2704
|
+
}
|
|
2705
|
+
${PhDocumentFieldsFragmentDoc}
|
|
2706
|
+
`;
|
|
2707
|
+
const UpdateRelationshipDocument = gql`
|
|
2708
|
+
mutation UpdateRelationship(
|
|
2709
|
+
$sourceIdentifier: String!
|
|
2710
|
+
$targetIdentifier: String!
|
|
2711
|
+
$relationshipType: String!
|
|
2712
|
+
$metadata: JSONObject
|
|
2713
|
+
$branch: String
|
|
2714
|
+
) {
|
|
2715
|
+
updateRelationship(
|
|
2716
|
+
sourceIdentifier: $sourceIdentifier
|
|
2717
|
+
targetIdentifier: $targetIdentifier
|
|
2718
|
+
relationshipType: $relationshipType
|
|
2719
|
+
metadata: $metadata
|
|
2549
2720
|
branch: $branch
|
|
2550
2721
|
) {
|
|
2551
2722
|
...PHDocumentFields
|
|
@@ -2739,6 +2910,12 @@ function getSdk(requester) {
|
|
|
2739
2910
|
GetDocumentIncomingRelationships(variables, options) {
|
|
2740
2911
|
return requester(GetDocumentIncomingRelationshipsDocument, variables, options);
|
|
2741
2912
|
},
|
|
2913
|
+
GetDocumentOutgoingRelationshipEdges(variables, options) {
|
|
2914
|
+
return requester(GetDocumentOutgoingRelationshipEdgesDocument, variables, options);
|
|
2915
|
+
},
|
|
2916
|
+
GetDocumentIncomingRelationshipEdges(variables, options) {
|
|
2917
|
+
return requester(GetDocumentIncomingRelationshipEdgesDocument, variables, options);
|
|
2918
|
+
},
|
|
2742
2919
|
FindDocuments(variables, options) {
|
|
2743
2920
|
return requester(FindDocumentsDocument, variables, options);
|
|
2744
2921
|
},
|
|
@@ -2772,6 +2949,9 @@ function getSdk(requester) {
|
|
|
2772
2949
|
AddRelationship(variables, options) {
|
|
2773
2950
|
return requester(AddRelationshipDocument, variables, options);
|
|
2774
2951
|
},
|
|
2952
|
+
UpdateRelationship(variables, options) {
|
|
2953
|
+
return requester(UpdateRelationshipDocument, variables, options);
|
|
2954
|
+
},
|
|
2775
2955
|
RemoveRelationship(variables, options) {
|
|
2776
2956
|
return requester(RemoveRelationshipDocument, variables, options);
|
|
2777
2957
|
},
|
|
@@ -2812,8 +2992,7 @@ function toDocumentModelResultPage(result) {
|
|
|
2812
2992
|
cursor: result.nextCursor ?? null,
|
|
2813
2993
|
hasNextPage: !!result.nextCursor,
|
|
2814
2994
|
hasPreviousPage: !!result.options.cursor,
|
|
2815
|
-
items: models.map(toGqlDocumentModelState)
|
|
2816
|
-
totalCount: result.results.length
|
|
2995
|
+
items: models.map(toGqlDocumentModelState)
|
|
2817
2996
|
};
|
|
2818
2997
|
}
|
|
2819
2998
|
/**
|
|
@@ -2845,8 +3024,32 @@ function toPhDocumentResultPage(result) {
|
|
|
2845
3024
|
cursor: result.nextCursor ?? null,
|
|
2846
3025
|
hasNextPage: !!result.nextCursor,
|
|
2847
3026
|
hasPreviousPage: !!result.options.cursor,
|
|
2848
|
-
items: result.results.map(toGqlPhDocument)
|
|
2849
|
-
|
|
3027
|
+
items: result.results.map(toGqlPhDocument)
|
|
3028
|
+
};
|
|
3029
|
+
}
|
|
3030
|
+
/**
|
|
3031
|
+
* Converts a relationship edge from the indexer to GraphQL format.
|
|
3032
|
+
*/
|
|
3033
|
+
function toGqlDocumentRelationship(relationship) {
|
|
3034
|
+
return {
|
|
3035
|
+
sourceId: relationship.sourceId,
|
|
3036
|
+
targetId: relationship.targetId,
|
|
3037
|
+
relationshipType: relationship.relationshipType,
|
|
3038
|
+
metadata: relationship.metadata ?? null,
|
|
3039
|
+
createdAt: relationship.createdAt,
|
|
3040
|
+
updatedAt: relationship.updatedAt
|
|
3041
|
+
};
|
|
3042
|
+
}
|
|
3043
|
+
/**
|
|
3044
|
+
* Converts a PagedResults of relationship edges to GraphQL
|
|
3045
|
+
* DocumentRelationshipResultPage format
|
|
3046
|
+
*/
|
|
3047
|
+
function toDocumentRelationshipResultPage(result) {
|
|
3048
|
+
return {
|
|
3049
|
+
cursor: result.nextCursor ?? null,
|
|
3050
|
+
hasNextPage: !!result.nextCursor,
|
|
3051
|
+
hasPreviousPage: !!result.options.cursor,
|
|
3052
|
+
items: result.results.map(toGqlDocumentRelationship)
|
|
2850
3053
|
};
|
|
2851
3054
|
}
|
|
2852
3055
|
/**
|
|
@@ -2896,8 +3099,8 @@ function fromInputMaybe(value) {
|
|
|
2896
3099
|
function toReactorPropagationMode(gqlMode) {
|
|
2897
3100
|
if (gqlMode == null) return;
|
|
2898
3101
|
switch (gqlMode) {
|
|
2899
|
-
case
|
|
2900
|
-
case
|
|
3102
|
+
case "CASCADE": return PropagationMode$1.Cascade;
|
|
3103
|
+
case "ORPHAN": return PropagationMode$1.None;
|
|
2901
3104
|
}
|
|
2902
3105
|
}
|
|
2903
3106
|
/**
|
|
@@ -2907,10 +3110,10 @@ function toReactorPropagationMode(gqlMode) {
|
|
|
2907
3110
|
*/
|
|
2908
3111
|
function toGqlActionEvaluation(evaluation) {
|
|
2909
3112
|
return evaluation.decision === "allow" ? {
|
|
2910
|
-
decision:
|
|
3113
|
+
decision: "ALLOW",
|
|
2911
3114
|
reason: null
|
|
2912
3115
|
} : {
|
|
2913
|
-
decision:
|
|
3116
|
+
decision: "DENY",
|
|
2914
3117
|
reason: evaluation.reason
|
|
2915
3118
|
};
|
|
2916
3119
|
}
|
|
@@ -3115,8 +3318,7 @@ function toOperationResultPage(result) {
|
|
|
3115
3318
|
cursor: result.nextCursor ?? null,
|
|
3116
3319
|
hasNextPage: !!result.nextCursor,
|
|
3117
3320
|
hasPreviousPage: !!result.options.cursor && result.options.cursor !== "0",
|
|
3118
|
-
items: result.results.map(serializeOperationForGraphQL)
|
|
3119
|
-
totalCount: result.results.length
|
|
3321
|
+
items: result.results.map(serializeOperationForGraphQL)
|
|
3120
3322
|
};
|
|
3121
3323
|
}
|
|
3122
3324
|
function toGqlDocumentChangeEvent(event) {
|
|
@@ -3188,6 +3390,21 @@ function pickDriveClient(reactorClient, driveContainerType, reactorDriveClient)
|
|
|
3188
3390
|
}
|
|
3189
3391
|
return reactorClient.drives;
|
|
3190
3392
|
}
|
|
3393
|
+
/**
|
|
3394
|
+
* A mutation's document re-read as the caller, since the client reads it back
|
|
3395
|
+
* as its signer. A caller who may write but not read gets it stripped.
|
|
3396
|
+
*/
|
|
3397
|
+
async function readAsCaller(reactorClient, document, subject, branch) {
|
|
3398
|
+
if (!subject) return document;
|
|
3399
|
+
try {
|
|
3400
|
+
return await reactorClient.get(document.header.id, {
|
|
3401
|
+
subject,
|
|
3402
|
+
branch
|
|
3403
|
+
});
|
|
3404
|
+
} catch (error) {
|
|
3405
|
+
throw new GraphQLError(`Failed to read the document back: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
3406
|
+
}
|
|
3407
|
+
}
|
|
3191
3408
|
async function documentModels(reactorClient, args) {
|
|
3192
3409
|
const namespace = fromInputMaybe(args.namespace);
|
|
3193
3410
|
let paging;
|
|
@@ -3295,6 +3512,54 @@ async function documentIncomingRelationships(reactorClient, args, subject) {
|
|
|
3295
3512
|
throw new GraphQLError(`Failed to convert incoming relationships to GraphQL: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
3296
3513
|
}
|
|
3297
3514
|
}
|
|
3515
|
+
async function documentOutgoingRelationshipEdges(reactorClient, args, subject) {
|
|
3516
|
+
const view = toRelationshipViewFilter(args.view, subject);
|
|
3517
|
+
const paging = toRelationshipPagingOptions(args.paging);
|
|
3518
|
+
let result;
|
|
3519
|
+
try {
|
|
3520
|
+
result = await reactorClient.getOutgoingRelationshipEdges(args.sourceIdentifier, fromInputMaybe(args.relationshipType), view, paging);
|
|
3521
|
+
} catch (error) {
|
|
3522
|
+
throw new GraphQLError(`Failed to fetch outgoing relationship edges: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
3523
|
+
}
|
|
3524
|
+
try {
|
|
3525
|
+
return toDocumentRelationshipResultPage(result);
|
|
3526
|
+
} catch (error) {
|
|
3527
|
+
throw new GraphQLError(`Failed to convert outgoing relationship edges to GraphQL: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
3528
|
+
}
|
|
3529
|
+
}
|
|
3530
|
+
async function documentIncomingRelationshipEdges(reactorClient, args, subject) {
|
|
3531
|
+
const view = toRelationshipViewFilter(args.view, subject);
|
|
3532
|
+
const paging = toRelationshipPagingOptions(args.paging);
|
|
3533
|
+
let result;
|
|
3534
|
+
try {
|
|
3535
|
+
result = await reactorClient.getIncomingRelationshipEdges(args.targetIdentifier, fromInputMaybe(args.relationshipType), view, paging);
|
|
3536
|
+
} catch (error) {
|
|
3537
|
+
throw new GraphQLError(`Failed to fetch incoming relationship edges: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
3538
|
+
}
|
|
3539
|
+
try {
|
|
3540
|
+
return toDocumentRelationshipResultPage(result);
|
|
3541
|
+
} catch (error) {
|
|
3542
|
+
throw new GraphQLError(`Failed to convert incoming relationship edges to GraphQL: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
3543
|
+
}
|
|
3544
|
+
}
|
|
3545
|
+
function toRelationshipViewFilter(input, subject) {
|
|
3546
|
+
if (!input) return subject ? { subject } : void 0;
|
|
3547
|
+
return {
|
|
3548
|
+
subject,
|
|
3549
|
+
branch: fromInputMaybe(input.branch),
|
|
3550
|
+
scopes: toMutableArray(fromInputMaybe(input.scopes))
|
|
3551
|
+
};
|
|
3552
|
+
}
|
|
3553
|
+
function toRelationshipPagingOptions(input) {
|
|
3554
|
+
if (!input) return;
|
|
3555
|
+
const cursor = fromInputMaybe(input.cursor);
|
|
3556
|
+
const limit = fromInputMaybe(input.limit);
|
|
3557
|
+
if (!cursor && !limit) return;
|
|
3558
|
+
return {
|
|
3559
|
+
cursor: cursor || "",
|
|
3560
|
+
limit: limit || 10
|
|
3561
|
+
};
|
|
3562
|
+
}
|
|
3298
3563
|
async function findDocuments(reactorClient, args, subject) {
|
|
3299
3564
|
let view = subject ? { subject } : void 0;
|
|
3300
3565
|
if (args.view) view = {
|
|
@@ -3327,13 +3592,42 @@ async function findDocuments(reactorClient, args, subject) {
|
|
|
3327
3592
|
throw new GraphQLError(`Failed to convert documents to GraphQL: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
3328
3593
|
}
|
|
3329
3594
|
}
|
|
3330
|
-
|
|
3595
|
+
/**
|
|
3596
|
+
* The answer for a job the caller may not see, shaped as the reactor answers
|
|
3597
|
+
* an unknown one so that the two cannot be told apart.
|
|
3598
|
+
*/
|
|
3599
|
+
function unknownJob(jobId) {
|
|
3600
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3601
|
+
return {
|
|
3602
|
+
id: jobId,
|
|
3603
|
+
documentId: "",
|
|
3604
|
+
status: JobStatus.FAILED,
|
|
3605
|
+
createdAtUtcIso: now,
|
|
3606
|
+
completedAtUtcIso: now,
|
|
3607
|
+
error: {
|
|
3608
|
+
name: "Error",
|
|
3609
|
+
message: "Job not found",
|
|
3610
|
+
stack: ""
|
|
3611
|
+
},
|
|
3612
|
+
consistencyToken: {
|
|
3613
|
+
version: 1,
|
|
3614
|
+
createdAtUtcIso: now,
|
|
3615
|
+
coordinates: []
|
|
3616
|
+
},
|
|
3617
|
+
meta: {
|
|
3618
|
+
batchId: jobId,
|
|
3619
|
+
batchJobIds: [jobId]
|
|
3620
|
+
}
|
|
3621
|
+
};
|
|
3622
|
+
}
|
|
3623
|
+
async function jobStatus(reactorClient, args, serves) {
|
|
3331
3624
|
let result;
|
|
3332
3625
|
try {
|
|
3333
3626
|
result = await reactorClient.getJobStatus(args.jobId);
|
|
3334
3627
|
} catch (error) {
|
|
3335
3628
|
throw new GraphQLError(`Failed to fetch job status: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
3336
3629
|
}
|
|
3630
|
+
if (!await serves(result.documentId)) result = unknownJob(args.jobId);
|
|
3337
3631
|
try {
|
|
3338
3632
|
return toGqlJobInfo(result);
|
|
3339
3633
|
} catch (error) {
|
|
@@ -3416,7 +3710,7 @@ async function evaluateActions(reactorClient, args, subject) {
|
|
|
3416
3710
|
anyDenied: result.anyDenied
|
|
3417
3711
|
};
|
|
3418
3712
|
}
|
|
3419
|
-
async function createDocument(reactorClient, args, reactorDriveClient) {
|
|
3713
|
+
async function createDocument(reactorClient, args, reactorDriveClient, subject) {
|
|
3420
3714
|
if (!args.document || typeof args.document !== "object") throw new GraphQLError("Invalid document: must be an object");
|
|
3421
3715
|
const document = args.document;
|
|
3422
3716
|
if (!document.header || typeof document.header !== "object") throw new GraphQLError("Invalid document: missing or invalid header");
|
|
@@ -3424,28 +3718,30 @@ async function createDocument(reactorClient, args, reactorDriveClient) {
|
|
|
3424
3718
|
let result;
|
|
3425
3719
|
try {
|
|
3426
3720
|
if (parentIdentifier) {
|
|
3427
|
-
const parent = await reactorClient.get(parentIdentifier);
|
|
3721
|
+
const parent = await reactorClient.get(parentIdentifier, subject && { subject });
|
|
3428
3722
|
if (isDriveContainerType(parent.header.documentType)) result = await pickDriveClient(reactorClient, parent.header.documentType, reactorDriveClient).addFile(parentIdentifier, document);
|
|
3429
3723
|
else result = await reactorClient.create(document, parentIdentifier);
|
|
3430
3724
|
} else result = await reactorClient.create(document);
|
|
3431
3725
|
} catch (error) {
|
|
3432
3726
|
throw new GraphQLError(`Failed to create document: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
3433
3727
|
}
|
|
3728
|
+
result = await readAsCaller(reactorClient, result, subject);
|
|
3434
3729
|
try {
|
|
3435
3730
|
return toGqlPhDocument(result);
|
|
3436
3731
|
} catch (error) {
|
|
3437
3732
|
throw new GraphQLError(`Failed to convert created document to GraphQL: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
3438
3733
|
}
|
|
3439
3734
|
}
|
|
3440
|
-
async function createEmptyDocument(reactorClient, args, reactorDriveClient) {
|
|
3735
|
+
async function createEmptyDocument(reactorClient, args, reactorDriveClient, subject) {
|
|
3441
3736
|
const parentIdentifier = fromInputMaybe(args.parentIdentifier);
|
|
3442
3737
|
const name = fromInputMaybe(args.name);
|
|
3443
3738
|
let result;
|
|
3444
3739
|
try {
|
|
3445
3740
|
if (parentIdentifier) {
|
|
3446
|
-
const parent = await reactorClient.get(parentIdentifier);
|
|
3741
|
+
const parent = await reactorClient.get(parentIdentifier, subject && { subject });
|
|
3447
3742
|
if (isDriveContainerType(parent.header.documentType)) {
|
|
3448
|
-
const
|
|
3743
|
+
const module = await reactorClient.getDocumentModelModule(args.documentType);
|
|
3744
|
+
const document = withSignaturePolicy(module.utils.createDocument(), await reactorClient.getCreateSignaturePolicy());
|
|
3449
3745
|
if (name) document.header.name = name;
|
|
3450
3746
|
result = await pickDriveClient(reactorClient, parent.header.documentType, reactorDriveClient).addFile(parentIdentifier, document);
|
|
3451
3747
|
} else result = await reactorClient.createEmpty(args.documentType, { parentIdentifier });
|
|
@@ -3453,13 +3749,14 @@ async function createEmptyDocument(reactorClient, args, reactorDriveClient) {
|
|
|
3453
3749
|
} catch (error) {
|
|
3454
3750
|
throw new GraphQLError(`Failed to create empty document: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
3455
3751
|
}
|
|
3752
|
+
result = await readAsCaller(reactorClient, result, subject);
|
|
3456
3753
|
try {
|
|
3457
3754
|
return toGqlPhDocument(result);
|
|
3458
3755
|
} catch (error) {
|
|
3459
3756
|
throw new GraphQLError(`Failed to convert created document to GraphQL: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
3460
3757
|
}
|
|
3461
3758
|
}
|
|
3462
|
-
async function createDocumentWithInitialState(reactorClient, args, reactorDriveClient) {
|
|
3759
|
+
async function createDocumentWithInitialState(reactorClient, args, reactorDriveClient, subject) {
|
|
3463
3760
|
const parentIdentifier = fromInputMaybe(args.parentIdentifier);
|
|
3464
3761
|
const name = fromInputMaybe(args.name);
|
|
3465
3762
|
const slug = fromInputMaybe(args.slug);
|
|
@@ -3470,7 +3767,7 @@ async function createDocumentWithInitialState(reactorClient, args, reactorDriveC
|
|
|
3470
3767
|
} catch (error) {
|
|
3471
3768
|
throw new GraphQLError(`Document model not found for type ${args.documentType}: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
3472
3769
|
}
|
|
3473
|
-
const document = module.utils.createDocument();
|
|
3770
|
+
const document = withSignaturePolicy(module.utils.createDocument(), await reactorClient.getCreateSignaturePolicy());
|
|
3474
3771
|
const allowedScopes = new Set(Object.keys(module.documentModel.global.specifications.at(-1)?.state ?? {}));
|
|
3475
3772
|
const state = document.state;
|
|
3476
3773
|
for (const [scope, scopeState] of Object.entries(args.initialState)) if (allowedScopes.has(scope) && scope in state) state[scope] = {
|
|
@@ -3487,7 +3784,7 @@ async function createDocumentWithInitialState(reactorClient, args, reactorDriveC
|
|
|
3487
3784
|
if (parentIdentifier) {
|
|
3488
3785
|
let parent;
|
|
3489
3786
|
try {
|
|
3490
|
-
parent = await reactorClient.get(parentIdentifier);
|
|
3787
|
+
parent = await reactorClient.get(parentIdentifier, subject && { subject });
|
|
3491
3788
|
} catch (error) {
|
|
3492
3789
|
throw new GraphQLError(`Parent document not found: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
3493
3790
|
}
|
|
@@ -3508,6 +3805,7 @@ async function createDocumentWithInitialState(reactorClient, args, reactorDriveC
|
|
|
3508
3805
|
} catch (error) {
|
|
3509
3806
|
throw new GraphQLError(`Failed to create document: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
3510
3807
|
}
|
|
3808
|
+
result = await readAsCaller(reactorClient, result, subject);
|
|
3511
3809
|
try {
|
|
3512
3810
|
return toGqlPhDocument(result);
|
|
3513
3811
|
} catch (error) {
|
|
@@ -3524,12 +3822,12 @@ const DEFAULT_BRANCH = "main";
|
|
|
3524
3822
|
* the schema cannot express: a signature travels as one string, and verification
|
|
3525
3823
|
* reads it as the tuple it was.
|
|
3526
3824
|
*/
|
|
3527
|
-
async function execute(reactorClient, args) {
|
|
3825
|
+
async function execute(reactorClient, args, subject) {
|
|
3528
3826
|
const actions = toSubmittableActions(args.actions);
|
|
3529
3827
|
const branch = fromInputMaybe(args.branch) ?? DEFAULT_BRANCH;
|
|
3530
3828
|
let result;
|
|
3531
3829
|
try {
|
|
3532
|
-
result = await reactorClient.execute(args.documentIdentifier, branch, actions);
|
|
3830
|
+
result = await reactorClient.execute(args.documentIdentifier, branch, actions, void 0, subject);
|
|
3533
3831
|
} catch (error) {
|
|
3534
3832
|
throw new GraphQLError(`Failed to execute actions: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
3535
3833
|
}
|
|
@@ -3557,7 +3855,7 @@ async function executeAsync(reactorClient, args) {
|
|
|
3557
3855
|
}
|
|
3558
3856
|
return toGqlJobInfo(job);
|
|
3559
3857
|
}
|
|
3560
|
-
async function mutateDocument(reactorClient, args) {
|
|
3858
|
+
async function mutateDocument(reactorClient, args, subject) {
|
|
3561
3859
|
let validatedActions;
|
|
3562
3860
|
try {
|
|
3563
3861
|
validatedActions = validateActions(args.actions);
|
|
@@ -3568,7 +3866,7 @@ async function mutateDocument(reactorClient, args) {
|
|
|
3568
3866
|
const branch = args.view?.branch ?? "main";
|
|
3569
3867
|
let result;
|
|
3570
3868
|
try {
|
|
3571
|
-
result = await reactorClient.execute(args.documentIdentifier, branch, validatedActions);
|
|
3869
|
+
result = await reactorClient.execute(args.documentIdentifier, branch, validatedActions, void 0, subject);
|
|
3572
3870
|
} catch (error) {
|
|
3573
3871
|
throw new GraphQLError(`Failed to mutate document: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
3574
3872
|
}
|
|
@@ -3595,7 +3893,7 @@ async function mutateDocumentAsync(reactorClient, args) {
|
|
|
3595
3893
|
}
|
|
3596
3894
|
return result.id;
|
|
3597
3895
|
}
|
|
3598
|
-
async function renameDocument(reactorClient, args, signal) {
|
|
3896
|
+
async function renameDocument(reactorClient, args, signal, subject) {
|
|
3599
3897
|
const branch = fromInputMaybe(args.branch);
|
|
3600
3898
|
let result;
|
|
3601
3899
|
try {
|
|
@@ -3603,13 +3901,14 @@ async function renameDocument(reactorClient, args, signal) {
|
|
|
3603
3901
|
} catch (error) {
|
|
3604
3902
|
throw new GraphQLError(`Failed to rename document: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
3605
3903
|
}
|
|
3904
|
+
result = await readAsCaller(reactorClient, result, subject, branch);
|
|
3606
3905
|
try {
|
|
3607
3906
|
return toGqlPhDocument(result);
|
|
3608
3907
|
} catch (error) {
|
|
3609
3908
|
throw new GraphQLError(`Failed to convert renamed document to GraphQL: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
3610
3909
|
}
|
|
3611
3910
|
}
|
|
3612
|
-
async function setPreferredEditor(reactorClient, args, signal) {
|
|
3911
|
+
async function setPreferredEditor(reactorClient, args, signal, subject) {
|
|
3613
3912
|
const branch = fromInputMaybe(args.branch);
|
|
3614
3913
|
const preferredEditor = fromInputMaybe(args.preferredEditor) ?? null;
|
|
3615
3914
|
let result;
|
|
@@ -3618,27 +3917,46 @@ async function setPreferredEditor(reactorClient, args, signal) {
|
|
|
3618
3917
|
} catch (error) {
|
|
3619
3918
|
throw new GraphQLError(`Failed to set preferred editor: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
3620
3919
|
}
|
|
3920
|
+
result = await readAsCaller(reactorClient, result, subject, branch);
|
|
3621
3921
|
try {
|
|
3622
3922
|
return toGqlPhDocument(result);
|
|
3623
3923
|
} catch (error) {
|
|
3624
3924
|
throw new GraphQLError(`Failed to convert updated document to GraphQL: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
3625
3925
|
}
|
|
3626
3926
|
}
|
|
3627
|
-
async function addRelationship(reactorClient, args) {
|
|
3927
|
+
async function addRelationship(reactorClient, args, subject) {
|
|
3628
3928
|
const branch = fromInputMaybe(args.branch);
|
|
3929
|
+
const metadata = fromInputMaybe(args.metadata);
|
|
3629
3930
|
let result;
|
|
3630
3931
|
try {
|
|
3631
|
-
result = await reactorClient.addRelationship(args.sourceIdentifier, args.targetIdentifier, args.relationshipType, branch);
|
|
3932
|
+
result = await reactorClient.addRelationship(args.sourceIdentifier, args.targetIdentifier, args.relationshipType, metadata, branch);
|
|
3632
3933
|
} catch (error) {
|
|
3633
3934
|
throw new GraphQLError(`Failed to add relationship: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
3634
3935
|
}
|
|
3936
|
+
result = await readAsCaller(reactorClient, result, subject, branch);
|
|
3937
|
+
try {
|
|
3938
|
+
return toGqlPhDocument(result);
|
|
3939
|
+
} catch (error) {
|
|
3940
|
+
throw new GraphQLError(`Failed to convert document to GraphQL: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
3941
|
+
}
|
|
3942
|
+
}
|
|
3943
|
+
async function updateRelationship(reactorClient, args, subject) {
|
|
3944
|
+
const branch = fromInputMaybe(args.branch);
|
|
3945
|
+
const metadata = fromInputMaybe(args.metadata) ?? null;
|
|
3946
|
+
let result;
|
|
3947
|
+
try {
|
|
3948
|
+
result = await reactorClient.updateRelationship(args.sourceIdentifier, args.targetIdentifier, args.relationshipType, metadata, branch);
|
|
3949
|
+
} catch (error) {
|
|
3950
|
+
throw new GraphQLError(`Failed to update relationship: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
3951
|
+
}
|
|
3952
|
+
result = await readAsCaller(reactorClient, result, subject, branch);
|
|
3635
3953
|
try {
|
|
3636
3954
|
return toGqlPhDocument(result);
|
|
3637
3955
|
} catch (error) {
|
|
3638
3956
|
throw new GraphQLError(`Failed to convert document to GraphQL: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
3639
3957
|
}
|
|
3640
3958
|
}
|
|
3641
|
-
async function removeRelationship(reactorClient, args) {
|
|
3959
|
+
async function removeRelationship(reactorClient, args, subject) {
|
|
3642
3960
|
const branch = fromInputMaybe(args.branch);
|
|
3643
3961
|
let result;
|
|
3644
3962
|
try {
|
|
@@ -3646,13 +3964,14 @@ async function removeRelationship(reactorClient, args) {
|
|
|
3646
3964
|
} catch (error) {
|
|
3647
3965
|
throw new GraphQLError(`Failed to remove relationship: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
3648
3966
|
}
|
|
3967
|
+
result = await readAsCaller(reactorClient, result, subject, branch);
|
|
3649
3968
|
try {
|
|
3650
3969
|
return toGqlPhDocument(result);
|
|
3651
3970
|
} catch (error) {
|
|
3652
3971
|
throw new GraphQLError(`Failed to convert document to GraphQL: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
3653
3972
|
}
|
|
3654
3973
|
}
|
|
3655
|
-
async function moveRelationship(reactorClient, args) {
|
|
3974
|
+
async function moveRelationship(reactorClient, args, subject) {
|
|
3656
3975
|
const branch = fromInputMaybe(args.branch);
|
|
3657
3976
|
let result;
|
|
3658
3977
|
try {
|
|
@@ -3660,6 +3979,10 @@ async function moveRelationship(reactorClient, args) {
|
|
|
3660
3979
|
} catch (error) {
|
|
3661
3980
|
throw new GraphQLError(`Failed to move relationship: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
3662
3981
|
}
|
|
3982
|
+
result = {
|
|
3983
|
+
source: await readAsCaller(reactorClient, result.source, subject, branch),
|
|
3984
|
+
target: await readAsCaller(reactorClient, result.target, subject, branch)
|
|
3985
|
+
};
|
|
3663
3986
|
try {
|
|
3664
3987
|
return {
|
|
3665
3988
|
source: toGqlPhDocument(result.source),
|
|
@@ -3778,14 +4101,16 @@ async function collectHeldSyncOperations(syncOps, gate, subject, signal) {
|
|
|
3778
4101
|
* client ordinal the switchboard has successfully applied, so the client
|
|
3779
4102
|
* can trim its own outbox)
|
|
3780
4103
|
*/
|
|
3781
|
-
function pollSyncEnvelopes(syncManager, args, forbiddenIds = /* @__PURE__ */ new Set(), heldOpIds = /* @__PURE__ */ new Set()) {
|
|
4104
|
+
function pollSyncEnvelopes(syncManager, args, forbiddenIds = /* @__PURE__ */ new Set(), heldOpIds = /* @__PURE__ */ new Set(), gatedOpIds) {
|
|
4105
|
+
const isHeld = (syncOp) => heldOpIds.has(syncOp.id) || gatedOpIds !== void 0 && !gatedOpIds.has(syncOp.id);
|
|
3782
4106
|
let remote;
|
|
3783
4107
|
try {
|
|
3784
4108
|
remote = syncManager.getById(args.channelId);
|
|
3785
4109
|
} catch (error) {
|
|
3786
4110
|
throw new GraphQLError(`Channel not found: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
3787
4111
|
}
|
|
3788
|
-
|
|
4112
|
+
remote.channel.notePoll();
|
|
4113
|
+
const deadLetters = remote.channel.deadLetter.items.filter((syncOp) => !forbiddenIds.has(syncOp.documentId) && !isHeld(syncOp)).map((syncOp) => ({
|
|
3789
4114
|
documentId: syncOp.documentId,
|
|
3790
4115
|
error: syncOp.error?.message ?? "Unknown error",
|
|
3791
4116
|
errorType: syncOperationErrorType(syncOp.error),
|
|
@@ -3834,7 +4159,7 @@ function pollSyncEnvelopes(syncManager, args, forbiddenIds = /* @__PURE__ */ new
|
|
|
3834
4159
|
hasMore = true;
|
|
3835
4160
|
break;
|
|
3836
4161
|
}
|
|
3837
|
-
if (
|
|
4162
|
+
if (isHeld(syncOp)) continue;
|
|
3838
4163
|
syncOp.deliveredCount ??= 0;
|
|
3839
4164
|
syncOp.emittedCount ??= 0;
|
|
3840
4165
|
while (syncOp.deliveredCount < syncOp.emittedCount && syncOp.operations[syncOp.deliveredCount].context.ordinal <= args.outboxLatest) syncOp.deliveredCount += 1;
|
|
@@ -3909,6 +4234,7 @@ function pushSyncEnvelopes(syncManager, args) {
|
|
|
3909
4234
|
throw new GraphQLError(`Channel not found: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
3910
4235
|
}
|
|
3911
4236
|
if (!envelope.operations || envelope.operations.length === 0) continue;
|
|
4237
|
+
remote.channel.notePoll();
|
|
3912
4238
|
const syncOps = envelopesToSyncOperations(envelope, remote.meta.name);
|
|
3913
4239
|
if (!remoteSyncOps.has(remote)) remoteSyncOps.set(remote, []);
|
|
3914
4240
|
remoteSyncOps.get(remote).push(...syncOps);
|
|
@@ -3946,6 +4272,16 @@ var DocumentModelSubgraph = class extends BaseSubgraph {
|
|
|
3946
4272
|
const documentName = getDocumentModelSchemaName(this.documentModel.documentModel.global);
|
|
3947
4273
|
return this.resolvers[`${documentName}Mutations`];
|
|
3948
4274
|
}
|
|
4275
|
+
/** Drops the items the host's own ACL refuses the caller. */
|
|
4276
|
+
async #readableItems(page, ctx) {
|
|
4277
|
+
if (this.authorizationService.isSupremeAdmin(ctx.user?.address)) return page;
|
|
4278
|
+
const items = [];
|
|
4279
|
+
for (const item of page.items) if (await this.canReadDocument(item.id, ctx)) items.push(item);
|
|
4280
|
+
return {
|
|
4281
|
+
...page,
|
|
4282
|
+
items
|
|
4283
|
+
};
|
|
4284
|
+
}
|
|
3949
4285
|
/**
|
|
3950
4286
|
* Generate __resolveType functions for union types found in the document model schema.
|
|
3951
4287
|
* Parses the state schema to find union definitions and their member types,
|
|
@@ -4006,7 +4342,7 @@ var DocumentModelSubgraph = class extends BaseSubgraph {
|
|
|
4006
4342
|
const result = await document(this.reactorClient, {
|
|
4007
4343
|
identifier,
|
|
4008
4344
|
view
|
|
4009
|
-
});
|
|
4345
|
+
}, this.viewSubject(ctx));
|
|
4010
4346
|
if (result.document.documentType !== documentType) throw new GraphQLError(`Document with id ${identifier} is not of type ${documentType}`);
|
|
4011
4347
|
await this.assertCanReadCanonical(result.document.id, ctx);
|
|
4012
4348
|
return result;
|
|
@@ -4016,17 +4352,8 @@ var DocumentModelSubgraph = class extends BaseSubgraph {
|
|
|
4016
4352
|
const result = await findDocuments(this.reactorClient, {
|
|
4017
4353
|
search: { type: documentType },
|
|
4018
4354
|
paging
|
|
4019
|
-
});
|
|
4020
|
-
|
|
4021
|
-
const filteredItems = [];
|
|
4022
|
-
for (const item of result.items) if (await this.canReadDocument(item.id, ctx)) filteredItems.push(item);
|
|
4023
|
-
return {
|
|
4024
|
-
...result,
|
|
4025
|
-
items: filteredItems,
|
|
4026
|
-
totalCount: filteredItems.length
|
|
4027
|
-
};
|
|
4028
|
-
}
|
|
4029
|
-
return result;
|
|
4355
|
+
}, this.viewSubject(ctx));
|
|
4356
|
+
return this.#readableItems(result, ctx);
|
|
4030
4357
|
},
|
|
4031
4358
|
findDocuments: async (_, args, ctx) => {
|
|
4032
4359
|
const { search, view, paging } = args;
|
|
@@ -4037,17 +4364,8 @@ var DocumentModelSubgraph = class extends BaseSubgraph {
|
|
|
4037
4364
|
},
|
|
4038
4365
|
view,
|
|
4039
4366
|
paging
|
|
4040
|
-
});
|
|
4041
|
-
|
|
4042
|
-
const filteredItems = [];
|
|
4043
|
-
for (const item of result.items) if (await this.canReadDocument(item.id, ctx)) filteredItems.push(item);
|
|
4044
|
-
return {
|
|
4045
|
-
...result,
|
|
4046
|
-
items: filteredItems,
|
|
4047
|
-
totalCount: filteredItems.length
|
|
4048
|
-
};
|
|
4049
|
-
}
|
|
4050
|
-
return result;
|
|
4367
|
+
}, this.viewSubject(ctx));
|
|
4368
|
+
return this.#readableItems(result, ctx);
|
|
4051
4369
|
},
|
|
4052
4370
|
documentOutgoingRelationships: async (_, args, ctx) => {
|
|
4053
4371
|
const { relationshipType, view, paging } = args;
|
|
@@ -4057,23 +4375,24 @@ var DocumentModelSubgraph = class extends BaseSubgraph {
|
|
|
4057
4375
|
relationshipType,
|
|
4058
4376
|
view,
|
|
4059
4377
|
paging
|
|
4060
|
-
});
|
|
4061
|
-
const
|
|
4378
|
+
}, this.viewSubject(ctx));
|
|
4379
|
+
const readable = await this.#readableItems(result, ctx);
|
|
4380
|
+
const filteredItems = readable.items.filter((item) => item.documentType === documentType);
|
|
4062
4381
|
return {
|
|
4063
|
-
...
|
|
4064
|
-
items: filteredItems
|
|
4065
|
-
totalCount: filteredItems.length
|
|
4382
|
+
...readable,
|
|
4383
|
+
items: filteredItems
|
|
4066
4384
|
};
|
|
4067
4385
|
},
|
|
4068
4386
|
documentIncomingRelationships: async (_, args, ctx) => {
|
|
4069
4387
|
const { relationshipType, view, paging } = args;
|
|
4070
4388
|
const handle = await this.assertCanRead(args.targetIdentifier, ctx);
|
|
4071
|
-
|
|
4389
|
+
const result = await documentIncomingRelationships(this.reactorClient, {
|
|
4072
4390
|
targetIdentifier: handle.fetchIdentifier,
|
|
4073
4391
|
relationshipType,
|
|
4074
4392
|
view,
|
|
4075
4393
|
paging
|
|
4076
|
-
});
|
|
4394
|
+
}, this.viewSubject(ctx));
|
|
4395
|
+
return this.#readableItems(result, ctx);
|
|
4077
4396
|
}
|
|
4078
4397
|
},
|
|
4079
4398
|
Mutation: { [documentName]: () => ({}) },
|
|
@@ -4091,14 +4410,14 @@ var DocumentModelSubgraph = class extends BaseSubgraph {
|
|
|
4091
4410
|
slug,
|
|
4092
4411
|
preferredEditor,
|
|
4093
4412
|
initialState: initialState ?? {}
|
|
4094
|
-
}, this.graphqlManager.reactorDriveClient);
|
|
4413
|
+
}, this.graphqlManager.reactorDriveClient, this.viewSubject(ctx));
|
|
4095
4414
|
else createdDoc = await createEmptyDocument(this.reactorClient, {
|
|
4096
4415
|
documentType,
|
|
4097
4416
|
parentIdentifier,
|
|
4098
4417
|
name
|
|
4099
|
-
}, this.graphqlManager.reactorDriveClient);
|
|
4418
|
+
}, this.graphqlManager.reactorDriveClient, this.viewSubject(ctx));
|
|
4100
4419
|
if (ctx.user?.address && createdDoc?.id) await this.documentPermissionService?.initializeDocumentProtection(createdDoc.id, ctx.user.address, this.authorizationService.config.defaultProtection);
|
|
4101
|
-
if (!initialState && !preferredEditor && name && createdDoc.name !== name) return toGqlPhDocument(await this.reactorClient.execute(createdDoc.id, "main", [setName(name)]));
|
|
4420
|
+
if (!initialState && !preferredEditor && name && createdDoc.name !== name) return toGqlPhDocument(await this.reactorClient.execute(createdDoc.id, "main", [setName(name)], void 0, this.viewSubject(ctx)));
|
|
4102
4421
|
return createdDoc;
|
|
4103
4422
|
},
|
|
4104
4423
|
createEmptyDocument: async (_, args, ctx) => {
|
|
@@ -4108,7 +4427,7 @@ var DocumentModelSubgraph = class extends BaseSubgraph {
|
|
|
4108
4427
|
const result = await createEmptyDocument(this.reactorClient, {
|
|
4109
4428
|
documentType,
|
|
4110
4429
|
parentIdentifier
|
|
4111
|
-
}, this.graphqlManager.reactorDriveClient);
|
|
4430
|
+
}, this.graphqlManager.reactorDriveClient, this.viewSubject(ctx));
|
|
4112
4431
|
if (ctx.user?.address && result?.id) await this.documentPermissionService?.initializeDocumentProtection(result.id, ctx.user.address, this.authorizationService.config.defaultProtection);
|
|
4113
4432
|
return result;
|
|
4114
4433
|
},
|
|
@@ -4116,11 +4435,11 @@ var DocumentModelSubgraph = class extends BaseSubgraph {
|
|
|
4116
4435
|
mutations[camelCase(op.name)] = async (_, args, ctx) => {
|
|
4117
4436
|
const { docId, input } = args;
|
|
4118
4437
|
const effectiveDocId = (await this.assertCanExecuteOperation(docId, op.name, ctx)).fetchIdentifier;
|
|
4119
|
-
if ((await this.reactorClient.get(effectiveDocId)).header.documentType !== documentType) throw new GraphQLError(`Document with id ${docId} is not of type ${documentType}`);
|
|
4438
|
+
if ((await this.reactorClient.get(effectiveDocId, { subject: this.viewSubject(ctx) })).header.documentType !== documentType) throw new GraphQLError(`Document with id ${docId} is not of type ${documentType}`);
|
|
4120
4439
|
const action = this.documentModel.actions[camelCase(op.name)];
|
|
4121
4440
|
if (!action) throw new GraphQLError(`Action ${op.name} not found`);
|
|
4122
4441
|
try {
|
|
4123
|
-
return toGqlPhDocument(await this.reactorClient.execute(effectiveDocId, "main", [action(input)]));
|
|
4442
|
+
return toGqlPhDocument(await this.reactorClient.execute(effectiveDocId, "main", [action(input)], void 0, this.viewSubject(ctx)));
|
|
4124
4443
|
} catch (error) {
|
|
4125
4444
|
throw new GraphQLError(error instanceof Error ? error.message : `Failed to ${op.name}`);
|
|
4126
4445
|
}
|
|
@@ -4128,7 +4447,7 @@ var DocumentModelSubgraph = class extends BaseSubgraph {
|
|
|
4128
4447
|
mutations[`${camelCase(op.name)}Async`] = async (_, args, ctx) => {
|
|
4129
4448
|
const { docId, input } = args;
|
|
4130
4449
|
const effectiveDocId = (await this.assertCanExecuteOperation(docId, op.name, ctx)).fetchIdentifier;
|
|
4131
|
-
if ((await this.reactorClient.get(effectiveDocId)).header.documentType !== documentType) throw new GraphQLError(`Document with id ${docId} is not of type ${documentType}`);
|
|
4450
|
+
if ((await this.reactorClient.get(effectiveDocId, { subject: this.viewSubject(ctx) })).header.documentType !== documentType) throw new GraphQLError(`Document with id ${docId} is not of type ${documentType}`);
|
|
4132
4451
|
const action = this.documentModel.actions[camelCase(op.name)];
|
|
4133
4452
|
if (!action) throw new GraphQLError(`Action ${op.name} not found`);
|
|
4134
4453
|
try {
|
|
@@ -4152,7 +4471,7 @@ const DRIVE_ID_HEADER = "drive-id";
|
|
|
4152
4471
|
* in the ownership set yet); other create-shaped operations that may
|
|
4153
4472
|
* synthesize a drive must be added here too.
|
|
4154
4473
|
*/
|
|
4155
|
-
const CACHE_BYPASS_OPERATIONS = new Set(["createDocument", "createEmptyDocument"]);
|
|
4474
|
+
const CACHE_BYPASS_OPERATIONS = /* @__PURE__ */ new Set(["createDocument", "createEmptyDocument"]);
|
|
4156
4475
|
const driveIdMap = /* @__PURE__ */ new WeakMap();
|
|
4157
4476
|
/** Internal — only `graphql-manager.ts` should call this. */
|
|
4158
4477
|
function getRequestDriveId(request) {
|
|
@@ -4222,6 +4541,7 @@ function wrongShardResponse(driveId) {
|
|
|
4222
4541
|
* 421 response.
|
|
4223
4542
|
*/
|
|
4224
4543
|
var DriveOwnershipCache = class {
|
|
4544
|
+
reactorClient;
|
|
4225
4545
|
drives = /* @__PURE__ */ new Set();
|
|
4226
4546
|
constructor(reactorClient) {
|
|
4227
4547
|
this.reactorClient = reactorClient;
|
|
@@ -4312,7 +4632,49 @@ const DefaultFeatureFlags = { enableDocumentModelSubgraphs: true };
|
|
|
4312
4632
|
* regenerateDocumentModelSubgraphs).
|
|
4313
4633
|
*/
|
|
4314
4634
|
const DOCUMENT_MODEL_SUBGRAPH_SOURCE = "document-models";
|
|
4635
|
+
function refuseConnection(connection, reason) {
|
|
4636
|
+
const { socket } = connection;
|
|
4637
|
+
socket?.close?.(4403, reason);
|
|
4638
|
+
return false;
|
|
4639
|
+
}
|
|
4640
|
+
function createWsAuthHandlers(opts) {
|
|
4641
|
+
const resolved = /* @__PURE__ */ new WeakMap();
|
|
4642
|
+
return {
|
|
4643
|
+
onConnect: async (connectionParams, connection) => {
|
|
4644
|
+
let user = null;
|
|
4645
|
+
if (opts.authService) try {
|
|
4646
|
+
user = await opts.authService.authenticateWebSocketConnection(connectionParams);
|
|
4647
|
+
} catch (error) {
|
|
4648
|
+
opts.logger.warn("Refusing WebSocket connection: @error", error instanceof Error ? error.message : error);
|
|
4649
|
+
return refuseConnection(connection, WS_CLOSE_REASON_BEARER_REJECTED);
|
|
4650
|
+
}
|
|
4651
|
+
if (user === null && opts.requireAuthenticatedCaller()) {
|
|
4652
|
+
opts.logger.warn("Refusing anonymous WebSocket connection: an authenticated caller is required");
|
|
4653
|
+
return refuseConnection(connection, WS_CLOSE_REASON_AUTHENTICATION_REQUIRED);
|
|
4654
|
+
}
|
|
4655
|
+
resolved.set(connection, user ?? void 0);
|
|
4656
|
+
return true;
|
|
4657
|
+
},
|
|
4658
|
+
context: (connectionParams, connection) => Promise.resolve(opts.buildContext(connectionParams, resolved.get(connection)))
|
|
4659
|
+
};
|
|
4660
|
+
}
|
|
4315
4661
|
var GraphQLManager = class {
|
|
4662
|
+
path;
|
|
4663
|
+
httpServer;
|
|
4664
|
+
wsServer;
|
|
4665
|
+
reactorClient;
|
|
4666
|
+
relationalDb;
|
|
4667
|
+
analyticsStore;
|
|
4668
|
+
syncManager;
|
|
4669
|
+
logger;
|
|
4670
|
+
httpAdapter;
|
|
4671
|
+
gatewayAdapter;
|
|
4672
|
+
authService;
|
|
4673
|
+
documentPermissionService;
|
|
4674
|
+
featureFlags;
|
|
4675
|
+
port;
|
|
4676
|
+
syncServingGate;
|
|
4677
|
+
httpRoutes;
|
|
4316
4678
|
initialized = false;
|
|
4317
4679
|
coreSubgraphsMap = /* @__PURE__ */ new Map();
|
|
4318
4680
|
contextFields = {};
|
|
@@ -4327,8 +4689,8 @@ var GraphQLManager = class {
|
|
|
4327
4689
|
subgraphHandlerCache = /* @__PURE__ */ new Map();
|
|
4328
4690
|
/** subgraphPath → the http adapter handle of its mounted route. */
|
|
4329
4691
|
subgraphRouteHandles = /* @__PURE__ */ new Map();
|
|
4330
|
-
/**
|
|
4331
|
-
|
|
4692
|
+
/** Mounted SSE routes, keyed by path: the supergraph's and each subgraph's. */
|
|
4693
|
+
sseRouteHandles = /* @__PURE__ */ new Map();
|
|
4332
4694
|
/**
|
|
4333
4695
|
* Package name → the subgraph instances registered from it, keyed by
|
|
4334
4696
|
* subgraph name. Tracks the package association so that removed packages
|
|
@@ -4384,27 +4746,34 @@ var GraphQLManager = class {
|
|
|
4384
4746
|
this.httpAdapter.setupMiddleware({ bodyLimit: "50mb" });
|
|
4385
4747
|
const driveRoutePath = path.posix.join(this.path, "d/:drive");
|
|
4386
4748
|
const driveMatcher = match(driveRoutePath);
|
|
4387
|
-
|
|
4749
|
+
const driveInfoHandler = async (request) => {
|
|
4388
4750
|
const url = new URL(request.url);
|
|
4389
4751
|
const matched = driveMatcher(url.pathname);
|
|
4390
4752
|
const driveIdOrSlug = matched ? matched.params.drive : void 0;
|
|
4391
4753
|
if (!driveIdOrSlug) return Response.json({ error: "Drive ID or slug is required" }, { status: 400 });
|
|
4754
|
+
const user = getAuthContext(request)?.user;
|
|
4392
4755
|
try {
|
|
4393
|
-
const driveDoc = await this.reactorClient.get(driveIdOrSlug);
|
|
4756
|
+
const driveDoc = await this.reactorClient.get(driveIdOrSlug, { subject: callerSubject(user) });
|
|
4757
|
+
if (!await this.authorizationService.canRead(driveDoc.header.id, user?.address)) {
|
|
4758
|
+
this.logger.debug(`Drive read refused: ${driveIdOrSlug}`);
|
|
4759
|
+
return Response.json({ error: "Drive not found" }, { status: 404 });
|
|
4760
|
+
}
|
|
4394
4761
|
const graphqlEndpoint = `${(request.headers.get("x-forwarded-proto")?.split(",")[0].trim() ?? url.protocol.replace(":", "")) + ":"}//${request.headers.get("x-forwarded-host")?.split(",")[0].trim() ?? request.headers.get("host") ?? ""}${(request.headers.get("x-forwarded-prefix")?.split(",")[0].trim().replace(/\/$/, "") ?? "") + (this.path === "/" ? "" : this.path)}/graphql/r`;
|
|
4762
|
+
const global = driveDoc.state.global;
|
|
4395
4763
|
return Response.json({
|
|
4396
4764
|
id: driveDoc.header.id,
|
|
4397
4765
|
slug: driveDoc.header.slug,
|
|
4398
4766
|
meta: driveDoc.header.meta,
|
|
4399
|
-
name:
|
|
4400
|
-
icon:
|
|
4767
|
+
name: global?.name || driveDoc.header.name,
|
|
4768
|
+
icon: global?.icon ?? void 0,
|
|
4401
4769
|
...graphqlEndpoint && { graphqlEndpoint }
|
|
4402
4770
|
});
|
|
4403
4771
|
} catch (error) {
|
|
4404
4772
|
this.logger.debug(`Drive not found: ${driveIdOrSlug}`, error);
|
|
4405
4773
|
return Response.json({ error: "Drive not found" }, { status: 404 });
|
|
4406
4774
|
}
|
|
4407
|
-
}
|
|
4775
|
+
};
|
|
4776
|
+
this.httpAdapter.mount(driveRoutePath, this.#authMiddleware ? this.#authMiddleware(driveInfoHandler) : driveInfoHandler);
|
|
4408
4777
|
this.logger.info(`Registered REST endpoint: GET ${driveRoutePath}`);
|
|
4409
4778
|
await this.#setupCoreSubgraphs("graphql", coreSubgraphs);
|
|
4410
4779
|
if (this.featureFlags.enableDocumentModelSubgraphs) await this.#setupDocumentModelSubgraphs("graphql", models);
|
|
@@ -4521,6 +4890,7 @@ var GraphQLManager = class {
|
|
|
4521
4890
|
routeHandle.dispose();
|
|
4522
4891
|
this.subgraphRouteHandles.delete(subgraphPath);
|
|
4523
4892
|
}
|
|
4893
|
+
this.#disposeSSERoute(subgraphPath);
|
|
4524
4894
|
const wsDisposer = this.subgraphWsDisposers.get(subgraphPath);
|
|
4525
4895
|
if (wsDisposer) {
|
|
4526
4896
|
this.subgraphWsDisposers.delete(subgraphPath);
|
|
@@ -4695,17 +5065,6 @@ var GraphQLManager = class {
|
|
|
4695
5065
|
...fields
|
|
4696
5066
|
};
|
|
4697
5067
|
}
|
|
4698
|
-
async #createWebSocketContext(connectionParams) {
|
|
4699
|
-
let user = null;
|
|
4700
|
-
if (this.authService) user = await this.authService.authenticateWebSocketConnection(connectionParams);
|
|
4701
|
-
const context = {
|
|
4702
|
-
headers: connectionParams,
|
|
4703
|
-
db: this.relationalDb,
|
|
4704
|
-
...this.getAdditionalContextFields()
|
|
4705
|
-
};
|
|
4706
|
-
if (user) context.user = user;
|
|
4707
|
-
return context;
|
|
4708
|
-
}
|
|
4709
5068
|
#makeContextFactory() {
|
|
4710
5069
|
return (request) => {
|
|
4711
5070
|
const authCtx = getAuthContext(request);
|
|
@@ -4724,7 +5083,20 @@ var GraphQLManager = class {
|
|
|
4724
5083
|
};
|
|
4725
5084
|
}
|
|
4726
5085
|
#makeWsContextFactory() {
|
|
4727
|
-
return (
|
|
5086
|
+
return createWsAuthHandlers({
|
|
5087
|
+
authService: this.authService,
|
|
5088
|
+
requireAuthenticatedCaller: () => this.#requireAuthMiddleware !== void 0,
|
|
5089
|
+
logger: this.logger,
|
|
5090
|
+
buildContext: (connectionParams, user) => {
|
|
5091
|
+
const context = {
|
|
5092
|
+
headers: connectionParams,
|
|
5093
|
+
db: this.relationalDb,
|
|
5094
|
+
...this.getAdditionalContextFields()
|
|
5095
|
+
};
|
|
5096
|
+
if (user) context.user = user;
|
|
5097
|
+
return context;
|
|
5098
|
+
}
|
|
5099
|
+
});
|
|
4728
5100
|
}
|
|
4729
5101
|
setSupergraph(supergraph, subgraphs) {
|
|
4730
5102
|
const accepted = [];
|
|
@@ -4858,10 +5230,7 @@ var GraphQLManager = class {
|
|
|
4858
5230
|
const allSubgraphs = this.#getAllSubgraphs();
|
|
4859
5231
|
const modules = Array.from(allSubgraphs.values()).filter((subgraph) => subgraph.hasSubscriptions).map((subgraph) => this.#buildSubgraphSchemaModule(subgraph));
|
|
4860
5232
|
if (modules.length === 0) {
|
|
4861
|
-
|
|
4862
|
-
this.sseRouteHandle.dispose();
|
|
4863
|
-
this.sseRouteHandle = void 0;
|
|
4864
|
-
}
|
|
5233
|
+
this.#disposeSSERoute(superGraphPath);
|
|
4865
5234
|
return;
|
|
4866
5235
|
}
|
|
4867
5236
|
try {
|
|
@@ -4887,8 +5256,16 @@ var GraphQLManager = class {
|
|
|
4887
5256
|
contextFactory: this.#makeContextFactory()
|
|
4888
5257
|
});
|
|
4889
5258
|
const handler = this.#composeFetchMiddleware(rawHandler);
|
|
4890
|
-
|
|
4891
|
-
this.
|
|
5259
|
+
this.#disposeSSERoute(basePath);
|
|
5260
|
+
this.sseRouteHandles.set(ssePath, this.httpAdapter.mount(ssePath, handler, { exact: true }));
|
|
5261
|
+
}
|
|
5262
|
+
#disposeSSERoute(basePath) {
|
|
5263
|
+
const ssePath = basePath + "/stream";
|
|
5264
|
+
const handle = this.sseRouteHandles.get(ssePath);
|
|
5265
|
+
if (handle !== void 0) {
|
|
5266
|
+
handle.dispose();
|
|
5267
|
+
this.sseRouteHandles.delete(ssePath);
|
|
5268
|
+
}
|
|
4892
5269
|
}
|
|
4893
5270
|
/**
|
|
4894
5271
|
* Compose the request-level fetch middleware chain. Auth runs first
|
|
@@ -5172,7 +5549,6 @@ const DocumentModelStateDTO = z.object({
|
|
|
5172
5549
|
}).strip();
|
|
5173
5550
|
const DocumentModelResultPageDTO = z.object({
|
|
5174
5551
|
items: z.array(DocumentModelStateDTO),
|
|
5175
|
-
totalCount: z.number().int(),
|
|
5176
5552
|
hasNextPage: z.boolean(),
|
|
5177
5553
|
hasPreviousPage: z.boolean(),
|
|
5178
5554
|
cursor: z.string().nullable().optional()
|
|
@@ -5193,7 +5569,20 @@ const PHDocumentDTO = z.object({
|
|
|
5193
5569
|
}).strip();
|
|
5194
5570
|
const PHDocumentResultPageDTO = z.object({
|
|
5195
5571
|
items: z.array(PHDocumentDTO),
|
|
5196
|
-
|
|
5572
|
+
hasNextPage: z.boolean(),
|
|
5573
|
+
hasPreviousPage: z.boolean(),
|
|
5574
|
+
cursor: z.string().nullable().optional()
|
|
5575
|
+
}).strip();
|
|
5576
|
+
const DocumentRelationshipDTO = z.object({
|
|
5577
|
+
sourceId: z.string(),
|
|
5578
|
+
targetId: z.string(),
|
|
5579
|
+
relationshipType: z.string(),
|
|
5580
|
+
metadata: JSONObjectDTO.nullable().optional(),
|
|
5581
|
+
createdAt: DateTimeDTO,
|
|
5582
|
+
updatedAt: DateTimeDTO
|
|
5583
|
+
}).strip();
|
|
5584
|
+
const DocumentRelationshipResultPageDTO = z.object({
|
|
5585
|
+
items: z.array(DocumentRelationshipDTO),
|
|
5197
5586
|
hasNextPage: z.boolean(),
|
|
5198
5587
|
hasPreviousPage: z.boolean(),
|
|
5199
5588
|
cursor: z.string().nullable().optional()
|
|
@@ -5248,7 +5637,6 @@ const OperationDTO = z.object({
|
|
|
5248
5637
|
}).strip();
|
|
5249
5638
|
const OperationResultPageDTO = z.object({
|
|
5250
5639
|
items: z.array(OperationDTO),
|
|
5251
|
-
totalCount: z.number().int(),
|
|
5252
5640
|
hasNextPage: z.boolean(),
|
|
5253
5641
|
hasPreviousPage: z.boolean(),
|
|
5254
5642
|
cursor: z.string().nullable().optional()
|
|
@@ -5301,6 +5689,12 @@ const operationValidators = {
|
|
|
5301
5689
|
GetDocumentIncomingRelationships: (data) => {
|
|
5302
5690
|
if (data.documentIncomingRelationships) PHDocumentResultPageDTO.parse(data.documentIncomingRelationships);
|
|
5303
5691
|
},
|
|
5692
|
+
GetDocumentOutgoingRelationshipEdges: (data) => {
|
|
5693
|
+
if (data.documentOutgoingRelationshipEdges) DocumentRelationshipResultPageDTO.parse(data.documentOutgoingRelationshipEdges);
|
|
5694
|
+
},
|
|
5695
|
+
GetDocumentIncomingRelationshipEdges: (data) => {
|
|
5696
|
+
if (data.documentIncomingRelationshipEdges) DocumentRelationshipResultPageDTO.parse(data.documentIncomingRelationshipEdges);
|
|
5697
|
+
},
|
|
5304
5698
|
FindDocuments: (data) => {
|
|
5305
5699
|
if (data.findDocuments) PHDocumentResultPageDTO.parse(data.findDocuments);
|
|
5306
5700
|
},
|
|
@@ -5334,6 +5728,9 @@ const operationValidators = {
|
|
|
5334
5728
|
AddRelationship: (data) => {
|
|
5335
5729
|
if (data.addRelationship) PHDocumentDTO.parse(data.addRelationship);
|
|
5336
5730
|
},
|
|
5731
|
+
UpdateRelationship: (data) => {
|
|
5732
|
+
if (data.updateRelationship) PHDocumentDTO.parse(data.updateRelationship);
|
|
5733
|
+
},
|
|
5337
5734
|
RemoveRelationship: (data) => {
|
|
5338
5735
|
if (data.removeRelationship) PHDocumentDTO.parse(data.removeRelationship);
|
|
5339
5736
|
},
|
|
@@ -5375,7 +5772,7 @@ function createReactorGraphQLClient(url, fetchImpl = fetch, headers) {
|
|
|
5375
5772
|
}
|
|
5376
5773
|
//#endregion
|
|
5377
5774
|
//#region src/graphql/reactor/schema.graphql
|
|
5378
|
-
var schema_default = "# Scalar types (for codegen - also defined in create-schema.ts)\nscalar JSONObject\nscalar DateTime\n\n# Input types\ninput PagingInput {\n limit: Int\n offset: Int\n cursor: String\n}\n\ninput ViewFilterInput {\n branch: String\n scopes: [String!]\n}\n\ninput SearchFilterInput {\n type: String\n parentId: String\n identifiers: [String!]\n}\n\ninput OperationsFilterInput {\n documentId: String!\n branch: String\n scopes: [String!]\n actionTypes: [String!]\n sinceRevision: Int\n timestampFrom: String\n timestampTo: String\n}\n\ninput DocumentOperationsFilterInput {\n branch: String\n scopes: [String!]\n actionTypes: [String!]\n sinceRevision: Int\n timestampFrom: String\n timestampTo: String\n}\n\n# Enums\nenum PropagationMode {\n CASCADE\n ORPHAN\n}\n\nenum DocumentChangeType {\n CREATED\n DELETED\n UPDATED\n PARENT_ADDED\n PARENT_REMOVED\n CHILD_ADDED\n CHILD_REMOVED\n}\n\n# Object types\ntype DocumentModelGlobalState {\n id: String!\n name: String!\n namespace: String\n version: String\n specification: JSONObject!\n}\n\ntype DocumentModelResultPage {\n items: [DocumentModelGlobalState!]!\n totalCount: Int!\n hasNextPage: Boolean!\n hasPreviousPage: Boolean!\n cursor: String\n}\n\ntype Revision {\n scope: String!\n revision: Int!\n}\n\ntype PHDocument {\n id: String!\n slug: String\n preferredEditor: String\n name: String!\n documentType: String!\n state: JSONObject!\n revisionsList: [Revision!]!\n createdAtUtcIso: DateTime!\n lastModifiedAtUtcIso: DateTime!\n operations(\n filter: DocumentOperationsFilterInput\n paging: PagingInput\n ): ReactorOperationResultPage\n}\n\ntype PHDocumentResultPage {\n items: [PHDocument!]!\n totalCount: Int!\n hasNextPage: Boolean!\n hasPreviousPage: Boolean!\n cursor: String\n}\n\ntype ReactorOperationResultPage {\n items: [ReactorOperation!]!\n totalCount: Int!\n hasNextPage: Boolean!\n hasPreviousPage: Boolean!\n cursor: String\n}\n\ntype DeadLetterInfo {\n documentId: String!\n error: String!\n \"\"\"\n How the origin classified the failure. A peer needs this to mirror the origin's\n quarantine decision; without it a held auth operation would freeze the document\n on the peer side while the origin kept syncing.\n \"\"\"\n errorType: String\n jobId: String!\n branch: String!\n scopes: [String!]!\n operationCount: Int!\n}\n\ntype PollSyncEnvelopesResult {\n envelopes: [SyncEnvelope!]!\n ackOrdinal: Int!\n deadLetters: [DeadLetterInfo!]!\n hasMore: Boolean!\n}\n\ntype DocumentWithChildren {\n document: PHDocument!\n childIds: [String!]!\n}\n\ntype MoveRelationshipResult {\n source: PHDocument!\n target: PHDocument!\n}\n\ntype JobInfo {\n id: String!\n status: String!\n\n \"\"\"\n What the job produced, once it has produced anything. Null until then, which\n is the state every job is in when it is handed back from a submission.\n \"\"\"\n result: JSONObject\n\n error: String\n createdAt: DateTime!\n completedAt: DateTime\n}\n\ntype DocumentChangeEvent {\n type: DocumentChangeType!\n documents: [PHDocument!]!\n context: DocumentChangeContext\n}\n\ntype DocumentChangeContext {\n parentId: String\n childId: String\n}\n\ntype JobChangeEvent {\n jobId: String!\n status: String!\n result: JSONObject!\n error: String\n}\n\ntype ReactorSignerUser {\n address: String!\n networkId: String!\n chainId: Int!\n}\n\ntype ReactorSignerApp {\n name: String!\n key: String!\n}\n\ntype ReactorSigner {\n user: ReactorSignerUser\n app: ReactorSignerApp\n signatures: [String!]!\n}\n\ntype ActionContext {\n signer: ReactorSigner\n}\n\ntype Action {\n id: String!\n type: String!\n timestampUtcMs: String!\n input: JSONObject!\n scope: String!\n context: ActionContext\n}\n\n\"\"\"\nOne operation an authorization preflight predicts a verdict for.\n\n`input` is what a conditional grant reads, so a candidate standing for a\nfilled-in form carries that form's input. Omitting it predicts the verdict an\nempty input earns, not the verdict the filled-in form will get.\n\"\"\"\ninput ActionCandidateInput {\n scope: String!\n type: String!\n input: JSONObject\n}\n\nenum AuthDecision {\n ALLOW\n DENY\n}\n\n\"\"\"\nOne candidate's predicted verdict. `reason` carries the refusal a DENY is\nrecorded with, and is null on an ALLOW.\n\"\"\"\ntype ActionEvaluation {\n decision: AuthDecision!\n reason: String\n}\n\n\"\"\"\nThe predicted verdicts for a set of candidates, in the order they were given,\nwith the aggregates a caller branches on.\n\nThe aggregates are redundant -- a verdict is binary -- and all four are returned\nso a caller reads the one its question is phrased in rather than negating\nanother. Over no candidates every aggregate is false: nothing is allowed and\nnothing is denied.\n\"\"\"\ntype ActionEvaluations {\n evaluations: [ActionEvaluation!]!\n allAllowed: Boolean!\n anyAllowed: Boolean!\n allDenied: Boolean!\n anyDenied: Boolean!\n}\n\n# Input types for sync operations\n\"\"\"\nAn action's context, as a caller submits it.\n\nDeclares every field `ActionContext` carries, because an input object rejects a\nfield it does not declare: an action stamped with the head it applies to - which\nis what a signing client sends - is refused outright if these are missing, and\nthat refusal is a whole-request one, not a per-field one.\n\n`prevOpHash` and `prevOpIndex` are the scope head the action was composed\nagainst. They are recorded rather than enforced; a signature is verified from\nthe params carried in its own tuple, which do not include them.\n\"\"\"\ninput ActionContextInput {\n \"\"\"The index of the last operation in the scope the action applies to.\"\"\"\n prevOpIndex: Int\n\n \"\"\"The hash of the state the action applies to.\"\"\"\n prevOpHash: String\n\n \"\"\"Covers replay of an operation that would otherwise be a no-op.\"\"\"\n nonce: String\n\n signer: ReactorSignerInput\n}\n\ninput ReactorSignerInput {\n user: ReactorSignerUserInput\n app: ReactorSignerAppInput\n signatures: [String!]!\n}\n\ninput ReactorSignerUserInput {\n address: String!\n networkId: String!\n chainId: Int!\n}\n\ninput ReactorSignerAppInput {\n name: String!\n key: String!\n}\n\ninput ActionInput {\n id: String!\n type: String!\n timestampUtcMs: String!\n input: JSONObject!\n scope: String!\n context: ActionContextInput\n}\n\n# Synchronization types\ntype ReactorOperation {\n index: Int!\n timestampUtcMs: String!\n hash: String!\n skip: Int!\n error: String\n deniedReason: String\n id: String\n action: Action!\n}\n\ninput OperationInput {\n index: Int!\n timestampUtcMs: String!\n hash: String!\n skip: Int!\n error: String\n deniedReason: String\n id: String\n action: ActionInput!\n}\n\ntype OperationContext {\n documentId: String!\n documentType: String!\n scope: String!\n branch: String!\n ordinal: Int!\n}\n\ninput OperationContextInput {\n documentId: String!\n documentType: String!\n scope: String!\n branch: String!\n ordinal: Int!\n}\n\ntype OperationWithContext {\n operation: ReactorOperation!\n context: OperationContext!\n}\n\ninput OperationWithContextInput {\n operation: OperationInput!\n context: OperationContextInput!\n}\n\ntype ChannelMeta {\n id: String!\n}\n\ninput ChannelMetaInput {\n id: String!\n}\n\ntype RemoteCursor {\n remoteName: String!\n cursorOrdinal: Int!\n lastSyncedAtUtcMs: String\n}\n\ninput RemoteCursorInput {\n remoteName: String!\n cursorOrdinal: Int!\n lastSyncedAtUtcMs: String\n}\n\nenum SyncEnvelopeType {\n OPERATIONS\n ACK\n}\n\ntype SyncEnvelope {\n type: SyncEnvelopeType!\n channelMeta: ChannelMeta!\n operations: [OperationWithContext!]\n cursor: RemoteCursor\n key: String\n dependsOn: [String!]\n}\n\ninput SyncEnvelopeInput {\n type: SyncEnvelopeType!\n channelMeta: ChannelMetaInput!\n operations: [OperationWithContextInput!]\n cursor: RemoteCursorInput\n key: String\n dependsOn: [String!]\n}\n\ninput RemoteFilterInput {\n documentId: [String!]!\n scope: [String!]!\n branch: String!\n}\n\ninput TouchChannelInput {\n id: String!\n name: String!\n collectionId: String!\n filter: RemoteFilterInput!\n sinceTimestampUtcMs: String!\n}\n\ntype TouchChannelResult {\n success: Boolean!\n ackOrdinal: Int!\n}\n\ntype Query {\n # Get document models for a namespace\n documentModels(\n namespace: String\n paging: PagingInput\n ): DocumentModelResultPage!\n\n # Get a specific document by ID or slug\n document(identifier: String!, view: ViewFilterInput): DocumentWithChildren\n\n # Get outgoing relationships of a given type from a source document\n documentOutgoingRelationships(\n sourceIdentifier: String!\n relationshipType: String!\n view: ViewFilterInput\n paging: PagingInput\n ): PHDocumentResultPage!\n\n # Get incoming relationships of a given type to a target document\n documentIncomingRelationships(\n targetIdentifier: String!\n relationshipType: String!\n view: ViewFilterInput\n paging: PagingInput\n ): PHDocumentResultPage!\n\n # Find documents by search criteria\n findDocuments(\n search: SearchFilterInput\n view: ViewFilterInput\n paging: PagingInput\n ): PHDocumentResultPage!\n\n # Get job status\n jobStatus(jobId: String!): JobInfo\n\n # Get operations for a document with filtering and pagination\n documentOperations(\n filter: OperationsFilterInput!\n paging: PagingInput\n ): ReactorOperationResultPage!\n\n \"\"\"\n Predicts whether the calling subject would be admitted to execute each of a\n set of candidate operations, without submitting any of them. A UI asks this to\n disable a control rather than offer an action that fails on submit.\n\n The answer is a prediction, not a promise:\n\n - Real admission compiles an append condition over everything it read and the\n store enforces it at write time. A preflight reads no future, so a policy\n change landing between this answer and the submit changes the verdict. The\n submit path stays the only authority.\n - The verdict is evaluated at the stream heads, so it is correct for a\n candidate about to be submitted. A backdated submission is out of contract;\n the reactor decides that one by position.\n - A candidate whose verdict depends on its input has to carry that input, since\n a conditional grant reads it.\n\n The subject is the authenticated caller and cannot be named in the request:\n answering for an arbitrary subject would disclose what a policy grants\n somebody else. It carries both the caller's address and the did:key of the app\n instance whose token authenticated the request, so a policy naming either\n matches the same principal the write path presents.\n\n Requires the reactor's authEnforcement feature flag. Without it the reactor\n holds no decision model, so this fails with extensions.code\n AUTH_EVALUATION_UNSUPPORTED rather than guessing.\n \"\"\"\n evaluateActions(\n documentIdentifier: String!\n branch: String\n candidates: [ActionCandidateInput!]!\n ): ActionEvaluations!\n\n \"\"\"\n Polls for sync envelopes from a channel.\n\n An operation stored before the API required an action id cannot be\n represented by this schema, and rather than serving a partial response beside\n a bare non-null violation, this fails with extensions.code\n MALFORMED_STORED_OPERATION naming the operation. That code means the failure\n is worth polling through: the document holding the operation needs repairing,\n but the channel serves every other document, and a peer that stopped polling\n would stop receiving those too.\n \"\"\"\n pollSyncEnvelopes(\n channelId: String!\n outboxAck: Int!\n outboxLatest: Int!\n ): PollSyncEnvelopesResult!\n}\n\ntype Mutation {\n # Create a new document\n createDocument(document: JSONObject!, parentIdentifier: String): PHDocument!\n\n # Create an empty document of specified type\n createEmptyDocument(\n documentType: String!\n parentIdentifier: String\n ): PHDocument!\n\n \"\"\"\n Applies actions to a document and waits for the result.\n\n Each action is coerced against `ActionInput`, so one missing a field the wire\n declares is refused before any work starts. The id in particular: it is hashed\n into the operation id and replay dedupes by it, so an action without one is\n stored under an operation id shared by every id-less operation on the same\n document, scope and branch.\n\n Named for `IReactorClient.execute`, which this is the wire form of. `branch`\n defaults to `main`.\n \"\"\"\n execute(\n documentIdentifier: String!\n actions: [ActionInput!]!\n branch: String\n ): PHDocument!\n\n \"\"\"\n Submits actions to a document and returns the job that will apply them.\n\n Coerced exactly as `execute` is, so a malformed action is refused here rather\n than surfacing later as a failed job. Returns the job rather than only its id,\n so a caller has the status and the creation time it would otherwise have to\n ask for; `result` is null until the job produces one.\n \"\"\"\n executeAsync(\n documentIdentifier: String!\n actions: [ActionInput!]!\n branch: String\n ): JobInfo!\n\n # Apply actions to a document (synchronous)\n mutateDocument(\n documentIdentifier: String!\n actions: [JSONObject!]!\n view: ViewFilterInput\n ): PHDocument!\n @deprecated(\n reason: \"Use execute. Actions here are untyped, so a malformed one is refused by a hand-written check rather than by the schema, and `view.scopes` is accepted but ignored.\"\n )\n\n # Submit actions to a document (asynchronous)\n mutateDocumentAsync(\n documentIdentifier: String!\n actions: [JSONObject!]!\n view: ViewFilterInput\n ): String!\n @deprecated(\n reason: \"Use executeAsync, which is typed and returns the job rather than only its id.\"\n )\n\n # Rename a document\n renameDocument(\n documentIdentifier: String!\n name: String!\n branch: String\n ): PHDocument!\n\n # Update the preferred editor recorded in the document header meta.\n # Pass null/omit to clear it.\n setPreferredEditor(\n documentIdentifier: String!\n preferredEditor: String\n branch: String\n ): PHDocument!\n\n # Add a relationship between two documents\n addRelationship(\n sourceIdentifier: String!\n targetIdentifier: String!\n relationshipType: String!\n branch: String\n ): PHDocument!\n\n # Remove a relationship between two documents\n removeRelationship(\n sourceIdentifier: String!\n targetIdentifier: String!\n relationshipType: String!\n branch: String\n ): PHDocument!\n\n # Move a relationship from one source to another\n moveRelationship(\n sourceParentIdentifier: String!\n targetParentIdentifier: String!\n targetIdentifier: String!\n relationshipType: String!\n branch: String\n ): MoveRelationshipResult!\n\n # Delete a single document\n deleteDocument(identifier: String!, propagate: PropagationMode): Boolean!\n\n # Delete multiple documents\n deleteDocuments(identifiers: [String!]!, propagate: PropagationMode): Boolean!\n\n # Touch (create or update) a channel for sync\n touchChannel(input: TouchChannelInput!): TouchChannelResult!\n\n # Push sync envelopes to a channel\n pushSyncEnvelopes(envelopes: [SyncEnvelopeInput!]!): Boolean!\n}\n\ntype Subscription {\n # Subscribe to document changes\n documentChanges(\n search: SearchFilterInput\n view: ViewFilterInput\n ): DocumentChangeEvent!\n\n # Subscribe to job changes\n jobChanges(jobId: String!): JobChangeEvent!\n}\n";
|
|
5775
|
+
var schema_default = "# Scalar types (for codegen - also defined in create-schema.ts)\nscalar JSONObject\nscalar DateTime\n\n# Input types\ninput PagingInput {\n limit: Int\n offset: Int\n cursor: String\n}\n\ninput ViewFilterInput {\n branch: String\n scopes: [String!]\n}\n\ninput SearchFilterInput {\n type: String\n parentId: String\n identifiers: [String!]\n}\n\ninput OperationsFilterInput {\n documentId: String!\n branch: String\n scopes: [String!]\n actionTypes: [String!]\n sinceRevision: Int\n timestampFrom: String\n timestampTo: String\n}\n\ninput DocumentOperationsFilterInput {\n branch: String\n scopes: [String!]\n actionTypes: [String!]\n sinceRevision: Int\n timestampFrom: String\n timestampTo: String\n}\n\n# Enums\nenum PropagationMode {\n CASCADE\n ORPHAN\n}\n\nenum DocumentChangeType {\n CREATED\n DELETED\n UPDATED\n PARENT_ADDED\n PARENT_REMOVED\n CHILD_ADDED\n CHILD_REMOVED\n}\n\n# Object types\ntype DocumentModelGlobalState {\n id: String!\n name: String!\n namespace: String\n version: String\n specification: JSONObject!\n}\n\ntype DocumentModelResultPage {\n items: [DocumentModelGlobalState!]!\n hasNextPage: Boolean!\n hasPreviousPage: Boolean!\n cursor: String\n}\n\ntype Revision {\n scope: String!\n revision: Int!\n}\n\ntype PHDocument {\n id: String!\n slug: String\n preferredEditor: String\n name: String!\n documentType: String!\n state: JSONObject!\n revisionsList: [Revision!]!\n createdAtUtcIso: DateTime!\n lastModifiedAtUtcIso: DateTime!\n operations(\n filter: DocumentOperationsFilterInput\n paging: PagingInput\n ): ReactorOperationResultPage\n}\n\ntype PHDocumentResultPage {\n items: [PHDocument!]!\n hasNextPage: Boolean!\n hasPreviousPage: Boolean!\n cursor: String\n}\n\n# A directed edge between two documents, with the metadata recorded against it.\ntype DocumentRelationship {\n sourceId: String!\n targetId: String!\n relationshipType: String!\n metadata: JSONObject\n createdAt: DateTime!\n updatedAt: DateTime!\n}\n\ntype DocumentRelationshipResultPage {\n items: [DocumentRelationship!]!\n hasNextPage: Boolean!\n hasPreviousPage: Boolean!\n cursor: String\n}\n\ntype ReactorOperationResultPage {\n items: [ReactorOperation!]!\n hasNextPage: Boolean!\n hasPreviousPage: Boolean!\n cursor: String\n}\n\ntype DeadLetterInfo {\n documentId: String!\n error: String!\n \"\"\"\n How the origin classified the failure. A peer needs this to mirror the origin's\n quarantine decision; without it a held auth operation would freeze the document\n on the peer side while the origin kept syncing.\n \"\"\"\n errorType: String\n jobId: String!\n branch: String!\n scopes: [String!]!\n operationCount: Int!\n}\n\ntype PollSyncEnvelopesResult {\n envelopes: [SyncEnvelope!]!\n ackOrdinal: Int!\n deadLetters: [DeadLetterInfo!]!\n hasMore: Boolean!\n}\n\ntype DocumentWithChildren {\n document: PHDocument!\n childIds: [String!]!\n}\n\ntype MoveRelationshipResult {\n source: PHDocument!\n target: PHDocument!\n}\n\ntype JobInfo {\n id: String!\n status: String!\n\n \"\"\"\n What the job produced, once it has produced anything. Null until then, which\n is the state every job is in when it is handed back from a submission.\n \"\"\"\n result: JSONObject\n\n error: String\n createdAt: DateTime!\n completedAt: DateTime\n}\n\ntype DocumentChangeEvent {\n type: DocumentChangeType!\n documents: [PHDocument!]!\n context: DocumentChangeContext\n}\n\ntype DocumentChangeContext {\n parentId: String\n childId: String\n}\n\ntype JobChangeEvent {\n jobId: String!\n status: String!\n\n \"\"\"\n What the job produced, once it has produced anything. Nullable for the same\n reason as `JobInfo.result`: a job that has not written yet has produced\n nothing, and an empty object would claim otherwise.\n \"\"\"\n result: JSONObject\n\n error: String\n}\n\ntype ReactorSignerUser {\n address: String!\n networkId: String!\n chainId: Int!\n}\n\ntype ReactorSignerApp {\n name: String!\n key: String!\n}\n\ntype ReactorSigner {\n user: ReactorSignerUser\n app: ReactorSignerApp\n signatures: [String!]!\n}\n\ntype ActionContext {\n signer: ReactorSigner\n}\n\ntype Action {\n id: String!\n type: String!\n timestampUtcMs: String!\n input: JSONObject!\n scope: String!\n context: ActionContext\n}\n\n\"\"\"\nOne operation an authorization preflight predicts a verdict for.\n\n`input` is what a conditional grant reads, so a candidate standing for a\nfilled-in form carries that form's input. Omitting it predicts the verdict an\nempty input earns, not the verdict the filled-in form will get.\n\"\"\"\ninput ActionCandidateInput {\n scope: String!\n type: String!\n input: JSONObject\n}\n\nenum AuthDecision {\n ALLOW\n DENY\n}\n\n\"\"\"\nOne candidate's predicted verdict. `reason` carries the refusal a DENY is\nrecorded with, and is null on an ALLOW.\n\"\"\"\ntype ActionEvaluation {\n decision: AuthDecision!\n reason: String\n}\n\n\"\"\"\nThe predicted verdicts for a set of candidates, in the order they were given,\nwith the aggregates a caller branches on.\n\nThe aggregates are redundant -- a verdict is binary -- and all four are returned\nso a caller reads the one its question is phrased in rather than negating\nanother. Over no candidates every aggregate is false: nothing is allowed and\nnothing is denied.\n\"\"\"\ntype ActionEvaluations {\n evaluations: [ActionEvaluation!]!\n allAllowed: Boolean!\n anyAllowed: Boolean!\n allDenied: Boolean!\n anyDenied: Boolean!\n}\n\n# Input types for sync operations\n\"\"\"\nAn action's context, as a caller submits it.\n\nDeclares every field `ActionContext` carries, because an input object rejects a\nfield it does not declare: an action stamped with the head it applies to - which\nis what a signing client sends - is refused outright if these are missing, and\nthat refusal is a whole-request one, not a per-field one.\n\n`prevOpHash` and `prevOpIndex` are the scope head the action was composed\nagainst. They are recorded rather than enforced; a signature is verified from\nthe params carried in its own tuple, which do not include them.\n\"\"\"\ninput ActionContextInput {\n \"\"\"\n The index of the last operation in the scope the action applies to.\n \"\"\"\n prevOpIndex: Int\n\n \"\"\"\n The hash of the state the action applies to.\n \"\"\"\n prevOpHash: String\n\n \"\"\"\n Covers replay of an operation that would otherwise be a no-op.\n \"\"\"\n nonce: String\n\n signer: ReactorSignerInput\n}\n\ninput ReactorSignerInput {\n user: ReactorSignerUserInput\n app: ReactorSignerAppInput\n signatures: [String!]!\n}\n\ninput ReactorSignerUserInput {\n address: String!\n networkId: String!\n chainId: Int!\n}\n\ninput ReactorSignerAppInput {\n name: String!\n key: String!\n}\n\ninput ActionInput {\n id: String!\n type: String!\n timestampUtcMs: String!\n input: JSONObject!\n scope: String!\n context: ActionContextInput\n}\n\n# Synchronization types\ntype ReactorOperation {\n index: Int!\n timestampUtcMs: String!\n hash: String!\n skip: Int!\n error: String\n deniedReason: String\n id: String\n action: Action!\n}\n\ninput OperationInput {\n index: Int!\n timestampUtcMs: String!\n hash: String!\n skip: Int!\n error: String\n deniedReason: String\n id: String\n action: ActionInput!\n}\n\ntype OperationContext {\n documentId: String!\n documentType: String!\n scope: String!\n branch: String!\n ordinal: Int!\n}\n\ninput OperationContextInput {\n documentId: String!\n documentType: String!\n scope: String!\n branch: String!\n ordinal: Int!\n}\n\ntype OperationWithContext {\n operation: ReactorOperation!\n context: OperationContext!\n}\n\ninput OperationWithContextInput {\n operation: OperationInput!\n context: OperationContextInput!\n}\n\ntype ChannelMeta {\n id: String!\n}\n\ninput ChannelMetaInput {\n id: String!\n}\n\ntype RemoteCursor {\n remoteName: String!\n cursorOrdinal: Int!\n lastSyncedAtUtcMs: String\n}\n\ninput RemoteCursorInput {\n remoteName: String!\n cursorOrdinal: Int!\n lastSyncedAtUtcMs: String\n}\n\nenum SyncEnvelopeType {\n OPERATIONS\n ACK\n}\n\ntype SyncEnvelope {\n type: SyncEnvelopeType!\n channelMeta: ChannelMeta!\n operations: [OperationWithContext!]\n cursor: RemoteCursor\n key: String\n dependsOn: [String!]\n}\n\ninput SyncEnvelopeInput {\n type: SyncEnvelopeType!\n channelMeta: ChannelMetaInput!\n operations: [OperationWithContextInput!]\n cursor: RemoteCursorInput\n key: String\n dependsOn: [String!]\n}\n\ninput RemoteFilterInput {\n documentId: [String!]!\n scope: [String!]!\n branch: String!\n}\n\ninput TouchChannelInput {\n id: String!\n name: String!\n collectionId: String!\n filter: RemoteFilterInput!\n sinceTimestampUtcMs: String!\n}\n\ntype TouchChannelResult {\n success: Boolean!\n ackOrdinal: Int!\n}\n\ntype Query {\n # Get document models for a namespace\n documentModels(\n namespace: String\n paging: PagingInput\n ): DocumentModelResultPage!\n\n # Get a specific document by ID or slug\n document(identifier: String!, view: ViewFilterInput): DocumentWithChildren\n\n # Get outgoing relationships of a given type from a source document\n documentOutgoingRelationships(\n sourceIdentifier: String!\n relationshipType: String!\n view: ViewFilterInput\n paging: PagingInput\n ): PHDocumentResultPage!\n\n # Get incoming relationships of a given type to a target document\n documentIncomingRelationships(\n targetIdentifier: String!\n relationshipType: String!\n view: ViewFilterInput\n paging: PagingInput\n ): PHDocumentResultPage!\n\n # Get the outgoing relationship edges of a source document. Unlike\n # documentOutgoingRelationships, which returns the documents at the far end,\n # this returns the edges themselves, carrying their metadata and timestamps.\n # Omit relationshipType to return edges of every type.\n documentOutgoingRelationshipEdges(\n sourceIdentifier: String!\n relationshipType: String\n view: ViewFilterInput\n paging: PagingInput\n ): DocumentRelationshipResultPage!\n\n # Get the incoming relationship edges of a target document. Unlike\n # documentIncomingRelationships, which returns the documents at the far end,\n # this returns the edges themselves, carrying their metadata and timestamps.\n # Omit relationshipType to return edges of every type.\n documentIncomingRelationshipEdges(\n targetIdentifier: String!\n relationshipType: String\n view: ViewFilterInput\n paging: PagingInput\n ): DocumentRelationshipResultPage!\n\n # Find documents by search criteria\n findDocuments(\n search: SearchFilterInput\n view: ViewFilterInput\n paging: PagingInput\n ): PHDocumentResultPage!\n\n # Get job status\n jobStatus(jobId: String!): JobInfo\n\n # Get operations for a document with filtering and pagination\n documentOperations(\n filter: OperationsFilterInput!\n paging: PagingInput\n ): ReactorOperationResultPage!\n\n \"\"\"\n Predicts whether the calling subject would be admitted to execute each of a\n set of candidate operations, without submitting any of them. A UI asks this to\n disable a control rather than offer an action that fails on submit.\n\n The answer is a prediction, not a promise:\n\n - Real admission compiles an append condition over everything it read and the\n store enforces it at write time. A preflight reads no future, so a policy\n change landing between this answer and the submit changes the verdict. The\n submit path stays the only authority.\n - The verdict is evaluated at the stream heads, so it is correct for a\n candidate about to be submitted. A backdated submission is out of contract;\n the reactor decides that one by position.\n - A candidate whose verdict depends on its input has to carry that input, since\n a conditional grant reads it.\n\n The subject is the authenticated caller and cannot be named in the request:\n answering for an arbitrary subject would disclose what a policy grants\n somebody else. It carries both the caller's address and the did:key of the app\n instance whose token authenticated the request, so a policy naming either\n matches the same principal the write path presents.\n\n Requires the reactor's authEnforcement feature flag. Without it the reactor\n holds no decision model, so this fails with extensions.code\n AUTH_EVALUATION_UNSUPPORTED rather than guessing.\n \"\"\"\n evaluateActions(\n documentIdentifier: String!\n branch: String\n candidates: [ActionCandidateInput!]!\n ): ActionEvaluations!\n\n \"\"\"\n Polls for sync envelopes from a channel.\n\n An operation stored before the API required an action id cannot be\n represented by this schema, and rather than serving a partial response beside\n a bare non-null violation, this fails with extensions.code\n MALFORMED_STORED_OPERATION naming the operation. That code means the failure\n is worth polling through: the document holding the operation needs repairing,\n but the channel serves every other document, and a peer that stopped polling\n would stop receiving those too.\n \"\"\"\n pollSyncEnvelopes(\n channelId: String!\n outboxAck: Int!\n outboxLatest: Int!\n ): PollSyncEnvelopesResult!\n}\n\ntype Mutation {\n # Create a new document\n createDocument(document: JSONObject!, parentIdentifier: String): PHDocument!\n\n # Create an empty document of specified type\n createEmptyDocument(\n documentType: String!\n parentIdentifier: String\n ): PHDocument!\n\n \"\"\"\n Applies actions to a document and waits for the result.\n\n Each action is coerced against `ActionInput`, so one missing a field the wire\n declares is refused before any work starts. The id in particular: it is hashed\n into the operation id and replay dedupes by it, so an action without one is\n stored under an operation id shared by every id-less operation on the same\n document, scope and branch.\n\n Named for `IReactorClient.execute`, which this is the wire form of. `branch`\n defaults to `main`.\n \"\"\"\n execute(\n documentIdentifier: String!\n actions: [ActionInput!]!\n branch: String\n ): PHDocument!\n\n \"\"\"\n Submits actions to a document and returns the job that will apply them.\n\n Coerced exactly as `execute` is, so a malformed action is refused here rather\n than surfacing later as a failed job. Returns the job rather than only its id,\n so a caller has the status and the creation time it would otherwise have to\n ask for; `result` is null until the job produces one.\n \"\"\"\n executeAsync(\n documentIdentifier: String!\n actions: [ActionInput!]!\n branch: String\n ): JobInfo!\n\n # Apply actions to a document (synchronous)\n mutateDocument(\n documentIdentifier: String!\n actions: [JSONObject!]!\n view: ViewFilterInput\n ): PHDocument!\n @deprecated(\n reason: \"Use execute. Actions here are untyped, so a malformed one is refused by a hand-written check rather than by the schema, and `view.scopes` is accepted but ignored.\"\n )\n\n # Submit actions to a document (asynchronous)\n mutateDocumentAsync(\n documentIdentifier: String!\n actions: [JSONObject!]!\n view: ViewFilterInput\n ): String!\n @deprecated(\n reason: \"Use executeAsync, which is typed and returns the job rather than only its id.\"\n )\n\n # Rename a document\n renameDocument(\n documentIdentifier: String!\n name: String!\n branch: String\n ): PHDocument!\n\n # Update the preferred editor recorded in the document header meta.\n # Pass null/omit to clear it.\n setPreferredEditor(\n documentIdentifier: String!\n preferredEditor: String\n branch: String\n ): PHDocument!\n\n # Add a relationship between two documents. Adding one that already exists is\n # a no-op, metadata included -- use updateRelationship to change an edge.\n addRelationship(\n sourceIdentifier: String!\n targetIdentifier: String!\n relationshipType: String!\n metadata: JSONObject\n branch: String\n ): PHDocument!\n\n # Replace the metadata of an existing relationship, preserving its createdAt.\n # Omitting metadata, or passing null, clears it.\n updateRelationship(\n sourceIdentifier: String!\n targetIdentifier: String!\n relationshipType: String!\n metadata: JSONObject\n branch: String\n ): PHDocument!\n\n # Remove a relationship between two documents\n removeRelationship(\n sourceIdentifier: String!\n targetIdentifier: String!\n relationshipType: String!\n branch: String\n ): PHDocument!\n\n # Move a relationship from one source to another\n moveRelationship(\n sourceParentIdentifier: String!\n targetParentIdentifier: String!\n targetIdentifier: String!\n relationshipType: String!\n branch: String\n ): MoveRelationshipResult!\n\n # Delete a single document\n deleteDocument(identifier: String!, propagate: PropagationMode): Boolean!\n\n # Delete multiple documents\n deleteDocuments(identifiers: [String!]!, propagate: PropagationMode): Boolean!\n\n # Touch (create or update) a channel for sync\n touchChannel(input: TouchChannelInput!): TouchChannelResult!\n\n # Push sync envelopes to a channel\n pushSyncEnvelopes(envelopes: [SyncEnvelopeInput!]!): Boolean!\n}\n\ntype Subscription {\n # Subscribe to document changes\n documentChanges(\n search: SearchFilterInput\n view: ViewFilterInput\n ): DocumentChangeEvent!\n\n # Subscribe to job changes\n jobChanges(jobId: String!): JobChangeEvent!\n}\n";
|
|
5379
5776
|
//#endregion
|
|
5380
5777
|
//#region src/graphql/reactor/pubsub.ts
|
|
5381
5778
|
const pubSub = new PubSub();
|
|
@@ -5386,26 +5783,79 @@ const SUBSCRIPTION_TRIGGERS = {
|
|
|
5386
5783
|
DOCUMENT_CHANGES: "DOCUMENT_CHANGES",
|
|
5387
5784
|
JOB_CHANGES: "JOB_CHANGES"
|
|
5388
5785
|
};
|
|
5389
|
-
|
|
5390
|
-
|
|
5391
|
-
|
|
5392
|
-
|
|
5393
|
-
|
|
5394
|
-
|
|
5395
|
-
|
|
5396
|
-
|
|
5786
|
+
function subjectKey(subject) {
|
|
5787
|
+
return `${subject.address ?? ""}|${subject.key ?? ""}`;
|
|
5788
|
+
}
|
|
5789
|
+
/**
|
|
5790
|
+
* The reactor's change feed, read once per distinct subject and shared by that
|
|
5791
|
+
* subject's GraphQL subscriptions. Each feed is read as its subject, so the
|
|
5792
|
+
* client withholds what that subject may not read; one feed read with no view
|
|
5793
|
+
* would gate every subscriber as the host.
|
|
5794
|
+
*/
|
|
5795
|
+
var DocumentChangeFeed = class {
|
|
5796
|
+
reactorClient;
|
|
5797
|
+
#pubSub = new PubSub();
|
|
5798
|
+
#feeds = /* @__PURE__ */ new Map();
|
|
5799
|
+
constructor(reactorClient) {
|
|
5800
|
+
this.reactorClient = reactorClient;
|
|
5801
|
+
}
|
|
5802
|
+
subscribe(subject) {
|
|
5803
|
+
const key = subjectKey(subject);
|
|
5804
|
+
const topic = `${SUBSCRIPTION_TRIGGERS.DOCUMENT_CHANGES}:${key}`;
|
|
5805
|
+
const iterator = this.#pubSub.asyncIterableIterator(topic);
|
|
5806
|
+
this.#acquire(key, topic, subject);
|
|
5807
|
+
let released = false;
|
|
5808
|
+
const release = () => {
|
|
5809
|
+
if (!released) {
|
|
5810
|
+
released = true;
|
|
5811
|
+
this.#release(key);
|
|
5812
|
+
}
|
|
5397
5813
|
};
|
|
5398
|
-
|
|
5399
|
-
|
|
5400
|
-
|
|
5401
|
-
|
|
5402
|
-
|
|
5403
|
-
|
|
5404
|
-
|
|
5405
|
-
|
|
5814
|
+
return {
|
|
5815
|
+
next: () => iterator.next(),
|
|
5816
|
+
return: () => {
|
|
5817
|
+
release();
|
|
5818
|
+
return iterator.return();
|
|
5819
|
+
},
|
|
5820
|
+
throw: (error) => {
|
|
5821
|
+
release();
|
|
5822
|
+
return iterator.throw(error);
|
|
5823
|
+
},
|
|
5824
|
+
[Symbol.asyncIterator]() {
|
|
5825
|
+
return this;
|
|
5826
|
+
}
|
|
5827
|
+
};
|
|
5828
|
+
}
|
|
5829
|
+
#acquire(key, topic, subject) {
|
|
5830
|
+
const feed = this.#feeds.get(key);
|
|
5831
|
+
if (feed) {
|
|
5832
|
+
feed.subscribers++;
|
|
5833
|
+
return;
|
|
5406
5834
|
}
|
|
5407
|
-
|
|
5408
|
-
}
|
|
5835
|
+
const view = { subject };
|
|
5836
|
+
const unsubscribe = this.reactorClient.subscribe({}, (event) => {
|
|
5837
|
+
const payload = {
|
|
5838
|
+
documentChanges: event,
|
|
5839
|
+
search: {},
|
|
5840
|
+
view
|
|
5841
|
+
};
|
|
5842
|
+
this.#pubSub.publish(topic, payload);
|
|
5843
|
+
}, view);
|
|
5844
|
+
this.#feeds.set(key, {
|
|
5845
|
+
unsubscribe,
|
|
5846
|
+
subscribers: 1
|
|
5847
|
+
});
|
|
5848
|
+
}
|
|
5849
|
+
#release(key) {
|
|
5850
|
+
const feed = this.#feeds.get(key);
|
|
5851
|
+
if (!feed) return;
|
|
5852
|
+
feed.subscribers--;
|
|
5853
|
+
if (feed.subscribers === 0) {
|
|
5854
|
+
this.#feeds.delete(key);
|
|
5855
|
+
feed.unsubscribe();
|
|
5856
|
+
}
|
|
5857
|
+
}
|
|
5858
|
+
};
|
|
5409
5859
|
const activeJobSubscriptions = /* @__PURE__ */ new Map();
|
|
5410
5860
|
function ensureJobSubscription(reactorClient, jobId) {
|
|
5411
5861
|
let subscription = activeJobSubscriptions.get(jobId);
|
|
@@ -5422,7 +5872,7 @@ function ensureJobSubscription(reactorClient, jobId) {
|
|
|
5422
5872
|
createdAt: jobInfo.createdAtUtcIso,
|
|
5423
5873
|
completedAt: jobInfo.completedAtUtcIso ?? null,
|
|
5424
5874
|
error: jobInfo.error?.message ?? null,
|
|
5425
|
-
result: jobInfo.result ??
|
|
5875
|
+
result: jobInfo.result ?? null
|
|
5426
5876
|
},
|
|
5427
5877
|
jobId,
|
|
5428
5878
|
documentId: jobInfo.documentId
|
|
@@ -5470,6 +5920,11 @@ var ReactorSubgraph = class extends BaseSubgraph {
|
|
|
5470
5920
|
}
|
|
5471
5921
|
name = "r";
|
|
5472
5922
|
hasSubscriptions = true;
|
|
5923
|
+
#documentChanges;
|
|
5924
|
+
get documentChanges() {
|
|
5925
|
+
this.#documentChanges ??= new DocumentChangeFeed(this.reactorClient);
|
|
5926
|
+
return this.#documentChanges;
|
|
5927
|
+
}
|
|
5473
5928
|
/**
|
|
5474
5929
|
* Check operation-level permissions for an array of actions.
|
|
5475
5930
|
* Delegates to base assertCanExecuteOperation for each action.
|
|
@@ -5500,6 +5955,38 @@ var ReactorSubgraph = class extends BaseSubgraph {
|
|
|
5500
5955
|
}
|
|
5501
5956
|
}
|
|
5502
5957
|
/**
|
|
5958
|
+
* Drops the relationship edges whose far-end document the caller cannot read.
|
|
5959
|
+
*
|
|
5960
|
+
* An edge names two documents, so returning it discloses the far end's id. The
|
|
5961
|
+
* document-shaped relationship queries already withhold far-end documents the
|
|
5962
|
+
* caller cannot read; the edge-shaped queries have to withhold the same ones,
|
|
5963
|
+
* or they become a way to enumerate around that check. Supreme admins read
|
|
5964
|
+
* everything, so the per-edge check is skipped for them.
|
|
5965
|
+
*
|
|
5966
|
+
* The cursor describes where to resume in the underlying unfiltered stream,
|
|
5967
|
+
* so it is left as it came. A filtered page is therefore often shorter than
|
|
5968
|
+
* the requested limit, and that is the stream continuing rather than a sign
|
|
5969
|
+
* it ended.
|
|
5970
|
+
*/
|
|
5971
|
+
async filterRelationshipEdges(page, farEnd, ctx) {
|
|
5972
|
+
if (this.authorizationService.isSupremeAdmin(ctx.user?.address)) return page;
|
|
5973
|
+
const decisions = /* @__PURE__ */ new Map();
|
|
5974
|
+
const items = [];
|
|
5975
|
+
for (const edge of page.items) {
|
|
5976
|
+
const documentId = edge[farEnd];
|
|
5977
|
+
let canRead = decisions.get(documentId);
|
|
5978
|
+
if (canRead === void 0) {
|
|
5979
|
+
canRead = await this.canReadDocument(documentId, ctx);
|
|
5980
|
+
decisions.set(documentId, canRead);
|
|
5981
|
+
}
|
|
5982
|
+
if (canRead) items.push(edge);
|
|
5983
|
+
}
|
|
5984
|
+
return {
|
|
5985
|
+
...page,
|
|
5986
|
+
items
|
|
5987
|
+
};
|
|
5988
|
+
}
|
|
5989
|
+
/**
|
|
5503
5990
|
* Adds to `forbidden` the canonical document ids in `syncOps` that the caller
|
|
5504
5991
|
* cannot read, checking each distinct id once. Sync operation document ids are
|
|
5505
5992
|
* canonical (never slugs), so no resolution is needed.
|
|
@@ -5527,14 +6014,18 @@ var ReactorSubgraph = class extends BaseSubgraph {
|
|
|
5527
6014
|
* queued for the adopter.
|
|
5528
6015
|
*
|
|
5529
6016
|
* The refusal is the shape a read denial takes, so a puller treats it as a
|
|
5530
|
-
* signal to authenticate again rather than as a transport failure.
|
|
6017
|
+
* signal to authenticate again rather than as a transport failure. Pushes are
|
|
6018
|
+
* refused the same way -- a push interacts with the same private queue, and
|
|
6019
|
+
* reports the liveness that keeps it from being reclaimed -- but they never
|
|
6020
|
+
* adopt, since the push path has no drive check to clear the claimant.
|
|
5531
6021
|
*
|
|
5532
|
-
* Nothing is enforced or adopted without a serving gate
|
|
5533
|
-
*
|
|
5534
|
-
* belong to
|
|
6022
|
+
* Nothing is enforced or adopted without a serving gate: with no gate the
|
|
6023
|
+
* channel serves every subject alike, so there is no subject for it to
|
|
6024
|
+
* belong to.
|
|
5535
6025
|
*/
|
|
5536
|
-
async #bindOrRefuseChannel(channelId, ctx) {
|
|
6026
|
+
async #bindOrRefuseChannel(channelId, ctx, options = {}) {
|
|
5537
6027
|
if (!this.syncServingGate) return;
|
|
6028
|
+
const { adopt = true, action = "poll" } = options;
|
|
5538
6029
|
let remote;
|
|
5539
6030
|
try {
|
|
5540
6031
|
remote = this.syncManager.getById(channelId);
|
|
@@ -5544,10 +6035,10 @@ var ReactorSubgraph = class extends BaseSubgraph {
|
|
|
5544
6035
|
const bound = remote.meta.options.boundAddress;
|
|
5545
6036
|
const address = ctx.user?.address;
|
|
5546
6037
|
if (bound === void 0) {
|
|
5547
|
-
if (address !== void 0) await this.syncManager.bindRemote(channelId, address);
|
|
6038
|
+
if (adopt && address !== void 0) await this.syncManager.bindRemote(channelId, address);
|
|
5548
6039
|
return;
|
|
5549
6040
|
}
|
|
5550
|
-
if (bound !== address) throw new ForbiddenError(
|
|
6041
|
+
if (bound !== address) throw new ForbiddenError(`to ${action} this sync channel`);
|
|
5551
6042
|
}
|
|
5552
6043
|
/** The address a channel created by this request belongs to, if any. */
|
|
5553
6044
|
#creatorBinding(ctx) {
|
|
@@ -5635,6 +6126,34 @@ var ReactorSubgraph = class extends BaseSubgraph {
|
|
|
5635
6126
|
throw error;
|
|
5636
6127
|
}
|
|
5637
6128
|
},
|
|
6129
|
+
documentOutgoingRelationshipEdges: async (_parent, args, ctx) => {
|
|
6130
|
+
this.logger.debug("documentOutgoingRelationshipEdges(@args)", args);
|
|
6131
|
+
try {
|
|
6132
|
+
const handle = await this.assertCanRead(args.sourceIdentifier, ctx);
|
|
6133
|
+
const result = await documentOutgoingRelationshipEdges(this.reactorClient, {
|
|
6134
|
+
...args,
|
|
6135
|
+
sourceIdentifier: handle.fetchIdentifier
|
|
6136
|
+
}, this.viewSubject(ctx));
|
|
6137
|
+
return await this.filterRelationshipEdges(result, "targetId", ctx);
|
|
6138
|
+
} catch (error) {
|
|
6139
|
+
this.logger.error("Error in documentOutgoingRelationshipEdges: @Error", error);
|
|
6140
|
+
throw error;
|
|
6141
|
+
}
|
|
6142
|
+
},
|
|
6143
|
+
documentIncomingRelationshipEdges: async (_parent, args, ctx) => {
|
|
6144
|
+
this.logger.debug("documentIncomingRelationshipEdges(@args)", args);
|
|
6145
|
+
try {
|
|
6146
|
+
const handle = await this.assertCanRead(args.targetIdentifier, ctx);
|
|
6147
|
+
const result = await documentIncomingRelationshipEdges(this.reactorClient, {
|
|
6148
|
+
...args,
|
|
6149
|
+
targetIdentifier: handle.fetchIdentifier
|
|
6150
|
+
}, this.viewSubject(ctx));
|
|
6151
|
+
return await this.filterRelationshipEdges(result, "sourceId", ctx);
|
|
6152
|
+
} catch (error) {
|
|
6153
|
+
this.logger.error("Error in documentIncomingRelationshipEdges: @Error", error);
|
|
6154
|
+
throw error;
|
|
6155
|
+
}
|
|
6156
|
+
},
|
|
5638
6157
|
findDocuments: async (_parent, args, ctx) => {
|
|
5639
6158
|
this.logger.debug("findDocuments(@args)", args);
|
|
5640
6159
|
try {
|
|
@@ -5656,10 +6175,10 @@ var ReactorSubgraph = class extends BaseSubgraph {
|
|
|
5656
6175
|
throw error;
|
|
5657
6176
|
}
|
|
5658
6177
|
},
|
|
5659
|
-
jobStatus: async (_parent, args) => {
|
|
6178
|
+
jobStatus: async (_parent, args, ctx) => {
|
|
5660
6179
|
this.logger.debug("jobStatus(@args)", args);
|
|
5661
6180
|
try {
|
|
5662
|
-
return await jobStatus(this.reactorClient, args);
|
|
6181
|
+
return await jobStatus(this.reactorClient, args, (id) => this.servesDocument(id, ctx));
|
|
5663
6182
|
} catch (error) {
|
|
5664
6183
|
this.logger.error("Error in jobStatus: @Error", error);
|
|
5665
6184
|
throw error;
|
|
@@ -5681,6 +6200,13 @@ var ReactorSubgraph = class extends BaseSubgraph {
|
|
|
5681
6200
|
throw error;
|
|
5682
6201
|
}
|
|
5683
6202
|
},
|
|
6203
|
+
/**
|
|
6204
|
+
* Gated on reading the document, the same gate every other read here
|
|
6205
|
+
* carries: a verdict is derived from the document's policy, so a caller
|
|
6206
|
+
* who may not read the document may not preflight against it either.
|
|
6207
|
+
* That gate decides whether the query runs; the answer comes entirely
|
|
6208
|
+
* from the reactor's decision model.
|
|
6209
|
+
*/
|
|
5684
6210
|
evaluateActions: async (_parent, args, ctx) => {
|
|
5685
6211
|
this.logger.debug("evaluateActions(@args)", args);
|
|
5686
6212
|
try {
|
|
@@ -5712,8 +6238,10 @@ var ReactorSubgraph = class extends BaseSubgraph {
|
|
|
5712
6238
|
if (await this.authorizationService.canRead(driveId, void 0)) await this.#collectForbiddenDocuments(remote.channel.outbox.items, forbiddenIds, ctx);
|
|
5713
6239
|
await this.#collectForbiddenDocuments(remote.channel.deadLetter.items, forbiddenIds, ctx);
|
|
5714
6240
|
}
|
|
5715
|
-
const
|
|
5716
|
-
const
|
|
6241
|
+
const gate = this.syncServingGate;
|
|
6242
|
+
const gated = gate ? [...remote.channel.outbox.items, ...remote.channel.deadLetter.items] : void 0;
|
|
6243
|
+
const heldOpIds = gate && gated ? await collectHeldSyncOperations(gated, gate, this.viewSubject(ctx)) : /* @__PURE__ */ new Set();
|
|
6244
|
+
const { envelopes, ackOrdinal, deadLetters, hasMore } = pollSyncEnvelopes(this.syncManager, args, forbiddenIds, heldOpIds, gated && new Set(gated.map((syncOp) => syncOp.id)));
|
|
5717
6245
|
return {
|
|
5718
6246
|
envelopes,
|
|
5719
6247
|
ackOrdinal,
|
|
@@ -5734,7 +6262,7 @@ var ReactorSubgraph = class extends BaseSubgraph {
|
|
|
5734
6262
|
const parent = await document(this.reactorClient, { identifier: args.parentIdentifier });
|
|
5735
6263
|
await this.assertCanWriteCanonical(parent.document.id, ctx);
|
|
5736
6264
|
} else this.assertCanCreate(ctx);
|
|
5737
|
-
const result = await createDocument(this.reactorClient, args, this.graphqlManager.reactorDriveClient);
|
|
6265
|
+
const result = await createDocument(this.reactorClient, args, this.graphqlManager.reactorDriveClient, this.viewSubject(ctx));
|
|
5738
6266
|
if (result?.id && isDriveContainerType(result.documentType)) this.graphqlManager.driveOwnershipCache.add(result.id);
|
|
5739
6267
|
if (ctx.user?.address && result?.id) await this.documentPermissionService?.initializeDocumentProtection(result.id, ctx.user.address, this.authorizationService.config.defaultProtection);
|
|
5740
6268
|
return result;
|
|
@@ -5750,7 +6278,7 @@ var ReactorSubgraph = class extends BaseSubgraph {
|
|
|
5750
6278
|
const parent = await document(this.reactorClient, { identifier: args.parentIdentifier });
|
|
5751
6279
|
await this.assertCanWriteCanonical(parent.document.id, ctx);
|
|
5752
6280
|
} else this.assertCanCreate(ctx);
|
|
5753
|
-
const result = await createEmptyDocument(this.reactorClient, args, this.graphqlManager.reactorDriveClient);
|
|
6281
|
+
const result = await createEmptyDocument(this.reactorClient, args, this.graphqlManager.reactorDriveClient, this.viewSubject(ctx));
|
|
5754
6282
|
if (result?.id && isDriveContainerType(result.documentType)) this.graphqlManager.driveOwnershipCache.add(result.id);
|
|
5755
6283
|
if (ctx.user?.address && result?.id) await this.documentPermissionService?.initializeDocumentProtection(result.id, ctx.user.address, this.authorizationService.config.defaultProtection);
|
|
5756
6284
|
return result;
|
|
@@ -5766,7 +6294,7 @@ var ReactorSubgraph = class extends BaseSubgraph {
|
|
|
5766
6294
|
return await execute(this.reactorClient, {
|
|
5767
6295
|
...args,
|
|
5768
6296
|
documentIdentifier: handle.fetchIdentifier
|
|
5769
|
-
});
|
|
6297
|
+
}, this.viewSubject(ctx));
|
|
5770
6298
|
} catch (error) {
|
|
5771
6299
|
this.logger.error("Error in execute(@args): @Error", args, error);
|
|
5772
6300
|
throw error;
|
|
@@ -5792,7 +6320,7 @@ var ReactorSubgraph = class extends BaseSubgraph {
|
|
|
5792
6320
|
return await mutateDocument(this.reactorClient, {
|
|
5793
6321
|
...args,
|
|
5794
6322
|
documentIdentifier: handle.fetchIdentifier
|
|
5795
|
-
});
|
|
6323
|
+
}, this.viewSubject(ctx));
|
|
5796
6324
|
} catch (error) {
|
|
5797
6325
|
this.logger.error("Error in mutateDocument(@args): @Error", args, error);
|
|
5798
6326
|
throw error;
|
|
@@ -5818,7 +6346,7 @@ var ReactorSubgraph = class extends BaseSubgraph {
|
|
|
5818
6346
|
return await renameDocument(this.reactorClient, {
|
|
5819
6347
|
...args,
|
|
5820
6348
|
documentIdentifier: handle.fetchIdentifier
|
|
5821
|
-
});
|
|
6349
|
+
}, void 0, this.viewSubject(ctx));
|
|
5822
6350
|
} catch (error) {
|
|
5823
6351
|
this.logger.error("Error in renameDocument(@args): @Error", args, error);
|
|
5824
6352
|
throw error;
|
|
@@ -5831,7 +6359,7 @@ var ReactorSubgraph = class extends BaseSubgraph {
|
|
|
5831
6359
|
return await setPreferredEditor(this.reactorClient, {
|
|
5832
6360
|
...args,
|
|
5833
6361
|
documentIdentifier: handle.fetchIdentifier
|
|
5834
|
-
});
|
|
6362
|
+
}, void 0, this.viewSubject(ctx));
|
|
5835
6363
|
} catch (error) {
|
|
5836
6364
|
this.logger.error("Error in setPreferredEditor(@args): @Error", args, error);
|
|
5837
6365
|
throw error;
|
|
@@ -5844,12 +6372,25 @@ var ReactorSubgraph = class extends BaseSubgraph {
|
|
|
5844
6372
|
return await addRelationship(this.reactorClient, {
|
|
5845
6373
|
...args,
|
|
5846
6374
|
sourceIdentifier: handle.fetchIdentifier
|
|
5847
|
-
});
|
|
6375
|
+
}, this.viewSubject(ctx));
|
|
5848
6376
|
} catch (error) {
|
|
5849
6377
|
this.logger.error("Error in addRelationship(@args): @Error", args, error);
|
|
5850
6378
|
throw error;
|
|
5851
6379
|
}
|
|
5852
6380
|
},
|
|
6381
|
+
updateRelationship: async (_parent, args, ctx) => {
|
|
6382
|
+
this.logger.debug("updateRelationship(@args)", args);
|
|
6383
|
+
try {
|
|
6384
|
+
const handle = await this.assertCanWrite(args.sourceIdentifier, ctx);
|
|
6385
|
+
return await updateRelationship(this.reactorClient, {
|
|
6386
|
+
...args,
|
|
6387
|
+
sourceIdentifier: handle.fetchIdentifier
|
|
6388
|
+
}, this.viewSubject(ctx));
|
|
6389
|
+
} catch (error) {
|
|
6390
|
+
this.logger.error("Error in updateRelationship(@args): @Error", args, error);
|
|
6391
|
+
throw error;
|
|
6392
|
+
}
|
|
6393
|
+
},
|
|
5853
6394
|
removeRelationship: async (_parent, args, ctx) => {
|
|
5854
6395
|
this.logger.debug("removeRelationship(@args)", args);
|
|
5855
6396
|
try {
|
|
@@ -5857,7 +6398,7 @@ var ReactorSubgraph = class extends BaseSubgraph {
|
|
|
5857
6398
|
return await removeRelationship(this.reactorClient, {
|
|
5858
6399
|
...args,
|
|
5859
6400
|
sourceIdentifier: handle.fetchIdentifier
|
|
5860
|
-
});
|
|
6401
|
+
}, this.viewSubject(ctx));
|
|
5861
6402
|
} catch (error) {
|
|
5862
6403
|
this.logger.error("Error in removeRelationship(@args): @Error", args, error);
|
|
5863
6404
|
throw error;
|
|
@@ -5872,7 +6413,7 @@ var ReactorSubgraph = class extends BaseSubgraph {
|
|
|
5872
6413
|
...args,
|
|
5873
6414
|
sourceParentIdentifier: sourceHandle.fetchIdentifier,
|
|
5874
6415
|
targetParentIdentifier: targetHandle.fetchIdentifier
|
|
5875
|
-
});
|
|
6416
|
+
}, this.viewSubject(ctx));
|
|
5876
6417
|
} catch (error) {
|
|
5877
6418
|
this.logger.error("Error in moveRelationship(@args): @Error @args", error, args);
|
|
5878
6419
|
throw error;
|
|
@@ -5928,6 +6469,16 @@ var ReactorSubgraph = class extends BaseSubgraph {
|
|
|
5928
6469
|
pushSyncEnvelopes: async (_parent, args, ctx) => {
|
|
5929
6470
|
this.logger.debug("pushSyncEnvelopes(@args)", args);
|
|
5930
6471
|
try {
|
|
6472
|
+
const boundChannelIds = /* @__PURE__ */ new Set();
|
|
6473
|
+
for (const envelope of args.envelopes) {
|
|
6474
|
+
const channelId = envelope.channelMeta.id;
|
|
6475
|
+
if (boundChannelIds.has(channelId)) continue;
|
|
6476
|
+
boundChannelIds.add(channelId);
|
|
6477
|
+
await this.#bindOrRefuseChannel(channelId, ctx, {
|
|
6478
|
+
adopt: false,
|
|
6479
|
+
action: "push to"
|
|
6480
|
+
});
|
|
6481
|
+
}
|
|
5931
6482
|
const checkedOperations = /* @__PURE__ */ new Map();
|
|
5932
6483
|
for (const envelope of args.envelopes) for (const op of envelope.operations ?? []) {
|
|
5933
6484
|
const documentId = op.context.documentId;
|
|
@@ -5973,10 +6524,7 @@ var ReactorSubgraph = class extends BaseSubgraph {
|
|
|
5973
6524
|
documentChanges: {
|
|
5974
6525
|
subscribe: (rootValue, args, ctx, info) => {
|
|
5975
6526
|
this.logger.debug("documentChanges subscription started");
|
|
5976
|
-
return withFilter(() => {
|
|
5977
|
-
ensureGlobalDocumentSubscription(this.reactorClient);
|
|
5978
|
-
return getPubSub().asyncIterableIterator(SUBSCRIPTION_TRIGGERS.DOCUMENT_CHANGES);
|
|
5979
|
-
}, async (payload, filterArgs, filterCtx) => {
|
|
6527
|
+
return withFilter(() => this.documentChanges.subscribe(this.viewSubject(ctx)), async (payload, filterArgs, filterCtx) => {
|
|
5980
6528
|
if (!payload) return false;
|
|
5981
6529
|
const search = {
|
|
5982
6530
|
type: filterArgs?.search?.type ?? void 0,
|
|
@@ -6003,6 +6551,7 @@ var ReactorSubgraph = class extends BaseSubgraph {
|
|
|
6003
6551
|
}, async (payload, filterArgs, filterCtx) => {
|
|
6004
6552
|
if (!payload || !filterArgs) return false;
|
|
6005
6553
|
if (!matchesJobFilter(payload, filterArgs)) return false;
|
|
6554
|
+
if (!await this.servesDocument(payload.documentId, filterCtx)) return false;
|
|
6006
6555
|
if (this.authorizationService.isSupremeAdmin(filterCtx?.user?.address)) return true;
|
|
6007
6556
|
if (!payload.documentId) return false;
|
|
6008
6557
|
return this.canReadDocument(payload.documentId, filterCtx);
|
|
@@ -6031,10 +6580,10 @@ const ADMIN_USERS = getAdminUsers();
|
|
|
6031
6580
|
//#endregion
|
|
6032
6581
|
//#region src/graphql/system/version.ts
|
|
6033
6582
|
function getVersion() {
|
|
6034
|
-
return "6.2.3-dev.
|
|
6583
|
+
return "6.2.3-dev.31";
|
|
6035
6584
|
}
|
|
6036
6585
|
function getGitHash() {
|
|
6037
|
-
return "
|
|
6586
|
+
return "5dd6b63b45454352e105a7f71aefac4f70e8bfbd";
|
|
6038
6587
|
}
|
|
6039
6588
|
function getGitUrl() {
|
|
6040
6589
|
return buildTreeUrl(getGitHash());
|
|
@@ -6063,6 +6612,10 @@ var SystemSubgraph = class extends BaseSubgraph {
|
|
|
6063
6612
|
};
|
|
6064
6613
|
//#endregion
|
|
6065
6614
|
//#region src/packages/http-loader.ts
|
|
6615
|
+
const EXACT_VERSION = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
6616
|
+
function piecesBaseUrl(registryUrl, packageName, version) {
|
|
6617
|
+
return `${registryUrl.endsWith("/") ? registryUrl : `${registryUrl}/`}-/cdn/${packageName}@${version}/node/pieces/`;
|
|
6618
|
+
}
|
|
6066
6619
|
/**
|
|
6067
6620
|
* Extract subgraph classes from the imported subgraphs/index.mjs module shape.
|
|
6068
6621
|
*
|
|
@@ -6135,7 +6688,8 @@ var HttpPackageLoader = class {
|
|
|
6135
6688
|
async loadUpgradeManifests(packageSpec) {
|
|
6136
6689
|
const { name: packageName } = this.parsePackageSpec(packageSpec);
|
|
6137
6690
|
if (!this.isValidPackageName(packageName)) throw new Error(`Invalid package name: ${packageName}`);
|
|
6138
|
-
const
|
|
6691
|
+
const module = await import(`${this.registryUrl}-/cdn/${packageSpec}/node/document-models/index.mjs`);
|
|
6692
|
+
const manifests = extractUpgradeManifests(module);
|
|
6139
6693
|
if (manifests.length > 0) this.logger.verbose(`Loaded ${manifests.length} upgrade manifests from ${packageName}`);
|
|
6140
6694
|
return manifests;
|
|
6141
6695
|
}
|
|
@@ -6161,6 +6715,27 @@ var HttpPackageLoader = class {
|
|
|
6161
6715
|
this.logger.verbose(`No processor factory found in ${packageName}`);
|
|
6162
6716
|
return null;
|
|
6163
6717
|
}
|
|
6718
|
+
async loadPieces(packageSpec) {
|
|
6719
|
+
const { name: packageName } = this.parsePackageSpec(packageSpec);
|
|
6720
|
+
if (!this.isValidPackageName(packageName)) throw new Error(`Invalid package name: ${packageName}`);
|
|
6721
|
+
const version = await this.packageVersion(packageSpec);
|
|
6722
|
+
if (!version) {
|
|
6723
|
+
this.logger.verbose(`No package version found for: ${packageSpec}`);
|
|
6724
|
+
return [];
|
|
6725
|
+
}
|
|
6726
|
+
const base = piecesBaseUrl(this.registryUrl, packageName, version);
|
|
6727
|
+
this.logger.verbose(`Importing pieces from: ${base}index.mjs`);
|
|
6728
|
+
let module;
|
|
6729
|
+
try {
|
|
6730
|
+
module = await import(`${base}index.mjs`);
|
|
6731
|
+
} catch (error) {
|
|
6732
|
+
this.logger.verbose(`No pieces found for: ${packageName}`, error);
|
|
6733
|
+
return [];
|
|
6734
|
+
}
|
|
6735
|
+
const pieces = piecesFromCdnList(module, base, packageName, this.logger, version);
|
|
6736
|
+
this.logger.verbose(`Loaded ${pieces.length} pieces from ${packageName}`);
|
|
6737
|
+
return pieces;
|
|
6738
|
+
}
|
|
6164
6739
|
/**
|
|
6165
6740
|
* Load document models from multiple packages.
|
|
6166
6741
|
* Continues loading even if some packages fail.
|
|
@@ -6184,6 +6759,17 @@ var HttpPackageLoader = class {
|
|
|
6184
6759
|
}
|
|
6185
6760
|
return allModels;
|
|
6186
6761
|
}
|
|
6762
|
+
async packageVersion(packageSpec) {
|
|
6763
|
+
try {
|
|
6764
|
+
const response = await fetch(`${this.registryUrl}-/cdn/${packageSpec}/package.json`);
|
|
6765
|
+
if (!response.ok) return void 0;
|
|
6766
|
+
const pkg = await response.json();
|
|
6767
|
+
return typeof pkg.version === "string" && EXACT_VERSION.test(pkg.version) ? pkg.version : void 0;
|
|
6768
|
+
} catch (error) {
|
|
6769
|
+
this.logger.verbose(`Could not read the version of ${packageSpec}`, error);
|
|
6770
|
+
return;
|
|
6771
|
+
}
|
|
6772
|
+
}
|
|
6187
6773
|
isValidPackageName(name) {
|
|
6188
6774
|
return /^(@[a-z0-9][-a-z0-9._]*\/)?[a-z0-9][-a-z0-9._]*$/i.test(name) && !name.includes("..") && name.length <= 214;
|
|
6189
6775
|
}
|
|
@@ -6258,8 +6844,13 @@ var HttpDocumentModelLoader = class {
|
|
|
6258
6844
|
* This class is used to load packages using the import keyword.
|
|
6259
6845
|
*/
|
|
6260
6846
|
var ImportPackageLoader = class {
|
|
6847
|
+
hostDir;
|
|
6261
6848
|
logger = childLogger(["reactor-api", "import-loader"]);
|
|
6262
6849
|
name = "ImportPackageLoader";
|
|
6850
|
+
constructor(hostDir = process.cwd()) {
|
|
6851
|
+
this.hostDir = hostDir;
|
|
6852
|
+
}
|
|
6853
|
+
reportedUnbuilt = /* @__PURE__ */ new Set();
|
|
6263
6854
|
async loadDocumentModels(identifier) {
|
|
6264
6855
|
this.logger.verbose(`Loading document models from package: ${identifier}`);
|
|
6265
6856
|
const pkgModule = await loadDocumentModels(identifier);
|
|
@@ -6302,6 +6893,31 @@ var ImportPackageLoader = class {
|
|
|
6302
6893
|
this.logger.verbose(` ➜ No Processor Factory found: ${identifier}`);
|
|
6303
6894
|
return null;
|
|
6304
6895
|
}
|
|
6896
|
+
async loadPieces(identifier) {
|
|
6897
|
+
this.logger.verbose(`Loading pieces from package: ${identifier}`);
|
|
6898
|
+
const { root, listPath } = pieceListLocation(identifier, this.hostDir);
|
|
6899
|
+
if (!existsSync(listPath)) {
|
|
6900
|
+
this.reportUnbuilt(identifier, root, listPath);
|
|
6901
|
+
return [];
|
|
6902
|
+
}
|
|
6903
|
+
const module = await import(
|
|
6904
|
+
/* @vite-ignore */
|
|
6905
|
+
pathToFileURL(listPath).href
|
|
6906
|
+
);
|
|
6907
|
+
const pieces = piecesFromListModule(module, {
|
|
6908
|
+
root,
|
|
6909
|
+
identifier,
|
|
6910
|
+
logger: this.logger
|
|
6911
|
+
});
|
|
6912
|
+
this.logger.verbose(` ➜ Loaded ${pieces.length} Pieces from: ${identifier}`);
|
|
6913
|
+
return pieces;
|
|
6914
|
+
}
|
|
6915
|
+
reportUnbuilt(identifier, root, listPath) {
|
|
6916
|
+
if (!isAbsolute(identifier) || this.reportedUnbuilt.has(identifier)) return;
|
|
6917
|
+
if (!existsSync(join(root, PIECE_SOURCE_LIST))) return;
|
|
6918
|
+
this.reportedUnbuilt.add(identifier);
|
|
6919
|
+
this.logger.warn("@pkg declares pieces but @list was never built; run `ph build`", identifier, listPath);
|
|
6920
|
+
}
|
|
6305
6921
|
};
|
|
6306
6922
|
//#endregion
|
|
6307
6923
|
//#region src/packages/package-manager.ts
|
|
@@ -6317,10 +6933,12 @@ var ImportPackageLoader = class {
|
|
|
6317
6933
|
* either bare (`'pkg'`) or as a subpath (`'pkg/subgraphs'`), since loaders
|
|
6318
6934
|
* import sub-entries like `${pkg}/subgraphs` / `${pkg}/document-models`.
|
|
6319
6935
|
*/
|
|
6320
|
-
function isExpectedLoaderMiss(error, pkg) {
|
|
6936
|
+
function isExpectedLoaderMiss(error, pkg, subPath) {
|
|
6321
6937
|
if (!(error instanceof Error)) return false;
|
|
6322
6938
|
const code = error.code;
|
|
6323
6939
|
if (code === "ERR_UNSUPPORTED_DIR_IMPORT") return true;
|
|
6940
|
+
if (code === "ERR_PACKAGE_PATH_NOT_EXPORTED" && subPath !== void 0 && error.message.includes(`'./${subPath}'`)) return true;
|
|
6941
|
+
if (subPath !== void 0 && new RegExp(`^Failed to fetch \\S*/${subPath}/index\\.mjs: 404$`).test(error.message)) return true;
|
|
6324
6942
|
if (error.message.startsWith("Invalid package name:")) return true;
|
|
6325
6943
|
if (code === "ERR_MODULE_NOT_FOUND") return error.message.includes(`'${pkg}'`) || error.message.includes(`"${pkg}"`) || error.message.includes(`'${pkg}/`) || error.message.includes(`"${pkg}/`);
|
|
6326
6944
|
return false;
|
|
@@ -6336,12 +6954,14 @@ function getUniqueUpgradeManifests(...upgradeManifests) {
|
|
|
6336
6954
|
return Array.from(uniqueManifests.values());
|
|
6337
6955
|
}
|
|
6338
6956
|
var PackageManager = class {
|
|
6957
|
+
options;
|
|
6339
6958
|
logger = childLogger(["reactor-api", "package-manager"]);
|
|
6340
6959
|
loaders;
|
|
6341
6960
|
docModelsMap = /* @__PURE__ */ new Map();
|
|
6342
6961
|
upgradeManifestsMap = /* @__PURE__ */ new Map();
|
|
6343
6962
|
subgraphsMap = /* @__PURE__ */ new Map();
|
|
6344
6963
|
processorMap = /* @__PURE__ */ new Map();
|
|
6964
|
+
piecesMap = /* @__PURE__ */ new Map();
|
|
6345
6965
|
configWatcher;
|
|
6346
6966
|
debouncedUpdateCallbacks = /* @__PURE__ */ new Map();
|
|
6347
6967
|
eventEmitter = new EventEmitter();
|
|
@@ -6360,10 +6980,12 @@ var PackageManager = class {
|
|
|
6360
6980
|
const upgradeManifestsMap = await this.loadUpgradeManifests(packages);
|
|
6361
6981
|
const subgraphsMap = await this.loadSubgraphs(packages);
|
|
6362
6982
|
const processorsMap = await this.loadProcessors(packages);
|
|
6983
|
+
const piecesMap = await this.loadPieces(packages);
|
|
6363
6984
|
this.upgradeManifestsMap = upgradeManifestsMap;
|
|
6364
6985
|
this.updatePackagesMap(documentModelsMap);
|
|
6365
6986
|
this.updateSubgraphsMap(subgraphsMap);
|
|
6366
6987
|
this.updateProcessorsMap(processorsMap);
|
|
6988
|
+
this.updatePiecesMap(piecesMap);
|
|
6367
6989
|
try {
|
|
6368
6990
|
this.subscribePackages(packages);
|
|
6369
6991
|
} catch (error) {
|
|
@@ -6373,7 +6995,8 @@ var PackageManager = class {
|
|
|
6373
6995
|
documentModels: getUniqueDocumentModels(...Array.from(documentModelsMap.values())),
|
|
6374
6996
|
upgradeManifests: this.getUniqueUpgradeManifests(),
|
|
6375
6997
|
subgraphs: subgraphsMap,
|
|
6376
|
-
processors: processorsMap
|
|
6998
|
+
processors: processorsMap,
|
|
6999
|
+
pieces: piecesMap
|
|
6377
7000
|
};
|
|
6378
7001
|
}
|
|
6379
7002
|
async loadDocumentModels(packages) {
|
|
@@ -6473,9 +7096,34 @@ var PackageManager = class {
|
|
|
6473
7096
|
}
|
|
6474
7097
|
return processorsMap;
|
|
6475
7098
|
}
|
|
7099
|
+
async loadPieces(packages) {
|
|
7100
|
+
this.logger.debug(`Loading pieces from packages: ${packages.join(", ")}`);
|
|
7101
|
+
const piecesMap = /* @__PURE__ */ new Map();
|
|
7102
|
+
for (const pkg of packages) {
|
|
7103
|
+
const allPieces = [];
|
|
7104
|
+
const failures = [];
|
|
7105
|
+
let succeeded = false;
|
|
7106
|
+
for (const loader of this.loaders) try {
|
|
7107
|
+
allPieces.push(...await loader.loadPieces(pkg));
|
|
7108
|
+
succeeded = true;
|
|
7109
|
+
break;
|
|
7110
|
+
} catch (error) {
|
|
7111
|
+
failures.push({
|
|
7112
|
+
loader: loader.name,
|
|
7113
|
+
error
|
|
7114
|
+
});
|
|
7115
|
+
this.logger.debug(`[${loader.name}] Failed to load pieces from package ${pkg}`, error);
|
|
7116
|
+
}
|
|
7117
|
+
this.maybeWarnAllLoadersFailed("pieces", pkg, succeeded, failures);
|
|
7118
|
+
if (allPieces.length > 0) this.logger.info("Loaded @count piece(s) from package @pkg: @pieces", allPieces.length, pkg, allPieces.map((piece) => piece.name).join(", "));
|
|
7119
|
+
piecesMap.set(pkg, allPieces);
|
|
7120
|
+
}
|
|
7121
|
+
return piecesMap;
|
|
7122
|
+
}
|
|
6476
7123
|
maybeWarnAllLoadersFailed(kind, pkg, succeeded, failures) {
|
|
6477
7124
|
if (succeeded || failures.length === 0) return;
|
|
6478
|
-
const
|
|
7125
|
+
const subPath = kind === "document models" ? "document-models" : kind;
|
|
7126
|
+
const realFailures = failures.filter(({ error }) => !isExpectedLoaderMiss(error, pkg, subPath));
|
|
6479
7127
|
if (realFailures.length === 0) return;
|
|
6480
7128
|
const details = realFailures.map(({ loader, error }) => {
|
|
6481
7129
|
return ` [${loader}] ${error instanceof Error ? error.message : String(error)}`;
|
|
@@ -6515,6 +7163,15 @@ var PackageManager = class {
|
|
|
6515
7163
|
else processorsMap.set(pkg, pkgProcessors);
|
|
6516
7164
|
this.updateProcessorsMap(processorsMap);
|
|
6517
7165
|
}
|
|
7166
|
+
async updatePiecesForPackage(pkg) {
|
|
7167
|
+
this.logger.debug(`Updating pieces for package: ${pkg}`);
|
|
7168
|
+
const pieces = await this.loadPieces([pkg]);
|
|
7169
|
+
const piecesMap = new Map(this.piecesMap);
|
|
7170
|
+
const pkgPieces = pieces.get(pkg) ?? [];
|
|
7171
|
+
if (pkgPieces.length === 0) piecesMap.delete(pkg);
|
|
7172
|
+
else piecesMap.set(pkg, pkgPieces);
|
|
7173
|
+
this.updatePiecesMap(piecesMap);
|
|
7174
|
+
}
|
|
6518
7175
|
/** Debounced per key so repeated subscribePackages calls reuse one timer. */
|
|
6519
7176
|
getDebouncedUpdateCallback(key, update) {
|
|
6520
7177
|
if (!this.debouncedUpdateCallbacks.has(key)) {
|
|
@@ -6533,10 +7190,12 @@ var PackageManager = class {
|
|
|
6533
7190
|
const onDocumentModels = this.getDebouncedUpdateCallback(`${pkg}:document-models`, () => this.updateDocumentModelsForPackage(pkg));
|
|
6534
7191
|
const onSubgraphs = this.getDebouncedUpdateCallback(`${pkg}:subgraphs`, () => this.updateSubgraphsForPackage(pkg));
|
|
6535
7192
|
const onProcessors = this.getDebouncedUpdateCallback(`${pkg}:processors`, () => this.updateProcessorsForPackage(pkg));
|
|
7193
|
+
const onPieces = this.getDebouncedUpdateCallback(`${pkg}:pieces`, () => this.updatePiecesForPackage(pkg));
|
|
6536
7194
|
for (const loader of this.loaders) {
|
|
6537
7195
|
if (loader.onDocumentModelsChange) unsubs.push(loader.onDocumentModelsChange(pkg, onDocumentModels));
|
|
6538
7196
|
if (loader.onSubgraphsChange) unsubs.push(loader.onSubgraphsChange(pkg, onSubgraphs));
|
|
6539
7197
|
if (loader.onProcessorsChange) unsubs.push(loader.onProcessorsChange(pkg, onProcessors));
|
|
7198
|
+
if (loader.onPiecesChange) unsubs.push(loader.onPiecesChange(pkg, onPieces));
|
|
6540
7199
|
}
|
|
6541
7200
|
}
|
|
6542
7201
|
return () => {
|
|
@@ -6584,6 +7243,19 @@ var PackageManager = class {
|
|
|
6584
7243
|
this.processorMap = processorsMap;
|
|
6585
7244
|
this.eventEmitter.emit("processorsChange", processorsMap);
|
|
6586
7245
|
}
|
|
7246
|
+
updatePiecesMap(piecesMap) {
|
|
7247
|
+
const oldPackages = Array.from(this.piecesMap.keys());
|
|
7248
|
+
const newPackages = Array.from(piecesMap.keys());
|
|
7249
|
+
oldPackages.filter((pkg) => !newPackages.includes(pkg)).forEach((pkg) => {
|
|
7250
|
+
this.logger.info("Removed pieces from: @pkg", pkg);
|
|
7251
|
+
});
|
|
7252
|
+
this.piecesMap = piecesMap;
|
|
7253
|
+
this.eventEmitter.emit("piecesChange", piecesMap);
|
|
7254
|
+
}
|
|
7255
|
+
/** The pieces every loaded package ships, keyed by the package. */
|
|
7256
|
+
getPieces() {
|
|
7257
|
+
return new Map(this.piecesMap);
|
|
7258
|
+
}
|
|
6587
7259
|
/**
|
|
6588
7260
|
* Remove `pkg` from all package maps and emit the change events, tearing
|
|
6589
7261
|
* down everything the package registered (subgraphs, processors, document
|
|
@@ -6602,9 +7274,11 @@ var PackageManager = class {
|
|
|
6602
7274
|
this.upgradeManifestsMap = dropKey(this.upgradeManifestsMap);
|
|
6603
7275
|
this.subgraphsMap = dropKey(this.subgraphsMap);
|
|
6604
7276
|
this.processorMap = dropKey(this.processorMap);
|
|
7277
|
+
this.piecesMap = dropKey(this.piecesMap);
|
|
6605
7278
|
this.updatePackagesMap(this.docModelsMap);
|
|
6606
7279
|
this.updateSubgraphsMap(this.subgraphsMap);
|
|
6607
7280
|
this.updateProcessorsMap(this.processorMap);
|
|
7281
|
+
this.updatePiecesMap(this.piecesMap);
|
|
6608
7282
|
}
|
|
6609
7283
|
onDocumentModelsChange(handler) {
|
|
6610
7284
|
this.eventEmitter.on("documentModelsChange", handler);
|
|
@@ -6615,6 +7289,9 @@ var PackageManager = class {
|
|
|
6615
7289
|
onProcessorsChange(handler) {
|
|
6616
7290
|
this.eventEmitter.on("processorsChange", handler);
|
|
6617
7291
|
}
|
|
7292
|
+
onPiecesChange(handler) {
|
|
7293
|
+
this.eventEmitter.on("piecesChange", handler);
|
|
7294
|
+
}
|
|
6618
7295
|
};
|
|
6619
7296
|
//#endregion
|
|
6620
7297
|
//#region src/config.ts
|
|
@@ -6632,20 +7309,48 @@ async function createStartupAttachmentBackend({ db, env = process.env, s3Depende
|
|
|
6632
7309
|
}
|
|
6633
7310
|
//#endregion
|
|
6634
7311
|
//#region src/services/attachment-access.service.ts
|
|
7312
|
+
/** The scope an attachment's bytes belong to: the document's own domain state. */
|
|
7313
|
+
const ATTACHMENT_SCOPE = "global";
|
|
7314
|
+
/** The branch an attachment reference is resolved against. */
|
|
7315
|
+
const ATTACHMENT_BRANCH = "main";
|
|
6635
7316
|
const HASH_PATTERN = /^[a-f0-9]{64}$/;
|
|
6636
7317
|
/**
|
|
6637
7318
|
* Composes document authorization with the projected document/ref
|
|
6638
7319
|
* relationship. Order is fixed: validate ref, resolve the canonical document
|
|
6639
|
-
* id,
|
|
6640
|
-
*
|
|
6641
|
-
* attachment
|
|
7320
|
+
* id, decide the read, check the reactor's read gate serves the document, then
|
|
7321
|
+
* check the reference index and that the subject may read a scope whose
|
|
7322
|
+
* operations reference the attachment. A denied document never reaches the
|
|
7323
|
+
* reference reader, and the facade never touches attachment metadata, storage
|
|
7324
|
+
* backends, or presigners.
|
|
7325
|
+
*
|
|
7326
|
+
* The named document decides alone: an attachment several documents reference
|
|
7327
|
+
* is readable through each of them on that document's own terms.
|
|
7328
|
+
*
|
|
7329
|
+
* The read is decided by the document's own policy when this composition has a
|
|
7330
|
+
* model to enforce it with, and by the host's permission tables when it does
|
|
7331
|
+
* not. That is not a preference between two equivalent answers: a host running
|
|
7332
|
+
* document policies keeps no rows in the permission tables, so asking them
|
|
7333
|
+
* about such a document returns whatever the host-wide policy says — under
|
|
7334
|
+
* `OPEN`, `true`, for everyone. Serving bytes on that answer hands the file to
|
|
7335
|
+
* anyone who learns its hash, which the document's own state may well have told
|
|
7336
|
+
* them. Both are kept because they are the two halves of one rule: an
|
|
7337
|
+
* attachment is readable by whoever may read the document that references it,
|
|
7338
|
+
* and each composition can only express that in its own terms.
|
|
6642
7339
|
*/
|
|
6643
7340
|
var AttachmentAccessService = class {
|
|
6644
|
-
|
|
7341
|
+
resolveCanonicalId;
|
|
7342
|
+
authorization;
|
|
7343
|
+
references;
|
|
7344
|
+
projection;
|
|
7345
|
+
readGate;
|
|
7346
|
+
scopeGate;
|
|
7347
|
+
constructor(resolveCanonicalId, authorization, references, projection, readGate, scopeGate) {
|
|
6645
7348
|
this.resolveCanonicalId = resolveCanonicalId;
|
|
6646
7349
|
this.authorization = authorization;
|
|
6647
7350
|
this.references = references;
|
|
6648
7351
|
this.projection = projection;
|
|
7352
|
+
this.readGate = readGate;
|
|
7353
|
+
this.scopeGate = scopeGate;
|
|
6649
7354
|
}
|
|
6650
7355
|
async canReadAttachment(request) {
|
|
6651
7356
|
if (this.projection.status !== "available") return { kind: "projection-unavailable" };
|
|
@@ -6657,14 +7362,51 @@ var AttachmentAccessService = class {
|
|
|
6657
7362
|
} catch {
|
|
6658
7363
|
return { kind: "denied" };
|
|
6659
7364
|
}
|
|
6660
|
-
if (!await this.
|
|
6661
|
-
|
|
7365
|
+
if (!await this.canReadDocument(documentId, request)) return { kind: "denied" };
|
|
7366
|
+
const subject = {
|
|
7367
|
+
address: request.userAddress,
|
|
7368
|
+
key: request.appKey
|
|
7369
|
+
};
|
|
7370
|
+
let served;
|
|
7371
|
+
try {
|
|
7372
|
+
served = await this.readGate.isServed(documentId, { subject });
|
|
7373
|
+
} catch {
|
|
7374
|
+
return { kind: "denied" };
|
|
7375
|
+
}
|
|
7376
|
+
if (!served) return { kind: "denied" };
|
|
7377
|
+
const scopes = await this.references.referencingScopes(documentId, ref);
|
|
7378
|
+
if (scopes.length === 0) return { kind: "denied" };
|
|
7379
|
+
let held;
|
|
7380
|
+
try {
|
|
7381
|
+
held = (await this.readGate.get(documentId, {
|
|
7382
|
+
subject,
|
|
7383
|
+
scopes
|
|
7384
|
+
})).state;
|
|
7385
|
+
} catch {
|
|
7386
|
+
return { kind: "denied" };
|
|
7387
|
+
}
|
|
7388
|
+
if (!scopes.some((scope) => scope in held)) return { kind: "denied" };
|
|
6662
7389
|
return {
|
|
6663
7390
|
kind: "allowed",
|
|
6664
7391
|
documentId,
|
|
6665
7392
|
ref
|
|
6666
7393
|
};
|
|
6667
7394
|
}
|
|
7395
|
+
/**
|
|
7396
|
+
* Whether the caller may read the state the reference lives in.
|
|
7397
|
+
*
|
|
7398
|
+
* A failure here is rethrown rather than turned into a denial. A read side
|
|
7399
|
+
* that is down must not read as a policy: a denial nobody can distinguish
|
|
7400
|
+
* from a refusal is one nobody investigates, and the route answers 500 for a
|
|
7401
|
+
* reason.
|
|
7402
|
+
*/
|
|
7403
|
+
async canReadDocument(documentId, request) {
|
|
7404
|
+
if (!this.scopeGate) return this.authorization.canRead(documentId, request.userAddress);
|
|
7405
|
+
return (await this.scopeGate.scopePredicateById(documentId, {
|
|
7406
|
+
address: request.userAddress,
|
|
7407
|
+
key: request.appKey
|
|
7408
|
+
}, ATTACHMENT_BRANCH))(ATTACHMENT_SCOPE);
|
|
7409
|
+
}
|
|
6668
7410
|
};
|
|
6669
7411
|
/**
|
|
6670
7412
|
* Parses and canonicalizes a caller-supplied ref through the shared parser.
|
|
@@ -6710,14 +7452,42 @@ function normalizeAttachmentRef(value) {
|
|
|
6710
7452
|
* context has no caller, and is rejected. The server never composes this
|
|
6711
7453
|
* middleware without the auth middleware in the chain (see
|
|
6712
7454
|
* `assertRequireAuthenticatedCallerAllowed` in `server.ts`).
|
|
7455
|
+
*
|
|
7456
|
+
* `exemptPaths` names the mounted paths that stay reachable anonymously — a
|
|
7457
|
+
* deployment whose product has a flow that runs before sign-in, such as
|
|
7458
|
+
* previewing an invitation from its code, needs somewhere to serve it from.
|
|
7459
|
+
* Each entry is a hole in the floor, so the match is deliberately the
|
|
7460
|
+
* narrowest one that works: the request's pathname, compared in full. A
|
|
7461
|
+
* prefix rule would exempt `/graphql/public-admin` along with
|
|
7462
|
+
* `/graphql/public`, and an exempt surface nobody can enumerate by reading
|
|
7463
|
+
* the configuration is not one anybody audits.
|
|
6713
7464
|
*/
|
|
6714
|
-
function createRequireAuthFetchMiddleware() {
|
|
7465
|
+
function createRequireAuthFetchMiddleware(exemptPaths = []) {
|
|
7466
|
+
const exempt = new Set(exemptPaths.map(normalizeExemptPath));
|
|
6715
7467
|
return (next) => async (request) => {
|
|
6716
7468
|
if (request.method === "OPTIONS") return next(request);
|
|
7469
|
+
if (exempt.size > 0 && exempt.has(normalizeExemptPath(new URL(request.url).pathname))) return next(request);
|
|
6717
7470
|
if (getAuthContext(request)?.user === void 0) return new globalThis.Response(JSON.stringify({ error: "Authentication required" }), { status: 401 });
|
|
6718
7471
|
return next(request);
|
|
6719
7472
|
};
|
|
6720
7473
|
}
|
|
7474
|
+
/**
|
|
7475
|
+
* The form both sides of the comparison are reduced to: surrounding whitespace
|
|
7476
|
+
* gone, and one trailing slash gone, so `/graphql/public/` and
|
|
7477
|
+
* `/graphql/public` are the same path. A client that appends a slash is not
|
|
7478
|
+
* asking for a different route, and a configuration that carries one is not
|
|
7479
|
+
* asking for a different rule.
|
|
7480
|
+
*
|
|
7481
|
+
* Deliberately NOT normalized: case, percent-encoding, and a missing leading
|
|
7482
|
+
* slash. The first two would let two spellings of one path diverge from how
|
|
7483
|
+
* the router itself matches; the third is a configuration error, and
|
|
7484
|
+
* `assertRequireAuthenticatedCallerAllowed` refuses to boot on it rather than
|
|
7485
|
+
* leaving an exemption that silently never applies.
|
|
7486
|
+
*/
|
|
7487
|
+
function normalizeExemptPath(path) {
|
|
7488
|
+
const trimmed = path.trim();
|
|
7489
|
+
return trimmed.length > 1 && trimmed.endsWith("/") ? trimmed.slice(0, -1) : trimmed;
|
|
7490
|
+
}
|
|
6721
7491
|
//#endregion
|
|
6722
7492
|
//#region src/migrations/001_create_document_permissions.ts
|
|
6723
7493
|
var _001_create_document_permissions_exports = /* @__PURE__ */ __exportAll({
|
|
@@ -6992,13 +7762,11 @@ var AuthService = class {
|
|
|
6992
7762
|
return new Response(JSON.stringify({ error: "Authentication failed" }), { status: 401 });
|
|
6993
7763
|
}
|
|
6994
7764
|
}
|
|
7765
|
+
/** Throws only for a bearer that is present and unusable, as HTTP 401s it. */
|
|
6995
7766
|
async authenticateWebSocketConnection(connectionParams) {
|
|
6996
7767
|
if (!this.resolvesIdentity) return null;
|
|
6997
7768
|
const authHeader = connectionParams.authorization;
|
|
6998
|
-
if (!authHeader)
|
|
6999
|
-
if (!this.config.enabled) return null;
|
|
7000
|
-
throw new Error("Missing authorization in connection parameters");
|
|
7001
|
-
}
|
|
7769
|
+
if (!authHeader) return null;
|
|
7002
7770
|
const token = authHeader.split(" ")[1];
|
|
7003
7771
|
if (!token) throw new Error("Invalid authorization format");
|
|
7004
7772
|
const verified = await this.verifyToken(token);
|
|
@@ -7122,6 +7890,7 @@ async function createRenownCredentialVerifier(config = {}) {
|
|
|
7122
7890
|
* - Users can be granted permission to execute specific operations
|
|
7123
7891
|
*/
|
|
7124
7892
|
var DocumentPermissionService = class {
|
|
7893
|
+
db;
|
|
7125
7894
|
config;
|
|
7126
7895
|
constructor(db, config = { defaultProtection: false }) {
|
|
7127
7896
|
this.db = db;
|
|
@@ -7392,12 +8161,16 @@ function createGetParentIdsFn(reactorClient) {
|
|
|
7392
8161
|
* Authorization gate for the /mcp endpoint (AUTH_REVIEW S-C1). MCP tools have
|
|
7393
8162
|
* unrestricted reactor access, so access is limited to supreme admins when
|
|
7394
8163
|
* auth is enabled. OPEN policy is public; any other policy without an
|
|
7395
|
-
* AuthService fails closed.
|
|
8164
|
+
* AuthService fails closed. The tools read as the caller: anonymous under OPEN,
|
|
8165
|
+
* else the admin's address and the app key its token carries.
|
|
7396
8166
|
*/
|
|
7397
8167
|
function createMcpRequestAuthorizer(authService, authorizationService) {
|
|
7398
8168
|
return async (req) => {
|
|
7399
8169
|
if (!authService) {
|
|
7400
|
-
if (authorizationService.config.policy === AuthorizationPolicy.OPEN) return {
|
|
8170
|
+
if (authorizationService.config.policy === AuthorizationPolicy.OPEN) return {
|
|
8171
|
+
authorized: true,
|
|
8172
|
+
subject: {}
|
|
8173
|
+
};
|
|
7401
8174
|
return {
|
|
7402
8175
|
authorized: false,
|
|
7403
8176
|
status: 401,
|
|
@@ -7420,10 +8193,39 @@ function createMcpRequestAuthorizer(authService, authorizationService) {
|
|
|
7420
8193
|
status: 403,
|
|
7421
8194
|
message: "Forbidden: MCP access requires an administrator"
|
|
7422
8195
|
};
|
|
7423
|
-
return {
|
|
8196
|
+
return {
|
|
8197
|
+
authorized: true,
|
|
8198
|
+
subject: {
|
|
8199
|
+
address: context.user.address,
|
|
8200
|
+
key: context.user.appKey
|
|
8201
|
+
}
|
|
8202
|
+
};
|
|
7424
8203
|
};
|
|
7425
8204
|
}
|
|
7426
8205
|
//#endregion
|
|
8206
|
+
//#region src/services/sync-serving-gate.ts
|
|
8207
|
+
/**
|
|
8208
|
+
* The gate sync serving evaluates a document's own policy through, deciding
|
|
8209
|
+
* what the reactor client's read gate decides at the same flag level.
|
|
8210
|
+
*
|
|
8211
|
+
* Under `authEnforcement` it is built here rather than taken off the client
|
|
8212
|
+
* because it also carries the host's closes-by-default setting, which withholds
|
|
8213
|
+
* the domain scopes of a document nobody has policied yet. That answer belongs
|
|
8214
|
+
* to serving alone -- replay must keep reading an uninitialized document in
|
|
8215
|
+
* full -- so the two gates are separate objects over the same model.
|
|
8216
|
+
*
|
|
8217
|
+
* Below it the registered model ignores the auth scope, so the client reads
|
|
8218
|
+
* through the policy alone, and so does serving: a policy written below the
|
|
8219
|
+
* flag still withholds, where serving ungated would sync to a peer what `get`
|
|
8220
|
+
* refuses it.
|
|
8221
|
+
*/
|
|
8222
|
+
function buildSyncServingGate(reactorModule, authorizationConfig, logger) {
|
|
8223
|
+
if (!reactorModule) return;
|
|
8224
|
+
const model = readDecisionModel(reactorModule.featureFlags, reactorModule.documentModelRegistry);
|
|
8225
|
+
if (!model) return new SyncScopeGate(new BareReadGate(), reactorModule.documentView, logger);
|
|
8226
|
+
return new SyncScopeGate(new ModelReadGate(model, reactorModule.documentView, reactorModule.featureFlags.authGroups, reactorModule.operationIndex, logger, { withholdUninitialized: authorizationConfig.defaultProtection }), reactorModule.documentView, logger);
|
|
8227
|
+
}
|
|
8228
|
+
//#endregion
|
|
7427
8229
|
//#region src/services/renown-config.ts
|
|
7428
8230
|
/** Reading credentials from this reactor's own read model needs a host-supplied
|
|
7429
8231
|
* verifier: core has no idea which subgraph serves that read model. */
|
|
@@ -7605,10 +8407,36 @@ function assertSkipCredentialVerificationAllowed(resolvesCallerIdentity, skipCre
|
|
|
7605
8407
|
* require-authenticated-caller middleware would reject every caller —
|
|
7606
8408
|
* authenticated ones included. Refuse to boot rather than run broken.
|
|
7607
8409
|
*/
|
|
7608
|
-
function assertRequireAuthenticatedCallerAllowed(requireAuthenticatedCaller, resolvesCallerIdentity) {
|
|
8410
|
+
function assertRequireAuthenticatedCallerAllowed(requireAuthenticatedCaller, resolvesCallerIdentity, exemptPaths = []) {
|
|
8411
|
+
/**
|
|
8412
|
+
* An exemption is only ever a hole in this floor, so configuring one while
|
|
8413
|
+
* the floor is off is not a harmless no-op: it reads, to anyone auditing the
|
|
8414
|
+
* configuration, as a surface that was deliberately opened — and therefore
|
|
8415
|
+
* as a floor that exists. Refuse rather than let the two drift.
|
|
8416
|
+
*/
|
|
8417
|
+
if (!requireAuthenticatedCaller && exemptPaths.length > 0) throw new Error("REQUIRE_AUTHENTICATED_CALLER_EXEMPT_PATHS is set but refused: REQUIRE_AUTHENTICATED_CALLER is off, so nothing is being exempted from anything and the configuration claims a protection the server is not applying. Enable the floor, or drop the exemptions.");
|
|
8418
|
+
/**
|
|
8419
|
+
* A path that cannot match anything is worse than no path: the operator
|
|
8420
|
+
* believes the flow is reachable, and finds out when the first user cannot
|
|
8421
|
+
* claim an invitation. `URL.pathname` is always absolute, so an entry that
|
|
8422
|
+
* does not start with `/` never matches, whatever the router does.
|
|
8423
|
+
*/
|
|
8424
|
+
const relative = exemptPaths.filter((path) => !path.trim().startsWith("/"));
|
|
8425
|
+
if (relative.length > 0) throw new Error(`REQUIRE_AUTHENTICATED_CALLER_EXEMPT_PATHS contains ${relative.map((path) => `"${path}"`).join(", ")}, which cannot match: an exempt path is compared against the request's pathname and must start with '/' (for example '/graphql/public').`);
|
|
7609
8426
|
if (!requireAuthenticatedCaller || resolvesCallerIdentity) return;
|
|
7610
8427
|
throw new Error("REQUIRE_AUTHENTICATED_CALLER is set but refused: it rejects every request without a resolved caller, and with neither AUTH_ENABLED nor RESOLVE_CALLER_IDENTITY the server never reads a bearer, so it would reject every caller, including authenticated ones. Enable identity resolution first (RESOLVE_CALLER_IDENTITY=true or AUTH_ENABLED=true).");
|
|
7611
8428
|
}
|
|
8429
|
+
/**
|
|
8430
|
+
* Deciding an attachment read by the document's policy needs a policy model to
|
|
8431
|
+
* decide with, and that is what auth enforcement supplies. Without one there is
|
|
8432
|
+
* nothing to consult, so the setting would silently leave the permission tables
|
|
8433
|
+
* in charge — configuration that describes a protection the server is not
|
|
8434
|
+
* applying, which is worse than no configuration at all. Refuse instead.
|
|
8435
|
+
*/
|
|
8436
|
+
function assertAttachmentPolicyReadsAllowed(attachmentReadsFollowDocumentPolicy, hasDecisionModel) {
|
|
8437
|
+
if (!attachmentReadsFollowDocumentPolicy || hasDecisionModel) return;
|
|
8438
|
+
throw new Error("ATTACHMENT_READS_FOLLOW_DOCUMENT_POLICY is set but refused: deciding an attachment read by the referencing document's policy requires a policy model to evaluate, and this composition has none, so the host permission tables would keep deciding while the configuration says otherwise. Enable auth enforcement first (REACTOR_AUTH_ENFORCEMENT=true).");
|
|
8439
|
+
}
|
|
7612
8440
|
function createReadinessGate() {
|
|
7613
8441
|
let ready = false;
|
|
7614
8442
|
return {
|
|
@@ -7663,27 +8491,6 @@ function makeDbClosers(knexInstance, pglite) {
|
|
|
7663
8491
|
return closers;
|
|
7664
8492
|
}
|
|
7665
8493
|
/**
|
|
7666
|
-
* The gate sync serving evaluates a document's own policy through, or undefined
|
|
7667
|
-
* when there is none to evaluate.
|
|
7668
|
-
*
|
|
7669
|
-
* It is built here rather than taken off the reactor client because it is not
|
|
7670
|
-
* the same gate reads use: it carries the host's closes-by-default setting,
|
|
7671
|
-
* which withholds the domain scopes of a document nobody has policied yet. That
|
|
7672
|
-
* answer belongs to serving alone -- replay must keep reading an uninitialized
|
|
7673
|
-
* document in full -- so the two gates are deliberately separate objects over
|
|
7674
|
-
* the same model.
|
|
7675
|
-
*
|
|
7676
|
-
* Undefined below `authEnforcement`, where the registered model ignores the auth
|
|
7677
|
-
* scope: gating through it would serve every domain scope of a policied document
|
|
7678
|
-
* to anyone, which is worse than not gating at all.
|
|
7679
|
-
*/
|
|
7680
|
-
function buildSyncServingGate(reactorModule, authorizationConfig, logger) {
|
|
7681
|
-
if (!reactorModule) return;
|
|
7682
|
-
const model = readDecisionModel(reactorModule.featureFlags, reactorModule.documentModelRegistry);
|
|
7683
|
-
if (!model) return;
|
|
7684
|
-
return new SyncScopeGate(new ModelReadGate(model, reactorModule.documentView, reactorModule.featureFlags.authGroups, reactorModule.operationIndex, logger, { withholdUninitialized: authorizationConfig.defaultProtection }), reactorModule.documentView, logger);
|
|
7685
|
-
}
|
|
7686
|
-
/**
|
|
7687
8494
|
* Resolves the gateway adapter type from the `GATEWAY_ADAPTER` env var.
|
|
7688
8495
|
* Defaults to "apollo" (the federation gateway, production behavior).
|
|
7689
8496
|
* "stitching" selects the in-process graphql-tools merge gateway (#1565
|
|
@@ -7798,6 +8605,8 @@ async function _setupCommonInfrastructure(options) {
|
|
|
7798
8605
|
let authEnabled = false;
|
|
7799
8606
|
let configuredResolveIdentity;
|
|
7800
8607
|
let configuredRequireAuth;
|
|
8608
|
+
let configuredExemptPaths;
|
|
8609
|
+
let configuredAttachmentPolicyReads;
|
|
7801
8610
|
let configuredRenown;
|
|
7802
8611
|
if (options.configFile) {
|
|
7803
8612
|
const config = getConfig(options.configFile);
|
|
@@ -7809,8 +8618,10 @@ async function _setupCommonInfrastructure(options) {
|
|
|
7809
8618
|
authEnabled = options.auth.enabled;
|
|
7810
8619
|
configuredResolveIdentity = options.auth.resolveIdentity;
|
|
7811
8620
|
configuredRequireAuth = options.auth.requireAuthenticatedCaller;
|
|
8621
|
+
configuredExemptPaths = options.auth.requireAuthenticatedCallerExemptPaths;
|
|
8622
|
+
configuredAttachmentPolicyReads = options.auth.attachmentReadsFollowDocumentPolicy;
|
|
7812
8623
|
}
|
|
7813
|
-
const { AUTH_ENABLED, RESOLVE_CALLER_IDENTITY, REQUIRE_AUTHENTICATED_CALLER, ADMINS, DEFAULT_PROTECTION, DOCUMENT_PERMISSIONS_ENABLED, SKIP_CREDENTIAL_VERIFICATION, CREDENTIAL_VERIFICATION_CACHE_TTL_MS } = process.env;
|
|
8624
|
+
const { AUTH_ENABLED, RESOLVE_CALLER_IDENTITY, REQUIRE_AUTHENTICATED_CALLER, REQUIRE_AUTHENTICATED_CALLER_EXEMPT_PATHS, ATTACHMENT_READS_FOLLOW_DOCUMENT_POLICY, ADMINS, DEFAULT_PROTECTION, DOCUMENT_PERMISSIONS_ENABLED, SKIP_CREDENTIAL_VERIFICATION, CREDENTIAL_VERIFICATION_CACHE_TTL_MS } = process.env;
|
|
7814
8625
|
if (AUTH_ENABLED !== void 0) authEnabled = AUTH_ENABLED === "true";
|
|
7815
8626
|
/**
|
|
7816
8627
|
* Whether the auth middleware reads the bearer and populates `ctx.user`,
|
|
@@ -7841,6 +8652,30 @@ async function _setupCommonInfrastructure(options) {
|
|
|
7841
8652
|
*/
|
|
7842
8653
|
let requireAuthenticatedCaller = configuredRequireAuth ?? false;
|
|
7843
8654
|
if (REQUIRE_AUTHENTICATED_CALLER !== void 0) requireAuthenticatedCaller = REQUIRE_AUTHENTICATED_CALLER === "true";
|
|
8655
|
+
/**
|
|
8656
|
+
* The paths the floor above does not apply to. Comma-separated, like
|
|
8657
|
+
* `ADMINS`, because a deployment configures this the same way it configures
|
|
8658
|
+
* everything else: one variable, one line, however many values it has.
|
|
8659
|
+
*
|
|
8660
|
+
* Empty entries are dropped rather than refused, so a trailing comma or a
|
|
8661
|
+
* value split across lines in a manifest is not a boot failure; a value that
|
|
8662
|
+
* cannot ever match is refused, in
|
|
8663
|
+
* {@link assertRequireAuthenticatedCallerAllowed}.
|
|
8664
|
+
*/
|
|
8665
|
+
let requireAuthenticatedCallerExemptPaths = configuredExemptPaths ?? [];
|
|
8666
|
+
if (REQUIRE_AUTHENTICATED_CALLER_EXEMPT_PATHS !== void 0) requireAuthenticatedCallerExemptPaths = REQUIRE_AUTHENTICATED_CALLER_EXEMPT_PATHS.split(",").map((path) => path.trim()).filter((path) => path.length > 0);
|
|
8667
|
+
/**
|
|
8668
|
+
* Whether an attachment read is decided by the referencing document's own
|
|
8669
|
+
* policy rather than by the host's permission tables.
|
|
8670
|
+
*
|
|
8671
|
+
* Off by default, and a deployment's choice rather than something derived
|
|
8672
|
+
* from the flags around it. Which model governs those bytes is configuration
|
|
8673
|
+
* in the same sense the storage backend behind them is: a host that has said
|
|
8674
|
+
* nothing keeps exactly the behaviour it has, and one that wants the change
|
|
8675
|
+
* asks for it and can take it back without disturbing anything else.
|
|
8676
|
+
*/
|
|
8677
|
+
let attachmentReadsFollowDocumentPolicy = configuredAttachmentPolicyReads ?? false;
|
|
8678
|
+
if (ATTACHMENT_READS_FOLLOW_DOCUMENT_POLICY !== void 0) attachmentReadsFollowDocumentPolicy = ATTACHMENT_READS_FOLLOW_DOCUMENT_POLICY === "true";
|
|
7844
8679
|
if (ADMINS !== void 0) admins = ADMINS.split(",").map((a) => a.toLowerCase());
|
|
7845
8680
|
let defaultProtection = false;
|
|
7846
8681
|
if (DEFAULT_PROTECTION !== void 0) defaultProtection = DEFAULT_PROTECTION.toLowerCase() === "true";
|
|
@@ -7855,7 +8690,7 @@ async function _setupCommonInfrastructure(options) {
|
|
|
7855
8690
|
const documentPermissionsRequested = options.documentPermissionService !== void 0 || DOCUMENT_PERMISSIONS_ENABLED === "true";
|
|
7856
8691
|
assertAuthRequiredForDocumentPermissions(authEnabled, documentPermissionsRequested);
|
|
7857
8692
|
assertSkipCredentialVerificationAllowed(resolveCallerIdentity, skipCredentialVerification, process.env);
|
|
7858
|
-
assertRequireAuthenticatedCallerAllowed(requireAuthenticatedCaller, resolveCallerIdentity);
|
|
8693
|
+
assertRequireAuthenticatedCallerAllowed(requireAuthenticatedCaller, resolveCallerIdentity, requireAuthenticatedCallerExemptPaths);
|
|
7859
8694
|
if (authEnabled && skipCredentialVerification) logger.warn("SECURITY: SKIP_CREDENTIAL_VERIFICATION is enabled — Renown credential verification is disabled and a bearer token's claimed address is NOT cryptographically bound to its signing key. Identity is unverifiable; use only in development or test.");
|
|
7860
8695
|
httpAdapter.getRoute("/health", () => new Response("OK", { status: 200 }));
|
|
7861
8696
|
const readiness = createReadinessGate();
|
|
@@ -7891,9 +8726,9 @@ async function _setupCommonInfrastructure(options) {
|
|
|
7891
8726
|
});
|
|
7892
8727
|
authFetchMiddleware = createAuthFetchMiddleware(authService);
|
|
7893
8728
|
if (requireAuthenticatedCaller) {
|
|
7894
|
-
requireAuthFetchMiddleware = createRequireAuthFetchMiddleware();
|
|
7895
|
-
logger.info("Require-authenticated-caller middleware enabled: anonymous callers are rejected with a 401 before any subgraph");
|
|
7896
|
-
}
|
|
8729
|
+
requireAuthFetchMiddleware = createRequireAuthFetchMiddleware(requireAuthenticatedCallerExemptPaths);
|
|
8730
|
+
logger.info(requireAuthenticatedCallerExemptPaths.length > 0 ? `Require-authenticated-caller middleware enabled: anonymous callers are rejected with a 401 before any subgraph, except on ${requireAuthenticatedCallerExemptPaths.join(", ")}` : "Require-authenticated-caller middleware enabled: anonymous callers are rejected with a 401 before any subgraph");
|
|
8731
|
+
} else logger.warn("Anonymous callers are admitted on every transport, including WebSocket subscriptions: REQUIRE_AUTHENTICATED_CALLER is not set. The authorization policy gates the reactor's own documents, but a package-provided subgraph authorizes nothing on its own. Set REQUIRE_AUTHENTICATED_CALLER=true to refuse anonymous callers before any subgraph sees them.");
|
|
7897
8732
|
}
|
|
7898
8733
|
const dbClosers = [];
|
|
7899
8734
|
const { relationalDb, analyticsStore, closers: analyticsClosers } = await initializeDatabaseAndAnalytics(options.dbPath, options.pgliteFactory);
|
|
@@ -7917,7 +8752,7 @@ async function _setupCommonInfrastructure(options) {
|
|
|
7917
8752
|
await mkdir(attachmentStoragePath, { recursive: true });
|
|
7918
8753
|
const { db: attachmentDb, knex: attachmentKnex, pglite: attachmentPglite } = getDbClient(options.dbPath, options.pgliteFactory);
|
|
7919
8754
|
dbClosers.push(...makeDbClosers(attachmentKnex, attachmentPglite));
|
|
7920
|
-
const ATTACHMENT_SWEEP_INTERVAL_MS =
|
|
8755
|
+
const ATTACHMENT_SWEEP_INTERVAL_MS = 36e5;
|
|
7921
8756
|
const attachmentBackend = await createStartupAttachmentBackend({ db: attachmentDb });
|
|
7922
8757
|
const attachmentBuilder = new AttachmentBuilder(attachmentDb, attachmentStoragePath).withReservationSweepMs(ATTACHMENT_SWEEP_INTERVAL_MS);
|
|
7923
8758
|
if (attachmentBackend) attachmentBuilder.withBackend(attachmentBackend);
|
|
@@ -7962,13 +8797,14 @@ async function _setupCommonInfrastructure(options) {
|
|
|
7962
8797
|
attachmentReferenceIndex,
|
|
7963
8798
|
packages,
|
|
7964
8799
|
dbClosers,
|
|
7965
|
-
readiness
|
|
8800
|
+
readiness,
|
|
8801
|
+
attachmentReadsFollowDocumentPolicy
|
|
7966
8802
|
};
|
|
7967
8803
|
}
|
|
7968
8804
|
/**
|
|
7969
8805
|
* Private helper function containing common setup logic for API initialization
|
|
7970
8806
|
*/
|
|
7971
|
-
async function _setupAPI(reactorClient, syncManager, reactorProcessorManager, httpAdapter, authFetchMiddleware, requireAuthFetchMiddleware, authService, port, packages, relationalDb, analyticsStore, documentPermissionService, processors, subgraphs, options, processorApp, readModels, attachments, attachmentReferenceIndex, attachmentReferenceProjection, authorizationConfig, documentModelRegistry, dbClosers = [], reactorDriveClient, syncServingGate, httpRoutes) {
|
|
8807
|
+
async function _setupAPI(reactorClient, syncManager, reactorProcessorManager, httpAdapter, authFetchMiddleware, requireAuthFetchMiddleware, authService, port, packages, relationalDb, analyticsStore, documentPermissionService, processors, subgraphs, options, processorApp, readModels, attachments, attachmentReferenceIndex, attachmentReferenceProjection, authorizationConfig, documentModelRegistry, dbClosers = [], reactorDriveClient, syncServingGate, httpRoutes, attachmentReadsFollowDocumentPolicy = false) {
|
|
7972
8808
|
const hostModuleBase = {
|
|
7973
8809
|
...createReactorHostModuleBase({
|
|
7974
8810
|
client: reactorClient,
|
|
@@ -8010,7 +8846,8 @@ async function _setupAPI(reactorClient, syncManager, reactorProcessorManager, ht
|
|
|
8010
8846
|
const { httpServer, wsServer } = await startServer(httpAdapter, port, options.https, logger);
|
|
8011
8847
|
const authorizationService = createAuthorizationService(authorizationConfig, documentPermissionService, createGetParentIdsFn(reactorClient));
|
|
8012
8848
|
logger.info(`Authorization service initialized (policy: ${authorizationConfig.policy})`);
|
|
8013
|
-
|
|
8849
|
+
assertAttachmentPolicyReadsAllowed(attachmentReadsFollowDocumentPolicy, syncServingGate !== void 0);
|
|
8850
|
+
const attachmentAccess = new AttachmentAccessService(createCanonicalDocumentIdResolver(reactorClient), authorizationService, attachmentReferenceIndex.store, attachmentReferenceProjection, reactorClient, attachmentReadsFollowDocumentPolicy ? syncServingGate : void 0);
|
|
8014
8851
|
const coreSubgraphs = DefaultCoreSubgraphs.slice();
|
|
8015
8852
|
coreSubgraphs.push(ReactorSubgraph);
|
|
8016
8853
|
if (documentPermissionService) {
|
|
@@ -8047,6 +8884,9 @@ async function _setupAPI(reactorClient, syncManager, reactorProcessorManager, ht
|
|
|
8047
8884
|
attachmentReferenceIndex,
|
|
8048
8885
|
attachmentAccess,
|
|
8049
8886
|
authService,
|
|
8887
|
+
requireAuthenticatedCaller: requireAuthFetchMiddleware !== void 0,
|
|
8888
|
+
authorizationService,
|
|
8889
|
+
relationalDb,
|
|
8050
8890
|
dispose
|
|
8051
8891
|
};
|
|
8052
8892
|
}
|
|
@@ -8093,7 +8933,7 @@ function buildApiDispose(args) {
|
|
|
8093
8933
|
};
|
|
8094
8934
|
}
|
|
8095
8935
|
async function initializeAndStartAPI(clientInitializer, options, processorApp) {
|
|
8096
|
-
const { port, httpAdapter, httpRoutes, authFetchMiddleware, requireAuthFetchMiddleware, authService, relationalDb, analyticsStore, documentPermissionService, authorizationConfig, attachments, attachmentReferenceIndex, packages, dbClosers, readiness } = await _setupCommonInfrastructure(options);
|
|
8936
|
+
const { port, httpAdapter, httpRoutes, authFetchMiddleware, requireAuthFetchMiddleware, authService, relationalDb, analyticsStore, documentPermissionService, authorizationConfig, attachments, attachmentReferenceIndex, packages, dbClosers, readiness, attachmentReadsFollowDocumentPolicy } = await _setupCommonInfrastructure(options);
|
|
8097
8937
|
const { documentModels, upgradeManifests, processors, subgraphs } = await packages.init();
|
|
8098
8938
|
const { module: reactorClientModule, reactorDriveClient, attachmentReferenceProjection = {
|
|
8099
8939
|
status: "unavailable",
|
|
@@ -8110,7 +8950,7 @@ async function initializeAndStartAPI(clientInitializer, options, processorApp) {
|
|
|
8110
8950
|
const documentModelRegistry = reactorClientModule.reactorModule?.documentModelRegistry;
|
|
8111
8951
|
if (!documentModelRegistry) throw new Error("DocumentModelRegistry not available from InProcessReactorClientModule");
|
|
8112
8952
|
return {
|
|
8113
|
-
...await _setupAPI(reactorClient, syncManager, reactorProcessorManager, httpAdapter, authFetchMiddleware, requireAuthFetchMiddleware, authService, port, packages, relationalDb, analyticsStore, documentPermissionService, processors, subgraphs, options, processorApp, (reactorClientModule.reactorModule?.readModelCoordinator)?.readModels ?? [], attachments, attachmentReferenceIndex, attachmentReferenceProjection, authorizationConfig, documentModelRegistry, dbClosers, reactorDriveClient, buildSyncServingGate(reactorClientModule.reactorModule, authorizationConfig, options.logger ?? defaultLogger), httpRoutes),
|
|
8953
|
+
...await _setupAPI(reactorClient, syncManager, reactorProcessorManager, httpAdapter, authFetchMiddleware, requireAuthFetchMiddleware, authService, port, packages, relationalDb, analyticsStore, documentPermissionService, processors, subgraphs, options, processorApp, (reactorClientModule.reactorModule?.readModelCoordinator)?.readModels ?? [], attachments, attachmentReferenceIndex, attachmentReferenceProjection, authorizationConfig, documentModelRegistry, dbClosers, reactorDriveClient, buildSyncServingGate(reactorClientModule.reactorModule, authorizationConfig, options.logger ?? defaultLogger), httpRoutes, attachmentReadsFollowDocumentPolicy),
|
|
8114
8954
|
client: reactorClient,
|
|
8115
8955
|
syncManager,
|
|
8116
8956
|
documentModelRegistry,
|
|
@@ -8243,7 +9083,7 @@ var PackageManagementService = class {
|
|
|
8243
9083
|
}
|
|
8244
9084
|
};
|
|
8245
9085
|
//#endregion
|
|
8246
|
-
export { ADMIN_USERS, ActionCandidateInputSchema, ActionContextInputSchema, ActionInputSchema, AddRelationshipDocument, AnalyticsSubgraph, AttachmentAccessService, AuthDecision, AuthDecisionSchema, AuthService, AuthSubgraph, AuthorizationPolicy, AuthorizedDocumentHandle, BaseSubgraph, CORE_PACKAGE_NAME, CanonicalDocumentIdResolutionError, ChannelMetaInputSchema, CreateDocumentDocument, CreateEmptyDocumentDocument, DeleteDocumentDocument, DeleteDocumentsDocument, DocumentChangeType, DocumentChangeTypeSchema, DocumentChangesDocument, DocumentOperationsFilterInputSchema, DocumentPermissionService, EvaluateActionsDocument, FindDocumentsDocument, GetDocumentDocument, GetDocumentIncomingRelationshipsDocument, GetDocumentModelsDocument, GetDocumentOperationsDocument, GetDocumentOutgoingRelationshipsDocument, GetDocumentWithOperationsDocument, GetJobStatusDocument, GraphQLManager, HttpDocumentModelLoader, HttpPackageLoader, HttpRouteService, ImportPackageLoader, InMemoryPackageStorage, InvalidNamespaceError, JobChangesDocument, MemoryWebhookStore, MoveRelationshipDocument, MutateDocumentAsyncDocument, MutateDocumentDocument, OperationContextInputSchema, OperationInputSchema, OperationWithContextInputSchema, OperationsFilterInputSchema, PACKAGE_ROUTE_SEGMENT, PGLITE_UTC_PARSERS, PackageManagementService, PackageManager, PackagesSubgraph, PagingInputSchema, PhDocumentFieldsFragmentDoc, PollSyncEnvelopesDocument, PropagationMode, PropagationModeSchema, PushSyncEnvelopesDocument, ReactorSignerAppInputSchema, ReactorSignerInputSchema, ReactorSignerUserInputSchema, ReactorSubgraph, RelationalWebhookStore, RemoteCursorInputSchema, RemoteFilterInputSchema, RemoveRelationshipDocument, RenameDocumentDocument, SearchFilterInputSchema, SetPreferredEditorDocument, SyncEnvelopeInputSchema, SyncEnvelopeType, SyncEnvelopeTypeSchema, SystemSubgraph, TouchChannelDocument, TouchChannelInputSchema, ViewFilterInputSchema, WEBHOOK_SEGMENT, WebhookService, assertAuthRequiredForDocumentPermissions, assertCredentialVerifierForSource, assertHostMountPath, assertRequireAuthenticatedCallerAllowed, assertSkipCredentialVerificationAllowed, buildGraphQlDocument, buildGraphQlDriveDocument, buildGraphqlOperation, buildGraphqlOperations, buildSubgraphSchemaModule, createAuthFetchMiddleware, createCanonicalDocumentIdResolver, createGatewayAdapter, createHttpAdapter, createMergedSchema, createReactorGraphQLClient, createRenownCredentialVerifier, createSchema, decodeExplorerUrlState, definedNonNullAnySchema, driveIdFromUrl, extractSubgraphsFromModule, generateDocumentModelSchema, getAuthContext, getDbClient, getDocumentModelSchemaName, getDocumentModelTypeDefs, getExplorerPrefix, getGitHash, getGitUrl, getSdk, getUniqueDocumentModels, getUniqueUpgradeManifests, getVersion, initAnalyticsStoreSql, initializeAndStartAPI, isDefinedNonNullAny, isExpectedLoaderMiss, isSubgraphClass, namespacePath, namespaceSegments, newWebhookToken, parseDriveUrl, parseWebhookBody, redactHeaders, renderGraphqlPlayground, resolvePackageName, resolveRenownConfig, verifyWebhook };
|
|
9086
|
+
export { ADMIN_USERS, ActionCandidateInputSchema, ActionContextInputSchema, ActionInputSchema, AddRelationshipDocument, AnalyticsSubgraph, AttachmentAccessService, AuthDecision, AuthDecisionSchema, AuthEvaluationUnsupportedError, AuthService, AuthSubgraph, AuthenticationRequiredError, AuthorizationPolicy, AuthorizedDocumentHandle, BaseSubgraph, CORE_PACKAGE_NAME, CanonicalDocumentIdResolutionError, ChannelMetaInputSchema, CreateDocumentDocument, CreateEmptyDocumentDocument, DeleteDocumentDocument, DeleteDocumentsDocument, DocumentChangeType, DocumentChangeTypeSchema, DocumentChangesDocument, DocumentOperationsFilterInputSchema, DocumentPermissionService, DocumentRelationshipFieldsFragmentDoc, EvaluateActionsDocument, FindDocumentsDocument, ForbiddenError, GetDocumentDocument, GetDocumentIncomingRelationshipEdgesDocument, GetDocumentIncomingRelationshipsDocument, GetDocumentModelsDocument, GetDocumentOperationsDocument, GetDocumentOutgoingRelationshipEdgesDocument, GetDocumentOutgoingRelationshipsDocument, GetDocumentWithOperationsDocument, GetJobStatusDocument, GraphQLManager, HttpDocumentModelLoader, HttpPackageLoader, HttpRouteService, ImportPackageLoader, InMemoryPackageStorage, InvalidNamespaceError, JobChangesDocument, MalformedStoredOperationError, MemoryWebhookStore, MoveRelationshipDocument, MutateDocumentAsyncDocument, MutateDocumentDocument, OperationContextInputSchema, OperationInputSchema, OperationWithContextInputSchema, OperationsFilterInputSchema, PACKAGE_ROUTE_SEGMENT, PGLITE_UTC_PARSERS, PackageManagementService, PackageManager, PackagesSubgraph, PagingInputSchema, PhDocumentFieldsFragmentDoc, PollSyncEnvelopesDocument, PropagationMode, PropagationModeSchema, PushSyncEnvelopesDocument, ReactorSignerAppInputSchema, ReactorSignerInputSchema, ReactorSignerUserInputSchema, ReactorSubgraph, RelationalWebhookStore, RemoteCursorInputSchema, RemoteFilterInputSchema, RemoveRelationshipDocument, RenameDocumentDocument, SearchFilterInputSchema, SetPreferredEditorDocument, SyncEnvelopeInputSchema, SyncEnvelopeType, SyncEnvelopeTypeSchema, SystemSubgraph, TouchChannelDocument, TouchChannelInputSchema, UpdateRelationshipDocument, ViewFilterInputSchema, WEBHOOK_SEGMENT, WS_AUTH_CLOSE_REASONS, WS_CLOSE_REASON_AUTHENTICATION_REQUIRED, WS_CLOSE_REASON_BEARER_REJECTED, WebhookService, assertAttachmentPolicyReadsAllowed, assertAuthRequiredForDocumentPermissions, assertCredentialVerifierForSource, assertHostMountPath, assertRequireAuthenticatedCallerAllowed, assertSkipCredentialVerificationAllowed, buildGraphQlDocument, buildGraphQlDriveDocument, buildGraphqlOperation, buildGraphqlOperations, buildSubgraphSchemaModule, callerSubject, createAuthFetchMiddleware, createCanonicalDocumentIdResolver, createGatewayAdapter, createHttpAdapter, createMergedSchema, createReactorGraphQLClient, createRenownCredentialVerifier, createSchema, createWsAuthHandlers, decodeExplorerUrlState, definedNonNullAnySchema, driveIdFromUrl, extractSubgraphsFromModule, generateDocumentModelSchema, getAuthContext, getDbClient, getDocumentModelSchemaName, getDocumentModelTypeDefs, getExplorerPrefix, getGitHash, getGitUrl, getSdk, getUniqueDocumentModels, getUniqueUpgradeManifests, getVersion, initAnalyticsStoreSql, initializeAndStartAPI, isDefinedNonNullAny, isExpectedLoaderMiss, isSubgraphClass, namespacePath, namespaceSegments, newWebhookToken, parseDriveUrl, parseWebhookBody, piecesBaseUrl, redactHeaders, renderGraphqlPlayground, resolvePackageName, resolveRenownConfig, verifyWebhook };
|
|
8247
9087
|
|
|
8248
9088
|
//# sourceMappingURL=index.mjs.map
|
|
8249
|
-
//# debugId=
|
|
9089
|
+
//# debugId=5c9c63cc-7da0-5a74-ae18-1418ddd6118f
|