@powerhousedao/reactor-api 6.2.2 → 6.2.3-dev.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,14 +1,19 @@
1
1
 
2
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="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";
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]="46ad8545-02e9-5fb8-b003-7b9b93f82f75")}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-XWVecaNn.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
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
- import path from "node:path";
10
+ import path, { join } from "node:path";
11
11
  import { match } from "path-to-regexp";
12
+ import posix from "node:path/posix";
13
+ import { Readable } from "node:stream";
14
+ import fs, { readFileSync, watchFile } from "node:fs";
15
+ import { pipeline } from "node:stream/promises";
16
+ import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
12
17
  import { buildSubgraphSchema } from "@apollo/subgraph";
13
18
  import { typeDefs } from "@powerhousedao/document-engineering/graphql";
14
19
  import { camelCase, kebabCase, pascalCase } from "change-case";
@@ -26,7 +31,6 @@ import { reactorDriveDocumentModelModule } from "@powerhousedao/reactor-drive";
26
31
  import { ReactorGroupV1 } from "@powerhousedao/reactor-group";
27
32
  import { driveDocumentModelModule } from "@powerhousedao/shared/document-drive";
28
33
  import EventEmitter from "node:events";
29
- import fs, { watchFile } from "node:fs";
30
34
  import { PostgresAnalyticsStore } from "@powerhousedao/analytics-engine-pg";
31
35
  import { AttachmentBuilder, AttachmentReferenceIndexBuilder, createRef, createS3AttachmentBackend, parseAttachmentStorageConfig, parseRef } from "@powerhousedao/reactor-attachments";
32
36
  import { createAttachmentClient } from "@powerhousedao/reactor-attachments/client";
