@a2a-exposed/control-core 0.7.33 → 0.8.1

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.
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /* @a2a-exposed/control-core */
2
2
 
3
- // src/auth/origin.ts
3
+ // src/tenants/approval.ts
4
4
  function httpsOrigin(value) {
5
5
  const raw = (value || "").trim();
6
6
  if (!raw) return "";
@@ -12,10 +12,6 @@ function httpsOrigin(value) {
12
12
  return "";
13
13
  }
14
14
  }
15
-
16
- // src/oidc/issuer.ts
17
- var CODE_TTL_MS = 10 * 60 * 1e3;
18
- var COOKIE_TTL_S = 10 * 60;
19
15
  function bytes(size) {
20
16
  const out = new Uint8Array(size);
21
17
  crypto.getRandomValues(out);
@@ -775,160 +771,537 @@ var routes = {
775
771
  tenants: "/api/v1/tenants"
776
772
  };
777
773
 
778
- // src/provision/cloudflare-api.ts
779
- var API = "https://api.cloudflare.com/client/v4";
780
- var SECRET_PARAM = /\b(?:id_token|access_token|refresh_token|client_secret|authorization|code|token)\s*(?:=|%3d|:)\s*\S/i;
781
- var JWT_SHAPED = /\beyJ[A-Za-z0-9_-]*\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/;
782
- function sanitizeCloudflareText(value) {
783
- const cleaned = value.replace(/[\u0000-\u001f]/g, " ").replace(/\s+/g, " ").trim().slice(0, 300);
784
- if (!cleaned) return "";
785
- if (SECRET_PARAM.test(cleaned) || /bearer\s+\S/i.test(cleaned) || JWT_SHAPED.test(cleaned)) return "redacted";
786
- return cleaned;
774
+ // src/provision/grant.ts
775
+ var TOKEN = "https://dash.cloudflare.com/oauth2/token";
776
+ var INFO2 = "cloudflare-refresh v1";
777
+ var SALT2 = "a2a-exposed";
778
+ var MAX_REFRESH = 4096;
779
+ var LOCK_MS = 15e3;
780
+ function b642(bytes2) {
781
+ let raw = "";
782
+ for (let i = 0; i < bytes2.length; i += 32768) raw += String.fromCharCode(...bytes2.subarray(i, i + 32768));
783
+ return btoa(raw);
787
784
  }
788
- function headerSafeToken(token) {
789
- return token.replace(/[\r\n\u0000]/g, "").trim();
785
+ function unb642(value) {
786
+ return Uint8Array.from(atob(value), (char) => char.charCodeAt(0));
790
787
  }
791
- function numericCode(value) {
792
- return typeof value === "number" && Number.isFinite(value) ? value : null;
788
+ async function derive2(secret, userId) {
789
+ if (secret.length < 32 || !userId) return null;
790
+ const base = await crypto.subtle.importKey("raw", new TextEncoder().encode(secret), "HKDF", false, ["deriveKey"]);
791
+ return crypto.subtle.deriveKey(
792
+ { name: "HKDF", hash: "SHA-256", salt: new TextEncoder().encode(SALT2), info: new TextEncoder().encode(`${INFO2}\0${userId}`) },
793
+ base,
794
+ { name: "AES-GCM", length: 256 },
795
+ false,
796
+ ["encrypt", "decrypt"]
797
+ );
793
798
  }
794
- function errorText(item) {
795
- const record = item;
796
- const parts = [];
797
- if (typeof record.message === "string") parts.push(record.message);
798
- if (Array.isArray(record.error_chain)) {
799
- for (const link of record.error_chain) {
800
- if (!link || typeof link !== "object") continue;
801
- const message = link.message;
802
- if (typeof message === "string") parts.push(message);
803
- }
804
- }
805
- return sanitizeCloudflareText(parts.join(" \u2014 "));
799
+ async function sealCloudflareRefresh(secret, userId, token) {
800
+ if (!token || token.length > MAX_REFRESH) return null;
801
+ const key = await derive2(secret, userId);
802
+ if (!key) return null;
803
+ const iv = crypto.getRandomValues(new Uint8Array(12));
804
+ const ct = new Uint8Array(await crypto.subtle.encrypt(
805
+ { name: "AES-GCM", iv, additionalData: new TextEncoder().encode(userId) },
806
+ key,
807
+ new TextEncoder().encode(token)
808
+ ));
809
+ return `v1.${b642(iv)}.${b642(ct)}`;
806
810
  }
807
- function cloudflareErrors(body) {
808
- if (!body || typeof body !== "object") return [];
809
- const errors = body.errors;
810
- if (!Array.isArray(errors)) return [];
811
- const out = [];
812
- for (const item of errors.slice(0, 8)) {
813
- if (!item || typeof item !== "object") continue;
814
- out.push({ code: numericCode(item.code), message: errorText(item) });
811
+ async function openCloudflareRefresh(secret, userId, sealed) {
812
+ if (!sealed) return null;
813
+ const key = await derive2(secret, userId);
814
+ if (!key) return null;
815
+ const [version, iv, ct, extra] = sealed.split(".");
816
+ if (version !== "v1" || !iv || !ct || extra !== void 0) return null;
817
+ try {
818
+ const plain = new TextDecoder().decode(await crypto.subtle.decrypt(
819
+ { name: "AES-GCM", iv: unb642(iv), additionalData: new TextEncoder().encode(userId) },
820
+ key,
821
+ unb642(ct)
822
+ ));
823
+ if (!plain || plain.length > MAX_REFRESH) return null;
824
+ return plain;
825
+ } catch {
826
+ return null;
815
827
  }
816
- return out;
817
828
  }
818
- function failureFrom(status, body, thrown) {
819
- const errors = cloudflareErrors(body);
820
- return { ok: false, status, code: errors.find((error) => error.code != null)?.code ?? null, errors, ...thrown ? { thrown } : {} };
829
+ function expiresAt(expiresIn, now) {
830
+ const seconds = typeof expiresIn === "number" ? expiresIn : typeof expiresIn === "string" ? Number(expiresIn) : NaN;
831
+ if (!Number.isFinite(seconds) || seconds <= 0 || seconds > 365 * 24 * 60 * 60) return null;
832
+ return new Date(now + seconds * 1e3).toISOString();
821
833
  }
822
- function thrownFailure(error) {
823
- const name = error instanceof Error && error.name ? error.name.slice(0, 80) : "Error";
824
- const message = sanitizeCloudflareText(error instanceof Error ? error.message : "");
825
- return { ok: false, status: 0, code: null, errors: [], thrown: { name, message } };
834
+ async function saveCloudflareGrant(db, secret, input) {
835
+ const now = input.now ?? Date.now();
836
+ const stamp = new Date(now).toISOString();
837
+ const refresh = input.refreshToken ? await sealCloudflareRefresh(secret, input.userId, input.refreshToken) : null;
838
+ await db.prepare(
839
+ `INSERT INTO cloudflare_grant ("userId", "accountId", choice, host, requested_scope, granted_scope, refresh_enc, access_expires_at, created_at, updated_at)
840
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
841
+ ON CONFLICT ("userId") DO UPDATE SET
842
+ "accountId" = excluded."accountId",
843
+ choice = excluded.choice,
844
+ host = excluded.host,
845
+ requested_scope = excluded.requested_scope,
846
+ granted_scope = excluded.granted_scope,
847
+ refresh_enc = excluded.refresh_enc,
848
+ access_expires_at = excluded.access_expires_at,
849
+ updated_at = excluded.updated_at`
850
+ ).bind(
851
+ input.userId,
852
+ input.accountId,
853
+ input.choice,
854
+ input.host,
855
+ input.requestedScope,
856
+ input.grantedScope,
857
+ refresh,
858
+ expiresAt(input.expiresIn, now),
859
+ stamp,
860
+ stamp
861
+ ).run();
826
862
  }
827
- var PERMISSION_CODES = /* @__PURE__ */ new Set([1e4, 7403]);
828
- var NO_ACCESS = "no access to the specified resource";
829
- function permissionNotReady(failure) {
830
- if (failure.code != null && PERMISSION_CODES.has(failure.code)) return true;
831
- return failure.errors.some((error) => error.code != null && PERMISSION_CODES.has(error.code) || error.message.toLowerCase().includes(NO_ACCESS));
863
+ async function readCloudflareGrant(db, userId) {
864
+ if (!userId) return null;
865
+ return db.prepare(
866
+ `SELECT "userId" AS userId, "accountId" AS accountId, choice, host, requested_scope, granted_scope, refresh_enc, access_expires_at
867
+ FROM cloudflare_grant WHERE "userId" = ?`
868
+ ).bind(userId).first();
832
869
  }
833
- function subdomainMissing(failure) {
834
- if (failure.status === 404) return true;
835
- return failure.errors.some((error) => error.code === 10063 || error.code === 10007 || /workers\.dev/i.test(error.message) && /subdomain|register|not found|does not have/i.test(error.message));
870
+ async function deleteCloudflareGrant(db, userId) {
871
+ if (!userId) return;
872
+ await db.prepare(`DELETE FROM cloudflare_grant WHERE "userId" = ?`).bind(userId).run();
836
873
  }
837
- function describeCfFailure(failure) {
838
- if (failure.thrown) {
839
- const detail = failure.thrown.message && failure.thrown.message !== "redacted" ? `${failure.thrown.name}: ${failure.thrown.message}` : failure.thrown.name;
840
- return `The request stops before Cloudflare answers (${detail}).`;
841
- }
842
- const parts = [];
843
- for (const error of failure.errors) {
844
- if (error.message === "redacted") {
845
- parts.push(error.code != null ? `code ${error.code}. Cloudflare's message is hidden because it contains a secret.` : "Cloudflare's message is hidden because it contains a secret.");
846
- } else if (error.code != null && error.message) parts.push(`code ${error.code}: ${error.message}`);
847
- else if (error.code != null) parts.push(`code ${error.code}`);
848
- else if (error.message) parts.push(error.message);
849
- }
850
- if (parts.length) return `Cloudflare says ${parts.join("; ")}.`;
851
- if (failure.status) return `Cloudflare answers HTTP ${failure.status}.`;
852
- return "The request stops before Cloudflare answers.";
874
+ function randomId() {
875
+ const buf = new Uint8Array(16);
876
+ crypto.getRandomValues(buf);
877
+ return [...buf].map((byte) => byte.toString(16).padStart(2, "0")).join("");
853
878
  }
854
- function callFetch(fetchImpl, url, init) {
855
- return fetchImpl.call(globalThis, url, init);
879
+ async function claimLock(db, userId, holder, now) {
880
+ const result = await db.prepare(
881
+ `INSERT INTO control_locks (name, holder, expires_ms) VALUES (?, ?, ?)
882
+ ON CONFLICT(name) DO UPDATE SET holder = excluded.holder, expires_ms = excluded.expires_ms
883
+ WHERE control_locks.expires_ms <= ?`
884
+ ).bind(`cf-refresh:${userId}`, holder, now + LOCK_MS, now).run();
885
+ return result.meta?.changes === 1;
856
886
  }
857
- async function cfCall(input) {
858
- const method = input.method.toUpperCase();
859
- let body;
860
- const headers = new Headers({ accept: "application/json" });
861
- try {
862
- headers.set("authorization", `Bearer ${headerSafeToken(input.token)}`);
863
- } catch (error) {
864
- return thrownFailure(error);
865
- }
866
- if (input.raw) {
867
- headers.set("content-type", input.raw.contentType);
868
- body = input.raw.body;
869
- } else if (input.json !== void 0) {
870
- headers.set("content-type", "application/json");
871
- body = JSON.stringify(input.json);
872
- }
873
- const init = {
874
- method,
875
- headers,
876
- redirect: "manual",
877
- signal: AbortSignal.timeout(input.fetchMs)
887
+ async function releaseLock(db, userId, holder) {
888
+ await db.prepare(
889
+ `UPDATE control_locks SET holder = '', expires_ms = 0 WHERE name = ? AND holder = ?`
890
+ ).bind(`cf-refresh:${userId}`, holder).run();
891
+ }
892
+ async function clearRefresh(db, userId, now) {
893
+ await db.prepare(
894
+ `UPDATE cloudflare_grant SET refresh_enc = NULL, updated_at = ? WHERE "userId" = ?`
895
+ ).bind(new Date(now).toISOString(), userId).run();
896
+ }
897
+ async function postRefresh(body, fetchMs, fetchImpl) {
898
+ const timeout = AbortSignal.timeout(fetchMs);
899
+ let rejectDeadline = () => {
878
900
  };
879
- if (body !== void 0 && method !== "GET" && method !== "HEAD") init.body = body;
880
- let response;
901
+ const deadline = new Promise((_, reject) => {
902
+ rejectDeadline = reject;
903
+ });
904
+ const timer = setTimeout(() => rejectDeadline(new DOMException("The operation was aborted", "TimeoutError")), fetchMs);
881
905
  try {
882
- response = await callFetch(input.fetchImpl, `${API}${input.path}`, init);
883
- } catch (error) {
884
- return thrownFailure(error);
885
- }
886
- if (response.status >= 300 && response.status < 400) {
887
- void response.body?.cancel().catch(() => {
888
- });
889
- return failureFrom(response.status, null);
890
- }
891
- const text = await response.text().catch(() => "");
892
- let parsed = null;
893
- if (text) {
906
+ const request = fetchImpl(TOKEN, {
907
+ method: "POST",
908
+ headers: { "content-type": "application/x-www-form-urlencoded", accept: "application/json" },
909
+ body,
910
+ signal: timeout
911
+ }).then(
912
+ (response) => ({ response, failed: false }),
913
+ () => ({ response: null, failed: true })
914
+ );
915
+ let winner;
894
916
  try {
895
- parsed = JSON.parse(text);
917
+ winner = await Promise.race([request, deadline]);
896
918
  } catch {
897
- parsed = null;
919
+ return { kind: "deadline" };
920
+ }
921
+ if (winner.failed || !winner.response) return timeout.aborted ? { kind: "deadline" } : { kind: "failed" };
922
+ if (!winner.response.ok) {
923
+ const status = winner.response.status;
924
+ void winner.response.body?.cancel().catch(() => {
925
+ });
926
+ return status === 400 || status === 401 ? { kind: "rejected", status } : { kind: "failed" };
927
+ }
928
+ try {
929
+ const parsed = await Promise.race([
930
+ winner.response.json().then((body2) => ({ body: body2, failed: false }), () => ({ body: void 0, failed: true })),
931
+ deadline
932
+ ]);
933
+ if (parsed.failed) return timeout.aborted ? { kind: "deadline" } : { kind: "failed" };
934
+ return { kind: "ok", body: parsed.body };
935
+ } catch {
936
+ return { kind: "deadline" };
898
937
  }
938
+ } finally {
939
+ clearTimeout(timer);
899
940
  }
900
- const success = parsed && typeof parsed === "object" ? parsed.success : void 0;
901
- if (!response.ok || success === false) return failureFrom(response.status, parsed);
902
- return { ok: true, status: response.status, body: parsed };
903
- }
904
- function resultObject(body) {
905
- if (!body || typeof body !== "object") return null;
906
- const result = body.result;
907
- return result && typeof result === "object" && !Array.isArray(result) ? result : null;
908
- }
909
- async function listWorkerNames(input) {
910
- const read = await cfCall({
911
- fetchImpl: input.fetchImpl,
912
- token: input.token,
913
- method: "GET",
914
- path: `/accounts/${input.accountId}/workers/scripts?per_page=100`,
915
- fetchMs: input.fetchMs
916
- });
917
- if (!read.ok) return { ok: false };
918
- const names = resultList(read.body).map((row) => typeof row.id === "string" ? row.id : "").filter((name) => name.length > 0 && name.length <= 64);
919
- const info = read.body && typeof read.body === "object" ? read.body.result_info : void 0;
920
- const total = typeof info?.total_count === "number" ? info.total_count : names.length;
921
- if (total > names.length) return { ok: false };
922
- return { ok: true, names };
923
941
  }
924
- function resultList(body) {
925
- if (!body || typeof body !== "object") return [];
926
- const result = body.result;
927
- if (!Array.isArray(result)) return [];
928
- return result.filter((row) => Boolean(row) && typeof row === "object");
929
- }
930
- function d1Rows(body) {
931
- const list = resultList(body);
942
+ async function refreshCloudflareGrant(input) {
943
+ const now = input.now ?? Date.now();
944
+ const row = await readCloudflareGrant(input.database, input.userId);
945
+ if (!row) return { ok: false, error: "no_grant" };
946
+ if (!row.refresh_enc) return { ok: false, error: "no_refresh" };
947
+ const holder = randomId();
948
+ const claimed = await claimLock(input.database, input.userId, holder, now);
949
+ if (!claimed) return { ok: false, error: "busy" };
950
+ try {
951
+ const current = await readCloudflareGrant(input.database, input.userId);
952
+ const token = await openCloudflareRefresh(input.secret, input.userId, current?.refresh_enc);
953
+ if (!token) return { ok: false, error: "no_refresh" };
954
+ const read = await postRefresh(new URLSearchParams({
955
+ grant_type: "refresh_token",
956
+ refresh_token: token,
957
+ client_id: input.clientId,
958
+ client_secret: input.clientSecret
959
+ }), input.fetchMs ?? 5e3, input.fetchImpl ?? fetch);
960
+ if (read.kind === "deadline") return { ok: false, error: "uncertain" };
961
+ if (read.kind === "rejected") {
962
+ await clearRefresh(input.database, input.userId, now);
963
+ return { ok: false, error: "invalid_grant" };
964
+ }
965
+ if (read.kind === "failed") return { ok: false, error: "failed" };
966
+ const body = read.body && typeof read.body === "object" ? read.body : null;
967
+ const accessToken = typeof body?.access_token === "string" ? body.access_token : "";
968
+ if (!accessToken) return { ok: false, error: "failed" };
969
+ const next = typeof body?.refresh_token === "string" ? body.refresh_token : "";
970
+ const sealed = next ? await sealCloudflareRefresh(input.secret, input.userId, next) : null;
971
+ if (next && !sealed) {
972
+ await clearRefresh(input.database, input.userId, now);
973
+ return { ok: false, error: "unreadable" };
974
+ }
975
+ const scope = typeof body?.scope === "string" ? body.scope : current?.requested_scope ?? "";
976
+ await input.database.prepare(
977
+ `UPDATE cloudflare_grant SET refresh_enc = ?, granted_scope = COALESCE(?, granted_scope), access_expires_at = ?, updated_at = ? WHERE "userId" = ?`
978
+ ).bind(
979
+ sealed,
980
+ typeof body?.scope === "string" ? body.scope : null,
981
+ expiresAt(body?.expires_in, now),
982
+ new Date(now).toISOString(),
983
+ input.userId
984
+ ).run();
985
+ return { ok: true, accessToken, scope };
986
+ } finally {
987
+ await releaseLock(input.database, input.userId, holder).catch(() => {
988
+ });
989
+ }
990
+ }
991
+
992
+ // src/provision/setup.ts
993
+ var USER_DETAILS_READ = "user-details.read";
994
+ var ACCOUNT_SETTINGS_READ = "account-settings.read";
995
+ var WORKERS_SCRIPTS_ADMIN = "workers-scripts.admin";
996
+ var D1_WRITE = "d1.write";
997
+ var WORKERS_ROUTES_WRITE = "workers-routes.write";
998
+ var DNS_WRITE = "dns.write";
999
+ var ZONE_READ = "zone.read";
1000
+ var TUNNEL_WRITE = "argotunnel.write";
1001
+ var OFFLINE_ACCESS = "offline_access";
1002
+ var SETUP_NOT_PROVISIONED = "Nothing has been created in your Cloudflare account yet.";
1003
+ var TUNNEL_CONNECTOR_LATER = "This page stores the fa\xE7ade choice. Tunnel creation is not implemented on this page.";
1004
+ var EVERY_PLAN = "This is available on every plan.";
1005
+ var AUTHORIZE = "https://dash.cloudflare.com/oauth2/auth";
1006
+ function parseSetupChoice(value) {
1007
+ return value === "inbox" || value === "facade" || value === "both" ? value : null;
1008
+ }
1009
+ function parseSetupHost(value) {
1010
+ return value === "workers" || value === "zone" ? value : null;
1011
+ }
1012
+ function includesInbox(choice) {
1013
+ return choice === "inbox" || choice === "both";
1014
+ }
1015
+ function includesFacade(choice) {
1016
+ return choice === "facade" || choice === "both";
1017
+ }
1018
+ function variant(choice) {
1019
+ if (choice === "inbox") return "a new inbox";
1020
+ if (choice === "facade") return "the fa\xE7ade";
1021
+ return "the new inbox and the fa\xE7ade";
1022
+ }
1023
+ function setupSummary(choice, host) {
1024
+ const what = choice === "inbox" ? "give your agent a public A2A address" : choice === "facade" ? "make an agent you already run reachable by other agents" : "give your agent a public A2A address and make an agent you already run reachable";
1025
+ const where = host === "workers" ? "a free Cloudflare address such as my-agent.yourname.workers.dev" : "your own domain, such as agent.example.com";
1026
+ return `You chose to ${what}, on ${where}.`;
1027
+ }
1028
+ function setupScopes(choice, host) {
1029
+ const inbox = includesInbox(choice);
1030
+ const facade = includesFacade(choice);
1031
+ const zone = host === "zone";
1032
+ const scopes = [
1033
+ {
1034
+ id: USER_DETAILS_READ,
1035
+ name: "User Details Read",
1036
+ why: "Confirms which Cloudflare user this is.",
1037
+ declined: "Without User Details Read, Cloudflare did not confirm the user. Choose the setup again."
1038
+ },
1039
+ {
1040
+ id: ACCOUNT_SETTINGS_READ,
1041
+ name: "Account Settings Read",
1042
+ why: "Reads the Cloudflare account you pick on the consent screen.",
1043
+ declined: "Without Account Settings Read, this dashboard cannot see which Cloudflare account you picked, so this setup cannot continue."
1044
+ },
1045
+ {
1046
+ id: WORKERS_SCRIPTS_ADMIN,
1047
+ name: "Workers Admin",
1048
+ why: facade && !inbox ? "Uploads the fa\xE7ade Worker into your account." : facade ? "Uploads the inbox Worker and the fa\xE7ade Worker into your account." : "Uploads the inbox Worker into your account.",
1049
+ declined: `Without Workers Admin, the Worker cannot be uploaded. Connect again to grant it. That blocks ${variant(choice)}.`
1050
+ },
1051
+ {
1052
+ id: D1_WRITE,
1053
+ name: "D1 Write",
1054
+ why: "Creates the D1 database that Worker uses.",
1055
+ declined: `Without D1 Write, the database cannot be created. That blocks ${variant(choice)}.`
1056
+ }
1057
+ ];
1058
+ if (zone) {
1059
+ scopes.push(
1060
+ {
1061
+ id: WORKERS_ROUTES_WRITE,
1062
+ name: "Workers Routes Write",
1063
+ why: "Attaches the Worker to a hostname on a domain you already have in Cloudflare.",
1064
+ declined: "Without Workers Routes Write, the Worker cannot be attached to a hostname on your domain. That blocks a setup on your own domain. A free address is set up with the permissions that address needs."
1065
+ },
1066
+ {
1067
+ id: DNS_WRITE,
1068
+ name: "DNS Write",
1069
+ why: "Creates the DNS record for that hostname.",
1070
+ declined: "Without DNS Write, the DNS record for your domain cannot be created. That blocks a setup on your own domain. A free address is set up with the permissions that address needs."
1071
+ },
1072
+ {
1073
+ id: ZONE_READ,
1074
+ name: "Zone Read",
1075
+ why: "Lists the domains on the Cloudflare account you pick, so this page can check that your address is on one of them.",
1076
+ declined: "Without Zone Read, the domains on your Cloudflare account cannot be listed. That blocks a setup on your own domain. A free address is set up with the permissions that address needs."
1077
+ }
1078
+ );
1079
+ }
1080
+ if (facade) {
1081
+ scopes.push({
1082
+ id: TUNNEL_WRITE,
1083
+ name: "Cloudflare Tunnel Write",
1084
+ why: "Creates a tunnel from the fa\xE7ade Worker to your existing agent. You still run cloudflared on a machine you control.",
1085
+ declined: inbox ? "Without Cloudflare Tunnel Write, a tunnel cannot be created. That blocks the fa\xE7ade. The inbox is created without a tunnel." : "Without Cloudflare Tunnel Write, a tunnel cannot be created. That blocks the fa\xE7ade."
1086
+ });
1087
+ }
1088
+ scopes.push({
1089
+ id: OFFLINE_ACCESS,
1090
+ name: "Refresh token (offline access)",
1091
+ why: "Lets this dashboard continue the setup after you leave this page. The token is stored encrypted. Cloudflare treats it as single-use.",
1092
+ declined: "Connect again and keep the refresh token to continue after you leave this page. Nothing has been created in your Cloudflare account."
1093
+ });
1094
+ return scopes;
1095
+ }
1096
+ function consentScope(choice, host) {
1097
+ return setupScopes(choice, host).map((scope) => scope.id).join(" ");
1098
+ }
1099
+ function cloudflareAuthorizeUrl(input) {
1100
+ const url = new URL(AUTHORIZE);
1101
+ url.searchParams.set("client_id", input.clientId);
1102
+ url.searchParams.set("redirect_uri", input.redirectUri);
1103
+ url.searchParams.set("response_type", "code");
1104
+ url.searchParams.set("scope", input.scope);
1105
+ url.searchParams.set("state", input.state);
1106
+ url.searchParams.set("code_challenge", input.codeChallenge);
1107
+ url.searchParams.set("code_challenge_method", "S256");
1108
+ return url.href;
1109
+ }
1110
+ function grantedScopeSet(scope) {
1111
+ if (scope == null) return { known: false, granted: /* @__PURE__ */ new Set() };
1112
+ const granted = new Set(scope.split(/[\s,]+/).map((part) => part.trim()).filter(Boolean));
1113
+ return { known: true, granted };
1114
+ }
1115
+ function inboxCanProvision(row) {
1116
+ const choice = parseSetupChoice(row.choice);
1117
+ const host = parseSetupHost(row.host);
1118
+ if (!choice || !host) return false;
1119
+ return grantCoversInbox(row, choice, host);
1120
+ }
1121
+ function grantCoversInbox(row, choice, host) {
1122
+ if (!includesInbox(choice) || !row.refresh_enc) return false;
1123
+ const { known, granted } = grantedScopeSet(row.granted_scope);
1124
+ if (!known) return false;
1125
+ return setupScopes(choice, host).every((scope) => scope.id === TUNNEL_WRITE || granted.has(scope.id));
1126
+ }
1127
+ function describeSetupGrant(row) {
1128
+ const choice = parseSetupChoice(row.choice);
1129
+ const host = parseSetupHost(row.host);
1130
+ if (!choice || !host) return null;
1131
+ const requested = setupScopes(choice, host);
1132
+ const { known, granted } = grantedScopeSet(row.granted_scope);
1133
+ const lines = requested.map((scope) => ({
1134
+ name: scope.name,
1135
+ why: scope.why,
1136
+ state: !known ? "unknown" : granted.has(scope.id) ? "granted" : "declined"
1137
+ }));
1138
+ const blocked = !known ? ["Cloudflare did not say which permissions were granted, so this setup is not confirmed."] : requested.filter((scope) => !granted.has(scope.id)).map((scope) => scope.declined);
1139
+ return {
1140
+ summary: setupSummary(choice, host),
1141
+ lines,
1142
+ blocked,
1143
+ refreshSaved: Boolean(row.refresh_enc),
1144
+ facade: includesFacade(choice),
1145
+ inbox: includesInbox(choice),
1146
+ host,
1147
+ needsReconnect: !known || !granted.has(WORKERS_SCRIPTS_ADMIN)
1148
+ };
1149
+ }
1150
+
1151
+ // src/provision/cloudflare-api.ts
1152
+ var API = "https://api.cloudflare.com/client/v4";
1153
+ var SECRET_PARAM = /\b(?:id_token|access_token|refresh_token|client_secret|authorization|code|token)\s*(?:=|%3d|:)\s*\S/i;
1154
+ var JWT_SHAPED = /\beyJ[A-Za-z0-9_-]*\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/;
1155
+ function sanitizeCloudflareText(value) {
1156
+ const cleaned = value.replace(/[\u0000-\u001f]/g, " ").replace(/\s+/g, " ").trim().slice(0, 300);
1157
+ if (!cleaned) return "";
1158
+ if (SECRET_PARAM.test(cleaned) || /bearer\s+\S/i.test(cleaned) || JWT_SHAPED.test(cleaned)) return "redacted";
1159
+ return cleaned;
1160
+ }
1161
+ function headerSafeToken(token) {
1162
+ return token.replace(/[\r\n\u0000]/g, "").trim();
1163
+ }
1164
+ function numericCode(value) {
1165
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
1166
+ }
1167
+ function errorText(item) {
1168
+ const record = item;
1169
+ const parts = [];
1170
+ if (typeof record.message === "string") parts.push(record.message);
1171
+ if (Array.isArray(record.error_chain)) {
1172
+ for (const link of record.error_chain) {
1173
+ if (!link || typeof link !== "object") continue;
1174
+ const message = link.message;
1175
+ if (typeof message === "string") parts.push(message);
1176
+ }
1177
+ }
1178
+ return sanitizeCloudflareText(parts.join(" \u2014 "));
1179
+ }
1180
+ function cloudflareErrors(body) {
1181
+ if (!body || typeof body !== "object") return [];
1182
+ const errors = body.errors;
1183
+ if (!Array.isArray(errors)) return [];
1184
+ const out = [];
1185
+ for (const item of errors.slice(0, 8)) {
1186
+ if (!item || typeof item !== "object") continue;
1187
+ out.push({ code: numericCode(item.code), message: errorText(item) });
1188
+ }
1189
+ return out;
1190
+ }
1191
+ function failureFrom(status, body, thrown) {
1192
+ const errors = cloudflareErrors(body);
1193
+ return { ok: false, status, code: errors.find((error) => error.code != null)?.code ?? null, errors, ...thrown ? { thrown } : {} };
1194
+ }
1195
+ function thrownFailure(error) {
1196
+ const name = error instanceof Error && error.name ? error.name.slice(0, 80) : "Error";
1197
+ const message = sanitizeCloudflareText(error instanceof Error ? error.message : "");
1198
+ return { ok: false, status: 0, code: null, errors: [], thrown: { name, message } };
1199
+ }
1200
+ var PERMISSION_CODES = /* @__PURE__ */ new Set([1e4, 7403]);
1201
+ var NO_ACCESS = "no access to the specified resource";
1202
+ function permissionNotReady(failure) {
1203
+ if (failure.code != null && PERMISSION_CODES.has(failure.code)) return true;
1204
+ return failure.errors.some((error) => error.code != null && PERMISSION_CODES.has(error.code) || error.message.toLowerCase().includes(NO_ACCESS));
1205
+ }
1206
+ function subdomainMissing(failure) {
1207
+ if (failure.status === 404) return true;
1208
+ return failure.errors.some((error) => error.code === 10063 || error.code === 10007 || /workers\.dev/i.test(error.message) && /subdomain|register|not found|does not have/i.test(error.message));
1209
+ }
1210
+ function describeCfFailure(failure) {
1211
+ if (failure.thrown) {
1212
+ const detail = failure.thrown.message && failure.thrown.message !== "redacted" ? `${failure.thrown.name}: ${failure.thrown.message}` : failure.thrown.name;
1213
+ return `The request stops before Cloudflare answers (${detail}).`;
1214
+ }
1215
+ const parts = [];
1216
+ for (const error of failure.errors) {
1217
+ if (error.message === "redacted") {
1218
+ parts.push(error.code != null ? `code ${error.code}. Cloudflare's message is hidden because it contains a secret.` : "Cloudflare's message is hidden because it contains a secret.");
1219
+ } else if (error.code != null && error.message) parts.push(`code ${error.code}: ${error.message}`);
1220
+ else if (error.code != null) parts.push(`code ${error.code}`);
1221
+ else if (error.message) parts.push(error.message);
1222
+ }
1223
+ if (parts.length) return `Cloudflare says ${parts.join("; ")}.`;
1224
+ if (failure.status) return `Cloudflare answers HTTP ${failure.status}.`;
1225
+ return "The request stops before Cloudflare answers.";
1226
+ }
1227
+ function callFetch(fetchImpl, url, init) {
1228
+ return fetchImpl.call(globalThis, url, init);
1229
+ }
1230
+ async function cfCall(input) {
1231
+ const method = input.method.toUpperCase();
1232
+ let body;
1233
+ const headers = new Headers({ accept: "application/json" });
1234
+ try {
1235
+ headers.set("authorization", `Bearer ${headerSafeToken(input.token)}`);
1236
+ } catch (error) {
1237
+ return thrownFailure(error);
1238
+ }
1239
+ if (input.raw) {
1240
+ headers.set("content-type", input.raw.contentType);
1241
+ body = input.raw.body;
1242
+ } else if (input.json !== void 0) {
1243
+ headers.set("content-type", "application/json");
1244
+ body = JSON.stringify(input.json);
1245
+ }
1246
+ const init = {
1247
+ method,
1248
+ headers,
1249
+ redirect: "manual",
1250
+ signal: AbortSignal.timeout(input.fetchMs)
1251
+ };
1252
+ if (body !== void 0 && method !== "GET" && method !== "HEAD") init.body = body;
1253
+ let response;
1254
+ try {
1255
+ response = await callFetch(input.fetchImpl, `${API}${input.path}`, init);
1256
+ } catch (error) {
1257
+ return thrownFailure(error);
1258
+ }
1259
+ if (response.status >= 300 && response.status < 400) {
1260
+ void response.body?.cancel().catch(() => {
1261
+ });
1262
+ return failureFrom(response.status, null);
1263
+ }
1264
+ const text = await response.text().catch(() => "");
1265
+ let parsed = null;
1266
+ if (text) {
1267
+ try {
1268
+ parsed = JSON.parse(text);
1269
+ } catch {
1270
+ parsed = null;
1271
+ }
1272
+ }
1273
+ const success = parsed && typeof parsed === "object" ? parsed.success : void 0;
1274
+ if (!response.ok || success === false) return failureFrom(response.status, parsed);
1275
+ return { ok: true, status: response.status, body: parsed };
1276
+ }
1277
+ function resultObject(body) {
1278
+ if (!body || typeof body !== "object") return null;
1279
+ const result = body.result;
1280
+ return result && typeof result === "object" && !Array.isArray(result) ? result : null;
1281
+ }
1282
+ async function listWorkerNames(input) {
1283
+ const read = await cfCall({
1284
+ fetchImpl: input.fetchImpl,
1285
+ token: input.token,
1286
+ method: "GET",
1287
+ path: `/accounts/${input.accountId}/workers/scripts?per_page=100`,
1288
+ fetchMs: input.fetchMs
1289
+ });
1290
+ if (!read.ok) return { ok: false };
1291
+ const names = resultList(read.body).map((row) => typeof row.id === "string" ? row.id : "").filter((name) => name.length > 0 && name.length <= 64);
1292
+ const info = read.body && typeof read.body === "object" ? read.body.result_info : void 0;
1293
+ const total = typeof info?.total_count === "number" ? info.total_count : names.length;
1294
+ if (total > names.length) return { ok: false };
1295
+ return { ok: true, names };
1296
+ }
1297
+ function resultList(body) {
1298
+ if (!body || typeof body !== "object") return [];
1299
+ const result = body.result;
1300
+ if (!Array.isArray(result)) return [];
1301
+ return result.filter((row) => Boolean(row) && typeof row === "object");
1302
+ }
1303
+ function d1Rows(body) {
1304
+ const list = resultList(body);
932
1305
  const first = list[0];
933
1306
  const rows = first?.results;
934
1307
  if (!Array.isArray(rows)) return [];
@@ -1193,219 +1566,34 @@ async function readInboxById(db, inboxId) {
1193
1566
  }
1194
1567
  async function readCloudflareInboxById(db, userId, inboxId) {
1195
1568
  if (!userId || !inboxId) return null;
1196
- return db.prepare(
1197
- `SELECT ${COLUMNS} FROM cloudflare_inbox WHERE id = ? AND "userId" = ?`
1198
- ).bind(inboxId, userId).first();
1199
- }
1200
- async function readCloudflareInbox(db, userId) {
1201
- const rows = await listCloudflareInboxes(db, userId);
1202
- const open = rows.find((row) => row.status === "pending" || row.status === "working");
1203
- if (open) return open;
1204
- const ready = rows.filter((row) => row.status === "ready");
1205
- return ready.at(-1) ?? null;
1206
- }
1207
- async function deleteCloudflareInbox(db, userId, inboxId) {
1208
- if (!userId || !inboxId) return false;
1209
- const result = await db.prepare(`DELETE FROM cloudflare_inbox WHERE id = ? AND "userId" = ?`).bind(inboxId, userId).run();
1210
- return result.meta?.changes === 1;
1211
- }
1212
- function cloudflareHostname(row) {
1213
- if (row.public_url) {
1214
- try {
1215
- const url = new URL(row.public_url);
1216
- if (url.protocol === "https:" && !url.username && !url.password && url.hostname) return url.hostname;
1217
- } catch {
1218
- }
1219
- }
1220
- if (row.host === "zone" && row.hostname) return row.hostname;
1221
- if (row.worker_name && row.workers_subdomain) return `${row.worker_name}.${row.workers_subdomain}.workers.dev`;
1222
- if (row.worker_name) return row.worker_name;
1223
- return "Setting up";
1224
- }
1225
-
1226
- // src/auth/cloudflare-grant.ts
1227
- var TOKEN = "https://dash.cloudflare.com/oauth2/token";
1228
- var INFO2 = "cloudflare-refresh v1";
1229
- var SALT2 = "a2a-exposed";
1230
- var MAX_REFRESH = 4096;
1231
- var LOCK_MS = 15e3;
1232
- function b642(bytes2) {
1233
- let raw = "";
1234
- for (let i = 0; i < bytes2.length; i += 32768) raw += String.fromCharCode(...bytes2.subarray(i, i + 32768));
1235
- return btoa(raw);
1236
- }
1237
- function unb642(value) {
1238
- return Uint8Array.from(atob(value), (char) => char.charCodeAt(0));
1239
- }
1240
- async function derive2(secret, userId) {
1241
- if (secret.length < 32 || !userId) return null;
1242
- const base = await crypto.subtle.importKey("raw", new TextEncoder().encode(secret), "HKDF", false, ["deriveKey"]);
1243
- return crypto.subtle.deriveKey(
1244
- { name: "HKDF", hash: "SHA-256", salt: new TextEncoder().encode(SALT2), info: new TextEncoder().encode(`${INFO2}\0${userId}`) },
1245
- base,
1246
- { name: "AES-GCM", length: 256 },
1247
- false,
1248
- ["encrypt", "decrypt"]
1249
- );
1250
- }
1251
- async function sealCloudflareRefresh(secret, userId, token) {
1252
- if (!token || token.length > MAX_REFRESH) return null;
1253
- const key = await derive2(secret, userId);
1254
- if (!key) return null;
1255
- const iv = crypto.getRandomValues(new Uint8Array(12));
1256
- const ct = new Uint8Array(await crypto.subtle.encrypt(
1257
- { name: "AES-GCM", iv, additionalData: new TextEncoder().encode(userId) },
1258
- key,
1259
- new TextEncoder().encode(token)
1260
- ));
1261
- return `v1.${b642(iv)}.${b642(ct)}`;
1262
- }
1263
- async function openCloudflareRefresh(secret, userId, sealed) {
1264
- if (!sealed) return null;
1265
- const key = await derive2(secret, userId);
1266
- if (!key) return null;
1267
- const [version, iv, ct, extra] = sealed.split(".");
1268
- if (version !== "v1" || !iv || !ct || extra !== void 0) return null;
1269
- try {
1270
- const plain = new TextDecoder().decode(await crypto.subtle.decrypt(
1271
- { name: "AES-GCM", iv: unb642(iv), additionalData: new TextEncoder().encode(userId) },
1272
- key,
1273
- unb642(ct)
1274
- ));
1275
- if (!plain || plain.length > MAX_REFRESH) return null;
1276
- return plain;
1277
- } catch {
1278
- return null;
1279
- }
1280
- }
1281
- function expiresAt(expiresIn, now) {
1282
- const seconds = typeof expiresIn === "number" ? expiresIn : typeof expiresIn === "string" ? Number(expiresIn) : NaN;
1283
- if (!Number.isFinite(seconds) || seconds <= 0 || seconds > 365 * 24 * 60 * 60) return null;
1284
- return new Date(now + seconds * 1e3).toISOString();
1285
- }
1286
- async function readCloudflareGrant(db, userId) {
1287
- if (!userId) return null;
1288
- return db.prepare(
1289
- `SELECT "userId" AS userId, "accountId" AS accountId, choice, host, requested_scope, granted_scope, refresh_enc, access_expires_at
1290
- FROM cloudflare_grant WHERE "userId" = ?`
1291
- ).bind(userId).first();
1292
- }
1293
- function randomId() {
1294
- const buf = new Uint8Array(16);
1295
- crypto.getRandomValues(buf);
1296
- return [...buf].map((byte) => byte.toString(16).padStart(2, "0")).join("");
1297
- }
1298
- async function claimLock(db, userId, holder, now) {
1299
- const result = await db.prepare(
1300
- `INSERT INTO control_locks (name, holder, expires_ms) VALUES (?, ?, ?)
1301
- ON CONFLICT(name) DO UPDATE SET holder = excluded.holder, expires_ms = excluded.expires_ms
1302
- WHERE control_locks.expires_ms <= ?`
1303
- ).bind(`cf-refresh:${userId}`, holder, now + LOCK_MS, now).run();
1304
- return result.meta?.changes === 1;
1569
+ return db.prepare(
1570
+ `SELECT ${COLUMNS} FROM cloudflare_inbox WHERE id = ? AND "userId" = ?`
1571
+ ).bind(inboxId, userId).first();
1305
1572
  }
1306
- async function releaseLock(db, userId, holder) {
1307
- await db.prepare(
1308
- `UPDATE control_locks SET holder = '', expires_ms = 0 WHERE name = ? AND holder = ?`
1309
- ).bind(`cf-refresh:${userId}`, holder).run();
1573
+ async function readCloudflareInbox(db, userId) {
1574
+ const rows = await listCloudflareInboxes(db, userId);
1575
+ const open = rows.find((row) => row.status === "pending" || row.status === "working");
1576
+ if (open) return open;
1577
+ const ready = rows.filter((row) => row.status === "ready");
1578
+ return ready.at(-1) ?? null;
1310
1579
  }
1311
- async function clearRefresh(db, userId, now) {
1312
- await db.prepare(
1313
- `UPDATE cloudflare_grant SET refresh_enc = NULL, updated_at = ? WHERE "userId" = ?`
1314
- ).bind(new Date(now).toISOString(), userId).run();
1580
+ async function deleteCloudflareInbox(db, userId, inboxId) {
1581
+ if (!userId || !inboxId) return false;
1582
+ const result = await db.prepare(`DELETE FROM cloudflare_inbox WHERE id = ? AND "userId" = ?`).bind(inboxId, userId).run();
1583
+ return result.meta?.changes === 1;
1315
1584
  }
1316
- async function postRefresh(body, fetchMs) {
1317
- const timeout = AbortSignal.timeout(fetchMs);
1318
- let rejectDeadline = () => {
1319
- };
1320
- const deadline = new Promise((_, reject) => {
1321
- rejectDeadline = reject;
1322
- });
1323
- const timer = setTimeout(() => rejectDeadline(new DOMException("The operation was aborted", "TimeoutError")), fetchMs);
1324
- try {
1325
- const request = fetch(TOKEN, {
1326
- method: "POST",
1327
- headers: { "content-type": "application/x-www-form-urlencoded", accept: "application/json" },
1328
- body,
1329
- signal: timeout
1330
- }).then(
1331
- (response) => ({ response, failed: false }),
1332
- () => ({ response: null, failed: true })
1333
- );
1334
- let winner;
1335
- try {
1336
- winner = await Promise.race([request, deadline]);
1337
- } catch {
1338
- return { kind: "deadline" };
1339
- }
1340
- if (winner.failed || !winner.response) return timeout.aborted ? { kind: "deadline" } : { kind: "failed" };
1341
- if (!winner.response.ok) {
1342
- const status = winner.response.status;
1343
- void winner.response.body?.cancel().catch(() => {
1344
- });
1345
- return status === 400 || status === 401 ? { kind: "rejected", status } : { kind: "failed" };
1346
- }
1585
+ function cloudflareHostname(row) {
1586
+ if (row.public_url) {
1347
1587
  try {
1348
- const parsed = await Promise.race([
1349
- winner.response.json().then((body2) => ({ body: body2, failed: false }), () => ({ body: void 0, failed: true })),
1350
- deadline
1351
- ]);
1352
- if (parsed.failed) return timeout.aborted ? { kind: "deadline" } : { kind: "failed" };
1353
- return { kind: "ok", body: parsed.body };
1588
+ const url = new URL(row.public_url);
1589
+ if (url.protocol === "https:" && !url.username && !url.password && url.hostname) return url.hostname;
1354
1590
  } catch {
1355
- return { kind: "deadline" };
1356
- }
1357
- } finally {
1358
- clearTimeout(timer);
1359
- }
1360
- }
1361
- async function refreshCloudflareGrant(input) {
1362
- const now = input.now ?? Date.now();
1363
- const row = await readCloudflareGrant(input.database, input.userId);
1364
- if (!row) return { ok: false, error: "no_grant" };
1365
- if (!row.refresh_enc) return { ok: false, error: "no_refresh" };
1366
- const holder = randomId();
1367
- const claimed = await claimLock(input.database, input.userId, holder, now);
1368
- if (!claimed) return { ok: false, error: "busy" };
1369
- try {
1370
- const current = await readCloudflareGrant(input.database, input.userId);
1371
- const token = await openCloudflareRefresh(input.secret, input.userId, current?.refresh_enc);
1372
- if (!token) return { ok: false, error: "no_refresh" };
1373
- const read = await postRefresh(new URLSearchParams({
1374
- grant_type: "refresh_token",
1375
- refresh_token: token,
1376
- client_id: input.clientId,
1377
- client_secret: input.clientSecret
1378
- }), input.fetchMs ?? 5e3);
1379
- if (read.kind === "deadline") return { ok: false, error: "uncertain" };
1380
- if (read.kind === "rejected") {
1381
- await clearRefresh(input.database, input.userId, now);
1382
- return { ok: false, error: "invalid_grant" };
1383
- }
1384
- if (read.kind === "failed") return { ok: false, error: "failed" };
1385
- const body = read.body && typeof read.body === "object" ? read.body : null;
1386
- const accessToken = typeof body?.access_token === "string" ? body.access_token : "";
1387
- if (!accessToken) return { ok: false, error: "failed" };
1388
- const next = typeof body?.refresh_token === "string" ? body.refresh_token : "";
1389
- const sealed = next ? await sealCloudflareRefresh(input.secret, input.userId, next) : null;
1390
- if (next && !sealed) {
1391
- await clearRefresh(input.database, input.userId, now);
1392
- return { ok: false, error: "unreadable" };
1393
1591
  }
1394
- const scope = typeof body?.scope === "string" ? body.scope : current?.requested_scope ?? "";
1395
- await input.database.prepare(
1396
- `UPDATE cloudflare_grant SET refresh_enc = ?, granted_scope = COALESCE(?, granted_scope), access_expires_at = ?, updated_at = ? WHERE "userId" = ?`
1397
- ).bind(
1398
- sealed,
1399
- typeof body?.scope === "string" ? body.scope : null,
1400
- expiresAt(body?.expires_in, now),
1401
- new Date(now).toISOString(),
1402
- input.userId
1403
- ).run();
1404
- return { ok: true, accessToken, scope };
1405
- } finally {
1406
- await releaseLock(input.database, input.userId, holder).catch(() => {
1407
- });
1408
1592
  }
1593
+ if (row.host === "zone" && row.hostname) return row.hostname;
1594
+ if (row.worker_name && row.workers_subdomain) return `${row.worker_name}.${row.workers_subdomain}.workers.dev`;
1595
+ if (row.worker_name) return row.worker_name;
1596
+ return "Setting up";
1409
1597
  }
1410
1598
 
1411
1599
  // src/provision/remove.ts
@@ -1514,7 +1702,8 @@ async function runRemovalStep(input) {
1514
1702
  clientSecret: input.clientSecret,
1515
1703
  userId: input.userId,
1516
1704
  fetchMs: Math.min(input.fetchMs ?? 1e4, 5e3),
1517
- now
1705
+ now,
1706
+ fetchImpl: input.fetchImpl
1518
1707
  });
1519
1708
  if (!refreshed.ok) {
1520
1709
  await save(input.database, row, { removing: true, passed: [...passed], failed: step, text: "The saved Cloudflare permission expired. Connect Cloudflare again, then try again.", cloudflare: "" }, now);
@@ -1538,147 +1727,6 @@ async function runRemovalStep(input) {
1538
1727
  return { finished: false, row: await readCloudflareInboxById(input.database, input.userId, row.id) };
1539
1728
  }
1540
1729
 
1541
- // src/auth/cloudflare-setup.ts
1542
- var USER_DETAILS_READ = "user-details.read";
1543
- var ACCOUNT_SETTINGS_READ = "account-settings.read";
1544
- var WORKERS_SCRIPTS_ADMIN = "workers-scripts.admin";
1545
- var D1_WRITE = "d1.write";
1546
- var WORKERS_ROUTES_WRITE = "workers-routes.write";
1547
- var DNS_WRITE = "dns.write";
1548
- var ZONE_READ = "zone.read";
1549
- var TUNNEL_WRITE = "argotunnel.write";
1550
- var OFFLINE_ACCESS = "offline_access";
1551
- function parseSetupChoice(value) {
1552
- return value === "inbox" || value === "facade" || value === "both" ? value : null;
1553
- }
1554
- function parseSetupHost(value) {
1555
- return value === "workers" || value === "zone" ? value : null;
1556
- }
1557
- function includesInbox(choice) {
1558
- return choice === "inbox" || choice === "both";
1559
- }
1560
- function includesFacade(choice) {
1561
- return choice === "facade" || choice === "both";
1562
- }
1563
- function variant(choice) {
1564
- if (choice === "inbox") return "a new inbox";
1565
- if (choice === "facade") return "the fa\xE7ade";
1566
- return "the new inbox and the fa\xE7ade";
1567
- }
1568
- function setupSummary(choice, host) {
1569
- const what = choice === "inbox" ? "give your agent a public A2A address" : choice === "facade" ? "make an agent you already run reachable by other agents" : "give your agent a public A2A address and make an agent you already run reachable";
1570
- const where = host === "workers" ? "a free Cloudflare address such as my-agent.yourname.workers.dev" : "your own domain, such as agent.example.com";
1571
- return `You chose to ${what}, on ${where}.`;
1572
- }
1573
- function setupScopes(choice, host) {
1574
- const inbox = includesInbox(choice);
1575
- const facade = includesFacade(choice);
1576
- const zone = host === "zone";
1577
- const scopes = [
1578
- {
1579
- id: USER_DETAILS_READ,
1580
- name: "User Details Read",
1581
- why: "Confirms which Cloudflare user this is.",
1582
- declined: "Without User Details Read, Cloudflare did not confirm the user. Choose the setup again."
1583
- },
1584
- {
1585
- id: ACCOUNT_SETTINGS_READ,
1586
- name: "Account Settings Read",
1587
- why: "Reads the Cloudflare account you pick on the consent screen.",
1588
- declined: "Without Account Settings Read, this dashboard cannot see which Cloudflare account you picked, so this setup cannot continue."
1589
- },
1590
- {
1591
- id: WORKERS_SCRIPTS_ADMIN,
1592
- name: "Workers Admin",
1593
- why: facade && !inbox ? "Uploads the fa\xE7ade Worker into your account." : facade ? "Uploads the inbox Worker and the fa\xE7ade Worker into your account." : "Uploads the inbox Worker into your account.",
1594
- declined: `Without Workers Admin, the Worker cannot be uploaded. Connect again to grant it. That blocks ${variant(choice)}.`
1595
- },
1596
- {
1597
- id: D1_WRITE,
1598
- name: "D1 Write",
1599
- why: "Creates the D1 database that Worker uses.",
1600
- declined: `Without D1 Write, the database cannot be created. That blocks ${variant(choice)}.`
1601
- }
1602
- ];
1603
- if (zone) {
1604
- scopes.push(
1605
- {
1606
- id: WORKERS_ROUTES_WRITE,
1607
- name: "Workers Routes Write",
1608
- why: "Attaches the Worker to a hostname on a domain you already have in Cloudflare.",
1609
- declined: "Without Workers Routes Write, the Worker cannot be attached to a hostname on your domain. That blocks a setup on your own domain. A free address is set up with the permissions that address needs."
1610
- },
1611
- {
1612
- id: DNS_WRITE,
1613
- name: "DNS Write",
1614
- why: "Creates the DNS record for that hostname.",
1615
- declined: "Without DNS Write, the DNS record for your domain cannot be created. That blocks a setup on your own domain. A free address is set up with the permissions that address needs."
1616
- },
1617
- {
1618
- id: ZONE_READ,
1619
- name: "Zone Read",
1620
- why: "Lists the domains on the Cloudflare account you pick, so this page can check that your address is on one of them.",
1621
- declined: "Without Zone Read, the domains on your Cloudflare account cannot be listed. That blocks a setup on your own domain. A free address is set up with the permissions that address needs."
1622
- }
1623
- );
1624
- }
1625
- if (facade) {
1626
- scopes.push({
1627
- id: TUNNEL_WRITE,
1628
- name: "Cloudflare Tunnel Write",
1629
- why: "Creates a tunnel from the fa\xE7ade Worker to your existing agent. You still run cloudflared on a machine you control.",
1630
- declined: inbox ? "Without Cloudflare Tunnel Write, a tunnel cannot be created. That blocks the fa\xE7ade. The inbox is created without a tunnel." : "Without Cloudflare Tunnel Write, a tunnel cannot be created. That blocks the fa\xE7ade."
1631
- });
1632
- }
1633
- scopes.push({
1634
- id: OFFLINE_ACCESS,
1635
- name: "Refresh token (offline access)",
1636
- why: "Lets this dashboard continue the setup after you leave this page. The token is stored encrypted. Cloudflare treats it as single-use.",
1637
- declined: "Connect again and keep the refresh token to continue after you leave this page. Nothing has been created in your Cloudflare account."
1638
- });
1639
- return scopes;
1640
- }
1641
- function grantedScopeSet(scope) {
1642
- if (scope == null) return { known: false, granted: /* @__PURE__ */ new Set() };
1643
- const granted = new Set(scope.split(/[\s,]+/).map((part) => part.trim()).filter(Boolean));
1644
- return { known: true, granted };
1645
- }
1646
- function inboxCanProvision(row) {
1647
- const choice = parseSetupChoice(row.choice);
1648
- const host = parseSetupHost(row.host);
1649
- if (!choice || !host) return false;
1650
- return grantCoversInbox(row, choice, host);
1651
- }
1652
- function grantCoversInbox(row, choice, host) {
1653
- if (!includesInbox(choice) || !row.refresh_enc) return false;
1654
- const { known, granted } = grantedScopeSet(row.granted_scope);
1655
- if (!known) return false;
1656
- return setupScopes(choice, host).every((scope) => scope.id === TUNNEL_WRITE || granted.has(scope.id));
1657
- }
1658
- function describeSetupGrant(row) {
1659
- const choice = parseSetupChoice(row.choice);
1660
- const host = parseSetupHost(row.host);
1661
- if (!choice || !host) return null;
1662
- const requested = setupScopes(choice, host);
1663
- const { known, granted } = grantedScopeSet(row.granted_scope);
1664
- const lines = requested.map((scope) => ({
1665
- name: scope.name,
1666
- why: scope.why,
1667
- state: !known ? "unknown" : granted.has(scope.id) ? "granted" : "declined"
1668
- }));
1669
- const blocked = !known ? ["Cloudflare did not say which permissions were granted, so this setup is not confirmed."] : requested.filter((scope) => !granted.has(scope.id)).map((scope) => scope.declined);
1670
- return {
1671
- summary: setupSummary(choice, host),
1672
- lines,
1673
- blocked,
1674
- refreshSaved: Boolean(row.refresh_enc),
1675
- facade: includesFacade(choice),
1676
- inbox: includesInbox(choice),
1677
- host,
1678
- needsReconnect: !known || !granted.has(WORKERS_SCRIPTS_ADMIN)
1679
- };
1680
- }
1681
-
1682
1730
  // src/provision/inbox.ts
1683
1731
  var INBOX_COMPATIBILITY_DATE = "2026-10-06";
1684
1732
  var WORKING_MS = 2 * 60 * 1e3;
@@ -2206,7 +2254,8 @@ async function runInboxStep(input) {
2206
2254
  clientSecret: input.clientSecret,
2207
2255
  userId: input.userId,
2208
2256
  fetchMs: Math.min(fetchMs, 5e3),
2209
- now
2257
+ now,
2258
+ fetchImpl
2210
2259
  });
2211
2260
  if (!refreshed.ok) {
2212
2261
  const problem = refreshProblem(refreshed.error);
@@ -2533,28 +2582,28 @@ async function runVerify(ctx) {
2533
2582
  return wizardNow(ctx);
2534
2583
  }
2535
2584
 
2536
- // sql-raw:/workspace/cc/worker/migrations/0001_init.sql
2585
+ // sql-raw:/home/runner/work/a2a-exposed/a2a-exposed/worker/migrations/0001_init.sql
2537
2586
  var init_default = "-- a2a-over-webhook schema\nCREATE TABLE IF NOT EXISTS peers (\n label TEXT PRIMARY KEY,\n token_hash TEXT NOT NULL UNIQUE,\n created_at TEXT NOT NULL,\n revoked_at TEXT\n);\nCREATE TABLE IF NOT EXISTS tasks (\n id TEXT PRIMARY KEY,\n context_id TEXT NOT NULL,\n peer TEXT NOT NULL,\n state TEXT NOT NULL,\n protocol TEXT NOT NULL,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL,\n status_json TEXT NOT NULL, -- TaskStatus (internal 0.3 shape)\n artifacts_json TEXT NOT NULL DEFAULT '[]',\n metadata_json TEXT,\n push_configs_json TEXT NOT NULL DEFAULT '[]'\n);\nCREATE INDEX IF NOT EXISTS tasks_state ON tasks(state, updated_at);\nCREATE INDEX IF NOT EXISTS tasks_ctx ON tasks(context_id);\nCREATE INDEX IF NOT EXISTS tasks_peer ON tasks(peer, updated_at);\nCREATE TABLE IF NOT EXISTS messages (\n seq INTEGER PRIMARY KEY AUTOINCREMENT,\n message_id TEXT NOT NULL,\n task_id TEXT NOT NULL,\n context_id TEXT NOT NULL,\n role TEXT NOT NULL,\n ts TEXT NOT NULL,\n message_json TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS messages_task ON messages(task_id, seq);\nCREATE TABLE IF NOT EXISTS history (\n seq INTEGER PRIMARY KEY AUTOINCREMENT,\n context_id TEXT NOT NULL,\n ts TEXT NOT NULL,\n dir TEXT NOT NULL, -- in | out | local\n peer TEXT,\n task_id TEXT,\n role TEXT,\n state TEXT,\n event TEXT,\n text TEXT,\n data_json TEXT\n);\nCREATE INDEX IF NOT EXISTS history_ctx ON history(context_id, seq);\nCREATE TABLE IF NOT EXISTS outbound (\n task_id TEXT PRIMARY KEY,\n context_id TEXT NOT NULL,\n peer TEXT NOT NULL,\n endpoint TEXT,\n protocol TEXT,\n push_token_hash TEXT,\n sent_at TEXT NOT NULL,\n task_json TEXT,\n updates_json TEXT NOT NULL DEFAULT '[]'\n);\nCREATE TABLE IF NOT EXISTS wakes (\n context_id TEXT PRIMARY KEY,\n last_sent_ms INTEGER NOT NULL DEFAULT 0,\n pending_json TEXT,\n pending_since_ms INTEGER\n);\nCREATE TABLE IF NOT EXISTS rate (\n peer TEXT NOT NULL,\n minute INTEGER NOT NULL,\n count INTEGER NOT NULL,\n PRIMARY KEY (peer, minute)\n);\nCREATE TABLE IF NOT EXISTS wake_budget (\n hour INTEGER PRIMARY KEY, -- unix hour\n count INTEGER NOT NULL\n);\n";
2538
2587
 
2539
- // sql-raw:/workspace/cc/worker/migrations/0002_wake_budget.sql
2588
+ // sql-raw:/home/runner/work/a2a-exposed/a2a-exposed/worker/migrations/0002_wake_budget.sql
2540
2589
  var wake_budget_default = "-- wake_budget (hourly wake cap) for databases whose 0001_init.sql predates the table.\n-- D1 records migrations by file name, so a database created by an earlier build with its own\n-- 0001_init.sql (e.g. the s2a2a prototype) never gets wake_budget from the current 0001.\n-- Idempotent: a no-op on databases created from the current 0001_init.sql.\nCREATE TABLE IF NOT EXISTS wake_budget (\n hour INTEGER PRIMARY KEY, -- unix hour\n count INTEGER NOT NULL\n);\n";
2541
2590
 
2542
- // sql-raw:/workspace/cc/worker/migrations/0003_device_pairing.sql
2591
+ // sql-raw:/home/runner/work/a2a-exposed/a2a-exposed/worker/migrations/0003_device_pairing.sql
2543
2592
  var device_pairing_default = '-- Device-flow pairing (OAuth 2.0 Device Authorization Grant, RFC 8628): a peer agent asks for a token, the inbox\n-- owner approves it (human: /device page + approval password; agent: owner API), the peer redeems it once.\n-- Numbered 0003 so it never collides with a 0002 migration from another branch; D1 applies unapplied files by name.\nCREATE TABLE IF NOT EXISTS device_requests (\n device_hash TEXT PRIMARY KEY, -- sha256(device_code); the device code itself is never stored\n user_code TEXT NOT NULL UNIQUE, -- normalised (no dash), e.g. WDJB4827\n client_id TEXT,\n client_name TEXT,\n agent_card_url TEXT,\n ip TEXT,\n country TEXT,\n status TEXT NOT NULL, -- pending | approved | denied (| redeemed, just before the row is deleted)\n created_ms INTEGER NOT NULL,\n expires_ms INTEGER NOT NULL,\n interval_s INTEGER NOT NULL,\n last_poll_ms INTEGER NOT NULL DEFAULT 0,\n failed_attempts INTEGER NOT NULL DEFAULT 0,\n label TEXT, -- peer label chosen at approval\n decided_ms INTEGER,\n decided_by TEXT -- human | agent | owner | lockout\n);\nCREATE INDEX IF NOT EXISTS device_requests_status ON device_requests(status, expires_ms);\n-- fixed-window counters: device requests per IP / globally, wrong approval passwords per IP / globally, token polls\nCREATE TABLE IF NOT EXISTS pairing_rate (\n key TEXT NOT NULL,\n window_start INTEGER NOT NULL,\n count INTEGER NOT NULL,\n expires_ms INTEGER NOT NULL,\n PRIMARY KEY (key, window_start)\n);\n-- owner settings; approval_password = {"alg":"pbkdf2-sha256","iterations":..,"salt":..,"hash":..,"setAt":..}\nCREATE TABLE IF NOT EXISTS settings (\n key TEXT PRIMARY KEY,\n value TEXT NOT NULL\n);\n-- where a peer token came from: NULL = `token issue`, \'pairing\' = device flow (with the user code it was approved under)\nALTER TABLE peers ADD COLUMN source TEXT;\nALTER TABLE peers ADD COLUMN user_code TEXT;\nALTER TABLE peers ADD COLUMN client_name TEXT;\n';
2544
2593
 
2545
- // sql-raw:/workspace/cc/worker/migrations/0004_pairing_replace.sql
2594
+ // sql-raw:/home/runner/work/a2a-exposed/a2a-exposed/worker/migrations/0004_pairing_replace.sql
2546
2595
  var pairing_replace_default = '-- Re-pairing: a requester that presents its current active token (Authorization: Bearer) replaces that token on\n-- approval. The new token takes over the label, so no orphan token or "-2" label is left. replaces_hash binds the\n-- request to the token that was presented: if the owner rotates or revokes that token before redemption, the swap\n-- is refused and a fresh label is used instead (a holder of a compromised old token cannot overwrite a rotated one).\nALTER TABLE device_requests ADD COLUMN replaces_label TEXT;\nALTER TABLE device_requests ADD COLUMN replaces_hash TEXT;\n';
2547
2596
 
2548
- // sql-raw:/workspace/cc/worker/migrations/0005_facade_owners.sql
2597
+ // sql-raw:/home/runner/work/a2a-exposed/a2a-exposed/worker/migrations/0005_facade_owners.sql
2549
2598
  var facade_owners_default = "-- Proxy / expose mode (UPSTREAM_URL): which peer label created each upstream task and context, so peers that share\n-- the fa\xE7ade's one upstream credential can't read, cancel or continue each other's tasks. Unused in inbox mode.\nCREATE TABLE IF NOT EXISTS facade_owners (\n kind TEXT NOT NULL, -- 'task' | 'context'\n id TEXT NOT NULL, -- the upstream's task / context id\n peer TEXT NOT NULL, -- peer label (peers.label) that created it through the fa\xE7ade\n created_at TEXT NOT NULL,\n PRIMARY KEY (kind, id)\n);\n";
2550
2599
 
2551
- // sql-raw:/workspace/cc/worker/migrations/0006_mcp.sql
2600
+ // sql-raw:/home/runner/work/a2a-exposed/a2a-exposed/worker/migrations/0006_mcp.sql
2552
2601
  var mcp_default = "-- Remote MCP server (/mcp): OAuth 2.1 clients (dynamic registration), one-time authorization codes, and the grants\n-- (owner-approved, inbox-scoped tokens) that `token list` shows and `token revoke` ends. Peer tokens stay in `peers`\n-- and are never accepted on /mcp; MCP tokens are never accepted on the A2A endpoint or /owner/*.\nCREATE TABLE IF NOT EXISTS oauth_clients (\n client_id TEXT PRIMARY KEY,\n client_name TEXT,\n redirect_uris_json TEXT NOT NULL,\n created_ms INTEGER NOT NULL,\n ip TEXT\n);\nCREATE TABLE IF NOT EXISTS oauth_codes (\n code_hash TEXT PRIMARY KEY,\n client_id TEXT NOT NULL,\n redirect_uri TEXT NOT NULL,\n code_challenge TEXT NOT NULL,\n scope TEXT NOT NULL,\n resource TEXT NOT NULL,\n created_ms INTEGER NOT NULL,\n expires_ms INTEGER NOT NULL\n);\nCREATE TABLE IF NOT EXISTS mcp_grants (\n label TEXT PRIMARY KEY,\n client_id TEXT NOT NULL,\n client_name TEXT,\n redirect_host TEXT,\n scope TEXT NOT NULL,\n access_hash TEXT UNIQUE,\n access_expires_ms INTEGER,\n refresh_hash TEXT UNIQUE,\n refresh_expires_ms INTEGER,\n created_at TEXT NOT NULL,\n last_used_at TEXT,\n revoked_at TEXT\n);\n-- Peers this inbox sends to from the Worker (MCP `send` / `poll_outbound`), uploaded by `peers sync`. The token is\n-- AES-GCM ciphertext under a key derived from the Worker's OWNER_TOKEN secret, so a D1 export alone doesn't reveal it.\nCREATE TABLE IF NOT EXISTS outbound_peers (\n alias TEXT PRIMARY KEY,\n url TEXT NOT NULL,\n token_enc TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\n";
2553
2602
 
2554
- // sql-raw:/workspace/cc/worker/migrations/0007_cimd.sql
2603
+ // sql-raw:/home/runner/work/a2a-exposed/a2a-exposed/worker/migrations/0007_cimd.sql
2555
2604
  var cimd_default = "-- MCP authorization (2026-07-28): OAuth Client ID Metadata Documents. A client_id that is an https URL names a JSON\n-- document the Worker fetches (SSRF-guarded, 5 KB, 5 s) and validates; valid documents are cached here for their\n-- Cache-Control max-age (60 s - 24 h, default 1 h). Errors and invalid documents are never cached.\nCREATE TABLE IF NOT EXISTS oauth_client_metadata (\n client_id TEXT PRIMARY KEY,\n client_name TEXT,\n redirect_uris_json TEXT NOT NULL,\n fetched_ms INTEGER NOT NULL,\n expires_ms INTEGER NOT NULL\n);\n";
2556
2605
 
2557
- // sql-raw:/workspace/cc/worker/migrations/0008_oidc_txns.sql
2606
+ // sql-raw:/home/runner/work/a2a-exposed/a2a-exposed/worker/migrations/0008_oidc_txns.sql
2558
2607
  var oidc_txns_default = "-- Short-lived OpenID Connect sign-in (authorization code + PKCE). The callback deletes the row;\n-- expired rows are removed when a new one is stored and by the pairing cleanup.\nCREATE TABLE IF NOT EXISTS oidc_txns (\n state TEXT PRIMARY KEY,\n nonce TEXT NOT NULL,\n verifier TEXT NOT NULL,\n kind TEXT NOT NULL,\n csrf TEXT NOT NULL,\n user_code TEXT,\n payload_json TEXT NOT NULL,\n expires_ms INTEGER NOT NULL\n);\n";
2559
2608
 
2560
2609
  // src/provision/inbox-migrations.ts
@@ -2569,22 +2618,22 @@ var INBOX_MIGRATIONS = [
2569
2618
  { name: "0008_oidc_txns.sql", sql: oidc_txns_default }
2570
2619
  ];
2571
2620
 
2572
- // sql-raw:/workspace/cc/control/migrations/0001_auth.sql
2621
+ // sql-raw:/home/runner/work/a2a-exposed/a2a-exposed/control/migrations/0001_auth.sql
2573
2622
  var auth_default = '-- Better Auth 1.7.7 (SQLite) plus invite codes. Append-only: do not edit this file.\nCREATE TABLE IF NOT EXISTS "user" (\n "id" text not null primary key,\n "name" text not null,\n "email" text not null unique,\n "emailVerified" integer not null,\n "image" text,\n "createdAt" date not null,\n "updatedAt" date not null\n);\nCREATE TABLE IF NOT EXISTS "session" (\n "id" text not null primary key,\n "expiresAt" date not null,\n "token" text not null unique,\n "createdAt" date not null,\n "updatedAt" date not null,\n "ipAddress" text,\n "userAgent" text,\n "userId" text not null references "user" ("id") on delete cascade\n);\nCREATE TABLE IF NOT EXISTS "account" (\n "id" text not null primary key,\n "accountId" text not null,\n "providerId" text not null,\n "userId" text not null references "user" ("id") on delete cascade,\n "accessToken" text,\n "refreshToken" text,\n "idToken" text,\n "accessTokenExpiresAt" date,\n "refreshTokenExpiresAt" date,\n "scope" text,\n "password" text,\n "createdAt" date not null,\n "updatedAt" date not null\n);\nCREATE TABLE IF NOT EXISTS "verification" (\n "id" text not null primary key,\n "identifier" text not null,\n "value" text not null,\n "expiresAt" date not null,\n "createdAt" date not null,\n "updatedAt" date not null\n);\nCREATE INDEX IF NOT EXISTS "session_userId_idx" ON "session" ("userId");\nCREATE INDEX IF NOT EXISTS "account_userId_idx" ON "account" ("userId");\nCREATE INDEX IF NOT EXISTS "verification_identifier_idx" ON "verification" ("identifier");\n\n-- Invite codes for an invite-only deployment. Unused unless INVITES_REQUIRED=1.\nCREATE TABLE IF NOT EXISTS invites (\n code TEXT PRIMARY KEY,\n email TEXT,\n created_at TEXT NOT NULL,\n expires_at TEXT,\n used_at TEXT,\n used_email TEXT\n);\n';
2574
2623
 
2575
- // sql-raw:/workspace/cc/control/migrations/0002_tenants.sql
2624
+ // sql-raw:/home/runner/work/a2a-exposed/a2a-exposed/control/migrations/0002_tenants.sql
2576
2625
  var tenants_default = "-- Tenants and the config outbox. Append-only: do not edit this file.\n-- Control D1 is the only writer. The data plane receives a copy through pushConfig.\nCREATE TABLE IF NOT EXISTS tenants (\n id TEXT PRIMARY KEY,\n name TEXT NOT NULL UNIQUE,\n owner_account_id TEXT NOT NULL,\n status TEXT NOT NULL,\n region TEXT NOT NULL,\n config_json TEXT NOT NULL,\n limits_json TEXT NOT NULL,\n approval_json TEXT NOT NULL,\n owner_token_hash TEXT NOT NULL,\n secrets_enc TEXT,\n config_version INTEGER NOT NULL,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS tenants_owner_idx ON tenants (owner_account_id);\n\nCREATE TABLE IF NOT EXISTS config_outbox (\n tenant_id TEXT NOT NULL,\n version INTEGER NOT NULL,\n created_at TEXT NOT NULL,\n PRIMARY KEY (tenant_id, version)\n);\n";
2577
2626
 
2578
- // sql-raw:/workspace/cc/control/migrations/0003_oidc.sql
2627
+ // sql-raw:/home/runner/work/a2a-exposed/a2a-exposed/control/migrations/0003_oidc.sql
2579
2628
  var oidc_default = '-- OpenID Connect issuer keys and per-tenant clients. Append-only: do not edit this file.\n-- `jwks` matches Better Auth\'s JWT plugin (RS256). The private key is encrypted by that plugin.\nCREATE TABLE IF NOT EXISTS "jwks" (\n "id" text not null primary key,\n "publicKey" text not null,\n "privateKey" text not null,\n "createdAt" date not null,\n "expiresAt" date,\n "alg" text,\n "crv" text\n);\n\nCREATE TABLE IF NOT EXISTS oauth_client (\n id TEXT PRIMARY KEY,\n secret_hash TEXT NOT NULL,\n owner_account_id TEXT NOT NULL,\n redirect_uris TEXT NOT NULL,\n created_at TEXT NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS oauth_code (\n code_hash TEXT PRIMARY KEY,\n client_id TEXT NOT NULL,\n user_id TEXT NOT NULL,\n redirect_uri TEXT NOT NULL,\n nonce TEXT NOT NULL,\n challenge TEXT NOT NULL,\n expires_at TEXT NOT NULL,\n used_at TEXT\n);\n';
2580
2629
 
2581
- // sql-raw:/workspace/cc/control/migrations/0004_device.sql
2630
+ // sql-raw:/home/runner/work/a2a-exposed/a2a-exposed/control/migrations/0004_device.sql
2582
2631
  var device_default = '-- Device authorization codes for CLI sign-in (Better Auth device-authorization plugin).\n-- Append-only: do not edit this file.\nCREATE TABLE IF NOT EXISTS "deviceCode" (\n "id" text not null primary key,\n "deviceCode" text not null unique,\n "userCode" text not null unique,\n "userId" text,\n "expiresAt" date not null,\n "status" text not null,\n "lastPolledAt" date,\n "pollingInterval" integer,\n "clientId" text,\n "scope" text\n);\n';
2583
2632
 
2584
- // sql-raw:/workspace/cc/control/migrations/0005_agent_limit.sql
2633
+ // sql-raw:/home/runner/work/a2a-exposed/a2a-exposed/control/migrations/0005_agent_limit.sql
2585
2634
  var agent_limit_default = "-- Free plan: two agents per account. The check runs inside the insert, so two\n-- creates cannot both pass a count taken before either write.\n-- Keep the bound in step with AGENT_LIMIT in src/tenants/create.ts.\n-- Append-only: do not edit this file.\nCREATE TRIGGER IF NOT EXISTS tenants_agent_limit\nBEFORE INSERT ON tenants\nFOR EACH ROW\nWHEN (SELECT COUNT(*) FROM tenants WHERE owner_account_id = NEW.owner_account_id) >= 2\nBEGIN\n SELECT RAISE(ABORT, 'agent_limit');\nEND;\n";
2586
2635
 
2587
- // sql-raw:/workspace/cc/control/migrations/0006_agent_limit_account.sql
2636
+ // sql-raw:/home/runner/work/a2a-exposed/a2a-exposed/control/migrations/0006_agent_limit_account.sql
2588
2637
  var agent_limit_account_default = `-- Per-account agent limit. Null uses control_settings.free_agent_limit, which the
2589
2638
  -- app sets from FREE_AGENT_LIMIT. Both null means no limit. Replaces the constant
2590
2639
  -- check in 0005 so a higher plan can raise one account without another migration.
@@ -2615,19 +2664,19 @@ BEGIN
2615
2664
  END;
2616
2665
  `;
2617
2666
 
2618
- // sql-raw:/workspace/cc/control/migrations/0007_outbox_lock.sql
2667
+ // sql-raw:/home/runner/work/a2a-exposed/a2a-exposed/control/migrations/0007_outbox_lock.sql
2619
2668
  var outbox_lock_default = "-- One lock for config delivery and agent delete. A flush that already loaded a row must not\n-- write the directory or the tenant object while delete is wiping them, and delete must not\n-- start that wipe while a delivery is still writing. Append-only: do not edit this file.\nCREATE TABLE IF NOT EXISTS control_locks (\n name TEXT PRIMARY KEY,\n holder TEXT NOT NULL,\n expires_ms INTEGER NOT NULL\n);\nINSERT INTO control_locks (name, holder, expires_ms) VALUES ('outbox', '', 0)\nON CONFLICT(name) DO NOTHING;\n";
2620
2669
 
2621
- // sql-raw:/workspace/cc/control/migrations/0008_account_provider.sql
2670
+ // sql-raw:/home/runner/work/a2a-exposed/a2a-exposed/control/migrations/0008_account_provider.sql
2622
2671
  var account_provider_default = '-- One external account identity belongs to one user. Two connect callbacks can\n-- both insert the same providerId and accountId. Those rows have to go before\n-- the unique index, or this migration fails and deploy stops. Keep the earliest\n-- row (then the lowest id). The user row stays. Append-only: do not edit this file.\nDELETE FROM "account"\nWHERE "id" NOT IN (\n SELECT "id" FROM "account" AS "candidate"\n WHERE NOT EXISTS (\n SELECT 1 FROM "account" AS "earlier"\n WHERE "earlier"."providerId" = "candidate"."providerId"\n AND "earlier"."accountId" = "candidate"."accountId"\n AND (\n "earlier"."createdAt" < "candidate"."createdAt"\n OR ("earlier"."createdAt" = "candidate"."createdAt" AND "earlier"."id" < "candidate"."id")\n )\n )\n);\nCREATE UNIQUE INDEX IF NOT EXISTS "account_provider_account_idx" ON "account" ("providerId", "accountId");\n';
2623
2672
 
2624
- // sql-raw:/workspace/cc/control/migrations/0009_feedback_rate.sql
2673
+ // sql-raw:/home/runner/work/a2a-exposed/a2a-exposed/control/migrations/0009_feedback_rate.sql
2625
2674
  var feedback_rate_default = "-- Per-address and per-owner-token counters for POST /api/feedback.\n-- Append-only: do not edit this file.\nCREATE TABLE IF NOT EXISTS feedback_rate (\n bucket TEXT NOT NULL,\n window_start INTEGER NOT NULL,\n count INTEGER NOT NULL,\n PRIMARY KEY (bucket, window_start)\n);\n";
2626
2675
 
2627
- // sql-raw:/workspace/cc/control/migrations/0010_cloudflare_grant.sql
2676
+ // sql-raw:/home/runner/work/a2a-exposed/a2a-exposed/control/migrations/0010_cloudflare_grant.sql
2628
2677
  var cloudflare_grant_default = '-- Setup consent for "Set me up on Cloudflare". refresh_enc is the sealed\n-- refresh token. The access token is not stored. Append-only: do not edit this file.\nCREATE TABLE IF NOT EXISTS cloudflare_grant (\n "userId" TEXT NOT NULL PRIMARY KEY REFERENCES "user" ("id") ON DELETE CASCADE,\n "accountId" TEXT NOT NULL,\n choice TEXT NOT NULL,\n host TEXT NOT NULL,\n requested_scope TEXT NOT NULL,\n granted_scope TEXT,\n refresh_enc TEXT,\n access_expires_at TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\n';
2629
2678
 
2630
- // sql-raw:/workspace/cc/control/migrations/0011_cloudflare_inbox.sql
2679
+ // sql-raw:/home/runner/work/a2a-exposed/a2a-exposed/control/migrations/0011_cloudflare_inbox.sql
2631
2680
  var cloudflare_inbox_default = `-- An inbox this dashboard created in the user's Cloudflare account.
2632
2681
  -- The owner token is stored only as a SHA-256 hash. Append-only: do not edit this file.
2633
2682
  CREATE TABLE IF NOT EXISTS cloudflare_inbox (
@@ -2646,13 +2695,13 @@ CREATE TABLE IF NOT EXISTS cloudflare_inbox (
2646
2695
  );
2647
2696
  `;
2648
2697
 
2649
- // sql-raw:/workspace/cc/control/migrations/0012_cloudflare_inbox_tag.sql
2698
+ // sql-raw:/home/runner/work/a2a-exposed/a2a-exposed/control/migrations/0012_cloudflare_inbox_tag.sql
2650
2699
  var cloudflare_inbox_tag_default = "-- Value this page stores before upload and writes into the Worker settings.\n-- A later try replaces a script only when that setting still matches.\n-- Append-only: do not edit this file.\nALTER TABLE cloudflare_inbox ADD COLUMN script_tag TEXT;\n";
2651
2700
 
2652
- // sql-raw:/workspace/cc/control/migrations/0013_cloudflare_inbox_wizard.sql
2701
+ // sql-raw:/home/runner/work/a2a-exposed/a2a-exposed/control/migrations/0013_cloudflare_inbox_wizard.sql
2653
2702
  var cloudflare_inbox_wizard_default = "-- Wizard progress for an inbox this page creates.\n-- wizard_step is the last step that passed. step_error is a short sanitised note.\n-- workers_subdomain is the account workers.dev name once it is known.\n-- Append-only: do not edit this file.\nALTER TABLE cloudflare_inbox ADD COLUMN wizard_step TEXT;\nALTER TABLE cloudflare_inbox ADD COLUMN workers_subdomain TEXT;\nALTER TABLE cloudflare_inbox ADD COLUMN step_error TEXT;\n";
2654
2703
 
2655
- // sql-raw:/workspace/cc/control/migrations/0014_cloudflare_inbox_agent.sql
2704
+ // sql-raw:/home/runner/work/a2a-exposed/a2a-exposed/control/migrations/0014_cloudflare_inbox_agent.sql
2656
2705
  var cloudflare_inbox_agent_default = '-- An account can have more than one Cloudflare agent. The old primary key was userId.\n-- Those rows keep that value as id. zone_id is the domain this setup attached, when it did.\n-- Append-only: do not edit this file.\nCREATE TABLE cloudflare_inbox_agent (\n id TEXT NOT NULL PRIMARY KEY,\n "userId" TEXT NOT NULL REFERENCES "user" ("id") ON DELETE CASCADE,\n cf_account_id TEXT NOT NULL,\n worker_name TEXT NOT NULL,\n d1_name TEXT NOT NULL,\n d1_id TEXT,\n public_url TEXT,\n owner_token_hash TEXT,\n host TEXT NOT NULL,\n hostname TEXT,\n status TEXT NOT NULL,\n script_tag TEXT,\n wizard_step TEXT,\n workers_subdomain TEXT,\n step_error TEXT,\n zone_id TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\n\nINSERT INTO cloudflare_inbox_agent (\n id, "userId", cf_account_id, worker_name, d1_name, d1_id, public_url, owner_token_hash,\n host, hostname, status, script_tag, wizard_step, workers_subdomain, step_error, zone_id,\n created_at, updated_at\n)\nSELECT\n "userId", "userId", cf_account_id, worker_name, d1_name, d1_id, public_url, owner_token_hash,\n host, hostname, status, script_tag, wizard_step, workers_subdomain, step_error, NULL,\n created_at, updated_at\nFROM cloudflare_inbox;\n\nDROP TABLE cloudflare_inbox;\nALTER TABLE cloudflare_inbox_agent RENAME TO cloudflare_inbox;\nCREATE INDEX IF NOT EXISTS cloudflare_inbox_user_idx ON cloudflare_inbox ("userId", created_at);\n';
2657
2706
 
2658
2707
  // src/core/migrations.ts
@@ -2674,8 +2723,12 @@ var CONTROL_MIGRATIONS = [
2674
2723
  { name: "0014_cloudflare_inbox_agent.sql", sql: cloudflare_inbox_agent_default }
2675
2724
  ];
2676
2725
  export {
2726
+ ACCOUNT_SETTINGS_READ,
2677
2727
  CONTROL_MIGRATIONS,
2678
2728
  CONTROL_MIGRATIONS_DIR,
2729
+ D1_WRITE,
2730
+ DNS_WRITE,
2731
+ EVERY_PLAN,
2679
2732
  HOST_RE,
2680
2733
  INBOX_COMPATIBILITY_DATE,
2681
2734
  INBOX_MIGRATIONS,
@@ -2683,24 +2736,34 @@ export {
2683
2736
  MAX_WAKE_PLAIN,
2684
2737
  MAX_WAKE_SEALED,
2685
2738
  MAX_WAKE_VALUE,
2739
+ OFFLINE_ACCESS,
2686
2740
  PERMISSION_DELAYS_MS,
2687
2741
  PERMISSION_TRIES,
2688
2742
  PERMISSION_WAIT,
2689
2743
  PROVISION_TAG_NAME,
2690
2744
  RESERVED_NAMES,
2691
2745
  RUN_WORKER_FIRST,
2746
+ SETUP_NOT_PROVISIONED,
2692
2747
  TENANT_NAME,
2748
+ TUNNEL_CONNECTOR_LATER,
2749
+ TUNNEL_WRITE,
2750
+ USER_DETAILS_READ,
2693
2751
  WAKE_PRESETS,
2694
2752
  WAKE_SECRET_KEYS,
2695
2753
  WIZARD_STEPS,
2754
+ WORKERS_ROUTES_WRITE,
2755
+ WORKERS_SCRIPTS_ADMIN,
2756
+ ZONE_READ,
2696
2757
  accountAgentLimit,
2697
2758
  attachHostname,
2698
2759
  callFetch,
2699
2760
  cfCall,
2761
+ cloudflareAuthorizeUrl,
2700
2762
  cloudflareErrors,
2701
2763
  cloudflareHostname,
2702
2764
  configFields,
2703
2765
  configuredFreeAgentLimit,
2766
+ consentScope,
2704
2767
  createCloudflareAgent,
2705
2768
  createD1,
2706
2769
  createTenant,
@@ -2708,6 +2771,7 @@ export {
2708
2771
  d1Rows,
2709
2772
  dashboardLinks,
2710
2773
  dataPlaneFromEnv,
2774
+ deleteCloudflareGrant,
2711
2775
  deleteCloudflareInbox,
2712
2776
  deleteD1,
2713
2777
  deleteTenant,
@@ -2715,27 +2779,38 @@ export {
2715
2779
  deleteWorkersDomain,
2716
2780
  deliverOutbox,
2717
2781
  describeCfFailure,
2782
+ describeSetupGrant,
2718
2783
  fieldsFromForm,
2719
2784
  findD1,
2720
2785
  flushOutbox,
2721
2786
  getD1,
2787
+ grantCoversInbox,
2788
+ grantedScopeSet,
2722
2789
  hashOwnerToken,
2723
2790
  headerSafeToken,
2791
+ httpsOrigin,
2792
+ inboxCanProvision,
2724
2793
  listAccounts,
2725
2794
  listCloudflareInboxes,
2726
2795
  listTenants,
2727
2796
  listWorkerNames,
2728
2797
  listWorkersDomains,
2729
2798
  listZones,
2799
+ newApprovalClient,
2730
2800
  newOwnerToken,
2731
2801
  normalizeHostname,
2802
+ openCloudflareRefresh,
2732
2803
  openWakeSecrets,
2804
+ parseSetupChoice,
2805
+ parseSetupHost,
2733
2806
  permissionNotReady,
2734
2807
  permissionRetryDue,
2735
2808
  pushFromRow,
2809
+ readCloudflareGrant,
2736
2810
  readCloudflareInbox,
2737
2811
  readCloudflareInboxById,
2738
2812
  readInboxById,
2813
+ refreshCloudflareGrant,
2739
2814
  registerWorkersDevSubdomain,
2740
2815
  removalItems,
2741
2816
  removalSteps,
@@ -2747,9 +2822,13 @@ export {
2747
2822
  runInboxStep,
2748
2823
  runRemovalStep,
2749
2824
  sanitizeCloudflareText,
2825
+ saveCloudflareGrant,
2750
2826
  saveWakeSettings,
2827
+ sealCloudflareRefresh,
2751
2828
  sealWakeSecrets,
2752
2829
  setWorkersDevEnabled,
2830
+ setupScopes,
2831
+ setupSummary,
2753
2832
  sqlStatements,
2754
2833
  subdomainMissing,
2755
2834
  tenantForAccount,