@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.d.mts +96 -17
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +373 -36
- package/dist/index.mjs.map +1 -1
- package/dist/src/packages/vite-loader.mjs +1 -1
- package/dist/{utils-Tw4hVMpc.mjs → utils-O_hBrLd0.mjs} +31 -5
- package/dist/utils-O_hBrLd0.mjs.map +1 -0
- package/package.json +14 -14
- package/dist/utils-Tw4hVMpc.mjs.map +0 -1
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]="
|
|
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-
|
|
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({
|
|
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: [
|
|
1371
|
-
$
|
|
1375
|
+
$actions: [ActionInput!]!
|
|
1376
|
+
$branch: String
|
|
1372
1377
|
) {
|
|
1373
|
-
mutateDocument(
|
|
1378
|
+
mutateDocument: execute(
|
|
1374
1379
|
documentIdentifier: $documentIdentifier
|
|
1375
1380
|
actions: $actions
|
|
1376
|
-
|
|
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: [
|
|
1387
|
-
$
|
|
1391
|
+
$actions: [ActionInput!]!
|
|
1392
|
+
$branch: String
|
|
1388
1393
|
) {
|
|
1389
|
-
mutateDocumentAsync(
|
|
1394
|
+
mutateDocumentAsync: executeAsync(
|
|
1390
1395
|
documentIdentifier: $documentIdentifier
|
|
1391
1396
|
actions: $actions
|
|
1392
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
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 = {
|
|
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
|
|
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
|
-
|
|
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.
|
|
4708
|
+
return "6.2.2-dev.55";
|
|
4393
4709
|
}
|
|
4394
4710
|
function getGitHash() {
|
|
4395
|
-
return "
|
|
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=
|
|
6695
|
+
//# debugId=71eb0bed-1554-5f30-8744-63f36b8d4c0c
|