@powerhousedao/reactor-api 6.0.0-dev.216 → 6.0.0-dev.217
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +24 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +172 -19
- package/dist/index.mjs.map +1 -1
- package/package.json +11 -11
package/dist/index.mjs
CHANGED
|
@@ -928,7 +928,7 @@ function parseBodyLimit(limit) {
|
|
|
928
928
|
var FastifyHttpAdapter = class {
|
|
929
929
|
#fetchRoutes = [];
|
|
930
930
|
#getRoutes = /* @__PURE__ */ new Map();
|
|
931
|
-
#nodeRoutes =
|
|
931
|
+
#nodeRoutes = [];
|
|
932
932
|
#setupOps = [];
|
|
933
933
|
#instance;
|
|
934
934
|
get handle() {
|
|
@@ -955,8 +955,11 @@ var FastifyHttpAdapter = class {
|
|
|
955
955
|
});
|
|
956
956
|
}
|
|
957
957
|
mountNodeRoute(method, path, handler) {
|
|
958
|
-
|
|
959
|
-
|
|
958
|
+
this.#nodeRoutes.push({
|
|
959
|
+
method,
|
|
960
|
+
matcher: match(normalizePath(path)),
|
|
961
|
+
handler
|
|
962
|
+
});
|
|
960
963
|
}
|
|
961
964
|
mountRawMiddleware(middleware) {
|
|
962
965
|
if (this.#instance) this.#instance.use(middleware);
|
|
@@ -1026,14 +1029,14 @@ var FastifyHttpAdapter = class {
|
|
|
1026
1029
|
#dispatch(req, reply) {
|
|
1027
1030
|
const pathname = new URL(req.url, "http://localhost").pathname;
|
|
1028
1031
|
const method = req.method.toUpperCase();
|
|
1029
|
-
const
|
|
1030
|
-
|
|
1031
|
-
const
|
|
1032
|
-
if (
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1032
|
+
for (const entry of this.#nodeRoutes) {
|
|
1033
|
+
if (entry.method !== method) continue;
|
|
1034
|
+
const result = entry.matcher(pathname);
|
|
1035
|
+
if (!result) continue;
|
|
1036
|
+
req.raw.params = result.params;
|
|
1037
|
+
reply.hijack();
|
|
1038
|
+
entry.handler(req.raw, reply.raw, req.body);
|
|
1039
|
+
return;
|
|
1037
1040
|
}
|
|
1038
1041
|
if (method === "GET") {
|
|
1039
1042
|
for (const entry of this.#getRoutes.values()) if (entry.matcher(pathname)) return this.#serveGetEntry(entry, req, reply);
|
|
@@ -1234,6 +1237,118 @@ var AuthService = class {
|
|
|
1234
1237
|
}
|
|
1235
1238
|
};
|
|
1236
1239
|
//#endregion
|
|
1240
|
+
//#region src/graphql/reactor/constants.ts
|
|
1241
|
+
/**
|
|
1242
|
+
* Document-type sentinel for drive documents. Drives are the unit
|
|
1243
|
+
* the LB shards on (via the `Drive-Id` request header) and the unit
|
|
1244
|
+
* the drive-ownership cache tracks on each switchboard instance.
|
|
1245
|
+
*/
|
|
1246
|
+
const DRIVE_DOCUMENT_TYPE = "powerhouse/document-drive";
|
|
1247
|
+
//#endregion
|
|
1248
|
+
//#region src/graphql/gateway/drive-ownership-cache.ts
|
|
1249
|
+
/**
|
|
1250
|
+
* In-memory record of which drives this switchboard instance owns.
|
|
1251
|
+
*
|
|
1252
|
+
* Populated at startup by walking the reactor for documents of type
|
|
1253
|
+
* `powerhouse/document-drive`. Mutated explicitly by resolver hooks
|
|
1254
|
+
* after successful drive create / delete operations. Read by the
|
|
1255
|
+
* drive-validation fetch middleware to short-circuit wrong-shard
|
|
1256
|
+
* requests with a structured 421 response.
|
|
1257
|
+
*/
|
|
1258
|
+
var DriveOwnershipCache = class {
|
|
1259
|
+
drives = /* @__PURE__ */ new Set();
|
|
1260
|
+
constructor(reactorClient) {
|
|
1261
|
+
this.reactorClient = reactorClient;
|
|
1262
|
+
}
|
|
1263
|
+
async init() {
|
|
1264
|
+
this.drives.clear();
|
|
1265
|
+
let page = await this.reactorClient.find({ type: DRIVE_DOCUMENT_TYPE });
|
|
1266
|
+
while (true) {
|
|
1267
|
+
for (const drive of page.results) this.drives.add(drive.header.id);
|
|
1268
|
+
if (!page.next) return;
|
|
1269
|
+
page = await page.next();
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
has(driveId) {
|
|
1273
|
+
return this.drives.has(driveId);
|
|
1274
|
+
}
|
|
1275
|
+
add(driveId) {
|
|
1276
|
+
this.drives.add(driveId);
|
|
1277
|
+
}
|
|
1278
|
+
remove(driveId) {
|
|
1279
|
+
this.drives.delete(driveId);
|
|
1280
|
+
}
|
|
1281
|
+
size() {
|
|
1282
|
+
return this.drives.size;
|
|
1283
|
+
}
|
|
1284
|
+
};
|
|
1285
|
+
//#endregion
|
|
1286
|
+
//#region src/graphql/gateway/drive-middleware.ts
|
|
1287
|
+
const DRIVE_ID_HEADER = "drive-id";
|
|
1288
|
+
/**
|
|
1289
|
+
* Operations that legitimately run before the target drive exists in the
|
|
1290
|
+
* cache. Drive creation is the obvious case (a brand-new drive cannot be
|
|
1291
|
+
* in the ownership set yet); other create-shaped operations that may
|
|
1292
|
+
* synthesize a drive must be added here too.
|
|
1293
|
+
*/
|
|
1294
|
+
const CACHE_BYPASS_OPERATIONS = new Set(["createDocument", "createEmptyDocument"]);
|
|
1295
|
+
const driveIdMap = /* @__PURE__ */ new WeakMap();
|
|
1296
|
+
/** Internal — only `graphql-manager.ts` should call this. */
|
|
1297
|
+
function getRequestDriveId(request) {
|
|
1298
|
+
return driveIdMap.get(request);
|
|
1299
|
+
}
|
|
1300
|
+
/**
|
|
1301
|
+
* Returns a fetch middleware that validates the `Drive-Id` header against
|
|
1302
|
+
* the in-memory ownership cache. Layout:
|
|
1303
|
+
*
|
|
1304
|
+
* - No header → pass through. The LB has already round-robined; nothing
|
|
1305
|
+
* to validate here.
|
|
1306
|
+
* - Header present and drive in cache → record on the request map (for
|
|
1307
|
+
* the context factory to read into `context.driveId`) and pass through.
|
|
1308
|
+
* - Header present, drive missing, but the operation is `createDocument`
|
|
1309
|
+
* or `createEmptyDocument` → pass through. The drive may be in the
|
|
1310
|
+
* process of being created.
|
|
1311
|
+
* - Otherwise → return `421 Misdirected Request` with a structured body.
|
|
1312
|
+
* The client (or LB) can surface this as a wrong-shard signal.
|
|
1313
|
+
*/
|
|
1314
|
+
function createDriveFetchMiddleware(cache) {
|
|
1315
|
+
return (next) => async (request) => {
|
|
1316
|
+
const driveId = request.headers.get(DRIVE_ID_HEADER) ?? "";
|
|
1317
|
+
if (driveId === "") return next(request);
|
|
1318
|
+
if (cache.has(driveId)) {
|
|
1319
|
+
driveIdMap.set(request, driveId);
|
|
1320
|
+
return next(request);
|
|
1321
|
+
}
|
|
1322
|
+
if (await isCacheBypassOperation(request)) return next(request);
|
|
1323
|
+
return wrongShardResponse(driveId);
|
|
1324
|
+
};
|
|
1325
|
+
}
|
|
1326
|
+
async function isCacheBypassOperation(request) {
|
|
1327
|
+
if (request.method !== "POST") return false;
|
|
1328
|
+
try {
|
|
1329
|
+
const body = await request.clone().json();
|
|
1330
|
+
if (typeof body.operationName === "string") return CACHE_BYPASS_OPERATIONS.has(body.operationName);
|
|
1331
|
+
if (typeof body.query === "string") return CACHE_BYPASS_OPERATIONS.has(extractOperationName(body.query));
|
|
1332
|
+
return false;
|
|
1333
|
+
} catch {
|
|
1334
|
+
return false;
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
const OPERATION_NAME_PATTERN = /\b(?:mutation|query|subscription)\s+(\w+)/;
|
|
1338
|
+
function extractOperationName(query) {
|
|
1339
|
+
const match = OPERATION_NAME_PATTERN.exec(query);
|
|
1340
|
+
return match ? match[1] : "";
|
|
1341
|
+
}
|
|
1342
|
+
function wrongShardResponse(driveId) {
|
|
1343
|
+
return new globalThis.Response(JSON.stringify({
|
|
1344
|
+
error: "wrong-shard",
|
|
1345
|
+
driveId
|
|
1346
|
+
}), {
|
|
1347
|
+
status: 421,
|
|
1348
|
+
headers: { "content-type": "application/json" }
|
|
1349
|
+
});
|
|
1350
|
+
}
|
|
1351
|
+
//#endregion
|
|
1237
1352
|
//#region src/utils/create-schema.ts
|
|
1238
1353
|
const logger = childLogger(["reactor-api", "create-schema"]);
|
|
1239
1354
|
/**
|
|
@@ -2722,7 +2837,6 @@ function matchesJobFilter(payload, args) {
|
|
|
2722
2837
|
}
|
|
2723
2838
|
//#endregion
|
|
2724
2839
|
//#region src/graphql/reactor/resolvers.ts
|
|
2725
|
-
const DRIVE_DOCUMENT_TYPE = "powerhouse/document-drive";
|
|
2726
2840
|
const POLL_SYNC_ENVELOPES_MAX_LIMIT = 100;
|
|
2727
2841
|
async function documentModels(reactorClient, args) {
|
|
2728
2842
|
const namespace = fromInputMaybe(args.namespace);
|
|
@@ -2919,7 +3033,7 @@ async function createDocument(reactorClient, args) {
|
|
|
2919
3033
|
const parentIdentifier = fromInputMaybe(args.parentIdentifier);
|
|
2920
3034
|
let result;
|
|
2921
3035
|
try {
|
|
2922
|
-
if (parentIdentifier) if ((await reactorClient.get(parentIdentifier)).header.documentType ===
|
|
3036
|
+
if (parentIdentifier) if ((await reactorClient.get(parentIdentifier)).header.documentType === "powerhouse/document-drive") result = await reactorClient.drives.addFile(parentIdentifier, document);
|
|
2923
3037
|
else result = await reactorClient.create(document, parentIdentifier);
|
|
2924
3038
|
else result = await reactorClient.create(document);
|
|
2925
3039
|
} catch (error) {
|
|
@@ -2936,7 +3050,7 @@ async function createEmptyDocument(reactorClient, args) {
|
|
|
2936
3050
|
const name = fromInputMaybe(args.name);
|
|
2937
3051
|
let result;
|
|
2938
3052
|
try {
|
|
2939
|
-
if (parentIdentifier) if ((await reactorClient.get(parentIdentifier)).header.documentType ===
|
|
3053
|
+
if (parentIdentifier) if ((await reactorClient.get(parentIdentifier)).header.documentType === "powerhouse/document-drive") {
|
|
2940
3054
|
const document = (await reactorClient.getDocumentModelModule(args.documentType)).utils.createDocument();
|
|
2941
3055
|
if (name) document.header.name = name;
|
|
2942
3056
|
result = await reactorClient.drives.addFile(parentIdentifier, document);
|
|
@@ -2983,7 +3097,7 @@ async function createDocumentWithInitialState(reactorClient, args) {
|
|
|
2983
3097
|
} catch (error) {
|
|
2984
3098
|
throw new GraphQLError(`Parent document not found: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
2985
3099
|
}
|
|
2986
|
-
if (parent.header.documentType ===
|
|
3100
|
+
if (parent.header.documentType === "powerhouse/document-drive") try {
|
|
2987
3101
|
result = await reactorClient.drives.addFile(parentIdentifier, document);
|
|
2988
3102
|
} catch (error) {
|
|
2989
3103
|
throw new GraphQLError(`Failed to create document in drive: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
@@ -3541,6 +3655,8 @@ var GraphQLManager = class {
|
|
|
3541
3655
|
authService = null;
|
|
3542
3656
|
subgraphWsDisposers = /* @__PURE__ */ new Map();
|
|
3543
3657
|
#authMiddleware;
|
|
3658
|
+
#driveMiddleware;
|
|
3659
|
+
driveOwnershipCache;
|
|
3544
3660
|
/** Cached document models for schema generation - updated on init and regenerate */
|
|
3545
3661
|
cachedDocumentModels = [];
|
|
3546
3662
|
subgraphHandlerCache = /* @__PURE__ */ new Map();
|
|
@@ -3561,11 +3677,15 @@ var GraphQLManager = class {
|
|
|
3561
3677
|
this.port = port;
|
|
3562
3678
|
this.authorizationService = authorizationService;
|
|
3563
3679
|
if (this.authConfig) this.authService = new AuthService(this.authConfig);
|
|
3680
|
+
this.driveOwnershipCache = new DriveOwnershipCache(this.reactorClient);
|
|
3564
3681
|
this.wsServer.setMaxListeners(0);
|
|
3565
3682
|
}
|
|
3566
3683
|
async init(coreSubgraphs, authMiddleware) {
|
|
3567
3684
|
this.#authMiddleware = authMiddleware;
|
|
3568
3685
|
this.logger.debug(`Initializing Subgraph Manager...`);
|
|
3686
|
+
await this.driveOwnershipCache.init();
|
|
3687
|
+
this.#driveMiddleware = createDriveFetchMiddleware(this.driveOwnershipCache);
|
|
3688
|
+
this.logger.debug(`Drive ownership cache populated with ${this.driveOwnershipCache.size()} drives`);
|
|
3569
3689
|
const models = (await this.reactorClient.getDocumentModelModules()).results;
|
|
3570
3690
|
this.cachedDocumentModels = models;
|
|
3571
3691
|
if (!models.find((it) => it.documentModel.global.name === "DocumentDrive")) throw new Error("DocumentDrive model required");
|
|
@@ -3725,6 +3845,7 @@ var GraphQLManager = class {
|
|
|
3725
3845
|
#makeContextFactory() {
|
|
3726
3846
|
return (request) => {
|
|
3727
3847
|
const authCtx = getAuthContext(request);
|
|
3848
|
+
const driveId = getRequestDriveId(request);
|
|
3728
3849
|
const headers = {};
|
|
3729
3850
|
request.headers.forEach((v, k) => {
|
|
3730
3851
|
headers[k] = v;
|
|
@@ -3733,6 +3854,7 @@ var GraphQLManager = class {
|
|
|
3733
3854
|
headers,
|
|
3734
3855
|
db: this.relationalDb,
|
|
3735
3856
|
...this.getAdditionalContextFields(),
|
|
3857
|
+
driveId,
|
|
3736
3858
|
user: authCtx?.user,
|
|
3737
3859
|
isAdmin: authCtx ? (addr) => !authCtx.auth_enabled ? true : authCtx.admins.includes(addr.toLowerCase()) : () => true
|
|
3738
3860
|
});
|
|
@@ -3771,7 +3893,7 @@ var GraphQLManager = class {
|
|
|
3771
3893
|
if (this.subgraphHandlerCache.has(subgraphPath)) continue;
|
|
3772
3894
|
const schema = createSchema(this.cachedDocumentModels, subgraph.resolvers, subgraph.typeDefs);
|
|
3773
3895
|
const rawHandler = await this.gatewayAdapter.createHandler(schema, this.#makeContextFactory());
|
|
3774
|
-
const fetchHandler = this.#
|
|
3896
|
+
const fetchHandler = this.#composeFetchMiddleware(rawHandler);
|
|
3775
3897
|
this.subgraphHandlerCache.set(subgraphPath, fetchHandler);
|
|
3776
3898
|
this.httpAdapter.mount(subgraphPath, fetchHandler);
|
|
3777
3899
|
if (subgraph.hasSubscriptions) {
|
|
@@ -3821,7 +3943,7 @@ var GraphQLManager = class {
|
|
|
3821
3943
|
async #createSupergraphGateway() {
|
|
3822
3944
|
const superGraphPath = path.join(this.path, "graphql");
|
|
3823
3945
|
const rawHandler = await this.gatewayAdapter.createSupergraphHandler(() => this.#getSubgraphDefinitions(), this.httpServer, this.#makeContextFactory());
|
|
3824
|
-
const fetchHandler = this.#
|
|
3946
|
+
const fetchHandler = this.#composeFetchMiddleware(rawHandler);
|
|
3825
3947
|
this.httpAdapter.mount(superGraphPath, fetchHandler);
|
|
3826
3948
|
this.#setupSupergraphSSE(superGraphPath);
|
|
3827
3949
|
if (!this.initialized) {
|
|
@@ -3861,9 +3983,20 @@ var GraphQLManager = class {
|
|
|
3861
3983
|
schema,
|
|
3862
3984
|
contextFactory: this.#makeContextFactory()
|
|
3863
3985
|
});
|
|
3864
|
-
const handler = this.#
|
|
3986
|
+
const handler = this.#composeFetchMiddleware(rawHandler);
|
|
3865
3987
|
this.httpAdapter.mount(ssePath, handler, { exact: true });
|
|
3866
3988
|
}
|
|
3989
|
+
/**
|
|
3990
|
+
* Compose the request-level fetch middleware chain. Auth runs first
|
|
3991
|
+
* (so we don't validate shard before knowing the request is even
|
|
3992
|
+
* authorized), drive-ownership validation runs after.
|
|
3993
|
+
*/
|
|
3994
|
+
#composeFetchMiddleware(rawHandler) {
|
|
3995
|
+
let handler = rawHandler;
|
|
3996
|
+
if (this.#driveMiddleware) handler = this.#driveMiddleware(handler);
|
|
3997
|
+
if (this.#authMiddleware) handler = this.#authMiddleware(handler);
|
|
3998
|
+
return handler;
|
|
3999
|
+
}
|
|
3867
4000
|
};
|
|
3868
4001
|
//#endregion
|
|
3869
4002
|
//#region src/graphql/packages/resolvers.ts
|
|
@@ -4379,6 +4512,21 @@ var ReactorSubgraph = class extends BaseSubgraph {
|
|
|
4379
4512
|
await this.assertCanExecuteOperation(documentId, operationType, ctx);
|
|
4380
4513
|
}
|
|
4381
4514
|
}
|
|
4515
|
+
/**
|
|
4516
|
+
* Returns the drive id when the given identifier (id or slug) refers
|
|
4517
|
+
* to a drive document, otherwise undefined. Used by deleteDocument to
|
|
4518
|
+
* decide whether to invalidate the drive-ownership cache after a
|
|
4519
|
+
* successful delete.
|
|
4520
|
+
*/
|
|
4521
|
+
async #resolveDriveId(identifier) {
|
|
4522
|
+
try {
|
|
4523
|
+
const doc = await this.reactorClient.get(identifier);
|
|
4524
|
+
if (doc.header.documentType === "powerhouse/document-drive") return doc.header.id;
|
|
4525
|
+
return;
|
|
4526
|
+
} catch {
|
|
4527
|
+
return;
|
|
4528
|
+
}
|
|
4529
|
+
}
|
|
4382
4530
|
typeDefs = gql(schema_default);
|
|
4383
4531
|
resolvers = {
|
|
4384
4532
|
PHDocument: { operations: async (parent, args, ctx) => {
|
|
@@ -4519,6 +4667,7 @@ var ReactorSubgraph = class extends BaseSubgraph {
|
|
|
4519
4667
|
if (!ctx.user?.address) throw new GraphQLError("Forbidden: authentication required to create documents");
|
|
4520
4668
|
} else if (!this.hasGlobalAdminAccess(ctx)) throw new GraphQLError("Forbidden: insufficient permissions to create documents");
|
|
4521
4669
|
const result = await createDocument(this.reactorClient, args);
|
|
4670
|
+
if (result?.id && result.documentType === "powerhouse/document-drive") this.graphqlManager.driveOwnershipCache.add(result.id);
|
|
4522
4671
|
if (this.authorizationService && ctx.user?.address && result?.id) await this.documentPermissionService?.initializeDocumentProtection(result.id, ctx.user.address, this.authorizationService.config.defaultProtection);
|
|
4523
4672
|
return result;
|
|
4524
4673
|
} catch (error) {
|
|
@@ -4536,6 +4685,7 @@ var ReactorSubgraph = class extends BaseSubgraph {
|
|
|
4536
4685
|
if (!ctx.user?.address) throw new GraphQLError("Forbidden: authentication required to create documents");
|
|
4537
4686
|
} else if (!this.hasGlobalAdminAccess(ctx)) throw new GraphQLError("Forbidden: insufficient permissions to create documents");
|
|
4538
4687
|
const result = await createEmptyDocument(this.reactorClient, args);
|
|
4688
|
+
if (result?.id && result.documentType === "powerhouse/document-drive") this.graphqlManager.driveOwnershipCache.add(result.id);
|
|
4539
4689
|
if (this.authorizationService && ctx.user?.address && result?.id) await this.documentPermissionService?.initializeDocumentProtection(result.id, ctx.user.address, this.authorizationService.config.defaultProtection);
|
|
4540
4690
|
return result;
|
|
4541
4691
|
} catch (error) {
|
|
@@ -4610,7 +4760,10 @@ var ReactorSubgraph = class extends BaseSubgraph {
|
|
|
4610
4760
|
this.logger.debug("deleteDocument(@args)", args);
|
|
4611
4761
|
try {
|
|
4612
4762
|
await this.assertCanWrite(args.identifier, ctx);
|
|
4613
|
-
|
|
4763
|
+
const driveIdToInvalidate = await this.#resolveDriveId(args.identifier);
|
|
4764
|
+
const result = await deleteDocument(this.reactorClient, args);
|
|
4765
|
+
if (result && driveIdToInvalidate) this.graphqlManager.driveOwnershipCache.remove(driveIdToInvalidate);
|
|
4766
|
+
return result;
|
|
4614
4767
|
} catch (error) {
|
|
4615
4768
|
this.logger.error("Error in deleteDocument(@args): @Error", error);
|
|
4616
4769
|
throw error;
|