1import { Credentials } from "@distilled.cloud/stripe";
2import * as Effect from "effect/Effect";
3import * as Redacted from "effect/Redacted";
4import { createPhysicalName } from "../PhysicalName.ts";
5import * as Provider from "../Provider.ts";
6import { Resource } from "../Resource.ts";
7import type { Providers } from "./Providers.ts";
8
9/**
10 * Stripe restricted-key permission. Names match the Dashboard RAK catalog
11 * (`resource_read` / `resource_write`). Write implies read.
12 *
13 * Stripe does not currently expose a public API to mint restricted keys
14 * (`POST /v2/iam/api_keys` is Stripe Apps private preview). Bindings still
15 * declare the permissions they need so a future mint can honor them.
16 */
17export type StripePermission =
18 | "accounts_read"
19 | "accounts_write"
20 | "apple_pay_domains_read"
21 | "apple_pay_domains_write"
22 | "apps_secrets_read"
23 | "apps_secrets_write"
24 | "billing_meters_read"
25 | "billing_meters_write"
26 | "billing_portal_read"
27 | "billing_portal_write"
28 | "checkout_sessions_read"
29 | "checkout_sessions_write"
30 | "coupons_read"
31 | "coupons_write"
32 | "credit_grants_read"
33 | "credit_grants_write"
34 | "customers_read"
35 | "customers_write"
36 | "entitlements_read"
37 | "entitlements_write"
38 | "files_read"
39 | "files_write"
40 | "issuing_read"
41 | "issuing_write"
42 | "payment_links_read"
43 | "payment_links_write"
44 | "payment_method_configurations_read"
45 | "payment_method_configurations_write"
46 | "payment_method_domains_read"
47 | "payment_method_domains_write"
48 | "plans_read"
49 | "plans_write"
50 | "prices_read"
51 | "prices_write"
52 | "products_read"
53 | "products_write"
54 | "promotion_codes_read"
55 | "promotion_codes_write"
56 | "radar_read"
57 | "radar_write"
58 | "shipping_rates_read"
59 | "shipping_rates_write"
60 | "tax_read"
61 | "tax_write"
62 | "terminal_read"
63 | "terminal_write"
64 | "webhook_endpoints_read"
65 | "webhook_endpoints_write";
66
67export interface RestrictedApiKeyProps {
68 /**
69 * Display name. If omitted, a unique name is generated from the stack,
70 * stage, and logical id.
71 */
72 name?: string;
73 /**
74 * Optional Dashboard-created restricted key (`rk_test_…` / `rk_live_…`).
75 * When omitted, the account secret from {@link Credentials} is used —
76 * Stripe has no public API to mint restricted keys.
77 */
78 value?: string;
79 /**
80 * Permissions this key should have. Additional permissions are merged
81 * in from bindings.
82 */
83 permissions?: StripePermission[];
84}
85
86/**
87 * Binding contract for {@link RestrictedApiKey}. A capability contributes
88 * the Stripe permissions it needs; they are merged onto the token.
89 */
90export type RestrictedApiKeyBinding = {
91 permissions?: StripePermission[];
92};
93
94export type RestrictedApiKey = Resource<
95 "Stripe.RestrictedApiKey",
96 RestrictedApiKeyProps,
97 {
98 /** Stable id (logical physical name — Stripe has no key object id). */
99 id: string;
100 /** Display name. */
101 name: string;
102 /** Secret or restricted key value. Returned only to Alchemy state. */
103 value: Redacted.Redacted<string>;
104 /** Union of prop and binding permissions. */
105 permissions: StripePermission[];
106 },
107 RestrictedApiKeyBinding,
108 Providers
109>;
110
111/**
112 * A least-privilege Stripe API key for a Function/Worker host.
113 *
114 * HTTP bindings call `token.bind(capability, { permissions })` the same
115 * way Cloudflare HTTP bindings attach policies to an AccountApiToken.
116 * Stripe restricted keys (`rk_…`) can only be created in the Dashboard
117 * today — `POST /v2/iam/api_keys` is Stripe Apps private preview. Until
118 * that API is generally available, this resource stores the collected
119 * permissions and injects either a user-supplied RAK or the account
120 * secret key.
121 *
122 * ### Creating a token
123 * **Example:** Host token, permissions from bindings
124 * ```typescript
125 * const token = yield* Stripe.RestrictedApiKey("ApiToken");
126 * yield* token.bind`RetrieveProduct`({
127 * permissions: ["products_read"],
128 * });
129 * ```
130 *
131 * **Example:** Dashboard-created restricted key
132 * ```typescript
133 * const token = yield* Stripe.RestrictedApiKey("ApiToken", {
134 * value: process.env.STRIPE_RESTRICTED_KEY,
135 * permissions: ["customers_read"],
136 * });
137 * ```
138 *
139 * @resource
140 * @product Secrets
141 */
142export const RestrictedApiKey = Resource<RestrictedApiKey>(
143 "Stripe.RestrictedApiKey",
144);
145
146const resolveName = (id: string, name: string | undefined) =>
147 Effect.gen(function* () {
148 return name ?? (yield* createPhysicalName({ id }));
149 });
150
151export const collectPermissions = (
152 props: StripePermission[] | undefined,
153 bindings: { data: RestrictedApiKeyBinding }[],
154): StripePermission[] => {
155 const set = new Set<StripePermission>(props ?? []);
156 for (const binding of bindings) {
157 for (const permission of binding.data.permissions ?? []) {
158 set.add(permission);
159 }
160 }
161 return [...set].sort();
162};
163
164export const RestrictedApiKeyProvider = () =>
165 Provider.succeed(RestrictedApiKey, {
166 stables: ["id"],
167
168 diff: Effect.fn(function* () {
169 return undefined;
170 }),
171
172 read: Effect.fn(function* ({ output }) {
173 return output;
174 }),
175
176 // Logical token only — there is no Stripe list of keys we created.
177 list: Effect.fn(function* () {
178 return [];
179 }),
180
181 reconcile: Effect.fn(function* ({ id, news, output, bindings }) {
182 const props = news ?? {};
183 const name = yield* resolveName(id, props.name);
184 const permissions = collectPermissions(props.permissions, bindings);
185 // TODO: Mint a real restricted key once Stripe publishes a public
186 // create-key API. `POST /v2/iam/api_keys` (Stripe Apps IAM preview)
187 // 404s as of 2026-08. Pass `permissions` (props + bindings) as the
188 // key's scopes, persist the returned `rk_…` as `value`, and teach
189 // list/delete to enumerate/revoke it. Until then this is a logical
190 // token: Dashboard `news.value` or the account secret.
191 const value =
192 props.value !== undefined
193 ? Redacted.make(props.value)
194 : yield* Effect.gen(function* () {
195 const resolve = yield* Credentials;
196 const cfg = yield* resolve;
197 return Redacted.make(Redacted.value(cfg.apiKey));
198 });
199 return {
200 id: output?.id ?? name,
201 name,
202 value,
203 permissions,
204 };
205 }),
206
207 delete: Effect.fn(function* () {
208 // Dashboard-created RAKs are not deleted; we never minted a Stripe
209 // object for the default account-secret path.
210 }),
211 });