@powerhousedao/reactor-api 6.2.3-dev.23 → 6.2.3-dev.25

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 (34) hide show
  1. package/dist/{adapter-gateway-apollo-CxJNBVyD.mjs → adapter-gateway-apollo-B54G_cXT.mjs} +15 -13
  2. package/dist/{adapter-gateway-apollo-CxJNBVyD.mjs.map → adapter-gateway-apollo-B54G_cXT.mjs.map} +1 -1
  3. package/dist/{adapter-gateway-apollo-DowrX6E_.mjs → adapter-gateway-apollo-D2DNlPY0.mjs} +3 -3
  4. package/dist/{adapter-gateway-mercurius-CZVy88Za.mjs → adapter-gateway-mercurius-rbKlwF9m.mjs} +4 -4
  5. package/dist/{adapter-gateway-mercurius-CZVy88Za.mjs.map → adapter-gateway-mercurius-rbKlwF9m.mjs.map} +1 -1
  6. package/dist/{adapter-gateway-stitching-CaZjvRzx.mjs → adapter-gateway-stitching-Dzv10u8D.mjs} +15 -10
  7. package/dist/{adapter-gateway-stitching-CaZjvRzx.mjs.map → adapter-gateway-stitching-Dzv10u8D.mjs.map} +1 -1
  8. package/dist/{adapter-http-express-D4KBmDcQ.mjs → adapter-http-express-Drbaxszc.mjs} +4 -4
  9. package/dist/{adapter-http-express-D4KBmDcQ.mjs.map → adapter-http-express-Drbaxszc.mjs.map} +1 -1
  10. package/dist/{adapter-http-fastify-U9j7HxQE.mjs → adapter-http-fastify-DMPW_huG.mjs} +6 -5
  11. package/dist/{adapter-http-fastify-U9j7HxQE.mjs.map → adapter-http-fastify-DMPW_huG.mjs.map} +1 -1
  12. package/dist/index.d.mts +595 -461
  13. package/dist/index.d.mts.map +1 -1
  14. package/dist/index.mjs +236 -88
  15. package/dist/index.mjs.map +1 -1
  16. package/dist/{pieces-eqj2RKgd.mjs → pieces-BOwV3Ql9.mjs} +6 -3
  17. package/dist/{pieces-eqj2RKgd.mjs.map → pieces-BOwV3Ql9.mjs.map} +1 -1
  18. package/dist/rolldown-runtime-D7D4PA-g.mjs +15 -0
  19. package/dist/{run-node-handler-Bqn0X6bT.mjs → run-node-handler-CQjyjOuN.mjs} +3 -3
  20. package/dist/{run-node-handler-Bqn0X6bT.mjs.map → run-node-handler-CQjyjOuN.mjs.map} +1 -1
  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 +5 -6
  26. package/dist/src/packages/vite-loader.d.mts.map +1 -1
  27. package/dist/src/packages/vite-loader.mjs +9 -6
  28. package/dist/src/packages/vite-loader.mjs.map +1 -1
  29. package/dist/{types-BXjLvTe3.d.mts → types-D4Qnqo_D.d.mts} +2 -2
  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 +17 -17
  34. package/dist/types-BXjLvTe3.d.mts.map +0 -1
package/dist/index.mjs CHANGED
@@ -1,6 +1,7 @@
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]="ab52a900-0b3c-526b-8f88-77c4b827d650")}catch(e){}}();
3
- import { C as createAuthorizationService, D as MalformedStoredOperationError, E as ForbiddenError, S as AuthorizedDocumentHandle, T as AuthenticationRequiredError, _ as loadSubgraphs, a as piecesFromCdnList, b as createCanonicalDocumentIdResolver, 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 AuthEvaluationUnsupportedError, x as AuthorizationPolicy, y as CanonicalDocumentIdResolutionError } from "./pieces-eqj2RKgd.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]="6828688f-9737-5852-b599-51439acc4894")}catch(e){}}();
3
+ import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
4
+ import { C as createAuthorizationService, D as MalformedStoredOperationError, E as ForbiddenError, S as AuthorizedDocumentHandle, T as AuthenticationRequiredError, _ as loadSubgraphs, a as piecesFromCdnList, b as createCanonicalDocumentIdResolver, 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 AuthEvaluationUnsupportedError, x as AuthorizationPolicy, y as CanonicalDocumentIdResolutionError } from "./pieces-BOwV3Ql9.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";
@@ -19,7 +20,7 @@ import { buildSubgraphSchema } from "@apollo/subgraph";
19
20
  import { typeDefs } from "@powerhousedao/document-engineering/graphql";
20
21
  import { camelCase, kebabCase, pascalCase } from "change-case";
21
22
  import { GraphQLJSON, GraphQLJSONObject } from "graphql-type-json";
22
- import { deserializeSignature, serializeSignature, setName } from "@powerhousedao/shared/document-model";
23
+ import { deserializeSignature, serializeSignature, setName, withSignaturePolicy } from "@powerhousedao/shared/document-model";
23
24
  import * as z$1 from "zod";
24
25
  import { z } from "zod";
25
26
  import { createHandler } from "graphql-sse/lib/use/fetch";
@@ -47,18 +48,6 @@ import { AtomicNodeFs } from "@powerhousedao/pglite-fs";
47
48
  import knex from "knex";
48
49
  import ClientPgLite from "knex-pglite";
49
50
  import { KyselyKnexDialect, PGColdDialect } from "kysely-knex";
50
- //#region \0rolldown/runtime.js
51
- var __defProp = Object.defineProperty;
52
- var __exportAll = (all, no_symbols) => {
53
- let target = {};
54
- for (var name in all) __defProp(target, name, {
55
- get: all[name],
56
- enumerable: true
57
- });
58
- if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
59
- return target;
60
- };
61
- //#endregion
62
51
  //#region src/graphql/analytics-subgraph.ts
