Fork Horse repository, listed by its owner. Publisher snapshot.

packages/custom-gatekeeper/src/custom.ts

Snapshot at 34e3175

Containing directory

6,221 bytes · typescriptView raw
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