@powerhousedao/reactor-api 6.2.3-dev.4 → 6.2.3-dev.40

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 (40) hide show
  1. package/README.md +7 -2
  2. package/dist/{adapter-gateway-apollo-CxJNBVyD.mjs → adapter-gateway-apollo-B54G_cXT.mjs} +15 -13
  3. package/dist/{adapter-gateway-apollo-CxJNBVyD.mjs.map → adapter-gateway-apollo-B54G_cXT.mjs.map} +1 -1
  4. package/dist/{adapter-gateway-apollo-DowrX6E_.mjs → adapter-gateway-apollo-D2DNlPY0.mjs} +3 -3
  5. package/dist/{adapter-gateway-mercurius-CZVy88Za.mjs → adapter-gateway-mercurius-rbKlwF9m.mjs} +4 -4
  6. package/dist/{adapter-gateway-mercurius-CZVy88Za.mjs.map → adapter-gateway-mercurius-rbKlwF9m.mjs.map} +1 -1
  7. package/dist/{adapter-gateway-stitching-CaZjvRzx.mjs → adapter-gateway-stitching-Dzv10u8D.mjs} +15 -10
  8. package/dist/{adapter-gateway-stitching-CaZjvRzx.mjs.map → adapter-gateway-stitching-Dzv10u8D.mjs.map} +1 -1
  9. package/dist/{adapter-http-express-B39rPhqH.mjs → adapter-http-express-CWqziB1p.mjs} +7 -7
  10. package/dist/adapter-http-express-CWqziB1p.mjs.map +1 -0
  11. package/dist/{adapter-http-fastify-BwXO6rqE.mjs → adapter-http-fastify-CYZQ6FFv.mjs} +10 -9
  12. package/dist/adapter-http-fastify-CYZQ6FFv.mjs.map +1 -0
  13. package/dist/index.d.mts +1248 -593
  14. package/dist/index.d.mts.map +1 -1
  15. package/dist/index.mjs +1784 -487
  16. package/dist/index.mjs.map +1 -1
  17. package/dist/{utils-XWVecaNn.mjs → pieces-DRY6haAO.mjs} +177 -9
  18. package/dist/pieces-DRY6haAO.mjs.map +1 -0
  19. package/dist/rolldown-runtime-8H4AJuhK.mjs +16 -0
  20. package/dist/run-node-handler-CQjyjOuN.mjs +52 -0
  21. package/dist/run-node-handler-CQjyjOuN.mjs.map +1 -0
  22. package/dist/src/packages/https-hooks.d.mts +4 -4
  23. package/dist/src/packages/https-hooks.d.mts.map +1 -1
  24. package/dist/src/packages/https-hooks.mjs +2 -2
  25. package/dist/src/packages/https-hooks.mjs.map +1 -1
  26. package/dist/src/packages/vite-loader.d.mts +19 -9
  27. package/dist/src/packages/vite-loader.d.mts.map +1 -1
  28. package/dist/src/packages/vite-loader.mjs +114 -46
  29. package/dist/src/packages/vite-loader.mjs.map +1 -1
  30. package/dist/{types-DcYyNcHJ.d.mts → types-DlTiz6AA.d.mts} +27 -4
  31. package/dist/types-DlTiz6AA.d.mts.map +1 -0
  32. package/dist/{websocket-DncAB9XK.mjs → websocket-_SH5aBcy.mjs} +3 -3
  33. package/dist/{websocket-DncAB9XK.mjs.map → websocket-_SH5aBcy.mjs.map} +1 -1
  34. package/package.json +18 -18
  35. package/dist/adapter-http-express-B39rPhqH.mjs.map +0 -1
  36. package/dist/adapter-http-fastify-BwXO6rqE.mjs.map +0 -1
  37. package/dist/path-normalize-BOQwY9wR.mjs +0 -23
  38. package/dist/path-normalize-BOQwY9wR.mjs.map +0 -1
  39. package/dist/types-DcYyNcHJ.d.mts.map +0 -1
  40. 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]="a15aa3df-1a6d-517d-93c4-3e8a84cefd09")}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]="a50faed2-4e40-54ba-873f-1808cedc1e5c")}catch(e){}}();
3
+ import { t as __exportAll } from "./rolldown-runtime-8H4AJuhK.mjs";
4
+ import { A as RefusalNotRecordedError, C as AuthorizationPolicy, D as AuthenticationRequiredError, E as AuthEvaluationUnsupportedError, O as ForbiddenError, S as createCanonicalDocumentIdResolver, T as createAuthorizationService, _ as loadProcessors, a as pieceListLocation, b as callerSubject, c as buildGraphQlDocument, d as buildGraphqlOperations, f as isSubgraphClass, g as loadDocumentModels, k as MalformedStoredOperationError, l as buildGraphQlDriveDocument, m as extractUpgradeManifests, o as piecesFromCdnList, p as debounce, r as PIECE_SOURCE_LIST, s as piecesFromListModule, u as buildGraphqlOperation, v as loadSubgraphs, w as AuthorizedDocumentHandle, x as CanonicalDocumentIdResolutionError, y as BaseSubgraph } from "./pieces-DRY6haAO.mjs";
4
5
  import { AnalyticsQueryEngine } from "@powerhousedao/analytics-engine-core";
5
6
  import { AnalyticsModel, AnalyticsResolvers, typedefs } from "@powerhousedao/analytics-engine-graphql";
6
7
  import { gql } from "graphql-tag";
7
- import { GraphQLError, Kind, parse, print } from "graphql";
8
- import { AuthEnforcementDisabledError, DEFAULT_DRIVE_CONTAINER_TYPES, DriveCollectionId, ModelReadGate, PropagationMode as PropagationMode$1, SyncScopeGate, consolidateSyncOperations, createReactorHostModuleBase, driveIdFromUrl, envelopesToSyncOperations, parseDriveUrl, readDecisionModel, syncOperationErrorType } from "@powerhousedao/reactor";
8
+ import { GraphQLError, Kind, Lexer, Source, TokenKind, parse, print, visit } from "graphql";
9
+ import { AuthEnforcementDisabledError, BareReadGate, ChannelError, ChannelErrorSource, DEFAULT_DRIVE_CONTAINER_TYPES, DriveCollectionId, JobStatus, ModelReadGate, PropagationMode as PropagationMode$1, SyncOperation, SyncScopeGate, consolidateSyncOperations, createReactorHostModuleBase, driveIdFromUrl, envelopesToSyncOperations, parseDriveUrl, readDecisionModel, supportsPurgeRefusals, syncOperationErrorType } from "@powerhousedao/reactor";
10
+ import { deserializeSignature, readPeerManifest, serializeSignature, setName, withSignaturePolicy } from "@powerhousedao/shared/document-model";
9
11
  import { ConsoleLogger, childLogger, documentModelDocumentModelModule } from "document-model";
10
- import path, { join } from "node:path";
12
+ import path, { isAbsolute, join } from "node:path";
11
13
  import { match } from "path-to-regexp";
12
14
  import posix from "node:path/posix";
13
15
  import { Readable } from "node:stream";
14
- import fs, { readFileSync, watchFile } from "node:fs";
16
+ import fs, { existsSync, readFileSync, watchFile } from "node:fs";
15
17
  import { pipeline } from "node:stream/promises";
16
18
  import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
19
+ import { pathToFileURL } from "node:url";
17
20
  import { buildSubgraphSchema } from "@apollo/subgraph";
18
21
  import { typeDefs } from "@powerhousedao/document-engineering/graphql";
19
22
  import { camelCase, kebabCase, pascalCase } from "change-case";
20
23
  import { GraphQLJSON, GraphQLJSONObject } from "graphql-type-json";
21
- import { deserializeSignature, serializeSignature, setName } 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";
@@ -32,7 +34,7 @@ import { ReactorGroupV1 } from "@powerhousedao/reactor-group";
32
34
  import { driveDocumentModelModule } from "@powerhousedao/shared/document-drive";
33
35
  import EventEmitter from "node:events";
34
36
  import { PostgresAnalyticsStore } from "@powerhousedao/analytics-engine-pg";
35
- import { AttachmentBuilder, AttachmentReferenceIndexBuilder, createRef, createS3AttachmentBackend, parseAttachmentStorageConfig, parseRef } from "@powerhousedao/reactor-attachments";
37
+ import { AttachmentBuilder, AttachmentNotFound, AttachmentReferenceIndexBuilder, createRef, createS3AttachmentBackend, parseAttachmentStorageConfig, parseRef, validateReserveMetadata } from "@powerhousedao/reactor-attachments";
36
38
  import { createAttachmentClient } from "@powerhousedao/reactor-attachments/client";
37
39
  import { setupMcpServer } from "@powerhousedao/reactor-mcp";
38
40
  import { mkdir } from "node:fs/promises";
@@ -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,17 +57,60 @@ 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
79
- var schema_default$2 = "# Auth Subgraph Schema\n# Contains all document permission and authorization related types\n\nscalar DateTime\n\n# Permission levels for documents\nenum DocumentPermissionLevel {\n # Can fetch and read the document\n READ\n # Can push updates and modify the document\n WRITE\n # Can manage document permissions and settings\n ADMIN\n}\n\n# Document permission entry\ntype DocumentPermissionEntry {\n documentId: String!\n userAddress: String!\n permission: DocumentPermissionLevel!\n grantedBy: String!\n createdAt: DateTime!\n updatedAt: DateTime!\n}\n\n# Operation user permission entry\ntype OperationUserPermission {\n documentId: String!\n operationType: String!\n userAddress: String!\n grantedBy: String!\n createdAt: DateTime!\n}\n\n# Combined document access info\ntype DocumentAccessInfo {\n documentId: String!\n permissions: [DocumentPermissionEntry!]!\n}\n\n# Operation permissions info\ntype OperationPermissionsInfo {\n documentId: String!\n operationType: String!\n userPermissions: [OperationUserPermission!]!\n}\n\n# Document protection info\ntype DocumentProtectionInfo {\n documentId: String!\n protected: Boolean!\n ownerAddress: String\n}\n\ntype Query {\n # Get permissions for a document (requires ADMIN permission or global admin)\n documentAccess(documentId: String!): DocumentAccessInfo!\n\n # Get protection status for a document (requires ADMIN permission or global admin)\n documentProtection(documentId: String!): DocumentProtectionInfo!\n\n # Get all documents the current user has explicit access to\n userDocumentPermissions: [DocumentPermissionEntry!]!\n\n # Get operation permissions for a document and operation type\n # (requires ADMIN permission or global admin)\n operationPermissions(\n documentId: String!\n operationType: String!\n ): OperationPermissionsInfo!\n\n # Check if the current user can execute a specific operation on a document\n canExecuteOperation(documentId: String!, operationType: String!): Boolean!\n}\n\ntype Mutation {\n # Set document protection status (requires ADMIN permission or global admin)\n setDocumentProtection(\n documentId: String!\n protected: Boolean!\n ): DocumentProtectionInfo!\n\n # Transfer document ownership (requires ADMIN permission or global admin)\n transferDocumentOwnership(\n documentId: String!\n newOwnerAddress: String!\n ): DocumentProtectionInfo!\n\n # Grant a user permission on a document (requires ADMIN permission or global admin)\n grantDocumentPermission(\n documentId: String!\n userAddress: String!\n permission: DocumentPermissionLevel!\n ): DocumentPermissionEntry!\n\n # Revoke a user's permission on a document (requires ADMIN permission or global admin)\n revokeDocumentPermission(documentId: String!, userAddress: String!): Boolean!\n\n # --- Operation Permissions ---\n\n # Grant a user permission to execute an operation on a document\n grantOperationPermission(\n documentId: String!\n operationType: String!\n userAddress: String!\n ): OperationUserPermission!\n\n # Revoke a user's permission to execute an operation\n revokeOperationPermission(\n documentId: String!\n operationType: String!\n userAddress: String!\n ): Boolean!\n}\n";
90
+ var schema_default$2 = "# Auth Subgraph Schema\n# Contains all document permission and authorization related types\n\nscalar DateTime\n\n# Permission levels for documents\nenum DocumentPermissionLevel {\n # Can fetch and read the document\n READ\n # Can push updates and modify the document\n WRITE\n # Can manage document permissions and settings\n ADMIN\n}\n\n# Document permission entry\ntype DocumentPermissionEntry {\n documentId: String!\n userAddress: String!\n permission: DocumentPermissionLevel!\n grantedBy: String!\n createdAt: DateTime!\n updatedAt: DateTime!\n}\n\n# Operation user permission entry\ntype OperationUserPermission {\n documentId: String!\n operationType: String!\n userAddress: String!\n grantedBy: String!\n createdAt: DateTime!\n}\n\n# Combined document access info\ntype DocumentAccessInfo {\n documentId: String!\n permissions: [DocumentPermissionEntry!]!\n}\n\n# Operation permissions info\ntype OperationPermissionsInfo {\n documentId: String!\n operationType: String!\n userPermissions: [OperationUserPermission!]!\n}\n\n# Document protection info\ntype DocumentProtectionInfo {\n documentId: String!\n protected: Boolean!\n ownerAddress: String\n}\n\ntype Query {\n # Get permissions for a document (requires ADMIN permission or global admin)\n documentAccess(\n documentIdOrSlug: String\n documentId: String @deprecated(reason: \"Use documentIdOrSlug.\")\n ): DocumentAccessInfo!\n\n # Get protection status for a document (requires ADMIN permission or global admin)\n documentProtection(\n documentIdOrSlug: String\n documentId: String @deprecated(reason: \"Use documentIdOrSlug.\")\n ): DocumentProtectionInfo!\n\n # Get all documents the current user has explicit access to\n userDocumentPermissions: [DocumentPermissionEntry!]!\n\n # Get operation permissions for a document and operation type\n # (requires ADMIN permission or global admin)\n operationPermissions(\n documentIdOrSlug: String\n documentId: String @deprecated(reason: \"Use documentIdOrSlug.\")\n operationType: String!\n ): OperationPermissionsInfo!\n\n # Check if the current user can execute a specific operation on a document\n canExecuteOperation(\n documentIdOrSlug: String\n documentId: String @deprecated(reason: \"Use documentIdOrSlug.\")\n operationType: String!\n ): Boolean!\n}\n\ntype Mutation {\n # Set document protection status (requires ADMIN permission or global admin)\n setDocumentProtection(\n documentIdOrSlug: String\n documentId: String @deprecated(reason: \"Use documentIdOrSlug.\")\n protected: Boolean!\n ): DocumentProtectionInfo!\n\n # Transfer document ownership (requires ADMIN permission or global admin)\n transferDocumentOwnership(\n documentIdOrSlug: String\n documentId: String @deprecated(reason: \"Use documentIdOrSlug.\")\n newOwnerAddress: String!\n ): DocumentProtectionInfo!\n\n # Grant a user permission on a document (requires ADMIN permission or global admin)\n grantDocumentPermission(\n documentIdOrSlug: String\n documentId: String @deprecated(reason: \"Use documentIdOrSlug.\")\n userAddress: String!\n permission: DocumentPermissionLevel!\n ): DocumentPermissionEntry!\n\n # Revoke a user's permission on a document (requires ADMIN permission or global admin)\n revokeDocumentPermission(\n documentIdOrSlug: String\n documentId: String @deprecated(reason: \"Use documentIdOrSlug.\")\n userAddress: String!\n ): Boolean!\n\n # --- Operation Permissions ---\n\n # Grant a user permission to execute an operation on a document\n grantOperationPermission(\n documentIdOrSlug: String\n documentId: String @deprecated(reason: \"Use documentIdOrSlug.\")\n operationType: String!\n userAddress: String!\n ): OperationUserPermission!\n\n # Revoke a user's permission to execute an operation\n revokeOperationPermission(\n documentIdOrSlug: String\n documentId: String @deprecated(reason: \"Use documentIdOrSlug.\")\n operationType: String!\n userAddress: String!\n ): Boolean!\n}\n";
91
+ //#endregion
92
+ //#region src/graphql/argument-aliases.ts
93
+ /** A renamed argument given under both names, or a required one under neither. */
94
+ var ArgumentAliasError = class extends GraphQLError {
95
+ constructor(message) {
96
+ super(message, { extensions: { code: "BAD_USER_INPUT" } });
97
+ }
98
+ };
99
+ /** The value of a required argument that may still arrive under its old name. */
100
+ function requireOneOf(args, name, deprecatedName) {
101
+ const current = args[name] ?? void 0;
102
+ const deprecated = args[deprecatedName] ?? void 0;
103
+ if (current !== void 0 && deprecated !== void 0) throw new ArgumentAliasError(`Pass ${name} or ${deprecatedName}, not both.`);
104
+ if (current === void 0 && deprecated === void 0) throw new ArgumentAliasError(`${name} is required.`);
105
+ return current ?? deprecated;
106
+ }
107
+ /** The value of an optional argument that may still arrive under its old name. */
108
+ function optionalOneOf(args, name, deprecatedName) {
109
+ const current = args[name] ?? void 0;
110
+ const deprecated = args[deprecatedName] ?? void 0;
111
+ if (current !== void 0 && deprecated !== void 0) throw new ArgumentAliasError(`Pass ${name} or ${deprecatedName}, not both.`);
112
+ return current ?? deprecated;
113
+ }
80
114
  //#endregion
81
115
  //#region src/graphql/auth/resolvers.ts
