@powerhousedao/reactor-api 6.2.2-dev.53 → 6.2.2-dev.55

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,11 +1,11 @@
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]="3473708d-6af5-5eab-859e-41e65d41fe17")}catch(e){}}();
3
- import { _ as createAuthorizationService, a as isSubgraphClass, d as loadSubgraphs, f as BaseSubgraph, g as AuthorizedDocumentHandle, h as AuthorizationPolicy, i as buildGraphqlOperations, l as loadDocumentModels, m as createCanonicalDocumentIdResolver, n as buildGraphQlDriveDocument, o as debounce, p as CanonicalDocumentIdResolutionError, r as buildGraphqlOperation, s as extractUpgradeManifests, t as buildGraphQlDocument, u as loadProcessors, v as AuthEvaluationUnsupportedError } from "./utils-Tw4hVMpc.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]="71eb0bed-1554-5f30-8744-63f36b8d4c0c")}catch(e){}}();
3
+ import { _ as createAuthorizationService, a as isSubgraphClass, b as MalformedStoredOperationError, d as loadSubgraphs, f as BaseSubgraph, g as AuthorizedDocumentHandle, h as AuthorizationPolicy, i as buildGraphqlOperations, l as loadDocumentModels, m as createCanonicalDocumentIdResolver, n as buildGraphQlDriveDocument, o as debounce, p as CanonicalDocumentIdResolutionError, r as buildGraphqlOperation, s as extractUpgradeManifests, t as buildGraphQlDocument, u as loadProcessors, v as AuthEvaluationUnsupportedError, y as ForbiddenError } from "./utils-O_hBrLd0.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";
7
7
  import { GraphQLError, Kind, parse, print } from "graphql";
8
- import { AuthEnforcementDisabledError, DEFAULT_DRIVE_CONTAINER_TYPES, DriveCollectionId, PropagationMode as PropagationMode$1, consolidateSyncOperations, driveIdFromUrl, envelopesToSyncOperations, parseDriveUrl, syncOperationErrorType } from "@powerhousedao/reactor";
8
+ import { AuthEnforcementDisabledError, DEFAULT_DRIVE_CONTAINER_TYPES, DriveCollectionId, ModelReadGate, PropagationMode as PropagationMode$1, SyncScopeGate, consolidateSyncOperations, driveIdFromUrl, envelopesToSyncOperations, parseDriveUrl, readDecisionModel, syncOperationErrorType } from "@powerhousedao/reactor";
9
9
  import { ConsoleLogger, childLogger, documentModelDocumentModelModule } from "document-model";
10
10
  import path from "node:path";
11
11
  import { match } from "path-to-regexp";
@@ -13,7 +13,7 @@ import { buildSubgraphSchema } from "@apollo/subgraph";
13
13
  import { typeDefs } from "@powerhousedao/document-engineering/graphql";
14
14
  import { camelCase, kebabCase, pascalCase } from "change-case";
15
15
  import { GraphQLJSONObject } from "graphql-type-json";
16
- import { setName } from "@powerhousedao/shared/document-model";
16
+ import { deserializeSignature, serializeSignature, setName } from "@powerhousedao/shared/document-model";
17
17
  import * as z$1 from "zod";
18
18
  import { z } from "zod";
19
19
  import { createHandler } from "graphql-sse/lib/use/fetch";
@@ -968,7 +968,12 @@ function ActionCandidateInputSchema() {
968
968
  });
969
969
  }
970
970
  function ActionContextInputSchema() {
971
- return z$1.object({ signer: z$1.lazy(() => ReactorSignerInputSchema().nullish()) });
971
+ return z$1.object({
972
+ nonce: z$1.string().nullish(),
973
+ prevOpHash: z$1.string().nullish(),
974
+ prevOpIndex: z$1.number().nullish(),
975
+ signer: z$1.lazy(() => ReactorSignerInputSchema().nullish())
976
+ });
972
977
  }
