dynamodb-reactive 0.6.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +62 -5
- package/dist/cloudflare.cjs +200 -18
- package/dist/cloudflare.cjs.map +1 -1
- package/dist/cloudflare.d.cts +137 -2
- package/dist/cloudflare.d.ts +137 -2
- package/dist/cloudflare.js +194 -19
- package/dist/cloudflare.js.map +1 -1
- package/dist/cluster.d.cts +1 -1
- package/dist/cluster.d.ts +1 -1
- package/dist/hub-cli.js +121 -0
- package/dist/{protocol-C4Syt_Bl.d.ts → protocol-xs_OqPqe.d.ts} +1 -1
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -628,16 +628,73 @@ override alarm() {
|
|
|
628
628
|
|
|
629
629
|
### Hubs for apps hosted elsewhere
|
|
630
630
|
|
|
631
|
-
An app on Vercel or AWS
|
|
631
|
+
An app on Vercel or AWS cannot hold WebSockets cheaply, so it runs a packaged hub on Cloudflare: the Cloudflare counterpart of `ReactiveEngine`. `defineHub` is the whole Worker; the app supplies only its path rules.
|
|
632
632
|
|
|
633
633
|
```ts
|
|
634
|
-
|
|
634
|
+
// hub/index.ts
|
|
635
|
+
import { defineHub } from 'dynamodb-reactive/cloudflare';
|
|
635
636
|
|
|
636
|
-
const
|
|
637
|
-
|
|
637
|
+
const isWorker = (principal: string) => principal.startsWith('worker:');
|
|
638
|
+
|
|
639
|
+
const hub = defineHub({
|
|
640
|
+
paths: (context) => ({
|
|
641
|
+
'workers.status': {
|
|
642
|
+
authorize: ({ principal }) => principal.startsWith('admin:'),
|
|
643
|
+
snapshot: () => ({ online: context.presentPrincipals() }),
|
|
644
|
+
},
|
|
645
|
+
'jobs.queued': {
|
|
646
|
+
authorize: ({ principal }) => isWorker(principal),
|
|
647
|
+
snapshot: () => ({ ready: true }),
|
|
648
|
+
},
|
|
649
|
+
}),
|
|
650
|
+
// Machine workers that hold a socket count as online; their status path is notified on change.
|
|
651
|
+
presence: { principals: isWorker, notify: ['workers.status'] },
|
|
652
|
+
// Machine workers get tickets from the hub with the notify secret.
|
|
653
|
+
machinePrincipal: isWorker,
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
export const ReactiveHub = hub.ReactiveHub;
|
|
657
|
+
export default hub.worker;
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
Deploy it with the bundled command, which writes the Wrangler config (Durable Object binding and migration, `workers_dev`, allowed origins), deploys, and puts the secrets. It needs `wrangler` installed and `CLOUDFLARE_API_TOKEN`:
|
|
661
|
+
|
|
662
|
+
```sh
|
|
663
|
+
dynamodb-reactive-hub deploy --name my-hub --entry hub/index.ts --account <account-id> --allowed-origins https://my.app --secret-root-env APP_SECRET
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
The hub is served at `https://my-hub.<account subdomain>.workers.dev`; keep that URL as a constant in the app. Its routes are `HUB_ROUTES`:
|
|
667
|
+
|
|
668
|
+
| Route | Auth | Purpose |
|
|
669
|
+
| -------------------------------- | ------------- | --------------------------------------------------------- |
|
|
670
|
+
| `POST /api/hub/token?principal=` | notify secret | ticket for a `machinePrincipal` |
|
|
671
|
+
| `GET /api/hub/socket?token=` | signed ticket | WebSocket upgrade |
|
|
672
|
+
| `POST /api/hub/reactive` | signed ticket | subscribe and unsubscribe |
|
|
673
|
+
| `POST /api/hub/notify` | notify secret | `{ path, patches? }` after the data behind a path changed |
|
|
674
|
+
| `GET /api/hub/presence` | notify secret | `{ principals }` holding a live socket |
|
|
675
|
+
|
|
676
|
+
No hub secret has to be distributed. Both are HMAC digests of a secret the app already holds (`--secret-root-env`), and every backend derives them with `deriveHubSecrets(root, name)`:
|
|
677
|
+
|
|
678
|
+
```ts
|
|
679
|
+
import { deriveHubSecrets, mintHubTicket, remoteHub } from 'dynamodb-reactive/cloudflare';
|
|
680
|
+
|
|
681
|
+
const HUB_ORIGIN = 'https://my-hub.example.workers.dev';
|
|
682
|
+
const secrets = await deriveHubSecrets(process.env.APP_SECRET!, 'my-hub');
|
|
683
|
+
|
|
684
|
+
// After a write, from Vercel, a Lambda or a worker:
|
|
685
|
+
await remoteHub({ url: `${HUB_ORIGIN}/api/hub/notify`, secret: secrets.notify }).notify(
|
|
686
|
+
'jobs.queued',
|
|
687
|
+
);
|
|
688
|
+
|
|
689
|
+
// In the app's ticket route, for the signed-in user:
|
|
690
|
+
const ticket = await mintHubTicket({
|
|
691
|
+
origin: HUB_ORIGIN,
|
|
692
|
+
secret: secrets.ticket,
|
|
693
|
+
principal: 'admin:me',
|
|
694
|
+
});
|
|
638
695
|
```
|
|
639
696
|
|
|
640
|
-
|
|
697
|
+
`remoteHub` calls abort after `timeoutMs` (default 3000 ms) and throw on a timeout or non-2xx answer, so wrap them where a lost notify must not fail the write. `fetchHubPresence` reads the presence route. When the data lives outside Cloudflare, a path's snapshot can be a small marker such as `{ ready: true }`, and the page re-fetches from its own API whenever it is pushed.
|
|
641
698
|
|
|
642
699
|
## Delivery model
|
|
643
700
|
|
package/dist/cloudflare.cjs
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
// ../client/src/log.ts
|
|
4
|
+
function error(...values) {
|
|
5
|
+
console.error(...values);
|
|
6
|
+
}
|
|
7
|
+
|
|
3
8
|
// ../client/src/cluster.ts
|
|
4
9
|
var DEFAULT_TICKET_LIFETIME_SECONDS = 60;
|
|
5
10
|
function isRecord(value) {
|
|
@@ -263,7 +268,7 @@ var HEARTBEAT_TIMEOUT_CLOSE_CODE = 4001;
|
|
|
263
268
|
function json(value, status = 200) {
|
|
264
269
|
return Response.json(value, { status, headers: { "cache-control": "no-store" } });
|
|
265
270
|
}
|
|
266
|
-
function
|
|
271
|
+
function error2(message, status) {
|
|
267
272
|
return json({ type: "error", message }, status);
|
|
268
273
|
}
|
|
269
274
|
function workersGlobal(name) {
|
|
@@ -335,7 +340,7 @@ function createDurableObjectHub(ctx, options) {
|
|
|
335
340
|
};
|
|
336
341
|
const accept = async (request) => {
|
|
337
342
|
const principal = request.headers.get(HUB_PRINCIPAL_HEADER);
|
|
338
|
-
if (!principal) return
|
|
343
|
+
if (!principal) return error2("Unauthorized", 401);
|
|
339
344
|
const Pair = options.WebSocketPair ?? workersGlobal("WebSocketPair");
|
|
340
345
|
if (!Pair) throw new Error("WebSocketPair is not available in this runtime");
|
|
341
346
|
const pair = new Pair();
|
|
@@ -370,10 +375,10 @@ function createDurableObjectHub(ctx, options) {
|
|
|
370
375
|
await hub.publish(action.path, action.patches);
|
|
371
376
|
return json({ type: "ok" });
|
|
372
377
|
default:
|
|
373
|
-
return
|
|
378
|
+
return error2("Unknown hub action", 400);
|
|
374
379
|
}
|
|
375
380
|
} catch (cause) {
|
|
376
|
-
if (cause instanceof ReactiveHubError) return
|
|
381
|
+
if (cause instanceof ReactiveHubError) return error2(cause.message, 400);
|
|
377
382
|
throw cause;
|
|
378
383
|
}
|
|
379
384
|
};
|
|
@@ -455,12 +460,14 @@ function hubClient(stub) {
|
|
|
455
460
|
publish: async (path, patches) => ok(await post({ action: "publish", path, patches }))
|
|
456
461
|
};
|
|
457
462
|
}
|
|
463
|
+
var DEFAULT_REMOTE_HUB_TIMEOUT_MS = 3e3;
|
|
458
464
|
function remoteHub(options) {
|
|
459
465
|
const send2 = async (body) => {
|
|
460
466
|
const response = await (options.fetch ?? fetch)(options.url, {
|
|
461
467
|
method: "POST",
|
|
462
468
|
headers: { authorization: `Bearer ${options.secret}`, "content-type": "application/json" },
|
|
463
|
-
body: JSON.stringify(body)
|
|
469
|
+
body: JSON.stringify(body),
|
|
470
|
+
signal: AbortSignal.timeout(options.timeoutMs ?? DEFAULT_REMOTE_HUB_TIMEOUT_MS)
|
|
464
471
|
});
|
|
465
472
|
if (!response.ok) throw new Error(`Hub notify failed: HTTP ${response.status}`);
|
|
466
473
|
};
|
|
@@ -510,31 +517,31 @@ async function handleReactiveRoutes(request, options) {
|
|
|
510
517
|
});
|
|
511
518
|
}
|
|
512
519
|
if (url.pathname === routes.notify) {
|
|
513
|
-
if (request.method !== "POST") return
|
|
514
|
-
if (!options.notifySecret) return
|
|
520
|
+
if (request.method !== "POST") return error2("Method not allowed", 405);
|
|
521
|
+
if (!options.notifySecret) return error2("Hub notify is not configured", 503);
|
|
515
522
|
if (!sameSecret(bearerToken(request), options.notifySecret)) {
|
|
516
|
-
return
|
|
523
|
+
return error2("Unauthorized", 401);
|
|
517
524
|
}
|
|
518
525
|
let body2;
|
|
519
526
|
try {
|
|
520
527
|
body2 = await request.json();
|
|
521
528
|
} catch {
|
|
522
|
-
return
|
|
529
|
+
return error2("Invalid JSON", 400);
|
|
523
530
|
}
|
|
524
531
|
const { path, patches } = body2 ?? {};
|
|
525
532
|
if (typeof path !== "string" || patches !== void 0 && !Array.isArray(patches)) {
|
|
526
|
-
return
|
|
533
|
+
return error2("Expected { path, patches? }", 400);
|
|
527
534
|
}
|
|
528
535
|
const client = hubClient(options.hub());
|
|
529
536
|
if (patches) await client.publish(path, patches);
|
|
530
537
|
else await client.notify(path);
|
|
531
538
|
return json({ type: "ok" });
|
|
532
539
|
}
|
|
533
|
-
if (!secret) return withCors(
|
|
540
|
+
if (!secret) return withCors(error2("Reactive hub is not configured", 503), corsOrigin);
|
|
534
541
|
if (url.pathname === routes.ticket) {
|
|
535
|
-
if (request.method !== "POST") return withCors(
|
|
542
|
+
if (request.method !== "POST") return withCors(error2("Method not allowed", 405), corsOrigin);
|
|
536
543
|
const principal2 = await options.authenticate(request);
|
|
537
|
-
if (!principal2) return withCors(
|
|
544
|
+
if (!principal2) return withCors(error2("Unauthorized", 401), corsOrigin);
|
|
538
545
|
const socketUrl = new URL(routes.socket, url);
|
|
539
546
|
socketUrl.protocol = "wss:";
|
|
540
547
|
return withCors(
|
|
@@ -551,7 +558,7 @@ async function handleReactiveRoutes(request, options) {
|
|
|
551
558
|
}
|
|
552
559
|
if (url.pathname === routes.socket) {
|
|
553
560
|
if (request.headers.get("upgrade") !== "websocket") {
|
|
554
|
-
return
|
|
561
|
+
return error2("Expected a WebSocket upgrade", 426);
|
|
555
562
|
}
|
|
556
563
|
const principal2 = await verifySignedClusterTicket(url.searchParams.get("token") ?? "", secret);
|
|
557
564
|
if (!principal2) return new Response("Unauthorized", { status: 401 });
|
|
@@ -559,24 +566,199 @@ async function handleReactiveRoutes(request, options) {
|
|
|
559
566
|
forwarded.headers.set(HUB_PRINCIPAL_HEADER, principal2);
|
|
560
567
|
return options.hub().fetch(forwarded);
|
|
561
568
|
}
|
|
562
|
-
if (request.method !== "POST") return withCors(
|
|
569
|
+
if (request.method !== "POST") return withCors(error2("Method not allowed", 405), corsOrigin);
|
|
563
570
|
const principal = await verifySignedClusterTicket(bearerToken(request), secret);
|
|
564
|
-
if (!principal) return withCors(
|
|
571
|
+
if (!principal) return withCors(error2("Unauthorized", 401), corsOrigin);
|
|
565
572
|
let body;
|
|
566
573
|
try {
|
|
567
574
|
body = await request.json();
|
|
568
575
|
} catch {
|
|
569
|
-
return withCors(
|
|
576
|
+
return withCors(error2("Invalid JSON", 400), corsOrigin);
|
|
570
577
|
}
|
|
571
|
-
if (!isHubRequest(body)) return withCors(
|
|
578
|
+
if (!isHubRequest(body)) return withCors(error2("Unsupported reactive request", 400), corsOrigin);
|
|
572
579
|
return withCors(await hubClient(options.hub()).request(body, principal), corsOrigin);
|
|
573
580
|
}
|
|
574
581
|
|
|
582
|
+
// ../client/src/cloudflare-hub.ts
|
|
583
|
+
var HUB_ROUTES = {
|
|
584
|
+
/** `POST` with the notify secret and `?principal=`: a ticket for a machine client. */
|
|
585
|
+
ticket: "/api/hub/token",
|
|
586
|
+
/** WebSocket upgrade with a signed ticket in `?token=`. */
|
|
587
|
+
socket: "/api/hub/socket",
|
|
588
|
+
/** Reactive HTTP subscribe and unsubscribe, with a signed ticket as the bearer. */
|
|
589
|
+
reactive: "/api/hub/reactive",
|
|
590
|
+
/** `POST { path, patches? }` with the notify secret, after the data behind a path changed. */
|
|
591
|
+
notify: "/api/hub/notify",
|
|
592
|
+
/** `GET` with the notify secret: `{ principals }`, the tracked principals holding a live socket. */
|
|
593
|
+
presence: "/api/hub/presence"
|
|
594
|
+
};
|
|
595
|
+
var HUB_ENV = {
|
|
596
|
+
TICKET_SECRET: "REACTIVE_TICKET_SECRET",
|
|
597
|
+
NOTIFY_SECRET: "HUB_NOTIFY_SECRET",
|
|
598
|
+
/** Comma-separated browser origins allowed to call the hub cross-origin. */
|
|
599
|
+
ALLOWED_ORIGINS: "ALLOWED_ORIGINS"
|
|
600
|
+
};
|
|
601
|
+
var HUB_DURABLE_OBJECT = { className: "ReactiveHub", binding: "REACTIVE_HUB" };
|
|
602
|
+
var HUB_INSTANCE = "hub";
|
|
603
|
+
var PRESENCE_ACTION_URL = "https://reactive-hub/presence";
|
|
604
|
+
function sameSecret2(given, expected) {
|
|
605
|
+
const left = new TextEncoder().encode(given);
|
|
606
|
+
const right = new TextEncoder().encode(expected);
|
|
607
|
+
let difference = left.length ^ right.length;
|
|
608
|
+
for (let index = 0; index < right.length; index++) {
|
|
609
|
+
difference |= (left[index] ?? 0) ^ right[index];
|
|
610
|
+
}
|
|
611
|
+
return difference === 0;
|
|
612
|
+
}
|
|
613
|
+
function parseOrigins(value) {
|
|
614
|
+
return (value ?? "").split(",").map((origin) => origin.trim().replace(/\/+$/u, "")).filter(Boolean);
|
|
615
|
+
}
|
|
616
|
+
function defineHub(config) {
|
|
617
|
+
class ReactiveHub {
|
|
618
|
+
#ctx;
|
|
619
|
+
#reactive;
|
|
620
|
+
constructor(ctx, _env) {
|
|
621
|
+
this.#ctx = ctx;
|
|
622
|
+
const context = {
|
|
623
|
+
present: (principal) => this.#reactive.present(principal),
|
|
624
|
+
presentPrincipals: () => this.#presentPrincipals()
|
|
625
|
+
};
|
|
626
|
+
const presence = config.presence;
|
|
627
|
+
this.#reactive = createDurableObjectHub(ctx, {
|
|
628
|
+
paths: config.paths(context),
|
|
629
|
+
...presence ? {
|
|
630
|
+
presence: {
|
|
631
|
+
principals: presence.principals,
|
|
632
|
+
...presence.timeoutMs === void 0 ? {} : { timeoutMs: presence.timeoutMs },
|
|
633
|
+
onChange: async () => {
|
|
634
|
+
for (const path of presence.notify ?? []) {
|
|
635
|
+
await this.#reactive.hub.notify(path);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
} : {}
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
#presentPrincipals() {
|
|
643
|
+
const principals = /* @__PURE__ */ new Set();
|
|
644
|
+
for (const socket of this.#ctx.getWebSockets()) {
|
|
645
|
+
const principal = this.#reactive.principalOf(socket);
|
|
646
|
+
if (this.#reactive.present(principal)) principals.add(principal);
|
|
647
|
+
}
|
|
648
|
+
return [...principals].sort();
|
|
649
|
+
}
|
|
650
|
+
async fetch(request) {
|
|
651
|
+
if (request.url === PRESENCE_ACTION_URL) {
|
|
652
|
+
return Response.json(
|
|
653
|
+
{ principals: this.#presentPrincipals() },
|
|
654
|
+
{ headers: { "cache-control": "no-store" } }
|
|
655
|
+
);
|
|
656
|
+
}
|
|
657
|
+
return this.#reactive.fetch(request);
|
|
658
|
+
}
|
|
659
|
+
webSocketMessage(socket, message) {
|
|
660
|
+
return this.#reactive.webSocketMessage(socket, message);
|
|
661
|
+
}
|
|
662
|
+
webSocketClose(socket, code) {
|
|
663
|
+
return this.#reactive.webSocketClose(socket, code);
|
|
664
|
+
}
|
|
665
|
+
webSocketError(socket) {
|
|
666
|
+
return this.#reactive.webSocketError(socket);
|
|
667
|
+
}
|
|
668
|
+
alarm() {
|
|
669
|
+
return this.#reactive.alarm();
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
const worker = {
|
|
673
|
+
async fetch(request, env) {
|
|
674
|
+
const stub = () => env.REACTIVE_HUB.get(env.REACTIVE_HUB.idFromName(HUB_INSTANCE));
|
|
675
|
+
const notifySecret = env.HUB_NOTIFY_SECRET;
|
|
676
|
+
const authorized = () => Boolean(notifySecret) && sameSecret2(bearerToken(request), notifySecret);
|
|
677
|
+
try {
|
|
678
|
+
if (new URL(request.url).pathname === HUB_ROUTES.presence) {
|
|
679
|
+
if (request.method !== "GET") return new Response(null, { status: 405 });
|
|
680
|
+
if (!notifySecret) return new Response(null, { status: 503 });
|
|
681
|
+
if (!authorized()) return new Response(null, { status: 401 });
|
|
682
|
+
return await stub().fetch(new Request(PRESENCE_ACTION_URL));
|
|
683
|
+
}
|
|
684
|
+
const response = await handleReactiveRoutes(request, {
|
|
685
|
+
routes: HUB_ROUTES,
|
|
686
|
+
secret: env.REACTIVE_TICKET_SECRET,
|
|
687
|
+
notifySecret,
|
|
688
|
+
allowedOrigins: parseOrigins(env.ALLOWED_ORIGINS),
|
|
689
|
+
hub: stub,
|
|
690
|
+
authenticate: (incoming) => {
|
|
691
|
+
const principal = new URL(incoming.url).searchParams.get("principal") ?? "";
|
|
692
|
+
return authorized() && config.machinePrincipal?.(principal) ? principal : void 0;
|
|
693
|
+
}
|
|
694
|
+
});
|
|
695
|
+
return response ?? new Response("Not found", { status: 404 });
|
|
696
|
+
} catch (cause) {
|
|
697
|
+
error("hub request failed", request.method, new URL(request.url).pathname, cause);
|
|
698
|
+
return Response.json({ error: "Hub error" }, { status: 500 });
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
};
|
|
702
|
+
return { ReactiveHub, worker };
|
|
703
|
+
}
|
|
704
|
+
async function hmacHex(key, label) {
|
|
705
|
+
const encoder2 = new TextEncoder();
|
|
706
|
+
const cryptoKey = await crypto.subtle.importKey(
|
|
707
|
+
"raw",
|
|
708
|
+
encoder2.encode(key),
|
|
709
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
710
|
+
false,
|
|
711
|
+
["sign"]
|
|
712
|
+
);
|
|
713
|
+
const signature = new Uint8Array(
|
|
714
|
+
await crypto.subtle.sign("HMAC", cryptoKey, encoder2.encode(label))
|
|
715
|
+
);
|
|
716
|
+
return Array.from(signature, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
717
|
+
}
|
|
718
|
+
async function deriveHubSecrets(root, name) {
|
|
719
|
+
const [ticket, notify] = await Promise.all([
|
|
720
|
+
hmacHex(root, `${name}-ticket`),
|
|
721
|
+
hmacHex(root, `${name}-notify`)
|
|
722
|
+
]);
|
|
723
|
+
return { ticket, notify };
|
|
724
|
+
}
|
|
725
|
+
async function mintHubTicket(options) {
|
|
726
|
+
const socketUrl = new URL(HUB_ROUTES.socket, options.origin);
|
|
727
|
+
socketUrl.protocol = "wss:";
|
|
728
|
+
const ticket = await createSignedClusterTicket({
|
|
729
|
+
secret: options.secret,
|
|
730
|
+
principal: options.principal,
|
|
731
|
+
url: socketUrl.toString(),
|
|
732
|
+
...options.lifetimeSeconds === void 0 ? {} : { lifetimeSeconds: options.lifetimeSeconds }
|
|
733
|
+
});
|
|
734
|
+
return { ...ticket, httpUrl: new URL(HUB_ROUTES.reactive, options.origin).toString() };
|
|
735
|
+
}
|
|
736
|
+
async function fetchHubPresence(options) {
|
|
737
|
+
try {
|
|
738
|
+
const response = await (options.fetch ?? fetch)(new URL(HUB_ROUTES.presence, options.origin), {
|
|
739
|
+
headers: { authorization: `Bearer ${options.secret}` },
|
|
740
|
+
signal: AbortSignal.timeout(options.timeoutMs ?? 3e3)
|
|
741
|
+
});
|
|
742
|
+
if (!response.ok) return void 0;
|
|
743
|
+
const { principals } = await response.json();
|
|
744
|
+
return Array.isArray(principals) && principals.every((value) => typeof value === "string") ? principals : void 0;
|
|
745
|
+
} catch {
|
|
746
|
+
return void 0;
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
575
750
|
exports.HEARTBEAT_TIMEOUT_CLOSE_CODE = HEARTBEAT_TIMEOUT_CLOSE_CODE;
|
|
751
|
+
exports.HUB_DURABLE_OBJECT = HUB_DURABLE_OBJECT;
|
|
752
|
+
exports.HUB_ENV = HUB_ENV;
|
|
576
753
|
exports.HUB_PRINCIPAL_HEADER = HUB_PRINCIPAL_HEADER;
|
|
754
|
+
exports.HUB_ROUTES = HUB_ROUTES;
|
|
577
755
|
exports.createDurableObjectHub = createDurableObjectHub;
|
|
756
|
+
exports.defineHub = defineHub;
|
|
757
|
+
exports.deriveHubSecrets = deriveHubSecrets;
|
|
758
|
+
exports.fetchHubPresence = fetchHubPresence;
|
|
578
759
|
exports.handleReactiveRoutes = handleReactiveRoutes;
|
|
579
760
|
exports.hubClient = hubClient;
|
|
761
|
+
exports.mintHubTicket = mintHubTicket;
|
|
580
762
|
exports.remoteHub = remoteHub;
|
|
581
763
|
//# sourceMappingURL=cloudflare.cjs.map
|
|
582
764
|
//# sourceMappingURL=cloudflare.cjs.map
|