@pithy-sh/cloudflare 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai/aiManager.d.ts +99 -0
- package/dist/ai/aiManager.d.ts.map +1 -0
- package/dist/ai/aiManager.js +148 -0
- package/dist/ai/vectorizeManager.d.ts +67 -0
- package/dist/ai/vectorizeManager.d.ts.map +1 -0
- package/dist/ai/vectorizeManager.js +101 -0
- package/dist/ai/vectorizeProvisioner.d.ts +123 -0
- package/dist/ai/vectorizeProvisioner.d.ts.map +1 -0
- package/dist/ai/vectorizeProvisioner.js +195 -0
- package/dist/client/accounts.d.ts +48 -0
- package/dist/client/accounts.d.ts.map +1 -0
- package/dist/client/accounts.js +55 -0
- package/dist/client/clients.d.ts +122 -0
- package/dist/client/clients.d.ts.map +1 -0
- package/dist/client/clients.js +216 -0
- package/dist/client/errors.d.ts +73 -0
- package/dist/client/errors.d.ts.map +1 -0
- package/dist/client/errors.js +98 -0
- package/dist/client/manager.d.ts +45 -0
- package/dist/client/manager.d.ts.map +1 -0
- package/dist/client/manager.js +51 -0
- package/dist/d1/d1Manager.d.ts +74 -0
- package/dist/d1/d1Manager.d.ts.map +1 -0
- package/dist/d1/d1Manager.js +128 -0
- package/dist/d1/d1PreparedStatement.d.ts +24 -0
- package/dist/d1/d1PreparedStatement.d.ts.map +1 -0
- package/dist/d1/d1PreparedStatement.js +91 -0
- package/dist/d1/d1Provisioner.d.ts +28 -0
- package/dist/d1/d1Provisioner.d.ts.map +1 -0
- package/dist/d1/d1Provisioner.js +67 -0
- package/dist/email/emailRoutingManager.d.ts +36 -0
- package/dist/email/emailRoutingManager.d.ts.map +1 -0
- package/dist/email/emailRoutingManager.js +124 -0
- package/dist/email/emailSendManager.d.ts +45 -0
- package/dist/email/emailSendManager.d.ts.map +1 -0
- package/dist/email/emailSendManager.js +52 -0
- package/dist/env/devVars.d.ts +65 -0
- package/dist/env/devVars.d.ts.map +1 -0
- package/dist/env/devVars.js +84 -0
- package/dist/hostnames/customHostnamesManager.d.ts +51 -0
- package/dist/hostnames/customHostnamesManager.d.ts.map +1 -0
- package/dist/hostnames/customHostnamesManager.js +84 -0
- package/dist/kv/kvManager.d.ts +76 -0
- package/dist/kv/kvManager.d.ts.map +1 -0
- package/dist/kv/kvManager.js +135 -0
- package/dist/kv/kvProvisioner.d.ts +28 -0
- package/dist/kv/kvProvisioner.d.ts.map +1 -0
- package/dist/kv/kvProvisioner.js +71 -0
- package/dist/media/assetSeeder.d.ts +31 -0
- package/dist/media/assetSeeder.d.ts.map +1 -0
- package/dist/media/assetSeeder.js +70 -0
- package/dist/media/imageManager.d.ts +34 -0
- package/dist/media/imageManager.d.ts.map +1 -0
- package/dist/media/imageManager.js +103 -0
- package/dist/media/ownership.d.ts +40 -0
- package/dist/media/ownership.d.ts.map +1 -0
- package/dist/media/ownership.js +46 -0
- package/dist/media/streamManager.d.ts +83 -0
- package/dist/media/streamManager.d.ts.map +1 -0
- package/dist/media/streamManager.js +145 -0
- package/dist/queue/queueManager.d.ts +84 -0
- package/dist/queue/queueManager.d.ts.map +1 -0
- package/dist/queue/queueManager.js +114 -0
- package/dist/r2/r2Credentials.d.ts +12 -0
- package/dist/r2/r2Credentials.d.ts.map +1 -0
- package/dist/r2/r2Credentials.js +13 -0
- package/dist/r2/r2Manager.d.ts +226 -0
- package/dist/r2/r2Manager.d.ts.map +1 -0
- package/dist/r2/r2Manager.js +405 -0
- package/dist/r2/r2Provisioner.d.ts +39 -0
- package/dist/r2/r2Provisioner.d.ts.map +1 -0
- package/dist/r2/r2Provisioner.js +86 -0
- package/dist/secrets/secretsStoreManager.d.ts +82 -0
- package/dist/secrets/secretsStoreManager.d.ts.map +1 -0
- package/dist/secrets/secretsStoreManager.js +147 -0
- package/dist/secrets/secretsStores.d.ts +47 -0
- package/dist/secrets/secretsStores.d.ts.map +1 -0
- package/dist/secrets/secretsStores.js +65 -0
- package/dist/test-utils/emailRoutingRules.d.ts +39 -0
- package/dist/test-utils/emailRoutingRules.d.ts.map +1 -0
- package/dist/test-utils/emailRoutingRules.js +89 -0
- package/dist/test-utils/fixtureReportSetup.d.ts +25 -0
- package/dist/test-utils/fixtureReportSetup.d.ts.map +1 -0
- package/dist/test-utils/fixtureReportSetup.js +30 -0
- package/dist/test-utils/fixtures.d.ts +224 -0
- package/dist/test-utils/fixtures.d.ts.map +1 -0
- package/dist/test-utils/fixtures.js +277 -0
- package/dist/test-utils/harness.d.ts +192 -0
- package/dist/test-utils/harness.d.ts.map +1 -0
- package/dist/test-utils/harness.js +351 -0
- package/dist/test-utils/inboundRecorder.d.ts +91 -0
- package/dist/test-utils/inboundRecorder.d.ts.map +1 -0
- package/dist/test-utils/inboundRecorder.js +141 -0
- package/dist/test-utils/integrationSetup.d.ts +22 -0
- package/dist/test-utils/integrationSetup.d.ts.map +1 -0
- package/dist/test-utils/integrationSetup.js +37 -0
- package/dist/test-utils/reap.d.ts +112 -0
- package/dist/test-utils/reap.d.ts.map +1 -0
- package/dist/test-utils/reap.js +185 -0
- package/dist/tokens/accountTokensManager.d.ts +157 -0
- package/dist/tokens/accountTokensManager.d.ts.map +1 -0
- package/dist/tokens/accountTokensManager.js +280 -0
- package/dist/tokens/permissions.d.ts +36 -0
- package/dist/tokens/permissions.d.ts.map +1 -0
- package/dist/tokens/permissions.js +50 -0
- package/dist/tokens/profiles.d.ts +102 -0
- package/dist/tokens/profiles.d.ts.map +1 -0
- package/dist/tokens/profiles.js +137 -0
- package/dist/turnstile/turnstileManager.d.ts +75 -0
- package/dist/turnstile/turnstileManager.d.ts.map +1 -0
- package/dist/turnstile/turnstileManager.js +138 -0
- package/dist/user/userManager.d.ts +36 -0
- package/dist/user/userManager.d.ts.map +1 -0
- package/dist/user/userManager.js +58 -0
- package/dist/workers/buildsManager.d.ts +116 -0
- package/dist/workers/buildsManager.d.ts.map +1 -0
- package/dist/workers/buildsManager.js +237 -0
- package/dist/workers/buildsTypes.d.ts +102 -0
- package/dist/workers/buildsTypes.d.ts.map +1 -0
- package/dist/workers/buildsTypes.js +87 -0
- package/dist/workers/workersBuildEvent.d.ts +64 -0
- package/dist/workers/workersBuildEvent.d.ts.map +1 -0
- package/dist/workers/workersBuildEvent.js +47 -0
- package/dist/workers/workersManager.d.ts +136 -0
- package/dist/workers/workersManager.d.ts.map +1 -0
- package/dist/workers/workersManager.js +312 -0
- package/dist/workers/workersProvisioner.d.ts +80 -0
- package/dist/workers/workersProvisioner.d.ts.map +1 -0
- package/dist/workers/workersProvisioner.js +106 -0
- package/dist/workflows/stepFailure.d.ts +180 -0
- package/dist/workflows/stepFailure.d.ts.map +1 -0
- package/dist/workflows/stepFailure.js +227 -0
- package/dist/workflows/workflowsClient.d.ts +87 -0
- package/dist/workflows/workflowsClient.d.ts.map +1 -0
- package/dist/workflows/workflowsClient.js +156 -0
- package/dist/zones/zonesManager.d.ts +50 -0
- package/dist/zones/zonesManager.d.ts.map +1 -0
- package/dist/zones/zonesManager.js +74 -0
- package/package.json +18 -9
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"accountTokensManager.d.ts","sourceRoot":"","sources":["../../src/tokens/accountTokensManager.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AASxB,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAE1D;;;;;GAKG;AACH,MAAM,WAAW,eAAe;IAC9B,kHAAkH;IAClH,oBAAoB,EAAE,MAAM,EAAE,CAAC;IAC/B,kGAAkG;IAClG,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAClC,2EAA2E;IAC3E,MAAM,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;CAC3B;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAEzE;AAED;;;GAGG;AACH,eAAO,MAAM,iBAAiB;;;iBAK8E,CAAC;AAC7G,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAEnE;;;;GAIG;AACH,eAAO,MAAM,kBAAkB;;;;;;;;;iBAO6D,CAAC;AAC7F,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAErE;;;;GAIG;AACH,eAAO,MAAM,mBAAmB;;;;;;;;iBAMuD,CAAC;AACxF,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEvE;;;;;;;;;GASG;AACH,qBAAa,8BAA+B,SAAQ,iBAAiB;IACnE,cAAc,IAAI,MAAM,CAEvB;IAED,qGAAqG;IAC/F,qBAAqB,IAAI,OAAO,CAAC,OAAO,CAAC,CAO9C;IAED;;;;;;;OAOG;IACG,WAAW,IAAI,OAAO,CAAC,mBAAmB,CAAC,CAKhD;IAED;;;;;;;;;;;;OAYG;IACG,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAW1D;IAED,qGAAqG;IAC/F,oBAAoB,IAAI,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAWzD;IAED;;;;;;OAMG;IACG,uBAAuB,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC,CAE7E;IAED;;;;;;;OAOG;IACG,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,eAAe,EAAE,GAAG,OAAO,CAAC,kBAAkB,CAAC,CA2BzF;IAED;;;;;OAKG;IACH,OAAO,CAAC,mBAAmB;IAqB3B,2GAA2G;IACrG,UAAU,IAAI,OAAO,CAAC,mBAAmB,EAAE,CAAC,CASjD;IAED,oGAAoG;IAC9F,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,GAAG,IAAI,CAAC,CAQvE;IAED,qCAAqC;IAC/B,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAIhD;IAED;;;;OAIG;IACG,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAYtD;IAED;;;;;OAKG;IACG,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAKrD;IAED;;;;;;OAMG;IACG,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,eAAe,EAAE,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAKzF;CACF"}
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import { CloudflareNotConfiguredError, CloudflareRequestError, cloudflareRequest, decodeResponse, isAuthorizationError, messageOf } from "../client/errors.js";
|
|
2
|
+
import { CloudflareManager } from "../client/manager.js";
|
|
3
|
+
import { CfTokenVerification } from "../user/userManager.js";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
//#region src/tokens/accountTokensManager.ts
|
|
6
|
+
/**
|
|
7
|
+
* The whole-account resource scope for a token policy: `{ "com.cloudflare.api.account.<id>": "*" }`.
|
|
8
|
+
* Account-level permission groups (Secrets Store, D1, Workers, …) scope to this. One helper so the
|
|
9
|
+
* exact resource key format lives in a single place instead of being hand-spelled per use case.
|
|
10
|
+
*/
|
|
11
|
+
function accountResource(accountId) {
|
|
12
|
+
return { [`com.cloudflare.api.account.${accountId}`]: "*" };
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* A permission group available to account-owned tokens, as returned by the permission-groups list.
|
|
16
|
+
* Only the id and name matter for resolution — the manager maps a requested name to its id.
|
|
17
|
+
*/
|
|
18
|
+
const CfPermissionGroup = z.object({
|
|
19
|
+
id: z.string().describe("The CF-assigned permission-group id, referenced in a token policy."),
|
|
20
|
+
name: z.string().describe("The human-readable permission-group name (e.g. 'Secrets Store Read').")
|
|
21
|
+
}).describe("A Cloudflare account-token permission group: the id a policy references and its display name.");
|
|
22
|
+
/**
|
|
23
|
+
* A freshly minted account-owned token. The `value` is the secret bearer credential — present **only**
|
|
24
|
+
* in the create response, never re-readable — so it is captured here and must be stored at once
|
|
25
|
+
* (e.g. into the Secrets Store). Never log it.
|
|
26
|
+
*/
|
|
27
|
+
const MintedAccountToken = z.object({
|
|
28
|
+
id: z.string().describe("The CF-assigned token id, used to address the token for get/delete."),
|
|
29
|
+
value: z.string().describe("The secret token value — returned once on create, never again. Store immediately."),
|
|
30
|
+
name: z.string().optional().describe("The token name as registered with Cloudflare."),
|
|
31
|
+
status: z.enum([
|
|
32
|
+
"active",
|
|
33
|
+
"disabled",
|
|
34
|
+
"expired"
|
|
35
|
+
]).optional().describe("The token's lifecycle status.")
|
|
36
|
+
}).describe("A newly created account-owned API token, including its one-time secret value.");
|
|
37
|
+
/**
|
|
38
|
+
* An existing account-owned token's metadata, as returned by list/get. No `value` — Cloudflare never
|
|
39
|
+
* returns a token's secret after creation — so this is only enough to find a token by name and
|
|
40
|
+
* address it for deletion.
|
|
41
|
+
*/
|
|
42
|
+
const AccountTokenSummary = z.object({
|
|
43
|
+
id: z.string().describe("The CF-assigned token id, used to address the token for deletion."),
|
|
44
|
+
name: z.string().describe("The token name, the key callers match on for idempotent re-mint."),
|
|
45
|
+
status: z.enum([
|
|
46
|
+
"active",
|
|
47
|
+
"disabled",
|
|
48
|
+
"expired"
|
|
49
|
+
]).optional().describe("The token's lifecycle status.")
|
|
50
|
+
}).describe("An existing account-owned API token's metadata (never its secret value).");
|
|
51
|
+
/**
|
|
52
|
+
* Out-of-Worker control plane for **account-owned** Cloudflare API tokens: mint scoped, least-privilege
|
|
53
|
+
* tokens, find them by name, and delete them. Account-owned (not user-bound) by design — CLAUDE.md
|
|
54
|
+
* prefers org-level tokens that outlive any one person.
|
|
55
|
+
*
|
|
56
|
+
* This is the reusable seam behind every "pithy mints its own scoped token" case. A caller hands it a
|
|
57
|
+
* token name and a set of {@link TokenPermission}s (permission-group names + resource scope); the
|
|
58
|
+
* manager resolves the names to ids against the live account list and creates the token. The secrets
|
|
59
|
+
* manager's runtime credential is the first concrete use; more follow without touching this class.
|
|
60
|
+
*/
|
|
61
|
+
var CloudflareAccountTokensManager = class extends CloudflareManager {
|
|
62
|
+
getServiceType() {
|
|
63
|
+
return "Cloudflare Account API Tokens";
|
|
64
|
+
}
|
|
65
|
+
/** Prove access by listing permission groups — a read, never a token create/delete. Never throws. */
|
|
66
|
+
async validateServiceAccess() {
|
|
67
|
+
try {
|
|
68
|
+
await this.listPermissionGroups();
|
|
69
|
+
return true;
|
|
70
|
+
} catch {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Verify the **calling** token against this account — `GET /accounts/{id}/tokens/verify`.
|
|
76
|
+
*
|
|
77
|
+
* Distinct from {@link CloudflareUserManager.verifyToken}, which hits the *user*-scoped
|
|
78
|
+
* `/user/tokens/verify` and returns `Invalid API Token` for an account-owned (`cfat_*`) credential.
|
|
79
|
+
* Since `CLAUDE.md` prefers account-owned tokens over user-bound ones, the user endpoint is the wrong
|
|
80
|
+
* one for the common case, and using it reports a working token as invalid.
|
|
81
|
+
*/
|
|
82
|
+
async verifyToken() {
|
|
83
|
+
const raw = await cloudflareRequest("verify account token", () => this.getClient().accounts.tokens.verify({ account_id: this.accountId }));
|
|
84
|
+
return decodeResponse(CfTokenVerification, raw, "account token verify");
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* The name of an account-owned token by id, or `null` — `GET /accounts/{id}/tokens/{token_id}`.
|
|
88
|
+
*
|
|
89
|
+
* Distinct from {@link CloudflareUserManager.getTokenName}, which hits `GET /user/tokens/:id` and
|
|
90
|
+
* answers `9109 — Valid user-level authentication not found` for a `cfat_*` caller. Both halves of
|
|
91
|
+
* the account-token identity read — verify and name — are account-scoped, or neither is.
|
|
92
|
+
*
|
|
93
|
+
* **Best effort, by contract.** Reading a token record needs `API Tokens Read`, which the
|
|
94
|
+
* least-privilege tokens {@link mintToken} produces deliberately do not carry: a 403 is the normal
|
|
95
|
+
* answer for a CI credential asking its own name, not a fault. So a denied read returns `null` and
|
|
96
|
+
* the caller falls back to the token id from {@link verifyToken} — which any account token can call
|
|
97
|
+
* with no permission at all. Every other failure still throws.
|
|
98
|
+
*/
|
|
99
|
+
async getTokenName(tokenId) {
|
|
100
|
+
const raw = await cloudflareRequest(`get account token ${tokenId}`, async () => {
|
|
101
|
+
try {
|
|
102
|
+
return await this.getClient().accounts.tokens.get(tokenId, { account_id: this.accountId });
|
|
103
|
+
} catch (error) {
|
|
104
|
+
if (isAuthorizationError(error)) return null;
|
|
105
|
+
throw error;
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
const parsed = TokenName.safeParse(raw);
|
|
109
|
+
return parsed.success ? parsed.data.name : null;
|
|
110
|
+
}
|
|
111
|
+
/** Every permission group available to account-owned tokens in this account (SDK auto-paginates). */
|
|
112
|
+
async listPermissionGroups() {
|
|
113
|
+
return cloudflareRequest("list account token permission groups", async () => {
|
|
114
|
+
const out = [];
|
|
115
|
+
for await (const group of this.getClient().accounts.tokens.permissionGroups.list({ account_id: this.accountId })) {
|
|
116
|
+
const parsed = CfPermissionGroup.safeParse(group);
|
|
117
|
+
if (parsed.success) out.push(parsed.data);
|
|
118
|
+
}
|
|
119
|
+
return out;
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Resolve permission-group names to their `{ id }` references against the account's live group list.
|
|
124
|
+
* Throws a clear, actionable error naming any name that does not exist, or any name that is
|
|
125
|
+
* **ambiguous** — Cloudflare reuses display names across resource scopes (e.g. account vs zone), so a
|
|
126
|
+
* name that maps to more than one id can't be resolved without picking the wrong scope. Either way a
|
|
127
|
+
* typo'd or ambiguous group fails loudly at mint time, never silently widening or narrowing scope.
|
|
128
|
+
*/
|
|
129
|
+
async resolvePermissionGroups(names) {
|
|
130
|
+
return this.resolveAgainstIndex(indexByName(await this.listPermissionGroups()), names);
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Mint a new account-owned token named `name` carrying `permissions`. Resolves every permission
|
|
134
|
+
* group name to its id (one group-list fetch for all policies), builds the CF policy set, and
|
|
135
|
+
* creates the token. Returns the token's one-time secret `value` — store it immediately. A 403 (the
|
|
136
|
+
* calling token cannot create tokens) is reraised as an actionable "grant 'Account API Tokens
|
|
137
|
+
* Write'" error so the cause is obvious; this doubles as the fail-fast preflight when provisioning
|
|
138
|
+
* runs the mint first.
|
|
139
|
+
*/
|
|
140
|
+
async mintToken(name, permissions) {
|
|
141
|
+
const index = indexByName(await this.listPermissionGroups());
|
|
142
|
+
const policies = permissions.map((permission) => ({
|
|
143
|
+
effect: permission.effect ?? "allow",
|
|
144
|
+
permission_groups: this.resolveAgainstIndex(index, permission.permissionGroupNames),
|
|
145
|
+
resources: permission.resources
|
|
146
|
+
}));
|
|
147
|
+
let raw;
|
|
148
|
+
try {
|
|
149
|
+
raw = await this.getClient().accounts.tokens.create({
|
|
150
|
+
account_id: this.accountId,
|
|
151
|
+
name,
|
|
152
|
+
policies
|
|
153
|
+
});
|
|
154
|
+
} catch (error) {
|
|
155
|
+
if (isAuthorizationError(error)) throw new CloudflareNotConfiguredError({
|
|
156
|
+
message: "The Cloudflare API token is not allowed to create account tokens.",
|
|
157
|
+
action: "Grant it 'Account API Tokens Write' (Account → API Tokens → Edit), then re-run.",
|
|
158
|
+
detail: `mint account token '${name}': ${messageOf(error)}`
|
|
159
|
+
}, { cause: error });
|
|
160
|
+
throw new CloudflareRequestError({
|
|
161
|
+
message: `Failed to mint account token '${name}'.`,
|
|
162
|
+
detail: messageOf(error)
|
|
163
|
+
}, { cause: error });
|
|
164
|
+
}
|
|
165
|
+
return decodeResponse(MintedAccountToken, raw, "account token create");
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Resolve names against a prefetched name→ids index, throwing on any unknown or ambiguous name.
|
|
169
|
+
* The shared core of {@link resolvePermissionGroups} and {@link mintToken}: take the group list once,
|
|
170
|
+
* resolve every name against it. Ambiguity (a name with more than one id) is a hard error, not a
|
|
171
|
+
* silent pick — see {@link resolvePermissionGroups}.
|
|
172
|
+
*/
|
|
173
|
+
resolveAgainstIndex(index, names) {
|
|
174
|
+
const unknown = names.filter((name) => !index.has(name));
|
|
175
|
+
if (unknown.length > 0) throw new CloudflareNotConfiguredError({
|
|
176
|
+
message: `Unknown Cloudflare permission group(s): ${unknown.join(", ")}.`,
|
|
177
|
+
action: "Check the exact permission-group names against the account's available groups.",
|
|
178
|
+
detail: `resolve permission groups: not found in account ${this.accountId} — ${unknown.join(", ")}`
|
|
179
|
+
});
|
|
180
|
+
const ambiguous = names.filter((name) => (index.get(name)?.length ?? 0) > 1);
|
|
181
|
+
if (ambiguous.length > 0) throw new CloudflareNotConfiguredError({
|
|
182
|
+
message: `Ambiguous Cloudflare permission group(s): ${ambiguous.join(", ")}.`,
|
|
183
|
+
action: "These names map to more than one permission group; scope the token by a unique group name.",
|
|
184
|
+
detail: `resolve permission groups: name maps to multiple ids in account ${this.accountId} — ${ambiguous.join(", ")}`
|
|
185
|
+
});
|
|
186
|
+
return names.map((name) => ({ id: index.get(name)[0] }));
|
|
187
|
+
}
|
|
188
|
+
/** Every account-owned token's metadata (SDK auto-paginates). No secret values — list is metadata only. */
|
|
189
|
+
async listTokens() {
|
|
190
|
+
return cloudflareRequest("list account tokens", async () => {
|
|
191
|
+
const out = [];
|
|
192
|
+
for await (const token of this.getClient().accounts.tokens.list({ account_id: this.accountId })) {
|
|
193
|
+
const parsed = AccountTokenSummary.safeParse(token);
|
|
194
|
+
if (parsed.success) out.push(parsed.data);
|
|
195
|
+
}
|
|
196
|
+
return out;
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
/** Find an account token by exact name, or `null` — for idempotent re-mint (SDK auto-paginates). */
|
|
200
|
+
async findTokenByName(name) {
|
|
201
|
+
return cloudflareRequest(`find account token ${name}`, async () => {
|
|
202
|
+
for await (const token of this.getClient().accounts.tokens.list({ account_id: this.accountId })) {
|
|
203
|
+
const parsed = AccountTokenSummary.safeParse(token);
|
|
204
|
+
if (parsed.success && parsed.data.name === name) return parsed.data;
|
|
205
|
+
}
|
|
206
|
+
return null;
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
/** Delete an account token by id. */
|
|
210
|
+
async deleteToken(tokenId) {
|
|
211
|
+
await cloudflareRequest(`delete account token ${tokenId}`, () => this.getClient().accounts.tokens.delete(tokenId, { account_id: this.accountId }));
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Delete every account token with the given name; returns how many were removed. Names are not
|
|
215
|
+
* unique in Cloudflare, so any duplicates (e.g. a prior interrupted run) are swept here. A no-op
|
|
216
|
+
* when none match. Used by teardown.
|
|
217
|
+
*/
|
|
218
|
+
async deleteTokensByName(name) {
|
|
219
|
+
return cloudflareRequest(`delete account tokens named ${name}`, async () => {
|
|
220
|
+
const ids = [];
|
|
221
|
+
for await (const token of this.getClient().accounts.tokens.list({ account_id: this.accountId })) {
|
|
222
|
+
const parsed = AccountTokenSummary.safeParse(token);
|
|
223
|
+
if (parsed.success && parsed.data.name === name) ids.push(parsed.data.id);
|
|
224
|
+
}
|
|
225
|
+
for (const id of ids) await this.getClient().accounts.tokens.delete(id, { account_id: this.accountId });
|
|
226
|
+
return ids.length;
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Roll an existing token's secret: regenerate its value **in place**, keeping the same token id,
|
|
231
|
+
* name, and policies (the dashboard's "Roll" action). Returns the new secret value — the only time
|
|
232
|
+
* Cloudflare hands it back — so it must be stored at once. The seam the deferred value-rotation
|
|
233
|
+
* builds on: a credential self-rolls without ever changing identity.
|
|
234
|
+
*/
|
|
235
|
+
async rollTokenValue(tokenId) {
|
|
236
|
+
const raw = await cloudflareRequest(`roll account token value ${tokenId}`, () => this.getClient().accounts.tokens.value.update(tokenId, {
|
|
237
|
+
account_id: this.accountId,
|
|
238
|
+
body: {}
|
|
239
|
+
}));
|
|
240
|
+
return decodeResponse(RolledTokenValue, raw, "account token value roll");
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Ensure a named token exists and return a **fresh** secret for it. If a token of this name already
|
|
244
|
+
* exists, roll its value in place (same id and policies, new secret); otherwise mint a new one.
|
|
245
|
+
* Either way the caller gets a usable secret to store — the idempotent path for a credential that
|
|
246
|
+
* lives in the Secrets Store, since Cloudflare never returns a token's existing secret. Mirrors the
|
|
247
|
+
* dashboard's roll-or-create.
|
|
248
|
+
*/
|
|
249
|
+
async rollToken(name, permissions) {
|
|
250
|
+
const existing = await this.findTokenByName(name);
|
|
251
|
+
if (!existing) return this.mintToken(name, permissions);
|
|
252
|
+
const value = await this.rollTokenValue(existing.id);
|
|
253
|
+
return {
|
|
254
|
+
id: existing.id,
|
|
255
|
+
value,
|
|
256
|
+
name: existing.name,
|
|
257
|
+
status: existing.status
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
/** The rolled secret value Cloudflare returns from a value-roll — a non-empty bearer string. */
|
|
262
|
+
const RolledTokenValue = z.string().min(1);
|
|
263
|
+
/** Just the token's name, the piece the account-token actor path needs. */
|
|
264
|
+
const TokenName = z.object({ name: z.string().describe("The token's registered name — the audit actor id for an account token.") }).describe("The one field a token-record read is narrowed to: its name.");
|
|
265
|
+
/**
|
|
266
|
+
* Index permission groups by display name, collecting **every** id that bears a name. A list rather
|
|
267
|
+
* than one id per name on purpose: Cloudflare reuses display names across scopes, and a name with
|
|
268
|
+
* more than one id is ambiguous — the resolver rejects it rather than silently pick one.
|
|
269
|
+
*/
|
|
270
|
+
function indexByName(groups) {
|
|
271
|
+
const index = /* @__PURE__ */ new Map();
|
|
272
|
+
for (const group of groups) {
|
|
273
|
+
const ids = index.get(group.name) ?? [];
|
|
274
|
+
ids.push(group.id);
|
|
275
|
+
index.set(group.name, ids);
|
|
276
|
+
}
|
|
277
|
+
return index;
|
|
278
|
+
}
|
|
279
|
+
//#endregion
|
|
280
|
+
export { AccountTokenSummary, CfPermissionGroup, CloudflareAccountTokensManager, MintedAccountToken, accountResource };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The permission catalog: short, stable permission **keys** (`d1:read`) mapped to the Cloudflare
|
|
3
|
+
* account **permission-group display names** they grant (`"D1 Read"`). Token profiles are declared in
|
|
4
|
+
* these keys, so the profiles read cleanly and the CF-specific names live in one place. The names are
|
|
5
|
+
* resolved to permission-group **ids** against the live account at mint time
|
|
6
|
+
* (`CloudflareAccountTokensManager.resolvePermissionGroups`), which fails loudly on an unknown or
|
|
7
|
+
* ambiguous name — so a name that drifts from Cloudflare's catalog is caught at mint, never silently
|
|
8
|
+
* mis-scoped. Verify the exact names against the account when adding a key; an adopter can override a
|
|
9
|
+
* profile's keys if their account differs.
|
|
10
|
+
*/
|
|
11
|
+
export declare const PERMISSION_GROUPS: {
|
|
12
|
+
readonly "d1:read": readonly ["D1 Read"];
|
|
13
|
+
readonly "d1:write": readonly ["D1 Write"];
|
|
14
|
+
readonly "workers:write": readonly ["Workers Scripts Write"];
|
|
15
|
+
readonly "secrets:read": readonly ["Secrets Store Read"];
|
|
16
|
+
readonly "secrets:write": readonly ["Secrets Store Write"];
|
|
17
|
+
readonly "email:routing": readonly ["Email Routing Rules Write"];
|
|
18
|
+
readonly "kv:write": readonly ["Workers KV Storage Write"];
|
|
19
|
+
readonly "r2:read": readonly ["Workers R2 Storage Read"];
|
|
20
|
+
readonly "r2:write": readonly ["Workers R2 Storage Write"];
|
|
21
|
+
readonly "vectorize:read": readonly ["Vectorize Read"];
|
|
22
|
+
readonly "vectorize:write": readonly ["Vectorize Write"];
|
|
23
|
+
readonly "ai:read": readonly ["Workers AI Read"];
|
|
24
|
+
readonly "zone:read": readonly ["Zone Read"];
|
|
25
|
+
};
|
|
26
|
+
/** A known permission key — one of {@link PERMISSION_GROUPS}'s keys. */
|
|
27
|
+
export type PermissionKey = keyof typeof PERMISSION_GROUPS;
|
|
28
|
+
/** Narrow an arbitrary string to a {@link PermissionKey}. */
|
|
29
|
+
export declare function isPermissionKey(key: string): key is PermissionKey;
|
|
30
|
+
/**
|
|
31
|
+
* Resolve permission keys to the CF permission-group display names they grant, de-duped in first-seen
|
|
32
|
+
* order. An unknown key fails with an actionable error naming the valid keys — caught at the CLI/config
|
|
33
|
+
* boundary before any CF call.
|
|
34
|
+
*/
|
|
35
|
+
export declare function resolvePermissionKeys(keys: string[]): string[];
|
|
36
|
+
//# sourceMappingURL=permissions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"permissions.d.ts","sourceRoot":"","sources":["../../src/tokens/permissions.ts"],"names":[],"mappings":"AAKA;;;;;;;;;GASG;AACH,eAAO,MAAM,iBAAiB;aAC5B,SAAS,YAAG,SAAS;aAIrB,UAAU,YAAG,UAAU;aACvB,eAAe,YAAG,uBAAuB;aACzC,cAAc,YAAG,oBAAoB;aACrC,eAAe,YAAG,qBAAqB;aACvC,eAAe,YAAG,2BAA2B;aAC7C,UAAU,YAAG,0BAA0B;aACvC,SAAS,YAAG,yBAAyB;aACrC,UAAU,YAAG,0BAA0B;aACvC,gBAAgB,YAAG,gBAAgB;aACnC,iBAAiB,YAAG,iBAAiB;aACrC,SAAS,YAAG,iBAAiB;aAK7B,WAAW,YAAG,WAAW;CACjB,CAAC;AAEX,wEAAwE;AACxE,MAAM,MAAM,aAAa,GAAG,MAAM,OAAO,iBAAiB,CAAC;AAE3D,6DAA6D;AAC7D,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,IAAI,aAAa,CAEjE;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAe9D"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { CloudflareNotConfiguredError } from "../client/errors.js";
|
|
2
|
+
//#region src/tokens/permissions.ts
|
|
3
|
+
/**
|
|
4
|
+
* The permission catalog: short, stable permission **keys** (`d1:read`) mapped to the Cloudflare
|
|
5
|
+
* account **permission-group display names** they grant (`"D1 Read"`). Token profiles are declared in
|
|
6
|
+
* these keys, so the profiles read cleanly and the CF-specific names live in one place. The names are
|
|
7
|
+
* resolved to permission-group **ids** against the live account at mint time
|
|
8
|
+
* (`CloudflareAccountTokensManager.resolvePermissionGroups`), which fails loudly on an unknown or
|
|
9
|
+
* ambiguous name — so a name that drifts from Cloudflare's catalog is caught at mint, never silently
|
|
10
|
+
* mis-scoped. Verify the exact names against the account when adding a key; an adopter can override a
|
|
11
|
+
* profile's keys if their account differs.
|
|
12
|
+
*/
|
|
13
|
+
const PERMISSION_GROUPS = {
|
|
14
|
+
"d1:read": ["D1 Read"],
|
|
15
|
+
"d1:write": ["D1 Write"],
|
|
16
|
+
"workers:write": ["Workers Scripts Write"],
|
|
17
|
+
"secrets:read": ["Secrets Store Read"],
|
|
18
|
+
"secrets:write": ["Secrets Store Write"],
|
|
19
|
+
"email:routing": ["Email Routing Rules Write"],
|
|
20
|
+
"kv:write": ["Workers KV Storage Write"],
|
|
21
|
+
"r2:read": ["Workers R2 Storage Read"],
|
|
22
|
+
"r2:write": ["Workers R2 Storage Write"],
|
|
23
|
+
"vectorize:read": ["Vectorize Read"],
|
|
24
|
+
"vectorize:write": ["Vectorize Write"],
|
|
25
|
+
"ai:read": ["Workers AI Read"],
|
|
26
|
+
"zone:read": ["Zone Read"]
|
|
27
|
+
};
|
|
28
|
+
/** Narrow an arbitrary string to a {@link PermissionKey}. */
|
|
29
|
+
function isPermissionKey(key) {
|
|
30
|
+
return key in PERMISSION_GROUPS;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Resolve permission keys to the CF permission-group display names they grant, de-duped in first-seen
|
|
34
|
+
* order. An unknown key fails with an actionable error naming the valid keys — caught at the CLI/config
|
|
35
|
+
* boundary before any CF call.
|
|
36
|
+
*/
|
|
37
|
+
function resolvePermissionKeys(keys) {
|
|
38
|
+
const names = [];
|
|
39
|
+
for (const key of keys) {
|
|
40
|
+
if (!isPermissionKey(key)) throw new CloudflareNotConfiguredError({
|
|
41
|
+
message: `Unknown token permission key: ${key}.`,
|
|
42
|
+
action: `Use one of: ${Object.keys(PERMISSION_GROUPS).join(", ")}.`,
|
|
43
|
+
detail: `resolve permission keys: ${key} is not in the permission catalog`
|
|
44
|
+
});
|
|
45
|
+
for (const name of PERMISSION_GROUPS[key]) if (!names.includes(name)) names.push(name);
|
|
46
|
+
}
|
|
47
|
+
return names;
|
|
48
|
+
}
|
|
49
|
+
//#endregion
|
|
50
|
+
export { PERMISSION_GROUPS, isPermissionKey, resolvePermissionKeys };
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { Capability, TokenProfileSeam } from "@pithy-sh/core/src/capability/capability";
|
|
2
|
+
import { type TokenPermission } from "./accountTokensManager";
|
|
3
|
+
import { type PermissionKey } from "./permissions";
|
|
4
|
+
/**
|
|
5
|
+
* Where a minted token's value is written.
|
|
6
|
+
*
|
|
7
|
+
* - `secrets-store` — the CF Secrets Store; a Worker reads it via its binding. The destination for a
|
|
8
|
+
* worker-consumer token; resolved from the token's declared secret (its registry backend) unless a
|
|
9
|
+
* profile or `--store` names it directly.
|
|
10
|
+
* - `dev-vars` — `<config>/<project>/tokens.json`, keyed by environment and outside every checkout.
|
|
11
|
+
* **Read by an operator, never by a command** — nothing resolves a credential from it. The store for
|
|
12
|
+
* the `ci-system` token: mint it here, read the value
|
|
13
|
+
* out, and set it as CI's `CLOUDFLARE_API_TOKEN`. It wrote `.dev.vars.<env>` *inside* the project until
|
|
14
|
+
* #182; the name stays because it is a public `--store` flag value.
|
|
15
|
+
* - `ephemeral` — nothing is written; the value is used in-process and discarded. The one-step CI
|
|
16
|
+
* path — CI can't read the CF Secrets Store, so it mints and uses the token in the same job.
|
|
17
|
+
*/
|
|
18
|
+
export type TokenStore = "secrets-store" | "dev-vars" | "ephemeral";
|
|
19
|
+
/** The valid stores, for validating a `--store` flag or a profile's `defaultStore`. */
|
|
20
|
+
export declare const TOKEN_STORES: readonly TokenStore[];
|
|
21
|
+
/** The name of the one CI system token — the single least-privilege credential a CI pipeline runs under. */
|
|
22
|
+
export declare const CI_SYSTEM_PROFILE = "ci-system";
|
|
23
|
+
/**
|
|
24
|
+
* A named token profile: the CF permissions a job needs (short {@link PermissionKey}s), the resource
|
|
25
|
+
* scope, the **secret** its minted value is stored under (whose registry backend is the destination),
|
|
26
|
+
* and an optional built-in store override. The consuming code is the source of truth for its access;
|
|
27
|
+
* adopters override any field per profile.
|
|
28
|
+
*/
|
|
29
|
+
export interface TokenProfile {
|
|
30
|
+
/** The profile's stable name — the key it is minted, listed, and revoked under. */
|
|
31
|
+
name: string;
|
|
32
|
+
/** The CF permissions the token grants, as short catalog keys (resolved to group names at mint). */
|
|
33
|
+
permissions: PermissionKey[];
|
|
34
|
+
/** The resource scope: `"account"` (the default) or an explicit CF resource map. */
|
|
35
|
+
resources?: "account" | Record<string, string>;
|
|
36
|
+
/** The secret-registry name the minted value is stored under; its declared backend is the destination. */
|
|
37
|
+
secret: string;
|
|
38
|
+
/**
|
|
39
|
+
* Whether the value behind {@link secret} differs per environment (the default) or is one value every
|
|
40
|
+
* environment shares. It decides the environment segment of the **CF Secrets Store entry name** the
|
|
41
|
+
* minted value is written to, and nothing else — never the variable key, which is a variable name
|
|
42
|
+
* and stays verbatim. A `global` profile writes one entry with the literal `global` in that slot,
|
|
43
|
+
* matching what provisioning wrote; an `environment` profile writes one entry per environment.
|
|
44
|
+
*/
|
|
45
|
+
secretScope?: TokenSecretScope;
|
|
46
|
+
/** A built-in destination override (`dev-vars`/`ephemeral`/`secrets-store`); else the secret's backend. */
|
|
47
|
+
defaultStore?: TokenStore;
|
|
48
|
+
/** Why this profile exists / what job consumes it. */
|
|
49
|
+
description: string;
|
|
50
|
+
}
|
|
51
|
+
/** Whether a profile's secret is one value per environment or one value shared by all of them. */
|
|
52
|
+
export type TokenSecretScope = "environment" | "global";
|
|
53
|
+
/** An adopter's override of a profile's defaults (from `pithy.config.ts` or CLI flags). Every field optional. */
|
|
54
|
+
export interface ProfileOverride {
|
|
55
|
+
/** Replace the profile's permission keys. */
|
|
56
|
+
permissions?: PermissionKey[];
|
|
57
|
+
/** Replace the profile's resource scope with an explicit CF resource map. */
|
|
58
|
+
resources?: Record<string, string>;
|
|
59
|
+
/** Replace where the value is written (`--store`). */
|
|
60
|
+
store?: TokenStore;
|
|
61
|
+
}
|
|
62
|
+
/** The env-var / secret name a profile's token is stored under: `CF_TOKEN_<PROFILE>`. */
|
|
63
|
+
export declare function tokenSecretName(profile: string): string;
|
|
64
|
+
/**
|
|
65
|
+
* Author a token profile: validates its permission keys against the catalog at author time, so a typo
|
|
66
|
+
* fails where the profile is declared, not deep in a mint call. Returns the profile unchanged, typed.
|
|
67
|
+
*/
|
|
68
|
+
export declare function defineTokenProfile(profile: TokenProfile): TokenProfile;
|
|
69
|
+
/**
|
|
70
|
+
* The seam fields this package reads. Core's {@link TokenProfileSeam} is the structural contract every
|
|
71
|
+
* capability declares against; `secretScope` is additive on top of it, so it is named here rather than
|
|
72
|
+
* in core — a seam value that omits it is still assignable, and one that carries it is read as declared.
|
|
73
|
+
*
|
|
74
|
+
* Exported so a capability can type its `tokenProfiles` entry against it. Declaring `secretScope` as an
|
|
75
|
+
* inline object literal directly in `tokenProfiles` still trips TypeScript's excess-property check
|
|
76
|
+
* against core's narrower `TokenProfileSeam`; declare the entry as its own `const` (with `as const` or
|
|
77
|
+
* `satisfies TokenProfileSeamInput`) and it flows through.
|
|
78
|
+
*/
|
|
79
|
+
export interface TokenProfileSeamInput extends TokenProfileSeam {
|
|
80
|
+
readonly secretScope?: string;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* The project's token-profile registry: the aggregating `ci-system` profile plus every composed
|
|
84
|
+
* capability's worker-consumer {@link Capability.tokenProfiles} slice. A capability profile that
|
|
85
|
+
* clashes with `ci-system` (or another capability's name) fails loudly at assembly. This is the one
|
|
86
|
+
* registry `pithy token` reads.
|
|
87
|
+
*/
|
|
88
|
+
export declare function resolveTokenProfiles(capabilities: readonly Capability[]): Record<string, TokenProfile>;
|
|
89
|
+
/**
|
|
90
|
+
* Resolve a profile by name from the aggregated registry, applying an adopter override. An unknown
|
|
91
|
+
* name fails with an actionable error listing the known profiles.
|
|
92
|
+
*/
|
|
93
|
+
export declare function resolveProfile(profiles: Record<string, TokenProfile>, name: string, override?: ProfileOverride): TokenProfile;
|
|
94
|
+
/** Build the account-scoped {@link TokenPermission}s for a set of permission keys — the mint input. */
|
|
95
|
+
export declare function permissionsForKeys(keys: PermissionKey[], accountId: string): TokenPermission[];
|
|
96
|
+
/**
|
|
97
|
+
* Turn a resolved profile into the {@link TokenPermission}s the account-tokens manager mints from: the
|
|
98
|
+
* profile's permission keys resolved to CF group names, scoped to the account (or the profile's
|
|
99
|
+
* explicit resources). One policy per profile — a token carries exactly the access its profile declares.
|
|
100
|
+
*/
|
|
101
|
+
export declare function profilePermissions(profile: TokenProfile, accountId: string): TokenPermission[];
|
|
102
|
+
//# sourceMappingURL=profiles.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"profiles.d.ts","sourceRoot":"","sources":["../../src/tokens/profiles.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,0CAA0C,CAAC;AAE7F,OAAO,EAAmB,KAAK,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAC/E,OAAO,EAAmB,KAAK,aAAa,EAAyB,MAAM,eAAe,CAAC;AAE3F;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,UAAU,GAAG,eAAe,GAAG,UAAU,GAAG,WAAW,CAAC;AAEpE,uFAAuF;AACvF,eAAO,MAAM,YAAY,EAAE,SAAS,UAAU,EAA+C,CAAC;AAE9F,4GAA4G;AAC5G,eAAO,MAAM,iBAAiB,cAAc,CAAC;AAe7C;;;;;GAKG;AACH,MAAM,WAAW,YAAY;IAC3B,mFAAmF;IACnF,IAAI,EAAE,MAAM,CAAC;IACb,oGAAoG;IACpG,WAAW,EAAE,aAAa,EAAE,CAAC;IAC7B,oFAAoF;IACpF,SAAS,CAAC,EAAE,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/C,0GAA0G;IAC1G,MAAM,EAAE,MAAM,CAAC;IACf;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,gBAAgB,CAAC;IAC/B,2GAA2G;IAC3G,YAAY,CAAC,EAAE,UAAU,CAAC;IAC1B,sDAAsD;IACtD,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,kGAAkG;AAClG,MAAM,MAAM,gBAAgB,GAAG,aAAa,GAAG,QAAQ,CAAC;AAKxD,iHAAiH;AACjH,MAAM,WAAW,eAAe;IAC9B,6CAA6C;IAC7C,WAAW,CAAC,EAAE,aAAa,EAAE,CAAC;IAC9B,6EAA6E;IAC7E,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACnC,sDAAsD;IACtD,KAAK,CAAC,EAAE,UAAU,CAAC;CACpB;AAED,yFAAyF;AACzF,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAEvD;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,YAAY,GAAG,YAAY,CAGtE;AAkCD;;;;;;;;;GASG;AACH,MAAM,WAAW,qBAAsB,SAAQ,gBAAgB;IAC7D,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC/B;AA6BD;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,YAAY,EAAE,SAAS,UAAU,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CActG;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAC5B,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,EACtC,IAAI,EAAE,MAAM,EACZ,QAAQ,CAAC,EAAE,eAAe,GACzB,YAAY,CAgBd;AAED,uGAAuG;AACvG,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,aAAa,EAAE,EAAE,SAAS,EAAE,MAAM,GAAG,eAAe,EAAE,CAE9F;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,GAAG,eAAe,EAAE,CAI9F"}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { CloudflareNotConfiguredError } from "../client/errors.js";
|
|
2
|
+
import { accountResource } from "./accountTokensManager.js";
|
|
3
|
+
import { isPermissionKey, resolvePermissionKeys } from "./permissions.js";
|
|
4
|
+
//#region src/tokens/profiles.ts
|
|
5
|
+
/** The valid stores, for validating a `--store` flag or a profile's `defaultStore`. */
|
|
6
|
+
const TOKEN_STORES = [
|
|
7
|
+
"secrets-store",
|
|
8
|
+
"dev-vars",
|
|
9
|
+
"ephemeral"
|
|
10
|
+
];
|
|
11
|
+
/** The name of the one CI system token — the single least-privilege credential a CI pipeline runs under. */
|
|
12
|
+
const CI_SYSTEM_PROFILE = "ci-system";
|
|
13
|
+
/**
|
|
14
|
+
* The base permissions the `ci-system` token always carries: deploy Workers, run migrations against
|
|
15
|
+
* remote D1, and read/write the Secrets Store (a deployed Worker binding CFSS secrets needs Secrets
|
|
16
|
+
* Store access at deploy). Every composed capability's {@link Capability.ciPermissions} unions on top.
|
|
17
|
+
*/
|
|
18
|
+
const CI_SYSTEM_BASE = [
|
|
19
|
+
"workers:write",
|
|
20
|
+
"d1:read",
|
|
21
|
+
"d1:write",
|
|
22
|
+
"secrets:read",
|
|
23
|
+
"secrets:write"
|
|
24
|
+
];
|
|
25
|
+
/** The valid secret scopes, for validating a capability's declaration. */
|
|
26
|
+
const TOKEN_SECRET_SCOPES = ["environment", "global"];
|
|
27
|
+
/** The env-var / secret name a profile's token is stored under: `CF_TOKEN_<PROFILE>`. */
|
|
28
|
+
function tokenSecretName(profile) {
|
|
29
|
+
return `CF_TOKEN_${profile.toUpperCase().replace(/[^A-Z0-9]+/g, "_")}`;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Author a token profile: validates its permission keys against the catalog at author time, so a typo
|
|
33
|
+
* fails where the profile is declared, not deep in a mint call. Returns the profile unchanged, typed.
|
|
34
|
+
*/
|
|
35
|
+
function defineTokenProfile(profile) {
|
|
36
|
+
resolvePermissionKeys(profile.permissions);
|
|
37
|
+
return profile;
|
|
38
|
+
}
|
|
39
|
+
/** Validate a capability's declared permission keys, throwing an actionable error on any unknown one. */
|
|
40
|
+
function assertPermissionKeys(keys, where) {
|
|
41
|
+
for (const key of keys) if (!isPermissionKey(key)) throw new CloudflareNotConfiguredError({
|
|
42
|
+
message: `${where} declares an unknown permission key: ${key}.`,
|
|
43
|
+
action: "Use a known permission key (d1:read, d1:write, workers:write, secrets:read, secrets:write, email:routing, kv:write)."
|
|
44
|
+
});
|
|
45
|
+
return keys;
|
|
46
|
+
}
|
|
47
|
+
/** Build the aggregating `ci-system` profile: the base permissions ∪ every capability's `ciPermissions`. */
|
|
48
|
+
function ciSystemProfile(capabilities) {
|
|
49
|
+
const permissions = [...CI_SYSTEM_BASE];
|
|
50
|
+
for (const capability of capabilities) for (const key of assertPermissionKeys(capability.ciPermissions ?? [], `Capability "${capability.name}"`)) if (!permissions.includes(key)) permissions.push(key);
|
|
51
|
+
return {
|
|
52
|
+
name: CI_SYSTEM_PROFILE,
|
|
53
|
+
permissions,
|
|
54
|
+
secret: tokenSecretName(CI_SYSTEM_PROFILE),
|
|
55
|
+
defaultStore: "dev-vars",
|
|
56
|
+
description: "The one CI pipeline credential: deploy Workers, migrate, and every composed capability's CI needs. Set it as CI's CLOUDFLARE_API_TOKEN."
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/** Build a concrete, validated worker-consumer {@link TokenProfile} from a capability's structural seam entry. */
|
|
60
|
+
function profileFromSeam(name, seam, capability) {
|
|
61
|
+
const store = seam.defaultStore;
|
|
62
|
+
if (store !== void 0 && !TOKEN_STORES.includes(store)) throw new CloudflareNotConfiguredError({
|
|
63
|
+
message: `Token profile "${name}" (capability "${capability}") declares an unknown store: ${store}.`,
|
|
64
|
+
action: `Use one of: ${TOKEN_STORES.join(", ")}.`
|
|
65
|
+
});
|
|
66
|
+
const scope = seam.secretScope;
|
|
67
|
+
if (scope !== void 0 && !TOKEN_SECRET_SCOPES.includes(scope)) throw new CloudflareNotConfiguredError({
|
|
68
|
+
message: `Token profile "${name}" (capability "${capability}") declares an unknown secret scope: ${scope}.`,
|
|
69
|
+
action: `Use one of: ${TOKEN_SECRET_SCOPES.join(", ")}.`
|
|
70
|
+
});
|
|
71
|
+
return {
|
|
72
|
+
name,
|
|
73
|
+
permissions: assertPermissionKeys(seam.permissions, `Token profile "${name}"`),
|
|
74
|
+
resources: seam.resources,
|
|
75
|
+
secret: seam.secret ?? tokenSecretName(name),
|
|
76
|
+
secretScope: scope,
|
|
77
|
+
defaultStore: store,
|
|
78
|
+
description: seam.description ?? name
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* The project's token-profile registry: the aggregating `ci-system` profile plus every composed
|
|
83
|
+
* capability's worker-consumer {@link Capability.tokenProfiles} slice. A capability profile that
|
|
84
|
+
* clashes with `ci-system` (or another capability's name) fails loudly at assembly. This is the one
|
|
85
|
+
* registry `pithy token` reads.
|
|
86
|
+
*/
|
|
87
|
+
function resolveTokenProfiles(capabilities) {
|
|
88
|
+
const profiles = { [CI_SYSTEM_PROFILE]: ciSystemProfile(capabilities) };
|
|
89
|
+
for (const capability of capabilities) for (const [name, seam] of Object.entries(capability.tokenProfiles ?? {})) {
|
|
90
|
+
if (profiles[name]) throw new CloudflareNotConfiguredError({
|
|
91
|
+
message: `Token profile "${name}" is declared more than once (capability "${capability.name}").`,
|
|
92
|
+
action: "Give each token profile a unique name."
|
|
93
|
+
});
|
|
94
|
+
profiles[name] = profileFromSeam(name, seam, capability.name);
|
|
95
|
+
}
|
|
96
|
+
return profiles;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Resolve a profile by name from the aggregated registry, applying an adopter override. An unknown
|
|
100
|
+
* name fails with an actionable error listing the known profiles.
|
|
101
|
+
*/
|
|
102
|
+
function resolveProfile(profiles, name, override) {
|
|
103
|
+
const base = profiles[name];
|
|
104
|
+
if (!base) throw new CloudflareNotConfiguredError({
|
|
105
|
+
message: `Unknown token profile: ${name}.`,
|
|
106
|
+
action: `Use one of: ${Object.keys(profiles).join(", ")}.`,
|
|
107
|
+
detail: `resolve token profile: ${name} is not in the registry`
|
|
108
|
+
});
|
|
109
|
+
if (!override) return base;
|
|
110
|
+
return {
|
|
111
|
+
...base,
|
|
112
|
+
permissions: override.permissions ?? base.permissions,
|
|
113
|
+
resources: override.resources ?? base.resources,
|
|
114
|
+
defaultStore: override.store ?? base.defaultStore
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
/** Build the account-scoped {@link TokenPermission}s for a set of permission keys — the mint input. */
|
|
118
|
+
function permissionsForKeys(keys, accountId) {
|
|
119
|
+
return [{
|
|
120
|
+
permissionGroupNames: resolvePermissionKeys(keys),
|
|
121
|
+
resources: accountResource(accountId)
|
|
122
|
+
}];
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Turn a resolved profile into the {@link TokenPermission}s the account-tokens manager mints from: the
|
|
126
|
+
* profile's permission keys resolved to CF group names, scoped to the account (or the profile's
|
|
127
|
+
* explicit resources). One policy per profile — a token carries exactly the access its profile declares.
|
|
128
|
+
*/
|
|
129
|
+
function profilePermissions(profile, accountId) {
|
|
130
|
+
const resources = profile.resources && profile.resources !== "account" ? profile.resources : accountResource(accountId);
|
|
131
|
+
return [{
|
|
132
|
+
permissionGroupNames: resolvePermissionKeys(profile.permissions),
|
|
133
|
+
resources
|
|
134
|
+
}];
|
|
135
|
+
}
|
|
136
|
+
//#endregion
|
|
137
|
+
export { CI_SYSTEM_PROFILE, TOKEN_STORES, defineTokenProfile, permissionsForKeys, profilePermissions, resolveProfile, resolveTokenProfiles, tokenSecretName };
|