@a2a-exposed/control-core 0.7.33 → 0.7.34
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/README.md +7 -4
- package/dist/index.js +590 -511
- package/dist/types/control/src/core/index.d.ts +5 -1
- package/dist/types/control/src/core/sql.d.ts +16 -0
- package/dist/types/control/src/env.d.ts +3 -3
- package/dist/types/control/src/provision/grant.d.ts +54 -0
- package/dist/types/control/src/provision/inbox-row.d.ts +1 -1
- package/dist/types/control/src/provision/inbox.d.ts +2 -2
- package/dist/types/control/src/provision/remove.d.ts +1 -1
- package/dist/types/control/src/tenants/approval.d.ts +16 -0
- package/dist/types/control/src/tenants/create.d.ts +1 -1
- package/dist/types/control/src/tenants/delete.d.ts +1 -1
- package/dist/types/control/src/tenants/push.d.ts +1 -1
- package/dist/types/control/src/tenants/rotate.d.ts +1 -1
- package/dist/types/control/src/tenants/wake.d.ts +1 -1
- package/package.json +1 -1
- package/dist/types/control/src/auth/invites.d.ts +0 -38
- /package/dist/types/control/src/{auth/cloudflare-setup.d.ts → provision/setup.d.ts} +0 -0
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/* @a2a-exposed/control-core */
|
|
2
2
|
|
|
3
|
-
// src/
|
|
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/
|
|
779
|
-
var
|
|
780
|
-
var
|
|
781
|
-
var
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
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
|
|
789
|
-
return
|
|
785
|
+
function unb642(value) {
|
|
786
|
+
return Uint8Array.from(atob(value), (char) => char.charCodeAt(0));
|
|
790
787
|
}
|
|
791
|
-
function
|
|
792
|
-
|
|
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
|
|
795
|
-
|
|
796
|
-
const
|
|
797
|
-
if (
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
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
|
|
808
|
-
if (!
|
|
809
|
-
const
|
|
810
|
-
if (!
|
|
811
|
-
const
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
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
|
|
819
|
-
const
|
|
820
|
-
|
|
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
|
|
823
|
-
const
|
|
824
|
-
const
|
|
825
|
-
|
|
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
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
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
|
|
834
|
-
if (
|
|
835
|
-
|
|
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
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
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
|
|
855
|
-
|
|
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
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
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
|
-
|
|
880
|
-
|
|
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
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
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
|
-
|
|
917
|
+
winner = await Promise.race([request, deadline]);
|
|
896
918
|
} catch {
|
|
897
|
-
|
|
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
|
|
925
|
-
|
|
926
|
-
const
|
|
927
|
-
if (!
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
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
|
|
1307
|
-
await db
|
|
1308
|
-
|
|
1309
|
-
|
|
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
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
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
|
-
|
|
1317
|
-
|
|
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
|
|
1349
|
-
|
|
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:/
|
|
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:/
|
|
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:/
|
|
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:/
|
|
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:/
|
|
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:/
|
|
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:/
|
|
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:/
|
|
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:/
|
|
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:/
|
|
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:/
|
|
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:/
|
|
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:/
|
|
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:/
|
|
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:/
|
|
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:/
|
|
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:/
|
|
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:/
|
|
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:/
|
|
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:/
|
|
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:/
|
|
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:/
|
|
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,
|