@@ -319,16 +323,1101 @@ async function createGatewayAdapter(type, logger) {
319
323
  async function createHttpAdapter(type) {
320
324
  switch (type) {
321
325
  case "express": {
322
- const { createExpressHttpAdapter } = await import("./adapter-http-express-CgVPgEMm.mjs");
326
+ const { createExpressHttpAdapter } = await import("./adapter-http-express-54gdI9bu.mjs");
323
327
  return createExpressHttpAdapter();
324
328
  }
325
329
  case "fastify": {
326
- const { createFastifyHttpAdapter } = await import("./adapter-http-fastify-D98iRljY.mjs");
330
+ const { createFastifyHttpAdapter } = await import("./adapter-http-fastify-DmccBVOe.mjs");
327
331
  return createFastifyHttpAdapter();
328
332
  }
329
333
  }
330
334
  }
331
335
  //#endregion
336
+ //#region src/http/namespace.ts
337
+ /**
338
+ * A package's namespace is its npm name, used verbatim as one or two path
339
+ * segments. Nothing is slugged: dropping the scope, folding `.`/`_` to `-` or
340
+ * lower-casing would all map distinct package names onto one namespace, and a
341
+ * lossy mapping means two packages can silently claim the same URL space.
342
+ * Verbatim is injective, so there is no collision left to detect.
343
+ */
344
+ /**
345
+ * npm's package-name grammar, per segment. Deliberately the same shape the HTTP
346
+ * package loader validates with, so a name that can be installed can be routed.
347
+ */
348
+ const SEGMENT = /^[a-z0-9][-a-z0-9._]*$/i;
349
+ /** Segments that would read as path traversal rather than as a name. */
350
+ const TRAVERSAL = new Set([".", ".."]);
351
+ var InvalidNamespaceError = class extends Error {};
352
+ /**
353
+ * Turns a package name into the path segments it occupies.
354
+ * `@powerhousedao/workflow` → ["@powerhousedao", "workflow"]
355
+ * `document-model` → ["document-model"]
356
+ */
357
+ function namespaceSegments(packageName) {
358
+ if (!packageName) throw new InvalidNamespaceError("Package name is empty");
359
+ const scoped = packageName.startsWith("@");
360
+ const parts = (scoped ? packageName.slice(1) : packageName).split("/");
361
+ if (parts.length !== (scoped ? 2 : 1)) throw new InvalidNamespaceError(`"${packageName}" is not a package name: expected ${scoped ? "@scope/name" : "name"}`);
362
+ for (const part of parts) if (TRAVERSAL.has(part) || !SEGMENT.test(part)) throw new InvalidNamespaceError(`"${packageName}" contains a segment that is not a valid npm name part: "${part}"`);
363
+ return scoped ? [`@${parts[0]}`, parts[1]] : parts;
364
+ }
365
+ /**
366
+ * The path a package's routes hang off, relative to the package prefix.
367
+ * Scoped names occupy two segments — the addressing unpkg and jsdelivr use.
368
+ */
369
+ function namespacePath(packageName) {
370
+ return namespaceSegments(packageName).join("/");
371
+ }
372
+ /**
373
+ * The same path with the leading `@` percent-encoded. Both routers match the
374
+ * raw pathname, so a client that percent-encodes each segment would otherwise
375
+ * 404. Registering both spellings avoids decoding the pathname before matching,
376
+ * which would turn `%2f` into `/` and let a crafted URL cross segment
377
+ * boundaries. `%40` cannot introduce a boundary, which is what makes this safe.
378
+ */
379
+ function encodedNamespacePath(packageName) {
380
+ const path = namespacePath(packageName);
381
+ return path.startsWith("@") ? `%40${path.slice(1)}` : void 0;
382
+ }
383
+ /**
384
+ * A route path a package supplied. Must be relative and must not climb out of
385
+ * the scope.
386
+ */
387
+ function assertRelativeRoutePath(path) {
388
+ if (path.startsWith("/")) throw new InvalidNamespaceError(`Route path "${path}" must be relative to the scope, not absolute`);
389
+ if (/^[a-z][a-z0-9+.-]*:/i.test(path)) throw new InvalidNamespaceError(`Route path "${path}" must be a path, not a URL`);
390
+ if (path.split("/").some((segment) => TRAVERSAL.has(segment))) throw new InvalidNamespaceError(`Route path "${path}" must not contain "." or ".." segments`);
391
+ return path.replace(/^\/+|\/+$/g, "");
392
+ }
393
+ /**
394
+ * Resolves the npm name a package is routed under from the key the package
395
+ * manager holds it by. That key is the specifier as configured, which is not
396
+ * always a name:
397
+ * - `@scope/pkg` — already a name.
398
+ * - `@scope/pkg@1.3.9` — a registry spec; only the loader strips the tag.
399
+ * - `/Users/me/project` — the local project, which switchboard registers by
400
+ * `process.cwd()`. Its real name lives in its package.json.
401
+ *
402
+ * Deriving a URL segment straight from the key would give a developer's home
403
+ * directory a route namespace, so the path form is resolved rather than slugged.
404
+ */
405
+ function resolvePackageName(key, readPackageJsonName = defaultReadName) {
406
+ const withoutVersion = stripVersionSpec(key);
407
+ if (isPackageName(withoutVersion)) return withoutVersion;
408
+ if (key.startsWith("/") || key.startsWith(".")) {
409
+ const name = readPackageJsonName(key);
410
+ if (name && isPackageName(name)) return name;
411
+ throw new InvalidNamespaceError(`Package at "${key}" has no usable name in its package.json, so it cannot host routes`);
412
+ }
413
+ throw new InvalidNamespaceError(`"${key}" is not a package name and not a path, so it cannot host routes`);
414
+ }
415
+ function isPackageName(value) {
416
+ try {
417
+ namespaceSegments(value);
418
+ return true;
419
+ } catch {
420
+ return false;
421
+ }
422
+ }
423
+ /** `@scope/pkg@1.3.9` → `@scope/pkg`; `pkg@1.0.0` → `pkg`. */
424
+ function stripVersionSpec(key) {
425
+ const at = key.lastIndexOf("@");
426
+ if (at <= 0) return key;
427
+ return key.slice(0, at);
428
+ }
429
+ function defaultReadName(dir) {
430
+ try {
431
+ const raw = readFileSync(join(dir, "package.json"), "utf8");
432
+ const parsed = JSON.parse(raw);
433
+ return typeof parsed.name === "string" ? parsed.name : void 0;
434
+ } catch {
435
+ return;
436
+ }
437
+ }
438
+ /**
439
+ * A mount path the host named for one of its own route groups.
440
+ *
441
+ * The host owns the whole URL space — it holds the adapter — so unlike a
442
+ * package it may name an absolute path. That is the difference between hosting
443
+ * and being hosted, and it is why this lives on the service rather than on a
444
+ * scope: a package is only ever handed a scope, and a scope has no way to
445
+ * express a path outside itself.
446
+ *
447
+ * What the host still may not name is anything that is not one stable literal
448
+ * prefix. A traversal segment, or a pattern segment, would make the group's own
449
+ * routes unpredictable relative to each other. Returns the normalized path,
450
+ * with a leading and no trailing slash.
451
+ */
452
+ function assertHostMountPath(path) {
453
+ if (!path.startsWith("/")) throw new InvalidNamespaceError(`Host mount path "${path}" must be absolute, e.g. "/webhooks"`);
454
+ const segments = path.split("/").filter((segment) => segment.length > 0);
455
+ if (segments.length === 0) throw new InvalidNamespaceError(`Host mount path "${path}" is the whole URL space, which is the adapter rather than a scope of it`);
456
+ for (const segment of segments) {
457
+ if (TRAVERSAL.has(segment)) throw new InvalidNamespaceError(`Host mount path "${path}" must not contain "." or ".." segments`);
458
+ if (segment.startsWith(":") || segment.includes("*")) throw new InvalidNamespaceError(`Host mount path "${path}" must be a literal prefix, not a pattern: "${segment}"`);
459
+ }
460
+ return `/${segments.join("/")}`;
461
+ }
462
+ //#endregion
463
+ //#region src/http/node-response.ts
464
+ /** Converts Node incoming headers to Fetch Headers. */
465
+ function fetchHeaders(req) {
466
+ const headers = new Headers();
467
+ for (const [key, value] of Object.entries(req.headers)) {
468
+ if (value === void 0) continue;
469
+ headers.set(key, Array.isArray(value) ? value.join(", ") : value);
470
+ }
471
+ return headers;
472
+ }
473
+ /**
474
+ * Where the request reached the host. `trustProxy` says whether the
475
+ * `x-forwarded-*` headers may be believed: they are client-written, so with no
476
+ * proxy in front a caller could pick the origin a route advertises. The `host`
477
+ * header is always read, being the request's own target.
478
+ */
479
+ function resolveTransport(req, trustProxy = false) {
480
+ const header = (name) => {
481
+ const value = req.headers[name];
482
+ return (Array.isArray(value) ? value[0] : value)?.split(",")[0]?.trim() || void 0;
483
+ };
484
+ const forwarded = (name) => trustProxy ? header(name) : void 0;
485
+ const encrypted = req.socket.encrypted === true;
486
+ const proto = forwarded("x-forwarded-proto") ?? (encrypted ? "https" : "http");
487
+ const host = forwarded("x-forwarded-host") ?? header("host") ?? "localhost";
488
+ const prefix = forwarded("x-forwarded-prefix") ?? "";
489
+ return {
490
+ proto,
491
+ host,
492
+ prefix,
493
+ baseUrl: `${proto}://${host}${prefix}`
494
+ };
495
+ }
496
+ var BodyTooLargeError = class extends Error {};
497
+ /**
498
+ * Buffers the request body, refusing anything past `maxBytes`.
499
+ *
500
+ * Written with events rather than `for await`, because an async iterator
501
+ * destroys the stream when the loop exits early — which tears down the socket
502
+ * before a 413 can be written to it. The stream is left paused instead, so the
503
+ * caller can answer and then close the connection deliberately.
504
+ */
505
+ function readBody(req, maxBytes) {
506
+ return new Promise((resolve, reject) => {
507
+ const chunks = [];
508
+ let size = 0;
509
+ let settled = false;
510
+ const settle = (error) => {
511
+ if (settled) return;
512
+ settled = true;
513
+ req.off("data", onData);
514
+ req.off("end", onEnd);
515
+ req.off("error", onError);
516
+ req.pause();
517
+ if (error) reject(error);
518
+ else resolve(Buffer.concat(chunks));
519
+ };
520
+ const onData = (chunk) => {
521
+ size += chunk.length;
522
+ if (size > maxBytes) {
523
+ settle(new BodyTooLargeError(`Body exceeds ${maxBytes} bytes`));
524
+ return;
525
+ }
526
+ chunks.push(chunk);
527
+ };
528
+ const onEnd = () => settle();
529
+ const onError = (error) => settle(error);
530
+ req.on("data", onData);
531
+ req.once("end", onEnd);
532
+ req.once("error", onError);
533
+ });
534
+ }
535
+ /**
536
+ * Copies a Fetch Response onto a Node response, streaming the body. A HEAD
537
+ * response carries headers only, however the handler filled the body.
538
+ */
539
+ async function writeResponse(res, response, method) {
540
+ if (res.writableEnded || res.headersSent) return;
541
+ res.statusCode = response.status;
542
+ response.headers.forEach((value, key) => {
543
+ res.setHeader(key, value);
544
+ });
545
+ if (!response.body || method.toUpperCase() === "HEAD") {
546
+ res.end();
547
+ return;
548
+ }
549
+ const source = Readable.fromWeb(response.body);
550
+ res.on("close", () => source.destroy());
551
+ await pipeline(source, res);
552
+ }
553
+ /** Answers with a JSON error, unless the response is already underway. */
554
+ function writeError(res, status, message) {
555
+ if (res.writableEnded || res.headersSent) {
556
+ res.destroy();
557
+ return;
558
+ }
559
+ const body = JSON.stringify({ error: message });
560
+ res.statusCode = status;
561
+ res.setHeader("content-type", "application/json");
562
+ res.setHeader("content-length", Buffer.byteLength(body));
563
+ res.end(body);
564
+ }
565
+ //#endregion
566
+ //#region src/http/route-service.ts
567
+ const logger$2 = childLogger(["reactor-api", "http-routes"]);
568
+ /** The segment package routes live under, inside the host's base path. */
569
+ const PACKAGE_ROUTE_SEGMENT = "api";
570
+ /** The namespace core's own subgraphs and services register under. */
571
+ const CORE_PACKAGE_NAME = "@powerhousedao/reactor-api";
572
+ /** Generous enough for any request a route should be buffering at all. */
573
+ const DEFAULT_MAX_BODY_BYTES$1 = 1048576;
574
+ const ANONYMOUS = {
575
+ user: void 0,
576
+ authEnabled: false
577
+ };
578
+ /** Owns the URL space packages serve from, handing each a scope bound to its own namespace. The prefix nests inside
579
+ * the host's base path (`<basePath>/api`), so core collisions are prevented by URL shape, not a reserved-name list. */
580
+ var HttpRouteService = class {
581
+ #adapter;
582
+ #authService;
583
+ #prefix;
584
+ #publicUrl;
585
+ #trustProxy;
586
+ #scopes = /* @__PURE__ */ new Map();
587
+ #webhooks;
588
+ constructor({ httpAdapter, basePath = "/", authService, webhooks, publicUrl = "", trustProxy = false }) {
589
+ this.#adapter = httpAdapter;
590
+ this.#authService = authService;
591
+ this.#webhooks = webhooks;
592
+ this.#publicUrl = publicUrl.replace(/\/+$/, "");
593
+ this.#trustProxy = trustProxy;
594
+ this.#prefix = posix.join("/", basePath, "api");
595
+ }
596
+ /** The absolute path packages are mounted under. */
597
+ get prefix() {
598
+ return this.#prefix;
599
+ }
600
+ /** The scope for a package; idempotent per name, so a reload cannot accumulate namespaces. Throws when two packages
601
+ * resolve to one namespace — with verbatim names that means one package configured twice, a genuine ambiguity. */
602
+ scopeFor(packageKey) {
603
+ const packageName = resolvePackageName(packageKey);
604
+ const namespace = namespacePath(packageName);
605
+ const encoded = encodedNamespacePath(packageName);
606
+ return this.#scope({
607
+ owner: packageName,
608
+ base: posix.join(this.#prefix, namespace),
609
+ encodedBase: encoded ? posix.join(this.#prefix, encoded) : void 0,
610
+ claim: `the route namespace "${namespace}"`,
611
+ webhooks: this.#webhooks?.scopeFor(packageName),
612
+ onDispose: () => this.#webhooks?.disposeScope(packageName)
613
+ });
614
+ }
615
+ /** A scope at a host-named path; on the service, not `IHttpScope`, so a package can never name one. Namespacing is package
616
+ * policy: the host owns its URL space and third parties hold its URLs, so the path is verbatim and outside the package prefix. */
617
+ hostScope(name, mountPath) {
618
+ const base = assertHostMountPath(mountPath);
619
+ if (base === this.#prefix || base.startsWith(`${this.#prefix}/`)) throw new InvalidNamespaceError(`Host scope "${name}" cannot mount at "${base}": that is the package route prefix, which packages own`);
620
+ return this.#scope({
621
+ owner: name,
622
+ base,
623
+ encodedBase: void 0,
624
+ claim: `the mount path "${base}"`,
625
+ webhooks: void 0,
626
+ onDispose: () => void 0
627
+ });
628
+ }
629
+ /** Idempotent per base path, so a package reload cannot accumulate namespaces. Two owners wanting one path is an error —
630
+ * with verbatim names that needs one package configured twice at different versions, a genuine ambiguity, not a clash. */
631
+ #scope(options) {
632
+ const { owner, base } = options;
633
+ const existing = this.#scopes.get(base);
634
+ if (existing) {
635
+ if (existing.owner !== owner) throw new Error(`"${owner}" wants ${options.claim}, which "${existing.owner}" already holds`);
636
+ return existing;
637
+ }
638
+ const scope = new HttpScope({
639
+ owner,
640
+ base,
641
+ encodedBase: options.encodedBase,
642
+ publicUrl: this.#publicUrl,
643
+ trustProxy: this.#trustProxy,
644
+ adapter: this.#adapter,
645
+ authService: this.#authService,
646
+ webhooks: options.webhooks,
647
+ onDispose: () => {
648
+ this.#scopes.delete(base);
649
+ options.onDispose();
650
+ }
651
+ });
652
+ this.#scopes.set(base, scope);
653
+ return scope;
654
+ }
655
+ /** Like {@link scopeFor}, but never throws: a key resolving to no usable npm name gets a scope that refuses
656
+ * registrations, so one unroutable package cannot stop the host booting — it fails only if it serves a route. */
657
+ scopeForOrNull(packageKey) {
658
+ try {
659
+ return this.scopeFor(packageKey);
660
+ } catch (error) {
661
+ if (!(error instanceof InvalidNamespaceError)) throw error;
662
+ logger$2.warn("Package \"@package\" cannot host HTTP routes: @reason", packageKey, error.message);
663
+ return new UnroutableScope(packageKey, error.message);
664
+ }
665
+ }
666
+ /** Releases a package's routes, e.g. when it is replaced or removed. */
667
+ disposeScope(packageKey) {
668
+ try {
669
+ const namespace = namespacePath(resolvePackageName(packageKey));
670
+ this.#scopes.get(posix.join(this.#prefix, namespace))?.dispose();
671
+ } catch (error) {
672
+ if (!(error instanceof InvalidNamespaceError)) throw error;
673
+ }
674
+ }
675
+ disposeAll() {
676
+ for (const scope of [...this.#scopes.values()]) scope.dispose();
677
+ }
678
+ };
679
+ var HttpScope = class {
680
+ owner;
681
+ #base;
682
+ #publicUrl;
683
+ #trustProxy;
684
+ #encodedBase;
685
+ #adapter;
686
+ #authService;
687
+ #onDispose;
688
+ #registrations = /* @__PURE__ */ new Map();
689
+ #webhooks;
690
+ constructor(options) {
691
+ this.owner = options.owner;
692
+ this.#base = options.base;
693
+ this.#publicUrl = options.publicUrl;
694
+ this.#trustProxy = options.trustProxy;
695
+ this.#encodedBase = options.encodedBase;
696
+ this.#adapter = options.adapter;
697
+ this.#authService = options.authService;
698
+ this.#onDispose = options.onDispose;
699
+ this.#webhooks = options.webhooks ?? unavailableWebhooks(options.owner);
700
+ }
701
+ get baseUrl() {
702
+ return `${this.#publicUrl}${this.#base}`;
703
+ }
704
+ get webhooks() {
705
+ return this.#webhooks;
706
+ }
707
+ get(path, a, b) {
708
+ return this.#shorthand("GET", path, a, b);
709
+ }
710
+ post(path, a, b) {
711
+ return this.#shorthand("POST", path, a, b);
712
+ }
713
+ put(path, a, b) {
714
+ return this.#shorthand("PUT", path, a, b);
715
+ }
716
+ patch(path, a, b) {
717
+ return this.#shorthand("PATCH", path, a, b);
718
+ }
719
+ delete(path, a, b) {
720
+ return this.#shorthand("DELETE", path, a, b);
721
+ }
722
+ head(path, a, b) {
723
+ return this.#shorthand("HEAD", path, a, b);
724
+ }
725
+ #shorthand(method, path, a, b) {
726
+ const options = typeof a === "function" ? {} : a;
727
+ const handler = typeof a === "function" ? a : b;
728
+ if (!handler) throw new Error(`Route ${method} ${path} was registered with no handler`);
729
+ return this.route({
730
+ ...options,
731
+ method,
732
+ path,
733
+ handler
734
+ });
735
+ }
736
+ route(spec) {
737
+ const body = spec.body ?? "parsed";
738
+ return this.#register(spec, (req, res, ctx) => this.#serve(spec, body, req, res, ctx));
739
+ }
740
+ nodeRoute(spec) {
741
+ return this.#register(spec, (req, res, ctx) => spec.handler(req, res, ctx));
742
+ }
743
+ dispose() {
744
+ for (const registration of this.#registrations.values()) for (const handle of registration.handles) handle.dispose();
745
+ this.#registrations.clear();
746
+ this.#onDispose();
747
+ }
748
+ /** Mounts one route under this scope's namespace, in both the verbatim and the
749
+ * percent-encoded spelling of a scoped name. */
750
+ #register(spec, run) {
751
+ const relative = assertRelativeRoutePath(spec.path);
752
+ const methods = Array.isArray(spec.method) ? spec.method : [spec.method];
753
+ const routePath = relative ? posix.join(this.#base, relative) : this.#base;
754
+ const key = `${methods.join(",")} ${routePath}`;
755
+ if (this.#registrations.has(key)) throw new Error(`Route ${key} is already registered by "${this.owner}"`);
756
+ const handles = [];
757
+ const paths = [routePath];
758
+ if (this.#encodedBase) paths.push(relative ? posix.join(this.#encodedBase, relative) : this.#encodedBase);
759
+ for (const path of paths) for (const method of methods) handles.push(this.#adapter.mountNodeRoute(method, path, (req, res) => {
760
+ this.#guard(spec, req, res, run);
761
+ }, {
762
+ rawBody: true,
763
+ prefix: spec.prefix
764
+ }));
765
+ this.#registrations.set(key, {
766
+ path: routePath,
767
+ handles
768
+ });
769
+ logger$2.debug("Registered @route for @package", key, this.owner);
770
+ return {
771
+ url: `${this.#publicUrl}${routePath}`,
772
+ dispose: () => {
773
+ const registration = this.#registrations.get(key);
774
+ if (!registration) return;
775
+ for (const handle of registration.handles) handle.dispose();
776
+ this.#registrations.delete(key);
777
+ }
778
+ };
779
+ }
780
+ /** Authenticate, then run. */
781
+ async #guard(spec, req, res, run) {
782
+ try {
783
+ const actor = await this.#authenticate(spec.auth ?? "renown", req, res);
784
+ if (actor === REJECTED) return;
785
+ await run(req, res, {
786
+ params: routeParams(req),
787
+ user: actor?.user,
788
+ authEnabled: actor?.authEnabled ?? false,
789
+ transport: resolveTransport(req, this.#trustProxy)
790
+ });
791
+ } catch (error) {
792
+ logger$2.error("Route handler failed for @package", this.owner, error);
793
+ writeError(res, 500, "Internal server error");
794
+ }
795
+ }
796
+ /** Resolves the caller, answering the request itself when the route refuses it;
797
+ * returns REJECTED in that case so the caller stops. */
798
+ async #authenticate(auth, req, res) {
799
+ if (auth === "public") return void 0;
800
+ if (typeof auth === "function") {
801
+ const result = await auth(req);
802
+ if (!result.authorized) {
803
+ if ("response" in result) await writeResponse(res, result.response, req.method ?? "GET");
804
+ else writeError(res, result.status, result.message);
805
+ return REJECTED;
806
+ }
807
+ return;
808
+ }
809
+ if (!this.#authService) return ANONYMOUS;
810
+ const result = await this.#authService.verifyBearer(req.headers.authorization);
811
+ if (result instanceof Response) {
812
+ await writeResponse(res, result, req.method ?? "GET");
813
+ return REJECTED;
814
+ }
815
+ if (result.auth_enabled && !result.user && auth === "renown") {
816
+ writeError(res, 401, "Authentication required");
817
+ return REJECTED;
818
+ }
819
+ return {
820
+ user: result.user,
821
+ authEnabled: result.auth_enabled
822
+ };
823
+ }
824
+ /** Builds the Fetch Request from the raw stream and runs the handler. */
825
+ async #serve(spec, body, req, res, ctx) {
826
+ const method = req.method ?? "GET";
827
+ const url = `${ctx.transport.baseUrl}${req.url ?? "/"}`;
828
+ const controller = new AbortController();
829
+ req.once("close", () => controller.abort());
830
+ let raw;
831
+ let init = {
832
+ method,
833
+ headers: fetchHeaders(req),
834
+ signal: controller.signal
835
+ };
836
+ if (method !== "GET" && method !== "HEAD" && body !== "none") if (body === "stream") init = {
837
+ ...init,
838
+ body: Readable.toWeb(req),
839
+ duplex: "half"
840
+ };
841
+ else {
842
+ try {
843
+ raw = await readBody(req, spec.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES$1);
844
+ } catch (error) {
845
+ if (error instanceof BodyTooLargeError) {
846
+ res.setHeader("connection", "close");
847
+ res.once("finish", () => req.destroy());
848
+ writeError(res, 413, "Payload too large");
849
+ return;
850
+ }
851
+ throw error;
852
+ }
853
+ if (raw.length) init = {
854
+ ...init,
855
+ body: new Uint8Array(raw.buffer, raw.byteOffset, raw.byteLength)
856
+ };
857
+ }
858
+ await writeResponse(res, await spec.handler(new Request(url, init), {
859
+ ...ctx,
860
+ rawBody: body === "raw" ? raw ?? Buffer.alloc(0) : void 0,
861
+ signal: controller.signal
862
+ }), method);
863
+ }
864
+ };
865
+ const REJECTED = Symbol("rejected");
866
+ function routeParams(req) {
867
+ const params = req.params;
868
+ if (!params || typeof params !== "object") return {};
869
+ const out = {};
870
+ for (const [key, value] of Object.entries(params)) if (typeof value === "string") out[key] = value;
871
+ else if (Array.isArray(value)) out[key] = value.join("/");
872
+ return out;
873
+ }
874
+ /** Stands in for a package that cannot be routed — its key resolves to no npm name. Registering a route throws with
875
+ * the reason; nothing else does, so the package still loads and its non-HTTP behaviour is unaffected. */
876
+ var UnroutableScope = class {
877
+ owner;
878
+ baseUrl = "";
879
+ #reason;
880
+ constructor(packageKey, reason) {
881
+ this.owner = packageKey;
882
+ this.#reason = reason;
883
+ }
884
+ #refuse() {
885
+ throw new Error(`Package "${this.owner}" cannot host HTTP routes: ${this.#reason}`);
886
+ }
887
+ get() {
888
+ this.#refuse();
889
+ }
890
+ post() {
891
+ this.#refuse();
892
+ }
893
+ put() {
894
+ this.#refuse();
895
+ }
896
+ patch() {
897
+ this.#refuse();
898
+ }
899
+ delete() {
900
+ this.#refuse();
901
+ }
902
+ head() {
903
+ this.#refuse();
904
+ }
905
+ route() {
906
+ this.#refuse();
907
+ }
908
+ nodeRoute() {
909
+ this.#refuse();
910
+ }
911
+ get webhooks() {
912
+ return unavailableWebhooks(this.owner);
913
+ }
914
+ dispose() {}
915
+ };
916
+ /** Stands in when the host has no webhook store configured. */
917
+ function unavailableWebhooks(packageName) {
918
+ return {
919
+ hasPublicOrigin: false,
920
+ register: () => Promise.reject(/* @__PURE__ */ new Error(`Webhooks are not available on this host, so "${packageName}" cannot register one`))
921
+ };
922
+ }
923
+ /** Header each scheme reads when the caller does not name one: the name the format is
924
+ * most often carried in, not a property of it, so `WebhookVerification.header` overrides any of them. */
925
+ const DEFAULT_SCHEME_HEADER = {
926
+ none: "",
927
+ token: "x-webhook-token",
928
+ hmac: "x-signature",
929
+ "hmac-prefixed": "x-hub-signature-256",
930
+ "hmac-timestamped": "stripe-signature"
931
+ };
932
+ const OK = { ok: true };
933
+ /** Constant-time over equal-length inputs; a length mismatch is already a rejection, so
934
+ * the early return leaks nothing a caller could not measure. */
935
+ function secureEquals(a, b) {
936
+ const left = Buffer.from(a, "utf8");
937
+ const right = Buffer.from(b, "utf8");
938
+ if (left.length !== right.length) return false;
939
+ return timingSafeEqual(left, right);
940
+ }
941
+ function hmacDigest(secret, payload, verification) {
942
+ return createHmac(verification.algorithm ?? "sha256", secret).update(payload).digest(verification.encoding ?? "hex");
943
+ }
944
+ /** Hex folds case, so an uppercased digest still verifies. Base64 must not: two distinct
945
+ * digests can differ only in case, so folding there would accept a signature that is not the one computed. */
946
+ function digestEquals(presented, expected, verification) {
947
+ const candidate = presented.trim();
948
+ if ((verification.encoding ?? "hex") === "base64") return secureEquals(candidate, expected);
949
+ return secureEquals(candidate.toLowerCase(), expected.toLowerCase());
950
+ }
951
+ /** The label `hmac-prefixed` expects before the digest. */
952
+ function signaturePrefix(verification) {
953
+ return verification.prefix ?? `${verification.algorithm ?? "sha256"}=`;
954
+ }
955
+ /** `t=<unix>,v1=<hex>[,v1=<hex>…]`. Several `v1` values appear while a secret is being
956
+ * rotated, and any one of them matching is a pass. */
957
+ function parseTimestampedHeader(value) {
958
+ const signatures = [];
959
+ let timestamp;
960
+ for (const part of value.split(",")) {
961
+ const separator = part.indexOf("=");
962
+ if (separator < 0) continue;
963
+ const key = part.slice(0, separator).trim();
964
+ const item = part.slice(separator + 1).trim();
965
+ if (key === "t") timestamp = Number(item);
966
+ else if (key === "v1") signatures.push(item);
967
+ }
968
+ return {
969
+ timestamp,
970
+ signatures
971
+ };
972
+ }
973
+ function schemeHeader(verification) {
974
+ return (verification.header ?? DEFAULT_SCHEME_HEADER[verification.scheme]).toLowerCase();
975
+ }
976
+ /** Verifies a delivery against the configured scheme; `raw` must be the exact bytes
977
+ * received. The reason is for the log only: callers answer a fixed 401, so a probing sender learns nothing from the body. */
978
+ function verifyWebhook(options) {
979
+ const { verification, headers, raw } = options;
980
+ if (verification.scheme === "none") return OK;
981
+ if (!verification.secret) return {
982
+ ok: false,
983
+ reason: "signing secret missing"
984
+ };
985
+ const header = schemeHeader(verification);
986
+ const presented = headers[header];
987
+ if (!presented) return {
988
+ ok: false,
989
+ reason: `header "${header}" absent`
990
+ };
991
+ const secret = verification.secret;
992
+ const tolerance = verification.toleranceSeconds ?? 300;
993
+ switch (verification.scheme) {
994
+ case "token": return secureEquals(presented, secret) ? OK : {
995
+ ok: false,
996
+ reason: "token mismatch"
997
+ };
998
+ case "hmac": return digestEquals(presented, hmacDigest(secret, raw, verification), verification) ? OK : {
999
+ ok: false,
1000
+ reason: "hmac mismatch"
1001
+ };
1002
+ case "hmac-prefixed": return digestEquals(presented, `${signaturePrefix(verification)}${hmacDigest(secret, raw, verification)}`, verification) ? OK : {
1003
+ ok: false,
1004
+ reason: "hmac mismatch"
1005
+ };
1006
+ case "hmac-timestamped": {
1007
+ const { timestamp, signatures } = parseTimestampedHeader(presented);
1008
+ if (timestamp === void 0 || !Number.isFinite(timestamp)) return {
1009
+ ok: false,
1010
+ reason: "no timestamp in the signature header"
1011
+ };
1012
+ if (signatures.length === 0) return {
1013
+ ok: false,
1014
+ reason: "no v1 signature in the header"
1015
+ };
1016
+ const nowMs = (options.now ?? /* @__PURE__ */ new Date()).getTime();
1017
+ if (Math.abs(nowMs / 1e3 - timestamp) > tolerance) return {
1018
+ ok: false,
1019
+ reason: "timestamp outside the replay window"
1020
+ };
1021
+ const expected = hmacDigest(secret, Buffer.concat([Buffer.from(`${timestamp}.`, "utf8"), raw]), verification);
1022
+ let matched = false;
1023
+ for (const signature of signatures) if (digestEquals(signature, expected, verification)) matched = true;
1024
+ return matched ? OK : {
1025
+ ok: false,
1026
+ reason: "hmac mismatch"
1027
+ };
1028
+ }
1029
+ }
1030
+ }
1031
+ /** Headers that would put a credential in a log or run journal. The configured signature
1032
+ * header joins them: it is an HMAC of the body under a live secret. */
1033
+ const REDACTED_HEADERS = new Set([
1034
+ "authorization",
1035
+ "proxy-authorization",
1036
+ "cookie",
1037
+ "set-cookie",
1038
+ "x-api-key",
1039
+ "x-webhook-token",
1040
+ "x-signature",
1041
+ "x-hub-signature",
1042
+ "x-hub-signature-256",
1043
+ "stripe-signature"
1044
+ ]);
1045
+ const REDACTED = "[redacted]";
1046
+ function redactHeaders(headers, extra) {
1047
+ const redacted = {};
1048
+ for (const [name, value] of Object.entries(headers)) {
1049
+ const key = name.toLowerCase();
1050
+ redacted[key] = REDACTED_HEADERS.has(key) || extra && key === extra.toLowerCase() ? REDACTED : value;
1051
+ }
1052
+ return redacted;
1053
+ }
1054
+ /** JSON and form bodies become objects; anything else stays decoded text, so a verified
1055
+ * XML or CSV payload is still reachable downstream. */
1056
+ function parseWebhookBody(raw, contentType) {
1057
+ if (raw.length === 0) return void 0;
1058
+ const type = (contentType ?? "").split(";")[0].trim().toLowerCase();
1059
+ const text = raw.toString("utf8");
1060
+ if (type === "application/json" || type.endsWith("+json")) try {
1061
+ return JSON.parse(text);
1062
+ } catch {
1063
+ return text;
1064
+ }
1065
+ if (type === "application/x-www-form-urlencoded") return Object.fromEntries(new URLSearchParams(text));
1066
+ return text;
1067
+ }
1068
+ //#endregion
1069
+ //#region src/http/webhook-service.ts
1070
+ const logger$1 = childLogger(["reactor-api", "webhooks"]);
1071
+ /** The flat, token-addressed path family. */
1072
+ const WEBHOOK_SEGMENT = "webhooks";
1073
+ const DEFAULT_MAX_BODY_BYTES = 1048576;
1074
+ const DEFAULT_DEDUPE_TTL_SECONDS = 300;
1075
+ /** One host route for `/webhooks/:token`, dispatching by token. The flat path leaks
1076
+ * neither package nor document; the namespace lives in the token record, which is also what stops one package's token reaching another's handler. */
1077
+ var WebhookService = class {
1078
+ #store;
1079
+ #path;
1080
+ #publicUrl;
1081
+ #scope;
1082
+ #registrations = /* @__PURE__ */ new Map();
1083
+ #routes = [];
1084
+ constructor({ store, basePath = "/", publicUrl }) {
1085
+ this.#store = store;
1086
+ this.#path = posix.join("/", basePath, WEBHOOK_SEGMENT);
1087
+ this.#publicUrl = publicUrl?.replace(/\/+$/, "");
1088
+ }
1089
+ /** The scope the endpoint family serves from, handed over once the route service exists;
1090
+ * separate from the constructor only because that service takes this one as an option. */
1091
+ attach(scope) {
1092
+ this.#scope = scope;
1093
+ }
1094
+ async init() {
1095
+ await this.#store.init();
1096
+ if (this.#routes.length) return;
1097
+ if (!this.#scope) throw new Error("The webhook service has no HTTP scope; the host must call attach() before a package registers");
1098
+ this.#routes = [this.#scope.nodeRoute({
1099
+ method: [
1100
+ "DELETE",
1101
+ "GET",
1102
+ "HEAD",
1103
+ "PATCH",
1104
+ "POST",
1105
+ "PUT"
1106
+ ],
1107
+ path: ":token",
1108
+ auth: "public",
1109
+ handler: (req, res, ctx) => void this.#deliver(req, res, ctx.params)
1110
+ })];
1111
+ }
1112
+ dispose() {
1113
+ for (const handle of this.#routes) handle.dispose();
1114
+ this.#routes = [];
1115
+ this.#registrations.clear();
1116
+ }
1117
+ /** The facade a package's HTTP scope exposes as `scope.webhooks`. */
1118
+ scopeFor(namespace) {
1119
+ return {
1120
+ hasPublicOrigin: this.hasPublicOrigin,
1121
+ register: async (spec) => {
1122
+ await this.init();
1123
+ const key = `${namespace}|${spec.name}`;
1124
+ this.#registrations.set(key, {
1125
+ namespace,
1126
+ spec
1127
+ });
1128
+ return {
1129
+ endpointFor: async (ownerKey) => {
1130
+ const row = await this.#store.ensure(namespace, spec.name, ownerKey);
1131
+ return {
1132
+ token: row.token,
1133
+ url: this.urlFor(row.token),
1134
+ createdAt: row.createdAt
1135
+ };
1136
+ },
1137
+ revoke: (ownerKey) => this.#store.revoke(namespace, spec.name, ownerKey),
1138
+ list: async () => (await this.#store.list(namespace, spec.name)).map((row) => ({
1139
+ key: row.ownerKey,
1140
+ token: row.token,
1141
+ url: this.urlFor(row.token),
1142
+ createdAt: row.createdAt
1143
+ }))
1144
+ };
1145
+ }
1146
+ };
1147
+ }
1148
+ /** Releases every registration a package made. */
1149
+ disposeScope(namespace) {
1150
+ for (const [key, registration] of this.#registrations) if (registration.namespace === namespace) this.#registrations.delete(key);
1151
+ }
1152
+ /** The URL to hand a provider. Absolute whenever the host knows its own origin: a
1153
+ * relative path is useless to the third party that has to call it. */
1154
+ urlFor(token) {
1155
+ const path = posix.join(this.#path, token);
1156
+ return this.#publicUrl ? `${this.#publicUrl}${path}` : path;
1157
+ }
1158
+ /** Whether `urlFor` can produce something a provider could call. */
1159
+ get hasPublicOrigin() {
1160
+ return this.#publicUrl !== void 0;
1161
+ }
1162
+ async #deliver(req, res, params) {
1163
+ const token = params.token;
1164
+ if (!token) {
1165
+ writeError(res, 404, "Unknown endpoint");
1166
+ return;
1167
+ }
1168
+ try {
1169
+ const row = await this.#store.find(token);
1170
+ if (!row) {
1171
+ writeError(res, 404, "Unknown endpoint");
1172
+ return;
1173
+ }
1174
+ const registrationKey = `${row.namespace}|${row.endpoint}`;
1175
+ const registration = this.#registrations.get(registrationKey);
1176
+ if (!registration) {
1177
+ writeError(res, 503, "Endpoint temporarily unavailable");
1178
+ return;
1179
+ }
1180
+ const { spec } = registration;
1181
+ const method = (req.method ?? "POST").toUpperCase();
1182
+ const perEndpoint = spec.policyFor ? await spec.policyFor(row.ownerKey) : {};
1183
+ if (!perEndpoint) {
1184
+ writeError(res, 404, "Unknown endpoint");
1185
+ return;
1186
+ }
1187
+ const policy = {
1188
+ ...spec.defaults,
1189
+ ...perEndpoint
1190
+ };
1191
+ if (policy.methods && !policy.methods.includes(method)) {
1192
+ writeError(res, 405, "Method not allowed");
1193
+ return;
1194
+ }
1195
+ let raw;
1196
+ try {
1197
+ raw = await readBody(req, policy.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES);
1198
+ } catch (error) {
1199
+ if (error instanceof BodyTooLargeError) {
1200
+ res.setHeader("connection", "close");
1201
+ res.once("finish", () => req.destroy());
1202
+ writeError(res, 413, "Payload too large");
1203
+ return;
1204
+ }
1205
+ throw error;
1206
+ }
1207
+ const verification = policy.verify;
1208
+ if (verification) {
1209
+ const result = verifyWebhook({
1210
+ verification,
1211
+ headers: headerRecord(req),
1212
+ raw
1213
+ });
1214
+ if (!result.ok) {
1215
+ logger$1.warn("Rejected webhook delivery for @package/@endpoint: @reason", row.namespace, row.endpoint, result.reason);
1216
+ writeError(res, 401, "Signature verification failed");
1217
+ return;
1218
+ }
1219
+ }
1220
+ const url = new URL(req.url ?? "/", "http://localhost");
1221
+ const queryParams = Object.fromEntries(url.searchParams);
1222
+ const signatureHeader = verification ? schemeHeader(verification) : void 0;
1223
+ const headers = headerRecord(req);
1224
+ const body = parseWebhookBody(raw, headers["content-type"]);
1225
+ const challenge = challengeValue(policy.challengeField, queryParams, headers, body);
1226
+ if (challenge !== void 0) {
1227
+ res.statusCode = 200;
1228
+ res.setHeader("content-type", "text/plain");
1229
+ res.end(challenge);
1230
+ return;
1231
+ }
1232
+ if (policy.dedupe) {
1233
+ const key = dedupeKey(policy.dedupe.field, queryParams, headers, body);
1234
+ if (key !== void 0 && await this.#store.seen(token, key, policy.dedupe.ttlSeconds ?? DEFAULT_DEDUPE_TTL_SECONDS)) {
1235
+ res.statusCode = 200;
1236
+ res.end();
1237
+ return;
1238
+ }
1239
+ }
1240
+ const reply = await spec.onRequest({
1241
+ key: row.ownerKey,
1242
+ method,
1243
+ path: url.pathname,
1244
+ queryParams,
1245
+ headers: redactHeaders(headers, signatureHeader),
1246
+ raw,
1247
+ body
1248
+ });
1249
+ res.statusCode = reply.status;
1250
+ if (reply.body !== void 0 && method !== "HEAD") {
1251
+ res.setHeader("content-type", reply.contentType ?? "text/plain");
1252
+ res.end(reply.body);
1253
+ } else res.end();
1254
+ } catch (error) {
1255
+ logger$1.error("Webhook delivery failed for @method", req.method, error);
1256
+ writeError(res, 500, "Internal server error");
1257
+ }
1258
+ }
1259
+ };
1260
+ function headerRecord(req) {
1261
+ const headers = {};
1262
+ for (const [name, value] of Object.entries(req.headers)) {
1263
+ if (value === void 0) continue;
1264
+ headers[name.toLowerCase()] = Array.isArray(value) ? value.join(", ") : value;
1265
+ }
1266
+ return headers;
1267
+ }
1268
+ function scalar(value) {
1269
+ if (typeof value === "string" && value !== "") return value;
1270
+ if (typeof value === "number") return String(value);
1271
+ }
1272
+ /** Walks a dot-separated path into the parsed body. */
1273
+ function bodyPath(path, body) {
1274
+ let current = body;
1275
+ for (const segment of path.split(".")) {
1276
+ if (!current || typeof current !== "object") return void 0;
1277
+ current = current[segment];
1278
+ }
1279
+ return scalar(current);
1280
+ }
1281
+ /** Resolves where a provider put a value. A bare string means query first, then a
1282
+ * top-level body field; `{ header }` and `{ body }` reach what that cannot — GitHub's delivery id is a header, Stripe's event id is nested. */
1283
+ function fieldValue(field, queryParams, headers, body) {
1284
+ if (typeof field === "object") return "header" in field ? scalar(headers[field.header.toLowerCase()]) : bodyPath(field.body, body);
1285
+ const fromQuery = scalar(queryParams[field]);
1286
+ if (fromQuery !== void 0) return fromQuery;
1287
+ return body && typeof body === "object" ? scalar(body[field]) : void 0;
1288
+ }
1289
+ function challengeValue(field, queryParams, headers, body) {
1290
+ return field === void 0 ? void 0 : fieldValue(field, queryParams, headers, body);
1291
+ }
1292
+ function dedupeKey(field, queryParams, headers, body) {
1293
+ return fieldValue(field, queryParams, headers, body);
1294
+ }
1295
+ //#endregion
1296
+ //#region src/http/webhook-store.ts
1297
+ const TOKEN_BYTES = 16;
1298
+ const TOKEN_PATTERN = /^[0-9a-f]{32}$/;
1299
+ /** Opaque and unguessable: the URL is a bearer capability held by a third party. */
1300
+ function newWebhookToken() {
1301
+ return randomBytes(TOKEN_BYTES).toString("hex");
1302
+ }
1303
+ const NAMESPACE = "reactor_webhooks";
1304
+ /** Relational implementation, over the reactor's own database. */
1305
+ var RelationalWebhookStore = class {
1306
+ #db;
1307
+ constructor(relationalDb) {
1308
+ this.relationalDb = relationalDb;
1309
+ }
1310
+ async init() {
1311
+ if (this.#db) return;
1312
+ const db = await this.relationalDb.createNamespace(NAMESPACE);
1313
+ await db.schema.createTable("webhook_endpoints").ifNotExists().addColumn("token", "text", (col) => col.primaryKey()).addColumn("namespace", "text", (col) => col.notNull()).addColumn("endpoint", "text", (col) => col.notNull()).addColumn("owner_key", "text", (col) => col.notNull()).addColumn("created_at", "text", (col) => col.notNull()).execute();
1314
+ await db.schema.createIndex("webhook_endpoints_owner").ifNotExists().on("webhook_endpoints").columns([
1315
+ "namespace",
1316
+ "endpoint",
1317
+ "owner_key"
1318
+ ]).unique().execute();
1319
+ await db.schema.createTable("webhook_deliveries").ifNotExists().addColumn("token", "text", (col) => col.notNull()).addColumn("dedupe_key", "text", (col) => col.notNull()).addColumn("seen_at", "text", (col) => col.notNull()).addColumn("claim_id", "text", (col) => col.notNull()).execute();
1320
+ await db.schema.createIndex("webhook_deliveries_key").ifNotExists().on("webhook_deliveries").columns(["token", "dedupe_key"]).unique().execute();
1321
+ await db.schema.createIndex("webhook_deliveries_seen_at").ifNotExists().on("webhook_deliveries").columns(["seen_at"]).execute();
1322
+ this.#db = db;
1323
+ }
1324
+ get #handle() {
1325
+ if (!this.#db) throw new Error("Webhook store used before init()");
1326
+ return this.#db;
1327
+ }
1328
+ async ensure(namespace, endpoint, ownerKey) {
1329
+ const existing = await this.#handle.selectFrom("webhook_endpoints").selectAll().where("namespace", "=", namespace).where("endpoint", "=", endpoint).where("owner_key", "=", ownerKey).executeTakeFirst();
1330
+ if (existing) return toRow(existing);
1331
+ const row = {
1332
+ token: newWebhookToken(),
1333
+ namespace,
1334
+ endpoint,
1335
+ owner_key: ownerKey,
1336
+ created_at: (/* @__PURE__ */ new Date()).toISOString()
1337
+ };
1338
+ await this.#handle.insertInto("webhook_endpoints").values(row).onConflict((oc) => oc.columns([
1339
+ "namespace",
1340
+ "endpoint",
1341
+ "owner_key"
1342
+ ]).doNothing()).execute();
1343
+ const settled = await this.#handle.selectFrom("webhook_endpoints").selectAll().where("namespace", "=", namespace).where("endpoint", "=", endpoint).where("owner_key", "=", ownerKey).executeTakeFirst();
1344
+ return settled ? toRow(settled) : toRow(row);
1345
+ }
1346
+ async find(token) {
1347
+ if (!TOKEN_PATTERN.test(token)) return void 0;
1348
+ const found = await this.#handle.selectFrom("webhook_endpoints").selectAll().where("token", "=", token).executeTakeFirst();
1349
+ return found ? toRow(found) : void 0;
1350
+ }
1351
+ async list(namespace, endpoint) {
1352
+ let query = this.#handle.selectFrom("webhook_endpoints").selectAll().where("namespace", "=", namespace);
1353
+ if (endpoint) query = query.where("endpoint", "=", endpoint);
1354
+ return (await query.execute()).map(toRow);
1355
+ }
1356
+ async revoke(namespace, endpoint, ownerKey) {
1357
+ await this.#handle.deleteFrom("webhook_endpoints").where("namespace", "=", namespace).where("endpoint", "=", endpoint).where("owner_key", "=", ownerKey).execute();
1358
+ }
1359
+ async seen(token, key, ttlSeconds) {
1360
+ const now = Date.now();
1361
+ const cutoff = (/* @__PURE__ */ new Date(now - ttlSeconds * 1e3)).toISOString();
1362
+ await this.#handle.deleteFrom("webhook_deliveries").where("seen_at", "<", cutoff).execute();
1363
+ const claimId = randomBytes(16).toString("hex");
1364
+ await this.#handle.insertInto("webhook_deliveries").values({
1365
+ token,
1366
+ dedupe_key: key,
1367
+ seen_at: new Date(now).toISOString(),
1368
+ claim_id: claimId
1369
+ }).onConflict((oc) => oc.columns(["token", "dedupe_key"]).doNothing()).execute();
1370
+ return (await this.#handle.selectFrom("webhook_deliveries").select("claim_id").where("token", "=", token).where("dedupe_key", "=", key).executeTakeFirst())?.claim_id !== claimId;
1371
+ }
1372
+ };
1373
+ function toRow(row) {
1374
+ return {
1375
+ token: row.token,
1376
+ namespace: row.namespace,
1377
+ endpoint: row.endpoint,
1378
+ ownerKey: row.owner_key,
1379
+ createdAt: row.created_at
1380
+ };
1381
+ }
1382
+ /** In-memory store, for hosts with no relational database and for tests. */
1383
+ var MemoryWebhookStore = class {
1384
+ #byToken = /* @__PURE__ */ new Map();
1385
+ #deliveries = /* @__PURE__ */ new Map();
1386
+ init() {
1387
+ return Promise.resolve();
1388
+ }
1389
+ ensure(namespace, endpoint, ownerKey) {
1390
+ for (const row of this.#byToken.values()) if (row.namespace === namespace && row.endpoint === endpoint && row.ownerKey === ownerKey) return Promise.resolve(row);
1391
+ const row = {
1392
+ token: newWebhookToken(),
1393
+ namespace,
1394
+ endpoint,
1395
+ ownerKey,
1396
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1397
+ };
1398
+ this.#byToken.set(row.token, row);
1399
+ return Promise.resolve(row);
1400
+ }
1401
+ find(token) {
1402
+ return Promise.resolve(this.#byToken.get(token));
1403
+ }
1404
+ list(namespace, endpoint) {
1405
+ return Promise.resolve([...this.#byToken.values()].filter((row) => row.namespace === namespace && (endpoint === void 0 || row.endpoint === endpoint)));
1406
+ }
1407
+ revoke(namespace, endpoint, ownerKey) {
1408
+ for (const [token, row] of this.#byToken) if (row.namespace === namespace && row.endpoint === endpoint && row.ownerKey === ownerKey) this.#byToken.delete(token);
1409
+ return Promise.resolve();
1410
+ }
1411
+ seen(token, key, ttlSeconds) {
1412
+ const now = Date.now();
1413
+ for (const [k, at] of this.#deliveries) if (at < now - ttlSeconds * 1e3) this.#deliveries.delete(k);
1414
+ const composite = `${token}|${key}`;
1415
+ if (this.#deliveries.has(composite)) return Promise.resolve(true);
1416
+ this.#deliveries.set(composite, now);
1417
+ return Promise.resolve(false);
1418
+ }
1419
+ };
1420
+ //#endregion
332
1421
  //#region src/utils/create-schema.ts
333
1422
  const logger = childLogger(["reactor-api", "create-schema"]);
334
1423
  /**
@@ -3252,7 +4341,9 @@ var GraphQLManager = class {
3252
4341
  */
3253
4342
  reactorDriveClient;
3254
4343
  authorizationService;
3255
- constructor(path, httpServer, wsServer, reactorClient, relationalDb, analyticsStore, syncManager, logger, httpAdapter, gatewayAdapter, authService, documentPermissionService, featureFlags = DefaultFeatureFlags, port = 4001, authorizationService, reactorDriveClient, syncServingGate) {
4344
+ /** Route service in use: the injected one, or one built on first need. */
4345
+ #fallbackRoutes;
4346
+ constructor(path, httpServer, wsServer, reactorClient, relationalDb, analyticsStore, syncManager, logger, httpAdapter, gatewayAdapter, authService, documentPermissionService, featureFlags = DefaultFeatureFlags, port = 4001, authorizationService, reactorDriveClient, syncServingGate, httpRoutes) {
3256
4347
  this.path = path;
3257
4348
  this.httpServer = httpServer;
3258
4349
  this.wsServer = wsServer;
@@ -3268,6 +4359,7 @@ var GraphQLManager = class {
3268
4359
  this.featureFlags = featureFlags;
3269
4360
  this.port = port;
3270
4361
  this.syncServingGate = syncServingGate;
4362
+ this.httpRoutes = httpRoutes;
3271
4363
  if (!authorizationService) throw new Error("GraphQLManager requires an authorizationService");
3272
4364
  this.authorizationService = authorizationService;
3273
4365
  this.reactorDriveClient = reactorDriveClient;
@@ -3349,6 +4441,7 @@ var GraphQLManager = class {
3349
4441
  if (!hasOperationSchemas(documentModel)) continue;
3350
4442
  try {
3351
4443
  const subgraphInstance = new DocumentModelSubgraph(documentModel, {
4444
+ http: this.scopeForPackage(CORE_PACKAGE_NAME),
3352
4445
  relationalDb: this.relationalDb,
3353
4446
  analyticsStore: this.analyticsStore,
3354
4447
  reactorClient: this.reactorClient,
@@ -3421,7 +4514,7 @@ var GraphQLManager = class {
3421
4514
  this.subgraphHandlerCache.delete(subgraphPath);
3422
4515
  const routeHandle = this.subgraphRouteHandles.get(subgraphPath);
3423
4516
  if (routeHandle !== void 0) {
3424
- this.httpAdapter.unmount(routeHandle);
4517
+ routeHandle.dispose();
3425
4518
  this.subgraphRouteHandles.delete(subgraphPath);
3426
4519
  }
3427
4520
  const wsDisposer = this.subgraphWsDisposers.get(subgraphPath);
@@ -3463,13 +4556,28 @@ var GraphQLManager = class {
3463
4556
  return this.authorizationService;
3464
4557
  }
3465
4558
  /**
4559
+ * The HTTP scope a package's subgraphs get. Falls back to a scope over a
4560
+ * throwaway service when route hosting is not configured, so a subgraph can
4561
+ * always rely on `args.http` existing.
4562
+ */
4563
+ scopeForPackage(packageName) {
4564
+ this.#fallbackRoutes ??= this.httpRoutes ?? new HttpRouteService({
4565
+ httpAdapter: this.httpAdapter,
4566
+ basePath: this.path,
4567
+ authService: this.authService
4568
+ });
4569
+ return this.#fallbackRoutes.scopeForOrNull(packageName);
4570
+ }
4571
+ /**
3466
4572
  * Register a subgraph class. `packageName` labels the resulting instance
3467
4573
  * with its contributing package so the instance can be torn down when the
3468
4574
  * package is removed or drops the subgraph (see unregisterPackage and
3469
- * prunePackageSubgraphs).
4575
+ * prunePackageSubgraphs), and decides the namespace its REST routes and
4576
+ * webhooks live under.
3470
4577
  */
3471
4578
  async registerSubgraph(subgraph, supergraph = "", core = false, packageName) {
3472
4579
  const subgraphInstance = new subgraph({
4580
+ http: this.scopeForPackage(packageName ?? "@powerhousedao/reactor-api"),
3473
4581
  relationalDb: this.relationalDb,
3474
4582
  analyticsStore: this.analyticsStore,
3475
4583
  reactorClient: this.reactorClient,
@@ -3490,12 +4598,25 @@ var GraphQLManager = class {
3490
4598
  * package has no registered subgraphs.
3491
4599
  */
3492
4600
  async unregisterPackage(packageName) {
4601
+ this.#disposeHttpScope(packageName);
3493
4602
  const byName = this.packageSubgraphs.get(packageName);
3494
4603
  if (!byName || byName.size === 0) return;
3495
4604
  for (const instance of Array.from(byName.values())) await this.#removeSubgraphInstance(instance);
3496
4605
  await this.updateRouter();
3497
4606
  }
3498
4607
  /**
4608
+ * Releases the package's HTTP scope: every route it mounted, and its webhook
4609
+ * registrations. Idempotent, and silent for a package that never had one.
4610
+ */
4611
+ #disposeHttpScope(packageName) {
4612
+ const routes = this.#fallbackRoutes ?? this.httpRoutes;
4613
+ try {
4614
+ routes?.disposeScope(packageName);
4615
+ } catch (error) {
4616
+ this.logger.warn(`Failed to release HTTP routes for "${packageName}": @error`, error);
4617
+ }
4618
+ }
4619
+ /**
3499
4620
  * Remove this package's registered subgraphs whose names are not in
3500
4621
  * `keepNames` — the package is still loaded but dropped some of its
3501
4622
  * subgraphs. Refreshes the router if anything was removed.
@@ -3730,7 +4851,7 @@ var GraphQLManager = class {
3730
4851
  const modules = Array.from(allSubgraphs.values()).filter((subgraph) => subgraph.hasSubscriptions).map((subgraph) => this.#buildSubgraphSchemaModule(subgraph));
3731
4852
  if (modules.length === 0) {
3732
4853
  if (this.sseRouteHandle !== void 0) {
3733
- this.httpAdapter.unmount(this.sseRouteHandle);
4854
+ this.sseRouteHandle.dispose();
3734
4855
  this.sseRouteHandle = void 0;
3735
4856
  }
3736
4857
  return;
@@ -3758,7 +4879,7 @@ var GraphQLManager = class {
3758
4879
  contextFactory: this.#makeContextFactory()
3759
4880
  });
3760
4881
  const handler = this.#composeFetchMiddleware(rawHandler);
3761
- if (this.sseRouteHandle !== void 0) this.httpAdapter.unmount(this.sseRouteHandle);
4882
+ if (this.sseRouteHandle !== void 0) this.sseRouteHandle.dispose();
3762
4883
  this.sseRouteHandle = this.httpAdapter.mount(ssePath, handler, { exact: true });
3763
4884
  }
3764
4885
  /**
@@ -4902,10 +6023,10 @@ const ADMIN_USERS = getAdminUsers();
4902
6023
  //#endregion
4903
6024
  //#region src/graphql/system/version.ts
4904
6025
  function getVersion() {
4905
- return "6.2.2";
6026
+ return "6.2.3-dev.1";
4906
6027
  }
4907
6028
  function getGitHash() {
4908
- return "f1edb55f5581a52badb784d25c78666b266f9caf";
6029
+ return "4f706b751c94b165e31129acf22704dbb96b56c2";
4909
6030
  }
4910
6031
  function getGitUrl() {
4911
6032
  return buildTreeUrl(getGitHash());
@@ -6557,8 +7678,8 @@ function buildSyncServingGate(reactorModule, authorizationConfig, logger) {
6557
7678
  /**
6558
7679
  * Sets up the subgraph manager and registers subgraphs
6559
7680
  */
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);
7681
+ async function setupGraphQLManager(httpAdapter, authFetchMiddleware, requireAuthFetchMiddleware, httpServer, wsServer, client, relationalDb, analyticsStore, syncManager, subgraphs, logger, authorizationService, authService, documentPermissionService, enableDocumentModelSubgraphs, port, reactorDriveClient, syncServingGate, httpRoutes) {
7682
+ 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, httpRoutes);
6562
7683
  await graphqlManager.init(subgraphs.core, authFetchMiddleware, requireAuthFetchMiddleware);
6563
7684
  for (const [packageName, collection] of subgraphs.extended.entries()) for (const subgraph of collection) await graphqlManager.registerSubgraph(subgraph, "graphql", false, packageName);
6564
7685
  await graphqlManager.updateRouter();
@@ -6567,7 +7688,7 @@ async function setupGraphQLManager(httpAdapter, authFetchMiddleware, requireAuth
6567
7688
  /**
6568
7689
  * Sets up event listeners for package manager changes
6569
7690
  */
6570
- function setupEventListeners(pkgManager, graphqlManager, reactorProcessorManager, module, documentModelRegistry) {
7691
+ function setupEventListeners(pkgManager, graphqlManager, reactorProcessorManager, moduleFor, documentModelRegistry) {
6571
7692
  pkgManager.onDocumentModelsChange((packagedModels) => {
6572
7693
  if (documentModelRegistry) {
6573
7694
  const newModules = getUniqueDocumentModels(Object.values(packagedModels).flat());
@@ -6611,7 +7732,7 @@ function setupEventListeners(pkgManager, graphqlManager, reactorProcessorManager
6611
7732
  knownProcessorPackages = new Set(processors.keys());
6612
7733
  for (const [packageName, fns] of processors) {
6613
7734
  await reactorProcessorManager.unregisterFactory(packageName);
6614
- const validBuilders = fns.map((fn) => fn(module)).filter((factory) => typeof factory === "function");
7735
+ const validBuilders = fns.map((fn) => fn(moduleFor(packageName))).filter((factory) => typeof factory === "function");
6615
7736
  if (!validBuilders.length) continue;
6616
7737
  await reactorProcessorManager.registerFactory(packageName, async (driveHeader) => (await Promise.all(validBuilders.map(async (driveFactory) => {
6617
7738
  try {
@@ -6790,9 +7911,25 @@ async function _setupCommonInfrastructure(options) {
6790
7911
  configFile: options.configFile,
6791
7912
  packages: options.packages ?? []
6792
7913
  });
7914
+ const publicUrl = resolvePublicOrigin(port);
7915
+ const webhooks = new WebhookService({
7916
+ store: new RelationalWebhookStore(relationalDb),
7917
+ basePath: config.basePath,
7918
+ publicUrl
7919
+ });
7920
+ const httpRoutes = new HttpRouteService({
7921
+ httpAdapter,
7922
+ basePath: config.basePath,
7923
+ authService,
7924
+ webhooks,
7925
+ publicUrl,
7926
+ trustProxy: true
7927
+ });
7928
+ webhooks.attach(httpRoutes.hostScope(CORE_PACKAGE_NAME, path.posix.join("/", config.basePath ?? "/", WEBHOOK_SEGMENT)));
6793
7929
  return {
6794
7930
  port,
6795
7931
  httpAdapter,
7932
+ httpRoutes,
6796
7933
  authFetchMiddleware,
6797
7934
  requireAuthFetchMiddleware,
6798
7935
  authService,
@@ -6810,8 +7947,8 @@ async function _setupCommonInfrastructure(options) {
6810
7947
  /**
6811
7948
  * Private helper function containing common setup logic for API initialization
6812
7949
  */
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) {
6814
- const hostModule = {
7950
+ 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, httpRoutes) {
7951
+ const hostModuleBase = {
6815
7952
  ...createReactorHostModuleBase({
6816
7953
  client: reactorClient,
6817
7954
  readModels,
@@ -6822,6 +7959,10 @@ async function _setupAPI(reactorClient, syncManager, reactorProcessorManager, ht
6822
7959
  }),
6823
7960
  attachments: createAttachmentClient(attachments.service)
6824
7961
  };
7962
+ const moduleFor = (packageName) => ({
7963
+ ...hostModuleBase,
7964
+ http: httpRoutes?.scopeForOrNull(packageName)
7965
+ });
6825
7966
  const mcpServerEnabled = options.mcp ?? true;
6826
7967
  const logger = options.logger ?? defaultLogger;
6827
7968
  const configuredProcessorEntries = Object.entries(options.processors ?? {});
@@ -6829,7 +7970,7 @@ async function _setupAPI(reactorClient, syncManager, reactorProcessorManager, ht
6829
7970
  for (const [packageName, fns] of processorEntries) {
6830
7971
  const validFactories = (await Promise.allSettled(fns.map(async (fn) => {
6831
7972
  try {
6832
- return fn(hostModule);
7973
+ return fn(moduleFor(packageName));
6833
7974
  } catch (e) {
6834
7975
  logger.error(`Error initializing processor factory for package ${packageName}:`, e);
6835
7976
  return null;
@@ -6858,8 +7999,8 @@ async function _setupAPI(reactorClient, syncManager, reactorProcessorManager, ht
6858
7999
  const graphqlManager = await setupGraphQLManager(httpAdapter, authFetchMiddleware, requireAuthFetchMiddleware, httpServer, wsServer, reactorClient, relationalDb, analyticsStore, syncManager, {
6859
8000
  extended: subgraphs,
6860
8001
  core: coreSubgraphs
6861
- }, logger.child(["graphql-manager"]), authorizationService, authService, documentPermissionService, options.enableDocumentModelSubgraphs, port, reactorDriveClient, syncServingGate);
6862
- setupEventListeners(packages, graphqlManager, reactorProcessorManager, hostModule, documentModelRegistry);
8002
+ }, logger.child(["graphql-manager"]), authorizationService, authService, documentPermissionService, options.enableDocumentModelSubgraphs, port, reactorDriveClient, syncServingGate, httpRoutes);
8003
+ setupEventListeners(packages, graphqlManager, reactorProcessorManager, moduleFor, documentModelRegistry);
6863
8004
  if (mcpServerEnabled) {
6864
8005
  await setupMcpServer({
6865
8006
  client: reactorClient,
@@ -6868,21 +8009,24 @@ async function _setupAPI(reactorClient, syncManager, reactorProcessorManager, ht
6868
8009
  }, httpAdapter);
6869
8010
  logger.info(`MCP server available at http://localhost:${port}/mcp`);
6870
8011
  }
8012
+ const dispose = buildApiDispose({
8013
+ graphqlManager,
8014
+ httpRoutes,
8015
+ httpServer,
8016
+ wsServer,
8017
+ dbClosers,
8018
+ logger
8019
+ });
6871
8020
  return {
6872
8021
  httpAdapter,
8022
+ httpRoutes: httpRoutes ?? new HttpRouteService({ httpAdapter }),
6873
8023
  graphqlManager,
6874
8024
  packages,
6875
8025
  attachments,
6876
8026
  attachmentReferenceIndex,
6877
8027
  attachmentAccess,
6878
8028
  authService,
6879
- dispose: buildApiDispose({
6880
- graphqlManager,
6881
- httpServer,
6882
- wsServer,
6883
- dbClosers,
6884
- logger
6885
- })
8029
+ dispose
6886
8030
  };
6887
8031
  }
6888
8032
  /**
@@ -6893,11 +8037,16 @@ async function _setupAPI(reactorClient, syncManager, reactorProcessorManager, ht
6893
8037
  * since this runs on the way to process exit.
6894
8038
  */
6895
8039
  function buildApiDispose(args) {
6896
- const { graphqlManager, httpServer, wsServer, dbClosers, logger } = args;
8040
+ const { graphqlManager, httpRoutes, httpServer, wsServer, dbClosers, logger } = args;
6897
8041
  let disposed = false;
6898
8042
  return async () => {
6899
8043
  if (disposed) return;
6900
8044
  disposed = true;
8045
+ try {
8046
+ httpRoutes?.disposeAll();
8047
+ } catch (error) {
8048
+ logger.error("API dispose: releasing HTTP routes failed: @error", error);
8049
+ }
6901
8050
  try {
6902
8051
  await graphqlManager.shutdown();
6903
8052
  } catch (error) {
@@ -6923,7 +8072,7 @@ function buildApiDispose(args) {
6923
8072
  };
6924
8073
  }
6925
8074
  async function initializeAndStartAPI(clientInitializer, options, processorApp) {
6926
- const { port, httpAdapter, authFetchMiddleware, requireAuthFetchMiddleware, authService, relationalDb, analyticsStore, documentPermissionService, authorizationConfig, attachments, attachmentReferenceIndex, packages, dbClosers, readiness } = await _setupCommonInfrastructure(options);
8075
+ const { port, httpAdapter, httpRoutes, authFetchMiddleware, requireAuthFetchMiddleware, authService, relationalDb, analyticsStore, documentPermissionService, authorizationConfig, attachments, attachmentReferenceIndex, packages, dbClosers, readiness } = await _setupCommonInfrastructure(options);
6927
8076
  const { documentModels, upgradeManifests, processors, subgraphs } = await packages.init();
6928
8077
  const { module: reactorClientModule, reactorDriveClient, attachmentReferenceProjection = {
6929
8078
  status: "unavailable",
@@ -6940,7 +8089,7 @@ async function initializeAndStartAPI(clientInitializer, options, processorApp) {
6940
8089
  const documentModelRegistry = reactorClientModule.reactorModule?.documentModelRegistry;
6941
8090
  if (!documentModelRegistry) throw new Error("DocumentModelRegistry not available from InProcessReactorClientModule");
6942
8091
  return {
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)),
8092
+ ...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), httpRoutes),
6944
8093
  client: reactorClient,
6945
8094
  syncManager,
6946
8095
  documentModelRegistry,
@@ -6949,6 +8098,26 @@ async function initializeAndStartAPI(clientInitializer, options, processorApp) {
6949
8098
  packageManager: packages
6950
8099
  };
6951
8100
  }
8101
+ /**
8102
+ * The origin to advertise in a webhook URL.
8103
+ *
8104
+ * A provider is a third party: it has to be given something it can resolve, so
8105
+ * a relative path is not an option. The platform variables come first, then a
8106
+ * bare deploy domain, and finally the local origin — which is right for `ph
8107
+ * dev` behind a tunnel and, at worst, obviously wrong rather than silently
8108
+ * unusable.
8109
+ */
8110
+ function resolvePublicOrigin(port) {
8111
+ const explicit = process.env.PUBLIC_URL ?? process.env.RENDER_EXTERNAL_URL;
8112
+ if (explicit) return withScheme(explicit);
8113
+ const domain = process.env.HEROKU_APP_DEFAULT_DOMAIN_NAME;
8114
+ if (domain) return withScheme(domain);
8115
+ return `http://localhost:${port}`;
8116
+ }
8117
+ function withScheme(origin) {
8118
+ const trimmed = origin.replace(/\/+$/, "");
8119
+ return /^https?:\/\//.test(trimmed) ? trimmed : `https://${trimmed}`;
8120
+ }
6952
8121
  //#endregion
6953
8122
  //#region src/services/package-storage.ts
6954
8123
  var InMemoryPackageStorage = class {
@@ -7053,7 +8222,7 @@ var PackageManagementService = class {
7053
8222
  }
7054
8223
  };
7055
8224
  //#endregion
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 };
8225
+ export { ADMIN_USERS, ActionCandidateInputSchema, ActionContextInputSchema, ActionInputSchema, AddRelationshipDocument, AnalyticsSubgraph, AttachmentAccessService, AuthDecision, AuthDecisionSchema, AuthService, AuthSubgraph, AuthorizationPolicy, AuthorizedDocumentHandle, BaseSubgraph, CORE_PACKAGE_NAME, CanonicalDocumentIdResolutionError, ChannelMetaInputSchema, CreateDocumentDocument, CreateEmptyDocumentDocument, DeleteDocumentDocument, DeleteDocumentsDocument, DocumentChangeType, DocumentChangeTypeSchema, DocumentChangesDocument, DocumentOperationsFilterInputSchema, DocumentPermissionService, EvaluateActionsDocument, FindDocumentsDocument, GetDocumentDocument, GetDocumentIncomingRelationshipsDocument, GetDocumentModelsDocument, GetDocumentOperationsDocument, GetDocumentOutgoingRelationshipsDocument, GetDocumentWithOperationsDocument, GetJobStatusDocument, GraphQLManager, HttpDocumentModelLoader, HttpPackageLoader, HttpRouteService, ImportPackageLoader, InMemoryPackageStorage, InvalidNamespaceError, JobChangesDocument, MemoryWebhookStore, MoveRelationshipDocument, MutateDocumentAsyncDocument, MutateDocumentDocument, OperationContextInputSchema, OperationInputSchema, OperationWithContextInputSchema, OperationsFilterInputSchema, PACKAGE_ROUTE_SEGMENT, PGLITE_UTC_PARSERS, PackageManagementService, PackageManager, PackagesSubgraph, PagingInputSchema, PhDocumentFieldsFragmentDoc, PollSyncEnvelopesDocument, PropagationMode, PropagationModeSchema, PushSyncEnvelopesDocument, ReactorSignerAppInputSchema, ReactorSignerInputSchema, ReactorSignerUserInputSchema, ReactorSubgraph, RelationalWebhookStore, RemoteCursorInputSchema, RemoteFilterInputSchema, RemoveRelationshipDocument, RenameDocumentDocument, SearchFilterInputSchema, SetPreferredEditorDocument, SyncEnvelopeInputSchema, SyncEnvelopeType, SyncEnvelopeTypeSchema, SystemSubgraph, TouchChannelDocument, TouchChannelInputSchema, ViewFilterInputSchema, WEBHOOK_SEGMENT, WebhookService, assertAuthRequiredForDocumentPermissions, assertCredentialVerifierForSource, assertHostMountPath, 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, namespacePath, namespaceSegments, newWebhookToken, parseDriveUrl, parseWebhookBody, redactHeaders, renderGraphqlPlayground, resolvePackageName, resolveRenownConfig, verifyWebhook };
7057
8226
 
7058
8227
  //# sourceMappingURL=index.mjs.map
7059
- //# debugId=f43e08dc-7398-57dd-8824-db5d97ccc6ca
8228
+ //# debugId=46ad8545-02e9-5fb8-b003-7b9b93f82f75