From 0f895a8cf93e508b7815421d4c47366353c60d61 Mon Sep 17 00:00:00 2001 From: Dotta Date: Fri, 6 Mar 2026 10:10:23 -0600 Subject: [PATCH] Enforce 10-minute TTL for generated company invites --- packages/shared/src/validators/access.ts | 1 - scripts/smoke/openclaw-join.sh | 2 +- server/src/__tests__/invite-expiry.test.ts | 10 ++++++++++ server/src/routes/access.ts | 7 ++++++- ui/src/api/access.ts | 1 - ui/src/pages/CompanySettings.tsx | 5 ++--- 6 files changed, 19 insertions(+), 7 deletions(-) create mode 100644 server/src/__tests__/invite-expiry.test.ts diff --git a/packages/shared/src/validators/access.ts b/packages/shared/src/validators/access.ts index 6a72149c..614b302e 100644 --- a/packages/shared/src/validators/access.ts +++ b/packages/shared/src/validators/access.ts @@ -9,7 +9,6 @@ import { export const createCompanyInviteSchema = z.object({ allowedJoinTypes: z.enum(INVITE_JOIN_TYPES).default("both"), - expiresInHours: z.number().int().min(1).max(24 * 30).optional().default(72), defaultsPayload: z.record(z.string(), z.unknown()).optional().nullable(), agentMessage: z.string().max(4000).optional().nullable(), }); diff --git a/scripts/smoke/openclaw-join.sh b/scripts/smoke/openclaw-join.sh index d5da01c2..151ae277 100755 --- a/scripts/smoke/openclaw-join.sh +++ b/scripts/smoke/openclaw-join.sh @@ -160,7 +160,7 @@ if [[ -z "$COMPANY_ID" ]]; then fi log "creating agent-only invite for company ${COMPANY_ID}" -INVITE_PAYLOAD="$(jq -nc '{allowedJoinTypes:"agent",expiresInHours:24}')" +INVITE_PAYLOAD="$(jq -nc '{allowedJoinTypes:"agent"}')" api_request "POST" "/companies/${COMPANY_ID}/invites" "$INVITE_PAYLOAD" if [[ "$RESPONSE_CODE" == "401" || "$RESPONSE_CODE" == "403" ]]; then fail_board_auth_required "Invite creation" diff --git a/server/src/__tests__/invite-expiry.test.ts b/server/src/__tests__/invite-expiry.test.ts new file mode 100644 index 00000000..c84a2a95 --- /dev/null +++ b/server/src/__tests__/invite-expiry.test.ts @@ -0,0 +1,10 @@ +import { describe, expect, it } from "vitest"; +import { companyInviteExpiresAt } from "../routes/access.js"; + +describe("companyInviteExpiresAt", () => { + it("sets invite expiration to 10 minutes after invite creation time", () => { + const createdAtMs = Date.parse("2026-03-06T00:00:00.000Z"); + const expiresAt = companyInviteExpiresAt(createdAtMs); + expect(expiresAt.toISOString()).toBe("2026-03-06T00:10:00.000Z"); + }); +}); diff --git a/server/src/routes/access.ts b/server/src/routes/access.ts index 71e34d2b..1d16bb96 100644 --- a/server/src/routes/access.ts +++ b/server/src/routes/access.ts @@ -37,6 +37,7 @@ const INVITE_TOKEN_PREFIX = "pcp_invite_"; const INVITE_TOKEN_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789"; const INVITE_TOKEN_SUFFIX_LENGTH = 8; const INVITE_TOKEN_MAX_RETRIES = 5; +const COMPANY_INVITE_TTL_MS = 10 * 60 * 1000; function createInviteToken() { const bytes = randomBytes(INVITE_TOKEN_SUFFIX_LENGTH); @@ -51,6 +52,10 @@ function createClaimSecret() { return `pcp_claim_${randomBytes(24).toString("hex")}`; } +export function companyInviteExpiresAt(nowMs: number = Date.now()) { + return new Date(nowMs + COMPANY_INVITE_TTL_MS); +} + function tokenHashesMatch(left: string, right: string) { const leftBytes = Buffer.from(left, "utf8"); const rightBytes = Buffer.from(right, "utf8"); @@ -1102,7 +1107,7 @@ export function accessRoutes( inviteType: "company_join" as const, allowedJoinTypes: req.body.allowedJoinTypes, defaultsPayload: mergeInviteDefaults(req.body.defaultsPayload ?? null, normalizedAgentMessage), - expiresAt: new Date(Date.now() + req.body.expiresInHours * 60 * 60 * 1000), + expiresAt: companyInviteExpiresAt(), invitedByUserId: req.actor.userId ?? null, }; diff --git a/ui/src/api/access.ts b/ui/src/api/access.ts index d6b3f03f..7e89afd6 100644 --- a/ui/src/api/access.ts +++ b/ui/src/api/access.ts @@ -69,7 +69,6 @@ export const accessApi = { companyId: string, input: { allowedJoinTypes?: "human" | "agent" | "both"; - expiresInHours?: number; defaultsPayload?: Record | null; agentMessage?: string | null; } = {}, diff --git a/ui/src/pages/CompanySettings.tsx b/ui/src/pages/CompanySettings.tsx index 1ec2d7ed..de3e2798 100644 --- a/ui/src/pages/CompanySettings.tsx +++ b/ui/src/pages/CompanySettings.tsx @@ -78,8 +78,7 @@ export function CompanySettings() { const inviteMutation = useMutation({ mutationFn: () => accessApi.createCompanyInvite(selectedCompanyId!, { - allowedJoinTypes: "agent", - expiresInHours: 72 + allowedJoinTypes: "agent" }), onSuccess: async (invite) => { setInviteError(null); @@ -320,7 +319,7 @@ export function CompanySettings() { Generate an agent snippet for join flows. - +