@powerhousedao/reactor-api 6.2.2-dev.9 → 6.2.2-staging.0
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/adapter-http-express-CgVPgEMm.mjs +225 -0
- package/dist/adapter-http-express-CgVPgEMm.mjs.map +1 -0
- package/dist/{adapter-http-fastify-B248RJtm.mjs → adapter-http-fastify-D98iRljY.mjs} +38 -26
- package/dist/adapter-http-fastify-D98iRljY.mjs.map +1 -0
- package/dist/index.d.mts +584 -50
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1258 -174
- package/dist/index.mjs.map +1 -1
- package/dist/path-normalize-DWWFLiy6.mjs +23 -0
- package/dist/path-normalize-DWWFLiy6.mjs.map +1 -0
- 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 +33 -8
- package/dist/src/packages/vite-loader.mjs.map +1 -1
- package/dist/{types-DnHgkahK.d.mts → types-Bz2niABC.d.mts} +13 -2
- package/dist/types-Bz2niABC.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 +22 -19
- package/dist/adapter-gateway-apollo-CxV_hMTv.mjs.map +0 -1
- package/dist/adapter-http-express-D5OpUIbV.mjs +0 -137
- package/dist/adapter-http-express-D5OpUIbV.mjs.map +0 -1
- package/dist/adapter-http-fastify-B248RJtm.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,32 +1,34 @@
|
|
|
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]="f43e08dc-7398-57dd-8824-db5d97ccc6ca")}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";
|
|
20
|
+
import lzString from "lz-string";
|
|
20
21
|
import { PubSub, withFilter } from "graphql-subscriptions";
|
|
21
22
|
import dotenv from "dotenv";
|
|
22
23
|
import { buildTreeUrl } from "@powerhousedao/shared";
|
|
23
24
|
import { getConfig } from "@powerhousedao/config/node";
|
|
24
25
|
import { reactorDriveDocumentModelModule } from "@powerhousedao/reactor-drive";
|
|
26
|
+
import { ReactorGroupV1 } from "@powerhousedao/reactor-group";
|
|
25
27
|
import { driveDocumentModelModule } from "@powerhousedao/shared/document-drive";
|
|
26
28
|
import EventEmitter from "node:events";
|
|
27
29
|
import fs, { watchFile } from "node:fs";
|
|
28
30
|
import { PostgresAnalyticsStore } from "@powerhousedao/analytics-engine-pg";
|
|
29
|
-
import { AttachmentBuilder } from "@powerhousedao/reactor-attachments";
|
|
31
|
+
import { AttachmentBuilder, AttachmentReferenceIndexBuilder, createRef, createS3AttachmentBackend, parseAttachmentStorageConfig, parseRef } from "@powerhousedao/reactor-attachments";
|
|
30
32
|
import { createAttachmentClient } from "@powerhousedao/reactor-attachments/client";
|
|
31
33
|
import { setupMcpServer } from "@powerhousedao/reactor-mcp";
|
|
32
34
|
import { mkdir } from "node:fs/promises";
|
|
@@ -34,7 +36,7 @@ import { tmpdir } from "node:os";
|
|
|
34
36
|
import { WebSocketServer } from "ws";
|
|
35
37
|
import { createRelationalDb } from "@powerhousedao/shared/processors";
|
|
36
38
|
import { Kysely, Migrator, sql } from "kysely";
|
|
37
|
-
import { fetchDelegationCredential, verifyAuthBearerToken } from "@renown/sdk";
|
|
39
|
+
import { fetchDelegationCredential, resolveSwitchboardEndpoint, verifyAuthBearerToken } from "@renown/sdk";
|
|
38
40
|
import { PGlite } from "@electric-sql/pglite";
|
|
39
41
|
import { AtomicNodeFs } from "@powerhousedao/pglite-fs";
|
|
40
42
|
import knex from "knex";
|
|
@@ -305,7 +307,7 @@ function createAuthFetchMiddleware(authService) {
|
|
|
305
307
|
async function createGatewayAdapter(type, logger) {
|
|
306
308
|
switch (type) {
|
|
307
309
|
case "apollo": {
|
|
308
|
-
const { ApolloGatewayAdapter } = await import("./adapter-gateway-apollo-
|
|
310
|
+
const { ApolloGatewayAdapter } = await import("./adapter-gateway-apollo-DdamNLPT.mjs");
|
|
309
311
|
return new ApolloGatewayAdapter(logger);
|
|
310
312
|
}
|
|
311
313
|
case "mercurius": {
|
|
@@ -317,11 +319,11 @@ async function createGatewayAdapter(type, logger) {
|
|
|
317
319
|
async function createHttpAdapter(type) {
|
|
318
320
|
switch (type) {
|
|
319
321
|
case "express": {
|
|
320
|
-
const { createExpressHttpAdapter } = await import("./adapter-http-express-
|
|
322
|
+
const { createExpressHttpAdapter } = await import("./adapter-http-express-CgVPgEMm.mjs");
|
|
321
323
|
return createExpressHttpAdapter();
|
|
322
324
|
}
|
|
323
325
|
case "fastify": {
|
|
324
|
-
const { createFastifyHttpAdapter } = await import("./adapter-http-fastify-
|
|
326
|
+
const { createFastifyHttpAdapter } = await import("./adapter-http-fastify-D98iRljY.mjs");
|
|
325
327
|
return createFastifyHttpAdapter();
|
|
326
328
|
}
|
|
327
329
|
}
|
|
@@ -396,7 +398,8 @@ const dedupeTypeDefinitions = (doc) => {
|
|
|
396
398
|
const buildSubgraphSchemaModule = (documentModels, resolvers, typeDefs) => {
|
|
397
399
|
const newResolvers = {
|
|
398
400
|
...resolvers,
|
|
399
|
-
JSONObject: GraphQLJSONObject
|
|
401
|
+
JSONObject: GraphQLJSONObject,
|
|
402
|
+
Unknown: GraphQLJSON
|
|
400
403
|
};
|
|
401
404
|
return {
|
|
402
405
|
typeDefs: getDocumentModelTypeDefs(documentModels, typeDefs),
|
|
@@ -404,7 +407,7 @@ const buildSubgraphSchemaModule = (documentModels, resolvers, typeDefs) => {
|
|
|
404
407
|
};
|
|
405
408
|
};
|
|
406
409
|
const createSchema = (documentModels, resolvers, typeDefs) => {
|
|
407
|
-
return buildSubgraphSchema(buildSubgraphSchemaModule(documentModels, resolvers, typeDefs));
|
|
410
|
+
return buildSubgraphSchema([buildSubgraphSchemaModule(documentModels, resolvers, typeDefs)]);
|
|
408
411
|
};
|
|
409
412
|
/**
|
|
410
413
|
* Create a merged GraphQL schema from multiple subgraph modules.
|
|
@@ -460,6 +463,10 @@ const getDocumentModelTypeDefs = (documentModels, typeDefs$1) => {
|
|
|
460
463
|
});
|
|
461
464
|
return dedupeTypeDefinitions(gql`
|
|
462
465
|
scalar JSONObject
|
|
466
|
+
scalar AttachmentRef
|
|
467
|
+
# Codegen scalars not in the document-engineering set.
|
|
468
|
+
scalar Unknown
|
|
469
|
+
scalar Address
|
|
463
470
|
${typeDefs.join("\n").replaceAll(";", "")}
|
|
464
471
|
|
|
465
472
|
type PHOperationContext {
|
|
@@ -878,6 +885,7 @@ function generateNewApiSchema(documentName, specification, _stateSchemaTypes, pr
|
|
|
878
885
|
return gql`
|
|
879
886
|
scalar DateTime
|
|
880
887
|
scalar JSONObject
|
|
888
|
+
scalar AttachmentRef
|
|
881
889
|
|
|
882
890
|
${revisionType}
|
|
883
891
|
|
|
@@ -926,6 +934,11 @@ function generateNewApiSchema(documentName, specification, _stateSchemaTypes, pr
|
|
|
926
934
|
}
|
|
927
935
|
//#endregion
|
|
928
936
|
//#region src/graphql/reactor/gen/graphql.ts
|
|
937
|
+
let AuthDecision = /* @__PURE__ */ function(AuthDecision) {
|
|
938
|
+
AuthDecision["Allow"] = "ALLOW";
|
|
939
|
+
AuthDecision["Deny"] = "DENY";
|
|
940
|
+
return AuthDecision;
|
|
941
|
+
}({});
|
|
929
942
|
let DocumentChangeType = /* @__PURE__ */ function(DocumentChangeType) {
|
|
930
943
|
DocumentChangeType["ChildAdded"] = "CHILD_ADDED";
|
|
931
944
|
DocumentChangeType["ChildRemoved"] = "CHILD_REMOVED";
|
|
@@ -948,11 +961,24 @@ let SyncEnvelopeType = /* @__PURE__ */ function(SyncEnvelopeType) {
|
|
|
948
961
|
}({});
|
|
949
962
|
const isDefinedNonNullAny = (v) => v !== void 0 && v !== null;
|
|
950
963
|
const definedNonNullAnySchema = z$1.any().refine((v) => isDefinedNonNullAny(v));
|
|
964
|
+
const AuthDecisionSchema = z$1.enum(AuthDecision);
|
|
951
965
|
const DocumentChangeTypeSchema = z$1.enum(DocumentChangeType);
|
|
952
966
|
const PropagationModeSchema = z$1.enum(PropagationMode);
|
|
953
967
|
const SyncEnvelopeTypeSchema = z$1.enum(SyncEnvelopeType);
|
|
968
|
+
function ActionCandidateInputSchema() {
|
|
969
|
+
return z$1.object({
|
|
970
|
+
input: z$1.custom((v) => v != null).nullish(),
|
|
971
|
+
scope: z$1.string(),
|
|
972
|
+
type: z$1.string()
|
|
973
|
+
});
|
|
974
|
+
}
|
|
954
975
|
function ActionContextInputSchema() {
|
|
955
|
-
return z$1.object({
|
|
976
|
+
return z$1.object({
|
|
977
|
+
nonce: z$1.string().nullish(),
|
|
978
|
+
prevOpHash: z$1.string().nullish(),
|
|
979
|
+
prevOpIndex: z$1.number().nullish(),
|
|
980
|
+
signer: z$1.lazy(() => ReactorSignerInputSchema().nullish())
|
|
981
|
+
});
|
|
956
982
|
}
|
|
957
983
|
function ActionInputSchema() {
|
|
958
984
|
return z$1.object({
|
|
@@ -989,6 +1015,7 @@ function OperationContextInputSchema() {
|
|
|
989
1015
|
function OperationInputSchema() {
|
|
990
1016
|
return z$1.object({
|
|
991
1017
|
action: z$1.lazy(() => ActionInputSchema()),
|
|
1018
|
+
deniedReason: z$1.string().nullish(),
|
|
992
1019
|
error: z$1.string().nullish(),
|
|
993
1020
|
hash: z$1.string(),
|
|
994
1021
|
id: z$1.string().nullish(),
|
|
@@ -1147,6 +1174,7 @@ const GetDocumentWithOperationsDocument = gql`
|
|
|
1147
1174
|
hash
|
|
1148
1175
|
skip
|
|
1149
1176
|
error
|
|
1177
|
+
deniedReason
|
|
1150
1178
|
id
|
|
1151
1179
|
action {
|
|
1152
1180
|
id
|
|
@@ -1259,6 +1287,7 @@ const GetDocumentOperationsDocument = gql`
|
|
|
1259
1287
|
hash
|
|
1260
1288
|
skip
|
|
1261
1289
|
error
|
|
1290
|
+
deniedReason
|
|
1262
1291
|
id
|
|
1263
1292
|
action {
|
|
1264
1293
|
id
|
|
@@ -1301,6 +1330,28 @@ const GetJobStatusDocument = gql`
|
|
|
1301
1330
|
}
|
|
1302
1331
|
}
|
|
1303
1332
|
`;
|
|
1333
|
+
const EvaluateActionsDocument = gql`
|
|
1334
|
+
query EvaluateActions(
|
|
1335
|
+
$documentIdentifier: String!
|
|
1336
|
+
$branch: String
|
|
1337
|
+
$candidates: [ActionCandidateInput!]!
|
|
1338
|
+
) {
|
|
1339
|
+
evaluateActions(
|
|
1340
|
+
documentIdentifier: $documentIdentifier
|
|
1341
|
+
branch: $branch
|
|
1342
|
+
candidates: $candidates
|
|
1343
|
+
) {
|
|
1344
|
+
evaluations {
|
|
1345
|
+
decision
|
|
1346
|
+
reason
|
|
1347
|
+
}
|
|
1348
|
+
allAllowed
|
|
1349
|
+
anyAllowed
|
|
1350
|
+
allDenied
|
|
1351
|
+
anyDenied
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
`;
|
|
1304
1355
|
const CreateDocumentDocument = gql`
|
|
1305
1356
|
mutation CreateDocument($document: JSONObject!, $parentIdentifier: String) {
|
|
1306
1357
|
createDocument(document: $document, parentIdentifier: $parentIdentifier) {
|
|
@@ -1326,13 +1377,13 @@ const CreateEmptyDocumentDocument = gql`
|
|
|
1326
1377
|
const MutateDocumentDocument = gql`
|
|
1327
1378
|
mutation MutateDocument(
|
|
1328
1379
|
$documentIdentifier: String!
|
|
1329
|
-
$actions: [
|
|
1330
|
-
$
|
|
1380
|
+
$actions: [ActionInput!]!
|
|
1381
|
+
$branch: String
|
|
1331
1382
|
) {
|
|
1332
|
-
mutateDocument(
|
|
1383
|
+
mutateDocument: execute(
|
|
1333
1384
|
documentIdentifier: $documentIdentifier
|
|
1334
1385
|
actions: $actions
|
|
1335
|
-
|
|
1386
|
+
branch: $branch
|
|
1336
1387
|
) {
|
|
1337
1388
|
...PHDocumentFields
|
|
1338
1389
|
}
|
|
@@ -1342,14 +1393,21 @@ const MutateDocumentDocument = gql`
|
|
|
1342
1393
|
const MutateDocumentAsyncDocument = gql`
|
|
1343
1394
|
mutation MutateDocumentAsync(
|
|
1344
1395
|
$documentIdentifier: String!
|
|
1345
|
-
$actions: [
|
|
1346
|
-
$
|
|
1396
|
+
$actions: [ActionInput!]!
|
|
1397
|
+
$branch: String
|
|
1347
1398
|
) {
|
|
1348
|
-
mutateDocumentAsync(
|
|
1399
|
+
mutateDocumentAsync: executeAsync(
|
|
1349
1400
|
documentIdentifier: $documentIdentifier
|
|
1350
1401
|
actions: $actions
|
|
1351
|
-
|
|
1352
|
-
)
|
|
1402
|
+
branch: $branch
|
|
1403
|
+
) {
|
|
1404
|
+
id
|
|
1405
|
+
status
|
|
1406
|
+
result
|
|
1407
|
+
error
|
|
1408
|
+
createdAt
|
|
1409
|
+
completedAt
|
|
1410
|
+
}
|
|
1353
1411
|
}
|
|
1354
1412
|
`;
|
|
1355
1413
|
const RenameDocumentDocument = gql`
|
|
@@ -1509,6 +1567,7 @@ const PollSyncEnvelopesDocument = gql`
|
|
|
1509
1567
|
hash
|
|
1510
1568
|
skip
|
|
1511
1569
|
error
|
|
1570
|
+
deniedReason
|
|
1512
1571
|
id
|
|
1513
1572
|
action {
|
|
1514
1573
|
id
|
|
@@ -1551,6 +1610,7 @@ const PollSyncEnvelopesDocument = gql`
|
|
|
1551
1610
|
deadLetters {
|
|
1552
1611
|
documentId
|
|
1553
1612
|
error
|
|
1613
|
+
errorType
|
|
1554
1614
|
}
|
|
1555
1615
|
hasMore
|
|
1556
1616
|
}
|
|
@@ -1595,6 +1655,9 @@ function getSdk(requester) {
|
|
|
1595
1655
|
GetJobStatus(variables, options) {
|
|
1596
1656
|
return requester(GetJobStatusDocument, variables, options);
|
|
1597
1657
|
},
|
|
1658
|
+
EvaluateActions(variables, options) {
|
|
1659
|
+
return requester(EvaluateActionsDocument, variables, options);
|
|
1660
|
+
},
|
|
1598
1661
|
CreateDocument(variables, options) {
|
|
1599
1662
|
return requester(CreateDocumentDocument, variables, options);
|
|
1600
1663
|
},
|
|
@@ -1745,28 +1808,132 @@ function toReactorPropagationMode(gqlMode) {
|
|
|
1745
1808
|
}
|
|
1746
1809
|
}
|
|
1747
1810
|
/**
|
|
1811
|
+
* Maps one predicted verdict to its GraphQL shape. A refusal's reason is the
|
|
1812
|
+
* consensus string the reactor would record for it, carried through verbatim so
|
|
1813
|
+
* a client can tell a deletion refusal from a missing grant; an allow has none.
|
|
1814
|
+
*/
|
|
1815
|
+
function toGqlActionEvaluation(evaluation) {
|
|
1816
|
+
return evaluation.decision === "allow" ? {
|
|
1817
|
+
decision: AuthDecision.Allow,
|
|
1818
|
+
reason: null
|
|
1819
|
+
} : {
|
|
1820
|
+
decision: AuthDecision.Deny,
|
|
1821
|
+
reason: evaluation.reason
|
|
1822
|
+
};
|
|
1823
|
+
}
|
|
1824
|
+
/**
|
|
1748
1825
|
* Converts readonly arrays to mutable arrays for ReactorClient
|
|
1749
1826
|
*/
|
|
1750
1827
|
function toMutableArray(arr) {
|
|
1751
1828
|
return arr ? [...arr] : void 0;
|
|
1752
1829
|
}
|
|
1753
1830
|
/**
|
|
1754
|
-
* Validates that
|
|
1831
|
+
* Validates that an incoming value has the structure of an Action.
|
|
1832
|
+
*
|
|
1833
|
+
* This is the whole guard on the way in. `mutateDocument` takes untyped JSON, so
|
|
1834
|
+
* nothing has checked these fields before a resolver runs, and the id in
|
|
1835
|
+
* particular has to be checked here: it is hashed into the operation id and
|
|
1836
|
+
* replay dedupes by it, so an action without one is stored under an operation id
|
|
1837
|
+
* shared by every id-less operation on the same document, scope and branch.
|
|
1838
|
+
*
|
|
1839
|
+
* The storage constraint would refuse such a row anyway, but only after a job
|
|
1840
|
+
* has been enqueued and failed. Refusing here answers the caller instead.
|
|
1755
1841
|
*/
|
|
1756
1842
|
function validateActionStructure(obj) {
|
|
1757
1843
|
if (!obj || typeof obj !== "object") return false;
|
|
1758
1844
|
const action = obj;
|
|
1845
|
+
if (typeof action.id !== "string" || !action.id) return false;
|
|
1759
1846
|
if (typeof action.type !== "string" || !action.type) return false;
|
|
1760
1847
|
if (typeof action.scope !== "string" || !action.scope) return false;
|
|
1848
|
+
if (typeof action.timestampUtcMs !== "string" || !action.timestampUtcMs) return false;
|
|
1761
1849
|
if (!("input" in action)) return false;
|
|
1762
1850
|
return true;
|
|
1763
1851
|
}
|
|
1764
1852
|
/**
|
|
1853
|
+
* Restores an action's signatures to the tuples verification reads.
|
|
1854
|
+
*
|
|
1855
|
+
* A signature is a five-param tuple, and GraphQL declares `signatures` as a
|
|
1856
|
+
* list of strings, so a client joins each one for transport. The sync path
|
|
1857
|
+
* already splits them on arrival; this is the same step for the mutation path,
|
|
1858
|
+
* which had none - an action submitted there kept whatever shape it arrived in,
|
|
1859
|
+
* so the same signature was persisted as a tuple or as one joined string
|
|
1860
|
+
* depending on which door it came through.
|
|
1861
|
+
*
|
|
1862
|
+
* A tuple passes through untouched, so an in-process caller handing over real
|
|
1863
|
+
* signatures is unaffected.
|
|
1864
|
+
*/
|
|
1865
|
+
function withDeserializedSignatures(action) {
|
|
1866
|
+
const signer = action.context?.signer;
|
|
1867
|
+
if (!signer?.signatures || signer.signatures.length === 0) return action;
|
|
1868
|
+
return {
|
|
1869
|
+
...action,
|
|
1870
|
+
context: {
|
|
1871
|
+
...action.context,
|
|
1872
|
+
signer: {
|
|
1873
|
+
...signer,
|
|
1874
|
+
signatures: signer.signatures.map(deserializeSignature)
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
};
|
|
1878
|
+
}
|
|
1879
|
+
/**
|
|
1880
|
+
* Converts actions the schema has already coerced into the actions the reactor
|
|
1881
|
+
* applies.
|
|
1882
|
+
*
|
|
1883
|
+
* The one conversion the schema cannot express is the signature: GraphQL
|
|
1884
|
+
* declares `signatures` as a list of strings, not of lists, so each arrives
|
|
1885
|
+
* joined and has to be read back as the tuple verification indexes into.
|
|
1886
|
+
*
|
|
1887
|
+
* Nothing is validated here. `ActionInput` states every field the reactor needs,
|
|
1888
|
+
* so anything that reached this point has them - unlike the untyped path, which
|
|
1889
|
+
* has to check by hand.
|
|
1890
|
+
*/
|
|
1891
|
+
function toSubmittableActions(actions) {
|
|
1892
|
+
return actions.map((action) => {
|
|
1893
|
+
const context = toSubmittableContext(action.context);
|
|
1894
|
+
const submittable = {
|
|
1895
|
+
id: action.id,
|
|
1896
|
+
type: action.type,
|
|
1897
|
+
timestampUtcMs: action.timestampUtcMs,
|
|
1898
|
+
input: action.input,
|
|
1899
|
+
scope: action.scope
|
|
1900
|
+
};
|
|
1901
|
+
return context ? {
|
|
1902
|
+
...submittable,
|
|
1903
|
+
context
|
|
1904
|
+
} : submittable;
|
|
1905
|
+
});
|
|
1906
|
+
}
|
|
1907
|
+
/**
|
|
1908
|
+
* `ReactorSignerInput` leaves `user` and `app` optional where `ActionSigner`
|
|
1909
|
+
* requires them, so an incomplete signer cannot be represented. It is carried
|
|
1910
|
+
* through as it arrived rather than filled in: the signature verifier is what
|
|
1911
|
+
* should refuse it, and inventing either field here would be inventing the part
|
|
1912
|
+
* that says who signed.
|
|
1913
|
+
*/
|
|
1914
|
+
function toSubmittableContext(context) {
|
|
1915
|
+
if (!context) return;
|
|
1916
|
+
const submittable = {};
|
|
1917
|
+
const prevOpIndex = fromInputMaybe(context.prevOpIndex);
|
|
1918
|
+
if (prevOpIndex !== void 0) submittable.prevOpIndex = prevOpIndex;
|
|
1919
|
+
const prevOpHash = fromInputMaybe(context.prevOpHash);
|
|
1920
|
+
if (prevOpHash !== void 0) submittable.prevOpHash = prevOpHash;
|
|
1921
|
+
const nonce = fromInputMaybe(context.nonce);
|
|
1922
|
+
if (nonce !== void 0) submittable.nonce = nonce;
|
|
1923
|
+
const signer = fromInputMaybe(context.signer);
|
|
1924
|
+
if (signer) submittable.signer = {
|
|
1925
|
+
user: fromInputMaybe(signer.user),
|
|
1926
|
+
app: fromInputMaybe(signer.app),
|
|
1927
|
+
signatures: signer.signatures.map(deserializeSignature)
|
|
1928
|
+
};
|
|
1929
|
+
return Object.keys(submittable).length > 0 ? submittable : void 0;
|
|
1930
|
+
}
|
|
1931
|
+
/**
|
|
1765
1932
|
* Converts a JSONObject to an Action, validating basic structure
|
|
1766
1933
|
*/
|
|
1767
1934
|
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;
|
|
1935
|
+
if (!validateActionStructure(obj)) throw new GraphQLError("Invalid action structure. Actions must have: id (string), type (string), scope (string), timestampUtcMs (string), and input (any)");
|
|
1936
|
+
return withDeserializedSignatures(obj);
|
|
1770
1937
|
}
|
|
1771
1938
|
/**
|
|
1772
1939
|
* Validates a list of actions by converting them from JSON and checking structure
|
|
@@ -1780,10 +1947,57 @@ function validateActions(actions) {
|
|
|
1780
1947
|
}
|
|
1781
1948
|
return convertedActions;
|
|
1782
1949
|
}
|
|
1950
|
+
/** The operation fields the schema declares non-null. */
|
|
1951
|
+
const REQUIRED_OPERATION_FIELDS = [
|
|
1952
|
+
"index",
|
|
1953
|
+
"timestampUtcMs",
|
|
1954
|
+
"hash",
|
|
1955
|
+
"skip"
|
|
1956
|
+
];
|
|
1957
|
+
/** The action fields the schema declares non-null. */
|
|
1958
|
+
const REQUIRED_ACTION_FIELDS = [
|
|
1959
|
+
"id",
|
|
1960
|
+
"type",
|
|
1961
|
+
"timestampUtcMs",
|
|
1962
|
+
"input",
|
|
1963
|
+
"scope"
|
|
1964
|
+
];
|
|
1965
|
+
/**
|
|
1966
|
+
* Names an operation well enough to find the row it came from, without assuming
|
|
1967
|
+
* the fields that might be the missing ones.
|
|
1968
|
+
*/
|
|
1969
|
+
function describeOperation(stored) {
|
|
1970
|
+
const parts = [typeof stored.id === "string" && stored.id ? `operation ${stored.id}` : "operation with no id"];
|
|
1971
|
+
if (typeof stored.index === "number") parts.push(`index ${stored.index}`);
|
|
1972
|
+
const scope = stored.action?.scope;
|
|
1973
|
+
if (typeof scope === "string" && scope) parts.push(`scope ${scope}`);
|
|
1974
|
+
return parts.join(", ");
|
|
1975
|
+
}
|
|
1976
|
+
/**
|
|
1977
|
+
* Refuses an operation the schema cannot represent, naming the field.
|
|
1978
|
+
*
|
|
1979
|
+
* The alternative is what execution does unaided: nullify the field, bubble that
|
|
1980
|
+
* up the non-null chain until it takes out the envelope's whole operation list,
|
|
1981
|
+
* and report a bare "Cannot return null for non-nullable field" with no way to
|
|
1982
|
+
* tell which of a hundred operations was at fault. Checked before the response
|
|
1983
|
+
* is built so the failure is typed and attributable.
|
|
1984
|
+
*
|
|
1985
|
+
* Presence is all that is checked. The schema's own serialization still rejects
|
|
1986
|
+
* a value of the wrong type, and inventing further rules here would put a second
|
|
1987
|
+
* contract beside the one the schema already states.
|
|
1988
|
+
*/
|
|
1989
|
+
function assertServable(operation) {
|
|
1990
|
+
const stored = operation;
|
|
1991
|
+
for (const field of REQUIRED_OPERATION_FIELDS) if (stored[field] === void 0 || stored[field] === null) throw new MalformedStoredOperationError(`${describeOperation(stored)} has no ${field}`);
|
|
1992
|
+
const action = stored.action;
|
|
1993
|
+
if (action === void 0 || action === null) throw new MalformedStoredOperationError(`${describeOperation(stored)} has no action`);
|
|
1994
|
+
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}`);
|
|
1995
|
+
}
|
|
1783
1996
|
/**
|
|
1784
1997
|
* Transforms an operation to serialize signatures from tuples to strings for GraphQL compatibility.
|
|
1785
1998
|
*/
|
|
1786
1999
|
function serializeOperationForGraphQL(operation) {
|
|
2000
|
+
assertServable(operation);
|
|
1787
2001
|
const signer = operation.action.context?.signer;
|
|
1788
2002
|
if (!signer?.signatures) return operation;
|
|
1789
2003
|
return {
|
|
@@ -1794,7 +2008,7 @@ function serializeOperationForGraphQL(operation) {
|
|
|
1794
2008
|
...operation.action.context,
|
|
1795
2009
|
signer: {
|
|
1796
2010
|
...signer,
|
|
1797
|
-
signatures: signer.signatures.map(
|
|
2011
|
+
signatures: signer.signatures.map(serializeSignature)
|
|
1798
2012
|
}
|
|
1799
2013
|
}
|
|
1800
2014
|
}
|
|
@@ -1904,9 +2118,10 @@ async function documentModels(reactorClient, args) {
|
|
|
1904
2118
|
throw new GraphQLError(`Failed to convert document models to GraphQL: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
1905
2119
|
}
|
|
1906
2120
|
}
|
|
1907
|
-
async function document(reactorClient, args) {
|
|
1908
|
-
let view;
|
|
2121
|
+
async function document(reactorClient, args, subject) {
|
|
2122
|
+
let view = subject ? { subject } : void 0;
|
|
1909
2123
|
if (args.view) view = {
|
|
2124
|
+
subject,
|
|
1910
2125
|
branch: fromInputMaybe(args.view.branch),
|
|
1911
2126
|
scopes: toMutableArray(fromInputMaybe(args.view.scopes))
|
|
1912
2127
|
};
|
|
@@ -1931,9 +2146,10 @@ async function document(reactorClient, args) {
|
|
|
1931
2146
|
throw new GraphQLError(`Failed to convert document to GraphQL: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
1932
2147
|
}
|
|
1933
2148
|
}
|
|
1934
|
-
async function documentOutgoingRelationships(reactorClient, args) {
|
|
1935
|
-
let view;
|
|
2149
|
+
async function documentOutgoingRelationships(reactorClient, args, subject) {
|
|
2150
|
+
let view = subject ? { subject } : void 0;
|
|
1936
2151
|
if (args.view) view = {
|
|
2152
|
+
subject,
|
|
1937
2153
|
branch: fromInputMaybe(args.view.branch),
|
|
1938
2154
|
scopes: toMutableArray(fromInputMaybe(args.view.scopes))
|
|
1939
2155
|
};
|
|
@@ -1958,9 +2174,10 @@ async function documentOutgoingRelationships(reactorClient, args) {
|
|
|
1958
2174
|
throw new GraphQLError(`Failed to convert outgoing relationships to GraphQL: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
1959
2175
|
}
|
|
1960
2176
|
}
|
|
1961
|
-
async function documentIncomingRelationships(reactorClient, args) {
|
|
1962
|
-
let view;
|
|
2177
|
+
async function documentIncomingRelationships(reactorClient, args, subject) {
|
|
2178
|
+
let view = subject ? { subject } : void 0;
|
|
1963
2179
|
if (args.view) view = {
|
|
2180
|
+
subject,
|
|
1964
2181
|
branch: fromInputMaybe(args.view.branch),
|
|
1965
2182
|
scopes: toMutableArray(fromInputMaybe(args.view.scopes))
|
|
1966
2183
|
};
|
|
@@ -1985,9 +2202,10 @@ async function documentIncomingRelationships(reactorClient, args) {
|
|
|
1985
2202
|
throw new GraphQLError(`Failed to convert incoming relationships to GraphQL: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
1986
2203
|
}
|
|
1987
2204
|
}
|
|
1988
|
-
async function findDocuments(reactorClient, args) {
|
|
1989
|
-
let view;
|
|
2205
|
+
async function findDocuments(reactorClient, args, subject) {
|
|
2206
|
+
let view = subject ? { subject } : void 0;
|
|
1990
2207
|
if (args.view) view = {
|
|
2208
|
+
subject,
|
|
1991
2209
|
branch: fromInputMaybe(args.view.branch),
|
|
1992
2210
|
scopes: toMutableArray(fromInputMaybe(args.view.scopes))
|
|
1993
2211
|
};
|
|
@@ -2029,11 +2247,12 @@ async function jobStatus(reactorClient, args) {
|
|
|
2029
2247
|
throw new GraphQLError(`Failed to convert job status to GraphQL: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
2030
2248
|
}
|
|
2031
2249
|
}
|
|
2032
|
-
async function documentOperations(reactorClient, args) {
|
|
2033
|
-
let view;
|
|
2250
|
+
async function documentOperations(reactorClient, args, subject) {
|
|
2251
|
+
let view = subject ? { subject } : void 0;
|
|
2034
2252
|
const branch = fromInputMaybe(args.filter.branch);
|
|
2035
2253
|
const scopes = toMutableArray(fromInputMaybe(args.filter.scopes));
|
|
2036
2254
|
if (branch || scopes) view = {
|
|
2255
|
+
subject,
|
|
2037
2256
|
branch,
|
|
2038
2257
|
scopes
|
|
2039
2258
|
};
|
|
@@ -2069,6 +2288,41 @@ async function documentOperations(reactorClient, args) {
|
|
|
2069
2288
|
throw new GraphQLError(`Failed to convert operations to GraphQL: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
2070
2289
|
}
|
|
2071
2290
|
}
|
|
2291
|
+
/**
|
|
2292
|
+
* Predicts the admission verdict for each candidate operation, deciding against
|
|
2293
|
+
* the document policy through the reactor's own decision model.
|
|
2294
|
+
*
|
|
2295
|
+
* The subject is supplied by the subgraph from the authenticated request, never
|
|
2296
|
+
* by the caller: answering for an arbitrary subject would disclose what a policy
|
|
2297
|
+
* grants somebody else. It carries the caller's address and the app key their
|
|
2298
|
+
* token authenticated with, which is the same pair a signed action presents, so
|
|
2299
|
+
* a grant naming either matches here exactly as it would at admission.
|
|
2300
|
+
*
|
|
2301
|
+
* A reactor without authEnforcement raises the named error, which becomes an
|
|
2302
|
+
* "unsupported" code rather than a denial.
|
|
2303
|
+
*/
|
|
2304
|
+
async function evaluateActions(reactorClient, args, subject) {
|
|
2305
|
+
const branch = fromInputMaybe(args.branch) ?? "main";
|
|
2306
|
+
const candidates = args.candidates.map((candidate) => ({
|
|
2307
|
+
scope: candidate.scope,
|
|
2308
|
+
type: candidate.type,
|
|
2309
|
+
input: fromInputMaybe(candidate.input)
|
|
2310
|
+
}));
|
|
2311
|
+
let result;
|
|
2312
|
+
try {
|
|
2313
|
+
result = await reactorClient.evaluateActions(args.documentIdentifier, branch, candidates, subject);
|
|
2314
|
+
} catch (error) {
|
|
2315
|
+
if (AuthEnforcementDisabledError.isError(error)) throw new AuthEvaluationUnsupportedError();
|
|
2316
|
+
throw new GraphQLError(`Failed to evaluate actions: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
2317
|
+
}
|
|
2318
|
+
return {
|
|
2319
|
+
evaluations: result.evaluations.map(toGqlActionEvaluation),
|
|
2320
|
+
allAllowed: result.allAllowed,
|
|
2321
|
+
anyAllowed: result.anyAllowed,
|
|
2322
|
+
allDenied: result.allDenied,
|
|
2323
|
+
anyDenied: result.anyDenied
|
|
2324
|
+
};
|
|
2325
|
+
}
|
|
2072
2326
|
async function createDocument(reactorClient, args, reactorDriveClient) {
|
|
2073
2327
|
if (!args.document || typeof args.document !== "object") throw new GraphQLError("Invalid document: must be an object");
|
|
2074
2328
|
const document = args.document;
|
|
@@ -2167,6 +2421,49 @@ async function createDocumentWithInitialState(reactorClient, args, reactorDriveC
|
|
|
2167
2421
|
throw new GraphQLError(`Failed to convert created document to GraphQL: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
2168
2422
|
}
|
|
2169
2423
|
}
|
|
2424
|
+
/** The branch a mutation applies to when the caller names none. */
|
|
2425
|
+
const DEFAULT_BRANCH = "main";
|
|
2426
|
+
/**
|
|
2427
|
+
* Applies actions to a document and waits for the result.
|
|
2428
|
+
*
|
|
2429
|
+
* The wire form of `IReactorClient.execute`, and named for it. Takes the actions
|
|
2430
|
+
* already coerced against `ActionInput`, so the only conversion left is the one
|
|
2431
|
+
* the schema cannot express: a signature travels as one string, and verification
|
|
2432
|
+
* reads it as the tuple it was.
|
|
2433
|
+
*/
|
|
2434
|
+
async function execute(reactorClient, args) {
|
|
2435
|
+
const actions = toSubmittableActions(args.actions);
|
|
2436
|
+
const branch = fromInputMaybe(args.branch) ?? DEFAULT_BRANCH;
|
|
2437
|
+
let result;
|
|
2438
|
+
try {
|
|
2439
|
+
result = await reactorClient.execute(args.documentIdentifier, branch, actions);
|
|
2440
|
+
} catch (error) {
|
|
2441
|
+
throw new GraphQLError(`Failed to execute actions: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
2442
|
+
}
|
|
2443
|
+
try {
|
|
2444
|
+
return toGqlPhDocument(result);
|
|
2445
|
+
} catch (error) {
|
|
2446
|
+
throw new GraphQLError(`Failed to convert executed document to GraphQL: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
2447
|
+
}
|
|
2448
|
+
}
|
|
2449
|
+
/**
|
|
2450
|
+
* Submits actions to a document and returns the job that will apply them.
|
|
2451
|
+
*
|
|
2452
|
+
* Returns the job rather than only its id, matching
|
|
2453
|
+
* `IReactorClient.executeAsync`. A job handed back from a submission has no
|
|
2454
|
+
* result yet, which is why `JobInfo.result` is nullable.
|
|
2455
|
+
*/
|
|
2456
|
+
async function executeAsync(reactorClient, args) {
|
|
2457
|
+
const actions = toSubmittableActions(args.actions);
|
|
2458
|
+
const branch = fromInputMaybe(args.branch) ?? DEFAULT_BRANCH;
|
|
2459
|
+
let job;
|
|
2460
|
+
try {
|
|
2461
|
+
job = await reactorClient.executeAsync(args.documentIdentifier, branch, actions);
|
|
2462
|
+
} catch (error) {
|
|
2463
|
+
throw new GraphQLError(`Failed to submit actions: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
2464
|
+
}
|
|
2465
|
+
return toGqlJobInfo(job);
|
|
2466
|
+
}
|
|
2170
2467
|
async function mutateDocument(reactorClient, args) {
|
|
2171
2468
|
let validatedActions;
|
|
2172
2469
|
try {
|
|
@@ -2300,7 +2597,14 @@ async function deleteDocuments(reactorClient, args) {
|
|
|
2300
2597
|
throw new GraphQLError(`Failed to delete documents: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
2301
2598
|
}
|
|
2302
2599
|
}
|
|
2303
|
-
|
|
2600
|
+
/**
|
|
2601
|
+
* Registers a sync channel, or reports the state of one already registered.
|
|
2602
|
+
*
|
|
2603
|
+
* `boundAddress` claims a channel at creation for the subject that created it.
|
|
2604
|
+
* An anonymously created channel is left unbound, which is not a permission but
|
|
2605
|
+
* an unanswered question: the first authenticated subject to use it adopts it.
|
|
2606
|
+
*/
|
|
2607
|
+
async function touchChannel(syncManager, args, boundAddress) {
|
|
2304
2608
|
try {
|
|
2305
2609
|
return {
|
|
2306
2610
|
success: true,
|
|
@@ -2312,7 +2616,10 @@ async function touchChannel(syncManager, args) {
|
|
|
2312
2616
|
scope: [...args.input.filter.scope],
|
|
2313
2617
|
branch: args.input.filter.branch
|
|
2314
2618
|
};
|
|
2315
|
-
const options = {
|
|
2619
|
+
const options = {
|
|
2620
|
+
sinceTimestampUtcMs: args.input.sinceTimestampUtcMs,
|
|
2621
|
+
boundAddress
|
|
2622
|
+
};
|
|
2316
2623
|
try {
|
|
2317
2624
|
await syncManager.add(args.input.name, DriveCollectionId.fromKey(args.input.collectionId), {
|
|
2318
2625
|
type: "polling",
|
|
@@ -2327,9 +2634,50 @@ async function touchChannel(syncManager, args) {
|
|
|
2327
2634
|
};
|
|
2328
2635
|
}
|
|
2329
2636
|
/**
|
|
2637
|
+
* The sync operations in `syncOps` this subject may not be served.
|
|
2638
|
+
*
|
|
2639
|
+
* A sync operation is one document, one branch and (from the outbox) one scope,
|
|
2640
|
+
* so the answer is per entry rather than per operation: an entry is withheld
|
|
2641
|
+
* whole or served whole, which is what keeps a scope's operation sequence
|
|
2642
|
+
* unbroken for whoever does receive it. A dead letter can name several scopes,
|
|
2643
|
+
* and is withheld only when the policy serves none of them -- the metadata
|
|
2644
|
+
* scopes are readable by every holder, so in practice a dead letter naming one
|
|
2645
|
+
* is always served.
|
|
2646
|
+
*
|
|
2647
|
+
* The policy is read once per document and branch, and the memo lives no longer
|
|
2648
|
+
* than this call: a poll must not answer from a policy that changed between
|
|
2649
|
+
* polls, since the widening case is exactly the one holding exists to serve.
|
|
2650
|
+
*
|
|
2651
|
+
* A read that fails for any reason other than the document being absent is
|
|
2652
|
+
* allowed to propagate. The poll then fails, the puller retries, and nothing is
|
|
2653
|
+
* served in the meantime -- which is the safe direction, and a visible one.
|
|
2654
|
+
*/
|
|
2655
|
+
async function collectHeldSyncOperations(syncOps, gate, subject, signal) {
|
|
2656
|
+
const held = /* @__PURE__ */ new Set();
|
|
2657
|
+
const predicates = /* @__PURE__ */ new Map();
|
|
2658
|
+
for (const syncOp of syncOps) {
|
|
2659
|
+
const key = `${syncOp.documentId}\u0000${syncOp.branch}`;
|
|
2660
|
+
let predicate = predicates.get(key);
|
|
2661
|
+
if (!predicate) {
|
|
2662
|
+
predicate = gate.scopePredicateById(syncOp.documentId, subject, syncOp.branch, signal);
|
|
2663
|
+
predicates.set(key, predicate);
|
|
2664
|
+
}
|
|
2665
|
+
const readable = await predicate;
|
|
2666
|
+
if (!syncOp.scopes.some((scope) => readable(scope))) held.add(syncOp.id);
|
|
2667
|
+
}
|
|
2668
|
+
return held;
|
|
2669
|
+
}
|
|
2670
|
+
/**
|
|
2330
2671
|
* Polls the switchboard for new sync envelopes and acknowledges previously
|
|
2331
2672
|
* received operations.
|
|
2332
2673
|
*
|
|
2674
|
+
* `forbiddenIds` and `heldOpIds` are two different refusals. A forbidden
|
|
2675
|
+
* document is consumed: its delivery counters are advanced as though it had
|
|
2676
|
+
* been sent, so it is never reconsidered. A held sync operation is withheld:
|
|
2677
|
+
* nothing about it is touched, so a policy that later widens serves it whole on
|
|
2678
|
+
* a subsequent poll. Which one applies is the caller's decision, and the two
|
|
2679
|
+
* are intersected, so a document refused either way is not served.
|
|
2680
|
+
*
|
|
2333
2681
|
* Ordinal frames of reference:
|
|
2334
2682
|
* - `outboxAck` / `outboxLatest`: switchboard's ordinals (used to trim/filter
|
|
2335
2683
|
* the switchboard's outbox)
|
|
@@ -2337,16 +2685,17 @@ async function touchChannel(syncManager, args) {
|
|
|
2337
2685
|
* client ordinal the switchboard has successfully applied, so the client
|
|
2338
2686
|
* can trim its own outbox)
|
|
2339
2687
|
*/
|
|
2340
|
-
function pollSyncEnvelopes(syncManager, args, forbiddenIds = /* @__PURE__ */ new Set()) {
|
|
2688
|
+
function pollSyncEnvelopes(syncManager, args, forbiddenIds = /* @__PURE__ */ new Set(), heldOpIds = /* @__PURE__ */ new Set()) {
|
|
2341
2689
|
let remote;
|
|
2342
2690
|
try {
|
|
2343
2691
|
remote = syncManager.getById(args.channelId);
|
|
2344
2692
|
} catch (error) {
|
|
2345
2693
|
throw new GraphQLError(`Channel not found: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
2346
2694
|
}
|
|
2347
|
-
const deadLetters = remote.channel.deadLetter.items.filter((syncOp) => !forbiddenIds.has(syncOp.documentId)).map((syncOp) => ({
|
|
2695
|
+
const deadLetters = remote.channel.deadLetter.items.filter((syncOp) => !forbiddenIds.has(syncOp.documentId) && !heldOpIds.has(syncOp.id)).map((syncOp) => ({
|
|
2348
2696
|
documentId: syncOp.documentId,
|
|
2349
2697
|
error: syncOp.error?.message ?? "Unknown error",
|
|
2698
|
+
errorType: syncOperationErrorType(syncOp.error),
|
|
2350
2699
|
jobId: syncOp.jobId,
|
|
2351
2700
|
branch: syncOp.branch,
|
|
2352
2701
|
scopes: syncOp.scopes,
|
|
@@ -2392,6 +2741,7 @@ function pollSyncEnvelopes(syncManager, args, forbiddenIds = /* @__PURE__ */ new
|
|
|
2392
2741
|
hasMore = true;
|
|
2393
2742
|
break;
|
|
2394
2743
|
}
|
|
2744
|
+
if (heldOpIds.has(syncOp.id)) continue;
|
|
2395
2745
|
syncOp.deliveredCount ??= 0;
|
|
2396
2746
|
syncOp.emittedCount ??= 0;
|
|
2397
2747
|
while (syncOp.deliveredCount < syncOp.emittedCount && syncOp.operations[syncOp.deliveredCount].context.ordinal <= args.outboxLatest) syncOp.deliveredCount += 1;
|
|
@@ -2863,6 +3213,12 @@ function filterLatestDocumentModelVersions(documentModels) {
|
|
|
2863
3213
|
return Array.from(latestByName.values());
|
|
2864
3214
|
}
|
|
2865
3215
|
const DefaultFeatureFlags = { enableDocumentModelSubgraphs: true };
|
|
3216
|
+
/**
|
|
3217
|
+
* The registration-source label for document-model-generated subgraphs, so
|
|
3218
|
+
* they can be pruned like package-contributed ones (see
|
|
3219
|
+
* regenerateDocumentModelSubgraphs).
|
|
3220
|
+
*/
|
|
3221
|
+
const DOCUMENT_MODEL_SUBGRAPH_SOURCE = "document-models";
|
|
2866
3222
|
var GraphQLManager = class {
|
|
2867
3223
|
initialized = false;
|
|
2868
3224
|
coreSubgraphsMap = /* @__PURE__ */ new Map();
|
|
@@ -2870,11 +3226,25 @@ var GraphQLManager = class {
|
|
|
2870
3226
|
subgraphs = /* @__PURE__ */ new Map();
|
|
2871
3227
|
subgraphWsDisposers = /* @__PURE__ */ new Map();
|
|
2872
3228
|
#authMiddleware;
|
|
3229
|
+
#requireAuthMiddleware;
|
|
2873
3230
|
#driveMiddleware;
|
|
2874
3231
|
driveOwnershipCache;
|
|
2875
3232
|
/** Cached document models for schema generation - updated on init and regenerate */
|
|
2876
3233
|
cachedDocumentModels = [];
|
|
2877
3234
|
subgraphHandlerCache = /* @__PURE__ */ new Map();
|
|
3235
|
+
/** subgraphPath → the http adapter handle of its mounted route. */
|
|
3236
|
+
subgraphRouteHandles = /* @__PURE__ */ new Map();
|
|
3237
|
+
/** Handle of the currently mounted supergraph SSE route, if any. */
|
|
3238
|
+
sseRouteHandle;
|
|
3239
|
+
/**
|
|
3240
|
+
* Package name → the subgraph instances registered from it, keyed by
|
|
3241
|
+
* subgraph name. Tracks the package association so that removed packages
|
|
3242
|
+
* can be torn down (see unregisterPackage / prunePackageSubgraphs).
|
|
3243
|
+
*/
|
|
3244
|
+
packageSubgraphs = /* @__PURE__ */ new Map();
|
|
3245
|
+
/** Per-subgraph handlers by name, before auth/drive middleware is applied,
|
|
3246
|
+
* for in-process queries by trusted internals. */
|
|
3247
|
+
#internalSubgraphHandlers = /* @__PURE__ */ new Map();
|
|
2878
3248
|
/**
|
|
2879
3249
|
* Optional reactor-drive client. When the switchboard is configured with a
|
|
2880
3250
|
* reactor-drive container, this is provided and the resolvers dispatch to
|
|
@@ -2882,7 +3252,7 @@ var GraphQLManager = class {
|
|
|
2882
3252
|
*/
|
|
2883
3253
|
reactorDriveClient;
|
|
2884
3254
|
authorizationService;
|
|
2885
|
-
constructor(path, httpServer, wsServer, reactorClient, relationalDb, analyticsStore, syncManager, logger, httpAdapter, gatewayAdapter, authService, documentPermissionService, featureFlags = DefaultFeatureFlags, port = 4001, authorizationService, reactorDriveClient) {
|
|
3255
|
+
constructor(path, httpServer, wsServer, reactorClient, relationalDb, analyticsStore, syncManager, logger, httpAdapter, gatewayAdapter, authService, documentPermissionService, featureFlags = DefaultFeatureFlags, port = 4001, authorizationService, reactorDriveClient, syncServingGate) {
|
|
2886
3256
|
this.path = path;
|
|
2887
3257
|
this.httpServer = httpServer;
|
|
2888
3258
|
this.wsServer = wsServer;
|
|
@@ -2897,14 +3267,16 @@ var GraphQLManager = class {
|
|
|
2897
3267
|
this.documentPermissionService = documentPermissionService;
|
|
2898
3268
|
this.featureFlags = featureFlags;
|
|
2899
3269
|
this.port = port;
|
|
3270
|
+
this.syncServingGate = syncServingGate;
|
|
2900
3271
|
if (!authorizationService) throw new Error("GraphQLManager requires an authorizationService");
|
|
2901
3272
|
this.authorizationService = authorizationService;
|
|
2902
3273
|
this.reactorDriveClient = reactorDriveClient;
|
|
2903
3274
|
this.driveOwnershipCache = new DriveOwnershipCache(this.reactorClient);
|
|
2904
3275
|
this.wsServer.setMaxListeners(0);
|
|
2905
3276
|
}
|
|
2906
|
-
async init(coreSubgraphs, authMiddleware) {
|
|
3277
|
+
async init(coreSubgraphs, authMiddleware, requireAuthMiddleware) {
|
|
2907
3278
|
this.#authMiddleware = authMiddleware;
|
|
3279
|
+
this.#requireAuthMiddleware = requireAuthMiddleware;
|
|
2908
3280
|
this.logger.debug(`Initializing Subgraph Manager...`);
|
|
2909
3281
|
await this.driveOwnershipCache.init();
|
|
2910
3282
|
this.#driveMiddleware = createDriveFetchMiddleware(this.driveOwnershipCache);
|
|
@@ -2914,7 +3286,7 @@ var GraphQLManager = class {
|
|
|
2914
3286
|
if (!models.find((it) => it.documentModel.global.name === "DocumentDrive")) throw new Error("DocumentDrive model required");
|
|
2915
3287
|
await this.gatewayAdapter.start(this.httpServer);
|
|
2916
3288
|
this.httpAdapter.setupMiddleware({ bodyLimit: "50mb" });
|
|
2917
|
-
const driveRoutePath = path.join(this.path, "d/:drive");
|
|
3289
|
+
const driveRoutePath = path.posix.join(this.path, "d/:drive");
|
|
2918
3290
|
const driveMatcher = match(driveRoutePath);
|
|
2919
3291
|
this.httpAdapter.mount(driveRoutePath, async (request) => {
|
|
2920
3292
|
const url = new URL(request.url);
|
|
@@ -2952,7 +3324,8 @@ var GraphQLManager = class {
|
|
|
2952
3324
|
try {
|
|
2953
3325
|
const models = (await this.reactorClient.getDocumentModelModules()).results;
|
|
2954
3326
|
this.cachedDocumentModels = models;
|
|
2955
|
-
await this.#setupDocumentModelSubgraphs("graphql", models);
|
|
3327
|
+
const registeredNames = await this.#setupDocumentModelSubgraphs("graphql", models);
|
|
3328
|
+
await this.prunePackageSubgraphs(DOCUMENT_MODEL_SUBGRAPH_SOURCE, registeredNames);
|
|
2956
3329
|
await this.updateRouter();
|
|
2957
3330
|
this.logger.info("Regenerated document model subgraphs with @count models", models.length);
|
|
2958
3331
|
} catch (error) {
|
|
@@ -2970,6 +3343,7 @@ var GraphQLManager = class {
|
|
|
2970
3343
|
}
|
|
2971
3344
|
async #setupDocumentModelSubgraphs(supergraph, documentModels) {
|
|
2972
3345
|
const latestDocumentModels = filterLatestDocumentModelVersions(documentModels);
|
|
3346
|
+
const registeredNames = /* @__PURE__ */ new Set();
|
|
2973
3347
|
for (const documentModel of latestDocumentModels) {
|
|
2974
3348
|
if (DOCUMENT_MODELS_TO_EXCLUDE.includes(documentModel.documentModel.global.id)) continue;
|
|
2975
3349
|
if (!hasOperationSchemas(documentModel)) continue;
|
|
@@ -2982,31 +3356,92 @@ var GraphQLManager = class {
|
|
|
2982
3356
|
syncManager: this.syncManager,
|
|
2983
3357
|
path: this.path,
|
|
2984
3358
|
documentPermissionService: this.documentPermissionService,
|
|
2985
|
-
authorizationService: this.authorizationService
|
|
3359
|
+
authorizationService: this.authorizationService,
|
|
3360
|
+
syncServingGate: this.syncServingGate
|
|
2986
3361
|
});
|
|
2987
|
-
await this.#addSubgraphInstance(subgraphInstance, supergraph, false);
|
|
3362
|
+
await this.#addSubgraphInstance(subgraphInstance, supergraph, false, DOCUMENT_MODEL_SUBGRAPH_SOURCE);
|
|
3363
|
+
registeredNames.add(subgraphInstance.name);
|
|
2988
3364
|
} catch (error) {
|
|
2989
3365
|
this.logger.error(`Failed to setup document model subgraph for ${documentModel.documentModel.global.id}`, error instanceof Error ? error.message : error);
|
|
2990
3366
|
this.logger.debug("@error", error);
|
|
2991
3367
|
}
|
|
2992
3368
|
}
|
|
3369
|
+
return registeredNames;
|
|
3370
|
+
}
|
|
3371
|
+
/** Core subgraph names are reserved: they register with core=true during
|
|
3372
|
+
* init() and no package subgraph may take one of their names. */
|
|
3373
|
+
#isCoreNameReserved(name) {
|
|
3374
|
+
for (const subgraphs of this.coreSubgraphsMap.values()) if (subgraphs.some((it) => it.name === name)) return true;
|
|
3375
|
+
return false;
|
|
2993
3376
|
}
|
|
2994
|
-
async #addSubgraphInstance(subgraphInstance, supergraph = "", core = false) {
|
|
3377
|
+
async #addSubgraphInstance(subgraphInstance, supergraph = "", core = false, packageName) {
|
|
3378
|
+
if (!core && this.#isCoreNameReserved(subgraphInstance.name)) {
|
|
3379
|
+
this.logger.error("Rejecting subgraph @name: name is reserved by a registered core subgraph", subgraphInstance.name);
|
|
3380
|
+
return;
|
|
3381
|
+
}
|
|
2995
3382
|
const subgraphsMap = core ? this.coreSubgraphsMap : this.subgraphs;
|
|
2996
|
-
const
|
|
2997
|
-
|
|
2998
|
-
if (existingSubgraph) {
|
|
3383
|
+
const existingSubgraph = subgraphsMap.get(supergraph)?.find((it) => it.name === subgraphInstance.name);
|
|
3384
|
+
if (existingSubgraph === subgraphInstance) {
|
|
2999
3385
|
this.logger.debug(`Skipping duplicate subgraph: ${subgraphInstance.name}`);
|
|
3000
3386
|
return existingSubgraph;
|
|
3001
3387
|
}
|
|
3388
|
+
if (existingSubgraph) await this.#removeSubgraphInstance(existingSubgraph);
|
|
3002
3389
|
await subgraphInstance.onSetup?.();
|
|
3390
|
+
const subgraphs = subgraphsMap.get(supergraph) ?? [];
|
|
3003
3391
|
subgraphs.push(subgraphInstance);
|
|
3004
3392
|
subgraphsMap.set(supergraph, subgraphs);
|
|
3005
3393
|
if (supergraph !== "" && supergraph !== "graphql") subgraphsMap.get("graphql")?.push(subgraphInstance);
|
|
3006
|
-
|
|
3394
|
+
if (packageName !== void 0) {
|
|
3395
|
+
let byName = this.packageSubgraphs.get(packageName);
|
|
3396
|
+
if (!byName) {
|
|
3397
|
+
byName = /* @__PURE__ */ new Map();
|
|
3398
|
+
this.packageSubgraphs.set(packageName, byName);
|
|
3399
|
+
}
|
|
3400
|
+
byName.set(subgraphInstance.name, subgraphInstance);
|
|
3401
|
+
}
|
|
3402
|
+
this.logger.info(`${existingSubgraph ? "Replaced" : "Registered"} ${this.path.endsWith("/") ? this.path : this.path + "/"}${supergraph ? supergraph + "/" : ""}${subgraphInstance.name} subgraph.`);
|
|
3007
3403
|
return subgraphInstance;
|
|
3008
3404
|
}
|
|
3009
3405
|
/**
|
|
3406
|
+
* Tear down a subgraph instance: disconnect it, drop it from every
|
|
3407
|
+
* bucket, unmount its route, dispose its WebSocket handler, and remove
|
|
3408
|
+
* its internal handler. Used when a subgraph is replaced or its package
|
|
3409
|
+
* is removed; #setupSubgraphs then never re-mounts it.
|
|
3410
|
+
*/
|
|
3411
|
+
async #removeSubgraphInstance(instance) {
|
|
3412
|
+
try {
|
|
3413
|
+
await instance.onDisconnect?.();
|
|
3414
|
+
} catch (error) {
|
|
3415
|
+
this.logger.error("Error disconnecting subgraph @name: @error", instance.name, error);
|
|
3416
|
+
}
|
|
3417
|
+
for (const subgraphsMap of [this.coreSubgraphsMap, this.subgraphs]) for (const [bucket, subgraphs] of subgraphsMap.entries()) {
|
|
3418
|
+
if (!subgraphs.includes(instance)) continue;
|
|
3419
|
+
subgraphsMap.set(bucket, subgraphs.filter((it) => it !== instance));
|
|
3420
|
+
const subgraphPath = this.#getSubgraphPath(instance, bucket);
|
|
3421
|
+
this.subgraphHandlerCache.delete(subgraphPath);
|
|
3422
|
+
const routeHandle = this.subgraphRouteHandles.get(subgraphPath);
|
|
3423
|
+
if (routeHandle !== void 0) {
|
|
3424
|
+
this.httpAdapter.unmount(routeHandle);
|
|
3425
|
+
this.subgraphRouteHandles.delete(subgraphPath);
|
|
3426
|
+
}
|
|
3427
|
+
const wsDisposer = this.subgraphWsDisposers.get(subgraphPath);
|
|
3428
|
+
if (wsDisposer) {
|
|
3429
|
+
this.subgraphWsDisposers.delete(subgraphPath);
|
|
3430
|
+
try {
|
|
3431
|
+
await wsDisposer.dispose();
|
|
3432
|
+
} catch (error) {
|
|
3433
|
+
this.logger.error("Error disposing subgraph websocket @name: @error", instance.name, error);
|
|
3434
|
+
}
|
|
3435
|
+
}
|
|
3436
|
+
}
|
|
3437
|
+
if (this.#internalSubgraphHandlers.get(instance.name)?.subgraph === instance) this.#internalSubgraphHandlers.delete(instance.name);
|
|
3438
|
+
for (const [packageName, byName] of this.packageSubgraphs) {
|
|
3439
|
+
if (byName.get(instance.name) !== instance) continue;
|
|
3440
|
+
byName.delete(instance.name);
|
|
3441
|
+
if (byName.size === 0) this.packageSubgraphs.delete(packageName);
|
|
3442
|
+
}
|
|
3443
|
+
}
|
|
3444
|
+
/**
|
|
3010
3445
|
* Register a pre-constructed subgraph instance.
|
|
3011
3446
|
* Use this when you need to pass custom dependencies to a subgraph.
|
|
3012
3447
|
*/
|
|
@@ -3027,7 +3462,13 @@ var GraphQLManager = class {
|
|
|
3027
3462
|
getAuthorizationService() {
|
|
3028
3463
|
return this.authorizationService;
|
|
3029
3464
|
}
|
|
3030
|
-
|
|
3465
|
+
/**
|
|
3466
|
+
* Register a subgraph class. `packageName` labels the resulting instance
|
|
3467
|
+
* with its contributing package so the instance can be torn down when the
|
|
3468
|
+
* package is removed or drops the subgraph (see unregisterPackage and
|
|
3469
|
+
* prunePackageSubgraphs).
|
|
3470
|
+
*/
|
|
3471
|
+
async registerSubgraph(subgraph, supergraph = "", core = false, packageName) {
|
|
3031
3472
|
const subgraphInstance = new subgraph({
|
|
3032
3473
|
relationalDb: this.relationalDb,
|
|
3033
3474
|
analyticsStore: this.analyticsStore,
|
|
@@ -3036,9 +3477,76 @@ var GraphQLManager = class {
|
|
|
3036
3477
|
syncManager: this.syncManager,
|
|
3037
3478
|
path: this.path,
|
|
3038
3479
|
documentPermissionService: this.documentPermissionService,
|
|
3039
|
-
authorizationService: this.authorizationService
|
|
3480
|
+
authorizationService: this.authorizationService,
|
|
3481
|
+
syncServingGate: this.syncServingGate
|
|
3040
3482
|
});
|
|
3041
|
-
return this.#addSubgraphInstance(subgraphInstance, supergraph, core);
|
|
3483
|
+
return this.#addSubgraphInstance(subgraphInstance, supergraph, core, packageName);
|
|
3484
|
+
}
|
|
3485
|
+
/**
|
|
3486
|
+
* Tear down every subgraph instance registered from `packageName`:
|
|
3487
|
+
* disconnects the instances, unmounts their routes, disposes their
|
|
3488
|
+
* WebSocket handlers, then refreshes the router so the supergraph SDL and
|
|
3489
|
+
* the supergraph SSE handler no longer reference them. No-op if the
|
|
3490
|
+
* package has no registered subgraphs.
|
|
3491
|
+
*/
|
|
3492
|
+
async unregisterPackage(packageName) {
|
|
3493
|
+
const byName = this.packageSubgraphs.get(packageName);
|
|
3494
|
+
if (!byName || byName.size === 0) return;
|
|
3495
|
+
for (const instance of Array.from(byName.values())) await this.#removeSubgraphInstance(instance);
|
|
3496
|
+
await this.updateRouter();
|
|
3497
|
+
}
|
|
3498
|
+
/**
|
|
3499
|
+
* Remove this package's registered subgraphs whose names are not in
|
|
3500
|
+
* `keepNames` — the package is still loaded but dropped some of its
|
|
3501
|
+
* subgraphs. Refreshes the router if anything was removed.
|
|
3502
|
+
*/
|
|
3503
|
+
async prunePackageSubgraphs(packageName, keepNames) {
|
|
3504
|
+
const byName = this.packageSubgraphs.get(packageName);
|
|
3505
|
+
if (!byName) return;
|
|
3506
|
+
let removed = 0;
|
|
3507
|
+
for (const [name, instance] of Array.from(byName.entries())) if (!keepNames.has(name)) {
|
|
3508
|
+
await this.#removeSubgraphInstance(instance);
|
|
3509
|
+
removed++;
|
|
3510
|
+
}
|
|
3511
|
+
if (removed > 0) await this.updateRouter();
|
|
3512
|
+
}
|
|
3513
|
+
/** Find a registered subgraph by name, core or package-contributed. */
|
|
3514
|
+
getSubgraphByName(name) {
|
|
3515
|
+
for (const subgraphs of [...this.coreSubgraphsMap.values(), ...this.subgraphs.values()]) {
|
|
3516
|
+
const found = subgraphs.find((subgraph) => subgraph.name === name);
|
|
3517
|
+
if (found) return found;
|
|
3518
|
+
}
|
|
3519
|
+
}
|
|
3520
|
+
/** Whether a registered subgraph has a live handler, i.e. whether
|
|
3521
|
+
* {@link executeSubgraphQuery} can reach it right now. */
|
|
3522
|
+
hasSubgraphHandler(name) {
|
|
3523
|
+
return this.#internalSubgraphHandlers.has(name);
|
|
3524
|
+
}
|
|
3525
|
+
/** Run a query against one registered subgraph in-process: the subgraph's own
|
|
3526
|
+
* handler, without a socket or the auth/drive middleware. Trusted callers. */
|
|
3527
|
+
async executeSubgraphQuery(subgraphName, query, variables) {
|
|
3528
|
+
const entry = this.#internalSubgraphHandlers.get(subgraphName);
|
|
3529
|
+
if (!entry) throw new Error(this.getSubgraphByName(subgraphName) ? `Subgraph "${subgraphName}" is registered but has no handler yet` : `Subgraph "${subgraphName}" is not registered`);
|
|
3530
|
+
const url = new URL(entry.path, `http://localhost:${this.port}/`);
|
|
3531
|
+
const response = await entry.handler(new Request(url, {
|
|
3532
|
+
method: "POST",
|
|
3533
|
+
headers: {
|
|
3534
|
+
"content-type": "application/json",
|
|
3535
|
+
accept: "application/json"
|
|
3536
|
+
},
|
|
3537
|
+
body: JSON.stringify({
|
|
3538
|
+
query,
|
|
3539
|
+
variables
|
|
3540
|
+
})
|
|
3541
|
+
}));
|
|
3542
|
+
if (!response.ok) {
|
|
3543
|
+
const detail = await response.text().catch(() => "");
|
|
3544
|
+
throw new Error(`Subgraph "${subgraphName}" query failed: ${response.status}${detail ? ` ${detail}` : ""}`);
|
|
3545
|
+
}
|
|
3546
|
+
const body = await response.json();
|
|
3547
|
+
if (body.errors?.length) throw new Error(body.errors.map((error) => error.message).join("; "));
|
|
3548
|
+
if (!body.data) throw new Error(`Subgraph "${subgraphName}" returned no data`);
|
|
3549
|
+
return body.data;
|
|
3042
3550
|
}
|
|
3043
3551
|
updateRouter = debounce(this._updateRouter.bind(this), 1e3);
|
|
3044
3552
|
async _updateRouter() {
|
|
@@ -3050,7 +3558,7 @@ var GraphQLManager = class {
|
|
|
3050
3558
|
} catch (error) {
|
|
3051
3559
|
this.logger.error("Failed to update Apollo Gateway supergraph: @error", error);
|
|
3052
3560
|
}
|
|
3053
|
-
const superGraphPath = path.join(this.path, "graphql");
|
|
3561
|
+
const superGraphPath = path.posix.join(this.path, "graphql");
|
|
3054
3562
|
this.#setupSupergraphSSE(superGraphPath);
|
|
3055
3563
|
}
|
|
3056
3564
|
getAdditionalContextFields = () => {
|
|
@@ -3094,10 +3602,18 @@ var GraphQLManager = class {
|
|
|
3094
3602
|
return (connectionParams) => this.#createWebSocketContext(connectionParams);
|
|
3095
3603
|
}
|
|
3096
3604
|
setSupergraph(supergraph, subgraphs) {
|
|
3097
|
-
|
|
3605
|
+
const accepted = [];
|
|
3606
|
+
for (const subgraph of subgraphs) {
|
|
3607
|
+
if (this.#isCoreNameReserved(subgraph.name)) {
|
|
3608
|
+
this.logger.error("Rejecting subgraph @name: name is reserved by a registered core subgraph", subgraph.name);
|
|
3609
|
+
continue;
|
|
3610
|
+
}
|
|
3611
|
+
accepted.push(subgraph);
|
|
3612
|
+
}
|
|
3613
|
+
this.subgraphs.set(supergraph, accepted);
|
|
3098
3614
|
const globalSubgraphs = this.subgraphs.get("graphql");
|
|
3099
|
-
if (globalSubgraphs) this.subgraphs.set("graphql", [...globalSubgraphs, ...
|
|
3100
|
-
else this.subgraphs.set("graphql",
|
|
3615
|
+
if (globalSubgraphs) this.subgraphs.set("graphql", [...globalSubgraphs, ...accepted]);
|
|
3616
|
+
else this.subgraphs.set("graphql", accepted);
|
|
3101
3617
|
return this.updateRouter();
|
|
3102
3618
|
}
|
|
3103
3619
|
async shutdown() {
|
|
@@ -3113,19 +3629,41 @@ var GraphQLManager = class {
|
|
|
3113
3629
|
});
|
|
3114
3630
|
}
|
|
3115
3631
|
#getSubgraphPath(subgraph, supergraph) {
|
|
3116
|
-
return path.join(
|
|
3632
|
+
return path.posix.join(this.path, supergraph, subgraph.name);
|
|
3633
|
+
}
|
|
3634
|
+
/** The in-process handler map is keyed by bare name, so two distinct
|
|
3635
|
+
* subgraphs sharing one name would shadow each other: keep the first. */
|
|
3636
|
+
#setInternalSubgraphHandler(subgraph, subgraphPath, handler) {
|
|
3637
|
+
const existing = this.#internalSubgraphHandlers.get(subgraph.name);
|
|
3638
|
+
if (existing && existing.subgraph !== subgraph) {
|
|
3639
|
+
this.logger.warn("Two subgraphs are named @name (@kept and @ignored); in-process queries keep the first", subgraph.name, existing.path, subgraphPath);
|
|
3640
|
+
return;
|
|
3641
|
+
}
|
|
3642
|
+
this.#internalSubgraphHandlers.set(subgraph.name, {
|
|
3643
|
+
subgraph,
|
|
3644
|
+
path: subgraphPath,
|
|
3645
|
+
handler
|
|
3646
|
+
});
|
|
3117
3647
|
}
|
|
3118
3648
|
async #setupSubgraphs(subgraphsMap) {
|
|
3119
3649
|
for (const [supergraph, subgraphs] of subgraphsMap.entries()) for (const subgraph of subgraphs) {
|
|
3120
3650
|
this.logger.debug(`Setting up subgraph ${subgraph.name}`);
|
|
3121
3651
|
const subgraphPath = this.#getSubgraphPath(subgraph, supergraph);
|
|
3122
3652
|
try {
|
|
3123
|
-
|
|
3653
|
+
const cached = this.subgraphHandlerCache.get(subgraphPath);
|
|
3654
|
+
if (cached) {
|
|
3655
|
+
if (cached.subgraph !== subgraph) this.logger.warn("Subgraph path @path already mounted by @kept; @name is shadowed and will not be mounted", subgraphPath, cached.subgraph.name, subgraph.name);
|
|
3656
|
+
continue;
|
|
3657
|
+
}
|
|
3124
3658
|
const schema = createSchema(this.cachedDocumentModels, subgraph.resolvers, subgraph.typeDefs);
|
|
3125
3659
|
const rawHandler = await this.gatewayAdapter.createHandler(schema, this.#makeContextFactory());
|
|
3126
3660
|
const fetchHandler = this.#composeFetchMiddleware(rawHandler);
|
|
3127
|
-
this.subgraphHandlerCache.set(subgraphPath,
|
|
3128
|
-
|
|
3661
|
+
this.subgraphHandlerCache.set(subgraphPath, {
|
|
3662
|
+
handler: fetchHandler,
|
|
3663
|
+
subgraph
|
|
3664
|
+
});
|
|
3665
|
+
this.#setInternalSubgraphHandler(subgraph, subgraphPath, rawHandler);
|
|
3666
|
+
this.subgraphRouteHandles.set(subgraphPath, this.httpAdapter.mount(subgraphPath, fetchHandler));
|
|
3129
3667
|
if (subgraph.hasSubscriptions) {
|
|
3130
3668
|
try {
|
|
3131
3669
|
if (this.subgraphWsDisposers.get(subgraphPath)) this.logger.warn("Overwriting existing WebSocket disposer for subgraph at path @path", subgraphPath);
|
|
@@ -3171,7 +3709,7 @@ var GraphQLManager = class {
|
|
|
3171
3709
|
}));
|
|
3172
3710
|
}
|
|
3173
3711
|
async #createSupergraphGateway() {
|
|
3174
|
-
const superGraphPath = path.join(this.path, "graphql");
|
|
3712
|
+
const superGraphPath = path.posix.join(this.path, "graphql");
|
|
3175
3713
|
const rawHandler = await this.gatewayAdapter.createSupergraphHandler(() => this.#getSubgraphDefinitions(), this.httpServer, this.#makeContextFactory());
|
|
3176
3714
|
const fetchHandler = this.#composeFetchMiddleware(rawHandler);
|
|
3177
3715
|
this.httpAdapter.mount(superGraphPath, fetchHandler);
|
|
@@ -3190,7 +3728,13 @@ var GraphQLManager = class {
|
|
|
3190
3728
|
#setupSupergraphSSE(superGraphPath) {
|
|
3191
3729
|
const allSubgraphs = this.#getAllSubgraphs();
|
|
3192
3730
|
const modules = Array.from(allSubgraphs.values()).filter((subgraph) => subgraph.hasSubscriptions).map((subgraph) => this.#buildSubgraphSchemaModule(subgraph));
|
|
3193
|
-
if (modules.length === 0)
|
|
3731
|
+
if (modules.length === 0) {
|
|
3732
|
+
if (this.sseRouteHandle !== void 0) {
|
|
3733
|
+
this.httpAdapter.unmount(this.sseRouteHandle);
|
|
3734
|
+
this.sseRouteHandle = void 0;
|
|
3735
|
+
}
|
|
3736
|
+
return;
|
|
3737
|
+
}
|
|
3194
3738
|
try {
|
|
3195
3739
|
const mergedSchema = createMergedSchema(modules);
|
|
3196
3740
|
this.#setupSSEHandler(mergedSchema, superGraphPath);
|
|
@@ -3214,16 +3758,20 @@ var GraphQLManager = class {
|
|
|
3214
3758
|
contextFactory: this.#makeContextFactory()
|
|
3215
3759
|
});
|
|
3216
3760
|
const handler = this.#composeFetchMiddleware(rawHandler);
|
|
3217
|
-
this.
|
|
3761
|
+
if (this.sseRouteHandle !== void 0) this.httpAdapter.unmount(this.sseRouteHandle);
|
|
3762
|
+
this.sseRouteHandle = this.httpAdapter.mount(ssePath, handler, { exact: true });
|
|
3218
3763
|
}
|
|
3219
3764
|
/**
|
|
3220
3765
|
* Compose the request-level fetch middleware chain. Auth runs first
|
|
3221
3766
|
* (so we don't validate shard before knowing the request is even
|
|
3222
|
-
* authorized)
|
|
3767
|
+
* authorized); the require-authenticated-caller middleware runs next,
|
|
3768
|
+
* before shard validation, so an anonymous caller is rejected with a
|
|
3769
|
+
* 401 rather than a 421; drive-ownership validation runs last.
|
|
3223
3770
|
*/
|
|
3224
3771
|
#composeFetchMiddleware(rawHandler) {
|
|
3225
3772
|
let handler = rawHandler;
|
|
3226
3773
|
if (this.#driveMiddleware) handler = this.#driveMiddleware(handler);
|
|
3774
|
+
if (this.#requireAuthMiddleware) handler = this.#requireAuthMiddleware(handler);
|
|
3227
3775
|
if (this.#authMiddleware) handler = this.#authMiddleware(handler);
|
|
3228
3776
|
return handler;
|
|
3229
3777
|
}
|
|
@@ -3329,6 +3877,31 @@ var PackagesSubgraph = class extends BaseSubgraph {
|
|
|
3329
3877
|
//#endregion
|
|
3330
3878
|
//#region src/graphql/playground.ts
|
|
3331
3879
|
/**
|
|
3880
|
+
* Decodes an `explorerURLState` URL parameter. Returns `null` for anything
|
|
3881
|
+
* that is not a valid lz-compressed payload with a non-empty `document`.
|
|
3882
|
+
*/
|
|
3883
|
+
function decodeExplorerUrlState(encoded) {
|
|
3884
|
+
try {
|
|
3885
|
+
const decompressed = lzString.decompressFromEncodedURIComponent(encoded);
|
|
3886
|
+
if (!decompressed) return null;
|
|
3887
|
+
const payload = JSON.parse(decompressed);
|
|
3888
|
+
if (typeof payload.document !== "string" || payload.document.length === 0) return null;
|
|
3889
|
+
const variables = typeof payload.variables === "string" ? payload.variables : void 0;
|
|
3890
|
+
let headers;
|
|
3891
|
+
if (typeof payload.headers === "string") {
|
|
3892
|
+
const parsed = JSON.parse(payload.headers);
|
|
3893
|
+
if (parsed && typeof parsed === "object") headers = parsed;
|
|
3894
|
+
}
|
|
3895
|
+
return {
|
|
3896
|
+
query: payload.document,
|
|
3897
|
+
variables,
|
|
3898
|
+
headers
|
|
3899
|
+
};
|
|
3900
|
+
} catch {
|
|
3901
|
+
return null;
|
|
3902
|
+
}
|
|
3903
|
+
}
|
|
3904
|
+
/**
|
|
3332
3905
|
* Pinned CDN versions for GraphiQL playground dependencies.
|
|
3333
3906
|
* Using pinned versions avoids unpkg.com redirect issues that can
|
|
3334
3907
|
* trigger CORS errors in the browser.
|
|
@@ -3339,7 +3912,29 @@ const CDN_VERSIONS = {
|
|
|
3339
3912
|
graphiql: "3.8.3",
|
|
3340
3913
|
pluginExplorer: "4.0.0"
|
|
3341
3914
|
};
|
|
3342
|
-
|
|
3915
|
+
/**
|
|
3916
|
+
* Escape a string for embedding as a single-quoted JS string literal inside
|
|
3917
|
+
* an HTML `<script>` block. The payload reaches the browser through an
|
|
3918
|
+
* attacker-controllable URL parameter, so the embedding must survive two
|
|
3919
|
+
* parsers: HTML (a `<\/script>` inside the string would end the script
|
|
3920
|
+
* element and start a new, attacker-authored one) and JS (backticks,
|
|
3921
|
+
* `${...}` and quotes in the content would otherwise break out of the
|
|
3922
|
+
* literal). Backslash, quote, CR and LF escapes keep the literal valid;
|
|
3923
|
+
* every `<` becomes `\u003c`, which no HTML parser reads as a tag opener.
|
|
3924
|
+
* The escapes resolve when the script parses, so the in-memory value is
|
|
3925
|
+
* byte-identical to the input.
|
|
3926
|
+
*/
|
|
3927
|
+
function jsStringEscape(value) {
|
|
3928
|
+
return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\r/g, "\\r").replace(/\n/g, "\\n").replace(/</g, "\\u003c");
|
|
3929
|
+
}
|
|
3930
|
+
/**
|
|
3931
|
+
* JSON is already a valid JS object/string literal; the only HTML-hostile
|
|
3932
|
+
* byte left is `<` (e.g. `<\/script>` inside a string value).
|
|
3933
|
+
*/
|
|
3934
|
+
function jsJsonEscape(json) {
|
|
3935
|
+
return json.replace(/</g, "\\u003c");
|
|
3936
|
+
}
|
|
3937
|
+
function renderGraphqlPlayground(url, query, headers = {}, variables) {
|
|
3343
3938
|
return `<!doctype html>
|
|
3344
3939
|
<html lang="en">
|
|
3345
3940
|
<head>
|
|
@@ -3379,16 +3974,21 @@ function renderGraphqlPlayground(url, query, headers = {}) {
|
|
|
3379
3974
|
<div id="graphiql">Loading...</div>
|
|
3380
3975
|
<script>
|
|
3381
3976
|
var fetcher = GraphiQL.createFetcher({
|
|
3382
|
-
url: '${url}',
|
|
3383
|
-
headers: ${JSON.stringify(headers)}
|
|
3977
|
+
url: '${jsStringEscape(url)}',
|
|
3978
|
+
headers: ${jsJsonEscape(JSON.stringify(headers))}
|
|
3384
3979
|
});
|
|
3385
|
-
var defaultQuery = ${query ?
|
|
3980
|
+
var defaultQuery = ${query ? `'${jsStringEscape(query)}'` : "undefined"};
|
|
3981
|
+
var defaultVariables = ${variables ? `'${jsStringEscape(variables)}'` : "undefined"};
|
|
3386
3982
|
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3983
|
+
// GraphiQL only applies defaultQuery/variables when the editor has
|
|
3984
|
+
// no persisted value. Overwrite any stale persisted state so a
|
|
3985
|
+
// returning user still gets the document-scoped query/variables
|
|
3986
|
+
// instead of their last session's.
|
|
3987
|
+
if (defaultQuery && localStorage.getItem("graphiql:query")) {
|
|
3988
|
+
localStorage.setItem("graphiql:query", defaultQuery);
|
|
3989
|
+
}
|
|
3990
|
+
if (defaultVariables && localStorage.getItem("graphiql:variables")) {
|
|
3991
|
+
localStorage.setItem("graphiql:variables", defaultVariables);
|
|
3392
3992
|
}
|
|
3393
3993
|
|
|
3394
3994
|
var explorerPlugin = GraphiQLPluginExplorer.explorerPlugin();
|
|
@@ -3398,7 +3998,8 @@ function renderGraphqlPlayground(url, query, headers = {}) {
|
|
|
3398
3998
|
fetcher: fetcher,
|
|
3399
3999
|
defaultEditorToolsVisibility: true,
|
|
3400
4000
|
plugins: [explorerPlugin],
|
|
3401
|
-
defaultQuery
|
|
4001
|
+
defaultQuery: defaultQuery,
|
|
4002
|
+
variables: defaultVariables
|
|
3402
4003
|
});
|
|
3403
4004
|
}
|
|
3404
4005
|
|
|
@@ -3536,6 +4137,17 @@ const DocumentChangeEventDTO = z.object({
|
|
|
3536
4137
|
documents: z.array(PHDocumentDTO),
|
|
3537
4138
|
context: DocumentChangeContextDTO.nullable().optional()
|
|
3538
4139
|
}).strip();
|
|
4140
|
+
const ActionEvaluationDTO = z.object({
|
|
4141
|
+
decision: z.enum(["ALLOW", "DENY"]),
|
|
4142
|
+
reason: z.string().nullable().optional()
|
|
4143
|
+
}).strip();
|
|
4144
|
+
const ActionEvaluationsDTO = z.object({
|
|
4145
|
+
evaluations: z.array(ActionEvaluationDTO),
|
|
4146
|
+
allAllowed: z.boolean(),
|
|
4147
|
+
anyAllowed: z.boolean(),
|
|
4148
|
+
allDenied: z.boolean(),
|
|
4149
|
+
anyDenied: z.boolean()
|
|
4150
|
+
}).strip();
|
|
3539
4151
|
const JobChangeEventDTO = z.object({
|
|
3540
4152
|
jobId: z.string(),
|
|
3541
4153
|
status: z.string(),
|
|
@@ -3569,6 +4181,9 @@ const operationValidators = {
|
|
|
3569
4181
|
GetJobStatus: (data) => {
|
|
3570
4182
|
if (data.jobStatus) JobInfoDTO.parse(data.jobStatus);
|
|
3571
4183
|
},
|
|
4184
|
+
EvaluateActions: (data) => {
|
|
4185
|
+
if (data.evaluateActions) ActionEvaluationsDTO.parse(data.evaluateActions);
|
|
4186
|
+
},
|
|
3572
4187
|
CreateDocument: (data) => {
|
|
3573
4188
|
if (data.createDocument) PHDocumentDTO.parse(data.createDocument);
|
|
3574
4189
|
},
|
|
@@ -3631,7 +4246,7 @@ function createReactorGraphQLClient(url, fetchImpl = fetch, headers) {
|
|
|
3631
4246
|
}
|
|
3632
4247
|
//#endregion
|
|
3633
4248
|
//#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";
|
|
4249
|
+
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
4250
|
//#endregion
|
|
3636
4251
|
//#region src/graphql/reactor/pubsub.ts
|
|
3637
4252
|
const pubSub = new PubSub();
|
|
@@ -3769,6 +4384,46 @@ var ReactorSubgraph = class extends BaseSubgraph {
|
|
|
3769
4384
|
if (!await this.canReadDocument(documentId, ctx)) forbidden.add(documentId);
|
|
3770
4385
|
}
|
|
3771
4386
|
}
|
|
4387
|
+
/**
|
|
4388
|
+
* Enforces which subject a sync channel answers to, adopting an unclaimed one.
|
|
4389
|
+
*
|
|
4390
|
+
* A channel is a private queue: it names a collection, and it accumulates
|
|
4391
|
+
* exactly the operations one subject is owed, so serving it to a second
|
|
4392
|
+
* subject would hand over that subject's cursor as well as its contents. An
|
|
4393
|
+
* unbound channel -- created anonymously, or created before binding existed --
|
|
4394
|
+
* is claimed by the first authenticated subject to use it, and answers to that
|
|
4395
|
+
* address alone thereafter. Adoption is safe precisely because serving
|
|
4396
|
+
* withholds rather than consumes: before it was claimed the channel served
|
|
4397
|
+
* only what an anonymous subject may read, and everything withheld is still
|
|
4398
|
+
* queued for the adopter.
|
|
4399
|
+
*
|
|
4400
|
+
* The refusal is the shape a read denial takes, so a puller treats it as a
|
|
4401
|
+
* signal to authenticate again rather than as a transport failure.
|
|
4402
|
+
*
|
|
4403
|
+
* Nothing is enforced or adopted without a serving gate. Below
|
|
4404
|
+
* `authEnforcement` there is no policy being enforced for the channel to
|
|
4405
|
+
* belong to, and refusing a poll there would break sync for no gain.
|
|
4406
|
+
*/
|
|
4407
|
+
async #bindOrRefuseChannel(channelId, ctx) {
|
|
4408
|
+
if (!this.syncServingGate) return;
|
|
4409
|
+
let remote;
|
|
4410
|
+
try {
|
|
4411
|
+
remote = this.syncManager.getById(channelId);
|
|
4412
|
+
} catch {
|
|
4413
|
+
return;
|
|
4414
|
+
}
|
|
4415
|
+
const bound = remote.meta.options.boundAddress;
|
|
4416
|
+
const address = ctx.user?.address;
|
|
4417
|
+
if (bound === void 0) {
|
|
4418
|
+
if (address !== void 0) await this.syncManager.bindRemote(channelId, address);
|
|
4419
|
+
return;
|
|
4420
|
+
}
|
|
4421
|
+
if (bound !== address) throw new ForbiddenError("to poll this sync channel");
|
|
4422
|
+
}
|
|
4423
|
+
/** The address a channel created by this request belongs to, if any. */
|
|
4424
|
+
#creatorBinding(ctx) {
|
|
4425
|
+
return this.syncServingGate ? ctx.user?.address : void 0;
|
|
4426
|
+
}
|
|
3772
4427
|
typeDefs = gql(schema_default);
|
|
3773
4428
|
resolvers = {
|
|
3774
4429
|
PHDocument: { operations: async (parent, args, ctx) => {
|
|
@@ -3787,7 +4442,7 @@ var ReactorSubgraph = class extends BaseSubgraph {
|
|
|
3787
4442
|
return await documentOperations(this.reactorClient, {
|
|
3788
4443
|
filter,
|
|
3789
4444
|
paging: args.paging
|
|
3790
|
-
});
|
|
4445
|
+
}, this.viewSubject(ctx));
|
|
3791
4446
|
} catch (error) {
|
|
3792
4447
|
this.logger.error("Error in PHDocument.operations: @Error", error);
|
|
3793
4448
|
throw error;
|
|
@@ -3810,7 +4465,7 @@ var ReactorSubgraph = class extends BaseSubgraph {
|
|
|
3810
4465
|
return await document(this.reactorClient, {
|
|
3811
4466
|
...args,
|
|
3812
4467
|
identifier: handle.fetchIdentifier
|
|
3813
|
-
});
|
|
4468
|
+
}, this.viewSubject(ctx));
|
|
3814
4469
|
} catch (error) {
|
|
3815
4470
|
this.logger.error("Error in document: @Error", error);
|
|
3816
4471
|
throw error;
|
|
@@ -3823,7 +4478,7 @@ var ReactorSubgraph = class extends BaseSubgraph {
|
|
|
3823
4478
|
return await documentOutgoingRelationships(this.reactorClient, {
|
|
3824
4479
|
...args,
|
|
3825
4480
|
sourceIdentifier: handle.fetchIdentifier
|
|
3826
|
-
});
|
|
4481
|
+
}, this.viewSubject(ctx));
|
|
3827
4482
|
} catch (error) {
|
|
3828
4483
|
this.logger.error("Error in documentOutgoingRelationships: @Error", error);
|
|
3829
4484
|
throw error;
|
|
@@ -3836,7 +4491,7 @@ var ReactorSubgraph = class extends BaseSubgraph {
|
|
|
3836
4491
|
const result = await documentIncomingRelationships(this.reactorClient, {
|
|
3837
4492
|
...args,
|
|
3838
4493
|
targetIdentifier: handle.fetchIdentifier
|
|
3839
|
-
});
|
|
4494
|
+
}, this.viewSubject(ctx));
|
|
3840
4495
|
if (!this.authorizationService.isSupremeAdmin(ctx.user?.address)) {
|
|
3841
4496
|
const filteredItems = [];
|
|
3842
4497
|
for (const item of result.items) if (await this.canReadDocument(item.id, ctx)) filteredItems.push(item);
|
|
@@ -3857,7 +4512,7 @@ var ReactorSubgraph = class extends BaseSubgraph {
|
|
|
3857
4512
|
const result = await findDocuments(this.reactorClient, {
|
|
3858
4513
|
...args,
|
|
3859
4514
|
search: args.search ?? {}
|
|
3860
|
-
});
|
|
4515
|
+
}, this.viewSubject(ctx));
|
|
3861
4516
|
if (!this.authorizationService.isSupremeAdmin(ctx.user?.address)) {
|
|
3862
4517
|
const filteredItems = [];
|
|
3863
4518
|
for (const item of result.items) if (await this.canReadDocument(item.id, ctx)) filteredItems.push(item);
|
|
@@ -3891,12 +4546,25 @@ var ReactorSubgraph = class extends BaseSubgraph {
|
|
|
3891
4546
|
...args.filter,
|
|
3892
4547
|
documentId: handle.fetchIdentifier
|
|
3893
4548
|
}
|
|
3894
|
-
});
|
|
4549
|
+
}, this.viewSubject(ctx));
|
|
3895
4550
|
} catch (error) {
|
|
3896
4551
|
this.logger.error("Error in documentOperations: @Error", error);
|
|
3897
4552
|
throw error;
|
|
3898
4553
|
}
|
|
3899
4554
|
},
|
|
4555
|
+
evaluateActions: async (_parent, args, ctx) => {
|
|
4556
|
+
this.logger.debug("evaluateActions(@args)", args);
|
|
4557
|
+
try {
|
|
4558
|
+
const handle = await this.assertCanRead(args.documentIdentifier, ctx);
|
|
4559
|
+
return await evaluateActions(this.reactorClient, {
|
|
4560
|
+
...args,
|
|
4561
|
+
documentIdentifier: handle.fetchIdentifier
|
|
4562
|
+
}, this.viewSubject(ctx));
|
|
4563
|
+
} catch (error) {
|
|
4564
|
+
this.logger.error("Error in evaluateActions: @Error", error);
|
|
4565
|
+
throw error;
|
|
4566
|
+
}
|
|
4567
|
+
},
|
|
3900
4568
|
pollSyncEnvelopes: async (_parent, args, ctx) => {
|
|
3901
4569
|
this.logger.debug("pollSyncEnvelopes(@args)", args);
|
|
3902
4570
|
try {
|
|
@@ -3909,12 +4577,14 @@ var ReactorSubgraph = class extends BaseSubgraph {
|
|
|
3909
4577
|
const driveId = remote.meta.collectionId.driveId;
|
|
3910
4578
|
const isAdmin = this.authorizationService.isSupremeAdmin(ctx.user?.address);
|
|
3911
4579
|
if (!isAdmin) await this.assertCanReadCanonical(driveId, ctx);
|
|
4580
|
+
await this.#bindOrRefuseChannel(args.channelId, ctx);
|
|
3912
4581
|
const forbiddenIds = /* @__PURE__ */ new Set();
|
|
3913
4582
|
if (!isAdmin) {
|
|
3914
4583
|
if (await this.authorizationService.canRead(driveId, void 0)) await this.#collectForbiddenDocuments(remote.channel.outbox.items, forbiddenIds, ctx);
|
|
3915
4584
|
await this.#collectForbiddenDocuments(remote.channel.deadLetter.items, forbiddenIds, ctx);
|
|
3916
4585
|
}
|
|
3917
|
-
const
|
|
4586
|
+
const heldOpIds = this.syncServingGate ? await collectHeldSyncOperations([...remote.channel.outbox.items, ...remote.channel.deadLetter.items], this.syncServingGate, this.viewSubject(ctx)) : /* @__PURE__ */ new Set();
|
|
4587
|
+
const { envelopes, ackOrdinal, deadLetters, hasMore } = pollSyncEnvelopes(this.syncManager, args, forbiddenIds, heldOpIds);
|
|
3918
4588
|
return {
|
|
3919
4589
|
envelopes,
|
|
3920
4590
|
ackOrdinal,
|
|
@@ -3960,6 +4630,32 @@ var ReactorSubgraph = class extends BaseSubgraph {
|
|
|
3960
4630
|
throw error;
|
|
3961
4631
|
}
|
|
3962
4632
|
},
|
|
4633
|
+
execute: async (_parent, args, ctx) => {
|
|
4634
|
+
this.logger.debug("execute(@args)", args);
|
|
4635
|
+
try {
|
|
4636
|
+
const handle = await this.assertCanExecuteOperations(args.documentIdentifier, args.actions, ctx);
|
|
4637
|
+
return await execute(this.reactorClient, {
|
|
4638
|
+
...args,
|
|
4639
|
+
documentIdentifier: handle.fetchIdentifier
|
|
4640
|
+
});
|
|
4641
|
+
} catch (error) {
|
|
4642
|
+
this.logger.error("Error in execute(@args): @Error", args, error);
|
|
4643
|
+
throw error;
|
|
4644
|
+
}
|
|
4645
|
+
},
|
|
4646
|
+
executeAsync: async (_parent, args, ctx) => {
|
|
4647
|
+
this.logger.debug("executeAsync(@args)", args);
|
|
4648
|
+
try {
|
|
4649
|
+
const handle = await this.assertCanExecuteOperations(args.documentIdentifier, args.actions, ctx);
|
|
4650
|
+
return await executeAsync(this.reactorClient, {
|
|
4651
|
+
...args,
|
|
4652
|
+
documentIdentifier: handle.fetchIdentifier
|
|
4653
|
+
});
|
|
4654
|
+
} catch (error) {
|
|
4655
|
+
this.logger.error("Error in executeAsync(@args): @Error", args, error);
|
|
4656
|
+
throw error;
|
|
4657
|
+
}
|
|
4658
|
+
},
|
|
3963
4659
|
mutateDocument: async (_parent, args, ctx) => {
|
|
3964
4660
|
this.logger.debug("mutateDocument(@args)", args);
|
|
3965
4661
|
try {
|
|
@@ -4093,7 +4789,8 @@ var ReactorSubgraph = class extends BaseSubgraph {
|
|
|
4093
4789
|
const driveId = DriveCollectionId.fromKey(args.input.collectionId).driveId;
|
|
4094
4790
|
await this.assertCanReadCanonical(driveId, ctx);
|
|
4095
4791
|
}
|
|
4096
|
-
|
|
4792
|
+
await this.#bindOrRefuseChannel(args.input.id, ctx);
|
|
4793
|
+
return await touchChannel(this.syncManager, args, this.#creatorBinding(ctx));
|
|
4097
4794
|
} catch (error) {
|
|
4098
4795
|
this.logger.error("Error in touchChannel(@args): @Error", args, error);
|
|
4099
4796
|
throw error;
|
|
@@ -4205,10 +4902,10 @@ const ADMIN_USERS = getAdminUsers();
|
|
|
4205
4902
|
//#endregion
|
|
4206
4903
|
//#region src/graphql/system/version.ts
|
|
4207
4904
|
function getVersion() {
|
|
4208
|
-
return "6.2.2-
|
|
4905
|
+
return "6.2.2-staging.0";
|
|
4209
4906
|
}
|
|
4210
4907
|
function getGitHash() {
|
|
4211
|
-
return "
|
|
4908
|
+
return "1537bf32c27de170e3df4a03abd40dd720abc65c";
|
|
4212
4909
|
}
|
|
4213
4910
|
function getGitUrl() {
|
|
4214
4911
|
return buildTreeUrl(getGitHash());
|
|
@@ -4306,6 +5003,13 @@ var HttpPackageLoader = class {
|
|
|
4306
5003
|
this.logger.verbose(`Loaded ${models.length} document models from ${packageName}`);
|
|
4307
5004
|
return models;
|
|
4308
5005
|
}
|
|
5006
|
+
async loadUpgradeManifests(packageSpec) {
|
|
5007
|
+
const { name: packageName } = this.parsePackageSpec(packageSpec);
|
|
5008
|
+
if (!this.isValidPackageName(packageName)) throw new Error(`Invalid package name: ${packageName}`);
|
|
5009
|
+
const manifests = extractUpgradeManifests(await import(`${this.registryUrl}-/cdn/${packageSpec}/node/document-models/index.mjs`));
|
|
5010
|
+
if (manifests.length > 0) this.logger.verbose(`Loaded ${manifests.length} upgrade manifests from ${packageName}`);
|
|
5011
|
+
return manifests;
|
|
5012
|
+
}
|
|
4309
5013
|
async loadSubgraphs(packageSpec) {
|
|
4310
5014
|
const { name: packageName } = this.parsePackageSpec(packageSpec);
|
|
4311
5015
|
if (!this.isValidPackageName(packageName)) throw new Error(`Invalid package name: ${packageName}`);
|
|
@@ -4431,7 +5135,7 @@ var ImportPackageLoader = class {
|
|
|
4431
5135
|
this.logger.verbose(`Loading document models from package: ${identifier}`);
|
|
4432
5136
|
const pkgModule = await loadDocumentModels(identifier);
|
|
4433
5137
|
if (pkgModule) {
|
|
4434
|
-
const models = Object.values(pkgModule);
|
|
5138
|
+
const models = Object.values(pkgModule).filter((m) => m !== null && typeof m === "object" && "documentModel" in m);
|
|
4435
5139
|
this.logger.verbose(` ➜ Loaded ${models.length} Document Models from: ${identifier}`);
|
|
4436
5140
|
return models;
|
|
4437
5141
|
} else {
|
|
@@ -4439,6 +5143,13 @@ var ImportPackageLoader = class {
|
|
|
4439
5143
|
return [];
|
|
4440
5144
|
}
|
|
4441
5145
|
}
|
|
5146
|
+
async loadUpgradeManifests(identifier) {
|
|
5147
|
+
const pkgModule = await loadDocumentModels(identifier);
|
|
5148
|
+
if (!pkgModule) return [];
|
|
5149
|
+
const manifests = extractUpgradeManifests(pkgModule);
|
|
5150
|
+
if (manifests.length > 0) this.logger.verbose(` ➜ Loaded ${manifests.length} Upgrade Manifests from: ${identifier}`);
|
|
5151
|
+
return manifests;
|
|
5152
|
+
}
|
|
4442
5153
|
async loadSubgraphs(identifier) {
|
|
4443
5154
|
this.logger.verbose(`Loading subgraphs from package: ${identifier}`);
|
|
4444
5155
|
const pkgModule = await loadSubgraphs(identifier);
|
|
@@ -4487,13 +5198,19 @@ function isExpectedLoaderMiss(error, pkg) {
|
|
|
4487
5198
|
}
|
|
4488
5199
|
function getUniqueDocumentModels(...documentModels) {
|
|
4489
5200
|
const uniqueModels = /* @__PURE__ */ new Map();
|
|
4490
|
-
for (const models of documentModels) for (const model of models) uniqueModels.set(model.documentModel.global.id
|
|
5201
|
+
for (const models of documentModels) for (const model of models) uniqueModels.set(`${model.documentModel.global.id}@${model.version ?? 1}`, model);
|
|
4491
5202
|
return Array.from(uniqueModels.values());
|
|
4492
5203
|
}
|
|
5204
|
+
function getUniqueUpgradeManifests(...upgradeManifests) {
|
|
5205
|
+
const uniqueManifests = /* @__PURE__ */ new Map();
|
|
5206
|
+
for (const manifests of upgradeManifests) for (const manifest of manifests) uniqueManifests.set(manifest.documentType, manifest);
|
|
5207
|
+
return Array.from(uniqueManifests.values());
|
|
5208
|
+
}
|
|
4493
5209
|
var PackageManager = class {
|
|
4494
5210
|
logger = childLogger(["reactor-api", "package-manager"]);
|
|
4495
5211
|
loaders;
|
|
4496
5212
|
docModelsMap = /* @__PURE__ */ new Map();
|
|
5213
|
+
upgradeManifestsMap = /* @__PURE__ */ new Map();
|
|
4497
5214
|
subgraphsMap = /* @__PURE__ */ new Map();
|
|
4498
5215
|
processorMap = /* @__PURE__ */ new Map();
|
|
4499
5216
|
configWatcher;
|
|
@@ -4511,8 +5228,10 @@ var PackageManager = class {
|
|
|
4511
5228
|
async loadPackages(packages) {
|
|
4512
5229
|
this.logger.info("Loading packages: @packages", packages.join(", "));
|
|
4513
5230
|
const documentModelsMap = await this.loadDocumentModels(packages);
|
|
5231
|
+
const upgradeManifestsMap = await this.loadUpgradeManifests(packages);
|
|
4514
5232
|
const subgraphsMap = await this.loadSubgraphs(packages);
|
|
4515
5233
|
const processorsMap = await this.loadProcessors(packages);
|
|
5234
|
+
this.upgradeManifestsMap = upgradeManifestsMap;
|
|
4516
5235
|
this.updatePackagesMap(documentModelsMap);
|
|
4517
5236
|
this.updateSubgraphsMap(subgraphsMap);
|
|
4518
5237
|
this.updateProcessorsMap(processorsMap);
|
|
@@ -4523,6 +5242,7 @@ var PackageManager = class {
|
|
|
4523
5242
|
}
|
|
4524
5243
|
return {
|
|
4525
5244
|
documentModels: getUniqueDocumentModels(...Array.from(documentModelsMap.values())),
|
|
5245
|
+
upgradeManifests: this.getUniqueUpgradeManifests(),
|
|
4526
5246
|
subgraphs: subgraphsMap,
|
|
4527
5247
|
processors: processorsMap
|
|
4528
5248
|
};
|
|
@@ -4533,6 +5253,7 @@ var PackageManager = class {
|
|
|
4533
5253
|
documentModelModuleMap.set("document-drive", [driveDocumentModelModule]);
|
|
4534
5254
|
documentModelModuleMap.set("document-model", [documentModelDocumentModelModule]);
|
|
4535
5255
|
documentModelModuleMap.set("reactor-drive", [reactorDriveDocumentModelModule]);
|
|
5256
|
+
documentModelModuleMap.set("reactor-group", [ReactorGroupV1]);
|
|
4536
5257
|
for (const pkg of packages) {
|
|
4537
5258
|
const allDocumentModels = [];
|
|
4538
5259
|
const failures = [];
|
|
@@ -4555,6 +5276,26 @@ var PackageManager = class {
|
|
|
4555
5276
|
}
|
|
4556
5277
|
return documentModelModuleMap;
|
|
4557
5278
|
}
|
|
5279
|
+
/** Upgrade manifests currently loaded across all packages, one per type. */
|
|
5280
|
+
getUniqueUpgradeManifests() {
|
|
5281
|
+
return getUniqueUpgradeManifests(...Array.from(this.upgradeManifestsMap.values()));
|
|
5282
|
+
}
|
|
5283
|
+
async loadUpgradeManifests(packages) {
|
|
5284
|
+
const manifestsMap = /* @__PURE__ */ new Map();
|
|
5285
|
+
for (const pkg of packages) for (const loader of this.loaders) {
|
|
5286
|
+
if (!loader.loadUpgradeManifests) continue;
|
|
5287
|
+
try {
|
|
5288
|
+
const manifests = await loader.loadUpgradeManifests(pkg);
|
|
5289
|
+
if (manifests.length > 0) {
|
|
5290
|
+
manifestsMap.set(pkg, manifests);
|
|
5291
|
+
break;
|
|
5292
|
+
}
|
|
5293
|
+
} catch (error) {
|
|
5294
|
+
this.logger.debug(`[${loader.name}] Failed to load upgrade manifests from package @pkg: @error`, pkg, error);
|
|
5295
|
+
}
|
|
5296
|
+
}
|
|
5297
|
+
return manifestsMap;
|
|
5298
|
+
}
|
|
4558
5299
|
async loadSubgraphs(packages) {
|
|
4559
5300
|
this.logger.debug(`Loading subgraphs from packages: ${packages.join(", ")}`);
|
|
4560
5301
|
const subgraphsMap = /* @__PURE__ */ new Map();
|
|
@@ -4601,7 +5342,6 @@ var PackageManager = class {
|
|
|
4601
5342
|
this.maybeWarnAllLoadersFailed("processors", pkg, succeeded, failures);
|
|
4602
5343
|
processorsMap.set(pkg, allProcessors);
|
|
4603
5344
|
}
|
|
4604
|
-
this.updateProcessorsMap(processorsMap);
|
|
4605
5345
|
return processorsMap;
|
|
4606
5346
|
}
|
|
4607
5347
|
maybeWarnAllLoadersFailed(kind, pkg, succeeded, failures) {
|
|
@@ -4616,23 +5356,58 @@ var PackageManager = class {
|
|
|
4616
5356
|
async updateDocumentModelsForPackage(pkg) {
|
|
4617
5357
|
this.logger.debug(`Updating document models for package: ${pkg}`);
|
|
4618
5358
|
const documentModels = await this.loadDocumentModels([pkg]);
|
|
5359
|
+
const upgradeManifests = await this.loadUpgradeManifests([pkg]);
|
|
5360
|
+
const upgradeManifestsMap = new Map(this.upgradeManifestsMap);
|
|
5361
|
+
const pkgManifests = upgradeManifests.get(pkg) ?? [];
|
|
5362
|
+
if (pkgManifests.length === 0) upgradeManifestsMap.delete(pkg);
|
|
5363
|
+
else upgradeManifestsMap.set(pkg, pkgManifests);
|
|
5364
|
+
this.upgradeManifestsMap = upgradeManifestsMap;
|
|
4619
5365
|
const documentModelsMap = new Map(this.docModelsMap);
|
|
4620
|
-
|
|
5366
|
+
const pkgModels = documentModels.get(pkg) ?? [];
|
|
5367
|
+
if (pkgModels.length === 0) documentModelsMap.delete(pkg);
|
|
5368
|
+
else documentModelsMap.set(pkg, pkgModels);
|
|
4621
5369
|
this.updatePackagesMap(documentModelsMap);
|
|
4622
5370
|
}
|
|
5371
|
+
async updateSubgraphsForPackage(pkg) {
|
|
5372
|
+
this.logger.debug(`Updating subgraphs for package: ${pkg}`);
|
|
5373
|
+
const subgraphs = await this.loadSubgraphs([pkg]);
|
|
5374
|
+
const subgraphsMap = new Map(this.subgraphsMap);
|
|
5375
|
+
const pkgSubgraphs = subgraphs.get(pkg) ?? [];
|
|
5376
|
+
if (pkgSubgraphs.length === 0) subgraphsMap.delete(pkg);
|
|
5377
|
+
else subgraphsMap.set(pkg, pkgSubgraphs);
|
|
5378
|
+
this.updateSubgraphsMap(subgraphsMap);
|
|
5379
|
+
}
|
|
5380
|
+
async updateProcessorsForPackage(pkg) {
|
|
5381
|
+
this.logger.debug(`Updating processors for package: ${pkg}`);
|
|
5382
|
+
const processors = await this.loadProcessors([pkg]);
|
|
5383
|
+
const processorsMap = new Map(this.processorMap);
|
|
5384
|
+
const pkgProcessors = processors.get(pkg) ?? [];
|
|
5385
|
+
if (pkgProcessors.length === 0) processorsMap.delete(pkg);
|
|
5386
|
+
else processorsMap.set(pkg, pkgProcessors);
|
|
5387
|
+
this.updateProcessorsMap(processorsMap);
|
|
5388
|
+
}
|
|
5389
|
+
/** Debounced per key so repeated subscribePackages calls reuse one timer. */
|
|
5390
|
+
getDebouncedUpdateCallback(key, update) {
|
|
5391
|
+
if (!this.debouncedUpdateCallbacks.has(key)) {
|
|
5392
|
+
const debouncedFn = debounce(update, 1e3);
|
|
5393
|
+
this.debouncedUpdateCallbacks.set(key, () => {
|
|
5394
|
+
debouncedFn().catch((error) => {
|
|
5395
|
+
this.logger.error("Failed to update package: @error", error);
|
|
5396
|
+
});
|
|
5397
|
+
});
|
|
5398
|
+
}
|
|
5399
|
+
return this.debouncedUpdateCallbacks.get(key);
|
|
5400
|
+
}
|
|
4623
5401
|
subscribePackages(packages) {
|
|
4624
5402
|
const unsubs = [];
|
|
4625
5403
|
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);
|
|
5404
|
+
const onDocumentModels = this.getDebouncedUpdateCallback(`${pkg}:document-models`, () => this.updateDocumentModelsForPackage(pkg));
|
|
5405
|
+
const onSubgraphs = this.getDebouncedUpdateCallback(`${pkg}:subgraphs`, () => this.updateSubgraphsForPackage(pkg));
|
|
5406
|
+
const onProcessors = this.getDebouncedUpdateCallback(`${pkg}:processors`, () => this.updateProcessorsForPackage(pkg));
|
|
5407
|
+
for (const loader of this.loaders) {
|
|
5408
|
+
if (loader.onDocumentModelsChange) unsubs.push(loader.onDocumentModelsChange(pkg, onDocumentModels));
|
|
5409
|
+
if (loader.onSubgraphsChange) unsubs.push(loader.onSubgraphsChange(pkg, onSubgraphs));
|
|
5410
|
+
if (loader.onProcessorsChange) unsubs.push(loader.onProcessorsChange(pkg, onProcessors));
|
|
4636
5411
|
}
|
|
4637
5412
|
}
|
|
4638
5413
|
return () => {
|
|
@@ -4680,6 +5455,28 @@ var PackageManager = class {
|
|
|
4680
5455
|
this.processorMap = processorsMap;
|
|
4681
5456
|
this.eventEmitter.emit("processorsChange", processorsMap);
|
|
4682
5457
|
}
|
|
5458
|
+
/**
|
|
5459
|
+
* Remove `pkg` from all package maps and emit the change events, tearing
|
|
5460
|
+
* down everything the package registered (subgraphs, processors, document
|
|
5461
|
+
* models). For an uninstalled package no loader can serve it again, so
|
|
5462
|
+
* nothing can re-add it.
|
|
5463
|
+
*/
|
|
5464
|
+
removePackage(pkg) {
|
|
5465
|
+
this.logger.info(`Removing package: ${pkg}`);
|
|
5466
|
+
const dropKey = (map) => {
|
|
5467
|
+
if (!map.has(pkg)) return map;
|
|
5468
|
+
const next = new Map(map);
|
|
5469
|
+
next.delete(pkg);
|
|
5470
|
+
return next;
|
|
5471
|
+
};
|
|
5472
|
+
this.docModelsMap = dropKey(this.docModelsMap);
|
|
5473
|
+
this.upgradeManifestsMap = dropKey(this.upgradeManifestsMap);
|
|
5474
|
+
this.subgraphsMap = dropKey(this.subgraphsMap);
|
|
5475
|
+
this.processorMap = dropKey(this.processorMap);
|
|
5476
|
+
this.updatePackagesMap(this.docModelsMap);
|
|
5477
|
+
this.updateSubgraphsMap(this.subgraphsMap);
|
|
5478
|
+
this.updateProcessorsMap(this.processorMap);
|
|
5479
|
+
}
|
|
4683
5480
|
onDocumentModelsChange(handler) {
|
|
4684
5481
|
this.eventEmitter.on("documentModelsChange", handler);
|
|
4685
5482
|
}
|
|
@@ -4695,6 +5492,104 @@ var PackageManager = class {
|
|
|
4695
5492
|
const config = { basePath: process.env.BASE_PATH || "/" };
|
|
4696
5493
|
const DefaultCoreSubgraphs = [AnalyticsSubgraph, SystemSubgraph];
|
|
4697
5494
|
//#endregion
|
|
5495
|
+
//#region src/attachment-backend.ts
|
|
5496
|
+
/** Filesystem is the default; explicit S3 is readiness-gated without fallback. */
|
|
5497
|
+
async function createStartupAttachmentBackend({ db, env = process.env, s3Dependencies }) {
|
|
5498
|
+
const storage = parseAttachmentStorageConfig(env);
|
|
5499
|
+
if (storage.kind === "filesystem") return void 0;
|
|
5500
|
+
const backend = createS3AttachmentBackend(db.withSchema("attachments"), storage.s3, s3Dependencies);
|
|
5501
|
+
if (!(await backend.health()).ready) throw new Error("S3 attachment backend readiness check failed");
|
|
5502
|
+
return backend;
|
|
5503
|
+
}
|
|
5504
|
+
//#endregion
|
|
5505
|
+
//#region src/services/attachment-access.service.ts
|
|
5506
|
+
const HASH_PATTERN = /^[a-f0-9]{64}$/;
|
|
5507
|
+
/**
|
|
5508
|
+
* Composes document authorization with the projected document/ref
|
|
5509
|
+
* relationship. Order is fixed: validate ref, resolve the canonical document
|
|
5510
|
+
* id, check `canRead`, then check the reference index. A denied document
|
|
5511
|
+
* never reaches the reference reader, and the facade never touches
|
|
5512
|
+
* attachment metadata, storage backends, or presigners.
|
|
5513
|
+
*/
|
|
5514
|
+
var AttachmentAccessService = class {
|
|
5515
|
+
constructor(resolveCanonicalId, authorization, references, projection) {
|
|
5516
|
+
this.resolveCanonicalId = resolveCanonicalId;
|
|
5517
|
+
this.authorization = authorization;
|
|
5518
|
+
this.references = references;
|
|
5519
|
+
this.projection = projection;
|
|
5520
|
+
}
|
|
5521
|
+
async canReadAttachment(request) {
|
|
5522
|
+
if (this.projection.status !== "available") return { kind: "projection-unavailable" };
|
|
5523
|
+
const ref = normalizeAttachmentRef(request.attachmentRef);
|
|
5524
|
+
if (ref === null) return { kind: "denied" };
|
|
5525
|
+
let documentId;
|
|
5526
|
+
try {
|
|
5527
|
+
documentId = await this.resolveCanonicalId(request.documentId);
|
|
5528
|
+
} catch {
|
|
5529
|
+
return { kind: "denied" };
|
|
5530
|
+
}
|
|
5531
|
+
if (!await this.authorization.canRead(documentId, request.userAddress)) return { kind: "denied" };
|
|
5532
|
+
if (!await this.references.hasReference(documentId, ref)) return { kind: "denied" };
|
|
5533
|
+
return {
|
|
5534
|
+
kind: "allowed",
|
|
5535
|
+
documentId,
|
|
5536
|
+
ref
|
|
5537
|
+
};
|
|
5538
|
+
}
|
|
5539
|
+
};
|
|
5540
|
+
/**
|
|
5541
|
+
* Parses and canonicalizes a caller-supplied ref through the shared parser.
|
|
5542
|
+
* Returns null for anything that is not a v1 ref over a 64-char SHA-256 hex
|
|
5543
|
+
* hash; hex case is normalized so index lookups use the canonical form.
|
|
5544
|
+
*/
|
|
5545
|
+
function normalizeAttachmentRef(value) {
|
|
5546
|
+
let hash;
|
|
5547
|
+
let version;
|
|
5548
|
+
try {
|
|
5549
|
+
const parsed = parseRef(value);
|
|
5550
|
+
hash = parsed.hash.toLowerCase();
|
|
5551
|
+
version = parsed.version;
|
|
5552
|
+
} catch {
|
|
5553
|
+
return null;
|
|
5554
|
+
}
|
|
5555
|
+
if (version !== 1 || !HASH_PATTERN.test(hash)) return null;
|
|
5556
|
+
return createRef(hash, version);
|
|
5557
|
+
}
|
|
5558
|
+
//#endregion
|
|
5559
|
+
//#region src/graphql/gateway/require-auth-middleware.ts
|
|
5560
|
+
/**
|
|
5561
|
+
* Returns a fetch middleware that rejects anonymous callers with a 401.
|
|
5562
|
+
*
|
|
5563
|
+
* This is the enforcement half of the resolve-and-don't-enforce split set up
|
|
5564
|
+
* by `RESOLVE_CALLER_IDENTITY`: the auth middleware must run before this
|
|
5565
|
+
* one — it verifies the bearer (an invalid token is still a 401 from
|
|
5566
|
+
* there) and records the resolved caller on the request. This middleware
|
|
5567
|
+
* then refuses the request when no caller was resolved, so under `OPEN`
|
|
5568
|
+
* (or `AUTH_ENABLED` with identity resolution) the whole generic surface —
|
|
5569
|
+
* document CRUD, sync, every custom subgraph — is reachable only by
|
|
5570
|
+
* authenticated callers, without locking out non-admins the way
|
|
5571
|
+
* `ADMIN_ONLY` does.
|
|
5572
|
+
*
|
|
5573
|
+
* It composes in one place in the fetch chain, so it covers every mounted
|
|
5574
|
+
* subgraph, the supergraph, and the SSE handler alike.
|
|
5575
|
+
*
|
|
5576
|
+
* OPTIONS is let through: a CORS preflight never carries a bearer, and
|
|
5577
|
+
* `AuthService.authenticateRequest` already answers it with an anonymous
|
|
5578
|
+
* context — refusing it would break every cross-origin client with a 401.
|
|
5579
|
+
*
|
|
5580
|
+
* Fails closed: a request that reaches this middleware without a resolved
|
|
5581
|
+
* context has no caller, and is rejected. The server never composes this
|
|
5582
|
+
* middleware without the auth middleware in the chain (see
|
|
5583
|
+
* `assertRequireAuthenticatedCallerAllowed` in `server.ts`).
|
|
5584
|
+
*/
|
|
5585
|
+
function createRequireAuthFetchMiddleware() {
|
|
5586
|
+
return (next) => async (request) => {
|
|
5587
|
+
if (request.method === "OPTIONS") return next(request);
|
|
5588
|
+
if (getAuthContext(request)?.user === void 0) return new globalThis.Response(JSON.stringify({ error: "Authentication required" }), { status: 401 });
|
|
5589
|
+
return next(request);
|
|
5590
|
+
};
|
|
5591
|
+
}
|
|
5592
|
+
//#endregion
|
|
4698
5593
|
//#region src/migrations/001_create_document_permissions.ts
|
|
4699
5594
|
var _001_create_document_permissions_exports = /* @__PURE__ */ __exportAll({
|
|
4700
5595
|
down: () => down$2,
|
|
@@ -4915,17 +5810,27 @@ var AuthService = class {
|
|
|
4915
5810
|
constructor(config) {
|
|
4916
5811
|
this.config = config;
|
|
4917
5812
|
}
|
|
4918
|
-
|
|
4919
|
-
|
|
5813
|
+
/**
|
|
5814
|
+
* Whether this service reads the bearer at all. Defaults to `enabled`, so a
|
|
5815
|
+
* config written before `resolveIdentity` existed behaves exactly as it did.
|
|
5816
|
+
*/
|
|
5817
|
+
get resolvesIdentity() {
|
|
5818
|
+
return this.config.resolveIdentity ?? this.config.enabled;
|
|
5819
|
+
}
|
|
5820
|
+
/** The context an unresolved request gets: no user, and no admin list to match against. */
|
|
5821
|
+
anonymousContext() {
|
|
5822
|
+
return {
|
|
4920
5823
|
user: void 0,
|
|
4921
5824
|
admins: [],
|
|
4922
|
-
auth_enabled:
|
|
5825
|
+
auth_enabled: this.config.enabled
|
|
4923
5826
|
};
|
|
4924
|
-
|
|
4925
|
-
|
|
5827
|
+
}
|
|
5828
|
+
async authenticateRequest(request) {
|
|
5829
|
+
if (!this.resolvesIdentity) return this.anonymousContext();
|
|
5830
|
+
if (request.method === "OPTIONS") return {
|
|
4926
5831
|
user: void 0,
|
|
4927
5832
|
admins: this.config.admins,
|
|
4928
|
-
auth_enabled:
|
|
5833
|
+
auth_enabled: this.config.enabled
|
|
4929
5834
|
};
|
|
4930
5835
|
return this.verifyBearer(request.headers.get("authorization") ?? void 0);
|
|
4931
5836
|
}
|
|
@@ -4934,16 +5839,12 @@ var AuthService = class {
|
|
|
4934
5839
|
* middleware that must enforce authentication on every request.
|
|
4935
5840
|
*/
|
|
4936
5841
|
async verifyBearer(authorization) {
|
|
4937
|
-
if (!this.
|
|
4938
|
-
user: void 0,
|
|
4939
|
-
admins: [],
|
|
4940
|
-
auth_enabled: false
|
|
4941
|
-
};
|
|
5842
|
+
if (!this.resolvesIdentity) return this.anonymousContext();
|
|
4942
5843
|
const token = authorization?.split(" ")[1];
|
|
4943
5844
|
if (!token) return {
|
|
4944
5845
|
user: void 0,
|
|
4945
5846
|
admins: this.config.admins,
|
|
4946
|
-
auth_enabled:
|
|
5847
|
+
auth_enabled: this.config.enabled
|
|
4947
5848
|
};
|
|
4948
5849
|
try {
|
|
4949
5850
|
const verified = await this.verifyToken(token);
|
|
@@ -4956,16 +5857,19 @@ var AuthService = class {
|
|
|
4956
5857
|
return {
|
|
4957
5858
|
user,
|
|
4958
5859
|
admins: this.config.admins,
|
|
4959
|
-
auth_enabled:
|
|
5860
|
+
auth_enabled: this.config.enabled
|
|
4960
5861
|
};
|
|
4961
5862
|
} catch {
|
|
4962
5863
|
return new Response(JSON.stringify({ error: "Authentication failed" }), { status: 401 });
|
|
4963
5864
|
}
|
|
4964
5865
|
}
|
|
4965
5866
|
async authenticateWebSocketConnection(connectionParams) {
|
|
4966
|
-
if (!this.
|
|
5867
|
+
if (!this.resolvesIdentity) return null;
|
|
4967
5868
|
const authHeader = connectionParams.authorization;
|
|
4968
|
-
if (!authHeader)
|
|
5869
|
+
if (!authHeader) {
|
|
5870
|
+
if (!this.config.enabled) return null;
|
|
5871
|
+
throw new Error("Missing authorization in connection parameters");
|
|
5872
|
+
}
|
|
4969
5873
|
const token = authHeader.split(" ")[1];
|
|
4970
5874
|
if (!token) throw new Error("Invalid authorization format");
|
|
4971
5875
|
const verified = await this.verifyToken(token);
|
|
@@ -4984,16 +5888,23 @@ var AuthService = class {
|
|
|
4984
5888
|
return await verifyAuthBearerToken(token);
|
|
4985
5889
|
}
|
|
4986
5890
|
/**
|
|
4987
|
-
* Extract user information from verification result
|
|
5891
|
+
* Extract user information from verification result.
|
|
5892
|
+
*
|
|
5893
|
+
* The issuer is required along with the credential subject. A credential that
|
|
5894
|
+
* verified must carry one -- resolving it is how the signature was checked --
|
|
5895
|
+
* so this rejects a shape that cannot have come from verification rather than
|
|
5896
|
+
* admitting a keyless principal.
|
|
4988
5897
|
*/
|
|
4989
5898
|
extractUserFromVerification(verified) {
|
|
4990
5899
|
try {
|
|
4991
5900
|
const { address, chainId, networkId } = verified.verifiableCredential.credentialSubject;
|
|
4992
|
-
|
|
5901
|
+
const appKey = verified.issuer;
|
|
5902
|
+
if (!address || !chainId || !networkId || !appKey) return null;
|
|
4993
5903
|
return {
|
|
4994
5904
|
address,
|
|
4995
5905
|
chainId,
|
|
4996
|
-
networkId
|
|
5906
|
+
networkId,
|
|
5907
|
+
appKey
|
|
4997
5908
|
};
|
|
4998
5909
|
} catch {
|
|
4999
5910
|
return null;
|
|
@@ -5042,14 +5953,33 @@ var AuthService = class {
|
|
|
5042
5953
|
}
|
|
5043
5954
|
}
|
|
5044
5955
|
async fetchCredentialExists(address, chainId, appId) {
|
|
5045
|
-
|
|
5956
|
+
if (!this.config.verifyCredential) throw new Error("AuthService: credential verification is enabled but no verifyCredential was provided");
|
|
5957
|
+
return this.config.verifyCredential({
|
|
5046
5958
|
address,
|
|
5047
5959
|
chainId,
|
|
5048
|
-
|
|
5049
|
-
})
|
|
5960
|
+
appId
|
|
5961
|
+
});
|
|
5050
5962
|
}
|
|
5051
5963
|
};
|
|
5052
5964
|
//#endregion
|
|
5965
|
+
//#region src/services/renown-credential-verifier.ts
|
|
5966
|
+
async function createRenownCredentialVerifier(config = {}) {
|
|
5967
|
+
const switchboardUrl = await resolveSwitchboardEndpoint({
|
|
5968
|
+
switchboardUrl: config.switchboardUrl,
|
|
5969
|
+
baseUrl: config.renownUrl
|
|
5970
|
+
});
|
|
5971
|
+
return async ({ address, chainId, appId }) => {
|
|
5972
|
+
return await fetchDelegationCredential({
|
|
5973
|
+
address,
|
|
5974
|
+
chainId,
|
|
5975
|
+
appDid: appId,
|
|
5976
|
+
switchboardUrl,
|
|
5977
|
+
baseUrl: switchboardUrl ? void 0 : config.renownUrl,
|
|
5978
|
+
discover: false
|
|
5979
|
+
}) !== void 0;
|
|
5980
|
+
};
|
|
5981
|
+
}
|
|
5982
|
+
//#endregion
|
|
5053
5983
|
//#region src/services/document-permission.service.ts
|
|
5054
5984
|
/**
|
|
5055
5985
|
* Service for managing document-level permissions.
|
|
@@ -5365,7 +6295,35 @@ function createMcpRequestAuthorizer(authService, authorizationService) {
|
|
|
5365
6295
|
};
|
|
5366
6296
|
}
|
|
5367
6297
|
//#endregion
|
|
6298
|
+
//#region src/services/renown-config.ts
|
|
6299
|
+
/** Reading credentials from this reactor's own read model needs a host-supplied
|
|
6300
|
+
* verifier: core has no idea which subgraph serves that read model. */
|
|
6301
|
+
function assertCredentialVerifierForSource(source, hasVerifier) {
|
|
6302
|
+
if (source !== "self" || hasVerifier) return;
|
|
6303
|
+
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.");
|
|
6304
|
+
}
|
|
6305
|
+
function envOverride(value) {
|
|
6306
|
+
const trimmed = value?.trim();
|
|
6307
|
+
return trimmed ? trimmed : void 0;
|
|
6308
|
+
}
|
|
6309
|
+
/** Resolve `auth.renown`; RENOWN_SOURCE / RENOWN_URL / SWITCHBOARD_URL win
|
|
6310
|
+
* over the config file, which wins over the SDK defaults. */
|
|
6311
|
+
function resolveRenownConfig(configured, env, logger) {
|
|
6312
|
+
return {
|
|
6313
|
+
source: resolveRenownSource(envOverride(env.RENOWN_SOURCE) ?? configured?.source, logger),
|
|
6314
|
+
url: envOverride(env.RENOWN_URL) ?? configured?.url,
|
|
6315
|
+
switchboardUrl: envOverride(env.SWITCHBOARD_URL) ?? configured?.switchboardUrl
|
|
6316
|
+
};
|
|
6317
|
+
}
|
|
6318
|
+
function resolveRenownSource(value, logger) {
|
|
6319
|
+
if (value === void 0 || value === null) return "remote";
|
|
6320
|
+
if (value === "self" || value === "remote") return value;
|
|
6321
|
+
logger.warn(`Ignoring invalid renown source "${value}" (expected "self" or "remote") — using "remote"`);
|
|
6322
|
+
return "remote";
|
|
6323
|
+
}
|
|
6324
|
+
//#endregion
|
|
5368
6325
|
//#region src/utils/db.ts
|
|
6326
|
+
const PGLITE_UTC_PARSERS = { 1114: (value) => /* @__PURE__ */ new Date(`${value.replace(" ", "T")}Z`) };
|
|
5369
6327
|
function isPG(connectionString) {
|
|
5370
6328
|
if (connectionString.startsWith("postgresql://") || connectionString.startsWith("postgres://")) return true;
|
|
5371
6329
|
return false;
|
|
@@ -5396,7 +6354,10 @@ function getDbClient(connectionString = void 0, pgliteFactory) {
|
|
|
5396
6354
|
if (cached) return cached;
|
|
5397
6355
|
const isPg = connectionString && isPG(connectionString);
|
|
5398
6356
|
const client = isPg ? "pg" : ClientPgLite;
|
|
5399
|
-
const pgliteInstance = isPg ? void 0 : pgliteFactory ? pgliteFactory(connectionString) : connectionString ? new PGlite({
|
|
6357
|
+
const pgliteInstance = isPg ? void 0 : pgliteFactory ? pgliteFactory(connectionString) : connectionString ? new PGlite({
|
|
6358
|
+
fs: new AtomicNodeFs(connectionString),
|
|
6359
|
+
parsers: PGLITE_UTC_PARSERS
|
|
6360
|
+
}) : new PGlite({ parsers: PGLITE_UTC_PARSERS });
|
|
5400
6361
|
const connection = isPg ? { connectionString } : { pglite: pgliteInstance };
|
|
5401
6362
|
if (connectionString && !isPg) {
|
|
5402
6363
|
const dirPath = path.resolve(connectionString, "..");
|
|
@@ -5502,12 +6463,23 @@ function assertAuthRequiredForDocumentPermissions(authEnabled, documentPermissio
|
|
|
5502
6463
|
* opt-in: it removes the only binding between a token's claimed address and its
|
|
5503
6464
|
* signing key. Fail-closed — unset NODE_ENV counts as production.
|
|
5504
6465
|
*/
|
|
5505
|
-
function assertSkipCredentialVerificationAllowed(
|
|
5506
|
-
if (!
|
|
6466
|
+
function assertSkipCredentialVerificationAllowed(resolvesCallerIdentity, skipCredentialVerification, env) {
|
|
6467
|
+
if (!resolvesCallerIdentity || !skipCredentialVerification) return;
|
|
5507
6468
|
const inAutomatedTest = env.VITEST === "true" || env.NODE_ENV === "test";
|
|
5508
6469
|
const acknowledged = env.ALLOW_INSECURE_SKIP_CREDENTIAL_VERIFICATION === "true";
|
|
5509
6470
|
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.");
|
|
5510
6471
|
}
|
|
6472
|
+
/**
|
|
6473
|
+
* Requiring an authenticated caller requires identity resolution to exist at
|
|
6474
|
+
* all: with neither AUTH_ENABLED nor RESOLVE_CALLER_IDENTITY the middleware
|
|
6475
|
+
* never reads a bearer, no `user` is ever resolved, and the
|
|
6476
|
+
* require-authenticated-caller middleware would reject every caller —
|
|
6477
|
+
* authenticated ones included. Refuse to boot rather than run broken.
|
|
6478
|
+
*/
|
|
6479
|
+
function assertRequireAuthenticatedCallerAllowed(requireAuthenticatedCaller, resolvesCallerIdentity) {
|
|
6480
|
+
if (!requireAuthenticatedCaller || resolvesCallerIdentity) return;
|
|
6481
|
+
throw new Error("REQUIRE_AUTHENTICATED_CALLER is set but refused: it rejects every request without a resolved caller, and with neither AUTH_ENABLED nor RESOLVE_CALLER_IDENTITY the server never reads a bearer, so it would reject every caller, including authenticated ones. Enable identity resolution first (RESOLVE_CALLER_IDENTITY=true or AUTH_ENABLED=true).");
|
|
6482
|
+
}
|
|
5511
6483
|
function createReadinessGate() {
|
|
5512
6484
|
let ready = false;
|
|
5513
6485
|
return {
|
|
@@ -5517,6 +6489,15 @@ function createReadinessGate() {
|
|
|
5517
6489
|
}
|
|
5518
6490
|
};
|
|
5519
6491
|
}
|
|
6492
|
+
/**
|
|
6493
|
+
* The GraphiQL explorer page's mount prefix. `path.posix.join` normalizes the
|
|
6494
|
+
* join with `basePath` — a naive template literal would produce `//explorer`
|
|
6495
|
+
* for the default `/` basePath, which express compiles into a route that only
|
|
6496
|
+
* matches `//explorer`, leaving `GET /explorer` a 404.
|
|
6497
|
+
*/
|
|
6498
|
+
function getExplorerPrefix(basePath) {
|
|
6499
|
+
return path.posix.join(basePath, "explorer");
|
|
6500
|
+
}
|
|
5520
6501
|
function resolveAttachmentStoragePath(options) {
|
|
5521
6502
|
if (options.attachmentStoragePath) return options.attachmentStoragePath;
|
|
5522
6503
|
if (options.dbPath && !options.dbPath.startsWith("postgres")) return path.resolve(options.dbPath, "..", "attachments");
|
|
@@ -5553,12 +6534,33 @@ function makeDbClosers(knexInstance, pglite) {
|
|
|
5553
6534
|
return closers;
|
|
5554
6535
|
}
|
|
5555
6536
|
/**
|
|
6537
|
+
* The gate sync serving evaluates a document's own policy through, or undefined
|
|
6538
|
+
* when there is none to evaluate.
|
|
6539
|
+
*
|
|
6540
|
+
* It is built here rather than taken off the reactor client because it is not
|
|
6541
|
+
* the same gate reads use: it carries the host's closes-by-default setting,
|
|
6542
|
+
* which withholds the domain scopes of a document nobody has policied yet. That
|
|
6543
|
+
* answer belongs to serving alone -- replay must keep reading an uninitialized
|
|
6544
|
+
* document in full -- so the two gates are deliberately separate objects over
|
|
6545
|
+
* the same model.
|
|
6546
|
+
*
|
|
6547
|
+
* Undefined below `authEnforcement`, where the registered model ignores the auth
|
|
6548
|
+
* scope: gating through it would serve every domain scope of a policied document
|
|
6549
|
+
* to anyone, which is worse than not gating at all.
|
|
6550
|
+
*/
|
|
6551
|
+
function buildSyncServingGate(reactorModule, authorizationConfig, logger) {
|
|
6552
|
+
if (!reactorModule) return;
|
|
6553
|
+
const model = readDecisionModel(reactorModule.featureFlags, reactorModule.documentModelRegistry);
|
|
6554
|
+
if (!model) return;
|
|
6555
|
+
return new SyncScopeGate(new ModelReadGate(model, reactorModule.documentView, reactorModule.featureFlags.authGroups, reactorModule.operationIndex, logger, { withholdUninitialized: authorizationConfig.defaultProtection }), reactorModule.documentView, logger);
|
|
6556
|
+
}
|
|
6557
|
+
/**
|
|
5556
6558
|
* Sets up the subgraph manager and registers subgraphs
|
|
5557
6559
|
*/
|
|
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);
|
|
5560
|
-
await graphqlManager.init(subgraphs.core, authFetchMiddleware);
|
|
5561
|
-
for (const [, collection] of subgraphs.extended.entries()) for (const subgraph of collection) await graphqlManager.registerSubgraph(subgraph, "graphql");
|
|
6560
|
+
async function setupGraphQLManager(httpAdapter, authFetchMiddleware, requireAuthFetchMiddleware, httpServer, wsServer, client, relationalDb, analyticsStore, syncManager, subgraphs, logger, authorizationService, authService, documentPermissionService, enableDocumentModelSubgraphs, port, reactorDriveClient, syncServingGate) {
|
|
6561
|
+
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);
|
|
6562
|
+
await graphqlManager.init(subgraphs.core, authFetchMiddleware, requireAuthFetchMiddleware);
|
|
6563
|
+
for (const [packageName, collection] of subgraphs.extended.entries()) for (const subgraph of collection) await graphqlManager.registerSubgraph(subgraph, "graphql", false, packageName);
|
|
5562
6564
|
await graphqlManager.updateRouter();
|
|
5563
6565
|
return graphqlManager;
|
|
5564
6566
|
}
|
|
@@ -5568,26 +6570,45 @@ async function setupGraphQLManager(httpAdapter, authFetchMiddleware, httpServer,
|
|
|
5568
6570
|
function setupEventListeners(pkgManager, graphqlManager, reactorProcessorManager, module, documentModelRegistry) {
|
|
5569
6571
|
pkgManager.onDocumentModelsChange((packagedModels) => {
|
|
5570
6572
|
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}`);
|
|
6573
|
+
const newModules = getUniqueDocumentModels(Object.values(packagedModels).flat());
|
|
6574
|
+
const incomingTypes = new Set(newModules.map((m) => m.documentModel.global.id));
|
|
6575
|
+
if (incomingTypes.size > 0) {
|
|
6576
|
+
documentModelRegistry.unregisterModules(...incomingTypes);
|
|
6577
|
+
const results = documentModelRegistry.registerModules(...newModules);
|
|
6578
|
+
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
6579
|
else defaultLogger.error(`Failed to register document model: ${result.error.message}`);
|
|
5579
6580
|
}
|
|
6581
|
+
const manifests = pkgManager.getUniqueUpgradeManifests();
|
|
6582
|
+
if (manifests.length > 0) {
|
|
6583
|
+
documentModelRegistry.unregisterUpgradeManifests(...manifests.map((m) => m.documentType));
|
|
6584
|
+
const manifestResults = documentModelRegistry.registerUpgradeManifests(...manifests);
|
|
6585
|
+
for (const result of manifestResults) if (result.status === "error") defaultLogger.error(`Failed to register upgrade manifest: ${result.error.message}`);
|
|
6586
|
+
}
|
|
5580
6587
|
}
|
|
5581
6588
|
graphqlManager.regenerateDocumentModelSubgraphs();
|
|
5582
6589
|
});
|
|
6590
|
+
let knownSubgraphPackages = /* @__PURE__ */ new Set();
|
|
5583
6591
|
pkgManager.onSubgraphsChange((packagedSubgraphs) => {
|
|
5584
6592
|
(async () => {
|
|
5585
|
-
for (const [, subgraphs] of packagedSubgraphs)
|
|
6593
|
+
for (const [packageName, subgraphs] of packagedSubgraphs) {
|
|
6594
|
+
const incomingNames = /* @__PURE__ */ new Set();
|
|
6595
|
+
for (const subgraph of subgraphs) {
|
|
6596
|
+
const instance = await graphqlManager.registerSubgraph(subgraph, "graphql", false, packageName);
|
|
6597
|
+
if (!instance) continue;
|
|
6598
|
+
incomingNames.add(instance.name);
|
|
6599
|
+
}
|
|
6600
|
+
await graphqlManager.prunePackageSubgraphs(packageName, incomingNames);
|
|
6601
|
+
}
|
|
6602
|
+
for (const packageName of knownSubgraphPackages) if (!packagedSubgraphs.has(packageName)) await graphqlManager.unregisterPackage(packageName);
|
|
6603
|
+
knownSubgraphPackages = new Set(packagedSubgraphs.keys());
|
|
5586
6604
|
await graphqlManager.updateRouter();
|
|
5587
6605
|
})();
|
|
5588
6606
|
});
|
|
6607
|
+
let knownProcessorPackages = /* @__PURE__ */ new Set();
|
|
5589
6608
|
pkgManager.onProcessorsChange((processors) => {
|
|
5590
6609
|
(async () => {
|
|
6610
|
+
for (const packageName of knownProcessorPackages) if (!processors.has(packageName)) await reactorProcessorManager.unregisterFactory(packageName);
|
|
6611
|
+
knownProcessorPackages = new Set(processors.keys());
|
|
5591
6612
|
for (const [packageName, fns] of processors) {
|
|
5592
6613
|
await reactorProcessorManager.unregisterFactory(packageName);
|
|
5593
6614
|
const validBuilders = fns.map((fn) => fn(module)).filter((factory) => typeof factory === "function");
|
|
@@ -5633,16 +6654,51 @@ async function _setupCommonInfrastructure(options) {
|
|
|
5633
6654
|
const logger = options.logger ?? defaultLogger;
|
|
5634
6655
|
let admins = [];
|
|
5635
6656
|
let authEnabled = false;
|
|
6657
|
+
let configuredResolveIdentity;
|
|
6658
|
+
let configuredRequireAuth;
|
|
6659
|
+
let configuredRenown;
|
|
5636
6660
|
if (options.configFile) {
|
|
5637
6661
|
const config = getConfig(options.configFile);
|
|
5638
6662
|
admins = config.auth?.admins.map((a) => a.toLowerCase()) ?? [];
|
|
5639
6663
|
authEnabled = config.auth?.enabled ?? false;
|
|
6664
|
+
configuredRenown = config.auth?.renown;
|
|
5640
6665
|
} else if (options.auth) {
|
|
5641
6666
|
admins = options.auth.admins.map((a) => a.toLowerCase());
|
|
5642
6667
|
authEnabled = options.auth.enabled;
|
|
6668
|
+
configuredResolveIdentity = options.auth.resolveIdentity;
|
|
6669
|
+
configuredRequireAuth = options.auth.requireAuthenticatedCaller;
|
|
5643
6670
|
}
|
|
5644
|
-
const { AUTH_ENABLED, ADMINS, DEFAULT_PROTECTION, DOCUMENT_PERMISSIONS_ENABLED, SKIP_CREDENTIAL_VERIFICATION, CREDENTIAL_VERIFICATION_CACHE_TTL_MS } = process.env;
|
|
6671
|
+
const { AUTH_ENABLED, RESOLVE_CALLER_IDENTITY, REQUIRE_AUTHENTICATED_CALLER, ADMINS, DEFAULT_PROTECTION, DOCUMENT_PERMISSIONS_ENABLED, SKIP_CREDENTIAL_VERIFICATION, CREDENTIAL_VERIFICATION_CACHE_TTL_MS } = process.env;
|
|
5645
6672
|
if (AUTH_ENABLED !== void 0) authEnabled = AUTH_ENABLED === "true";
|
|
6673
|
+
/**
|
|
6674
|
+
* Whether the auth middleware reads the bearer and populates `ctx.user`,
|
|
6675
|
+
* independently of the authorization policy.
|
|
6676
|
+
*
|
|
6677
|
+
* Defaults to `authEnabled`, so a deployment that never sets it behaves
|
|
6678
|
+
* exactly as it did: auth on means the token is read AND the policy is
|
|
6679
|
+
* `ADMIN_ONLY`; auth off means neither. Setting it explicitly is what
|
|
6680
|
+
* separates the two — a server can then know who is calling while the policy
|
|
6681
|
+
* stays `OPEN`, which is the combination custom subgraphs need and the one
|
|
6682
|
+
* `AUTH_ENABLED` alone cannot express.
|
|
6683
|
+
*/
|
|
6684
|
+
let resolveCallerIdentity = configuredResolveIdentity ?? authEnabled;
|
|
6685
|
+
if (RESOLVE_CALLER_IDENTITY !== void 0) resolveCallerIdentity = RESOLVE_CALLER_IDENTITY === "true";
|
|
6686
|
+
/**
|
|
6687
|
+
* Whether the require-authenticated-caller middleware is active: anonymous
|
|
6688
|
+
* callers are rejected with a 401 before any subgraph sees the request.
|
|
6689
|
+
*
|
|
6690
|
+
* Off by default, so nothing changes for existing deployments. This is the
|
|
6691
|
+
* enforcement half of the `RESOLVE_CALLER_IDENTITY` split: that one lets a
|
|
6692
|
+
* server know who is calling while the policy stays `OPEN`, and this one
|
|
6693
|
+
* closes the hole it leaves open — under `OPEN`, an anonymous caller
|
|
6694
|
+
* reaches the whole generic surface (document CRUD, sync, every custom
|
|
6695
|
+
* subgraph), because `OpenAuthorizationService` answers `true` to
|
|
6696
|
+
* everything. `ADMIN_ONLY` is the only alternative today, and it locks
|
|
6697
|
+
* out every non-admin; this expresses "authenticated callers allowed,
|
|
6698
|
+
* anonymous not".
|
|
6699
|
+
*/
|
|
6700
|
+
let requireAuthenticatedCaller = configuredRequireAuth ?? false;
|
|
6701
|
+
if (REQUIRE_AUTHENTICATED_CALLER !== void 0) requireAuthenticatedCaller = REQUIRE_AUTHENTICATED_CALLER === "true";
|
|
5646
6702
|
if (ADMINS !== void 0) admins = ADMINS.split(",").map((a) => a.toLowerCase());
|
|
5647
6703
|
let defaultProtection = false;
|
|
5648
6704
|
if (DEFAULT_PROTECTION !== void 0) defaultProtection = DEFAULT_PROTECTION.toLowerCase() === "true";
|
|
@@ -5656,30 +6712,46 @@ async function _setupCommonInfrastructure(options) {
|
|
|
5656
6712
|
}
|
|
5657
6713
|
const documentPermissionsRequested = options.documentPermissionService !== void 0 || DOCUMENT_PERMISSIONS_ENABLED === "true";
|
|
5658
6714
|
assertAuthRequiredForDocumentPermissions(authEnabled, documentPermissionsRequested);
|
|
5659
|
-
assertSkipCredentialVerificationAllowed(
|
|
6715
|
+
assertSkipCredentialVerificationAllowed(resolveCallerIdentity, skipCredentialVerification, process.env);
|
|
6716
|
+
assertRequireAuthenticatedCallerAllowed(requireAuthenticatedCaller, resolveCallerIdentity);
|
|
5660
6717
|
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
6718
|
httpAdapter.getRoute("/health", () => new Response("OK", { status: 200 }));
|
|
5662
6719
|
const readiness = createReadinessGate();
|
|
5663
6720
|
httpAdapter.getRoute("/ready", () => readiness.isReady() ? new Response("OK", { status: 200 }) : new Response("starting", { status: 503 }));
|
|
5664
|
-
const explorerPrefix =
|
|
6721
|
+
const explorerPrefix = getExplorerPrefix(config.basePath);
|
|
5665
6722
|
httpAdapter.getRoute(`${explorerPrefix}/:endpoint?`, (request) => {
|
|
5666
6723
|
const url = new URL(request.url);
|
|
5667
6724
|
const suffix = url.pathname.slice(explorerPrefix.length).replace(/^\//, "");
|
|
5668
6725
|
const endpoint = suffix ? `/${suffix}` : "/graphql";
|
|
5669
|
-
const
|
|
5670
|
-
|
|
6726
|
+
const explorerState = decodeExplorerUrlState(url.searchParams.get("explorerURLState") ?? "");
|
|
6727
|
+
const query = explorerState?.query ?? url.searchParams.get("query") ?? void 0;
|
|
6728
|
+
return new Response(renderGraphqlPlayground(endpoint, query, explorerState?.headers ?? {}, explorerState?.variables), { headers: { "Content-Type": "text/html" } });
|
|
5671
6729
|
});
|
|
5672
6730
|
let authFetchMiddleware;
|
|
6731
|
+
let requireAuthFetchMiddleware;
|
|
5673
6732
|
let authService;
|
|
5674
|
-
if (authEnabled) {
|
|
5675
|
-
logger.info("Setting up Auth middleware");
|
|
6733
|
+
if (resolveCallerIdentity || authEnabled) {
|
|
6734
|
+
logger.info("Setting up Auth middleware (policy enforcement: @enabled, caller identity: @resolve)", authEnabled, resolveCallerIdentity);
|
|
6735
|
+
const renown = options.renown ?? resolveRenownConfig(configuredRenown, process.env, logger);
|
|
6736
|
+
assertCredentialVerifierForSource(renown.source, options.verifyCredential !== void 0);
|
|
6737
|
+
if (options.verifyCredential) logger.info("Renown credentials will be verified by the host's verifier");
|
|
6738
|
+
else logger.info("Renown credentials will be verified against @url", renown.switchboardUrl ?? renown.url ?? "the default Renown instance");
|
|
5676
6739
|
authService = new AuthService({
|
|
5677
6740
|
enabled: authEnabled,
|
|
6741
|
+
resolveIdentity: resolveCallerIdentity,
|
|
5678
6742
|
admins,
|
|
5679
6743
|
skipCredentialVerification,
|
|
5680
|
-
credentialVerificationCacheTtlMs
|
|
6744
|
+
credentialVerificationCacheTtlMs,
|
|
6745
|
+
verifyCredential: options.verifyCredential ?? await createRenownCredentialVerifier({
|
|
6746
|
+
renownUrl: renown.url,
|
|
6747
|
+
switchboardUrl: renown.switchboardUrl
|
|
6748
|
+
})
|
|
5681
6749
|
});
|
|
5682
6750
|
authFetchMiddleware = createAuthFetchMiddleware(authService);
|
|
6751
|
+
if (requireAuthenticatedCaller) {
|
|
6752
|
+
requireAuthFetchMiddleware = createRequireAuthFetchMiddleware();
|
|
6753
|
+
logger.info("Require-authenticated-caller middleware enabled: anonymous callers are rejected with a 401 before any subgraph");
|
|
6754
|
+
}
|
|
5683
6755
|
}
|
|
5684
6756
|
const dbClosers = [];
|
|
5685
6757
|
const { relationalDb, analyticsStore, closers: analyticsClosers } = await initializeDatabaseAndAnalytics(options.dbPath, options.pgliteFactory);
|
|
@@ -5703,7 +6775,12 @@ async function _setupCommonInfrastructure(options) {
|
|
|
5703
6775
|
await mkdir(attachmentStoragePath, { recursive: true });
|
|
5704
6776
|
const { db: attachmentDb, knex: attachmentKnex, pglite: attachmentPglite } = getDbClient(options.dbPath, options.pgliteFactory);
|
|
5705
6777
|
dbClosers.push(...makeDbClosers(attachmentKnex, attachmentPglite));
|
|
5706
|
-
const
|
|
6778
|
+
const ATTACHMENT_SWEEP_INTERVAL_MS = 3600 * 1e3;
|
|
6779
|
+
const attachmentBackend = await createStartupAttachmentBackend({ db: attachmentDb });
|
|
6780
|
+
const attachmentBuilder = new AttachmentBuilder(attachmentDb, attachmentStoragePath).withReservationSweepMs(ATTACHMENT_SWEEP_INTERVAL_MS);
|
|
6781
|
+
if (attachmentBackend) attachmentBuilder.withBackend(attachmentBackend);
|
|
6782
|
+
const attachments = await attachmentBuilder.build();
|
|
6783
|
+
const attachmentReferenceIndex = await new AttachmentReferenceIndexBuilder(attachmentDb).build();
|
|
5707
6784
|
dbClosers.push(() => {
|
|
5708
6785
|
attachments.destroy();
|
|
5709
6786
|
return Promise.resolve();
|
|
@@ -5717,12 +6794,14 @@ async function _setupCommonInfrastructure(options) {
|
|
|
5717
6794
|
port,
|
|
5718
6795
|
httpAdapter,
|
|
5719
6796
|
authFetchMiddleware,
|
|
6797
|
+
requireAuthFetchMiddleware,
|
|
5720
6798
|
authService,
|
|
5721
6799
|
relationalDb,
|
|
5722
6800
|
analyticsStore,
|
|
5723
6801
|
documentPermissionService,
|
|
5724
6802
|
authorizationConfig,
|
|
5725
6803
|
attachments,
|
|
6804
|
+
attachmentReferenceIndex,
|
|
5726
6805
|
packages,
|
|
5727
6806
|
dbClosers,
|
|
5728
6807
|
readiness
|
|
@@ -5731,26 +6810,17 @@ async function _setupCommonInfrastructure(options) {
|
|
|
5731
6810
|
/**
|
|
5732
6811
|
* Private helper function containing common setup logic for API initialization
|
|
5733
6812
|
*/
|
|
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) {
|
|
6813
|
+
async function _setupAPI(reactorClient, syncManager, reactorProcessorManager, httpAdapter, authFetchMiddleware, requireAuthFetchMiddleware, authService, port, packages, relationalDb, analyticsStore, documentPermissionService, processors, subgraphs, options, processorApp, readModels, attachments, attachmentReferenceIndex, attachmentReferenceProjection, authorizationConfig, documentModelRegistry, dbClosers = [], reactorDriveClient, syncServingGate) {
|
|
5735
6814
|
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
|
-
}
|
|
6815
|
+
...createReactorHostModuleBase({
|
|
6816
|
+
client: reactorClient,
|
|
6817
|
+
readModels,
|
|
6818
|
+
relationalDb,
|
|
6819
|
+
analyticsStore,
|
|
6820
|
+
processorApp,
|
|
6821
|
+
config: options.processorConfig
|
|
6822
|
+
}),
|
|
6823
|
+
attachments: createAttachmentClient(attachments.service)
|
|
5754
6824
|
};
|
|
5755
6825
|
const mcpServerEnabled = options.mcp ?? true;
|
|
5756
6826
|
const logger = options.logger ?? defaultLogger;
|
|
@@ -5778,16 +6848,17 @@ async function _setupAPI(reactorClient, syncManager, reactorProcessorManager, ht
|
|
|
5778
6848
|
const { httpServer, wsServer } = await startServer(httpAdapter, port, options.https, logger);
|
|
5779
6849
|
const authorizationService = createAuthorizationService(authorizationConfig, documentPermissionService, createGetParentIdsFn(reactorClient));
|
|
5780
6850
|
logger.info(`Authorization service initialized (policy: ${authorizationConfig.policy})`);
|
|
6851
|
+
const attachmentAccess = new AttachmentAccessService(createCanonicalDocumentIdResolver(reactorClient), authorizationService, attachmentReferenceIndex.store, attachmentReferenceProjection);
|
|
5781
6852
|
const coreSubgraphs = DefaultCoreSubgraphs.slice();
|
|
5782
6853
|
coreSubgraphs.push(ReactorSubgraph);
|
|
5783
6854
|
if (documentPermissionService) {
|
|
5784
6855
|
coreSubgraphs.push(AuthSubgraph);
|
|
5785
6856
|
logger.info("Auth subgraph registered (document permissions enabled)");
|
|
5786
6857
|
}
|
|
5787
|
-
const graphqlManager = await setupGraphQLManager(httpAdapter, authFetchMiddleware, httpServer, wsServer, reactorClient, relationalDb, analyticsStore, syncManager, {
|
|
6858
|
+
const graphqlManager = await setupGraphQLManager(httpAdapter, authFetchMiddleware, requireAuthFetchMiddleware, httpServer, wsServer, reactorClient, relationalDb, analyticsStore, syncManager, {
|
|
5788
6859
|
extended: subgraphs,
|
|
5789
6860
|
core: coreSubgraphs
|
|
5790
|
-
}, logger.child(["graphql-manager"]), authorizationService, authService, documentPermissionService, options.enableDocumentModelSubgraphs, port, reactorDriveClient);
|
|
6861
|
+
}, logger.child(["graphql-manager"]), authorizationService, authService, documentPermissionService, options.enableDocumentModelSubgraphs, port, reactorDriveClient, syncServingGate);
|
|
5791
6862
|
setupEventListeners(packages, graphqlManager, reactorProcessorManager, hostModule, documentModelRegistry);
|
|
5792
6863
|
if (mcpServerEnabled) {
|
|
5793
6864
|
await setupMcpServer({
|
|
@@ -5802,6 +6873,8 @@ async function _setupAPI(reactorClient, syncManager, reactorProcessorManager, ht
|
|
|
5802
6873
|
graphqlManager,
|
|
5803
6874
|
packages,
|
|
5804
6875
|
attachments,
|
|
6876
|
+
attachmentReferenceIndex,
|
|
6877
|
+
attachmentAccess,
|
|
5805
6878
|
authService,
|
|
5806
6879
|
dispose: buildApiDispose({
|
|
5807
6880
|
graphqlManager,
|
|
@@ -5850,9 +6923,15 @@ function buildApiDispose(args) {
|
|
|
5850
6923
|
};
|
|
5851
6924
|
}
|
|
5852
6925
|
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
|
|
6926
|
+
const { port, httpAdapter, authFetchMiddleware, requireAuthFetchMiddleware, authService, relationalDb, analyticsStore, documentPermissionService, authorizationConfig, attachments, attachmentReferenceIndex, packages, dbClosers, readiness } = await _setupCommonInfrastructure(options);
|
|
6927
|
+
const { documentModels, upgradeManifests, processors, subgraphs } = await packages.init();
|
|
6928
|
+
const { module: reactorClientModule, reactorDriveClient, attachmentReferenceProjection = {
|
|
6929
|
+
status: "unavailable",
|
|
6930
|
+
reason: "initializer-did-not-report"
|
|
6931
|
+
} } = await clientInitializer(documentModels, {
|
|
6932
|
+
attachmentReferenceWriter: attachmentReferenceIndex.store,
|
|
6933
|
+
upgradeManifests
|
|
6934
|
+
});
|
|
5856
6935
|
const reactorClient = reactorClientModule.client;
|
|
5857
6936
|
const syncManager = reactorClientModule.reactorModule?.syncModule?.syncManager;
|
|
5858
6937
|
if (!syncManager) throw new Error("SyncManager not available from InProcessReactorClientModule");
|
|
@@ -5861,11 +6940,13 @@ async function initializeAndStartAPI(clientInitializer, options, processorApp) {
|
|
|
5861
6940
|
const documentModelRegistry = reactorClientModule.reactorModule?.documentModelRegistry;
|
|
5862
6941
|
if (!documentModelRegistry) throw new Error("DocumentModelRegistry not available from InProcessReactorClientModule");
|
|
5863
6942
|
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),
|
|
6943
|
+
...await _setupAPI(reactorClient, syncManager, reactorProcessorManager, httpAdapter, authFetchMiddleware, requireAuthFetchMiddleware, authService, port, packages, relationalDb, analyticsStore, documentPermissionService, processors, subgraphs, options, processorApp, (reactorClientModule.reactorModule?.readModelCoordinator)?.readModels ?? [], attachments, attachmentReferenceIndex, attachmentReferenceProjection, authorizationConfig, documentModelRegistry, dbClosers, reactorDriveClient, buildSyncServingGate(reactorClientModule.reactorModule, authorizationConfig, options.logger ?? defaultLogger)),
|
|
5865
6944
|
client: reactorClient,
|
|
5866
6945
|
syncManager,
|
|
5867
6946
|
documentModelRegistry,
|
|
5868
|
-
readiness
|
|
6947
|
+
readiness,
|
|
6948
|
+
attachmentReferenceProjection,
|
|
6949
|
+
packageManager: packages
|
|
5869
6950
|
};
|
|
5870
6951
|
}
|
|
5871
6952
|
//#endregion
|
|
@@ -5896,6 +6977,7 @@ var PackageManagementService = class {
|
|
|
5896
6977
|
defaultRegistryUrl;
|
|
5897
6978
|
httpLoader;
|
|
5898
6979
|
documentModelRegistry;
|
|
6980
|
+
packageManager;
|
|
5899
6981
|
logger = childLogger(["reactor-api", "package-management-service"]);
|
|
5900
6982
|
onModelsChanged;
|
|
5901
6983
|
loadedModulesCache = /* @__PURE__ */ new Map();
|
|
@@ -5904,6 +6986,7 @@ var PackageManagementService = class {
|
|
|
5904
6986
|
this.defaultRegistryUrl = options.defaultRegistryUrl;
|
|
5905
6987
|
this.httpLoader = options.httpLoader;
|
|
5906
6988
|
this.documentModelRegistry = options.documentModelRegistry;
|
|
6989
|
+
this.packageManager = options.packageManager;
|
|
5907
6990
|
}
|
|
5908
6991
|
setOnModelsChanged(callback) {
|
|
5909
6992
|
this.onModelsChanged = callback;
|
|
@@ -5946,6 +7029,7 @@ var PackageManagementService = class {
|
|
|
5946
7029
|
this.loadedModulesCache.delete(name);
|
|
5947
7030
|
if (this.httpLoader) this.httpLoader.documentModelLoader.removeFromCache(name);
|
|
5948
7031
|
if (this.documentModelRegistry) this.documentModelRegistry.unregisterModules(...existing.documentTypes);
|
|
7032
|
+
if (this.packageManager) this.packageManager.removePackage(name);
|
|
5949
7033
|
this.triggerModelsChanged();
|
|
5950
7034
|
this.logger.info("Uninstalled package @name", name);
|
|
5951
7035
|
return true;
|
|
@@ -5969,7 +7053,7 @@ var PackageManagementService = class {
|
|
|
5969
7053
|
}
|
|
5970
7054
|
};
|
|
5971
7055
|
//#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 };
|
|
7056
|
+
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, assertRequireAuthenticatedCallerAllowed, assertSkipCredentialVerificationAllowed, buildGraphQlDocument, buildGraphQlDriveDocument, buildGraphqlOperation, buildGraphqlOperations, buildSubgraphSchemaModule, createAuthFetchMiddleware, createCanonicalDocumentIdResolver, createGatewayAdapter, createHttpAdapter, createMergedSchema, createReactorGraphQLClient, createRenownCredentialVerifier, createSchema, decodeExplorerUrlState, definedNonNullAnySchema, driveIdFromUrl, extractSubgraphsFromModule, generateDocumentModelSchema, getAuthContext, getDbClient, getDocumentModelSchemaName, getDocumentModelTypeDefs, getExplorerPrefix, getGitHash, getGitUrl, getSdk, getUniqueDocumentModels, getUniqueUpgradeManifests, getVersion, initAnalyticsStoreSql, initializeAndStartAPI, isDefinedNonNullAny, isExpectedLoaderMiss, isSubgraphClass, parseDriveUrl, renderGraphqlPlayground, resolveRenownConfig };
|
|
5973
7057
|
|
|
5974
7058
|
//# sourceMappingURL=index.mjs.map
|
|
5975
|
-
//# debugId=
|
|
7059
|
+
//# debugId=f43e08dc-7398-57dd-8824-db5d97ccc6ca
|