@powerhousedao/reactor-api 6.2.3-dev.3 → 6.2.3-dev.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/{adapter-gateway-apollo-Cn9E96GF.mjs → adapter-gateway-apollo-B54G_cXT.mjs} +18 -17
  2. package/dist/adapter-gateway-apollo-B54G_cXT.mjs.map +1 -0
  3. package/dist/{adapter-gateway-apollo-C_RdohOv.mjs → adapter-gateway-apollo-D2DNlPY0.mjs} +3 -3
  4. package/dist/{adapter-gateway-mercurius-CjaUcMgX.mjs → adapter-gateway-mercurius-rbKlwF9m.mjs} +7 -6
  5. package/dist/adapter-gateway-mercurius-rbKlwF9m.mjs.map +1 -0
  6. package/dist/{adapter-gateway-stitching-HmWjRMhm.mjs → adapter-gateway-stitching-Dzv10u8D.mjs} +18 -14
  7. package/dist/adapter-gateway-stitching-Dzv10u8D.mjs.map +1 -0
  8. package/dist/{adapter-http-express-B39rPhqH.mjs → adapter-http-express-CWqziB1p.mjs} +7 -7
  9. package/dist/adapter-http-express-CWqziB1p.mjs.map +1 -0
  10. package/dist/{adapter-http-fastify-BwXO6rqE.mjs → adapter-http-fastify-CYZQ6FFv.mjs} +10 -9
  11. package/dist/adapter-http-fastify-CYZQ6FFv.mjs.map +1 -0
  12. package/dist/index.d.mts +863 -453
  13. package/dist/index.d.mts.map +1 -1
  14. package/dist/index.mjs +1000 -258
  15. package/dist/index.mjs.map +1 -1
  16. package/dist/{utils-XWVecaNn.mjs → pieces-BzaJGkRl.mjs} +130 -9
  17. package/dist/pieces-BzaJGkRl.mjs.map +1 -0
  18. package/dist/rolldown-runtime-8H4AJuhK.mjs +16 -0
  19. package/dist/run-node-handler-CQjyjOuN.mjs +52 -0
  20. package/dist/run-node-handler-CQjyjOuN.mjs.map +1 -0
  21. package/dist/src/packages/https-hooks.d.mts +4 -4
  22. package/dist/src/packages/https-hooks.d.mts.map +1 -1
  23. package/dist/src/packages/https-hooks.mjs +2 -2
  24. package/dist/src/packages/https-hooks.mjs.map +1 -1
  25. package/dist/src/packages/vite-loader.d.mts +10 -6
  26. package/dist/src/packages/vite-loader.d.mts.map +1 -1
  27. package/dist/src/packages/vite-loader.mjs +71 -9
  28. package/dist/src/packages/vite-loader.mjs.map +1 -1
  29. package/dist/{types-DcYyNcHJ.d.mts → types-D4Qnqo_D.d.mts} +26 -3
  30. package/dist/types-D4Qnqo_D.d.mts.map +1 -0
  31. package/dist/{websocket-DncAB9XK.mjs → websocket-_SH5aBcy.mjs} +3 -3
  32. package/dist/{websocket-DncAB9XK.mjs.map → websocket-_SH5aBcy.mjs.map} +1 -1
  33. package/package.json +18 -18
  34. package/dist/adapter-gateway-apollo-Cn9E96GF.mjs.map +0 -1
  35. package/dist/adapter-gateway-mercurius-CjaUcMgX.mjs.map +0 -1
  36. package/dist/adapter-gateway-stitching-HmWjRMhm.mjs.map +0 -1
  37. package/dist/adapter-http-express-B39rPhqH.mjs.map +0 -1
  38. package/dist/adapter-http-fastify-BwXO6rqE.mjs.map +0 -1
  39. package/dist/path-normalize-BOQwY9wR.mjs +0 -23
  40. package/dist/path-normalize-BOQwY9wR.mjs.map +0 -1
  41. package/dist/types-DcYyNcHJ.d.mts.map +0 -1
  42. 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]="5e1dfde6-8724-5f15-80bf-467e42b71238")}catch(e){}}();
