@powerhousedao/reactor-api 6.2.2-dev.8 → 6.2.2-dev.80
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/{adapter-gateway-apollo-CxV_hMTv.mjs → adapter-gateway-apollo-DdamNLPT.mjs} +10 -4
- package/dist/adapter-gateway-apollo-DdamNLPT.mjs.map +1 -0
- package/dist/index.d.mts +458 -36
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +928 -139
- package/dist/index.mjs.map +1 -1
- package/dist/src/packages/https-hooks.mjs +1 -1
- package/dist/src/packages/vite-loader.d.mts +3 -2
- package/dist/src/packages/vite-loader.d.mts.map +1 -1
- package/dist/src/packages/vite-loader.mjs +21 -8
- package/dist/src/packages/vite-loader.mjs.map +1 -1
- package/dist/{types-DnHgkahK.d.mts → types-D2ZrjmCk.d.mts} +8 -2
- package/dist/types-D2ZrjmCk.d.mts.map +1 -0
- package/dist/{utils-iibOQ50e.mjs → utils-lHoPB-ft.mjs} +130 -23
- package/dist/utils-lHoPB-ft.mjs.map +1 -0
- package/package.json +21 -19
- package/dist/adapter-gateway-apollo-CxV_hMTv.mjs.map +0 -1
- package/dist/types-DnHgkahK.d.mts.map +0 -1
- package/dist/utils-iibOQ50e.mjs.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
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 { a as isSubgraphClass,
|
|
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]="0d1c78be-0026-5af3-b8dd-9282b69fde6d")}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-lHoPB-ft.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 { DEFAULT_DRIVE_CONTAINER_TYPES, DriveCollectionId, PropagationMode as PropagationMode$1, consolidateSyncOperations, driveIdFromUrl, envelopesToSyncOperations, parseDriveUrl } from "@powerhousedao/reactor";
|
|
8
|
+
import { AuthEnforcementDisabledError, DEFAULT_DRIVE_CONTAINER_TYPES, DriveCollectionId, ModelReadGate, PropagationMode as PropagationMode$1, SyncScopeGate, consolidateSyncOperations, createReactorHostModuleBase, 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";
|
|
12
12
|
import { buildSubgraphSchema } from "@apollo/subgraph";
|
|
13
13
|
import { typeDefs } from "@powerhousedao/document-engineering/graphql";
|
|
14
14
|
import { camelCase, kebabCase, pascalCase } from "change-case";
|
|
15
|
-
import { GraphQLJSONObject } from "graphql-type-json";
|
|
16
|
-
import { setName } from "@powerhousedao/shared/document-model";
|
|
15
|
+
import { GraphQLJSON, GraphQLJSONObject } from "graphql-type-json";
|
|
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";
|
|
@@ -22,11 +22,12 @@ import dotenv from "dotenv";
|
|
|
22
22
|
import { buildTreeUrl } from "@powerhousedao/shared";
|
|
23
23
|
import { getConfig } from "@powerhousedao/config/node";
|
|
24
24
|
import { reactorDriveDocumentModelModule } from "@powerhousedao/reactor-drive";
|
|
25
|
+
import { ReactorGroupV1 } from "@powerhousedao/reactor-group";
|
|
25
26
|
import { driveDocumentModelModule } from "@powerhousedao/shared/document-drive";
|
|
26
27
|
import EventEmitter from "node:events";
|
|
27
28
|
import fs, { watchFile } from "node:fs";
|
|
28
29
|
import { PostgresAnalyticsStore } from "@powerhousedao/analytics-engine-pg";
|
|
29
|
-
import { AttachmentBuilder } from "@powerhousedao/reactor-attachments";
|
|
30
|
+
import { AttachmentBuilder, AttachmentReferenceIndexBuilder, createRef, createS3AttachmentBackend, parseAttachmentStorageConfig, parseRef } from "@powerhousedao/reactor-attachments";
|
|
30
31
|
import { createAttachmentClient } from "@powerhousedao/reactor-attachments/client";
|
|
31
32
|
import { setupMcpServer } from "@powerhousedao/reactor-mcp";
|
|
32
33
|
import { mkdir } from "node:fs/promises";
|
|
@@ -34,7 +35,7 @@ import { tmpdir } from "node:os";
|
|
|
34
35
|
import { WebSocketServer } from "ws";
|
|
35
36
|
import { createRelationalDb } from "@powerhousedao/shared/processors";
|
|
36
37
|
import { Kysely, Migrator, sql } from "kysely";
|
|
37
|
-
import { fetchDelegationCredential, verifyAuthBearerToken } from "@renown/sdk";
|
|
38
|
+
import { fetchDelegationCredential, resolveSwitchboardEndpoint, verifyAuthBearerToken } from "@renown/sdk";
|
|
38
39
|
import { PGlite } from "@electric-sql/pglite";
|
|
39
40
|
import { AtomicNodeFs } from "@powerhousedao/pglite-fs";
|
|
40
41
|
import knex from "knex";
|
|
@@ -305,7 +306,7 @@ function createAuthFetchMiddleware(authService) {
|
|
|
305
306
|
async function createGatewayAdapter(type, logger) {
|
|
306
307
|
switch (type) {
|
|
307
308
|
case "apollo": {
|
|
308
|
-
const { ApolloGatewayAdapter } = await import("./adapter-gateway-apollo-
|
|
309
|
+
const { ApolloGatewayAdapter } = await import("./adapter-gateway-apollo-DdamNLPT.mjs");
|
|
309
310
|
return new ApolloGatewayAdapter(logger);
|
|
310
311
|
}
|
|
311
312
|
case "mercurius": {
|
|
@@ -396,7 +397,8 @@ const dedupeTypeDefinitions = (doc) => {
|
|
|
396
397
|
const buildSubgraphSchemaModule = (documentModels, resolvers, typeDefs) => {
|
|
397
398
|
const newResolvers = {
|
|
398
399
|
...resolvers,
|
|
399
|
-
JSONObject: GraphQLJSONObject
|
|
400
|
+
JSONObject: GraphQLJSONObject,
|
|
401
|
+
Unknown: GraphQLJSON
|
|
400
402
|
};
|
|
401
403
|
return {
|
|
402
404
|
typeDefs: getDocumentModelTypeDefs(documentModels, typeDefs),
|
|
@@ -404,7 +406,7 @@ const buildSubgraphSchemaModule = (documentModels, resolvers, typeDefs) => {
|
|
|
404
406
|
};
|
|
405
407
|
};
|
|
406
408
|
const createSchema = (documentModels, resolvers, typeDefs) => {
|
|
407
|
-
return buildSubgraphSchema(buildSubgraphSchemaModule(documentModels, resolvers, typeDefs));
|
|
409
|
+
return buildSubgraphSchema([buildSubgraphSchemaModule(documentModels, resolvers, typeDefs)]);
|
|
408
410
|
};
|
|
409
411
|
/**
|
|
410
412
|
* Create a merged GraphQL schema from multiple subgraph modules.
|
|
@@ -460,6 +462,10 @@ const getDocumentModelTypeDefs = (documentModels, typeDefs$1) => {
|
|
|
460
462
|
});
|
|
461
463
|
return dedupeTypeDefinitions(gql`
|
|
462
464
|
scalar JSONObject
|
|
465
|
+
scalar AttachmentRef
|
|
466
|
+
# Codegen scalars not in the document-engineering set.
|
|
467
|
+
scalar Unknown
|
|
468
|
+
scalar Address
|
|
463
469
|
${typeDefs.join("\n").replaceAll(";", "")}
|
|
464
470
|
|
|
465
471
|
type PHOperationContext {
|
|
@@ -878,6 +884,7 @@ function generateNewApiSchema(documentName, specification, _stateSchemaTypes, pr
|
|
|
878
884
|
return gql`
|
|
879
885
|
scalar DateTime
|
|
880
886
|
scalar JSONObject
|
|
887
|
+
scalar AttachmentRef
|
|
881
888
|
|
|
882
889
|
${revisionType}
|
|
883
890
|
|
|
@@ -926,6 +933,11 @@ function generateNewApiSchema(documentName, specification, _stateSchemaTypes, pr
|
|
|
926
933
|
}
|
|
927
934
|
//#endregion
|
|
928
935
|
//#region src/graphql/reactor/gen/graphql.ts
|
|
936
|
+
let AuthDecision = /* @__PURE__ */ function(AuthDecision) {
|
|
937
|
+
AuthDecision["Allow"] = "ALLOW";
|
|
938
|
+
AuthDecision["Deny"] = "DENY";
|
|
939
|
+
return AuthDecision;
|
|
940
|
+
}({});
|
|
929
941
|
let DocumentChangeType = /* @__PURE__ */ function(DocumentChangeType) {
|
|
930
942
|
DocumentChangeType["ChildAdded"] = "CHILD_ADDED";
|
|
931
943
|
DocumentChangeType["ChildRemoved"] = "CHILD_REMOVED";
|
|
@@ -948,11 +960,24 @@ let SyncEnvelopeType = /* @__PURE__ */ function(SyncEnvelopeType) {
|
|
|
948
960
|
}({});
|
|
949
961
|
const isDefinedNonNullAny = (v) => v !== void 0 && v !== null;
|
|
950
962
|
const definedNonNullAnySchema = z$1.any().refine((v) => isDefinedNonNullAny(v));
|
|
963
|
+
const AuthDecisionSchema = z$1.enum(AuthDecision);
|
|
951
964
|
const DocumentChangeTypeSchema = z$1.enum(DocumentChangeType);
|
|
952
965
|
const PropagationModeSchema = z$1.enum(PropagationMode);
|
|
953
966
|
const SyncEnvelopeTypeSchema = z$1.enum(SyncEnvelopeType);
|
|
967
|
+
function ActionCandidateInputSchema() {
|
|
968
|
+
return z$1.object({
|
|
969
|
+
input: z$1.custom((v) => v != null).nullish(),
|
|
970
|
+
scope: z$1.string(),
|
|
971
|
+
type: z$1.string()
|
|
972
|
+
});
|
|
973
|
+
}
|
|
954
974
|
function ActionContextInputSchema() {
|
|
955
|
-
return z$1.object({
|
|
975
|
+
return z$1.object({
|
|
976
|
+
nonce: z$1.string().nullish(),
|
|
977
|
+
prevOpHash: z$1.string().nullish(),
|
|
978
|
+
prevOpIndex: z$1.number().nullish(),
|
|
979
|
+
signer: z$1.lazy(() => ReactorSignerInputSchema().nullish())
|
|
980
|
+
});
|
|
956
981
|
}
|
|
957
982
|
function ActionInputSchema() {
|
|
958
983
|
return z$1.object({
|
|
@@ -989,6 +1014,7 @@ function OperationContextInputSchema() {
|
|
|
989
1014
|
function OperationInputSchema() {
|
|
990
1015
|
return z$1.object({
|
|
991
1016
|
action: z$1.lazy(() => ActionInputSchema()),
|
|
1017
|
+
deniedReason: z$1.string().nullish(),
|
|
992
1018
|
error: z$1.string().nullish(),
|
|
993
1019
|
hash: z$1.string(),
|
|
994
1020
|
id: z$1.string().nullish(),
|
|
@@ -1147,6 +1173,7 @@ const GetDocumentWithOperationsDocument = gql`
|
|
|
1147
1173
|
hash
|
|
1148
1174
|
skip
|
|
1149
1175
|
error
|
|
1176
|
+
deniedReason
|
|
1150
1177
|
id
|
|
1151
1178
|
action {
|
|
1152
1179
|
id
|
|
@@ -1259,6 +1286,7 @@ const GetDocumentOperationsDocument = gql`
|
|
|
1259
1286
|
hash
|
|
1260
1287
|
skip
|
|
1261
1288
|
error
|
|
1289
|
+
deniedReason
|
|
1262
1290
|
id
|
|
1263
1291
|
action {
|
|
1264
1292
|
id
|
|
@@ -1301,6 +1329,28 @@ const GetJobStatusDocument = gql`
|
|
|
1301
1329
|
}
|
|
1302
1330
|
}
|
|
1303
1331
|
`;
|
|
1332
|
+
const EvaluateActionsDocument = gql`
|
|
1333
|
+
query EvaluateActions(
|
|
1334
|
+
$documentIdentifier: String!
|
|
1335
|
+
$branch: String
|
|
1336
|
+
$candidates: [ActionCandidateInput!]!
|
|
1337
|
+
) {
|
|
1338
|
+
evaluateActions(
|
|
1339
|
+
documentIdentifier: $documentIdentifier
|
|
1340
|
+
branch: $branch
|
|
1341
|
+
candidates: $candidates
|
|
1342
|
+
) {
|
|
1343
|
+
evaluations {
|
|
1344
|
+
decision
|
|
1345
|
+
reason
|
|
1346
|
+
}
|
|
1347
|
+
allAllowed
|
|
1348
|
+
anyAllowed
|
|
1349
|
+
allDenied
|
|
1350
|
+
anyDenied
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
`;
|
|
1304
1354
|
const CreateDocumentDocument = gql`
|
|
1305
1355
|
mutation CreateDocument($document: JSONObject!, $parentIdentifier: String) {
|
|
1306
1356
|
createDocument(document: $document, parentIdentifier: $parentIdentifier) {
|
|
@@ -1326,13 +1376,13 @@ const CreateEmptyDocumentDocument = gql`
|
|
|
1326
1376
|
const MutateDocumentDocument = gql`
|
|
1327
1377
|
mutation MutateDocument(
|
|
1328
1378
|
$documentIdentifier: String!
|
|
1329
|
-
$actions: [
|
|
1330
|
-
$
|
|
1379
|
+
$actions: [ActionInput!]!
|
|
1380
|
+
$branch: String
|
|
1331
1381
|
) {
|
|
1332
|
-
mutateDocument(
|
|
1382
|
+
mutateDocument: execute(
|
|
1333
1383
|
documentIdentifier: $documentIdentifier
|
|
1334
1384
|
actions: $actions
|
|
1335
|
-
|
|
1385
|
+
branch: $branch
|
|
1336
1386
|
) {
|
|
1337
1387
|
...PHDocumentFields
|
|
1338
1388
|
}
|
|
@@ -1342,14 +1392,21 @@ const MutateDocumentDocument = gql`
|
|
|
1342
1392
|
const MutateDocumentAsyncDocument = gql`
|
|
1343
1393
|
mutation MutateDocumentAsync(
|
|
1344
1394
|
$documentIdentifier: String!
|
|
1345
|
-
$actions: [
|
|
1346
|
-
$
|
|
1395
|
+
$actions: [ActionInput!]!
|
|
1396
|
+
$branch: String
|
|
1347
1397
|
) {
|
|
1348
|
-
mutateDocumentAsync(
|
|
1398
|
+
mutateDocumentAsync: executeAsync(
|
|
1349
1399
|
documentIdentifier: $documentIdentifier
|
|
1350
1400
|
actions: $actions
|
|
1351
|
-
|
|
1352
|
-
)
|
|
1401
|
+
branch: $branch
|
|
1402
|
+
) {
|
|
1403
|
+
id
|
|
1404
|
+
status
|
|
1405
|
+
result
|
|
1406
|
+
error
|
|
1407
|
+
createdAt
|
|
1408
|
+
completedAt
|
|
1409
|
+
}
|
|
1353
1410
|
}
|
|
1354
1411
|
`;
|
|
1355
1412
|
const RenameDocumentDocument = gql`
|
|
@@ -1509,6 +1566,7 @@ const PollSyncEnvelopesDocument = gql`
|
|
|
1509
1566
|
hash
|
|
1510
1567
|
skip
|
|
1511
1568
|
error
|
|
1569
|
+
deniedReason
|
|
1512
1570
|
id
|
|
1513
1571
|
action {
|
|
1514
1572
|
id
|
|
@@ -1551,6 +1609,7 @@ const PollSyncEnvelopesDocument = gql`
|
|
|
1551
1609
|
deadLetters {
|
|
1552
1610
|
documentId
|
|
1553
1611
|
error
|
|
1612
|
+
errorType
|
|
1554
1613
|
}
|
|
1555
1614
|
hasMore
|
|
1556
1615
|
}
|
|
@@ -1595,6 +1654,9 @@ function getSdk(requester) {
|
|
|
1595
1654
|
GetJobStatus(variables, options) {
|
|
1596
1655
|
return requester(GetJobStatusDocument, variables, options);
|
|
1597
1656
|
},
|
|
1657
|
+
EvaluateActions(variables, options) {
|
|
1658
|
+
return requester(EvaluateActionsDocument, variables, options);
|
|
1659
|
+
},
|
|
1598
1660
|
CreateDocument(variables, options) {
|
|
1599
1661
|
return requester(CreateDocumentDocument, variables, options);
|
|
1600
1662
|
},
|
|
@@ -1745,28 +1807,132 @@ function toReactorPropagationMode(gqlMode) {
|
|
|
1745
1807
|
}
|
|
1746
1808
|
}
|
|
1747
1809
|
/**
|
|
1810
|
+
* Maps one predicted verdict to its GraphQL shape. A refusal's reason is the
|
|
1811
|
+
* consensus string the reactor would record for it, carried through verbatim so
|
|
1812
|
+
* a client can tell a deletion refusal from a missing grant; an allow has none.
|
|
1813
|
+
*/
|
|
1814
|
+
function toGqlActionEvaluation(evaluation) {
|
|
1815
|
+
return evaluation.decision === "allow" ? {
|
|
1816
|
+
decision: AuthDecision.Allow,
|
|
1817
|
+
reason: null
|
|
1818
|
+
} : {
|
|
1819
|
+
decision: AuthDecision.Deny,
|
|
1820
|
+
reason: evaluation.reason
|
|
1821
|
+
};
|
|
1822
|
+
}
|
|
1823
|
+
/**
|
|
1748
1824
|
* Converts readonly arrays to mutable arrays for ReactorClient
|
|
1749
1825
|
*/
|
|
1750
1826
|
function toMutableArray(arr) {
|
|
1751
1827
|
return arr ? [...arr] : void 0;
|
|
1752
1828
|
}
|
|
1753
1829
|
/**
|
|
1754
|
-
* Validates that
|
|
1830
|
+
* Validates that an incoming value has the structure of an Action.
|
|
1831
|
+
*
|
|
1832
|
+
* This is the whole guard on the way in. `mutateDocument` takes untyped JSON, so
|
|
1833
|
+
* nothing has checked these fields before a resolver runs, and the id in
|
|
1834
|
+
* particular has to be checked here: it is hashed into the operation id and
|
|
1835
|
+
* replay dedupes by it, so an action without one is stored under an operation id
|
|
1836
|
+
* shared by every id-less operation on the same document, scope and branch.
|
|
1837
|
+
*
|
|
1838
|
+
* The storage constraint would refuse such a row anyway, but only after a job
|
|
1839
|
+
* has been enqueued and failed. Refusing here answers the caller instead.
|
|
1755
1840
|
*/
|
|
1756
1841
|
function validateActionStructure(obj) {
|
|
1757
1842
|
if (!obj || typeof obj !== "object") return false;
|
|
1758
1843
|
const action = obj;
|
|
1844
|
+
if (typeof action.id !== "string" || !action.id) return false;
|
|
1759
1845
|
if (typeof action.type !== "string" || !action.type) return false;
|
|
1760
1846
|
if (typeof action.scope !== "string" || !action.scope) return false;
|
|
1847
|
+
if (typeof action.timestampUtcMs !== "string" || !action.timestampUtcMs) return false;
|
|
1761
1848
|
if (!("input" in action)) return false;
|
|
1762
1849
|
return true;
|
|
1763
1850
|
}
|
|
1764
1851
|
/**
|
|
1852
|
+
* Restores an action's signatures to the tuples verification reads.
|
|
1853
|
+
*
|
|
1854
|
+
* A signature is a five-param tuple, and GraphQL declares `signatures` as a
|
|
1855
|
+
* list of strings, so a client joins each one for transport. The sync path
|
|
1856
|
+
* already splits them on arrival; this is the same step for the mutation path,
|
|
1857
|
+
* which had none - an action submitted there kept whatever shape it arrived in,
|
|
1858
|
+
* so the same signature was persisted as a tuple or as one joined string
|
|
1859
|
+
* depending on which door it came through.
|
|
1860
|
+
*
|
|
1861
|
+
* A tuple passes through untouched, so an in-process caller handing over real
|
|
1862
|
+
* signatures is unaffected.
|
|
1863
|
+
*/
|
|
1864
|
+
function withDeserializedSignatures(action) {
|
|
1865
|
+
const signer = action.context?.signer;
|
|
1866
|
+
if (!signer?.signatures || signer.signatures.length === 0) return action;
|
|
1867
|
+
return {
|
|
1868
|
+
...action,
|
|
1869
|
+
context: {
|
|
1870
|
+
...action.context,
|
|
1871
|
+
signer: {
|
|
1872
|
+
...signer,
|
|
1873
|
+
signatures: signer.signatures.map(deserializeSignature)
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
};
|
|
1877
|
+
}
|
|
1878
|
+
/**
|
|
1879
|
+
* Converts actions the schema has already coerced into the actions the reactor
|
|
1880
|
+
* applies.
|
|
1881
|
+
*
|
|
1882
|
+
* The one conversion the schema cannot express is the signature: GraphQL
|
|
1883
|
+
* declares `signatures` as a list of strings, not of lists, so each arrives
|
|
1884
|
+
* joined and has to be read back as the tuple verification indexes into.
|
|
1885
|
+
*
|
|
1886
|
+
* Nothing is validated here. `ActionInput` states every field the reactor needs,
|
|
1887
|
+
* so anything that reached this point has them - unlike the untyped path, which
|
|
1888
|
+
* has to check by hand.
|
|
1889
|
+
*/
|
|
1890
|
+
function toSubmittableActions(actions) {
|
|
1891
|
+
return actions.map((action) => {
|
|
1892
|
+
const context = toSubmittableContext(action.context);
|
|
1893
|
+
const submittable = {
|
|
1894
|
+
id: action.id,
|
|
1895
|
+
type: action.type,
|
|
1896
|
+
timestampUtcMs: action.timestampUtcMs,
|
|
1897
|
+
input: action.input,
|
|
1898
|
+
scope: action.scope
|
|
1899
|
+
};
|
|
1900
|
+
return context ? {
|
|
1901
|
+
...submittable,
|
|
1902
|
+
context
|
|
1903
|
+
} : submittable;
|
|
1904
|
+
});
|
|
1905
|
+
}
|
|
1906
|
+
/**
|
|
1907
|
+
* `ReactorSignerInput` leaves `user` and `app` optional where `ActionSigner`
|
|
1908
|
+
* requires them, so an incomplete signer cannot be represented. It is carried
|
|
1909
|
+
* through as it arrived rather than filled in: the signature verifier is what
|
|
1910
|
+
* should refuse it, and inventing either field here would be inventing the part
|
|
1911
|
+
* that says who signed.
|
|
1912
|
+
*/
|
|
1913
|
+
function toSubmittableContext(context) {
|
|
1914
|
+
if (!context) return;
|
|
1915
|
+
const submittable = {};
|
|
1916
|
+
const prevOpIndex = fromInputMaybe(context.prevOpIndex);
|
|
1917
|
+
if (prevOpIndex !== void 0) submittable.prevOpIndex = prevOpIndex;
|
|
1918
|
+
const prevOpHash = fromInputMaybe(context.prevOpHash);
|
|
1919
|
+
if (prevOpHash !== void 0) submittable.prevOpHash = prevOpHash;
|
|
1920
|
+
const nonce = fromInputMaybe(context.nonce);
|
|
1921
|
+
if (nonce !== void 0) submittable.nonce = nonce;
|
|
1922
|
+
const signer = fromInputMaybe(context.signer);
|
|
1923
|
+
if (signer) submittable.signer = {
|
|
1924
|
+
user: fromInputMaybe(signer.user),
|
|
1925
|
+
app: fromInputMaybe(signer.app),
|
|
1926
|
+
signatures: signer.signatures.map(deserializeSignature)
|
|
1927
|
+
};
|
|
1928
|
+
return Object.keys(submittable).length > 0 ? submittable : void 0;
|
|
1929
|
+
}
|
|
1930
|
+
/**
|
|
1765
1931
|
* Converts a JSONObject to an Action, validating basic structure
|
|
1766
1932
|
*/
|
|
1767
1933
|
function jsonObjectToAction(obj) {
|
|
1768
|
-
if (!validateActionStructure(obj)) throw new GraphQLError("Invalid action structure. Actions must have: type (string), scope (string), and input (any)");
|
|
1769
|
-
return obj;
|
|
1934
|
+
if (!validateActionStructure(obj)) throw new GraphQLError("Invalid action structure. Actions must have: id (string), type (string), scope (string), timestampUtcMs (string), and input (any)");
|
|
1935
|
+
return withDeserializedSignatures(obj);
|
|
1770
1936
|
}
|
|
1771
1937
|
/**
|
|
1772
1938
|
* Validates a list of actions by converting them from JSON and checking structure
|
|
@@ -1780,10 +1946,57 @@ function validateActions(actions) {
|
|
|
1780
1946
|
}
|
|
1781
1947
|
return convertedActions;
|
|
1782
1948
|
}
|
|
1949
|
+
/** The operation fields the schema declares non-null. */
|
|
1950
|
+
const REQUIRED_OPERATION_FIELDS = [
|
|
1951
|
+
"index",
|
|
1952
|
+
"timestampUtcMs",
|
|
1953
|
+
"hash",
|
|
1954
|
+
"skip"
|
|
1955
|
+
];
|
|
1956
|
+
/** The action fields the schema declares non-null. */
|
|
1957
|
+
const REQUIRED_ACTION_FIELDS = [
|
|
1958
|
+
"id",
|
|
1959
|
+
"type",
|
|
1960
|
+
"timestampUtcMs",
|
|
1961
|
+
"input",
|
|
1962
|
+
"scope"
|
|
1963
|
+
];
|
|
1964
|
+
/**
|
|
1965
|
+
* Names an operation well enough to find the row it came from, without assuming
|
|
1966
|
+
* the fields that might be the missing ones.
|
|
1967
|
+
*/
|
|
1968
|
+
function describeOperation(stored) {
|
|
1969
|
+
const parts = [typeof stored.id === "string" && stored.id ? `operation ${stored.id}` : "operation with no id"];
|
|
1970
|
+
if (typeof stored.index === "number") parts.push(`index ${stored.index}`);
|
|
1971
|
+
const scope = stored.action?.scope;
|
|
1972
|
+
if (typeof scope === "string" && scope) parts.push(`scope ${scope}`);
|
|
1973
|
+
return parts.join(", ");
|
|
1974
|
+
}
|
|
1975
|
+
/**
|
|
1976
|
+
* Refuses an operation the schema cannot represent, naming the field.
|
|
1977
|
+
*
|
|
1978
|
+
* The alternative is what execution does unaided: nullify the field, bubble that
|
|
1979
|
+
* up the non-null chain until it takes out the envelope's whole operation list,
|
|
1980
|
+
* and report a bare "Cannot return null for non-nullable field" with no way to
|
|
1981
|
+
* tell which of a hundred operations was at fault. Checked before the response
|
|
1982
|
+
* is built so the failure is typed and attributable.
|
|
1983
|
+
*
|
|
1984
|
+
* Presence is all that is checked. The schema's own serialization still rejects
|
|
1985
|
+
* a value of the wrong type, and inventing further rules here would put a second
|
|
1986
|
+
* contract beside the one the schema already states.
|
|
1987
|
+
*/
|
|
1988
|
+
function assertServable(operation) {
|
|
1989
|
+
const stored = operation;
|
|
1990
|
+
for (const field of REQUIRED_OPERATION_FIELDS) if (stored[field] === void 0 || stored[field] === null) throw new MalformedStoredOperationError(`${describeOperation(stored)} has no ${field}`);
|
|
1991
|
+
const action = stored.action;
|
|
1992
|
+
if (action === void 0 || action === null) throw new MalformedStoredOperationError(`${describeOperation(stored)} has no action`);
|
|
1993
|
+
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}`);
|
|
1994
|
+
}
|
|
1783
1995
|
/**
|
|
1784
1996
|
* Transforms an operation to serialize signatures from tuples to strings for GraphQL compatibility.
|
|
1785
1997
|
*/
|
|
1786
1998
|
function serializeOperationForGraphQL(operation) {
|
|
1999
|
+
assertServable(operation);
|
|
1787
2000
|
const signer = operation.action.context?.signer;
|
|
1788
2001
|
if (!signer?.signatures) return operation;
|
|
1789
2002
|
return {
|
|
@@ -1794,7 +2007,7 @@ function serializeOperationForGraphQL(operation) {
|
|
|
1794
2007
|
...operation.action.context,
|
|
1795
2008
|
signer: {
|
|
1796
2009
|
...signer,
|
|
1797
|
-
signatures: signer.signatures.map(
|
|
2010
|
+
signatures: signer.signatures.map(serializeSignature)
|
|
1798
2011
|
}
|
|
1799
2012
|
}
|
|
1800
2013
|
}
|
|
@@ -1904,9 +2117,10 @@ async function documentModels(reactorClient, args) {
|
|
|
1904
2117
|
throw new GraphQLError(`Failed to convert document models to GraphQL: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
1905
2118
|
}
|
|
1906
2119
|
}
|
|
1907
|
-
async function document(reactorClient, args) {
|
|
1908
|
-
let view;
|
|
2120
|
+
async function document(reactorClient, args, subject) {
|
|
2121
|
+
let view = subject ? { subject } : void 0;
|
|
1909
2122
|
if (args.view) view = {
|
|
2123
|
+
subject,
|
|
1910
2124
|
branch: fromInputMaybe(args.view.branch),
|
|
1911
2125
|
scopes: toMutableArray(fromInputMaybe(args.view.scopes))
|
|
1912
2126
|
};
|
|
@@ -1931,9 +2145,10 @@ async function document(reactorClient, args) {
|
|
|
1931
2145
|
throw new GraphQLError(`Failed to convert document to GraphQL: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
1932
2146
|
}
|
|
1933
2147
|
}
|
|
1934
|
-
async function documentOutgoingRelationships(reactorClient, args) {
|
|
1935
|
-
let view;
|
|
2148
|
+
async function documentOutgoingRelationships(reactorClient, args, subject) {
|
|
2149
|
+
let view = subject ? { subject } : void 0;
|
|
1936
2150
|
if (args.view) view = {
|
|
2151
|
+
subject,
|
|
1937
2152
|
branch: fromInputMaybe(args.view.branch),
|
|
1938
2153
|
scopes: toMutableArray(fromInputMaybe(args.view.scopes))
|
|
1939
2154
|
};
|
|
@@ -1958,9 +2173,10 @@ async function documentOutgoingRelationships(reactorClient, args) {
|
|
|
1958
2173
|
throw new GraphQLError(`Failed to convert outgoing relationships to GraphQL: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
1959
2174
|
}
|
|
1960
2175
|
}
|
|
1961
|
-
async function documentIncomingRelationships(reactorClient, args) {
|
|
1962
|
-
let view;
|
|
2176
|
+
async function documentIncomingRelationships(reactorClient, args, subject) {
|
|
2177
|
+
let view = subject ? { subject } : void 0;
|
|
1963
2178
|
if (args.view) view = {
|
|
2179
|
+
subject,
|
|
1964
2180
|
branch: fromInputMaybe(args.view.branch),
|
|
1965
2181
|
scopes: toMutableArray(fromInputMaybe(args.view.scopes))
|
|
1966
2182
|
};
|
|
@@ -1985,9 +2201,10 @@ async function documentIncomingRelationships(reactorClient, args) {
|
|
|
1985
2201
|
throw new GraphQLError(`Failed to convert incoming relationships to GraphQL: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
1986
2202
|
}
|
|
1987
2203
|
}
|
|
1988
|
-
async function findDocuments(reactorClient, args) {
|
|
1989
|
-
let view;
|
|
2204
|
+
async function findDocuments(reactorClient, args, subject) {
|
|
2205
|
+
let view = subject ? { subject } : void 0;
|
|
1990
2206
|
if (args.view) view = {
|
|
2207
|
+
subject,
|
|
1991
2208
|
branch: fromInputMaybe(args.view.branch),
|
|
1992
2209
|
scopes: toMutableArray(fromInputMaybe(args.view.scopes))
|
|
1993
2210
|
};
|
|
@@ -2029,11 +2246,12 @@ async function jobStatus(reactorClient, args) {
|
|
|
2029
2246
|
throw new GraphQLError(`Failed to convert job status to GraphQL: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
2030
2247
|
}
|
|
2031
2248
|
}
|
|
2032
|
-
async function documentOperations(reactorClient, args) {
|
|
2033
|
-
let view;
|
|
2249
|
+
async function documentOperations(reactorClient, args, subject) {
|
|
2250
|
+
let view = subject ? { subject } : void 0;
|
|
2034
2251
|
const branch = fromInputMaybe(args.filter.branch);
|
|
2035
2252
|
const scopes = toMutableArray(fromInputMaybe(args.filter.scopes));
|
|
2036
2253
|
if (branch || scopes) view = {
|
|
2254
|
+
subject,
|
|
2037
2255
|
branch,
|
|
2038
2256
|
scopes
|
|
2039
2257
|
};
|
|
@@ -2069,6 +2287,41 @@ async function documentOperations(reactorClient, args) {
|
|
|
2069
2287
|
throw new GraphQLError(`Failed to convert operations to GraphQL: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
2070
2288
|
}
|
|
2071
2289
|
}
|
|
2290
|
+
/**
|
|
2291
|
+
* Predicts the admission verdict for each candidate operation, deciding against
|
|
2292
|
+
* the document policy through the reactor's own decision model.
|
|
2293
|
+
*
|
|
2294
|
+
* The subject is supplied by the subgraph from the authenticated request, never
|
|
2295
|
+
* by the caller: answering for an arbitrary subject would disclose what a policy
|
|
2296
|
+
* grants somebody else. It carries the caller's address and the app key their
|
|
2297
|
+
* token authenticated with, which is the same pair a signed action presents, so
|
|
2298
|
+
* a grant naming either matches here exactly as it would at admission.
|
|
2299
|
+
*
|
|
2300
|
+
* A reactor without authEnforcement raises the named error, which becomes an
|
|
2301
|
+
* "unsupported" code rather than a denial.
|
|
2302
|
+
*/
|
|
2303
|
+
async function evaluateActions(reactorClient, args, subject) {
|
|
2304
|
+
const branch = fromInputMaybe(args.branch) ?? "main";
|
|
2305
|
+
const candidates = args.candidates.map((candidate) => ({
|
|
2306
|
+
scope: candidate.scope,
|
|
2307
|
+
type: candidate.type,
|
|
2308
|
+
input: fromInputMaybe(candidate.input)
|
|
2309
|
+
}));
|
|
2310
|
+
let result;
|
|
2311
|
+
try {
|
|
2312
|
+
result = await reactorClient.evaluateActions(args.documentIdentifier, branch, candidates, subject);
|
|
2313
|
+
} catch (error) {
|
|
2314
|
+
if (AuthEnforcementDisabledError.isError(error)) throw new AuthEvaluationUnsupportedError();
|
|
2315
|
+
throw new GraphQLError(`Failed to evaluate actions: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
2316
|
+
}
|
|
2317
|
+
return {
|
|
2318
|
+
evaluations: result.evaluations.map(toGqlActionEvaluation),
|
|
2319
|
+
allAllowed: result.allAllowed,
|
|
2320
|
+
anyAllowed: result.anyAllowed,
|
|
2321
|
+
allDenied: result.allDenied,
|
|
2322
|
+
anyDenied: result.anyDenied
|
|
2323
|
+
};
|
|
2324
|
+
}
|
|
2072
2325
|
async function createDocument(reactorClient, args, reactorDriveClient) {
|
|
2073
2326
|
if (!args.document || typeof args.document !== "object") throw new GraphQLError("Invalid document: must be an object");
|
|
2074
2327
|
const document = args.document;
|
|
@@ -2167,6 +2420,49 @@ async function createDocumentWithInitialState(reactorClient, args, reactorDriveC
|
|
|
2167
2420
|
throw new GraphQLError(`Failed to convert created document to GraphQL: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
2168
2421
|
}
|
|
2169
2422
|
}
|
|
2423
|
+
/** The branch a mutation applies to when the caller names none. */
|
|
2424
|
+
const DEFAULT_BRANCH = "main";
|
|
2425
|
+
/**
|
|
2426
|
+
* Applies actions to a document and waits for the result.
|
|
2427
|
+
*
|
|
2428
|
+
* The wire form of `IReactorClient.execute`, and named for it. Takes the actions
|
|
2429
|
+
* already coerced against `ActionInput`, so the only conversion left is the one
|
|
2430
|
+
* the schema cannot express: a signature travels as one string, and verification
|
|
2431
|
+
* reads it as the tuple it was.
|
|
2432
|
+
*/
|
|
2433
|
+
async function execute(reactorClient, args) {
|
|
2434
|
+
const actions = toSubmittableActions(args.actions);
|
|
2435
|
+
const branch = fromInputMaybe(args.branch) ?? DEFAULT_BRANCH;
|
|
2436
|
+
let result;
|
|
2437
|
+
try {
|
|
2438
|
+
result = await reactorClient.execute(args.documentIdentifier, branch, actions);
|
|
2439
|
+
} catch (error) {
|
|
2440
|
+
throw new GraphQLError(`Failed to execute actions: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
2441
|
+
}
|
|
2442
|
+
try {
|
|
2443
|
+
return toGqlPhDocument(result);
|
|
2444
|
+
} catch (error) {
|
|
2445
|
+
throw new GraphQLError(`Failed to convert executed document to GraphQL: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
2446
|
+
}
|
|
2447
|
+
}
|
|
2448
|
+
/**
|
|
2449
|
+
* Submits actions to a document and returns the job that will apply them.
|
|
2450
|
+
*
|
|
2451
|
+
* Returns the job rather than only its id, matching
|
|
2452
|
+
* `IReactorClient.executeAsync`. A job handed back from a submission has no
|
|
2453
|
+
* result yet, which is why `JobInfo.result` is nullable.
|
|
2454
|
+
*/
|
|
2455
|
+
async function executeAsync(reactorClient, args) {
|
|
2456
|
+
const actions = toSubmittableActions(args.actions);
|
|
2457
|
+
const branch = fromInputMaybe(args.branch) ?? DEFAULT_BRANCH;
|
|
2458
|
+
let job;
|
|
2459
|
+
try {
|
|
2460
|
+
job = await reactorClient.executeAsync(args.documentIdentifier, branch, actions);
|
|
2461
|
+
} catch (error) {
|
|
2462
|
+
throw new GraphQLError(`Failed to submit actions: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
2463
|
+
}
|
|
2464
|
+
return toGqlJobInfo(job);
|
|
2465
|
+
}
|
|
2170
2466
|
async function mutateDocument(reactorClient, args) {
|
|
2171
2467
|
let validatedActions;
|
|
2172
2468
|
try {
|
|
@@ -2300,7 +2596,14 @@ async function deleteDocuments(reactorClient, args) {
|
|
|
2300
2596
|
throw new GraphQLError(`Failed to delete documents: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
2301
2597
|
}
|
|
2302
2598
|
}
|
|
2303
|
-
|
|
2599
|
+
/**
|
|
2600
|
+
* Registers a sync channel, or reports the state of one already registered.
|
|
2601
|
+
*
|
|
2602
|
+
* `boundAddress` claims a channel at creation for the subject that created it.
|
|
2603
|
+
* An anonymously created channel is left unbound, which is not a permission but
|
|
2604
|
+
* an unanswered question: the first authenticated subject to use it adopts it.
|
|
2605
|
+
*/
|
|
2606
|
+
async function touchChannel(syncManager, args, boundAddress) {
|
|
2304
2607
|
try {
|
|
2305
2608
|
return {
|
|
2306
2609
|
success: true,
|
|
@@ -2312,7 +2615,10 @@ async function touchChannel(syncManager, args) {
|
|
|
2312
2615
|
scope: [...args.input.filter.scope],
|
|
2313
2616
|
branch: args.input.filter.branch
|
|
2314
2617
|
};
|
|
2315
|
-
const options = {
|
|
2618
|
+
const options = {
|
|
2619
|
+
sinceTimestampUtcMs: args.input.sinceTimestampUtcMs,
|
|
2620
|
+
boundAddress
|
|
2621
|
+
};
|
|
2316
2622
|
try {
|
|
2317
2623
|
await syncManager.add(args.input.name, DriveCollectionId.fromKey(args.input.collectionId), {
|
|
2318
2624
|
type: "polling",
|
|
@@ -2327,9 +2633,50 @@ async function touchChannel(syncManager, args) {
|
|
|
2327
2633
|
};
|
|
2328
2634
|
}
|
|
2329
2635
|
/**
|
|
2636
|
+
* The sync operations in `syncOps` this subject may not be served.
|
|
2637
|
+
*
|
|
2638
|
+
* A sync operation is one document, one branch and (from the outbox) one scope,
|
|
2639
|
+
* so the answer is per entry rather than per operation: an entry is withheld
|
|
2640
|
+
* whole or served whole, which is what keeps a scope's operation sequence
|
|
2641
|
+
* unbroken for whoever does receive it. A dead letter can name several scopes,
|
|
2642
|
+
* and is withheld only when the policy serves none of them -- the metadata
|
|
2643
|
+
* scopes are readable by every holder, so in practice a dead letter naming one
|
|
2644
|
+
* is always served.
|
|
2645
|
+
*
|
|
2646
|
+
* The policy is read once per document and branch, and the memo lives no longer
|
|
2647
|
+
* than this call: a poll must not answer from a policy that changed between
|
|
2648
|
+
* polls, since the widening case is exactly the one holding exists to serve.
|
|
2649
|
+
*
|
|
2650
|
+
* A read that fails for any reason other than the document being absent is
|
|
2651
|
+
* allowed to propagate. The poll then fails, the puller retries, and nothing is
|
|
2652
|
+
* served in the meantime -- which is the safe direction, and a visible one.
|
|
2653
|
+
*/
|
|
2654
|
+
async function collectHeldSyncOperations(syncOps, gate, subject, signal) {
|
|
2655
|
+
const held = /* @__PURE__ */ new Set();
|
|
2656
|
+
const predicates = /* @__PURE__ */ new Map();
|
|
2657
|
+
for (const syncOp of syncOps) {
|
|
2658
|
+
const key = `${syncOp.documentId}\u0000${syncOp.branch}`;
|
|
2659
|
+
let predicate = predicates.get(key);
|
|
2660
|
+
if (!predicate) {
|
|
2661
|
+
predicate = gate.scopePredicateById(syncOp.documentId, subject, syncOp.branch, signal);
|
|
2662
|
+
predicates.set(key, predicate);
|
|
2663
|
+
}
|
|
2664
|
+
const readable = await predicate;
|
|
2665
|
+
if (!syncOp.scopes.some((scope) => readable(scope))) held.add(syncOp.id);
|
|
2666
|
+
}
|
|
2667
|
+
return held;
|
|
2668
|
+
}
|
|
2669
|
+
/**
|
|
2330
2670
|
* Polls the switchboard for new sync envelopes and acknowledges previously
|
|
2331
2671
|
* received operations.
|
|
2332
2672
|
*
|
|
2673
|
+
* `forbiddenIds` and `heldOpIds` are two different refusals. A forbidden
|
|
2674
|
+
* document is consumed: its delivery counters are advanced as though it had
|
|
2675
|
+
* been sent, so it is never reconsidered. A held sync operation is withheld:
|
|
2676
|
+
* nothing about it is touched, so a policy that later widens serves it whole on
|
|
2677
|
+
* a subsequent poll. Which one applies is the caller's decision, and the two
|
|
2678
|
+
* are intersected, so a document refused either way is not served.
|
|
2679
|
+
*
|
|
2333
2680
|
* Ordinal frames of reference:
|
|
2334
2681
|
* - `outboxAck` / `outboxLatest`: switchboard's ordinals (used to trim/filter
|
|
2335
2682
|
* the switchboard's outbox)
|
|
@@ -2337,16 +2684,17 @@ async function touchChannel(syncManager, args) {
|
|
|
2337
2684
|
* client ordinal the switchboard has successfully applied, so the client
|
|
2338
2685
|
* can trim its own outbox)
|
|
2339
2686
|
*/
|
|
2340
|
-
function pollSyncEnvelopes(syncManager, args, forbiddenIds = /* @__PURE__ */ new Set()) {
|
|
2687
|
+
function pollSyncEnvelopes(syncManager, args, forbiddenIds = /* @__PURE__ */ new Set(), heldOpIds = /* @__PURE__ */ new Set()) {
|
|
2341
2688
|
let remote;
|
|
2342
2689
|
try {
|
|
2343
2690
|
remote = syncManager.getById(args.channelId);
|
|
2344
2691
|
} catch (error) {
|
|
2345
2692
|
throw new GraphQLError(`Channel not found: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
2346
2693
|
}
|
|
2347
|
-
const deadLetters = remote.channel.deadLetter.items.filter((syncOp) => !forbiddenIds.has(syncOp.documentId)).map((syncOp) => ({
|
|
2694
|
+
const deadLetters = remote.channel.deadLetter.items.filter((syncOp) => !forbiddenIds.has(syncOp.documentId) && !heldOpIds.has(syncOp.id)).map((syncOp) => ({
|
|
2348
2695
|
documentId: syncOp.documentId,
|
|
2349
2696
|
error: syncOp.error?.message ?? "Unknown error",
|
|
2697
|
+
errorType: syncOperationErrorType(syncOp.error),
|
|
2350
2698
|
jobId: syncOp.jobId,
|
|
2351
2699
|
branch: syncOp.branch,
|
|
2352
2700
|
scopes: syncOp.scopes,
|
|
@@ -2392,6 +2740,7 @@ function pollSyncEnvelopes(syncManager, args, forbiddenIds = /* @__PURE__ */ new
|
|
|
2392
2740
|
hasMore = true;
|
|
2393
2741
|
break;
|
|
2394
2742
|
}
|
|
2743
|
+
if (heldOpIds.has(syncOp.id)) continue;
|
|
2395
2744
|
syncOp.deliveredCount ??= 0;
|
|
2396
2745
|
syncOp.emittedCount ??= 0;
|
|
2397
2746
|
while (syncOp.deliveredCount < syncOp.emittedCount && syncOp.operations[syncOp.deliveredCount].context.ordinal <= args.outboxLatest) syncOp.deliveredCount += 1;
|
|
@@ -2875,6 +3224,9 @@ var GraphQLManager = class {
|
|
|
2875
3224
|
/** Cached document models for schema generation - updated on init and regenerate */
|
|
2876
3225
|
cachedDocumentModels = [];
|
|
2877
3226
|
subgraphHandlerCache = /* @__PURE__ */ new Map();
|
|
3227
|
+
/** Per-subgraph handlers by name, before auth/drive middleware is applied,
|
|
3228
|
+
* for in-process queries by trusted internals. */
|
|
3229
|
+
#internalSubgraphHandlers = /* @__PURE__ */ new Map();
|
|
2878
3230
|
/**
|
|
2879
3231
|
* Optional reactor-drive client. When the switchboard is configured with a
|
|
2880
3232
|
* reactor-drive container, this is provided and the resolvers dispatch to
|
|
@@ -2882,7 +3234,7 @@ var GraphQLManager = class {
|
|
|
2882
3234
|
*/
|
|
2883
3235
|
reactorDriveClient;
|
|
2884
3236
|
authorizationService;
|
|
2885
|
-
constructor(path, httpServer, wsServer, reactorClient, relationalDb, analyticsStore, syncManager, logger, httpAdapter, gatewayAdapter, authService, documentPermissionService, featureFlags = DefaultFeatureFlags, port = 4001, authorizationService, reactorDriveClient) {
|
|
3237
|
+
constructor(path, httpServer, wsServer, reactorClient, relationalDb, analyticsStore, syncManager, logger, httpAdapter, gatewayAdapter, authService, documentPermissionService, featureFlags = DefaultFeatureFlags, port = 4001, authorizationService, reactorDriveClient, syncServingGate) {
|
|
2886
3238
|
this.path = path;
|
|
2887
3239
|
this.httpServer = httpServer;
|
|
2888
3240
|
this.wsServer = wsServer;
|
|
@@ -2897,6 +3249,7 @@ var GraphQLManager = class {
|
|
|
2897
3249
|
this.documentPermissionService = documentPermissionService;
|
|
2898
3250
|
this.featureFlags = featureFlags;
|
|
2899
3251
|
this.port = port;
|
|
3252
|
+
this.syncServingGate = syncServingGate;
|
|
2900
3253
|
if (!authorizationService) throw new Error("GraphQLManager requires an authorizationService");
|
|
2901
3254
|
this.authorizationService = authorizationService;
|
|
2902
3255
|
this.reactorDriveClient = reactorDriveClient;
|
|
@@ -2914,7 +3267,7 @@ var GraphQLManager = class {
|
|
|
2914
3267
|
if (!models.find((it) => it.documentModel.global.name === "DocumentDrive")) throw new Error("DocumentDrive model required");
|
|
2915
3268
|
await this.gatewayAdapter.start(this.httpServer);
|
|
2916
3269
|
this.httpAdapter.setupMiddleware({ bodyLimit: "50mb" });
|
|
2917
|
-
const driveRoutePath = path.join(this.path, "d/:drive");
|
|
3270
|
+
const driveRoutePath = path.posix.join(this.path, "d/:drive");
|
|
2918
3271
|
const driveMatcher = match(driveRoutePath);
|
|
2919
3272
|
this.httpAdapter.mount(driveRoutePath, async (request) => {
|
|
2920
3273
|
const url = new URL(request.url);
|
|
@@ -2982,7 +3335,8 @@ var GraphQLManager = class {
|
|
|
2982
3335
|
syncManager: this.syncManager,
|
|
2983
3336
|
path: this.path,
|
|
2984
3337
|
documentPermissionService: this.documentPermissionService,
|
|
2985
|
-
authorizationService: this.authorizationService
|
|
3338
|
+
authorizationService: this.authorizationService,
|
|
3339
|
+
syncServingGate: this.syncServingGate
|
|
2986
3340
|
});
|
|
2987
3341
|
await this.#addSubgraphInstance(subgraphInstance, supergraph, false);
|
|
2988
3342
|
} catch (error) {
|
|
@@ -2993,19 +3347,45 @@ var GraphQLManager = class {
|
|
|
2993
3347
|
}
|
|
2994
3348
|
async #addSubgraphInstance(subgraphInstance, supergraph = "", core = false) {
|
|
2995
3349
|
const subgraphsMap = core ? this.coreSubgraphsMap : this.subgraphs;
|
|
2996
|
-
const
|
|
2997
|
-
|
|
2998
|
-
if (existingSubgraph) {
|
|
3350
|
+
const existingSubgraph = subgraphsMap.get(supergraph)?.find((it) => it.name === subgraphInstance.name);
|
|
3351
|
+
if (existingSubgraph === subgraphInstance) {
|
|
2999
3352
|
this.logger.debug(`Skipping duplicate subgraph: ${subgraphInstance.name}`);
|
|
3000
3353
|
return existingSubgraph;
|
|
3001
3354
|
}
|
|
3355
|
+
if (existingSubgraph) await this.#removeSubgraphInstance(existingSubgraph, subgraphsMap);
|
|
3002
3356
|
await subgraphInstance.onSetup?.();
|
|
3357
|
+
const subgraphs = subgraphsMap.get(supergraph) ?? [];
|
|
3003
3358
|
subgraphs.push(subgraphInstance);
|
|
3004
3359
|
subgraphsMap.set(supergraph, subgraphs);
|
|
3005
3360
|
if (supergraph !== "" && supergraph !== "graphql") subgraphsMap.get("graphql")?.push(subgraphInstance);
|
|
3006
|
-
this.logger.info(
|
|
3361
|
+
this.logger.info(`${existingSubgraph ? "Replaced" : "Registered"} ${this.path.endsWith("/") ? this.path : this.path + "/"}${supergraph ? supergraph + "/" : ""}${subgraphInstance.name} subgraph.`);
|
|
3007
3362
|
return subgraphInstance;
|
|
3008
3363
|
}
|
|
3364
|
+
/** Tear down a replaced subgraph: onDisconnect, drop it from every bucket,
|
|
3365
|
+
* and invalidate its cached handlers so #setupSubgraphs rebuilds them. */
|
|
3366
|
+
async #removeSubgraphInstance(instance, subgraphsMap) {
|
|
3367
|
+
try {
|
|
3368
|
+
await instance.onDisconnect?.();
|
|
3369
|
+
} catch (error) {
|
|
3370
|
+
this.logger.error("Error disconnecting subgraph @name: @error", instance.name, error);
|
|
3371
|
+
}
|
|
3372
|
+
for (const [bucket, subgraphs] of subgraphsMap.entries()) {
|
|
3373
|
+
if (!subgraphs.includes(instance)) continue;
|
|
3374
|
+
subgraphsMap.set(bucket, subgraphs.filter((it) => it !== instance));
|
|
3375
|
+
const subgraphPath = this.#getSubgraphPath(instance, bucket);
|
|
3376
|
+
this.subgraphHandlerCache.delete(subgraphPath);
|
|
3377
|
+
const wsDisposer = this.subgraphWsDisposers.get(subgraphPath);
|
|
3378
|
+
if (wsDisposer) {
|
|
3379
|
+
this.subgraphWsDisposers.delete(subgraphPath);
|
|
3380
|
+
try {
|
|
3381
|
+
await wsDisposer.dispose();
|
|
3382
|
+
} catch (error) {
|
|
3383
|
+
this.logger.error("Error disposing subgraph websocket @name: @error", instance.name, error);
|
|
3384
|
+
}
|
|
3385
|
+
}
|
|
3386
|
+
}
|
|
3387
|
+
if (this.#internalSubgraphHandlers.get(instance.name)?.subgraph === instance) this.#internalSubgraphHandlers.delete(instance.name);
|
|
3388
|
+
}
|
|
3009
3389
|
/**
|
|
3010
3390
|
* Register a pre-constructed subgraph instance.
|
|
3011
3391
|
* Use this when you need to pass custom dependencies to a subgraph.
|
|
@@ -3036,10 +3416,49 @@ var GraphQLManager = class {
|
|
|
3036
3416
|
syncManager: this.syncManager,
|
|
3037
3417
|
path: this.path,
|
|
3038
3418
|
documentPermissionService: this.documentPermissionService,
|
|
3039
|
-
authorizationService: this.authorizationService
|
|
3419
|
+
authorizationService: this.authorizationService,
|
|
3420
|
+
syncServingGate: this.syncServingGate
|
|
3040
3421
|
});
|
|
3041
3422
|
return this.#addSubgraphInstance(subgraphInstance, supergraph, core);
|
|
3042
3423
|
}
|
|
3424
|
+
/** Find a registered subgraph by name, core or package-contributed. */
|
|
3425
|
+
getSubgraphByName(name) {
|
|
3426
|
+
for (const subgraphs of [...this.coreSubgraphsMap.values(), ...this.subgraphs.values()]) {
|
|
3427
|
+
const found = subgraphs.find((subgraph) => subgraph.name === name);
|
|
3428
|
+
if (found) return found;
|
|
3429
|
+
}
|
|
3430
|
+
}
|
|
3431
|
+
/** Whether a registered subgraph has a live handler, i.e. whether
|
|
3432
|
+
* {@link executeSubgraphQuery} can reach it right now. */
|
|
3433
|
+
hasSubgraphHandler(name) {
|
|
3434
|
+
return this.#internalSubgraphHandlers.has(name);
|
|
3435
|
+
}
|
|
3436
|
+
/** Run a query against one registered subgraph in-process: the subgraph's own
|
|
3437
|
+
* handler, without a socket or the auth/drive middleware. Trusted callers. */
|
|
3438
|
+
async executeSubgraphQuery(subgraphName, query, variables) {
|
|
3439
|
+
const entry = this.#internalSubgraphHandlers.get(subgraphName);
|
|
3440
|
+
if (!entry) throw new Error(this.getSubgraphByName(subgraphName) ? `Subgraph "${subgraphName}" is registered but has no handler yet` : `Subgraph "${subgraphName}" is not registered`);
|
|
3441
|
+
const url = new URL(entry.path, `http://localhost:${this.port}/`);
|
|
3442
|
+
const response = await entry.handler(new Request(url, {
|
|
3443
|
+
method: "POST",
|
|
3444
|
+
headers: {
|
|
3445
|
+
"content-type": "application/json",
|
|
3446
|
+
accept: "application/json"
|
|
3447
|
+
},
|
|
3448
|
+
body: JSON.stringify({
|
|
3449
|
+
query,
|
|
3450
|
+
variables
|
|
3451
|
+
})
|
|
3452
|
+
}));
|
|
3453
|
+
if (!response.ok) {
|
|
3454
|
+
const detail = await response.text().catch(() => "");
|
|
3455
|
+
throw new Error(`Subgraph "${subgraphName}" query failed: ${response.status}${detail ? ` ${detail}` : ""}`);
|
|
3456
|
+
}
|
|
3457
|
+
const body = await response.json();
|
|
3458
|
+
if (body.errors?.length) throw new Error(body.errors.map((error) => error.message).join("; "));
|
|
3459
|
+
if (!body.data) throw new Error(`Subgraph "${subgraphName}" returned no data`);
|
|
3460
|
+
return body.data;
|
|
3461
|
+
}
|
|
3043
3462
|
updateRouter = debounce(this._updateRouter.bind(this), 1e3);
|
|
3044
3463
|
async _updateRouter() {
|
|
3045
3464
|
this.logger.debug("Updating router");
|
|
@@ -3050,7 +3469,7 @@ var GraphQLManager = class {
|
|
|
3050
3469
|
} catch (error) {
|
|
3051
3470
|
this.logger.error("Failed to update Apollo Gateway supergraph: @error", error);
|
|
3052
3471
|
}
|
|
3053
|
-
const superGraphPath = path.join(this.path, "graphql");
|
|
3472
|
+
const superGraphPath = path.posix.join(this.path, "graphql");
|
|
3054
3473
|
this.#setupSupergraphSSE(superGraphPath);
|
|
3055
3474
|
}
|
|
3056
3475
|
getAdditionalContextFields = () => {
|
|
@@ -3113,7 +3532,21 @@ var GraphQLManager = class {
|
|
|
3113
3532
|
});
|
|
3114
3533
|
}
|
|
3115
3534
|
#getSubgraphPath(subgraph, supergraph) {
|
|
3116
|
-
return path.join(subgraph.path ?? "", supergraph, subgraph.name);
|
|
3535
|
+
return path.posix.join(subgraph.path ?? "", supergraph, subgraph.name);
|
|
3536
|
+
}
|
|
3537
|
+
/** The in-process handler map is keyed by bare name, so two distinct
|
|
3538
|
+
* subgraphs sharing one name would shadow each other: keep the first. */
|
|
3539
|
+
#setInternalSubgraphHandler(subgraph, subgraphPath, handler) {
|
|
3540
|
+
const existing = this.#internalSubgraphHandlers.get(subgraph.name);
|
|
3541
|
+
if (existing && existing.subgraph !== subgraph) {
|
|
3542
|
+
this.logger.warn("Two subgraphs are named @name (@kept and @ignored); in-process queries keep the first", subgraph.name, existing.path, subgraphPath);
|
|
3543
|
+
return;
|
|
3544
|
+
}
|
|
3545
|
+
this.#internalSubgraphHandlers.set(subgraph.name, {
|
|
3546
|
+
subgraph,
|
|
3547
|
+
path: subgraphPath,
|
|
3548
|
+
handler
|
|
3549
|
+
});
|
|
3117
3550
|
}
|
|
3118
3551
|
async #setupSubgraphs(subgraphsMap) {
|
|
3119
3552
|
for (const [supergraph, subgraphs] of subgraphsMap.entries()) for (const subgraph of subgraphs) {
|
|
@@ -3125,6 +3558,7 @@ var GraphQLManager = class {
|
|
|
3125
3558
|
const rawHandler = await this.gatewayAdapter.createHandler(schema, this.#makeContextFactory());
|
|
3126
3559
|
const fetchHandler = this.#composeFetchMiddleware(rawHandler);
|
|
3127
3560
|
this.subgraphHandlerCache.set(subgraphPath, fetchHandler);
|
|
3561
|
+
this.#setInternalSubgraphHandler(subgraph, subgraphPath, rawHandler);
|
|
3128
3562
|
this.httpAdapter.mount(subgraphPath, fetchHandler);
|
|
3129
3563
|
if (subgraph.hasSubscriptions) {
|
|
3130
3564
|
try {
|
|
@@ -3171,7 +3605,7 @@ var GraphQLManager = class {
|
|
|
3171
3605
|
}));
|
|
3172
3606
|
}
|
|
3173
3607
|
async #createSupergraphGateway() {
|
|
3174
|
-
const superGraphPath = path.join(this.path, "graphql");
|
|
3608
|
+
const superGraphPath = path.posix.join(this.path, "graphql");
|
|
3175
3609
|
const rawHandler = await this.gatewayAdapter.createSupergraphHandler(() => this.#getSubgraphDefinitions(), this.httpServer, this.#makeContextFactory());
|
|
3176
3610
|
const fetchHandler = this.#composeFetchMiddleware(rawHandler);
|
|
3177
3611
|
this.httpAdapter.mount(superGraphPath, fetchHandler);
|
|
@@ -3536,6 +3970,17 @@ const DocumentChangeEventDTO = z.object({
|
|
|
3536
3970
|
documents: z.array(PHDocumentDTO),
|
|
3537
3971
|
context: DocumentChangeContextDTO.nullable().optional()
|
|
3538
3972
|
}).strip();
|
|
3973
|
+
const ActionEvaluationDTO = z.object({
|
|
3974
|
+
decision: z.enum(["ALLOW", "DENY"]),
|
|
3975
|
+
reason: z.string().nullable().optional()
|
|
3976
|
+
}).strip();
|
|
3977
|
+
const ActionEvaluationsDTO = z.object({
|
|
3978
|
+
evaluations: z.array(ActionEvaluationDTO),
|
|
3979
|
+
allAllowed: z.boolean(),
|
|
3980
|
+
anyAllowed: z.boolean(),
|
|
3981
|
+
allDenied: z.boolean(),
|
|
3982
|
+
anyDenied: z.boolean()
|
|
3983
|
+
}).strip();
|
|
3539
3984
|
const JobChangeEventDTO = z.object({
|
|
3540
3985
|
jobId: z.string(),
|
|
3541
3986
|
status: z.string(),
|
|
@@ -3569,6 +4014,9 @@ const operationValidators = {
|
|
|
3569
4014
|
GetJobStatus: (data) => {
|
|
3570
4015
|
if (data.jobStatus) JobInfoDTO.parse(data.jobStatus);
|
|
3571
4016
|
},
|
|
4017
|
+
EvaluateActions: (data) => {
|
|
4018
|
+
if (data.evaluateActions) ActionEvaluationsDTO.parse(data.evaluateActions);
|
|
4019
|
+
},
|
|
3572
4020
|
CreateDocument: (data) => {
|
|
3573
4021
|
if (data.createDocument) PHDocumentDTO.parse(data.createDocument);
|
|
3574
4022
|
},
|
|
@@ -3631,7 +4079,7 @@ function createReactorGraphQLClient(url, fetchImpl = fetch, headers) {
|
|
|
3631
4079
|
}
|
|
3632
4080
|
//#endregion
|
|
3633
4081
|
//#region src/graphql/reactor/schema.graphql
|
|
3634
|
-
var schema_default = "# Scalar types (for codegen - also defined in create-schema.ts)\nscalar JSONObject\nscalar DateTime\n\n# Input types\ninput PagingInput {\n limit: Int\n offset: Int\n cursor: String\n}\n\ninput ViewFilterInput {\n branch: String\n scopes: [String!]\n}\n\ninput SearchFilterInput {\n type: String\n parentId: String\n identifiers: [String!]\n}\n\ninput OperationsFilterInput {\n documentId: String!\n branch: String\n scopes: [String!]\n actionTypes: [String!]\n sinceRevision: Int\n timestampFrom: String\n timestampTo: String\n}\n\ninput DocumentOperationsFilterInput {\n branch: String\n scopes: [String!]\n actionTypes: [String!]\n sinceRevision: Int\n timestampFrom: String\n timestampTo: String\n}\n\n# Enums\nenum PropagationMode {\n CASCADE\n ORPHAN\n}\n\nenum DocumentChangeType {\n CREATED\n DELETED\n UPDATED\n PARENT_ADDED\n PARENT_REMOVED\n CHILD_ADDED\n CHILD_REMOVED\n}\n\n# Object types\ntype DocumentModelGlobalState {\n id: String!\n name: String!\n namespace: String\n version: String\n specification: JSONObject!\n}\n\ntype DocumentModelResultPage {\n items: [DocumentModelGlobalState!]!\n totalCount: Int!\n hasNextPage: Boolean!\n hasPreviousPage: Boolean!\n cursor: String\n}\n\ntype Revision {\n scope: String!\n revision: Int!\n}\n\ntype PHDocument {\n id: String!\n slug: String\n preferredEditor: String\n name: String!\n documentType: String!\n state: JSONObject!\n revisionsList: [Revision!]!\n createdAtUtcIso: DateTime!\n lastModifiedAtUtcIso: DateTime!\n operations(\n filter: DocumentOperationsFilterInput\n paging: PagingInput\n ): ReactorOperationResultPage\n}\n\ntype PHDocumentResultPage {\n items: [PHDocument!]!\n totalCount: Int!\n hasNextPage: Boolean!\n hasPreviousPage: Boolean!\n cursor: String\n}\n\ntype ReactorOperationResultPage {\n items: [ReactorOperation!]!\n totalCount: Int!\n hasNextPage: Boolean!\n hasPreviousPage: Boolean!\n cursor: String\n}\n\ntype DeadLetterInfo {\n documentId: String!\n error: String!\n jobId: String!\n branch: String!\n scopes: [String!]!\n operationCount: Int!\n}\n\ntype PollSyncEnvelopesResult {\n envelopes: [SyncEnvelope!]!\n ackOrdinal: Int!\n deadLetters: [DeadLetterInfo!]!\n hasMore: Boolean!\n}\n\ntype DocumentWithChildren {\n document: PHDocument!\n childIds: [String!]!\n}\n\ntype MoveRelationshipResult {\n source: PHDocument!\n target: PHDocument!\n}\n\ntype JobInfo {\n id: String!\n status: String!\n result: JSONObject!\n error: String\n createdAt: DateTime!\n completedAt: DateTime\n}\n\ntype DocumentChangeEvent {\n type: DocumentChangeType!\n documents: [PHDocument!]!\n context: DocumentChangeContext\n}\n\ntype DocumentChangeContext {\n parentId: String\n childId: String\n}\n\ntype JobChangeEvent {\n jobId: String!\n status: String!\n result: JSONObject!\n error: String\n}\n\ntype ReactorSignerUser {\n address: String!\n networkId: String!\n chainId: Int!\n}\n\ntype ReactorSignerApp {\n name: String!\n key: String!\n}\n\ntype ReactorSigner {\n user: ReactorSignerUser\n app: ReactorSignerApp\n signatures: [String!]!\n}\n\ntype ActionContext {\n signer: ReactorSigner\n}\n\ntype Action {\n id: String!\n type: String!\n timestampUtcMs: String!\n input: JSONObject!\n scope: String!\n context: ActionContext\n}\n\n# Input types for sync operations\ninput ActionContextInput {\n signer: ReactorSignerInput\n}\n\ninput ReactorSignerInput {\n user: ReactorSignerUserInput\n app: ReactorSignerAppInput\n signatures: [String!]!\n}\n\ninput ReactorSignerUserInput {\n address: String!\n networkId: String!\n chainId: Int!\n}\n\ninput ReactorSignerAppInput {\n name: String!\n key: String!\n}\n\ninput ActionInput {\n id: String!\n type: String!\n timestampUtcMs: String!\n input: JSONObject!\n scope: String!\n context: ActionContextInput\n}\n\n# Synchronization types\ntype ReactorOperation {\n index: Int!\n timestampUtcMs: String!\n hash: String!\n skip: Int!\n error: String\n id: String\n action: Action!\n}\n\ninput OperationInput {\n index: Int!\n timestampUtcMs: String!\n hash: String!\n skip: Int!\n error: String\n id: String\n action: ActionInput!\n}\n\ntype OperationContext {\n documentId: String!\n documentType: String!\n scope: String!\n branch: String!\n ordinal: Int!\n}\n\ninput OperationContextInput {\n documentId: String!\n documentType: String!\n scope: String!\n branch: String!\n ordinal: Int!\n}\n\ntype OperationWithContext {\n operation: ReactorOperation!\n context: OperationContext!\n}\n\ninput OperationWithContextInput {\n operation: OperationInput!\n context: OperationContextInput!\n}\n\ntype ChannelMeta {\n id: String!\n}\n\ninput ChannelMetaInput {\n id: String!\n}\n\ntype RemoteCursor {\n remoteName: String!\n cursorOrdinal: Int!\n lastSyncedAtUtcMs: String\n}\n\ninput RemoteCursorInput {\n remoteName: String!\n cursorOrdinal: Int!\n lastSyncedAtUtcMs: String\n}\n\nenum SyncEnvelopeType {\n OPERATIONS\n ACK\n}\n\ntype SyncEnvelope {\n type: SyncEnvelopeType!\n channelMeta: ChannelMeta!\n operations: [OperationWithContext!]\n cursor: RemoteCursor\n key: String\n dependsOn: [String!]\n}\n\ninput SyncEnvelopeInput {\n type: SyncEnvelopeType!\n channelMeta: ChannelMetaInput!\n operations: [OperationWithContextInput!]\n cursor: RemoteCursorInput\n key: String\n dependsOn: [String!]\n}\n\ninput RemoteFilterInput {\n documentId: [String!]!\n scope: [String!]!\n branch: String!\n}\n\ninput TouchChannelInput {\n id: String!\n name: String!\n collectionId: String!\n filter: RemoteFilterInput!\n sinceTimestampUtcMs: String!\n}\n\ntype TouchChannelResult {\n success: Boolean!\n ackOrdinal: Int!\n}\n\ntype Query {\n # Get document models for a namespace\n documentModels(\n namespace: String\n paging: PagingInput\n ): DocumentModelResultPage!\n\n # Get a specific document by ID or slug\n document(identifier: String!, view: ViewFilterInput): DocumentWithChildren\n\n # Get outgoing relationships of a given type from a source document\n documentOutgoingRelationships(\n sourceIdentifier: String!\n relationshipType: String!\n view: ViewFilterInput\n paging: PagingInput\n ): PHDocumentResultPage!\n\n # Get incoming relationships of a given type to a target document\n documentIncomingRelationships(\n targetIdentifier: String!\n relationshipType: String!\n view: ViewFilterInput\n paging: PagingInput\n ): PHDocumentResultPage!\n\n # Find documents by search criteria\n findDocuments(\n search: SearchFilterInput\n view: ViewFilterInput\n paging: PagingInput\n ): PHDocumentResultPage!\n\n # Get job status\n jobStatus(jobId: String!): JobInfo\n\n # Get operations for a document with filtering and pagination\n documentOperations(\n filter: OperationsFilterInput!\n paging: PagingInput\n ): ReactorOperationResultPage!\n\n # Poll for sync envelopes from a channel\n pollSyncEnvelopes(\n channelId: String!\n outboxAck: Int!\n outboxLatest: Int!\n ): PollSyncEnvelopesResult!\n}\n\ntype Mutation {\n # Create a new document\n createDocument(document: JSONObject!, parentIdentifier: String): PHDocument!\n\n # Create an empty document of specified type\n createEmptyDocument(\n documentType: String!\n parentIdentifier: String\n ): PHDocument!\n\n # Apply actions to a document (synchronous)\n mutateDocument(\n documentIdentifier: String!\n actions: [JSONObject!]!\n view: ViewFilterInput\n ): PHDocument!\n\n # Submit actions to a document (asynchronous)\n mutateDocumentAsync(\n documentIdentifier: String!\n actions: [JSONObject!]!\n view: ViewFilterInput\n ): String!\n\n # Rename a document\n renameDocument(\n documentIdentifier: String!\n name: String!\n branch: String\n ): PHDocument!\n\n # Update the preferred editor recorded in the document header meta.\n # Pass null/omit to clear it.\n setPreferredEditor(\n documentIdentifier: String!\n preferredEditor: String\n branch: String\n ): PHDocument!\n\n # Add a relationship between two documents\n addRelationship(\n sourceIdentifier: String!\n targetIdentifier: String!\n relationshipType: String!\n branch: String\n ): PHDocument!\n\n # Remove a relationship between two documents\n removeRelationship(\n sourceIdentifier: String!\n targetIdentifier: String!\n relationshipType: String!\n branch: String\n ): PHDocument!\n\n # Move a relationship from one source to another\n moveRelationship(\n sourceParentIdentifier: String!\n targetParentIdentifier: String!\n targetIdentifier: String!\n relationshipType: String!\n branch: String\n ): MoveRelationshipResult!\n\n # Delete a single document\n deleteDocument(identifier: String!, propagate: PropagationMode): Boolean!\n\n # Delete multiple documents\n deleteDocuments(identifiers: [String!]!, propagate: PropagationMode): Boolean!\n\n # Touch (create or update) a channel for sync\n touchChannel(input: TouchChannelInput!): TouchChannelResult!\n\n # Push sync envelopes to a channel\n pushSyncEnvelopes(envelopes: [SyncEnvelopeInput!]!): Boolean!\n}\n\ntype Subscription {\n # Subscribe to document changes\n documentChanges(\n search: SearchFilterInput\n view: ViewFilterInput\n ): DocumentChangeEvent!\n\n # Subscribe to job changes\n jobChanges(jobId: String!): JobChangeEvent!\n}\n";
|
|
4082
|
+
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";
|
|
3635
4083
|
//#endregion
|
|
3636
4084
|
//#region src/graphql/reactor/pubsub.ts
|
|
3637
4085
|
const pubSub = new PubSub();
|
|
@@ -3769,6 +4217,46 @@ var ReactorSubgraph = class extends BaseSubgraph {
|
|
|
3769
4217
|
if (!await this.canReadDocument(documentId, ctx)) forbidden.add(documentId);
|
|
3770
4218
|
}
|
|
3771
4219
|
}
|
|
4220
|
+
/**
|
|
4221
|
+
* Enforces which subject a sync channel answers to, adopting an unclaimed one.
|
|
4222
|
+
*
|
|
4223
|
+
* A channel is a private queue: it names a collection, and it accumulates
|
|
4224
|
+
* exactly the operations one subject is owed, so serving it to a second
|
|
4225
|
+
* subject would hand over that subject's cursor as well as its contents. An
|
|
4226
|
+
* unbound channel -- created anonymously, or created before binding existed --
|
|
4227
|
+
* is claimed by the first authenticated subject to use it, and answers to that
|
|
4228
|
+
* address alone thereafter. Adoption is safe precisely because serving
|
|
4229
|
+
* withholds rather than consumes: before it was claimed the channel served
|
|
4230
|
+
* only what an anonymous subject may read, and everything withheld is still
|
|
4231
|
+
* queued for the adopter.
|
|
4232
|
+
*
|
|
4233
|
+
* The refusal is the shape a read denial takes, so a puller treats it as a
|
|
4234
|
+
* signal to authenticate again rather than as a transport failure.
|
|
4235
|
+
*
|
|
4236
|
+
* Nothing is enforced or adopted without a serving gate. Below
|
|
4237
|
+
* `authEnforcement` there is no policy being enforced for the channel to
|
|
4238
|
+
* belong to, and refusing a poll there would break sync for no gain.
|
|
4239
|
+
*/
|
|
4240
|
+
async #bindOrRefuseChannel(channelId, ctx) {
|
|
4241
|
+
if (!this.syncServingGate) return;
|
|
4242
|
+
let remote;
|
|
4243
|
+
try {
|
|
4244
|
+
remote = this.syncManager.getById(channelId);
|
|
4245
|
+
} catch {
|
|
4246
|
+
return;
|
|
4247
|
+
}
|
|
4248
|
+
const bound = remote.meta.options.boundAddress;
|
|
4249
|
+
const address = ctx.user?.address;
|
|
4250
|
+
if (bound === void 0) {
|
|
4251
|
+
if (address !== void 0) await this.syncManager.bindRemote(channelId, address);
|
|
4252
|
+
return;
|
|
4253
|
+
}
|
|
4254
|
+
if (bound !== address) throw new ForbiddenError("to poll this sync channel");
|
|
4255
|
+
}
|
|
4256
|
+
/** The address a channel created by this request belongs to, if any. */
|
|
4257
|
+
#creatorBinding(ctx) {
|
|
4258
|
+
return this.syncServingGate ? ctx.user?.address : void 0;
|
|
4259
|
+
}
|
|
3772
4260
|
typeDefs = gql(schema_default);
|
|
3773
4261
|
resolvers = {
|
|
3774
4262
|
PHDocument: { operations: async (parent, args, ctx) => {
|
|
@@ -3787,7 +4275,7 @@ var ReactorSubgraph = class extends BaseSubgraph {
|
|
|
3787
4275
|
return await documentOperations(this.reactorClient, {
|
|
3788
4276
|
filter,
|
|
3789
4277
|
paging: args.paging
|
|
3790
|
-
});
|
|
4278
|
+
}, this.viewSubject(ctx));
|
|
3791
4279
|
} catch (error) {
|
|
3792
4280
|
this.logger.error("Error in PHDocument.operations: @Error", error);
|
|
3793
4281
|
throw error;
|
|
@@ -3810,7 +4298,7 @@ var ReactorSubgraph = class extends BaseSubgraph {
|
|
|
3810
4298
|
return await document(this.reactorClient, {
|
|
3811
4299
|
...args,
|
|
3812
4300
|
identifier: handle.fetchIdentifier
|
|
3813
|
-
});
|
|
4301
|
+
}, this.viewSubject(ctx));
|
|
3814
4302
|
} catch (error) {
|
|
3815
4303
|
this.logger.error("Error in document: @Error", error);
|
|
3816
4304
|
throw error;
|
|
@@ -3823,7 +4311,7 @@ var ReactorSubgraph = class extends BaseSubgraph {
|
|
|
3823
4311
|
return await documentOutgoingRelationships(this.reactorClient, {
|
|
3824
4312
|
...args,
|
|
3825
4313
|
sourceIdentifier: handle.fetchIdentifier
|
|
3826
|
-
});
|
|
4314
|
+
}, this.viewSubject(ctx));
|
|
3827
4315
|
} catch (error) {
|
|
3828
4316
|
this.logger.error("Error in documentOutgoingRelationships: @Error", error);
|
|
3829
4317
|
throw error;
|
|
@@ -3836,7 +4324,7 @@ var ReactorSubgraph = class extends BaseSubgraph {
|
|
|
3836
4324
|
const result = await documentIncomingRelationships(this.reactorClient, {
|
|
3837
4325
|
...args,
|
|
3838
4326
|
targetIdentifier: handle.fetchIdentifier
|
|
3839
|
-
});
|
|
4327
|
+
}, this.viewSubject(ctx));
|
|
3840
4328
|
if (!this.authorizationService.isSupremeAdmin(ctx.user?.address)) {
|
|
3841
4329
|
const filteredItems = [];
|
|
3842
4330
|
for (const item of result.items) if (await this.canReadDocument(item.id, ctx)) filteredItems.push(item);
|
|
@@ -3857,7 +4345,7 @@ var ReactorSubgraph = class extends BaseSubgraph {
|
|
|
3857
4345
|
const result = await findDocuments(this.reactorClient, {
|
|
3858
4346
|
...args,
|
|
3859
4347
|
search: args.search ?? {}
|
|
3860
|
-
});
|
|
4348
|
+
}, this.viewSubject(ctx));
|
|
3861
4349
|
if (!this.authorizationService.isSupremeAdmin(ctx.user?.address)) {
|
|
3862
4350
|
const filteredItems = [];
|
|
3863
4351
|
for (const item of result.items) if (await this.canReadDocument(item.id, ctx)) filteredItems.push(item);
|
|
@@ -3891,12 +4379,25 @@ var ReactorSubgraph = class extends BaseSubgraph {
|
|
|
3891
4379
|
...args.filter,
|
|
3892
4380
|
documentId: handle.fetchIdentifier
|
|
3893
4381
|
}
|
|
3894
|
-
});
|
|
4382
|
+
}, this.viewSubject(ctx));
|
|
3895
4383
|
} catch (error) {
|
|
3896
4384
|
this.logger.error("Error in documentOperations: @Error", error);
|
|
3897
4385
|
throw error;
|
|
3898
4386
|
}
|
|
3899
4387
|
},
|
|
4388
|
+
evaluateActions: async (_parent, args, ctx) => {
|
|
4389
|
+
this.logger.debug("evaluateActions(@args)", args);
|
|
4390
|
+
try {
|
|
4391
|
+
const handle = await this.assertCanRead(args.documentIdentifier, ctx);
|
|
4392
|
+
return await evaluateActions(this.reactorClient, {
|
|
4393
|
+
...args,
|
|
4394
|
+
documentIdentifier: handle.fetchIdentifier
|
|
4395
|
+
}, this.viewSubject(ctx));
|
|
4396
|
+
} catch (error) {
|
|
4397
|
+
this.logger.error("Error in evaluateActions: @Error", error);
|
|
4398
|
+
throw error;
|
|
4399
|
+
}
|
|
4400
|
+
},
|
|
3900
4401
|
pollSyncEnvelopes: async (_parent, args, ctx) => {
|
|
3901
4402
|
this.logger.debug("pollSyncEnvelopes(@args)", args);
|
|
3902
4403
|
try {
|
|
@@ -3909,12 +4410,14 @@ var ReactorSubgraph = class extends BaseSubgraph {
|
|
|
3909
4410
|
const driveId = remote.meta.collectionId.driveId;
|
|
3910
4411
|
const isAdmin = this.authorizationService.isSupremeAdmin(ctx.user?.address);
|
|
3911
4412
|
if (!isAdmin) await this.assertCanReadCanonical(driveId, ctx);
|
|
4413
|
+
await this.#bindOrRefuseChannel(args.channelId, ctx);
|
|
3912
4414
|
const forbiddenIds = /* @__PURE__ */ new Set();
|
|
3913
4415
|
if (!isAdmin) {
|
|
3914
4416
|
if (await this.authorizationService.canRead(driveId, void 0)) await this.#collectForbiddenDocuments(remote.channel.outbox.items, forbiddenIds, ctx);
|
|
3915
4417
|
await this.#collectForbiddenDocuments(remote.channel.deadLetter.items, forbiddenIds, ctx);
|
|
3916
4418
|
}
|
|
3917
|
-
const
|
|
4419
|
+
const heldOpIds = this.syncServingGate ? await collectHeldSyncOperations([...remote.channel.outbox.items, ...remote.channel.deadLetter.items], this.syncServingGate, this.viewSubject(ctx)) : /* @__PURE__ */ new Set();
|
|
4420
|
+
const { envelopes, ackOrdinal, deadLetters, hasMore } = pollSyncEnvelopes(this.syncManager, args, forbiddenIds, heldOpIds);
|
|
3918
4421
|
return {
|
|
3919
4422
|
envelopes,
|
|
3920
4423
|
ackOrdinal,
|
|
@@ -3960,6 +4463,32 @@ var ReactorSubgraph = class extends BaseSubgraph {
|
|
|
3960
4463
|
throw error;
|
|
3961
4464
|
}
|
|
3962
4465
|
},
|
|
4466
|
+
execute: async (_parent, args, ctx) => {
|
|
4467
|
+
this.logger.debug("execute(@args)", args);
|
|
4468
|
+
try {
|
|
4469
|
+
const handle = await this.assertCanExecuteOperations(args.documentIdentifier, args.actions, ctx);
|
|
4470
|
+
return await execute(this.reactorClient, {
|
|
4471
|
+
...args,
|
|
4472
|
+
documentIdentifier: handle.fetchIdentifier
|
|
4473
|
+
});
|
|
4474
|
+
} catch (error) {
|
|
4475
|
+
this.logger.error("Error in execute(@args): @Error", args, error);
|
|
4476
|
+
throw error;
|
|
4477
|
+
}
|
|
4478
|
+
},
|
|
4479
|
+
executeAsync: async (_parent, args, ctx) => {
|
|
4480
|
+
this.logger.debug("executeAsync(@args)", args);
|
|
4481
|
+
try {
|
|
4482
|
+
const handle = await this.assertCanExecuteOperations(args.documentIdentifier, args.actions, ctx);
|
|
4483
|
+
return await executeAsync(this.reactorClient, {
|
|
4484
|
+
...args,
|
|
4485
|
+
documentIdentifier: handle.fetchIdentifier
|
|
4486
|
+
});
|
|
4487
|
+
} catch (error) {
|
|
4488
|
+
this.logger.error("Error in executeAsync(@args): @Error", args, error);
|
|
4489
|
+
throw error;
|
|
4490
|
+
}
|
|
4491
|
+
},
|
|
3963
4492
|
mutateDocument: async (_parent, args, ctx) => {
|
|
3964
4493
|
this.logger.debug("mutateDocument(@args)", args);
|
|
3965
4494
|
try {
|
|
@@ -4093,7 +4622,8 @@ var ReactorSubgraph = class extends BaseSubgraph {
|
|
|
4093
4622
|
const driveId = DriveCollectionId.fromKey(args.input.collectionId).driveId;
|
|
4094
4623
|
await this.assertCanReadCanonical(driveId, ctx);
|
|
4095
4624
|
}
|
|
4096
|
-
|
|
4625
|
+
await this.#bindOrRefuseChannel(args.input.id, ctx);
|
|
4626
|
+
return await touchChannel(this.syncManager, args, this.#creatorBinding(ctx));
|
|
4097
4627
|
} catch (error) {
|
|
4098
4628
|
this.logger.error("Error in touchChannel(@args): @Error", args, error);
|
|
4099
4629
|
throw error;
|
|
@@ -4205,10 +4735,10 @@ const ADMIN_USERS = getAdminUsers();
|
|
|
4205
4735
|
//#endregion
|
|
4206
4736
|
//#region src/graphql/system/version.ts
|
|
4207
4737
|
function getVersion() {
|
|
4208
|
-
return "6.2.2-dev.
|
|
4738
|
+
return "6.2.2-dev.80";
|
|
4209
4739
|
}
|
|
4210
4740
|
function getGitHash() {
|
|
4211
|
-
return "
|
|
4741
|
+
return "6cfb6bbf07eba913327975c751e2b3e712efb3a2";
|
|
4212
4742
|
}
|
|
4213
4743
|
function getGitUrl() {
|
|
4214
4744
|
return buildTreeUrl(getGitHash());
|
|
@@ -4306,6 +4836,13 @@ var HttpPackageLoader = class {
|
|
|
4306
4836
|
this.logger.verbose(`Loaded ${models.length} document models from ${packageName}`);
|
|
4307
4837
|
return models;
|
|
4308
4838
|
}
|
|
4839
|
+
async loadUpgradeManifests(packageSpec) {
|
|
4840
|
+
const { name: packageName } = this.parsePackageSpec(packageSpec);
|
|
4841
|
+
if (!this.isValidPackageName(packageName)) throw new Error(`Invalid package name: ${packageName}`);
|
|
4842
|
+
const manifests = extractUpgradeManifests(await import(`${this.registryUrl}-/cdn/${packageSpec}/node/document-models/index.mjs`));
|
|
4843
|
+
if (manifests.length > 0) this.logger.verbose(`Loaded ${manifests.length} upgrade manifests from ${packageName}`);
|
|
4844
|
+
return manifests;
|
|
4845
|
+
}
|
|
4309
4846
|
async loadSubgraphs(packageSpec) {
|
|
4310
4847
|
const { name: packageName } = this.parsePackageSpec(packageSpec);
|
|
4311
4848
|
if (!this.isValidPackageName(packageName)) throw new Error(`Invalid package name: ${packageName}`);
|
|
@@ -4431,7 +4968,7 @@ var ImportPackageLoader = class {
|
|
|
4431
4968
|
this.logger.verbose(`Loading document models from package: ${identifier}`);
|
|
4432
4969
|
const pkgModule = await loadDocumentModels(identifier);
|
|
4433
4970
|
if (pkgModule) {
|
|
4434
|
-
const models = Object.values(pkgModule);
|
|
4971
|
+
const models = Object.values(pkgModule).filter((m) => m !== null && typeof m === "object" && "documentModel" in m);
|
|
4435
4972
|
this.logger.verbose(` ➜ Loaded ${models.length} Document Models from: ${identifier}`);
|
|
4436
4973
|
return models;
|
|
4437
4974
|
} else {
|
|
@@ -4439,6 +4976,13 @@ var ImportPackageLoader = class {
|
|
|
4439
4976
|
return [];
|
|
4440
4977
|
}
|
|
4441
4978
|
}
|
|
4979
|
+
async loadUpgradeManifests(identifier) {
|
|
4980
|
+
const pkgModule = await loadDocumentModels(identifier);
|
|
4981
|
+
if (!pkgModule) return [];
|
|
4982
|
+
const manifests = extractUpgradeManifests(pkgModule);
|
|
4983
|
+
if (manifests.length > 0) this.logger.verbose(` ➜ Loaded ${manifests.length} Upgrade Manifests from: ${identifier}`);
|
|
4984
|
+
return manifests;
|
|
4985
|
+
}
|
|
4442
4986
|
async loadSubgraphs(identifier) {
|
|
4443
4987
|
this.logger.verbose(`Loading subgraphs from package: ${identifier}`);
|
|
4444
4988
|
const pkgModule = await loadSubgraphs(identifier);
|
|
@@ -4487,13 +5031,19 @@ function isExpectedLoaderMiss(error, pkg) {
|
|
|
4487
5031
|
}
|
|
4488
5032
|
function getUniqueDocumentModels(...documentModels) {
|
|
4489
5033
|
const uniqueModels = /* @__PURE__ */ new Map();
|
|
4490
|
-
for (const models of documentModels) for (const model of models) uniqueModels.set(model.documentModel.global.id
|
|
5034
|
+
for (const models of documentModels) for (const model of models) uniqueModels.set(`${model.documentModel.global.id}@${model.version ?? 1}`, model);
|
|
4491
5035
|
return Array.from(uniqueModels.values());
|
|
4492
5036
|
}
|
|
5037
|
+
function getUniqueUpgradeManifests(...upgradeManifests) {
|
|
5038
|
+
const uniqueManifests = /* @__PURE__ */ new Map();
|
|
5039
|
+
for (const manifests of upgradeManifests) for (const manifest of manifests) uniqueManifests.set(manifest.documentType, manifest);
|
|
5040
|
+
return Array.from(uniqueManifests.values());
|
|
5041
|
+
}
|
|
4493
5042
|
var PackageManager = class {
|
|
4494
5043
|
logger = childLogger(["reactor-api", "package-manager"]);
|
|
4495
5044
|
loaders;
|
|
4496
5045
|
docModelsMap = /* @__PURE__ */ new Map();
|
|
5046
|
+
upgradeManifestsMap = /* @__PURE__ */ new Map();
|
|
4497
5047
|
subgraphsMap = /* @__PURE__ */ new Map();
|
|
4498
5048
|
processorMap = /* @__PURE__ */ new Map();
|
|
4499
5049
|
configWatcher;
|
|
@@ -4511,8 +5061,10 @@ var PackageManager = class {
|
|
|
4511
5061
|
async loadPackages(packages) {
|
|
4512
5062
|
this.logger.info("Loading packages: @packages", packages.join(", "));
|
|
4513
5063
|
const documentModelsMap = await this.loadDocumentModels(packages);
|
|
5064
|
+
const upgradeManifestsMap = await this.loadUpgradeManifests(packages);
|
|
4514
5065
|
const subgraphsMap = await this.loadSubgraphs(packages);
|
|
4515
5066
|
const processorsMap = await this.loadProcessors(packages);
|
|
5067
|
+
this.upgradeManifestsMap = upgradeManifestsMap;
|
|
4516
5068
|
this.updatePackagesMap(documentModelsMap);
|
|
4517
5069
|
this.updateSubgraphsMap(subgraphsMap);
|
|
4518
5070
|
this.updateProcessorsMap(processorsMap);
|
|
@@ -4523,6 +5075,7 @@ var PackageManager = class {
|
|
|
4523
5075
|
}
|
|
4524
5076
|
return {
|
|
4525
5077
|
documentModels: getUniqueDocumentModels(...Array.from(documentModelsMap.values())),
|
|
5078
|
+
upgradeManifests: this.getUniqueUpgradeManifests(),
|
|
4526
5079
|
subgraphs: subgraphsMap,
|
|
4527
5080
|
processors: processorsMap
|
|
4528
5081
|
};
|
|
@@ -4533,6 +5086,7 @@ var PackageManager = class {
|
|
|
4533
5086
|
documentModelModuleMap.set("document-drive", [driveDocumentModelModule]);
|
|
4534
5087
|
documentModelModuleMap.set("document-model", [documentModelDocumentModelModule]);
|
|
4535
5088
|
documentModelModuleMap.set("reactor-drive", [reactorDriveDocumentModelModule]);
|
|
5089
|
+
documentModelModuleMap.set("reactor-group", [ReactorGroupV1]);
|
|
4536
5090
|
for (const pkg of packages) {
|
|
4537
5091
|
const allDocumentModels = [];
|
|
4538
5092
|
const failures = [];
|
|
@@ -4555,6 +5109,26 @@ var PackageManager = class {
|
|
|
4555
5109
|
}
|
|
4556
5110
|
return documentModelModuleMap;
|
|
4557
5111
|
}
|
|
5112
|
+
/** Upgrade manifests currently loaded across all packages, one per type. */
|
|
5113
|
+
getUniqueUpgradeManifests() {
|
|
5114
|
+
return getUniqueUpgradeManifests(...Array.from(this.upgradeManifestsMap.values()));
|
|
5115
|
+
}
|
|
5116
|
+
async loadUpgradeManifests(packages) {
|
|
5117
|
+
const manifestsMap = /* @__PURE__ */ new Map();
|
|
5118
|
+
for (const pkg of packages) for (const loader of this.loaders) {
|
|
5119
|
+
if (!loader.loadUpgradeManifests) continue;
|
|
5120
|
+
try {
|
|
5121
|
+
const manifests = await loader.loadUpgradeManifests(pkg);
|
|
5122
|
+
if (manifests.length > 0) {
|
|
5123
|
+
manifestsMap.set(pkg, manifests);
|
|
5124
|
+
break;
|
|
5125
|
+
}
|
|
5126
|
+
} catch (error) {
|
|
5127
|
+
this.logger.debug(`[${loader.name}] Failed to load upgrade manifests from package @pkg: @error`, pkg, error);
|
|
5128
|
+
}
|
|
5129
|
+
}
|
|
5130
|
+
return manifestsMap;
|
|
5131
|
+
}
|
|
4558
5132
|
async loadSubgraphs(packages) {
|
|
4559
5133
|
this.logger.debug(`Loading subgraphs from packages: ${packages.join(", ")}`);
|
|
4560
5134
|
const subgraphsMap = /* @__PURE__ */ new Map();
|
|
@@ -4601,7 +5175,6 @@ var PackageManager = class {
|
|
|
4601
5175
|
this.maybeWarnAllLoadersFailed("processors", pkg, succeeded, failures);
|
|
4602
5176
|
processorsMap.set(pkg, allProcessors);
|
|
4603
5177
|
}
|
|
4604
|
-
this.updateProcessorsMap(processorsMap);
|
|
4605
5178
|
return processorsMap;
|
|
4606
5179
|
}
|
|
4607
5180
|
maybeWarnAllLoadersFailed(kind, pkg, succeeded, failures) {
|
|
@@ -4616,23 +5189,50 @@ var PackageManager = class {
|
|
|
4616
5189
|
async updateDocumentModelsForPackage(pkg) {
|
|
4617
5190
|
this.logger.debug(`Updating document models for package: ${pkg}`);
|
|
4618
5191
|
const documentModels = await this.loadDocumentModels([pkg]);
|
|
5192
|
+
const upgradeManifests = await this.loadUpgradeManifests([pkg]);
|
|
5193
|
+
const upgradeManifestsMap = new Map(this.upgradeManifestsMap);
|
|
5194
|
+
upgradeManifestsMap.set(pkg, upgradeManifests.get(pkg) ?? []);
|
|
5195
|
+
this.upgradeManifestsMap = upgradeManifestsMap;
|
|
4619
5196
|
const documentModelsMap = new Map(this.docModelsMap);
|
|
4620
5197
|
documentModelsMap.set(pkg, documentModels.get(pkg) ?? []);
|
|
4621
5198
|
this.updatePackagesMap(documentModelsMap);
|
|
4622
5199
|
}
|
|
5200
|
+
async updateSubgraphsForPackage(pkg) {
|
|
5201
|
+
this.logger.debug(`Updating subgraphs for package: ${pkg}`);
|
|
5202
|
+
const subgraphs = await this.loadSubgraphs([pkg]);
|
|
5203
|
+
const subgraphsMap = new Map(this.subgraphsMap);
|
|
5204
|
+
subgraphsMap.set(pkg, subgraphs.get(pkg) ?? []);
|
|
5205
|
+
this.updateSubgraphsMap(subgraphsMap);
|
|
5206
|
+
}
|
|
5207
|
+
async updateProcessorsForPackage(pkg) {
|
|
5208
|
+
this.logger.debug(`Updating processors for package: ${pkg}`);
|
|
5209
|
+
const processors = await this.loadProcessors([pkg]);
|
|
5210
|
+
const processorsMap = new Map(this.processorMap);
|
|
5211
|
+
processorsMap.set(pkg, processors.get(pkg) ?? []);
|
|
5212
|
+
this.updateProcessorsMap(processorsMap);
|
|
5213
|
+
}
|
|
5214
|
+
/** Debounced per key so repeated subscribePackages calls reuse one timer. */
|
|
5215
|
+
getDebouncedUpdateCallback(key, update) {
|
|
5216
|
+
if (!this.debouncedUpdateCallbacks.has(key)) {
|
|
5217
|
+
const debouncedFn = debounce(update, 1e3);
|
|
5218
|
+
this.debouncedUpdateCallbacks.set(key, () => {
|
|
5219
|
+
debouncedFn().catch((error) => {
|
|
5220
|
+
this.logger.error("Failed to update package: @error", error);
|
|
5221
|
+
});
|
|
5222
|
+
});
|
|
5223
|
+
}
|
|
5224
|
+
return this.debouncedUpdateCallbacks.get(key);
|
|
5225
|
+
}
|
|
4623
5226
|
subscribePackages(packages) {
|
|
4624
5227
|
const unsubs = [];
|
|
4625
5228
|
for (const pkg of packages) {
|
|
4626
|
-
|
|
4627
|
-
|
|
4628
|
-
|
|
4629
|
-
|
|
4630
|
-
|
|
4631
|
-
|
|
4632
|
-
|
|
4633
|
-
for (const loader of this.loaders) if (loader.onDocumentModelsChange) {
|
|
4634
|
-
const unsub = loader.onDocumentModelsChange(pkg, debouncedCallback);
|
|
4635
|
-
unsubs.push(unsub);
|
|
5229
|
+
const onDocumentModels = this.getDebouncedUpdateCallback(`${pkg}:document-models`, () => this.updateDocumentModelsForPackage(pkg));
|
|
5230
|
+
const onSubgraphs = this.getDebouncedUpdateCallback(`${pkg}:subgraphs`, () => this.updateSubgraphsForPackage(pkg));
|
|
5231
|
+
const onProcessors = this.getDebouncedUpdateCallback(`${pkg}:processors`, () => this.updateProcessorsForPackage(pkg));
|
|
5232
|
+
for (const loader of this.loaders) {
|
|
5233
|
+
if (loader.onDocumentModelsChange) unsubs.push(loader.onDocumentModelsChange(pkg, onDocumentModels));
|
|
5234
|
+
if (loader.onSubgraphsChange) unsubs.push(loader.onSubgraphsChange(pkg, onSubgraphs));
|
|
5235
|
+
if (loader.onProcessorsChange) unsubs.push(loader.onProcessorsChange(pkg, onProcessors));
|
|
4636
5236
|
}
|
|
4637
5237
|
}
|
|
4638
5238
|
return () => {
|
|
@@ -4695,6 +5295,70 @@ var PackageManager = class {
|
|
|
4695
5295
|
const config = { basePath: process.env.BASE_PATH || "/" };
|
|
4696
5296
|
const DefaultCoreSubgraphs = [AnalyticsSubgraph, SystemSubgraph];
|
|
4697
5297
|
//#endregion
|
|
5298
|
+
//#region src/attachment-backend.ts
|
|
5299
|
+
/** Filesystem is the default; explicit S3 is readiness-gated without fallback. */
|
|
5300
|
+
async function createStartupAttachmentBackend({ db, env = process.env, s3Dependencies }) {
|
|
5301
|
+
const storage = parseAttachmentStorageConfig(env);
|
|
5302
|
+
if (storage.kind === "filesystem") return void 0;
|
|
5303
|
+
const backend = createS3AttachmentBackend(db.withSchema("attachments"), storage.s3, s3Dependencies);
|
|
5304
|
+
if (!(await backend.health()).ready) throw new Error("S3 attachment backend readiness check failed");
|
|
5305
|
+
return backend;
|
|
5306
|
+
}
|
|
5307
|
+
//#endregion
|
|
5308
|
+
//#region src/services/attachment-access.service.ts
|
|
5309
|
+
const HASH_PATTERN = /^[a-f0-9]{64}$/;
|
|
5310
|
+
/**
|
|
5311
|
+
* Composes document authorization with the projected document/ref
|
|
5312
|
+
* relationship. Order is fixed: validate ref, resolve the canonical document
|
|
5313
|
+
* id, check `canRead`, then check the reference index. A denied document
|
|
5314
|
+
* never reaches the reference reader, and the facade never touches
|
|
5315
|
+
* attachment metadata, storage backends, or presigners.
|
|
5316
|
+
*/
|
|
5317
|
+
var AttachmentAccessService = class {
|
|
5318
|
+
constructor(resolveCanonicalId, authorization, references, projection) {
|
|
5319
|
+
this.resolveCanonicalId = resolveCanonicalId;
|
|
5320
|
+
this.authorization = authorization;
|
|
5321
|
+
this.references = references;
|
|
5322
|
+
this.projection = projection;
|
|
5323
|
+
}
|
|
5324
|
+
async canReadAttachment(request) {
|
|
5325
|
+
if (this.projection.status !== "available") return { kind: "projection-unavailable" };
|
|
5326
|
+
const ref = normalizeAttachmentRef(request.attachmentRef);
|
|
5327
|
+
if (ref === null) return { kind: "denied" };
|
|
5328
|
+
let documentId;
|
|
5329
|
+
try {
|
|
5330
|
+
documentId = await this.resolveCanonicalId(request.documentId);
|
|
5331
|
+
} catch {
|
|
5332
|
+
return { kind: "denied" };
|
|
5333
|
+
}
|
|
5334
|
+
if (!await this.authorization.canRead(documentId, request.userAddress)) return { kind: "denied" };
|
|
5335
|
+
if (!await this.references.hasReference(documentId, ref)) return { kind: "denied" };
|
|
5336
|
+
return {
|
|
5337
|
+
kind: "allowed",
|
|
5338
|
+
documentId,
|
|
5339
|
+
ref
|
|
5340
|
+
};
|
|
5341
|
+
}
|
|
5342
|
+
};
|
|
5343
|
+
/**
|
|
5344
|
+
* Parses and canonicalizes a caller-supplied ref through the shared parser.
|
|
5345
|
+
* Returns null for anything that is not a v1 ref over a 64-char SHA-256 hex
|
|
5346
|
+
* hash; hex case is normalized so index lookups use the canonical form.
|
|
5347
|
+
*/
|
|
5348
|
+
function normalizeAttachmentRef(value) {
|
|
5349
|
+
let hash;
|
|
5350
|
+
let version;
|
|
5351
|
+
try {
|
|
5352
|
+
const parsed = parseRef(value);
|
|
5353
|
+
hash = parsed.hash.toLowerCase();
|
|
5354
|
+
version = parsed.version;
|
|
5355
|
+
} catch {
|
|
5356
|
+
return null;
|
|
5357
|
+
}
|
|
5358
|
+
if (version !== 1 || !HASH_PATTERN.test(hash)) return null;
|
|
5359
|
+
return createRef(hash, version);
|
|
5360
|
+
}
|
|
5361
|
+
//#endregion
|
|
4698
5362
|
//#region src/migrations/001_create_document_permissions.ts
|
|
4699
5363
|
var _001_create_document_permissions_exports = /* @__PURE__ */ __exportAll({
|
|
4700
5364
|
down: () => down$2,
|
|
@@ -4915,17 +5579,27 @@ var AuthService = class {
|
|
|
4915
5579
|
constructor(config) {
|
|
4916
5580
|
this.config = config;
|
|
4917
5581
|
}
|
|
4918
|
-
|
|
4919
|
-
|
|
5582
|
+
/**
|
|
5583
|
+
* Whether this service reads the bearer at all. Defaults to `enabled`, so a
|
|
5584
|
+
* config written before `resolveIdentity` existed behaves exactly as it did.
|
|
5585
|
+
*/
|
|
5586
|
+
get resolvesIdentity() {
|
|
5587
|
+
return this.config.resolveIdentity ?? this.config.enabled;
|
|
5588
|
+
}
|
|
5589
|
+
/** The context an unresolved request gets: no user, and no admin list to match against. */
|
|
5590
|
+
anonymousContext() {
|
|
5591
|
+
return {
|
|
4920
5592
|
user: void 0,
|
|
4921
5593
|
admins: [],
|
|
4922
|
-
auth_enabled:
|
|
5594
|
+
auth_enabled: this.config.enabled
|
|
4923
5595
|
};
|
|
4924
|
-
|
|
4925
|
-
|
|
5596
|
+
}
|
|
5597
|
+
async authenticateRequest(request) {
|
|
5598
|
+
if (!this.resolvesIdentity) return this.anonymousContext();
|
|
5599
|
+
if (request.method === "OPTIONS") return {
|
|
4926
5600
|
user: void 0,
|
|
4927
5601
|
admins: this.config.admins,
|
|
4928
|
-
auth_enabled:
|
|
5602
|
+
auth_enabled: this.config.enabled
|
|
4929
5603
|
};
|
|
4930
5604
|
return this.verifyBearer(request.headers.get("authorization") ?? void 0);
|
|
4931
5605
|
}
|
|
@@ -4934,16 +5608,12 @@ var AuthService = class {
|
|
|
4934
5608
|
* middleware that must enforce authentication on every request.
|
|
4935
5609
|
*/
|
|
4936
5610
|
async verifyBearer(authorization) {
|
|
4937
|
-
if (!this.
|
|
4938
|
-
user: void 0,
|
|
4939
|
-
admins: [],
|
|
4940
|
-
auth_enabled: false
|
|
4941
|
-
};
|
|
5611
|
+
if (!this.resolvesIdentity) return this.anonymousContext();
|
|
4942
5612
|
const token = authorization?.split(" ")[1];
|
|
4943
5613
|
if (!token) return {
|
|
4944
5614
|
user: void 0,
|
|
4945
5615
|
admins: this.config.admins,
|
|
4946
|
-
auth_enabled:
|
|
5616
|
+
auth_enabled: this.config.enabled
|
|
4947
5617
|
};
|
|
4948
5618
|
try {
|
|
4949
5619
|
const verified = await this.verifyToken(token);
|
|
@@ -4956,16 +5626,19 @@ var AuthService = class {
|
|
|
4956
5626
|
return {
|
|
4957
5627
|
user,
|
|
4958
5628
|
admins: this.config.admins,
|
|
4959
|
-
auth_enabled:
|
|
5629
|
+
auth_enabled: this.config.enabled
|
|
4960
5630
|
};
|
|
4961
5631
|
} catch {
|
|
4962
5632
|
return new Response(JSON.stringify({ error: "Authentication failed" }), { status: 401 });
|
|
4963
5633
|
}
|
|
4964
5634
|
}
|
|
4965
5635
|
async authenticateWebSocketConnection(connectionParams) {
|
|
4966
|
-
if (!this.
|
|
5636
|
+
if (!this.resolvesIdentity) return null;
|
|
4967
5637
|
const authHeader = connectionParams.authorization;
|
|
4968
|
-
if (!authHeader)
|
|
5638
|
+
if (!authHeader) {
|
|
5639
|
+
if (!this.config.enabled) return null;
|
|
5640
|
+
throw new Error("Missing authorization in connection parameters");
|
|
5641
|
+
}
|
|
4969
5642
|
const token = authHeader.split(" ")[1];
|
|
4970
5643
|
if (!token) throw new Error("Invalid authorization format");
|
|
4971
5644
|
const verified = await this.verifyToken(token);
|
|
@@ -4984,16 +5657,23 @@ var AuthService = class {
|
|
|
4984
5657
|
return await verifyAuthBearerToken(token);
|
|
4985
5658
|
}
|
|
4986
5659
|
/**
|
|
4987
|
-
* Extract user information from verification result
|
|
5660
|
+
* Extract user information from verification result.
|
|
5661
|
+
*
|
|
5662
|
+
* The issuer is required along with the credential subject. A credential that
|
|
5663
|
+
* verified must carry one -- resolving it is how the signature was checked --
|
|
5664
|
+
* so this rejects a shape that cannot have come from verification rather than
|
|
5665
|
+
* admitting a keyless principal.
|
|
4988
5666
|
*/
|
|
4989
5667
|
extractUserFromVerification(verified) {
|
|
4990
5668
|
try {
|
|
4991
5669
|
const { address, chainId, networkId } = verified.verifiableCredential.credentialSubject;
|
|
4992
|
-
|
|
5670
|
+
const appKey = verified.issuer;
|
|
5671
|
+
if (!address || !chainId || !networkId || !appKey) return null;
|
|
4993
5672
|
return {
|
|
4994
5673
|
address,
|
|
4995
5674
|
chainId,
|
|
4996
|
-
networkId
|
|
5675
|
+
networkId,
|
|
5676
|
+
appKey
|
|
4997
5677
|
};
|
|
4998
5678
|
} catch {
|
|
4999
5679
|
return null;
|
|
@@ -5042,14 +5722,33 @@ var AuthService = class {
|
|
|
5042
5722
|
}
|
|
5043
5723
|
}
|
|
5044
5724
|
async fetchCredentialExists(address, chainId, appId) {
|
|
5045
|
-
|
|
5725
|
+
if (!this.config.verifyCredential) throw new Error("AuthService: credential verification is enabled but no verifyCredential was provided");
|
|
5726
|
+
return this.config.verifyCredential({
|
|
5046
5727
|
address,
|
|
5047
5728
|
chainId,
|
|
5048
|
-
|
|
5049
|
-
})
|
|
5729
|
+
appId
|
|
5730
|
+
});
|
|
5050
5731
|
}
|
|
5051
5732
|
};
|
|
5052
5733
|
//#endregion
|
|
5734
|
+
//#region src/services/renown-credential-verifier.ts
|
|
5735
|
+
async function createRenownCredentialVerifier(config = {}) {
|
|
5736
|
+
const switchboardUrl = await resolveSwitchboardEndpoint({
|
|
5737
|
+
switchboardUrl: config.switchboardUrl,
|
|
5738
|
+
baseUrl: config.renownUrl
|
|
5739
|
+
});
|
|
5740
|
+
return async ({ address, chainId, appId }) => {
|
|
5741
|
+
return await fetchDelegationCredential({
|
|
5742
|
+
address,
|
|
5743
|
+
chainId,
|
|
5744
|
+
appDid: appId,
|
|
5745
|
+
switchboardUrl,
|
|
5746
|
+
baseUrl: switchboardUrl ? void 0 : config.renownUrl,
|
|
5747
|
+
discover: false
|
|
5748
|
+
}) !== void 0;
|
|
5749
|
+
};
|
|
5750
|
+
}
|
|
5751
|
+
//#endregion
|
|
5053
5752
|
//#region src/services/document-permission.service.ts
|
|
5054
5753
|
/**
|
|
5055
5754
|
* Service for managing document-level permissions.
|
|
@@ -5365,7 +6064,35 @@ function createMcpRequestAuthorizer(authService, authorizationService) {
|
|
|
5365
6064
|
};
|
|
5366
6065
|
}
|
|
5367
6066
|
//#endregion
|
|
6067
|
+
//#region src/services/renown-config.ts
|
|
6068
|
+
/** Reading credentials from this reactor's own read model needs a host-supplied
|
|
6069
|
+
* verifier: core has no idea which subgraph serves that read model. */
|
|
6070
|
+
function assertCredentialVerifierForSource(source, hasVerifier) {
|
|
6071
|
+
if (source !== "self" || hasVerifier) return;
|
|
6072
|
+
throw new Error("Renown credential verification is set to \"self\" (auth.renown.source or RENOWN_SOURCE) but no verifyCredential was provided. A host that reads its own read model must pass one — see apps/switchboard, which builds it from @renown/sdk createLocalCredentialVerifier — or set RENOWN_SOURCE=remote to verify against a Renown instance.");
|
|
6073
|
+
}
|
|
6074
|
+
function envOverride(value) {
|
|
6075
|
+
const trimmed = value?.trim();
|
|
6076
|
+
return trimmed ? trimmed : void 0;
|
|
6077
|
+
}
|
|
6078
|
+
/** Resolve `auth.renown`; RENOWN_SOURCE / RENOWN_URL / SWITCHBOARD_URL win
|
|
6079
|
+
* over the config file, which wins over the SDK defaults. */
|
|
6080
|
+
function resolveRenownConfig(configured, env, logger) {
|
|
6081
|
+
return {
|
|
6082
|
+
source: resolveRenownSource(envOverride(env.RENOWN_SOURCE) ?? configured?.source, logger),
|
|
6083
|
+
url: envOverride(env.RENOWN_URL) ?? configured?.url,
|
|
6084
|
+
switchboardUrl: envOverride(env.SWITCHBOARD_URL) ?? configured?.switchboardUrl
|
|
6085
|
+
};
|
|
6086
|
+
}
|
|
6087
|
+
function resolveRenownSource(value, logger) {
|
|
6088
|
+
if (value === void 0 || value === null) return "remote";
|
|
6089
|
+
if (value === "self" || value === "remote") return value;
|
|
6090
|
+
logger.warn(`Ignoring invalid renown source "${value}" (expected "self" or "remote") — using "remote"`);
|
|
6091
|
+
return "remote";
|
|
6092
|
+
}
|
|
6093
|
+
//#endregion
|
|
5368
6094
|
//#region src/utils/db.ts
|
|
6095
|
+
const PGLITE_UTC_PARSERS = { 1114: (value) => /* @__PURE__ */ new Date(`${value.replace(" ", "T")}Z`) };
|
|
5369
6096
|
function isPG(connectionString) {
|
|
5370
6097
|
if (connectionString.startsWith("postgresql://") || connectionString.startsWith("postgres://")) return true;
|
|
5371
6098
|
return false;
|
|
@@ -5396,7 +6123,10 @@ function getDbClient(connectionString = void 0, pgliteFactory) {
|
|
|
5396
6123
|
if (cached) return cached;
|
|
5397
6124
|
const isPg = connectionString && isPG(connectionString);
|
|
5398
6125
|
const client = isPg ? "pg" : ClientPgLite;
|
|
5399
|
-
const pgliteInstance = isPg ? void 0 : pgliteFactory ? pgliteFactory(connectionString) : connectionString ? new PGlite({
|
|
6126
|
+
const pgliteInstance = isPg ? void 0 : pgliteFactory ? pgliteFactory(connectionString) : connectionString ? new PGlite({
|
|
6127
|
+
fs: new AtomicNodeFs(connectionString),
|
|
6128
|
+
parsers: PGLITE_UTC_PARSERS
|
|
6129
|
+
}) : new PGlite({ parsers: PGLITE_UTC_PARSERS });
|
|
5400
6130
|
const connection = isPg ? { connectionString } : { pglite: pgliteInstance };
|
|
5401
6131
|
if (connectionString && !isPg) {
|
|
5402
6132
|
const dirPath = path.resolve(connectionString, "..");
|
|
@@ -5502,8 +6232,8 @@ function assertAuthRequiredForDocumentPermissions(authEnabled, documentPermissio
|
|
|
5502
6232
|
* opt-in: it removes the only binding between a token's claimed address and its
|
|
5503
6233
|
* signing key. Fail-closed — unset NODE_ENV counts as production.
|
|
5504
6234
|
*/
|
|
5505
|
-
function assertSkipCredentialVerificationAllowed(
|
|
5506
|
-
if (!
|
|
6235
|
+
function assertSkipCredentialVerificationAllowed(resolvesCallerIdentity, skipCredentialVerification, env) {
|
|
6236
|
+
if (!resolvesCallerIdentity || !skipCredentialVerification) return;
|
|
5507
6237
|
const inAutomatedTest = env.VITEST === "true" || env.NODE_ENV === "test";
|
|
5508
6238
|
const acknowledged = env.ALLOW_INSECURE_SKIP_CREDENTIAL_VERIFICATION === "true";
|
|
5509
6239
|
if (!inAutomatedTest && !acknowledged) throw new Error("SKIP_CREDENTIAL_VERIFICATION is set but refused: it disables the live Renown credential check — the only check binding a token's claimed address to the key that signed it — so honoring it allows identity spoofing, including of admins. It is never safe in production. For local or sandbox use, also set ALLOW_INSECURE_SKIP_CREDENTIAL_VERIFICATION=true to acknowledge the risk; automated test runs (VITEST=true or NODE_ENV=test) are exempt.");
|
|
@@ -5553,10 +6283,31 @@ function makeDbClosers(knexInstance, pglite) {
|
|
|
5553
6283
|
return closers;
|
|
5554
6284
|
}
|
|
5555
6285
|
/**
|
|
6286
|
+
* The gate sync serving evaluates a document's own policy through, or undefined
|
|
6287
|
+
* when there is none to evaluate.
|
|
6288
|
+
*
|
|
6289
|
+
* It is built here rather than taken off the reactor client because it is not
|
|
6290
|
+
* the same gate reads use: it carries the host's closes-by-default setting,
|
|
6291
|
+
* which withholds the domain scopes of a document nobody has policied yet. That
|
|
6292
|
+
* answer belongs to serving alone -- replay must keep reading an uninitialized
|
|
6293
|
+
* document in full -- so the two gates are deliberately separate objects over
|
|
6294
|
+
* the same model.
|
|
6295
|
+
*
|
|
6296
|
+
* Undefined below `authEnforcement`, where the registered model ignores the auth
|
|
6297
|
+
* scope: gating through it would serve every domain scope of a policied document
|
|
6298
|
+
* to anyone, which is worse than not gating at all.
|
|
6299
|
+
*/
|
|
6300
|
+
function buildSyncServingGate(reactorModule, authorizationConfig, logger) {
|
|
6301
|
+
if (!reactorModule) return;
|
|
6302
|
+
const model = readDecisionModel(reactorModule.featureFlags, reactorModule.documentModelRegistry);
|
|
6303
|
+
if (!model) return;
|
|
6304
|
+
return new SyncScopeGate(new ModelReadGate(model, reactorModule.documentView, reactorModule.featureFlags.authGroups, reactorModule.operationIndex, logger, { withholdUninitialized: authorizationConfig.defaultProtection }), reactorModule.documentView, logger);
|
|
6305
|
+
}
|
|
6306
|
+
/**
|
|
5556
6307
|
* Sets up the subgraph manager and registers subgraphs
|
|
5557
6308
|
*/
|
|
5558
|
-
async function setupGraphQLManager(httpAdapter, authFetchMiddleware, httpServer, wsServer, client, relationalDb, analyticsStore, syncManager, subgraphs, logger, authorizationService, authService, documentPermissionService, enableDocumentModelSubgraphs, port, reactorDriveClient) {
|
|
5559
|
-
const graphqlManager = new GraphQLManager(config.basePath, httpServer, wsServer, client, relationalDb, analyticsStore, syncManager, logger, httpAdapter, await createGatewayAdapter("apollo", logger), authService, documentPermissionService, { enableDocumentModelSubgraphs }, port, authorizationService, reactorDriveClient);
|
|
6309
|
+
async function setupGraphQLManager(httpAdapter, authFetchMiddleware, httpServer, wsServer, client, relationalDb, analyticsStore, syncManager, subgraphs, logger, authorizationService, authService, documentPermissionService, enableDocumentModelSubgraphs, port, reactorDriveClient, syncServingGate) {
|
|
6310
|
+
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);
|
|
5560
6311
|
await graphqlManager.init(subgraphs.core, authFetchMiddleware);
|
|
5561
6312
|
for (const [, collection] of subgraphs.extended.entries()) for (const subgraph of collection) await graphqlManager.registerSubgraph(subgraph, "graphql");
|
|
5562
6313
|
await graphqlManager.updateRouter();
|
|
@@ -5568,15 +6319,20 @@ async function setupGraphQLManager(httpAdapter, authFetchMiddleware, httpServer,
|
|
|
5568
6319
|
function setupEventListeners(pkgManager, graphqlManager, reactorProcessorManager, module, documentModelRegistry) {
|
|
5569
6320
|
pkgManager.onDocumentModelsChange((packagedModels) => {
|
|
5570
6321
|
if (documentModelRegistry) {
|
|
5571
|
-
const newModules = Object.values(packagedModels).flat();
|
|
5572
|
-
const
|
|
5573
|
-
|
|
5574
|
-
|
|
5575
|
-
|
|
5576
|
-
const results
|
|
5577
|
-
for (const result of results) if (result.status === "success") defaultLogger.info(`Registered new document model: ${result.item.documentModel.global.id}`);
|
|
6322
|
+
const newModules = getUniqueDocumentModels(Object.values(packagedModels).flat());
|
|
6323
|
+
const incomingTypes = new Set(newModules.map((m) => m.documentModel.global.id));
|
|
6324
|
+
if (incomingTypes.size > 0) {
|
|
6325
|
+
documentModelRegistry.unregisterModules(...incomingTypes);
|
|
6326
|
+
const results = documentModelRegistry.registerModules(...newModules);
|
|
6327
|
+
for (const result of results) if (result.status === "success") defaultLogger.info(`Registered document model: ${result.item.documentModel.global.id} v${result.item.version ?? 1}`);
|
|
5578
6328
|
else defaultLogger.error(`Failed to register document model: ${result.error.message}`);
|
|
5579
6329
|
}
|
|
6330
|
+
const manifests = pkgManager.getUniqueUpgradeManifests();
|
|
6331
|
+
if (manifests.length > 0) {
|
|
6332
|
+
documentModelRegistry.unregisterUpgradeManifests(...manifests.map((m) => m.documentType));
|
|
6333
|
+
const manifestResults = documentModelRegistry.registerUpgradeManifests(...manifests);
|
|
6334
|
+
for (const result of manifestResults) if (result.status === "error") defaultLogger.error(`Failed to register upgrade manifest: ${result.error.message}`);
|
|
6335
|
+
}
|
|
5580
6336
|
}
|
|
5581
6337
|
graphqlManager.regenerateDocumentModelSubgraphs();
|
|
5582
6338
|
});
|
|
@@ -5633,16 +6389,33 @@ async function _setupCommonInfrastructure(options) {
|
|
|
5633
6389
|
const logger = options.logger ?? defaultLogger;
|
|
5634
6390
|
let admins = [];
|
|
5635
6391
|
let authEnabled = false;
|
|
6392
|
+
let configuredResolveIdentity;
|
|
6393
|
+
let configuredRenown;
|
|
5636
6394
|
if (options.configFile) {
|
|
5637
6395
|
const config = getConfig(options.configFile);
|
|
5638
6396
|
admins = config.auth?.admins.map((a) => a.toLowerCase()) ?? [];
|
|
5639
6397
|
authEnabled = config.auth?.enabled ?? false;
|
|
6398
|
+
configuredRenown = config.auth?.renown;
|
|
5640
6399
|
} else if (options.auth) {
|
|
5641
6400
|
admins = options.auth.admins.map((a) => a.toLowerCase());
|
|
5642
6401
|
authEnabled = options.auth.enabled;
|
|
6402
|
+
configuredResolveIdentity = options.auth.resolveIdentity;
|
|
5643
6403
|
}
|
|
5644
|
-
const { AUTH_ENABLED, ADMINS, DEFAULT_PROTECTION, DOCUMENT_PERMISSIONS_ENABLED, SKIP_CREDENTIAL_VERIFICATION, CREDENTIAL_VERIFICATION_CACHE_TTL_MS } = process.env;
|
|
6404
|
+
const { AUTH_ENABLED, RESOLVE_CALLER_IDENTITY, ADMINS, DEFAULT_PROTECTION, DOCUMENT_PERMISSIONS_ENABLED, SKIP_CREDENTIAL_VERIFICATION, CREDENTIAL_VERIFICATION_CACHE_TTL_MS } = process.env;
|
|
5645
6405
|
if (AUTH_ENABLED !== void 0) authEnabled = AUTH_ENABLED === "true";
|
|
6406
|
+
/**
|
|
6407
|
+
* Whether the auth middleware reads the bearer and populates `ctx.user`,
|
|
6408
|
+
* independently of the authorization policy.
|
|
6409
|
+
*
|
|
6410
|
+
* Defaults to `authEnabled`, so a deployment that never sets it behaves
|
|
6411
|
+
* exactly as it did: auth on means the token is read AND the policy is
|
|
6412
|
+
* `ADMIN_ONLY`; auth off means neither. Setting it explicitly is what
|
|
6413
|
+
* separates the two — a server can then know who is calling while the policy
|
|
6414
|
+
* stays `OPEN`, which is the combination custom subgraphs need and the one
|
|
6415
|
+
* `AUTH_ENABLED` alone cannot express.
|
|
6416
|
+
*/
|
|
6417
|
+
let resolveCallerIdentity = configuredResolveIdentity ?? authEnabled;
|
|
6418
|
+
if (RESOLVE_CALLER_IDENTITY !== void 0) resolveCallerIdentity = RESOLVE_CALLER_IDENTITY === "true";
|
|
5646
6419
|
if (ADMINS !== void 0) admins = ADMINS.split(",").map((a) => a.toLowerCase());
|
|
5647
6420
|
let defaultProtection = false;
|
|
5648
6421
|
if (DEFAULT_PROTECTION !== void 0) defaultProtection = DEFAULT_PROTECTION.toLowerCase() === "true";
|
|
@@ -5656,7 +6429,7 @@ async function _setupCommonInfrastructure(options) {
|
|
|
5656
6429
|
}
|
|
5657
6430
|
const documentPermissionsRequested = options.documentPermissionService !== void 0 || DOCUMENT_PERMISSIONS_ENABLED === "true";
|
|
5658
6431
|
assertAuthRequiredForDocumentPermissions(authEnabled, documentPermissionsRequested);
|
|
5659
|
-
assertSkipCredentialVerificationAllowed(
|
|
6432
|
+
assertSkipCredentialVerificationAllowed(resolveCallerIdentity, skipCredentialVerification, process.env);
|
|
5660
6433
|
if (authEnabled && skipCredentialVerification) logger.warn("SECURITY: SKIP_CREDENTIAL_VERIFICATION is enabled — Renown credential verification is disabled and a bearer token's claimed address is NOT cryptographically bound to its signing key. Identity is unverifiable; use only in development or test.");
|
|
5661
6434
|
httpAdapter.getRoute("/health", () => new Response("OK", { status: 200 }));
|
|
5662
6435
|
const readiness = createReadinessGate();
|
|
@@ -5671,13 +6444,22 @@ async function _setupCommonInfrastructure(options) {
|
|
|
5671
6444
|
});
|
|
5672
6445
|
let authFetchMiddleware;
|
|
5673
6446
|
let authService;
|
|
5674
|
-
if (authEnabled) {
|
|
5675
|
-
logger.info("Setting up Auth middleware");
|
|
6447
|
+
if (resolveCallerIdentity || authEnabled) {
|
|
6448
|
+
logger.info("Setting up Auth middleware (policy enforcement: @enabled, caller identity: @resolve)", authEnabled, resolveCallerIdentity);
|
|
6449
|
+
const renown = options.renown ?? resolveRenownConfig(configuredRenown, process.env, logger);
|
|
6450
|
+
assertCredentialVerifierForSource(renown.source, options.verifyCredential !== void 0);
|
|
6451
|
+
if (options.verifyCredential) logger.info("Renown credentials will be verified by the host's verifier");
|
|
6452
|
+
else logger.info("Renown credentials will be verified against @url", renown.switchboardUrl ?? renown.url ?? "the default Renown instance");
|
|
5676
6453
|
authService = new AuthService({
|
|
5677
6454
|
enabled: authEnabled,
|
|
6455
|
+
resolveIdentity: resolveCallerIdentity,
|
|
5678
6456
|
admins,
|
|
5679
6457
|
skipCredentialVerification,
|
|
5680
|
-
credentialVerificationCacheTtlMs
|
|
6458
|
+
credentialVerificationCacheTtlMs,
|
|
6459
|
+
verifyCredential: options.verifyCredential ?? await createRenownCredentialVerifier({
|
|
6460
|
+
renownUrl: renown.url,
|
|
6461
|
+
switchboardUrl: renown.switchboardUrl
|
|
6462
|
+
})
|
|
5681
6463
|
});
|
|
5682
6464
|
authFetchMiddleware = createAuthFetchMiddleware(authService);
|
|
5683
6465
|
}
|
|
@@ -5703,7 +6485,12 @@ async function _setupCommonInfrastructure(options) {
|
|
|
5703
6485
|
await mkdir(attachmentStoragePath, { recursive: true });
|
|
5704
6486
|
const { db: attachmentDb, knex: attachmentKnex, pglite: attachmentPglite } = getDbClient(options.dbPath, options.pgliteFactory);
|
|
5705
6487
|
dbClosers.push(...makeDbClosers(attachmentKnex, attachmentPglite));
|
|
5706
|
-
const
|
|
6488
|
+
const ATTACHMENT_SWEEP_INTERVAL_MS = 3600 * 1e3;
|
|
6489
|
+
const attachmentBackend = await createStartupAttachmentBackend({ db: attachmentDb });
|
|
6490
|
+
const attachmentBuilder = new AttachmentBuilder(attachmentDb, attachmentStoragePath).withReservationSweepMs(ATTACHMENT_SWEEP_INTERVAL_MS);
|
|
6491
|
+
if (attachmentBackend) attachmentBuilder.withBackend(attachmentBackend);
|
|
6492
|
+
const attachments = await attachmentBuilder.build();
|
|
6493
|
+
const attachmentReferenceIndex = await new AttachmentReferenceIndexBuilder(attachmentDb).build();
|
|
5707
6494
|
dbClosers.push(() => {
|
|
5708
6495
|
attachments.destroy();
|
|
5709
6496
|
return Promise.resolve();
|
|
@@ -5723,6 +6510,7 @@ async function _setupCommonInfrastructure(options) {
|
|
|
5723
6510
|
documentPermissionService,
|
|
5724
6511
|
authorizationConfig,
|
|
5725
6512
|
attachments,
|
|
6513
|
+
attachmentReferenceIndex,
|
|
5726
6514
|
packages,
|
|
5727
6515
|
dbClosers,
|
|
5728
6516
|
readiness
|
|
@@ -5731,26 +6519,17 @@ async function _setupCommonInfrastructure(options) {
|
|
|
5731
6519
|
/**
|
|
5732
6520
|
* Private helper function containing common setup logic for API initialization
|
|
5733
6521
|
*/
|
|
5734
|
-
async function _setupAPI(reactorClient, syncManager, reactorProcessorManager, httpAdapter, authFetchMiddleware, authService, port, packages, relationalDb, analyticsStore, documentPermissionService, processors, subgraphs, options, processorApp, readModels, attachments, authorizationConfig, documentModelRegistry, dbClosers = [], reactorDriveClient) {
|
|
6522
|
+
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) {
|
|
5735
6523
|
const hostModule = {
|
|
5736
|
-
|
|
5737
|
-
|
|
5738
|
-
|
|
5739
|
-
|
|
5740
|
-
|
|
5741
|
-
|
|
5742
|
-
|
|
5743
|
-
|
|
5744
|
-
|
|
5745
|
-
id: jobInfo.id,
|
|
5746
|
-
status: jobInfo.status
|
|
5747
|
-
};
|
|
5748
|
-
} },
|
|
5749
|
-
getReadModel(name) {
|
|
5750
|
-
const model = readModels.find((m) => m.name === name);
|
|
5751
|
-
if (!model) throw new Error(`Read model "${name}" not found`);
|
|
5752
|
-
return model;
|
|
5753
|
-
}
|
|
6524
|
+
...createReactorHostModuleBase({
|
|
6525
|
+
client: reactorClient,
|
|
6526
|
+
readModels,
|
|
6527
|
+
relationalDb,
|
|
6528
|
+
analyticsStore,
|
|
6529
|
+
processorApp,
|
|
6530
|
+
config: options.processorConfig
|
|
6531
|
+
}),
|
|
6532
|
+
attachments: createAttachmentClient(attachments.service)
|
|
5754
6533
|
};
|
|
5755
6534
|
const mcpServerEnabled = options.mcp ?? true;
|
|
5756
6535
|
const logger = options.logger ?? defaultLogger;
|
|
@@ -5778,6 +6557,7 @@ async function _setupAPI(reactorClient, syncManager, reactorProcessorManager, ht
|
|
|
5778
6557
|
const { httpServer, wsServer } = await startServer(httpAdapter, port, options.https, logger);
|
|
5779
6558
|
const authorizationService = createAuthorizationService(authorizationConfig, documentPermissionService, createGetParentIdsFn(reactorClient));
|
|
5780
6559
|
logger.info(`Authorization service initialized (policy: ${authorizationConfig.policy})`);
|
|
6560
|
+
const attachmentAccess = new AttachmentAccessService(createCanonicalDocumentIdResolver(reactorClient), authorizationService, attachmentReferenceIndex.store, attachmentReferenceProjection);
|
|
5781
6561
|
const coreSubgraphs = DefaultCoreSubgraphs.slice();
|
|
5782
6562
|
coreSubgraphs.push(ReactorSubgraph);
|
|
5783
6563
|
if (documentPermissionService) {
|
|
@@ -5787,7 +6567,7 @@ async function _setupAPI(reactorClient, syncManager, reactorProcessorManager, ht
|
|
|
5787
6567
|
const graphqlManager = await setupGraphQLManager(httpAdapter, authFetchMiddleware, httpServer, wsServer, reactorClient, relationalDb, analyticsStore, syncManager, {
|
|
5788
6568
|
extended: subgraphs,
|
|
5789
6569
|
core: coreSubgraphs
|
|
5790
|
-
}, logger.child(["graphql-manager"]), authorizationService, authService, documentPermissionService, options.enableDocumentModelSubgraphs, port, reactorDriveClient);
|
|
6570
|
+
}, logger.child(["graphql-manager"]), authorizationService, authService, documentPermissionService, options.enableDocumentModelSubgraphs, port, reactorDriveClient, syncServingGate);
|
|
5791
6571
|
setupEventListeners(packages, graphqlManager, reactorProcessorManager, hostModule, documentModelRegistry);
|
|
5792
6572
|
if (mcpServerEnabled) {
|
|
5793
6573
|
await setupMcpServer({
|
|
@@ -5802,6 +6582,8 @@ async function _setupAPI(reactorClient, syncManager, reactorProcessorManager, ht
|
|
|
5802
6582
|
graphqlManager,
|
|
5803
6583
|
packages,
|
|
5804
6584
|
attachments,
|
|
6585
|
+
attachmentReferenceIndex,
|
|
6586
|
+
attachmentAccess,
|
|
5805
6587
|
authService,
|
|
5806
6588
|
dispose: buildApiDispose({
|
|
5807
6589
|
graphqlManager,
|
|
@@ -5850,9 +6632,15 @@ function buildApiDispose(args) {
|
|
|
5850
6632
|
};
|
|
5851
6633
|
}
|
|
5852
6634
|
async function initializeAndStartAPI(clientInitializer, options, processorApp) {
|
|
5853
|
-
const { port, httpAdapter, authFetchMiddleware, authService, relationalDb, analyticsStore, documentPermissionService, authorizationConfig, attachments, packages, dbClosers, readiness } = await _setupCommonInfrastructure(options);
|
|
5854
|
-
const { documentModels, processors, subgraphs } = await packages.init();
|
|
5855
|
-
const { module: reactorClientModule, reactorDriveClient
|
|
6635
|
+
const { port, httpAdapter, authFetchMiddleware, authService, relationalDb, analyticsStore, documentPermissionService, authorizationConfig, attachments, attachmentReferenceIndex, packages, dbClosers, readiness } = await _setupCommonInfrastructure(options);
|
|
6636
|
+
const { documentModels, upgradeManifests, processors, subgraphs } = await packages.init();
|
|
6637
|
+
const { module: reactorClientModule, reactorDriveClient, attachmentReferenceProjection = {
|
|
6638
|
+
status: "unavailable",
|
|
6639
|
+
reason: "initializer-did-not-report"
|
|
6640
|
+
} } = await clientInitializer(documentModels, {
|
|
6641
|
+
attachmentReferenceWriter: attachmentReferenceIndex.store,
|
|
6642
|
+
upgradeManifests
|
|
6643
|
+
});
|
|
5856
6644
|
const reactorClient = reactorClientModule.client;
|
|
5857
6645
|
const syncManager = reactorClientModule.reactorModule?.syncModule?.syncManager;
|
|
5858
6646
|
if (!syncManager) throw new Error("SyncManager not available from InProcessReactorClientModule");
|
|
@@ -5861,11 +6649,12 @@ async function initializeAndStartAPI(clientInitializer, options, processorApp) {
|
|
|
5861
6649
|
const documentModelRegistry = reactorClientModule.reactorModule?.documentModelRegistry;
|
|
5862
6650
|
if (!documentModelRegistry) throw new Error("DocumentModelRegistry not available from InProcessReactorClientModule");
|
|
5863
6651
|
return {
|
|
5864
|
-
...await _setupAPI(reactorClient, syncManager, reactorProcessorManager, httpAdapter, authFetchMiddleware, authService, port, packages, relationalDb, analyticsStore, documentPermissionService, processors, subgraphs, options, processorApp, (reactorClientModule.reactorModule?.readModelCoordinator)?.readModels ?? [], attachments, authorizationConfig, documentModelRegistry, dbClosers, reactorDriveClient),
|
|
6652
|
+
...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)),
|
|
5865
6653
|
client: reactorClient,
|
|
5866
6654
|
syncManager,
|
|
5867
6655
|
documentModelRegistry,
|
|
5868
|
-
readiness
|
|
6656
|
+
readiness,
|
|
6657
|
+
attachmentReferenceProjection
|
|
5869
6658
|
};
|
|
5870
6659
|
}
|
|
5871
6660
|
//#endregion
|
|
@@ -5969,7 +6758,7 @@ var PackageManagementService = class {
|
|
|
5969
6758
|
}
|
|
5970
6759
|
};
|
|
5971
6760
|
//#endregion
|
|
5972
|
-
export { ADMIN_USERS, ActionContextInputSchema, ActionInputSchema, AddRelationshipDocument, AnalyticsSubgraph, AuthService, AuthSubgraph, AuthorizationPolicy, AuthorizedDocumentHandle, BaseSubgraph, ChannelMetaInputSchema, CreateDocumentDocument, CreateEmptyDocumentDocument, DeleteDocumentDocument, DeleteDocumentsDocument, DocumentChangeType, DocumentChangeTypeSchema, DocumentChangesDocument, DocumentOperationsFilterInputSchema, DocumentPermissionService, FindDocumentsDocument, GetDocumentDocument, GetDocumentIncomingRelationshipsDocument, GetDocumentModelsDocument, GetDocumentOperationsDocument, GetDocumentOutgoingRelationshipsDocument, GetDocumentWithOperationsDocument, GetJobStatusDocument, GraphQLManager, HttpDocumentModelLoader, HttpPackageLoader, ImportPackageLoader, InMemoryPackageStorage, JobChangesDocument, MoveRelationshipDocument, MutateDocumentAsyncDocument, MutateDocumentDocument, OperationContextInputSchema, OperationInputSchema, OperationWithContextInputSchema, OperationsFilterInputSchema, PackageManagementService, PackageManager, PackagesSubgraph, PagingInputSchema, PhDocumentFieldsFragmentDoc, PollSyncEnvelopesDocument, PropagationMode, PropagationModeSchema, PushSyncEnvelopesDocument, ReactorSignerAppInputSchema, ReactorSignerInputSchema, ReactorSignerUserInputSchema, ReactorSubgraph, RemoteCursorInputSchema, RemoteFilterInputSchema, RemoveRelationshipDocument, RenameDocumentDocument, SearchFilterInputSchema, SetPreferredEditorDocument, SyncEnvelopeInputSchema, SyncEnvelopeType, SyncEnvelopeTypeSchema, SystemSubgraph, TouchChannelDocument, TouchChannelInputSchema, ViewFilterInputSchema, assertAuthRequiredForDocumentPermissions, assertSkipCredentialVerificationAllowed, buildGraphQlDocument, buildGraphQlDriveDocument, buildGraphqlOperation, buildGraphqlOperations, buildSubgraphSchemaModule, createAuthFetchMiddleware, createGatewayAdapter, createHttpAdapter, createMergedSchema, createReactorGraphQLClient, createSchema, definedNonNullAnySchema, driveIdFromUrl, extractSubgraphsFromModule, generateDocumentModelSchema, getAuthContext, getDbClient, getDocumentModelSchemaName, getDocumentModelTypeDefs, getGitHash, getGitUrl, getSdk, getUniqueDocumentModels, getVersion, initAnalyticsStoreSql, initializeAndStartAPI, isDefinedNonNullAny, isExpectedLoaderMiss, isSubgraphClass, parseDriveUrl, renderGraphqlPlayground };
|
|
6761
|
+
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 };
|
|
5973
6762
|
|
|
5974
6763
|
//# sourceMappingURL=index.mjs.map
|
|
5975
|
-
//# debugId=
|
|
6764
|
+
//# debugId=0d1c78be-0026-5af3-b8dd-9282b69fde6d
|