Skip to content

Commit 2c9b5ac

Browse files
CoderCococlaude
andauthored
fix(discord): split Function URL permissions and trim credentials (#41)
## Summary - **`terraform/interactions.tf`**: The single `aws_lambda_permission` with `action = lambda:InvokeFunction` + `function_url_auth_type = NONE` was rejected by AWS with `InvalidParameterValueException` — AWS only accepts `function_url_auth_type` paired with `lambda:InvokeFunctionUrl`. Split into two statements: one for `lambda:InvokeFunctionUrl` (with `function_url_auth_type = NONE`) and one for `lambda:InvokeFunction` (without it). Both permissions are required since October 2025 for Discord's endpoint validation to pass. - **`app/packages/shared/src/secrets/secretsStore.ts`**: Added `.trim()` to all four credential accessors (`getBotToken`, `getPublicKey`, `putBotToken`, `putPublicKey`) to strip surrounding whitespace from pasted Discord credentials — a common copy-paste artifact that causes 401s from the Discord API. - **`.gitignore`**: Added `.claude/worktrees/` and `app/packages/server/src/generated/tfstate.ts` (auto-generated; should never be committed with real state). ## Test plan - [ ] `terraform plan` shows two `aws_lambda_permission` resources, no destructive changes - [ ] `terraform apply` completes without `InvalidParameterValueException` - [ ] Discord endpoint validation succeeds after apply (re-enter URL in Developer Portal if needed) - [ ] Pasting a bot token or public key with leading/trailing whitespace via the UI saves and reads back trimmed 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent ff71f60 commit 2c9b5ac

4 files changed

Lines changed: 34 additions & 19 deletions

File tree

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,5 +27,10 @@ Thumbs.db
2727
# lives at .claude/settings.json (commit that instead if you want the
2828
# allowlist shared across the team).
2929
.claude/settings.local.json
30+
.claude/worktrees/
31+
32+
# Auto-generated at dev/build time — committed as null, real state must not
33+
# be committed (contains AWS ARNs/account IDs).
34+
app/packages/server/src/generated/tfstate.ts
3035

3136
.make

app/packages/server/src/generated/tfstate.ts

Lines changed: 0 additions & 2 deletions
This file was deleted.

app/packages/shared/src/secrets/secretsStore.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,26 +53,28 @@ async function putSecret(secretId: string, value: string): Promise<void> {
5353

5454
/** Return the configured bot token, or `null` if not set / still on the placeholder. */
5555
export async function getBotToken(secretArn: string): Promise<string | null> {
56-
const value = await getSecret(secretArn);
56+
const raw = await getSecret(secretArn);
57+
const value = raw?.trim() ?? null;
5758
if (!value || value === SECRET_PLACEHOLDER) return null;
5859
return value;
5960
}
6061

6162
/** Return the configured Ed25519 public key (hex), or `null` if not set / still on the placeholder. */
6263
export async function getPublicKey(secretArn: string): Promise<string | null> {
63-
const value = await getSecret(secretArn);
64+
const raw = await getSecret(secretArn);
65+
const value = raw?.trim() ?? null;
6466
if (!value || value === SECRET_PLACEHOLDER) return null;
6567
return value;
6668
}
6769

68-
/** Persist a new bot token. */
70+
/** Persist a new bot token, trimmed of surrounding whitespace. */
6971
export async function putBotToken(secretArn: string, value: string): Promise<void> {
70-
await putSecret(secretArn, value);
72+
await putSecret(secretArn, value.trim());
7173
}
7274

73-
/** Persist a new public key (hex). */
75+
/** Persist a new public key (hex), trimmed of surrounding whitespace. */
7476
export async function putPublicKey(secretArn: string, value: string): Promise<void> {
75-
await putSecret(secretArn, value);
77+
await putSecret(secretArn, value.trim());
7678
}
7779

7880
/** Drop the in-process secrets cache. Exposed for the Nest app's "save credentials" path. */

terraform/interactions.tf

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -74,12 +74,12 @@ resource "aws_lambda_function" "interactions" {
7474

7575
environment {
7676
variables = {
77-
AWS_REGION_ = var.aws_region
78-
TABLE_NAME = aws_dynamodb_table.discord.name
79-
DISCORD_PUBLIC_KEY_SECRET_ARN = aws_secretsmanager_secret.discord_public_key.arn
80-
FOLLOWUP_LAMBDA_NAME = aws_lambda_function.followup.function_name
81-
GAME_NAMES = join(",", keys(var.game_servers))
82-
HOSTED_ZONE_NAME = var.hosted_zone_name
77+
AWS_REGION_ = var.aws_region
78+
TABLE_NAME = aws_dynamodb_table.discord.name
79+
DISCORD_PUBLIC_KEY_SECRET_ARN = aws_secretsmanager_secret.discord_public_key.arn
80+
FOLLOWUP_LAMBDA_NAME = aws_lambda_function.followup.function_name
81+
GAME_NAMES = join(",", keys(var.game_servers))
82+
HOSTED_ZONE_NAME = var.hosted_zone_name
8383
}
8484
}
8585

@@ -105,16 +105,26 @@ resource "aws_lambda_function_url" "interactions" {
105105
}
106106
}
107107

108-
# Since October 2025, Lambda Function URLs require both lambda:InvokeFunctionUrl
109-
# (created automatically by aws_lambda_function_url) and lambda:InvokeFunction.
110-
resource "aws_lambda_permission" "interactions_url_invoke" {
111-
statement_id = "FunctionURLInvokeAllowPublicAccess"
112-
action = "lambda:InvokeFunction"
108+
# Since October 2025, Lambda Function URLs require BOTH lambda:InvokeFunctionUrl
109+
# AND lambda:InvokeFunction in the resource policy — without the second one,
110+
# Discord's endpoint validation gets 403 before the handler runs. AWS only
111+
# accepts function_url_auth_type paired with lambda:InvokeFunctionUrl, so the
112+
# two grants are split into two statements.
113+
resource "aws_lambda_permission" "interactions_url_invoke_url" {
114+
statement_id = "FunctionURLInvokeUrlAllowPublicAccess"
115+
action = "lambda:InvokeFunctionUrl"
113116
function_name = aws_lambda_function.interactions.function_name
114117
principal = "*"
115118
function_url_auth_type = "NONE"
116119
}
117120

121+
resource "aws_lambda_permission" "interactions_url_invoke" {
122+
statement_id = "FunctionURLInvokeAllowPublicAccess"
123+
action = "lambda:InvokeFunction"
124+
function_name = aws_lambda_function.interactions.function_name
125+
principal = "*"
126+
}
127+
118128
output "interactions_invoke_url" {
119129
description = "Paste this into the 'Interactions Endpoint URL' field in the Discord Developer Portal"
120130
value = "https://discord.${var.hosted_zone_name}/"

0 commit comments

Comments
 (0)