3
- import { _ as createAuthorizationService, a as isSubgraphClass, b as MalformedStoredOperationError, d as loadSubgraphs, f as BaseSubgraph, g as AuthorizedDocumentHandle, h as AuthorizationPolicy, i as buildGraphqlOperations, l as loadDocumentModels, m as createCanonicalDocumentIdResolver, n as buildGraphQlDriveDocument, o as debounce, p as CanonicalDocumentIdResolutionError, r as buildGraphqlOperation, s as extractUpgradeManifests, t as buildGraphQlDocument, u as loadProcessors, v as AuthEvaluationUnsupportedError, y as ForbiddenError } from "./utils-XWVecaNn.mjs";
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]="69ff78c8-e6e1-5d53-97e4-a5e8a75417ed")}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-BzaJGkRl.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
8
  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";
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
- resolvers = AnalyticsResolvers;
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-C_RdohOv.mjs");
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-CjaUcMgX.mjs");
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-HmWjRMhm.mjs");
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-B39rPhqH.mjs");
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-BwXO6rqE.mjs");
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") if (body === "stream") init = {
841
- ...init,
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: new Uint8Array(raw.buffer, raw.byteOffset, raw.byteLength)
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;
@@ -1452,7 +1474,7 @@ const stripScalarDefinitions = (doc) => {
1452
1474
  * whole gateway. We dedupe these by name; field/operation/extension nodes are
1453
1475
  * left untouched.
1454
1476
  */
1455
- const TYPE_DEFINITION_KINDS = new Set([
1477
+ const TYPE_DEFINITION_KINDS = /* @__PURE__ */ new Set([
1456
1478
  Kind.OBJECT_TYPE_DEFINITION,
1457
1479
  Kind.ENUM_TYPE_DEFINITION,
1458
1480
  Kind.INPUT_OBJECT_TYPE_DEFINITION,
@@ -1554,7 +1576,7 @@ const getDocumentModelTypeDefs = (documentModels, typeDefs$1) => {
1554
1576
  stateJSON: JSONObject
1555
1577
  }\n`;
1556
1578
  });
1557
- return dedupeTypeDefinitions(gql`
1579
+ const schema = gql`
1558
1580
  scalar JSONObject
1559
1581
  scalar AttachmentRef
1560
1582
  # Codegen scalars not in the document-engineering set.
@@ -1630,7 +1652,8 @@ const getDocumentModelTypeDefs = (documentModels, typeDefs$1) => {
1630
1652
  }
1631
1653
 
1632
1654
  ${stripScalarDefinitions(typeDefs$1)}
1633
- `);
1655
+ `;
1656
+ return dedupeTypeDefinitions(schema);
1634
1657
  };
1635
1658
  /**
1636
1659
  * Extract type names from a GraphQL schema.
@@ -1726,9 +1749,7 @@ function generateStateInputTypes(stateSchema, excludeTypeNames) {
1726
1749
  case Kind.OBJECT_TYPE_DEFINITION:
1727
1750
  objectNames.add(def.name.value);
1728
1751
  break;
1729
- case Kind.INPUT_OBJECT_TYPE_DEFINITION:
1730
- existingInputNames.add(def.name.value);
1731
- break;
1752
+ case Kind.INPUT_OBJECT_TYPE_DEFINITION: existingInputNames.add(def.name.value);
1732
1753
  }
1733
1754
  const inputTypes = [];
1734
1755
  for (const def of ast.definitions) {
@@ -1757,7 +1778,7 @@ function applyGraphQLTypePrefixes(schema, prefix, externalTypeNames = []) {
1757
1778
  if (!schema || !schema.trim()) return schema;
1758
1779
  let processedSchema = schema;
1759
1780
  const localTypeNames = extractTypeNames(schema);
1760
- const allTypeNames = [...new Set([...localTypeNames, ...externalTypeNames])];
1781
+ const allTypeNames = [.../* @__PURE__ */ new Set([...localTypeNames, ...externalTypeNames])];
1761
1782
  if (allTypeNames.length === 0) return schema;
1762
1783
  allTypeNames.forEach((typeName) => {
1763
1784
  const typeRegex = new RegExp(`(?<![_A-Za-z0-9])(${typeName})(?![_A-Za-z0-9])|\\[(${typeName})\\]|\\[(${typeName})!\\]|\\[(${typeName})\\]!|\\[(${typeName})!\\]!`, "g");
@@ -1785,8 +1806,9 @@ function generateDocumentModelSchema(documentModel, options = {}) {
1785
1806
  const localStateTypeNames = extractTypeNames(localStateSchema ?? "");
1786
1807
  const stateTypeNames = [...globalStateTypeNames, ...localStateTypeNames];
1787
1808
  const allOperationTypeNames = specification?.modules.flatMap((module) => module.operations.flatMap((op) => extractTypeNames(op.schema ?? ""))) ?? [];
1788
- const allTypeNames = [...new Set([...stateTypeNames, ...allOperationTypeNames])];
1789
- const prefixedStateInputTypes = applyGraphQLTypePrefixes(extractInputTypeDefinitions(globalStateSchema ?? "", new Set(allOperationTypeNames)), documentName, allTypeNames);
1809
+ const allTypeNames = [.../* @__PURE__ */ new Set([...stateTypeNames, ...allOperationTypeNames])];
1810
+ const operationInputTypeNames = new Set(allOperationTypeNames);
1811
+ const prefixedStateInputTypes = applyGraphQLTypePrefixes(extractInputTypeDefinitions(globalStateSchema ?? "", operationInputTypeNames), documentName, allTypeNames);
1790
1812
  const hasValidSchema = (schema) => !!(schema && /\b(input|type|enum|union|interface)\s+\w+/.test(schema));
1791
1813
  const stateSchemaTypes = globalStateSchema ? applyGraphQLTypePrefixes(globalStateSchema.replaceAll("scalar DateTime", "").replaceAll(/input (.*?) {[\s\S]*?}/g, ""), documentName, allTypeNames) : "";
1792
1814
  if (useNewApi) return generateNewApiSchema(documentName, specification, stateSchemaTypes, prefixedStateInputTypes, allTypeNames, hasValidSchema);
@@ -1916,7 +1938,6 @@ function generateNewApiSchema(documentName, specification, _stateSchemaTypes, pr
1916
1938
  """
1917
1939
  type ${documentName}_DocumentResultPage {
1918
1940
  items: [${documentName}MutationResult!]!
1919
- totalCount: Int!
1920
1941
  hasNextPage: Boolean!
1921
1942
  hasPreviousPage: Boolean!
1922
1943
  cursor: String
@@ -2222,6 +2243,16 @@ const PhDocumentFieldsFragmentDoc = gql`
2222
2243
  lastModifiedAtUtcIso
2223
2244
  }
2224
2245
  `;
2246
+ const DocumentRelationshipFieldsFragmentDoc = gql`
2247
+ fragment DocumentRelationshipFields on DocumentRelationship {
2248
+ sourceId
2249
+ targetId
2250
+ relationshipType
2251
+ metadata
2252
+ createdAt
2253
+ updatedAt
2254
+ }
2255
+ `;
2225
2256
  const GetDocumentModelsDocument = gql`
2226
2257
  query GetDocumentModels($namespace: String, $paging: PagingInput) {
2227
2258
  documentModels(namespace: $namespace, paging: $paging) {
@@ -2232,7 +2263,6 @@ const GetDocumentModelsDocument = gql`
2232
2263
  version
2233
2264
  specification
2234
2265
  }
2235
- totalCount
2236
2266
  hasNextPage
2237
2267
  hasPreviousPage
2238
2268
  cursor
@@ -2291,7 +2321,6 @@ const GetDocumentWithOperationsDocument = gql`
2291
2321
  }
2292
2322
  }
2293
2323
  }
2294
- totalCount
2295
2324
  hasNextPage
2296
2325
  hasPreviousPage
2297
2326
  cursor
@@ -2318,7 +2347,6 @@ const GetDocumentOutgoingRelationshipsDocument = gql`
2318
2347
  items {
2319
2348
  ...PHDocumentFields
2320
2349
  }
2321
- totalCount
2322
2350
  hasNextPage
2323
2351
  hasPreviousPage
2324
2352
  cursor
@@ -2342,7 +2370,6 @@ const GetDocumentIncomingRelationshipsDocument = gql`
2342
2370
  items {
2343
2371
  ...PHDocumentFields
2344
2372
  }
2345
- totalCount
2346
2373
  hasNextPage
2347
2374
  hasPreviousPage
2348
2375
  cursor
@@ -2350,6 +2377,52 @@ const GetDocumentIncomingRelationshipsDocument = gql`
2350
2377
  }
2351
2378
  ${PhDocumentFieldsFragmentDoc}
2352
2379
  `;
2380
+ const GetDocumentOutgoingRelationshipEdgesDocument = gql`
2381
+ query GetDocumentOutgoingRelationshipEdges(
2382
+ $sourceIdentifier: String!
2383
+ $relationshipType: String
2384
+ $view: ViewFilterInput
2385
+ $paging: PagingInput
2386
+ ) {
2387
+ documentOutgoingRelationshipEdges(
2388
+ sourceIdentifier: $sourceIdentifier
2389
+ relationshipType: $relationshipType
2390
+ view: $view
2391
+ paging: $paging
2392
+ ) {
2393
+ items {
2394
+ ...DocumentRelationshipFields
2395
+ }
2396
+ hasNextPage
2397
+ hasPreviousPage
2398
+ cursor
2399
+ }
2400
+ }
2401
+ ${DocumentRelationshipFieldsFragmentDoc}
2402
+ `;
2403
+ const GetDocumentIncomingRelationshipEdgesDocument = gql`
2404
+ query GetDocumentIncomingRelationshipEdges(
2405
+ $targetIdentifier: String!
2406
+ $relationshipType: String
2407
+ $view: ViewFilterInput
2408
+ $paging: PagingInput
2409
+ ) {
2410
+ documentIncomingRelationshipEdges(
2411
+ targetIdentifier: $targetIdentifier
2412
+ relationshipType: $relationshipType
2413
+ view: $view
2414
+ paging: $paging
2415
+ ) {
2416
+ items {
2417
+ ...DocumentRelationshipFields
2418
+ }
2419
+ hasNextPage
2420
+ hasPreviousPage
2421
+ cursor
2422
+ }
2423
+ }
2424
+ ${DocumentRelationshipFieldsFragmentDoc}
2425
+ `;
2353
2426
  const FindDocumentsDocument = gql`
2354
2427
  query FindDocuments(
2355
2428
  $search: SearchFilterInput
@@ -2360,7 +2433,6 @@ const FindDocumentsDocument = gql`
2360
2433
  items {
2361
2434
  ...PHDocumentFields
2362
2435
  }
2363
- totalCount
2364
2436
  hasNextPage
2365
2437
  hasPreviousPage
2366
2438
  cursor
@@ -2404,7 +2476,6 @@ const GetDocumentOperationsDocument = gql`
2404
2476
  }
2405
2477
  }
2406
2478
  }
2407
- totalCount
2408
2479
  hasNextPage
2409
2480
  hasPreviousPage
2410
2481
  cursor
@@ -2540,12 +2611,34 @@ const AddRelationshipDocument = gql`
2540
2611
  $sourceIdentifier: String!
2541
2612
  $targetIdentifier: String!
2542
2613
  $relationshipType: String!
2614
+ $metadata: JSONObject
2543
2615
  $branch: String
2544
2616
  ) {
2545
2617
  addRelationship(
2546
2618
  sourceIdentifier: $sourceIdentifier
2547
2619
  targetIdentifier: $targetIdentifier
2548
2620
  relationshipType: $relationshipType
2621
+ metadata: $metadata
2622
+ branch: $branch
2623
+ ) {
2624
+ ...PHDocumentFields
2625
+ }
2626
+ }
2627
+ ${PhDocumentFieldsFragmentDoc}
2628
+ `;
2629
+ const UpdateRelationshipDocument = gql`
2630
+ mutation UpdateRelationship(
2631
+ $sourceIdentifier: String!
2632
+ $targetIdentifier: String!
2633
+ $relationshipType: String!
2634
+ $metadata: JSONObject
2635
+ $branch: String
2636
+ ) {
2637
+ updateRelationship(
2638
+ sourceIdentifier: $sourceIdentifier
2639
+ targetIdentifier: $targetIdentifier
2640
+ relationshipType: $relationshipType
2641
+ metadata: $metadata
2549
2642
  branch: $branch
2550
2643
  ) {
2551
2644
  ...PHDocumentFields
@@ -2739,6 +2832,12 @@ function getSdk(requester) {
2739
2832
  GetDocumentIncomingRelationships(variables, options) {
2740
2833
  return requester(GetDocumentIncomingRelationshipsDocument, variables, options);
2741
2834
  },
2835
+ GetDocumentOutgoingRelationshipEdges(variables, options) {
2836
+ return requester(GetDocumentOutgoingRelationshipEdgesDocument, variables, options);
2837
+ },
2838
+ GetDocumentIncomingRelationshipEdges(variables, options) {
2839
+ return requester(GetDocumentIncomingRelationshipEdgesDocument, variables, options);
2840
+ },
2742
2841
  FindDocuments(variables, options) {
2743
2842
  return requester(FindDocumentsDocument, variables, options);
2744
2843
  },
@@ -2772,6 +2871,9 @@ function getSdk(requester) {
2772
2871
  AddRelationship(variables, options) {
2773
2872
  return requester(AddRelationshipDocument, variables, options);
2774
2873
  },
2874
+ UpdateRelationship(variables, options) {
2875
+ return requester(UpdateRelationshipDocument, variables, options);
2876
+ },
2775
2877
  RemoveRelationship(variables, options) {
2776
2878
  return requester(RemoveRelationshipDocument, variables, options);
2777
2879
  },
@@ -2812,8 +2914,7 @@ function toDocumentModelResultPage(result) {
2812
2914
  cursor: result.nextCursor ?? null,
2813
2915
  hasNextPage: !!result.nextCursor,
2814
2916
  hasPreviousPage: !!result.options.cursor,
2815
- items: models.map(toGqlDocumentModelState),
2816
- totalCount: result.results.length
2917
+ items: models.map(toGqlDocumentModelState)
2817
2918
  };
2818
2919
  }
2819
2920
  /**
@@ -2845,8 +2946,32 @@ function toPhDocumentResultPage(result) {
2845
2946
  cursor: result.nextCursor ?? null,
2846
2947
  hasNextPage: !!result.nextCursor,
2847
2948
  hasPreviousPage: !!result.options.cursor,
2848
- items: result.results.map(toGqlPhDocument),
2849
- totalCount: result.totalCount ?? result.results.length
2949
+ items: result.results.map(toGqlPhDocument)
2950
+ };
2951
+ }
2952
+ /**
2953
+ * Converts a relationship edge from the indexer to GraphQL format.
2954
+ */
2955
+ function toGqlDocumentRelationship(relationship) {
2956
+ return {
2957
+ sourceId: relationship.sourceId,
2958
+ targetId: relationship.targetId,
2959
+ relationshipType: relationship.relationshipType,
2960
+ metadata: relationship.metadata ?? null,
2961
+ createdAt: relationship.createdAt,
2962
+ updatedAt: relationship.updatedAt
2963
+ };
2964
+ }
2965
+ /**
2966
+ * Converts a PagedResults of relationship edges to GraphQL
2967
+ * DocumentRelationshipResultPage format
2968
+ */
2969
+ function toDocumentRelationshipResultPage(result) {
2970
+ return {
2971
+ cursor: result.nextCursor ?? null,
2972
+ hasNextPage: !!result.nextCursor,
2973
+ hasPreviousPage: !!result.options.cursor,
2974
+ items: result.results.map(toGqlDocumentRelationship)
2850
2975
  };
2851
2976
  }
2852
2977
  /**
@@ -2896,8 +3021,8 @@ function fromInputMaybe(value) {
2896
3021
  function toReactorPropagationMode(gqlMode) {
2897
3022
  if (gqlMode == null) return;
2898
3023
  switch (gqlMode) {
2899
- case PropagationMode.Cascade: return PropagationMode$1.Cascade;
2900
- case PropagationMode.Orphan: return PropagationMode$1.None;
3024
+ case "CASCADE": return PropagationMode$1.Cascade;
3025
+ case "ORPHAN": return PropagationMode$1.None;
2901
3026
  }
2902
3027
  }
2903
3028
  /**
@@ -2907,10 +3032,10 @@ function toReactorPropagationMode(gqlMode) {
2907
3032
  */
2908
3033
  function toGqlActionEvaluation(evaluation) {
2909
3034
  return evaluation.decision === "allow" ? {
2910
- decision: AuthDecision.Allow,
3035
+ decision: "ALLOW",
2911
3036
  reason: null
2912
3037
  } : {
2913
- decision: AuthDecision.Deny,
3038
+ decision: "DENY",
2914
3039
  reason: evaluation.reason
2915
3040
  };
2916
3041
  }
@@ -3115,8 +3240,7 @@ function toOperationResultPage(result) {
3115
3240
  cursor: result.nextCursor ?? null,
3116
3241
  hasNextPage: !!result.nextCursor,
3117
3242
  hasPreviousPage: !!result.options.cursor && result.options.cursor !== "0",
3118
- items: result.results.map(serializeOperationForGraphQL),
3119
- totalCount: result.results.length
3243
+ items: result.results.map(serializeOperationForGraphQL)
3120
3244
  };
3121
3245
  }
3122
3246
  function toGqlDocumentChangeEvent(event) {
@@ -3188,6 +3312,21 @@ function pickDriveClient(reactorClient, driveContainerType, reactorDriveClient)
3188
3312
  }
3189
3313
  return reactorClient.drives;
3190
3314
  }
3315
+ /**
3316
+ * A mutation's document re-read as the caller, since the client reads it back
3317
+ * as its signer. A caller who may write but not read gets it stripped.
3318
+ */
3319
+ async function readAsCaller(reactorClient, document, subject, branch) {
3320
+ if (!subject) return document;
3321
+ try {
3322
+ return await reactorClient.get(document.header.id, {
3323
+ subject,
3324
+ branch
3325
+ });
3326
+ } catch (error) {
3327
+ throw new GraphQLError(`Failed to read the document back: ${error instanceof Error ? error.message : "Unknown error"}`);
3328
+ }
3329
+ }
3191
3330
  async function documentModels(reactorClient, args) {
3192
3331
  const namespace = fromInputMaybe(args.namespace);
3193
3332
  let paging;
@@ -3295,6 +3434,54 @@ async function documentIncomingRelationships(reactorClient, args, subject) {
3295
3434
  throw new GraphQLError(`Failed to convert incoming relationships to GraphQL: ${error instanceof Error ? error.message : "Unknown error"}`);
3296
3435
  }
3297
3436
  }
3437
+ async function documentOutgoingRelationshipEdges(reactorClient, args, subject) {
3438
+ const view = toRelationshipViewFilter(args.view, subject);
3439
+ const paging = toRelationshipPagingOptions(args.paging);
3440
+ let result;
3441
+ try {
3442
+ result = await reactorClient.getOutgoingRelationshipEdges(args.sourceIdentifier, fromInputMaybe(args.relationshipType), view, paging);
3443
+ } catch (error) {
3444
+ throw new GraphQLError(`Failed to fetch outgoing relationship edges: ${error instanceof Error ? error.message : "Unknown error"}`);
3445
+ }
3446
+ try {
3447
+ return toDocumentRelationshipResultPage(result);
3448
+ } catch (error) {
3449
+ throw new GraphQLError(`Failed to convert outgoing relationship edges to GraphQL: ${error instanceof Error ? error.message : "Unknown error"}`);
3450
+ }
3451
+ }
3452
+ async function documentIncomingRelationshipEdges(reactorClient, args, subject) {
3453
+ const view = toRelationshipViewFilter(args.view, subject);
3454
+ const paging = toRelationshipPagingOptions(args.paging);
3455
+ let result;
3456
+ try {
3457
+ result = await reactorClient.getIncomingRelationshipEdges(args.targetIdentifier, fromInputMaybe(args.relationshipType), view, paging);
3458
+ } catch (error) {
3459
+ throw new GraphQLError(`Failed to fetch incoming relationship edges: ${error instanceof Error ? error.message : "Unknown error"}`);
3460
+ }
3461
+ try {
3462
+ return toDocumentRelationshipResultPage(result);
3463
+ } catch (error) {
3464
+ throw new GraphQLError(`Failed to convert incoming relationship edges to GraphQL: ${error instanceof Error ? error.message : "Unknown error"}`);
3465
+ }
3466
+ }
3467
+ function toRelationshipViewFilter(input, subject) {
3468
+ if (!input) return subject ? { subject } : void 0;
3469
+ return {
3470
+ subject,
3471
+ branch: fromInputMaybe(input.branch),
3472
+ scopes: toMutableArray(fromInputMaybe(input.scopes))
3473
+ };
3474
+ }
3475
+ function toRelationshipPagingOptions(input) {
3476
+ if (!input) return;
3477
+ const cursor = fromInputMaybe(input.cursor);
3478
+ const limit = fromInputMaybe(input.limit);
3479
+ if (!cursor && !limit) return;
3480
+ return {
3481
+ cursor: cursor || "",
3482
+ limit: limit || 10
3483
+ };
3484
+ }
3298
3485
  async function findDocuments(reactorClient, args, subject) {
3299
3486
  let view = subject ? { subject } : void 0;
3300
3487
  if (args.view) view = {
@@ -3327,13 +3514,42 @@ async function findDocuments(reactorClient, args, subject) {
3327
3514
  throw new GraphQLError(`Failed to convert documents to GraphQL: ${error instanceof Error ? error.message : "Unknown error"}`);
3328
3515
  }
3329
3516
  }
3330
- async function jobStatus(reactorClient, args) {
3517
+ /**
3518
+ * The answer for a job the caller may not see, shaped as the reactor answers
3519
+ * an unknown one so that the two cannot be told apart.
3520
+ */
3521
+ function unknownJob(jobId) {
3522
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3523
+ return {
3524
+ id: jobId,
3525
+ documentId: "",
3526
+ status: JobStatus.FAILED,
3527
+ createdAtUtcIso: now,
3528
+ completedAtUtcIso: now,
3529
+ error: {
3530
+ name: "Error",
3531
+ message: "Job not found",
3532
+ stack: ""
3533
+ },
3534
+ consistencyToken: {
3535
+ version: 1,
3536
+ createdAtUtcIso: now,
3537
+ coordinates: []
3538
+ },
3539
+ meta: {
3540
+ batchId: jobId,
3541
+ batchJobIds: [jobId]
3542
+ }
3543
+ };
3544
+ }
3545
+ async function jobStatus(reactorClient, args, serves) {
3331
3546
  let result;
3332
3547
  try {
3333
3548
  result = await reactorClient.getJobStatus(args.jobId);
3334
3549
  } catch (error) {
3335
3550
  throw new GraphQLError(`Failed to fetch job status: ${error instanceof Error ? error.message : "Unknown error"}`);
3336
3551
  }
3552
+ if (!await serves(result.documentId)) result = unknownJob(args.jobId);
3337
3553
  try {
3338
3554
  return toGqlJobInfo(result);
3339
3555
  } catch (error) {
@@ -3416,7 +3632,7 @@ async function evaluateActions(reactorClient, args, subject) {
3416
3632
  anyDenied: result.anyDenied
3417
3633
  };
3418
3634
  }
3419
- async function createDocument(reactorClient, args, reactorDriveClient) {
3635
+ async function createDocument(reactorClient, args, reactorDriveClient, subject) {
3420
3636
  if (!args.document || typeof args.document !== "object") throw new GraphQLError("Invalid document: must be an object");
3421
3637
  const document = args.document;
3422
3638
  if (!document.header || typeof document.header !== "object") throw new GraphQLError("Invalid document: missing or invalid header");
@@ -3424,28 +3640,30 @@ async function createDocument(reactorClient, args, reactorDriveClient) {
3424
3640
  let result;
3425
3641
  try {
3426
3642
  if (parentIdentifier) {
3427
- const parent = await reactorClient.get(parentIdentifier);
3643
+ const parent = await reactorClient.get(parentIdentifier, subject && { subject });
3428
3644
  if (isDriveContainerType(parent.header.documentType)) result = await pickDriveClient(reactorClient, parent.header.documentType, reactorDriveClient).addFile(parentIdentifier, document);
3429
3645
  else result = await reactorClient.create(document, parentIdentifier);
3430
3646
  } else result = await reactorClient.create(document);
3431
3647
  } catch (error) {
3432
3648
  throw new GraphQLError(`Failed to create document: ${error instanceof Error ? error.message : "Unknown error"}`);
3433
3649
  }
3650
+ result = await readAsCaller(reactorClient, result, subject);
3434
3651
  try {
3435
3652
  return toGqlPhDocument(result);
3436
3653
  } catch (error) {
3437
3654
  throw new GraphQLError(`Failed to convert created document to GraphQL: ${error instanceof Error ? error.message : "Unknown error"}`);
3438
3655
  }
3439
3656
  }
3440
- async function createEmptyDocument(reactorClient, args, reactorDriveClient) {
3657
+ async function createEmptyDocument(reactorClient, args, reactorDriveClient, subject) {
3441
3658
  const parentIdentifier = fromInputMaybe(args.parentIdentifier);
3442
3659
  const name = fromInputMaybe(args.name);
3443
3660
  let result;
3444
3661
  try {
3445
3662
  if (parentIdentifier) {
3446
- const parent = await reactorClient.get(parentIdentifier);
3663
+ const parent = await reactorClient.get(parentIdentifier, subject && { subject });
3447
3664
  if (isDriveContainerType(parent.header.documentType)) {
3448
- const document = (await reactorClient.getDocumentModelModule(args.documentType)).utils.createDocument();
3665
+ const module = await reactorClient.getDocumentModelModule(args.documentType);
3666
+ const document = withSignaturePolicy(module.utils.createDocument(), await reactorClient.getCreateSignaturePolicy());
3449
3667
  if (name) document.header.name = name;
3450
3668
  result = await pickDriveClient(reactorClient, parent.header.documentType, reactorDriveClient).addFile(parentIdentifier, document);
3451
3669
  } else result = await reactorClient.createEmpty(args.documentType, { parentIdentifier });
@@ -3453,13 +3671,14 @@ async function createEmptyDocument(reactorClient, args, reactorDriveClient) {
3453
3671
  } catch (error) {
3454
3672
  throw new GraphQLError(`Failed to create empty document: ${error instanceof Error ? error.message : "Unknown error"}`);
3455
3673
  }
3674
+ result = await readAsCaller(reactorClient, result, subject);
3456
3675
  try {
3457
3676
  return toGqlPhDocument(result);
3458
3677
  } catch (error) {
3459
3678
  throw new GraphQLError(`Failed to convert created document to GraphQL: ${error instanceof Error ? error.message : "Unknown error"}`);
3460
3679
  }
3461
3680
  }
3462
- async function createDocumentWithInitialState(reactorClient, args, reactorDriveClient) {
3681
+ async function createDocumentWithInitialState(reactorClient, args, reactorDriveClient, subject) {
3463
3682
  const parentIdentifier = fromInputMaybe(args.parentIdentifier);
3464
3683
  const name = fromInputMaybe(args.name);
3465
3684
  const slug = fromInputMaybe(args.slug);
@@ -3470,7 +3689,7 @@ async function createDocumentWithInitialState(reactorClient, args, reactorDriveC
3470
3689
  } catch (error) {
3471
3690
  throw new GraphQLError(`Document model not found for type ${args.documentType}: ${error instanceof Error ? error.message : "Unknown error"}`);
3472
3691
  }
3473
- const document = module.utils.createDocument();
3692
+ const document = withSignaturePolicy(module.utils.createDocument(), await reactorClient.getCreateSignaturePolicy());
3474
3693
  const allowedScopes = new Set(Object.keys(module.documentModel.global.specifications.at(-1)?.state ?? {}));
3475
3694
  const state = document.state;
3476
3695
  for (const [scope, scopeState] of Object.entries(args.initialState)) if (allowedScopes.has(scope) && scope in state) state[scope] = {
@@ -3487,7 +3706,7 @@ async function createDocumentWithInitialState(reactorClient, args, reactorDriveC
3487
3706
  if (parentIdentifier) {
3488
3707
  let parent;
3489
3708
  try {
3490
- parent = await reactorClient.get(parentIdentifier);
3709
+ parent = await reactorClient.get(parentIdentifier, subject && { subject });
3491
3710
  } catch (error) {
3492
3711
  throw new GraphQLError(`Parent document not found: ${error instanceof Error ? error.message : "Unknown error"}`);
3493
3712
  }
@@ -3508,6 +3727,7 @@ async function createDocumentWithInitialState(reactorClient, args, reactorDriveC
3508
3727
  } catch (error) {
3509
3728
  throw new GraphQLError(`Failed to create document: ${error instanceof Error ? error.message : "Unknown error"}`);
3510
3729
  }
3730
+ result = await readAsCaller(reactorClient, result, subject);
3511
3731
  try {
3512
3732
  return toGqlPhDocument(result);
3513
3733
  } catch (error) {
@@ -3524,12 +3744,12 @@ const DEFAULT_BRANCH = "main";
3524
3744
  * the schema cannot express: a signature travels as one string, and verification
3525
3745
  * reads it as the tuple it was.
3526
3746
  */
3527
- async function execute(reactorClient, args) {
3747
+ async function execute(reactorClient, args, subject) {
3528
3748
  const actions = toSubmittableActions(args.actions);
3529
3749
  const branch = fromInputMaybe(args.branch) ?? DEFAULT_BRANCH;
3530
3750
  let result;
3531
3751
  try {
3532
- result = await reactorClient.execute(args.documentIdentifier, branch, actions);
3752
+ result = await reactorClient.execute(args.documentIdentifier, branch, actions, void 0, subject);
3533
3753
  } catch (error) {
3534
3754
  throw new GraphQLError(`Failed to execute actions: ${error instanceof Error ? error.message : "Unknown error"}`);
3535
3755
  }
@@ -3557,7 +3777,7 @@ async function executeAsync(reactorClient, args) {
3557
3777
  }
3558
3778
  return toGqlJobInfo(job);
3559
3779
  }
3560
- async function mutateDocument(reactorClient, args) {
3780
+ async function mutateDocument(reactorClient, args, subject) {
3561
3781
  let validatedActions;
3562
3782
  try {
3563
3783
  validatedActions = validateActions(args.actions);
@@ -3568,7 +3788,7 @@ async function mutateDocument(reactorClient, args) {
3568
3788
  const branch = args.view?.branch ?? "main";
3569
3789
  let result;
3570
3790
  try {
3571
- result = await reactorClient.execute(args.documentIdentifier, branch, validatedActions);
3791
+ result = await reactorClient.execute(args.documentIdentifier, branch, validatedActions, void 0, subject);
3572
3792
  } catch (error) {
3573
3793
  throw new GraphQLError(`Failed to mutate document: ${error instanceof Error ? error.message : "Unknown error"}`);
3574
3794
  }
@@ -3595,7 +3815,7 @@ async function mutateDocumentAsync(reactorClient, args) {
3595
3815
  }
3596
3816
  return result.id;
3597
3817
  }
3598
- async function renameDocument(reactorClient, args, signal) {
3818
+ async function renameDocument(reactorClient, args, signal, subject) {
3599
3819
  const branch = fromInputMaybe(args.branch);
3600
3820
  let result;
3601
3821
  try {
@@ -3603,13 +3823,14 @@ async function renameDocument(reactorClient, args, signal) {
3603
3823
  } catch (error) {
3604
3824
  throw new GraphQLError(`Failed to rename document: ${error instanceof Error ? error.message : "Unknown error"}`);
3605
3825
  }
3826
+ result = await readAsCaller(reactorClient, result, subject, branch);
3606
3827
  try {
3607
3828
  return toGqlPhDocument(result);
3608
3829
  } catch (error) {
3609
3830
  throw new GraphQLError(`Failed to convert renamed document to GraphQL: ${error instanceof Error ? error.message : "Unknown error"}`);
3610
3831
  }
3611
3832
  }
3612
- async function setPreferredEditor(reactorClient, args, signal) {
3833
+ async function setPreferredEditor(reactorClient, args, signal, subject) {
3613
3834
  const branch = fromInputMaybe(args.branch);
3614
3835
  const preferredEditor = fromInputMaybe(args.preferredEditor) ?? null;
3615
3836
  let result;
@@ -3618,27 +3839,46 @@ async function setPreferredEditor(reactorClient, args, signal) {
3618
3839
  } catch (error) {
3619
3840
  throw new GraphQLError(`Failed to set preferred editor: ${error instanceof Error ? error.message : "Unknown error"}`);
3620
3841
  }
3842
+ result = await readAsCaller(reactorClient, result, subject, branch);
3621
3843
  try {
3622
3844
  return toGqlPhDocument(result);
3623
3845
  } catch (error) {
3624
3846
  throw new GraphQLError(`Failed to convert updated document to GraphQL: ${error instanceof Error ? error.message : "Unknown error"}`);
3625
3847
  }
3626
3848
  }
3627
- async function addRelationship(reactorClient, args) {
3849
+ async function addRelationship(reactorClient, args, subject) {
3628
3850
  const branch = fromInputMaybe(args.branch);
3851
+ const metadata = fromInputMaybe(args.metadata);
3629
3852
  let result;
3630
3853
  try {
3631
- result = await reactorClient.addRelationship(args.sourceIdentifier, args.targetIdentifier, args.relationshipType, branch);
3854
+ result = await reactorClient.addRelationship(args.sourceIdentifier, args.targetIdentifier, args.relationshipType, metadata, branch);
3632
3855
  } catch (error) {
3633
3856
  throw new GraphQLError(`Failed to add relationship: ${error instanceof Error ? error.message : "Unknown error"}`);
3634
3857
  }
3858
+ result = await readAsCaller(reactorClient, result, subject, branch);
3859
+ try {
3860
+ return toGqlPhDocument(result);
3861
+ } catch (error) {
3862
+ throw new GraphQLError(`Failed to convert document to GraphQL: ${error instanceof Error ? error.message : "Unknown error"}`);
3863
+ }
3864
+ }
3865
+ async function updateRelationship(reactorClient, args, subject) {
3866
+ const branch = fromInputMaybe(args.branch);
3867
+ const metadata = fromInputMaybe(args.metadata) ?? null;
3868
+ let result;
3869
+ try {
3870
+ result = await reactorClient.updateRelationship(args.sourceIdentifier, args.targetIdentifier, args.relationshipType, metadata, branch);
3871
+ } catch (error) {
3872
+ throw new GraphQLError(`Failed to update relationship: ${error instanceof Error ? error.message : "Unknown error"}`);
3873
+ }
3874
+ result = await readAsCaller(reactorClient, result, subject, branch);
3635
3875
  try {
3636
3876
  return toGqlPhDocument(result);
3637
3877
  } catch (error) {
3638
3878
  throw new GraphQLError(`Failed to convert document to GraphQL: ${error instanceof Error ? error.message : "Unknown error"}`);
3639
3879
  }
3640
3880
  }
3641
- async function removeRelationship(reactorClient, args) {
3881
+ async function removeRelationship(reactorClient, args, subject) {
3642
3882
  const branch = fromInputMaybe(args.branch);
3643
3883
  let result;
3644
3884
  try {
@@ -3646,13 +3886,14 @@ async function removeRelationship(reactorClient, args) {
3646
3886
  } catch (error) {
3647
3887
  throw new GraphQLError(`Failed to remove relationship: ${error instanceof Error ? error.message : "Unknown error"}`);
3648
3888
  }
3889
+ result = await readAsCaller(reactorClient, result, subject, branch);
3649
3890
  try {
3650
3891
  return toGqlPhDocument(result);
3651
3892
  } catch (error) {
3652
3893
  throw new GraphQLError(`Failed to convert document to GraphQL: ${error instanceof Error ? error.message : "Unknown error"}`);
3653
3894
  }
3654
3895
  }
3655
- async function moveRelationship(reactorClient, args) {
3896
+ async function moveRelationship(reactorClient, args, subject) {
3656
3897
  const branch = fromInputMaybe(args.branch);
3657
3898
  let result;
3658
3899
  try {
@@ -3660,6 +3901,10 @@ async function moveRelationship(reactorClient, args) {
3660
3901
  } catch (error) {
3661
3902
  throw new GraphQLError(`Failed to move relationship: ${error instanceof Error ? error.message : "Unknown error"}`);
3662
3903
  }
3904
+ result = {
3905
+ source: await readAsCaller(reactorClient, result.source, subject, branch),
3906
+ target: await readAsCaller(reactorClient, result.target, subject, branch)
3907
+ };
3663
3908
  try {
3664
3909
  return {
3665
3910
  source: toGqlPhDocument(result.source),
@@ -3778,14 +4023,16 @@ async function collectHeldSyncOperations(syncOps, gate, subject, signal) {
3778
4023
  * client ordinal the switchboard has successfully applied, so the client
3779
4024
  * can trim its own outbox)
3780
4025
  */
3781
- function pollSyncEnvelopes(syncManager, args, forbiddenIds = /* @__PURE__ */ new Set(), heldOpIds = /* @__PURE__ */ new Set()) {
4026
+ function pollSyncEnvelopes(syncManager, args, forbiddenIds = /* @__PURE__ */ new Set(), heldOpIds = /* @__PURE__ */ new Set(), gatedOpIds) {
4027
+ const isHeld = (syncOp) => heldOpIds.has(syncOp.id) || gatedOpIds !== void 0 && !gatedOpIds.has(syncOp.id);
3782
4028
  let remote;
3783
4029
  try {
3784
4030
  remote = syncManager.getById(args.channelId);
3785
4031
  } catch (error) {
3786
4032
  throw new GraphQLError(`Channel not found: ${error instanceof Error ? error.message : "Unknown error"}`);
3787
4033
  }
3788
- const deadLetters = remote.channel.deadLetter.items.filter((syncOp) => !forbiddenIds.has(syncOp.documentId) && !heldOpIds.has(syncOp.id)).map((syncOp) => ({
4034
+ remote.channel.notePoll();
4035
+ const deadLetters = remote.channel.deadLetter.items.filter((syncOp) => !forbiddenIds.has(syncOp.documentId) && !isHeld(syncOp)).map((syncOp) => ({
3789
4036
  documentId: syncOp.documentId,
3790
4037
  error: syncOp.error?.message ?? "Unknown error",
3791
4038
  errorType: syncOperationErrorType(syncOp.error),
@@ -3834,7 +4081,7 @@ function pollSyncEnvelopes(syncManager, args, forbiddenIds = /* @__PURE__ */ new
3834
4081
  hasMore = true;
3835
4082
  break;
3836
4083
  }
3837
- if (heldOpIds.has(syncOp.id)) continue;
4084
+ if (isHeld(syncOp)) continue;
3838
4085
  syncOp.deliveredCount ??= 0;
3839
4086
  syncOp.emittedCount ??= 0;
3840
4087
  while (syncOp.deliveredCount < syncOp.emittedCount && syncOp.operations[syncOp.deliveredCount].context.ordinal <= args.outboxLatest) syncOp.deliveredCount += 1;
@@ -3909,6 +4156,7 @@ function pushSyncEnvelopes(syncManager, args) {
3909
4156
  throw new GraphQLError(`Channel not found: ${error instanceof Error ? error.message : "Unknown error"}`);
3910
4157
  }
3911
4158
  if (!envelope.operations || envelope.operations.length === 0) continue;
4159
+ remote.channel.notePoll();
3912
4160
  const syncOps = envelopesToSyncOperations(envelope, remote.meta.name);
3913
4161
  if (!remoteSyncOps.has(remote)) remoteSyncOps.set(remote, []);
3914
4162
  remoteSyncOps.get(remote).push(...syncOps);
@@ -3946,6 +4194,16 @@ var DocumentModelSubgraph = class extends BaseSubgraph {
3946
4194
  const documentName = getDocumentModelSchemaName(this.documentModel.documentModel.global);
3947
4195
  return this.resolvers[`${documentName}Mutations`];
3948
4196
  }
4197
+ /** Drops the items the host's own ACL refuses the caller. */
4198
+ async #readableItems(page, ctx) {
4199
+ if (this.authorizationService.isSupremeAdmin(ctx.user?.address)) return page;
4200
+ const items = [];
4201
+ for (const item of page.items) if (await this.canReadDocument(item.id, ctx)) items.push(item);
4202
+ return {
4203
+ ...page,
4204
+ items
4205
+ };
4206
+ }
3949
4207
  /**
3950
4208
  * Generate __resolveType functions for union types found in the document model schema.
3951
4209
  * Parses the state schema to find union definitions and their member types,
@@ -4006,7 +4264,7 @@ var DocumentModelSubgraph = class extends BaseSubgraph {
4006
4264
  const result = await document(this.reactorClient, {
4007
4265
  identifier,
4008
4266
  view
4009
- });
4267
+ }, this.viewSubject(ctx));
4010
4268
  if (result.document.documentType !== documentType) throw new GraphQLError(`Document with id ${identifier} is not of type ${documentType}`);
4011
4269
  await this.assertCanReadCanonical(result.document.id, ctx);
4012
4270
  return result;
@@ -4016,17 +4274,8 @@ var DocumentModelSubgraph = class extends BaseSubgraph {
4016
4274
  const result = await findDocuments(this.reactorClient, {
4017
4275
  search: { type: documentType },
4018
4276
  paging
4019
- });
4020
- if (!this.authorizationService.isSupremeAdmin(ctx.user?.address)) {
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;
4277
+ }, this.viewSubject(ctx));
4278
+ return this.#readableItems(result, ctx);
4030
4279
  },
4031
4280
  findDocuments: async (_, args, ctx) => {
4032
4281
  const { search, view, paging } = args;
@@ -4037,17 +4286,8 @@ var DocumentModelSubgraph = class extends BaseSubgraph {
4037
4286
  },
4038
4287
  view,
4039
4288
  paging
4040
- });
4041
- if (!this.authorizationService.isSupremeAdmin(ctx.user?.address)) {
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;
4289
+ }, this.viewSubject(ctx));
4290
+ return this.#readableItems(result, ctx);
4051
4291
  },
4052
4292
  documentOutgoingRelationships: async (_, args, ctx) => {
4053
4293
  const { relationshipType, view, paging } = args;
@@ -4057,23 +4297,24 @@ var DocumentModelSubgraph = class extends BaseSubgraph {
4057
4297
  relationshipType,
4058
4298
  view,
4059
4299
  paging
4060
- });
4061
- const filteredItems = result.items.filter((item) => item.documentType === documentType);
4300
+ }, this.viewSubject(ctx));
4301
+ const readable = await this.#readableItems(result, ctx);
4302
+ const filteredItems = readable.items.filter((item) => item.documentType === documentType);
4062
4303
  return {
4063
- ...result,
4064
- items: filteredItems,
4065
- totalCount: filteredItems.length
4304
+ ...readable,
4305
+ items: filteredItems
4066
4306
  };
4067
4307
  },
4068
4308
  documentIncomingRelationships: async (_, args, ctx) => {
4069
4309
  const { relationshipType, view, paging } = args;
4070
4310
  const handle = await this.assertCanRead(args.targetIdentifier, ctx);
4071
- return documentIncomingRelationships(this.reactorClient, {
4311
+ const result = await documentIncomingRelationships(this.reactorClient, {
4072
4312
  targetIdentifier: handle.fetchIdentifier,
4073
4313
  relationshipType,
4074
4314
  view,
4075
4315
  paging
4076
- });
4316
+ }, this.viewSubject(ctx));
4317
+ return this.#readableItems(result, ctx);
4077
4318
  }
4078
4319
  },
4079
4320
  Mutation: { [documentName]: () => ({}) },
@@ -4091,14 +4332,14 @@ var DocumentModelSubgraph = class extends BaseSubgraph {
4091
4332
  slug,
4092
4333
  preferredEditor,
4093
4334
  initialState: initialState ?? {}
4094
- }, this.graphqlManager.reactorDriveClient);
4335
+ }, this.graphqlManager.reactorDriveClient, this.viewSubject(ctx));
4095
4336
  else createdDoc = await createEmptyDocument(this.reactorClient, {
4096
4337
  documentType,
4097
4338
  parentIdentifier,
4098
4339
  name
4099
- }, this.graphqlManager.reactorDriveClient);
4340
+ }, this.graphqlManager.reactorDriveClient, this.viewSubject(ctx));
4100
4341
  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)]));
4342
+ if (!initialState && !preferredEditor && name && createdDoc.name !== name) return toGqlPhDocument(await this.reactorClient.execute(createdDoc.id, "main", [setName(name)], void 0, this.viewSubject(ctx)));
4102
4343
  return createdDoc;
4103
4344
  },
4104
4345
  createEmptyDocument: async (_, args, ctx) => {
@@ -4108,7 +4349,7 @@ var DocumentModelSubgraph = class extends BaseSubgraph {
4108
4349
  const result = await createEmptyDocument(this.reactorClient, {
4109
4350
  documentType,
4110
4351
  parentIdentifier
4111
- }, this.graphqlManager.reactorDriveClient);
4352
+ }, this.graphqlManager.reactorDriveClient, this.viewSubject(ctx));
4112
4353
  if (ctx.user?.address && result?.id) await this.documentPermissionService?.initializeDocumentProtection(result.id, ctx.user.address, this.authorizationService.config.defaultProtection);
4113
4354
  return result;
4114
4355
  },
@@ -4116,11 +4357,11 @@ var DocumentModelSubgraph = class extends BaseSubgraph {
4116
4357
  mutations[camelCase(op.name)] = async (_, args, ctx) => {
4117
4358
  const { docId, input } = args;
4118
4359
  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}`);
4360
+ 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
4361
  const action = this.documentModel.actions[camelCase(op.name)];
4121
4362
  if (!action) throw new GraphQLError(`Action ${op.name} not found`);
4122
4363
  try {
4123
- return toGqlPhDocument(await this.reactorClient.execute(effectiveDocId, "main", [action(input)]));
4364
+ return toGqlPhDocument(await this.reactorClient.execute(effectiveDocId, "main", [action(input)], void 0, this.viewSubject(ctx)));
4124
4365
  } catch (error) {
4125
4366
  throw new GraphQLError(error instanceof Error ? error.message : `Failed to ${op.name}`);
4126
4367
  }
@@ -4128,7 +4369,7 @@ var DocumentModelSubgraph = class extends BaseSubgraph {
4128
4369
  mutations[`${camelCase(op.name)}Async`] = async (_, args, ctx) => {
4129
4370
  const { docId, input } = args;
4130
4371
  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}`);
4372
+ 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
4373
  const action = this.documentModel.actions[camelCase(op.name)];
4133
4374
  if (!action) throw new GraphQLError(`Action ${op.name} not found`);
4134
4375
  try {
@@ -4152,7 +4393,7 @@ const DRIVE_ID_HEADER = "drive-id";
4152
4393
  * in the ownership set yet); other create-shaped operations that may
4153
4394
  * synthesize a drive must be added here too.
4154
4395
  */
4155
- const CACHE_BYPASS_OPERATIONS = new Set(["createDocument", "createEmptyDocument"]);
4396
+ const CACHE_BYPASS_OPERATIONS = /* @__PURE__ */ new Set(["createDocument", "createEmptyDocument"]);
4156
4397
  const driveIdMap = /* @__PURE__ */ new WeakMap();
4157
4398
  /** Internal — only `graphql-manager.ts` should call this. */
4158
4399
  function getRequestDriveId(request) {
@@ -4222,6 +4463,7 @@ function wrongShardResponse(driveId) {
4222
4463
  * 421 response.
4223
4464
  */
4224
4465
  var DriveOwnershipCache = class {
4466
+ reactorClient;
4225
4467
  drives = /* @__PURE__ */ new Set();
4226
4468
  constructor(reactorClient) {
4227
4469
  this.reactorClient = reactorClient;
@@ -4312,7 +4554,49 @@ const DefaultFeatureFlags = { enableDocumentModelSubgraphs: true };
4312
4554
  * regenerateDocumentModelSubgraphs).
4313
4555
  */
4314
4556
  const DOCUMENT_MODEL_SUBGRAPH_SOURCE = "document-models";
4557
+ function refuseConnection(connection, reason) {
4558
+ const { socket } = connection;
4559
+ socket?.close?.(4403, reason);
4560
+ return false;
4561
+ }
4562
+ function createWsAuthHandlers(opts) {
4563
+ const resolved = /* @__PURE__ */ new WeakMap();
4564
+ return {
4565
+ onConnect: async (connectionParams, connection) => {
4566
+ let user = null;
4567
+ if (opts.authService) try {
4568
+ user = await opts.authService.authenticateWebSocketConnection(connectionParams);
4569
+ } catch (error) {
4570
+ opts.logger.warn("Refusing WebSocket connection: @error", error instanceof Error ? error.message : error);
4571
+ return refuseConnection(connection, WS_CLOSE_REASON_BEARER_REJECTED);
4572
+ }
4573
+ if (user === null && opts.requireAuthenticatedCaller()) {
4574
+ opts.logger.warn("Refusing anonymous WebSocket connection: an authenticated caller is required");
4575
+ return refuseConnection(connection, WS_CLOSE_REASON_AUTHENTICATION_REQUIRED);
4576
+ }
4577
+ resolved.set(connection, user ?? void 0);
4578
+ return true;
4579
+ },
4580
+ context: (connectionParams, connection) => Promise.resolve(opts.buildContext(connectionParams, resolved.get(connection)))
4581
+ };
4582
+ }
4315
4583
  var GraphQLManager = class {
4584
+ path;
4585
+ httpServer;
4586
+ wsServer;
4587
+ reactorClient;
4588
+ relationalDb;
4589
+ analyticsStore;
4590
+ syncManager;
4591
+ logger;
4592
+ httpAdapter;
4593
+ gatewayAdapter;
4594
+ authService;
4595
+ documentPermissionService;
4596
+ featureFlags;
4597
+ port;
4598
+ syncServingGate;
4599
+ httpRoutes;
4316
4600
  initialized = false;
4317
4601
  coreSubgraphsMap = /* @__PURE__ */ new Map();
4318
4602
  contextFields = {};
@@ -4327,8 +4611,8 @@ var GraphQLManager = class {
4327
4611
  subgraphHandlerCache = /* @__PURE__ */ new Map();
4328
4612
  /** subgraphPath → the http adapter handle of its mounted route. */
4329
4613
  subgraphRouteHandles = /* @__PURE__ */ new Map();
4330
- /** Handle of the currently mounted supergraph SSE route, if any. */
4331
- sseRouteHandle;
4614
+ /** Mounted SSE routes, keyed by path: the supergraph's and each subgraph's. */
4615
+ sseRouteHandles = /* @__PURE__ */ new Map();
4332
4616
  /**
4333
4617
  * Package name → the subgraph instances registered from it, keyed by
4334
4618
  * subgraph name. Tracks the package association so that removed packages
@@ -4384,27 +4668,34 @@ var GraphQLManager = class {
4384
4668
  this.httpAdapter.setupMiddleware({ bodyLimit: "50mb" });
4385
4669
  const driveRoutePath = path.posix.join(this.path, "d/:drive");
4386
4670
  const driveMatcher = match(driveRoutePath);
4387
- this.httpAdapter.mount(driveRoutePath, async (request) => {
4671
+ const driveInfoHandler = async (request) => {
4388
4672
  const url = new URL(request.url);
4389
4673
  const matched = driveMatcher(url.pathname);
4390
4674
  const driveIdOrSlug = matched ? matched.params.drive : void 0;
4391
4675
  if (!driveIdOrSlug) return Response.json({ error: "Drive ID or slug is required" }, { status: 400 });
4676
+ const user = getAuthContext(request)?.user;
4392
4677
  try {
4393
- const driveDoc = await this.reactorClient.get(driveIdOrSlug);
4678
+ const driveDoc = await this.reactorClient.get(driveIdOrSlug, { subject: callerSubject(user) });
4679
+ if (!await this.authorizationService.canRead(driveDoc.header.id, user?.address)) {
4680
+ this.logger.debug(`Drive read refused: ${driveIdOrSlug}`);
4681
+ return Response.json({ error: "Drive not found" }, { status: 404 });
4682
+ }
4394
4683
  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`;
4684
+ const global = driveDoc.state.global;
4395
4685
  return Response.json({
4396
4686
  id: driveDoc.header.id,
4397
4687
  slug: driveDoc.header.slug,
4398
4688
  meta: driveDoc.header.meta,
4399
- name: driveDoc.state.global.name || driveDoc.header.name,
4400
- icon: driveDoc.state.global.icon ?? void 0,
4689
+ name: global?.name || driveDoc.header.name,
4690
+ icon: global?.icon ?? void 0,
4401
4691
  ...graphqlEndpoint && { graphqlEndpoint }
4402
4692
  });
4403
4693
  } catch (error) {
4404
4694
  this.logger.debug(`Drive not found: ${driveIdOrSlug}`, error);
4405
4695
  return Response.json({ error: "Drive not found" }, { status: 404 });
4406
4696
  }
4407
- });
4697
+ };
4698
+ this.httpAdapter.mount(driveRoutePath, this.#authMiddleware ? this.#authMiddleware(driveInfoHandler) : driveInfoHandler);
4408
4699
  this.logger.info(`Registered REST endpoint: GET ${driveRoutePath}`);
4409
4700
  await this.#setupCoreSubgraphs("graphql", coreSubgraphs);
4410
4701
  if (this.featureFlags.enableDocumentModelSubgraphs) await this.#setupDocumentModelSubgraphs("graphql", models);
@@ -4521,6 +4812,7 @@ var GraphQLManager = class {
4521
4812
  routeHandle.dispose();
4522
4813
  this.subgraphRouteHandles.delete(subgraphPath);
4523
4814
  }
4815
+ this.#disposeSSERoute(subgraphPath);
4524
4816
  const wsDisposer = this.subgraphWsDisposers.get(subgraphPath);
4525
4817
  if (wsDisposer) {
4526
4818
  this.subgraphWsDisposers.delete(subgraphPath);
@@ -4695,17 +4987,6 @@ var GraphQLManager = class {
4695
4987
  ...fields
4696
4988
  };
4697
4989
  }
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
4990
  #makeContextFactory() {
4710
4991
  return (request) => {
4711
4992
  const authCtx = getAuthContext(request);
@@ -4724,7 +5005,20 @@ var GraphQLManager = class {
4724
5005
  };
4725
5006
  }
4726
5007
  #makeWsContextFactory() {
4727
- return (connectionParams) => this.#createWebSocketContext(connectionParams);
5008
+ return createWsAuthHandlers({
5009
+ authService: this.authService,
5010
+ requireAuthenticatedCaller: () => this.#requireAuthMiddleware !== void 0,
5011
+ logger: this.logger,
5012
+ buildContext: (connectionParams, user) => {
5013
+ const context = {
5014
+ headers: connectionParams,
5015
+ db: this.relationalDb,
5016
+ ...this.getAdditionalContextFields()
5017
+ };
5018
+ if (user) context.user = user;
5019
+ return context;
5020
+ }
5021
+ });
4728
5022
  }
4729
5023
  setSupergraph(supergraph, subgraphs) {
4730
5024
  const accepted = [];
@@ -4858,10 +5152,7 @@ var GraphQLManager = class {
4858
5152
  const allSubgraphs = this.#getAllSubgraphs();
4859
5153
  const modules = Array.from(allSubgraphs.values()).filter((subgraph) => subgraph.hasSubscriptions).map((subgraph) => this.#buildSubgraphSchemaModule(subgraph));
4860
5154
  if (modules.length === 0) {
4861
- if (this.sseRouteHandle !== void 0) {
4862
- this.sseRouteHandle.dispose();
4863
- this.sseRouteHandle = void 0;
4864
- }
5155
+ this.#disposeSSERoute(superGraphPath);
4865
5156
  return;
4866
5157
  }
4867
5158
  try {
@@ -4887,8 +5178,16 @@ var GraphQLManager = class {
4887
5178
  contextFactory: this.#makeContextFactory()
4888
5179
  });
4889
5180
  const handler = this.#composeFetchMiddleware(rawHandler);
4890
- if (this.sseRouteHandle !== void 0) this.sseRouteHandle.dispose();
4891
- this.sseRouteHandle = this.httpAdapter.mount(ssePath, handler, { exact: true });
5181
+ this.#disposeSSERoute(basePath);
5182
+ this.sseRouteHandles.set(ssePath, this.httpAdapter.mount(ssePath, handler, { exact: true }));
5183
+ }
5184
+ #disposeSSERoute(basePath) {
5185
+ const ssePath = basePath + "/stream";
5186
+ const handle = this.sseRouteHandles.get(ssePath);
5187
+ if (handle !== void 0) {
5188
+ handle.dispose();
5189
+ this.sseRouteHandles.delete(ssePath);
5190
+ }
4892
5191
  }
4893
5192
  /**
4894
5193
  * Compose the request-level fetch middleware chain. Auth runs first
@@ -5172,7 +5471,6 @@ const DocumentModelStateDTO = z.object({
5172
5471
  }).strip();
5173
5472
  const DocumentModelResultPageDTO = z.object({
5174
5473
  items: z.array(DocumentModelStateDTO),
5175
- totalCount: z.number().int(),
5176
5474
  hasNextPage: z.boolean(),
5177
5475
  hasPreviousPage: z.boolean(),
5178
5476
  cursor: z.string().nullable().optional()
@@ -5193,7 +5491,20 @@ const PHDocumentDTO = z.object({
5193
5491
  }).strip();
5194
5492
  const PHDocumentResultPageDTO = z.object({
5195
5493
  items: z.array(PHDocumentDTO),
5196
- totalCount: z.number().int(),
5494
+ hasNextPage: z.boolean(),
5495
+ hasPreviousPage: z.boolean(),
5496
+ cursor: z.string().nullable().optional()
5497
+ }).strip();
5498
+ const DocumentRelationshipDTO = z.object({
5499
+ sourceId: z.string(),
5500
+ targetId: z.string(),
5501
+ relationshipType: z.string(),
5502
+ metadata: JSONObjectDTO.nullable().optional(),
5503
+ createdAt: DateTimeDTO,
5504
+ updatedAt: DateTimeDTO
5505
+ }).strip();
5506
+ const DocumentRelationshipResultPageDTO = z.object({
5507
+ items: z.array(DocumentRelationshipDTO),
5197
5508
  hasNextPage: z.boolean(),
5198
5509
  hasPreviousPage: z.boolean(),
5199
5510
  cursor: z.string().nullable().optional()
@@ -5248,7 +5559,6 @@ const OperationDTO = z.object({
5248
5559
  }).strip();
5249
5560
  const OperationResultPageDTO = z.object({
5250
5561
  items: z.array(OperationDTO),
5251
- totalCount: z.number().int(),
5252
5562
  hasNextPage: z.boolean(),
5253
5563
  hasPreviousPage: z.boolean(),
5254
5564
  cursor: z.string().nullable().optional()
@@ -5301,6 +5611,12 @@ const operationValidators = {
5301
5611
  GetDocumentIncomingRelationships: (data) => {
5302
5612
  if (data.documentIncomingRelationships) PHDocumentResultPageDTO.parse(data.documentIncomingRelationships);
5303
5613
  },
5614
+ GetDocumentOutgoingRelationshipEdges: (data) => {
5615
+ if (data.documentOutgoingRelationshipEdges) DocumentRelationshipResultPageDTO.parse(data.documentOutgoingRelationshipEdges);
5616
+ },
5617
+ GetDocumentIncomingRelationshipEdges: (data) => {
5618
+ if (data.documentIncomingRelationshipEdges) DocumentRelationshipResultPageDTO.parse(data.documentIncomingRelationshipEdges);
5619
+ },
5304
5620
  FindDocuments: (data) => {
5305
5621
  if (data.findDocuments) PHDocumentResultPageDTO.parse(data.findDocuments);
5306
5622
  },
@@ -5334,6 +5650,9 @@ const operationValidators = {
5334
5650
  AddRelationship: (data) => {
5335
5651
  if (data.addRelationship) PHDocumentDTO.parse(data.addRelationship);
5336
5652
  },
5653
+ UpdateRelationship: (data) => {
5654
+ if (data.updateRelationship) PHDocumentDTO.parse(data.updateRelationship);
5655
+ },
5337
5656
  RemoveRelationship: (data) => {
5338
5657
  if (data.removeRelationship) PHDocumentDTO.parse(data.removeRelationship);
5339
5658
  },
@@ -5375,7 +5694,7 @@ function createReactorGraphQLClient(url, fetchImpl = fetch, headers) {
5375
5694
  }
5376
5695
  //#endregion
5377
5696
  //#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";
5697
+ 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
5698
  //#endregion
5380
5699
  //#region src/graphql/reactor/pubsub.ts
5381
5700
  const pubSub = new PubSub();
@@ -5386,26 +5705,79 @@ const SUBSCRIPTION_TRIGGERS = {
5386
5705
  DOCUMENT_CHANGES: "DOCUMENT_CHANGES",
5387
5706
  JOB_CHANGES: "JOB_CHANGES"
5388
5707
  };
5389
- let globalDocumentUnsubscribe = null;
5390
- let documentSubscriberCount = 0;
5391
- function ensureGlobalDocumentSubscription(reactorClient) {
5392
- if (documentSubscriberCount === 0) globalDocumentUnsubscribe = reactorClient.subscribe({}, (event) => {
5393
- const payload = {
5394
- documentChanges: event,
5395
- search: {},
5396
- view: void 0
5708
+ function subjectKey(subject) {
5709
+ return `${subject.address ?? ""}|${subject.key ?? ""}`;
5710
+ }
5711
+ /**
5712
+ * The reactor's change feed, read once per distinct subject and shared by that
5713
+ * subject's GraphQL subscriptions. Each feed is read as its subject, so the
5714
+ * client withholds what that subject may not read; one feed read with no view
5715
+ * would gate every subscriber as the host.
5716
+ */
5717
+ var DocumentChangeFeed = class {
5718
+ reactorClient;
5719
+ #pubSub = new PubSub();
5720
+ #feeds = /* @__PURE__ */ new Map();
5721
+ constructor(reactorClient) {
5722
+ this.reactorClient = reactorClient;
5723
+ }
5724
+ subscribe(subject) {
5725
+ const key = subjectKey(subject);
5726
+ const topic = `${SUBSCRIPTION_TRIGGERS.DOCUMENT_CHANGES}:${key}`;
5727
+ const iterator = this.#pubSub.asyncIterableIterator(topic);
5728
+ this.#acquire(key, topic, subject);
5729
+ let released = false;
5730
+ const release = () => {
5731
+ if (!released) {
5732
+ released = true;
5733
+ this.#release(key);
5734
+ }
5397
5735
  };
5398
- pubSub.publish(SUBSCRIPTION_TRIGGERS.DOCUMENT_CHANGES, payload);
5399
- });
5400
- documentSubscriberCount++;
5401
- return () => {
5402
- documentSubscriberCount--;
5403
- if (documentSubscriberCount === 0 && globalDocumentUnsubscribe) {
5404
- globalDocumentUnsubscribe();
5405
- globalDocumentUnsubscribe = null;
5736
+ return {
5737
+ next: () => iterator.next(),
5738
+ return: () => {
5739
+ release();
5740
+ return iterator.return();
5741
+ },
5742
+ throw: (error) => {
5743
+ release();
5744
+ return iterator.throw(error);
5745
+ },
5746
+ [Symbol.asyncIterator]() {
5747
+ return this;
5748
+ }
5749
+ };
5750
+ }
5751
+ #acquire(key, topic, subject) {
5752
+ const feed = this.#feeds.get(key);
5753
+ if (feed) {
5754
+ feed.subscribers++;
5755
+ return;
5406
5756
  }
5407
- };
5408
- }
5757
+ const view = { subject };
5758
+ const unsubscribe = this.reactorClient.subscribe({}, (event) => {
5759
+ const payload = {
5760
+ documentChanges: event,
5761
+ search: {},
5762
+ view
5763
+ };
5764
+ this.#pubSub.publish(topic, payload);
5765
+ }, view);
5766
+ this.#feeds.set(key, {
5767
+ unsubscribe,
5768
+ subscribers: 1
5769
+ });
5770
+ }
5771
+ #release(key) {
5772
+ const feed = this.#feeds.get(key);
5773
+ if (!feed) return;
5774
+ feed.subscribers--;
5775
+ if (feed.subscribers === 0) {
5776
+ this.#feeds.delete(key);
5777
+ feed.unsubscribe();
5778
+ }
5779
+ }
5780
+ };
5409
5781
  const activeJobSubscriptions = /* @__PURE__ */ new Map();
5410
5782
  function ensureJobSubscription(reactorClient, jobId) {
5411
5783
  let subscription = activeJobSubscriptions.get(jobId);
@@ -5422,7 +5794,7 @@ function ensureJobSubscription(reactorClient, jobId) {
5422
5794
  createdAt: jobInfo.createdAtUtcIso,
5423
5795
  completedAt: jobInfo.completedAtUtcIso ?? null,
5424
5796
  error: jobInfo.error?.message ?? null,
5425
- result: jobInfo.result ?? {}
5797
+ result: jobInfo.result ?? null
5426
5798
  },
5427
5799
  jobId,
5428
5800
  documentId: jobInfo.documentId
@@ -5470,6 +5842,11 @@ var ReactorSubgraph = class extends BaseSubgraph {
5470
5842
  }
5471
5843
  name = "r";
5472
5844
  hasSubscriptions = true;
5845
+ #documentChanges;
5846
+ get documentChanges() {
5847
+ this.#documentChanges ??= new DocumentChangeFeed(this.reactorClient);
5848
+ return this.#documentChanges;
5849
+ }
5473
5850
  /**
5474
5851
  * Check operation-level permissions for an array of actions.
5475
5852
  * Delegates to base assertCanExecuteOperation for each action.
@@ -5500,6 +5877,38 @@ var ReactorSubgraph = class extends BaseSubgraph {
5500
5877
  }
5501
5878
  }
5502
5879
  /**
5880
+ * Drops the relationship edges whose far-end document the caller cannot read.
5881
+ *
5882
+ * An edge names two documents, so returning it discloses the far end's id. The
5883
+ * document-shaped relationship queries already withhold far-end documents the
5884
+ * caller cannot read; the edge-shaped queries have to withhold the same ones,
5885
+ * or they become a way to enumerate around that check. Supreme admins read
5886
+ * everything, so the per-edge check is skipped for them.
5887
+ *
5888
+ * The cursor describes where to resume in the underlying unfiltered stream,
5889
+ * so it is left as it came. A filtered page is therefore often shorter than
5890
+ * the requested limit, and that is the stream continuing rather than a sign
5891
+ * it ended.
5892
+ */
5893
+ async filterRelationshipEdges(page, farEnd, ctx) {
5894
+ if (this.authorizationService.isSupremeAdmin(ctx.user?.address)) return page;
5895
+ const decisions = /* @__PURE__ */ new Map();
5896
+ const items = [];
5897
+ for (const edge of page.items) {
5898
+ const documentId = edge[farEnd];
5899
+ let canRead = decisions.get(documentId);
5900
+ if (canRead === void 0) {
5901
+ canRead = await this.canReadDocument(documentId, ctx);
5902
+ decisions.set(documentId, canRead);
5903
+ }
5904
+ if (canRead) items.push(edge);
5905
+ }
5906
+ return {
5907
+ ...page,
5908
+ items
5909
+ };
5910
+ }
5911
+ /**
5503
5912
  * Adds to `forbidden` the canonical document ids in `syncOps` that the caller
5504
5913
  * cannot read, checking each distinct id once. Sync operation document ids are
5505
5914
  * canonical (never slugs), so no resolution is needed.
@@ -5527,14 +5936,18 @@ var ReactorSubgraph = class extends BaseSubgraph {
5527
5936
  * queued for the adopter.
5528
5937
  *
5529
5938
  * 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.
5939
+ * signal to authenticate again rather than as a transport failure. Pushes are
5940
+ * refused the same way -- a push interacts with the same private queue, and
5941
+ * reports the liveness that keeps it from being reclaimed -- but they never
5942
+ * adopt, since the push path has no drive check to clear the claimant.
5531
5943
  *
5532
- * Nothing is enforced or adopted without a serving gate. Below
5533
- * `authEnforcement` there is no policy being enforced for the channel to
5534
- * belong to, and refusing a poll there would break sync for no gain.
5944
+ * Nothing is enforced or adopted without a serving gate: with no gate the
5945
+ * channel serves every subject alike, so there is no subject for it to
5946
+ * belong to.
5535
5947
  */
5536
- async #bindOrRefuseChannel(channelId, ctx) {
5948
+ async #bindOrRefuseChannel(channelId, ctx, options = {}) {
5537
5949
  if (!this.syncServingGate) return;
5950
+ const { adopt = true, action = "poll" } = options;
5538
5951
  let remote;
5539
5952
  try {
5540
5953
  remote = this.syncManager.getById(channelId);
@@ -5544,10 +5957,10 @@ var ReactorSubgraph = class extends BaseSubgraph {
5544
5957
  const bound = remote.meta.options.boundAddress;
5545
5958
  const address = ctx.user?.address;
5546
5959
  if (bound === void 0) {
5547
- if (address !== void 0) await this.syncManager.bindRemote(channelId, address);
5960
+ if (adopt && address !== void 0) await this.syncManager.bindRemote(channelId, address);
5548
5961
  return;
5549
5962
  }
5550
- if (bound !== address) throw new ForbiddenError("to poll this sync channel");
5963
+ if (bound !== address) throw new ForbiddenError(`to ${action} this sync channel`);
5551
5964
  }
5552
5965
  /** The address a channel created by this request belongs to, if any. */
5553
5966
  #creatorBinding(ctx) {
@@ -5635,6 +6048,34 @@ var ReactorSubgraph = class extends BaseSubgraph {
5635
6048
  throw error;
5636
6049
  }
5637
6050
  },
6051
+ documentOutgoingRelationshipEdges: async (_parent, args, ctx) => {
6052
+ this.logger.debug("documentOutgoingRelationshipEdges(@args)", args);
6053
+ try {
6054
+ const handle = await this.assertCanRead(args.sourceIdentifier, ctx);
6055
+ const result = await documentOutgoingRelationshipEdges(this.reactorClient, {
6056
+ ...args,
6057
+ sourceIdentifier: handle.fetchIdentifier
6058
+ }, this.viewSubject(ctx));
6059
+ return await this.filterRelationshipEdges(result, "targetId", ctx);
6060
+ } catch (error) {
6061
+ this.logger.error("Error in documentOutgoingRelationshipEdges: @Error", error);
6062
+ throw error;
6063
+ }
6064
+ },
6065
+ documentIncomingRelationshipEdges: async (_parent, args, ctx) => {
6066
+ this.logger.debug("documentIncomingRelationshipEdges(@args)", args);
6067
+ try {
6068
+ const handle = await this.assertCanRead(args.targetIdentifier, ctx);
6069
+ const result = await documentIncomingRelationshipEdges(this.reactorClient, {
6070
+ ...args,
6071
+ targetIdentifier: handle.fetchIdentifier
6072
+ }, this.viewSubject(ctx));
6073
+ return await this.filterRelationshipEdges(result, "sourceId", ctx);
6074
+ } catch (error) {
6075
+ this.logger.error("Error in documentIncomingRelationshipEdges: @Error", error);
6076
+ throw error;
6077
+ }
6078
+ },
5638
6079
  findDocuments: async (_parent, args, ctx) => {
5639
6080
  this.logger.debug("findDocuments(@args)", args);
5640
6081
  try {
@@ -5656,10 +6097,10 @@ var ReactorSubgraph = class extends BaseSubgraph {
5656
6097
  throw error;
5657
6098
  }
5658
6099
  },
5659
- jobStatus: async (_parent, args) => {
6100
+ jobStatus: async (_parent, args, ctx) => {
5660
6101
  this.logger.debug("jobStatus(@args)", args);
5661
6102
  try {
5662
- return await jobStatus(this.reactorClient, args);
6103
+ return await jobStatus(this.reactorClient, args, (id) => this.servesDocument(id, ctx));
5663
6104
  } catch (error) {
5664
6105
  this.logger.error("Error in jobStatus: @Error", error);
5665
6106
  throw error;
@@ -5681,6 +6122,13 @@ var ReactorSubgraph = class extends BaseSubgraph {
5681
6122
  throw error;
5682
6123
  }
5683
6124
  },
6125
+ /**
6126
+ * Gated on reading the document, the same gate every other read here
6127
+ * carries: a verdict is derived from the document's policy, so a caller
6128
+ * who may not read the document may not preflight against it either.
6129
+ * That gate decides whether the query runs; the answer comes entirely
6130
+ * from the reactor's decision model.
6131
+ */
5684
6132
  evaluateActions: async (_parent, args, ctx) => {
5685
6133
  this.logger.debug("evaluateActions(@args)", args);
5686
6134
  try {
@@ -5712,8 +6160,10 @@ var ReactorSubgraph = class extends BaseSubgraph {
5712
6160
  if (await this.authorizationService.canRead(driveId, void 0)) await this.#collectForbiddenDocuments(remote.channel.outbox.items, forbiddenIds, ctx);
5713
6161
  await this.#collectForbiddenDocuments(remote.channel.deadLetter.items, forbiddenIds, ctx);
5714
6162
  }
5715
- const heldOpIds = this.syncServingGate ? await collectHeldSyncOperations([...remote.channel.outbox.items, ...remote.channel.deadLetter.items], this.syncServingGate, this.viewSubject(ctx)) : /* @__PURE__ */ new Set();
5716
- const { envelopes, ackOrdinal, deadLetters, hasMore } = pollSyncEnvelopes(this.syncManager, args, forbiddenIds, heldOpIds);
6163
+ const gate = this.syncServingGate;
6164
+ const gated = gate ? [...remote.channel.outbox.items, ...remote.channel.deadLetter.items] : void 0;
6165
+ const heldOpIds = gate && gated ? await collectHeldSyncOperations(gated, gate, this.viewSubject(ctx)) : /* @__PURE__ */ new Set();
6166
+ const { envelopes, ackOrdinal, deadLetters, hasMore } = pollSyncEnvelopes(this.syncManager, args, forbiddenIds, heldOpIds, gated && new Set(gated.map((syncOp) => syncOp.id)));
5717
6167
  return {
5718
6168
  envelopes,
5719
6169
  ackOrdinal,
@@ -5734,7 +6184,7 @@ var ReactorSubgraph = class extends BaseSubgraph {
5734
6184
  const parent = await document(this.reactorClient, { identifier: args.parentIdentifier });
5735
6185
  await this.assertCanWriteCanonical(parent.document.id, ctx);
5736
6186
  } else this.assertCanCreate(ctx);
5737
- const result = await createDocument(this.reactorClient, args, this.graphqlManager.reactorDriveClient);
6187
+ const result = await createDocument(this.reactorClient, args, this.graphqlManager.reactorDriveClient, this.viewSubject(ctx));
5738
6188
  if (result?.id && isDriveContainerType(result.documentType)) this.graphqlManager.driveOwnershipCache.add(result.id);
5739
6189
  if (ctx.user?.address && result?.id) await this.documentPermissionService?.initializeDocumentProtection(result.id, ctx.user.address, this.authorizationService.config.defaultProtection);
5740
6190
  return result;
@@ -5750,7 +6200,7 @@ var ReactorSubgraph = class extends BaseSubgraph {
5750
6200
  const parent = await document(this.reactorClient, { identifier: args.parentIdentifier });
5751
6201
  await this.assertCanWriteCanonical(parent.document.id, ctx);
5752
6202
  } else this.assertCanCreate(ctx);
5753
- const result = await createEmptyDocument(this.reactorClient, args, this.graphqlManager.reactorDriveClient);
6203
+ const result = await createEmptyDocument(this.reactorClient, args, this.graphqlManager.reactorDriveClient, this.viewSubject(ctx));
5754
6204
  if (result?.id && isDriveContainerType(result.documentType)) this.graphqlManager.driveOwnershipCache.add(result.id);
5755
6205
  if (ctx.user?.address && result?.id) await this.documentPermissionService?.initializeDocumentProtection(result.id, ctx.user.address, this.authorizationService.config.defaultProtection);
5756
6206
  return result;
@@ -5766,7 +6216,7 @@ var ReactorSubgraph = class extends BaseSubgraph {
5766
6216
  return await execute(this.reactorClient, {
5767
6217
  ...args,
5768
6218
  documentIdentifier: handle.fetchIdentifier
5769
- });
6219
+ }, this.viewSubject(ctx));
5770
6220
  } catch (error) {
5771
6221
  this.logger.error("Error in execute(@args): @Error", args, error);
5772
6222
  throw error;
@@ -5792,7 +6242,7 @@ var ReactorSubgraph = class extends BaseSubgraph {
5792
6242
  return await mutateDocument(this.reactorClient, {
5793
6243
  ...args,
5794
6244
  documentIdentifier: handle.fetchIdentifier
5795
- });
6245
+ }, this.viewSubject(ctx));
5796
6246
  } catch (error) {
5797
6247
  this.logger.error("Error in mutateDocument(@args): @Error", args, error);
5798
6248
  throw error;
@@ -5818,7 +6268,7 @@ var ReactorSubgraph = class extends BaseSubgraph {
5818
6268
  return await renameDocument(this.reactorClient, {
5819
6269
  ...args,
5820
6270
  documentIdentifier: handle.fetchIdentifier
5821
- });
6271
+ }, void 0, this.viewSubject(ctx));
5822
6272
  } catch (error) {
5823
6273
  this.logger.error("Error in renameDocument(@args): @Error", args, error);
5824
6274
  throw error;
@@ -5831,7 +6281,7 @@ var ReactorSubgraph = class extends BaseSubgraph {
5831
6281
  return await setPreferredEditor(this.reactorClient, {
5832
6282
  ...args,
5833
6283
  documentIdentifier: handle.fetchIdentifier
5834
- });
6284
+ }, void 0, this.viewSubject(ctx));
5835
6285
  } catch (error) {
5836
6286
  this.logger.error("Error in setPreferredEditor(@args): @Error", args, error);
5837
6287
  throw error;
@@ -5844,12 +6294,25 @@ var ReactorSubgraph = class extends BaseSubgraph {
5844
6294
  return await addRelationship(this.reactorClient, {
5845
6295
  ...args,
5846
6296
  sourceIdentifier: handle.fetchIdentifier
5847
- });
6297
+ }, this.viewSubject(ctx));
5848
6298
  } catch (error) {
5849
6299
  this.logger.error("Error in addRelationship(@args): @Error", args, error);
5850
6300
  throw error;
5851
6301
  }
5852
6302
  },
6303
+ updateRelationship: async (_parent, args, ctx) => {
6304
+ this.logger.debug("updateRelationship(@args)", args);
6305
+ try {
6306
+ const handle = await this.assertCanWrite(args.sourceIdentifier, ctx);
6307
+ return await updateRelationship(this.reactorClient, {
6308
+ ...args,
6309
+ sourceIdentifier: handle.fetchIdentifier
6310
+ }, this.viewSubject(ctx));
6311
+ } catch (error) {
6312
+ this.logger.error("Error in updateRelationship(@args): @Error", args, error);
6313
+ throw error;
6314
+ }
6315
+ },
5853
6316
  removeRelationship: async (_parent, args, ctx) => {
5854
6317
  this.logger.debug("removeRelationship(@args)", args);
5855
6318
  try {
@@ -5857,7 +6320,7 @@ var ReactorSubgraph = class extends BaseSubgraph {
5857
6320
  return await removeRelationship(this.reactorClient, {
5858
6321
  ...args,
5859
6322
  sourceIdentifier: handle.fetchIdentifier
5860
- });
6323
+ }, this.viewSubject(ctx));
5861
6324
  } catch (error) {
5862
6325
  this.logger.error("Error in removeRelationship(@args): @Error", args, error);
5863
6326
  throw error;
@@ -5872,7 +6335,7 @@ var ReactorSubgraph = class extends BaseSubgraph {
5872
6335
  ...args,
5873
6336
  sourceParentIdentifier: sourceHandle.fetchIdentifier,
5874
6337
  targetParentIdentifier: targetHandle.fetchIdentifier
5875
- });
6338
+ }, this.viewSubject(ctx));
5876
6339
  } catch (error) {
5877
6340
  this.logger.error("Error in moveRelationship(@args): @Error @args", error, args);
5878
6341
  throw error;
@@ -5928,6 +6391,16 @@ var ReactorSubgraph = class extends BaseSubgraph {
5928
6391
  pushSyncEnvelopes: async (_parent, args, ctx) => {
5929
6392
  this.logger.debug("pushSyncEnvelopes(@args)", args);
5930
6393
  try {
6394
+ const boundChannelIds = /* @__PURE__ */ new Set();
6395
+ for (const envelope of args.envelopes) {
6396
+ const channelId = envelope.channelMeta.id;
6397
+ if (boundChannelIds.has(channelId)) continue;
6398
+ boundChannelIds.add(channelId);
6399
+ await this.#bindOrRefuseChannel(channelId, ctx, {
6400
+ adopt: false,
6401
+ action: "push to"
6402
+ });
6403
+ }
5931
6404
  const checkedOperations = /* @__PURE__ */ new Map();
5932
6405
  for (const envelope of args.envelopes) for (const op of envelope.operations ?? []) {
5933
6406
  const documentId = op.context.documentId;
@@ -5973,10 +6446,7 @@ var ReactorSubgraph = class extends BaseSubgraph {
5973
6446
  documentChanges: {
5974
6447
  subscribe: (rootValue, args, ctx, info) => {
5975
6448
  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) => {
6449
+ return withFilter(() => this.documentChanges.subscribe(this.viewSubject(ctx)), async (payload, filterArgs, filterCtx) => {
5980
6450
  if (!payload) return false;
5981
6451
  const search = {
5982
6452
  type: filterArgs?.search?.type ?? void 0,
@@ -6003,6 +6473,7 @@ var ReactorSubgraph = class extends BaseSubgraph {
6003
6473
  }, async (payload, filterArgs, filterCtx) => {
6004
6474
  if (!payload || !filterArgs) return false;
6005
6475
  if (!matchesJobFilter(payload, filterArgs)) return false;
6476
+ if (!await this.servesDocument(payload.documentId, filterCtx)) return false;
6006
6477
  if (this.authorizationService.isSupremeAdmin(filterCtx?.user?.address)) return true;
6007
6478
  if (!payload.documentId) return false;
6008
6479
  return this.canReadDocument(payload.documentId, filterCtx);
@@ -6031,10 +6502,10 @@ const ADMIN_USERS = getAdminUsers();
6031
6502
  //#endregion
6032
6503
  //#region src/graphql/system/version.ts
6033
6504
  function getVersion() {
6034
- return "6.2.3-dev.3";
6505
+ return "6.2.3-dev.30";
6035
6506
  }
6036
6507
  function getGitHash() {
6037
- return "95368873997a75b7682107123c76184d56d51741";
6508
+ return "749528c71e7ec43fe26897b49fc818fbc483d7dc";
6038
6509
  }
6039
6510
  function getGitUrl() {
6040
6511
  return buildTreeUrl(getGitHash());
@@ -6135,7 +6606,8 @@ var HttpPackageLoader = class {
6135
6606
  async loadUpgradeManifests(packageSpec) {
6136
6607
  const { name: packageName } = this.parsePackageSpec(packageSpec);
6137
6608
  if (!this.isValidPackageName(packageName)) throw new Error(`Invalid package name: ${packageName}`);
6138
- const manifests = extractUpgradeManifests(await import(`${this.registryUrl}-/cdn/${packageSpec}/node/document-models/index.mjs`));
6609
+ const module = await import(`${this.registryUrl}-/cdn/${packageSpec}/node/document-models/index.mjs`);
6610
+ const manifests = extractUpgradeManifests(module);
6139
6611
  if (manifests.length > 0) this.logger.verbose(`Loaded ${manifests.length} upgrade manifests from ${packageName}`);
6140
6612
  return manifests;
6141
6613
  }
@@ -6161,6 +6633,22 @@ var HttpPackageLoader = class {
6161
6633
  this.logger.verbose(`No processor factory found in ${packageName}`);
6162
6634
  return null;
6163
6635
  }
6636
+ async loadPieces(packageSpec) {
6637
+ const { name: packageName } = this.parsePackageSpec(packageSpec);
6638
+ if (!this.isValidPackageName(packageName)) throw new Error(`Invalid package name: ${packageName}`);
6639
+ const base = `${this.registryUrl}-/cdn/${packageSpec}/node/pieces/`;
6640
+ this.logger.verbose(`Importing pieces from: ${base}index.mjs`);
6641
+ let module;
6642
+ try {
6643
+ module = await import(`${base}index.mjs`);
6644
+ } catch (error) {
6645
+ this.logger.verbose(`No pieces found for: ${packageName}`, error);
6646
+ return [];
6647
+ }
6648
+ const pieces = piecesFromCdnList(module, base, packageName, this.logger);
6649
+ this.logger.verbose(`Loaded ${pieces.length} pieces from ${packageName}`);
6650
+ return pieces;
6651
+ }
6164
6652
  /**
6165
6653
  * Load document models from multiple packages.
6166
6654
  * Continues loading even if some packages fail.
@@ -6258,8 +6746,13 @@ var HttpDocumentModelLoader = class {
6258
6746
  * This class is used to load packages using the import keyword.
6259
6747
  */
6260
6748
  var ImportPackageLoader = class {
6749
+ hostDir;
6261
6750
  logger = childLogger(["reactor-api", "import-loader"]);
6262
6751
  name = "ImportPackageLoader";
6752
+ constructor(hostDir = process.cwd()) {
6753
+ this.hostDir = hostDir;
6754
+ }
6755
+ reportedUnbuilt = /* @__PURE__ */ new Set();
6263
6756
  async loadDocumentModels(identifier) {
6264
6757
  this.logger.verbose(`Loading document models from package: ${identifier}`);
6265
6758
  const pkgModule = await loadDocumentModels(identifier);
@@ -6302,6 +6795,31 @@ var ImportPackageLoader = class {
6302
6795
  this.logger.verbose(` ➜ No Processor Factory found: ${identifier}`);
6303
6796
  return null;
6304
6797
  }
6798
+ async loadPieces(identifier) {
6799
+ this.logger.verbose(`Loading pieces from package: ${identifier}`);
6800
+ const { root, listPath } = pieceListLocation(identifier, this.hostDir);
6801
+ if (!existsSync(listPath)) {
6802
+ this.reportUnbuilt(identifier, root, listPath);
6803
+ return [];
6804
+ }
6805
+ const module = await import(
6806
+ /* @vite-ignore */
6807
+ pathToFileURL(listPath).href
6808
+ );
6809
+ const pieces = piecesFromListModule(module, {
6810
+ root,
6811
+ identifier,
6812
+ logger: this.logger
6813
+ });
6814
+ this.logger.verbose(` ➜ Loaded ${pieces.length} Pieces from: ${identifier}`);
6815
+ return pieces;
6816
+ }
6817
+ reportUnbuilt(identifier, root, listPath) {
6818
+ if (!isAbsolute(identifier) || this.reportedUnbuilt.has(identifier)) return;
6819
+ if (!existsSync(join(root, PIECE_SOURCE_LIST))) return;
6820
+ this.reportedUnbuilt.add(identifier);
6821
+ this.logger.warn("@pkg declares pieces but @list was never built; run `ph build`", identifier, listPath);
6822
+ }
6305
6823
  };
6306
6824
  //#endregion
6307
6825
  //#region src/packages/package-manager.ts
@@ -6317,10 +6835,12 @@ var ImportPackageLoader = class {
6317
6835
  * either bare (`'pkg'`) or as a subpath (`'pkg/subgraphs'`), since loaders
6318
6836
  * import sub-entries like `${pkg}/subgraphs` / `${pkg}/document-models`.
6319
6837
  */
6320
- function isExpectedLoaderMiss(error, pkg) {
6838
+ function isExpectedLoaderMiss(error, pkg, subPath) {
6321
6839
  if (!(error instanceof Error)) return false;
6322
6840
  const code = error.code;
6323
6841
  if (code === "ERR_UNSUPPORTED_DIR_IMPORT") return true;
6842
+ if (code === "ERR_PACKAGE_PATH_NOT_EXPORTED" && subPath !== void 0 && error.message.includes(`'./${subPath}'`)) return true;
6843
+ if (subPath !== void 0 && new RegExp(`^Failed to fetch \\S*/${subPath}/index\\.mjs: 404$`).test(error.message)) return true;
6324
6844
  if (error.message.startsWith("Invalid package name:")) return true;
6325
6845
  if (code === "ERR_MODULE_NOT_FOUND") return error.message.includes(`'${pkg}'`) || error.message.includes(`"${pkg}"`) || error.message.includes(`'${pkg}/`) || error.message.includes(`"${pkg}/`);
6326
6846
  return false;
@@ -6336,12 +6856,14 @@ function getUniqueUpgradeManifests(...upgradeManifests) {
6336
6856
  return Array.from(uniqueManifests.values());
6337
6857
  }
6338
6858
  var PackageManager = class {
6859
+ options;
6339
6860
  logger = childLogger(["reactor-api", "package-manager"]);
6340
6861
  loaders;
6341
6862
  docModelsMap = /* @__PURE__ */ new Map();
6342
6863
  upgradeManifestsMap = /* @__PURE__ */ new Map();
6343
6864
  subgraphsMap = /* @__PURE__ */ new Map();
6344
6865
  processorMap = /* @__PURE__ */ new Map();
6866
+ piecesMap = /* @__PURE__ */ new Map();
6345
6867
  configWatcher;
6346
6868
  debouncedUpdateCallbacks = /* @__PURE__ */ new Map();
6347
6869
  eventEmitter = new EventEmitter();
@@ -6360,10 +6882,12 @@ var PackageManager = class {
6360
6882
  const upgradeManifestsMap = await this.loadUpgradeManifests(packages);
6361
6883
  const subgraphsMap = await this.loadSubgraphs(packages);
6362
6884
  const processorsMap = await this.loadProcessors(packages);
6885
+ const piecesMap = await this.loadPieces(packages);
6363
6886
  this.upgradeManifestsMap = upgradeManifestsMap;
6364
6887
  this.updatePackagesMap(documentModelsMap);
6365
6888
  this.updateSubgraphsMap(subgraphsMap);
6366
6889
  this.updateProcessorsMap(processorsMap);
6890
+ this.updatePiecesMap(piecesMap);
6367
6891
  try {
6368
6892
  this.subscribePackages(packages);
6369
6893
  } catch (error) {
@@ -6373,7 +6897,8 @@ var PackageManager = class {
6373
6897
  documentModels: getUniqueDocumentModels(...Array.from(documentModelsMap.values())),
6374
6898
  upgradeManifests: this.getUniqueUpgradeManifests(),
6375
6899
  subgraphs: subgraphsMap,
6376
- processors: processorsMap
6900
+ processors: processorsMap,
6901
+ pieces: piecesMap
6377
6902
  };
6378
6903
  }
6379
6904
  async loadDocumentModels(packages) {
@@ -6473,9 +6998,34 @@ var PackageManager = class {
6473
6998
  }
6474
6999
  return processorsMap;
6475
7000
  }
7001
+ async loadPieces(packages) {
7002
+ this.logger.debug(`Loading pieces from packages: ${packages.join(", ")}`);
7003
+ const piecesMap = /* @__PURE__ */ new Map();
7004
+ for (const pkg of packages) {
7005
+ const allPieces = [];
7006
+ const failures = [];
7007
+ let succeeded = false;
7008
+ for (const loader of this.loaders) try {
7009
+ allPieces.push(...await loader.loadPieces(pkg));
7010
+ succeeded = true;
7011
+ break;
7012
+ } catch (error) {
7013
+ failures.push({
7014
+ loader: loader.name,
7015
+ error
7016
+ });
7017
+ this.logger.debug(`[${loader.name}] Failed to load pieces from package ${pkg}`, error);
7018
+ }
7019
+ this.maybeWarnAllLoadersFailed("pieces", pkg, succeeded, failures);
7020
+ if (allPieces.length > 0) this.logger.info("Loaded @count piece(s) from package @pkg: @pieces", allPieces.length, pkg, allPieces.map((piece) => piece.name).join(", "));
7021
+ piecesMap.set(pkg, allPieces);
7022
+ }
7023
+ return piecesMap;
7024
+ }
6476
7025
  maybeWarnAllLoadersFailed(kind, pkg, succeeded, failures) {
6477
7026
  if (succeeded || failures.length === 0) return;
6478
- const realFailures = failures.filter(({ error }) => !isExpectedLoaderMiss(error, pkg));
7027
+ const subPath = kind === "document models" ? "document-models" : kind;
7028
+ const realFailures = failures.filter(({ error }) => !isExpectedLoaderMiss(error, pkg, subPath));
6479
7029
  if (realFailures.length === 0) return;
6480
7030
  const details = realFailures.map(({ loader, error }) => {
6481
7031
  return ` [${loader}] ${error instanceof Error ? error.message : String(error)}`;
@@ -6515,6 +7065,15 @@ var PackageManager = class {
6515
7065
  else processorsMap.set(pkg, pkgProcessors);
6516
7066
  this.updateProcessorsMap(processorsMap);
6517
7067
  }
7068
+ async updatePiecesForPackage(pkg) {
7069
+ this.logger.debug(`Updating pieces for package: ${pkg}`);
7070
+ const pieces = await this.loadPieces([pkg]);
7071
+ const piecesMap = new Map(this.piecesMap);
7072
+ const pkgPieces = pieces.get(pkg) ?? [];
7073
+ if (pkgPieces.length === 0) piecesMap.delete(pkg);
7074
+ else piecesMap.set(pkg, pkgPieces);
7075
+ this.updatePiecesMap(piecesMap);
7076
+ }
6518
7077
  /** Debounced per key so repeated subscribePackages calls reuse one timer. */
6519
7078
  getDebouncedUpdateCallback(key, update) {
6520
7079
  if (!this.debouncedUpdateCallbacks.has(key)) {
@@ -6533,10 +7092,12 @@ var PackageManager = class {
6533
7092
  const onDocumentModels = this.getDebouncedUpdateCallback(`${pkg}:document-models`, () => this.updateDocumentModelsForPackage(pkg));
6534
7093
  const onSubgraphs = this.getDebouncedUpdateCallback(`${pkg}:subgraphs`, () => this.updateSubgraphsForPackage(pkg));
6535
7094
  const onProcessors = this.getDebouncedUpdateCallback(`${pkg}:processors`, () => this.updateProcessorsForPackage(pkg));
7095
+ const onPieces = this.getDebouncedUpdateCallback(`${pkg}:pieces`, () => this.updatePiecesForPackage(pkg));
6536
7096
  for (const loader of this.loaders) {
6537
7097
  if (loader.onDocumentModelsChange) unsubs.push(loader.onDocumentModelsChange(pkg, onDocumentModels));
6538
7098
  if (loader.onSubgraphsChange) unsubs.push(loader.onSubgraphsChange(pkg, onSubgraphs));
6539
7099
  if (loader.onProcessorsChange) unsubs.push(loader.onProcessorsChange(pkg, onProcessors));
7100
+ if (loader.onPiecesChange) unsubs.push(loader.onPiecesChange(pkg, onPieces));
6540
7101
  }
6541
7102
  }
6542
7103
  return () => {
@@ -6584,6 +7145,19 @@ var PackageManager = class {
6584
7145
  this.processorMap = processorsMap;
6585
7146
  this.eventEmitter.emit("processorsChange", processorsMap);
6586
7147
  }
7148
+ updatePiecesMap(piecesMap) {
7149
+ const oldPackages = Array.from(this.piecesMap.keys());
7150
+ const newPackages = Array.from(piecesMap.keys());
7151
+ oldPackages.filter((pkg) => !newPackages.includes(pkg)).forEach((pkg) => {
7152
+ this.logger.info("Removed pieces from: @pkg", pkg);
7153
+ });
7154
+ this.piecesMap = piecesMap;
7155
+ this.eventEmitter.emit("piecesChange", piecesMap);
7156
+ }
7157
+ /** The pieces every loaded package ships, keyed by the package. */
7158
+ getPieces() {
7159
+ return new Map(this.piecesMap);
7160
+ }
6587
7161
  /**
6588
7162
  * Remove `pkg` from all package maps and emit the change events, tearing
6589
7163
  * down everything the package registered (subgraphs, processors, document
@@ -6602,9 +7176,11 @@ var PackageManager = class {
6602
7176
  this.upgradeManifestsMap = dropKey(this.upgradeManifestsMap);
6603
7177
  this.subgraphsMap = dropKey(this.subgraphsMap);
6604
7178
  this.processorMap = dropKey(this.processorMap);
7179
+ this.piecesMap = dropKey(this.piecesMap);
6605
7180
  this.updatePackagesMap(this.docModelsMap);
6606
7181
  this.updateSubgraphsMap(this.subgraphsMap);
6607
7182
  this.updateProcessorsMap(this.processorMap);
7183
+ this.updatePiecesMap(this.piecesMap);
6608
7184
  }
6609
7185
  onDocumentModelsChange(handler) {
6610
7186
  this.eventEmitter.on("documentModelsChange", handler);
@@ -6615,6 +7191,9 @@ var PackageManager = class {
6615
7191
  onProcessorsChange(handler) {
6616
7192
  this.eventEmitter.on("processorsChange", handler);
6617
7193
  }
7194
+ onPiecesChange(handler) {
7195
+ this.eventEmitter.on("piecesChange", handler);
7196
+ }
6618
7197
  };
6619
7198
  //#endregion
6620
7199
  //#region src/config.ts
@@ -6632,20 +7211,48 @@ async function createStartupAttachmentBackend({ db, env = process.env, s3Depende
6632
7211
  }
6633
7212
  //#endregion
6634
7213
  //#region src/services/attachment-access.service.ts
7214
+ /** The scope an attachment's bytes belong to: the document's own domain state. */
7215
+ const ATTACHMENT_SCOPE = "global";
7216
+ /** The branch an attachment reference is resolved against. */
7217
+ const ATTACHMENT_BRANCH = "main";
6635
7218
  const HASH_PATTERN = /^[a-f0-9]{64}$/;
6636
7219
  /**
6637
7220
  * Composes document authorization with the projected document/ref
6638
7221
  * relationship. Order is fixed: validate ref, resolve the canonical document
6639
- * id, check `canRead`, then check the reference index. A denied document
6640
- * never reaches the reference reader, and the facade never touches
6641
- * attachment metadata, storage backends, or presigners.
7222
+ * id, decide the read, check the reactor's read gate serves the document, then
7223
+ * check the reference index and that the subject may read a scope whose
7224
+ * operations reference the attachment. A denied document never reaches the
7225
+ * reference reader, and the facade never touches attachment metadata, storage
7226
+ * backends, or presigners.
7227
+ *
7228
+ * The named document decides alone: an attachment several documents reference
7229
+ * is readable through each of them on that document's own terms.
7230
+ *
7231
+ * The read is decided by the document's own policy when this composition has a
7232
+ * model to enforce it with, and by the host's permission tables when it does
7233
+ * not. That is not a preference between two equivalent answers: a host running
7234
+ * document policies keeps no rows in the permission tables, so asking them
7235
+ * about such a document returns whatever the host-wide policy says — under
7236
+ * `OPEN`, `true`, for everyone. Serving bytes on that answer hands the file to
7237
+ * anyone who learns its hash, which the document's own state may well have told
7238
+ * them. Both are kept because they are the two halves of one rule: an
7239
+ * attachment is readable by whoever may read the document that references it,
7240
+ * and each composition can only express that in its own terms.
6642
7241
  */
6643
7242
  var AttachmentAccessService = class {
6644
- constructor(resolveCanonicalId, authorization, references, projection) {
7243
+ resolveCanonicalId;
7244
+ authorization;
7245
+ references;
7246
+ projection;
7247
+ readGate;
7248
+ scopeGate;
7249
+ constructor(resolveCanonicalId, authorization, references, projection, readGate, scopeGate) {
6645
7250
  this.resolveCanonicalId = resolveCanonicalId;
6646
7251
  this.authorization = authorization;
6647
7252
  this.references = references;
6648
7253
  this.projection = projection;
7254
+ this.readGate = readGate;
7255
+ this.scopeGate = scopeGate;
6649
7256
  }
6650
7257
  async canReadAttachment(request) {
6651
7258
  if (this.projection.status !== "available") return { kind: "projection-unavailable" };
@@ -6657,14 +7264,51 @@ var AttachmentAccessService = class {
6657
7264
  } catch {
6658
7265
  return { kind: "denied" };
6659
7266
  }
6660
- if (!await this.authorization.canRead(documentId, request.userAddress)) return { kind: "denied" };
6661
- if (!await this.references.hasReference(documentId, ref)) return { kind: "denied" };
7267
+ if (!await this.canReadDocument(documentId, request)) return { kind: "denied" };
7268
+ const subject = {
7269
+ address: request.userAddress,
7270
+ key: request.appKey
7271
+ };
7272
+ let served;
7273
+ try {
7274
+ served = await this.readGate.isServed(documentId, { subject });
7275
+ } catch {
7276
+ return { kind: "denied" };
7277
+ }
7278
+ if (!served) return { kind: "denied" };
7279
+ const scopes = await this.references.referencingScopes(documentId, ref);
7280
+ if (scopes.length === 0) return { kind: "denied" };
7281
+ let held;
7282
+ try {
7283
+ held = (await this.readGate.get(documentId, {
7284
+ subject,
7285
+ scopes
7286
+ })).state;
7287
+ } catch {
7288
+ return { kind: "denied" };
7289
+ }
7290
+ if (!scopes.some((scope) => scope in held)) return { kind: "denied" };
6662
7291
  return {
6663
7292
  kind: "allowed",
6664
7293
  documentId,
6665
7294
  ref
6666
7295
  };
6667
7296
  }
7297
+ /**
7298
+ * Whether the caller may read the state the reference lives in.
7299
+ *
7300
+ * A failure here is rethrown rather than turned into a denial. A read side
7301
+ * that is down must not read as a policy: a denial nobody can distinguish
7302
+ * from a refusal is one nobody investigates, and the route answers 500 for a
7303
+ * reason.
7304
+ */
7305
+ async canReadDocument(documentId, request) {
7306
+ if (!this.scopeGate) return this.authorization.canRead(documentId, request.userAddress);
7307
+ return (await this.scopeGate.scopePredicateById(documentId, {
7308
+ address: request.userAddress,
7309
+ key: request.appKey
7310
+ }, ATTACHMENT_BRANCH))(ATTACHMENT_SCOPE);
7311
+ }
6668
7312
  };
6669
7313
  /**
6670
7314
  * Parses and canonicalizes a caller-supplied ref through the shared parser.
@@ -6710,14 +7354,42 @@ function normalizeAttachmentRef(value) {
6710
7354
  * context has no caller, and is rejected. The server never composes this
6711
7355
  * middleware without the auth middleware in the chain (see
6712
7356
  * `assertRequireAuthenticatedCallerAllowed` in `server.ts`).
7357
+ *
7358
+ * `exemptPaths` names the mounted paths that stay reachable anonymously — a
7359
+ * deployment whose product has a flow that runs before sign-in, such as
7360
+ * previewing an invitation from its code, needs somewhere to serve it from.
7361
+ * Each entry is a hole in the floor, so the match is deliberately the
7362
+ * narrowest one that works: the request's pathname, compared in full. A
7363
+ * prefix rule would exempt `/graphql/public-admin` along with
7364
+ * `/graphql/public`, and an exempt surface nobody can enumerate by reading
7365
+ * the configuration is not one anybody audits.
6713
7366
  */
6714
- function createRequireAuthFetchMiddleware() {
7367
+ function createRequireAuthFetchMiddleware(exemptPaths = []) {
7368
+ const exempt = new Set(exemptPaths.map(normalizeExemptPath));
6715
7369
  return (next) => async (request) => {
6716
7370
  if (request.method === "OPTIONS") return next(request);
7371
+ if (exempt.size > 0 && exempt.has(normalizeExemptPath(new URL(request.url).pathname))) return next(request);
6717
7372
  if (getAuthContext(request)?.user === void 0) return new globalThis.Response(JSON.stringify({ error: "Authentication required" }), { status: 401 });
6718
7373
  return next(request);
6719
7374
  };
6720
7375
  }
7376
+ /**
7377
+ * The form both sides of the comparison are reduced to: surrounding whitespace
7378
+ * gone, and one trailing slash gone, so `/graphql/public/` and
7379
+ * `/graphql/public` are the same path. A client that appends a slash is not
7380
+ * asking for a different route, and a configuration that carries one is not
7381
+ * asking for a different rule.
7382
+ *
7383
+ * Deliberately NOT normalized: case, percent-encoding, and a missing leading
7384
+ * slash. The first two would let two spellings of one path diverge from how
7385
+ * the router itself matches; the third is a configuration error, and
7386
+ * `assertRequireAuthenticatedCallerAllowed` refuses to boot on it rather than
7387
+ * leaving an exemption that silently never applies.
7388
+ */
7389
+ function normalizeExemptPath(path) {
7390
+ const trimmed = path.trim();
7391
+ return trimmed.length > 1 && trimmed.endsWith("/") ? trimmed.slice(0, -1) : trimmed;
7392
+ }
6721
7393
  //#endregion
6722
7394
  //#region src/migrations/001_create_document_permissions.ts
6723
7395
  var _001_create_document_permissions_exports = /* @__PURE__ */ __exportAll({
@@ -6992,13 +7664,11 @@ var AuthService = class {
6992
7664
  return new Response(JSON.stringify({ error: "Authentication failed" }), { status: 401 });
6993
7665
  }
6994
7666
  }
7667
+ /** Throws only for a bearer that is present and unusable, as HTTP 401s it. */
6995
7668
  async authenticateWebSocketConnection(connectionParams) {
6996
7669
  if (!this.resolvesIdentity) return null;
6997
7670
  const authHeader = connectionParams.authorization;
6998
- if (!authHeader) {
6999
- if (!this.config.enabled) return null;
7000
- throw new Error("Missing authorization in connection parameters");
7001
- }
7671
+ if (!authHeader) return null;
7002
7672
  const token = authHeader.split(" ")[1];
7003
7673
  if (!token) throw new Error("Invalid authorization format");
7004
7674
  const verified = await this.verifyToken(token);
@@ -7122,6 +7792,7 @@ async function createRenownCredentialVerifier(config = {}) {
7122
7792
  * - Users can be granted permission to execute specific operations
7123
7793
  */
7124
7794
  var DocumentPermissionService = class {
7795
+ db;
7125
7796
  config;
7126
7797
  constructor(db, config = { defaultProtection: false }) {
7127
7798
  this.db = db;
@@ -7392,12 +8063,16 @@ function createGetParentIdsFn(reactorClient) {
7392
8063
  * Authorization gate for the /mcp endpoint (AUTH_REVIEW S-C1). MCP tools have
7393
8064
  * unrestricted reactor access, so access is limited to supreme admins when
7394
8065
  * auth is enabled. OPEN policy is public; any other policy without an
7395
- * AuthService fails closed.
8066
+ * AuthService fails closed. The tools read as the caller: anonymous under OPEN,
8067
+ * else the admin's address and the app key its token carries.
7396
8068
  */
7397
8069
  function createMcpRequestAuthorizer(authService, authorizationService) {
7398
8070
  return async (req) => {
7399
8071
  if (!authService) {
7400
- if (authorizationService.config.policy === AuthorizationPolicy.OPEN) return { authorized: true };
8072
+ if (authorizationService.config.policy === AuthorizationPolicy.OPEN) return {
8073
+ authorized: true,
8074
+ subject: {}
8075
+ };
7401
8076
  return {
7402
8077
  authorized: false,
7403
8078
  status: 401,
@@ -7420,10 +8095,39 @@ function createMcpRequestAuthorizer(authService, authorizationService) {
7420
8095
  status: 403,
7421
8096
  message: "Forbidden: MCP access requires an administrator"
7422
8097
  };
7423
- return { authorized: true };
8098
+ return {
8099
+ authorized: true,
8100
+ subject: {
8101
+ address: context.user.address,
8102
+ key: context.user.appKey
8103
+ }
8104
+ };
7424
8105
  };
7425
8106
  }
7426
8107
  //#endregion
8108
+ //#region src/services/sync-serving-gate.ts
8109
+ /**
8110
+ * The gate sync serving evaluates a document's own policy through, deciding
8111
+ * what the reactor client's read gate decides at the same flag level.
8112
+ *
8113
+ * Under `authEnforcement` it is built here rather than taken off the client
8114
+ * because it also carries the host's closes-by-default setting, which withholds
8115
+ * the domain scopes of a document nobody has policied yet. That answer belongs
8116
+ * to serving alone -- replay must keep reading an uninitialized document in
8117
+ * full -- so the two gates are separate objects over the same model.
8118
+ *
8119
+ * Below it the registered model ignores the auth scope, so the client reads
8120
+ * through the policy alone, and so does serving: a policy written below the
8121
+ * flag still withholds, where serving ungated would sync to a peer what `get`
8122
+ * refuses it.
8123
+ */
8124
+ function buildSyncServingGate(reactorModule, authorizationConfig, logger) {
8125
+ if (!reactorModule) return;
8126
+ const model = readDecisionModel(reactorModule.featureFlags, reactorModule.documentModelRegistry);
8127
+ if (!model) return new SyncScopeGate(new BareReadGate(), reactorModule.documentView, logger);
8128
+ return new SyncScopeGate(new ModelReadGate(model, reactorModule.documentView, reactorModule.featureFlags.authGroups, reactorModule.operationIndex, logger, { withholdUninitialized: authorizationConfig.defaultProtection }), reactorModule.documentView, logger);
8129
+ }
8130
+ //#endregion
7427
8131
  //#region src/services/renown-config.ts
7428
8132
  /** Reading credentials from this reactor's own read model needs a host-supplied
7429
8133
  * verifier: core has no idea which subgraph serves that read model. */
@@ -7605,10 +8309,36 @@ function assertSkipCredentialVerificationAllowed(resolvesCallerIdentity, skipCre
7605
8309
  * require-authenticated-caller middleware would reject every caller —
7606
8310
  * authenticated ones included. Refuse to boot rather than run broken.
7607
8311
  */
7608
- function assertRequireAuthenticatedCallerAllowed(requireAuthenticatedCaller, resolvesCallerIdentity) {
8312
+ function assertRequireAuthenticatedCallerAllowed(requireAuthenticatedCaller, resolvesCallerIdentity, exemptPaths = []) {
8313
+ /**
8314
+ * An exemption is only ever a hole in this floor, so configuring one while
8315
+ * the floor is off is not a harmless no-op: it reads, to anyone auditing the
8316
+ * configuration, as a surface that was deliberately opened — and therefore
8317
+ * as a floor that exists. Refuse rather than let the two drift.
8318
+ */
8319
+ 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.");
8320
+ /**
8321
+ * A path that cannot match anything is worse than no path: the operator
8322
+ * believes the flow is reachable, and finds out when the first user cannot
8323
+ * claim an invitation. `URL.pathname` is always absolute, so an entry that
8324
+ * does not start with `/` never matches, whatever the router does.
8325
+ */
8326
+ const relative = exemptPaths.filter((path) => !path.trim().startsWith("/"));
8327
+ 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
8328
  if (!requireAuthenticatedCaller || resolvesCallerIdentity) return;
7610
8329
  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
8330
  }
8331
+ /**
8332
+ * Deciding an attachment read by the document's policy needs a policy model to
8333
+ * decide with, and that is what auth enforcement supplies. Without one there is
8334
+ * nothing to consult, so the setting would silently leave the permission tables
8335
+ * in charge — configuration that describes a protection the server is not
8336
+ * applying, which is worse than no configuration at all. Refuse instead.
8337
+ */
8338
+ function assertAttachmentPolicyReadsAllowed(attachmentReadsFollowDocumentPolicy, hasDecisionModel) {
8339
+ if (!attachmentReadsFollowDocumentPolicy || hasDecisionModel) return;
8340
+ 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).");
8341
+ }
7612
8342
  function createReadinessGate() {
7613
8343
  let ready = false;
7614
8344
  return {
@@ -7663,27 +8393,6 @@ function makeDbClosers(knexInstance, pglite) {
7663
8393
  return closers;
7664
8394
  }
7665
8395
  /**
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
8396
  * Resolves the gateway adapter type from the `GATEWAY_ADAPTER` env var.
7688
8397
  * Defaults to "apollo" (the federation gateway, production behavior).
7689
8398
  * "stitching" selects the in-process graphql-tools merge gateway (#1565
@@ -7798,6 +8507,8 @@ async function _setupCommonInfrastructure(options) {
7798
8507
  let authEnabled = false;
7799
8508
  let configuredResolveIdentity;
7800
8509
  let configuredRequireAuth;
8510
+ let configuredExemptPaths;
8511
+ let configuredAttachmentPolicyReads;
7801
8512
  let configuredRenown;
7802
8513
  if (options.configFile) {
7803
8514
  const config = getConfig(options.configFile);
@@ -7809,8 +8520,10 @@ async function _setupCommonInfrastructure(options) {
7809
8520
  authEnabled = options.auth.enabled;
7810
8521
  configuredResolveIdentity = options.auth.resolveIdentity;
7811
8522
  configuredRequireAuth = options.auth.requireAuthenticatedCaller;
8523
+ configuredExemptPaths = options.auth.requireAuthenticatedCallerExemptPaths;
8524
+ configuredAttachmentPolicyReads = options.auth.attachmentReadsFollowDocumentPolicy;
7812
8525
  }
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;
8526
+ 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
8527
  if (AUTH_ENABLED !== void 0) authEnabled = AUTH_ENABLED === "true";
7815
8528
  /**
7816
8529
  * Whether the auth middleware reads the bearer and populates `ctx.user`,
@@ -7841,6 +8554,30 @@ async function _setupCommonInfrastructure(options) {
7841
8554
  */
7842
8555
  let requireAuthenticatedCaller = configuredRequireAuth ?? false;
7843
8556
  if (REQUIRE_AUTHENTICATED_CALLER !== void 0) requireAuthenticatedCaller = REQUIRE_AUTHENTICATED_CALLER === "true";
8557
+ /**
8558
+ * The paths the floor above does not apply to. Comma-separated, like
8559
+ * `ADMINS`, because a deployment configures this the same way it configures
8560
+ * everything else: one variable, one line, however many values it has.
8561
+ *
8562
+ * Empty entries are dropped rather than refused, so a trailing comma or a
8563
+ * value split across lines in a manifest is not a boot failure; a value that
8564
+ * cannot ever match is refused, in
8565
+ * {@link assertRequireAuthenticatedCallerAllowed}.
8566
+ */
8567
+ let requireAuthenticatedCallerExemptPaths = configuredExemptPaths ?? [];
8568
+ if (REQUIRE_AUTHENTICATED_CALLER_EXEMPT_PATHS !== void 0) requireAuthenticatedCallerExemptPaths = REQUIRE_AUTHENTICATED_CALLER_EXEMPT_PATHS.split(",").map((path) => path.trim()).filter((path) => path.length > 0);
8569
+ /**
8570
+ * Whether an attachment read is decided by the referencing document's own
8571
+ * policy rather than by the host's permission tables.
8572
+ *
8573
+ * Off by default, and a deployment's choice rather than something derived
8574
+ * from the flags around it. Which model governs those bytes is configuration
8575
+ * in the same sense the storage backend behind them is: a host that has said
8576
+ * nothing keeps exactly the behaviour it has, and one that wants the change
8577
+ * asks for it and can take it back without disturbing anything else.
8578
+ */
8579
+ let attachmentReadsFollowDocumentPolicy = configuredAttachmentPolicyReads ?? false;
8580
+ if (ATTACHMENT_READS_FOLLOW_DOCUMENT_POLICY !== void 0) attachmentReadsFollowDocumentPolicy = ATTACHMENT_READS_FOLLOW_DOCUMENT_POLICY === "true";
7844
8581
  if (ADMINS !== void 0) admins = ADMINS.split(",").map((a) => a.toLowerCase());
7845
8582
  let defaultProtection = false;
7846
8583
  if (DEFAULT_PROTECTION !== void 0) defaultProtection = DEFAULT_PROTECTION.toLowerCase() === "true";
@@ -7855,7 +8592,7 @@ async function _setupCommonInfrastructure(options) {
7855
8592
  const documentPermissionsRequested = options.documentPermissionService !== void 0 || DOCUMENT_PERMISSIONS_ENABLED === "true";
7856
8593
  assertAuthRequiredForDocumentPermissions(authEnabled, documentPermissionsRequested);
7857
8594
  assertSkipCredentialVerificationAllowed(resolveCallerIdentity, skipCredentialVerification, process.env);
7858
- assertRequireAuthenticatedCallerAllowed(requireAuthenticatedCaller, resolveCallerIdentity);
8595
+ assertRequireAuthenticatedCallerAllowed(requireAuthenticatedCaller, resolveCallerIdentity, requireAuthenticatedCallerExemptPaths);
7859
8596
  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
8597
  httpAdapter.getRoute("/health", () => new Response("OK", { status: 200 }));
7861
8598
  const readiness = createReadinessGate();
@@ -7891,9 +8628,9 @@ async function _setupCommonInfrastructure(options) {
7891
8628
  });
7892
8629
  authFetchMiddleware = createAuthFetchMiddleware(authService);
7893
8630
  if (requireAuthenticatedCaller) {
7894
- requireAuthFetchMiddleware = createRequireAuthFetchMiddleware();
7895
- logger.info("Require-authenticated-caller middleware enabled: anonymous callers are rejected with a 401 before any subgraph");
7896
- }
8631
+ requireAuthFetchMiddleware = createRequireAuthFetchMiddleware(requireAuthenticatedCallerExemptPaths);
8632
+ 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");
8633
+ } 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
8634
  }
7898
8635
  const dbClosers = [];
7899
8636
  const { relationalDb, analyticsStore, closers: analyticsClosers } = await initializeDatabaseAndAnalytics(options.dbPath, options.pgliteFactory);
@@ -7917,7 +8654,7 @@ async function _setupCommonInfrastructure(options) {
7917
8654
  await mkdir(attachmentStoragePath, { recursive: true });
7918
8655
  const { db: attachmentDb, knex: attachmentKnex, pglite: attachmentPglite } = getDbClient(options.dbPath, options.pgliteFactory);
7919
8656
  dbClosers.push(...makeDbClosers(attachmentKnex, attachmentPglite));
7920
- const ATTACHMENT_SWEEP_INTERVAL_MS = 3600 * 1e3;
8657
+ const ATTACHMENT_SWEEP_INTERVAL_MS = 36e5;
7921
8658
  const attachmentBackend = await createStartupAttachmentBackend({ db: attachmentDb });
7922
8659
  const attachmentBuilder = new AttachmentBuilder(attachmentDb, attachmentStoragePath).withReservationSweepMs(ATTACHMENT_SWEEP_INTERVAL_MS);
7923
8660
  if (attachmentBackend) attachmentBuilder.withBackend(attachmentBackend);
@@ -7962,13 +8699,14 @@ async function _setupCommonInfrastructure(options) {
7962
8699
  attachmentReferenceIndex,
7963
8700
  packages,
7964
8701
  dbClosers,
7965
- readiness
8702
+ readiness,
8703
+ attachmentReadsFollowDocumentPolicy
7966
8704
  };
7967
8705
  }
7968
8706
  /**
7969
8707
  * Private helper function containing common setup logic for API initialization
7970
8708
  */
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) {
8709
+ 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
8710
  const hostModuleBase = {
7973
8711
  ...createReactorHostModuleBase({
7974
8712
  client: reactorClient,
@@ -8010,7 +8748,8 @@ async function _setupAPI(reactorClient, syncManager, reactorProcessorManager, ht
8010
8748
  const { httpServer, wsServer } = await startServer(httpAdapter, port, options.https, logger);
8011
8749
  const authorizationService = createAuthorizationService(authorizationConfig, documentPermissionService, createGetParentIdsFn(reactorClient));
8012
8750
  logger.info(`Authorization service initialized (policy: ${authorizationConfig.policy})`);
8013
- const attachmentAccess = new AttachmentAccessService(createCanonicalDocumentIdResolver(reactorClient), authorizationService, attachmentReferenceIndex.store, attachmentReferenceProjection);
8751
+ assertAttachmentPolicyReadsAllowed(attachmentReadsFollowDocumentPolicy, syncServingGate !== void 0);
8752
+ const attachmentAccess = new AttachmentAccessService(createCanonicalDocumentIdResolver(reactorClient), authorizationService, attachmentReferenceIndex.store, attachmentReferenceProjection, reactorClient, attachmentReadsFollowDocumentPolicy ? syncServingGate : void 0);
8014
8753
  const coreSubgraphs = DefaultCoreSubgraphs.slice();
8015
8754
  coreSubgraphs.push(ReactorSubgraph);
8016
8755
  if (documentPermissionService) {
@@ -8047,6 +8786,9 @@ async function _setupAPI(reactorClient, syncManager, reactorProcessorManager, ht
8047
8786
  attachmentReferenceIndex,
8048
8787
  attachmentAccess,
8049
8788
  authService,
8789
+ requireAuthenticatedCaller: requireAuthFetchMiddleware !== void 0,
8790
+ authorizationService,
8791
+ relationalDb,
8050
8792
  dispose
8051
8793
  };
8052
8794
  }
@@ -8093,7 +8835,7 @@ function buildApiDispose(args) {
8093
8835
  };
8094
8836
  }
8095
8837
  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);
8838
+ const { port, httpAdapter, httpRoutes, authFetchMiddleware, requireAuthFetchMiddleware, authService, relationalDb, analyticsStore, documentPermissionService, authorizationConfig, attachments, attachmentReferenceIndex, packages, dbClosers, readiness, attachmentReadsFollowDocumentPolicy } = await _setupCommonInfrastructure(options);
8097
8839
  const { documentModels, upgradeManifests, processors, subgraphs } = await packages.init();
8098
8840
  const { module: reactorClientModule, reactorDriveClient, attachmentReferenceProjection = {
8099
8841
  status: "unavailable",
@@ -8110,7 +8852,7 @@ async function initializeAndStartAPI(clientInitializer, options, processorApp) {
8110
8852
  const documentModelRegistry = reactorClientModule.reactorModule?.documentModelRegistry;
8111
8853
  if (!documentModelRegistry) throw new Error("DocumentModelRegistry not available from InProcessReactorClientModule");
8112
8854
  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),
8855
+ ...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
8856
  client: reactorClient,
8115
8857
  syncManager,
8116
8858
  documentModelRegistry,
@@ -8243,7 +8985,7 @@ var PackageManagementService = class {
8243
8985
  }
8244
8986
  };
8245
8987
  //#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 };
8988
+ 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, redactHeaders, renderGraphqlPlayground, resolvePackageName, resolveRenownConfig, verifyWebhook };
8247
8989
 
8248
8990
  //# sourceMappingURL=index.mjs.map
8249
- //# debugId=5e1dfde6-8724-5f15-80bf-467e42b71238
8991
+ //# debugId=69ff78c8-e6e1-5d53-97e4-a5e8a75417ed