@void/platform 0.23.0 → 0.24.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.
@@ -556,6 +556,30 @@ var init_plans = __esm({
556
556
  });
557
557
 
558
558
  // ../../home/runner/work/void/void/packages/platform/dist/index.mjs
559
+ function parsePlatformAppAccessSetup(value) {
560
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Invalid app access setup.");
561
+ const input2 = value;
562
+ if (input2.version !== 1 || !["public", "protected"].includes(String(input2.mode)) || typeof input2.managed !== "boolean" || typeof input2.accountId !== "string" || !input2.accountId) throw new Error("Invalid app access setup.");
563
+ const ids2 = /* @__PURE__ */ __name((name) => {
564
+ const entries2 = input2[name];
565
+ if (!Array.isArray(entries2) || entries2.length > 16 || entries2.some((id2) => typeof id2 !== "string" || !id2.trim() || id2.length > 128) || new Set(entries2).size !== entries2.length) throw new Error(`Invalid app access ${name}.`);
566
+ return [...entries2];
567
+ }, "ids");
568
+ const identityProviderIds = ids2("identityProviderIds");
569
+ const policyIds = ids2("policyIds");
570
+ if (input2.mode === "protected" && (!input2.managed || !identityProviderIds.length || !policyIds.length)) throw new Error("Protected apps require identity providers, policies, and an Access-management token.");
571
+ if (input2.mode === "public" && (identityProviderIds.length || policyIds.length)) throw new Error("Public app access cannot include identity policies.");
572
+ if (input2.managed ? typeof input2.apiToken !== "string" || !input2.apiToken.trim() || /\s/.test(input2.apiToken) || input2.apiToken.length > 8192 : input2.apiToken !== void 0) throw new Error("Managed app access requires a token; unmanaged public access must not retain one.");
573
+ return {
574
+ version: 1,
575
+ accountId: input2.accountId,
576
+ mode: input2.mode,
577
+ managed: input2.managed,
578
+ ...input2.managed ? { apiToken: input2.apiToken } : {},
579
+ identityProviderIds,
580
+ policyIds
581
+ };
582
+ }
559
583
  function platformAuthenticationRecoveryMessage(value) {
560
584
  return JSON.stringify([
561
585
  "void-platform-auth-recovery-v1",
@@ -854,6 +878,11 @@ function createPlatformDeploymentManifest(input2) {
854
878
  ...input2.capabilities
855
879
  };
856
880
  if (capabilities.managedBuilds && !capabilities.githubActions) throw new Error("Managed builds require the GitHub Actions capability.");
881
+ if (input2.appAccess !== void 0 && ![
882
+ "public",
883
+ "platform",
884
+ "custom"
885
+ ].includes(input2.appAccess)) throw new Error("appAccess must be public, platform, or custom.");
857
886
  const providers2 = input2.authentication?.providers ?? ["github"];
858
887
  if (providers2.some((provider) => !PLATFORM_LOGIN_KINDS.includes(provider)) || new Set(providers2).size !== providers2.length) throw new Error("Authentication providers must be unique supported login kinds.");
859
888
  const configurable = input2.authentication?.configurable === true || input2.authentication?.accessProtection === true || providers2.some((provider) => provider === "oidc" || provider === "cloudflare-access");
@@ -862,6 +891,7 @@ function createPlatformDeploymentManifest(input2) {
862
891
  if (providers2.includes("google") !== capabilities.googleOAuth) throw new Error("The Google provider and googleOAuth capability must agree.");
863
892
  const dashboardUrl = input2.controlPlane?.dashboardUrl === void 0 ? void 0 : normalizePlatformDashboardUrl(input2.controlPlane.dashboardUrl);
864
893
  const manifest = {
894
+ ...input2.appAccess !== void 0 ? { appAccess: input2.appAccess } : {},
865
895
  application: {
866
896
  baseDomain,
867
897
  dedicatedZone: input2.application.dedicatedZone ?? false,
@@ -888,7 +918,7 @@ function createPlatformDeploymentManifest(input2) {
888
918
  displayName: requiredString$1(input2.displayName, "displayName").trim(),
889
919
  installationSlug,
890
920
  resources: resources2,
891
- schemaVersion: configurable ? 3 : workersDev ? 2 : 1
921
+ schemaVersion: input2.appAccess !== void 0 ? 4 : configurable ? 3 : workersDev ? 2 : 1
892
922
  };
893
923
  createApplicationRoutes(manifest.application);
894
924
  for (const [key, value] of Object.entries(manifest.controlPlane)) {
@@ -1008,12 +1038,14 @@ function parseAuthentication(value) {
1008
1038
  ...authentication.setupPending === true ? { setupPending: true } : {}
1009
1039
  };
1010
1040
  }
1011
- var PLATFORM_LOGIN_KINDS, GOOGLE_OIDC_ISSUER, PLATFORM_AUTH_SETUP_PROOF_PREFIX, CORE_CAPABILITIES, CONTROL_PLANE_DEFAULTS, PLATFORM_DISABLED_KEY, PLATFORM_DISABLED_VALUE, PLATFORM_USAGE_PROTOCOL_VERSION, PLATFORM_PROJECT_INGRESS_SOURCE, PLATFORM_APPLICATION_KEY, PLATFORM_HEALTH_HOSTNAME_LABEL;
1041
+ var PLATFORM_APP_ACCESS_SETUP_PROOF_PREFIX, PLATFORM_LOGIN_KINDS, GOOGLE_OIDC_ISSUER, PLATFORM_AUTH_SETUP_PROOF_PREFIX, CORE_CAPABILITIES, CONTROL_PLANE_DEFAULTS, PLATFORM_DISABLED_KEY, PLATFORM_DISABLED_VALUE, PLATFORM_USAGE_PROTOCOL_VERSION, PLATFORM_PROJECT_INGRESS_SOURCE, PLATFORM_APPLICATION_KEY, PLATFORM_HEALTH_HOSTNAME_LABEL;
1012
1042
  var init_dist = __esm({
1013
1043
  "../../home/runner/work/void/void/packages/platform/dist/index.mjs"() {
1014
1044
  "use strict";
1015
1045
  init_performance2();
1016
1046
  init_plans();
1047
+ PLATFORM_APP_ACCESS_SETUP_PROOF_PREFIX = "void-platform-app-access-v1";
1048
+ __name(parsePlatformAppAccessSetup, "parsePlatformAppAccessSetup");
1017
1049
  __name(platformAuthenticationRecoveryMessage, "platformAuthenticationRecoveryMessage");
1018
1050
  PLATFORM_LOGIN_KINDS = [
1019
1051
  "github",
@@ -30326,24 +30358,46 @@ __export(cloudflare_api_exports, {
30326
30358
  updateHyperdrive: () => updateHyperdrive
30327
30359
  });
30328
30360
  async function cfFetch(url2, apiToken, init) {
30329
- const response = await fetch(url2, {
30330
- ...init,
30331
- headers: {
30332
- Authorization: `Bearer ${apiToken}`,
30333
- "Content-Type": "application/json",
30334
- ...init?.headers
30335
- }
30336
- });
30337
- const json3 = await response.json();
30338
- if (!json3.success) {
30339
- const message2 = json3.errors.map((error63) => error63.message).join(", ");
30340
- throw new CloudflareApiError(
30341
- `Cloudflare API error: ${message2}`,
30342
- response.status,
30343
- json3.errors.map((error63) => error63.code)
30361
+ const deadline = new AbortController();
30362
+ const method = init?.method ?? "GET";
30363
+ const pathname = new URL(url2).pathname;
30364
+ const timer = setTimeout(() => {
30365
+ deadline.abort(
30366
+ new Error(
30367
+ `Cloudflare API request timed out after 60 seconds: ${method} ${pathname}.` + (method === "GET" || method === "HEAD" ? "" : " The write may have succeeded; inspect its state before retrying.")
30368
+ )
30344
30369
  );
30370
+ }, 6e4);
30371
+ const signal2 = init?.signal ? AbortSignal.any([init.signal, deadline.signal]) : deadline.signal;
30372
+ try {
30373
+ signal2.throwIfAborted();
30374
+ const response = await fetch(url2, {
30375
+ ...init,
30376
+ signal: signal2,
30377
+ headers: {
30378
+ Authorization: `Bearer ${apiToken}`,
30379
+ "Content-Type": "application/json",
30380
+ ...init?.headers
30381
+ }
30382
+ });
30383
+ const json3 = await response.json();
30384
+ if (!json3.success) {
30385
+ const message2 = json3.errors.map((error63) => error63.message).join(", ");
30386
+ throw new CloudflareApiError(
30387
+ `Cloudflare API error: ${message2}`,
30388
+ response.status,
30389
+ json3.errors.map((error63) => error63.code)
30390
+ );
30391
+ }
30392
+ return json3.result;
30393
+ } catch (error63) {
30394
+ if (deadline.signal.aborted && !init?.signal?.aborted) {
30395
+ throw deadline.signal.reason;
30396
+ }
30397
+ throw error63;
30398
+ } finally {
30399
+ clearTimeout(timer);
30345
30400
  }
30346
- return json3.result;
30347
30401
  }
30348
30402
  async function createD1Database(accountId, apiToken, name) {
30349
30403
  return cfFetch(`${CF_API_BASE}/${accountId}/d1/database`, apiToken, {
@@ -51729,10 +51783,10 @@ function cloudDomainFromHost(host) {
51729
51783
  function systemHealthCheckNames(installed) {
51730
51784
  return ["api", "proxy", ...installed ? [] : ["dashboard"], "dispatch", "db"];
51731
51785
  }
51732
- async function checkUrl(name, url2, headers) {
51786
+ async function checkUrl(name, url2, headers, request = fetch) {
51733
51787
  const start = Date.now();
51734
51788
  try {
51735
- const res = await fetch(url2, {
51789
+ const res = await request(url2, {
51736
51790
  headers,
51737
51791
  // Do not forward probe credentials to a login redirect or treat its HTML
51738
51792
  // login page as a successful health check.
@@ -51837,7 +51891,8 @@ async function systemHealth(db, env2, requestUrl) {
51837
51891
  checkUrl(
51838
51892
  "proxy",
51839
51893
  `${proxyUrl.replace(/\/$/, "")}/health`,
51840
- installed ? dispatchHeaders : void 0
51894
+ installed ? dispatchHeaders : void 0,
51895
+ installed && env2.PLATFORM_PROXY ? (url2, init) => env2.PLATFORM_PROXY.fetch(url2, init) : void 0
51841
51896
  ),
51842
51897
  ...!installed ? [
51843
51898
  checkUrl(
@@ -57083,6 +57138,27 @@ function healthCheckApplication(name, hostname3) {
57083
57138
  policies: bypassPolicy("Void platform health check")
57084
57139
  };
57085
57140
  }
57141
+ function publicAppManagementDestination(apiUrl) {
57142
+ const url2 = new URL(apiUrl);
57143
+ if (url2.protocol !== "https:" || url2.username || url2.password) {
57144
+ throw new Error("Public app management requires an HTTPS dispatch origin.");
57145
+ }
57146
+ return url2.host + "/__void/app-access-management";
57147
+ }
57148
+ function publicAppManagementApplication(name, destination) {
57149
+ return {
57150
+ name,
57151
+ destinations: [destination],
57152
+ policies: [
57153
+ {
57154
+ name: "App access management check",
57155
+ decision: "deny",
57156
+ precedence: 1,
57157
+ include: [{ everyone: {} }]
57158
+ }
57159
+ ]
57160
+ };
57161
+ }
57086
57162
  var ZeroTrustCloudflareError, ZeroTrustCloudflareClient;
57087
57163
  var init_cloudflare = __esm({
57088
57164
  "../../home/runner/work/void/void/platform/packages/api/src/zero-trust/cloudflare.ts"() {
@@ -57398,6 +57474,78 @@ var init_cloudflare = __esm({
57398
57474
  throw error63;
57399
57475
  }
57400
57476
  }
57477
+ /** An installer/operator-owned health exception is verified, never modified or adopted. */
57478
+ async verifyExistingHealthApplication(application, hostname3, current) {
57479
+ const id2 = text2(application.id, "application id");
57480
+ const name = text2(application.name, "application name");
57481
+ const audience = text2(application.aud, "application audience");
57482
+ const destination = `${hostname3}/health`;
57483
+ if (current && (id2 !== current.id || name !== current.name || audience !== current.audience) || application.type !== "self_hosted" || application.domain !== destination || !sameDestinations(application, [destination]) || application.destinations !== void 0 && (!Array.isArray(application.destinations) || application.destinations.some((entry) => {
57484
+ const value = object3(entry, "Access application destination");
57485
+ return value.type !== "public" || value.uri !== destination;
57486
+ })) || application.target_criteria != null && (!Array.isArray(application.target_criteria) || application.target_criteria.length > 0)) {
57487
+ throw new Error(
57488
+ `The existing Access health application must cover only ${destination} and retain its recorded identity.`
57489
+ );
57490
+ }
57491
+ const policies = await this.request(
57492
+ `apps/${encodeURIComponent(id2)}/policies?page=1&per_page=2`
57493
+ );
57494
+ const policy = policies?.[0];
57495
+ if (!Array.isArray(policies) || policies.length !== 1 || policy?.decision !== "bypass" || JSON.stringify(policy.include) !== '[{"everyone":{}}]' || policy.require !== void 0 && (!Array.isArray(policy.require) || policy.require.length > 0) || policy.exclude !== void 0 && (!Array.isArray(policy.exclude) || policy.exclude.length > 0)) {
57496
+ throw new Error(
57497
+ `The existing Access policy for ${destination} must remain the reserved health-check exception.`
57498
+ );
57499
+ }
57500
+ return { id: id2, name, audience, destinations: [destination] };
57501
+ }
57502
+ async ensureHealthApplication(name, hostname3, current) {
57503
+ if (current && current.name !== name) {
57504
+ const application2 = await this.application(current.id);
57505
+ if (!application2) {
57506
+ throw new Error(
57507
+ "The existing Access health application is missing. Restore it before reconciling project Zero Trust."
57508
+ );
57509
+ }
57510
+ return this.verifyExistingHealthApplication(application2, hostname3, current);
57511
+ }
57512
+ const { application, inventory } = await this.locateOwnedApplication(current?.id, name);
57513
+ if (application) {
57514
+ return this.ensureApplication(healthCheckApplication(name, hostname3), {
57515
+ id: text2(application.id, "application id"),
57516
+ name
57517
+ });
57518
+ }
57519
+ const matches = [...inventory.values()].filter(
57520
+ (entry) => destinationList(entry).includes(`${hostname3}/health`)
57521
+ );
57522
+ if (matches.length > 1) {
57523
+ throw new Error(
57524
+ `Multiple Access applications cover ${hostname3}/health. Reconcile them before retrying.`
57525
+ );
57526
+ }
57527
+ if (matches[0]) {
57528
+ const existing = await this.application(text2(matches[0].id, "application id"));
57529
+ if (!existing) {
57530
+ throw new Error(
57531
+ "The existing Access health application disappeared. Inspect Access before retrying."
57532
+ );
57533
+ }
57534
+ return this.verifyExistingHealthApplication(existing, hostname3);
57535
+ }
57536
+ return this.ensureApplication(healthCheckApplication(name, hostname3), { name });
57537
+ }
57538
+ async inspectHealthApplication(current, managedName, hostname3) {
57539
+ if (current.name === managedName) {
57540
+ return this.inspectApplication(current, healthCheckApplication(current.name, hostname3));
57541
+ }
57542
+ const application = await this.application(current.id);
57543
+ if (!application) {
57544
+ return { present: false, drifted: true };
57545
+ }
57546
+ await this.verifyExistingHealthApplication(application, hostname3, current);
57547
+ return { present: true, drifted: false };
57548
+ }
57401
57549
  async deleteApplication(id2, expectedName) {
57402
57550
  if (!id2 && !expectedName) {
57403
57551
  return;
@@ -57419,6 +57567,8 @@ var init_cloudflare = __esm({
57419
57567
  __name(reusablePolicyReferences, "reusablePolicyReferences");
57420
57568
  __name(bypassPolicy, "bypassPolicy");
57421
57569
  __name(healthCheckApplication, "healthCheckApplication");
57570
+ __name(publicAppManagementDestination, "publicAppManagementDestination");
57571
+ __name(publicAppManagementApplication, "publicAppManagementApplication");
57422
57572
  }
57423
57573
  });
57424
57574
 
@@ -57465,8 +57615,8 @@ function timestamp2(value, label) {
57465
57615
  }
57466
57616
  return result;
57467
57617
  }
57468
- function ids(value, label) {
57469
- if (!Array.isArray(value) || value.length < 1 || value.length > 16) {
57618
+ function ids(value, label, minimum = 1) {
57619
+ if (!Array.isArray(value) || value.length < minimum || value.length > 16) {
57470
57620
  throw new ZeroTrustConfigurationError(`${label} must contain between 1 and 16 ids`);
57471
57621
  }
57472
57622
  const result = value.map((entry) => requiredText(entry, `${label} id`, 128));
@@ -57588,6 +57738,9 @@ function parseZeroTrustConfiguration(value) {
57588
57738
  if (typeof input2.enabled !== "boolean" || typeof input2.protectNewProjects !== "boolean") {
57589
57739
  throw new ZeroTrustConfigurationError("Zero Trust configuration flags are invalid");
57590
57740
  }
57741
+ if (input2.publicAccess !== void 0 && input2.publicAccess !== true || input2.managePublicCustomDomains !== void 0 && input2.managePublicCustomDomains !== true || input2.publicAccess && input2.protectNewProjects) {
57742
+ throw new ZeroTrustConfigurationError("invalid public app access configuration");
57743
+ }
57591
57744
  const teamDomain = requiredText(input2.teamDomain, "Access team domain", 512).replace(/\/+$/, "");
57592
57745
  let parsedTeamDomain;
57593
57746
  try {
@@ -57602,9 +57755,29 @@ function parseZeroTrustConfiguration(value) {
57602
57755
  "Access team domain must be an HTTPS cloudflareaccess.com origin"
57603
57756
  );
57604
57757
  }
57758
+ let publicAccessGuardDestination;
57759
+ if (input2.publicAccess) {
57760
+ publicAccessGuardDestination = requiredText(
57761
+ input2.publicAccessGuardDestination,
57762
+ "public app management destination",
57763
+ 512
57764
+ );
57765
+ try {
57766
+ if (publicAppManagementDestination("https://" + publicAccessGuardDestination) !== publicAccessGuardDestination) {
57767
+ throw new Error("invalid destination");
57768
+ }
57769
+ } catch {
57770
+ throw new ZeroTrustConfigurationError("Invalid public app management destination");
57771
+ }
57772
+ }
57605
57773
  const wildcardApplication = parseApplication(input2.wildcardApplication);
57606
57774
  const healthApplication = parseApplication(input2.healthApplication ?? null);
57607
57775
  const operation = parseOperation(input2.operation);
57776
+ if (input2.publicAccess && (operation?.existingProjects === "protected" || Array.isArray(input2.identityProviderIds) && input2.identityProviderIds.length > 0 || Array.isArray(input2.policyIds) && input2.policyIds.length > 0)) {
57777
+ throw new ZeroTrustConfigurationError(
57778
+ "Public app access cannot contain identity policies or protected project choices"
57779
+ );
57780
+ }
57608
57781
  if (operation?.phase === "bypass-delete" && (input2.enabled || wildcardApplication)) {
57609
57782
  throw new ZeroTrustConfigurationError("Zero Trust bypass cleanup requires wildcard removal");
57610
57783
  }
@@ -57620,8 +57793,14 @@ function parseZeroTrustConfiguration(value) {
57620
57793
  protectNewProjects: input2.protectNewProjects,
57621
57794
  accountId: requiredText(input2.accountId, "Cloudflare account id", 128),
57622
57795
  teamDomain,
57623
- identityProviderIds: ids(input2.identityProviderIds, "identity providers"),
57624
- policyIds: ids(input2.policyIds, "policies"),
57796
+ ...input2.publicAccess === true ? { publicAccess: true, publicAccessGuardDestination } : {},
57797
+ ...input2.managePublicCustomDomains === true ? { managePublicCustomDomains: true } : {},
57798
+ identityProviderIds: ids(
57799
+ input2.identityProviderIds,
57800
+ "identity providers",
57801
+ input2.publicAccess ? 0 : 1
57802
+ ),
57803
+ policyIds: ids(input2.policyIds, "policies", input2.publicAccess ? 0 : 1),
57625
57804
  credential: parseCredential(input2.credential),
57626
57805
  wildcardApplicationName: requiredText(
57627
57806
  input2.wildcardApplicationName,
@@ -57648,8 +57827,13 @@ function normalizeZeroTrustConfigureInput(input2) {
57648
57827
  const apiToken = requiredText(value.apiToken, "Cloudflare API token", 8192);
57649
57828
  return {
57650
57829
  apiToken,
57651
- identityProviderIds: ids(value.identityProviderIds, "identity providers"),
57652
- policyIds: ids(value.policyIds, "policies"),
57830
+ ...value.publicAccess === true ? { publicAccess: true, managePublicCustomDomains: true } : {},
57831
+ identityProviderIds: ids(
57832
+ value.identityProviderIds,
57833
+ "identity providers",
57834
+ value.publicAccess === true ? 0 : 1
57835
+ ),
57836
+ policyIds: ids(value.policyIds, "policies", value.publicAccess === true ? 0 : 1),
57653
57837
  protectNewProjects: value.protectNewProjects,
57654
57838
  ...existingProjects ? { existingProjects } : {}
57655
57839
  };
@@ -57713,6 +57897,7 @@ var init_configuration2 = __esm({
57713
57897
  init_drizzle_orm();
57714
57898
  init_platform_settings();
57715
57899
  init_schema2();
57900
+ init_cloudflare();
57716
57901
  ZERO_TRUST_SETTING = "zeroTrust.configuration";
57717
57902
  ZERO_TRUST_CREDENTIAL_ID = "zero-trust-cloudflare-api-token";
57718
57903
  __name(zeroTrustConfigurationStatus, "zeroTrustConfigurationStatus");
@@ -57935,22 +58120,36 @@ async function updateProjectState(db, projectId, values2) {
57935
58120
  }
57936
58121
  return row;
57937
58122
  }
57938
- async function projectDestinations(env2, db, project2, protectedProject) {
58123
+ async function projectDestinations(env2, db, project2, protectedProject, verifiedDomainId) {
57939
58124
  const application = readPlatformApplication(
57940
58125
  await resolveZeroTrustApplicationEnvironment(env2, db)
57941
58126
  );
57942
58127
  const platform = [...new Set(platformProjectHostnames(project2, application))];
57943
- if (!protectedProject) {
58128
+ const reserved = [env2.AUTH_REDIRECT_URI, env2.PROXY_URL, env2.EMAIL_GATEWAY_ORIGIN].filter((value) => !!value && URL.canParse(value)).map((value) => new URL(value).hostname.toLowerCase());
58129
+ const checkHostnames = /* @__PURE__ */ __name((values2) => {
58130
+ if (values2.some((hostname3) => reserved.includes(hostname3.toLowerCase()))) {
58131
+ throw new ZeroTrustUnavailableError(
58132
+ "The project hostname overlaps a platform API, proxy, or email endpoint. Choose another hostname."
58133
+ );
58134
+ }
58135
+ }, "checkHostnames");
58136
+ checkHostnames(platform);
58137
+ if (!protectedProject && !(await readZeroTrustConfigurationStrict(db))?.managePublicCustomDomains) {
57944
58138
  return { platform, custom: [] };
57945
58139
  }
57946
58140
  const custom2 = [
57947
- ...new Set((await getDomainsByProjectId(db, project2.id)).map((domain2) => domain2.hostname))
58141
+ ...new Set(
58142
+ (await getDomainsByProjectId(db, project2.id)).filter(
58143
+ (domain2) => protectedProject || domain2.status === "active" || domain2.id === verifiedDomainId
58144
+ ).map((domain2) => domain2.hostname)
58145
+ )
57948
58146
  ];
57949
58147
  if (custom2.length > MAX_ZERO_TRUST_CUSTOM_DOMAINS_PER_PROJECT) {
57950
58148
  throw new ZeroTrustUnavailableError(
57951
58149
  `Project Zero Trust supports at most ${MAX_ZERO_TRUST_CUSTOM_DOMAINS_PER_PROJECT} custom domains per project.`
57952
58150
  );
57953
58151
  }
58152
+ checkHostnames(custom2);
57954
58153
  return { platform, custom: custom2 };
57955
58154
  }
57956
58155
  async function markProjectCustomDomainsChanged(db, projectId) {
@@ -58382,7 +58581,18 @@ async function checkpointProjectReconciliation(env2, db, projectId, options) {
58382
58581
  }
58383
58582
  const existing = await getProjectZeroTrustState(lockedDb, projectId);
58384
58583
  const desired = options?.desiredProtection ?? existing?.desired_protected ?? initialProjectProtection(configuration, project2);
58385
- const destinations = await projectDestinations(env2, lockedDb, project2, desired);
58584
+ if (desired && configuration.publicAccess) {
58585
+ throw new ZeroTrustUnavailableError(
58586
+ "Choose company identity providers and policies in Admin \u2192 Zero Trust before protecting a project."
58587
+ );
58588
+ }
58589
+ const destinations = await projectDestinations(
58590
+ env2,
58591
+ lockedDb,
58592
+ project2,
58593
+ desired,
58594
+ options?.verifiedDomainId
58595
+ );
58386
58596
  const values2 = {
58387
58597
  desired_protected: desired,
58388
58598
  status: "reconciling",
@@ -58392,13 +58602,13 @@ async function checkpointProjectReconciliation(env2, db, projectId, options) {
58392
58602
  if (!desired && !existing?.bypass_application_name) {
58393
58603
  values2.bypass_application_name = await applicationName(env2, `public-${project2.id}`);
58394
58604
  }
58395
- if (desired && destinations.custom.length > 0 && !existing?.protected_application_name) {
58605
+ if ((desired || configuration.managePublicCustomDomains) && destinations.custom.length > 0 && !existing?.protected_application_name) {
58396
58606
  values2.protected_application_name = await applicationName(env2, `protected-${project2.id}`);
58397
58607
  }
58398
58608
  return putProjectState(lockedDb, projectId, values2);
58399
58609
  });
58400
58610
  }
58401
- async function reconcilePublicProjectLocked(env2, lockedDb, project2, current, platformDestinations, client2, configuration, routingBudget) {
58611
+ async function reconcilePublicProjectLocked(env2, lockedDb, project2, current, platformDestinations, customDestinations, client2, configuration, routingBudget) {
58402
58612
  if (!current.bypass_application_name) {
58403
58613
  throw new Error("The project public application checkpoint is missing.");
58404
58614
  }
@@ -58414,18 +58624,39 @@ async function reconcilePublicProjectLocked(env2, lockedDb, project2, current, p
58414
58624
  bypass_application_id: bypass.id,
58415
58625
  bypass_application_name: bypass.name
58416
58626
  });
58627
+ let publicCustom = null;
58628
+ if (configuration.managePublicCustomDomains && customDestinations.length) {
58629
+ if (!current.protected_application_name) {
58630
+ throw new Error("The custom-domain Access checkpoint is missing.");
58631
+ }
58632
+ publicCustom = await client2.ensureApplication(
58633
+ {
58634
+ name: current.protected_application_name,
58635
+ destinations: customDestinations,
58636
+ policies: bypassPolicy("Void project is public")
58637
+ },
58638
+ projectApplicationReference(current, "protected")
58639
+ );
58640
+ await updateProjectState(lockedDb, project2.id, {
58641
+ protected_application_id: publicCustom.id,
58642
+ protected_application_name: publicCustom.name,
58643
+ protected_application_audience: publicCustom.audience
58644
+ });
58645
+ }
58417
58646
  if (!await publishProjectZeroTrustRoutingLocked(env2, lockedDb, project2, null, routingBudget)) {
58418
58647
  return await getProjectZeroTrustState(lockedDb, project2.id);
58419
58648
  }
58420
58649
  const row = await updateProjectState(lockedDb, project2.id, { effective_protected: false });
58421
- await client2.deleteApplication(row.protected_application_id, row.protected_application_name);
58650
+ if (!publicCustom) {
58651
+ await client2.deleteApplication(row.protected_application_id, row.protected_application_name);
58652
+ }
58422
58653
  return updateProjectState(lockedDb, project2.id, {
58423
58654
  effective_protected: false,
58424
58655
  status: "ready",
58425
58656
  reconciled_operation: completedProjectOperation(configuration, true),
58426
- protected_application_id: null,
58427
- protected_application_name: null,
58428
- protected_application_audience: null,
58657
+ protected_application_id: publicCustom?.id ?? null,
58658
+ protected_application_name: publicCustom?.name ?? null,
58659
+ protected_application_audience: publicCustom?.audience ?? null,
58429
58660
  last_error: null
58430
58661
  });
58431
58662
  }
@@ -58448,7 +58679,8 @@ async function reconcileProjectZeroTrustLocked(env2, lockedDb, projectId, record
58448
58679
  env2,
58449
58680
  lockedDb,
58450
58681
  project2,
58451
- existing.desired_protected
58682
+ existing.desired_protected,
58683
+ options?.verifiedDomainId
58452
58684
  );
58453
58685
  return recordErrors(async () => {
58454
58686
  let row = existing;
@@ -58459,6 +58691,7 @@ async function reconcileProjectZeroTrustLocked(env2, lockedDb, projectId, record
58459
58691
  project2,
58460
58692
  row,
58461
58693
  destinations.platform,
58694
+ destinations.custom,
58462
58695
  client2,
58463
58696
  configuration,
58464
58697
  options?.routingBudget
@@ -58580,6 +58813,7 @@ async function reconcilePublicProjectForOperation(env2, db, projectId, configura
58580
58813
  project2,
58581
58814
  row,
58582
58815
  destinations.platform,
58816
+ destinations.custom,
58583
58817
  client2,
58584
58818
  configuration,
58585
58819
  routingBudget
@@ -58659,7 +58893,7 @@ async function cleanupProjectZeroTrust(env2, db, projectId) {
58659
58893
  await client2.deleteApplication(row.bypass_application_id, row.bypass_application_name);
58660
58894
  await client2.deleteApplication(row.protected_application_id, row.protected_application_name);
58661
58895
  }
58662
- async function reconcileProjectZeroTrustDomains(env2, db, projectId) {
58896
+ async function reconcileProjectZeroTrustDomains(env2, db, projectId, options) {
58663
58897
  const [configuration, row] = await Promise.all([
58664
58898
  readZeroTrustConfigurationStrict(db),
58665
58899
  getProjectZeroTrustState(db, projectId)
@@ -58667,7 +58901,10 @@ async function reconcileProjectZeroTrustDomains(env2, db, projectId) {
58667
58901
  if (!configuration?.enabled || configuration.operation?.kind === "disable" || !row) {
58668
58902
  return row;
58669
58903
  }
58670
- return reconcileProjectZeroTrust(env2, db, projectId, { duringOperation: true });
58904
+ return reconcileProjectZeroTrust(env2, db, projectId, {
58905
+ duringOperation: true,
58906
+ verifiedDomainId: options?.verifiedDomainId
58907
+ });
58671
58908
  }
58672
58909
  var init_project = __esm({
58673
58910
  "../../home/runner/work/void/void/platform/packages/api/src/zero-trust/project.ts"() {
@@ -128664,6 +128901,7 @@ function ZeroTrustPage({ path, platformName, username }) {
128664
128901
  operation?.existingProjects === "public" ? "public" : "protected"
128665
128902
  );
128666
128903
  const badge = statusBadge(configured, enabled, status2 ?? null);
128904
+ const publicOnly = configured && status2 !== "invalid_configuration" && identityProviderIds.length === 0 && policyIds.length === 0;
128667
128905
  return /* @__PURE__ */ jsx5(Layout2, { platformName, title: "Zero Trust", username, children: /* @__PURE__ */ jsxs2("div", { className: "void-admin-zero-trust", children: [
128668
128906
  /* @__PURE__ */ jsxs2("header", { className: "void-admin-zero-trust-intro", children: [
128669
128907
  /* @__PURE__ */ jsx5(Heading, { level: 1, children: "Project Zero Trust" }),
@@ -128734,6 +128972,7 @@ function ZeroTrustPage({ path, platformName, username }) {
128734
128972
  operation && /* @__PURE__ */ jsx5("p", { children: "To replace an expired or revoked token, keep the saved settings and submit a new token. The pending operation will resume." })
128735
128973
  ] }),
128736
128974
  /* @__PURE__ */ jsxs2("form", { method: "post", action: `${HOME4}/configure`, className: "void-admin-zero-trust-form", children: [
128975
+ publicOnly && /* @__PURE__ */ jsx5("p", { children: "Void manages public app rules for this account. Leave policy fields blank to replace its token, or enter company identities and policies to configure protection." }),
128737
128976
  /* @__PURE__ */ jsx5(
128738
128977
  FormTextInput,
128739
128978
  {
@@ -128751,8 +128990,8 @@ function ZeroTrustPage({ path, platformName, username }) {
128751
128990
  label: "Identity provider IDs",
128752
128991
  name: "identityProviderIds",
128753
128992
  initialValue: identityProviderIds.join(", "),
128754
- description: "Separate IDs with commas or spaces.",
128755
- required: true,
128993
+ description: publicOnly ? "Leave blank to keep public app rules, or enter company IDs to configure protection." : "Separate IDs with commas or spaces.",
128994
+ required: !publicOnly,
128756
128995
  width: 480
128757
128996
  }
128758
128997
  ),
@@ -128763,7 +129002,7 @@ function ZeroTrustPage({ path, platformName, username }) {
128763
129002
  name: "policyIds",
128764
129003
  initialValue: policyIds.join(", "),
128765
129004
  description: "Separate IDs with commas or spaces.",
128766
- required: true,
129005
+ required: !publicOnly,
128767
129006
  width: 480
128768
129007
  }
128769
129008
  ),
@@ -128803,7 +129042,7 @@ function ZeroTrustPage({ path, platformName, username }) {
128803
129042
  {
128804
129043
  method: "post",
128805
129044
  action: `${HOME4}/disable`,
128806
- "data-confirm": "Disable Zero Trust and make every project public?",
129045
+ "data-confirm": "Remove Void-managed Access protection? Apps may become public or be blocked by account-wide Default-Deny.",
128807
129046
  children: /* @__PURE__ */ jsx5(
128808
129047
  Button,
128809
129048
  {
@@ -129616,21 +129855,28 @@ async function zeroTrustStatus(env2, db, options = {}) {
129616
129855
  try {
129617
129856
  env2 = await resolveZeroTrustApplicationEnvironment(env2, db, configuration);
129618
129857
  const client2 = await clientForConfiguration(env2, configuration);
129619
- const selection = await client2.validateSelection({
129858
+ const selection = configuration.publicAccess ? await client2.getOrganization() : await client2.validateSelection({
129620
129859
  identityProviderIds: configuration.identityProviderIds,
129621
129860
  policyIds: configuration.policyIds
129622
129861
  });
129623
- live = await client2.inspectApplication(configuration.wildcardApplication, {
129624
- name: configuration.wildcardApplicationName,
129625
- destinations: zeroTrustWildcardDestinations(readPlatformApplication(env2)),
129626
- allowedIdentityProviderIds: configuration.identityProviderIds,
129627
- policies: reusablePolicyReferences(configuration.policyIds)
129628
- });
129862
+ live = await client2.inspectApplication(
129863
+ configuration.wildcardApplication,
129864
+ configuration.publicAccess ? publicAppManagementApplication(
129865
+ configuration.wildcardApplicationName,
129866
+ configuration.publicAccessGuardDestination
129867
+ ) : {
129868
+ name: configuration.wildcardApplicationName,
129869
+ destinations: zeroTrustWildcardDestinations(readPlatformApplication(env2)),
129870
+ allowedIdentityProviderIds: configuration.identityProviderIds,
129871
+ policies: reusablePolicyReferences(configuration.policyIds)
129872
+ }
129873
+ );
129629
129874
  const healthHostname = platformHealthHostname(readPlatformApplication(env2));
129630
129875
  if (healthHostname) {
129631
- const health = configuration.healthApplication ? await client2.inspectApplication(
129876
+ const health = configuration.healthApplication ? await client2.inspectHealthApplication(
129632
129877
  configuration.healthApplication,
129633
- healthCheckApplication(configuration.healthApplication.name, healthHostname)
129878
+ configuration.healthApplicationName,
129879
+ healthHostname
129634
129880
  ) : { present: false, drifted: true };
129635
129881
  live = {
129636
129882
  present: live.present && health.present,
@@ -149935,10 +150181,17 @@ async function handleManifestOnlyDeploy(context) {
149935
150181
  terminalEvent = event;
149936
150182
  return;
149937
150183
  }
149938
- persistedEvents = persistedEvents.then(
149939
- () => appendDeploymentEvent(context.get("db"), preDeployment.id, cutoverOwnerId, event)
149940
- );
149941
- void persistedEvents.catch(() => {
150184
+ persistedEvents = persistedEvents.then(async () => {
150185
+ for (let attempt = 0; attempt < 2; attempt++) {
150186
+ try {
150187
+ await appendDeploymentEvent(context.get("db"), preDeployment.id, cutoverOwnerId, event);
150188
+ return;
150189
+ } catch (error63) {
150190
+ if (attempt === 1) {
150191
+ logger.warn("progress", "persist_failed", { event: event.event, err: String(error63) });
150192
+ }
150193
+ }
150194
+ }
149942
150195
  });
149943
150196
  return;
149944
150197
  }
@@ -152796,7 +153049,14 @@ async function deploymentExecutionStatus(db, installationId2) {
152796
153049
  });
152797
153050
  }
152798
153051
  async function getDeploymentOperation(db, id2) {
152799
- const [operation] = await db.select().from(deploymentOperations).where(eq(deploymentOperations.id, id2));
153052
+ const [operation] = await db.select({
153053
+ ...getTableColumns(deploymentOperations),
153054
+ // The stored lease is a timestamp without time zone. Resolve it in the
153055
+ // SQL session's zone so observers receive an unambiguous UTC instant.
153056
+ lease_expires_at: sql`to_char(
153057
+ ${deploymentOperations.lease_expires_at} AT TIME ZONE current_setting('TimeZone') AT TIME ZONE 'UTC',
153058
+ 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')`
153059
+ }).from(deploymentOperations).where(eq(deploymentOperations.id, id2));
152800
153060
  return operation ?? null;
152801
153061
  }
152802
153062
  async function findDeploymentOperation(db, projectId, actorId, key) {
@@ -152886,7 +153146,18 @@ function deploymentEventPage(operation, since) {
152886
153146
  deploymentId: operation.id,
152887
153147
  events,
152888
153148
  lastSeq,
152889
- terminal: operation.status === "terminal" && lastSeq >= operation.last_seq
153149
+ terminal: operation.status === "terminal" && lastSeq >= operation.last_seq,
153150
+ ...operation.status !== "terminal" && {
153151
+ heartbeat: {
153152
+ event: "heartbeat",
153153
+ deploymentId: operation.id,
153154
+ step: operation.status === "recovering" ? "recovery" : operation.events.at(-1)?.event.event ?? "queued",
153155
+ status: operation.status,
153156
+ ...operation.lease_expires_at && {
153157
+ recoveryAt: new Date(operation.lease_expires_at).toISOString()
153158
+ }
153159
+ }
153160
+ }
152890
153161
  };
152891
153162
  }
152892
153163
  function deploymentReceiptResponse(db, id2) {
@@ -152922,7 +153193,8 @@ function deploymentReceiptResponse(db, id2) {
152922
153193
  return;
152923
153194
  }
152924
153195
  if (!page.events.length) {
152925
- controller.enqueue(encoder12.encode('{"event":"heartbeat","step":"queued"}\n'));
153196
+ controller.enqueue(encoder12.encode(`${JSON.stringify(page.heartbeat)}
153197
+ `));
152926
153198
  }
152927
153199
  timer = setTimeout(() => {
152928
153200
  void poll();
@@ -154842,10 +155114,10 @@ async function runConfigureOperation(env2, db, initial, suppliedClient, supplied
154842
155114
  try {
154843
155115
  env2 = await resolveZeroTrustApplicationEnvironment(env2, db, configuration);
154844
155116
  const client2 = suppliedClient ?? await clientForConfiguration(env2, configuration);
154845
- const selection = suppliedSelection ?? await client2.validateSelection({
155117
+ const selection = suppliedSelection ?? await (configuration.publicAccess ? client2.getOrganization() : client2.validateSelection({
154846
155118
  identityProviderIds: configuration.identityProviderIds,
154847
155119
  policyIds: configuration.policyIds
154848
- });
155120
+ }));
154849
155121
  if (selection.teamDomain !== configuration.teamDomain) {
154850
155122
  configuration = await patchConfiguration(db, configuration, {
154851
155123
  teamDomain: selection.teamDomain
@@ -154944,15 +155216,13 @@ async function runConfigureOperation(env2, db, initial, suppliedClient, supplied
154944
155216
  }
154945
155217
  const name = configuration.wildcardApplicationName;
154946
155218
  configuration = await withZeroTrustOperationLock(db, configuration, async (lockedDb) => {
154947
- const health = healthHostname && configuration.healthApplicationName ? await client2.ensureApplication(
154948
- healthCheckApplication(configuration.healthApplicationName, healthHostname),
154949
- {
154950
- id: configuration.healthApplication?.id,
154951
- name: configuration.healthApplicationName
154952
- }
155219
+ const health = healthHostname && configuration.healthApplicationName ? await client2.ensureHealthApplication(
155220
+ configuration.healthApplicationName,
155221
+ healthHostname,
155222
+ configuration.healthApplication
154953
155223
  ) : configuration.healthApplication;
154954
155224
  const wildcard = await client2.ensureApplication(
154955
- {
155225
+ configuration.publicAccess ? publicAppManagementApplication(name, configuration.publicAccessGuardDestination) : {
154956
155226
  name,
154957
155227
  destinations: zeroTrustWildcardDestinations(readPlatformApplication(env2)),
154958
155228
  allowedIdentityProviderIds: configuration.identityProviderIds,
@@ -155140,10 +155410,12 @@ async function runDisableOperation(env2, db, initial) {
155140
155410
  configuration.wildcardApplication?.id,
155141
155411
  configuration.wildcardApplicationName
155142
155412
  );
155143
- await client2.deleteApplication(
155144
- configuration.healthApplication?.id,
155145
- configuration.healthApplicationName
155146
- );
155413
+ if (!configuration.healthApplication || configuration.healthApplication.name === configuration.healthApplicationName) {
155414
+ await client2.deleteApplication(
155415
+ configuration.healthApplication?.id,
155416
+ configuration.healthApplicationName
155417
+ );
155418
+ }
155147
155419
  return advanceOperation(lockedDb, configuration, "bypass-delete", {
155148
155420
  enabled: false,
155149
155421
  wildcardApplication: null,
@@ -155229,7 +155501,9 @@ async function activatePlatformApplication(env2, db, value) {
155229
155501
  if (!current) {
155230
155502
  throw new ZeroTrustUnavailableError("Project Zero Trust configuration is unavailable.");
155231
155503
  }
155232
- assertWildcardExcludesControlPlane(env2, zeroTrustWildcardDestinations(target));
155504
+ if (!current.publicAccess) {
155505
+ assertWildcardExcludesControlPlane(env2, zeroTrustWildcardDestinations(target));
155506
+ }
155233
155507
  const now = (/* @__PURE__ */ new Date()).toISOString();
155234
155508
  const configuration = {
155235
155509
  ...current,
@@ -155284,6 +155558,16 @@ function assertWildcardExcludesControlPlane(env2, destinations, requestUrl) {
155284
155558
  }
155285
155559
  __name(assertWildcardExcludesControlPlane, "assertWildcardExcludesControlPlane");
155286
155560
  function assertExistingProjectsChoice(current, input2) {
155561
+ if (input2.publicAccess && (input2.existingProjects === "protected" || input2.protectNewProjects || input2.identityProviderIds.length || input2.policyIds.length)) {
155562
+ throw new ZeroTrustUnavailableError(
155563
+ "Public app access cannot require identities or protect new projects."
155564
+ );
155565
+ }
155566
+ if (input2.publicAccess && current?.enabled && !current.publicAccess) {
155567
+ throw new ZeroTrustUnavailableError(
155568
+ "Disable the existing protected configuration before enabling public-only app rules."
155569
+ );
155570
+ }
155287
155571
  const operation = current?.operation;
155288
155572
  if (operation) {
155289
155573
  if (input2.existingProjects === void 0 || input2.existingProjects === operation.existingProjects) {
@@ -155317,11 +155601,19 @@ async function configurePlatformZeroTrust(env2, db, input2, options = {}) {
155317
155601
  const previous = await readZeroTrustConfigurationStrict(db);
155318
155602
  assertExistingProjectsChoice(previous, input2);
155319
155603
  const replacingDisableCredential = previous?.operation?.kind === "disable";
155320
- if (!replacingDisableCredential) {
155604
+ if (!replacingDisableCredential && !input2.publicAccess) {
155321
155605
  assertWildcardExcludesControlPlane(env2, destinations, options.requestUrl);
155322
155606
  }
155607
+ const application = readPlatformApplication(env2);
155608
+ const publicGuardHostname = platformHealthHostname(application) ?? (application.workersDev && env2.DISPATCH_WORKER_NAME ? `${env2.DISPATCH_WORKER_NAME}.${application.workersDev.subdomain}.workers.dev` : null);
155609
+ if (input2.publicAccess && !publicGuardHostname) {
155610
+ throw new ZeroTrustUnavailableError(
155611
+ "The installation\u2019s reserved dispatch hostname is unavailable. Repair the installation before configuring public app rules."
155612
+ );
155613
+ }
155614
+ const publicAccessGuardDestination = input2.publicAccess ? publicAppManagementDestination("https://" + publicGuardHostname) : void 0;
155323
155615
  const client2 = new ZeroTrustCloudflareClient(accountId, input2.apiToken);
155324
- const selection = replacingDisableCredential ? await client2.getOrganization() : await client2.validateSelection(input2);
155616
+ const selection = replacingDisableCredential || input2.publicAccess ? await client2.getOrganization() : await client2.validateSelection(input2);
155325
155617
  const credential = await encryptPlatformCredential(
155326
155618
  env2,
155327
155619
  installationId(env2),
@@ -155337,7 +155629,7 @@ async function configurePlatformZeroTrust(env2, db, input2, options = {}) {
155337
155629
  assertExistingProjectsChoice(current, input2);
155338
155630
  if (current?.operation) {
155339
155631
  const sameIds = /* @__PURE__ */ __name((left, right) => left.length === right.length && left.every((id2) => right.includes(id2)), "sameIds");
155340
- if (current.accountId !== accountId || current.protectNewProjects !== input2.protectNewProjects || !sameIds(current.identityProviderIds, input2.identityProviderIds) || !sameIds(current.policyIds, input2.policyIds)) {
155632
+ if (current.accountId !== accountId || !!current.publicAccess !== !!input2.publicAccess || current.protectNewProjects !== input2.protectNewProjects || !sameIds(current.identityProviderIds, input2.identityProviderIds) || !sameIds(current.policyIds, input2.policyIds)) {
155341
155633
  throw new ZeroTrustUnavailableError(
155342
155634
  "A saved Zero Trust operation is pending. To replace its API token, configure again with the same identity providers, policies, and project selections."
155343
155635
  );
@@ -155355,6 +155647,8 @@ async function configurePlatformZeroTrust(env2, db, input2, options = {}) {
155355
155647
  const now = (/* @__PURE__ */ new Date()).toISOString();
155356
155648
  return {
155357
155649
  version: 1,
155650
+ ...input2.publicAccess ? { publicAccess: true, publicAccessGuardDestination } : {},
155651
+ ...input2.managePublicCustomDomains || current?.managePublicCustomDomains ? { managePublicCustomDomains: true } : {},
155358
155652
  revision: (current?.revision ?? 0) + 1,
155359
155653
  enabled: current?.enabled ?? false,
155360
155654
  protectNewProjects: input2.protectNewProjects,
@@ -155541,6 +155835,7 @@ zeroTrustOperatorRoutes.post("/zero-trust/configure", async (context) => {
155541
155835
  return context.json(
155542
155836
  preview("zero-trust.configure", "platform", {
155543
155837
  accountId: context.env.CF_ACCOUNT_ID,
155838
+ ...input2.publicAccess ? { appAccess: "public" } : {},
155544
155839
  identityProviderIds: input2.identityProviderIds,
155545
155840
  policyIds: input2.policyIds,
155546
155841
  protectNewProjects: input2.protectNewProjects,
@@ -156616,7 +156911,9 @@ app11.post("/configure", async (context) => {
156616
156911
  const form = await context.req.parseBody();
156617
156912
  let input2;
156618
156913
  try {
156914
+ const saved = await readZeroTrustConfigurationStrict(context.get("db"));
156619
156915
  input2 = normalizeZeroTrustConfigureInput({
156916
+ publicAccess: !!saved?.publicAccess && !values(form.identityProviderIds).length && !values(form.policyIds).length,
156620
156917
  apiToken: form.apiToken,
156621
156918
  identityProviderIds: values(form.identityProviderIds),
156622
156919
  policyIds: values(form.policyIds),
@@ -158108,7 +158405,7 @@ var import_headless = __toESM(require_xterm_headless(), 1);
158108
158405
  // ../../home/runner/work/void/void/packages/void/package.json
158109
158406
  var package_default = {
158110
158407
  name: "void",
158111
- version: "0.23.0",
158408
+ version: "0.24.0",
158112
158409
  repository: {
158113
158410
  type: "git",
158114
158411
  url: "git+https://github.com/voidzero-dev/void.git",
@@ -158483,13 +158780,12 @@ var package_default = {
158483
158780
  undici: "8.11.2"
158484
158781
  },
158485
158782
  devDependencies: {
158486
- "@clack/core": "^1.5.1",
158487
- "@clack/prompts": "catalog:",
158488
158783
  "@electric-sql/pglite": "0.5.8",
158489
158784
  "@types/better-sqlite3": "^9.6.0",
158490
158785
  "@types/node": "catalog:",
158491
158786
  "@types/pg": "^8.23.1",
158492
158787
  "@types/proper-lockfile": "^4.1.4",
158788
+ "@voidzero-dev/vite-plus-prompts": "workspace:*",
158493
158789
  arktype: "^2.2.5",
158494
158790
  "ci-info": "^4.4.0",
158495
158791
  "estree-walker": "^3.0.3",
@@ -160127,6 +160423,7 @@ var BuildManager = class extends Container {
160127
160423
  SOURCE_TOKEN: sourceToken,
160128
160424
  VOID_TOKEN: voidToken,
160129
160425
  VOID_API_URL: voidApiUrl,
160426
+ ...this.env.PROXY_URL ? { VOID_PROXY_URL: this.env.PROXY_URL } : {},
160130
160427
  VOID_PROJECT: config2.projectSlug,
160131
160428
  VOID_CLI_VERSION: PINNED_VOID_CLI_VERSION,
160132
160429
  DO_LOG_URL: "https://manager/logs",
@@ -162308,7 +162605,8 @@ var platformAccessProtection = /* @__PURE__ */ __name(async (context, next) => {
162308
162605
  const path = context.req.path;
162309
162606
  const method = context.req.method;
162310
162607
  const probe = context.env.PLATFORM_HEALTH_PROBE_TOKEN && context.req.header("X-Void-Platform-Probe") === context.env.PLATFORM_HEALTH_PROBE_TOKEN;
162311
- if (path === "/.well-known/void-platform" && method === "GET" || probe && (path === "/health" && method === "GET" || path === "/internal/platform/sandbox-drain" && method === "GET" || path === "/__void/platform/deployment-execution" && method === "GET" || path === "/internal/platform/email-bootstrap" && method === "POST" || path === "/internal/platform/domain-preflight" && method === "POST" || path === "/internal/platform/domain-activate" && method === "POST")) {
162608
+ if (path === "/.well-known/void-platform" && method === "GET" || probe && (path === "/health" && method === "GET" || path === "/internal/platform/sandbox-drain" && method === "GET" || path === "/__void/platform/deployment-execution" && method === "GET" || path === "/internal/platform/email-bootstrap" && method === "POST" || // The handler also requires its installation-specific setup HMAC.
162609
+ path === "/internal/platform/app-access" && method === "POST" || path === "/internal/platform/domain-preflight" && method === "POST" || path === "/internal/platform/domain-activate" && method === "POST")) {
162312
162610
  return next();
162313
162611
  }
162314
162612
  if (path === "/auth/github-oidc" && method === "POST") {
@@ -164356,6 +164654,185 @@ operatorApi.notFound(
164356
164654
  (context) => context.json({ error: "not_found", message: "Unknown operator API endpoint." }, 404)
164357
164655
  );
164358
164656
 
164657
+ // ../../home/runner/work/void/void/platform/packages/api/src/platform-app-access.ts
164658
+ init_performance2();
164659
+ init_dist();
164660
+ init_drizzle_orm();
164661
+ init_platform_settings();
164662
+ init_cloudflare();
164663
+ init_configuration2();
164664
+ init_errors4();
164665
+ init_lock();
164666
+ init_status();
164667
+ var SETTING = "appAccess.installation.v1";
164668
+ function sameSelection(current, setup) {
164669
+ return current.accountId === setup.accountId && !!current.publicAccess === (setup.mode === "public") && current.protectNewProjects === (setup.mode === "protected") && JSON.stringify(current.identityProviderIds) === JSON.stringify(setup.identityProviderIds) && JSON.stringify(current.policyIds) === JSON.stringify(setup.policyIds);
164670
+ }
164671
+ __name(sameSelection, "sameSelection");
164672
+ async function initializePlatformAppAccess(env2, db, setup) {
164673
+ if (!env2.PLATFORM_APP_ACCESS || !env2.PLATFORM_INSTALLATION_ID || setup.accountId !== env2.CF_ACCOUNT_ID || setup.mode !== (env2.PLATFORM_APP_ACCESS === "public" ? "public" : "protected")) {
164674
+ throw new Error("App access does not match the installation\u2019s account or saved choice.");
164675
+ }
164676
+ const { apiToken: _token, ...identity } = setup;
164677
+ const digest2 = new Uint8Array(
164678
+ await crypto.subtle.digest("SHA-256", new TextEncoder().encode(JSON.stringify(identity)))
164679
+ );
164680
+ const requestHash = Array.from(digest2, (byte) => byte.toString(16).padStart(2, "0")).join("");
164681
+ const claim2 = await db.transaction(async (tx) => {
164682
+ await lockZeroTrustConfiguration(tx, "exclusive");
164683
+ const value = await getPlatformSetting(tx, SETTING);
164684
+ const receipt = value;
164685
+ if (receipt && (receipt.version !== 1 || typeof receipt.requestHash !== "string" || typeof receipt.complete !== "boolean")) {
164686
+ throw new Error("The saved app access setup receipt is invalid.");
164687
+ }
164688
+ if (receipt?.complete) {
164689
+ return { complete: true };
164690
+ }
164691
+ if (receipt && receipt.requestHash !== requestHash) {
164692
+ throw new Error("Resume with the saved app access selections.");
164693
+ }
164694
+ const result = await tx.execute(
164695
+ sql`SELECT installation_id, lifecycle_manifest FROM __void_platform_installation WHERE singleton = true FOR SHARE`
164696
+ );
164697
+ const row = result.rows[0];
164698
+ if (!row || row.installation_id !== env2.PLATFORM_INSTALLATION_ID || row.lifecycle_manifest?.id !== env2.PLATFORM_INSTALLATION_ID || row.lifecycle_manifest?.deployment?.appAccess !== env2.PLATFORM_APP_ACCESS || !["provisioning", "deploying", "verifying"].includes(row.lifecycle_manifest?.status ?? "")) {
164699
+ throw new Error(
164700
+ "App access initialization is only available during the recorded installation setup."
164701
+ );
164702
+ }
164703
+ const current2 = await readZeroTrustConfigurationStrict(tx);
164704
+ if (!receipt && current2) {
164705
+ if (current2.operation || current2.lastError) {
164706
+ throw new Error("Reconcile the existing app access settings before resuming installation.");
164707
+ }
164708
+ await setPlatformSetting(tx, SETTING, {
164709
+ version: 1,
164710
+ requestHash,
164711
+ complete: true
164712
+ });
164713
+ return { complete: true };
164714
+ }
164715
+ if (current2 && !sameSelection(current2, setup)) {
164716
+ throw new Error(
164717
+ "App access was changed outside installation. Inspect Admin \u2192 Zero Trust before resuming."
164718
+ );
164719
+ }
164720
+ await setPlatformSetting(tx, SETTING, {
164721
+ version: 1,
164722
+ requestHash,
164723
+ complete: !setup.managed
164724
+ });
164725
+ return { complete: !setup.managed };
164726
+ });
164727
+ if (claim2.complete) {
164728
+ return { ready: true };
164729
+ }
164730
+ const current = await readZeroTrustConfigurationStrict(db);
164731
+ if (current?.operation?.kind === "disable") {
164732
+ throw new Error(
164733
+ "An administrator is disabling app access. Finish that operation before resuming installation."
164734
+ );
164735
+ }
164736
+ const existingProjects = current?.operation ? current.operation.existingProjects ?? void 0 : current?.enabled ? void 0 : setup.mode === "protected" ? "protected" : "public";
164737
+ await configurePlatformZeroTrust(env2, db, {
164738
+ apiToken: setup.apiToken,
164739
+ identityProviderIds: setup.identityProviderIds,
164740
+ policyIds: setup.policyIds,
164741
+ protectNewProjects: setup.mode === "protected",
164742
+ ...existingProjects ? { existingProjects } : {},
164743
+ ...setup.mode === "public" ? { publicAccess: true } : {},
164744
+ managePublicCustomDomains: true
164745
+ });
164746
+ const status2 = await zeroTrustStatus(env2, db, { check: true });
164747
+ if (!status2.enabled || status2.status !== "ready" || !status2.live?.present || status2.live.drifted || status2.projects.some((project2) => project2.state === "error" || project2.state === "reconciling")) {
164748
+ return { ready: false };
164749
+ }
164750
+ await db.transaction(async (tx) => {
164751
+ await lockZeroTrustConfiguration(tx, "exclusive");
164752
+ const receipt = await getPlatformSetting(tx, SETTING);
164753
+ const current2 = await readZeroTrustConfigurationStrict(tx);
164754
+ if (receipt?.requestHash !== requestHash || !current2 || !sameSelection(current2, setup) || current2.operation || current2.lastError) {
164755
+ throw new Error("App access changed while setup was being verified. Resume installation.");
164756
+ }
164757
+ await setPlatformSetting(tx, SETTING, { ...receipt, complete: true });
164758
+ });
164759
+ return { ready: true };
164760
+ }
164761
+ __name(initializePlatformAppAccess, "initializePlatformAppAccess");
164762
+ async function platformAppAccess(context) {
164763
+ const token = context.env.PLATFORM_HEALTH_PROBE_TOKEN;
164764
+ if (!token || context.req.header("X-Void-Platform-Probe") !== token || !context.env.PLATFORM_INSTALLATION_ID || !context.env.PLATFORM_APP_ACCESS) {
164765
+ return context.json({ error: "not found" }, 404);
164766
+ }
164767
+ const setupKey = context.env.PLATFORM_APP_ACCESS_SETUP_TOKEN;
164768
+ const signature = context.req.header("X-Void-App-Access-Setup") ?? "";
164769
+ if (!setupKey || setupKey.length < 32 || !/^[A-Za-z0-9_-]{43}$/.test(signature)) {
164770
+ return context.json({ error: "not found" }, 404);
164771
+ }
164772
+ const body = await context.req.text();
164773
+ const key = await crypto.subtle.importKey(
164774
+ "raw",
164775
+ new TextEncoder().encode(setupKey),
164776
+ { name: "HMAC", hash: "SHA-256" },
164777
+ false,
164778
+ ["verify"]
164779
+ );
164780
+ const proof = Uint8Array.from(
164781
+ atob(signature.replaceAll("-", "+").replaceAll("_", "/")),
164782
+ (byte) => byte.charCodeAt(0)
164783
+ );
164784
+ if (!await crypto.subtle.verify(
164785
+ "HMAC",
164786
+ key,
164787
+ proof,
164788
+ new TextEncoder().encode(
164789
+ `${PLATFORM_APP_ACCESS_SETUP_PROOF_PREFIX}
164790
+ ${context.env.PLATFORM_INSTALLATION_ID}
164791
+ ${body}`
164792
+ )
164793
+ )) {
164794
+ return context.json({ error: "not found" }, 404);
164795
+ }
164796
+ let setup;
164797
+ try {
164798
+ setup = parsePlatformAppAccessSetup(JSON.parse(body));
164799
+ } catch {
164800
+ return context.json({ error: "Invalid app access setup." }, 400);
164801
+ }
164802
+ try {
164803
+ const receipt = await getPlatformSetting(context.get("db"), SETTING);
164804
+ if (!receipt?.complete && context.env.PLATFORM_TRAFFIC_STATE !== "staging" && await context.env.ROUTING.get(PLATFORM_DISABLED_KEY) !== PLATFORM_DISABLED_VALUE) {
164805
+ return context.json({ error: "Close app traffic before initializing app access." }, 409);
164806
+ }
164807
+ const result = await initializePlatformAppAccess(context.env, context.get("db"), setup);
164808
+ return context.json(
164809
+ { installationId: context.env.PLATFORM_INSTALLATION_ID, ...result },
164810
+ result.ready ? 200 : 202
164811
+ );
164812
+ } catch (error63) {
164813
+ console.error("[platform] app access setup failed", error63);
164814
+ if (error63 instanceof ZeroTrustCloudflareError && [401, 403].includes(error63.status)) {
164815
+ return context.json(
164816
+ {
164817
+ error: "The app access token needs Access Apps and Policies Edit plus Organizations, Identity Providers, and Groups Read in the hosting account.",
164818
+ code: "app_access_credentials"
164819
+ },
164820
+ 403
164821
+ );
164822
+ }
164823
+ if (error63 instanceof ZeroTrustUnavailableError) {
164824
+ return context.json({ error: error63.message }, 409);
164825
+ }
164826
+ return context.json(
164827
+ {
164828
+ error: "App access is not ready. Check the Access token, identity providers, and policy selections, then resume."
164829
+ },
164830
+ 503
164831
+ );
164832
+ }
164833
+ }
164834
+ __name(platformAppAccess, "platformAppAccess");
164835
+
164359
164836
  // ../../home/runner/work/void/void/platform/packages/api/src/platform-domain-activation.ts
164360
164837
  init_performance2();
164361
164838
  init_configuration2();
@@ -164869,7 +165346,7 @@ async function tryActivateDomain(env2, db, domain2, project2, options) {
164869
165346
  let slugRouting = null;
164870
165347
  let activated = false;
164871
165348
  if (gateOk && wasPending) {
164872
- await reconcileProjectZeroTrustDomains(env2, db, project2.id);
165349
+ await reconcileProjectZeroTrustDomains(env2, db, project2.id, { verifiedDomainId: domain2.id });
164873
165350
  await db.transaction(async (tx) => {
164874
165351
  await tx.execute(sql`SELECT id FROM projects WHERE id = ${project2.id} FOR UPDATE`);
164875
165352
  const txDb = tx;
@@ -174689,7 +175166,7 @@ var app36 = new Hono3().use("*", async (context, next) => {
174689
175166
  return context.json({ error: "Internal Server Error" }, 500);
174690
175167
  }).use("*", async (context, next) => {
174691
175168
  const healthProbeToken = context.env.PLATFORM_HEALTH_PROBE_TOKEN;
174692
- const installerProbe = healthProbeToken !== void 0 && healthProbeToken !== "" && context.req.header("X-Void-Platform-Probe") === healthProbeToken && (context.req.path === "/health" || context.req.path === "/.well-known/void-platform" || context.req.path === "/internal/platform/sandbox-drain" || context.req.path === "/__void/platform/deployment-execution" || context.req.path === "/internal/platform/email-bootstrap" && context.req.method === "POST" || context.req.path === "/internal/platform/domain-preflight" && context.req.method === "POST" || context.req.path === "/internal/platform/domain-activate" && context.req.method === "POST");
175169
+ const installerProbe = healthProbeToken !== void 0 && healthProbeToken !== "" && context.req.header("X-Void-Platform-Probe") === healthProbeToken && (context.req.path === "/health" || context.req.path === "/.well-known/void-platform" || context.req.path === "/internal/platform/sandbox-drain" || context.req.path === "/__void/platform/deployment-execution" || context.req.path === "/internal/platform/email-bootstrap" && context.req.method === "POST" || context.req.path === "/internal/platform/app-access" && context.req.method === "POST" || context.req.path === "/internal/platform/domain-preflight" && context.req.method === "POST" || context.req.path === "/internal/platform/domain-activate" && context.req.method === "POST");
174693
175170
  const maintenanceSandboxDrain = context.env.PLATFORM_TRAFFIC_STATE === "sandbox-drain" && isSandboxDrainMaintenanceRequest(context.req.raw);
174694
175171
  const authenticationSetup = context.env.PLATFORM_AUTH_SETUP_TOKEN && context.req.header("X-Void-Auth-Setup") && context.req.method === "POST" && context.req.path === "/internal/platform/auth-initialize";
174695
175172
  if (!authenticationSetup && !installerProbe && !maintenanceSandboxDrain && (context.env.PLATFORM_TRAFFIC_STATE === "staging" || context.env.PLATFORM_TRAFFIC_STATE === "sandbox-drain" || context.env.PLATFORM_INSTALLATION_ID && await context.env.ROUTING.get(PLATFORM_DISABLED_KEY) === PLATFORM_DISABLED_VALUE)) {
@@ -174764,7 +175241,7 @@ var app36 = new Hono3().use("*", async (context, next) => {
174764
175241
  usageProtocolVersion: USAGE_PROTOCOL_VERSION
174765
175242
  })
174766
175243
  );
174767
- }).post("/internal/platform/domain-preflight", platformDomainPreflight).post("/internal/platform/domain-activate", platformDomainActivation).post("/internal/platform/email-bootstrap", async (context) => {
175244
+ }).post("/internal/platform/app-access", platformAppAccess).post("/internal/platform/domain-preflight", platformDomainPreflight).post("/internal/platform/domain-activate", platformDomainActivation).post("/internal/platform/email-bootstrap", async (context) => {
174768
175245
  const healthProbeToken = context.env.PLATFORM_HEALTH_PROBE_TOKEN;
174769
175246
  if (!healthProbeToken || context.req.header("X-Void-Platform-Probe") !== healthProbeToken || !context.env.PLATFORM_INSTALLATION_ID || !getPlatformCapabilities(context.env).email) {
174770
175247
  return context.json({ error: "not found" }, 404);