63
52
  var AnalyticsSubgraph = class extends BaseSubgraph {
64
53
  analyticsStore;
@@ -312,15 +301,15 @@ function createAuthFetchMiddleware(authService) {
312
301
  async function createGatewayAdapter(type, logger) {
313
302
  switch (type) {
314
303
  case "apollo": {
315
- const { ApolloGatewayAdapter } = await import("./adapter-gateway-apollo-DowrX6E_.mjs");
304
+ const { ApolloGatewayAdapter } = await import("./adapter-gateway-apollo-D2DNlPY0.mjs");
316
305
  return new ApolloGatewayAdapter(logger);
317
306
  }
318
307
  case "mercurius": {
319
- const { MercuriusGatewayAdapter } = await import("./adapter-gateway-mercurius-CZVy88Za.mjs");
308
+ const { MercuriusGatewayAdapter } = await import("./adapter-gateway-mercurius-rbKlwF9m.mjs");
320
309
  return new MercuriusGatewayAdapter(logger);
321
310
  }
322
311
  case "stitching": {
323
- const { StitchingGatewayAdapter } = await import("./adapter-gateway-stitching-CaZjvRzx.mjs");
312
+ const { StitchingGatewayAdapter } = await import("./adapter-gateway-stitching-Dzv10u8D.mjs");
324
313
  return new StitchingGatewayAdapter(logger);
325
314
  }
326
315
  }
@@ -328,11 +317,11 @@ async function createGatewayAdapter(type, logger) {
328
317
  async function createHttpAdapter(type) {
329
318
  switch (type) {
330
319
  case "express": {
331
- const { createExpressHttpAdapter } = await import("./adapter-http-express-D4KBmDcQ.mjs");
320
+ const { createExpressHttpAdapter } = await import("./adapter-http-express-Drbaxszc.mjs");
332
321
  return createExpressHttpAdapter();
333
322
  }
334
323
  case "fastify": {
335
- const { createFastifyHttpAdapter } = await import("./adapter-http-fastify-U9j7HxQE.mjs");
324
+ const { createFastifyHttpAdapter } = await import("./adapter-http-fastify-DMPW_huG.mjs");
336
325
  return createFastifyHttpAdapter();
337
326
  }
338
327
  }
@@ -360,7 +349,7 @@ const WS_AUTH_CLOSE_REASONS = [WS_CLOSE_REASON_AUTHENTICATION_REQUIRED, WS_CLOSE
360
349
  */
361
350
  const SEGMENT = /^[a-z0-9][-a-z0-9._]*$/i;
362
351
  /** Segments that would read as path traversal rather than as a name. */
363
- const TRAVERSAL = new Set([".", ".."]);
352
+ const TRAVERSAL = /* @__PURE__ */ new Set([".", ".."]);
364
353
  var InvalidNamespaceError = class extends Error {};
365
354
  /**
366
355
  * Turns a package name into the path segments it occupies.
@@ -846,27 +835,29 @@ var HttpScope = class {
846
835
  headers: fetchHeaders(req),
847
836
  signal: controller.signal
848
837
  };
849
- if (method !== "GET" && method !== "HEAD" && body !== "none") if (body === "stream") init = {
850
- ...init,
851
- body: Readable.toWeb(req),
852
- duplex: "half"
853
- };
854
- else {
855
- try {
856
- raw = await readBody(req, spec.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES$1);
857
- } catch (error) {
858
- if (error instanceof BodyTooLargeError) {
859
- res.setHeader("connection", "close");
860
- res.once("finish", () => req.destroy());
861
- writeError(res, 413, "Payload too large");
862
- return;
863
- }
864
- throw error;
865
- }
866
- if (raw.length) init = {
838
+ if (method !== "GET" && method !== "HEAD" && body !== "none") {
839
+ if (body === "stream") init = {
867
840
  ...init,
868
- body: new Uint8Array(raw.buffer, raw.byteOffset, raw.byteLength)
841
+ body: Readable.toWeb(req),
842
+ duplex: "half"
869
843
  };
844
+ else {
845
+ try {
846
+ raw = await readBody(req, spec.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES$1);
847
+ } catch (error) {
848
+ if (error instanceof BodyTooLargeError) {
849
+ res.setHeader("connection", "close");
850
+ res.once("finish", () => req.destroy());
851
+ writeError(res, 413, "Payload too large");
852
+ return;
853
+ }
854
+ throw error;
855
+ }
856
+ if (raw.length) init = {
857
+ ...init,
858
+ body: new Uint8Array(raw.buffer, raw.byteOffset, raw.byteLength)
859
+ };
860
+ }
870
861
  }
871
862
  await writeResponse(res, await spec.handler(new Request(url, init), {
872
863
  ...ctx,
@@ -1043,7 +1034,7 @@ function verifyWebhook(options) {
1043
1034
  }
1044
1035
  /** Headers that would put a credential in a log or run journal. The configured signature
1045
1036
  * header joins them: it is an HMAC of the body under a live secret. */
1046
- const REDACTED_HEADERS = new Set([
1037
+ const REDACTED_HEADERS = /* @__PURE__ */ new Set([
1047
1038
  "authorization",
1048
1039
  "proxy-authorization",
1049
1040
  "cookie",
@@ -1316,6 +1307,7 @@ function newWebhookToken() {
1316
1307
  const NAMESPACE = "reactor_webhooks";
1317
1308
  /** Relational implementation, over the reactor's own database. */
1318
1309
  var RelationalWebhookStore = class {
1310
+ relationalDb;
1319
1311
  #db;
1320
1312
  constructor(relationalDb) {
1321
1313
  this.relationalDb = relationalDb;
@@ -1461,7 +1453,7 @@ const stripScalarDefinitions = (doc) => {
1461
1453
  * whole gateway. We dedupe these by name; field/operation/extension nodes are
1462
1454
  * left untouched.
1463
1455
  */
1464
- const TYPE_DEFINITION_KINDS = new Set([
1456
+ const TYPE_DEFINITION_KINDS = /* @__PURE__ */ new Set([
1465
1457
  Kind.OBJECT_TYPE_DEFINITION,
1466
1458
  Kind.ENUM_TYPE_DEFINITION,
1467
1459
  Kind.INPUT_OBJECT_TYPE_DEFINITION,
@@ -1563,7 +1555,7 @@ const getDocumentModelTypeDefs = (documentModels, typeDefs$1) => {
1563
1555
  stateJSON: JSONObject
1564
1556
  }\n`;
1565
1557
  });
1566
- return dedupeTypeDefinitions(gql`
1558
+ const schema = gql`
1567
1559
  scalar JSONObject
1568
1560
  scalar AttachmentRef
1569
1561
  # Codegen scalars not in the document-engineering set.
@@ -1639,7 +1631,8 @@ const getDocumentModelTypeDefs = (documentModels, typeDefs$1) => {
1639
1631
  }
1640
1632
 
1641
1633
  ${stripScalarDefinitions(typeDefs$1)}
1642
- `);
1634
+ `;
1635
+ return dedupeTypeDefinitions(schema);
1643
1636
  };
1644
1637
  /**
1645
1638
  * Extract type names from a GraphQL schema.
@@ -1735,9 +1728,7 @@ function generateStateInputTypes(stateSchema, excludeTypeNames) {
1735
1728
  case Kind.OBJECT_TYPE_DEFINITION:
1736
1729
  objectNames.add(def.name.value);
1737
1730
  break;
1738
- case Kind.INPUT_OBJECT_TYPE_DEFINITION:
1739
- existingInputNames.add(def.name.value);
1740
- break;
1731
+ case Kind.INPUT_OBJECT_TYPE_DEFINITION: existingInputNames.add(def.name.value);
1741
1732
  }
1742
1733
  const inputTypes = [];
1743
1734
  for (const def of ast.definitions) {
@@ -1766,7 +1757,7 @@ function applyGraphQLTypePrefixes(schema, prefix, externalTypeNames = []) {
1766
1757
  if (!schema || !schema.trim()) return schema;
1767
1758
  let processedSchema = schema;
1768
1759
  const localTypeNames = extractTypeNames(schema);
1769
- const allTypeNames = [...new Set([...localTypeNames, ...externalTypeNames])];
1760
+ const allTypeNames = [.../* @__PURE__ */ new Set([...localTypeNames, ...externalTypeNames])];
1770
1761
  if (allTypeNames.length === 0) return schema;
1771
1762
  allTypeNames.forEach((typeName) => {
1772
1763
  const typeRegex = new RegExp(`(?<![_A-Za-z0-9])(${typeName})(?![_A-Za-z0-9])|\\[(${typeName})\\]|\\[(${typeName})!\\]|\\[(${typeName})\\]!|\\[(${typeName})!\\]!`, "g");
@@ -1794,8 +1785,9 @@ function generateDocumentModelSchema(documentModel, options = {}) {
1794
1785
  const localStateTypeNames = extractTypeNames(localStateSchema ?? "");
1795
1786
  const stateTypeNames = [...globalStateTypeNames, ...localStateTypeNames];
1796
1787
  const allOperationTypeNames = specification?.modules.flatMap((module) => module.operations.flatMap((op) => extractTypeNames(op.schema ?? ""))) ?? [];
1797
- const allTypeNames = [...new Set([...stateTypeNames, ...allOperationTypeNames])];
1798
- const prefixedStateInputTypes = applyGraphQLTypePrefixes(extractInputTypeDefinitions(globalStateSchema ?? "", new Set(allOperationTypeNames)), documentName, allTypeNames);
1788
+ const allTypeNames = [.../* @__PURE__ */ new Set([...stateTypeNames, ...allOperationTypeNames])];
1789
+ const operationInputTypeNames = new Set(allOperationTypeNames);
1790
+ const prefixedStateInputTypes = applyGraphQLTypePrefixes(extractInputTypeDefinitions(globalStateSchema ?? "", operationInputTypeNames), documentName, allTypeNames);
1799
1791
  const hasValidSchema = (schema) => !!(schema && /\b(input|type|enum|union|interface)\s+\w+/.test(schema));
1800
1792
  const stateSchemaTypes = globalStateSchema ? applyGraphQLTypePrefixes(globalStateSchema.replaceAll("scalar DateTime", "").replaceAll(/input (.*?) {[\s\S]*?}/g, ""), documentName, allTypeNames) : "";
1801
1793
  if (useNewApi) return generateNewApiSchema(documentName, specification, stateSchemaTypes, prefixedStateInputTypes, allTypeNames, hasValidSchema);
@@ -3020,8 +3012,8 @@ function fromInputMaybe(value) {
3020
3012
  function toReactorPropagationMode(gqlMode) {
3021
3013
  if (gqlMode == null) return;
3022
3014
  switch (gqlMode) {
3023
- case PropagationMode.Cascade: return PropagationMode$1.Cascade;
3024
- case PropagationMode.Orphan: return PropagationMode$1.None;
3015
+ case "CASCADE": return PropagationMode$1.Cascade;
3016
+ case "ORPHAN": return PropagationMode$1.None;
3025
3017
  }
3026
3018
  }
3027
3019
  /**
@@ -3031,10 +3023,10 @@ function toReactorPropagationMode(gqlMode) {
3031
3023
  */
3032
3024
  function toGqlActionEvaluation(evaluation) {
3033
3025
  return evaluation.decision === "allow" ? {
3034
- decision: AuthDecision.Allow,
3026
+ decision: "ALLOW",
3035
3027
  reason: null
3036
3028
  } : {
3037
- decision: AuthDecision.Deny,
3029
+ decision: "DENY",
3038
3030
  reason: evaluation.reason
3039
3031
  };
3040
3032
  }
@@ -3617,7 +3609,8 @@ async function createEmptyDocument(reactorClient, args, reactorDriveClient) {
3617
3609
  if (parentIdentifier) {
3618
3610
  const parent = await reactorClient.get(parentIdentifier);
3619
3611
  if (isDriveContainerType(parent.header.documentType)) {
3620
- const document = (await reactorClient.getDocumentModelModule(args.documentType)).utils.createDocument();
3612
+ const module = await reactorClient.getDocumentModelModule(args.documentType);
3613
+ const document = withSignaturePolicy(module.utils.createDocument(), await reactorClient.getCreateSignaturePolicy());
3621
3614
  if (name) document.header.name = name;
3622
3615
  result = await pickDriveClient(reactorClient, parent.header.documentType, reactorDriveClient).addFile(parentIdentifier, document);
3623
3616
  } else result = await reactorClient.createEmpty(args.documentType, { parentIdentifier });
@@ -3642,7 +3635,7 @@ async function createDocumentWithInitialState(reactorClient, args, reactorDriveC
3642
3635
  } catch (error) {
3643
3636
  throw new GraphQLError(`Document model not found for type ${args.documentType}: ${error instanceof Error ? error.message : "Unknown error"}`);
3644
3637
  }
3645
- const document = module.utils.createDocument();
3638
+ const document = withSignaturePolicy(module.utils.createDocument(), await reactorClient.getCreateSignaturePolicy());
3646
3639
  const allowedScopes = new Set(Object.keys(module.documentModel.global.specifications.at(-1)?.state ?? {}));
3647
3640
  const state = document.state;
3648
3641
  for (const [scope, scopeState] of Object.entries(args.initialState)) if (allowedScopes.has(scope) && scope in state) state[scope] = {
@@ -4343,7 +4336,7 @@ const DRIVE_ID_HEADER = "drive-id";
4343
4336
  * in the ownership set yet); other create-shaped operations that may
4344
4337
  * synthesize a drive must be added here too.
4345
4338
  */
4346
- const CACHE_BYPASS_OPERATIONS = new Set(["createDocument", "createEmptyDocument"]);
4339
+ const CACHE_BYPASS_OPERATIONS = /* @__PURE__ */ new Set(["createDocument", "createEmptyDocument"]);
4347
4340
  const driveIdMap = /* @__PURE__ */ new WeakMap();
4348
4341
  /** Internal — only `graphql-manager.ts` should call this. */
4349
4342
  function getRequestDriveId(request) {
@@ -4413,6 +4406,7 @@ function wrongShardResponse(driveId) {
4413
4406
  * 421 response.
4414
4407
  */
4415
4408
  var DriveOwnershipCache = class {
4409
+ reactorClient;
4416
4410
  drives = /* @__PURE__ */ new Set();
4417
4411
  constructor(reactorClient) {
4418
4412
  this.reactorClient = reactorClient;
@@ -4530,6 +4524,22 @@ function createWsAuthHandlers(opts) {
4530
4524
  };
4531
4525
  }
4532
4526
  var GraphQLManager = class {
4527
+ path;
4528
+ httpServer;
4529
+ wsServer;
4530
+ reactorClient;
4531
+ relationalDb;
4532
+ analyticsStore;
4533
+ syncManager;
4534
+ logger;
4535
+ httpAdapter;
4536
+ gatewayAdapter;
4537
+ authService;
4538
+ documentPermissionService;
4539
+ featureFlags;
4540
+ port;
4541
+ syncServingGate;
4542
+ httpRoutes;
4533
4543
  initialized = false;
4534
4544
  coreSubgraphsMap = /* @__PURE__ */ new Map();
4535
4545
  contextFields = {};
@@ -4544,8 +4554,8 @@ var GraphQLManager = class {
4544
4554
  subgraphHandlerCache = /* @__PURE__ */ new Map();
4545
4555
  /** subgraphPath → the http adapter handle of its mounted route. */
4546
4556
  subgraphRouteHandles = /* @__PURE__ */ new Map();
4547
- /** Handle of the currently mounted supergraph SSE route, if any. */
4548
- sseRouteHandle;
4557
+ /** Mounted SSE routes, keyed by path: the supergraph's and each subgraph's. */
4558
+ sseRouteHandles = /* @__PURE__ */ new Map();
4549
4559
  /**
4550
4560
  * Package name → the subgraph instances registered from it, keyed by
4551
4561
  * subgraph name. Tracks the package association so that removed packages
@@ -4743,6 +4753,7 @@ var GraphQLManager = class {
4743
4753
  routeHandle.dispose();
4744
4754
  this.subgraphRouteHandles.delete(subgraphPath);
4745
4755
  }
4756
+ this.#disposeSSERoute(subgraphPath);
4746
4757
  const wsDisposer = this.subgraphWsDisposers.get(subgraphPath);
4747
4758
  if (wsDisposer) {
4748
4759
  this.subgraphWsDisposers.delete(subgraphPath);
@@ -5082,10 +5093,7 @@ var GraphQLManager = class {
5082
5093
  const allSubgraphs = this.#getAllSubgraphs();
5083
5094
  const modules = Array.from(allSubgraphs.values()).filter((subgraph) => subgraph.hasSubscriptions).map((subgraph) => this.#buildSubgraphSchemaModule(subgraph));
5084
5095
  if (modules.length === 0) {
5085
- if (this.sseRouteHandle !== void 0) {
5086
- this.sseRouteHandle.dispose();
5087
- this.sseRouteHandle = void 0;
5088
- }
5096
+ this.#disposeSSERoute(superGraphPath);
5089
5097
  return;
5090
5098
  }
5091
5099
  try {
@@ -5111,8 +5119,16 @@ var GraphQLManager = class {
5111
5119
  contextFactory: this.#makeContextFactory()
5112
5120
  });
5113
5121
  const handler = this.#composeFetchMiddleware(rawHandler);
5114
- if (this.sseRouteHandle !== void 0) this.sseRouteHandle.dispose();
5115
- this.sseRouteHandle = this.httpAdapter.mount(ssePath, handler, { exact: true });
5122
+ this.#disposeSSERoute(basePath);
5123
+ this.sseRouteHandles.set(ssePath, this.httpAdapter.mount(ssePath, handler, { exact: true }));
5124
+ }
5125
+ #disposeSSERoute(basePath) {
5126
+ const ssePath = basePath + "/stream";
5127
+ const handle = this.sseRouteHandles.get(ssePath);
5128
+ if (handle !== void 0) {
5129
+ handle.dispose();
5130
+ this.sseRouteHandles.delete(ssePath);
5131
+ }
5116
5132
  }
5117
5133
  /**
5118
5134
  * Compose the request-level fetch middleware chain. Auth runs first
@@ -5623,7 +5639,7 @@ function createReactorGraphQLClient(url, fetchImpl = fetch, headers) {
5623
5639
  }
5624
5640
  //#endregion
5625
5641
  //#region src/graphql/reactor/schema.graphql
5626
- 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\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 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\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 \"\"\"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 # 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";
5642
+ 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\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 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\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";
5627
5643
  //#endregion
5628
5644
  //#region src/graphql/reactor/pubsub.ts
5629
5645
  const pubSub = new PubSub();
@@ -5997,6 +6013,13 @@ var ReactorSubgraph = class extends BaseSubgraph {
5997
6013
  throw error;
5998
6014
  }
5999
6015
  },
6016
+ /**
6017
+ * Gated on reading the document, the same gate every other read here
6018
+ * carries: a verdict is derived from the document's policy, so a caller
6019
+ * who may not read the document may not preflight against it either.
6020
+ * That gate decides whether the query runs; the answer comes entirely
6021
+ * from the reactor's decision model.
6022
+ */
6000
6023
  evaluateActions: async (_parent, args, ctx) => {
6001
6024
  this.logger.debug("evaluateActions(@args)", args);
6002
6025
  try {
@@ -6372,10 +6395,10 @@ const ADMIN_USERS = getAdminUsers();
6372
6395
  //#endregion
6373
6396
  //#region src/graphql/system/version.ts
6374
6397
  function getVersion() {
6375
- return "6.2.3-dev.23";
6398
+ return "6.2.3-dev.25";
6376
6399
  }
6377
6400
  function getGitHash() {
6378
- return "7b5cf362aaa580a9e468dd0e3d98e0dca9630e91";
6401
+ return "e44ced14916428517ebc9e26d9bfb53d1d86cbc0";
6379
6402
  }
6380
6403
  function getGitUrl() {
6381
6404
  return buildTreeUrl(getGitHash());
@@ -6476,7 +6499,8 @@ var HttpPackageLoader = class {
6476
6499
  async loadUpgradeManifests(packageSpec) {
6477
6500
  const { name: packageName } = this.parsePackageSpec(packageSpec);
6478
6501
  if (!this.isValidPackageName(packageName)) throw new Error(`Invalid package name: ${packageName}`);
6479
- const manifests = extractUpgradeManifests(await import(`${this.registryUrl}-/cdn/${packageSpec}/node/document-models/index.mjs`));
6502
+ const module = await import(`${this.registryUrl}-/cdn/${packageSpec}/node/document-models/index.mjs`);
6503
+ const manifests = extractUpgradeManifests(module);
6480
6504
  if (manifests.length > 0) this.logger.verbose(`Loaded ${manifests.length} upgrade manifests from ${packageName}`);
6481
6505
  return manifests;
6482
6506
  }
@@ -6667,10 +6691,11 @@ var ImportPackageLoader = class {
6667
6691
  this.reportUnbuilt(identifier, root, listPath);
6668
6692
  return [];
6669
6693
  }
6670
- const pieces = piecesFromListModule(await import(
6694
+ const module = await import(
6671
6695
  /* @vite-ignore */
6672
6696
  pathToFileURL(listPath).href
6673
- ), {
6697
+ );
6698
+ const pieces = piecesFromListModule(module, {
6674
6699
  root,
6675
6700
  identifier,
6676
6701
  logger: this.logger
@@ -6720,6 +6745,7 @@ function getUniqueUpgradeManifests(...upgradeManifests) {
6720
6745
  return Array.from(uniqueManifests.values());
6721
6746
  }
6722
6747
  var PackageManager = class {
6748
+ options;
6723
6749
  logger = childLogger(["reactor-api", "package-manager"]);
6724
6750
  loaders;
6725
6751
  docModelsMap = /* @__PURE__ */ new Map();
@@ -7074,20 +7100,41 @@ async function createStartupAttachmentBackend({ db, env = process.env, s3Depende
7074
7100
  }
7075
7101
  //#endregion
7076
7102
  //#region src/services/attachment-access.service.ts
7103
+ /** The scope an attachment's bytes belong to: the document's own domain state. */
7104
+ const ATTACHMENT_SCOPE = "global";
7105
+ /** The branch an attachment reference is resolved against. */
7106
+ const ATTACHMENT_BRANCH = "main";
7077
7107
  const HASH_PATTERN = /^[a-f0-9]{64}$/;
7078
7108
  /**
7079
7109
  * Composes document authorization with the projected document/ref
7080
7110
  * relationship. Order is fixed: validate ref, resolve the canonical document
7081
- * id, check `canRead`, then check the reference index. A denied document
7111
+ * id, decide the read, then check the reference index. A denied document
7082
7112
  * never reaches the reference reader, and the facade never touches
7083
7113
  * attachment metadata, storage backends, or presigners.
7114
+ *
7115
+ * The read is decided by the document's own policy when this composition has a
7116
+ * model to enforce it with, and by the host's permission tables when it does
7117
+ * not. That is not a preference between two equivalent answers: a host running
7118
+ * document policies keeps no rows in the permission tables, so asking them
7119
+ * about such a document returns whatever the host-wide policy says — under
7120
+ * `OPEN`, `true`, for everyone. Serving bytes on that answer hands the file to
7121
+ * anyone who learns its hash, which the document's own state may well have told
7122
+ * them. Both are kept because they are the two halves of one rule: an
7123
+ * attachment is readable by whoever may read the document that references it,
7124
+ * and each composition can only express that in its own terms.
7084
7125
  */
7085
7126
  var AttachmentAccessService = class {
7086
- constructor(resolveCanonicalId, authorization, references, projection) {
7127
+ resolveCanonicalId;
7128
+ authorization;
7129
+ references;
7130
+ projection;
7131
+ scopeGate;
7132
+ constructor(resolveCanonicalId, authorization, references, projection, scopeGate) {
7087
7133
  this.resolveCanonicalId = resolveCanonicalId;
7088
7134
  this.authorization = authorization;
7089
7135
  this.references = references;
7090
7136
  this.projection = projection;
7137
+ this.scopeGate = scopeGate;
7091
7138
  }
7092
7139
  async canReadAttachment(request) {
7093
7140
  if (this.projection.status !== "available") return { kind: "projection-unavailable" };
@@ -7099,7 +7146,7 @@ var AttachmentAccessService = class {
7099
7146
  } catch {
7100
7147
  return { kind: "denied" };
7101
7148
  }
7102
- if (!await this.authorization.canRead(documentId, request.userAddress)) return { kind: "denied" };
7149
+ if (!await this.canReadDocument(documentId, request)) return { kind: "denied" };
7103
7150
  if (!await this.references.hasReference(documentId, ref)) return { kind: "denied" };
7104
7151
  return {
7105
7152
  kind: "allowed",
@@ -7107,6 +7154,21 @@ var AttachmentAccessService = class {
7107
7154
  ref
7108
7155
  };
7109
7156
  }
7157
+ /**
7158
+ * Whether the caller may read the state the reference lives in.
7159
+ *
7160
+ * A failure here is rethrown rather than turned into a denial. A read side
7161
+ * that is down must not read as a policy: a denial nobody can distinguish
7162
+ * from a refusal is one nobody investigates, and the route answers 500 for a
7163
+ * reason.
7164
+ */
7165
+ async canReadDocument(documentId, request) {
7166
+ if (!this.scopeGate) return this.authorization.canRead(documentId, request.userAddress);
7167
+ return (await this.scopeGate.scopePredicateById(documentId, {
7168
+ address: request.userAddress,
7169
+ key: request.appKey
7170
+ }, ATTACHMENT_BRANCH))(ATTACHMENT_SCOPE);
7171
+ }
7110
7172
  };
7111
7173
  /**
7112
7174
  * Parses and canonicalizes a caller-supplied ref through the shared parser.
@@ -7152,14 +7214,42 @@ function normalizeAttachmentRef(value) {
7152
7214
  * context has no caller, and is rejected. The server never composes this
7153
7215
  * middleware without the auth middleware in the chain (see
7154
7216
  * `assertRequireAuthenticatedCallerAllowed` in `server.ts`).
7217
+ *
7218
+ * `exemptPaths` names the mounted paths that stay reachable anonymously — a
7219
+ * deployment whose product has a flow that runs before sign-in, such as
7220
+ * previewing an invitation from its code, needs somewhere to serve it from.
7221
+ * Each entry is a hole in the floor, so the match is deliberately the
7222
+ * narrowest one that works: the request's pathname, compared in full. A
7223
+ * prefix rule would exempt `/graphql/public-admin` along with
7224
+ * `/graphql/public`, and an exempt surface nobody can enumerate by reading
7225
+ * the configuration is not one anybody audits.
7155
7226
  */
7156
- function createRequireAuthFetchMiddleware() {
7227
+ function createRequireAuthFetchMiddleware(exemptPaths = []) {
7228
+ const exempt = new Set(exemptPaths.map(normalizeExemptPath));
7157
7229
  return (next) => async (request) => {
7158
7230
  if (request.method === "OPTIONS") return next(request);
7231
+ if (exempt.size > 0 && exempt.has(normalizeExemptPath(new URL(request.url).pathname))) return next(request);
7159
7232
  if (getAuthContext(request)?.user === void 0) return new globalThis.Response(JSON.stringify({ error: "Authentication required" }), { status: 401 });
7160
7233
  return next(request);
7161
7234
  };
7162
7235
  }
7236
+ /**
7237
+ * The form both sides of the comparison are reduced to: surrounding whitespace
7238
+ * gone, and one trailing slash gone, so `/graphql/public/` and
7239
+ * `/graphql/public` are the same path. A client that appends a slash is not
7240
+ * asking for a different route, and a configuration that carries one is not
7241
+ * asking for a different rule.
7242
+ *
7243
+ * Deliberately NOT normalized: case, percent-encoding, and a missing leading
7244
+ * slash. The first two would let two spellings of one path diverge from how
7245
+ * the router itself matches; the third is a configuration error, and
7246
+ * `assertRequireAuthenticatedCallerAllowed` refuses to boot on it rather than
7247
+ * leaving an exemption that silently never applies.
7248
+ */
7249
+ function normalizeExemptPath(path) {
7250
+ const trimmed = path.trim();
7251
+ return trimmed.length > 1 && trimmed.endsWith("/") ? trimmed.slice(0, -1) : trimmed;
7252
+ }
7163
7253
  //#endregion
7164
7254
  //#region src/migrations/001_create_document_permissions.ts
7165
7255
  var _001_create_document_permissions_exports = /* @__PURE__ */ __exportAll({
@@ -7562,6 +7652,7 @@ async function createRenownCredentialVerifier(config = {}) {
7562
7652
  * - Users can be granted permission to execute specific operations
7563
7653
  */
7564
7654
  var DocumentPermissionService = class {
7655
+ db;
7565
7656
  config;
7566
7657
  constructor(db, config = { defaultProtection: false }) {
7567
7658
  this.db = db;
@@ -8045,10 +8136,36 @@ function assertSkipCredentialVerificationAllowed(resolvesCallerIdentity, skipCre
8045
8136
  * require-authenticated-caller middleware would reject every caller —
8046
8137
  * authenticated ones included. Refuse to boot rather than run broken.
8047
8138
  */
8048
- function assertRequireAuthenticatedCallerAllowed(requireAuthenticatedCaller, resolvesCallerIdentity) {
8139
+ function assertRequireAuthenticatedCallerAllowed(requireAuthenticatedCaller, resolvesCallerIdentity, exemptPaths = []) {
8140
+ /**
8141
+ * An exemption is only ever a hole in this floor, so configuring one while
8142
+ * the floor is off is not a harmless no-op: it reads, to anyone auditing the
8143
+ * configuration, as a surface that was deliberately opened — and therefore
8144
+ * as a floor that exists. Refuse rather than let the two drift.
8145
+ */
8146
+ 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.");
8147
+ /**
8148
+ * A path that cannot match anything is worse than no path: the operator
8149
+ * believes the flow is reachable, and finds out when the first user cannot
8150
+ * claim an invitation. `URL.pathname` is always absolute, so an entry that
8151
+ * does not start with `/` never matches, whatever the router does.
8152
+ */
8153
+ const relative = exemptPaths.filter((path) => !path.trim().startsWith("/"));
8154
+ 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').`);
8049
8155
  if (!requireAuthenticatedCaller || resolvesCallerIdentity) return;
8050
8156
  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).");
8051
8157
  }
8158
+ /**
8159
+ * Deciding an attachment read by the document's policy needs a policy model to
8160
+ * decide with, and that is what auth enforcement supplies. Without one there is
8161
+ * nothing to consult, so the setting would silently leave the permission tables
8162
+ * in charge — configuration that describes a protection the server is not
8163
+ * applying, which is worse than no configuration at all. Refuse instead.
8164
+ */
8165
+ function assertAttachmentPolicyReadsAllowed(attachmentReadsFollowDocumentPolicy, hasDecisionModel) {
8166
+ if (!attachmentReadsFollowDocumentPolicy || hasDecisionModel) return;
8167
+ 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).");
8168
+ }
8052
8169
  function createReadinessGate() {
8053
8170
  let ready = false;
8054
8171
  return {
@@ -8238,6 +8355,8 @@ async function _setupCommonInfrastructure(options) {
8238
8355
  let authEnabled = false;
8239
8356
  let configuredResolveIdentity;
8240
8357
  let configuredRequireAuth;
8358
+ let configuredExemptPaths;
8359
+ let configuredAttachmentPolicyReads;
8241
8360
  let configuredRenown;
8242
8361
  if (options.configFile) {
8243
8362
  const config = getConfig(options.configFile);
@@ -8249,8 +8368,10 @@ async function _setupCommonInfrastructure(options) {
8249
8368
  authEnabled = options.auth.enabled;
8250
8369
  configuredResolveIdentity = options.auth.resolveIdentity;
8251
8370
  configuredRequireAuth = options.auth.requireAuthenticatedCaller;
8371
+ configuredExemptPaths = options.auth.requireAuthenticatedCallerExemptPaths;
8372
+ configuredAttachmentPolicyReads = options.auth.attachmentReadsFollowDocumentPolicy;
8252
8373
  }
8253
- 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;
8374
+ 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;
8254
8375
  if (AUTH_ENABLED !== void 0) authEnabled = AUTH_ENABLED === "true";
8255
8376
  /**
8256
8377
  * Whether the auth middleware reads the bearer and populates `ctx.user`,
@@ -8281,6 +8402,30 @@ async function _setupCommonInfrastructure(options) {
8281
8402
  */
8282
8403
  let requireAuthenticatedCaller = configuredRequireAuth ?? false;
8283
8404
  if (REQUIRE_AUTHENTICATED_CALLER !== void 0) requireAuthenticatedCaller = REQUIRE_AUTHENTICATED_CALLER === "true";
8405
+ /**
8406
+ * The paths the floor above does not apply to. Comma-separated, like
8407
+ * `ADMINS`, because a deployment configures this the same way it configures
8408
+ * everything else: one variable, one line, however many values it has.
8409
+ *
8410
+ * Empty entries are dropped rather than refused, so a trailing comma or a
8411
+ * value split across lines in a manifest is not a boot failure; a value that
8412
+ * cannot ever match is refused, in
8413
+ * {@link assertRequireAuthenticatedCallerAllowed}.
8414
+ */
8415
+ let requireAuthenticatedCallerExemptPaths = configuredExemptPaths ?? [];
8416
+ if (REQUIRE_AUTHENTICATED_CALLER_EXEMPT_PATHS !== void 0) requireAuthenticatedCallerExemptPaths = REQUIRE_AUTHENTICATED_CALLER_EXEMPT_PATHS.split(",").map((path) => path.trim()).filter((path) => path.length > 0);
8417
+ /**
8418
+ * Whether an attachment read is decided by the referencing document's own
8419
+ * policy rather than by the host's permission tables.
8420
+ *
8421
+ * Off by default, and a deployment's choice rather than something derived
8422
+ * from the flags around it. Which model governs those bytes is configuration
8423
+ * in the same sense the storage backend behind them is: a host that has said
8424
+ * nothing keeps exactly the behaviour it has, and one that wants the change
8425
+ * asks for it and can take it back without disturbing anything else.
8426
+ */
8427
+ let attachmentReadsFollowDocumentPolicy = configuredAttachmentPolicyReads ?? false;
8428
+ if (ATTACHMENT_READS_FOLLOW_DOCUMENT_POLICY !== void 0) attachmentReadsFollowDocumentPolicy = ATTACHMENT_READS_FOLLOW_DOCUMENT_POLICY === "true";
8284
8429
  if (ADMINS !== void 0) admins = ADMINS.split(",").map((a) => a.toLowerCase());
8285
8430
  let defaultProtection = false;
8286
8431
  if (DEFAULT_PROTECTION !== void 0) defaultProtection = DEFAULT_PROTECTION.toLowerCase() === "true";
@@ -8295,7 +8440,7 @@ async function _setupCommonInfrastructure(options) {
8295
8440
  const documentPermissionsRequested = options.documentPermissionService !== void 0 || DOCUMENT_PERMISSIONS_ENABLED === "true";
8296
8441
  assertAuthRequiredForDocumentPermissions(authEnabled, documentPermissionsRequested);
8297
8442
  assertSkipCredentialVerificationAllowed(resolveCallerIdentity, skipCredentialVerification, process.env);
8298
- assertRequireAuthenticatedCallerAllowed(requireAuthenticatedCaller, resolveCallerIdentity);
8443
+ assertRequireAuthenticatedCallerAllowed(requireAuthenticatedCaller, resolveCallerIdentity, requireAuthenticatedCallerExemptPaths);
8299
8444
  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.");
8300
8445
  httpAdapter.getRoute("/health", () => new Response("OK", { status: 200 }));
8301
8446
  const readiness = createReadinessGate();
@@ -8331,8 +8476,8 @@ async function _setupCommonInfrastructure(options) {
8331
8476
  });
8332
8477
  authFetchMiddleware = createAuthFetchMiddleware(authService);
8333
8478
  if (requireAuthenticatedCaller) {
8334
- requireAuthFetchMiddleware = createRequireAuthFetchMiddleware();
8335
- logger.info("Require-authenticated-caller middleware enabled: anonymous callers are rejected with a 401 before any subgraph");
8479
+ requireAuthFetchMiddleware = createRequireAuthFetchMiddleware(requireAuthenticatedCallerExemptPaths);
8480
+ 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");
8336
8481
  } 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.");
8337
8482
  }
8338
8483
  const dbClosers = [];
@@ -8357,7 +8502,7 @@ async function _setupCommonInfrastructure(options) {
8357
8502
  await mkdir(attachmentStoragePath, { recursive: true });
8358
8503
  const { db: attachmentDb, knex: attachmentKnex, pglite: attachmentPglite } = getDbClient(options.dbPath, options.pgliteFactory);
8359
8504
  dbClosers.push(...makeDbClosers(attachmentKnex, attachmentPglite));
8360
- const ATTACHMENT_SWEEP_INTERVAL_MS = 3600 * 1e3;
8505
+ const ATTACHMENT_SWEEP_INTERVAL_MS = 36e5;
8361
8506
  const attachmentBackend = await createStartupAttachmentBackend({ db: attachmentDb });
8362
8507
  const attachmentBuilder = new AttachmentBuilder(attachmentDb, attachmentStoragePath).withReservationSweepMs(ATTACHMENT_SWEEP_INTERVAL_MS);
8363
8508
  if (attachmentBackend) attachmentBuilder.withBackend(attachmentBackend);
@@ -8402,13 +8547,14 @@ async function _setupCommonInfrastructure(options) {
8402
8547
  attachmentReferenceIndex,
8403
8548
  packages,
8404
8549
  dbClosers,
8405
- readiness
8550
+ readiness,
8551
+ attachmentReadsFollowDocumentPolicy
8406
8552
  };
8407
8553
  }
8408
8554
  /**
8409
8555
  * Private helper function containing common setup logic for API initialization
8410
8556
  */
8411
- 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) {
8557
+ 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) {
8412
8558
  const hostModuleBase = {
8413
8559
  ...createReactorHostModuleBase({
8414
8560
  client: reactorClient,
@@ -8450,7 +8596,8 @@ async function _setupAPI(reactorClient, syncManager, reactorProcessorManager, ht
8450
8596
  const { httpServer, wsServer } = await startServer(httpAdapter, port, options.https, logger);
8451
8597
  const authorizationService = createAuthorizationService(authorizationConfig, documentPermissionService, createGetParentIdsFn(reactorClient));
8452
8598
  logger.info(`Authorization service initialized (policy: ${authorizationConfig.policy})`);
8453
- const attachmentAccess = new AttachmentAccessService(createCanonicalDocumentIdResolver(reactorClient), authorizationService, attachmentReferenceIndex.store, attachmentReferenceProjection);
8599
+ assertAttachmentPolicyReadsAllowed(attachmentReadsFollowDocumentPolicy, syncServingGate !== void 0);
8600
+ const attachmentAccess = new AttachmentAccessService(createCanonicalDocumentIdResolver(reactorClient), authorizationService, attachmentReferenceIndex.store, attachmentReferenceProjection, attachmentReadsFollowDocumentPolicy ? syncServingGate : void 0);
8454
8601
  const coreSubgraphs = DefaultCoreSubgraphs.slice();
8455
8602
  coreSubgraphs.push(ReactorSubgraph);
8456
8603
  if (documentPermissionService) {
@@ -8487,6 +8634,7 @@ async function _setupAPI(reactorClient, syncManager, reactorProcessorManager, ht
8487
8634
  attachmentReferenceIndex,
8488
8635
  attachmentAccess,
8489
8636
  authService,
8637
+ requireAuthenticatedCaller: requireAuthFetchMiddleware !== void 0,
8490
8638
  authorizationService,
8491
8639
  relationalDb,
8492
8640
  dispose
@@ -8535,7 +8683,7 @@ function buildApiDispose(args) {
8535
8683
  };
8536
8684
  }
8537
8685
  async function initializeAndStartAPI(clientInitializer, options, processorApp) {
8538
- const { port, httpAdapter, httpRoutes, authFetchMiddleware, requireAuthFetchMiddleware, authService, relationalDb, analyticsStore, documentPermissionService, authorizationConfig, attachments, attachmentReferenceIndex, packages, dbClosers, readiness } = await _setupCommonInfrastructure(options);
8686
+ const { port, httpAdapter, httpRoutes, authFetchMiddleware, requireAuthFetchMiddleware, authService, relationalDb, analyticsStore, documentPermissionService, authorizationConfig, attachments, attachmentReferenceIndex, packages, dbClosers, readiness, attachmentReadsFollowDocumentPolicy } = await _setupCommonInfrastructure(options);
8539
8687
  const { documentModels, upgradeManifests, processors, subgraphs } = await packages.init();
8540
8688
  const { module: reactorClientModule, reactorDriveClient, attachmentReferenceProjection = {
8541
8689
  status: "unavailable",
@@ -8552,7 +8700,7 @@ async function initializeAndStartAPI(clientInitializer, options, processorApp) {
8552
8700
  const documentModelRegistry = reactorClientModule.reactorModule?.documentModelRegistry;
8553
8701
  if (!documentModelRegistry) throw new Error("DocumentModelRegistry not available from InProcessReactorClientModule");
8554
8702
  return {
8555
- ...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),
8703
+ ...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),
8556
8704
  client: reactorClient,
8557
8705
  syncManager,
8558
8706
  documentModelRegistry,
@@ -8685,7 +8833,7 @@ var PackageManagementService = class {
8685
8833
  }
8686
8834
  };
8687
8835
  //#endregion
8688
- 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, assertAuthRequiredForDocumentPermissions, assertCredentialVerifierForSource, assertHostMountPath, assertRequireAuthenticatedCallerAllowed, assertSkipCredentialVerificationAllowed, buildGraphQlDocument, buildGraphQlDriveDocument, buildGraphqlOperation, buildGraphqlOperations, buildSubgraphSchemaModule, 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 };
8836
+ 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, 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 };
8689
8837
 
8690
8838
  //# sourceMappingURL=index.mjs.map
8691
- //# debugId=ab52a900-0b3c-526b-8f88-77c4b827d650
8839
+ //# debugId=6828688f-9737-5852-b599-51439acc4894