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
6 changes: 3 additions & 3 deletions .claude/agents/discord-iam-reviewer.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ tools: Bash, Read, Grep, Glob

You review changes to the Discord serverless path for security regressions. The architecture (from CLAUDE.md):

- `@gsd/lambda-interactions` is exposed via a public Function URL. It MUST verify the Ed25519 signature against the public key in Secrets Manager before doing anything else.
- `@hyveon/lambda-interactions` is exposed via a public Function URL. It MUST verify the Ed25519 signature against the public key in Secrets Manager before doing anything else.
- The interactions Lambda enforces `allowedGuilds` from `pk="CONFIG#discord"` in DynamoDB. This is the only allowlist gate.
- `canRun()` in `@gsd/shared/canRun` is the single permission resolver. Order: guild allowlist → admin user/role → per-game user/role + action gate.
- Slash commands are JSON descriptors in `@gsd/shared/commands.ts`. Adding one requires a new entry in `actionForCommand()` so `canRun()` gets the right bucket.
- `canRun()` in `@hyveon/shared/canRun` is the single permission resolver. Order: guild allowlist → admin user/role → per-game user/role + action gate.
- Slash commands are JSON descriptors in `@hyveon/shared/commands.ts`. Adding one requires a new entry in `actionForCommand()` so `canRun()` gets the right bucket.
- Per-guild command registration only — never global commands.
- Neither the bot token nor the public key is ever returned to the client; `getRedacted()` exposes booleans.
- The full deploy IAM policy `GameServerDeployAll` lives only in `docs/setup.md`.
Expand Down
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ Thumbs.db

# Auto-generated at dev/build time — committed as null, real state must not
# be committed (contains AWS ARNs/account IDs).
app/packages/server/src/generated/tfstate.ts
app/packages/desktop-main/src/generated/tfstate.ts

# Playwright artifacts (traces, videos, screenshots) and HTML report.
app/packages/web/test-results/
Expand Down
40 changes: 20 additions & 20 deletions CLAUDE.md

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ COPY package.json package-lock.json ./
# Copy every workspace member's package.json so npm ci can resolve them.
COPY app/package.json app/
COPY app/packages/shared/package.json app/packages/shared/
COPY app/packages/server/package.json app/packages/server/
COPY app/packages/desktop-main/package.json app/packages/desktop-main/
COPY app/packages/web/package.json app/packages/web/
COPY app/packages/lambda/interactions/package.json app/packages/lambda/interactions/
COPY app/packages/lambda/followup/package.json app/packages/lambda/followup/
Expand All @@ -36,4 +36,4 @@ EXPOSE 3001

ENV NODE_ENV=production

CMD ["node", "packages/server/dist/main.js"]
CMD ["node", "packages/desktop-main/dist/main.js"]
18 changes: 9 additions & 9 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ LAMBDAS_STAMP := $(STAMP)/lambdas.stamp

# ── Source globs for change detection ───────────────────────────────────────
SHARED_SRCS := $(shell find $(APP_DIR)/packages/shared/src -name '*.ts' 2>/dev/null)
SERVER_SRCS := $(shell find $(APP_DIR)/packages/server/src -name '*.ts' 2>/dev/null)
SERVER_SRCS := $(shell find $(APP_DIR)/packages/desktop-main/src -name '*.ts' 2>/dev/null)
WEB_SRCS := $(shell find $(APP_DIR)/packages/web/src -name '*.ts' -o -name '*.tsx' -o -name '*.css' 2>/dev/null) \
$(APP_DIR)/packages/web/index.html \
$(APP_DIR)/packages/web/vite.config.ts
Expand Down Expand Up @@ -41,25 +41,25 @@ install: $(INSTALL_STAMP)

# ── Shared ───────────────────────────────────────────────────────────────────
$(SHARED_STAMP): $(INSTALL_STAMP) $(SHARED_SRCS) $(TS_CONFIGS)
cd $(APP_DIR) && npm run build -w @gsd/shared
cd $(APP_DIR) && npm run build -w @hyveon/shared
touch $@

# ── Server ───────────────────────────────────────────────────────────────────
$(SERVER_STAMP): $(SHARED_STAMP) $(SERVER_SRCS) $(TS_CONFIGS)
cd $(APP_DIR) && npm run build -w @gsd/server
cd $(APP_DIR) && npm run build -w @hyveon/desktop-main
touch $@

