@powerhousedao/reactor-api 6.2.0-dev.2 → 6.2.0-dev.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,6 +1,6 @@
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]="30eaf1c5-796a-5cab-8266-cfe233f9e901")}catch(e){}}();
3
- import { a as isSubgraphClass, c as loadDocumentModels, d as BaseSubgraph, i as buildGraphqlOperations, l as loadProcessors, n as buildGraphQlDriveDocument, o as debounce, r as buildGraphqlOperation, t as buildGraphQlDocument, u as loadSubgraphs } from "./utils-BFkbSO_H.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]="cae89b22-c317-5acb-b1ec-dc325f23f6cf")}catch(e){}}();
3
+ import { a as isSubgraphClass, c as loadDocumentModels, d as BaseSubgraph, f as AuthorizationPolicy, i as buildGraphqlOperations, l as loadProcessors, m as createAuthorizationService, n as buildGraphQlDriveDocument, o as debounce, p as AuthorizedDocumentHandle, r as buildGraphqlOperation, t as buildGraphQlDocument, u as loadSubgraphs } from "./utils-Dh9tl892.mjs";
4
4
  import { AnalyticsQueryEngine } from "@powerhousedao/analytics-engine-core";
5
5
  import { AnalyticsModel, AnalyticsResolvers, typedefs } from "@powerhousedao/analytics-engine-graphql";
6
6
  import { gql } from "graphql-tag";
@@ -8,13 +8,12 @@ import { GraphQLError, Kind, parse, print } from "graphql";
8
8
  import { ConsoleLogger, childLogger, documentModelDocumentModelModule } from "document-model";
9
9
  import path from "node:path";
10
10
  import { match } from "path-to-regexp";
11
- import { verifyAuthBearerToken } from "@renown/sdk";
12
11
  import { buildSubgraphSchema } from "@apollo/subgraph";
13
12
  import { typeDefs } from "@powerhousedao/document-engineering/graphql";
14
13
  import { camelCase, kebabCase, pascalCase } from "change-case";
15
14
  import { GraphQLJSONObject } from "graphql-type-json";
16
15
  import { setName } from "@powerhousedao/shared/document-model";
17
- import { DEFAULT_DRIVE_CONTAINER_TYPES, PropagationMode as PropagationMode$1, consolidateSyncOperations, driveIdFromUrl, envelopesToSyncOperations, parseDriveUrl } from "@powerhousedao/reactor";
16
+ import { DEFAULT_DRIVE_CONTAINER_TYPES, DriveCollectionId, PropagationMode as PropagationMode$1, consolidateSyncOperations, driveIdFromUrl, envelopesToSyncOperations, parseDriveUrl } from "@powerhousedao/reactor";
18
17
  import * as z$1 from "zod";
19
18
  import { z } from "zod";
20
19
  import { createHandler } from "graphql-sse/lib/use/fetch";
@@ -35,6 +34,7 @@ import { tmpdir } from "node:os";
35
34
  import { WebSocketServer } from "ws";
36
35
  import { createRelationalDb } from "@powerhousedao/shared/processors";
37
36
  import { Kysely, Migrator, sql } from "kysely";
37
+ import { verifyAuthBearerToken } from "@renown/sdk";
38
38
  import { PGlite } from "@electric-sql/pglite";
39
39
  import { AtomicNodeFs } from "@powerhousedao/pglite-fs";
40
40
  import knex from "knex";
