Fork Horse repository, listed by its owner. Publisher snapshot.
packages/forkhorse-gatekeeper/src/worker/index.ts
Snapshot at 34e3175
1import { WorkerEntrypoint } from "cloudflare:workers";
2import type { GatekeeperConnectCallback, GatekeeperConnectOptions, GatekeeperVendor as Vendor, SupportedResource, VendorDescription } from "@gadgets/workshop-shared/gatekeeper";
3import { generateNonce } from "@gadgets/gatekeeper-kit/connect-nonce";
4import { connectHandoffPageHtml, connectMutationError } from "@gadgets/gatekeeper-kit/connect-pages";
5import { readTextCapped } from "@gadgets/gatekeeper-kit/response-body";
6import { RESOURCE, type Env } from "./account.ts";
7import brand from "../../../../brand.json";
8import { markIconUrl } from "../../../forkhorse-dashboard/src/mark.ts";
9import { TYPES } from "./api-types.ts";
10export { UserAccount, AgentAccount, AgentVerifier } from "./account.ts";
11export { ForkGatekeeper } from "./fork.ts";
12
13export class GatekeeperVendor extends WorkerEntrypoint<Env> implements Vendor {
14 async describe(): Promise<VendorDescription> {
15 return { displayName: brand.name, url: `https://${brand.domains.primary}`, tagline: brand.tagline,
16 logo: { url: markIconUrl(brand.mark) },
17 description: "Connect an enrolled agent to read its forks and submit forward-only Git updates.",
18 autoProvisionsAccount: false, providesAuth: false };
19 }
20 async connectAccount(callback: Fetcher<GatekeeperConnectCallback>, options?: GatekeeperConnectOptions): Promise<{ url: string }> {
21 if (options?.scopes === "auth" || (options?.resourceUrlPatterns !== undefined && (options.resourceUrlPatterns.length === 0 || options.resourceUrlPatterns.some(p => p !== RESOURCE.urlPattern)))) {
22 throw new Error("Unsupported connection scope.");
23 }
24 const id = this.ctx.exports.UserAccount.newUniqueId();
25 const nonce = generateNonce();
26 await this.ctx.exports.UserAccount.get(id).initiate(callback, nonce);
27 return { url: `${this.env.BASE_URL}/${id}/${nonce}` };
28 }
29 async getSupportedResources(): Promise<SupportedResource[]> { return [RESOURCE]; }
30 async getTypeScriptTypes(): Promise<string> { return TYPES; }
31}
32
33const escape = (value: string) => value.replace(/[&<>"']/g, c => ({"&":"&","<":"<",">":">",'"':""","'":"'"})[c]!);
34function html(body: string, status = 200): Response {
35 return new Response(body, { status, headers: {
36 "content-type": "text/html; charset=utf-8", "cache-control": "no-store",
37 "referrer-policy": "no-referrer", "x-content-type-options": "nosniff",
38 "content-security-policy": "default-src 'none'; style-src 'unsafe-inline'; form-action 'self'; frame-ancestors 'none'; base-uri 'none'",
39 } });
40}
41function connectForm(action: string): string {
42 return `<!doctype html><html><meta charset="utf-8"><title>${escape(brand.name)}</title>
43 <style>body{font:16px system-ui;max-width:36rem;margin:4rem auto;padding:1rem}label{display:block;margin:1rem 0}input{display:block;width:100%;padding:.6rem;box-sizing:border-box}button{padding:.7rem}</style>
44 <h1>Connect ${escape(brand.name)}</h1><p>Use an enrolled agent credential for your project. Owner credentials are refused.</p>
45 <form method="post" action="${escape(action)}"><label>Core origin<input name="coreOrigin" type="url" required placeholder="https://core.idiot.tax"></label>
46 <label>Project ID<input name="project" maxlength="48" required autocomplete="off"></label>
47 <label>Enrolled agent credential<input name="agentCredential" type="password" maxlength="256" required autocomplete="off"></label>
48 <button>Connect agent</button></form></html>`;
49}
50
51export default {
52 async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
53 const url = new URL(request.url);
54 const base = new URL(env.BASE_URL);
55 const prefix = base.pathname.replace(/\/$/, "");
56 if (url.origin !== base.origin || url.search || !url.pathname.startsWith(`${prefix}/`)) return html("Not found.", 404);
57 const match = /^([a-f0-9]{64})\/([a-f0-9]{64})$/.exec(url.pathname.slice(prefix.length + 1));
58 if (!match) return html(`<h1>${escape(brand.name)}</h1><p>Open your connected agent account in the OS sidebar.</p>`, 404);
59 const source = ctx.exports.UserAccount.get(ctx.exports.UserAccount.idFromString(match[1]));
60 if (request.method === "GET") {
61 if (!await source.validNonce(match[2])) return html("Connection link expired. Start again.", 400);
62 const response = html(connectForm(url.href));
63 // A no-referrer HTML form sends Origin: null. Keep same-origin POST verification usable
64 // without disclosing the nonce-bearing URL to another origin.
65 response.headers.set("referrer-policy", "same-origin");
66 return response;
67 }
68 if (request.method !== "POST") return html("Method not allowed.", 405);
69 if (connectMutationError(request, { origin: base.origin, contentType: "application/x-www-form-urlencoded" })) {
70 return html("Invalid connection submission.", 403);
71 }
72 try {
73 const fields = new URLSearchParams(await readTextCapped(request, 8192));
74 const handoff = await source.completeConnection(match[2], {
75 coreOrigin: fields.get("coreOrigin") ?? "", project: fields.get("project") ?? "",
76 agentCredential: fields.get("agentCredential") ?? "",
77 });
78 // The kit handoff navigates to the initiating Workshop; credentials never enter this page.
79 return new Response(connectHandoffPageHtml(handoff), { headers: {
80 "content-type": "text/html; charset=utf-8", "cache-control": "no-store", "referrer-policy": "no-referrer",
81 } });
82 } catch { return html("Agent connection failed. Verify your credential and start again.", 400); }
83 },
84};
85