Fork Horse repository, listed by its owner. Publisher snapshot.
packages/custom-gatekeeper/src/custom.ts
Snapshot at 34e3175
1import {
2 DurableObject,
3 RpcStub,
4 RpcTarget,
5 WorkerEntrypoint,
6} from "cloudflare:workers";
7import { skipRpcValidation, validateRpc } from "capnweb-validate";
8import type {
9 AccountDescription,
10 ApprovalQueue,
11 Gatekeeper,
12 GatekeeperConnectCallback,
13 GatekeeperConnectOptions,
14 GatekeeperUser,
15 GatekeeperUserVerifier,
16 ResourceConfiguratorFrame,
17 ResourceDescription,
18 SupportedResource,
19 VendorDescription,
20} from "@gadgets/workshop-shared/gatekeeper";
21import type { CustomDeploymentInfo, CustomSession } from "./types.js";
22import TYPES_CODE from "./types-code.js";
23
24const CUSTOM_ICON = {
25 url:
26 "data:image/svg+xml," +
27 encodeURIComponent(
28 "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 256 256' fill='none' stroke='currentColor' stroke-width='20'><path d='M52 72h152v112H52z'/><path d='m52 88 76 52 76-52'/></svg>",
29 ),
30};
31
32type ObservationQueue = Pick<ApprovalQueue, "authorizeObservation"> &
33 Partial<{ [Symbol.dispose](): void }>;
34
35/** The admin and connector label: the deployment's configured display name (`customGatekeeper.name`), else the default. */
36export function gatekeeperDisplayName(name?: string): string {
37 return name?.trim() || "Custom Gatekeeper";
38}
39
40export function describeCustomVendor(name?: string): VendorDescription {
41 return {
42 displayName: gatekeeperDisplayName(name),
43 url: "https://github.com/cloudflare/cloudflare-os-starter",
44 logo: CUSTOM_ICON,
45 color: "#e8f2ff",
46 tagline: "Example organization-specific capability",
47 description:
48 "A minimal Gatekeeper to copy when connecting CloudflareOS to your organization's systems.",
49 autoProvisionsAccount: true,
50 providesAuth: false,
51 };
52}
53
54export function describeCustomAccount(name?: string): AccountDescription {
55 return {
56 displayName: gatekeeperDisplayName(name),
57 avatar: CUSTOM_ICON,
58 singleton: { tsType: "CustomSession" },
59 };
60}
61
62@validateRpc()
63export class CustomSessionImpl extends RpcTarget implements CustomSession {
64 readonly #approvalQueue: ObservationQueue;
65 readonly #info: CustomDeploymentInfo;
66
67 constructor(approvalQueue: ObservationQueue, info: CustomDeploymentInfo) {
68 super();
69 this.#approvalQueue = approvalQueue;
70 this.#info = info;
71 }
72
73 async getDeploymentInfo(): Promise<CustomDeploymentInfo> {
74 await this.#approvalQueue.authorizeObservation({
75 title: "Read deployment information",
76 description: "Read the custom information configured by this deployment.",
77 });
78 return this.#info;
79 }
80
81 [Symbol.dispose](): void {
82 this.#approvalQueue[Symbol.dispose]?.();
83 }
84}
85
86@validateRpc()
87export class CustomGatekeeper extends DurableObject<Cloudflare.Env> implements Gatekeeper<CustomSession> {
88 async describe(): Promise<ResourceDescription> {
89 return {
90 url: "custom://deployment-info",
91 title: "Deployment information",
92 snippet: "Organization-specific information supplied by this deployment.",
93 suggestedBindingName: "CUSTOM",
94 tsType: "CustomSession",
95 };
96 }
97
98 async getTypeScriptTypes(): Promise<string> {
99 return TYPES_CODE;
100 }
101
102 async getAutoApprovableActions(): Promise<[]> {
103 return [];
104 }
105
106 async startSession(approvalQueue: RpcStub<ApprovalQueue>): Promise<CustomSession> {
107 return new CustomSessionImpl(approvalQueue.dup(), {
108 name: this.env.CUSTOM_NAME,
109 message: this.env.CUSTOM_MESSAGE,
110 });
111 }
112
113 async addObserver(_id: string, _user: Fetcher<GatekeeperUserVerifier>): Promise<void> {}
114 async removeObserver(_id: string): Promise<void> {}
115
116 async applyAction(action: number): Promise<void> {
117 throw new Error(`Custom Gatekeeper has no actions (${action}).`);
118 }
119
120 async rejectAction(_action: number): Promise<void> {}
121
122 async revertAction(_action: number): Promise<void> {
123 throw new Error("Custom Gatekeeper has no actions to revert.");
124 }
125}
126
127@validateRpc()
128export class CustomAccount extends WorkerEntrypoint<Cloudflare.Env> implements GatekeeperUser {
129 async describe(): Promise<AccountDescription> {
130 return describeCustomAccount(this.env.CUSTOM_NAME);
131 }
132
133 async getSingletonGatekeeperClass(): Promise<DurableObjectClass<Gatekeeper<CustomSession>>> {
134 return this.ctx.exports.CustomGatekeeper({});
135 }
136
137 async getSupportedResources(): Promise<SupportedResource[]> {
138 return [];
139 }
140
141 getGatekeeperClassFor(_url: string): never {
142 throw new Error("Custom Gatekeeper has no URL-addressed resources.");
143 }
144
145 startResourceConfigurator(_resourceUrlPattern: string): Promise<ResourceConfiguratorFrame> {
146 throw new Error("Custom Gatekeeper has no URL-addressed resources.");
147 }
148
149 async ensureResources(_resourceUrlPatterns: string[]): Promise<{ url?: string }> {
150 return {};
151 }
152
153 async revoke(): Promise<void> {}
154
155 reconnect(): Promise<{ url: string }> {
156 throw new Error("Custom Gatekeeper has no credentials to reconnect.");
157 }
158
159 /** Reject staged reconnects: this auto-provisioned account has no credentials. */
160 commitReconnect(_stageId: string): Promise<void> {
161 throw new Error("Custom Gatekeeper has no credentials to reconnect.");
162 }
163
164 async getAuthenticatedEmail(): Promise<string | null> {
165 return null;
166 }
167
168 @skipRpcValidation()
169 async getVerifier(): Promise<Fetcher<GatekeeperUserVerifier>> {
170 return this.ctx.exports.CustomVerifier({});
171 }
172}
173
174@validateRpc()
175export class CustomVerifier extends WorkerEntrypoint<Cloudflare.Env> implements GatekeeperUserVerifier {
176 verify(): void {}
177}
178
179@validateRpc()
180export class GatekeeperVendor extends WorkerEntrypoint<Cloudflare.Env> {
181 async describe(): Promise<VendorDescription> {
182 return describeCustomVendor(this.env.CUSTOM_NAME);
183 }
184
185 @skipRpcValidation()
186 async createAccount(): Promise<Fetcher<GatekeeperUser>> {
187 return this.ctx.exports.CustomAccount({});
188 }
189
190 connectAccount(
191 _callback: Fetcher<GatekeeperConnectCallback>,
192 _options?: GatekeeperConnectOptions,
193 ): Promise<{ url: string }> {
194 throw new Error("Custom Gatekeeper is auto-provisioned and has no connect flow.");
195 }
196
197 async getSupportedResources(_options?: { userId?: string }): Promise<SupportedResource[]> {
198 return [];
199 }
200
201 async getTypeScriptTypes(): Promise<string> {
202 return TYPES_CODE;
203 }
204}
205