973
978
  function ActionInputSchema() {
974
979
  return z$1.object({
@@ -1367,13 +1372,13 @@ const CreateEmptyDocumentDocument = gql`
1367
1372
  const MutateDocumentDocument = gql`
1368
1373
  mutation MutateDocument(
1369
1374
  $documentIdentifier: String!
1370
- $actions: [JSONObject!]!
1371
- $view: ViewFilterInput
1375
+ $actions: [ActionInput!]!
1376
+ $branch: String
1372
1377
  ) {
1373
- mutateDocument(
1378
+ mutateDocument: execute(
1374
1379
  documentIdentifier: $documentIdentifier
1375
1380
  actions: $actions
1376
- view: $view
1381
+ branch: $branch
1377
1382
  ) {
1378
1383
  ...PHDocumentFields
1379
1384
  }
@@ -1383,14 +1388,21 @@ const MutateDocumentDocument = gql`
1383
1388
  const MutateDocumentAsyncDocument = gql`
1384
1389
  mutation MutateDocumentAsync(
1385
1390
  $documentIdentifier: String!
1386
- $actions: [JSONObject!]!
1387
- $view: ViewFilterInput
1391
+ $actions: [ActionInput!]!
1392
+ $branch: String
1388
1393
  ) {
1389
- mutateDocumentAsync(
1394
+ mutateDocumentAsync: executeAsync(
1390
1395
  documentIdentifier: $documentIdentifier
1391
1396
  actions: $actions
1392
- view: $view
1393
- )
1397
+ branch: $branch
1398
+ ) {
1399
+ id
1400
+ status
1401
+ result
1402
+ error
1403
+ createdAt
1404
+ completedAt
1405
+ }
1394
1406
  }
1395
1407
  `;
1396
1408
  const RenameDocumentDocument = gql`
@@ -1811,22 +1823,112 @@ function toMutableArray(arr) {
1811
1823
  return arr ? [...arr] : void 0;
1812
1824
  }
1813
1825
  /**
1814
- * Validates that a JSONObject represents a valid Action structure
1826
+ * Validates that an incoming value has the structure of an Action.
1827
+ *
1828
+ * This is the whole guard on the way in. `mutateDocument` takes untyped JSON, so
1829
+ * nothing has checked these fields before a resolver runs, and the id in
1830
+ * particular has to be checked here: it is hashed into the operation id and
1831
+ * replay dedupes by it, so an action without one is stored under an operation id
1832
+ * shared by every id-less operation on the same document, scope and branch.
1833
+ *
1834
+ * The storage constraint would refuse such a row anyway, but only after a job
1835
+ * has been enqueued and failed. Refusing here answers the caller instead.
1815
1836
  */
1816
1837
  function validateActionStructure(obj) {
1817
1838
  if (!obj || typeof obj !== "object") return false;
1818
1839
  const action = obj;
1840
+ if (typeof action.id !== "string" || !action.id) return false;
1819
1841
  if (typeof action.type !== "string" || !action.type) return false;
1820
1842
  if (typeof action.scope !== "string" || !action.scope) return false;
1843
+ if (typeof action.timestampUtcMs !== "string" || !action.timestampUtcMs) return false;
1821
1844
  if (!("input" in action)) return false;
1822
1845
  return true;
1823
1846
  }
1824
1847
  /**
1848
+ * Restores an action's signatures to the tuples verification reads.
1849
+ *
1850
+ * A signature is a five-param tuple, and GraphQL declares `signatures` as a
1851
+ * list of strings, so a client joins each one for transport. The sync path
1852
+ * already splits them on arrival; this is the same step for the mutation path,
1853
+ * which had none - an action submitted there kept whatever shape it arrived in,
1854
+ * so the same signature was persisted as a tuple or as one joined string
1855
+ * depending on which door it came through.
1856
+ *
1857
+ * A tuple passes through untouched, so an in-process caller handing over real
1858
+ * signatures is unaffected.
1859
+ */
1860
+ function withDeserializedSignatures(action) {
1861
+ const signer = action.context?.signer;
1862
+ if (!signer?.signatures || signer.signatures.length === 0) return action;
1863
+ return {
1864
+ ...action,
1865
+ context: {
1866
+ ...action.context,
1867
+ signer: {
1868
+ ...signer,
1869
+ signatures: signer.signatures.map(deserializeSignature)
1870
+ }
1871
+ }
1872
+ };
1873
+ }
1874
+ /**
1875
+ * Converts actions the schema has already coerced into the actions the reactor
1876
+ * applies.
1877
+ *
1878
+ * The one conversion the schema cannot express is the signature: GraphQL
1879
+ * declares `signatures` as a list of strings, not of lists, so each arrives
1880
+ * joined and has to be read back as the tuple verification indexes into.
1881
+ *
1882
+ * Nothing is validated here. `ActionInput` states every field the reactor needs,
1883
+ * so anything that reached this point has them - unlike the untyped path, which
1884
+ * has to check by hand.
1885
+ */
1886
+ function toSubmittableActions(actions) {
1887
+ return actions.map((action) => {
1888
+ const context = toSubmittableContext(action.context);
1889
+ const submittable = {
1890
+ id: action.id,
1891
+ type: action.type,
1892
+ timestampUtcMs: action.timestampUtcMs,
1893
+ input: action.input,
1894
+ scope: action.scope
1895
+ };
1896
+ return context ? {
1897
+ ...submittable,
1898
+ context
1899
+ } : submittable;
1900
+ });
1901
+ }
1902
+ /**
1903
+ * `ReactorSignerInput` leaves `user` and `app` optional where `ActionSigner`
1904
+ * requires them, so an incomplete signer cannot be represented. It is carried
1905
+ * through as it arrived rather than filled in: the signature verifier is what
1906
+ * should refuse it, and inventing either field here would be inventing the part
1907
+ * that says who signed.
1908
+ */
1909
+ function toSubmittableContext(context) {
1910
+ if (!context) return;
1911
+ const submittable = {};
1912
+ const prevOpIndex = fromInputMaybe(context.prevOpIndex);
1913
+ if (prevOpIndex !== void 0) submittable.prevOpIndex = prevOpIndex;
1914
+ const prevOpHash = fromInputMaybe(context.prevOpHash);
1915
+ if (prevOpHash !== void 0) submittable.prevOpHash = prevOpHash;
1916
+ const nonce = fromInputMaybe(context.nonce);
1917
+ if (nonce !== void 0) submittable.nonce = nonce;
1918
+ const signer = fromInputMaybe(context.signer);
1919
+ if (signer) submittable.signer = {
1920
+ user: fromInputMaybe(signer.user),
1921
+ app: fromInputMaybe(signer.app),
1922
+ signatures: signer.signatures.map(deserializeSignature)
1923
+ };
1924
+ return Object.keys(submittable).length > 0 ? submittable : void 0;
1925
+ }
1926
+ /**
1825
1927
  * Converts a JSONObject to an Action, validating basic structure
1826
1928
  */
1827
1929
  function jsonObjectToAction(obj) {
1828
- if (!validateActionStructure(obj)) throw new GraphQLError("Invalid action structure. Actions must have: type (string), scope (string), and input (any)");
1829
- return obj;
1930
+ if (!validateActionStructure(obj)) throw new GraphQLError("Invalid action structure. Actions must have: id (string), type (string), scope (string), timestampUtcMs (string), and input (any)");
1931
+ return withDeserializedSignatures(obj);
1830
1932
  }
1831
1933
  /**
1832
1934
  * Validates a list of actions by converting them from JSON and checking structure
@@ -1840,10 +1942,57 @@ function validateActions(actions) {
1840
1942
  }
1841
1943
  return convertedActions;
1842
1944
  }
1945
+ /** The operation fields the schema declares non-null. */
1946
+ const REQUIRED_OPERATION_FIELDS = [
1947
+ "index",
1948
+ "timestampUtcMs",
1949
+ "hash",
1950
+ "skip"
1951
+ ];
1952
+ /** The action fields the schema declares non-null. */
1953
+ const REQUIRED_ACTION_FIELDS = [
1954
+ "id",
1955
+ "type",
1956
+ "timestampUtcMs",
1957
+ "input",
1958
+ "scope"
1959
+ ];
1960
+ /**
1961
+ * Names an operation well enough to find the row it came from, without assuming
1962
+ * the fields that might be the missing ones.
1963
+ */
1964
+ function describeOperation(stored) {
1965
+ const parts = [typeof stored.id === "string" && stored.id ? `operation ${stored.id}` : "operation with no id"];
1966
+ if (typeof stored.index === "number") parts.push(`index ${stored.index}`);
1967
+ const scope = stored.action?.scope;
1968
+ if (typeof scope === "string" && scope) parts.push(`scope ${scope}`);
1969
+ return parts.join(", ");
1970
+ }
1971
+ /**
1972
+ * Refuses an operation the schema cannot represent, naming the field.
1973
+ *
1974
+ * The alternative is what execution does unaided: nullify the field, bubble that
1975
+ * up the non-null chain until it takes out the envelope's whole operation list,
1976
+ * and report a bare "Cannot return null for non-nullable field" with no way to
1977
+ * tell which of a hundred operations was at fault. Checked before the response
1978
+ * is built so the failure is typed and attributable.
1979
+ *
1980
+ * Presence is all that is checked. The schema's own serialization still rejects
1981
+ * a value of the wrong type, and inventing further rules here would put a second
1982
+ * contract beside the one the schema already states.
1983
+ */
1984
+ function assertServable(operation) {
1985
+ const stored = operation;
1986
+ for (const field of REQUIRED_OPERATION_FIELDS) if (stored[field] === void 0 || stored[field] === null) throw new MalformedStoredOperationError(`${describeOperation(stored)} has no ${field}`);
1987
+ const action = stored.action;
1988
+ if (action === void 0 || action === null) throw new MalformedStoredOperationError(`${describeOperation(stored)} has no action`);
1989
+ for (const field of REQUIRED_ACTION_FIELDS) if (action[field] === void 0 || action[field] === null) throw new MalformedStoredOperationError(`${describeOperation(stored)} has an action with no ${field}`);
1990
+ }
1843
1991
  /**
1844
1992
  * Transforms an operation to serialize signatures from tuples to strings for GraphQL compatibility.
1845
1993
  */
1846
1994
  function serializeOperationForGraphQL(operation) {
1995
+ assertServable(operation);
1847
1996
  const signer = operation.action.context?.signer;
1848
1997
  if (!signer?.signatures) return operation;
1849
1998
  return {
@@ -1854,7 +2003,7 @@ function serializeOperationForGraphQL(operation) {
1854
2003
  ...operation.action.context,
1855
2004
  signer: {
1856
2005
  ...signer,
1857
- signatures: signer.signatures.map((sig) => Array.isArray(sig) ? sig.join(", ") : sig)
2006
+ signatures: signer.signatures.map(serializeSignature)
1858
2007
  }
1859
2008
  }
1860
2009
  }
@@ -2267,6 +2416,49 @@ async function createDocumentWithInitialState(reactorClient, args, reactorDriveC
2267
2416
  throw new GraphQLError(`Failed to convert created document to GraphQL: ${error instanceof Error ? error.message : "Unknown error"}`);
2268
2417
  }
2269
2418
  }
2419
+ /** The branch a mutation applies to when the caller names none. */
2420
+ const DEFAULT_BRANCH = "main";
2421
+ /**
2422
+ * Applies actions to a document and waits for the result.
2423
+ *
2424
+ * The wire form of `IReactorClient.execute`, and named for it. Takes the actions
2425
+ * already coerced against `ActionInput`, so the only conversion left is the one
2426
+ * the schema cannot express: a signature travels as one string, and verification
2427
+ * reads it as the tuple it was.
2428
+ */
2429
+ async function execute(reactorClient, args) {
2430
+ const actions = toSubmittableActions(args.actions);
2431
+ const branch = fromInputMaybe(args.branch) ?? DEFAULT_BRANCH;
2432
+ let result;
2433
+ try {
2434
+ result = await reactorClient.execute(args.documentIdentifier, branch, actions);
2435
+ } catch (error) {
2436
+ throw new GraphQLError(`Failed to execute actions: ${error instanceof Error ? error.message : "Unknown error"}`);
2437
+ }
2438
+ try {
2439
+ return toGqlPhDocument(result);
2440
+ } catch (error) {
2441
+ throw new GraphQLError(`Failed to convert executed document to GraphQL: ${error instanceof Error ? error.message : "Unknown error"}`);
2442
+ }
2443
+ }
2444
+ /**
2445
+ * Submits actions to a document and returns the job that will apply them.
2446
+ *
2447
+ * Returns the job rather than only its id, matching
2448
+ * `IReactorClient.executeAsync`. A job handed back from a submission has no
2449
+ * result yet, which is why `JobInfo.result` is nullable.
2450
+ */
2451
+ async function executeAsync(reactorClient, args) {
2452
+ const actions = toSubmittableActions(args.actions);
2453
+ const branch = fromInputMaybe(args.branch) ?? DEFAULT_BRANCH;
2454
+ let job;
2455
+ try {
2456
+ job = await reactorClient.executeAsync(args.documentIdentifier, branch, actions);
2457
+ } catch (error) {
2458
+ throw new GraphQLError(`Failed to submit actions: ${error instanceof Error ? error.message : "Unknown error"}`);
2459
+ }
2460
+ return toGqlJobInfo(job);
2461
+ }
2270
2462
  async function mutateDocument(reactorClient, args) {
2271
2463
  let validatedActions;
2272
2464
  try {
@@ -2400,7 +2592,14 @@ async function deleteDocuments(reactorClient, args) {
2400
2592
  throw new GraphQLError(`Failed to delete documents: ${error instanceof Error ? error.message : "Unknown error"}`);
2401
2593
  }
2402
2594
  }
2403
- async function touchChannel(syncManager, args) {
2595
+ /**
2596
+ * Registers a sync channel, or reports the state of one already registered.
2597
+ *
2598
+ * `boundAddress` claims a channel at creation for the subject that created it.
2599
+ * An anonymously created channel is left unbound, which is not a permission but
2600
+ * an unanswered question: the first authenticated subject to use it adopts it.
2601
+ */
2602
+ async function touchChannel(syncManager, args, boundAddress) {
2404
2603
  try {
2405
2604
  return {
2406
2605
  success: true,
@@ -2412,7 +2611,10 @@ async function touchChannel(syncManager, args) {
2412
2611
  scope: [...args.input.filter.scope],
2413
2612
  branch: args.input.filter.branch
2414
2613
  };
2415
- const options = { sinceTimestampUtcMs: args.input.sinceTimestampUtcMs };
2614
+ const options = {
2615
+ sinceTimestampUtcMs: args.input.sinceTimestampUtcMs,
2616
+ boundAddress
2617
+ };
2416
2618
  try {
2417
2619
  await syncManager.add(args.input.name, DriveCollectionId.fromKey(args.input.collectionId), {
2418
2620
  type: "polling",
@@ -2427,9 +2629,50 @@ async function touchChannel(syncManager, args) {
2427
2629
  };
2428
2630
  }
2429
2631
  /**
2632
+ * The sync operations in `syncOps` this subject may not be served.
2633
+ *
2634
+ * A sync operation is one document, one branch and (from the outbox) one scope,
2635
+ * so the answer is per entry rather than per operation: an entry is withheld
2636
+ * whole or served whole, which is what keeps a scope's operation sequence
2637
+ * unbroken for whoever does receive it. A dead letter can name several scopes,
2638
+ * and is withheld only when the policy serves none of them -- the metadata
2639
+ * scopes are readable by every holder, so in practice a dead letter naming one
2640
+ * is always served.
2641
+ *
2642
+ * The policy is read once per document and branch, and the memo lives no longer
2643
+ * than this call: a poll must not answer from a policy that changed between
2644
+ * polls, since the widening case is exactly the one holding exists to serve.
2645
+ *
2646
+ * A read that fails for any reason other than the document being absent is
2647
+ * allowed to propagate. The poll then fails, the puller retries, and nothing is
2648
+ * served in the meantime -- which is the safe direction, and a visible one.
2649
+ */
2650
+ async function collectHeldSyncOperations(syncOps, gate, subject, signal) {
2651
+ const held = /* @__PURE__ */ new Set();
2652
+ const predicates = /* @__PURE__ */ new Map();
2653
+ for (const syncOp of syncOps) {
2654
+ const key = `${syncOp.documentId}\u0000${syncOp.branch}`;
2655
+ let predicate = predicates.get(key);
2656
+ if (!predicate) {
2657
+ predicate = gate.scopePredicateById(syncOp.documentId, subject, syncOp.branch, signal);
2658
+ predicates.set(key, predicate);
2659
+ }
2660
+ const readable = await predicate;
2661
+ if (!syncOp.scopes.some((scope) => readable(scope))) held.add(syncOp.id);
2662
+ }
2663
+ return held;
2664
+ }
2665
+ /**
2430
2666
  * Polls the switchboard for new sync envelopes and acknowledges previously
2431
2667
  * received operations.
2432
2668
  *
2669
+ * `forbiddenIds` and `heldOpIds` are two different refusals. A forbidden
2670
+ * document is consumed: its delivery counters are advanced as though it had
2671
+ * been sent, so it is never reconsidered. A held sync operation is withheld:
2672
+ * nothing about it is touched, so a policy that later widens serves it whole on
2673
+ * a subsequent poll. Which one applies is the caller's decision, and the two
2674
+ * are intersected, so a document refused either way is not served.
2675
+ *
2433
2676
  * Ordinal frames of reference:
2434
2677
  * - `outboxAck` / `outboxLatest`: switchboard's ordinals (used to trim/filter
2435
2678
  * the switchboard's outbox)
@@ -2437,14 +2680,14 @@ async function touchChannel(syncManager, args) {
2437
2680
  * client ordinal the switchboard has successfully applied, so the client
2438
2681
  * can trim its own outbox)
2439
2682
  */
2440
- function pollSyncEnvelopes(syncManager, args, forbiddenIds = /* @__PURE__ */ new Set()) {
2683
+ function pollSyncEnvelopes(syncManager, args, forbiddenIds = /* @__PURE__ */ new Set(), heldOpIds = /* @__PURE__ */ new Set()) {
2441
2684
  let remote;
2442
2685
  try {
2443
2686
  remote = syncManager.getById(args.channelId);
2444
2687
  } catch (error) {
2445
2688
  throw new GraphQLError(`Channel not found: ${error instanceof Error ? error.message : "Unknown error"}`);
2446
2689
  }
2447
- const deadLetters = remote.channel.deadLetter.items.filter((syncOp) => !forbiddenIds.has(syncOp.documentId)).map((syncOp) => ({
2690
+ const deadLetters = remote.channel.deadLetter.items.filter((syncOp) => !forbiddenIds.has(syncOp.documentId) && !heldOpIds.has(syncOp.id)).map((syncOp) => ({
2448
2691
  documentId: syncOp.documentId,
2449
2692
  error: syncOp.error?.message ?? "Unknown error",
2450
2693
  errorType: syncOperationErrorType(syncOp.error),
@@ -2493,6 +2736,7 @@ function pollSyncEnvelopes(syncManager, args, forbiddenIds = /* @__PURE__ */ new
2493
2736
  hasMore = true;
2494
2737
  break;
2495
2738
  }
2739
+ if (heldOpIds.has(syncOp.id)) continue;
2496
2740
  syncOp.deliveredCount ??= 0;
2497
2741
  syncOp.emittedCount ??= 0;
2498
2742
  while (syncOp.deliveredCount < syncOp.emittedCount && syncOp.operations[syncOp.deliveredCount].context.ordinal <= args.outboxLatest) syncOp.deliveredCount += 1;
@@ -2986,7 +3230,7 @@ var GraphQLManager = class {
2986
3230
  */
2987
3231
  reactorDriveClient;
2988
3232
  authorizationService;
2989
- constructor(path, httpServer, wsServer, reactorClient, relationalDb, analyticsStore, syncManager, logger, httpAdapter, gatewayAdapter, authService, documentPermissionService, featureFlags = DefaultFeatureFlags, port = 4001, authorizationService, reactorDriveClient) {
3233
+ constructor(path, httpServer, wsServer, reactorClient, relationalDb, analyticsStore, syncManager, logger, httpAdapter, gatewayAdapter, authService, documentPermissionService, featureFlags = DefaultFeatureFlags, port = 4001, authorizationService, reactorDriveClient, syncServingGate) {
2990
3234
  this.path = path;
2991
3235
  this.httpServer = httpServer;
2992
3236
  this.wsServer = wsServer;
@@ -3001,6 +3245,7 @@ var GraphQLManager = class {
3001
3245
  this.documentPermissionService = documentPermissionService;
3002
3246
  this.featureFlags = featureFlags;
3003
3247
  this.port = port;
3248
+ this.syncServingGate = syncServingGate;
3004
3249
  if (!authorizationService) throw new Error("GraphQLManager requires an authorizationService");
3005
3250
  this.authorizationService = authorizationService;
3006
3251
  this.reactorDriveClient = reactorDriveClient;
@@ -3086,7 +3331,8 @@ var GraphQLManager = class {
3086
3331
  syncManager: this.syncManager,
3087
3332
  path: this.path,
3088
3333
  documentPermissionService: this.documentPermissionService,
3089
- authorizationService: this.authorizationService
3334
+ authorizationService: this.authorizationService,
3335
+ syncServingGate: this.syncServingGate
3090
3336
  });
3091
3337
  await this.#addSubgraphInstance(subgraphInstance, supergraph, false);
3092
3338
  } catch (error) {
@@ -3140,7 +3386,8 @@ var GraphQLManager = class {
3140
3386
  syncManager: this.syncManager,
3141
3387
  path: this.path,
3142
3388
  documentPermissionService: this.documentPermissionService,
3143
- authorizationService: this.authorizationService
3389
+ authorizationService: this.authorizationService,
3390
+ syncServingGate: this.syncServingGate
3144
3391
  });
3145
3392
  return this.#addSubgraphInstance(subgraphInstance, supergraph, core);
3146
3393
  }
@@ -3802,7 +4049,7 @@ function createReactorGraphQLClient(url, fetchImpl = fetch, headers) {
3802
4049
  }
3803
4050
  //#endregion
3804
4051
  //#region src/graphql/reactor/schema.graphql
3805
- var schema_default = "# Scalar types (for codegen - also defined in create-schema.ts)\nscalar JSONObject\nscalar DateTime\n\n# Input types\ninput PagingInput {\n limit: Int\n offset: Int\n cursor: String\n}\n\ninput ViewFilterInput {\n branch: String\n scopes: [String!]\n}\n\ninput SearchFilterInput {\n type: String\n parentId: String\n identifiers: [String!]\n}\n\ninput OperationsFilterInput {\n documentId: String!\n branch: String\n scopes: [String!]\n actionTypes: [String!]\n sinceRevision: Int\n timestampFrom: String\n timestampTo: String\n}\n\ninput DocumentOperationsFilterInput {\n branch: String\n scopes: [String!]\n actionTypes: [String!]\n sinceRevision: Int\n timestampFrom: String\n timestampTo: String\n}\n\n# Enums\nenum PropagationMode {\n CASCADE\n ORPHAN\n}\n\nenum DocumentChangeType {\n CREATED\n DELETED\n UPDATED\n PARENT_ADDED\n PARENT_REMOVED\n CHILD_ADDED\n CHILD_REMOVED\n}\n\n# Object types\ntype DocumentModelGlobalState {\n id: String!\n name: String!\n namespace: String\n version: String\n specification: JSONObject!\n}\n\ntype DocumentModelResultPage {\n items: [DocumentModelGlobalState!]!\n totalCount: Int!\n hasNextPage: Boolean!\n hasPreviousPage: Boolean!\n cursor: String\n}\n\ntype Revision {\n scope: String!\n revision: Int!\n}\n\ntype PHDocument {\n id: String!\n slug: String\n preferredEditor: String\n name: String!\n documentType: String!\n state: JSONObject!\n revisionsList: [Revision!]!\n createdAtUtcIso: DateTime!\n lastModifiedAtUtcIso: DateTime!\n operations(\n filter: DocumentOperationsFilterInput\n paging: PagingInput\n ): ReactorOperationResultPage\n}\n\ntype PHDocumentResultPage {\n items: [PHDocument!]!\n totalCount: Int!\n hasNextPage: Boolean!\n hasPreviousPage: Boolean!\n cursor: String\n}\n\ntype ReactorOperationResultPage {\n items: [ReactorOperation!]!\n totalCount: Int!\n hasNextPage: Boolean!\n hasPreviousPage: Boolean!\n cursor: String\n}\n\ntype DeadLetterInfo {\n documentId: String!\n error: String!\n \"\"\"\n How the origin classified the failure. A peer needs this to mirror the origin's\n quarantine decision; without it a held auth operation would freeze the document\n on the peer side while the origin kept syncing.\n \"\"\"\n errorType: String\n jobId: String!\n branch: String!\n scopes: [String!]!\n operationCount: Int!\n}\n\ntype PollSyncEnvelopesResult {\n envelopes: [SyncEnvelope!]!\n ackOrdinal: Int!\n deadLetters: [DeadLetterInfo!]!\n hasMore: Boolean!\n}\n\ntype DocumentWithChildren {\n document: PHDocument!\n childIds: [String!]!\n}\n\ntype MoveRelationshipResult {\n source: PHDocument!\n target: PHDocument!\n}\n\ntype JobInfo {\n id: String!\n status: String!\n 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\"\"\"\nOne operation an authorization preflight predicts a verdict for.\n\n`input` is what a conditional grant reads, so a candidate standing for a\nfilled-in form carries that form's input. Omitting it predicts the verdict an\nempty input earns, not the verdict the filled-in form will get.\n\"\"\"\ninput ActionCandidateInput {\n scope: String!\n type: String!\n input: JSONObject\n}\n\nenum AuthDecision {\n ALLOW\n DENY\n}\n\n\"\"\"\nOne candidate's predicted verdict. `reason` carries the refusal a DENY is\nrecorded with, and is null on an ALLOW.\n\"\"\"\ntype ActionEvaluation {\n decision: AuthDecision!\n reason: String\n}\n\n\"\"\"\nThe predicted verdicts for a set of candidates, in the order they were given,\nwith the aggregates a caller branches on.\n\nThe aggregates are redundant -- a verdict is binary -- and all four are returned\nso a caller reads the one its question is phrased in rather than negating\nanother. Over no candidates every aggregate is false: nothing is allowed and\nnothing is denied.\n\"\"\"\ntype ActionEvaluations {\n evaluations: [ActionEvaluation!]!\n allAllowed: Boolean!\n anyAllowed: Boolean!\n allDenied: Boolean!\n anyDenied: Boolean!\n}\n\n# Input types for sync operations\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 deniedReason: String\n id: String\n action: Action!\n}\n\ninput OperationInput {\n index: Int!\n timestampUtcMs: String!\n hash: String!\n skip: Int!\n error: String\n deniedReason: String\n id: String\n action: ActionInput!\n}\n\ntype OperationContext {\n documentId: String!\n documentType: String!\n scope: String!\n branch: String!\n ordinal: Int!\n}\n\ninput OperationContextInput {\n documentId: String!\n documentType: String!\n scope: String!\n branch: String!\n ordinal: Int!\n}\n\ntype OperationWithContext {\n operation: ReactorOperation!\n context: OperationContext!\n}\n\ninput OperationWithContextInput {\n operation: OperationInput!\n context: OperationContextInput!\n}\n\ntype ChannelMeta {\n id: String!\n}\n\ninput ChannelMetaInput {\n id: String!\n}\n\ntype RemoteCursor {\n remoteName: String!\n cursorOrdinal: Int!\n lastSyncedAtUtcMs: String\n}\n\ninput RemoteCursorInput {\n remoteName: String!\n cursorOrdinal: Int!\n lastSyncedAtUtcMs: String\n}\n\nenum SyncEnvelopeType {\n OPERATIONS\n ACK\n}\n\ntype SyncEnvelope {\n type: SyncEnvelopeType!\n channelMeta: ChannelMeta!\n operations: [OperationWithContext!]\n cursor: RemoteCursor\n key: String\n dependsOn: [String!]\n}\n\ninput SyncEnvelopeInput {\n type: SyncEnvelopeType!\n channelMeta: ChannelMetaInput!\n operations: [OperationWithContextInput!]\n cursor: RemoteCursorInput\n key: String\n dependsOn: [String!]\n}\n\ninput RemoteFilterInput {\n documentId: [String!]!\n scope: [String!]!\n branch: String!\n}\n\ninput TouchChannelInput {\n id: String!\n name: String!\n collectionId: String!\n filter: RemoteFilterInput!\n sinceTimestampUtcMs: String!\n}\n\ntype TouchChannelResult {\n success: Boolean!\n ackOrdinal: Int!\n}\n\ntype Query {\n # Get document models for a namespace\n documentModels(\n namespace: String\n paging: PagingInput\n ): DocumentModelResultPage!\n\n # Get a specific document by ID or slug\n document(identifier: String!, view: ViewFilterInput): DocumentWithChildren\n\n # Get outgoing relationships of a given type from a source document\n documentOutgoingRelationships(\n sourceIdentifier: String!\n relationshipType: String!\n view: ViewFilterInput\n paging: PagingInput\n ): PHDocumentResultPage!\n\n # Get incoming relationships of a given type to a target document\n documentIncomingRelationships(\n targetIdentifier: String!\n relationshipType: String!\n view: ViewFilterInput\n paging: PagingInput\n ): PHDocumentResultPage!\n\n # Find documents by search criteria\n findDocuments(\n search: SearchFilterInput\n view: ViewFilterInput\n paging: PagingInput\n ): PHDocumentResultPage!\n\n # Get job status\n jobStatus(jobId: String!): JobInfo\n\n # Get operations for a document with filtering and pagination\n documentOperations(\n filter: OperationsFilterInput!\n paging: PagingInput\n ): ReactorOperationResultPage!\n\n \"\"\"\n Predicts whether the calling subject would be admitted to execute each of a\n set of candidate operations, without submitting any of them. A UI asks this to\n disable a control rather than offer an action that fails on submit.\n\n The answer is a prediction, not a promise:\n\n - Real admission compiles an append condition over everything it read and the\n store enforces it at write time. A preflight reads no future, so a policy\n change landing between this answer and the submit changes the verdict. The\n submit path stays the only authority.\n - The verdict is evaluated at the stream heads, so it is correct for a\n candidate about to be submitted. A backdated submission is out of contract;\n the reactor decides that one by position.\n - A candidate whose verdict depends on its input has to carry that input, since\n a conditional grant reads it.\n\n The subject is the authenticated caller and cannot be named in the request:\n answering for an arbitrary subject would disclose what a policy grants\n somebody else. It carries both the caller's address and the did:key of the app\n instance whose token authenticated the request, so a policy naming either\n matches the same principal the write path presents.\n\n Requires the reactor's authEnforcement feature flag. Without it the reactor\n holds no decision model, so this fails with extensions.code\n AUTH_EVALUATION_UNSUPPORTED rather than guessing.\n \"\"\"\n evaluateActions(\n documentIdentifier: String!\n branch: String\n candidates: [ActionCandidateInput!]!\n ): ActionEvaluations!\n\n # 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";
4052
+ var schema_default = "# Scalar types (for codegen - also defined in create-schema.ts)\nscalar JSONObject\nscalar DateTime\n\n# Input types\ninput PagingInput {\n limit: Int\n offset: Int\n cursor: String\n}\n\ninput ViewFilterInput {\n branch: String\n scopes: [String!]\n}\n\ninput SearchFilterInput {\n type: String\n parentId: String\n identifiers: [String!]\n}\n\ninput OperationsFilterInput {\n documentId: String!\n branch: String\n scopes: [String!]\n actionTypes: [String!]\n sinceRevision: Int\n timestampFrom: String\n timestampTo: String\n}\n\ninput DocumentOperationsFilterInput {\n branch: String\n scopes: [String!]\n actionTypes: [String!]\n sinceRevision: Int\n timestampFrom: String\n timestampTo: String\n}\n\n# Enums\nenum PropagationMode {\n CASCADE\n ORPHAN\n}\n\nenum DocumentChangeType {\n CREATED\n DELETED\n UPDATED\n PARENT_ADDED\n PARENT_REMOVED\n CHILD_ADDED\n CHILD_REMOVED\n}\n\n# Object types\ntype DocumentModelGlobalState {\n id: String!\n name: String!\n namespace: String\n version: String\n specification: JSONObject!\n}\n\ntype DocumentModelResultPage {\n items: [DocumentModelGlobalState!]!\n totalCount: Int!\n hasNextPage: Boolean!\n hasPreviousPage: Boolean!\n cursor: String\n}\n\ntype Revision {\n scope: String!\n revision: Int!\n}\n\ntype PHDocument {\n id: String!\n slug: String\n preferredEditor: String\n name: String!\n documentType: String!\n state: JSONObject!\n revisionsList: [Revision!]!\n createdAtUtcIso: DateTime!\n lastModifiedAtUtcIso: DateTime!\n operations(\n filter: DocumentOperationsFilterInput\n paging: PagingInput\n ): ReactorOperationResultPage\n}\n\ntype PHDocumentResultPage {\n items: [PHDocument!]!\n totalCount: Int!\n hasNextPage: Boolean!\n hasPreviousPage: Boolean!\n cursor: String\n}\n\ntype ReactorOperationResultPage {\n items: [ReactorOperation!]!\n totalCount: Int!\n hasNextPage: Boolean!\n hasPreviousPage: Boolean!\n cursor: String\n}\n\ntype DeadLetterInfo {\n documentId: String!\n error: String!\n \"\"\"\n How the origin classified the failure. A peer needs this to mirror the origin's\n quarantine decision; without it a held auth operation would freeze the document\n on the peer side while the origin kept syncing.\n \"\"\"\n errorType: String\n jobId: String!\n branch: String!\n scopes: [String!]!\n operationCount: Int!\n}\n\ntype PollSyncEnvelopesResult {\n envelopes: [SyncEnvelope!]!\n ackOrdinal: Int!\n deadLetters: [DeadLetterInfo!]!\n hasMore: Boolean!\n}\n\ntype DocumentWithChildren {\n document: PHDocument!\n childIds: [String!]!\n}\n\ntype MoveRelationshipResult {\n source: PHDocument!\n target: PHDocument!\n}\n\ntype JobInfo {\n id: String!\n status: String!\n\n \"\"\"\n What the job produced, once it has produced anything. Null until then, which\n is the state every job is in when it is handed back from a submission.\n \"\"\"\n result: JSONObject\n\n error: String\n createdAt: DateTime!\n completedAt: DateTime\n}\n\ntype DocumentChangeEvent {\n type: DocumentChangeType!\n documents: [PHDocument!]!\n context: DocumentChangeContext\n}\n\ntype DocumentChangeContext {\n parentId: String\n childId: String\n}\n\ntype JobChangeEvent {\n jobId: String!\n status: String!\n result: JSONObject!\n error: String\n}\n\ntype ReactorSignerUser {\n address: String!\n networkId: String!\n chainId: Int!\n}\n\ntype ReactorSignerApp {\n name: String!\n key: String!\n}\n\ntype ReactorSigner {\n user: ReactorSignerUser\n app: ReactorSignerApp\n signatures: [String!]!\n}\n\ntype ActionContext {\n signer: ReactorSigner\n}\n\ntype Action {\n id: String!\n type: String!\n timestampUtcMs: String!\n input: JSONObject!\n scope: String!\n context: ActionContext\n}\n\n\"\"\"\nOne operation an authorization preflight predicts a verdict for.\n\n`input` is what a conditional grant reads, so a candidate standing for a\nfilled-in form carries that form's input. Omitting it predicts the verdict an\nempty input earns, not the verdict the filled-in form will get.\n\"\"\"\ninput ActionCandidateInput {\n scope: String!\n type: String!\n input: JSONObject\n}\n\nenum AuthDecision {\n ALLOW\n DENY\n}\n\n\"\"\"\nOne candidate's predicted verdict. `reason` carries the refusal a DENY is\nrecorded with, and is null on an ALLOW.\n\"\"\"\ntype ActionEvaluation {\n decision: AuthDecision!\n reason: String\n}\n\n\"\"\"\nThe predicted verdicts for a set of candidates, in the order they were given,\nwith the aggregates a caller branches on.\n\nThe aggregates are redundant -- a verdict is binary -- and all four are returned\nso a caller reads the one its question is phrased in rather than negating\nanother. Over no candidates every aggregate is false: nothing is allowed and\nnothing is denied.\n\"\"\"\ntype ActionEvaluations {\n evaluations: [ActionEvaluation!]!\n allAllowed: Boolean!\n anyAllowed: Boolean!\n allDenied: Boolean!\n anyDenied: Boolean!\n}\n\n# Input types for sync operations\n\"\"\"\nAn action's context, as a caller submits it.\n\nDeclares every field `ActionContext` carries, because an input object rejects a\nfield it does not declare: an action stamped with the head it applies to - which\nis what a signing client sends - is refused outright if these are missing, and\nthat refusal is a whole-request one, not a per-field one.\n\n`prevOpHash` and `prevOpIndex` are the scope head the action was composed\nagainst. They are recorded rather than enforced; a signature is verified from\nthe params carried in its own tuple, which do not include them.\n\"\"\"\ninput ActionContextInput {\n \"\"\"The index of the last operation in the scope the action applies to.\"\"\"\n prevOpIndex: Int\n\n \"\"\"The hash of the state the action applies to.\"\"\"\n prevOpHash: String\n\n \"\"\"Covers replay of an operation that would otherwise be a no-op.\"\"\"\n nonce: String\n\n signer: ReactorSignerInput\n}\n\ninput ReactorSignerInput {\n user: ReactorSignerUserInput\n app: ReactorSignerAppInput\n signatures: [String!]!\n}\n\ninput ReactorSignerUserInput {\n address: String!\n networkId: String!\n chainId: Int!\n}\n\ninput ReactorSignerAppInput {\n name: String!\n key: String!\n}\n\ninput ActionInput {\n id: String!\n type: String!\n timestampUtcMs: String!\n input: JSONObject!\n scope: String!\n context: ActionContextInput\n}\n\n# Synchronization types\ntype ReactorOperation {\n index: Int!\n timestampUtcMs: String!\n hash: String!\n skip: Int!\n error: String\n deniedReason: String\n id: String\n action: Action!\n}\n\ninput OperationInput {\n index: Int!\n timestampUtcMs: String!\n hash: String!\n skip: Int!\n error: String\n deniedReason: String\n id: String\n action: ActionInput!\n}\n\ntype OperationContext {\n documentId: String!\n documentType: String!\n scope: String!\n branch: String!\n ordinal: Int!\n}\n\ninput OperationContextInput {\n documentId: String!\n documentType: String!\n scope: String!\n branch: String!\n ordinal: Int!\n}\n\ntype OperationWithContext {\n operation: ReactorOperation!\n context: OperationContext!\n}\n\ninput OperationWithContextInput {\n operation: OperationInput!\n context: OperationContextInput!\n}\n\ntype ChannelMeta {\n id: String!\n}\n\ninput ChannelMetaInput {\n id: String!\n}\n\ntype RemoteCursor {\n remoteName: String!\n cursorOrdinal: Int!\n lastSyncedAtUtcMs: String\n}\n\ninput RemoteCursorInput {\n remoteName: String!\n cursorOrdinal: Int!\n lastSyncedAtUtcMs: String\n}\n\nenum SyncEnvelopeType {\n OPERATIONS\n ACK\n}\n\ntype SyncEnvelope {\n type: SyncEnvelopeType!\n channelMeta: ChannelMeta!\n operations: [OperationWithContext!]\n cursor: RemoteCursor\n key: String\n dependsOn: [String!]\n}\n\ninput SyncEnvelopeInput {\n type: SyncEnvelopeType!\n channelMeta: ChannelMetaInput!\n operations: [OperationWithContextInput!]\n cursor: RemoteCursorInput\n key: String\n dependsOn: [String!]\n}\n\ninput RemoteFilterInput {\n documentId: [String!]!\n scope: [String!]!\n branch: String!\n}\n\ninput TouchChannelInput {\n id: String!\n name: String!\n collectionId: String!\n filter: RemoteFilterInput!\n sinceTimestampUtcMs: String!\n}\n\ntype TouchChannelResult {\n success: Boolean!\n ackOrdinal: Int!\n}\n\ntype Query {\n # Get document models for a namespace\n documentModels(\n namespace: String\n paging: PagingInput\n ): DocumentModelResultPage!\n\n # Get a specific document by ID or slug\n document(identifier: String!, view: ViewFilterInput): DocumentWithChildren\n\n # Get outgoing relationships of a given type from a source document\n documentOutgoingRelationships(\n sourceIdentifier: String!\n relationshipType: String!\n view: ViewFilterInput\n paging: PagingInput\n ): PHDocumentResultPage!\n\n # Get incoming relationships of a given type to a target document\n documentIncomingRelationships(\n targetIdentifier: String!\n relationshipType: String!\n view: ViewFilterInput\n paging: PagingInput\n ): PHDocumentResultPage!\n\n # Find documents by search criteria\n findDocuments(\n search: SearchFilterInput\n view: ViewFilterInput\n paging: PagingInput\n ): PHDocumentResultPage!\n\n # Get job status\n jobStatus(jobId: String!): JobInfo\n\n # Get operations for a document with filtering and pagination\n documentOperations(\n filter: OperationsFilterInput!\n paging: PagingInput\n ): ReactorOperationResultPage!\n\n \"\"\"\n Predicts whether the calling subject would be admitted to execute each of a\n set of candidate operations, without submitting any of them. A UI asks this to\n disable a control rather than offer an action that fails on submit.\n\n The answer is a prediction, not a promise:\n\n - Real admission compiles an append condition over everything it read and the\n store enforces it at write time. A preflight reads no future, so a policy\n change landing between this answer and the submit changes the verdict. The\n submit path stays the only authority.\n - The verdict is evaluated at the stream heads, so it is correct for a\n candidate about to be submitted. A backdated submission is out of contract;\n the reactor decides that one by position.\n - A candidate whose verdict depends on its input has to carry that input, since\n a conditional grant reads it.\n\n The subject is the authenticated caller and cannot be named in the request:\n answering for an arbitrary subject would disclose what a policy grants\n somebody else. It carries both the caller's address and the did:key of the app\n instance whose token authenticated the request, so a policy naming either\n matches the same principal the write path presents.\n\n Requires the reactor's authEnforcement feature flag. Without it the reactor\n holds no decision model, so this fails with extensions.code\n AUTH_EVALUATION_UNSUPPORTED rather than guessing.\n \"\"\"\n evaluateActions(\n documentIdentifier: String!\n branch: String\n candidates: [ActionCandidateInput!]!\n ): ActionEvaluations!\n\n \"\"\"\n Polls for sync envelopes from a channel.\n\n An operation stored before the API required an action id cannot be\n represented by this schema, and rather than serving a partial response beside\n a bare non-null violation, this fails with extensions.code\n MALFORMED_STORED_OPERATION naming the operation. That code means the failure\n is worth polling through: the document holding the operation needs repairing,\n but the channel serves every other document, and a peer that stopped polling\n would stop receiving those too.\n \"\"\"\n pollSyncEnvelopes(\n channelId: String!\n outboxAck: Int!\n outboxLatest: Int!\n ): PollSyncEnvelopesResult!\n}\n\ntype Mutation {\n # Create a new document\n createDocument(document: JSONObject!, parentIdentifier: String): PHDocument!\n\n # Create an empty document of specified type\n createEmptyDocument(\n documentType: String!\n parentIdentifier: String\n ): PHDocument!\n\n \"\"\"\n Applies actions to a document and waits for the result.\n\n Each action is coerced against `ActionInput`, so one missing a field the wire\n declares is refused before any work starts. The id in particular: it is hashed\n into the operation id and replay dedupes by it, so an action without one is\n stored under an operation id shared by every id-less operation on the same\n document, scope and branch.\n\n Named for `IReactorClient.execute`, which this is the wire form of. `branch`\n defaults to `main`.\n \"\"\"\n execute(\n documentIdentifier: String!\n actions: [ActionInput!]!\n branch: String\n ): PHDocument!\n\n \"\"\"\n Submits actions to a document and returns the job that will apply them.\n\n Coerced exactly as `execute` is, so a malformed action is refused here rather\n than surfacing later as a failed job. Returns the job rather than only its id,\n so a caller has the status and the creation time it would otherwise have to\n ask for; `result` is null until the job produces one.\n \"\"\"\n executeAsync(\n documentIdentifier: String!\n actions: [ActionInput!]!\n branch: String\n ): JobInfo!\n\n # Apply actions to a document (synchronous)\n mutateDocument(\n documentIdentifier: String!\n actions: [JSONObject!]!\n view: ViewFilterInput\n ): PHDocument!\n @deprecated(\n reason: \"Use execute. Actions here are untyped, so a malformed one is refused by a hand-written check rather than by the schema, and `view.scopes` is accepted but ignored.\"\n )\n\n # Submit actions to a document (asynchronous)\n mutateDocumentAsync(\n documentIdentifier: String!\n actions: [JSONObject!]!\n view: ViewFilterInput\n ): String!\n @deprecated(\n reason: \"Use executeAsync, which is typed and returns the job rather than only its id.\"\n )\n\n # Rename a document\n renameDocument(\n documentIdentifier: String!\n name: String!\n branch: String\n ): PHDocument!\n\n # Update the preferred editor recorded in the document header meta.\n # Pass null/omit to clear it.\n setPreferredEditor(\n documentIdentifier: String!\n preferredEditor: String\n branch: String\n ): PHDocument!\n\n # Add a relationship between two documents\n addRelationship(\n sourceIdentifier: String!\n targetIdentifier: String!\n relationshipType: String!\n branch: String\n ): PHDocument!\n\n # Remove a relationship between two documents\n removeRelationship(\n sourceIdentifier: String!\n targetIdentifier: String!\n relationshipType: String!\n branch: String\n ): PHDocument!\n\n # Move a relationship from one source to another\n moveRelationship(\n sourceParentIdentifier: String!\n targetParentIdentifier: String!\n targetIdentifier: String!\n relationshipType: String!\n branch: String\n ): MoveRelationshipResult!\n\n # Delete a single document\n deleteDocument(identifier: String!, propagate: PropagationMode): Boolean!\n\n # Delete multiple documents\n deleteDocuments(identifiers: [String!]!, propagate: PropagationMode): Boolean!\n\n # Touch (create or update) a channel for sync\n touchChannel(input: TouchChannelInput!): TouchChannelResult!\n\n # Push sync envelopes to a channel\n pushSyncEnvelopes(envelopes: [SyncEnvelopeInput!]!): Boolean!\n}\n\ntype Subscription {\n # Subscribe to document changes\n documentChanges(\n search: SearchFilterInput\n view: ViewFilterInput\n ): DocumentChangeEvent!\n\n # Subscribe to job changes\n jobChanges(jobId: String!): JobChangeEvent!\n}\n";
3806
4053
  //#endregion
3807
4054
  //#region src/graphql/reactor/pubsub.ts
3808
4055
  const pubSub = new PubSub();
@@ -3940,6 +4187,46 @@ var ReactorSubgraph = class extends BaseSubgraph {
3940
4187
  if (!await this.canReadDocument(documentId, ctx)) forbidden.add(documentId);
3941
4188
  }
3942
4189
  }
4190
+ /**
4191
+ * Enforces which subject a sync channel answers to, adopting an unclaimed one.
4192
+ *
4193
+ * A channel is a private queue: it names a collection, and it accumulates
4194
+ * exactly the operations one subject is owed, so serving it to a second
4195
+ * subject would hand over that subject's cursor as well as its contents. An
4196
+ * unbound channel -- created anonymously, or created before binding existed --
4197
+ * is claimed by the first authenticated subject to use it, and answers to that
4198
+ * address alone thereafter. Adoption is safe precisely because serving
4199
+ * withholds rather than consumes: before it was claimed the channel served
4200
+ * only what an anonymous subject may read, and everything withheld is still
4201
+ * queued for the adopter.
4202
+ *
4203
+ * The refusal is the shape a read denial takes, so a puller treats it as a
4204
+ * signal to authenticate again rather than as a transport failure.
4205
+ *
4206
+ * Nothing is enforced or adopted without a serving gate. Below
4207
+ * `authEnforcement` there is no policy being enforced for the channel to
4208
+ * belong to, and refusing a poll there would break sync for no gain.
4209
+ */
4210
+ async #bindOrRefuseChannel(channelId, ctx) {
4211
+ if (!this.syncServingGate) return;
4212
+ let remote;
4213
+ try {
4214
+ remote = this.syncManager.getById(channelId);
4215
+ } catch {
4216
+ return;
4217
+ }
4218
+ const bound = remote.meta.options.boundAddress;
4219
+ const address = ctx.user?.address;
4220
+ if (bound === void 0) {
4221
+ if (address !== void 0) await this.syncManager.bindRemote(channelId, address);
4222
+ return;
4223
+ }
4224
+ if (bound !== address) throw new ForbiddenError("to poll this sync channel");
4225
+ }
4226
+ /** The address a channel created by this request belongs to, if any. */
4227
+ #creatorBinding(ctx) {
4228
+ return this.syncServingGate ? ctx.user?.address : void 0;
4229
+ }
3943
4230
  typeDefs = gql(schema_default);
3944
4231
  resolvers = {
3945
4232
  PHDocument: { operations: async (parent, args, ctx) => {
@@ -4093,12 +4380,14 @@ var ReactorSubgraph = class extends BaseSubgraph {
4093
4380
  const driveId = remote.meta.collectionId.driveId;
4094
4381
  const isAdmin = this.authorizationService.isSupremeAdmin(ctx.user?.address);
4095
4382
  if (!isAdmin) await this.assertCanReadCanonical(driveId, ctx);
4383
+ await this.#bindOrRefuseChannel(args.channelId, ctx);
4096
4384
  const forbiddenIds = /* @__PURE__ */ new Set();
4097
4385
  if (!isAdmin) {
4098
4386
  if (await this.authorizationService.canRead(driveId, void 0)) await this.#collectForbiddenDocuments(remote.channel.outbox.items, forbiddenIds, ctx);
4099
4387
  await this.#collectForbiddenDocuments(remote.channel.deadLetter.items, forbiddenIds, ctx);
4100
4388
  }
4101
- const { envelopes, ackOrdinal, deadLetters, hasMore } = pollSyncEnvelopes(this.syncManager, args, forbiddenIds);
4389
+ const heldOpIds = this.syncServingGate ? await collectHeldSyncOperations([...remote.channel.outbox.items, ...remote.channel.deadLetter.items], this.syncServingGate, this.viewSubject(ctx)) : /* @__PURE__ */ new Set();
4390
+ const { envelopes, ackOrdinal, deadLetters, hasMore } = pollSyncEnvelopes(this.syncManager, args, forbiddenIds, heldOpIds);
4102
4391
  return {
4103
4392
  envelopes,
4104
4393
  ackOrdinal,
@@ -4144,6 +4433,32 @@ var ReactorSubgraph = class extends BaseSubgraph {
4144
4433
  throw error;
4145
4434
  }
4146
4435
  },
4436
+ execute: async (_parent, args, ctx) => {
4437
+ this.logger.debug("execute(@args)", args);
4438
+ try {
4439
+ const handle = await this.assertCanExecuteOperations(args.documentIdentifier, args.actions, ctx);
4440
+ return await execute(this.reactorClient, {
4441
+ ...args,
4442
+ documentIdentifier: handle.fetchIdentifier
4443
+ });
4444
+ } catch (error) {
4445
+ this.logger.error("Error in execute(@args): @Error", args, error);
4446
+ throw error;
4447
+ }
4448
+ },
4449
+ executeAsync: async (_parent, args, ctx) => {
4450
+ this.logger.debug("executeAsync(@args)", args);
4451
+ try {
4452
+ const handle = await this.assertCanExecuteOperations(args.documentIdentifier, args.actions, ctx);
4453
+ return await executeAsync(this.reactorClient, {
4454
+ ...args,
4455
+ documentIdentifier: handle.fetchIdentifier
4456
+ });
4457
+ } catch (error) {
4458
+ this.logger.error("Error in executeAsync(@args): @Error", args, error);
4459
+ throw error;
4460
+ }
4461
+ },
4147
4462
  mutateDocument: async (_parent, args, ctx) => {
4148
4463
  this.logger.debug("mutateDocument(@args)", args);
4149
4464
  try {
@@ -4277,7 +4592,8 @@ var ReactorSubgraph = class extends BaseSubgraph {
4277
4592
  const driveId = DriveCollectionId.fromKey(args.input.collectionId).driveId;
4278
4593
  await this.assertCanReadCanonical(driveId, ctx);
4279
4594
  }
4280
- return await touchChannel(this.syncManager, args);
4595
+ await this.#bindOrRefuseChannel(args.input.id, ctx);
4596
+ return await touchChannel(this.syncManager, args, this.#creatorBinding(ctx));
4281
4597
  } catch (error) {
4282
4598
  this.logger.error("Error in touchChannel(@args): @Error", args, error);
4283
4599
  throw error;
@@ -4389,10 +4705,10 @@ const ADMIN_USERS = getAdminUsers();
4389
4705
  //#endregion
4390
4706
  //#region src/graphql/system/version.ts
4391
4707
  function getVersion() {
4392
- return "6.2.2-dev.53";
4708
+ return "6.2.2-dev.55";
4393
4709
  }
4394
4710
  function getGitHash() {
4395
- return "0d0b7e3227c12c038a0c44dd17c3ef450498dd33";
4711
+ return "a81c8b2f0201aedffc227bccabd49ceffa3806de";
4396
4712
  }
4397
4713
  function getGitUrl() {
4398
4714
  return buildTreeUrl(getGitHash());
@@ -5905,10 +6221,31 @@ function makeDbClosers(knexInstance, pglite) {
5905
6221
  return closers;
5906
6222
  }
5907
6223
  /**
6224
+ * The gate sync serving evaluates a document's own policy through, or undefined
6225
+ * when there is none to evaluate.
6226
+ *
6227
+ * It is built here rather than taken off the reactor client because it is not
6228
+ * the same gate reads use: it carries the host's closes-by-default setting,
6229
+ * which withholds the domain scopes of a document nobody has policied yet. That
6230
+ * answer belongs to serving alone -- replay must keep reading an uninitialized
6231
+ * document in full -- so the two gates are deliberately separate objects over
6232
+ * the same model.
6233
+ *
6234
+ * Undefined below `authEnforcement`, where the registered model ignores the auth
6235
+ * scope: gating through it would serve every domain scope of a policied document
6236
+ * to anyone, which is worse than not gating at all.
6237
+ */
6238
+ function buildSyncServingGate(reactorModule, authorizationConfig, logger) {
6239
+ if (!reactorModule) return;
6240
+ const model = readDecisionModel(reactorModule.featureFlags, reactorModule.documentModelRegistry);
6241
+ if (!model) return;
6242
+ return new SyncScopeGate(new ModelReadGate(model, reactorModule.documentView, reactorModule.featureFlags.authGroups, reactorModule.operationIndex, logger, { withholdUninitialized: authorizationConfig.defaultProtection }), reactorModule.documentView, logger);
6243
+ }
6244
+ /**
5908
6245
  * Sets up the subgraph manager and registers subgraphs
5909
6246
  */
5910
- async function setupGraphQLManager(httpAdapter, authFetchMiddleware, httpServer, wsServer, client, relationalDb, analyticsStore, syncManager, subgraphs, logger, authorizationService, authService, documentPermissionService, enableDocumentModelSubgraphs, port, reactorDriveClient) {
5911
- const graphqlManager = new GraphQLManager(config.basePath, httpServer, wsServer, client, relationalDb, analyticsStore, syncManager, logger, httpAdapter, await createGatewayAdapter("apollo", logger), authService, documentPermissionService, { enableDocumentModelSubgraphs }, port, authorizationService, reactorDriveClient);
6247
+ async function setupGraphQLManager(httpAdapter, authFetchMiddleware, httpServer, wsServer, client, relationalDb, analyticsStore, syncManager, subgraphs, logger, authorizationService, authService, documentPermissionService, enableDocumentModelSubgraphs, port, reactorDriveClient, syncServingGate) {
6248
+ const graphqlManager = new GraphQLManager(config.basePath, httpServer, wsServer, client, relationalDb, analyticsStore, syncManager, logger, httpAdapter, await createGatewayAdapter("apollo", logger), authService, documentPermissionService, { enableDocumentModelSubgraphs }, port, authorizationService, reactorDriveClient, syncServingGate);
5912
6249
  await graphqlManager.init(subgraphs.core, authFetchMiddleware);
5913
6250
  for (const [, collection] of subgraphs.extended.entries()) for (const subgraph of collection) await graphqlManager.registerSubgraph(subgraph, "graphql");
5914
6251
  await graphqlManager.updateRouter();
@@ -6104,7 +6441,7 @@ async function _setupCommonInfrastructure(options) {
6104
6441
  /**
6105
6442
  * Private helper function containing common setup logic for API initialization
6106
6443
  */
6107
- async function _setupAPI(reactorClient, syncManager, reactorProcessorManager, httpAdapter, authFetchMiddleware, authService, port, packages, relationalDb, analyticsStore, documentPermissionService, processors, subgraphs, options, processorApp, readModels, attachments, attachmentReferenceIndex, attachmentReferenceProjection, authorizationConfig, documentModelRegistry, dbClosers = [], reactorDriveClient) {
6444
+ async function _setupAPI(reactorClient, syncManager, reactorProcessorManager, httpAdapter, authFetchMiddleware, authService, port, packages, relationalDb, analyticsStore, documentPermissionService, processors, subgraphs, options, processorApp, readModels, attachments, attachmentReferenceIndex, attachmentReferenceProjection, authorizationConfig, documentModelRegistry, dbClosers = [], reactorDriveClient, syncServingGate) {
6108
6445
  const hostModule = {
6109
6446
  relationalDb,
6110
6447
  analyticsStore,
@@ -6161,7 +6498,7 @@ async function _setupAPI(reactorClient, syncManager, reactorProcessorManager, ht
6161
6498
  const graphqlManager = await setupGraphQLManager(httpAdapter, authFetchMiddleware, httpServer, wsServer, reactorClient, relationalDb, analyticsStore, syncManager, {
6162
6499
  extended: subgraphs,
6163
6500
  core: coreSubgraphs
6164
- }, logger.child(["graphql-manager"]), authorizationService, authService, documentPermissionService, options.enableDocumentModelSubgraphs, port, reactorDriveClient);
6501
+ }, logger.child(["graphql-manager"]), authorizationService, authService, documentPermissionService, options.enableDocumentModelSubgraphs, port, reactorDriveClient, syncServingGate);
6165
6502
  setupEventListeners(packages, graphqlManager, reactorProcessorManager, hostModule, documentModelRegistry);
6166
6503
  if (mcpServerEnabled) {
6167
6504
  await setupMcpServer({
@@ -6243,7 +6580,7 @@ async function initializeAndStartAPI(clientInitializer, options, processorApp) {
6243
6580
  const documentModelRegistry = reactorClientModule.reactorModule?.documentModelRegistry;
6244
6581
  if (!documentModelRegistry) throw new Error("DocumentModelRegistry not available from InProcessReactorClientModule");
6245
6582
  return {
6246
- ...await _setupAPI(reactorClient, syncManager, reactorProcessorManager, httpAdapter, authFetchMiddleware, authService, port, packages, relationalDb, analyticsStore, documentPermissionService, processors, subgraphs, options, processorApp, (reactorClientModule.reactorModule?.readModelCoordinator)?.readModels ?? [], attachments, attachmentReferenceIndex, attachmentReferenceProjection, authorizationConfig, documentModelRegistry, dbClosers, reactorDriveClient),
6583
+ ...await _setupAPI(reactorClient, syncManager, reactorProcessorManager, httpAdapter, authFetchMiddleware, authService, port, packages, relationalDb, analyticsStore, documentPermissionService, processors, subgraphs, options, processorApp, (reactorClientModule.reactorModule?.readModelCoordinator)?.readModels ?? [], attachments, attachmentReferenceIndex, attachmentReferenceProjection, authorizationConfig, documentModelRegistry, dbClosers, reactorDriveClient, buildSyncServingGate(reactorClientModule.reactorModule, authorizationConfig, options.logger ?? defaultLogger)),
6247
6584
  client: reactorClient,
6248
6585
  syncManager,
6249
6586
  documentModelRegistry,
@@ -6355,4 +6692,4 @@ var PackageManagementService = class {
6355
6692
  export { ADMIN_USERS, ActionCandidateInputSchema, ActionContextInputSchema, ActionInputSchema, AddRelationshipDocument, AnalyticsSubgraph, AttachmentAccessService, AuthDecision, AuthDecisionSchema, AuthService, AuthSubgraph, AuthorizationPolicy, AuthorizedDocumentHandle, BaseSubgraph, CanonicalDocumentIdResolutionError, ChannelMetaInputSchema, CreateDocumentDocument, CreateEmptyDocumentDocument, DeleteDocumentDocument, DeleteDocumentsDocument, DocumentChangeType, DocumentChangeTypeSchema, DocumentChangesDocument, DocumentOperationsFilterInputSchema, DocumentPermissionService, EvaluateActionsDocument, FindDocumentsDocument, GetDocumentDocument, GetDocumentIncomingRelationshipsDocument, GetDocumentModelsDocument, GetDocumentOperationsDocument, GetDocumentOutgoingRelationshipsDocument, GetDocumentWithOperationsDocument, GetJobStatusDocument, GraphQLManager, HttpDocumentModelLoader, HttpPackageLoader, ImportPackageLoader, InMemoryPackageStorage, JobChangesDocument, MoveRelationshipDocument, MutateDocumentAsyncDocument, MutateDocumentDocument, OperationContextInputSchema, OperationInputSchema, OperationWithContextInputSchema, OperationsFilterInputSchema, PGLITE_UTC_PARSERS, 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, assertCredentialVerifierForSource, assertSkipCredentialVerificationAllowed, buildGraphQlDocument, buildGraphQlDriveDocument, buildGraphqlOperation, buildGraphqlOperations, buildSubgraphSchemaModule, createAuthFetchMiddleware, createCanonicalDocumentIdResolver, createGatewayAdapter, createHttpAdapter, createMergedSchema, createReactorGraphQLClient, createRenownCredentialVerifier, createSchema, definedNonNullAnySchema, driveIdFromUrl, extractSubgraphsFromModule, generateDocumentModelSchema, getAuthContext, getDbClient, getDocumentModelSchemaName, getDocumentModelTypeDefs, getGitHash, getGitUrl, getSdk, getUniqueDocumentModels, getUniqueUpgradeManifests, getVersion, initAnalyticsStoreSql, initializeAndStartAPI, isDefinedNonNullAny, isExpectedLoaderMiss, isSubgraphClass, parseDriveUrl, renderGraphqlPlayground, resolveRenownConfig };
6356
6693
 
6357
6694
  //# sourceMappingURL=index.mjs.map
6358
- //# debugId=3473708d-6af5-5eab-859e-41e65d41fe17
6695
+ //# debugId=71eb0bed-1554-5f30-8744-63f36b8d4c0c