@@ -85,18 +85,14 @@ async function documentAccess(service, args) {
85
85
  async function userDocumentPermissions(service, userAddress) {
86
86
  return service.getUserDocuments(userAddress);
87
87
  }
88
- async function grantDocumentPermission(service, args, grantedByAddress, isGlobalAdmin) {
88
+ async function grantDocumentPermission(service, authorizationService, args, grantedByAddress) {
89
89
  if (!grantedByAddress) throw new GraphQLError("Authentication required");
90
- if (!isGlobalAdmin) {
91
- if (!await service.canManageDocument(args.documentId, grantedByAddress)) throw new GraphQLError("Forbidden: You must be an admin of this document to grant permissions");
92
- }
90
+ if (!await authorizationService.canManage(args.documentId, grantedByAddress)) throw new GraphQLError("Forbidden: You must be an admin of this document to grant permissions");
93
91
  return service.grantPermission(args.documentId, args.userAddress, args.permission, grantedByAddress);
94
92
  }
95
- async function revokeDocumentPermission(service, args, revokedByAddress, isGlobalAdmin) {
93
+ async function revokeDocumentPermission(service, authorizationService, args, revokedByAddress) {
96
94
  if (!revokedByAddress) throw new GraphQLError("Authentication required");
97
- if (!isGlobalAdmin) {
98
- if (!await service.canManageDocument(args.documentId, revokedByAddress)) throw new GraphQLError("Forbidden: You must be an admin of this document to revoke permissions");
99
- }
95
+ if (!await authorizationService.canManage(args.documentId, revokedByAddress)) throw new GraphQLError("Forbidden: You must be an admin of this document to revoke permissions");
100
96
  await service.revokePermission(args.documentId, args.userAddress);
101
97
  return true;
102
98
  }
@@ -127,18 +123,14 @@ async function removeUserFromGroup(service, args) {
127
123
  async function getGroupMembers(service, groupId) {
128
124
  return service.getGroupMembers(groupId);
129
125
  }
130
- async function grantGroupPermission(service, args, grantedByAddress, isGlobalAdmin) {
126
+ async function grantGroupPermission(service, authorizationService, args, grantedByAddress) {
131
127
  if (!grantedByAddress) throw new GraphQLError("Authentication required");
132
- if (!isGlobalAdmin) {
133
- if (!await service.canManageDocument(args.documentId, grantedByAddress)) throw new GraphQLError("Forbidden: You must be an admin of this document to grant permissions");
134
- }
128
+ if (!await authorizationService.canManage(args.documentId, grantedByAddress)) throw new GraphQLError("Forbidden: You must be an admin of this document to grant permissions");
135
129
  return service.grantGroupPermission(args.documentId, args.groupId, args.permission, grantedByAddress);
136
130
  }
137
- async function revokeGroupPermission(service, args, revokedByAddress, isGlobalAdmin) {
131
+ async function revokeGroupPermission(service, authorizationService, args, revokedByAddress) {
138
132
  if (!revokedByAddress) throw new GraphQLError("Authentication required");
139
- if (!isGlobalAdmin) {
140
- if (!await service.canManageDocument(args.documentId, revokedByAddress)) throw new GraphQLError("Forbidden: You must be an admin of this document to revoke permissions");
141
- }
133
+ if (!await authorizationService.canManage(args.documentId, revokedByAddress)) throw new GraphQLError("Forbidden: You must be an admin of this document to revoke permissions");
142
134
  await service.revokeGroupPermission(args.documentId, args.groupId);
143
135
  return true;
144
136
  }
@@ -152,59 +144,43 @@ async function operationPermissions(service, args) {
152
144
  groupPermissions
153
145
  };
154
146
  }
155
- async function canExecuteOperation(service, args, userAddress) {
156
- return service.canExecuteOperation(args.documentId, args.operationType, userAddress);
147
+ async function canExecuteOperation(authorizationService, args, userAddress) {
148
+ return authorizationService.canMutate(args.documentId, args.operationType, userAddress);
157
149
  }
158
- async function grantOperationPermission(service, args, grantedByAddress, isGlobalAdmin) {
150
+ async function grantOperationPermission(service, authorizationService, args, grantedByAddress) {
159
151
  if (!grantedByAddress) throw new GraphQLError("Authentication required");
160
- if (!isGlobalAdmin) {
161
- if (!await service.canManageDocument(args.documentId, grantedByAddress)) throw new GraphQLError("Forbidden: You must be an admin of this document to grant operation permissions");
162
- }
152
+ if (!await authorizationService.canManage(args.documentId, grantedByAddress)) throw new GraphQLError("Forbidden: You must be an admin of this document to grant operation permissions");
163
153
  return service.grantOperationPermission(args.documentId, args.operationType, args.userAddress, grantedByAddress);
164
154
  }
165
- async function revokeOperationPermission(service, args, revokedByAddress, isGlobalAdmin) {
155
+ async function revokeOperationPermission(service, authorizationService, args, revokedByAddress) {
166
156
  if (!revokedByAddress) throw new GraphQLError("Authentication required");
167
- if (!isGlobalAdmin) {
168
- if (!await service.canManageDocument(args.documentId, revokedByAddress)) throw new GraphQLError("Forbidden: You must be an admin of this document to revoke operation permissions");
169
- }
157
+ if (!await authorizationService.canManage(args.documentId, revokedByAddress)) throw new GraphQLError("Forbidden: You must be an admin of this document to revoke operation permissions");
170
158
  await service.revokeOperationPermission(args.documentId, args.operationType, args.userAddress);
171
159
  return true;
172
160
  }
173
- async function grantGroupOperationPermission(service, args, grantedByAddress, isGlobalAdmin) {
161
+ async function grantGroupOperationPermission(service, authorizationService, args, grantedByAddress) {
174
162
  if (!grantedByAddress) throw new GraphQLError("Authentication required");
175
- if (!isGlobalAdmin) {
176
- if (!await service.canManageDocument(args.documentId, grantedByAddress)) throw new GraphQLError("Forbidden: You must be an admin of this document to grant operation permissions");
177
- }
163
+ if (!await authorizationService.canManage(args.documentId, grantedByAddress)) throw new GraphQLError("Forbidden: You must be an admin of this document to grant operation permissions");
178
164
  return service.grantGroupOperationPermission(args.documentId, args.operationType, args.groupId, grantedByAddress);
179
165
  }
180
- async function revokeGroupOperationPermission(service, args, revokedByAddress, isGlobalAdmin) {
166
+ async function revokeGroupOperationPermission(service, authorizationService, args, revokedByAddress) {
181
167
  if (!revokedByAddress) throw new GraphQLError("Authentication required");
182
- if (!isGlobalAdmin) {
183
- if (!await service.canManageDocument(args.documentId, revokedByAddress)) throw new GraphQLError("Forbidden: You must be an admin of this document to revoke operation permissions");
184
- }
168
+ if (!await authorizationService.canManage(args.documentId, revokedByAddress)) throw new GraphQLError("Forbidden: You must be an admin of this document to revoke operation permissions");
185
169
  await service.revokeGroupOperationPermission(args.documentId, args.operationType, args.groupId);
186
170
  return true;
187
171
  }
188
172
  async function documentProtection(service, args) {
189
173
  return service.getDocumentProtection(args.documentId);
190
174
  }
191
- async function setDocumentProtection(service, authorizationService, args, userAddress, isGlobalAdmin) {
175
+ async function setDocumentProtection(service, authorizationService, args, userAddress) {
192
176
  if (!userAddress) throw new GraphQLError("Authentication required");
193
- if (!isGlobalAdmin) {
194
- if (authorizationService) {
195
- if (!await authorizationService.canManage(args.documentId, userAddress)) throw new GraphQLError("Forbidden: You must be an admin of this document to change protection");
196
- } else if (!await service.canManageDocument(args.documentId, userAddress)) throw new GraphQLError("Forbidden: You must be an admin of this document to change protection");
197
- }
177
+ if (!await authorizationService.canManage(args.documentId, userAddress)) throw new GraphQLError("Forbidden: You must be an admin of this document to change protection");
198
178
  await service.setDocumentProtection(args.documentId, args.protected);
199
179
  return service.getDocumentProtection(args.documentId);
200
180
  }
201
- async function transferDocumentOwnership(service, authorizationService, args, userAddress, isGlobalAdmin) {
181
+ async function transferDocumentOwnership(service, authorizationService, args, userAddress) {
202
182
  if (!userAddress) throw new GraphQLError("Authentication required");
203
- if (!isGlobalAdmin) {
204
- if (authorizationService) {
205
- if (!await authorizationService.canManage(args.documentId, userAddress)) throw new GraphQLError("Forbidden: You must be an admin of this document to transfer ownership");
206
- } else if (!await service.canManageDocument(args.documentId, userAddress)) throw new GraphQLError("Forbidden: You must be an admin of this document to transfer ownership");
207
- }
183
+ if (!await authorizationService.canManage(args.documentId, userAddress)) throw new GraphQLError("Forbidden: You must be an admin of this document to transfer ownership");
208
184
  const previousOwner = await service.getDocumentOwner(args.documentId);
209
185
  if (previousOwner) await service.revokePermission(args.documentId, previousOwner);
210
186
  await service.setDocumentOwner(args.documentId, args.newOwnerAddress);
@@ -297,9 +273,8 @@ var AuthSubgraph = class extends BaseSubgraph {
297
273
  },
298
274
  canExecuteOperation: async (_parent, args, ctx) => {
299
275
  this.logger.debug("canExecuteOperation(@args)", args);
300
- if (!this.documentPermissionService) throw new GraphQLError("DocumentPermissionService not available");
301
276
  try {
302
- return await canExecuteOperation(this.documentPermissionService, args, ctx.user?.address);
277
+ return await canExecuteOperation(this.authorizationService, await this.withCanonicalDocumentId(args, ctx), ctx.user?.address);
303
278
  } catch (error) {
304
279
  this.logger.error("Error in canExecuteOperation: @error", error);
305
280
  throw error;
@@ -322,8 +297,7 @@ var AuthSubgraph = class extends BaseSubgraph {
322
297
  this.logger.debug("setDocumentProtection(@args)", args);
323
298
  if (!this.documentPermissionService) throw new GraphQLError("DocumentPermissionService not available");
324
299
  try {
325
- const isGlobalAdmin = ctx.isAdmin?.(ctx.user?.address ?? "") ?? false;
326
- return await setDocumentProtection(this.documentPermissionService, this.authorizationService, args, ctx.user?.address, isGlobalAdmin);
300
+ return await setDocumentProtection(this.documentPermissionService, this.authorizationService, await this.withCanonicalDocumentId(args, ctx), ctx.user?.address);
327
301
  } catch (error) {
328
302
  this.logger.error("Error in setDocumentProtection: @error", error);
329
303
  throw error;
@@ -333,8 +307,7 @@ var AuthSubgraph = class extends BaseSubgraph {
333
307
  this.logger.debug("transferDocumentOwnership(@args)", args);
334
308
  if (!this.documentPermissionService) throw new GraphQLError("DocumentPermissionService not available");
335
309
  try {
336
- const isGlobalAdmin = ctx.isAdmin?.(ctx.user?.address ?? "") ?? false;
337
- return await transferDocumentOwnership(this.documentPermissionService, this.authorizationService, args, ctx.user?.address, isGlobalAdmin);
310
+ return await transferDocumentOwnership(this.documentPermissionService, this.authorizationService, await this.withCanonicalDocumentId(args, ctx), ctx.user?.address);
338
311
  } catch (error) {
339
312
  this.logger.error("Error in transferDocumentOwnership: @error", error);
340
313
  throw error;
@@ -344,8 +317,11 @@ var AuthSubgraph = class extends BaseSubgraph {
344
317
  this.logger.debug("grantDocumentPermission(@args)", args);
345
318
  if (!this.documentPermissionService) throw new GraphQLError("DocumentPermissionService not available");
346
319
  try {
347
- const isGlobalAdmin = ctx.isAdmin?.(ctx.user?.address ?? "") ?? false;
348
- return await grantDocumentPermission(this.documentPermissionService, args, ctx.user?.address, isGlobalAdmin);
320
+ const resolved = await this.withCanonicalDocumentId(args, ctx);
321
+ return await grantDocumentPermission(this.documentPermissionService, this.authorizationService, {
322
+ ...resolved,
323
+ permission: resolved.permission
324
+ }, ctx.user?.address);
349
325
  } catch (error) {
350
326
  this.logger.error("Error in grantDocumentPermission: @error", error);
351
327
  throw error;
@@ -355,8 +331,7 @@ var AuthSubgraph = class extends BaseSubgraph {
355
331
  this.logger.debug("revokeDocumentPermission(@args)", args);
356
332
  if (!this.documentPermissionService) throw new GraphQLError("DocumentPermissionService not available");
357
333
  try {
358
- const isGlobalAdmin = ctx.isAdmin?.(ctx.user?.address ?? "") ?? false;
359
- return await revokeDocumentPermission(this.documentPermissionService, args, ctx.user?.address, isGlobalAdmin);
334
+ return await revokeDocumentPermission(this.documentPermissionService, this.authorizationService, await this.withCanonicalDocumentId(args, ctx), ctx.user?.address);
360
335
  } catch (error) {
361
336
  this.logger.error("Error in revokeDocumentPermission: @error", error);
362
337
  throw error;
@@ -406,8 +381,11 @@ var AuthSubgraph = class extends BaseSubgraph {
406
381
  this.logger.debug("grantGroupPermission(@args)", args);
407
382
  if (!this.documentPermissionService) throw new GraphQLError("DocumentPermissionService not available");
408
383
  try {
409
- const isGlobalAdmin = ctx.isAdmin?.(ctx.user?.address ?? "") ?? false;
410
- return await grantGroupPermission(this.documentPermissionService, args, ctx.user?.address, isGlobalAdmin);
384
+ const resolved = await this.withCanonicalDocumentId(args, ctx);
385
+ return await grantGroupPermission(this.documentPermissionService, this.authorizationService, {
386
+ ...resolved,
387
+ permission: resolved.permission
388
+ }, ctx.user?.address);
411
389
  } catch (error) {
412
390
  this.logger.error("Error in grantGroupPermission: @error", error);
413
391
  throw error;
@@ -417,8 +395,7 @@ var AuthSubgraph = class extends BaseSubgraph {
417
395
  this.logger.debug("revokeGroupPermission(@args)", args);
418
396
  if (!this.documentPermissionService) throw new GraphQLError("DocumentPermissionService not available");
419
397
  try {
420
- const isGlobalAdmin = ctx.isAdmin?.(ctx.user?.address ?? "") ?? false;
421
- return await revokeGroupPermission(this.documentPermissionService, args, ctx.user?.address, isGlobalAdmin);
398
+ return await revokeGroupPermission(this.documentPermissionService, this.authorizationService, await this.withCanonicalDocumentId(args, ctx), ctx.user?.address);
422
399
  } catch (error) {
423
400
  this.logger.error("Error in revokeGroupPermission: @error", error);
424
401
  throw error;
@@ -428,8 +405,7 @@ var AuthSubgraph = class extends BaseSubgraph {
428
405
  this.logger.debug("grantOperationPermission(@args)", args);
429
406
  if (!this.documentPermissionService) throw new GraphQLError("DocumentPermissionService not available");
430
407
  try {
431
- const isGlobalAdmin = ctx.isAdmin?.(ctx.user?.address ?? "") ?? false;
432
- return await grantOperationPermission(this.documentPermissionService, args, ctx.user?.address, isGlobalAdmin);
408
+ return await grantOperationPermission(this.documentPermissionService, this.authorizationService, await this.withCanonicalDocumentId(args, ctx), ctx.user?.address);
433
409
  } catch (error) {
434
410
  this.logger.error("Error in grantOperationPermission: @error", error);
435
411
  throw error;
@@ -439,8 +415,7 @@ var AuthSubgraph = class extends BaseSubgraph {
439
415
  this.logger.debug("revokeOperationPermission(@args)", args);
440
416
  if (!this.documentPermissionService) throw new GraphQLError("DocumentPermissionService not available");
441
417
  try {
442
- const isGlobalAdmin = ctx.isAdmin?.(ctx.user?.address ?? "") ?? false;
443
- return await revokeOperationPermission(this.documentPermissionService, args, ctx.user?.address, isGlobalAdmin);
418
+ return await revokeOperationPermission(this.documentPermissionService, this.authorizationService, await this.withCanonicalDocumentId(args, ctx), ctx.user?.address);
444
419
  } catch (error) {
445
420
  this.logger.error("Error in revokeOperationPermission: @error", error);
446
421
  throw error;
@@ -450,8 +425,7 @@ var AuthSubgraph = class extends BaseSubgraph {
450
425
  this.logger.debug("grantGroupOperationPermission(@args)", args);
451
426
  if (!this.documentPermissionService) throw new GraphQLError("DocumentPermissionService not available");
452
427
  try {
453
- const isGlobalAdmin = ctx.isAdmin?.(ctx.user?.address ?? "") ?? false;
454
- return await grantGroupOperationPermission(this.documentPermissionService, args, ctx.user?.address, isGlobalAdmin);
428
+ return await grantGroupOperationPermission(this.documentPermissionService, this.authorizationService, await this.withCanonicalDocumentId(args, ctx), ctx.user?.address);
455
429
  } catch (error) {
456
430
  this.logger.error("Error in grantGroupOperationPermission: @error", error);
457
431
  throw error;
@@ -461,8 +435,7 @@ var AuthSubgraph = class extends BaseSubgraph {
461
435
  this.logger.debug("revokeGroupOperationPermission(@args)", args);
462
436
  if (!this.documentPermissionService) throw new GraphQLError("DocumentPermissionService not available");
463
437
  try {
464
- const isGlobalAdmin = ctx.isAdmin?.(ctx.user?.address ?? "") ?? false;
465
- return await revokeGroupOperationPermission(this.documentPermissionService, args, ctx.user?.address, isGlobalAdmin);
438
+ return await revokeGroupOperationPermission(this.documentPermissionService, this.authorizationService, await this.withCanonicalDocumentId(args, ctx), ctx.user?.address);
466
439
  } catch (error) {
467
440
  this.logger.error("Error in revokeGroupOperationPermission: @error", error);
468
441
  throw error;
@@ -533,133 +506,6 @@ async function createHttpAdapter(type) {
533
506
  }
534
507
  }
535
508
  //#endregion
536
- //#region src/services/auth.service.ts
537
- var AuthService = class {
538
- config;
539
- constructor(config) {
540
- this.config = config;
541
- }
542
- async authenticateRequest(request) {
543
- if (!this.config.enabled) return {
544
- user: void 0,
545
- admins: [],
546
- auth_enabled: false
547
- };
548
- const method = request.method;
549
- if (method === "OPTIONS" || method === "GET") return {
550
- user: void 0,
551
- admins: this.config.admins,
552
- auth_enabled: true
553
- };
554
- return this.verifyBearer(request.headers.get("authorization") ?? void 0);
555
- }
556
- /**
557
- * Verify a Bearer token regardless of HTTP method. Use this from non-GraphQL
558
- * middleware that must enforce authentication on every request.
559
- */
560
- async verifyBearer(authorization) {
561
- if (!this.config.enabled) return {
562
- user: void 0,
563
- admins: [],
564
- auth_enabled: false
565
- };
566
- const token = authorization?.split(" ")[1];
567
- if (!token) return {
568
- user: void 0,
569
- admins: this.config.admins,
570
- auth_enabled: true
571
- };
572
- try {
573
- const verified = await this.verifyToken(token);
574
- if (!verified) return new Response(JSON.stringify({ error: "Verification failed" }), { status: 401 });
575
- const user = this.extractUserFromVerification(verified);
576
- if (!user) return new Response(JSON.stringify({ error: "Missing credentials" }), { status: 401 });
577
- if (!this.config.skipCredentialVerification) {
578
- if (!await this.verifyCredentialExists(user.address, user.chainId, verified.issuer)) return new Response(JSON.stringify({ error: "Credentials no longer valid" }), { status: 401 });
579
- }
580
- return {
581
- user,
582
- admins: this.config.admins,
583
- auth_enabled: true
584
- };
585
- } catch {
586
- return new Response(JSON.stringify({ error: "Authentication failed" }), { status: 401 });
587
- }
588
- }
589
- async authenticateWebSocketConnection(connectionParams) {
590
- if (!this.config.enabled) return null;
591
- const authHeader = connectionParams.authorization;
592
- if (!authHeader) throw new Error("Missing authorization in connection parameters");
593
- const token = authHeader.split(" ")[1];
594
- if (!token) throw new Error("Invalid authorization format");
595
- const verified = await this.verifyToken(token);
596
- if (!verified) throw new Error("Token verification failed");
597
- const user = this.extractUserFromVerification(verified);
598
- if (!user) throw new Error("Invalid credentials");
599
- if (!this.config.skipCredentialVerification) {
600
- if (!await this.verifyCredentialExists(user.address, user.chainId, verified.issuer)) throw new Error("Credentials no longer valid");
601
- }
602
- return user;
603
- }
604
- /**
605
- * Verify the auth bearer token
606
- */
607
- async verifyToken(token) {
608
- return await verifyAuthBearerToken(token);
609
- }
610
- /**
611
- * Extract user information from verification result
612
- */
613
- extractUserFromVerification(verified) {
614
- try {
615
- const { address, chainId, networkId } = verified.verifiableCredential.credentialSubject;
616
- if (!address || !chainId || !networkId) return null;
617
- return {
618
- address,
619
- chainId,
620
- networkId
621
- };
622
- } catch {
623
- return null;
624
- }
625
- }
626
- /**
627
- * Get additional context fields for GraphQL
628
- */
629
- getAdditionalContextFields() {
630
- if (!this.config.enabled) return { isAdmin: () => true };
631
- return { isAdmin: (address) => this.config.enabled && this.config.admins?.includes(address.toLowerCase()) };
632
- }
633
- /**
634
- * Get user context for GraphQL
635
- */
636
- getUserContext(user) {
637
- if (!user) return {};
638
- return { user: {
639
- address: user.address.toLowerCase(),
640
- chainId: user.chainId,
641
- networkId: user.networkId
642
- } };
643
- }
644
- /**
645
- * Verify that the credential still exists on the Renown API
646
- */
647
- async verifyCredentialExists(address, chainId, appId) {
648
- const url = `https://www.renown.id/api/auth/credential?address=${address}&chainId=${chainId}&connectId=${appId}&appId=${appId}`;
649
- try {
650
- const response = await fetch(url, { method: "GET" });
651
- const credential = (await response.json()).credential;
652
- const appIdVerfied = credential.credentialSubject.id;
653
- const addressVerfied = credential.issuer.id.split(":")[4];
654
- const chainIdVerfied = credential.issuer.id.split(":")[3];
655
- if (response.status !== 200) return false;
656
- return appIdVerfied === appId && addressVerfied.toLocaleLowerCase() === address.toLocaleLowerCase() && chainIdVerfied === chainId.toString();
657
- } catch {
658
- return false;
659
- }
660
- }
661
- };
662
- //#endregion
663
509
  //#region src/utils/create-schema.ts
664
510
  const logger = childLogger(["reactor-api", "create-schema"]);
665
511
  /**
@@ -1246,7 +1092,6 @@ function ActionContextInputSchema() {
1246
1092
  }
1247
1093
  function ActionInputSchema() {
1248
1094
  return z$1.object({
1249
- attachments: z$1.array(z$1.lazy(() => AttachmentInputSchema())).nullish(),
1250
1095
  context: z$1.lazy(() => ActionContextInputSchema().nullish()),
1251
1096
  id: z$1.string(),
1252
1097
  input: z$1.custom((v) => v != null),
@@ -1255,15 +1100,6 @@ function ActionInputSchema() {
1255
1100
  type: z$1.string()
1256
1101
  });
1257
1102
  }
1258
- function AttachmentInputSchema() {
1259
- return z$1.object({
1260
- data: z$1.string(),
1261
- extension: z$1.string().nullish(),
1262
- fileName: z$1.string().nullish(),
1263
- hash: z$1.string(),
1264
- mimeType: z$1.string()
1265
- });
1266
- }
1267
1103
  function ChannelMetaInputSchema() {
1268
1104
  return z$1.object({ id: z$1.string() });
1269
1105
  }
@@ -1454,13 +1290,6 @@ const GetDocumentWithOperationsDocument = gql`
1454
1290
  timestampUtcMs
1455
1291
  input
1456
1292
  scope
1457
- attachments {
1458
- data
1459
- mimeType
1460
- hash
1461
- extension
1462
- fileName
1463
- }
1464
1293
  context {
1465
1294
  signer {
1466
1295
  user {
@@ -1573,13 +1402,6 @@ const GetDocumentOperationsDocument = gql`
1573
1402
  timestampUtcMs
1574
1403
  input
1575
1404
  scope
1576
- attachments {
1577
- data
1578
- mimeType
1579
- hash
1580
- extension
1581
- fileName
1582
- }
1583
1405
  context {
1584
1406
  signer {
1585
1407
  user {
@@ -1830,13 +1652,6 @@ const PollSyncEnvelopesDocument = gql`
1830
1652
  timestampUtcMs
1831
1653
  input
1832
1654
  scope
1833
- attachments {
1834
- data
1835
- mimeType
1836
- hash
1837
- extension
1838
- fileName
1839
- }
1840
1655
  context {
1841
1656
  signer {
1842
1657
  user {
@@ -2153,6 +1968,17 @@ function toGqlDocumentChangeEvent(event) {
2153
1968
  } : null
2154
1969
  };
2155
1970
  }
1971
+ /**
1972
+ * Distinct document ids a change event references: affected documents plus the
1973
+ * parent/child ids in `context` (delete/relationship events carry only context).
1974
+ */
1975
+ function collectChangedDocumentIds(event) {
1976
+ const ids = /* @__PURE__ */ new Set();
1977
+ for (const doc of event.documents) if (doc.header.id) ids.add(doc.header.id);
1978
+ if (event.context?.parentId) ids.add(event.context.parentId);
1979
+ if (event.context?.childId) ids.add(event.context.childId);
1980
+ return [...ids];
1981
+ }
2156
1982
  function matchesSearchFilter(event, search) {
2157
1983
  if (search.type) {
2158
1984
  if (!event.documents.some((doc) => doc.header.documentType === search.type)) return false;
@@ -2624,7 +2450,7 @@ async function touchChannel(syncManager, args) {
2624
2450
  };
2625
2451
  const options = { sinceTimestampUtcMs: args.input.sinceTimestampUtcMs };
2626
2452
  try {
2627
- await syncManager.add(args.input.name, args.input.collectionId, {
2453
+ await syncManager.add(args.input.name, DriveCollectionId.fromKey(args.input.collectionId), {
2628
2454
  type: "polling",
2629
2455
  parameters: {}
2630
2456
  }, filter, options, args.input.id);
@@ -2647,14 +2473,14 @@ async function touchChannel(syncManager, args) {
2647
2473
  * client ordinal the switchboard has successfully applied, so the client
2648
2474
  * can trim its own outbox)
2649
2475
  */
2650
- function pollSyncEnvelopes(syncManager, args) {
2476
+ function pollSyncEnvelopes(syncManager, args, forbiddenIds = /* @__PURE__ */ new Set()) {
2651
2477
  let remote;
2652
2478
  try {
2653
2479
  remote = syncManager.getById(args.channelId);
2654
2480
  } catch (error) {
2655
2481
  throw new GraphQLError(`Channel not found: ${error instanceof Error ? error.message : "Unknown error"}`);
2656
2482
  }
2657
- const deadLetters = remote.channel.deadLetter.items.map((syncOp) => ({
2483
+ const deadLetters = remote.channel.deadLetter.items.filter((syncOp) => !forbiddenIds.has(syncOp.documentId)).map((syncOp) => ({
2658
2484
  documentId: syncOp.documentId,
2659
2485
  error: syncOp.error?.message ?? "Unknown error",
2660
2486
  jobId: syncOp.jobId,
@@ -2678,6 +2504,12 @@ function pollSyncEnvelopes(syncManager, args) {
2678
2504
  }
2679
2505
  }
2680
2506
  const operations = remote.channel.outbox.items;
2507
+ if (forbiddenIds.size > 0) {
2508
+ for (const syncOp of operations) if (forbiddenIds.has(syncOp.documentId)) {
2509
+ syncOp.deliveredCount = syncOp.operations.length;
2510
+ syncOp.emittedCount = syncOp.operations.length;
2511
+ }
2512
+ }
2681
2513
  if (operations.length === 0) return {
2682
2514
  envelopes: [],
2683
2515
  ackOrdinal: remote.channel.inbox.ackOrdinal,
@@ -2869,7 +2701,7 @@ var DocumentModelSubgraph = class extends BaseSubgraph {
2869
2701
  view
2870
2702
  });
2871
2703
  if (result.document.documentType !== documentType) throw new GraphQLError(`Document with id ${identifier} is not of type ${documentType}`);
2872
- await this.assertCanRead(result.document.id, ctx);
2704
+ await this.assertCanReadCanonical(result.document.id, ctx);
2873
2705
  return result;
2874
2706
  },
2875
2707
  documents: async (_, args, ctx) => {
@@ -2878,7 +2710,7 @@ var DocumentModelSubgraph = class extends BaseSubgraph {
2878
2710
  search: { type: documentType },
2879
2711
  paging
2880
2712
  });
2881
- if (!this.hasGlobalAdminAccess(ctx) && this.documentPermissionService) {
2713
+ if (!this.authorizationService.isSupremeAdmin(ctx.user?.address)) {
2882
2714
  const filteredItems = [];
2883
2715
  for (const item of result.items) if (await this.canReadDocument(item.id, ctx)) filteredItems.push(item);
2884
2716
  return {
@@ -2899,7 +2731,7 @@ var DocumentModelSubgraph = class extends BaseSubgraph {
2899
2731
  view,
2900
2732
  paging
2901
2733
  });
2902
- if (!this.hasGlobalAdminAccess(ctx) && this.documentPermissionService) {
2734
+ if (!this.authorizationService.isSupremeAdmin(ctx.user?.address)) {
2903
2735
  const filteredItems = [];
2904
2736
  for (const item of result.items) if (await this.canReadDocument(item.id, ctx)) filteredItems.push(item);
2905
2737
  return {
@@ -2911,10 +2743,10 @@ var DocumentModelSubgraph = class extends BaseSubgraph {
2911
2743
  return result;
2912
2744
  },
2913
2745
  documentOutgoingRelationships: async (_, args, ctx) => {
2914
- const { sourceIdentifier, relationshipType, view, paging } = args;
2915
- await this.assertCanRead(sourceIdentifier, ctx);
2746
+ const { relationshipType, view, paging } = args;
2747
+ const handle = await this.assertCanRead(args.sourceIdentifier, ctx);
2916
2748
  const result = await documentOutgoingRelationships(this.reactorClient, {
2917
- sourceIdentifier,
2749
+ sourceIdentifier: handle.fetchIdentifier,
2918
2750
  relationshipType,
2919
2751
  view,
2920
2752
  paging
@@ -2927,10 +2759,10 @@ var DocumentModelSubgraph = class extends BaseSubgraph {
2927
2759
  };
2928
2760
  },
2929
2761
  documentIncomingRelationships: async (_, args, ctx) => {
2930
- const { targetIdentifier, relationshipType, view, paging } = args;
2931
- await this.assertCanRead(targetIdentifier, ctx);
2762
+ const { relationshipType, view, paging } = args;
2763
+ const handle = await this.assertCanRead(args.targetIdentifier, ctx);
2932
2764
  return documentIncomingRelationships(this.reactorClient, {
2933
- targetIdentifier,
2765
+ targetIdentifier: handle.fetchIdentifier,
2934
2766
  relationshipType,
2935
2767
  view,
2936
2768
  paging
@@ -2940,11 +2772,10 @@ var DocumentModelSubgraph = class extends BaseSubgraph {
2940
2772
  Mutation: { [documentName]: () => ({}) },
2941
2773
  [`${documentName}Mutations`]: {
2942
2774
  createDocument: async (_, args, ctx) => {
2943
- const { parentIdentifier, name, slug, preferredEditor, initialState } = args;
2944
- if (parentIdentifier) await this.assertCanWrite(parentIdentifier, ctx);
2945
- else if (this.authorizationService) {
2946
- if (!ctx.user?.address) throw new GraphQLError("Forbidden: authentication required to create documents");
2947
- } else if (!this.hasGlobalAdminAccess(ctx)) throw new GraphQLError("Forbidden: insufficient permissions to create documents");
2775
+ const { name, slug, preferredEditor, initialState } = args;
2776
+ let parentIdentifier = args.parentIdentifier;
2777
+ if (parentIdentifier) parentIdentifier = (await this.assertCanWrite(parentIdentifier, ctx)).fetchIdentifier;
2778
+ else this.assertCanCreate(ctx);
2948
2779
  let createdDoc;
2949
2780
  if (initialState || preferredEditor) createdDoc = await createDocumentWithInitialState(this.reactorClient, {
2950
2781
  documentType,
@@ -2959,46 +2790,42 @@ var DocumentModelSubgraph = class extends BaseSubgraph {
2959
2790
  parentIdentifier,
2960
2791
  name
2961
2792
  }, this.graphqlManager.reactorDriveClient);
2962
- if (this.authorizationService && ctx.user?.address && createdDoc?.id) await this.documentPermissionService?.initializeDocumentProtection(createdDoc.id, ctx.user.address, this.authorizationService.config.defaultProtection);
2793
+ if (ctx.user?.address && createdDoc?.id) await this.documentPermissionService?.initializeDocumentProtection(createdDoc.id, ctx.user.address, this.authorizationService.config.defaultProtection);
2963
2794
  if (!initialState && !preferredEditor && name && createdDoc.name !== name) return toGqlPhDocument(await this.reactorClient.execute(createdDoc.id, "main", [setName(name)]));
2964
2795
  return createdDoc;
2965
2796
  },
2966
2797
  createEmptyDocument: async (_, args, ctx) => {
2967
- const { parentIdentifier } = args;
2968
- if (parentIdentifier) await this.assertCanWrite(parentIdentifier, ctx);
2969
- else if (this.authorizationService) {
2970
- if (!ctx.user?.address) throw new GraphQLError("Forbidden: authentication required to create documents");
2971
- } else if (!this.hasGlobalAdminAccess(ctx)) throw new GraphQLError("Forbidden: insufficient permissions to create documents");
2798
+ let parentIdentifier = args.parentIdentifier;
2799
+ if (parentIdentifier) parentIdentifier = (await this.assertCanWrite(parentIdentifier, ctx)).fetchIdentifier;
2800
+ else this.assertCanCreate(ctx);
2972
2801
  const result = await createEmptyDocument(this.reactorClient, {
2973
2802
  documentType,
2974
2803
  parentIdentifier
2975
2804
  }, this.graphqlManager.reactorDriveClient);
2976
- if (this.authorizationService && ctx.user?.address && result?.id) await this.documentPermissionService?.initializeDocumentProtection(result.id, ctx.user.address, this.authorizationService.config.defaultProtection);
2805
+ if (ctx.user?.address && result?.id) await this.documentPermissionService?.initializeDocumentProtection(result.id, ctx.user.address, this.authorizationService.config.defaultProtection);
2977
2806
  return result;
2978
2807
  },
2979
2808
  ...operations.reduce((mutations, op) => {
2980
2809
  mutations[camelCase(op.name)] = async (_, args, ctx) => {
2981
2810
  const { docId, input } = args;
2982
- if (!this.authorizationService) await this.assertCanWrite(docId, ctx);
2983
- await this.assertCanExecuteOperation(docId, op.name, ctx);
2984
- if ((await this.reactorClient.get(docId)).header.documentType !== documentType) throw new GraphQLError(`Document with id ${docId} is not of type ${documentType}`);
2811
+ const effectiveDocId = (await this.assertCanExecuteOperation(docId, op.name, ctx)).fetchIdentifier;
2812
+ if ((await this.reactorClient.get(effectiveDocId)).header.documentType !== documentType) throw new GraphQLError(`Document with id ${docId} is not of type ${documentType}`);
2985
2813
  const action = this.documentModel.actions[camelCase(op.name)];
2986
2814
  if (!action) throw new GraphQLError(`Action ${op.name} not found`);
2987
2815
  try {
2988
- return toGqlPhDocument(await this.reactorClient.execute(docId, "main", [action(input)]));
2816
+ return toGqlPhDocument(await this.reactorClient.execute(effectiveDocId, "main", [action(input)]));
2989
2817
  } catch (error) {
2990
2818
  throw new GraphQLError(error instanceof Error ? error.message : `Failed to ${op.name}`);
2991
2819
  }
2992
2820
  };
2993
2821
  mutations[`${camelCase(op.name)}Async`] = async (_, args, ctx) => {
2994
2822
  const { docId, input } = args;
2995
- if (!this.authorizationService) await this.assertCanWrite(docId, ctx);
2996
- await this.assertCanExecuteOperation(docId, op.name, ctx);
2997
- if ((await this.reactorClient.get(docId)).header.documentType !== documentType) throw new GraphQLError(`Document with id ${docId} is not of type ${documentType}`);
2823
+ const effectiveDocId = (await this.assertCanExecuteOperation(docId, op.name, ctx)).fetchIdentifier;
2824
+ if ((await this.reactorClient.get(effectiveDocId)).header.documentType !== documentType) throw new GraphQLError(`Document with id ${docId} is not of type ${documentType}`);
2998
2825
  const action = this.documentModel.actions[camelCase(op.name)];
2999
2826
  if (!action) throw new GraphQLError(`Action ${op.name} not found`);
3000
2827
  try {
3001
- return (await this.reactorClient.executeAsync(docId, "main", [action(input)])).id;
2828
+ return (await this.reactorClient.executeAsync(effectiveDocId, "main", [action(input)])).id;
3002
2829
  } catch (error) {
3003
2830
  throw new GraphQLError(error instanceof Error ? error.message : `Failed to ${op.name}`);
3004
2831
  }
@@ -3177,7 +3004,6 @@ var GraphQLManager = class {
3177
3004
  coreSubgraphsMap = /* @__PURE__ */ new Map();
3178
3005
  contextFields = {};
3179
3006
  subgraphs = /* @__PURE__ */ new Map();
3180
- authService = null;
3181
3007
  subgraphWsDisposers = /* @__PURE__ */ new Map();
3182
3008
  #authMiddleware;
3183
3009
  #driveMiddleware;
@@ -3191,7 +3017,8 @@ var GraphQLManager = class {
3191
3017
  * it for reactor-drive parents.
3192
3018
  */
3193
3019
  reactorDriveClient;
3194
- constructor(path, httpServer, wsServer, reactorClient, relationalDb, analyticsStore, syncManager, logger, httpAdapter, gatewayAdapter, authConfig, documentPermissionService, featureFlags = DefaultFeatureFlags, port = 4001, authorizationService, reactorDriveClient) {
3020
+ authorizationService;
3021
+ constructor(path, httpServer, wsServer, reactorClient, relationalDb, analyticsStore, syncManager, logger, httpAdapter, gatewayAdapter, authService, documentPermissionService, featureFlags = DefaultFeatureFlags, port = 4001, authorizationService, reactorDriveClient) {
3195
3022
  this.path = path;
3196
3023
  this.httpServer = httpServer;
3197
3024
  this.wsServer = wsServer;
@@ -3202,13 +3029,13 @@ var GraphQLManager = class {
3202
3029
  this.logger = logger;
3203
3030
  this.httpAdapter = httpAdapter;
3204
3031
  this.gatewayAdapter = gatewayAdapter;
3205
- this.authConfig = authConfig;
3032
+ this.authService = authService;
3206
3033
  this.documentPermissionService = documentPermissionService;
3207
3034
  this.featureFlags = featureFlags;
3208
3035
  this.port = port;
3036
+ if (!authorizationService) throw new Error("GraphQLManager requires an authorizationService");
3209
3037
  this.authorizationService = authorizationService;
3210
3038
  this.reactorDriveClient = reactorDriveClient;
3211
- if (this.authConfig) this.authService = new AuthService(this.authConfig);
3212
3039
  this.driveOwnershipCache = new DriveOwnershipCache(this.reactorClient);
3213
3040
  this.wsServer.setMaxListeners(0);
3214
3041
  }
@@ -3328,6 +3155,14 @@ var GraphQLManager = class {
3328
3155
  getBasePath() {
3329
3156
  return this.path;
3330
3157
  }
3158
+ /**
3159
+ * Get the authorization service shared with subgraphs. Use this when
3160
+ * constructing a subgraph instance externally for
3161
+ * {@link registerSubgraphInstance}.
3162
+ */
3163
+ getAuthorizationService() {
3164
+ return this.authorizationService;
3165
+ }
3331
3166
  async registerSubgraph(subgraph, supergraph = "", core = false) {
3332
3167
  const subgraphInstance = new subgraph({
3333
3168
  relationalDb: this.relationalDb,
@@ -3387,8 +3222,7 @@ var GraphQLManager = class {
3387
3222
  db: this.relationalDb,
3388
3223
  ...this.getAdditionalContextFields(),
3389
3224
  driveId,
3390
- user: authCtx?.user,
3391
- isAdmin: authCtx ? (addr) => !authCtx.auth_enabled ? true : authCtx.admins.includes(addr.toLowerCase()) : () => true
3225
+ user: authCtx?.user
3392
3226
  });
3393
3227
  };
3394
3228
  }
@@ -3532,8 +3366,8 @@ var GraphQLManager = class {
3532
3366
  };
3533
3367
  //#endregion
3534
3368
  //#region src/graphql/packages/resolvers.ts
3535
- function requireAdmin(ctx) {
3536
- if (!(ctx.isAdmin?.(ctx.user?.address ?? "") ?? false)) throw new GraphQLError("Admin access required");
3369
+ function requireAdmin(authorizationService, ctx) {
3370
+ if (!authorizationService.isSupremeAdmin(ctx.user?.address)) throw new GraphQLError("Admin access required");
3537
3371
  }
3538
3372
  function formatPackageInfo(info) {
3539
3373
  return {
@@ -3551,16 +3385,16 @@ async function installedPackage(service, args) {
3551
3385
  const pkg = await service.getInstalledPackage(args.name);
3552
3386
  return pkg ? formatPackageInfo(pkg) : null;
3553
3387
  }
3554
- async function installPackage(service, args, ctx) {
3555
- requireAdmin(ctx);
3388
+ async function installPackage(service, authorizationService, args, ctx) {
3389
+ requireAdmin(authorizationService, ctx);
3556
3390
  const result = await service.installPackage(args.name, args.registryUrl ?? void 0);
3557
3391
  return {
3558
3392
  package: formatPackageInfo(result.package),
3559
3393
  documentModelsLoaded: result.documentModelsLoaded
3560
3394
  };
3561
3395
  }
3562
- async function uninstallPackage(service, args, ctx) {
3563
- requireAdmin(ctx);
3396
+ async function uninstallPackage(service, authorizationService, args, ctx) {
3397
+ requireAdmin(authorizationService, ctx);
3564
3398
  return service.uninstallPackage(args.name);
3565
3399
  }
3566
3400
  //#endregion
@@ -3606,7 +3440,7 @@ var PackagesSubgraph = class extends BaseSubgraph {
3606
3440
  installPackage: async (_parent, args, ctx) => {
3607
3441
  this.logger.debug("installPackage(@args)", args);
3608
3442
  try {
3609
- return await installPackage(this.packageManagementService, args, ctx);
3443
+ return await installPackage(this.packageManagementService, this.authorizationService, args, ctx);
3610
3444
  } catch (error) {
3611
3445
  this.logger.error("Error in installPackage: @error", error);
3612
3446
  throw error;
@@ -3615,7 +3449,7 @@ var PackagesSubgraph = class extends BaseSubgraph {
3615
3449
  uninstallPackage: async (_parent, args, ctx) => {
3616
3450
  this.logger.debug("uninstallPackage(@args)", args);
3617
3451
  try {
3618
- return await uninstallPackage(this.packageManagementService, args, ctx);
3452
+ return await uninstallPackage(this.packageManagementService, this.authorizationService, args, ctx);
3619
3453
  } catch (error) {
3620
3454
  this.logger.error("Error in uninstallPackage: @error", error);
3621
3455
  throw error;
@@ -3801,20 +3635,12 @@ const ActionSignerDTO = z.object({
3801
3635
  app: ActionSignerAppDTO.nullable().optional()
3802
3636
  }).strip();
3803
3637
  const ActionContextDTO = z.object({ signer: ActionSignerDTO.nullable().optional() }).strip();
3804
- const AttachmentDTO = z.object({
3805
- data: z.string(),
3806
- mimeType: z.string(),
3807
- hash: z.string(),
3808
- extension: z.string().nullable().optional(),
3809
- fileName: z.string().nullable().optional()
3810
- }).strip();
3811
3638
  const OperationActionDTO = z.object({
3812
3639
  id: z.string(),
3813
3640
  type: z.string(),
3814
3641
  timestampUtcMs: z.string(),
3815
3642
  input: z.unknown(),
3816
3643
  scope: z.string(),
3817
- attachments: z.array(AttachmentDTO).nullable().optional(),
3818
3644
  context: ActionContextDTO.nullable().optional()
3819
3645
  }).strip();
3820
3646
  const OperationDTO = z.object({
@@ -3941,7 +3767,7 @@ function createReactorGraphQLClient(url, fetchImpl = fetch, headers) {
3941
3767
  }
3942
3768
  //#endregion
3943
3769
  //#region src/graphql/reactor/schema.graphql
3944
- 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 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 result: JSONObject!\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 attachments: [Attachment!]\n context: ActionContext\n}\n\ntype Attachment {\n data: String!\n mimeType: String!\n hash: String!\n extension: String\n fileName: String\n}\n\n# Input types for sync operations\ninput ActionContextInput {\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 attachments: [AttachmentInput!]\n context: ActionContextInput\n}\n\ninput AttachmentInput {\n data: String!\n mimeType: String!\n hash: String!\n extension: String\n fileName: String\n}\n\n# Synchronization types\ntype ReactorOperation {\n index: Int!\n timestampUtcMs: String!\n hash: String!\n skip: Int!\n error: 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 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 # Poll for sync envelopes from a channel\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 # Apply actions to a document (synchronous)\n mutateDocument(\n documentIdentifier: String!\n actions: [JSONObject!]!\n view: ViewFilterInput\n ): PHDocument!\n\n # Submit actions to a document (asynchronous)\n mutateDocumentAsync(\n documentIdentifier: String!\n actions: [JSONObject!]!\n view: ViewFilterInput\n ): String!\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";
3770
+ 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 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 result: JSONObject!\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# Input types for sync operations\ninput ActionContextInput {\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 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 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 # Poll for sync envelopes from a channel\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 # Apply actions to a document (synchronous)\n mutateDocument(\n documentIdentifier: String!\n actions: [JSONObject!]!\n view: ViewFilterInput\n ): PHDocument!\n\n # Submit actions to a document (asynchronous)\n mutateDocumentAsync(\n documentIdentifier: String!\n actions: [JSONObject!]!\n view: ViewFilterInput\n ): String!\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";
3945
3771
  //#endregion
3946
3772
  //#region src/graphql/reactor/pubsub.ts
3947
3773
  const pubSub = new PubSub();
@@ -3990,7 +3816,8 @@ function ensureJobSubscription(reactorClient, jobId) {
3990
3816
  error: jobInfo.error?.message ?? null,
3991
3817
  result: jobInfo.result ?? {}
3992
3818
  },
3993
- jobId
3819
+ jobId,
3820
+ documentId: jobInfo.documentId
3994
3821
  };
3995
3822
  pubSub.publish(SUBSCRIPTION_TRIGGERS.JOB_CHANGES, payload);
3996
3823
  const isTerminal = String(jobInfo.status) === "FAILED" || String(jobInfo.status) === "READ_MODELS_READY" || jobInfo.completedAtUtcIso !== void 0;
@@ -4040,12 +3867,14 @@ var ReactorSubgraph = class extends BaseSubgraph {
4040
3867
  * Delegates to base assertCanExecuteOperation for each action.
4041
3868
  */
4042
3869
  async assertCanExecuteOperations(documentId, actions, ctx) {
3870
+ let handle = AuthorizedDocumentHandle.skipped(documentId);
4043
3871
  for (const action of actions) {
4044
3872
  if (!action || typeof action !== "object") continue;
4045
3873
  const operationType = action.type;
4046
3874
  if (typeof operationType !== "string") continue;
4047
- await this.assertCanExecuteOperation(documentId, operationType, ctx);
3875
+ handle = await this.assertCanExecuteOperation(documentId, operationType, ctx);
4048
3876
  }
3877
+ return handle;
4049
3878
  }
4050
3879
  /**
4051
3880
  * Returns the drive id when the given identifier (id or slug) refers
@@ -4062,11 +3891,25 @@ var ReactorSubgraph = class extends BaseSubgraph {
4062
3891
  return;
4063
3892
  }
4064
3893
  }
3894
+ /**
3895
+ * Adds to `forbidden` the canonical document ids in `syncOps` that the caller
3896
+ * cannot read, checking each distinct id once. Sync operation document ids are
3897
+ * canonical (never slugs), so no resolution is needed.
3898
+ */
3899
+ async #collectForbiddenDocuments(syncOps, forbidden, ctx) {
3900
+ const checked = /* @__PURE__ */ new Set();
3901
+ for (const syncOp of syncOps) {
3902
+ const documentId = syncOp.documentId;
3903
+ if (checked.has(documentId) || forbidden.has(documentId)) continue;
3904
+ checked.add(documentId);
3905
+ if (!await this.canReadDocument(documentId, ctx)) forbidden.add(documentId);
3906
+ }
3907
+ }
4065
3908
  typeDefs = gql(schema_default);
4066
3909
  resolvers = {
4067
3910
  PHDocument: { operations: async (parent, args, ctx) => {
4068
3911
  this.logger.debug("PHDocument.operations(@parent.id, @args)", parent.id, args);
4069
- await this.assertCanRead(parent.id, ctx);
3912
+ await this.assertCanReadCanonical(parent.id, ctx);
4070
3913
  try {
4071
3914
  const filter = {
4072
3915
  documentId: parent.id,
@@ -4099,8 +3942,11 @@ var ReactorSubgraph = class extends BaseSubgraph {
4099
3942
  document: async (_parent, args, ctx) => {
4100
3943
  this.logger.debug("document(@args)", args);
4101
3944
  try {
4102
- await this.assertCanRead(args.identifier, ctx);
4103
- return await document(this.reactorClient, args);
3945
+ const handle = await this.assertCanRead(args.identifier, ctx);
3946
+ return await document(this.reactorClient, {
3947
+ ...args,
3948
+ identifier: handle.fetchIdentifier
3949
+ });
4104
3950
  } catch (error) {
4105
3951
  this.logger.error("Error in document: @Error", error);
4106
3952
  throw error;
@@ -4109,8 +3955,11 @@ var ReactorSubgraph = class extends BaseSubgraph {
4109
3955
  documentOutgoingRelationships: async (_parent, args, ctx) => {
4110
3956
  this.logger.debug("documentOutgoingRelationships(@args)", args);
4111
3957
  try {
4112
- await this.assertCanRead(args.sourceIdentifier, ctx);
4113
- return await documentOutgoingRelationships(this.reactorClient, args);
3958
+ const handle = await this.assertCanRead(args.sourceIdentifier, ctx);
3959
+ return await documentOutgoingRelationships(this.reactorClient, {
3960
+ ...args,
3961
+ sourceIdentifier: handle.fetchIdentifier
3962
+ });
4114
3963
  } catch (error) {
4115
3964
  this.logger.error("Error in documentOutgoingRelationships: @Error", error);
4116
3965
  throw error;
@@ -4119,9 +3968,12 @@ var ReactorSubgraph = class extends BaseSubgraph {
4119
3968
  documentIncomingRelationships: async (_parent, args, ctx) => {
4120
3969
  this.logger.debug("documentIncomingRelationships(@args)", args);
4121
3970
  try {
4122
- await this.assertCanRead(args.targetIdentifier, ctx);
4123
- const result = await documentIncomingRelationships(this.reactorClient, args);
4124
- if (!this.hasGlobalAdminAccess(ctx) && this.documentPermissionService) {
3971
+ const handle = await this.assertCanRead(args.targetIdentifier, ctx);
3972
+ const result = await documentIncomingRelationships(this.reactorClient, {
3973
+ ...args,
3974
+ targetIdentifier: handle.fetchIdentifier
3975
+ });
3976
+ if (!this.authorizationService.isSupremeAdmin(ctx.user?.address)) {
4125
3977
  const filteredItems = [];
4126
3978
  for (const item of result.items) if (await this.canReadDocument(item.id, ctx)) filteredItems.push(item);
4127
3979
  return {
@@ -4142,7 +3994,7 @@ var ReactorSubgraph = class extends BaseSubgraph {
4142
3994
  ...args,
4143
3995
  search: args.search ?? {}
4144
3996
  });
4145
- if (!this.hasGlobalAdminAccess(ctx) && this.documentPermissionService) {
3997
+ if (!this.authorizationService.isSupremeAdmin(ctx.user?.address)) {
4146
3998
  const filteredItems = [];
4147
3999
  for (const item of result.items) if (await this.canReadDocument(item.id, ctx)) filteredItems.push(item);
4148
4000
  return {
@@ -4168,17 +4020,37 @@ var ReactorSubgraph = class extends BaseSubgraph {
4168
4020
  documentOperations: async (_parent, args, ctx) => {
4169
4021
  this.logger.debug("documentOperations(@args)", args);
4170
4022
  try {
4171
- await this.assertCanRead(args.filter.documentId, ctx);
4172
- return await documentOperations(this.reactorClient, args);
4023
+ const handle = await this.assertCanRead(args.filter.documentId, ctx);
4024
+ return await documentOperations(this.reactorClient, {
4025
+ ...args,
4026
+ filter: {
4027
+ ...args.filter,
4028
+ documentId: handle.fetchIdentifier
4029
+ }
4030
+ });
4173
4031
  } catch (error) {
4174
4032
  this.logger.error("Error in documentOperations: @Error", error);
4175
4033
  throw error;
4176
4034
  }
4177
4035
  },
4178
- pollSyncEnvelopes: (_parent, args) => {
4036
+ pollSyncEnvelopes: async (_parent, args, ctx) => {
4179
4037
  this.logger.debug("pollSyncEnvelopes(@args)", args);
4180
4038
  try {
4181
- const { envelopes, ackOrdinal, deadLetters, hasMore } = pollSyncEnvelopes(this.syncManager, args);
4039
+ let remote;
4040
+ try {
4041
+ remote = this.syncManager.getById(args.channelId);
4042
+ } catch (error) {
4043
+ throw new GraphQLError(`Channel not found: ${error instanceof Error ? error.message : "Unknown error"}`);
4044
+ }
4045
+ const driveId = remote.collectionId.driveId;
4046
+ const isAdmin = this.authorizationService.isSupremeAdmin(ctx.user?.address);
4047
+ if (!isAdmin) await this.assertCanReadCanonical(driveId, ctx);
4048
+ const forbiddenIds = /* @__PURE__ */ new Set();
4049
+ if (!isAdmin) {
4050
+ if (await this.authorizationService.canRead(driveId, void 0)) await this.#collectForbiddenDocuments(remote.channel.outbox.items, forbiddenIds, ctx);
4051
+ await this.#collectForbiddenDocuments(remote.channel.deadLetter.items, forbiddenIds, ctx);
4052
+ }
4053
+ const { envelopes, ackOrdinal, deadLetters, hasMore } = pollSyncEnvelopes(this.syncManager, args, forbiddenIds);
4182
4054
  return {
4183
4055
  envelopes,
4184
4056
  ackOrdinal,
@@ -4197,13 +4069,11 @@ var ReactorSubgraph = class extends BaseSubgraph {
4197
4069
  try {
4198
4070
  if (args.parentIdentifier) {
4199
4071
  const parent = await document(this.reactorClient, { identifier: args.parentIdentifier });
4200
- await this.assertCanWrite(parent.document.id, ctx);
4201
- } else if (this.authorizationService) {
4202
- if (!ctx.user?.address) throw new GraphQLError("Forbidden: authentication required to create documents");
4203
- } else if (!this.hasGlobalAdminAccess(ctx)) throw new GraphQLError("Forbidden: insufficient permissions to create documents");
4072
+ await this.assertCanWriteCanonical(parent.document.id, ctx);
4073
+ } else this.assertCanCreate(ctx);
4204
4074
  const result = await createDocument(this.reactorClient, args, this.graphqlManager.reactorDriveClient);
4205
4075
  if (result?.id && isDriveContainerType(result.documentType)) this.graphqlManager.driveOwnershipCache.add(result.id);
4206
- if (this.authorizationService && ctx.user?.address && result?.id) await this.documentPermissionService?.initializeDocumentProtection(result.id, ctx.user.address, this.authorizationService.config.defaultProtection);
4076
+ if (ctx.user?.address && result?.id) await this.documentPermissionService?.initializeDocumentProtection(result.id, ctx.user.address, this.authorizationService.config.defaultProtection);
4207
4077
  return result;
4208
4078
  } catch (error) {
4209
4079
  this.logger.error("Error in createDocument(@args): @Error", args, error);
@@ -4215,13 +4085,11 @@ var ReactorSubgraph = class extends BaseSubgraph {
4215
4085
  try {
4216
4086
  if (args.parentIdentifier) {
4217
4087
  const parent = await document(this.reactorClient, { identifier: args.parentIdentifier });
4218
- await this.assertCanWrite(parent.document.id, ctx);
4219
- } else if (this.authorizationService) {
4220
- if (!ctx.user?.address) throw new GraphQLError("Forbidden: authentication required to create documents");
4221
- } else if (!this.hasGlobalAdminAccess(ctx)) throw new GraphQLError("Forbidden: insufficient permissions to create documents");
4088
+ await this.assertCanWriteCanonical(parent.document.id, ctx);
4089
+ } else this.assertCanCreate(ctx);
4222
4090
  const result = await createEmptyDocument(this.reactorClient, args, this.graphqlManager.reactorDriveClient);
4223
4091
  if (result?.id && isDriveContainerType(result.documentType)) this.graphqlManager.driveOwnershipCache.add(result.id);
4224
- if (this.authorizationService && ctx.user?.address && result?.id) await this.documentPermissionService?.initializeDocumentProtection(result.id, ctx.user.address, this.authorizationService.config.defaultProtection);
4092
+ if (ctx.user?.address && result?.id) await this.documentPermissionService?.initializeDocumentProtection(result.id, ctx.user.address, this.authorizationService.config.defaultProtection);
4225
4093
  return result;
4226
4094
  } catch (error) {
4227
4095
  this.logger.error("Error in createEmptyDocument(@args): @Error", args, error);
@@ -4231,9 +4099,11 @@ var ReactorSubgraph = class extends BaseSubgraph {
4231
4099
  mutateDocument: async (_parent, args, ctx) => {
4232
4100
  this.logger.debug("mutateDocument(@args)", args);
4233
4101
  try {
4234
- if (!this.authorizationService) await this.assertCanWrite(args.documentIdentifier, ctx);
4235
- await this.assertCanExecuteOperations(args.documentIdentifier, args.actions, ctx);
4236
- return await mutateDocument(this.reactorClient, args);
4102
+ const handle = await this.assertCanExecuteOperations(args.documentIdentifier, args.actions, ctx);
4103
+ return await mutateDocument(this.reactorClient, {
4104
+ ...args,
4105
+ documentIdentifier: handle.fetchIdentifier
4106
+ });
4237
4107
  } catch (error) {
4238
4108
  this.logger.error("Error in mutateDocument(@args): @Error", args, error);
4239
4109
  throw error;
@@ -4242,9 +4112,11 @@ var ReactorSubgraph = class extends BaseSubgraph {
4242
4112
  mutateDocumentAsync: async (_parent, args, ctx) => {
4243
4113
  this.logger.debug("mutateDocumentAsync(@args)", args);
4244
4114
  try {
4245
- if (!this.authorizationService) await this.assertCanWrite(args.documentIdentifier, ctx);
4246
- await this.assertCanExecuteOperations(args.documentIdentifier, args.actions, ctx);
4247
- return await mutateDocumentAsync(this.reactorClient, args);
4115
+ const handle = await this.assertCanExecuteOperations(args.documentIdentifier, args.actions, ctx);
4116
+ return await mutateDocumentAsync(this.reactorClient, {
4117
+ ...args,
4118
+ documentIdentifier: handle.fetchIdentifier
4119
+ });
4248
4120
  } catch (error) {
4249
4121
  this.logger.error("Error in mutateDocumentAsync(@args): @Error", args, error);
4250
4122
  throw error;
@@ -4253,8 +4125,11 @@ var ReactorSubgraph = class extends BaseSubgraph {
4253
4125
  renameDocument: async (_parent, args, ctx) => {
4254
4126
  this.logger.debug("renameDocument(@args)", args);
4255
4127
  try {
4256
- await this.assertCanWrite(args.documentIdentifier, ctx);
4257
- return await renameDocument(this.reactorClient, args);
4128
+ const handle = await this.assertCanWrite(args.documentIdentifier, ctx);
4129
+ return await renameDocument(this.reactorClient, {
4130
+ ...args,
4131
+ documentIdentifier: handle.fetchIdentifier
4132
+ });
4258
4133
  } catch (error) {
4259
4134
  this.logger.error("Error in renameDocument(@args): @Error", args, error);
4260
4135
  throw error;
@@ -4263,8 +4138,11 @@ var ReactorSubgraph = class extends BaseSubgraph {
4263
4138
  setPreferredEditor: async (_parent, args, ctx) => {
4264
4139
  this.logger.debug("setPreferredEditor(@args)", args);
4265
4140
  try {
4266
- await this.assertCanWrite(args.documentIdentifier, ctx);
4267
- return await setPreferredEditor(this.reactorClient, args);
4141
+ const handle = await this.assertCanWrite(args.documentIdentifier, ctx);
4142
+ return await setPreferredEditor(this.reactorClient, {
4143
+ ...args,
4144
+ documentIdentifier: handle.fetchIdentifier
4145
+ });
4268
4146
  } catch (error) {
4269
4147
  this.logger.error("Error in setPreferredEditor(@args): @Error", args, error);
4270
4148
  throw error;
@@ -4273,8 +4151,11 @@ var ReactorSubgraph = class extends BaseSubgraph {
4273
4151
  addRelationship: async (_parent, args, ctx) => {
4274
4152
  this.logger.debug("addRelationship(@args)", args);
4275
4153
  try {
4276
- await this.assertCanWrite(args.sourceIdentifier, ctx);
4277
- return await addRelationship(this.reactorClient, args);
4154
+ const handle = await this.assertCanWrite(args.sourceIdentifier, ctx);
4155
+ return await addRelationship(this.reactorClient, {
4156
+ ...args,
4157
+ sourceIdentifier: handle.fetchIdentifier
4158
+ });
4278
4159
  } catch (error) {
4279
4160
  this.logger.error("Error in addRelationship(@args): @Error", args, error);
4280
4161
  throw error;
@@ -4283,8 +4164,11 @@ var ReactorSubgraph = class extends BaseSubgraph {
4283
4164
  removeRelationship: async (_parent, args, ctx) => {
4284
4165
  this.logger.debug("removeRelationship(@args)", args);
4285
4166
  try {
4286
- await this.assertCanWrite(args.sourceIdentifier, ctx);
4287
- return await removeRelationship(this.reactorClient, args);
4167
+ const handle = await this.assertCanWrite(args.sourceIdentifier, ctx);
4168
+ return await removeRelationship(this.reactorClient, {
4169
+ ...args,
4170
+ sourceIdentifier: handle.fetchIdentifier
4171
+ });
4288
4172
  } catch (error) {
4289
4173
  this.logger.error("Error in removeRelationship(@args): @Error", args, error);
4290
4174
  throw error;
@@ -4293,9 +4177,13 @@ var ReactorSubgraph = class extends BaseSubgraph {
4293
4177
  moveRelationship: async (_parent, args, ctx) => {
4294
4178
  this.logger.debug("moveRelationship(@args)", args);
4295
4179
  try {
4296
- await this.assertCanWrite(args.sourceParentIdentifier, ctx);
4297
- await this.assertCanWrite(args.targetParentIdentifier, ctx);
4298
- return await moveRelationship(this.reactorClient, args);
4180
+ const sourceHandle = await this.assertCanWrite(args.sourceParentIdentifier, ctx);
4181
+ const targetHandle = await this.assertCanWrite(args.targetParentIdentifier, ctx);
4182
+ return await moveRelationship(this.reactorClient, {
4183
+ ...args,
4184
+ sourceParentIdentifier: sourceHandle.fetchIdentifier,
4185
+ targetParentIdentifier: targetHandle.fetchIdentifier
4186
+ });
4299
4187
  } catch (error) {
4300
4188
  this.logger.error("Error in moveRelationship(@args): @Error @args", error, args);
4301
4189
  throw error;
@@ -4304,9 +4192,12 @@ var ReactorSubgraph = class extends BaseSubgraph {
4304
4192
  deleteDocument: async (_parent, args, ctx) => {
4305
4193
  this.logger.debug("deleteDocument(@args)", args);
4306
4194
  try {
4307
- await this.assertCanWrite(args.identifier, ctx);
4308
- const driveIdToInvalidate = await this.#resolveDriveId(args.identifier);
4309
- const result = await deleteDocument(this.reactorClient, args, this.graphqlManager.reactorDriveClient);
4195
+ const identifier = (await this.assertCanWrite(args.identifier, ctx)).fetchIdentifier;
4196
+ const driveIdToInvalidate = await this.#resolveDriveId(identifier);
4197
+ const result = await deleteDocument(this.reactorClient, {
4198
+ ...args,
4199
+ identifier
4200
+ }, this.graphqlManager.reactorDriveClient);
4310
4201
  if (result && driveIdToInvalidate) this.graphqlManager.driveOwnershipCache.remove(driveIdToInvalidate);
4311
4202
  return result;
4312
4203
  } catch (error) {
@@ -4317,25 +4208,49 @@ var ReactorSubgraph = class extends BaseSubgraph {
4317
4208
  deleteDocuments: async (_parent, args, ctx) => {
4318
4209
  this.logger.debug("deleteDocuments(@args)", args);
4319
4210
  try {
4320
- for (const identifier of args.identifiers) await this.assertCanWrite(identifier, ctx);
4321
- return await deleteDocuments(this.reactorClient, args);
4211
+ const identifiers = [];
4212
+ for (const identifier of args.identifiers) {
4213
+ const handle = await this.assertCanWrite(identifier, ctx);
4214
+ identifiers.push(handle.fetchIdentifier);
4215
+ }
4216
+ return await deleteDocuments(this.reactorClient, {
4217
+ ...args,
4218
+ identifiers
4219
+ });
4322
4220
  } catch (error) {
4323
4221
  this.logger.error("Error in deleteDocuments(@args): @Error", args, error);
4324
4222
  throw error;
4325
4223
  }
4326
4224
  },
4327
- touchChannel: async (_parent, args) => {
4225
+ touchChannel: async (_parent, args, ctx) => {
4328
4226
  this.logger.debug("touchChannel(@args)", args);
4329
4227
  try {
4228
+ if (!this.authorizationService.isSupremeAdmin(ctx.user?.address)) {
4229
+ const driveId = DriveCollectionId.fromKey(args.input.collectionId).driveId;
4230
+ await this.assertCanReadCanonical(driveId, ctx);
4231
+ }
4330
4232
  return await touchChannel(this.syncManager, args);
4331
4233
  } catch (error) {
4332
4234
  this.logger.error("Error in touchChannel(@args): @Error", args, error);
4333
4235
  throw error;
4334
4236
  }
4335
4237
  },
4336
- pushSyncEnvelopes: async (_parent, args) => {
4238
+ pushSyncEnvelopes: async (_parent, args, ctx) => {
4337
4239
  this.logger.debug("pushSyncEnvelopes(@args)", args);
4338
4240
  try {
4241
+ const checkedOperations = /* @__PURE__ */ new Map();
4242
+ for (const envelope of args.envelopes) for (const op of envelope.operations ?? []) {
4243
+ const documentId = op.context.documentId;
4244
+ const operationType = op.operation.action.type;
4245
+ let checkedTypes = checkedOperations.get(documentId);
4246
+ if (!checkedTypes) {
4247
+ checkedTypes = /* @__PURE__ */ new Set();
4248
+ checkedOperations.set(documentId, checkedTypes);
4249
+ }
4250
+ if (checkedTypes.has(operationType)) continue;
4251
+ checkedTypes.add(operationType);
4252
+ await this.assertCanExecuteOperationCanonical(documentId, operationType, ctx);
4253
+ }
4339
4254
  const mutableArgs = { envelopes: args.envelopes.map((envelope) => ({
4340
4255
  type: envelope.type,
4341
4256
  channelMeta: { id: envelope.channelMeta.id },
@@ -4366,31 +4281,43 @@ var ReactorSubgraph = class extends BaseSubgraph {
4366
4281
  },
4367
4282
  Subscription: {
4368
4283
  documentChanges: {
4369
- subscribe: withFilter((() => {
4284
+ subscribe: (rootValue, args, ctx, info) => {
4370
4285
  this.logger.debug("documentChanges subscription started");
4371
- ensureGlobalDocumentSubscription(this.reactorClient);
4372
- return getPubSub().asyncIterableIterator(SUBSCRIPTION_TRIGGERS.DOCUMENT_CHANGES);
4373
- }), ((payload, args) => {
4374
- if (!payload) return false;
4375
- const search = {
4376
- type: args.search?.type ?? void 0,
4377
- parentId: args.search?.parentId ?? void 0
4378
- };
4379
- return matchesSearchFilter(payload.documentChanges, search);
4380
- })),
4286
+ return withFilter(() => {
4287
+ ensureGlobalDocumentSubscription(this.reactorClient);
4288
+ return getPubSub().asyncIterableIterator(SUBSCRIPTION_TRIGGERS.DOCUMENT_CHANGES);
4289
+ }, async (payload, filterArgs, filterCtx) => {
4290
+ if (!payload) return false;
4291
+ const search = {
4292
+ type: filterArgs?.search?.type ?? void 0,
4293
+ parentId: filterArgs?.search?.parentId ?? void 0
4294
+ };
4295
+ if (!matchesSearchFilter(payload.documentChanges, search)) return false;
4296
+ if (this.authorizationService.isSupremeAdmin(filterCtx?.user?.address)) return true;
4297
+ const documentIds = collectChangedDocumentIds(payload.documentChanges);
4298
+ if (documentIds.length === 0) return false;
4299
+ for (const documentId of documentIds) if (!await this.canReadDocument(documentId, filterCtx)) return false;
4300
+ return true;
4301
+ })(rootValue, args, ctx, info);
4302
+ },
4381
4303
  resolve: (payload) => {
4382
4304
  return toGqlDocumentChangeEvent(payload.documentChanges);
4383
4305
  }
4384
4306
  },
4385
4307
  jobChanges: {
4386
- subscribe: withFilter(((_parent, args) => {
4308
+ subscribe: (rootValue, args, ctx, info) => {
4387
4309
  this.logger.debug("jobChanges(@args) subscription started", args);
4388
- ensureJobSubscription(this.reactorClient, args.jobId);
4389
- return getPubSub().asyncIterableIterator(SUBSCRIPTION_TRIGGERS.JOB_CHANGES);
4390
- }), ((payload, args) => {
4391
- if (!payload) return false;
4392
- return matchesJobFilter(payload, args);
4393
- })),
4310
+ return withFilter(() => {
4311
+ ensureJobSubscription(this.reactorClient, args.jobId);
4312
+ return getPubSub().asyncIterableIterator(SUBSCRIPTION_TRIGGERS.JOB_CHANGES);
4313
+ }, async (payload, filterArgs, filterCtx) => {
4314
+ if (!payload || !filterArgs) return false;
4315
+ if (!matchesJobFilter(payload, filterArgs)) return false;
4316
+ if (this.authorizationService.isSupremeAdmin(filterCtx?.user?.address)) return true;
4317
+ if (!payload.documentId) return false;
4318
+ return this.canReadDocument(payload.documentId, filterCtx);
4319
+ })(rootValue, args, ctx, info);
4320
+ },
4394
4321
  resolve: (payload) => {
4395
4322
  return payload.jobChanges;
4396
4323
  }
@@ -4414,10 +4341,10 @@ const ADMIN_USERS = getAdminUsers();
4414
4341
  //#endregion
4415
4342
  //#region src/graphql/system/version.ts
4416
4343
  function getVersion() {
4417
- return "6.2.0-dev.2";
4344
+ return "6.2.0-dev.20";
4418
4345
  }
4419
4346
  function getGitHash() {
4420
- return "2f6b59fedf158291a28658f048853eeca309be9d";
4347
+ return "69a4177a74abeda6e2812c8c19f79e3defc14df0";
4421
4348
  }
4422
4349
  function getGitUrl() {
4423
4350
  return buildTreeUrl(getGitHash());
@@ -5038,104 +4965,159 @@ async function runMigrations(db) {
5038
4965
  }
5039
4966
  }
5040
4967
  //#endregion
5041
- //#region src/services/authorization.service.ts
5042
- /**
5043
- * Central authorization service — single source of truth for all permission checks.
5044
- *
5045
- * Authorization model:
5046
- * 1. Supreme admin (ADMINS env) → ALLOW ALL
5047
- * 2. Is document protected?
5048
- * a. NOT protected:
5049
- * - READ: anyone (even anonymous) → ALLOW
5050
- * - WRITE: authenticated user → ALLOW
5051
- * b. PROTECTED:
5052
- * - READ: requires explicit READ/WRITE/ADMIN grant (direct or via group/parent)
5053
- * - WRITE: requires explicit WRITE/ADMIN grant (direct or via group/parent)
5054
- * 3. Operation restricted? → Check OperationUserPermission
5055
- * 4. Document owner = implicit ADMIN
5056
- * 5. Drive protected = all children effectively protected
5057
- */
5058
- var AuthorizationService = class {
4968
+ //#region src/services/auth.service.ts
4969
+ const DEFAULT_CREDENTIAL_CACHE_TTL_MS = 6e4;
4970
+ const CREDENTIAL_CACHE_MAX_ENTRIES = 1e3;
4971
+ var AuthService = class {
5059
4972
  config;
5060
- constructor(documentPermissionService, config) {
5061
- this.documentPermissionService = documentPermissionService;
4973
+ credentialCache = /* @__PURE__ */ new Map();
4974
+ constructor(config) {
5062
4975
  this.config = config;
5063
4976
  }
4977
+ async authenticateRequest(request) {
4978
+ if (!this.config.enabled) return {
4979
+ user: void 0,
4980
+ admins: [],
4981
+ auth_enabled: false
4982
+ };
4983
+ const method = request.method;
4984
+ if (method === "OPTIONS" || method === "GET") return {
4985
+ user: void 0,
4986
+ admins: this.config.admins,
4987
+ auth_enabled: true
4988
+ };
4989
+ return this.verifyBearer(request.headers.get("authorization") ?? void 0);
4990
+ }
5064
4991
  /**
5065
- * Check if a user is a supreme admin (from ADMINS env var).
4992
+ * Verify a Bearer token regardless of HTTP method. Use this from non-GraphQL
4993
+ * middleware that must enforce authentication on every request.
5066
4994
  */
5067
- isSupremeAdmin(userAddress) {
5068
- if (!userAddress) return false;
5069
- return this.config.admins.includes(userAddress.toLowerCase());
4995
+ async verifyBearer(authorization) {
4996
+ if (!this.config.enabled) return {
4997
+ user: void 0,
4998
+ admins: [],
4999
+ auth_enabled: false
5000
+ };
5001
+ const token = authorization?.split(" ")[1];
5002
+ if (!token) return {
5003
+ user: void 0,
5004
+ admins: this.config.admins,
5005
+ auth_enabled: true
5006
+ };
5007
+ try {
5008
+ const verified = await this.verifyToken(token);
5009
+ if (!verified) return new Response(JSON.stringify({ error: "Verification failed" }), { status: 401 });
5010
+ const user = this.extractUserFromVerification(verified);
5011
+ if (!user) return new Response(JSON.stringify({ error: "Missing credentials" }), { status: 401 });
5012
+ if (!this.config.skipCredentialVerification) {
5013
+ if (!await this.verifyCredentialExists(user.address, user.chainId, verified.issuer)) return new Response(JSON.stringify({ error: "Credentials no longer valid" }), { status: 401 });
5014
+ }
5015
+ return {
5016
+ user,
5017
+ admins: this.config.admins,
5018
+ auth_enabled: true
5019
+ };
5020
+ } catch {
5021
+ return new Response(JSON.stringify({ error: "Authentication failed" }), { status: 401 });
5022
+ }
5023
+ }
5024
+ async authenticateWebSocketConnection(connectionParams) {
5025
+ if (!this.config.enabled) return null;
5026
+ const authHeader = connectionParams.authorization;
5027
+ if (!authHeader) throw new Error("Missing authorization in connection parameters");
5028
+ const token = authHeader.split(" ")[1];
5029
+ if (!token) throw new Error("Invalid authorization format");
5030
+ const verified = await this.verifyToken(token);
5031
+ if (!verified) throw new Error("Token verification failed");
5032
+ const user = this.extractUserFromVerification(verified);
5033
+ if (!user) throw new Error("Invalid credentials");
5034
+ if (!this.config.skipCredentialVerification) {
5035
+ if (!await this.verifyCredentialExists(user.address, user.chainId, verified.issuer)) throw new Error("Credentials no longer valid");
5036
+ }
5037
+ return user;
5070
5038
  }
5071
5039
  /**
5072
- * Check if a user can read a document.
5073
- *
5074
- * - Supreme admin → yes
5075
- * - Not protected → anyone can read (even anonymous)
5076
- * - Protected → requires READ/WRITE/ADMIN grant (direct, group, or parent inheritance)
5077
- * - Owner → yes (implicit ADMIN)
5040
+ * Verify the auth bearer token
5078
5041
  */
5079
- async canRead(documentId, userAddress, getParentIds) {
5080
- if (this.isSupremeAdmin(userAddress)) return true;
5081
- if (!(getParentIds ? await this.documentPermissionService.isProtectedWithAncestors(documentId, getParentIds) : await this.documentPermissionService.isDocumentProtected(documentId))) return true;
5082
- if (!userAddress) return false;
5083
- const owner = await this.documentPermissionService.getDocumentOwner(documentId);
5084
- if (owner && owner === userAddress.toLowerCase()) return true;
5085
- if (getParentIds) return this.documentPermissionService.canRead(documentId, userAddress, getParentIds);
5086
- return this.documentPermissionService.canReadDocument(documentId, userAddress);
5042
+ async verifyToken(token) {
5043
+ return await verifyAuthBearerToken(token);
5087
5044
  }
5088
5045
  /**
5089
- * Check if a user can write to a document.
5090
- *
5091
- * - Supreme admin → yes
5092
- * - Not protected → anyone can write (even anonymous)
5093
- * - Protected → requires authentication + WRITE/ADMIN grant
5094
- * - Owner → yes (implicit ADMIN)
5046
+ * Extract user information from verification result
5095
5047
  */
5096
- async canWrite(documentId, userAddress, getParentIds) {
5097
- if (this.isSupremeAdmin(userAddress)) return true;
5098
- if (!(getParentIds ? await this.documentPermissionService.isProtectedWithAncestors(documentId, getParentIds) : await this.documentPermissionService.isDocumentProtected(documentId))) return true;
5099
- if (!userAddress) return false;
5100
- const owner = await this.documentPermissionService.getDocumentOwner(documentId);
5101
- if (owner && owner === userAddress.toLowerCase()) return true;
5102
- if (getParentIds) return this.documentPermissionService.canWrite(documentId, userAddress, getParentIds);
5103
- return this.documentPermissionService.canWriteDocument(documentId, userAddress);
5048
+ extractUserFromVerification(verified) {
5049
+ try {
5050
+ const { address, chainId, networkId } = verified.verifiableCredential.credentialSubject;
5051
+ if (!address || !chainId || !networkId) return null;
5052
+ return {
5053
+ address,
5054
+ chainId,
5055
+ networkId
5056
+ };
5057
+ } catch {
5058
+ return null;
5059
+ }
5104
5060
  }
5105
5061
  /**
5106
- * Check if a user can manage a document (change permissions, protection, transfer ownership).
5062
+ * Verify that the credential still exists on the Renown API.
5107
5063
  *
5108
- * - Supreme admin yes
5109
- * - Owner yes
5110
- * - Has ADMIN grant yes
5064
+ * Results are cached per (address, chainId, issuer) for a short TTL so the
5065
+ * blocking external round-trip is not paid on every request. Concurrent
5066
+ * checks for the same key share a single in-flight request, and entries
5067
+ * that resolve to false are evicted immediately so failed or revoked
5068
+ * credentials are re-checked on the next request.
5111
5069
  */
5112
- async canManage(documentId, userAddress, _getParentIds) {
5113
- if (this.isSupremeAdmin(userAddress)) return true;
5114
- if (!userAddress) return false;
5115
- const owner = await this.documentPermissionService.getDocumentOwner(documentId);
5116
- if (owner && owner === userAddress.toLowerCase()) return true;
5117
- return this.documentPermissionService.canManageDocument(documentId, userAddress);
5070
+ verifyCredentialExists(address, chainId, appId) {
5071
+ const ttlMs = this.config.credentialVerificationCacheTtlMs ?? DEFAULT_CREDENTIAL_CACHE_TTL_MS;
5072
+ if (ttlMs <= 0) return this.fetchCredentialExists(address, chainId, appId);
5073
+ const key = `${address.toLowerCase()}:${chainId}:${appId}`;
5074
+ const now = Date.now();
5075
+ const cached = this.credentialCache.get(key);
5076
+ if (cached && cached.expiresAt > now) return cached.exists;
5077
+ this.pruneCredentialCache(now);
5078
+ const entry = {
5079
+ exists: this.fetchCredentialExists(address, chainId, appId).then((exists) => {
5080
+ if (!exists && this.credentialCache.get(key) === entry) this.credentialCache.delete(key);
5081
+ return exists;
5082
+ }),
5083
+ expiresAt: now + ttlMs
5084
+ };
5085
+ this.credentialCache.set(key, entry);
5086
+ return entry.exists;
5118
5087
  }
5119
5088
  /**
5120
- * Check if a user can execute a specific operation.
5121
- * If the operation is not restricted, falls through to the standard write check.
5122
- * If the operation is restricted, requires an explicit OperationUserPermission grant.
5089
+ * Enforce the cache size cap before inserting a new entry: drop expired
5090
+ * entries first, then evict oldest-inserted entries (insertion order
5091
+ * matches expiry order since the TTL is constant) until under the cap, so
5092
+ * a flood of distinct keys cannot grow the map without bound.
5123
5093
  */
5124
- async canExecuteOperation(documentId, operationType, userAddress, getParentIds) {
5125
- if (this.isSupremeAdmin(userAddress)) return true;
5126
- if (!await this.documentPermissionService.isOperationRestricted(documentId, operationType)) return this.canWrite(documentId, userAddress, getParentIds);
5127
- return this.documentPermissionService.canExecuteOperation(documentId, operationType, userAddress?.toLowerCase());
5094
+ pruneCredentialCache(now) {
5095
+ if (this.credentialCache.size < CREDENTIAL_CACHE_MAX_ENTRIES) return;
5096
+ for (const [key, entry] of this.credentialCache) if (entry.expiresAt <= now) this.credentialCache.delete(key);
5097
+ while (this.credentialCache.size >= CREDENTIAL_CACHE_MAX_ENTRIES) {
5098
+ const oldestKey = this.credentialCache.keys().next().value;
5099
+ if (oldestKey === void 0) break;
5100
+ this.credentialCache.delete(oldestKey);
5101
+ }
5128
5102
  }
5129
5103
  /**
5130
- * Combined check for mutations: can the user write + execute the operation?
5131
- * This enables READ-only users with operation grants to execute specific operations.
5132
- * For restricted operations, only the operation grant is checked (bypasses write check),
5133
- * allowing READ-only users with an explicit operation grant to execute that operation.
5104
+ * Fetch the credential from the Renown API and validate it against the
5105
+ * expected address, chainId and issuer. Never throws; returns false on any
5106
+ * network or validation failure.
5134
5107
  */
5135
- async canMutate(documentId, operationType, userAddress, getParentIds) {
5136
- if (this.isSupremeAdmin(userAddress)) return true;
5137
- if (await this.documentPermissionService.isOperationRestricted(documentId, operationType)) return this.documentPermissionService.canExecuteOperation(documentId, operationType, userAddress?.toLowerCase());
5138
- return this.canWrite(documentId, userAddress, getParentIds);
5108
+ async fetchCredentialExists(address, chainId, appId) {
5109
+ const url = `https://www.renown.id/api/auth/credential?address=${address}&chainId=${chainId}&connectId=${appId}&appId=${appId}`;
5110
+ try {
5111
+ const response = await fetch(url, { method: "GET" });
5112
+ if (response.status !== 200) return false;
5113
+ const credential = (await response.json()).credential;
5114
+ const appIdVerfied = credential.credentialSubject.id;
5115
+ const addressVerfied = credential.issuer.id.split(":")[4];
5116
+ const chainIdVerfied = credential.issuer.id.split(":")[3];
5117
+ return appIdVerfied === appId && addressVerfied.toLocaleLowerCase() === address.toLocaleLowerCase() && chainIdVerfied === chainId.toString();
5118
+ } catch {
5119
+ return false;
5120
+ }
5139
5121
  }
5140
5122
  };
5141
5123
  //#endregion
@@ -5233,63 +5215,6 @@ var DocumentPermissionService = class {
5233
5215
  await this.db.deleteFrom("OperationGroupPermission").where("documentId", "=", documentId).execute();
5234
5216
  }
5235
5217
  /**
5236
- * Check if a user can read a document.
5237
- * Returns true if user has READ, WRITE, or ADMIN permission (direct or via group)
5238
- */
5239
- async canReadDocument(documentId, userAddress) {
5240
- if (!userAddress) return false;
5241
- if (await this.getUserPermission(documentId, userAddress) !== null) return true;
5242
- return await this.getUserGroupPermission(documentId, userAddress) !== null;
5243
- }
5244
- /**
5245
- * Check if a user can write to a document.
5246
- * Returns true if user has WRITE or ADMIN permission (direct or via group)
5247
- */
5248
- async canWriteDocument(documentId, userAddress) {
5249
- if (!userAddress) return false;
5250
- const directPermission = await this.getUserPermission(documentId, userAddress);
5251
- if (directPermission === "WRITE" || directPermission === "ADMIN") return true;
5252
- const groupPermission = await this.getUserGroupPermission(documentId, userAddress);
5253
- return groupPermission === "WRITE" || groupPermission === "ADMIN";
5254
- }
5255
- /**
5256
- * Check if a user can manage a document (change permissions, settings).
5257
- * Returns true if user has ADMIN permission (direct or via group)
5258
- */
5259
- async canManageDocument(documentId, userAddress) {
5260
- if (!userAddress) return false;
5261
- if (await this.getUserPermission(documentId, userAddress) === "ADMIN") return true;
5262
- return await this.getUserGroupPermission(documentId, userAddress) === "ADMIN";
5263
- }
5264
- /**
5265
- * Check if a user can read a document, including parent permission inheritance.
5266
- * Returns true if user has permission on the document OR any parent in the hierarchy.
5267
- */
5268
- async canRead(documentId, userAddress, getParentIds) {
5269
- if (await this.canReadDocument(documentId, userAddress)) return true;
5270
- const parentIds = await getParentIds(documentId);
5271
- for (const parentId of parentIds) if (await this.canRead(parentId, userAddress, getParentIds)) return true;
5272
- return false;
5273
- }
5274
- /**
5275
- * Check if a user can write to a document, including parent permission inheritance.
5276
- * Returns true if user has write permission on the document OR any parent in the hierarchy.
5277
- */
5278
- async canWrite(documentId, userAddress, getParentIds) {
5279
- if (await this.canWriteDocument(documentId, userAddress)) return true;
5280
- const parentIds = await getParentIds(documentId);
5281
- for (const parentId of parentIds) if (await this.canWrite(parentId, userAddress, getParentIds)) return true;
5282
- return false;
5283
- }
5284
- /**
5285
- * Filter a list of document IDs to only include those the user can read.
5286
- */
5287
- async filterReadableDocuments(documentIds, userAddress, getParentIds) {
5288
- const results = [];
5289
- for (const docId of documentIds) if (await this.canRead(docId, userAddress, getParentIds)) results.push(docId);
5290
- return results;
5291
- }
5292
- /**
5293
5218
  * Create a new group
5294
5219
  */
5295
5220
  async createGroup(name, description) {
@@ -5518,11 +5443,10 @@ var DocumentPermissionService = class {
5518
5443
  ]).where("documentId", "=", documentId).where("operationType", "=", operationType).execute();
5519
5444
  }
5520
5445
  /**
5521
- * Check if a user can execute a specific operation on a document.
5522
- * Returns true if user has direct permission or is in a group with permission.
5446
+ * Whether an operation-permission row exists for the user on this
5447
+ * operation, either directly or via a group the user belongs to.
5523
5448
  */
5524
- async canExecuteOperation(documentId, operationType, userAddress) {
5525
- if (!userAddress) return false;
5449
+ async hasOperationGrant(documentId, operationType, userAddress) {
5526
5450
  const normalizedAddress = userAddress.toLowerCase();
5527
5451
  if (await this.db.selectFrom("OperationUserPermission").select("userAddress").where("documentId", "=", documentId).where("operationType", "=", operationType).where("userAddress", "=", normalizedAddress).executeTakeFirst()) return true;
5528
5452
  return !!await this.db.selectFrom("OperationGroupPermission").innerJoin("UserGroup", "UserGroup.groupId", "OperationGroupPermission.groupId").select("OperationGroupPermission.groupId").where("OperationGroupPermission.documentId", "=", documentId).where("OperationGroupPermission.operationType", "=", operationType).where("UserGroup.userAddress", "=", normalizedAddress).executeTakeFirst();
@@ -5649,6 +5573,60 @@ var DocumentPermissionService = class {
5649
5573
  }
5650
5574
  };
5651
5575
  //#endregion
5576
+ //#region src/services/get-parent-ids.ts
5577
+ /**
5578
+ * The canonical parent-document resolver used for permission inheritance:
5579
+ * a document's parents are the sources of its incoming "child"
5580
+ * relationships. Lookup failures resolve to "no parents" so a relationship
5581
+ * store outage degrades to no inherited permissions rather than an error.
5582
+ */
5583
+ function createGetParentIdsFn(reactorClient) {
5584
+ return async (documentId) => {
5585
+ try {
5586
+ return (await reactorClient.getIncomingRelationships(documentId, "child")).results.map((doc) => doc.header.id);
5587
+ } catch {
5588
+ return [];
5589
+ }
5590
+ };
5591
+ }
5592
+ //#endregion
5593
+ //#region src/services/mcp-request-authorizer.ts
5594
+ /**
5595
+ * Authorization gate for the /mcp endpoint (AUTH_REVIEW S-C1). MCP tools have
5596
+ * unrestricted reactor access, so access is limited to supreme admins when
5597
+ * auth is enabled. OPEN policy is public; any other policy without an
5598
+ * AuthService fails closed.
5599
+ */
5600
+ function createMcpRequestAuthorizer(authService, authorizationService) {
5601
+ return async (req) => {
5602
+ if (!authService) {
5603
+ if (authorizationService.config.policy === AuthorizationPolicy.OPEN) return { authorized: true };
5604
+ return {
5605
+ authorized: false,
5606
+ status: 401,
5607
+ message: "Authentication required"
5608
+ };
5609
+ }
5610
+ const context = await authService.verifyBearer(req.headers.authorization);
5611
+ if (context instanceof Response) return {
5612
+ authorized: false,
5613
+ status: context.status,
5614
+ message: "Authentication failed"
5615
+ };
5616
+ if (!context.user) return {
5617
+ authorized: false,
5618
+ status: 401,
5619
+ message: "Authentication required"
5620
+ };
5621
+ if (!authorizationService.isSupremeAdmin(context.user.address)) return {
5622
+ authorized: false,
5623
+ status: 403,
5624
+ message: "Forbidden: MCP access requires an administrator"
5625
+ };
5626
+ return { authorized: true };
5627
+ };
5628
+ }
5629
+ //#endregion
5652
5630
  //#region src/utils/db.ts
5653
5631
  function isPG(connectionString) {
5654
5632
  if (connectionString.startsWith("postgresql://") || connectionString.startsWith("postgres://")) return true;
@@ -5774,6 +5752,33 @@ const initAnalyticsStoreSql = [
5774
5752
  //#region src/server.ts
5775
5753
  const defaultLogger = childLogger(["reactor-api", "server"]);
5776
5754
  const DEFAULT_PORT = 4e3;
5755
+ /**
5756
+ * Doc-perms require auth: with auth off no `user` is ever resolved, so every
5757
+ * authorization check fails closed. Refuse to boot rather than run broken.
5758
+ */
5759
+ function assertAuthRequiredForDocumentPermissions(authEnabled, documentPermissionsRequested) {
5760
+ if (!authEnabled && documentPermissionsRequested) throw new Error("Document permissions require authentication: AUTH_ENABLED is false but document permissions were requested (DOCUMENT_PERMISSIONS_ENABLED=true or a documentPermissionService was provided). Enable authentication (AUTH_ENABLED=true, or auth.enabled in the config file) or disable document permissions.");
5761
+ }
5762
+ /**
5763
+ * Refuses SKIP_CREDENTIAL_VERIFICATION at boot outside tests or an explicit
5764
+ * opt-in: it removes the only binding between a token's claimed address and its
5765
+ * signing key. Fail-closed — unset NODE_ENV counts as production.
5766
+ */
5767
+ function assertSkipCredentialVerificationAllowed(authEnabled, skipCredentialVerification, env) {
5768
+ if (!authEnabled || !skipCredentialVerification) return;
5769
+ const inAutomatedTest = env.VITEST === "true" || env.NODE_ENV === "test";
5770
+ const acknowledged = env.ALLOW_INSECURE_SKIP_CREDENTIAL_VERIFICATION === "true";
5771
+ if (!inAutomatedTest && !acknowledged) throw new Error("SKIP_CREDENTIAL_VERIFICATION is set but refused: it disables the live Renown credential check — the only check binding a token's claimed address to the key that signed it — so honoring it allows identity spoofing, including of admins. It is never safe in production. For local or sandbox use, also set ALLOW_INSECURE_SKIP_CREDENTIAL_VERIFICATION=true to acknowledge the risk; automated test runs (VITEST=true or NODE_ENV=test) are exempt.");
5772
+ }
5773
+ function createReadinessGate() {
5774
+ let ready = false;
5775
+ return {
5776
+ isReady: () => ready,
5777
+ markReady: () => {
5778
+ ready = true;
5779
+ }
5780
+ };
5781
+ }
5777
5782
  function resolveAttachmentStoragePath(options) {
5778
5783
  if (options.attachmentStoragePath) return options.attachmentStoragePath;
5779
5784
  if (options.dbPath && !options.dbPath.startsWith("postgres")) return path.resolve(options.dbPath, "..", "attachments");
@@ -5812,11 +5817,8 @@ function makeDbClosers(knexInstance, pglite) {
5812
5817
  /**
5813
5818
  * Sets up the subgraph manager and registers subgraphs
5814
5819
  */
5815
- async function setupGraphQLManager(httpAdapter, authFetchMiddleware, httpServer, wsServer, client, relationalDb, analyticsStore, syncManager, subgraphs, logger, auth, documentPermissionService, enableDocumentModelSubgraphs, port, authorizationService, reactorDriveClient) {
5816
- const graphqlManager = new GraphQLManager(config.basePath, httpServer, wsServer, client, relationalDb, analyticsStore, syncManager, logger, httpAdapter, await createGatewayAdapter("apollo", logger), {
5817
- enabled: auth?.enabled ?? false,
5818
- admins: auth?.admins ?? []
5819
- }, documentPermissionService, { enableDocumentModelSubgraphs }, port, authorizationService, reactorDriveClient);
5820
+ async function setupGraphQLManager(httpAdapter, authFetchMiddleware, httpServer, wsServer, client, relationalDb, analyticsStore, syncManager, subgraphs, logger, authorizationService, authService, documentPermissionService, enableDocumentModelSubgraphs, port, reactorDriveClient) {
5821
+ const graphqlManager = new GraphQLManager(config.basePath, httpServer, wsServer, client, relationalDb, analyticsStore, syncManager, logger, httpAdapter, await createGatewayAdapter("apollo", logger), authService, documentPermissionService, { enableDocumentModelSubgraphs }, port, authorizationService, reactorDriveClient);
5820
5822
  await graphqlManager.init(subgraphs.core, authFetchMiddleware);
5821
5823
  for (const [, collection] of subgraphs.extended.entries()) for (const subgraph of collection) await graphqlManager.registerSubgraph(subgraph, "graphql");
5822
5824
  await graphqlManager.updateRouter();
@@ -5890,6 +5892,7 @@ async function startServer(httpAdapter, port, httpsOptions, logger) {
5890
5892
  async function _setupCommonInfrastructure(options) {
5891
5893
  const port = options.port ?? DEFAULT_PORT;
5892
5894
  const { adapter: httpAdapter } = await createHttpAdapter("express");
5895
+ const logger = options.logger ?? defaultLogger;
5893
5896
  let admins = [];
5894
5897
  let authEnabled = false;
5895
5898
  if (options.configFile) {
@@ -5900,17 +5903,26 @@ async function _setupCommonInfrastructure(options) {
5900
5903
  admins = options.auth.admins.map((a) => a.toLowerCase());
5901
5904
  authEnabled = options.auth.enabled;
5902
5905
  }
5903
- const { AUTH_ENABLED, ADMINS, DEFAULT_PROTECTION, DOCUMENT_PERMISSIONS_ENABLED, SKIP_CREDENTIAL_VERIFICATION } = process.env;
5906
+ const { AUTH_ENABLED, ADMINS, DEFAULT_PROTECTION, DOCUMENT_PERMISSIONS_ENABLED, SKIP_CREDENTIAL_VERIFICATION, CREDENTIAL_VERIFICATION_CACHE_TTL_MS } = process.env;
5904
5907
  if (AUTH_ENABLED !== void 0) authEnabled = AUTH_ENABLED === "true";
5905
5908
  if (ADMINS !== void 0) admins = ADMINS.split(",").map((a) => a.toLowerCase());
5906
5909
  let defaultProtection = false;
5907
5910
  if (DEFAULT_PROTECTION !== void 0) defaultProtection = DEFAULT_PROTECTION.toLowerCase() === "true";
5908
- const { USERS, GUESTS, FREE_ENTRY } = process.env;
5909
- if (USERS || GUESTS || FREE_ENTRY) console.warn("[DEPRECATION WARNING] The USERS, GUESTS, and FREE_ENTRY environment variables are no longer supported. Access control is now managed per-document via the DocumentProtection system. Use DEFAULT_PROTECTION=true for strict mode, or manage protection per document via the GraphQL API. See the auth documentation for migration guidance.");
5910
5911
  let skipCredentialVerification = false;
5911
5912
  if (SKIP_CREDENTIAL_VERIFICATION !== void 0) skipCredentialVerification = SKIP_CREDENTIAL_VERIFICATION === "true";
5912
- const logger = options.logger ?? defaultLogger;
5913
+ let credentialVerificationCacheTtlMs;
5914
+ if (CREDENTIAL_VERIFICATION_CACHE_TTL_MS !== void 0) {
5915
+ const parsed = Number(CREDENTIAL_VERIFICATION_CACHE_TTL_MS);
5916
+ if (CREDENTIAL_VERIFICATION_CACHE_TTL_MS.trim() !== "" && Number.isFinite(parsed) && parsed >= 0) credentialVerificationCacheTtlMs = parsed;
5917
+ else logger.warn(`Ignoring invalid CREDENTIAL_VERIFICATION_CACHE_TTL_MS="${CREDENTIAL_VERIFICATION_CACHE_TTL_MS}" (expected a non-negative number of milliseconds; 0 disables caching) — using the default TTL`);
5918
+ }
5919
+ const documentPermissionsRequested = options.documentPermissionService !== void 0 || DOCUMENT_PERMISSIONS_ENABLED === "true";
5920
+ assertAuthRequiredForDocumentPermissions(authEnabled, documentPermissionsRequested);
5921
+ assertSkipCredentialVerificationAllowed(authEnabled, skipCredentialVerification, process.env);
5922
+ 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.");
5913
5923
  httpAdapter.getRoute("/health", () => new Response("OK", { status: 200 }));
5924
+ const readiness = createReadinessGate();
5925
+ httpAdapter.getRoute("/ready", () => readiness.isReady() ? new Response("OK", { status: 200 }) : new Response("starting", { status: 503 }));
5914
5926
  const explorerPrefix = `${config.basePath}/explorer`;
5915
5927
  httpAdapter.getRoute(`${explorerPrefix}/:endpoint?`, (request) => {
5916
5928
  const url = new URL(request.url);
@@ -5926,7 +5938,8 @@ async function _setupCommonInfrastructure(options) {
5926
5938
  authService = new AuthService({
5927
5939
  enabled: authEnabled,
5928
5940
  admins,
5929
- skipCredentialVerification
5941
+ skipCredentialVerification,
5942
+ credentialVerificationCacheTtlMs
5930
5943
  });
5931
5944
  authFetchMiddleware = createAuthFetchMiddleware(authService);
5932
5945
  }
@@ -5942,14 +5955,12 @@ async function _setupCommonInfrastructure(options) {
5942
5955
  documentPermissionService = new DocumentPermissionService(db, { defaultProtection });
5943
5956
  logger.info("Document permission service initialized");
5944
5957
  }
5945
- let authorizationService;
5946
- if (documentPermissionService) {
5947
- authorizationService = new AuthorizationService(documentPermissionService, {
5948
- admins,
5949
- defaultProtection
5950
- });
5951
- logger.info("Authorization service initialized");
5952
- }
5958
+ const policy = documentPermissionService ? AuthorizationPolicy.DOCUMENT_PERMISSIONS : authEnabled ? AuthorizationPolicy.ADMIN_ONLY : AuthorizationPolicy.OPEN;
5959
+ const authorizationConfig = {
5960
+ admins,
5961
+ defaultProtection,
5962
+ policy
5963
+ };
5953
5964
  const attachmentStoragePath = resolveAttachmentStoragePath(options);
5954
5965
  await mkdir(attachmentStoragePath, { recursive: true });
5955
5966
  const { db: attachmentDb, knex: attachmentKnex, pglite: attachmentPglite } = getDbClient(options.dbPath, options.pgliteFactory);
@@ -5969,23 +5980,20 @@ async function _setupCommonInfrastructure(options) {
5969
5980
  httpAdapter,
5970
5981
  authFetchMiddleware,
5971
5982
  authService,
5972
- auth: {
5973
- enabled: authEnabled,
5974
- admins
5975
- },
5976
5983
  relationalDb,
5977
5984
  analyticsStore,
5978
5985
  documentPermissionService,
5979
- authorizationService,
5986
+ authorizationConfig,
5980
5987
  attachments,
5981
5988
  packages,
5982
- dbClosers
5989
+ dbClosers,
5990
+ readiness
5983
5991
  };
5984
5992
  }
5985
5993
  /**
5986
5994
  * Private helper function containing common setup logic for API initialization
5987
5995
  */
5988
- async function _setupAPI(reactorClient, syncManager, reactorProcessorManager, httpAdapter, authFetchMiddleware, authService, port, packages, relationalDb, analyticsStore, documentPermissionService, processors, subgraphs, options, auth, processorApp, readModels, attachments, authorizationService, documentModelRegistry, dbClosers = [], reactorDriveClient) {
5996
+ async function _setupAPI(reactorClient, syncManager, reactorProcessorManager, httpAdapter, authFetchMiddleware, authService, port, packages, relationalDb, analyticsStore, documentPermissionService, processors, subgraphs, options, processorApp, readModels, attachments, authorizationConfig, documentModelRegistry, dbClosers = [], reactorDriveClient) {
5989
5997
  const hostModule = {
5990
5998
  relationalDb,
5991
5999
  analyticsStore,
@@ -6030,6 +6038,8 @@ async function _setupAPI(reactorClient, syncManager, reactorProcessorManager, ht
6030
6038
  }))).flat());
6031
6039
  }
6032
6040
  const { httpServer, wsServer } = await startServer(httpAdapter, port, options.https, logger);
6041
+ const authorizationService = createAuthorizationService(authorizationConfig, documentPermissionService, createGetParentIdsFn(reactorClient));
6042
+ logger.info(`Authorization service initialized (policy: ${authorizationConfig.policy})`);
6033
6043
  const coreSubgraphs = DefaultCoreSubgraphs.slice();
6034
6044
  coreSubgraphs.push(ReactorSubgraph);
6035
6045
  if (documentPermissionService) {
@@ -6039,12 +6049,13 @@ async function _setupAPI(reactorClient, syncManager, reactorProcessorManager, ht
6039
6049
  const graphqlManager = await setupGraphQLManager(httpAdapter, authFetchMiddleware, httpServer, wsServer, reactorClient, relationalDb, analyticsStore, syncManager, {
6040
6050
  extended: subgraphs,
6041
6051
  core: coreSubgraphs
6042
- }, logger.child(["graphql-manager"]), auth, documentPermissionService, options.enableDocumentModelSubgraphs, port, authorizationService, reactorDriveClient);
6052
+ }, logger.child(["graphql-manager"]), authorizationService, authService, documentPermissionService, options.enableDocumentModelSubgraphs, port, reactorDriveClient);
6043
6053
  setupEventListeners(packages, graphqlManager, reactorProcessorManager, hostModule, documentModelRegistry);
6044
6054
  if (mcpServerEnabled) {
6045
6055
  await setupMcpServer({
6046
6056
  client: reactorClient,
6047
- syncManager
6057
+ syncManager,
6058
+ authorizeRequest: createMcpRequestAuthorizer(authService, authorizationService)
6048
6059
  }, httpAdapter);
6049
6060
  logger.info(`MCP server available at http://localhost:${port}/mcp`);
6050
6061
  }
@@ -6101,7 +6112,7 @@ function buildApiDispose(args) {
6101
6112
  };
6102
6113
  }
6103
6114
  async function initializeAndStartAPI(clientInitializer, options, processorApp) {
6104
- const { port, httpAdapter, authFetchMiddleware, authService, auth, relationalDb, analyticsStore, documentPermissionService, authorizationService, attachments, packages, dbClosers } = await _setupCommonInfrastructure(options);
6115
+ const { port, httpAdapter, authFetchMiddleware, authService, relationalDb, analyticsStore, documentPermissionService, authorizationConfig, attachments, packages, dbClosers, readiness } = await _setupCommonInfrastructure(options);
6105
6116
  const { documentModels, processors, subgraphs } = await packages.init();
6106
6117
  const { module: reactorClientModule, reactorDriveClient } = await clientInitializer(documentModels);
6107
6118
  const reactorClient = reactorClientModule.client;
@@ -6112,10 +6123,11 @@ async function initializeAndStartAPI(clientInitializer, options, processorApp) {
6112
6123
  const documentModelRegistry = reactorClientModule.reactorModule?.documentModelRegistry;
6113
6124
  if (!documentModelRegistry) throw new Error("DocumentModelRegistry not available from ReactorClientModule");
6114
6125
  return {
6115
- ...await _setupAPI(reactorClient, syncManager, reactorProcessorManager, httpAdapter, authFetchMiddleware, authService, port, packages, relationalDb, analyticsStore, documentPermissionService, processors, subgraphs, options, auth, processorApp, (reactorClientModule.reactorModule?.readModelCoordinator)?.readModels ?? [], attachments, authorizationService, documentModelRegistry, dbClosers, reactorDriveClient),
6126
+ ...await _setupAPI(reactorClient, syncManager, reactorProcessorManager, httpAdapter, authFetchMiddleware, authService, port, packages, relationalDb, analyticsStore, documentPermissionService, processors, subgraphs, options, processorApp, (reactorClientModule.reactorModule?.readModelCoordinator)?.readModels ?? [], attachments, authorizationConfig, documentModelRegistry, dbClosers, reactorDriveClient),
6116
6127
  client: reactorClient,
6117
6128
  syncManager,
6118
- documentModelRegistry
6129
+ documentModelRegistry,
6130
+ readiness
6119
6131
  };
6120
6132
  }
6121
6133
  //#endregion
@@ -6219,7 +6231,7 @@ var PackageManagementService = class {
6219
6231
  }
6220
6232
  };
6221
6233
  //#endregion
6222
- export { ADMIN_USERS, ActionContextInputSchema, ActionInputSchema, AddRelationshipDocument, AnalyticsSubgraph, AttachmentInputSchema, AuthService, AuthSubgraph, BaseSubgraph, ChannelMetaInputSchema, CreateDocumentDocument, CreateEmptyDocumentDocument, DeleteDocumentDocument, DeleteDocumentsDocument, DocumentChangeType, DocumentChangeTypeSchema, DocumentChangesDocument, DocumentOperationsFilterInputSchema, DocumentPermissionService, FindDocumentsDocument, GetDocumentDocument, GetDocumentIncomingRelationshipsDocument, GetDocumentModelsDocument, GetDocumentOperationsDocument, GetDocumentOutgoingRelationshipsDocument, GetDocumentWithOperationsDocument, GetJobStatusDocument, GraphQLManager, HttpDocumentModelLoader, HttpPackageLoader, ImportPackageLoader, InMemoryPackageStorage, JobChangesDocument, MoveRelationshipDocument, MutateDocumentAsyncDocument, MutateDocumentDocument, OperationContextInputSchema, OperationInputSchema, OperationWithContextInputSchema, OperationsFilterInputSchema, PackageManagementService, PackageManager, PackagesSubgraph, PagingInputSchema, PhDocumentFieldsFragmentDoc, PollSyncEnvelopesDocument, PropagationMode, PropagationModeSchema, PushSyncEnvelopesDocument, ReactorSignerAppInputSchema, ReactorSignerInputSchema, ReactorSignerUserInputSchema, ReactorSubgraph, RemoteCursorInputSchema, RemoteFilterInputSchema, RemoveRelationshipDocument, RenameDocumentDocument, SearchFilterInputSchema, SetPreferredEditorDocument, SyncEnvelopeInputSchema, SyncEnvelopeType, SyncEnvelopeTypeSchema, SystemSubgraph, TouchChannelDocument, TouchChannelInputSchema, ViewFilterInputSchema, buildGraphQlDocument, buildGraphQlDriveDocument, buildGraphqlOperation, buildGraphqlOperations, buildSubgraphSchemaModule, createAuthFetchMiddleware, createGatewayAdapter, createHttpAdapter, createMergedSchema, createReactorGraphQLClient, createSchema, definedNonNullAnySchema, driveIdFromUrl, extractSubgraphsFromModule, generateDocumentModelSchema, getAuthContext, getDbClient, getDocumentModelSchemaName, getDocumentModelTypeDefs, getGitHash, getGitUrl, getSdk, getUniqueDocumentModels, getVersion, initAnalyticsStoreSql, initializeAndStartAPI, isDefinedNonNullAny, isExpectedLoaderMiss, isSubgraphClass, parseDriveUrl, renderGraphqlPlayground };
6234
+ export { ADMIN_USERS, ActionContextInputSchema, ActionInputSchema, AddRelationshipDocument, AnalyticsSubgraph, AuthService, AuthSubgraph, AuthorizationPolicy, AuthorizedDocumentHandle, BaseSubgraph, ChannelMetaInputSchema, CreateDocumentDocument, CreateEmptyDocumentDocument, DeleteDocumentDocument, DeleteDocumentsDocument, DocumentChangeType, DocumentChangeTypeSchema, DocumentChangesDocument, DocumentOperationsFilterInputSchema, DocumentPermissionService, FindDocumentsDocument, GetDocumentDocument, GetDocumentIncomingRelationshipsDocument, GetDocumentModelsDocument, GetDocumentOperationsDocument, GetDocumentOutgoingRelationshipsDocument, GetDocumentWithOperationsDocument, GetJobStatusDocument, GraphQLManager, HttpDocumentModelLoader, HttpPackageLoader, ImportPackageLoader, InMemoryPackageStorage, JobChangesDocument, MoveRelationshipDocument, MutateDocumentAsyncDocument, MutateDocumentDocument, OperationContextInputSchema, OperationInputSchema, OperationWithContextInputSchema, OperationsFilterInputSchema, PackageManagementService, PackageManager, PackagesSubgraph, PagingInputSchema, PhDocumentFieldsFragmentDoc, PollSyncEnvelopesDocument, PropagationMode, PropagationModeSchema, PushSyncEnvelopesDocument, ReactorSignerAppInputSchema, ReactorSignerInputSchema, ReactorSignerUserInputSchema, ReactorSubgraph, RemoteCursorInputSchema, RemoteFilterInputSchema, RemoveRelationshipDocument, RenameDocumentDocument, SearchFilterInputSchema, SetPreferredEditorDocument, SyncEnvelopeInputSchema, SyncEnvelopeType, SyncEnvelopeTypeSchema, SystemSubgraph, TouchChannelDocument, TouchChannelInputSchema, ViewFilterInputSchema, assertAuthRequiredForDocumentPermissions, assertSkipCredentialVerificationAllowed, buildGraphQlDocument, buildGraphQlDriveDocument, buildGraphqlOperation, buildGraphqlOperations, buildSubgraphSchemaModule, createAuthFetchMiddleware, createGatewayAdapter, createHttpAdapter, createMergedSchema, createReactorGraphQLClient, createSchema, definedNonNullAnySchema, driveIdFromUrl, extractSubgraphsFromModule, generateDocumentModelSchema, getAuthContext, getDbClient, getDocumentModelSchemaName, getDocumentModelTypeDefs, getGitHash, getGitUrl, getSdk, getUniqueDocumentModels, getVersion, initAnalyticsStoreSql, initializeAndStartAPI, isDefinedNonNullAny, isExpectedLoaderMiss, isSubgraphClass, parseDriveUrl, renderGraphqlPlayground };
6223
6235
 
6224
6236
  //# sourceMappingURL=index.mjs.map
6225
- //# debugId=30eaf1c5-796a-5cab-8266-cfe233f9e901
6237
+ //# debugId=cae89b22-c317-5acb-b1ec-dc325f23f6cf