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
4 changes: 2 additions & 2 deletions .agents/skills/mock-discord-testing/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Discord continuity is inferred from **task thread → active cloud job**: root-c
| Mock state endpoint | `http://127.0.0.1:3014/mock/state` |
| Mock event injection endpoint | `http://127.0.0.1:3014/mock/events` |
| Example scenario | `packages/communication/scripts/mock-discord.example.json` |
| Gateway secret source | `R_DISCORD_GATEWAY_SECRET`, falling back to `ENCRYPTION_KEY` |
| Gateway secret source | `R_DISCORD_GATEWAY_SECRET` (auto-generated on Discord save; local-dev env default also available) |
| Mock bot identity | `RoomoteBot` (id `100000000000000001`) |
| Mock application id | `200000000000000001` |
| Mock guild id | `300000000000000001` |
Expand Down Expand Up @@ -57,7 +57,7 @@ The API resolves Discord credentials from real env vars first (`resolveDiscordRu
```bash
R_DISCORD_BOT_TOKEN=mock-discord-token # must match the harness botToken (default: mock-discord-token)
DISCORD_API_BASE_URL=http://127.0.0.1:3014/api/v10 # reroutes ALL outbound Discord REST calls to the harness
# R_DISCORD_GATEWAY_SECRET is optional; the events endpoint falls back to ENCRYPTION_KEY
# R_DISCORD_GATEWAY_SECRET is required; development injects a local default when unset.
```

The harness reads the same gateway secret via dotenvx, so the `x-roomote-discord-gateway-secret` header matches automatically. Without any secret configured the events endpoint returns 503; with a mismatched secret it returns 401.
Expand Down
5 changes: 5 additions & 0 deletions .env.production.example
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ PREVIEW_AUTH_PUBLIC_KEY=
# R_AUTO_GENERATE_KEYS=true
# Random single-line secrets generated by the operator.
ENCRYPTION_KEY=
# Shared internal secret for Discord-gateway→API event delivery (API + BullMQ).
# Required when Discord is enabled. Use a dedicated random value — do not reuse
# ENCRYPTION_KEY. The installer and Railway/Render/Coolify templates generate
# it; UI Discord save also mints it when missing.
# R_DISCORD_GATEWAY_SECRET=
ARTIFACT_SIGNING_KEY=
DASHBOARD_PASSWORD=
# Optional dedicated secret for signing sign-in session cookies (>=32 chars).
Expand Down
7 changes: 5 additions & 2 deletions SELF_HOSTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,7 @@ base64 < preview-auth-private-pkcs8.pem | tr -d '\n' # PREVIEW_AUTH_PRIVATE_KEY
base64 < preview-auth-public.pem | tr -d '\n' # PREVIEW_AUTH_PUBLIC_KEY

openssl rand -base64 32 # ENCRYPTION_KEY
openssl rand -base64 32 # R_DISCORD_GATEWAY_SECRET (dedicated; do not reuse ENCRYPTION_KEY)
openssl rand -base64 32 # ARTIFACT_SIGNING_KEY
openssl rand -base64 24 # DASHBOARD_PASSWORD
openssl rand -hex 16 # SETUP_TOKEN
Expand All @@ -496,8 +497,10 @@ example PaaS platforms such as Railway or Render), leave the four
keypairs once at first startup, stores them encrypted with `ENCRYPTION_KEY` in
the database, and reuses them on every later boot. Env-provided key values
always take precedence. The random string secrets (`ENCRYPTION_KEY`,
`ARTIFACT_SIGNING_KEY`, `DASHBOARD_PASSWORD`, `SETUP_TOKEN`) are still
required and can come from the platform's secret generator.
`R_DISCORD_GATEWAY_SECRET`, `ARTIFACT_SIGNING_KEY`, `DASHBOARD_PASSWORD`,
`SETUP_TOKEN`) are still required and can come from the platform's secret
generator. API and BullMQ must share the same `R_DISCORD_GATEWAY_SECRET` value
when Discord is enabled; that value must not be the same as `ENCRYPTION_KEY`.

### Port exposure and the queue dashboard

Expand Down
59 changes: 59 additions & 0 deletions apps/api/src/handlers/discord/__tests__/auth.test.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 4 additions & 10 deletions apps/api/src/handlers/discord/auth.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { timingSafeEqual } from 'node:crypto';

import { Env } from '@roomote/env';
import { resolveDiscordGatewaySecret } from '@roomote/db/server';

const DISCORD_GATEWAY_SECRET_HEADER =
'x-roomote-discord-gateway-secret' as const;
Expand All @@ -12,12 +12,6 @@ type DiscordGatewayAuthError = {
status: 503 | 401;
};

function configuredGatewaySecret(): string | null {
const value =
process.env.R_DISCORD_GATEWAY_SECRET?.trim() || Env.ENCRYPTION_KEY?.trim();
return value || null;
}

function secretsMatch(expected: string, received: string): boolean {
const expectedBytes = Buffer.from(expected, 'utf8');
const receivedBytes = Buffer.from(received, 'utf8');
Expand All @@ -27,10 +21,10 @@ function secretsMatch(expected: string, received: string): boolean {
);
}

export function verifyDiscordGatewaySecret(
export async function verifyDiscordGatewaySecret(
received: string | undefined,
): DiscordGatewayAuthError | null {
const expected = configuredGatewaySecret();
): Promise<DiscordGatewayAuthError | null> {
const expected = await resolveDiscordGatewaySecret();
if (!expected) {
return {
error: 'discord_gateway_secret_not_configured',
Expand Down
2 changes: 1 addition & 1 deletion apps/api/src/handlers/discord/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -431,7 +431,7 @@ async function processDiscordGatewayEvent(event: DiscordGatewayEvent) {
export const discord = new Hono();

discord.post('/events', async (c) => {
const authError = verifyDiscordGatewaySecret(
const authError = await verifyDiscordGatewaySecret(
c.req.header(DISCORD_GATEWAY_SECRET_HEADER),
);
if (authError) {
Expand Down
11 changes: 1 addition & 10 deletions apps/discord-gateway/src/config.test.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 3 additions & 8 deletions apps/discord-gateway/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ function withoutTrailingSlashes(value: string): string {
export type DiscordGatewayConfig = {
port: number;
apiEventsUrl: string;
apiSecret: string | null;
/** Snapshot used for secret/credential env resolution when embedded. */
processEnv: NodeJS.ProcessEnv;
apiTimeoutMs: number;
leaderLeaseTtlSeconds: number;
leaderLeaseRenewMs: number;
Expand All @@ -44,13 +45,7 @@ export function resolveDiscordGatewayConfig(
return {
port: positiveInteger(env.PORT, DEFAULT_PORT),
apiEventsUrl: `${withoutTrailingSlashes(apiBaseUrl)}/api/internal/discord/events`,
// ENCRYPTION_KEY is already shared between API/control-plane processes.
// A dedicated secret is preferred, while this fallback keeps existing
// self-hosted and local deployments bootable during the rollout.
apiSecret:
env.R_DISCORD_GATEWAY_SECRET?.trim() ||
env.ENCRYPTION_KEY?.trim() ||
null,
processEnv: env,
apiTimeoutMs: positiveInteger(
env.DISCORD_GATEWAY_API_TIMEOUT_MS,
DEFAULT_API_TIMEOUT_MS,
Expand Down
27 changes: 27 additions & 0 deletions apps/discord-gateway/src/credentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,30 @@ export async function resolveDiscordGatewayCredentials(): Promise<DiscordGateway
.digest('hex'),
};
}

/**
* Resolve the dedicated gateway↔API transport secret: live process.env first,
* then the embedded env snapshot, then the deployment vault.
*/
export async function resolveDiscordGatewayApiSecret(
env: NodeJS.ProcessEnv = process.env,
): Promise<string | null> {
const fromLive = process.env.R_DISCORD_GATEWAY_SECRET?.trim() || null;
if (fromLive) {
return fromLive;
}

const fromSnapshot = env.R_DISCORD_GATEWAY_SECRET?.trim() || null;
if (fromSnapshot) {
return fromSnapshot;
}

const dbServer = (await import('@roomote/db/server')) as unknown as {
resolveDiscordGatewaySecret?: () => Promise<string | null>;
};
if (dbServer.resolveDiscordGatewaySecret) {
return dbServer.resolveDiscordGatewaySecret();
}

return null;
}
2 changes: 1 addition & 1 deletion apps/discord-gateway/src/embedded.test.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading