Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/api/events/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const IS_CLOUDFLARE = process.env.DEPLOY_TARGET === "cloudflare";
* On the Cloudflare deploy the browser opens a WebSocket, not this SSE
* stream: the worker entry (`worker-cf.ts`) intercepts the `Upgrade:
* websocket` request to `/api/events` ahead of OpenNext and terminates it
* against the `MymirBroker` Durable Object (zero-cost while idle via the
* against the `PiyazBroker` Durable Object (zero-cost while idle via the
* Hibernation API). This handler therefore only runs on Cloudflare for a
* non-upgrade GET, where it short-circuits to 204 with `Retry-After` so a
* stray `EventSource` cannot open a wall-clock-billed stream or
Expand Down
868 changes: 846 additions & 22 deletions cloudflare-env.d.ts

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion components/providers/RealtimeBridge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const MAX_BACKOFF_MS = 30_000;
* `EventSource` against the in-memory SSE broker (`_broker.node.ts`). On
* Cloudflare Workers a long-lived SSE connection bills the wall-clock as
* compute and isolates do not share broker state, so the client instead opens
* a WebSocket to the hibernating `MymirBroker` Durable Object (zero compute
* a WebSocket to the hibernating `PiyazBroker` Durable Object (zero compute
* while idle). Both transports feed the same invalidation switch
* ({@link applyRealtimeEvent}); the DO sends SSE-framed payloads over the
* socket so the wire shape matches the EventSource path.
Expand Down
4 changes: 2 additions & 2 deletions lib/auth/verified-oauth-clients.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import "server-only";
* not on the name. Clients not on this list render their raw registered name
* verbatim so the user sees exactly what asked for access.
*
* Populated from `MYMIR_VERIFIED_OAUTH_CLIENT_IDS` (comma-separated client
* Populated from `PIYAZ_VERIFIED_OAUTH_CLIENT_IDS` (comma-separated client
* ids). The parsed set is memoized keyed on the raw env string — env vars
* are fixed per deploy on both Workers and self-host, so in practice the
* parse runs once per process/isolate, and the re-key keeps the function
Expand All @@ -31,7 +31,7 @@ let verifiedClientIds: ReadonlySet<string> = new Set();
* @returns True when the client is on the verified allowlist.
*/
export function isVerifiedOAuthClient(clientId: string): boolean {
const raw = process.env.MYMIR_VERIFIED_OAUTH_CLIENT_IDS ?? "";
const raw = process.env.PIYAZ_VERIFIED_OAUTH_CLIENT_IDS ?? "";
if (raw !== cachedRaw) {
cachedRaw = raw;
verifiedClientIds = new Set(
Expand Down
2 changes: 1 addition & 1 deletion lib/realtime/_broker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@
* `next.config.ts`'s webpack alias rewrites this import to
* `./_broker.workers` on Cloudflare builds and to `./_broker.node`
* everywhere else. The Node variant is the existing in-memory broker;
* the Workers variant proxies to the `MymirBroker` Durable Object.
* the Workers variant proxies to the `PiyazBroker` Durable Object.
*/
export * from "./_broker.node";
36 changes: 18 additions & 18 deletions lib/realtime/_broker.workers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export {
} from "./_broker.node";

/** Stable name for the single broker DO that owns every user's subs. */
const BROKER_DO_NAME = "mymir-broker-global";
const BROKER_DO_NAME = "piyaz-broker-global";

/** Canonical request URL the adapter targets — fixed so signatures match. */
const BROKER_URL = "https://broker/";
Expand Down Expand Up @@ -80,7 +80,7 @@ function resolveBrokerSecret(): string | null {
*
* @param method - HTTP method (POST or GET).
* @param body - Body bytes or `null` for upgrade.
* @param userId - `X-Mymir-User-Id` value or empty string.
* @param userId - `X-Piyaz-User-Id` value or empty string.
* @returns `RequestInit` with method, headers, and body populated.
*/
async function signedRequestInit(
Expand Down Expand Up @@ -157,27 +157,27 @@ class WorkersBroker {
* context is unavailable so misconfigured deploys are diagnosable
* without spamming.
*
* @param namespace - Explicit `MYMIR_BROKER` binding from the worker
* @param namespace - Explicit `PIYAZ_BROKER` binding from the worker
* entry's `env`; omit inside route handlers.
* @returns The DO stub, or `null` when `MYMIR_BROKER` is not bound.
* @returns The DO stub, or `null` when `PIYAZ_BROKER` is not bound.
*/
private stub(namespace?: DurableObjectNamespace): DurableObjectStub | null {
let resolved = namespace;
if (!resolved) {
try {
resolved = (
getCloudflareContext({ async: false }).env as {
MYMIR_BROKER?: DurableObjectNamespace;
PIYAZ_BROKER?: DurableObjectNamespace;
}
).MYMIR_BROKER;
).PIYAZ_BROKER;
} catch {
resolved = undefined;
}
}
if (!resolved) {
if (!warnedMissingBinding) {
console.error(
"[realtime] MYMIR_BROKER binding missing — realtime fanout will silently no-op",
"[realtime] PIYAZ_BROKER binding missing — realtime fanout will silently no-op",
);
warnedMissingBinding = true;
}
Expand Down Expand Up @@ -245,7 +245,7 @@ class WorkersBroker {
*
* @param userId - Caller user id.
* @param keys - Resource keys to register (no TTL).
* @param namespace - Explicit `MYMIR_BROKER` binding from the worker
* @param namespace - Explicit `PIYAZ_BROKER` binding from the worker
* entry's `env`; omit inside route handlers.
* @throws When the binding or signing secret is missing, or the DO
* fetch rejects.
Expand All @@ -258,7 +258,7 @@ class WorkersBroker {
const stub = this.stub(namespace);
if (!stub) {
throw new Error(
"MymirBroker binding missing — cannot register subscriptions",
"PiyazBroker binding missing — cannot register subscriptions",
);
}
const msg: BrokerMessage = {
Expand Down Expand Up @@ -339,7 +339,7 @@ class WorkersBroker {
* incoming frames into the SSE response stream.
*
* @param userId - Caller user id; attached as the DO-side tag.
* @param namespace - Explicit `MYMIR_BROKER` binding from the worker
* @param namespace - Explicit `PIYAZ_BROKER` binding from the worker
* entry's `env`; omit inside route handlers.
* @returns The client end of the WebSocket pair.
* @throws When the binding is missing, the secret is missing, or the DO
Expand All @@ -352,7 +352,7 @@ class WorkersBroker {
const stub = this.stub(namespace);
if (!stub) {
throw new Error(
"MymirBroker binding missing — cannot open WebSocket to DO",
"PiyazBroker binding missing — cannot open WebSocket to DO",
);
}
const { init, secretPresent } = await signedRequestInit(
Expand All @@ -368,7 +368,7 @@ class WorkersBroker {
}
const resp = await stub.fetch(BROKER_URL, init);
if (resp.status !== 101 || !resp.webSocket) {
throw new Error(`MymirBroker upgrade failed: status ${resp.status}`);
throw new Error(`PiyazBroker upgrade failed: status ${resp.status}`);
}
return resp.webSocket;
}
Expand All @@ -381,7 +381,7 @@ class WorkersBroker {
*/
attach(_userId: string, _conn: Connection): void {
throw new Error(
"MymirBroker WorkersBroker: attach is not callable from Workers; use connect(userId) to obtain a WebSocket from the DO",
"PiyazBroker WorkersBroker: attach is not callable from Workers; use connect(userId) to obtain a WebSocket from the DO",
);
}

Expand All @@ -393,7 +393,7 @@ class WorkersBroker {
*/
tryAttach(_userId: string, _conn: Connection): boolean {
throw new Error(
"MymirBroker WorkersBroker: tryAttach is not callable from Workers; use connect(userId) to obtain a WebSocket from the DO",
"PiyazBroker WorkersBroker: tryAttach is not callable from Workers; use connect(userId) to obtain a WebSocket from the DO",
);
}

Expand All @@ -405,7 +405,7 @@ class WorkersBroker {
*/
isAtConnectionLimit(_userId: string): boolean {
throw new Error(
"MymirBroker WorkersBroker: isAtConnectionLimit is not callable from Workers; use connect(userId) to obtain a WebSocket from the DO",
"PiyazBroker WorkersBroker: isAtConnectionLimit is not callable from Workers; use connect(userId) to obtain a WebSocket from the DO",
);
}

Expand All @@ -432,7 +432,7 @@ class WorkersBroker {
*/
*subscribers(_key: ResourceKey): Iterable<string> {
throw new Error(
"MymirBroker WorkersBroker: subscribers is not callable from Workers; use connect(userId) to obtain a WebSocket from the DO",
"PiyazBroker WorkersBroker: subscribers is not callable from Workers; use connect(userId) to obtain a WebSocket from the DO",
);
}

Expand All @@ -444,7 +444,7 @@ class WorkersBroker {
*/
pruneExpired(_userId: string): void {
throw new Error(
"MymirBroker WorkersBroker: pruneExpired is not callable from Workers; use connect(userId) to obtain a WebSocket from the DO",
"PiyazBroker WorkersBroker: pruneExpired is not callable from Workers; use connect(userId) to obtain a WebSocket from the DO",
);
}

Expand All @@ -456,7 +456,7 @@ class WorkersBroker {
*/
_resetForTests(): void {
throw new Error(
"MymirBroker WorkersBroker: _resetForTests is not callable from Workers; use connect(userId) to obtain a WebSocket from the DO",
"PiyazBroker WorkersBroker: _resetForTests is not callable from Workers; use connect(userId) to obtain a WebSocket from the DO",
);
}
}
Expand Down
14 changes: 7 additions & 7 deletions lib/realtime/broker-auth.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
/**
* HMAC-SHA256 signing envelope for the `MymirBroker` Durable Object RPC
* HMAC-SHA256 signing envelope for the `PiyazBroker` Durable Object RPC
* surface. Authenticates every fetch the Workers broker adapter sends to
* the DO so callers cannot spoof a `userId` or fabricate dispatches even
* if they obtain the `MYMIR_BROKER` binding through some other path.
* if they obtain the `PIYAZ_BROKER` binding through some other path.
*
* Signing inputs (canonical string):
*
Expand All @@ -15,11 +15,11 @@
* 60-second freshness window
* - `bodyHashHex` — SHA-256 hex digest of the raw request body bytes
* (`""` when there is no body)
* - `userId` — value of `X-Mymir-User-Id` when set (upgrade only),
* - `userId` — value of `X-Piyaz-User-Id` when set (upgrade only),
* empty string otherwise; included so the user-id header cannot be
* swapped post-signing
*
* Header format: `X-Mymir-Broker-Sig: t=<ts>,n=<nonce>,v=<hex>`.
* Header format: `X-Piyaz-Broker-Sig: t=<ts>,n=<nonce>,v=<hex>`.
*
* Workers-only: this file is imported by `_broker.workers.ts` (adapter)
* and `broker-do.ts` (DO). The self-host bundle ignores both, so this
Expand All @@ -30,10 +30,10 @@
export const BROKER_SIG_MAX_SKEW_MS = 60_000;

/** Header name carrying the signed envelope. */
export const BROKER_SIG_HEADER = "X-Mymir-Broker-Sig";
export const BROKER_SIG_HEADER = "X-Piyaz-Broker-Sig";

/** Header name carrying the user id on the WS upgrade. */
export const BROKER_USER_ID_HEADER = "X-Mymir-User-Id";
export const BROKER_USER_ID_HEADER = "X-Piyaz-User-Id";

/**
* Parse a signed-envelope header into its three fields, or `null` when
Expand Down Expand Up @@ -114,7 +114,7 @@ export async function hmacSha256Hex(
* @param ts - Unix milliseconds.
* @param nonce - Random nonce hex.
* @param bodyHashHex - SHA-256 hex of the body (or empty string).
* @param userId - `X-Mymir-User-Id` header value, or empty string.
* @param userId - `X-Piyaz-User-Id` header value, or empty string.
* @returns Canonical signing string.
*/
export function buildSigningString(
Expand Down
10 changes: 5 additions & 5 deletions lib/realtime/broker-do.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
} from "./broker-auth";

/**
* Wire message sent to the `MymirBroker` Durable Object over the
* Wire message sent to the `PiyazBroker` Durable Object over the
* `fetch(request)` boundary. The DO holds the single broker-global view of
* every user's subscriptions and connected WebSockets.
*/
Expand Down Expand Up @@ -62,7 +62,7 @@ const MAX_CONNECTIONS_PER_USER = 20;
* sockets. This is what keeps a hibernating socket (which, unlike SSE,
* never reconnects to re-register) receiving events after an idle cycle.
*/
export class MymirBroker extends DurableObject<BrokerEnv> {
export class PiyazBroker extends DurableObject<BrokerEnv> {
private subs = new Map<string, Map<ResourceKey, number | null>>();

/** False until {@link ensureHydrated} rebuilds {@link subs} after a wake. */
Expand All @@ -73,7 +73,7 @@ export class MymirBroker extends DurableObject<BrokerEnv> {
* HMAC envelope, then routes WebSocket upgrades to the hibernation
* accept path and JSON RPCs to the subscription / dispatch handlers.
*
* The DO is only reachable to Workers that hold the `MYMIR_BROKER`
* The DO is only reachable to Workers that hold the `PIYAZ_BROKER`
* binding, but the binding alone is not authentication: any caller
* with the binding could spoof `userId` or fabricate dispatches if the
* envelope check were skipped. The check rejects every unsigned or
Expand Down Expand Up @@ -163,7 +163,7 @@ export class MymirBroker extends DurableObject<BrokerEnv> {

/**
* Accept a new WebSocket on behalf of the user named in
* `X-Mymir-User-Id`. Enforces the per-user cap before allocating the
* `X-Piyaz-User-Id`. Enforces the per-user cap before allocating the
* socket pair so a saturated user is rejected without consuming resources.
*
* @param request - Upgrade request carrying the user id header.
Expand All @@ -173,7 +173,7 @@ export class MymirBroker extends DurableObject<BrokerEnv> {
private handleUpgrade(request: Request): Response {
const userId = request.headers.get(BROKER_USER_ID_HEADER);
if (!userId) {
return new Response("Missing X-Mymir-User-Id", { status: 400 });
return new Response("Missing X-Piyaz-User-Id", { status: 400 });
}
if (this.ctx.getWebSockets(userId).length >= MAX_CONNECTIONS_PER_USER) {
return new Response("Connection limit reached", { status: 429 });
Expand Down
2 changes: 1 addition & 1 deletion lib/realtime/broker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*
* - Self-host (default): in-memory `Broker` from `./_broker.node`.
* - Cloudflare Workers (`DEPLOY_TARGET=cloudflare`): Durable Object-backed
* adapter from `./_broker.workers` that proxies to `MymirBroker`.
* adapter from `./_broker.workers` that proxies to `PiyazBroker`.
*
* Call sites import from this file so the alias swap is transparent.
*/
Expand Down
9 changes: 5 additions & 4 deletions open-next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,18 @@ import doQueue from "@opennextjs/cloudflare/overrides/queue/do-queue";
* `NEXT_INC_CACHE_R2_BUCKET` R2 binding.
* - `queue: doQueue` — revalidation queue backed by OpenNext's built-in
* `DOQueueHandler` Durable Object (declared as `NEXT_CACHE_DO_QUEUE` in
* `wrangler.jsonc`; separate from `MymirBroker`).
* `wrangler.jsonc`; separate from `PiyazBroker`).
* - `tagCache: d1NextTagCache` — tag-revalidation cache backed by the
* `NEXT_TAG_CACHE_D1` D1 database.
* - `enableCacheInterception: false` — matches the OpenNext default. The
* flag is documented as "should be false when PPR is used", and Next 16's
* PPR support is on the roadmap (see MYMR-167 follow-ups). Flip to
* `true` only after measuring the cache-hit win against the PPR loss.
*
* `MymirBroker` is injected into `.open-next/worker.js` by
* `scripts/postbuild-cf.ts`; re-exporting it here is dead code because the
* OpenNext worker template does not consume user exports from this file.
* `PiyazBroker` is re-exported from the worker entry (`worker-cf.ts`) and
* resolved at runtime via the `PIYAZ_BROKER` binding in `wrangler.jsonc`;
* re-exporting it here is dead code because the OpenNext worker template
* does not consume user exports from this file.
*/
export default defineCloudflareConfig({
incrementalCache: r2IncrementalCache,
Expand Down
4 changes: 2 additions & 2 deletions scripts/assert-deploy-ready.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
* - `env.production` exists.
* - No KV namespace `id` in `env.production` is all zeros.
* - No D1 `database_id` in `env.production` is the zero UUID.
* - No R2 binding in `env.production` references a `mymir-placeholder-*` bucket.
* - No R2 binding in `env.production` references a `piyaz-placeholder-*` bucket.
* - Every required production secret is registered in the production
* Wrangler env: BROKER_DO_SECRET (broker DO HMAC key), BETTER_AUTH_SECRET
* (Better-auth signing key), DATABASE_URL / DATABASE_SERVICE_ROLE_URL /
Expand All @@ -30,7 +30,7 @@ const WRANGLER_JSONC = path.join(ROOT, "wrangler.jsonc");

const ZERO_KV_ID = "00000000000000000000000000000000";
const ZERO_D1_ID = "00000000-0000-0000-0000-000000000000";
const PLACEHOLDER_BUCKET_RE = /^mymir-placeholder-/i;
const PLACEHOLDER_BUCKET_RE = /^piyaz-placeholder-/i;

interface KvBinding {
binding: string;
Expand Down
2 changes: 1 addition & 1 deletion tests/auth/verified-oauth-clients.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { test, expect, afterEach } from "bun:test";
import { isVerifiedOAuthClient } from "@/lib/auth/verified-oauth-clients";

const ENV_KEY = "MYMIR_VERIFIED_OAUTH_CLIENT_IDS";
const ENV_KEY = "PIYAZ_VERIFIED_OAUTH_CLIENT_IDS";
const originalValue = process.env[ENV_KEY];

afterEach(() => {
Expand Down
Loading