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;
}
}