82
116
  async function documentAccess(service, authorizationService, args, userAddress) {
@@ -172,7 +206,7 @@ var AuthSubgraph = class extends BaseSubgraph {
172
206
  this.logger.debug("documentAccess(@args)", args);
173
207
  if (!this.documentPermissionService) throw new GraphQLError("DocumentPermissionService not available");
174
208
  try {
175
- return await documentAccess(this.documentPermissionService, this.authorizationService, await this.withCanonicalDocumentId(args, ctx), ctx.user?.address);
209
+ return await documentAccess(this.documentPermissionService, this.authorizationService, await this.#withCanonicalDocumentArgs(args, ctx), ctx.user?.address);
176
210
  } catch (error) {
177
211
  this.logger.error("Error in documentAccess: @error", error);
178
212
  throw error;
@@ -193,7 +227,7 @@ var AuthSubgraph = class extends BaseSubgraph {
193
227
  this.logger.debug("operationPermissions(@args)", args);
194
228
  if (!this.documentPermissionService) throw new GraphQLError("DocumentPermissionService not available");
195
229
  try {
196
- return await operationPermissions(this.documentPermissionService, this.authorizationService, await this.withCanonicalDocumentId(args, ctx), ctx.user?.address);
230
+ return await operationPermissions(this.documentPermissionService, this.authorizationService, await this.#withCanonicalDocumentArgs(args, ctx), ctx.user?.address);
197
231
  } catch (error) {
198
232
  this.logger.error("Error in operationPermissions: @error", error);
199
233
  throw error;
@@ -202,7 +236,7 @@ var AuthSubgraph = class extends BaseSubgraph {
202
236
  canExecuteOperation: async (_parent, args, ctx) => {
203
237
  this.logger.debug("canExecuteOperation(@args)", args);
204
238
  try {
205
- return await canExecuteOperation(this.authorizationService, await this.withCanonicalDocumentId(args, ctx), ctx.user?.address);
239
+ return await canExecuteOperation(this.authorizationService, await this.#withCanonicalDocumentArgs(args, ctx), ctx.user?.address);
206
240
  } catch (error) {
207
241
  this.logger.error("Error in canExecuteOperation: @error", error);
208
242
  throw error;
@@ -212,7 +246,7 @@ var AuthSubgraph = class extends BaseSubgraph {
212
246
  this.logger.debug("documentProtection(@args)", args);
213
247
  if (!this.documentPermissionService) throw new GraphQLError("DocumentPermissionService not available");
214
248
  try {
215
- return await documentProtection(this.documentPermissionService, this.authorizationService, await this.withCanonicalDocumentId(args, ctx), ctx.user?.address);
249
+ return await documentProtection(this.documentPermissionService, this.authorizationService, await this.#withCanonicalDocumentArgs(args, ctx), ctx.user?.address);
216
250
  } catch (error) {
217
251
  this.logger.error("Error in documentProtection: @error", error);
218
252
  throw error;
@@ -224,7 +258,7 @@ var AuthSubgraph = class extends BaseSubgraph {
224
258
  this.logger.debug("setDocumentProtection(@args)", args);
225
259
  if (!this.documentPermissionService) throw new GraphQLError("DocumentPermissionService not available");
226
260
  try {
227
- return await setDocumentProtection(this.documentPermissionService, this.authorizationService, await this.withCanonicalDocumentId(args, ctx), ctx.user?.address);
261
+ return await setDocumentProtection(this.documentPermissionService, this.authorizationService, await this.#withCanonicalDocumentArgs(args, ctx), ctx.user?.address);
228
262
  } catch (error) {
229
263
  this.logger.error("Error in setDocumentProtection: @error", error);
230
264
  throw error;
@@ -234,7 +268,7 @@ var AuthSubgraph = class extends BaseSubgraph {
234
268
  this.logger.debug("transferDocumentOwnership(@args)", args);
235
269
  if (!this.documentPermissionService) throw new GraphQLError("DocumentPermissionService not available");
236
270
  try {
237
- return await transferDocumentOwnership(this.documentPermissionService, this.authorizationService, await this.withCanonicalDocumentId(args, ctx), ctx.user?.address);
271
+ return await transferDocumentOwnership(this.documentPermissionService, this.authorizationService, await this.#withCanonicalDocumentArgs(args, ctx), ctx.user?.address);
238
272
  } catch (error) {
239
273
  this.logger.error("Error in transferDocumentOwnership: @error", error);
240
274
  throw error;
@@ -244,7 +278,7 @@ var AuthSubgraph = class extends BaseSubgraph {
244
278
  this.logger.debug("grantDocumentPermission(@args)", args);
245
279
  if (!this.documentPermissionService) throw new GraphQLError("DocumentPermissionService not available");
246
280
  try {
247
- const resolved = await this.withCanonicalDocumentId(args, ctx);
281
+ const resolved = await this.#withCanonicalDocumentArgs(args, ctx);
248
282
  return await grantDocumentPermission(this.documentPermissionService, this.authorizationService, {
249
283
  ...resolved,
250
284
  permission: resolved.permission
@@ -258,7 +292,7 @@ var AuthSubgraph = class extends BaseSubgraph {
258
292
  this.logger.debug("revokeDocumentPermission(@args)", args);
259
293
  if (!this.documentPermissionService) throw new GraphQLError("DocumentPermissionService not available");
260
294
  try {
261
- return await revokeDocumentPermission(this.documentPermissionService, this.authorizationService, await this.withCanonicalDocumentId(args, ctx), ctx.user?.address);
295
+ return await revokeDocumentPermission(this.documentPermissionService, this.authorizationService, await this.#withCanonicalDocumentArgs(args, ctx), ctx.user?.address);
262
296
  } catch (error) {
263
297
  this.logger.error("Error in revokeDocumentPermission: @error", error);
264
298
  throw error;
@@ -268,7 +302,7 @@ var AuthSubgraph = class extends BaseSubgraph {
268
302
  this.logger.debug("grantOperationPermission(@args)", args);
269
303
  if (!this.documentPermissionService) throw new GraphQLError("DocumentPermissionService not available");
270
304
  try {
271
- return await grantOperationPermission(this.documentPermissionService, this.authorizationService, await this.withCanonicalDocumentId(args, ctx), ctx.user?.address);
305
+ return await grantOperationPermission(this.documentPermissionService, this.authorizationService, await this.#withCanonicalDocumentArgs(args, ctx), ctx.user?.address);
272
306
  } catch (error) {
273
307
  this.logger.error("Error in grantOperationPermission: @error", error);
274
308
  throw error;
@@ -278,7 +312,7 @@ var AuthSubgraph = class extends BaseSubgraph {
278
312
  this.logger.debug("revokeOperationPermission(@args)", args);
279
313
  if (!this.documentPermissionService) throw new GraphQLError("DocumentPermissionService not available");
280
314
  try {
281
- return await revokeOperationPermission(this.documentPermissionService, this.authorizationService, await this.withCanonicalDocumentId(args, ctx), ctx.user?.address);
315
+ return await revokeOperationPermission(this.documentPermissionService, this.authorizationService, await this.#withCanonicalDocumentArgs(args, ctx), ctx.user?.address);
282
316
  } catch (error) {
283
317
  this.logger.error("Error in revokeOperationPermission: @error", error);
284
318
  throw error;
@@ -286,6 +320,16 @@ var AuthSubgraph = class extends BaseSubgraph {
286
320
  }
287
321
  }
288
322
  };
323
+ #withCanonicalDocumentArgs(args, ctx) {
324
+ const { documentIdOrSlug, documentId, ...rest } = args;
325
+ return this.withCanonicalDocumentId({
326
+ ...rest,
327
+ documentId: requireOneOf({
328
+ documentIdOrSlug,
329
+ documentId
330
+ }, "documentIdOrSlug", "documentId")
331
+ }, ctx);
332
+ }
289
333
  onSetup() {
290
334
  this.logger.debug("Setting up AuthSubgraph");
291
335
  return Promise.resolve();
@@ -311,15 +355,15 @@ function createAuthFetchMiddleware(authService) {
311
355
  async function createGatewayAdapter(type, logger) {
312
356
  switch (type) {
313
357
  case "apollo": {
314
- const { ApolloGatewayAdapter } = await import("./adapter-gateway-apollo-DowrX6E_.mjs");
358
+ const { ApolloGatewayAdapter } = await import("./adapter-gateway-apollo-D2DNlPY0.mjs");
315
359
  return new ApolloGatewayAdapter(logger);
316
360
  }
317
361
  case "mercurius": {
318
- const { MercuriusGatewayAdapter } = await import("./adapter-gateway-mercurius-CZVy88Za.mjs");
362
+ const { MercuriusGatewayAdapter } = await import("./adapter-gateway-mercurius-rbKlwF9m.mjs");
319
363
  return new MercuriusGatewayAdapter(logger);
320
364
  }
321
365
  case "stitching": {
322
- const { StitchingGatewayAdapter } = await import("./adapter-gateway-stitching-CaZjvRzx.mjs");
366
+ const { StitchingGatewayAdapter } = await import("./adapter-gateway-stitching-Dzv10u8D.mjs");
323
367
  return new StitchingGatewayAdapter(logger);
324
368
  }
325
369
  }
@@ -327,11 +371,11 @@ async function createGatewayAdapter(type, logger) {
327
371
  async function createHttpAdapter(type) {
328
372
  switch (type) {
329
373
  case "express": {
330
- const { createExpressHttpAdapter } = await import("./adapter-http-express-B39rPhqH.mjs");
374
+ const { createExpressHttpAdapter } = await import("./adapter-http-express-CWqziB1p.mjs");
331
375
  return createExpressHttpAdapter();
332
376
  }
333
377
  case "fastify": {
334
- const { createFastifyHttpAdapter } = await import("./adapter-http-fastify-BwXO6rqE.mjs");
378
+ const { createFastifyHttpAdapter } = await import("./adapter-http-fastify-CYZQ6FFv.mjs");
335
379
  return createFastifyHttpAdapter();
336
380
  }
337
381
  }
@@ -359,7 +403,7 @@ const WS_AUTH_CLOSE_REASONS = [WS_CLOSE_REASON_AUTHENTICATION_REQUIRED, WS_CLOSE
359
403
  */
360
404
  const SEGMENT = /^[a-z0-9][-a-z0-9._]*$/i;
361
405
  /** Segments that would read as path traversal rather than as a name. */
362
- const TRAVERSAL = new Set([".", ".."]);
406
+ const TRAVERSAL = /* @__PURE__ */ new Set([".", ".."]);
363
407
  var InvalidNamespaceError = class extends Error {};
364
408
  /**
365
409
  * Turns a package name into the path segments it occupies.
@@ -845,27 +889,29 @@ var HttpScope = class {
845
889
  headers: fetchHeaders(req),
846
890
  signal: controller.signal
847
891
  };
848
- if (method !== "GET" && method !== "HEAD" && body !== "none") if (body === "stream") init = {
849
- ...init,
850
- body: Readable.toWeb(req),
851
- duplex: "half"
852
- };
853
- else {
854
- try {
855
- raw = await readBody(req, spec.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES$1);
856
- } catch (error) {
857
- if (error instanceof BodyTooLargeError) {
858
- res.setHeader("connection", "close");
859
- res.once("finish", () => req.destroy());
860
- writeError(res, 413, "Payload too large");
861
- return;
862
- }
863
- throw error;
864
- }
865
- if (raw.length) init = {
892
+ if (method !== "GET" && method !== "HEAD" && body !== "none") {
893
+ if (body === "stream") init = {
866
894
  ...init,
867
- body: new Uint8Array(raw.buffer, raw.byteOffset, raw.byteLength)
895
+ body: Readable.toWeb(req),
896
+ duplex: "half"
868
897
  };
898
+ else {
899
+ try {
900
+ raw = await readBody(req, spec.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES$1);
901
+ } catch (error) {
902
+ if (error instanceof BodyTooLargeError) {
903
+ res.setHeader("connection", "close");
904
+ res.once("finish", () => req.destroy());
905
+ writeError(res, 413, "Payload too large");
906
+ return;
907
+ }
908
+ throw error;
909
+ }
910
+ if (raw.length) init = {
911
+ ...init,
912
+ body: new Uint8Array(raw.buffer, raw.byteOffset, raw.byteLength)
913
+ };
914
+ }
869
915
  }
870
916
  await writeResponse(res, await spec.handler(new Request(url, init), {
871
917
  ...ctx,
@@ -1042,7 +1088,7 @@ function verifyWebhook(options) {
1042
1088
  }
1043
1089
  /** Headers that would put a credential in a log or run journal. The configured signature
1044
1090
  * header joins them: it is an HMAC of the body under a live secret. */
1045
- const REDACTED_HEADERS = new Set([
1091
+ const REDACTED_HEADERS = /* @__PURE__ */ new Set([
1046
1092
  "authorization",
1047
1093
  "proxy-authorization",
1048
1094
  "cookie",
@@ -1315,6 +1361,7 @@ function newWebhookToken() {
1315
1361
  const NAMESPACE = "reactor_webhooks";
1316
1362
  /** Relational implementation, over the reactor's own database. */
1317
1363
  var RelationalWebhookStore = class {
1364
+ relationalDb;
1318
1365
  #db;
1319
1366
  constructor(relationalDb) {
1320
1367
  this.relationalDb = relationalDb;
@@ -1329,7 +1376,11 @@ var RelationalWebhookStore = class {
1329
1376
  "owner_key"
1330
1377
  ]).unique().execute();
1331
1378
  await db.schema.createTable("webhook_deliveries").ifNotExists().addColumn("token", "text", (col) => col.notNull()).addColumn("dedupe_key", "text", (col) => col.notNull()).addColumn("seen_at", "text", (col) => col.notNull()).addColumn("claim_id", "text", (col) => col.notNull()).execute();
1379
+ try {
1380
+ await db.schema.alterTable("webhook_deliveries").addColumn("expires_at", "text").execute();
1381
+ } catch {}
1332
1382
  await db.schema.createIndex("webhook_deliveries_key").ifNotExists().on("webhook_deliveries").columns(["token", "dedupe_key"]).unique().execute();
1383
+ await db.schema.createIndex("webhook_deliveries_expires_at").ifNotExists().on("webhook_deliveries").columns(["expires_at"]).execute();
1333
1384
  await db.schema.createIndex("webhook_deliveries_seen_at").ifNotExists().on("webhook_deliveries").columns(["seen_at"]).execute();
1334
1385
  this.#db = db;
1335
1386
  }
@@ -1370,13 +1421,16 @@ var RelationalWebhookStore = class {
1370
1421
  }
1371
1422
  async seen(token, key, ttlSeconds) {
1372
1423
  const now = Date.now();
1424
+ const nowIso = new Date(now).toISOString();
1373
1425
  const cutoff = (/* @__PURE__ */ new Date(now - ttlSeconds * 1e3)).toISOString();
1374
- await this.#handle.deleteFrom("webhook_deliveries").where("seen_at", "<", cutoff).execute();
1426
+ await this.#handle.deleteFrom("webhook_deliveries").where((eb) => eb.or([eb("expires_at", "<", nowIso), eb.and([eb("expires_at", "is", null), eb("seen_at", "<", cutoff)])])).execute();
1427
+ await this.#handle.deleteFrom("webhook_deliveries").where("token", "=", token).where("dedupe_key", "=", key).where("seen_at", "<", cutoff).execute();
1375
1428
  const claimId = randomBytes(16).toString("hex");
1376
1429
  await this.#handle.insertInto("webhook_deliveries").values({
1377
1430
  token,
1378
1431
  dedupe_key: key,
1379
- seen_at: new Date(now).toISOString(),
1432
+ seen_at: nowIso,
1433
+ expires_at: new Date(now + ttlSeconds * 1e3).toISOString(),
1380
1434
  claim_id: claimId
1381
1435
  }).onConflict((oc) => oc.columns(["token", "dedupe_key"]).doNothing()).execute();
1382
1436
  return (await this.#handle.selectFrom("webhook_deliveries").select("claim_id").where("token", "=", token).where("dedupe_key", "=", key).executeTakeFirst())?.claim_id !== claimId;
@@ -1422,10 +1476,14 @@ var MemoryWebhookStore = class {
1422
1476
  }
1423
1477
  seen(token, key, ttlSeconds) {
1424
1478
  const now = Date.now();
1425
- for (const [k, at] of this.#deliveries) if (at < now - ttlSeconds * 1e3) this.#deliveries.delete(k);
1479
+ for (const [k, entry] of this.#deliveries) if (entry.expiresAt < now) this.#deliveries.delete(k);
1426
1480
  const composite = `${token}|${key}`;
1427
- if (this.#deliveries.has(composite)) return Promise.resolve(true);
1428
- this.#deliveries.set(composite, now);
1481
+ const existing = this.#deliveries.get(composite);
1482
+ if (existing && existing.seenAt >= now - ttlSeconds * 1e3) return Promise.resolve(true);
1483
+ this.#deliveries.set(composite, {
1484
+ seenAt: now,
1485
+ expiresAt: now + ttlSeconds * 1e3
1486
+ });
1429
1487
  return Promise.resolve(false);
1430
1488
  }
1431
1489
  };
@@ -1460,7 +1518,7 @@ const stripScalarDefinitions = (doc) => {
1460
1518
  * whole gateway. We dedupe these by name; field/operation/extension nodes are
1461
1519
  * left untouched.
1462
1520
  */
1463
- const TYPE_DEFINITION_KINDS = new Set([
1521
+ const TYPE_DEFINITION_KINDS = /* @__PURE__ */ new Set([
1464
1522
  Kind.OBJECT_TYPE_DEFINITION,
1465
1523
  Kind.ENUM_TYPE_DEFINITION,
1466
1524
  Kind.INPUT_OBJECT_TYPE_DEFINITION,
@@ -1496,6 +1554,98 @@ const dedupeTypeDefinitions = (doc) => {
1496
1554
  definitions
1497
1555
  };
1498
1556
  };
1557
+ const EMPTY_DOCUMENT = {
1558
+ kind: Kind.DOCUMENT,
1559
+ definitions: []
1560
+ };
1561
+ const OUTPUT_TYPE_KINDS = [
1562
+ Kind.OBJECT_TYPE_DEFINITION,
1563
+ Kind.OBJECT_TYPE_EXTENSION,
1564
+ Kind.ENUM_TYPE_DEFINITION,
1565
+ Kind.ENUM_TYPE_EXTENSION,
1566
+ Kind.UNION_TYPE_DEFINITION,
1567
+ Kind.UNION_TYPE_EXTENSION,
1568
+ Kind.INTERFACE_TYPE_DEFINITION,
1569
+ Kind.INTERFACE_TYPE_EXTENSION
1570
+ ];
1571
+ const STATE_PREFIX_KINDS = new Set(OUTPUT_TYPE_KINDS);
1572
+ const ALL_PREFIX_KINDS = /* @__PURE__ */ new Set([
1573
+ ...OUTPUT_TYPE_KINDS,
1574
+ Kind.INPUT_OBJECT_TYPE_DEFINITION,
1575
+ Kind.INPUT_OBJECT_TYPE_EXTENSION
1576
+ ]);
1577
+ const TYPE_NAME_KINDS = /* @__PURE__ */ new Set([
1578
+ ...ALL_PREFIX_KINDS,
1579
+ Kind.SCALAR_TYPE_DEFINITION,
1580
+ Kind.SCALAR_TYPE_EXTENSION,
1581
+ Kind.NAMED_TYPE
1582
+ ]);
1583
+ const LOCAL_STATE_STUBS = /* @__PURE__ */ new Set([
1584
+ "AccountSnapshotLocalState",
1585
+ "BudgetStatementLocalState",
1586
+ "ScopeFrameworkLocalState"
1587
+ ]);
1588
+ /** Parse SDL; blank or comment-only input yields an empty document. */
1589
+ function parseSdl(sdl) {
1590
+ const source = new Source(sdl);
1591
+ if (new Lexer(source).advance().kind === TokenKind.EOF) return EMPTY_DOCUMENT;
1592
+ try {
1593
+ return parse(source, { noLocation: true });
1594
+ } catch (error) {
1595
+ const withoutSemicolons = sdl.replaceAll(";", "");
1596
+ if (withoutSemicolons === sdl) throw error;
1597
+ return parse(withoutSemicolons, { noLocation: true });
1598
+ }
1599
+ }
1600
+ function definedTypeNames(doc, kinds) {
1601
+ return doc.definitions.filter((def) => kinds.has(def.kind)).map((def) => def.name.value);
1602
+ }
1603
+ /** Prefix type definition names and type references found in `names`. */
1604
+ function prefixTypeNames(doc, prefix, names) {
1605
+ return visit(doc, { enter(node) {
1606
+ if (!TYPE_NAME_KINDS.has(node.kind)) return void 0;
1607
+ const { name } = node;
1608
+ if (!names.has(name.value)) return void 0;
1609
+ return {
1610
+ ...node,
1611
+ name: {
1612
+ ...name,
1613
+ value: `${prefix}_${name.value}`
1614
+ }
1615
+ };
1616
+ } });
1617
+ }
1618
+ /** State schema definitions without inputs, `DateTime` or the given object types. */
1619
+ function stateTypeDefinitions(sdl, droppedObjectTypes = /* @__PURE__ */ new Set()) {
1620
+ return parseSdl(sdl).definitions.filter((def) => def.kind !== Kind.INPUT_OBJECT_TYPE_DEFINITION && def.kind !== Kind.INPUT_OBJECT_TYPE_EXTENSION && !(def.kind === Kind.SCALAR_TYPE_DEFINITION && def.name.value === "DateTime") && !(def.kind === Kind.OBJECT_TYPE_DEFINITION && droppedObjectTypes.has(def.name.value)));
1621
+ }
1622
+ /**
1623
+ * Prefixed operation schemas per module. The first definition of each module
1624
+ * carries a "Module: <Name>" description unless it has its own.
1625
+ */
1626
+ function buildModuleSchemas(specification, documentName, allTypeNames, hasValidSchema) {
1627
+ const names = new Set(allTypeNames);
1628
+ return specification?.modules.map((module) => {
1629
+ const definitions = module.operations.filter((op) => hasValidSchema(op.schema)).flatMap((op) => {
1630
+ const doc = parseSdl(op.schema ?? "");
1631
+ return prefixTypeNames(doc, documentName, /* @__PURE__ */ new Set([...names, ...definedTypeNames(doc, ALL_PREFIX_KINDS)])).definitions;
1632
+ });
1633
+ if (definitions.length === 0) return "";
1634
+ const [first, ...rest] = definitions;
1635
+ const header = "description" in first && !first.description ? {
1636
+ ...first,
1637
+ description: {
1638
+ kind: Kind.STRING,
1639
+ value: `Module: ${pascalCase(module.name)}`,
1640
+ block: true
1641
+ }
1642
+ } : first;
1643
+ return print({
1644
+ kind: Kind.DOCUMENT,
1645
+ definitions: [header, ...rest]
1646
+ });
1647
+ }).filter(Boolean).join("\n") ?? "";
1648
+ }
1499
1649
  const buildSubgraphSchemaModule = (documentModels, resolvers, typeDefs) => {
1500
1650
  const newResolvers = {
1501
1651
  ...resolvers,
@@ -1534,20 +1684,13 @@ const getDocumentModelTypeDefs = (documentModels, typeDefs$1) => {
1534
1684
  const latestSpec = documentModel.global.specifications.at(-1);
1535
1685
  const globalSchema = latestSpec?.state.global.schema ?? "";
1536
1686
  const localSchema = latestSpec?.state.local.schema ?? "";
1537
- let tmpDmSchema = `
1538
- ${globalSchema.replaceAll("scalar DateTime", "").replaceAll(/input (.*?) {[\s\S]*?}/g, "")};
1539
-
1540
- ${localSchema.replaceAll("scalar DateTime", "").replaceAll(/input (.*?) {[\s\S]*?}/g, "").replaceAll("type AccountSnapshotLocalState", "").replaceAll("type BudgetStatementLocalState", "").replaceAll("type ScopeFrameworkLocalState", "")};
1541
-
1542
- \n`;
1543
- (tmpDmSchema.match(/(type|enum|union|interface)\s+(\w+)[\s{]/g)?.map((f) => f.replaceAll("type ", "").replaceAll("enum ", "").replaceAll("union ", "").replaceAll("interface ", "").replaceAll("{", "").trim()))?.forEach((f) => {
1544
- const typeRegex = new RegExp(`(?<![_A-Za-z0-9])(${f})(?![_A-Za-z0-9])|\\[(${f})\\]|\\[(${f})!\\]|\\[(${f})\\]!|\\[(${f})!\\]!`, "g");
1545
- tmpDmSchema = tmpDmSchema.replace(typeRegex, (match, p1, p2, p3, p4, p5) => {
1546
- if (match.startsWith("[")) return match.replace(p2 || p3 || p4 || p5, `${dmSchemaName}_${p2 || p3 || p4 || p5}`);
1547
- return `${dmSchemaName}_${p1}`;
1548
- });
1549
- });
1550
- dmSchema += tmpDmSchema;
1687
+ const definitions = [...stateTypeDefinitions(globalSchema), ...stateTypeDefinitions(localSchema, LOCAL_STATE_STUBS)];
1688
+ const stateDoc = {
1689
+ kind: Kind.DOCUMENT,
1690
+ definitions
1691
+ };
1692
+ const prefixed = prefixTypeNames(stateDoc, dmSchemaName, new Set(definedTypeNames(stateDoc, STATE_PREFIX_KINDS)));
1693
+ if (definitions.length > 0) dmSchema += `${print(prefixed)}\n`;
1551
1694
  dmSchema += `
1552
1695
  type ${dmSchemaName} implements IDocument {
1553
1696
  id: String!
@@ -1562,7 +1705,7 @@ const getDocumentModelTypeDefs = (documentModels, typeDefs$1) => {
1562
1705
  stateJSON: JSONObject
1563
1706
  }\n`;
1564
1707
  });
1565
- return dedupeTypeDefinitions(gql`
1708
+ const schema = gql`
1566
1709
  scalar JSONObject
1567
1710
  scalar AttachmentRef
1568
1711
  # Codegen scalars not in the document-engineering set.
@@ -1613,7 +1756,7 @@ const getDocumentModelTypeDefs = (documentModels, typeDefs$1) => {
1613
1756
  operations(first: Int, skip: Int): [Operation!]!
1614
1757
  stateJSON: JSONObject
1615
1758
  }
1616
- ${dmSchema.replaceAll(";", "")}
1759
+ ${dmSchema}
1617
1760
 
1618
1761
  type GqlDocument implements IDocument {
1619
1762
  id: String!
@@ -1638,34 +1781,32 @@ const getDocumentModelTypeDefs = (documentModels, typeDefs$1) => {
1638
1781
  }
1639
1782
 
1640
1783
  ${stripScalarDefinitions(typeDefs$1)}
1641
- `);
1784
+ `;
1785
+ return dedupeTypeDefinitions(schema);
1642
1786
  };
1643
1787
  /**
1644
- * Extract type names from a GraphQL schema.
1645
- * @param {string} schema - GraphQL schema string
1646
- * @returns {string[]} Array of type names
1788
+ * Extract the names of types defined in a GraphQL schema.
1789
+ * Unparseable schemas yield no names.
1647
1790
  */
1648
1791
  function extractTypeNames(schema) {
1649
- const found = schema.match(/(type|enum|union|interface|input)\s+(\w+)[\s{]/g);
1650
- if (!found) return [];
1651
- return found.map((f) => f.replaceAll("type ", "").replaceAll("enum ", "").replaceAll("union ", "").replaceAll("interface ", "").replaceAll("input ", "").replaceAll("{", "").trim());
1792
+ try {
1793
+ return definedTypeNames(parseSdl(schema), ALL_PREFIX_KINDS);
1794
+ } catch (error) {
1795
+ logger.debug(`Skipping unparseable schema: ${String(error)}`);
1796
+ return [];
1797
+ }
1652
1798
  }
1653
1799
  /**
1654
- * Extract input type definitions from a GraphQL schema.
1655
- * @param {string} schema - GraphQL schema string
1656
- * @param {Set<string>} excludeTypeNames - Type names to exclude from extraction
1657
- * @returns {string} All input type definitions as a string
1800
+ * Print the input type definitions of a GraphQL schema.
1801
+ * @param excludeTypeNames - Input type names to leave out
1658
1802
  */
1659
1803
  function extractInputTypeDefinitions(schema, excludeTypeNames = /* @__PURE__ */ new Set()) {
1660
- const inputTypeRegex = /input\s+(\w+)\s*\{[^}]*\}/g;
1661
- const matches = [];
1662
- let match;
1663
- while ((match = inputTypeRegex.exec(schema)) !== null) {
1664
- const typeName = match[1];
1665
- if (!excludeTypeNames.has(typeName)) matches.push(match[0]);
1666
- }
1667
- if (matches.length === 0) return "";
1668
- return matches.join("\n\n");
1804
+ const definitions = parseSdl(schema).definitions.filter((def) => def.kind === Kind.INPUT_OBJECT_TYPE_DEFINITION && !excludeTypeNames.has(def.name.value));
1805
+ if (definitions.length === 0) return "";
1806
+ return print({
1807
+ kind: Kind.DOCUMENT,
1808
+ definitions
1809
+ });
1669
1810
  }
1670
1811
  /**
1671
1812
  * Extract the root state type name from a scope's GraphQL schema.
@@ -1734,9 +1875,7 @@ function generateStateInputTypes(stateSchema, excludeTypeNames) {
1734
1875
  case Kind.OBJECT_TYPE_DEFINITION:
1735
1876
  objectNames.add(def.name.value);
1736
1877
  break;
1737
- case Kind.INPUT_OBJECT_TYPE_DEFINITION:
1738
- existingInputNames.add(def.name.value);
1739
- break;
1878
+ case Kind.INPUT_OBJECT_TYPE_DEFINITION: existingInputNames.add(def.name.value);
1740
1879
  }
1741
1880
  const inputTypes = [];
1742
1881
  for (const def of ast.definitions) {
@@ -1763,18 +1902,10 @@ function generateStateInputTypes(stateSchema, excludeTypeNames) {
1763
1902
  */
1764
1903
  function applyGraphQLTypePrefixes(schema, prefix, externalTypeNames = []) {
1765
1904
  if (!schema || !schema.trim()) return schema;
1766
- let processedSchema = schema;
1767
- const localTypeNames = extractTypeNames(schema);
1768
- const allTypeNames = [...new Set([...localTypeNames, ...externalTypeNames])];
1769
- if (allTypeNames.length === 0) return schema;
1770
- allTypeNames.forEach((typeName) => {
1771
- const typeRegex = new RegExp(`(?<![_A-Za-z0-9])(${typeName})(?![_A-Za-z0-9])|\\[(${typeName})\\]|\\[(${typeName})!\\]|\\[(${typeName})\\]!|\\[(${typeName})!\\]!`, "g");
1772
- processedSchema = processedSchema.replace(typeRegex, (match, p1, p2, p3, p4, p5) => {
1773
- if (match.startsWith("[")) return match.replace(p2 || p3 || p4 || p5, `${prefix}_${p2 || p3 || p4 || p5}`);
1774
- return `${prefix}_${p1}`;
1775
- });
1776
- });
1777
- return processedSchema;
1905
+ const doc = parseSdl(schema);
1906
+ const names = /* @__PURE__ */ new Set([...definedTypeNames(doc, ALL_PREFIX_KINDS), ...externalTypeNames]);
1907
+ if (names.size === 0) return schema;
1908
+ return print(prefixTypeNames(doc, prefix, names));
1778
1909
  }
1779
1910
  /**
1780
1911
  * Generate a GraphQL schema for a document model.
@@ -1793,11 +1924,11 @@ function generateDocumentModelSchema(documentModel, options = {}) {
1793
1924
  const localStateTypeNames = extractTypeNames(localStateSchema ?? "");
1794
1925
  const stateTypeNames = [...globalStateTypeNames, ...localStateTypeNames];
1795
1926
  const allOperationTypeNames = specification?.modules.flatMap((module) => module.operations.flatMap((op) => extractTypeNames(op.schema ?? ""))) ?? [];
1796
- const allTypeNames = [...new Set([...stateTypeNames, ...allOperationTypeNames])];
1797
- const prefixedStateInputTypes = applyGraphQLTypePrefixes(extractInputTypeDefinitions(globalStateSchema ?? "", new Set(allOperationTypeNames)), documentName, allTypeNames);
1927
+ const allTypeNames = [.../* @__PURE__ */ new Set([...stateTypeNames, ...allOperationTypeNames])];
1928
+ const operationInputTypeNames = new Set(allOperationTypeNames);
1929
+ const prefixedStateInputTypes = applyGraphQLTypePrefixes(extractInputTypeDefinitions(globalStateSchema ?? "", operationInputTypeNames), documentName, allTypeNames);
1798
1930
  const hasValidSchema = (schema) => !!(schema && /\b(input|type|enum|union|interface)\s+\w+/.test(schema));
1799
- const stateSchemaTypes = globalStateSchema ? applyGraphQLTypePrefixes(globalStateSchema.replaceAll("scalar DateTime", "").replaceAll(/input (.*?) {[\s\S]*?}/g, ""), documentName, allTypeNames) : "";
1800
- if (useNewApi) return generateNewApiSchema(documentName, specification, stateSchemaTypes, prefixedStateInputTypes, allTypeNames, hasValidSchema);
1931
+ if (useNewApi) return generateNewApiSchema(documentName, specification, prefixedStateInputTypes, allTypeNames, hasValidSchema);
1801
1932
  return generateLegacyApiSchema(documentName, specification, prefixedStateInputTypes, allTypeNames, hasValidSchema);
1802
1933
  }
1803
1934
  /**
@@ -1807,10 +1938,7 @@ function generateLegacyApiSchema(documentName, specification, prefixedStateInput
1807
1938
  const createDocumentMutation = `${documentName}_createDocument(name:String!, driveId:String): String`;
1808
1939
  const operationMutations = specification?.modules.flatMap((module) => module.operations.filter((op) => op.name && hasValidSchema(op.schema)).map((op) => `${documentName}_${camelCase(op.name)}(
1809
1940
  driveId: String, docId: PHID, input: ${documentName}_${pascalCase(op.name)}Input): Int`)).join("\n ") ?? "";
1810
- const moduleSchemas = specification?.modules.filter((module) => module.operations.some((op) => hasValidSchema(op.schema))).map((module) => `"""
1811
- Module: ${pascalCase(module.name)}
1812
- """
1813
- ${module.operations.filter((op) => hasValidSchema(op.schema)).map((op) => applyGraphQLTypePrefixes(op.schema ?? "", documentName, allTypeNames)).join("\n ")}`).join("\n") ?? "";
1941
+ const moduleSchemas = buildModuleSchemas(specification, documentName, allTypeNames, hasValidSchema);
1814
1942
  return gql`
1815
1943
  """
1816
1944
  Queries: ${documentName} Document
@@ -1847,7 +1975,7 @@ function generateLegacyApiSchema(documentName, specification, prefixedStateInput
1847
1975
  * in getDocumentModelTypeDefs() which is used during schema composition.
1848
1976
  * Including them here would cause duplicate type definitions.
1849
1977
  */
1850
- function generateNewApiSchema(documentName, specification, _stateSchemaTypes, prefixedStateInputTypes, allTypeNames, hasValidSchema) {
1978
+ function generateNewApiSchema(documentName, specification, prefixedStateInputTypes, allTypeNames, hasValidSchema) {
1851
1979
  const stateType = `${documentName}_FullState!`;
1852
1980
  const sharedBaseTypes = `
1853
1981
  """Hash configuration for document state"""
@@ -1890,7 +2018,7 @@ function generateNewApiSchema(documentName, specification, _stateSchemaTypes, pr
1890
2018
 
1891
2019
  input ${documentName}_SearchFilterInput {
1892
2020
  parentId: String
1893
- identifiers: [String!]
2021
+ identifiers: [String!] @deprecated(reason: "Ignored. Filter by parentId.")
1894
2022
  }
1895
2023
  `;
1896
2024
  const revisionType = RevisionType;
@@ -1924,7 +2052,6 @@ function generateNewApiSchema(documentName, specification, _stateSchemaTypes, pr
1924
2052
  """
1925
2053
  type ${documentName}_DocumentResultPage {
1926
2054
  items: [${documentName}MutationResult!]!
1927
- totalCount: Int!
1928
2055
  hasNextPage: Boolean!
1929
2056
  hasPreviousPage: Boolean!
1930
2057
  cursor: String
@@ -1933,7 +2060,7 @@ function generateNewApiSchema(documentName, specification, _stateSchemaTypes, pr
1933
2060
  const queries = `
1934
2061
  type ${documentName}Queries {
1935
2062
  """Get a specific ${documentName} document by identifier"""
1936
- document(identifier: String!, view: ${documentName}_ViewFilterInput): ${documentName}_DocumentWithChildren
2063
+ document(idOrSlug: String, identifier: String @deprecated(reason: "Use idOrSlug."), view: ${documentName}_ViewFilterInput): ${documentName}_DocumentWithChildren
1937
2064
 
1938
2065
  """Get all ${documentName} documents (paged)"""
1939
2066
  documents(paging: ${documentName}_PagingInput): ${documentName}_DocumentResultPage!
@@ -1942,10 +2069,10 @@ function generateNewApiSchema(documentName, specification, _stateSchemaTypes, pr
1942
2069
  findDocuments(search: ${documentName}_SearchFilterInput, view: ${documentName}_ViewFilterInput, paging: ${documentName}_PagingInput): ${documentName}_DocumentResultPage!
1943
2070
 
1944
2071
  """Get outgoing relationships of a ${documentName} document"""
1945
- documentOutgoingRelationships(sourceIdentifier: String!, relationshipType: String!, view: ${documentName}_ViewFilterInput, paging: ${documentName}_PagingInput): ${documentName}_DocumentResultPage!
2072
+ documentOutgoingRelationships(sourceIdOrSlug: String, sourceIdentifier: String @deprecated(reason: "Use sourceIdOrSlug."), relationshipType: String!, view: ${documentName}_ViewFilterInput, paging: ${documentName}_PagingInput): ${documentName}_DocumentResultPage!
1946
2073
 
1947
2074
  """Get incoming relationships to a ${documentName} document"""
1948
- documentIncomingRelationships(targetIdentifier: String!, relationshipType: String!, view: ${documentName}_ViewFilterInput, paging: ${documentName}_PagingInput): ${documentName}_DocumentResultPage!
2075
+ documentIncomingRelationships(targetIdOrSlug: String, targetIdentifier: String @deprecated(reason: "Use targetIdOrSlug."), relationshipType: String!, view: ${documentName}_ViewFilterInput, paging: ${documentName}_PagingInput): ${documentName}_DocumentResultPage!
1949
2076
  }
1950
2077
  `;
1951
2078
  let initialStateInputSchema = "";
@@ -1976,13 +2103,10 @@ function generateNewApiSchema(documentName, specification, _stateSchemaTypes, pr
1976
2103
  initialStateInputSchema = inputTypeDefs ? `${inputTypeDefs}\n\n${wrapper}` : wrapper;
1977
2104
  }
1978
2105
  }
1979
- const createDocumentMutation = initialStateInputSchema ? `createDocument(name: String!, parentIdentifier: String, slug: String, preferredEditor: String, initialState: ${documentName}_InitialStateInput): ${documentName}MutationResult!` : `createDocument(name: String!, parentIdentifier: String, preferredEditor: String): ${documentName}MutationResult!`;
1980
- const createEmptyDocumentMutation = `createEmptyDocument(parentIdentifier: String): ${documentName}MutationResult!`;
1981
- const operationMutations = specification?.modules.flatMap((module) => module.operations.filter((op) => op.name && hasValidSchema(op.schema)).flatMap((op) => [`${camelCase(op.name)}(docId: PHID!, input: ${documentName}_${pascalCase(op.name)}Input!): ${documentName}MutationResult!`, `${camelCase(op.name)}Async(docId: PHID!, input: ${documentName}_${pascalCase(op.name)}Input!): String!`])).join("\n ") ?? "";
1982
- const moduleSchemas = specification?.modules.filter((module) => module.operations.some((op) => hasValidSchema(op.schema))).map((module) => `"""
1983
- Module: ${pascalCase(module.name)}
1984
- """
1985
- ${module.operations.filter((op) => hasValidSchema(op.schema)).map((op) => applyGraphQLTypePrefixes(op.schema ?? "", documentName, allTypeNames)).join("\n ")}`).join("\n") ?? "";
2106
+ const createDocumentMutation = initialStateInputSchema ? `createDocument(name: String!, parentIdOrSlug: String, parentIdentifier: String @deprecated(reason: "Use parentIdOrSlug."), slug: String, preferredEditor: String, initialState: ${documentName}_InitialStateInput): ${documentName}MutationResult!` : `createDocument(name: String!, parentIdOrSlug: String, parentIdentifier: String @deprecated(reason: "Use parentIdOrSlug."), preferredEditor: String): ${documentName}MutationResult!`;
2107
+ const createEmptyDocumentMutation = `createEmptyDocument(parentIdOrSlug: String, parentIdentifier: String @deprecated(reason: "Use parentIdOrSlug.")): ${documentName}MutationResult!`;
2108
+ const operationMutations = specification?.modules.flatMap((module) => module.operations.filter((op) => op.name && hasValidSchema(op.schema)).flatMap((op) => [`${camelCase(op.name)}(documentIdOrSlug: String, docId: PHID @deprecated(reason: "Use documentIdOrSlug."), input: ${documentName}_${pascalCase(op.name)}Input!): ${documentName}MutationResult!`, `${camelCase(op.name)}Async(documentIdOrSlug: String, docId: PHID @deprecated(reason: "Use documentIdOrSlug."), input: ${documentName}_${pascalCase(op.name)}Input!): String!`])).join("\n ") ?? "";
2109
+ const moduleSchemas = buildModuleSchemas(specification, documentName, allTypeNames, hasValidSchema);
1986
2110
  return gql`
1987
2111
  scalar DateTime
1988
2112
  scalar JSONObject
@@ -2135,7 +2259,8 @@ function OperationsFilterInputSchema() {
2135
2259
  return z$1.object({
2136
2260
  actionTypes: z$1.array(z$1.string()).nullish(),
2137
2261
  branch: z$1.string().nullish(),
2138
- documentId: z$1.string(),
2262
+ documentId: z$1.string().nullish(),
2263
+ documentIdOrSlug: z$1.string().nullish(),
2139
2264
  scopes: z$1.array(z$1.string()).nullish(),
2140
2265
  sinceRevision: z$1.number().nullish(),
2141
2266
  timestampFrom: z$1.string().nullish(),
@@ -2200,11 +2325,19 @@ function SyncEnvelopeInputSchema() {
2200
2325
  type: SyncEnvelopeTypeSchema
2201
2326
  });
2202
2327
  }
2328
+ function SyncRefusalInputSchema() {
2329
+ return z$1.object({
2330
+ branch: z$1.string(),
2331
+ documentId: z$1.string(),
2332
+ kind: z$1.string().nullish()
2333
+ });
2334
+ }
2203
2335
  function TouchChannelInputSchema() {
2204
2336
  return z$1.object({
2205
2337
  collectionId: z$1.string(),
2206
2338
  filter: z$1.lazy(() => RemoteFilterInputSchema()),
2207
2339
  id: z$1.string(),
2340
+ manifest: z$1.custom((v) => v != null).nullish(),
2208
2341
  name: z$1.string(),
2209
2342
  sinceTimestampUtcMs: z$1.string()
2210
2343
  });
@@ -2230,6 +2363,16 @@ const PhDocumentFieldsFragmentDoc = gql`
2230
2363
  lastModifiedAtUtcIso
2231
2364
  }
2232
2365
  `;
2366
+ const DocumentRelationshipFieldsFragmentDoc = gql`
2367
+ fragment DocumentRelationshipFields on DocumentRelationship {
2368
+ sourceId
2369
+ targetId
2370
+ relationshipType
2371
+ metadata
2372
+ createdAt
2373
+ updatedAt
2374
+ }
2375
+ `;
2233
2376
  const GetDocumentModelsDocument = gql`
2234
2377
  query GetDocumentModels($namespace: String, $paging: PagingInput) {
2235
2378
  documentModels(namespace: $namespace, paging: $paging) {
@@ -2240,7 +2383,6 @@ const GetDocumentModelsDocument = gql`
2240
2383
  version
2241
2384
  specification
2242
2385
  }
2243
- totalCount
2244
2386
  hasNextPage
2245
2387
  hasPreviousPage
2246
2388
  cursor
@@ -2299,7 +2441,6 @@ const GetDocumentWithOperationsDocument = gql`
2299
2441
  }
2300
2442
  }
2301
2443
  }
2302
- totalCount
2303
2444
  hasNextPage
2304
2445
  hasPreviousPage
2305
2446
  cursor
@@ -2326,7 +2467,6 @@ const GetDocumentOutgoingRelationshipsDocument = gql`
2326
2467
  items {
2327
2468
  ...PHDocumentFields
2328
2469
  }
2329
- totalCount
2330
2470
  hasNextPage
2331
2471
  hasPreviousPage
2332
2472
  cursor
@@ -2350,7 +2490,6 @@ const GetDocumentIncomingRelationshipsDocument = gql`
2350
2490
  items {
2351
2491
  ...PHDocumentFields
2352
2492
  }
2353
- totalCount
2354
2493
  hasNextPage
2355
2494
  hasPreviousPage
2356
2495
  cursor
@@ -2358,6 +2497,52 @@ const GetDocumentIncomingRelationshipsDocument = gql`
2358
2497
  }
2359
2498
  ${PhDocumentFieldsFragmentDoc}
2360
2499
  `;
2500
+ const GetDocumentOutgoingRelationshipEdgesDocument = gql`
2501
+ query GetDocumentOutgoingRelationshipEdges(
2502
+ $sourceIdentifier: String!
2503
+ $relationshipType: String
2504
+ $view: ViewFilterInput
2505
+ $paging: PagingInput
2506
+ ) {
2507
+ documentOutgoingRelationshipEdges(
2508
+ sourceIdentifier: $sourceIdentifier
2509
+ relationshipType: $relationshipType
2510
+ view: $view
2511
+ paging: $paging
2512
+ ) {
2513
+ items {
2514
+ ...DocumentRelationshipFields
2515
+ }
2516
+ hasNextPage
2517
+ hasPreviousPage
2518
+ cursor
2519
+ }
2520
+ }
2521
+ ${DocumentRelationshipFieldsFragmentDoc}
2522
+ `;
2523
+ const GetDocumentIncomingRelationshipEdgesDocument = gql`
2524
+ query GetDocumentIncomingRelationshipEdges(
2525
+ $targetIdentifier: String!
2526
+ $relationshipType: String
2527
+ $view: ViewFilterInput
2528
+ $paging: PagingInput
2529
+ ) {
2530
+ documentIncomingRelationshipEdges(
2531
+ targetIdentifier: $targetIdentifier
2532
+ relationshipType: $relationshipType
2533
+ view: $view
2534
+ paging: $paging
2535
+ ) {
2536
+ items {
2537
+ ...DocumentRelationshipFields
2538
+ }
2539
+ hasNextPage
2540
+ hasPreviousPage
2541
+ cursor
2542
+ }
2543
+ }
2544
+ ${DocumentRelationshipFieldsFragmentDoc}
2545
+ `;
2361
2546
  const FindDocumentsDocument = gql`
2362
2547
  query FindDocuments(
2363
2548
  $search: SearchFilterInput
@@ -2368,7 +2553,6 @@ const FindDocumentsDocument = gql`
2368
2553
  items {
2369
2554
  ...PHDocumentFields
2370
2555
  }
2371
- totalCount
2372
2556
  hasNextPage
2373
2557
  hasPreviousPage
2374
2558
  cursor
@@ -2412,7 +2596,6 @@ const GetDocumentOperationsDocument = gql`
2412
2596
  }
2413
2597
  }
2414
2598
  }
2415
- totalCount
2416
2599
  hasNextPage
2417
2600
  hasPreviousPage
2418
2601
  cursor
@@ -2548,12 +2731,34 @@ const AddRelationshipDocument = gql`
2548
2731
  $sourceIdentifier: String!
2549
2732
  $targetIdentifier: String!
2550
2733
  $relationshipType: String!
2734
+ $metadata: JSONObject
2551
2735
  $branch: String
2552
2736
  ) {
2553
2737
  addRelationship(
2554
2738
  sourceIdentifier: $sourceIdentifier
2555
2739
  targetIdentifier: $targetIdentifier
2556
2740
  relationshipType: $relationshipType
2741
+ metadata: $metadata
2742
+ branch: $branch
2743
+ ) {
2744
+ ...PHDocumentFields
2745
+ }
2746
+ }
2747
+ ${PhDocumentFieldsFragmentDoc}
2748
+ `;
2749
+ const UpdateRelationshipDocument = gql`
2750
+ mutation UpdateRelationship(
2751
+ $sourceIdentifier: String!
2752
+ $targetIdentifier: String!
2753
+ $relationshipType: String!
2754
+ $metadata: JSONObject
2755
+ $branch: String
2756
+ ) {
2757
+ updateRelationship(
2758
+ sourceIdentifier: $sourceIdentifier
2759
+ targetIdentifier: $targetIdentifier
2760
+ relationshipType: $relationshipType
2761
+ metadata: $metadata
2557
2762
  branch: $branch
2558
2763
  ) {
2559
2764
  ...PHDocumentFields
@@ -2650,11 +2855,15 @@ const PollSyncEnvelopesDocument = gql`
2650
2855
  $channelId: String!
2651
2856
  $outboxAck: Int!
2652
2857
  $outboxLatest: Int!
2858
+ $manifestRevision: String
2859
+ $refusals: [SyncRefusalInput!]
2653
2860
  ) {
2654
2861
  pollSyncEnvelopes(
2655
2862
  channelId: $channelId
2656
2863
  outboxAck: $outboxAck
2657
2864
  outboxLatest: $outboxLatest
2865
+ manifestRevision: $manifestRevision
2866
+ refusals: $refusals
2658
2867
  ) {
2659
2868
  envelopes {
2660
2869
  type
@@ -2714,6 +2923,8 @@ const PollSyncEnvelopesDocument = gql`
2714
2923
  errorType
2715
2924
  }
2716
2925
  hasMore
2926
+ manifestRevision
2927
+ peerManifestRevision
2717
2928
  }
2718
2929
  }
2719
2930
  `;
@@ -2722,12 +2933,19 @@ const TouchChannelDocument = gql`
2722
2933
  touchChannel(input: $input) {
2723
2934
  success
2724
2935
  ackOrdinal
2936
+ manifest
2725
2937
  }
2726
2938
  }
2727
2939
  `;
2728
2940
  const PushSyncEnvelopesDocument = gql`
2729
- mutation PushSyncEnvelopes($envelopes: [SyncEnvelopeInput!]!) {
2730
- pushSyncEnvelopes(envelopes: $envelopes)
2941
+ mutation PushSyncEnvelopes(
2942
+ $envelopes: [SyncEnvelopeInput!]!
2943
+ $peerManifestRevision: String
2944
+ ) {
2945
+ pushSyncEnvelopes(
2946
+ envelopes: $envelopes
2947
+ peerManifestRevision: $peerManifestRevision
2948
+ )
2731
2949
  }
2732
2950
  `;
2733
2951
  function getSdk(requester) {
@@ -2747,6 +2965,12 @@ function getSdk(requester) {
2747
2965
  GetDocumentIncomingRelationships(variables, options) {
2748
2966
  return requester(GetDocumentIncomingRelationshipsDocument, variables, options);
2749
2967
  },
2968
+ GetDocumentOutgoingRelationshipEdges(variables, options) {
2969
+ return requester(GetDocumentOutgoingRelationshipEdgesDocument, variables, options);
2970
+ },
2971
+ GetDocumentIncomingRelationshipEdges(variables, options) {
2972
+ return requester(GetDocumentIncomingRelationshipEdgesDocument, variables, options);
2973
+ },
2750
2974
  FindDocuments(variables, options) {
2751
2975
  return requester(FindDocumentsDocument, variables, options);
2752
2976
  },
@@ -2780,6 +3004,9 @@ function getSdk(requester) {
2780
3004
  AddRelationship(variables, options) {
2781
3005
  return requester(AddRelationshipDocument, variables, options);
2782
3006
  },
3007
+ UpdateRelationship(variables, options) {
3008
+ return requester(UpdateRelationshipDocument, variables, options);
3009
+ },
2783
3010
  RemoveRelationship(variables, options) {
2784
3011
  return requester(RemoveRelationshipDocument, variables, options);
2785
3012
  },
@@ -2820,8 +3047,7 @@ function toDocumentModelResultPage(result) {
2820
3047
  cursor: result.nextCursor ?? null,
2821
3048
  hasNextPage: !!result.nextCursor,
2822
3049
  hasPreviousPage: !!result.options.cursor,
2823
- items: models.map(toGqlDocumentModelState),
2824
- totalCount: result.results.length
3050
+ items: models.map(toGqlDocumentModelState)
2825
3051
  };
2826
3052
  }
2827
3053
  /**
@@ -2853,8 +3079,32 @@ function toPhDocumentResultPage(result) {
2853
3079
  cursor: result.nextCursor ?? null,
2854
3080
  hasNextPage: !!result.nextCursor,
2855
3081
  hasPreviousPage: !!result.options.cursor,
2856
- items: result.results.map(toGqlPhDocument),
2857
- totalCount: result.totalCount ?? result.results.length
3082
+ items: result.results.map(toGqlPhDocument)
3083
+ };
3084
+ }
3085
+ /**
3086
+ * Converts a relationship edge from the indexer to GraphQL format.
3087
+ */
3088
+ function toGqlDocumentRelationship(relationship) {
3089
+ return {
3090
+ sourceId: relationship.sourceId,
3091
+ targetId: relationship.targetId,
3092
+ relationshipType: relationship.relationshipType,
3093
+ metadata: relationship.metadata ?? null,
3094
+ createdAt: relationship.createdAt,
3095
+ updatedAt: relationship.updatedAt
3096
+ };
3097
+ }
3098
+ /**
3099
+ * Converts a PagedResults of relationship edges to GraphQL
3100
+ * DocumentRelationshipResultPage format
3101
+ */
3102
+ function toDocumentRelationshipResultPage(result) {
3103
+ return {
3104
+ cursor: result.nextCursor ?? null,
3105
+ hasNextPage: !!result.nextCursor,
3106
+ hasPreviousPage: !!result.options.cursor,
3107
+ items: result.results.map(toGqlDocumentRelationship)
2858
3108
  };
2859
3109
  }
2860
3110
  /**
@@ -2904,8 +3154,8 @@ function fromInputMaybe(value) {
2904
3154
  function toReactorPropagationMode(gqlMode) {
2905
3155
  if (gqlMode == null) return;
2906
3156
  switch (gqlMode) {
2907
- case PropagationMode.Cascade: return PropagationMode$1.Cascade;
2908
- case PropagationMode.Orphan: return PropagationMode$1.None;
3157
+ case "CASCADE": return PropagationMode$1.Cascade;
3158
+ case "ORPHAN": return PropagationMode$1.None;
2909
3159
  }
2910
3160
  }
2911
3161
  /**
@@ -2915,10 +3165,10 @@ function toReactorPropagationMode(gqlMode) {
2915
3165
  */
2916
3166
  function toGqlActionEvaluation(evaluation) {
2917
3167
  return evaluation.decision === "allow" ? {
2918
- decision: AuthDecision.Allow,
3168
+ decision: "ALLOW",
2919
3169
  reason: null
2920
3170
  } : {
2921
- decision: AuthDecision.Deny,
3171
+ decision: "DENY",
2922
3172
  reason: evaluation.reason
2923
3173
  };
2924
3174
  }
@@ -3123,8 +3373,7 @@ function toOperationResultPage(result) {
3123
3373
  cursor: result.nextCursor ?? null,
3124
3374
  hasNextPage: !!result.nextCursor,
3125
3375
  hasPreviousPage: !!result.options.cursor && result.options.cursor !== "0",
3126
- items: result.results.map(serializeOperationForGraphQL),
3127
- totalCount: result.results.length
3376
+ items: result.results.map(serializeOperationForGraphQL)
3128
3377
  };
3129
3378
  }
3130
3379
  function toGqlDocumentChangeEvent(event) {
@@ -3196,6 +3445,21 @@ function pickDriveClient(reactorClient, driveContainerType, reactorDriveClient)
3196
3445
  }
3197
3446
  return reactorClient.drives;
3198
3447
  }
3448
+ /**
3449
+ * A mutation's document re-read as the caller, since the client reads it back
3450
+ * as its signer. A caller who may write but not read gets it stripped.
3451
+ */
3452
+ async function readAsCaller(reactorClient, document, subject, branch) {
3453
+ if (!subject) return document;
3454
+ try {
3455
+ return await reactorClient.get(document.header.id, {
3456
+ subject,
3457
+ branch
3458
+ });
3459
+ } catch (error) {
3460
+ throw new GraphQLError(`Failed to read the document back: ${error instanceof Error ? error.message : "Unknown error"}`);
3461
+ }
3462
+ }
3199
3463
  async function documentModels(reactorClient, args) {
3200
3464
  const namespace = fromInputMaybe(args.namespace);
3201
3465
  let paging;
@@ -3228,13 +3492,13 @@ async function document(reactorClient, args, subject) {
3228
3492
  };
3229
3493
  let result;
3230
3494
  try {
3231
- result = await reactorClient.get(args.identifier, view);
3495
+ result = await reactorClient.get(args.idOrSlug, view);
3232
3496
  } catch (error) {
3233
3497
  throw new GraphQLError(`Failed to fetch document: ${error instanceof Error ? error.message : "Unknown error"}`);
3234
3498
  }
3235
3499
  let children;
3236
3500
  try {
3237
- children = await reactorClient.getOutgoingRelationships(args.identifier, "child", view);
3501
+ children = await reactorClient.getOutgoingRelationships(args.idOrSlug, "child", view);
3238
3502
  } catch (error) {
3239
3503
  throw new GraphQLError(`Failed to fetch children: ${error instanceof Error ? error.message : "Unknown error"}`);
3240
3504
  }
@@ -3265,7 +3529,7 @@ async function documentOutgoingRelationships(reactorClient, args, subject) {
3265
3529
  }
3266
3530
  let result;
3267
3531
  try {
3268
- result = await reactorClient.getOutgoingRelationships(args.sourceIdentifier, args.relationshipType, view, paging);
3532
+ result = await reactorClient.getOutgoingRelationships(args.sourceIdOrSlug, args.relationshipType, view, paging);
3269
3533
  } catch (error) {
3270
3534
  throw new GraphQLError(`Failed to fetch outgoing relationships: ${error instanceof Error ? error.message : "Unknown error"}`);
3271
3535
  }
@@ -3293,7 +3557,7 @@ async function documentIncomingRelationships(reactorClient, args, subject) {
3293
3557
  }
3294
3558
  let result;
3295
3559
  try {
3296
- result = await reactorClient.getIncomingRelationships(args.targetIdentifier, args.relationshipType, view, paging);
3560
+ result = await reactorClient.getIncomingRelationships(args.targetIdOrSlug, args.relationshipType, view, paging);
3297
3561
  } catch (error) {
3298
3562
  throw new GraphQLError(`Failed to fetch incoming relationships: ${error instanceof Error ? error.message : "Unknown error"}`);
3299
3563
  }
@@ -3303,6 +3567,54 @@ async function documentIncomingRelationships(reactorClient, args, subject) {
3303
3567
  throw new GraphQLError(`Failed to convert incoming relationships to GraphQL: ${error instanceof Error ? error.message : "Unknown error"}`);
3304
3568
  }
3305
3569
  }
3570
+ async function documentOutgoingRelationshipEdges(reactorClient, args, subject) {
3571
+ const view = toRelationshipViewFilter(args.view, subject);
3572
+ const paging = toRelationshipPagingOptions(args.paging);
3573
+ let result;
3574
+ try {
3575
+ result = await reactorClient.getOutgoingRelationshipEdges(args.sourceIdOrSlug, fromInputMaybe(args.relationshipType), view, paging);
3576
+ } catch (error) {
3577
+ throw new GraphQLError(`Failed to fetch outgoing relationship edges: ${error instanceof Error ? error.message : "Unknown error"}`);
3578
+ }
3579
+ try {
3580
+ return toDocumentRelationshipResultPage(result);
3581
+ } catch (error) {
3582
+ throw new GraphQLError(`Failed to convert outgoing relationship edges to GraphQL: ${error instanceof Error ? error.message : "Unknown error"}`);
3583
+ }
3584
+ }
3585
+ async function documentIncomingRelationshipEdges(reactorClient, args, subject) {
3586
+ const view = toRelationshipViewFilter(args.view, subject);
3587
+ const paging = toRelationshipPagingOptions(args.paging);
3588
+ let result;
3589
+ try {
3590
+ result = await reactorClient.getIncomingRelationshipEdges(args.targetIdOrSlug, fromInputMaybe(args.relationshipType), view, paging);
3591
+ } catch (error) {
3592
+ throw new GraphQLError(`Failed to fetch incoming relationship edges: ${error instanceof Error ? error.message : "Unknown error"}`);
3593
+ }
3594
+ try {
3595
+ return toDocumentRelationshipResultPage(result);
3596
+ } catch (error) {
3597
+ throw new GraphQLError(`Failed to convert incoming relationship edges to GraphQL: ${error instanceof Error ? error.message : "Unknown error"}`);
3598
+ }
3599
+ }
3600
+ function toRelationshipViewFilter(input, subject) {
3601
+ if (!input) return subject ? { subject } : void 0;
3602
+ return {
3603
+ subject,
3604
+ branch: fromInputMaybe(input.branch),
3605
+ scopes: toMutableArray(fromInputMaybe(input.scopes))
3606
+ };
3607
+ }
3608
+ function toRelationshipPagingOptions(input) {
3609
+ if (!input) return;
3610
+ const cursor = fromInputMaybe(input.cursor);
3611
+ const limit = fromInputMaybe(input.limit);
3612
+ if (!cursor && !limit) return;
3613
+ return {
3614
+ cursor: cursor || "",
3615
+ limit: limit || 10
3616
+ };
3617
+ }
3306
3618
  async function findDocuments(reactorClient, args, subject) {
3307
3619
  let view = subject ? { subject } : void 0;
3308
3620
  if (args.view) view = {
@@ -3335,13 +3647,42 @@ async function findDocuments(reactorClient, args, subject) {
3335
3647
  throw new GraphQLError(`Failed to convert documents to GraphQL: ${error instanceof Error ? error.message : "Unknown error"}`);
3336
3648
  }
3337
3649
  }
3338
- async function jobStatus(reactorClient, args) {
3650
+ /**
3651
+ * The answer for a job the caller may not see, shaped as the reactor answers
3652
+ * an unknown one so that the two cannot be told apart.
3653
+ */
3654
+ function unknownJob(jobId) {
3655
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3656
+ return {
3657
+ id: jobId,
3658
+ documentId: "",
3659
+ status: JobStatus.FAILED,
3660
+ createdAtUtcIso: now,
3661
+ completedAtUtcIso: now,
3662
+ error: {
3663
+ name: "Error",
3664
+ message: "Job not found",
3665
+ stack: ""
3666
+ },
3667
+ consistencyToken: {
3668
+ version: 1,
3669
+ createdAtUtcIso: now,
3670
+ coordinates: []
3671
+ },
3672
+ meta: {
3673
+ batchId: jobId,
3674
+ batchJobIds: [jobId]
3675
+ }
3676
+ };
3677
+ }
3678
+ async function jobStatus(reactorClient, args, serves) {
3339
3679
  let result;
3340
3680
  try {
3341
3681
  result = await reactorClient.getJobStatus(args.jobId);
3342
3682
  } catch (error) {
3343
3683
  throw new GraphQLError(`Failed to fetch job status: ${error instanceof Error ? error.message : "Unknown error"}`);
3344
3684
  }
3685
+ if (!await serves(result.documentId)) result = unknownJob(args.jobId);
3345
3686
  try {
3346
3687
  return toGqlJobInfo(result);
3347
3688
  } catch (error) {
@@ -3379,7 +3720,7 @@ async function documentOperations(reactorClient, args, subject) {
3379
3720
  }
3380
3721
  let result;
3381
3722
  try {
3382
- result = await reactorClient.getOperations(args.filter.documentId, view, operationFilter, paging);
3723
+ result = await reactorClient.getOperations(args.filter.documentIdOrSlug, view, operationFilter, paging);
3383
3724
  } catch (error) {
3384
3725
  throw new GraphQLError(`Failed to fetch document operations: ${error instanceof Error ? error.message : "Unknown error"}`);
3385
3726
  }
@@ -3411,7 +3752,7 @@ async function evaluateActions(reactorClient, args, subject) {
3411
3752
  }));
3412
3753
  let result;
3413
3754
  try {
3414
- result = await reactorClient.evaluateActions(args.documentIdentifier, branch, candidates, subject);
3755
+ result = await reactorClient.evaluateActions(args.documentIdOrSlug, branch, candidates, subject);
3415
3756
  } catch (error) {
3416
3757
  if (AuthEnforcementDisabledError.isError(error)) throw new AuthEvaluationUnsupportedError();
3417
3758
  throw new GraphQLError(`Failed to evaluate actions: ${error instanceof Error ? error.message : "Unknown error"}`);
@@ -3424,51 +3765,54 @@ async function evaluateActions(reactorClient, args, subject) {
3424
3765
  anyDenied: result.anyDenied
3425
3766
  };
3426
3767
  }
3427
- async function createDocument(reactorClient, args, reactorDriveClient) {
3768
+ async function createDocument(reactorClient, args, reactorDriveClient, subject) {
3428
3769
  if (!args.document || typeof args.document !== "object") throw new GraphQLError("Invalid document: must be an object");
3429
3770
  const document = args.document;
3430
3771
  if (!document.header || typeof document.header !== "object") throw new GraphQLError("Invalid document: missing or invalid header");
3431
- const parentIdentifier = fromInputMaybe(args.parentIdentifier);
3772
+ const parentIdOrSlug = fromInputMaybe(args.parentIdOrSlug);
3432
3773
  let result;
3433
3774
  try {
3434
- if (parentIdentifier) {
3435
- const parent = await reactorClient.get(parentIdentifier);
3436
- if (isDriveContainerType(parent.header.documentType)) result = await pickDriveClient(reactorClient, parent.header.documentType, reactorDriveClient).addFile(parentIdentifier, document);
3437
- else result = await reactorClient.create(document, parentIdentifier);
3775
+ if (parentIdOrSlug) {
3776
+ const parent = await reactorClient.get(parentIdOrSlug, subject && { subject });
3777
+ if (isDriveContainerType(parent.header.documentType)) result = await pickDriveClient(reactorClient, parent.header.documentType, reactorDriveClient).addFile(parent.header.id, document);
3778
+ else result = await reactorClient.create(document, parentIdOrSlug);
3438
3779
  } else result = await reactorClient.create(document);
3439
3780
  } catch (error) {
3440
3781
  throw new GraphQLError(`Failed to create document: ${error instanceof Error ? error.message : "Unknown error"}`);
3441
3782
  }
3783
+ result = await readAsCaller(reactorClient, result, subject);
3442
3784
  try {
3443
3785
  return toGqlPhDocument(result);
3444
3786
  } catch (error) {
3445
3787
  throw new GraphQLError(`Failed to convert created document to GraphQL: ${error instanceof Error ? error.message : "Unknown error"}`);
3446
3788
  }
3447
3789
  }
3448
- async function createEmptyDocument(reactorClient, args, reactorDriveClient) {
3449
- const parentIdentifier = fromInputMaybe(args.parentIdentifier);
3790
+ async function createEmptyDocument(reactorClient, args, reactorDriveClient, subject) {
3791
+ const parentIdOrSlug = fromInputMaybe(args.parentIdOrSlug);
3450
3792
  const name = fromInputMaybe(args.name);
3451
3793
  let result;
3452
3794
  try {
3453
- if (parentIdentifier) {
3454
- const parent = await reactorClient.get(parentIdentifier);
3795
+ if (parentIdOrSlug) {
3796
+ const parent = await reactorClient.get(parentIdOrSlug, subject && { subject });
3455
3797
  if (isDriveContainerType(parent.header.documentType)) {
3456
- const document = (await reactorClient.getDocumentModelModule(args.documentType)).utils.createDocument();
3798
+ const module = await reactorClient.getDocumentModelModule(args.documentType);
3799
+ const document = withSignaturePolicy(module.utils.createDocument(), await reactorClient.getCreateSignaturePolicy());
3457
3800
  if (name) document.header.name = name;
3458
- result = await pickDriveClient(reactorClient, parent.header.documentType, reactorDriveClient).addFile(parentIdentifier, document);
3459
- } else result = await reactorClient.createEmpty(args.documentType, { parentIdentifier });
3801
+ result = await pickDriveClient(reactorClient, parent.header.documentType, reactorDriveClient).addFile(parent.header.id, document);
3802
+ } else result = await reactorClient.createEmpty(args.documentType, { parentIdentifier: parentIdOrSlug });
3460
3803
  } else result = await reactorClient.createEmpty(args.documentType, {});
3461
3804
  } catch (error) {
3462
3805
  throw new GraphQLError(`Failed to create empty document: ${error instanceof Error ? error.message : "Unknown error"}`);
3463
3806
  }
3807
+ result = await readAsCaller(reactorClient, result, subject);
3464
3808
  try {
3465
3809
  return toGqlPhDocument(result);
3466
3810
  } catch (error) {
3467
3811
  throw new GraphQLError(`Failed to convert created document to GraphQL: ${error instanceof Error ? error.message : "Unknown error"}`);
3468
3812
  }
3469
3813
  }
3470
- async function createDocumentWithInitialState(reactorClient, args, reactorDriveClient) {
3471
- const parentIdentifier = fromInputMaybe(args.parentIdentifier);
3814
+ async function createDocumentWithInitialState(reactorClient, args, reactorDriveClient, subject) {
3815
+ const parentIdOrSlug = fromInputMaybe(args.parentIdOrSlug);
3472
3816
  const name = fromInputMaybe(args.name);
3473
3817
  const slug = fromInputMaybe(args.slug);
3474
3818
  const preferredEditor = fromInputMaybe(args.preferredEditor);
@@ -3478,7 +3822,7 @@ async function createDocumentWithInitialState(reactorClient, args, reactorDriveC
3478
3822
  } catch (error) {
3479
3823
  throw new GraphQLError(`Document model not found for type ${args.documentType}: ${error instanceof Error ? error.message : "Unknown error"}`);
3480
3824
  }
3481
- const document = module.utils.createDocument();
3825
+ const document = withSignaturePolicy(module.utils.createDocument(), await reactorClient.getCreateSignaturePolicy());
3482
3826
  const allowedScopes = new Set(Object.keys(module.documentModel.global.specifications.at(-1)?.state ?? {}));
3483
3827
  const state = document.state;
3484
3828
  for (const [scope, scopeState] of Object.entries(args.initialState)) if (allowedScopes.has(scope) && scope in state) state[scope] = {
@@ -3492,22 +3836,22 @@ async function createDocumentWithInitialState(reactorClient, args, reactorDriveC
3492
3836
  preferredEditor
3493
3837
  };
3494
3838
  let result;
3495
- if (parentIdentifier) {
3839
+ if (parentIdOrSlug) {
3496
3840
  let parent;
3497
3841
  try {
3498
- parent = await reactorClient.get(parentIdentifier);
3842
+ parent = await reactorClient.get(parentIdOrSlug, subject && { subject });
3499
3843
  } catch (error) {
3500
3844
  throw new GraphQLError(`Parent document not found: ${error instanceof Error ? error.message : "Unknown error"}`);
3501
3845
  }
3502
3846
  if (isDriveContainerType(parent.header.documentType)) {
3503
3847
  const driveClient = pickDriveClient(reactorClient, parent.header.documentType, reactorDriveClient);
3504
3848
  try {
3505
- result = await driveClient.addFile(parentIdentifier, document);
3849
+ result = await driveClient.addFile(parent.header.id, document);
3506
3850
  } catch (error) {
3507
3851
  throw new GraphQLError(`Failed to create document in drive: ${error instanceof Error ? error.message : "Unknown error"}`);
3508
3852
  }
3509
3853
  } else try {
3510
- result = await reactorClient.create(document, parentIdentifier);
3854
+ result = await reactorClient.create(document, parentIdOrSlug);
3511
3855
  } catch (error) {
3512
3856
  throw new GraphQLError(`Failed to create document with parent: ${error instanceof Error ? error.message : "Unknown error"}`);
3513
3857
  }
@@ -3516,6 +3860,7 @@ async function createDocumentWithInitialState(reactorClient, args, reactorDriveC
3516
3860
  } catch (error) {
3517
3861
  throw new GraphQLError(`Failed to create document: ${error instanceof Error ? error.message : "Unknown error"}`);
3518
3862
  }
3863
+ result = await readAsCaller(reactorClient, result, subject);
3519
3864
  try {
3520
3865
  return toGqlPhDocument(result);
3521
3866
  } catch (error) {
@@ -3532,12 +3877,12 @@ const DEFAULT_BRANCH = "main";
3532
3877
  * the schema cannot express: a signature travels as one string, and verification
3533
3878
  * reads it as the tuple it was.
3534
3879
  */
3535
- async function execute(reactorClient, args) {
3880
+ async function execute(reactorClient, args, subject) {
3536
3881
  const actions = toSubmittableActions(args.actions);
3537
3882
  const branch = fromInputMaybe(args.branch) ?? DEFAULT_BRANCH;
3538
3883
  let result;
3539
3884
  try {
3540
- result = await reactorClient.execute(args.documentIdentifier, branch, actions);
3885
+ result = await reactorClient.execute(args.documentIdOrSlug, branch, actions, void 0, subject);
3541
3886
  } catch (error) {
3542
3887
  throw new GraphQLError(`Failed to execute actions: ${error instanceof Error ? error.message : "Unknown error"}`);
3543
3888
  }
@@ -3559,13 +3904,13 @@ async function executeAsync(reactorClient, args) {
3559
3904
  const branch = fromInputMaybe(args.branch) ?? DEFAULT_BRANCH;
3560
3905
  let job;
3561
3906
  try {
3562
- job = await reactorClient.executeAsync(args.documentIdentifier, branch, actions);
3907
+ job = await reactorClient.executeAsync(args.documentIdOrSlug, branch, actions);
3563
3908
  } catch (error) {
3564
3909
  throw new GraphQLError(`Failed to submit actions: ${error instanceof Error ? error.message : "Unknown error"}`);
3565
3910
  }
3566
3911
  return toGqlJobInfo(job);
3567
3912
  }
3568
- async function mutateDocument(reactorClient, args) {
3913
+ async function mutateDocument(reactorClient, args, subject) {
3569
3914
  let validatedActions;
3570
3915
  try {
3571
3916
  validatedActions = validateActions(args.actions);
@@ -3576,7 +3921,7 @@ async function mutateDocument(reactorClient, args) {
3576
3921
  const branch = args.view?.branch ?? "main";
3577
3922
  let result;
3578
3923
  try {
3579
- result = await reactorClient.execute(args.documentIdentifier, branch, validatedActions);
3924
+ result = await reactorClient.execute(args.documentIdentifier, branch, validatedActions, void 0, subject);
3580
3925
  } catch (error) {
3581
3926
  throw new GraphQLError(`Failed to mutate document: ${error instanceof Error ? error.message : "Unknown error"}`);
3582
3927
  }
@@ -3603,71 +3948,96 @@ async function mutateDocumentAsync(reactorClient, args) {
3603
3948
  }
3604
3949
  return result.id;
3605
3950
  }
3606
- async function renameDocument(reactorClient, args, signal) {
3951
+ async function renameDocument(reactorClient, args, signal, subject) {
3607
3952
  const branch = fromInputMaybe(args.branch);
3608
3953
  let result;
3609
3954
  try {
3610
- result = await reactorClient.rename(args.documentIdentifier, args.name, branch, signal);
3955
+ result = await reactorClient.rename(args.documentIdOrSlug, args.name, branch, signal);
3611
3956
  } catch (error) {
3612
3957
  throw new GraphQLError(`Failed to rename document: ${error instanceof Error ? error.message : "Unknown error"}`);
3613
3958
  }
3959
+ result = await readAsCaller(reactorClient, result, subject, branch);
3614
3960
  try {
3615
3961
  return toGqlPhDocument(result);
3616
3962
  } catch (error) {
3617
3963
  throw new GraphQLError(`Failed to convert renamed document to GraphQL: ${error instanceof Error ? error.message : "Unknown error"}`);
3618
3964
  }
3619
3965
  }
3620
- async function setPreferredEditor(reactorClient, args, signal) {
3966
+ async function setPreferredEditor(reactorClient, args, signal, subject) {
3621
3967
  const branch = fromInputMaybe(args.branch);
3622
3968
  const preferredEditor = fromInputMaybe(args.preferredEditor) ?? null;
3623
3969
  let result;
3624
3970
  try {
3625
- result = await reactorClient.setPreferredEditor(args.documentIdentifier, preferredEditor, branch, signal);
3971
+ result = await reactorClient.setPreferredEditor(args.documentIdOrSlug, preferredEditor, branch, signal);
3626
3972
  } catch (error) {
3627
3973
  throw new GraphQLError(`Failed to set preferred editor: ${error instanceof Error ? error.message : "Unknown error"}`);
3628
3974
  }
3975
+ result = await readAsCaller(reactorClient, result, subject, branch);
3629
3976
  try {
3630
3977
  return toGqlPhDocument(result);
3631
3978
  } catch (error) {
3632
3979
  throw new GraphQLError(`Failed to convert updated document to GraphQL: ${error instanceof Error ? error.message : "Unknown error"}`);
3633
3980
  }
3634
3981
  }
3635
- async function addRelationship(reactorClient, args) {
3982
+ async function addRelationship(reactorClient, args, subject) {
3636
3983
  const branch = fromInputMaybe(args.branch);
3984
+ const metadata = fromInputMaybe(args.metadata);
3637
3985
  let result;
3638
3986
  try {
3639
- result = await reactorClient.addRelationship(args.sourceIdentifier, args.targetIdentifier, args.relationshipType, branch);
3987
+ result = await reactorClient.addRelationship(args.sourceIdOrSlug, args.targetIdOrSlug, args.relationshipType, metadata, branch);
3640
3988
  } catch (error) {
3641
3989
  throw new GraphQLError(`Failed to add relationship: ${error instanceof Error ? error.message : "Unknown error"}`);
3642
3990
  }
3991
+ result = await readAsCaller(reactorClient, result, subject, branch);
3992
+ try {
3993
+ return toGqlPhDocument(result);
3994
+ } catch (error) {
3995
+ throw new GraphQLError(`Failed to convert document to GraphQL: ${error instanceof Error ? error.message : "Unknown error"}`);
3996
+ }
3997
+ }
3998
+ async function updateRelationship(reactorClient, args, subject) {
3999
+ const branch = fromInputMaybe(args.branch);
4000
+ const metadata = fromInputMaybe(args.metadata) ?? null;
4001
+ let result;
4002
+ try {
4003
+ result = await reactorClient.updateRelationship(args.sourceIdOrSlug, args.targetIdOrSlug, args.relationshipType, metadata, branch);
4004
+ } catch (error) {
4005
+ throw new GraphQLError(`Failed to update relationship: ${error instanceof Error ? error.message : "Unknown error"}`);
4006
+ }
4007
+ result = await readAsCaller(reactorClient, result, subject, branch);
3643
4008
  try {
3644
4009
  return toGqlPhDocument(result);
3645
4010
  } catch (error) {
3646
4011
  throw new GraphQLError(`Failed to convert document to GraphQL: ${error instanceof Error ? error.message : "Unknown error"}`);
3647
4012
  }
3648
4013
  }
3649
- async function removeRelationship(reactorClient, args) {
4014
+ async function removeRelationship(reactorClient, args, subject) {
3650
4015
  const branch = fromInputMaybe(args.branch);
3651
4016
  let result;
3652
4017
  try {
3653
- result = await reactorClient.removeRelationship(args.sourceIdentifier, args.targetIdentifier, args.relationshipType, branch);
4018
+ result = await reactorClient.removeRelationship(args.sourceIdOrSlug, args.targetIdOrSlug, args.relationshipType, branch);
3654
4019
  } catch (error) {
3655
4020
  throw new GraphQLError(`Failed to remove relationship: ${error instanceof Error ? error.message : "Unknown error"}`);
3656
4021
  }
4022
+ result = await readAsCaller(reactorClient, result, subject, branch);
3657
4023
  try {
3658
4024
  return toGqlPhDocument(result);
3659
4025
  } catch (error) {
3660
4026
  throw new GraphQLError(`Failed to convert document to GraphQL: ${error instanceof Error ? error.message : "Unknown error"}`);
3661
4027
  }
3662
4028
  }
3663
- async function moveRelationship(reactorClient, args) {
4029
+ async function moveRelationship(reactorClient, args, subject) {
3664
4030
  const branch = fromInputMaybe(args.branch);
3665
4031
  let result;
3666
4032
  try {
3667
- result = await reactorClient.moveRelationship(args.sourceParentIdentifier, args.targetParentIdentifier, args.targetIdentifier, args.relationshipType, branch);
4033
+ result = await reactorClient.moveRelationship(args.sourceParentIdOrSlug, args.targetParentIdOrSlug, args.targetIdOrSlug, args.relationshipType, branch);
3668
4034
  } catch (error) {
3669
4035
  throw new GraphQLError(`Failed to move relationship: ${error instanceof Error ? error.message : "Unknown error"}`);
3670
4036
  }
4037
+ result = {
4038
+ source: await readAsCaller(reactorClient, result.source, subject, branch),
4039
+ target: await readAsCaller(reactorClient, result.target, subject, branch)
4040
+ };
3671
4041
  try {
3672
4042
  return {
3673
4043
  source: toGqlPhDocument(result.source),
@@ -3680,9 +4050,12 @@ async function moveRelationship(reactorClient, args) {
3680
4050
  async function deleteDocument(reactorClient, args, reactorDriveClient) {
3681
4051
  const propagate = toReactorPropagationMode(args.propagate);
3682
4052
  try {
3683
- const driveParent = (await reactorClient.getIncomingRelationships(args.identifier, "child")).results.find((p) => isDriveContainerType(p.header.documentType));
3684
- if (driveParent) await pickDriveClient(reactorClient, driveParent.header.documentType, reactorDriveClient).removeNode(driveParent.header.id, args.identifier);
3685
- else await reactorClient.deleteDocument(args.identifier, propagate);
4053
+ const driveParent = (await reactorClient.getIncomingRelationships(args.idOrSlug, "child")).results.find((p) => isDriveContainerType(p.header.documentType));
4054
+ if (driveParent) {
4055
+ const driveClient = pickDriveClient(reactorClient, driveParent.header.documentType, reactorDriveClient);
4056
+ const documentId = await reactorClient.resolveIdOrSlug(args.idOrSlug);
4057
+ await driveClient.removeNode(driveParent.header.id, documentId);
4058
+ } else await reactorClient.deleteDocument(args.idOrSlug, propagate);
3686
4059
  return true;
3687
4060
  } catch (error) {
3688
4061
  throw new GraphQLError(`Failed to delete document: ${error instanceof Error ? error.message : "Unknown error"}`);
@@ -3690,9 +4063,9 @@ async function deleteDocument(reactorClient, args, reactorDriveClient) {
3690
4063
  }
3691
4064
  async function deleteDocuments(reactorClient, args) {
3692
4065
  const propagate = toReactorPropagationMode(args.propagate);
3693
- const identifiers = [...args.identifiers];
4066
+ const idsOrSlugs = [...args.idsOrSlugs];
3694
4067
  try {
3695
- await reactorClient.deleteDocuments(identifiers, propagate);
4068
+ await reactorClient.deleteDocuments(idsOrSlugs, propagate);
3696
4069
  return true;
3697
4070
  } catch (error) {
3698
4071
  throw new GraphQLError(`Failed to delete documents: ${error instanceof Error ? error.message : "Unknown error"}`);
@@ -3706,12 +4079,19 @@ async function deleteDocuments(reactorClient, args) {
3706
4079
  * an unanswered question: the first authenticated subject to use it adopts it.
3707
4080
  */
3708
4081
  async function touchChannel(syncManager, args, boundAddress) {
4082
+ const peer = readPeerManifest(args.input.manifest);
4083
+ let existing;
3709
4084
  try {
4085
+ existing = syncManager.getById(args.input.id);
4086
+ } catch {}
4087
+ if (existing) {
4088
+ await syncManager.setPeerManifest(args.input.id, peer);
3710
4089
  return {
3711
4090
  success: true,
3712
- ackOrdinal: syncManager.getById(args.input.id).channel.inbox.ackOrdinal
4091
+ ackOrdinal: existing.channel.inbox.ackOrdinal,
4092
+ manifest: syncManager.localManifest()
3713
4093
  };
3714
- } catch {}
4094
+ }
3715
4095
  const filter = {
3716
4096
  documentId: [...args.input.filter.documentId],
3717
4097
  scope: [...args.input.filter.scope],
@@ -3725,13 +4105,45 @@ async function touchChannel(syncManager, args, boundAddress) {
3725
4105
  await syncManager.add(args.input.name, DriveCollectionId.fromKey(args.input.collectionId), {
3726
4106
  type: "polling",
3727
4107
  parameters: {}
3728
- }, filter, options, args.input.id);
4108
+ }, filter, options, args.input.id, peer);
3729
4109
  } catch (error) {
3730
4110
  throw new GraphQLError(`Failed to create channel: ${error instanceof Error ? error.message : "Unknown error"}`);
3731
4111
  }
3732
4112
  return {
3733
4113
  success: true,
3734
- ackOrdinal: 0
4114
+ ackOrdinal: 0,
4115
+ manifest: syncManager.localManifest()
4116
+ };
4117
+ }
4118
+ async function syncHolds(syncManager, args) {
4119
+ return (await syncManager.listHolds({
4120
+ remoteName: args.remoteName ?? void 0,
4121
+ documentId: args.documentId ?? void 0
4122
+ })).map((hold) => ({
4123
+ ...hold,
4124
+ reason: {
4125
+ ...hold.reason,
4126
+ peerSupports: [...hold.reason.peerSupports]
4127
+ },
4128
+ heldAtUtcMs: String(hold.heldAtUtcMs)
4129
+ }));
4130
+ }
4131
+ function peerAgreement(syncManager, args) {
4132
+ const agreement = syncManager.agreement();
4133
+ const members = [...agreement.members([args.collectionId])].map(([remoteName, supports]) => ({
4134
+ remoteName,
4135
+ announced: "revision" in supports,
4136
+ protocols: supports.protocols,
4137
+ features: supports.features
4138
+ }));
4139
+ return {
4140
+ collectionId: args.collectionId,
4141
+ local: agreement.local(),
4142
+ members,
4143
+ limitedBy: Object.keys(agreement.basis().wanted).map((protocol) => ({
4144
+ protocol,
4145
+ remoteNames: agreement.limitedBy(args.collectionId, protocol)
4146
+ }))
3735
4147
  };
3736
4148
  }
3737
4149
  /**
@@ -3786,7 +4198,8 @@ async function collectHeldSyncOperations(syncOps, gate, subject, signal) {
3786
4198
  * client ordinal the switchboard has successfully applied, so the client
3787
4199
  * can trim its own outbox)
3788
4200
  */
3789
- function pollSyncEnvelopes(syncManager, args, forbiddenIds = /* @__PURE__ */ new Set(), heldOpIds = /* @__PURE__ */ new Set()) {
4201
+ function pollSyncEnvelopes(syncManager, args, forbiddenIds = /* @__PURE__ */ new Set(), heldOpIds = /* @__PURE__ */ new Set(), gatedOpIds) {
4202
+ const isHeld = (syncOp) => heldOpIds.has(syncOp.id) || gatedOpIds !== void 0 && !gatedOpIds.has(syncOp.id);
3790
4203
  let remote;
3791
4204
  try {
3792
4205
  remote = syncManager.getById(args.channelId);
@@ -3794,7 +4207,11 @@ function pollSyncEnvelopes(syncManager, args, forbiddenIds = /* @__PURE__ */ new
3794
4207
  throw new GraphQLError(`Channel not found: ${error instanceof Error ? error.message : "Unknown error"}`);
3795
4208
  }
3796
4209
  remote.channel.notePoll();
3797
- const deadLetters = remote.channel.deadLetter.items.filter((syncOp) => !forbiddenIds.has(syncOp.documentId) && !heldOpIds.has(syncOp.id)).map((syncOp) => ({
4210
+ const revisions = {
4211
+ manifestRevision: syncManager.localManifest().revision,
4212
+ peerManifestRevision: remote.meta.peer?.manifest?.revision ?? null
4213
+ };
4214
+ const deadLetters = remote.channel.deadLetter.items.filter((syncOp) => !forbiddenIds.has(syncOp.documentId) && !isHeld(syncOp)).map((syncOp) => ({
3798
4215
  documentId: syncOp.documentId,
3799
4216
  error: syncOp.error?.message ?? "Unknown error",
3800
4217
  errorType: syncOperationErrorType(syncOp.error),
@@ -3829,7 +4246,8 @@ function pollSyncEnvelopes(syncManager, args, forbiddenIds = /* @__PURE__ */ new
3829
4246
  envelopes: [],
3830
4247
  ackOrdinal: remote.channel.inbox.ackOrdinal,
3831
4248
  deadLetters,
3832
- hasMore: false
4249
+ hasMore: false,
4250
+ ...revisions
3833
4251
  };
3834
4252
  const sorted = [...operations].sort((a, b) => {
3835
4253
  return (a.operations[0]?.context.ordinal ?? 0) - (b.operations[0]?.context.ordinal ?? 0);
@@ -3843,9 +4261,10 @@ function pollSyncEnvelopes(syncManager, args, forbiddenIds = /* @__PURE__ */ new
3843
4261
  hasMore = true;
3844
4262
  break;
3845
4263
  }
3846
- if (heldOpIds.has(syncOp.id)) continue;
4264
+ if (isHeld(syncOp)) continue;
3847
4265
  syncOp.deliveredCount ??= 0;
3848
4266
  syncOp.emittedCount ??= 0;
4267
+ if (!forbiddenIds.has(syncOp.documentId)) while (syncOp.deliveredCount > 0 && syncOp.operations[syncOp.deliveredCount - 1].context.ordinal > args.outboxLatest) syncOp.deliveredCount -= 1;
3849
4268
  while (syncOp.deliveredCount < syncOp.emittedCount && syncOp.operations[syncOp.deliveredCount].context.ordinal <= args.outboxLatest) syncOp.deliveredCount += 1;
3850
4269
  const remaining = syncOp.operations.slice(syncOp.deliveredCount);
3851
4270
  if (remaining.length === 0) continue;
@@ -3897,9 +4316,71 @@ function pollSyncEnvelopes(syncManager, args, forbiddenIds = /* @__PURE__ */ new
3897
4316
  envelopes,
3898
4317
  ackOrdinal: remote.channel.inbox.ackOrdinal,
3899
4318
  deadLetters,
3900
- hasMore
4319
+ hasMore,
4320
+ ...revisions
3901
4321
  };
3902
4322
  }
4323
+ const MARKER_REFUSAL = "marker";
4324
+ /**
4325
+ * The client's UNSUPPORTED_PROTOCOL refusals of polled rows. Each becomes a hold
4326
+ * for that client, as a pushed refusal does, rather than counting as delivered.
4327
+ * A refused marker is not a hold: {@link recordPollMarkerRefusals} takes it.
4328
+ * A kind this server does not know is ignored.
4329
+ */
4330
+ function holdPollRefusals(syncManager, channelId, all) {
4331
+ const refusals = all?.filter((refusal) => refusal.kind === void 0 || refusal.kind === null);
4332
+ if (!refusals?.length) return;
4333
+ let remote;
4334
+ try {
4335
+ remote = syncManager.getById(channelId);
4336
+ } catch {
4337
+ return;
4338
+ }
4339
+ const refused = refusals.map((refusal) => {
4340
+ const syncOp = new SyncOperation(crypto.randomUUID(), "", [], remote.meta.name, refusal.documentId, [], refusal.branch, []);
4341
+ syncOp.failed(new ChannelError(ChannelErrorSource.Outbox, /* @__PURE__ */ new Error(`Refused by ${remote.meta.name}`), "UNSUPPORTED_PROTOCOL"));
4342
+ return syncOp;
4343
+ });
4344
+ remote.channel.deadLetter.add(...refused);
4345
+ }
4346
+ /**
4347
+ * The client's refusals of purge markers it polled. Persisted before the poll is
4348
+ * served; a failure fails the poll, and the client reports them again. The sync
4349
+ * manager keeps only refusals of markers this remote was owed.
4350
+ */
4351
+ async function recordPollMarkerRefusals(syncManager, channelId, all) {
4352
+ const refusals = all?.filter((refusal) => refusal.kind === MARKER_REFUSAL);
4353
+ if (!refusals?.length) return;
4354
+ if (!supportsPurgeRefusals(syncManager)) return;
4355
+ let remote;
4356
+ try {
4357
+ remote = syncManager.getById(channelId);
4358
+ } catch {
4359
+ return;
4360
+ }
4361
+ try {
4362
+ await syncManager.recordPolledMarkerRefusals(remote.meta.name, refusals.map(({ documentId, branch }) => ({
4363
+ documentId,
4364
+ branch
4365
+ })));
4366
+ } catch (error) {
4367
+ throw new RefusalNotRecordedError(error);
4368
+ }
4369
+ }
4370
+ /**
4371
+ * A poll naming no revision is from a client without peer agreement: it is
4372
+ * recorded silent, and what it can no longer run is held, before it is served.
4373
+ */
4374
+ async function silenceUnversionedPoll(syncManager, channelId, manifestRevision) {
4375
+ if (typeof manifestRevision === "string") return;
4376
+ let remote;
4377
+ try {
4378
+ remote = syncManager.getById(channelId);
4379
+ } catch {
4380
+ return;
4381
+ }
4382
+ if (remote.meta.peer?.manifest) await syncManager.setPeerManifest(channelId, null);
4383
+ }
3903
4384
  /**
3904
4385
  * Receives sync envelopes pushed by a client and adds them to the
3905
4386
  * appropriate channel inboxes.
@@ -3908,7 +4389,7 @@ function pollSyncEnvelopes(syncManager, args, forbiddenIds = /* @__PURE__ */ new
3908
4389
  * It must be preserved because the inbox mailbox tracks applied ordinals
3909
4390
  * and returns the highest one as `ackOrdinal` in pollSyncEnvelopes.
3910
4391
  */
3911
- function pushSyncEnvelopes(syncManager, args) {
4392
+ async function pushSyncEnvelopes(syncManager, args) {
3912
4393
  const remoteSyncOps = /* @__PURE__ */ new Map();
3913
4394
  for (const envelope of args.envelopes) {
3914
4395
  let remote;
@@ -3929,7 +4410,12 @@ function pushSyncEnvelopes(syncManager, args) {
3929
4410
  for (const syncOp of consolidated) syncOp.jobDependencies = syncOp.jobDependencies.filter((dep) => validKeys.has(dep));
3930
4411
  remote.channel.inbox.add(...consolidated);
3931
4412
  }
3932
- return Promise.resolve(true);
4413
+ try {
4414
+ await syncManager.receiptsStored?.([...remoteSyncOps.keys()].map((remote) => remote.meta.name));
4415
+ } catch (error) {
4416
+ throw new GraphQLError(`Failed to store received markers: ${error instanceof Error ? error.message : String(error)}`);
4417
+ }
4418
+ return true;
3933
4419
  }
3934
4420
  //#endregion
3935
4421
  //#region src/graphql/document-model-subgraph.ts
@@ -3956,6 +4442,16 @@ var DocumentModelSubgraph = class extends BaseSubgraph {
3956
4442
  const documentName = getDocumentModelSchemaName(this.documentModel.documentModel.global);
3957
4443
  return this.resolvers[`${documentName}Mutations`];
3958
4444
  }
4445
+ /** Drops the items the host's own ACL refuses the caller. */
4446
+ async #readableItems(page, ctx) {
4447
+ if (this.authorizationService.isSupremeAdmin(ctx.user?.address)) return page;
4448
+ const items = [];
4449
+ for (const item of page.items) if (await this.canReadDocument(item.id, ctx)) items.push(item);
4450
+ return {
4451
+ ...page,
4452
+ items
4453
+ };
4454
+ }
3959
4455
  /**
3960
4456
  * Generate __resolveType functions for union types found in the document model schema.
3961
4457
  * Parses the state schema to find union definitions and their member types,
@@ -4011,13 +4507,14 @@ var DocumentModelSubgraph = class extends BaseSubgraph {
4011
4507
  Query: { [documentName]: () => ({}) },
4012
4508
  [`${documentName}Queries`]: {
4013
4509
  document: async (_, args, ctx) => {
4014
- const { identifier, view } = args;
4015
- if (!identifier) throw new GraphQLError("Document identifier is required");
4510
+ const idOrSlug = requireOneOf(args, "idOrSlug", "identifier");
4511
+ const { view } = args;
4512
+ if (!idOrSlug) throw new GraphQLError("Document identifier is required");
4016
4513
  const result = await document(this.reactorClient, {
4017
- identifier,
4514
+ idOrSlug,
4018
4515
  view
4019
- });
4020
- if (result.document.documentType !== documentType) throw new GraphQLError(`Document with id ${identifier} is not of type ${documentType}`);
4516
+ }, this.viewSubject(ctx));
4517
+ if (result.document.documentType !== documentType) throw new GraphQLError(`Document with id ${idOrSlug} is not of type ${documentType}`);
4021
4518
  await this.assertCanReadCanonical(result.document.id, ctx);
4022
4519
  return result;
4023
4520
  },
@@ -4026,17 +4523,8 @@ var DocumentModelSubgraph = class extends BaseSubgraph {
4026
4523
  const result = await findDocuments(this.reactorClient, {
4027
4524
  search: { type: documentType },
4028
4525
  paging
4029
- });
4030
- if (!this.authorizationService.isSupremeAdmin(ctx.user?.address)) {
4031
- const filteredItems = [];
4032
- for (const item of result.items) if (await this.canReadDocument(item.id, ctx)) filteredItems.push(item);
4033
- return {
4034
- ...result,
4035
- items: filteredItems,
4036
- totalCount: filteredItems.length
4037
- };
4038
- }
4039
- return result;
4526
+ }, this.viewSubject(ctx));
4527
+ return this.#readableItems(result, ctx);
4040
4528
  },
4041
4529
  findDocuments: async (_, args, ctx) => {
4042
4530
  const { search, view, paging } = args;
@@ -4047,98 +4535,94 @@ var DocumentModelSubgraph = class extends BaseSubgraph {
4047
4535
  },
4048
4536
  view,
4049
4537
  paging
4050
- });
4051
- if (!this.authorizationService.isSupremeAdmin(ctx.user?.address)) {
4052
- const filteredItems = [];
4053
- for (const item of result.items) if (await this.canReadDocument(item.id, ctx)) filteredItems.push(item);
4054
- return {
4055
- ...result,
4056
- items: filteredItems,
4057
- totalCount: filteredItems.length
4058
- };
4059
- }
4060
- return result;
4538
+ }, this.viewSubject(ctx));
4539
+ return this.#readableItems(result, ctx);
4061
4540
  },
4062
4541
  documentOutgoingRelationships: async (_, args, ctx) => {
4542
+ const sourceIdOrSlug = requireOneOf(args, "sourceIdOrSlug", "sourceIdentifier");
4063
4543
  const { relationshipType, view, paging } = args;
4064
- const handle = await this.assertCanRead(args.sourceIdentifier, ctx);
4544
+ const handle = await this.assertCanRead(sourceIdOrSlug, ctx);
4065
4545
  const result = await documentOutgoingRelationships(this.reactorClient, {
4066
- sourceIdentifier: handle.fetchIdentifier,
4546
+ sourceIdOrSlug: handle.fetchIdentifier,
4067
4547
  relationshipType,
4068
4548
  view,
4069
4549
  paging
4070
- });
4071
- const filteredItems = result.items.filter((item) => item.documentType === documentType);
4550
+ }, this.viewSubject(ctx));
4551
+ const readable = await this.#readableItems(result, ctx);
4552
+ const filteredItems = readable.items.filter((item) => item.documentType === documentType);
4072
4553
  return {
4073
- ...result,
4074
- items: filteredItems,
4075
- totalCount: filteredItems.length
4554
+ ...readable,
4555
+ items: filteredItems
4076
4556
  };
4077
4557
  },
4078
4558
  documentIncomingRelationships: async (_, args, ctx) => {
4559
+ const targetIdOrSlug = requireOneOf(args, "targetIdOrSlug", "targetIdentifier");
4079
4560
  const { relationshipType, view, paging } = args;
4080
- const handle = await this.assertCanRead(args.targetIdentifier, ctx);
4081
- return documentIncomingRelationships(this.reactorClient, {
4082
- targetIdentifier: handle.fetchIdentifier,
4561
+ const handle = await this.assertCanRead(targetIdOrSlug, ctx);
4562
+ const result = await documentIncomingRelationships(this.reactorClient, {
4563
+ targetIdOrSlug: handle.fetchIdentifier,
4083
4564
  relationshipType,
4084
4565
  view,
4085
4566
  paging
4086
- });
4567
+ }, this.viewSubject(ctx));
4568
+ return this.#readableItems(result, ctx);
4087
4569
  }
4088
4570
  },
4089
4571
  Mutation: { [documentName]: () => ({}) },
4090
4572
  [`${documentName}Mutations`]: {
4091
4573
  createDocument: async (_, args, ctx) => {
4092
4574
  const { name, slug, preferredEditor, initialState } = args;
4093
- let parentIdentifier = args.parentIdentifier;
4094
- if (parentIdentifier) parentIdentifier = (await this.assertCanWrite(parentIdentifier, ctx)).fetchIdentifier;
4575
+ let parentIdOrSlug = optionalOneOf(args, "parentIdOrSlug", "parentIdentifier");
4576
+ if (parentIdOrSlug) parentIdOrSlug = (await this.assertCanWrite(parentIdOrSlug, ctx)).fetchIdentifier;
4095
4577
  else this.assertCanCreate(ctx);
4096
4578
  let createdDoc;
4097
4579
  if (initialState || preferredEditor) createdDoc = await createDocumentWithInitialState(this.reactorClient, {
4098
4580
  documentType,
4099
- parentIdentifier,
4581
+ parentIdOrSlug,
4100
4582
  name,
4101
4583
  slug,
4102
4584
  preferredEditor,
4103
4585
  initialState: initialState ?? {}
4104
- }, this.graphqlManager.reactorDriveClient);
4586
+ }, this.graphqlManager.reactorDriveClient, this.viewSubject(ctx));
4105
4587
  else createdDoc = await createEmptyDocument(this.reactorClient, {
4106
4588
  documentType,
4107
- parentIdentifier,
4589
+ parentIdOrSlug,
4108
4590
  name
4109
- }, this.graphqlManager.reactorDriveClient);
4591
+ }, this.graphqlManager.reactorDriveClient, this.viewSubject(ctx));
4110
4592
  if (ctx.user?.address && createdDoc?.id) await this.documentPermissionService?.initializeDocumentProtection(createdDoc.id, ctx.user.address, this.authorizationService.config.defaultProtection);
4111
- if (!initialState && !preferredEditor && name && createdDoc.name !== name) return toGqlPhDocument(await this.reactorClient.execute(createdDoc.id, "main", [setName(name)]));
4593
+ if (!initialState && !preferredEditor && name && createdDoc.name !== name) return toGqlPhDocument(await this.reactorClient.execute(createdDoc.id, "main", [setName(name)], void 0, this.viewSubject(ctx)));
4112
4594
  return createdDoc;
4113
4595
  },
4114
4596
  createEmptyDocument: async (_, args, ctx) => {
4115
- let parentIdentifier = args.parentIdentifier;
4116
- if (parentIdentifier) parentIdentifier = (await this.assertCanWrite(parentIdentifier, ctx)).fetchIdentifier;
4597
+ let parentIdOrSlug = optionalOneOf(args, "parentIdOrSlug", "parentIdentifier");
4598
+ if (parentIdOrSlug) parentIdOrSlug = (await this.assertCanWrite(parentIdOrSlug, ctx)).fetchIdentifier;
4117
4599
  else this.assertCanCreate(ctx);
4118
4600
  const result = await createEmptyDocument(this.reactorClient, {
4119
4601
  documentType,
4120
- parentIdentifier
4121
- }, this.graphqlManager.reactorDriveClient);
4602
+ parentIdOrSlug
4603
+ }, this.graphqlManager.reactorDriveClient, this.viewSubject(ctx));
4122
4604
  if (ctx.user?.address && result?.id) await this.documentPermissionService?.initializeDocumentProtection(result.id, ctx.user.address, this.authorizationService.config.defaultProtection);
4123
4605
  return result;
4124
4606
  },
4125
4607
  ...operations.reduce((mutations, op) => {
4126
4608
  mutations[camelCase(op.name)] = async (_, args, ctx) => {
4127
- const { docId, input } = args;
4128
- const effectiveDocId = (await this.assertCanExecuteOperation(docId, op.name, ctx)).fetchIdentifier;
4129
- if ((await this.reactorClient.get(effectiveDocId)).header.documentType !== documentType) throw new GraphQLError(`Document with id ${docId} is not of type ${documentType}`);
4609
+ const documentIdOrSlug = requireOneOf(args, "documentIdOrSlug", "docId");
4610
+ const { input } = args;
4611
+ const effectiveDocId = (await this.assertCanExecuteOperation(documentIdOrSlug, op.name, ctx)).fetchIdentifier;
4612
+ if ((await this.reactorClient.get(effectiveDocId, { subject: this.viewSubject(ctx) })).header.documentType !== documentType) throw new GraphQLError(`Document with id ${documentIdOrSlug} is not of type ${documentType}`);
4130
4613
  const action = this.documentModel.actions[camelCase(op.name)];
4131
4614
  if (!action) throw new GraphQLError(`Action ${op.name} not found`);
4132
4615
  try {
4133
- return toGqlPhDocument(await this.reactorClient.execute(effectiveDocId, "main", [action(input)]));
4616
+ return toGqlPhDocument(await this.reactorClient.execute(effectiveDocId, "main", [action(input)], void 0, this.viewSubject(ctx)));
4134
4617
  } catch (error) {
4135
4618
  throw new GraphQLError(error instanceof Error ? error.message : `Failed to ${op.name}`);
4136
4619
  }
4137
4620
  };
4138
4621
  mutations[`${camelCase(op.name)}Async`] = async (_, args, ctx) => {
4139
- const { docId, input } = args;
4140
- const effectiveDocId = (await this.assertCanExecuteOperation(docId, op.name, ctx)).fetchIdentifier;
4141
- if ((await this.reactorClient.get(effectiveDocId)).header.documentType !== documentType) throw new GraphQLError(`Document with id ${docId} is not of type ${documentType}`);
4622
+ const documentIdOrSlug = requireOneOf(args, "documentIdOrSlug", "docId");
4623
+ const { input } = args;
4624
+ const effectiveDocId = (await this.assertCanExecuteOperation(documentIdOrSlug, op.name, ctx)).fetchIdentifier;
4625
+ if ((await this.reactorClient.get(effectiveDocId, { subject: this.viewSubject(ctx) })).header.documentType !== documentType) throw new GraphQLError(`Document with id ${documentIdOrSlug} is not of type ${documentType}`);
4142
4626
  const action = this.documentModel.actions[camelCase(op.name)];
4143
4627
  if (!action) throw new GraphQLError(`Action ${op.name} not found`);
4144
4628
  try {
@@ -4162,7 +4646,7 @@ const DRIVE_ID_HEADER = "drive-id";
4162
4646
  * in the ownership set yet); other create-shaped operations that may
4163
4647
  * synthesize a drive must be added here too.
4164
4648
  */
4165
- const CACHE_BYPASS_OPERATIONS = new Set(["createDocument", "createEmptyDocument"]);
4649
+ const CACHE_BYPASS_OPERATIONS = /* @__PURE__ */ new Set(["createDocument", "createEmptyDocument"]);
4166
4650
  const driveIdMap = /* @__PURE__ */ new WeakMap();
4167
4651
  /** Internal — only `graphql-manager.ts` should call this. */
4168
4652
  function getRequestDriveId(request) {
@@ -4232,6 +4716,7 @@ function wrongShardResponse(driveId) {
4232
4716
  * 421 response.
4233
4717
  */
4234
4718
  var DriveOwnershipCache = class {
4719
+ reactorClient;
4235
4720
  drives = /* @__PURE__ */ new Set();
4236
4721
  constructor(reactorClient) {
4237
4722
  this.reactorClient = reactorClient;
@@ -4363,8 +4848,8 @@ var GraphQLManager = class {
4363
4848
  subgraphHandlerCache = /* @__PURE__ */ new Map();
4364
4849
  /** subgraphPath → the http adapter handle of its mounted route. */
4365
4850
  subgraphRouteHandles = /* @__PURE__ */ new Map();
4366
- /** Handle of the currently mounted supergraph SSE route, if any. */
4367
- sseRouteHandle;
4851
+ /** Mounted SSE routes, keyed by path: the supergraph's and each subgraph's. */
4852
+ sseRouteHandles = /* @__PURE__ */ new Map();
4368
4853
  /**
4369
4854
  * Package name → the subgraph instances registered from it, keyed by
4370
4855
  * subgraph name. Tracks the package association so that removed packages
@@ -4383,26 +4868,43 @@ var GraphQLManager = class {
4383
4868
  authorizationService;
4384
4869
  /** Route service in use: the injected one, or one built on first need. */
4385
4870
  #fallbackRoutes;
4386
- constructor(path, httpServer, wsServer, reactorClient, relationalDb, analyticsStore, syncManager, logger, httpAdapter, gatewayAdapter, authService, documentPermissionService, featureFlags = DefaultFeatureFlags, port = 4001, authorizationService, reactorDriveClient, syncServingGate, httpRoutes) {
4387
- this.path = path;
4388
- this.httpServer = httpServer;
4389
- this.wsServer = wsServer;
4390
- this.reactorClient = reactorClient;
4391
- this.relationalDb = relationalDb;
4392
- this.analyticsStore = analyticsStore;
4393
- this.syncManager = syncManager;
4394
- this.logger = logger;
4395
- this.httpAdapter = httpAdapter;
4396
- this.gatewayAdapter = gatewayAdapter;
4397
- this.authService = authService;
4398
- this.documentPermissionService = documentPermissionService;
4399
- this.featureFlags = featureFlags;
4400
- this.port = port;
4401
- this.syncServingGate = syncServingGate;
4402
- this.httpRoutes = httpRoutes;
4403
- if (!authorizationService) throw new Error("GraphQLManager requires an authorizationService");
4404
- this.authorizationService = authorizationService;
4405
- this.reactorDriveClient = reactorDriveClient;
4871
+ path;
4872
+ httpServer;
4873
+ wsServer;
4874
+ reactorClient;
4875
+ relationalDb;
4876
+ analyticsStore;
4877
+ syncManager;
4878
+ logger;
4879
+ httpAdapter;
4880
+ gatewayAdapter;
4881
+ authService;
4882
+ documentPermissionService;
4883
+ featureFlags;
4884
+ port;
4885
+ syncServingGate;
4886
+ httpRoutes;
4887
+ attachments;
4888
+ constructor(options) {
4889
+ this.path = options.path;
4890
+ this.httpServer = options.httpServer;
4891
+ this.wsServer = options.wsServer;
4892
+ this.reactorClient = options.reactorClient;
4893
+ this.relationalDb = options.relationalDb;
4894
+ this.analyticsStore = options.analyticsStore;
4895
+ this.syncManager = options.syncManager;
4896
+ this.logger = options.logger;
4897
+ this.httpAdapter = options.httpAdapter;
4898
+ this.gatewayAdapter = options.gatewayAdapter;
4899
+ this.authService = options.authService;
4900
+ this.documentPermissionService = options.documentPermissionService;
4901
+ this.featureFlags = options.featureFlags ?? DefaultFeatureFlags;
4902
+ this.port = options.port ?? 4001;
4903
+ this.authorizationService = options.authorizationService;
4904
+ this.reactorDriveClient = options.reactorDriveClient;
4905
+ this.syncServingGate = options.syncServingGate;
4906
+ this.httpRoutes = options.httpRoutes;
4907
+ this.attachments = options.attachments;
4406
4908
  this.driveOwnershipCache = new DriveOwnershipCache(this.reactorClient);
4407
4909
  this.wsServer.setMaxListeners(0);
4408
4910
  }
@@ -4425,23 +4927,25 @@ var GraphQLManager = class {
4425
4927
  const matched = driveMatcher(url.pathname);
4426
4928
  const driveIdOrSlug = matched ? matched.params.drive : void 0;
4427
4929
  if (!driveIdOrSlug) return Response.json({ error: "Drive ID or slug is required" }, { status: 400 });
4930
+ const user = getAuthContext(request)?.user;
4428
4931
  try {
4429
- const driveDoc = await this.reactorClient.get(driveIdOrSlug);
4430
- if (!await this.authorizationService.canRead(driveDoc.header.id, getAuthContext(request)?.user?.address)) {
4932
+ const driveDoc = await this.reactorClient.get(driveIdOrSlug, { subject: callerSubject(user) });
4933
+ if (!await this.authorizationService.canRead(driveDoc.header.id, user?.address)) {
4431
4934
  this.logger.debug(`Drive read refused: ${driveIdOrSlug}`);
4432
4935
  return Response.json({ error: "Drive not found" }, { status: 404 });
4433
4936
  }
4434
4937
  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`;
4938
+ const global = driveDoc.state.global;
4435
4939
  return Response.json({
4436
4940
  id: driveDoc.header.id,
4437
4941
  slug: driveDoc.header.slug,
4438
4942
  meta: driveDoc.header.meta,
4439
- name: driveDoc.state.global.name || driveDoc.header.name,
4440
- icon: driveDoc.state.global.icon ?? void 0,
4943
+ name: global?.name || driveDoc.header.name,
4944
+ icon: global?.icon ?? void 0,
4441
4945
  ...graphqlEndpoint && { graphqlEndpoint }
4442
4946
  });
4443
4947
  } catch (error) {
4444
- this.logger.debug(`Drive not found: ${driveIdOrSlug}`, error);
4948
+ this.logger.debug("Drive not found: @drive (@error)", driveIdOrSlug, error);
4445
4949
  return Response.json({ error: "Drive not found" }, { status: 404 });
4446
4950
  }
4447
4951
  };
@@ -4474,7 +4978,7 @@ var GraphQLManager = class {
4474
4978
  for (const subgraph of coreSubgraphs) try {
4475
4979
  await this.registerSubgraph(subgraph, supergraph, true);
4476
4980
  } catch (error) {
4477
- this.logger.error(`Failed to setup core subgraph ${subgraph.name}`, error);
4981
+ this.logger.error("Failed to setup core subgraph @subgraph: @error", subgraph.name, error);
4478
4982
  }
4479
4983
  return this.#setupSubgraphs(this.coreSubgraphsMap);
4480
4984
  }
@@ -4495,12 +4999,13 @@ var GraphQLManager = class {
4495
4999
  path: this.path,
4496
5000
  documentPermissionService: this.documentPermissionService,
4497
5001
  authorizationService: this.authorizationService,
4498
- syncServingGate: this.syncServingGate
5002
+ syncServingGate: this.syncServingGate,
5003
+ attachments: this.attachments
4499
5004
  });
4500
5005
  await this.#addSubgraphInstance(subgraphInstance, supergraph, false, DOCUMENT_MODEL_SUBGRAPH_SOURCE);
4501
5006
  registeredNames.add(subgraphInstance.name);
4502
5007
  } catch (error) {
4503
- this.logger.error(`Failed to setup document model subgraph for ${documentModel.documentModel.global.id}`, error instanceof Error ? error.message : error);
5008
+ this.logger.error("Failed to setup document model subgraph for @documentType: @error", documentModel.documentModel.global.id, error instanceof Error ? error.message : error);
4504
5009
  this.logger.debug("@error", error);
4505
5010
  }
4506
5011
  }
@@ -4562,6 +5067,7 @@ var GraphQLManager = class {
4562
5067
  routeHandle.dispose();
4563
5068
  this.subgraphRouteHandles.delete(subgraphPath);
4564
5069
  }
5070
+ this.#disposeSSERoute(subgraphPath);
4565
5071
  const wsDisposer = this.subgraphWsDisposers.get(subgraphPath);
4566
5072
  if (wsDisposer) {
4567
5073
  this.subgraphWsDisposers.delete(subgraphPath);
@@ -4631,7 +5137,8 @@ var GraphQLManager = class {
4631
5137
  path: this.path,
4632
5138
  documentPermissionService: this.documentPermissionService,
4633
5139
  authorizationService: this.authorizationService,
4634
- syncServingGate: this.syncServingGate
5140
+ syncServingGate: this.syncServingGate,
5141
+ attachments: this.attachments
4635
5142
  });
4636
5143
  return this.#addSubgraphInstance(subgraphInstance, supergraph, core, packageName);
4637
5144
  }
@@ -4658,7 +5165,7 @@ var GraphQLManager = class {
4658
5165
  try {
4659
5166
  routes?.disposeScope(packageName);
4660
5167
  } catch (error) {
4661
- this.logger.warn(`Failed to release HTTP routes for "${packageName}": @error`, error);
5168
+ this.logger.warn(`Failed to release HTTP routes for "@package": @error`, packageName, error);
4662
5169
  }
4663
5170
  }
4664
5171
  /**
@@ -4758,15 +5265,12 @@ var GraphQLManager = class {
4758
5265
  authService: this.authService,
4759
5266
  requireAuthenticatedCaller: () => this.#requireAuthMiddleware !== void 0,
4760
5267
  logger: this.logger,
4761
- buildContext: (connectionParams, user) => {
4762
- const context = {
4763
- headers: connectionParams,
4764
- db: this.relationalDb,
4765
- ...this.getAdditionalContextFields()
4766
- };
4767
- if (user) context.user = user;
4768
- return context;
4769
- }
5268
+ buildContext: (connectionParams, user) => ({
5269
+ headers: connectionParams,
5270
+ db: this.relationalDb,
5271
+ ...this.getAdditionalContextFields(),
5272
+ user
5273
+ })
4770
5274
  });
4771
5275
  }
4772
5276
  setSupergraph(supergraph, subgraphs) {
@@ -4901,10 +5405,7 @@ var GraphQLManager = class {
4901
5405
  const allSubgraphs = this.#getAllSubgraphs();
4902
5406
  const modules = Array.from(allSubgraphs.values()).filter((subgraph) => subgraph.hasSubscriptions).map((subgraph) => this.#buildSubgraphSchemaModule(subgraph));
4903
5407
  if (modules.length === 0) {
4904
- if (this.sseRouteHandle !== void 0) {
4905
- this.sseRouteHandle.dispose();
4906
- this.sseRouteHandle = void 0;
4907
- }
5408
+ this.#disposeSSERoute(superGraphPath);
4908
5409
  return;
4909
5410
  }
4910
5411
  try {
@@ -4930,8 +5431,16 @@ var GraphQLManager = class {
4930
5431
  contextFactory: this.#makeContextFactory()
4931
5432
  });
4932
5433
  const handler = this.#composeFetchMiddleware(rawHandler);
4933
- if (this.sseRouteHandle !== void 0) this.sseRouteHandle.dispose();
4934
- this.sseRouteHandle = this.httpAdapter.mount(ssePath, handler, { exact: true });
5434
+ this.#disposeSSERoute(basePath);
5435
+ this.sseRouteHandles.set(ssePath, this.httpAdapter.mount(ssePath, handler, { exact: true }));
5436
+ }
5437
+ #disposeSSERoute(basePath) {
5438
+ const ssePath = basePath + "/stream";
5439
+ const handle = this.sseRouteHandles.get(ssePath);
5440
+ if (handle !== void 0) {
5441
+ handle.dispose();
5442
+ this.sseRouteHandles.delete(ssePath);
5443
+ }
4935
5444
  }
4936
5445
  /**
4937
5446
  * Compose the request-level fetch middleware chain. Auth runs first
@@ -5215,7 +5724,6 @@ const DocumentModelStateDTO = z.object({
5215
5724
  }).strip();
5216
5725
  const DocumentModelResultPageDTO = z.object({
5217
5726
  items: z.array(DocumentModelStateDTO),
5218
- totalCount: z.number().int(),
5219
5727
  hasNextPage: z.boolean(),
5220
5728
  hasPreviousPage: z.boolean(),
5221
5729
  cursor: z.string().nullable().optional()
@@ -5236,7 +5744,20 @@ const PHDocumentDTO = z.object({
5236
5744
  }).strip();
5237
5745
  const PHDocumentResultPageDTO = z.object({
5238
5746
  items: z.array(PHDocumentDTO),
5239
- totalCount: z.number().int(),
5747
+ hasNextPage: z.boolean(),
5748
+ hasPreviousPage: z.boolean(),
5749
+ cursor: z.string().nullable().optional()
5750
+ }).strip();
5751
+ const DocumentRelationshipDTO = z.object({
5752
+ sourceId: z.string(),
5753
+ targetId: z.string(),
5754
+ relationshipType: z.string(),
5755
+ metadata: JSONObjectDTO.nullable().optional(),
5756
+ createdAt: DateTimeDTO,
5757
+ updatedAt: DateTimeDTO
5758
+ }).strip();
5759
+ const DocumentRelationshipResultPageDTO = z.object({
5760
+ items: z.array(DocumentRelationshipDTO),
5240
5761
  hasNextPage: z.boolean(),
5241
5762
  hasPreviousPage: z.boolean(),
5242
5763
  cursor: z.string().nullable().optional()
@@ -5291,7 +5812,6 @@ const OperationDTO = z.object({
5291
5812
  }).strip();
5292
5813
  const OperationResultPageDTO = z.object({
5293
5814
  items: z.array(OperationDTO),
5294
- totalCount: z.number().int(),
5295
5815
  hasNextPage: z.boolean(),
5296
5816
  hasPreviousPage: z.boolean(),
5297
5817
  cursor: z.string().nullable().optional()
@@ -5344,6 +5864,12 @@ const operationValidators = {
5344
5864
  GetDocumentIncomingRelationships: (data) => {
5345
5865
  if (data.documentIncomingRelationships) PHDocumentResultPageDTO.parse(data.documentIncomingRelationships);
5346
5866
  },
5867
+ GetDocumentOutgoingRelationshipEdges: (data) => {
5868
+ if (data.documentOutgoingRelationshipEdges) DocumentRelationshipResultPageDTO.parse(data.documentOutgoingRelationshipEdges);
5869
+ },
5870
+ GetDocumentIncomingRelationshipEdges: (data) => {
5871
+ if (data.documentIncomingRelationshipEdges) DocumentRelationshipResultPageDTO.parse(data.documentIncomingRelationshipEdges);
5872
+ },
5347
5873
  FindDocuments: (data) => {
5348
5874
  if (data.findDocuments) PHDocumentResultPageDTO.parse(data.findDocuments);
5349
5875
  },
@@ -5377,6 +5903,9 @@ const operationValidators = {
5377
5903
  AddRelationship: (data) => {
5378
5904
  if (data.addRelationship) PHDocumentDTO.parse(data.addRelationship);
5379
5905
  },
5906
+ UpdateRelationship: (data) => {
5907
+ if (data.updateRelationship) PHDocumentDTO.parse(data.updateRelationship);
5908
+ },
5380
5909
  RemoveRelationship: (data) => {
5381
5910
  if (data.removeRelationship) PHDocumentDTO.parse(data.removeRelationship);
5382
5911
  },
@@ -5418,7 +5947,7 @@ function createReactorGraphQLClient(url, fetchImpl = fetch, headers) {
5418
5947
  }
5419
5948
  //#endregion
5420
5949
  //#region src/graphql/reactor/schema.graphql
5421
- 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";
5950
+ 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 @deprecated(reason: \"Ignored. Filter by type or parentId.\")\n}\n\ninput OperationsFilterInput {\n documentIdOrSlug: String\n documentId: String @deprecated(reason: \"Use documentIdOrSlug.\")\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 # The server's current manifest revision\n manifestRevision: String\n # The client's manifest revision as the server holds it\n peerManifestRevision: String\n}\n\ntype SyncHoldReason {\n protocol: String!\n version: Int!\n peerSupports: [Int!]!\n}\n\n# A document held back from a remote whose peer cannot run its versions\ntype SyncHold {\n remoteName: String!\n documentId: String!\n branch: String!\n reason: SyncHoldReason!\n heldAtUtcMs: String!\n}\n\ntype PeerAgreementMember {\n remoteName: String!\n # False for a peer that announced nothing: it gets the baselines\n announced: Boolean!\n protocols: JSONObject!\n features: JSONObject!\n}\n\ntype PeerAgreementLimit {\n protocol: String!\n # The remotes whose peers lack the version new documents would take\n remoteNames: [String!]!\n}\n\ntype PeerAgreement {\n collectionId: String!\n local: JSONObject!\n members: [PeerAgreementMember!]!\n limitedBy: [PeerAgreementLimit!]!\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 SyncRefusalInput {\n documentId: String!\n branch: String!\n # \"marker\" for a refused purge marker; absent for UNSUPPORTED_PROTOCOL\n kind: String\n}\n\ninput TouchChannelInput {\n id: String!\n name: String!\n collectionId: String!\n filter: RemoteFilterInput!\n sinceTimestampUtcMs: String!\n # The client's peer manifest; absent from a client without peer agreement\n manifest: JSONObject\n}\n\ntype TouchChannelResult {\n success: Boolean!\n ackOrdinal: Int!\n # The server's peer manifest\n manifest: JSONObject\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(\n idOrSlug: String\n identifier: String @deprecated(reason: \"Use idOrSlug.\")\n view: ViewFilterInput\n ): DocumentWithChildren\n\n # Get outgoing relationships of a given type from a source document\n documentOutgoingRelationships(\n sourceIdOrSlug: String\n sourceIdentifier: String @deprecated(reason: \"Use sourceIdOrSlug.\")\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 targetIdOrSlug: String\n targetIdentifier: String @deprecated(reason: \"Use targetIdOrSlug.\")\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 sourceIdOrSlug: String\n sourceIdentifier: String @deprecated(reason: \"Use sourceIdOrSlug.\")\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 targetIdOrSlug: String\n targetIdentifier: String @deprecated(reason: \"Use targetIdOrSlug.\")\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 documentIdOrSlug: String\n documentIdentifier: String @deprecated(reason: \"Use documentIdOrSlug.\")\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 # The client's manifest revision; absent from a client without peer agreement\n manifestRevision: String\n # Documents the client refused from earlier polls: held, or a refused marker\n refusals: [SyncRefusalInput!]\n ): PollSyncEnvelopesResult!\n\n # Supreme admin, or the address the named remote's channel is bound to\n syncHolds(remoteName: String, documentId: String): [SyncHold!]!\n\n # Supreme admin, or an address bound to a channel in the collection\n peerAgreement(collectionId: String!): PeerAgreement!\n}\n\ntype Mutation {\n # Create a new document\n createDocument(\n document: JSONObject!\n parentIdOrSlug: String\n parentIdentifier: String @deprecated(reason: \"Use parentIdOrSlug.\")\n ): PHDocument!\n\n # Create an empty document of specified type\n createEmptyDocument(\n documentType: String!\n parentIdOrSlug: String\n parentIdentifier: String @deprecated(reason: \"Use parentIdOrSlug.\")\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 documentIdOrSlug: String\n documentIdentifier: String @deprecated(reason: \"Use documentIdOrSlug.\")\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 documentIdOrSlug: String\n documentIdentifier: String @deprecated(reason: \"Use documentIdOrSlug.\")\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 documentIdOrSlug: String\n documentIdentifier: String @deprecated(reason: \"Use documentIdOrSlug.\")\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 documentIdOrSlug: String\n documentIdentifier: String @deprecated(reason: \"Use documentIdOrSlug.\")\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 sourceIdOrSlug: String\n sourceIdentifier: String @deprecated(reason: \"Use sourceIdOrSlug.\")\n targetIdOrSlug: String\n targetIdentifier: String @deprecated(reason: \"Use targetIdOrSlug.\")\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 sourceIdOrSlug: String\n sourceIdentifier: String @deprecated(reason: \"Use sourceIdOrSlug.\")\n targetIdOrSlug: String\n targetIdentifier: String @deprecated(reason: \"Use targetIdOrSlug.\")\n relationshipType: String!\n metadata: JSONObject\n branch: String\n ): PHDocument!\n\n # Remove a relationship between two documents\n removeRelationship(\n sourceIdOrSlug: String\n sourceIdentifier: String @deprecated(reason: \"Use sourceIdOrSlug.\")\n targetIdOrSlug: String\n targetIdentifier: String @deprecated(reason: \"Use targetIdOrSlug.\")\n relationshipType: String!\n branch: String\n ): PHDocument!\n\n # Move a relationship from one source to another\n moveRelationship(\n sourceParentIdOrSlug: String\n sourceParentIdentifier: String\n @deprecated(reason: \"Use sourceParentIdOrSlug.\")\n targetParentIdOrSlug: String\n targetParentIdentifier: String\n @deprecated(reason: \"Use targetParentIdOrSlug.\")\n targetIdOrSlug: String\n targetIdentifier: String @deprecated(reason: \"Use targetIdOrSlug.\")\n relationshipType: String!\n branch: String\n ): MoveRelationshipResult!\n\n # Delete a single document\n deleteDocument(\n idOrSlug: String\n identifier: String @deprecated(reason: \"Use idOrSlug.\")\n propagate: PropagationMode\n ): Boolean!\n\n # Delete multiple documents\n deleteDocuments(\n idsOrSlugs: [String!]\n identifiers: [String!] @deprecated(reason: \"Use idsOrSlugs.\")\n propagate: PropagationMode\n ): 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(\n envelopes: [SyncEnvelopeInput!]!\n # The server's manifest revision the client gated these envelopes under\n peerManifestRevision: String\n ): 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";
5422
5951
  //#endregion
5423
5952
  //#region src/graphql/reactor/pubsub.ts
5424
5953
  const pubSub = new PubSub();
@@ -5429,26 +5958,79 @@ const SUBSCRIPTION_TRIGGERS = {
5429
5958
  DOCUMENT_CHANGES: "DOCUMENT_CHANGES",
5430
5959
  JOB_CHANGES: "JOB_CHANGES"
5431
5960
  };
5432
- let globalDocumentUnsubscribe = null;
5433
- let documentSubscriberCount = 0;
5434
- function ensureGlobalDocumentSubscription(reactorClient) {
5435
- if (documentSubscriberCount === 0) globalDocumentUnsubscribe = reactorClient.subscribe({}, (event) => {
5436
- const payload = {
5437
- documentChanges: event,
5438
- search: {},
5439
- view: void 0
5961
+ function subjectKey(subject) {
5962
+ return `${subject.address ?? ""}|${subject.key ?? ""}`;
5963
+ }
5964
+ /**
5965
+ * The reactor's change feed, read once per distinct subject and shared by that
5966
+ * subject's GraphQL subscriptions. Each feed is read as its subject, so the
5967
+ * client withholds what that subject may not read; one feed read with no view
5968
+ * would gate every subscriber as the host.
5969
+ */
5970
+ var DocumentChangeFeed = class {
5971
+ reactorClient;
5972
+ #pubSub = new PubSub();
5973
+ #feeds = /* @__PURE__ */ new Map();
5974
+ constructor(reactorClient) {
5975
+ this.reactorClient = reactorClient;
5976
+ }
5977
+ subscribe(subject) {
5978
+ const key = subjectKey(subject);
5979
+ const topic = `${SUBSCRIPTION_TRIGGERS.DOCUMENT_CHANGES}:${key}`;
5980
+ const iterator = this.#pubSub.asyncIterableIterator(topic);
5981
+ this.#acquire(key, topic, subject);
5982
+ let released = false;
5983
+ const release = () => {
5984
+ if (!released) {
5985
+ released = true;
5986
+ this.#release(key);
5987
+ }
5440
5988
  };
5441
- pubSub.publish(SUBSCRIPTION_TRIGGERS.DOCUMENT_CHANGES, payload);
5442
- });
5443
- documentSubscriberCount++;
5444
- return () => {
5445
- documentSubscriberCount--;
5446
- if (documentSubscriberCount === 0 && globalDocumentUnsubscribe) {
5447
- globalDocumentUnsubscribe();
5448
- globalDocumentUnsubscribe = null;
5989
+ return {
5990
+ next: () => iterator.next(),
5991
+ return: () => {
5992
+ release();
5993
+ return iterator.return();
5994
+ },
5995
+ throw: (error) => {
5996
+ release();
5997
+ return iterator.throw(error);
5998
+ },
5999
+ [Symbol.asyncIterator]() {
6000
+ return this;
6001
+ }
6002
+ };
6003
+ }
6004
+ #acquire(key, topic, subject) {
6005
+ const feed = this.#feeds.get(key);
6006
+ if (feed) {
6007
+ feed.subscribers++;
6008
+ return;
5449
6009
  }
5450
- };
5451
- }
6010
+ const view = { subject };
6011
+ const unsubscribe = this.reactorClient.subscribe({}, (event) => {
6012
+ const payload = {
6013
+ documentChanges: event,
6014
+ search: {},
6015
+ view
6016
+ };
6017
+ this.#pubSub.publish(topic, payload);
6018
+ }, view);
6019
+ this.#feeds.set(key, {
6020
+ unsubscribe,
6021
+ subscribers: 1
6022
+ });
6023
+ }
6024
+ #release(key) {
6025
+ const feed = this.#feeds.get(key);
6026
+ if (!feed) return;
6027
+ feed.subscribers--;
6028
+ if (feed.subscribers === 0) {
6029
+ this.#feeds.delete(key);
6030
+ feed.unsubscribe();
6031
+ }
6032
+ }
6033
+ };
5452
6034
  const activeJobSubscriptions = /* @__PURE__ */ new Map();
5453
6035
  function ensureJobSubscription(reactorClient, jobId) {
5454
6036
  let subscription = activeJobSubscriptions.get(jobId);
@@ -5465,7 +6047,7 @@ function ensureJobSubscription(reactorClient, jobId) {
5465
6047
  createdAt: jobInfo.createdAtUtcIso,
5466
6048
  completedAt: jobInfo.completedAtUtcIso ?? null,
5467
6049
  error: jobInfo.error?.message ?? null,
5468
- result: jobInfo.result ?? {}
6050
+ result: jobInfo.result ?? null
5469
6051
  },
5470
6052
  jobId,
5471
6053
  documentId: jobInfo.documentId
@@ -5513,6 +6095,11 @@ var ReactorSubgraph = class extends BaseSubgraph {
5513
6095
  }
5514
6096
  name = "r";
5515
6097
  hasSubscriptions = true;
6098
+ #documentChanges;
6099
+ get documentChanges() {
6100
+ this.#documentChanges ??= new DocumentChangeFeed(this.reactorClient);
6101
+ return this.#documentChanges;
6102
+ }
5516
6103
  /**
5517
6104
  * Check operation-level permissions for an array of actions.
5518
6105
  * Delegates to base assertCanExecuteOperation for each action.
@@ -5543,6 +6130,38 @@ var ReactorSubgraph = class extends BaseSubgraph {
5543
6130
  }
5544
6131
  }
5545
6132
  /**
6133
+ * Drops the relationship edges whose far-end document the caller cannot read.
6134
+ *
6135
+ * An edge names two documents, so returning it discloses the far end's id. The
6136
+ * document-shaped relationship queries already withhold far-end documents the
6137
+ * caller cannot read; the edge-shaped queries have to withhold the same ones,
6138
+ * or they become a way to enumerate around that check. Supreme admins read
6139
+ * everything, so the per-edge check is skipped for them.
6140
+ *
6141
+ * The cursor describes where to resume in the underlying unfiltered stream,
6142
+ * so it is left as it came. A filtered page is therefore often shorter than
6143
+ * the requested limit, and that is the stream continuing rather than a sign
6144
+ * it ended.
6145
+ */
6146
+ async filterRelationshipEdges(page, farEnd, ctx) {
6147
+ if (this.authorizationService.isSupremeAdmin(ctx.user?.address)) return page;
6148
+ const decisions = /* @__PURE__ */ new Map();
6149
+ const items = [];
6150
+ for (const edge of page.items) {
6151
+ const documentId = edge[farEnd];
6152
+ let canRead = decisions.get(documentId);
6153
+ if (canRead === void 0) {
6154
+ canRead = await this.canReadDocument(documentId, ctx);
6155
+ decisions.set(documentId, canRead);
6156
+ }
6157
+ if (canRead) items.push(edge);
6158
+ }
6159
+ return {
6160
+ ...page,
6161
+ items
6162
+ };
6163
+ }
6164
+ /**
5546
6165
  * Adds to `forbidden` the canonical document ids in `syncOps` that the caller
5547
6166
  * cannot read, checking each distinct id once. Sync operation document ids are
5548
6167
  * canonical (never slugs), so no resolution is needed.
@@ -5575,9 +6194,9 @@ var ReactorSubgraph = class extends BaseSubgraph {
5575
6194
  * reports the liveness that keeps it from being reclaimed -- but they never
5576
6195
  * adopt, since the push path has no drive check to clear the claimant.
5577
6196
  *
5578
- * Nothing is enforced or adopted without a serving gate. Below
5579
- * `authEnforcement` there is no policy being enforced for the channel to
5580
- * belong to, and refusing a poll there would break sync for no gain.
6197
+ * Nothing is enforced or adopted without a serving gate: with no gate the
6198
+ * channel serves every subject alike, so there is no subject for it to
6199
+ * belong to.
5581
6200
  */
5582
6201
  async #bindOrRefuseChannel(channelId, ctx, options = {}) {
5583
6202
  if (!this.syncServingGate) return;
@@ -5607,7 +6226,7 @@ var ReactorSubgraph = class extends BaseSubgraph {
5607
6226
  await this.assertCanReadCanonical(parent.id, ctx);
5608
6227
  try {
5609
6228
  const filter = {
5610
- documentId: parent.id,
6229
+ documentIdOrSlug: parent.id,
5611
6230
  branch: args.filter?.branch,
5612
6231
  scopes: args.filter?.scopes,
5613
6232
  actionTypes: args.filter?.actionTypes,
@@ -5637,10 +6256,11 @@ var ReactorSubgraph = class extends BaseSubgraph {
5637
6256
  document: async (_parent, args, ctx) => {
5638
6257
  this.logger.debug("document(@args)", args);
5639
6258
  try {
5640
- const handle = await this.assertCanRead(args.identifier, ctx);
6259
+ const idOrSlug = requireOneOf(args, "idOrSlug", "identifier");
6260
+ const handle = await this.assertCanRead(idOrSlug, ctx);
5641
6261
  return await document(this.reactorClient, {
5642
- ...args,
5643
- identifier: handle.fetchIdentifier
6262
+ idOrSlug: handle.fetchIdentifier,
6263
+ view: args.view
5644
6264
  }, this.viewSubject(ctx));
5645
6265
  } catch (error) {
5646
6266
  this.logger.error("Error in document: @Error", error);
@@ -5650,10 +6270,13 @@ var ReactorSubgraph = class extends BaseSubgraph {
5650
6270
  documentOutgoingRelationships: async (_parent, args, ctx) => {
5651
6271
  this.logger.debug("documentOutgoingRelationships(@args)", args);
5652
6272
  try {
5653
- const handle = await this.assertCanRead(args.sourceIdentifier, ctx);
6273
+ const sourceIdOrSlug = requireOneOf(args, "sourceIdOrSlug", "sourceIdentifier");
6274
+ const handle = await this.assertCanRead(sourceIdOrSlug, ctx);
5654
6275
  return await documentOutgoingRelationships(this.reactorClient, {
5655
- ...args,
5656
- sourceIdentifier: handle.fetchIdentifier
6276
+ sourceIdOrSlug: handle.fetchIdentifier,
6277
+ relationshipType: args.relationshipType,
6278
+ view: args.view,
6279
+ paging: args.paging
5657
6280
  }, this.viewSubject(ctx));
5658
6281
  } catch (error) {
5659
6282
  this.logger.error("Error in documentOutgoingRelationships: @Error", error);
@@ -5663,10 +6286,13 @@ var ReactorSubgraph = class extends BaseSubgraph {
5663
6286
  documentIncomingRelationships: async (_parent, args, ctx) => {
5664
6287
  this.logger.debug("documentIncomingRelationships(@args)", args);
5665
6288
  try {
5666
- const handle = await this.assertCanRead(args.targetIdentifier, ctx);
6289
+ const targetIdOrSlug = requireOneOf(args, "targetIdOrSlug", "targetIdentifier");
6290
+ const handle = await this.assertCanRead(targetIdOrSlug, ctx);
5667
6291
  const result = await documentIncomingRelationships(this.reactorClient, {
5668
- ...args,
5669
- targetIdentifier: handle.fetchIdentifier
6292
+ targetIdOrSlug: handle.fetchIdentifier,
6293
+ relationshipType: args.relationshipType,
6294
+ view: args.view,
6295
+ paging: args.paging
5670
6296
  }, this.viewSubject(ctx));
5671
6297
  if (!this.authorizationService.isSupremeAdmin(ctx.user?.address)) {
5672
6298
  const filteredItems = [];
@@ -5682,6 +6308,40 @@ var ReactorSubgraph = class extends BaseSubgraph {
5682
6308
  throw error;
5683
6309
  }
5684
6310
  },
6311
+ documentOutgoingRelationshipEdges: async (_parent, args, ctx) => {
6312
+ this.logger.debug("documentOutgoingRelationshipEdges(@args)", args);
6313
+ try {
6314
+ const sourceIdOrSlug = requireOneOf(args, "sourceIdOrSlug", "sourceIdentifier");
6315
+ const handle = await this.assertCanRead(sourceIdOrSlug, ctx);
6316
+ const result = await documentOutgoingRelationshipEdges(this.reactorClient, {
6317
+ sourceIdOrSlug: handle.fetchIdentifier,
6318
+ relationshipType: args.relationshipType,
6319
+ view: args.view,
6320
+ paging: args.paging
6321
+ }, this.viewSubject(ctx));
6322
+ return await this.filterRelationshipEdges(result, "targetId", ctx);
6323
+ } catch (error) {
6324
+ this.logger.error("Error in documentOutgoingRelationshipEdges: @Error", error);
6325
+ throw error;
6326
+ }
6327
+ },
6328
+ documentIncomingRelationshipEdges: async (_parent, args, ctx) => {
6329
+ this.logger.debug("documentIncomingRelationshipEdges(@args)", args);
6330
+ try {
6331
+ const targetIdOrSlug = requireOneOf(args, "targetIdOrSlug", "targetIdentifier");
6332
+ const handle = await this.assertCanRead(targetIdOrSlug, ctx);
6333
+ const result = await documentIncomingRelationshipEdges(this.reactorClient, {
6334
+ targetIdOrSlug: handle.fetchIdentifier,
6335
+ relationshipType: args.relationshipType,
6336
+ view: args.view,
6337
+ paging: args.paging
6338
+ }, this.viewSubject(ctx));
6339
+ return await this.filterRelationshipEdges(result, "sourceId", ctx);
6340
+ } catch (error) {
6341
+ this.logger.error("Error in documentIncomingRelationshipEdges: @Error", error);
6342
+ throw error;
6343
+ }
6344
+ },
5685
6345
  findDocuments: async (_parent, args, ctx) => {
5686
6346
  this.logger.debug("findDocuments(@args)", args);
5687
6347
  try {
@@ -5703,10 +6363,10 @@ var ReactorSubgraph = class extends BaseSubgraph {
5703
6363
  throw error;
5704
6364
  }
5705
6365
  },
5706
- jobStatus: async (_parent, args) => {
6366
+ jobStatus: async (_parent, args, ctx) => {
5707
6367
  this.logger.debug("jobStatus(@args)", args);
5708
6368
  try {
5709
- return await jobStatus(this.reactorClient, args);
6369
+ return await jobStatus(this.reactorClient, args, (id) => this.servesDocument(id, ctx));
5710
6370
  } catch (error) {
5711
6371
  this.logger.error("Error in jobStatus: @Error", error);
5712
6372
  throw error;
@@ -5715,32 +6375,63 @@ var ReactorSubgraph = class extends BaseSubgraph {
5715
6375
  documentOperations: async (_parent, args, ctx) => {
5716
6376
  this.logger.debug("documentOperations(@args)", args);
5717
6377
  try {
5718
- const handle = await this.assertCanRead(args.filter.documentId, ctx);
6378
+ const { documentIdOrSlug, documentId, ...filter } = args.filter;
6379
+ const handle = await this.assertCanRead(requireOneOf({
6380
+ documentIdOrSlug,
6381
+ documentId
6382
+ }, "documentIdOrSlug", "documentId"), ctx);
5719
6383
  return await documentOperations(this.reactorClient, {
5720
- ...args,
5721
6384
  filter: {
5722
- ...args.filter,
5723
- documentId: handle.fetchIdentifier
5724
- }
6385
+ ...filter,
6386
+ documentIdOrSlug: handle.fetchIdentifier
6387
+ },
6388
+ paging: args.paging
5725
6389
  }, this.viewSubject(ctx));
5726
6390
  } catch (error) {
5727
6391
  this.logger.error("Error in documentOperations: @Error", error);
5728
6392
  throw error;
5729
6393
  }
5730
6394
  },
6395
+ /**
6396
+ * Gated on reading the document, the same gate every other read here
6397
+ * carries: a verdict is derived from the document's policy, so a caller
6398
+ * who may not read the document may not preflight against it either.
6399
+ * That gate decides whether the query runs; the answer comes entirely
6400
+ * from the reactor's decision model.
6401
+ */
5731
6402
  evaluateActions: async (_parent, args, ctx) => {
5732
6403
  this.logger.debug("evaluateActions(@args)", args);
5733
6404
  try {
5734
- const handle = await this.assertCanRead(args.documentIdentifier, ctx);
6405
+ const documentIdOrSlug = requireOneOf(args, "documentIdOrSlug", "documentIdentifier");
6406
+ const handle = await this.assertCanRead(documentIdOrSlug, ctx);
5735
6407
  return await evaluateActions(this.reactorClient, {
5736
- ...args,
5737
- documentIdentifier: handle.fetchIdentifier
6408
+ documentIdOrSlug: handle.fetchIdentifier,
6409
+ branch: args.branch,
6410
+ candidates: args.candidates
5738
6411
  }, this.viewSubject(ctx));
5739
6412
  } catch (error) {
5740
6413
  this.logger.error("Error in evaluateActions: @Error", error);
5741
6414
  throw error;
5742
6415
  }
5743
6416
  },
6417
+ syncHolds: async (_parent, args, ctx) => {
6418
+ const address = ctx.user?.address;
6419
+ if (!this.authorizationService.isSupremeAdmin(address)) {
6420
+ let bound;
6421
+ try {
6422
+ bound = args.remoteName ? this.syncManager.getByName(args.remoteName).meta.options.boundAddress : void 0;
6423
+ } catch {
6424
+ bound = void 0;
6425
+ }
6426
+ if (address === void 0 || bound !== address) throw new ForbiddenError("to list sync holds");
6427
+ }
6428
+ return syncHolds(this.syncManager, args);
6429
+ },
6430
+ peerAgreement: (_parent, args, ctx) => {
6431
+ const address = ctx.user?.address;
6432
+ if (!this.authorizationService.isSupremeAdmin(address) && (address === void 0 || !this.syncManager.list().some((remote) => remote.meta.collectionId.key === args.collectionId && remote.meta.options.boundAddress === address))) throw new ForbiddenError("to read peer agreement");
6433
+ return peerAgreement(this.syncManager, args);
6434
+ },
5744
6435
  pollSyncEnvelopes: async (_parent, args, ctx) => {
5745
6436
  this.logger.debug("pollSyncEnvelopes(@args)", args);
5746
6437
  try {
@@ -5754,19 +6445,18 @@ var ReactorSubgraph = class extends BaseSubgraph {
5754
6445
  const isAdmin = this.authorizationService.isSupremeAdmin(ctx.user?.address);
5755
6446
  if (!isAdmin) await this.assertCanReadCanonical(driveId, ctx);
5756
6447
  await this.#bindOrRefuseChannel(args.channelId, ctx);
6448
+ holdPollRefusals(this.syncManager, args.channelId, args.refusals);
6449
+ await recordPollMarkerRefusals(this.syncManager, args.channelId, args.refusals);
6450
+ await silenceUnversionedPoll(this.syncManager, args.channelId, args.manifestRevision);
5757
6451
  const forbiddenIds = /* @__PURE__ */ new Set();
5758
6452
  if (!isAdmin) {
5759
6453
  if (await this.authorizationService.canRead(driveId, void 0)) await this.#collectForbiddenDocuments(remote.channel.outbox.items, forbiddenIds, ctx);
5760
6454
  await this.#collectForbiddenDocuments(remote.channel.deadLetter.items, forbiddenIds, ctx);
5761
6455
  }
5762
- const heldOpIds = this.syncServingGate ? await collectHeldSyncOperations([...remote.channel.outbox.items, ...remote.channel.deadLetter.items], this.syncServingGate, this.viewSubject(ctx)) : /* @__PURE__ */ new Set();
5763
- const { envelopes, ackOrdinal, deadLetters, hasMore } = pollSyncEnvelopes(this.syncManager, args, forbiddenIds, heldOpIds);
5764
- return {
5765
- envelopes,
5766
- ackOrdinal,
5767
- deadLetters,
5768
- hasMore
5769
- };
6456
+ const gate = this.syncServingGate;
6457
+ const gated = gate ? [...remote.channel.outbox.items, ...remote.channel.deadLetter.items] : void 0;
6458
+ const heldOpIds = gate && gated ? await collectHeldSyncOperations(gated, gate, this.viewSubject(ctx)) : /* @__PURE__ */ new Set();
6459
+ return pollSyncEnvelopes(this.syncManager, args, forbiddenIds, heldOpIds, gated && new Set(gated.map((syncOp) => syncOp.id)));
5770
6460
  } catch (error) {
5771
6461
  this.logger.error("Error in pollSyncEnvelopes(@args): @Error", args, error);
5772
6462
  throw error;
@@ -5777,11 +6467,15 @@ var ReactorSubgraph = class extends BaseSubgraph {
5777
6467
  createDocument: async (_parent, args, ctx) => {
5778
6468
  this.logger.debug("createDocument(@args)", args);
5779
6469
  try {
5780
- if (args.parentIdentifier) {
5781
- const parent = await document(this.reactorClient, { identifier: args.parentIdentifier });
6470
+ const parentIdOrSlug = optionalOneOf(args, "parentIdOrSlug", "parentIdentifier");
6471
+ if (parentIdOrSlug) {
6472
+ const parent = await document(this.reactorClient, { idOrSlug: parentIdOrSlug });
5782
6473
  await this.assertCanWriteCanonical(parent.document.id, ctx);
5783
6474
  } else this.assertCanCreate(ctx);
5784
- const result = await createDocument(this.reactorClient, args, this.graphqlManager.reactorDriveClient);
6475
+ const result = await createDocument(this.reactorClient, {
6476
+ document: args.document,
6477
+ parentIdOrSlug
6478
+ }, this.graphqlManager.reactorDriveClient, this.viewSubject(ctx));
5785
6479
  if (result?.id && isDriveContainerType(result.documentType)) this.graphqlManager.driveOwnershipCache.add(result.id);
5786
6480
  if (ctx.user?.address && result?.id) await this.documentPermissionService?.initializeDocumentProtection(result.id, ctx.user.address, this.authorizationService.config.defaultProtection);
5787
6481
  return result;
@@ -5793,11 +6487,15 @@ var ReactorSubgraph = class extends BaseSubgraph {
5793
6487
  createEmptyDocument: async (_parent, args, ctx) => {
5794
6488
  this.logger.debug("createEmptyDocument(@args)", args);
5795
6489
  try {
5796
- if (args.parentIdentifier) {
5797
- const parent = await document(this.reactorClient, { identifier: args.parentIdentifier });
6490
+ const parentIdOrSlug = optionalOneOf(args, "parentIdOrSlug", "parentIdentifier");
6491
+ if (parentIdOrSlug) {
6492
+ const parent = await document(this.reactorClient, { idOrSlug: parentIdOrSlug });
5798
6493
  await this.assertCanWriteCanonical(parent.document.id, ctx);
5799
6494
  } else this.assertCanCreate(ctx);
5800
- const result = await createEmptyDocument(this.reactorClient, args, this.graphqlManager.reactorDriveClient);
6495
+ const result = await createEmptyDocument(this.reactorClient, {
6496
+ documentType: args.documentType,
6497
+ parentIdOrSlug
6498
+ }, this.graphqlManager.reactorDriveClient, this.viewSubject(ctx));
5801
6499
  if (result?.id && isDriveContainerType(result.documentType)) this.graphqlManager.driveOwnershipCache.add(result.id);
5802
6500
  if (ctx.user?.address && result?.id) await this.documentPermissionService?.initializeDocumentProtection(result.id, ctx.user.address, this.authorizationService.config.defaultProtection);
5803
6501
  return result;
@@ -5809,11 +6507,13 @@ var ReactorSubgraph = class extends BaseSubgraph {
5809
6507
  execute: async (_parent, args, ctx) => {
5810
6508
  this.logger.debug("execute(@args)", args);
5811
6509
  try {
5812
- const handle = await this.assertCanExecuteOperations(args.documentIdentifier, args.actions, ctx);
6510
+ const documentIdOrSlug = requireOneOf(args, "documentIdOrSlug", "documentIdentifier");
6511
+ const handle = await this.assertCanExecuteOperations(documentIdOrSlug, args.actions, ctx);
5813
6512
  return await execute(this.reactorClient, {
5814
- ...args,
5815
- documentIdentifier: handle.fetchIdentifier
5816
- });
6513
+ documentIdOrSlug: handle.fetchIdentifier,
6514
+ actions: args.actions,
6515
+ branch: args.branch
6516
+ }, this.viewSubject(ctx));
5817
6517
  } catch (error) {
5818
6518
  this.logger.error("Error in execute(@args): @Error", args, error);
5819
6519
  throw error;
@@ -5822,10 +6522,12 @@ var ReactorSubgraph = class extends BaseSubgraph {
5822
6522
  executeAsync: async (_parent, args, ctx) => {
5823
6523
  this.logger.debug("executeAsync(@args)", args);
5824
6524
  try {
5825
- const handle = await this.assertCanExecuteOperations(args.documentIdentifier, args.actions, ctx);
6525
+ const documentIdOrSlug = requireOneOf(args, "documentIdOrSlug", "documentIdentifier");
6526
+ const handle = await this.assertCanExecuteOperations(documentIdOrSlug, args.actions, ctx);
5826
6527
  return await executeAsync(this.reactorClient, {
5827
- ...args,
5828
- documentIdentifier: handle.fetchIdentifier
6528
+ documentIdOrSlug: handle.fetchIdentifier,
6529
+ actions: args.actions,
6530
+ branch: args.branch
5829
6531
  });
5830
6532
  } catch (error) {
5831
6533
  this.logger.error("Error in executeAsync(@args): @Error", args, error);
@@ -5839,7 +6541,7 @@ var ReactorSubgraph = class extends BaseSubgraph {
5839
6541
  return await mutateDocument(this.reactorClient, {
5840
6542
  ...args,
5841
6543
  documentIdentifier: handle.fetchIdentifier
5842
- });
6544
+ }, this.viewSubject(ctx));
5843
6545
  } catch (error) {
5844
6546
  this.logger.error("Error in mutateDocument(@args): @Error", args, error);
5845
6547
  throw error;
@@ -5861,11 +6563,13 @@ var ReactorSubgraph = class extends BaseSubgraph {
5861
6563
  renameDocument: async (_parent, args, ctx) => {
5862
6564
  this.logger.debug("renameDocument(@args)", args);
5863
6565
  try {
5864
- const handle = await this.assertCanWrite(args.documentIdentifier, ctx);
6566
+ const documentIdOrSlug = requireOneOf(args, "documentIdOrSlug", "documentIdentifier");
6567
+ const handle = await this.assertCanWrite(documentIdOrSlug, ctx);
5865
6568
  return await renameDocument(this.reactorClient, {
5866
- ...args,
5867
- documentIdentifier: handle.fetchIdentifier
5868
- });
6569
+ documentIdOrSlug: handle.fetchIdentifier,
6570
+ name: args.name,
6571
+ branch: args.branch
6572
+ }, void 0, this.viewSubject(ctx));
5869
6573
  } catch (error) {
5870
6574
  this.logger.error("Error in renameDocument(@args): @Error", args, error);
5871
6575
  throw error;
@@ -5874,11 +6578,13 @@ var ReactorSubgraph = class extends BaseSubgraph {
5874
6578
  setPreferredEditor: async (_parent, args, ctx) => {
5875
6579
  this.logger.debug("setPreferredEditor(@args)", args);
5876
6580
  try {
5877
- const handle = await this.assertCanWrite(args.documentIdentifier, ctx);
6581
+ const documentIdOrSlug = requireOneOf(args, "documentIdOrSlug", "documentIdentifier");
6582
+ const handle = await this.assertCanWrite(documentIdOrSlug, ctx);
5878
6583
  return await setPreferredEditor(this.reactorClient, {
5879
- ...args,
5880
- documentIdentifier: handle.fetchIdentifier
5881
- });
6584
+ documentIdOrSlug: handle.fetchIdentifier,
6585
+ preferredEditor: args.preferredEditor,
6586
+ branch: args.branch
6587
+ }, void 0, this.viewSubject(ctx));
5882
6588
  } catch (error) {
5883
6589
  this.logger.error("Error in setPreferredEditor(@args): @Error", args, error);
5884
6590
  throw error;
@@ -5887,24 +6593,51 @@ var ReactorSubgraph = class extends BaseSubgraph {
5887
6593
  addRelationship: async (_parent, args, ctx) => {
5888
6594
  this.logger.debug("addRelationship(@args)", args);
5889
6595
  try {
5890
- const handle = await this.assertCanWrite(args.sourceIdentifier, ctx);
6596
+ const sourceIdOrSlug = requireOneOf(args, "sourceIdOrSlug", "sourceIdentifier");
6597
+ const targetIdOrSlug = requireOneOf(args, "targetIdOrSlug", "targetIdentifier");
6598
+ const handle = await this.assertCanWrite(sourceIdOrSlug, ctx);
5891
6599
  return await addRelationship(this.reactorClient, {
5892
- ...args,
5893
- sourceIdentifier: handle.fetchIdentifier
5894
- });
6600
+ sourceIdOrSlug: handle.fetchIdentifier,
6601
+ targetIdOrSlug,
6602
+ relationshipType: args.relationshipType,
6603
+ metadata: args.metadata,
6604
+ branch: args.branch
6605
+ }, this.viewSubject(ctx));
5895
6606
  } catch (error) {
5896
6607
  this.logger.error("Error in addRelationship(@args): @Error", args, error);
5897
6608
  throw error;
5898
6609
  }
5899
6610
  },
6611
+ updateRelationship: async (_parent, args, ctx) => {
6612
+ this.logger.debug("updateRelationship(@args)", args);
6613
+ try {
6614
+ const sourceIdOrSlug = requireOneOf(args, "sourceIdOrSlug", "sourceIdentifier");
6615
+ const targetIdOrSlug = requireOneOf(args, "targetIdOrSlug", "targetIdentifier");
6616
+ const handle = await this.assertCanWrite(sourceIdOrSlug, ctx);
6617
+ return await updateRelationship(this.reactorClient, {
6618
+ sourceIdOrSlug: handle.fetchIdentifier,
6619
+ targetIdOrSlug,
6620
+ relationshipType: args.relationshipType,
6621
+ metadata: args.metadata,
6622
+ branch: args.branch
6623
+ }, this.viewSubject(ctx));
6624
+ } catch (error) {
6625
+ this.logger.error("Error in updateRelationship(@args): @Error", args, error);
6626
+ throw error;
6627
+ }
6628
+ },
5900
6629
  removeRelationship: async (_parent, args, ctx) => {
5901
6630
  this.logger.debug("removeRelationship(@args)", args);
5902
6631
  try {
5903
- const handle = await this.assertCanWrite(args.sourceIdentifier, ctx);
6632
+ const sourceIdOrSlug = requireOneOf(args, "sourceIdOrSlug", "sourceIdentifier");
6633
+ const targetIdOrSlug = requireOneOf(args, "targetIdOrSlug", "targetIdentifier");
6634
+ const handle = await this.assertCanWrite(sourceIdOrSlug, ctx);
5904
6635
  return await removeRelationship(this.reactorClient, {
5905
- ...args,
5906
- sourceIdentifier: handle.fetchIdentifier
5907
- });
6636
+ sourceIdOrSlug: handle.fetchIdentifier,
6637
+ targetIdOrSlug,
6638
+ relationshipType: args.relationshipType,
6639
+ branch: args.branch
6640
+ }, this.viewSubject(ctx));
5908
6641
  } catch (error) {
5909
6642
  this.logger.error("Error in removeRelationship(@args): @Error", args, error);
5910
6643
  throw error;
@@ -5913,26 +6646,31 @@ var ReactorSubgraph = class extends BaseSubgraph {
5913
6646
  moveRelationship: async (_parent, args, ctx) => {
5914
6647
  this.logger.debug("moveRelationship(@args)", args);
5915
6648
  try {
5916
- const sourceHandle = await this.assertCanWrite(args.sourceParentIdentifier, ctx);
5917
- const targetHandle = await this.assertCanWrite(args.targetParentIdentifier, ctx);
6649
+ const sourceParentIdOrSlug = requireOneOf(args, "sourceParentIdOrSlug", "sourceParentIdentifier");
6650
+ const targetParentIdOrSlug = requireOneOf(args, "targetParentIdOrSlug", "targetParentIdentifier");
6651
+ const targetIdOrSlug = requireOneOf(args, "targetIdOrSlug", "targetIdentifier");
6652
+ const sourceHandle = await this.assertCanWrite(sourceParentIdOrSlug, ctx);
6653
+ const targetHandle = await this.assertCanWrite(targetParentIdOrSlug, ctx);
5918
6654
  return await moveRelationship(this.reactorClient, {
5919
- ...args,
5920
- sourceParentIdentifier: sourceHandle.fetchIdentifier,
5921
- targetParentIdentifier: targetHandle.fetchIdentifier
5922
- });
6655
+ sourceParentIdOrSlug: sourceHandle.fetchIdentifier,
6656
+ targetParentIdOrSlug: targetHandle.fetchIdentifier,
6657
+ targetIdOrSlug,
6658
+ relationshipType: args.relationshipType,
6659
+ branch: args.branch
6660
+ }, this.viewSubject(ctx));
5923
6661
  } catch (error) {
5924
- this.logger.error("Error in moveRelationship(@args): @Error @args", error, args);
6662
+ this.logger.error("Error in moveRelationship(@args): @Error", args, error);
5925
6663
  throw error;
5926
6664
  }
5927
6665
  },
5928
6666
  deleteDocument: async (_parent, args, ctx) => {
5929
6667
  this.logger.debug("deleteDocument(@args)", args);
5930
6668
  try {
5931
- const identifier = (await this.assertCanWrite(args.identifier, ctx)).fetchIdentifier;
5932
- const driveIdToInvalidate = await this.#resolveDriveId(identifier);
6669
+ const idOrSlug = (await this.assertCanWrite(requireOneOf(args, "idOrSlug", "identifier"), ctx)).fetchIdentifier;
6670
+ const driveIdToInvalidate = await this.#resolveDriveId(idOrSlug);
5933
6671
  const result = await deleteDocument(this.reactorClient, {
5934
- ...args,
5935
- identifier
6672
+ idOrSlug,
6673
+ propagate: args.propagate
5936
6674
  }, this.graphqlManager.reactorDriveClient);
5937
6675
  if (result && driveIdToInvalidate) this.graphqlManager.driveOwnershipCache.remove(driveIdToInvalidate);
5938
6676
  return result;
@@ -5944,14 +6682,14 @@ var ReactorSubgraph = class extends BaseSubgraph {
5944
6682
  deleteDocuments: async (_parent, args, ctx) => {
5945
6683
  this.logger.debug("deleteDocuments(@args)", args);
5946
6684
  try {
5947
- const identifiers = [];
5948
- for (const identifier of args.identifiers) {
5949
- const handle = await this.assertCanWrite(identifier, ctx);
5950
- identifiers.push(handle.fetchIdentifier);
6685
+ const idsOrSlugs = [];
6686
+ for (const idOrSlug of requireOneOf(args, "idsOrSlugs", "identifiers")) {
6687
+ const handle = await this.assertCanWrite(idOrSlug, ctx);
6688
+ idsOrSlugs.push(handle.fetchIdentifier);
5951
6689
  }
5952
6690
  return await deleteDocuments(this.reactorClient, {
5953
- ...args,
5954
- identifiers
6691
+ idsOrSlugs,
6692
+ propagate: args.propagate
5955
6693
  });
5956
6694
  } catch (error) {
5957
6695
  this.logger.error("Error in deleteDocuments(@args): @Error", args, error);
@@ -6030,10 +6768,7 @@ var ReactorSubgraph = class extends BaseSubgraph {
6030
6768
  documentChanges: {
6031
6769
  subscribe: (rootValue, args, ctx, info) => {
6032
6770
  this.logger.debug("documentChanges subscription started");
6033
- return withFilter(() => {
6034
- ensureGlobalDocumentSubscription(this.reactorClient);
6035
- return getPubSub().asyncIterableIterator(SUBSCRIPTION_TRIGGERS.DOCUMENT_CHANGES);
6036
- }, async (payload, filterArgs, filterCtx) => {
6771
+ return withFilter(() => this.documentChanges.subscribe(this.viewSubject(ctx)), async (payload, filterArgs, filterCtx) => {
6037
6772
  if (!payload) return false;
6038
6773
  const search = {
6039
6774
  type: filterArgs?.search?.type ?? void 0,
@@ -6060,6 +6795,7 @@ var ReactorSubgraph = class extends BaseSubgraph {
6060
6795
  }, async (payload, filterArgs, filterCtx) => {
6061
6796
  if (!payload || !filterArgs) return false;
6062
6797
  if (!matchesJobFilter(payload, filterArgs)) return false;
6798
+ if (!await this.servesDocument(payload.documentId, filterCtx)) return false;
6063
6799
  if (this.authorizationService.isSupremeAdmin(filterCtx?.user?.address)) return true;
6064
6800
  if (!payload.documentId) return false;
6065
6801
  return this.canReadDocument(payload.documentId, filterCtx);
@@ -6088,10 +6824,10 @@ const ADMIN_USERS = getAdminUsers();
6088
6824
  //#endregion
6089
6825
  //#region src/graphql/system/version.ts
6090
6826
  function getVersion() {
6091
- return "6.2.3-dev.4";
6827
+ return "6.2.3-dev.40";
6092
6828
  }
6093
6829
  function getGitHash() {
6094
- return "e30f072e1e13237f4a9ce0f7c5ca18700cab7845";
6830
+ return "9a25fe4dcac53275b3e4e818fa6259f56a2532bb";
6095
6831
  }
6096
6832
  function getGitUrl() {
6097
6833
  return buildTreeUrl(getGitHash());
@@ -6120,6 +6856,10 @@ var SystemSubgraph = class extends BaseSubgraph {
6120
6856
  };
6121
6857
  //#endregion
6122
6858
  //#region src/packages/http-loader.ts
6859
+ const EXACT_VERSION = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
6860
+ function piecesBaseUrl(registryUrl, packageName, version) {
6861
+ return `${registryUrl.endsWith("/") ? registryUrl : `${registryUrl}/`}-/cdn/${packageName}@${version}/node/pieces/`;
6862
+ }
6123
6863
  /**
6124
6864
  * Extract subgraph classes from the imported subgraphs/index.mjs module shape.
6125
6865
  *
@@ -6192,7 +6932,8 @@ var HttpPackageLoader = class {
6192
6932
  async loadUpgradeManifests(packageSpec) {
6193
6933
  const { name: packageName } = this.parsePackageSpec(packageSpec);
6194
6934
  if (!this.isValidPackageName(packageName)) throw new Error(`Invalid package name: ${packageName}`);
6195
- const manifests = extractUpgradeManifests(await import(`${this.registryUrl}-/cdn/${packageSpec}/node/document-models/index.mjs`));
6935
+ const module = await import(`${this.registryUrl}-/cdn/${packageSpec}/node/document-models/index.mjs`);
6936
+ const manifests = extractUpgradeManifests(module);
6196
6937
  if (manifests.length > 0) this.logger.verbose(`Loaded ${manifests.length} upgrade manifests from ${packageName}`);
6197
6938
  return manifests;
6198
6939
  }
@@ -6218,6 +6959,27 @@ var HttpPackageLoader = class {
6218
6959
  this.logger.verbose(`No processor factory found in ${packageName}`);
6219
6960
  return null;
6220
6961
  }
6962
+ async loadPieces(packageSpec) {
6963
+ const { name: packageName } = this.parsePackageSpec(packageSpec);
6964
+ if (!this.isValidPackageName(packageName)) throw new Error(`Invalid package name: ${packageName}`);
6965
+ const version = await this.packageVersion(packageSpec);
6966
+ if (!version) {
6967
+ this.logger.verbose(`No package version found for: ${packageSpec}`);
6968
+ return [];
6969
+ }
6970
+ const base = piecesBaseUrl(this.registryUrl, packageName, version);
6971
+ this.logger.verbose(`Importing pieces from: ${base}index.mjs`);
6972
+ let module;
6973
+ try {
6974
+ module = await import(`${base}index.mjs`);
6975
+ } catch (error) {
6976
+ this.logger.verbose("No pieces found for @package: @error", packageName, error);
6977
+ return [];
6978
+ }
6979
+ const pieces = piecesFromCdnList(module, base, packageName, this.logger, version);
6980
+ this.logger.verbose(`Loaded ${pieces.length} pieces from ${packageName}`);
6981
+ return pieces;
6982
+ }
6221
6983
  /**
6222
6984
  * Load document models from multiple packages.
6223
6985
  * Continues loading even if some packages fail.
@@ -6234,13 +6996,23 @@ var HttpPackageLoader = class {
6234
6996
  logger?.info(logMsg);
6235
6997
  this.logger.info(logMsg);
6236
6998
  } catch (error) {
6237
- const errMsg = `Failed to load package ${trimmedName}`;
6238
- logger?.error(errMsg, error);
6239
- this.logger.error(errMsg, error);
6999
+ logger?.error(`Failed to load package ${trimmedName}`, error);
7000
+ this.logger.error("Failed to load package @package: @error", trimmedName, error);
6240
7001
  }
6241
7002
  }
6242
7003
  return allModels;
6243
7004
  }
7005
+ async packageVersion(packageSpec) {
7006
+ try {
7007
+ const response = await fetch(`${this.registryUrl}-/cdn/${packageSpec}/package.json`);
7008
+ if (!response.ok) return void 0;
7009
+ const pkg = await response.json();
7010
+ return typeof pkg.version === "string" && EXACT_VERSION.test(pkg.version) ? pkg.version : void 0;
7011
+ } catch (error) {
7012
+ this.logger.verbose("Could not read the version of @package: @error", packageSpec, error);
7013
+ return;
7014
+ }
7015
+ }
6244
7016
  isValidPackageName(name) {
6245
7017
  return /^(@[a-z0-9][-a-z0-9._]*\/)?[a-z0-9][-a-z0-9._]*$/i.test(name) && !name.includes("..") && name.length <= 214;
6246
7018
  }
@@ -6315,8 +7087,13 @@ var HttpDocumentModelLoader = class {
6315
7087
  * This class is used to load packages using the import keyword.
6316
7088
  */
6317
7089
  var ImportPackageLoader = class {
7090
+ hostDir;
6318
7091
  logger = childLogger(["reactor-api", "import-loader"]);
6319
7092
  name = "ImportPackageLoader";
7093
+ constructor(hostDir = process.cwd()) {
7094
+ this.hostDir = hostDir;
7095
+ }
7096
+ reportedUnbuilt = /* @__PURE__ */ new Set();
6320
7097
  async loadDocumentModels(identifier) {
6321
7098
  this.logger.verbose(`Loading document models from package: ${identifier}`);
6322
7099
  const pkgModule = await loadDocumentModels(identifier);
@@ -6359,6 +7136,31 @@ var ImportPackageLoader = class {
6359
7136
  this.logger.verbose(` ➜ No Processor Factory found: ${identifier}`);
6360
7137
  return null;
6361
7138
  }
7139
+ async loadPieces(identifier) {
7140
+ this.logger.verbose(`Loading pieces from package: ${identifier}`);
7141
+ const { root, listPath } = pieceListLocation(identifier, this.hostDir);
7142
+ if (!existsSync(listPath)) {
7143
+ this.reportUnbuilt(identifier, root, listPath);
7144
+ return [];
7145
+ }
7146
+ const module = await import(
7147
+ /* @vite-ignore */
7148
+ pathToFileURL(listPath).href
7149
+ );
7150
+ const pieces = piecesFromListModule(module, {
7151
+ root,
7152
+ identifier,
7153
+ logger: this.logger
7154
+ });
7155
+ this.logger.verbose(` ➜ Loaded ${pieces.length} Pieces from: ${identifier}`);
7156
+ return pieces;
7157
+ }
7158
+ reportUnbuilt(identifier, root, listPath) {
7159
+ if (!isAbsolute(identifier) || this.reportedUnbuilt.has(identifier)) return;
7160
+ if (!existsSync(join(root, PIECE_SOURCE_LIST))) return;
7161
+ this.reportedUnbuilt.add(identifier);
7162
+ this.logger.warn("@pkg declares pieces but @list was never built; run `ph build`", identifier, listPath);
7163
+ }
6362
7164
  };
6363
7165
  //#endregion
6364
7166
  //#region src/packages/package-manager.ts
@@ -6374,10 +7176,12 @@ var ImportPackageLoader = class {
6374
7176
  * either bare (`'pkg'`) or as a subpath (`'pkg/subgraphs'`), since loaders
6375
7177
  * import sub-entries like `${pkg}/subgraphs` / `${pkg}/document-models`.
6376
7178
  */
6377
- function isExpectedLoaderMiss(error, pkg) {
7179
+ function isExpectedLoaderMiss(error, pkg, subPath) {
6378
7180
  if (!(error instanceof Error)) return false;
6379
7181
  const code = error.code;
6380
7182
  if (code === "ERR_UNSUPPORTED_DIR_IMPORT") return true;
7183
+ if (code === "ERR_PACKAGE_PATH_NOT_EXPORTED" && subPath !== void 0 && error.message.includes(`'./${subPath}'`)) return true;
7184
+ if (subPath !== void 0 && new RegExp(`^Failed to fetch \\S*/${subPath}/index\\.mjs: 404$`).test(error.message)) return true;
6381
7185
  if (error.message.startsWith("Invalid package name:")) return true;
6382
7186
  if (code === "ERR_MODULE_NOT_FOUND") return error.message.includes(`'${pkg}'`) || error.message.includes(`"${pkg}"`) || error.message.includes(`'${pkg}/`) || error.message.includes(`"${pkg}/`);
6383
7187
  return false;
@@ -6393,12 +7197,14 @@ function getUniqueUpgradeManifests(...upgradeManifests) {
6393
7197
  return Array.from(uniqueManifests.values());
6394
7198
  }
6395
7199
  var PackageManager = class {
7200
+ options;
6396
7201
  logger = childLogger(["reactor-api", "package-manager"]);
6397
7202
  loaders;
6398
7203
  docModelsMap = /* @__PURE__ */ new Map();
6399
7204
  upgradeManifestsMap = /* @__PURE__ */ new Map();
6400
7205
  subgraphsMap = /* @__PURE__ */ new Map();
6401
7206
  processorMap = /* @__PURE__ */ new Map();
7207
+ piecesMap = /* @__PURE__ */ new Map();
6402
7208
  configWatcher;
6403
7209
  debouncedUpdateCallbacks = /* @__PURE__ */ new Map();
6404
7210
  eventEmitter = new EventEmitter();
@@ -6417,10 +7223,12 @@ var PackageManager = class {
6417
7223
  const upgradeManifestsMap = await this.loadUpgradeManifests(packages);
6418
7224
  const subgraphsMap = await this.loadSubgraphs(packages);
6419
7225
  const processorsMap = await this.loadProcessors(packages);
7226
+ const piecesMap = await this.loadPieces(packages);
6420
7227
  this.upgradeManifestsMap = upgradeManifestsMap;
6421
7228
  this.updatePackagesMap(documentModelsMap);
6422
7229
  this.updateSubgraphsMap(subgraphsMap);
6423
7230
  this.updateProcessorsMap(processorsMap);
7231
+ this.updatePiecesMap(piecesMap);
6424
7232
  try {
6425
7233
  this.subscribePackages(packages);
6426
7234
  } catch (error) {
@@ -6430,7 +7238,8 @@ var PackageManager = class {
6430
7238
  documentModels: getUniqueDocumentModels(...Array.from(documentModelsMap.values())),
6431
7239
  upgradeManifests: this.getUniqueUpgradeManifests(),
6432
7240
  subgraphs: subgraphsMap,
6433
- processors: processorsMap
7241
+ processors: processorsMap,
7242
+ pieces: piecesMap
6434
7243
  };
6435
7244
  }
6436
7245
  async loadDocumentModels(packages) {
@@ -6499,7 +7308,7 @@ var PackageManager = class {
6499
7308
  loader: loader.name,
6500
7309
  error
6501
7310
  });
6502
- this.logger.debug(`[${loader.name}] Failed to load subgraphs from package ${pkg}`, error);
7311
+ this.logger.debug("[@loader] Failed to load subgraphs from package @package: @error", loader.name, pkg, error);
6503
7312
  }
6504
7313
  this.maybeWarnAllLoadersFailed("subgraphs", pkg, succeeded, failures);
6505
7314
  subgraphsMap.set(pkg, allSubgraphs);
@@ -6523,16 +7332,41 @@ var PackageManager = class {
6523
7332
  loader: loader.name,
6524
7333
  error
6525
7334
  });
6526
- this.logger.debug(`[${loader.name}] Failed to load processors from package ${pkg}`, error);
7335
+ this.logger.debug("[@loader] Failed to load processors from package @package: @error", loader.name, pkg, error);
6527
7336
  }
6528
7337
  this.maybeWarnAllLoadersFailed("processors", pkg, succeeded, failures);
6529
7338
  processorsMap.set(pkg, allProcessors);
6530
7339
  }
6531
7340
  return processorsMap;
6532
7341
  }
7342
+ async loadPieces(packages) {
7343
+ this.logger.debug(`Loading pieces from packages: ${packages.join(", ")}`);
7344
+ const piecesMap = /* @__PURE__ */ new Map();
7345
+ for (const pkg of packages) {
7346
+ const allPieces = [];
7347
+ const failures = [];
7348
+ let succeeded = false;
7349
+ for (const loader of this.loaders) try {
7350
+ allPieces.push(...await loader.loadPieces(pkg));
7351
+ succeeded = true;
7352
+ break;
7353
+ } catch (error) {
7354
+ failures.push({
7355
+ loader: loader.name,
7356
+ error
7357
+ });
7358
+ this.logger.debug("[@loader] Failed to load pieces from package @package: @error", loader.name, pkg, error);
7359
+ }
7360
+ this.maybeWarnAllLoadersFailed("pieces", pkg, succeeded, failures);
7361
+ if (allPieces.length > 0) this.logger.info("Loaded @count piece(s) from package @pkg: @pieces", allPieces.length, pkg, allPieces.map((piece) => piece.name).join(", "));
7362
+ piecesMap.set(pkg, allPieces);
7363
+ }
7364
+ return piecesMap;
7365
+ }
6533
7366
  maybeWarnAllLoadersFailed(kind, pkg, succeeded, failures) {
6534
7367
  if (succeeded || failures.length === 0) return;
6535
- const realFailures = failures.filter(({ error }) => !isExpectedLoaderMiss(error, pkg));
7368
+ const subPath = kind === "document models" ? "document-models" : kind;
7369
+ const realFailures = failures.filter(({ error }) => !isExpectedLoaderMiss(error, pkg, subPath));
6536
7370
  if (realFailures.length === 0) return;
6537
7371
  const details = realFailures.map(({ loader, error }) => {
6538
7372
  return ` [${loader}] ${error instanceof Error ? error.message : String(error)}`;
@@ -6572,6 +7406,15 @@ var PackageManager = class {
6572
7406
  else processorsMap.set(pkg, pkgProcessors);
6573
7407
  this.updateProcessorsMap(processorsMap);
6574
7408
  }
7409
+ async updatePiecesForPackage(pkg) {
7410
+ this.logger.debug(`Updating pieces for package: ${pkg}`);
7411
+ const pieces = await this.loadPieces([pkg]);
7412
+ const piecesMap = new Map(this.piecesMap);
7413
+ const pkgPieces = pieces.get(pkg) ?? [];
7414
+ if (pkgPieces.length === 0) piecesMap.delete(pkg);
7415
+ else piecesMap.set(pkg, pkgPieces);
7416
+ this.updatePiecesMap(piecesMap);
7417
+ }
6575
7418
  /** Debounced per key so repeated subscribePackages calls reuse one timer. */
6576
7419
  getDebouncedUpdateCallback(key, update) {
6577
7420
  if (!this.debouncedUpdateCallbacks.has(key)) {
@@ -6590,10 +7433,12 @@ var PackageManager = class {
6590
7433
  const onDocumentModels = this.getDebouncedUpdateCallback(`${pkg}:document-models`, () => this.updateDocumentModelsForPackage(pkg));
6591
7434
  const onSubgraphs = this.getDebouncedUpdateCallback(`${pkg}:subgraphs`, () => this.updateSubgraphsForPackage(pkg));
6592
7435
  const onProcessors = this.getDebouncedUpdateCallback(`${pkg}:processors`, () => this.updateProcessorsForPackage(pkg));
7436
+ const onPieces = this.getDebouncedUpdateCallback(`${pkg}:pieces`, () => this.updatePiecesForPackage(pkg));
6593
7437
  for (const loader of this.loaders) {
6594
7438
  if (loader.onDocumentModelsChange) unsubs.push(loader.onDocumentModelsChange(pkg, onDocumentModels));
6595
7439
  if (loader.onSubgraphsChange) unsubs.push(loader.onSubgraphsChange(pkg, onSubgraphs));
6596
7440
  if (loader.onProcessorsChange) unsubs.push(loader.onProcessorsChange(pkg, onProcessors));
7441
+ if (loader.onPiecesChange) unsubs.push(loader.onPiecesChange(pkg, onPieces));
6597
7442
  }
6598
7443
  }
6599
7444
  return () => {
@@ -6641,6 +7486,19 @@ var PackageManager = class {
6641
7486
  this.processorMap = processorsMap;
6642
7487
  this.eventEmitter.emit("processorsChange", processorsMap);
6643
7488
  }
7489
+ updatePiecesMap(piecesMap) {
7490
+ const oldPackages = Array.from(this.piecesMap.keys());
7491
+ const newPackages = Array.from(piecesMap.keys());
7492
+ oldPackages.filter((pkg) => !newPackages.includes(pkg)).forEach((pkg) => {
7493
+ this.logger.info("Removed pieces from: @pkg", pkg);
7494
+ });
7495
+ this.piecesMap = piecesMap;
7496
+ this.eventEmitter.emit("piecesChange", piecesMap);
7497
+ }
7498
+ /** The pieces every loaded package ships, keyed by the package. */
7499
+ getPieces() {
7500
+ return new Map(this.piecesMap);
7501
+ }
6644
7502
  /**
6645
7503
  * Remove `pkg` from all package maps and emit the change events, tearing
6646
7504
  * down everything the package registered (subgraphs, processors, document
@@ -6659,9 +7517,11 @@ var PackageManager = class {
6659
7517
  this.upgradeManifestsMap = dropKey(this.upgradeManifestsMap);
6660
7518
  this.subgraphsMap = dropKey(this.subgraphsMap);
6661
7519
  this.processorMap = dropKey(this.processorMap);
7520
+ this.piecesMap = dropKey(this.piecesMap);
6662
7521
  this.updatePackagesMap(this.docModelsMap);
6663
7522
  this.updateSubgraphsMap(this.subgraphsMap);
6664
7523
  this.updateProcessorsMap(this.processorMap);
7524
+ this.updatePiecesMap(this.piecesMap);
6665
7525
  }
6666
7526
  onDocumentModelsChange(handler) {
6667
7527
  this.eventEmitter.on("documentModelsChange", handler);
@@ -6672,6 +7532,9 @@ var PackageManager = class {
6672
7532
  onProcessorsChange(handler) {
6673
7533
  this.eventEmitter.on("processorsChange", handler);
6674
7534
  }
7535
+ onPiecesChange(handler) {
7536
+ this.eventEmitter.on("piecesChange", handler);
7537
+ }
6675
7538
  };
6676
7539
  //#endregion
6677
7540
  //#region src/config.ts
@@ -6689,20 +7552,55 @@ async function createStartupAttachmentBackend({ db, env = process.env, s3Depende
6689
7552
  }
6690
7553
  //#endregion
6691
7554
  //#region src/services/attachment-access.service.ts
7555
+ /** The scope an attachment's bytes belong to: the document's own domain state. */
7556
+ const ATTACHMENT_SCOPE = "global";
7557
+ /** The branch an attachment reference is resolved against. */
7558
+ const ATTACHMENT_BRANCH = "main";
6692
7559
  const HASH_PATTERN = /^[a-f0-9]{64}$/;
6693
7560
  /**
6694
7561
  * Composes document authorization with the projected document/ref
6695
7562
  * relationship. Order is fixed: validate ref, resolve the canonical document
6696
- * id, check `canRead`, then check the reference index. A denied document
6697
- * never reaches the reference reader, and the facade never touches
6698
- * attachment metadata, storage backends, or presigners.
7563
+ * id, decide the read, check the reactor's read gate serves the document, then
7564
+ * check the reference index and that the subject may read a scope whose
7565
+ * operations reference the attachment. A denied document never reaches the
7566
+ * reference reader, and the facade never touches attachment metadata, storage
7567
+ * backends, or presigners.
7568
+ *
7569
+ * The named document decides alone: an attachment several documents reference
7570
+ * is readable through each of them on that document's own terms.
7571
+ *
7572
+ * The read is decided by the document's own policy when this composition has a
7573
+ * model to enforce it with, and by the host's permission tables when it does
7574
+ * not. That is not a preference between two equivalent answers: a host running
7575
+ * document policies keeps no rows in the permission tables, so asking them
7576
+ * about such a document returns whatever the host-wide policy says — under
7577
+ * `OPEN`, `true`, for everyone. Serving bytes on that answer hands the file to
7578
+ * anyone who learns its hash, which the document's own state may well have told
7579
+ * them. Both are kept because they are the two halves of one rule: an
7580
+ * attachment is readable by whoever may read the document that references it,
7581
+ * and each composition can only express that in its own terms. The caller
7582
+ * decision mirrors the routes' `requireAuth`.
6699
7583
  */
6700
7584
  var AttachmentAccessService = class {
6701
- constructor(resolveCanonicalId, authorization, references, projection) {
7585
+ resolveCanonicalId;
7586
+ authorization;
7587
+ references;
7588
+ projection;
7589
+ readGate;
7590
+ scopeGate;
7591
+ options;
7592
+ constructor(resolveCanonicalId, authorization, references, projection, readGate, scopeGate, options = {}) {
6702
7593
  this.resolveCanonicalId = resolveCanonicalId;
6703
7594
  this.authorization = authorization;
6704
7595
  this.references = references;
6705
7596
  this.projection = projection;
7597
+ this.readGate = readGate;
7598
+ this.scopeGate = scopeGate;
7599
+ this.options = options;
7600
+ }
7601
+ admitCaller(request) {
7602
+ if ((request.intent === "read" ? this.options.refuseAnonymousReads ?? false : this.options.refuseAnonymousWrites ?? true) && !request.userAddress) return Promise.resolve({ kind: "unauthenticated" });
7603
+ return Promise.resolve({ kind: "admitted" });
6706
7604
  }
6707
7605
  async canReadAttachment(request) {
6708
7606
  if (this.projection.status !== "available") return { kind: "projection-unavailable" };
@@ -6714,14 +7612,51 @@ var AttachmentAccessService = class {
6714
7612
  } catch {
6715
7613
  return { kind: "denied" };
6716
7614
  }
6717
- if (!await this.authorization.canRead(documentId, request.userAddress)) return { kind: "denied" };
6718
- if (!await this.references.hasReference(documentId, ref)) return { kind: "denied" };
7615
+ if (!await this.canReadDocument(documentId, request)) return { kind: "denied" };
7616
+ const subject = {
7617
+ address: request.userAddress,
7618
+ key: request.appKey
7619
+ };
7620
+ let served;
7621
+ try {
7622
+ served = await this.readGate.isServed(documentId, { subject });
7623
+ } catch {
7624
+ return { kind: "denied" };
7625
+ }
7626
+ if (!served) return { kind: "denied" };
7627
+ const scopes = await this.references.referencingScopes(documentId, ref);
7628
+ if (scopes.length === 0) return { kind: "denied" };
7629
+ let held;
7630
+ try {
7631
+ held = (await this.readGate.get(documentId, {
7632
+ subject,
7633
+ scopes
7634
+ })).state;
7635
+ } catch {
7636
+ return { kind: "denied" };
7637
+ }
7638
+ if (!scopes.some((scope) => scope in held)) return { kind: "denied" };
6719
7639
  return {
6720
7640
  kind: "allowed",
6721
7641
  documentId,
6722
7642
  ref
6723
7643
  };
6724
7644
  }
7645
+ /**
7646
+ * Whether the caller may read the state the reference lives in.
7647
+ *
7648
+ * A failure here is rethrown rather than turned into a denial. A read side
7649
+ * that is down must not read as a policy: a denial nobody can distinguish
7650
+ * from a refusal is one nobody investigates, and the route answers 500 for a
7651
+ * reason.
7652
+ */
7653
+ async canReadDocument(documentId, request) {
7654
+ if (!this.scopeGate) return this.authorization.canRead(documentId, request.userAddress);
7655
+ return (await this.scopeGate.scopePredicateById(documentId, {
7656
+ address: request.userAddress,
7657
+ key: request.appKey
7658
+ }, ATTACHMENT_BRANCH))(ATTACHMENT_SCOPE);
7659
+ }
6725
7660
  };
6726
7661
  /**
6727
7662
  * Parses and canonicalizes a caller-supplied ref through the shared parser.
@@ -6742,6 +7677,140 @@ function normalizeAttachmentRef(value) {
6742
7677
  return createRef(hash, version);
6743
7678
  }
6744
7679
  //#endregion
7680
+ //#region src/services/authorized-attachment.service.ts
7681
+ /** Matches the attachment routes' `documentId` bound. */
7682
+ const MAX_DOCUMENT_ID_LEN = 512;
7683
+ /** The reference index is not maintained in this composition. */
7684
+ var AttachmentAccessUnavailable = class extends Error {
7685
+ constructor() {
7686
+ super("Attachment access is unavailable");
7687
+ this.name = "AttachmentAccessUnavailable";
7688
+ }
7689
+ };
7690
+ /** The access decision itself failed; the cause is logged, not surfaced. */
7691
+ var AttachmentAccessFailed = class extends Error {
7692
+ constructor(cause) {
7693
+ super("Attachment access decision failed", { cause });
7694
+ this.name = "AttachmentAccessFailed";
7695
+ }
7696
+ };
7697
+ /** An `IAttachmentService` whose reads and writes are decided for one subject. */
7698
+ var AuthorizedAttachmentService = class {
7699
+ inner;
7700
+ access;
7701
+ subject;
7702
+ logger;
7703
+ constructor(inner, access, subject, logger) {
7704
+ this.inner = inner;
7705
+ this.access = access;
7706
+ this.subject = subject;
7707
+ this.logger = logger;
7708
+ }
7709
+ /** Hash-first only: an upload-first handle can send after it expires. */
7710
+ async reserve(options) {
7711
+ const { mimeType, fileName, extension, clientHash, sizeBytes } = options;
7712
+ if (clientHash === void 0) throw new Error("Attachment reservations require a client hash");
7713
+ const copy = {
7714
+ mimeType,
7715
+ fileName,
7716
+ extension,
7717
+ clientHash,
7718
+ sizeBytes
7719
+ };
7720
+ await this.admit("write");
7721
+ validateReserveMetadata(copy);
7722
+ return this.inner.reserve(copy);
7723
+ }
7724
+ async stat(ref, options) {
7725
+ const decision = await this.decideRead(ref, options);
7726
+ return this.inner.stat(decision.ref, {
7727
+ ...options,
7728
+ documentId: decision.documentId
7729
+ });
7730
+ }
7731
+ async get(ref, options) {
7732
+ const decision = await this.decideRead(ref, options);
7733
+ return this.inner.get(decision.ref, {
7734
+ ...options,
7735
+ documentId: decision.documentId
7736
+ });
7737
+ }
7738
+ async getDownloadTarget(ref, options) {
7739
+ const decision = await this.decideRead(ref, options);
7740
+ return this.inner.getDownloadTarget(decision.ref, {
7741
+ ...options,
7742
+ documentId: decision.documentId
7743
+ });
7744
+ }
7745
+ async decideRead(ref, options) {
7746
+ const documentId = readDocumentId(options);
7747
+ if (documentId === null) throw new AttachmentNotFound(ref);
7748
+ await this.admit("read");
7749
+ let decision;
7750
+ try {
7751
+ decision = await this.access.canReadAttachment({
7752
+ documentId,
7753
+ attachmentRef: ref,
7754
+ userAddress: this.subject.address,
7755
+ appKey: this.subject.key
7756
+ });
7757
+ } catch (err) {
7758
+ throw this.accessFailed(err);
7759
+ }
7760
+ if (decision.kind === "allowed") return {
7761
+ documentId: decision.documentId,
7762
+ ref: decision.ref
7763
+ };
7764
+ if (decision.kind === "projection-unavailable") throw new AttachmentAccessUnavailable();
7765
+ throw new AttachmentNotFound(ref);
7766
+ }
7767
+ async admit(intent) {
7768
+ let result;
7769
+ try {
7770
+ result = await this.access.admitCaller({
7771
+ intent,
7772
+ userAddress: this.subject.address,
7773
+ appKey: this.subject.key
7774
+ });
7775
+ } catch (err) {
7776
+ throw this.accessFailed(err);
7777
+ }
7778
+ if (result.kind !== "admitted") throw new AuthenticationRequiredError();
7779
+ }
7780
+ accessFailed(err) {
7781
+ this.logger.error("Attachment access decision failed: @error", err);
7782
+ return new AttachmentAccessFailed(err);
7783
+ }
7784
+ };
7785
+ /** The options' `documentId` when it is one the routes would accept, else null. */
7786
+ function readDocumentId(options) {
7787
+ if (typeof options !== "object" || options === null) return null;
7788
+ if (isAbortSignal(options)) return null;
7789
+ const value = options.documentId;
7790
+ if (typeof value !== "string") return null;
7791
+ if (value.trim().length === 0 || value.length > MAX_DOCUMENT_ID_LEN) return null;
7792
+ return value;
7793
+ }
7794
+ /** Duck-checked too, so a signal from another realm is still a signal. */
7795
+ function isAbortSignal(value) {
7796
+ if (value instanceof AbortSignal) return true;
7797
+ const candidate = value;
7798
+ return typeof candidate.aborted === "boolean" && typeof candidate.addEventListener === "function";
7799
+ }
7800
+ var AttachmentClientProvider = class {
7801
+ service;
7802
+ access;
7803
+ logger;
7804
+ constructor(service, access, logger) {
7805
+ this.service = service;
7806
+ this.access = access;
7807
+ this.logger = logger;
7808
+ }
7809
+ forSubject(subject) {
7810
+ return createAttachmentClient(new AuthorizedAttachmentService(this.service, this.access, subject, this.logger));
7811
+ }
7812
+ };
7813
+ //#endregion
6745
7814
  //#region src/graphql/gateway/require-auth-middleware.ts
6746
7815
  /**
6747
7816
  * Returns a fetch middleware that rejects anonymous callers with a 401.
@@ -6767,14 +7836,42 @@ function normalizeAttachmentRef(value) {
6767
7836
  * context has no caller, and is rejected. The server never composes this
6768
7837
  * middleware without the auth middleware in the chain (see
6769
7838
  * `assertRequireAuthenticatedCallerAllowed` in `server.ts`).
7839
+ *
7840
+ * `exemptPaths` names the mounted paths that stay reachable anonymously — a
7841
+ * deployment whose product has a flow that runs before sign-in, such as
7842
+ * previewing an invitation from its code, needs somewhere to serve it from.
7843
+ * Each entry is a hole in the floor, so the match is deliberately the
7844
+ * narrowest one that works: the request's pathname, compared in full. A
7845
+ * prefix rule would exempt `/graphql/public-admin` along with
7846
+ * `/graphql/public`, and an exempt surface nobody can enumerate by reading
7847
+ * the configuration is not one anybody audits.
6770
7848
  */
6771
- function createRequireAuthFetchMiddleware() {
7849
+ function createRequireAuthFetchMiddleware(exemptPaths = []) {
7850
+ const exempt = new Set(exemptPaths.map(normalizeExemptPath));
6772
7851
  return (next) => async (request) => {
6773
7852
  if (request.method === "OPTIONS") return next(request);
7853
+ if (exempt.size > 0 && exempt.has(normalizeExemptPath(new URL(request.url).pathname))) return next(request);
6774
7854
  if (getAuthContext(request)?.user === void 0) return new globalThis.Response(JSON.stringify({ error: "Authentication required" }), { status: 401 });
6775
7855
  return next(request);
6776
7856
  };
6777
7857
  }
7858
+ /**
7859
+ * The form both sides of the comparison are reduced to: surrounding whitespace
7860
+ * gone, and one trailing slash gone, so `/graphql/public/` and
7861
+ * `/graphql/public` are the same path. A client that appends a slash is not
7862
+ * asking for a different route, and a configuration that carries one is not
7863
+ * asking for a different rule.
7864
+ *
7865
+ * Deliberately NOT normalized: case, percent-encoding, and a missing leading
7866
+ * slash. The first two would let two spellings of one path diverge from how
7867
+ * the router itself matches; the third is a configuration error, and
7868
+ * `assertRequireAuthenticatedCallerAllowed` refuses to boot on it rather than
7869
+ * leaving an exemption that silently never applies.
7870
+ */
7871
+ function normalizeExemptPath(path) {
7872
+ const trimmed = path.trim();
7873
+ return trimmed.length > 1 && trimmed.endsWith("/") ? trimmed.slice(0, -1) : trimmed;
7874
+ }
6778
7875
  //#endregion
6779
7876
  //#region src/migrations/001_create_document_permissions.ts
6780
7877
  var _001_create_document_permissions_exports = /* @__PURE__ */ __exportAll({
@@ -7177,6 +8274,7 @@ async function createRenownCredentialVerifier(config = {}) {
7177
8274
  * - Users can be granted permission to execute specific operations
7178
8275
  */
7179
8276
  var DocumentPermissionService = class {
8277
+ db;
7180
8278
  config;
7181
8279
  constructor(db, config = { defaultProtection: false }) {
7182
8280
  this.db = db;
@@ -7252,8 +8350,38 @@ var DocumentPermissionService = class {
7252
8350
  * Delete all permissions for a document (used when deleting a document)
7253
8351
  */
7254
8352
  async deleteAllDocumentPermissions(documentId) {
7255
- await this.db.deleteFrom("DocumentPermission").where("documentId", "=", documentId).execute();
7256
- await this.db.deleteFrom("OperationUserPermission").where("documentId", "=", documentId).execute();
8353
+ const permissions = await this.db.deleteFrom("DocumentPermission").where("documentId", "=", documentId).returning("id").execute();
8354
+ const operations = await this.db.deleteFrom("OperationUserPermission").where("documentId", "=", documentId).returning("id").execute();
8355
+ return {
8356
+ DocumentPermission: permissions.length,
8357
+ OperationUserPermission: operations.length
8358
+ };
8359
+ }
8360
+ /** Delete a document's protection row, which names its owner. */
8361
+ async deleteDocumentProtection(documentId) {
8362
+ return (await this.db.deleteFrom("DocumentProtection").where("documentId", "=", documentId).returning("documentId").execute()).length;
8363
+ }
8364
+ /** Every row that names the address, for a subject access request. */
8365
+ async rowsForAddress(address) {
8366
+ const lower = address.toLowerCase();
8367
+ return {
8368
+ permissions: await this.db.selectFrom("DocumentPermission").select([
8369
+ "documentId",
8370
+ "userAddress",
8371
+ "permission",
8372
+ "grantedBy",
8373
+ "createdAt",
8374
+ "updatedAt"
8375
+ ]).where((eb) => eb.or([eb("userAddress", "=", lower), eb("grantedBy", "=", lower)])).orderBy("documentId").execute(),
8376
+ operationPermissions: await this.db.selectFrom("OperationUserPermission").select([
8377
+ "documentId",
8378
+ "operationType",
8379
+ "userAddress",
8380
+ "grantedBy",
8381
+ "createdAt"
8382
+ ]).where((eb) => eb.or([eb("userAddress", "=", lower), eb("grantedBy", "=", lower)])).orderBy("documentId").execute(),
8383
+ ownedDocumentIds: (await this.db.selectFrom("DocumentProtection").select("documentId").where("ownerAddress", "=", lower).orderBy("documentId").execute()).map((row) => row.documentId)
8384
+ };
7257
8385
  }
7258
8386
  /**
7259
8387
  * Grant a user permission to execute an operation on a document
@@ -7447,12 +8575,16 @@ function createGetParentIdsFn(reactorClient) {
7447
8575
  * Authorization gate for the /mcp endpoint (AUTH_REVIEW S-C1). MCP tools have
7448
8576
  * unrestricted reactor access, so access is limited to supreme admins when
7449
8577
  * auth is enabled. OPEN policy is public; any other policy without an
7450
- * AuthService fails closed.
8578
+ * AuthService fails closed. The tools read as the caller: anonymous under OPEN,
8579
+ * else the admin's address and the app key its token carries.
7451
8580
  */
7452
8581
  function createMcpRequestAuthorizer(authService, authorizationService) {
7453
8582
  return async (req) => {
7454
8583
  if (!authService) {
7455
- if (authorizationService.config.policy === AuthorizationPolicy.OPEN) return { authorized: true };
8584
+ if (authorizationService.config.policy === AuthorizationPolicy.OPEN) return {
8585
+ authorized: true,
8586
+ subject: {}
8587
+ };
7456
8588
  return {
7457
8589
  authorized: false,
7458
8590
  status: 401,
@@ -7475,10 +8607,39 @@ function createMcpRequestAuthorizer(authService, authorizationService) {
7475
8607
  status: 403,
7476
8608
  message: "Forbidden: MCP access requires an administrator"
7477
8609
  };
7478
- return { authorized: true };
8610
+ return {
8611
+ authorized: true,
8612
+ subject: {
8613
+ address: context.user.address,
8614
+ key: context.user.appKey
8615
+ }
8616
+ };
7479
8617
  };
7480
8618
  }
7481
8619
  //#endregion
8620
+ //#region src/services/sync-serving-gate.ts
8621
+ /**
8622
+ * The gate sync serving evaluates a document's own policy through, deciding
8623
+ * what the reactor client's read gate decides at the same flag level.
8624
+ *
8625
+ * Under `authEnforcement` it is built here rather than taken off the client
8626
+ * because it also carries the host's closes-by-default setting, which withholds
8627
+ * the domain scopes of a document nobody has policied yet. That answer belongs
8628
+ * to serving alone -- replay must keep reading an uninitialized document in
8629
+ * full -- so the two gates are separate objects over the same model.
8630
+ *
8631
+ * Below it the registered model ignores the auth scope, so the client reads
8632
+ * through the policy alone, and so does serving: a policy written below the
8633
+ * flag still withholds, where serving ungated would sync to a peer what `get`
8634
+ * refuses it.
8635
+ */
8636
+ function buildSyncServingGate(reactorModule, authorizationConfig, logger) {
8637
+ if (!reactorModule) return;
8638
+ const model = readDecisionModel(reactorModule.featureFlags, reactorModule.documentModelRegistry);
8639
+ if (!model) return new SyncScopeGate(new BareReadGate(), reactorModule.documentView, logger);
8640
+ return new SyncScopeGate(new ModelReadGate(model, reactorModule.documentView, reactorModule.featureFlags.authGroups, reactorModule.operationIndex, logger, { withholdUninitialized: authorizationConfig.defaultProtection }), reactorModule.documentView, logger);
8641
+ }
8642
+ //#endregion
7482
8643
  //#region src/services/renown-config.ts
7483
8644
  /** Reading credentials from this reactor's own read model needs a host-supplied
7484
8645
  * verifier: core has no idea which subgraph serves that read model. */
@@ -7660,10 +8821,36 @@ function assertSkipCredentialVerificationAllowed(resolvesCallerIdentity, skipCre
7660
8821
  * require-authenticated-caller middleware would reject every caller —
7661
8822
  * authenticated ones included. Refuse to boot rather than run broken.
7662
8823
  */
7663
- function assertRequireAuthenticatedCallerAllowed(requireAuthenticatedCaller, resolvesCallerIdentity) {
8824
+ function assertRequireAuthenticatedCallerAllowed(requireAuthenticatedCaller, resolvesCallerIdentity, exemptPaths = []) {
8825
+ /**
8826
+ * An exemption is only ever a hole in this floor, so configuring one while
8827
+ * the floor is off is not a harmless no-op: it reads, to anyone auditing the
8828
+ * configuration, as a surface that was deliberately opened — and therefore
8829
+ * as a floor that exists. Refuse rather than let the two drift.
8830
+ */
8831
+ 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.");
8832
+ /**
8833
+ * A path that cannot match anything is worse than no path: the operator
8834
+ * believes the flow is reachable, and finds out when the first user cannot
8835
+ * claim an invitation. `URL.pathname` is always absolute, so an entry that
8836
+ * does not start with `/` never matches, whatever the router does.
8837
+ */
8838
+ const relative = exemptPaths.filter((path) => !path.trim().startsWith("/"));
8839
+ 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').`);
7664
8840
  if (!requireAuthenticatedCaller || resolvesCallerIdentity) return;
7665
8841
  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).");
7666
8842
  }
8843
+ /**
8844
+ * Deciding an attachment read by the document's policy needs a policy model to
8845
+ * decide with, and that is what auth enforcement supplies. Without one there is
8846
+ * nothing to consult, so the setting would silently leave the permission tables
8847
+ * in charge — configuration that describes a protection the server is not
8848
+ * applying, which is worse than no configuration at all. Refuse instead.
8849
+ */
8850
+ function assertAttachmentPolicyReadsAllowed(attachmentReadsFollowDocumentPolicy, hasDecisionModel) {
8851
+ if (!attachmentReadsFollowDocumentPolicy || hasDecisionModel) return;
8852
+ 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).");
8853
+ }
7667
8854
  function createReadinessGate() {
7668
8855
  let ready = false;
7669
8856
  return {
@@ -7718,27 +8905,6 @@ function makeDbClosers(knexInstance, pglite) {
7718
8905
  return closers;
7719
8906
  }
7720
8907
  /**
7721
- * The gate sync serving evaluates a document's own policy through, or undefined
7722
- * when there is none to evaluate.
7723
- *
7724
- * It is built here rather than taken off the reactor client because it is not
7725
- * the same gate reads use: it carries the host's closes-by-default setting,
7726
- * which withholds the domain scopes of a document nobody has policied yet. That
7727
- * answer belongs to serving alone -- replay must keep reading an uninitialized
7728
- * document in full -- so the two gates are deliberately separate objects over
7729
- * the same model.
7730
- *
7731
- * Undefined below `authEnforcement`, where the registered model ignores the auth
7732
- * scope: gating through it would serve every domain scope of a policied document
7733
- * to anyone, which is worse than not gating at all.
7734
- */
7735
- function buildSyncServingGate(reactorModule, authorizationConfig, logger) {
7736
- if (!reactorModule) return;
7737
- const model = readDecisionModel(reactorModule.featureFlags, reactorModule.documentModelRegistry);
7738
- if (!model) return;
7739
- return new SyncScopeGate(new ModelReadGate(model, reactorModule.documentView, reactorModule.featureFlags.authGroups, reactorModule.operationIndex, logger, { withholdUninitialized: authorizationConfig.defaultProtection }), reactorModule.documentView, logger);
7740
- }
7741
- /**
7742
8908
  * Resolves the gateway adapter type from the `GATEWAY_ADAPTER` env var.
7743
8909
  * Defaults to "apollo" (the federation gateway, production behavior).
7744
8910
  * "stitching" selects the in-process graphql-tools merge gateway (#1565
@@ -7754,8 +8920,28 @@ function resolveGatewayAdapterType(logger) {
7754
8920
  /**
7755
8921
  * Sets up the subgraph manager and registers subgraphs
7756
8922
  */
7757
- async function setupGraphQLManager(httpAdapter, authFetchMiddleware, requireAuthFetchMiddleware, httpServer, wsServer, client, relationalDb, analyticsStore, syncManager, subgraphs, logger, authorizationService, authService, documentPermissionService, enableDocumentModelSubgraphs, port, reactorDriveClient, syncServingGate, httpRoutes) {
7758
- const graphqlManager = new GraphQLManager(config.basePath, httpServer, wsServer, client, relationalDb, analyticsStore, syncManager, logger, httpAdapter, await createGatewayAdapter(resolveGatewayAdapterType(logger), logger), authService, documentPermissionService, { enableDocumentModelSubgraphs }, port, authorizationService, reactorDriveClient, syncServingGate, httpRoutes);
8923
+ async function setupGraphQLManager({ httpAdapter, authFetchMiddleware, requireAuthFetchMiddleware, httpServer, wsServer, client, relationalDb, analyticsStore, syncManager, subgraphs, logger, authorizationService, authService, documentPermissionService, enableDocumentModelSubgraphs, port, reactorDriveClient, syncServingGate, httpRoutes, attachments }) {
8924
+ const graphqlManager = new GraphQLManager({
8925
+ path: config.basePath,
8926
+ httpServer,
8927
+ wsServer,
8928
+ reactorClient: client,
8929
+ relationalDb,
8930
+ analyticsStore,
8931
+ syncManager,
8932
+ logger,
8933
+ httpAdapter,
8934
+ gatewayAdapter: await createGatewayAdapter(resolveGatewayAdapterType(logger), logger),
8935
+ authService,
8936
+ documentPermissionService,
8937
+ featureFlags: { enableDocumentModelSubgraphs },
8938
+ port,
8939
+ authorizationService,
8940
+ reactorDriveClient,
8941
+ syncServingGate,
8942
+ httpRoutes,
8943
+ attachments
8944
+ });
7759
8945
  await graphqlManager.init(subgraphs.core, authFetchMiddleware, requireAuthFetchMiddleware);
7760
8946
  for (const [packageName, collection] of subgraphs.extended.entries()) for (const subgraph of collection) await graphqlManager.registerSubgraph(subgraph, "graphql", false, packageName);
7761
8947
  await graphqlManager.updateRouter();
@@ -7853,6 +9039,8 @@ async function _setupCommonInfrastructure(options) {
7853
9039
  let authEnabled = false;
7854
9040
  let configuredResolveIdentity;
7855
9041
  let configuredRequireAuth;
9042
+ let configuredExemptPaths;
9043
+ let configuredAttachmentPolicyReads;
7856
9044
  let configuredRenown;
7857
9045
  if (options.configFile) {
7858
9046
  const config = getConfig(options.configFile);
@@ -7864,8 +9052,10 @@ async function _setupCommonInfrastructure(options) {
7864
9052
  authEnabled = options.auth.enabled;
7865
9053
  configuredResolveIdentity = options.auth.resolveIdentity;
7866
9054
  configuredRequireAuth = options.auth.requireAuthenticatedCaller;
9055
+ configuredExemptPaths = options.auth.requireAuthenticatedCallerExemptPaths;
9056
+ configuredAttachmentPolicyReads = options.auth.attachmentReadsFollowDocumentPolicy;
7867
9057
  }
7868
- 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;
9058
+ 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;
7869
9059
  if (AUTH_ENABLED !== void 0) authEnabled = AUTH_ENABLED === "true";
7870
9060
  /**
7871
9061
  * Whether the auth middleware reads the bearer and populates `ctx.user`,
@@ -7896,6 +9086,30 @@ async function _setupCommonInfrastructure(options) {
7896
9086
  */
7897
9087
  let requireAuthenticatedCaller = configuredRequireAuth ?? false;
7898
9088
  if (REQUIRE_AUTHENTICATED_CALLER !== void 0) requireAuthenticatedCaller = REQUIRE_AUTHENTICATED_CALLER === "true";
9089
+ /**
9090
+ * The paths the floor above does not apply to. Comma-separated, like
9091
+ * `ADMINS`, because a deployment configures this the same way it configures
9092
+ * everything else: one variable, one line, however many values it has.
9093
+ *
9094
+ * Empty entries are dropped rather than refused, so a trailing comma or a
9095
+ * value split across lines in a manifest is not a boot failure; a value that
9096
+ * cannot ever match is refused, in
9097
+ * {@link assertRequireAuthenticatedCallerAllowed}.
9098
+ */
9099
+ let requireAuthenticatedCallerExemptPaths = configuredExemptPaths ?? [];
9100
+ if (REQUIRE_AUTHENTICATED_CALLER_EXEMPT_PATHS !== void 0) requireAuthenticatedCallerExemptPaths = REQUIRE_AUTHENTICATED_CALLER_EXEMPT_PATHS.split(",").map((path) => path.trim()).filter((path) => path.length > 0);
9101
+ /**
9102
+ * Whether an attachment read is decided by the referencing document's own
9103
+ * policy rather than by the host's permission tables.
9104
+ *
9105
+ * Off by default, and a deployment's choice rather than something derived
9106
+ * from the flags around it. Which model governs those bytes is configuration
9107
+ * in the same sense the storage backend behind them is: a host that has said
9108
+ * nothing keeps exactly the behaviour it has, and one that wants the change
9109
+ * asks for it and can take it back without disturbing anything else.
9110
+ */
9111
+ let attachmentReadsFollowDocumentPolicy = configuredAttachmentPolicyReads ?? false;
9112
+ if (ATTACHMENT_READS_FOLLOW_DOCUMENT_POLICY !== void 0) attachmentReadsFollowDocumentPolicy = ATTACHMENT_READS_FOLLOW_DOCUMENT_POLICY === "true";
7899
9113
  if (ADMINS !== void 0) admins = ADMINS.split(",").map((a) => a.toLowerCase());
7900
9114
  let defaultProtection = false;
7901
9115
  if (DEFAULT_PROTECTION !== void 0) defaultProtection = DEFAULT_PROTECTION.toLowerCase() === "true";
@@ -7910,7 +9124,7 @@ async function _setupCommonInfrastructure(options) {
7910
9124
  const documentPermissionsRequested = options.documentPermissionService !== void 0 || DOCUMENT_PERMISSIONS_ENABLED === "true";
7911
9125
  assertAuthRequiredForDocumentPermissions(authEnabled, documentPermissionsRequested);
7912
9126
  assertSkipCredentialVerificationAllowed(resolveCallerIdentity, skipCredentialVerification, process.env);
7913
- assertRequireAuthenticatedCallerAllowed(requireAuthenticatedCaller, resolveCallerIdentity);
9127
+ assertRequireAuthenticatedCallerAllowed(requireAuthenticatedCaller, resolveCallerIdentity, requireAuthenticatedCallerExemptPaths);
7914
9128
  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.");
7915
9129
  httpAdapter.getRoute("/health", () => new Response("OK", { status: 200 }));
7916
9130
  const readiness = createReadinessGate();
@@ -7946,8 +9160,8 @@ async function _setupCommonInfrastructure(options) {
7946
9160
  });
7947
9161
  authFetchMiddleware = createAuthFetchMiddleware(authService);
7948
9162
  if (requireAuthenticatedCaller) {
7949
- requireAuthFetchMiddleware = createRequireAuthFetchMiddleware();
7950
- logger.info("Require-authenticated-caller middleware enabled: anonymous callers are rejected with a 401 before any subgraph");
9163
+ requireAuthFetchMiddleware = createRequireAuthFetchMiddleware(requireAuthenticatedCallerExemptPaths);
9164
+ 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");
7951
9165
  } 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.");
7952
9166
  }
7953
9167
  const dbClosers = [];
@@ -7972,7 +9186,7 @@ async function _setupCommonInfrastructure(options) {
7972
9186
  await mkdir(attachmentStoragePath, { recursive: true });
7973
9187
  const { db: attachmentDb, knex: attachmentKnex, pglite: attachmentPglite } = getDbClient(options.dbPath, options.pgliteFactory);
7974
9188
  dbClosers.push(...makeDbClosers(attachmentKnex, attachmentPglite));
7975
- const ATTACHMENT_SWEEP_INTERVAL_MS = 3600 * 1e3;
9189
+ const ATTACHMENT_SWEEP_INTERVAL_MS = 36e5;
7976
9190
  const attachmentBackend = await createStartupAttachmentBackend({ db: attachmentDb });
7977
9191
  const attachmentBuilder = new AttachmentBuilder(attachmentDb, attachmentStoragePath).withReservationSweepMs(ATTACHMENT_SWEEP_INTERVAL_MS);
7978
9192
  if (attachmentBackend) attachmentBuilder.withBackend(attachmentBackend);
@@ -8008,6 +9222,7 @@ async function _setupCommonInfrastructure(options) {
8008
9222
  httpRoutes,
8009
9223
  authFetchMiddleware,
8010
9224
  requireAuthFetchMiddleware,
9225
+ authEnabled,
8011
9226
  authService,
8012
9227
  relationalDb,
8013
9228
  analyticsStore,
@@ -8017,13 +9232,14 @@ async function _setupCommonInfrastructure(options) {
8017
9232
  attachmentReferenceIndex,
8018
9233
  packages,
8019
9234
  dbClosers,
8020
- readiness
9235
+ readiness,
9236
+ attachmentReadsFollowDocumentPolicy
8021
9237
  };
8022
9238
  }
8023
9239
  /**
8024
9240
  * Private helper function containing common setup logic for API initialization
8025
9241
  */
8026
- 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) {
9242
+ async function _setupAPI(reactorClient, syncManager, reactorProcessorManager, httpAdapter, authFetchMiddleware, requireAuthFetchMiddleware, authEnabled, authService, port, packages, relationalDb, analyticsStore, documentPermissionService, processors, subgraphs, options, processorApp, readModels, attachments, attachmentReferenceIndex, attachmentReferenceProjection, authorizationConfig, documentModelRegistry, dbClosers = [], reactorDriveClient, syncServingGate, httpRoutes, attachmentReadsFollowDocumentPolicy = false) {
8027
9243
  const hostModuleBase = {
8028
9244
  ...createReactorHostModuleBase({
8029
9245
  client: reactorClient,
@@ -8048,7 +9264,7 @@ async function _setupAPI(reactorClient, syncManager, reactorProcessorManager, ht
8048
9264
  try {
8049
9265
  return fn(moduleFor(packageName));
8050
9266
  } catch (e) {
8051
- logger.error(`Error initializing processor factory for package ${packageName}:`, e);
9267
+ logger.error("Error initializing processor factory for package @package: @error", packageName, e);
8052
9268
  return null;
8053
9269
  }
8054
9270
  }))).filter((factory) => factory.status === "fulfilled" && factory.value !== null && typeof factory.value === "function");
@@ -8065,17 +9281,44 @@ async function _setupAPI(reactorClient, syncManager, reactorProcessorManager, ht
8065
9281
  const { httpServer, wsServer } = await startServer(httpAdapter, port, options.https, logger);
8066
9282
  const authorizationService = createAuthorizationService(authorizationConfig, documentPermissionService, createGetParentIdsFn(reactorClient));
8067
9283
  logger.info(`Authorization service initialized (policy: ${authorizationConfig.policy})`);
8068
- const attachmentAccess = new AttachmentAccessService(createCanonicalDocumentIdResolver(reactorClient), authorizationService, attachmentReferenceIndex.store, attachmentReferenceProjection);
9284
+ assertAttachmentPolicyReadsAllowed(attachmentReadsFollowDocumentPolicy, syncServingGate !== void 0);
9285
+ const floor = requireAuthFetchMiddleware !== void 0;
9286
+ const attachmentAccess = new AttachmentAccessService(createCanonicalDocumentIdResolver(reactorClient), authorizationService, attachmentReferenceIndex.store, attachmentReferenceProjection, reactorClient, attachmentReadsFollowDocumentPolicy ? syncServingGate : void 0, {
9287
+ refuseAnonymousWrites: authEnabled || floor,
9288
+ refuseAnonymousReads: floor
9289
+ });
9290
+ const attachmentClientProvider = new AttachmentClientProvider(attachments.service, attachmentAccess, logger);
8069
9291
  const coreSubgraphs = DefaultCoreSubgraphs.slice();
8070
9292
  coreSubgraphs.push(ReactorSubgraph);
8071
9293
  if (documentPermissionService) {
8072
9294
  coreSubgraphs.push(AuthSubgraph);
8073
9295
  logger.info("Auth subgraph registered (document permissions enabled)");
8074
9296
  }
8075
- const graphqlManager = await setupGraphQLManager(httpAdapter, authFetchMiddleware, requireAuthFetchMiddleware, httpServer, wsServer, reactorClient, relationalDb, analyticsStore, syncManager, {
8076
- extended: subgraphs,
8077
- core: coreSubgraphs
8078
- }, logger.child(["graphql-manager"]), authorizationService, authService, documentPermissionService, options.enableDocumentModelSubgraphs, port, reactorDriveClient, syncServingGate, httpRoutes);
9297
+ const graphqlManager = await setupGraphQLManager({
9298
+ httpAdapter,
9299
+ authFetchMiddleware,
9300
+ requireAuthFetchMiddleware,
9301
+ httpServer,
9302
+ wsServer,
9303
+ client: reactorClient,
9304
+ relationalDb,
9305
+ analyticsStore,
9306
+ syncManager,
9307
+ subgraphs: {
9308
+ extended: subgraphs,
9309
+ core: coreSubgraphs
9310
+ },
9311
+ logger: logger.child(["graphql-manager"]),
9312
+ authorizationService,
9313
+ authService,
9314
+ documentPermissionService,
9315
+ enableDocumentModelSubgraphs: options.enableDocumentModelSubgraphs,
9316
+ port,
9317
+ reactorDriveClient,
9318
+ syncServingGate,
9319
+ httpRoutes,
9320
+ attachments: attachmentClientProvider
9321
+ });
8079
9322
  setupEventListeners(packages, graphqlManager, reactorProcessorManager, moduleFor, documentModelRegistry);
8080
9323
  if (mcpServerEnabled) {
8081
9324
  await setupMcpServer({
@@ -8101,7 +9344,11 @@ async function _setupAPI(reactorClient, syncManager, reactorProcessorManager, ht
8101
9344
  attachments,
8102
9345
  attachmentReferenceIndex,
8103
9346
  attachmentAccess,
9347
+ attachmentClientProvider,
8104
9348
  authService,
9349
+ requireAuthenticatedCaller: floor,
9350
+ authorizationService,
9351
+ relationalDb,
8105
9352
  dispose
8106
9353
  };
8107
9354
  }
@@ -8148,7 +9395,7 @@ function buildApiDispose(args) {
8148
9395
  };
8149
9396
  }
8150
9397
  async function initializeAndStartAPI(clientInitializer, options, processorApp) {
8151
- const { port, httpAdapter, httpRoutes, authFetchMiddleware, requireAuthFetchMiddleware, authService, relationalDb, analyticsStore, documentPermissionService, authorizationConfig, attachments, attachmentReferenceIndex, packages, dbClosers, readiness } = await _setupCommonInfrastructure(options);
9398
+ const { port, httpAdapter, httpRoutes, authFetchMiddleware, requireAuthFetchMiddleware, authEnabled, authService, relationalDb, analyticsStore, documentPermissionService, authorizationConfig, attachments, attachmentReferenceIndex, packages, dbClosers, readiness, attachmentReadsFollowDocumentPolicy } = await _setupCommonInfrastructure(options);
8152
9399
  const { documentModels, upgradeManifests, processors, subgraphs } = await packages.init();
8153
9400
  const { module: reactorClientModule, reactorDriveClient, attachmentReferenceProjection = {
8154
9401
  status: "unavailable",
@@ -8165,13 +9412,14 @@ async function initializeAndStartAPI(clientInitializer, options, processorApp) {
8165
9412
  const documentModelRegistry = reactorClientModule.reactorModule?.documentModelRegistry;
8166
9413
  if (!documentModelRegistry) throw new Error("DocumentModelRegistry not available from InProcessReactorClientModule");
8167
9414
  return {
8168
- ...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),
9415
+ ...await _setupAPI(reactorClient, syncManager, reactorProcessorManager, httpAdapter, authFetchMiddleware, requireAuthFetchMiddleware, authEnabled, 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),
8169
9416
  client: reactorClient,
8170
9417
  syncManager,
8171
9418
  documentModelRegistry,
8172
9419
  readiness,
8173
9420
  attachmentReferenceProjection,
8174
- packageManager: packages
9421
+ packageManager: packages,
9422
+ documentPermissionService
8175
9423
  };
8176
9424
  }
8177
9425
  /**
@@ -8195,6 +9443,55 @@ function withScheme(origin) {
8195
9443
  return /^https?:\/\//.test(trimmed) ? trimmed : `https://${trimmed}`;
8196
9444
  }
8197
9445
  //#endregion
9446
+ //#region src/services/privacy-permissions.ts
9447
+ /**
9448
+ * reactor-privacy's permission lookup and eraser, over this host's
9449
+ * DocumentPermissionService. Typed structurally on the privacy side.
9450
+ */
9451
+ var PrivacyPermissionAdapter = class {
9452
+ service;
9453
+ constructor(service) {
9454
+ this.service = service;
9455
+ }
9456
+ async rowsForAddress(address) {
9457
+ const lower = address.toLowerCase();
9458
+ const found = await this.service.rowsForAddress(address);
9459
+ const rows = [];
9460
+ for (const row of found.permissions) for (const column of ["userAddress", "grantedBy"]) {
9461
+ if (row[column] !== lower) continue;
9462
+ rows.push({
9463
+ table: "DocumentPermission",
9464
+ column,
9465
+ documentId: row.documentId,
9466
+ detail: { permission: row.permission }
9467
+ });
9468
+ }
9469
+ for (const row of found.operationPermissions) for (const column of ["userAddress", "grantedBy"]) {
9470
+ if (row[column] !== lower) continue;
9471
+ rows.push({
9472
+ table: "OperationUserPermission",
9473
+ column,
9474
+ documentId: row.documentId,
9475
+ detail: { operationType: row.operationType }
9476
+ });
9477
+ }
9478
+ for (const documentId of found.ownedDocumentIds) rows.push({
9479
+ table: "DocumentProtection",
9480
+ column: "ownerAddress",
9481
+ documentId
9482
+ });
9483
+ return rows;
9484
+ }
9485
+ async erasePermissions(documentId) {
9486
+ const permissions = await this.service.deleteAllDocumentPermissions(documentId);
9487
+ const protection = await this.service.deleteDocumentProtection(documentId);
9488
+ return {
9489
+ ...permissions,
9490
+ DocumentProtection: protection
9491
+ };
9492
+ }
9493
+ };
9494
+ //#endregion
8198
9495
  //#region src/services/package-storage.ts
8199
9496
  var InMemoryPackageStorage = class {
8200
9497
  packages = /* @__PURE__ */ new Map();
@@ -8298,7 +9595,7 @@ var PackageManagementService = class {
8298
9595
  }
8299
9596
  };
8300
9597
  //#endregion
8301
- 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, 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 };
9598
+ export { ADMIN_USERS, ActionCandidateInputSchema, ActionContextInputSchema, ActionInputSchema, AddRelationshipDocument, AnalyticsSubgraph, AttachmentAccessFailed, AttachmentAccessService, AttachmentAccessUnavailable, AttachmentClientProvider, AuthDecision, AuthDecisionSchema, AuthEvaluationUnsupportedError, AuthService, AuthSubgraph, AuthenticationRequiredError, AuthorizationPolicy, AuthorizedAttachmentService, 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, PrivacyPermissionAdapter, PropagationMode, PropagationModeSchema, PushSyncEnvelopesDocument, ReactorSignerAppInputSchema, ReactorSignerInputSchema, ReactorSignerUserInputSchema, ReactorSubgraph, RefusalNotRecordedError, RelationalWebhookStore, RemoteCursorInputSchema, RemoteFilterInputSchema, RemoveRelationshipDocument, RenameDocumentDocument, SearchFilterInputSchema, SetPreferredEditorDocument, SyncEnvelopeInputSchema, SyncEnvelopeType, SyncEnvelopeTypeSchema, SyncRefusalInputSchema, SystemSubgraph, TouchChannelDocument, TouchChannelInputSchema, UpdateRelationshipDocument, ViewFilterInputSchema, WEBHOOK_SEGMENT, WS_AUTH_CLOSE_REASONS, WS_CLOSE_REASON_AUTHENTICATION_REQUIRED, WS_CLOSE_REASON_BEARER_REJECTED, WebhookService, assertAttachmentPolicyReadsAllowed, assertAuthRequiredForDocumentPermissions, assertCredentialVerifierForSource, assertHostMountPath, assertRequireAuthenticatedCallerAllowed, assertSkipCredentialVerificationAllowed, buildGraphQlDocument, buildGraphQlDriveDocument, buildGraphqlOperation, buildGraphqlOperations, buildSubgraphSchemaModule, callerSubject, createAuthFetchMiddleware, createCanonicalDocumentIdResolver, createGatewayAdapter, createHttpAdapter, createMergedSchema, createReactorGraphQLClient, createRenownCredentialVerifier, createSchema, createWsAuthHandlers, decodeExplorerUrlState, definedNonNullAnySchema, driveIdFromUrl, extractSubgraphsFromModule, generateDocumentModelSchema, getAuthContext, getDbClient, getDocumentModelSchemaName, getDocumentModelTypeDefs, getExplorerPrefix, getGitHash, getGitUrl, getSdk, getUniqueDocumentModels, getUniqueUpgradeManifests, getVersion, initAnalyticsStoreSql, initializeAndStartAPI, isDefinedNonNullAny, isExpectedLoaderMiss, isSubgraphClass, namespacePath, namespaceSegments, newWebhookToken, parseDriveUrl, parseWebhookBody, piecesBaseUrl, redactHeaders, renderGraphqlPlayground, resolvePackageName, resolveRenownConfig, verifyWebhook };
8302
9599
 
8303
9600
  //# sourceMappingURL=index.mjs.map
8304
- //# debugId=a15aa3df-1a6d-517d-93c4-3e8a84cefd09
9601
+ //# debugId=a50faed2-4e40-54ba-873f-1808cedc1e5c