# ── Web ──────────────────────────────────────────────────────────────────────
$(WEB_STAMP): $(SHARED_STAMP) $(WEB_SRCS) $(TS_CONFIGS)
cd $(APP_DIR) && npm run build -w @gsd/web
cd $(APP_DIR) && npm run build -w @hyveon/web
touch $@

# ── Lambdas ──────────────────────────────────────────────────────────────────
$(LAMBDAS_STAMP): $(SHARED_STAMP) $(LAMBDA_SRCS) $(TS_CONFIGS)
cd $(APP_DIR) && npm run build -w @gsd/lambda-interactions \
-w @gsd/lambda-followup \
-w @gsd/lambda-update-dns \
-w @gsd/lambda-watchdog
cd $(APP_DIR) && npm run build -w @hyveon/lambda-interactions \
-w @hyveon/lambda-followup \
-w @hyveon/lambda-update-dns \
-w @hyveon/lambda-watchdog
touch $@

# ── Composite build targets ───────────────────────────────────────────────────
Expand Down Expand Up @@ -117,7 +117,7 @@ tf-destroy: tf-init
# ── Clean ──────────────────────────────────────────────────────────────────────
clean:
rm -rf $(APP_DIR)/packages/shared/dist \
$(APP_DIR)/packages/server/dist \
$(APP_DIR)/packages/desktop-main/dist \
$(APP_DIR)/packages/web/dist
find $(APP_DIR)/packages/lambda -type d -name dist -exec rm -rf {} +
rm -rf $(STAMP)
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ Component deep-dives:

