import { DurableObject, RpcStub, RpcTarget, WorkerEntrypoint, } from "cloudflare:workers"; import { skipRpcValidation, validateRpc } from "capnweb-validate"; import type { AccountDescription, ApprovalQueue, Gatekeeper, GatekeeperConnectCallback, GatekeeperConnectOptions, GatekeeperUser, GatekeeperUserVerifier, ResourceConfiguratorFrame, ResourceDescription, SupportedResource, VendorDescription, } from "@gadgets/workshop-shared/gatekeeper"; import type { CustomDeploymentInfo, CustomSession } from "./types.js"; import TYPES_CODE from "./types-code.js"; const CUSTOM_ICON = { url: "data:image/svg+xml," + encodeURIComponent( "", ), }; type ObservationQueue = Pick & Partial<{ [Symbol.dispose](): void }>; /** The admin and connector label: the deployment's configured display name (`customGatekeeper.name`), else the default. */ export function gatekeeperDisplayName(name?: string): string { return name?.trim() || "Custom Gatekeeper"; } export function describeCustomVendor(name?: string): VendorDescription { return { displayName: gatekeeperDisplayName(name), url: "https://github.com/cloudflare/cloudflare-os-starter", logo: CUSTOM_ICON, color: "#e8f2ff", tagline: "Example organization-specific capability", description: "A minimal Gatekeeper to copy when connecting CloudflareOS to your organization's systems.", autoProvisionsAccount: true, providesAuth: false, }; } export function describeCustomAccount(name?: string): AccountDescription { return { displayName: gatekeeperDisplayName(name), avatar: CUSTOM_ICON, singleton: { tsType: "CustomSession" }, }; } @validateRpc() export class CustomSessionImpl extends RpcTarget implements CustomSession { readonly #approvalQueue: ObservationQueue; readonly #info: CustomDeploymentInfo; constructor(approvalQueue: ObservationQueue, info: CustomDeploymentInfo) { super(); this.#approvalQueue = approvalQueue; this.#info = info; } async getDeploymentInfo(): Promise { await this.#approvalQueue.authorizeObservation({ title: "Read deployment information", description: "Read the custom information configured by this deployment.", }); return this.#info; } [Symbol.dispose](): void { this.#approvalQueue[Symbol.dispose]?.(); } } @validateRpc() export class CustomGatekeeper extends DurableObject implements Gatekeeper { async describe(): Promise { return { url: "custom://deployment-info", title: "Deployment information", snippet: "Organization-specific information supplied by this deployment.", suggestedBindingName: "CUSTOM", tsType: "CustomSession", }; } async getTypeScriptTypes(): Promise { return TYPES_CODE; } async getAutoApprovableActions(): Promise<[]> { return []; } async startSession(approvalQueue: RpcStub): Promise { return new CustomSessionImpl(approvalQueue.dup(), { name: this.env.CUSTOM_NAME, message: this.env.CUSTOM_MESSAGE, }); } async addObserver(_id: string, _user: Fetcher): Promise {} async removeObserver(_id: string): Promise {} async applyAction(action: number): Promise { throw new Error(`Custom Gatekeeper has no actions (${action}).`); } async rejectAction(_action: number): Promise {} async revertAction(_action: number): Promise { throw new Error("Custom Gatekeeper has no actions to revert."); } } @validateRpc() export class CustomAccount extends WorkerEntrypoint implements GatekeeperUser { async describe(): Promise { return describeCustomAccount(this.env.CUSTOM_NAME); } async getSingletonGatekeeperClass(): Promise>> { return this.ctx.exports.CustomGatekeeper({}); } async getSupportedResources(): Promise { return []; } getGatekeeperClassFor(_url: string): never { throw new Error("Custom Gatekeeper has no URL-addressed resources."); } startResourceConfigurator(_resourceUrlPattern: string): Promise { throw new Error("Custom Gatekeeper has no URL-addressed resources."); } async ensureResources(_resourceUrlPatterns: string[]): Promise<{ url?: string }> { return {}; } async revoke(): Promise {} reconnect(): Promise<{ url: string }> { throw new Error("Custom Gatekeeper has no credentials to reconnect."); } /** Reject staged reconnects: this auto-provisioned account has no credentials. */ commitReconnect(_stageId: string): Promise { throw new Error("Custom Gatekeeper has no credentials to reconnect."); } async getAuthenticatedEmail(): Promise { return null; } @skipRpcValidation() async getVerifier(): Promise> { return this.ctx.exports.CustomVerifier({}); } } @validateRpc() export class CustomVerifier extends WorkerEntrypoint implements GatekeeperUserVerifier { verify(): void {} } @validateRpc() export class GatekeeperVendor extends WorkerEntrypoint { async describe(): Promise { return describeCustomVendor(this.env.CUSTOM_NAME); } @skipRpcValidation() async createAccount(): Promise> { return this.ctx.exports.CustomAccount({}); } connectAccount( _callback: Fetcher, _options?: GatekeeperConnectOptions, ): Promise<{ url: string }> { throw new Error("Custom Gatekeeper is auto-provisioned and has no connect flow."); } async getSupportedResources(_options?: { userId?: string }): Promise { return []; } async getTypeScriptTypes(): Promise { return TYPES_CODE; } }