create-safest-tools 0.2.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,6 +5,9 @@ import type { GenericOAuthConfig } from "better-auth/plugins/generic-oauth";
5
5
 
6
6
  const INVITATION_COOKIE = "safest_invitation";
7
7
  const INVITATION_MAX_AGE_SECONDS = 7 * 24 * 60 * 60;
8
+ const OWNER_SETUP_COOKIE = "safest_owner_setup";
9
+ const OWNER_SETUP_RATE_LIMIT = 12;
10
+ const OWNER_SETUP_WINDOW_SECONDS = 10 * 60;
8
11
 
9
12
  type AuthEmailBinding = {
10
13
  send(message: {
@@ -52,6 +55,20 @@ type InvitationRow = {
52
55
  expires_at: string;
53
56
  };
54
57
 
58
+ export type OwnerSetupContext = {
59
+ id: string;
60
+ email: string;
61
+ purpose: "bootstrap" | "recovery";
62
+ expiresAt: string;
63
+ };
64
+
65
+ type OwnerSetupRow = {
66
+ id: string;
67
+ email: string;
68
+ purpose: "bootstrap" | "recovery";
69
+ expires_at: string;
70
+ };
71
+
55
72
  const encoder = new TextEncoder();
56
73
 
57
74
  function bytesToBase64Url(bytes: Uint8Array): string {
@@ -116,6 +133,46 @@ async function invitationCookieValue(id: string, env: BetterAuthEnv): Promise<st
116
133
  return `${id}.${await hmac(`${INVITATION_COOKIE}:${id}`, authSecret(env))}`;
117
134
  }
118
135
 
136
+ async function ownerSetupCookieValue(id: string, env: BetterAuthEnv): Promise<string> {
137
+ return `${id}.${await hmac(`${OWNER_SETUP_COOKIE}:${id}`, authSecret(env))}`;
138
+ }
139
+
140
+ function signedCookieId(value: string | undefined): { id: string; signature: string } | null {
141
+ if (!value) return null;
142
+ const separator = value.lastIndexOf(".");
143
+ if (separator <= 0) return null;
144
+ const id = value.slice(0, separator);
145
+ const signature = value.slice(separator + 1);
146
+ return /^[0-9a-f-]{36}$/iu.test(id) ? { id, signature } : null;
147
+ }
148
+
149
+ export async function ownerSetupContextFromRequest(
150
+ request: Request | undefined,
151
+ env: BetterAuthEnv,
152
+ ): Promise<OwnerSetupContext | null> {
153
+ const signed = signedCookieId(parseCookies(request?.headers.get("cookie") ?? null).get(OWNER_SETUP_COOKIE));
154
+ if (!signed) return null;
155
+ const expected = await hmac(`${OWNER_SETUP_COOKIE}:${signed.id}`, authSecret(env));
156
+ if (!constantTimeEqual(signed.signature, expected)) return null;
157
+ const row = await env.DB.prepare(`
158
+ SELECT id, email, purpose, expires_at
159
+ FROM owner_setup_tokens
160
+ WHERE id = ?1 AND status = 'pending' AND expires_at > ?2
161
+ LIMIT 1
162
+ `).bind(signed.id, new Date().toISOString()).first<OwnerSetupRow>();
163
+ return row ? { id: row.id, email: row.email, purpose: row.purpose, expiresAt: row.expires_at } : null;
164
+ }
165
+
166
+ async function ownerSetupAllowsEmail(
167
+ request: Request | undefined,
168
+ env: BetterAuthEnv,
169
+ emailValue: unknown,
170
+ ): Promise<boolean> {
171
+ const email = typeof emailValue === "string" ? emailValue.trim().toLowerCase() : "";
172
+ const setup = await ownerSetupContextFromRequest(request, env);
173
+ return setup?.purpose === "bootstrap" && setup.email.trim().toLowerCase() === email;
174
+ }
175
+
119
176
  export async function invitationIdFromRequest(
120
177
  request: Request | undefined,
121
178
  env: BetterAuthEnv,
@@ -179,6 +236,7 @@ async function identityMayCreateUser(
179
236
  "SELECT id FROM operator_users WHERE email = ?1 AND status = 'active' LIMIT 1",
180
237
  ).bind(email).first();
181
238
  if (existing) return;
239
+ if (await ownerSetupAllowsEmail(request, env, email)) return;
182
240
  const invitationId = await invitationIdFromRequest(request, env);
183
241
  const invitation = invitationId ? await findPendingInvitationById(env, invitationId) : null;
184
242
  if (!invitation) {
@@ -202,6 +260,7 @@ async function identityMaySignIn(
202
260
  "SELECT status FROM operator_users WHERE email = ?1 LIMIT 1",
203
261
  ).bind(email).first<{ status: string }>() : null;
204
262
  if (operator?.status === "active") return;
263
+ if (request && await ownerSetupAllowsEmail(request, env, email)) return;
205
264
  if (request && await invitationAllowsEmail(request, env, email)) return;
206
265
  return { error: "account_unavailable", errorDescription: "This Safest Resolve account is unavailable." };
207
266
  }
@@ -393,7 +452,8 @@ function createBetterAuth(env: BetterAuthEnv, ctx?: ExecutionContext) {
393
452
  sendOnSignIn: !development,
394
453
  autoSignInAfterVerification: true,
395
454
  expiresIn: 24 * 60 * 60,
396
- async sendVerificationEmail({ user, url }) {
455
+ async sendVerificationEmail({ user, url }, request) {
456
+ if (await ownerSetupAllowsEmail(request, env, user.email)) return;
397
457
  await deliverAuthEmail(env, ctx, {
398
458
  to: user.email,
399
459
  subject: "Verify your Safest Resolve email",
@@ -543,6 +603,10 @@ export function appendBetterAuthCsrfCookie(
543
603
  "set-cookie",
544
604
  `${INVITATION_COOKIE}=; Path=/; Max-Age=0; HttpOnly; SameSite=Lax${secure ? "; Secure" : ""}`,
545
605
  );
606
+ headers.append(
607
+ "set-cookie",
608
+ `${OWNER_SETUP_COOKIE}=; Path=/; Max-Age=0; HttpOnly; SameSite=Lax${secure ? "; Secure" : ""}`,
609
+ );
546
610
  return new Response(response.body, {
547
611
  status: response.status,
548
612
  statusText: response.statusText,
@@ -550,6 +614,90 @@ export function appendBetterAuthCsrfCookie(
550
614
  });
551
615
  }
552
616
 
617
+ export function clearOwnerSetupCookie(response: Response, request: Request): Response {
618
+ const secure = new URL(request.url).protocol === "https:";
619
+ const headers = new Headers(response.headers);
620
+ headers.append(
621
+ "set-cookie",
622
+ `${OWNER_SETUP_COOKIE}=; Path=/; Max-Age=0; HttpOnly; SameSite=Lax${secure ? "; Secure" : ""}`,
623
+ );
624
+ return new Response(response.body, { status: response.status, statusText: response.statusText, headers });
625
+ }
626
+
627
+ async function enforceOwnerSetupRateLimit(request: Request, env: BetterAuthEnv): Promise<boolean> {
628
+ const pepper = env.RATE_LIMIT_PEPPER;
629
+ if (typeof pepper !== "string" || new TextEncoder().encode(pepper).byteLength < 32) {
630
+ throw new Error("RATE_LIMIT_PEPPER must be configured for owner setup.");
631
+ }
632
+ const now = new Date();
633
+ const windowStart = new Date(Math.floor(now.valueOf() / (OWNER_SETUP_WINDOW_SECONDS * 1_000))
634
+ * OWNER_SETUP_WINDOW_SECONDS * 1_000).toISOString();
635
+ const network = (request.headers.get("cf-connecting-ip") ?? "unknown").slice(0, 200);
636
+ const bucketKey = await sha256(`${pepper}:owner-setup:${windowStart}:${network}`);
637
+ const row = await env.DB.prepare(`
638
+ INSERT INTO owner_setup_rate_limits (bucket_key, window_start, request_count, updated_at)
639
+ VALUES (?1, ?2, 1, ?3)
640
+ ON CONFLICT(bucket_key) DO UPDATE SET
641
+ request_count = owner_setup_rate_limits.request_count + 1,
642
+ updated_at = excluded.updated_at
643
+ WHERE owner_setup_rate_limits.request_count < ?4
644
+ RETURNING request_count
645
+ `).bind(bucketKey, windowStart, now.toISOString(), OWNER_SETUP_RATE_LIMIT).first<{ request_count: number }>();
646
+ await env.DB.prepare(`DELETE FROM owner_setup_rate_limits WHERE updated_at < ?1`)
647
+ .bind(new Date(now.valueOf() - 60 * 60_000).toISOString()).run();
648
+ return Boolean(row);
649
+ }
650
+
651
+ function ownerSetupError(status = 400): Response {
652
+ return Response.json({
653
+ error: { code: "invalid_owner_setup", message: "This owner setup link is invalid or expired." },
654
+ }, { status, headers: { "cache-control": "no-store", "referrer-policy": "no-referrer" } });
655
+ }
656
+
657
+ export async function inspectOwnerSetup(request: Request, env: BetterAuthEnv): Promise<Response> {
658
+ if (request.method !== "POST") return new Response("Method Not Allowed", { status: 405, headers: { allow: "POST" } });
659
+ if (!await enforceOwnerSetupRateLimit(request, env)) {
660
+ return Response.json({
661
+ error: { code: "owner_setup_rate_limited", message: "Too many setup attempts. Try again in ten minutes." },
662
+ }, { status: 429, headers: { "cache-control": "no-store", "retry-after": String(OWNER_SETUP_WINDOW_SECONDS) } });
663
+ }
664
+ const body = await request.json().catch(() => null) as { token?: unknown } | null;
665
+ const token = typeof body?.token === "string" ? body.token : "";
666
+ if (!/^[A-Za-z0-9_-]{43,200}$/u.test(token)) return ownerSetupError();
667
+ const now = new Date().toISOString();
668
+ const setup = await env.DB.prepare(`
669
+ SELECT id, email, purpose, expires_at
670
+ FROM owner_setup_tokens
671
+ WHERE token_hash = ?1 AND status = 'pending' AND expires_at > ?2
672
+ LIMIT 1
673
+ `).bind(await sha256(token), now).first<OwnerSetupRow>();
674
+ if (!setup) return ownerSetupError();
675
+ const eligible = setup.purpose === "bootstrap"
676
+ ? !await env.DB.prepare(`
677
+ SELECT id FROM operator_users
678
+ WHERE status = 'active' AND roles_json LIKE '%"owner"%'
679
+ LIMIT 1
680
+ `).first()
681
+ : Boolean(await env.DB.prepare(`
682
+ SELECT id FROM operator_users
683
+ WHERE email = ?1 AND status = 'active' AND roles_json LIKE '%"owner"%'
684
+ LIMIT 1
685
+ `).bind(setup.email).first());
686
+ if (!eligible) return ownerSetupError();
687
+ const secure = new URL(request.url).protocol === "https:";
688
+ const maxAge = Math.max(0, Math.floor((Date.parse(setup.expires_at) - Date.now()) / 1_000));
689
+ const headers = new Headers({ "cache-control": "no-store", "referrer-policy": "no-referrer" });
690
+ headers.append(
691
+ "set-cookie",
692
+ `${OWNER_SETUP_COOKIE}=${await ownerSetupCookieValue(setup.id, env)}; Path=/; Max-Age=${maxAge}; ` +
693
+ `HttpOnly; SameSite=Lax${secure ? "; Secure" : ""}`,
694
+ );
695
+ return Response.json({
696
+ schema_version: "1",
697
+ setup: { email: setup.email, purpose: setup.purpose, expires_at: setup.expires_at },
698
+ }, { headers });
699
+ }
700
+
553
701
  export async function inspectInvitation(
554
702
  request: Request,
555
703
  env: BetterAuthEnv,
@@ -202,7 +202,7 @@ function operatorAvatarUrl(user: OperatorUserRow): string | undefined {
202
202
  function operatorSession(
203
203
  user: OperatorUserRow,
204
204
  authorization?: { roles: OperatorRole[]; queueScope: string[] | null; fieldScope: Record<string, string[]> | null },
205
- authMethod: OperatorSession["authMethod"] = "cloudflare_access",
205
+ authMethod: OperatorSession["authMethod"] = "owner_setup",
206
206
  ): OperatorSession {
207
207
  const roles = authorization?.roles ?? parseRoles(user.roles_json);
208
208
  const avatarUrl = operatorAvatarUrl(user);
@@ -275,10 +275,19 @@ export async function resolveBetterAuthOperator(
275
275
  db: D1Database,
276
276
  identity: { email: string; name: string; image?: string | null },
277
277
  invitationId: string | null,
278
+ ownerSetupId: string | null = null,
278
279
  ): Promise<OperatorSession | null> {
279
280
  const email = cleanEmail(identity.email);
280
281
  let user = await db.prepare(`SELECT * FROM operator_users WHERE email = ?1 LIMIT 1`)
281
282
  .bind(email).first<OperatorUserRow>();
283
+ if (ownerSetupId) {
284
+ const owner = await claimInitialOwner(db, ownerSetupId, {
285
+ email,
286
+ displayName: identity.name,
287
+ avatarUrl: identity.image,
288
+ });
289
+ if (owner) return owner;
290
+ }
282
291
  if (user) {
283
292
  if (user.status !== "active") return null;
284
293
  return operatorSession(user, await operatorAuthorization(db, user), "better_auth");
@@ -385,18 +394,6 @@ export function clearOperatorSessionCookies(response: Response, request: Request
385
394
  return new Response(response.body, { status: response.status, statusText: response.statusText, headers });
386
395
  }
387
396
 
388
- async function issueSession(db: D1Database, user: OperatorUserRow): Promise<IssuedOperatorSession> {
389
- const createdAt = new Date().toISOString();
390
- const expiresAt = new Date(Date.parse(createdAt) + SESSION_HOURS * 3_600_000).toISOString();
391
- const sessionToken = randomToken();
392
- const csrfToken = randomToken();
393
- await db.prepare(`
394
- INSERT INTO infrastructure_owner_sessions (id, user_id, token_hash, csrf_hash, expires_at, last_seen_at, created_at)
395
- VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?6)
396
- `).bind(crypto.randomUUID(), user.id, await sha256(sessionToken), await sha256(csrfToken), expiresAt, createdAt).run();
397
- return { session: operatorSession(user), sessionToken, csrfToken, expiresAt };
398
- }
399
-
400
397
  export async function resolveOperatorAccountSession(
401
398
  request: Request,
402
399
  db: D1Database,
@@ -427,37 +424,127 @@ export async function verifyOperatorCsrf(request: Request, csrfHash: string): Pr
427
424
  )) throw new ApiError(403, "csrf_validation_failed", "Refresh the page and try again.");
428
425
  }
429
426
 
430
- export async function bootstrapOwner(
427
+ export async function claimInitialOwner(
431
428
  db: D1Database,
432
- identity: { email: string; displayName: string; avatarUrl?: string },
433
- ): Promise<IssuedOperatorSession> {
429
+ setupId: string,
430
+ identity: { email: string; displayName: string; avatarUrl?: string | null },
431
+ ): Promise<OperatorSession | null> {
434
432
  const email = cleanEmail(identity.email);
435
433
  const displayName = cleanDisplayName(identity.displayName, email.split("@", 1)[0]);
436
434
  const avatarUrl = cleanAvatarUrl(identity.avatarUrl);
437
435
  const timestamp = new Date().toISOString();
438
- let user = await db.prepare(`SELECT * FROM operator_users WHERE email = ?1`).bind(email).first<OperatorUserRow>();
439
- if (user) {
440
- await db.prepare(`
441
- UPDATE operator_users SET display_name = ?2, avatar_url = ?3, roles_json = '["owner"]', status = 'active', updated_at = ?4
442
- WHERE id = ?1
443
- `).bind(user.id, displayName, avatarUrl, timestamp).run();
444
- user = { ...user, display_name: displayName, avatar_url: avatarUrl, roles_json: '["owner"]', status: "active", updated_at: timestamp };
445
- } else {
446
- user = {
447
- id: crypto.randomUUID(), email, display_name: displayName, avatar_url: avatarUrl,
448
- avatar_object_key: null, avatar_uploaded_at: null,
449
- roles_json: '["owner"]', status: "active", accepted_at: timestamp,
450
- last_login_at: timestamp, created_at: timestamp, updated_at: timestamp,
451
- };
452
- await db.prepare(`
453
- INSERT INTO operator_users (
454
- id, email, display_name, avatar_url, roles_json, status, accepted_at, last_login_at, created_at, updated_at
455
- ) VALUES (?1, ?2, ?3, ?4, ?5, 'active', ?6, ?6, ?6, ?6)
456
- `).bind(user.id, user.email, user.display_name, user.avatar_url, user.roles_json, timestamp).run();
457
- }
458
- await db.prepare(`UPDATE operator_users SET last_login_at = ?2 WHERE id = ?1`).bind(user.id, timestamp).run();
459
- await auditEvent(db, "operator.owner_bootstrap", user.id, "cloudflare_access", user.id, { email }, timestamp).run();
460
- return issueSession(db, user);
436
+ const existing = await db.prepare(`SELECT * FROM operator_users WHERE email = ?1 LIMIT 1`)
437
+ .bind(email).first<OperatorUserRow>();
438
+ const userId = existing?.id ?? crypto.randomUUID();
439
+ const tokenPredicate = `
440
+ EXISTS (
441
+ SELECT 1 FROM owner_setup_tokens
442
+ WHERE id = ?1 AND email = ?2 AND purpose = 'bootstrap'
443
+ AND status = 'pending' AND expires_at > ?3
444
+ )
445
+ AND NOT EXISTS (
446
+ SELECT 1 FROM operator_users
447
+ WHERE status = 'active' AND roles_json LIKE '%"owner"%'
448
+ )
449
+ `;
450
+ const userStatement = existing
451
+ ? db.prepare(`
452
+ UPDATE operator_users
453
+ SET display_name = ?4, avatar_url = ?5, roles_json = '["owner"]', status = 'active',
454
+ accepted_at = COALESCE(accepted_at, ?3), last_login_at = ?3, updated_at = ?3
455
+ WHERE id = ?6 AND ${tokenPredicate}
456
+ `).bind(setupId, email, timestamp, displayName, avatarUrl, userId)
457
+ : db.prepare(`
458
+ INSERT INTO operator_users (
459
+ id, email, display_name, avatar_url, roles_json, status,
460
+ accepted_at, last_login_at, created_at, updated_at
461
+ )
462
+ SELECT ?6, ?2, ?4, ?5, '["owner"]', 'active', ?3, ?3, ?3, ?3
463
+ WHERE ${tokenPredicate}
464
+ `).bind(setupId, email, timestamp, displayName, avatarUrl, userId);
465
+ const auditId = crypto.randomUUID();
466
+ const results = await db.batch([
467
+ userStatement,
468
+ db.prepare(`
469
+ INSERT INTO audit_events (
470
+ id, idempotency_key, action, actor_type, actor_id,
471
+ target_type, target_id, details_json, created_at
472
+ )
473
+ SELECT ?4, ?4, 'operator.owner_bootstrap', 'system', ?1,
474
+ 'operator_user', ?5, ?6, ?3
475
+ FROM owner_setup_tokens
476
+ WHERE id = ?1 AND email = ?2 AND purpose = 'bootstrap'
477
+ AND status = 'pending' AND expires_at > ?3
478
+ `).bind(setupId, email, timestamp, auditId, userId, canonicalJson({ email, method: "one_time_setup" })),
479
+ db.prepare(`
480
+ UPDATE owner_setup_tokens
481
+ SET status = 'consumed', consumed_at = ?3
482
+ WHERE id = ?1 AND email = ?2 AND purpose = 'bootstrap'
483
+ AND status = 'pending' AND expires_at > ?3
484
+ `).bind(setupId, email, timestamp),
485
+ ]);
486
+ if (!results[0]?.meta.changes || !results[2]?.meta.changes) return null;
487
+ const user = await db.prepare(`SELECT * FROM operator_users WHERE id = ?1 LIMIT 1`)
488
+ .bind(userId).first<OperatorUserRow>();
489
+ return user ? operatorSession(user, undefined, "better_auth") : null;
490
+ }
491
+
492
+ export async function recoverOwnerWithSetup(
493
+ db: D1Database,
494
+ setupId: string,
495
+ emailValue: string,
496
+ ): Promise<IssuedOperatorSession | null> {
497
+ const email = cleanEmail(emailValue);
498
+ const timestamp = new Date().toISOString();
499
+ const user = await db.prepare(`
500
+ SELECT * FROM operator_users
501
+ WHERE email = ?1 AND status = 'active' AND roles_json LIKE '%"owner"%'
502
+ LIMIT 1
503
+ `).bind(email).first<OperatorUserRow>();
504
+ if (!user) return null;
505
+ const expiresAt = new Date(Date.parse(timestamp) + SESSION_HOURS * 3_600_000).toISOString();
506
+ const sessionToken = randomToken();
507
+ const csrfToken = randomToken();
508
+ const sessionId = crypto.randomUUID();
509
+ const auditId = crypto.randomUUID();
510
+ const results = await db.batch([
511
+ db.prepare(`
512
+ INSERT INTO infrastructure_owner_sessions (
513
+ id, user_id, token_hash, csrf_hash, expires_at, last_seen_at, created_at
514
+ )
515
+ SELECT ?4, ?5, ?6, ?7, ?8, ?3, ?3
516
+ FROM owner_setup_tokens
517
+ WHERE id = ?1 AND email = ?2 AND purpose = 'recovery'
518
+ AND status = 'pending' AND expires_at > ?3
519
+ `).bind(
520
+ setupId, email, timestamp, sessionId, user.id,
521
+ await sha256(sessionToken), await sha256(csrfToken), expiresAt,
522
+ ),
523
+ db.prepare(`
524
+ INSERT INTO audit_events (
525
+ id, idempotency_key, action, actor_type, actor_id,
526
+ target_type, target_id, details_json, created_at
527
+ )
528
+ SELECT ?4, ?4, 'operator.owner_recovered', 'system', ?1,
529
+ 'operator_user', ?5, ?6, ?3
530
+ FROM owner_setup_tokens
531
+ WHERE id = ?1 AND email = ?2 AND purpose = 'recovery'
532
+ AND status = 'pending' AND expires_at > ?3
533
+ `).bind(setupId, email, timestamp, auditId, user.id, canonicalJson({ email, method: "one_time_setup" })),
534
+ db.prepare(`
535
+ UPDATE owner_setup_tokens
536
+ SET status = 'consumed', consumed_at = ?3
537
+ WHERE id = ?1 AND email = ?2 AND purpose = 'recovery'
538
+ AND status = 'pending' AND expires_at > ?3
539
+ `).bind(setupId, email, timestamp),
540
+ ]);
541
+ if (!results[0]?.meta.changes || !results[2]?.meta.changes) return null;
542
+ return {
543
+ session: operatorSession(user, await operatorAuthorization(db, user), "owner_setup"),
544
+ sessionToken,
545
+ csrfToken,
546
+ expiresAt,
547
+ };
461
548
  }
462
549
 
463
550
  function auditEvent(
@@ -65,7 +65,7 @@ export interface OperatorPermissions {
65
65
 
66
66
  export interface OperatorSession {
67
67
  actor: { type: "cloudflare_access" | "admin_key" | "operator_user"; id: string };
68
- authMethod: "cloudflare_access" | "admin_key" | "better_auth";
68
+ authMethod: "cloudflare_access" | "owner_setup" | "admin_key" | "better_auth";
69
69
  verified: boolean;
70
70
  displayName: string;
71
71
  email?: string;
@@ -1,5 +1,5 @@
1
1
  /* eslint-disable */
2
- // Generated by Wrangler by running `wrangler types --env-file=config/dev.vars.example --include-runtime=false --strict-vars=false` (hash: 4f5ea689965d3fb5059f8a820ffef604)
2
+ // Generated by Wrangler by running `wrangler types --env-file=config/dev.vars.example --include-runtime=false --strict-vars=false` (hash: 2cca359aa7fa3dcf738a197c47478066)
3
3
  interface __BaseEnv_Env {
4
4
  PROFILE_MEDIA: R2Bucket;
5
5
  WORKFLOW_ARTIFACTS: R2Bucket;
@@ -34,8 +34,6 @@ interface __BaseEnv_Env {
34
34
  CREDENTIAL_ENCRYPTION_KEY: string;
35
35
  RATE_LIMIT_PEPPER: string;
36
36
  INTEGRATION_API_KEYS_JSON: string;
37
- ACCESS_AUD: string;
38
- INFRASTRUCTURE_OWNER_EMAILS: string;
39
37
  TURNSTILE_SITE_KEY: string;
40
38
  TURNSTILE_SECRET_KEY: string;
41
39
  ACTION_WEBHOOK_URL: string;
@@ -61,5 +59,5 @@ type StringifyValues<EnvType extends Record<string, unknown>> = {
61
59
  [Binding in keyof EnvType]: EnvType[Binding] extends string ? EnvType[Binding] : string;
62
60
  };
63
61
  declare namespace NodeJS {
64
- interface ProcessEnv extends StringifyValues<Pick<Cloudflare.Env, "PUBLIC_BASE_URL" | "ALLOWED_ORIGINS" | "CONTEXT_TOKEN_TTL_SECONDS" | "CLAIM_TTL_MINUTES" | "REPORT_RETENTION_DAYS" | "MESSAGE_RETENTION_DAYS" | "APPEAL_WINDOW_DAYS" | "ALLOWED_ACTION_CODES" | "OPERATIONS_DLQ_NAME" | "PUBLIC_INTEGRATION_ID" | "PUBLIC_REPORT_LIMIT_PER_10_MINUTES" | "PUBLIC_REPORT_SURGE_PER_5_MINUTES" | "OPERATOR_NOTIFICATION_WEBHOOK_URL" | "EMAIL_FROM_ADDRESS" | "EMAIL_SUBJECT_PREFIX" | "ADMIN_API_KEY" | "BETTER_AUTH_SECRET" | "AUTH_EMAIL_FROM" | "CONTEXT_SIGNING_SECRET" | "CREDENTIAL_ENCRYPTION_KEY" | "RATE_LIMIT_PEPPER" | "INTEGRATION_API_KEYS_JSON" | "ACCESS_AUD" | "INFRASTRUCTURE_OWNER_EMAILS" | "TURNSTILE_SITE_KEY" | "TURNSTILE_SECRET_KEY" | "ACTION_WEBHOOK_URL" | "ACTION_WEBHOOK_SECRET" | "NOTIFICATION_WEBHOOK_URL" | "NOTIFICATION_WEBHOOK_SECRET" | "OPERATOR_NOTIFICATION_WEBHOOK_SECRET" | "EXAMPLE_ENRICHMENT_SECRET" | "AI_LOCAL_FIXTURE_ENABLED" | "AI_LOCAL_FIXTURE_JSON">> {}
62
+ interface ProcessEnv extends StringifyValues<Pick<Cloudflare.Env, "PUBLIC_BASE_URL" | "ALLOWED_ORIGINS" | "CONTEXT_TOKEN_TTL_SECONDS" | "CLAIM_TTL_MINUTES" | "REPORT_RETENTION_DAYS" | "MESSAGE_RETENTION_DAYS" | "APPEAL_WINDOW_DAYS" | "ALLOWED_ACTION_CODES" | "OPERATIONS_DLQ_NAME" | "PUBLIC_INTEGRATION_ID" | "PUBLIC_REPORT_LIMIT_PER_10_MINUTES" | "PUBLIC_REPORT_SURGE_PER_5_MINUTES" | "OPERATOR_NOTIFICATION_WEBHOOK_URL" | "EMAIL_FROM_ADDRESS" | "EMAIL_SUBJECT_PREFIX" | "ADMIN_API_KEY" | "BETTER_AUTH_SECRET" | "AUTH_EMAIL_FROM" | "CONTEXT_SIGNING_SECRET" | "CREDENTIAL_ENCRYPTION_KEY" | "RATE_LIMIT_PEPPER" | "INTEGRATION_API_KEYS_JSON" | "TURNSTILE_SITE_KEY" | "TURNSTILE_SECRET_KEY" | "ACTION_WEBHOOK_URL" | "ACTION_WEBHOOK_SECRET" | "NOTIFICATION_WEBHOOK_URL" | "NOTIFICATION_WEBHOOK_SECRET" | "OPERATOR_NOTIFICATION_WEBHOOK_SECRET" | "EXAMPLE_ENRICHMENT_SECRET" | "AI_LOCAL_FIXTURE_ENABLED" | "AI_LOCAL_FIXTURE_JSON">> {}
65
63
  }