- [**Architecture**](https://codercoco.github.io/Hyveon/architecture/) — full diagram + `/server-start` sequence.
- [**Terraform**](https://codercoco.github.io/Hyveon/components/terraform/) — every `.tf` file, variables, outputs, gotchas.
- [**Management app**](https://codercoco.github.io/Hyveon/components/management-app/) — Nest.js API, React dashboard, `@gsd/shared`.
- [**Management app**](https://codercoco.github.io/Hyveon/components/management-app/) — Nest.js API, React dashboard, `@hyveon/shared`.
- [**Lambdas**](https://codercoco.github.io/Hyveon/components/lambdas/) — interactions, followup, update-dns, watchdog.

## Quick start
Expand Down Expand Up @@ -117,9 +117,9 @@ pennies/month. Playing 4 hours/day, 5 days/week ≈ **$10–12/month**, vs.
Hyveon/
├── app/ # Nest.js + React monorepo (npm workspaces)
│ └── packages/
│ ├── shared/ # @gsd/shared
│ ├── server/ # @gsd/server (Nest.js API)
│ ├── web/ # @gsd/web (React + Vite)
│ ├── shared/ # @hyveon/shared
│ ├── desktop-main/ # @hyveon/desktop-main (Nest.js API)
│ ├── web/ # @hyveon/web (React + Vite)
│ └── lambda/
│ ├── interactions/ # Discord Function URL entry point
│ ├── followup/ # Async ECS work + Discord PATCH
Expand Down
8 changes: 4 additions & 4 deletions app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
"type": "module",
"scripts": {
"predev": "node scripts/embed-tfstate.mjs",
"dev": "concurrently -n server,client -c cyan,magenta \"npm run dev -w @gsd/server\" \"npm run dev -w @gsd/web\"",
"dev": "concurrently -n server,client -c cyan,magenta \"npm run dev -w @hyveon/desktop-main\" \"npm run dev -w @hyveon/web\"",
"prebuild": "node scripts/embed-tfstate.mjs",
"build": "npm run build -w @gsd/shared && npm run build -w @gsd/server && npm run build -w @gsd/web",
"build:lambdas": "npm run build -w @gsd/shared && npm run build -w @gsd/lambda-interactions -w @gsd/lambda-followup -w @gsd/lambda-update-dns -w @gsd/lambda-watchdog -w @gsd/lambda-efs-seeder",
"start": "node packages/server/dist/main.js",
"build": "npm run build -w @hyveon/shared && npm run build -w @hyveon/desktop-main && npm run build -w @hyveon/web",
"build:lambdas": "npm run build -w @hyveon/shared && npm run build -w @hyveon/lambda-interactions -w @hyveon/lambda-followup -w @hyveon/lambda-update-dns -w @hyveon/lambda-watchdog -w @hyveon/lambda-efs-seeder",
"start": "node packages/desktop-main/dist/main.js",
"test": "vitest run",
"test:coverage": "vitest run --coverage",
"test:watch": "vitest",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "@gsd/server",
"name": "@hyveon/desktop-main",
"version": "1.0.0",
"private": true,
"type": "module",
Expand All @@ -17,7 +17,7 @@
"@aws-sdk/client-ecs": "^3.600.0",
"@aws-sdk/client-secrets-manager": "^3.600.0",
"@aws-sdk/lib-dynamodb": "^3.600.0",
"@gsd/shared": "*",
"@hyveon/shared": "*",
"@nestjs/common": "^10.4.0",
"@nestjs/core": "^10.4.0",
"@nestjs/platform-express": "^10.4.0",
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
import { Injectable } from '@nestjs/common';
import { logger } from '../logger.js';
import { DiscordConfigService } from './DiscordConfigService.js';
import { COMMAND_DESCRIPTORS } from '@gsd/shared';
import { COMMAND_DESCRIPTORS } from '@hyveon/shared';

const DISCORD_API = 'https://discord.com/api/v10';

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
/**
* Tests for the DynamoDB + Secrets Manager-backed DiscordConfigService.
*
* The service is a thin wrapper around `@gsd/shared/ddb/configStore` and
* `@gsd/shared/secrets/secretsStore` — the stores themselves have their own
* The service is a thin wrapper around `@hyveon/shared/ddb/configStore` and
* `@hyveon/shared/secrets/secretsStore` — the stores themselves have their own
* tests under the shared package. Here we validate the wiring: that the
* right stores get called with the right args, that the redacted view
* strips both secrets, and that the controller-facing contract (same method
Expand All @@ -21,8 +21,8 @@ const putBotTokenMock = vi.fn();
const putPublicKeyMock = vi.fn();
const invalidateSecretsCacheMock = vi.fn();

vi.mock('@gsd/shared', async () => {
const actual = await vi.importActual<typeof import('@gsd/shared')>('@gsd/shared');
vi.mock('@hyveon/shared', async () => {
const actual = await vi.importActual<typeof import('@hyveon/shared')>('@hyveon/shared');
return {
...actual,
getDiscordConfig: (...args: unknown[]) => getDiscordConfigMock(...args),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
* `/api/discord/config`.
*
* The InteractionsLambda has its own copy of the read paths (via
* `@gsd/shared`), so this service only exists to back the management UI's
* `@hyveon/shared`), so this service only exists to back the management UI's
* configuration tab.
*/
import { Injectable } from '@nestjs/common';
Expand All @@ -30,10 +30,10 @@ import {
type DiscordAction,
type DiscordConfig,
type RedactedDiscordConfig,
} from '@gsd/shared';
} from '@hyveon/shared';

/** Slash-command action that can be gated via permissions. */
export type { DiscordAction } from '@gsd/shared';
export type { DiscordAction } from '@hyveon/shared';

function emptyConfig(): DiscordConfig {
return {
Expand All @@ -47,7 +47,7 @@ function emptyConfig(): DiscordConfig {
/**
* Management-side interface to the Discord DynamoDB row and the two Secrets
* Manager secrets. The interactions/followup Lambdas have their own read
* paths via `@gsd/shared`; this service backs the web UI's Credentials /
* paths via `@hyveon/shared`; this service backs the web UI's Credentials /
* Permissions tabs.
*
* Security invariant: the raw `botToken` and `publicKey` values are **never**
Expand Down
2 changes: 1 addition & 1 deletion app/packages/lambda/efs-seeder/package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "@gsd/lambda-efs-seeder",
"name": "@hyveon/lambda-efs-seeder",
"version": "1.0.0",
"private": true,
"type": "module",
Expand Down
4 changes: 2 additions & 2 deletions app/packages/lambda/followup/package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "@gsd/lambda-followup",
"name": "@hyveon/lambda-followup",
"version": "1.0.0",
"private": true,
"type": "module",
Expand All @@ -9,7 +9,7 @@
"clean": "rm -rf dist"
},
"dependencies": {
"@gsd/shared": "*"
"@hyveon/shared": "*"
},
"devDependencies": {
"@aws-sdk/client-ec2": "^3.600.0",
Expand Down
4 changes: 2 additions & 2 deletions app/packages/lambda/followup/src/handler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ import {

const getEffectiveDiscordConfigMock = vi.fn();
const putPendingMock = vi.fn();
vi.mock('@gsd/shared', async () => {
const actual = await vi.importActual<typeof import('@gsd/shared')>('@gsd/shared');
vi.mock('@hyveon/shared', async () => {
const actual = await vi.importActual<typeof import('@hyveon/shared')>('@hyveon/shared');
return {
...actual,
getEffectiveDiscordConfig: (...args: unknown[]) => getEffectiveDiscordConfigMock(...args),
Expand Down
6 changes: 3 additions & 3 deletions app/packages/lambda/followup/src/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ import {
EC2Client,
DescribeNetworkInterfacesCommand,
} from '@aws-sdk/client-ec2';
import { canRun, formatGameStatus, getEffectiveDiscordConfig, putPending } from '@gsd/shared';
import type { DiscordAction, DiscordConfig, GameStatus } from '@gsd/shared';
import { canRun, formatGameStatus, getEffectiveDiscordConfig, putPending } from '@hyveon/shared';
import type { DiscordAction, DiscordConfig, GameStatus } from '@hyveon/shared';

interface FollowupEvent {
kind: 'start' | 'stop' | 'status' | 'list';
Expand Down Expand Up @@ -265,7 +265,7 @@ function recheck(event: FollowupEvent, cfg: DiscordConfig, action: DiscordAction
* (Discord's 3-second budget doesn't leave room for ECS calls). Does the slow work —
* `RunTask` / `StopTask` / `DescribeTasks` — then PATCHes the original interaction
* message via the webhook endpoint. For `start`, also writes a `PENDING#{taskArn}`
* row to DynamoDB so `@gsd/lambda-update-dns` can PATCH the same interaction once
* row to DynamoDB so `@hyveon/lambda-update-dns` can PATCH the same interaction once
* the task reaches RUNNING and an IP/hostname is resolved.
*/
export const handler = async (event: FollowupEvent): Promise<void> => {
Expand Down
4 changes: 2 additions & 2 deletions app/packages/lambda/interactions/package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "@gsd/lambda-interactions",
"name": "@hyveon/lambda-interactions",
"version": "1.0.0",
"private": true,
"type": "module",
Expand All @@ -9,7 +9,7 @@
"clean": "rm -rf dist"
},
"dependencies": {
"@gsd/shared": "*",
"@hyveon/shared": "*",
"@noble/ed25519": "^2.1.0"
},
"devDependencies": {
Expand Down
4 changes: 2 additions & 2 deletions app/packages/lambda/interactions/src/handler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ vi.mock('@noble/ed25519', () => ({
// Mock the shared config + secrets stores so we never hit AWS.
const getPublicKeyMock = vi.fn();
const getEffectiveDiscordConfigMock = vi.fn();
vi.mock('@gsd/shared', async () => {
const actual = await vi.importActual<typeof import('@gsd/shared')>('@gsd/shared');
vi.mock('@hyveon/shared', async () => {
const actual = await vi.importActual<typeof import('@hyveon/shared')>('@hyveon/shared');
return {
...actual,
getPublicKey: (...args: unknown[]) => getPublicKeyMock(...args),
Expand Down
4 changes: 2 additions & 2 deletions app/packages/lambda/interactions/src/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@
import { verifyAsync } from '@noble/ed25519';
import { LambdaClient, InvokeCommand } from '@aws-sdk/client-lambda';
import type { APIGatewayProxyEventV2, APIGatewayProxyResultV2 } from 'aws-lambda';
import { canRun, getEffectiveDiscordConfig, getPublicKey } from '@gsd/shared';
import type { DiscordAction, DiscordConfig } from '@gsd/shared';
import { canRun, getEffectiveDiscordConfig, getPublicKey } from '@hyveon/shared';
import type { DiscordAction, DiscordConfig } from '@hyveon/shared';

/** Discord interaction types we care about. Full list in discord-api-types. */
const INTERACTION_PING = 1;
Expand Down
4 changes: 2 additions & 2 deletions app/packages/lambda/update-dns/package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "@gsd/lambda-update-dns",
"name": "@hyveon/lambda-update-dns",
"version": "1.0.0",
"private": true,
"type": "module",
Expand All @@ -9,7 +9,7 @@
"clean": "rm -rf dist"
},
"dependencies": {
"@gsd/shared": "*"
"@hyveon/shared": "*"
},
"devDependencies": {
"@aws-sdk/client-ec2": "^3.600.0",
Expand Down
4 changes: 2 additions & 2 deletions app/packages/lambda/update-dns/src/handler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ import {

const getPendingMock = vi.fn();
const deletePendingMock = vi.fn();
vi.mock('@gsd/shared', async () => {
const actual = await vi.importActual<typeof import('@gsd/shared')>('@gsd/shared');
vi.mock('@hyveon/shared', async () => {
const actual = await vi.importActual<typeof import('@hyveon/shared')>('@hyveon/shared');
return {
...actual,
getPending: (...args: unknown[]) => getPendingMock(...args),
Expand Down
2 changes: 1 addition & 1 deletion app/packages/lambda/update-dns/src/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import {
ChangeResourceRecordSetsCommand,
ListResourceRecordSetsCommand,
} from '@aws-sdk/client-route-53';
import { deletePending, formatGameStatus, getPending } from '@gsd/shared';
import { deletePending, formatGameStatus, getPending } from '@hyveon/shared';

const HOSTED_ZONE_ID = requireEnv('HOSTED_ZONE_ID');
const DOMAIN_NAME = requireEnv('DOMAIN_NAME');
Expand Down
2 changes: 1 addition & 1 deletion app/packages/lambda/watchdog/package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "@gsd/lambda-watchdog",
"name": "@hyveon/lambda-watchdog",
"version": "1.0.0",
"private": true,
"type": "module",
Expand Down
2 changes: 1 addition & 1 deletion app/packages/lambda/watchdog/src/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ async function listAllRunningTaskArns(): Promise<string[]> {
* `NetworkPacketsIn` on its ENI via CloudWatch and bumps a consecutive-idle counter
* stored as an ECS task tag (no DynamoDB/SSM state). After `watchdog_idle_checks`
* consecutive idle intervals, the task is stopped — which triggers the DNS-delete
* path in `@gsd/lambda-update-dns`.
* path in `@hyveon/lambda-update-dns`.
*/
export const handler = async (): Promise<{ checked: number }> => {
console.log(`Watchdog running — cluster: ${ECS_CLUSTER}, games: ${GAME_NAMES.join(',')}`);
Expand Down
2 changes: 1 addition & 1 deletion app/packages/shared/package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "@gsd/shared",
"name": "@hyveon/shared",
"version": "1.0.0",
"private": true,
"type": "module",
Expand Down
2 changes: 1 addition & 1 deletion app/packages/web/package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "@gsd/web",
"name": "@hyveon/web",
"version": "1.0.0",
"private": true,
"type": "module",
Expand Down
2 changes: 1 addition & 1 deletion app/packages/web/playwright.integration.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { join } from 'node:path';

const fixtureDir = fileURLToPath(new URL('e2e/fixtures', import.meta.url));
const tfstatePath = join(fixtureDir, 'tfstate.fixture.json');
const serverDist = fileURLToPath(new URL('../../packages/server/dist', import.meta.url));
const serverDist = fileURLToPath(new URL('../../packages/desktop-main/dist', import.meta.url));

export default defineConfig({
testDir: './e2e/integration-specs',
Expand Down
4 changes: 2 additions & 2 deletions app/scripts/embed-tfstate.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/usr/bin/env node
// Reads Terraform state at build time and writes
// app/packages/server/src/generated/tfstate.ts with the parsed state
// app/packages/desktop-main/src/generated/tfstate.ts with the parsed state
// embedded as a JSON literal. ConfigService imports it as a fallback when the
// state file is not present at runtime (e.g. dev without a terraform apply).
//
Expand Down Expand Up @@ -35,7 +35,7 @@ function getRepoRoot() {
}
}

const outDir = join(__dirname, '../packages/server/src/generated');
const outDir = join(__dirname, '../packages/desktop-main/src/generated');
const outPath = join(outDir, 'tfstate.ts');

mkdirSync(outDir, { recursive: true });
Expand Down
2 changes: 1 addition & 1 deletion app/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"files": [],
"references": [
{ "path": "./packages/shared" },
{ "path": "./packages/server" },
{ "path": "./packages/desktop-main" },
{ "path": "./packages/web" },
{ "path": "./packages/lambda/interactions" },
{ "path": "./packages/lambda/followup" },
Expand Down
Loading
Loading