Skip to content

Commit b03f960

Browse files
authored
[wrangler] Add event codes to temporary deployments (#15776)
1 parent a1f05a3 commit b03f960

15 files changed

Lines changed: 651 additions & 18 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"wrangler": minor
3+
---
4+
5+
Add event-code support to temporary Worker deployments
6+
7+
Use `wrangler deploy --temporary --event-code <code>` to provision an account for an event. Wrangler requires explicit server acknowledgement before caching the account and keeps the event code out of its cache and telemetry.

‎packages/workers-auth/src/context.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ export interface OAuthFlowTemporaryContext {
2121
prompt: (question: string, notice: string) => Promise<boolean>;
2222
}
2323

24+
/** Optional provisioning inputs for a newly-created temporary account. */
25+
export interface TemporaryAccountRequest {
26+
eventCode?: string;
27+
}
28+
2429
/**
2530
* The branded OAuth consent pages the provider redirects the browser to after
2631
* the user grants or denies consent.

‎packages/workers-auth/src/core/factory.ts‎

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import { createPreferences } from "./preferences";
3737
import { createTemporaryTermsPrompt } from "./temporary-terms";
3838
import type { UserAuthConfig } from "../config-file/auth";
3939
import type { TemporaryPreviewAccount } from "../config-file/temporary";
40+
import type { TemporaryAccountRequest } from "../context";
4041
import type { CredentialStore } from "../credential-store";
4142
import type {
4243
LoginOrRefreshFailureReason,
@@ -81,7 +82,10 @@ export interface CloudflareAuth {
8182
/** The currently-active credential store for the active profile. */
8283
getCredentialStore: () => CredentialStore;
8384
/** Mark whether `--temporary` is permitted for the current invocation. */
84-
setTemporaryAllowed: (allowed: boolean) => void;
85+
setTemporaryAllowed: (
86+
allowed: boolean,
87+
request?: TemporaryAccountRequest
88+
) => void;
8589

8690
/** Resolve API credentials (env / temporary account / stored OAuth token). */
8791
getAPIToken: () => ApiCredentials | undefined;
@@ -272,8 +276,11 @@ export function createCloudflareAuth(
272276
return credentialStorage.getActiveStore(oauthFlow.getActiveProfile());
273277
}
274278

275-
function setTemporaryAllowed(allowed: boolean): void {
276-
oauthFlow.setTemporaryAllowed(allowed);
279+
function setTemporaryAllowed(
280+
allowed: boolean,
281+
request?: TemporaryAccountRequest
282+
): void {
283+
oauthFlow.setTemporaryAllowed(allowed, request);
277284
}
278285

279286
function getAPIToken(): ApiCredentials | undefined {

‎packages/workers-auth/src/flow.ts‎

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { getOrCreateTemporaryPreviewAccount } from "./temporary";
1515
import { exchangeRefreshTokenForAccessToken } from "./token-exchange";
1616
import type { AuthConfigStorage } from "./config-file/auth";
1717
import type { TemporaryPreviewAccount } from "./config-file/temporary";
18-
import type { OAuthFlowContext } from "./context";
18+
import type { OAuthFlowContext, TemporaryAccountRequest } from "./context";
1919
import type {
2020
ApiCredentials,
2121
ComplianceConfig,
@@ -177,7 +177,10 @@ export interface OAuthFlowAPI {
177177
* process (e.g. in tests) — each invocation starts a fresh temporary session.
178178
* No-op when the flow was created without a `temporary` context.
179179
*/
180-
setTemporaryAllowed(allowed: boolean): void;
180+
setTemporaryAllowed(
181+
allowed: boolean,
182+
request?: TemporaryAccountRequest
183+
): void;
181184

182185
/**
183186
* Whether `--temporary` is permitted for this invocation (see
@@ -226,7 +229,7 @@ export function createOAuthFlow(ctx: OAuthFlowContext): OAuthFlowAPI {
226229
typeof ctx.clientId === "function" ? ctx.clientId() : ctx.clientId;
227230
const consent = ctx.consent;
228231

229-
let temporaryAllowed = false;
232+
let temporaryRequest: TemporaryAccountRequest | undefined;
230233
let activeTemporaryAccount: TemporaryPreviewAccount | undefined;
231234

232235
const redirectUrl = new URL(ctx.redirectUri);
@@ -509,13 +512,17 @@ export function createOAuthFlow(ctx: OAuthFlowContext): OAuthFlowAPI {
509512
});
510513
}
511514

512-
function setTemporaryAllowed(allowed: boolean): void {
513-
temporaryAllowed = allowed && ctx.temporary !== undefined;
515+
function setTemporaryAllowed(
516+
allowed: boolean,
517+
request?: TemporaryAccountRequest
518+
): void {
519+
temporaryRequest =
520+
allowed && ctx.temporary !== undefined ? (request ?? {}) : undefined;
514521
activeTemporaryAccount = undefined;
515522
}
516523

517524
function isTemporaryAllowed(): boolean {
518-
return temporaryAllowed;
525+
return temporaryRequest !== undefined;
519526
}
520527

521528
function getActiveTemporaryAccount(): TemporaryPreviewAccount | undefined {
@@ -536,6 +543,7 @@ export function createOAuthFlow(ctx: OAuthFlowContext): OAuthFlowAPI {
536543
const result = await getOrCreateTemporaryPreviewAccount({
537544
...ctx.temporary,
538545
logger: ctx.logger,
546+
...(temporaryRequest ? { request: temporaryRequest } : {}),
539547
});
540548
activeTemporaryAccount = result.account;
541549
return result;

‎packages/workers-auth/src/index.ts‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,11 @@ export {
2222
getAccessHeaders,
2323
} from "./access";
2424

25-
export type { OAuthFlowContext, OAuthFlowLogger } from "./context";
25+
export type {
26+
OAuthFlowContext,
27+
OAuthFlowLogger,
28+
TemporaryAccountRequest,
29+
} from "./context";
2630

2731
export {
2832
getAuthUrlFromEnv,

‎packages/workers-auth/src/temporary.ts‎

Lines changed: 134 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import type {
1010
TemporaryAccountStorage,
1111
TemporaryPreviewAccount,
1212
} from "./config-file/temporary";
13-
import type { OAuthFlowLogger } from "./context";
13+
import type { OAuthFlowLogger, TemporaryAccountRequest } from "./context";
1414
import type { PowSolution } from "./pow";
1515

1616
export const TEMPORARY_TERMS_URLS = {
@@ -37,12 +37,115 @@ type TemporaryAccountPayload = {
3737
url?: string;
3838
expiresAt?: string;
3939
};
40+
eventCodeAccepted?: boolean;
4041
};
4142

4243
type TemporaryAccountResponse = {
4344
result?: TemporaryAccountPayload;
4445
};
4546

47+
const EVENT_ACCOUNT_ERRORS = new Map<
48+
number,
49+
{ message: string; telemetryMessage: string; fatal?: true }
50+
>([
51+
[
52+
1035,
53+
{
54+
message: "The event code is malformed. Check it and try again.",
55+
telemetryMessage: "deploy temporary event code invalid",
56+
},
57+
],
58+
[
59+
1036,
60+
{
61+
message: "The event code was not found. Check it and try again.",
62+
telemetryMessage: "deploy temporary event not found",
63+
},
64+
],
65+
[
66+
1037,
67+
{
68+
message: "This event has not started yet.",
69+
telemetryMessage: "deploy temporary event not started",
70+
},
71+
],
72+
[
73+
1038,
74+
{
75+
message: "This event has ended.",
76+
telemetryMessage: "deploy temporary event expired",
77+
},
78+
],
79+
[
80+
1039,
81+
{
82+
message: "This event is disabled. Contact the event organizer.",
83+
telemetryMessage: "deploy temporary event disabled",
84+
},
85+
],
86+
[
87+
1040,
88+
{
89+
message: "This event has no temporary accounts remaining.",
90+
telemetryMessage: "deploy temporary event exhausted",
91+
},
92+
],
93+
[
94+
1041,
95+
{
96+
message:
97+
"The event account could not be prepared. Try again later or contact the event organizer.",
98+
telemetryMessage: "deploy temporary event provisioning unavailable",
99+
fatal: true,
100+
},
101+
],
102+
]);
103+
104+
function getResponseErrorCode(body: unknown): number | undefined {
105+
if (typeof body !== "object" || body === null || !("errors" in body)) {
106+
return undefined;
107+
}
108+
109+
const { errors } = body;
110+
if (!Array.isArray(errors) || errors.length === 0) {
111+
return undefined;
112+
}
113+
114+
const [error] = errors;
115+
if (typeof error !== "object" || error === null || !("code" in error)) {
116+
return undefined;
117+
}
118+
119+
return typeof error.code === "number" ? error.code : undefined;
120+
}
121+
122+
async function getEventAccountError(
123+
response: Response
124+
): Promise<UserError | FatalError | undefined> {
125+
let body: unknown;
126+
try {
127+
body = await response.json();
128+
} catch {
129+
return undefined;
130+
}
131+
132+
const errorCode = getResponseErrorCode(body);
133+
if (errorCode === undefined) {
134+
return undefined;
135+
}
136+
137+
const eventError = EVENT_ACCOUNT_ERRORS.get(errorCode);
138+
if (!eventError) {
139+
return undefined;
140+
}
141+
142+
const options = { telemetryMessage: eventError.telemetryMessage };
143+
if (eventError.fatal) {
144+
return new FatalError(eventError.message, options);
145+
}
146+
return new UserError(eventError.message, options);
147+
}
148+
46149
function getTemporaryPreviewUrl(): string {
47150
return `${getCloudflareApiBaseUrl(COMPLIANCE_REGION_CONFIG_PUBLIC)}/provisioning/previews`;
48151
}
@@ -166,7 +269,8 @@ async function requestPowSolution(
166269
* endpoint
167270
*/
168271
export async function createTemporaryPreviewAccount(
169-
logger: OAuthFlowLogger
272+
logger: OAuthFlowLogger,
273+
request?: TemporaryAccountRequest
170274
): Promise<TemporaryPreviewAccount> {
171275
const pow = await requestPowSolution(logger);
172276

@@ -179,10 +283,17 @@ export async function createTemporaryPreviewAccount(
179283
acceptTermsOfService: "yes",
180284
challengeToken: pow.challengeToken,
181285
solution: pow.solution,
286+
...(request?.eventCode ? { eventCode: request.eventCode } : {}),
182287
}),
183288
});
184289

185290
if (!response.ok) {
291+
if (request?.eventCode) {
292+
const eventError = await getEventAccountError(response);
293+
if (eventError) {
294+
throw eventError;
295+
}
296+
}
186297
throw new FatalError(
187298
`Failed to create a temporary preview account (${response.status} ${response.statusText}).`,
188299
{ telemetryMessage: "deploy temporary account create failed" }
@@ -207,6 +318,7 @@ export async function createTemporaryPreviewAccount(
207318
const accountExpiresAt = previewAccount?.account?.expiresAt;
208319
const claimUrl = previewAccount?.claim?.url;
209320
const claimExpiresAt = previewAccount?.claim?.expiresAt;
321+
const eventCodeAccepted = previewAccount?.eventCodeAccepted;
210322

211323
if (
212324
accountId === undefined ||
@@ -222,6 +334,15 @@ export async function createTemporaryPreviewAccount(
222334
);
223335
}
224336

337+
if (request?.eventCode && eventCodeAccepted !== true) {
338+
throw new FatalError(
339+
"Failed to create a temporary preview account because the response did not acknowledge the event code.",
340+
{
341+
telemetryMessage: "deploy temporary event acknowledgement missing",
342+
}
343+
);
344+
}
345+
225346
return {
226347
account: {
227348
id: accountId,
@@ -245,6 +366,7 @@ export async function getOrCreateTemporaryPreviewAccount(options: {
245366
storage: TemporaryAccountStorage;
246367
prompt: (question: string, notice: string) => Promise<boolean>;
247368
logger: OAuthFlowLogger;
369+
request?: TemporaryAccountRequest;
248370
}): Promise<{
249371
account: TemporaryPreviewAccount;
250372
cached: boolean;
@@ -253,6 +375,14 @@ export async function getOrCreateTemporaryPreviewAccount(options: {
253375
options.storage
254376
);
255377
if (cachedPreviewAccount) {
378+
if (options.request?.eventCode) {
379+
throw new UserError(
380+
"A temporary account is already cached. Rerun without --event-code to reuse it, or run `wrangler logout` and retry to create an account for this event.",
381+
{
382+
telemetryMessage: "deploy temporary event cache conflict",
383+
}
384+
);
385+
}
256386
return { account: cachedPreviewAccount, cached: true };
257387
}
258388

@@ -268,7 +398,8 @@ export async function getOrCreateTemporaryPreviewAccount(options: {
268398
}
269399

270400
const temporaryPreviewAccount = await createTemporaryPreviewAccount(
271-
options.logger
401+
options.logger,
402+
options.request
272403
);
273404
options.storage.write(temporaryPreviewAccount);
274405

0 commit comments

Comments
 (0)