@pithy-sh/cloudflare 0.5.0 → 0.7.0

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.
Files changed (59) hide show
  1. package/dist/analytics/analyticsManager.d.ts +78 -0
  2. package/dist/analytics/analyticsManager.d.ts.map +1 -0
  3. package/dist/analytics/analyticsManager.js +206 -0
  4. package/dist/analytics/datasets.d.ts +26 -0
  5. package/dist/analytics/datasets.d.ts.map +1 -0
  6. package/dist/analytics/datasets.js +26 -0
  7. package/dist/analytics/window.d.ts +50 -0
  8. package/dist/analytics/window.d.ts.map +1 -0
  9. package/dist/analytics/window.js +67 -0
  10. package/dist/artifacts/artifactsManager.d.ts +52 -0
  11. package/dist/artifacts/artifactsManager.d.ts.map +1 -0
  12. package/dist/artifacts/artifactsManager.js +150 -0
  13. package/dist/client/clients.d.ts +13 -1
  14. package/dist/client/clients.d.ts.map +1 -1
  15. package/dist/client/clients.js +22 -1
  16. package/dist/client/errors.d.ts +4 -3
  17. package/dist/client/errors.d.ts.map +1 -1
  18. package/dist/media/imageManager.d.ts +25 -0
  19. package/dist/media/imageManager.d.ts.map +1 -1
  20. package/dist/media/imageManager.js +58 -2
  21. package/dist/media/streamManager.d.ts +15 -0
  22. package/dist/media/streamManager.d.ts.map +1 -1
  23. package/dist/media/streamManager.js +28 -1
  24. package/dist/oauth/scopes.d.ts +2 -2
  25. package/dist/oauth/scopes.js +2 -2
  26. package/dist/queue/queueProvisioner.d.ts +30 -0
  27. package/dist/queue/queueProvisioner.d.ts.map +1 -0
  28. package/dist/queue/queueProvisioner.js +72 -0
  29. package/dist/r2/r2Manager.d.ts +11 -1
  30. package/dist/r2/r2Manager.d.ts.map +1 -1
  31. package/dist/r2/r2Manager.js +18 -1
  32. package/dist/tokens/accountTokensManager.d.ts +1 -0
  33. package/dist/tokens/accountTokensManager.d.ts.map +1 -1
  34. package/dist/tokens/accountTokensManager.js +2 -1
  35. package/dist/tokens/permissions.d.ts +10 -0
  36. package/dist/tokens/permissions.d.ts.map +1 -1
  37. package/dist/tokens/permissions.js +12 -2
  38. package/dist/tokens/profiles.d.ts +6 -30
  39. package/dist/tokens/profiles.d.ts.map +1 -1
  40. package/dist/tokens/profiles.js +44 -36
  41. package/dist/tokens/runtime.d.ts +16 -0
  42. package/dist/tokens/runtime.d.ts.map +1 -0
  43. package/dist/tokens/runtime.js +18 -0
  44. package/package.json +3 -3
  45. package/src/analytics/analyticsManager.ts +311 -0
  46. package/src/analytics/datasets.ts +30 -0
  47. package/src/analytics/window.ts +101 -0
  48. package/src/artifacts/artifactsManager.ts +199 -0
  49. package/src/client/clients.ts +28 -1
  50. package/src/client/errors.ts +4 -3
  51. package/src/media/imageManager.ts +93 -1
  52. package/src/media/streamManager.ts +34 -0
  53. package/src/oauth/scopes.ts +2 -2
  54. package/src/queue/queueProvisioner.ts +79 -0
  55. package/src/r2/r2Manager.ts +29 -2
  56. package/src/tokens/accountTokensManager.ts +12 -0
  57. package/src/tokens/permissions.ts +27 -0
  58. package/src/tokens/profiles.ts +54 -70
  59. package/src/tokens/runtime.ts +18 -0
@@ -0,0 +1,72 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
3
+ import { cloudflareRequest, decodeResponse, isNotFoundError } from "../client/errors.js";
4
+ import { CloudflareManager } from "../client/manager.js";
5
+ import { z } from "zod";
6
+ //#region src/queue/queueProvisioner.ts
7
+ /** A queue's identity, decoded from the Queues API's `queue_id` and `queue_name`. */
8
+ const QueueInfo = z.object({
9
+ id: z.string().describe("The CF-assigned queue id, which the REST API addresses a queue by."),
10
+ name: z.string().describe("The queue name a producer binding names it by.")
11
+ }).describe("A Cloudflare Queue's identity, as the Queues API returns it.");
12
+ /** The Queues API's own spelling of a queue's identity, mapped onto {@link QueueInfo}. */
13
+ const QueueRecord = z.object({
14
+ queue_id: z.string().describe("The CF-assigned queue id."),
15
+ queue_name: z.string().describe("The queue name.")
16
+ }).transform(({ queue_id, queue_name }) => ({
17
+ id: queue_id,
18
+ name: queue_name
19
+ })).describe("A queue as the Queues API returns it.");
20
+ /**
21
+ * Account-level Queues control plane: **find, create and delete queues** (#662).
22
+ *
23
+ * `pithy provision` creates the queue behind every `queue` binding a composition declares, and `pithy feature
24
+ * destroy` deletes a feature's. A `queues.producers` entry names a queue, and `wrangler deploy` refuses one that
25
+ * does not exist, so the entry is only honest because this stands the queue up. Addressed by account — unlike
26
+ * {@link CloudflareQueueManager}, which targets one queue to send into.
27
+ */
28
+ var CloudflareQueueProvisioner = class extends CloudflareManager {
29
+ getServiceType() {
30
+ return "Cloudflare Queues (control plane)";
31
+ }
32
+ /** Prove access by listing queues — a read, never a create or a delete. Never throws. */
33
+ async validateServiceAccess() {
34
+ try {
35
+ await this.getClient().queues.list({ account_id: this.accountId });
36
+ return true;
37
+ } catch {
38
+ return false;
39
+ }
40
+ }
41
+ /** Find a queue by exact name in the account, or `null` — for idempotent provisioning. */
42
+ async findQueueByName(name) {
43
+ return cloudflareRequest(`find queue ${name}`, async () => {
44
+ for await (const queue of this.getClient().queues.list({ account_id: this.accountId })) {
45
+ const parsed = QueueRecord.safeParse(queue);
46
+ if (parsed.success && parsed.data.name === name) return parsed.data;
47
+ }
48
+ return null;
49
+ });
50
+ }
51
+ /** Create a queue by name; returns its id and name. */
52
+ async createQueue(name) {
53
+ const response = await cloudflareRequest(`create queue ${name}`, () => this.getClient().queues.create({
54
+ account_id: this.accountId,
55
+ queue_name: name
56
+ }));
57
+ return decodeResponse(QueueRecord, response, "queue create");
58
+ }
59
+ /** Delete a queue by id. Idempotent — a missing queue is not an error, so teardown can re-run safely. */
60
+ async deleteQueue(queueId) {
61
+ await cloudflareRequest(`delete queue ${queueId}`, async () => {
62
+ try {
63
+ await this.getClient().queues.delete(queueId, { account_id: this.accountId });
64
+ } catch (error) {
65
+ if (isNotFoundError(error)) return;
66
+ throw error;
67
+ }
68
+ });
69
+ }
70
+ };
71
+ //#endregion
72
+ export { CloudflareQueueProvisioner, QueueInfo };
@@ -8,9 +8,18 @@ import { R2Credentials } from "./r2Credentials";
8
8
  * the bucket it targets. R2 signs presigned URLs with the S3 keys, not the CF API token — both
9
9
  * come from config (no environment coupling).
10
10
  */
11
- export interface R2ManagerConfig extends CloudflareManagerConfig, R2Credentials {
11
+ export interface R2ManagerConfig extends Omit<CloudflareManagerConfig, "apiToken">, R2Credentials {
12
12
  /** The R2 bucket all object operations target. */
13
13
  bucketName: string;
14
+ /**
15
+ * A Cloudflare API token, for the one call here that is not S3: {@link CloudflareR2Manager.validateServiceAccess}.
16
+ *
17
+ * **Absent in a Worker (#728).** Every object operation is S3, signed with the key pair, so a Worker's
18
+ * object store holds no API token: the application's one is `runtime`, and R2 needs none of it. Without
19
+ * one, `validateServiceAccess` answers `false` and nothing calls the Cloudflare API. An empty string is
20
+ * still refused, because that is an unresolved value rather than a decision.
21
+ */
22
+ apiToken?: string;
14
23
  }
15
24
  /** How long a presigned URL stays valid. Shared by every presign method so the option reads the same. */
16
25
  export interface PresignOptions {
@@ -97,6 +106,7 @@ export declare class CloudflareR2Manager extends CloudflareManager {
97
106
  private readonly accessKeyId;
98
107
  private readonly secretAccessKey;
99
108
  private s3Client;
109
+ private readonly s3Only;
100
110
  constructor(config: R2ManagerConfig);
101
111
  /** The S3-compatible client for R2, lazily created and cached for the manager's lifetime. */
102
112
  private getS3Client;
@@ -1 +1 @@
1
- {"version":3,"file":"r2Manager.d.ts","sourceRoot":"","sources":["../../src/r2/r2Manager.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"r2Manager.d.ts","sourceRoot":"","sources":["../../src/r2/r2Manager.ts"],"names":[],"mappings":"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"}
@@ -11,6 +11,13 @@ import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
11
11
  const PRESIGN_EXPIRY_SECONDS = 3600;
12
12
  /** How many parts one `ListParts` page returns. S3's own cap; the drain loop below relies on it. */
13
13
  const LIST_PARTS_PAGE_SIZE = 1e3;
14
+ /**
15
+ * What the base client is built with when the config carries no API token. Never sent: {@link noApiToken}
16
+ * is the provider, and the base reads the token through it at every call and refuses an empty one.
17
+ */
18
+ const S3_ONLY = "r2-s3-only";
19
+ /** The token provider of an S3-only manager: no token, so any Cloudflare API call is refused before it is sent. */
20
+ const noApiToken = () => "";
14
21
  /** An object's metadata as R2 reports it, without fetching the body. */
15
22
  const R2ObjectHead = z.object({
16
23
  size: z.number().int().nonnegative().describe("The object's size in bytes."),
@@ -70,8 +77,17 @@ var CloudflareR2Manager = class extends CloudflareManager {
70
77
  accessKeyId;
71
78
  secretAccessKey;
72
79
  s3Client = null;
80
+ s3Only;
73
81
  constructor(config) {
74
- super(config);
82
+ super(config.apiToken === void 0 ? {
83
+ ...config,
84
+ apiToken: S3_ONLY,
85
+ tokenProvider: noApiToken
86
+ } : {
87
+ ...config,
88
+ apiToken: config.apiToken
89
+ });
90
+ this.s3Only = config.apiToken === void 0;
75
91
  if (!config.bucketName) throw new CloudflareNotConfiguredError({ detail: "Missing bucketName for R2 access." });
76
92
  const credentials = R2Credentials.safeParse(config);
77
93
  if (!credentials.success) throw new CloudflareNotConfiguredError({ detail: `Invalid R2 credentials: ${credentials.error.message}` });
@@ -395,6 +411,7 @@ var CloudflareR2Manager = class extends CloudflareManager {
395
411
  * not merely that an S3 client could be built. Never throws.
396
412
  */
397
413
  async validateServiceAccess() {
414
+ if (this.s3Only) return false;
398
415
  try {
399
416
  await this.getClient().r2.buckets.get(this.bucketName, { account_id: this.accountId });
400
417
  return true;
@@ -107,6 +107,7 @@ export declare const AccountTokenSummary: z.ZodObject<{
107
107
  }, z.core.$strip>>>, unknown>;
108
108
  resources: z.ZodRecord<z.ZodString, z.ZodUnknown>;
109
109
  }, z.core.$strip>>>, unknown>>;
110
+ last_used_on: z.ZodCatch<z.ZodOptional<z.ZodNullable<z.ZodString>>>;
110
111
  }, z.core.$strip>;
111
112
  export type AccountTokenSummary = z.output<typeof AccountTokenSummary>;
112
113
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"accountTokensManager.d.ts","sourceRoot":"","sources":["../../src/tokens/accountTokensManager.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"accountTokensManager.d.ts","sourceRoot":"","sources":["../../src/tokens/accountTokensManager.ts"],"names":[],"mappings":"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"}
@@ -110,7 +110,8 @@ const AccountTokenSummary = z.object({
110
110
  "disabled",
111
111
  "expired"
112
112
  ]).describe("The lifecycle state Cloudflare holds for the token.").optional()).describe("The token's lifecycle status. Reported, because a `disabled` or `expired` token cannot attach a route however its policies read. `nullish` for the reason {@link AccountTokenPolicy} gives."),
113
- policies: nullAsAbsent(z.array(AccountTokenPolicy).optional()).catch(void 0).describe("The token's access policies, when the list response carries them. **A live token's own scope is the record of how it was minted** — it is how a `ci-system` token minted before zone-scoped routes (#651) can be told from one minted after, without reading the token record (which needs a grant these least-privilege tokens deliberately lack). Absent and empty are different facts: a response that says nothing means the scope could not be read, never that the token has none.")
113
+ policies: nullAsAbsent(z.array(AccountTokenPolicy).optional()).catch(void 0).describe("The token's access policies, when the list response carries them. **A live token's own scope is the record of how it was minted** — it is how a `ci-system` token minted before zone-scoped routes (#651) can be told from one minted after, without reading the token record (which needs a grant these least-privilege tokens deliberately lack). Absent and empty are different facts: a response that says nothing means the scope could not be read, never that the token has none."),
114
+ last_used_on: z.string().nullable().optional().catch(void 0).describe("When something last authenticated with the token, as Cloudflare measures it against the live account. Read because `pithy token forget` acts on it (#762): `null` means never, so a local copy may be its only one. Absent means the response did not say, which is not a claim either way.")
114
115
  }).describe("An existing account-owned API token's metadata (never its secret value).");
115
116
  /**
116
117
  * Out-of-Worker control plane for **account-owned** Cloudflare API tokens: mint scoped, least-privilege
@@ -25,6 +25,11 @@ export declare const PERMISSION_GROUPS: {
25
25
  readonly "ai:read": readonly ["Workers AI Read"];
26
26
  readonly "zone:read": readonly ["Zone Read"];
27
27
  readonly "routes:write": readonly ["Workers Routes Write"];
28
+ readonly "analytics:read": readonly ["Account Analytics Read"];
29
+ readonly "images:write": readonly ["Images Write"];
30
+ readonly "stream:write": readonly ["Stream Write"];
31
+ readonly "queues:write": readonly ["Queues Write"];
32
+ readonly "artifacts:write": readonly ["Artifacts Write"];
28
33
  };
29
34
  /** A known permission key — one of {@link PERMISSION_GROUPS}'s keys. */
30
35
  export type PermissionKey = keyof typeof PERMISSION_GROUPS;
@@ -61,6 +66,11 @@ export declare const OAUTH_SCOPES: {
61
66
  readonly "ai:read": readonly ["ai.read"];
62
67
  readonly "zone:read": readonly ["zone.read"];
63
68
  readonly "routes:write": readonly ["workers-routes.write"];
69
+ readonly "analytics:read": readonly ["account-analytics.read"];
70
+ readonly "images:write": readonly ["images.write"];
71
+ readonly "stream:write": readonly ["stream.write"];
72
+ readonly "queues:write": readonly ["queues.write"];
73
+ readonly "artifacts:write": readonly ["artifacts.write"];
64
74
  };
65
75
  /**
66
76
  * Resolve permission keys to the OAuth scopes a session needs to act — or mint — with them, de-duped in
@@ -1 +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;aAiBzB,cAAc,YAAG,sBAAsB;CAC/B,CAAC;AAEX,wEAAwE;AACxE,MAAM,MAAM,aAAa,GAAG,MAAM,OAAO,iBAAiB,CAAC;AAE3D;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;CAeoC,CAAC;AAE9D;;;GAGG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,EAAE,CAShE;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,uBAAuB,EAAE,SAAS,aAAa,EAAqB,CAAC;AAElF,6DAA6D;AAC7D,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,IAAI,aAAa,CAEjE;AAED,6FAA6F;AAC7F,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAE3D;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAe9D"}
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;aAiBzB,cAAc,YAAG,sBAAsB;aAQvC,gBAAgB,YAAG,wBAAwB;aAK3C,cAAc,YAAG,cAAc;aAC/B,cAAc,YAAG,cAAc;aAI/B,cAAc,YAAG,cAAc;aAI/B,iBAAiB,YAAG,iBAAiB;CAC7B,CAAC;AAEX,wEAAwE;AACxE,MAAM,MAAM,aAAa,GAAG,MAAM,OAAO,iBAAiB,CAAC;AAE3D;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;CAoBoC,CAAC;AAE9D;;;GAGG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,EAAE,CAShE;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,uBAAuB,EAAE,SAAS,aAAa,EAAqB,CAAC;AAElF,6DAA6D;AAC7D,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,IAAI,aAAa,CAEjE;AAED,6FAA6F;AAC7F,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAE3D;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAe9D"}
@@ -26,7 +26,12 @@ const PERMISSION_GROUPS = {
26
26
  "vectorize:write": ["Vectorize Write"],
27
27
  "ai:read": ["Workers AI Read"],
28
28
  "zone:read": ["Zone Read"],
29
- "routes:write": ["Workers Routes Write"]
29
+ "routes:write": ["Workers Routes Write"],
30
+ "analytics:read": ["Account Analytics Read"],
31
+ "images:write": ["Images Write"],
32
+ "stream:write": ["Stream Write"],
33
+ "queues:write": ["Queues Write"],
34
+ "artifacts:write": ["Artifacts Write"]
30
35
  };
31
36
  /**
32
37
  * **The third column: the OAuth scope each key needs a signed-in session to hold (#759).**
@@ -60,7 +65,12 @@ const OAUTH_SCOPES = {
60
65
  "vectorize:write": ["vectorize.write"],
61
66
  "ai:read": ["ai.read"],
62
67
  "zone:read": ["zone.read"],
63
- "routes:write": ["workers-routes.write"]
68
+ "routes:write": ["workers-routes.write"],
69
+ "analytics:read": ["account-analytics.read"],
70
+ "images:write": ["images.write"],
71
+ "stream:write": ["stream.write"],
72
+ "queues:write": ["queues.write"],
73
+ "artifacts:write": ["artifacts.write"]
64
74
  };
65
75
  /**
66
76
  * Resolve permission keys to the OAuth scopes a session needs to act — or mint — with them, de-duped in
@@ -1,13 +1,13 @@
1
1
  // SPDX-FileCopyrightText: 2026 Pithy
2
2
  // SPDX-License-Identifier: MIT
3
- import type { Capability, TokenProfileSeam } from "@pithy-sh/core/src/capability/capability";
3
+ import type { Capability } from "@pithy-sh/core/src/capability/capability";
4
4
  import { type TokenPermission } from "./accountTokensManager";
5
5
  import { type PermissionKey } from "./permissions";
6
6
  /**
7
7
  * Where a minted token's value is written.
8
8
  *
9
- * - `secrets-store` — the CF Secrets Store; a Worker reads it via its binding. The destination for a
10
- * worker-consumer token; resolved from the token's declared secret (its registry backend) unless a
9
+ * - `secrets-store` — the CF Secrets Store; a Worker reads it via its binding. The destination for the
10
+ * `runtime` token; resolved from the token's declared secret (its registry backend) unless a
11
11
  * profile or `--store` names it directly.
12
12
  * - `dev-vars` — `<config>/<project>/tokens.json`, keyed by environment and outside every checkout.
13
13
  * **Read by an operator, never by a command** — nothing resolves a credential from it. The store for
@@ -37,21 +37,11 @@ export interface TokenProfile {
37
37
  resources?: "account" | Record<string, string>;
38
38
  /** The secret-registry name the minted value is stored under; its declared backend is the destination. */
39
39
  secret: string;
40
- /**
41
- * Whether the value behind {@link secret} differs per environment (the default) or is one value every
42
- * environment shares. It decides the environment segment of the **CF Secrets Store entry name** the
43
- * minted value is written to, and nothing else — never the variable key, which is a variable name
44
- * and stays verbatim. A `global` profile writes one entry with the literal `global` in that slot,
45
- * matching what provisioning wrote; an `environment` profile writes one entry per environment.
46
- */
47
- secretScope?: TokenSecretScope;
48
40
  /** A built-in destination override (`dev-vars`/`ephemeral`/`secrets-store`); else the secret's backend. */
49
41
  defaultStore?: TokenStore;
50
42
  /** Why this profile exists / what job consumes it. */
51
43
  description: string;
52
44
  }
53
- /** Whether a profile's secret is one value per environment or one value shared by all of them. */
54
- export type TokenSecretScope = "environment" | "global";
55
45
  /** An adopter's override of a profile's defaults (from `pithy.config.ts` or CLI flags). Every field optional. */
56
46
  export interface ProfileOverride {
57
47
  /** Replace the profile's permission keys. */
@@ -69,23 +59,9 @@ export declare function tokenSecretName(profile: string): string;
69
59
  */
70
60
  export declare function defineTokenProfile(profile: TokenProfile): TokenProfile;
71
61
  /**
72
- * The seam fields this package reads. Core's {@link TokenProfileSeam} is the structural contract every
73
- * capability declares against; `secretScope` is additive on top of it, so it is named here rather than
74
- * in core — a seam value that omits it is still assignable, and one that carries it is read as declared.
75
- *
76
- * Exported so a capability can type its `tokenProfiles` entry against it. Declaring `secretScope` as an
77
- * inline object literal directly in `tokenProfiles` still trips TypeScript's excess-property check
78
- * against core's narrower `TokenProfileSeam`; declare the entry as its own `const` (with `as const` or
79
- * `satisfies TokenProfileSeamInput`) and it flows through.
80
- */
81
- export interface TokenProfileSeamInput extends TokenProfileSeam {
82
- readonly secretScope?: string;
83
- }
84
- /**
85
- * The project's token-profile registry: the aggregating `ci-system` profile plus every composed
86
- * capability's worker-consumer {@link Capability.tokenProfiles} slice. A capability profile that
87
- * clashes with `ci-system` (or another capability's name) fails loudly at assembly. This is the one
88
- * registry `pithy token` reads.
62
+ * The project's token-profile registry: the aggregating `ci-system` profile and, when any composed
63
+ * capability declares {@link Capability.runtimePermissions}, the aggregating `runtime` profile. Nothing
64
+ * else — a capability cannot add a profile. This is the one registry `pithy token` reads.
89
65
  */
90
66
  export declare function resolveTokenProfiles(capabilities: readonly Capability[]): Record<string, TokenProfile>;
91
67
  /**
@@ -1 +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,EAAiB,MAAM,wBAAwB,CAAC;AAC9F,OAAO,EAIL,KAAK,aAAa,EAEnB,MAAM,eAAe,CAAC;AAEvB;;;;;;;;;;;;;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;AAsBD,uGAAuG;AACvG,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,aAAa,EAAE,EAAE,SAAS,EAAE,MAAM,GAAG,eAAe,EAAE,CAG9F;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,GAAG,eAAe,EAAE,CAG9E;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,GAAG,eAAe,EAAE,CAK9F"}
1
+ {"version":3,"file":"profiles.d.ts","sourceRoot":"","sources":["../../src/tokens/profiles.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,0CAA0C,CAAC;AAE3E,OAAO,EAAmB,KAAK,eAAe,EAAiB,MAAM,wBAAwB,CAAC;AAC9F,OAAO,EAIL,KAAK,aAAa,EAEnB,MAAM,eAAe,CAAC;AAGvB;;;;;;;;;;;;;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,2GAA2G;IAC3G,YAAY,CAAC,EAAE,UAAU,CAAC;IAC1B,sDAAsD;IACtD,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,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;AAiFD;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,YAAY,EAAE,SAAS,UAAU,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAMtG;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;AAsBD,uGAAuG;AACvG,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,aAAa,EAAE,EAAE,SAAS,EAAE,MAAM,GAAG,eAAe,EAAE,CAG9F;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,GAAG,eAAe,EAAE,CAG9E;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,GAAG,eAAe,EAAE,CAK9F"}
@@ -3,6 +3,7 @@
3
3
  import { CloudflareNotConfiguredError } from "../client/errors.js";
4
4
  import { accountResource, zoneResources } from "./accountTokensManager.js";
5
5
  import { PERMISSION_GROUPS, isPermissionKey, isZoneScopedPermission, resolvePermissionKeys } from "./permissions.js";
6
+ import { RUNTIME_PROFILE, RUNTIME_TOKEN_SECRET } from "./runtime.js";
6
7
  //#region src/tokens/profiles.ts
7
8
  /** The valid stores, for validating a `--store` flag or a profile's `defaultStore`. */
8
9
  const TOKEN_STORES = [
@@ -24,8 +25,6 @@ const CI_SYSTEM_BASE = [
24
25
  "secrets:read",
25
26
  "secrets:write"
26
27
  ];
27
- /** The valid secret scopes, for validating a capability's declaration. */
28
- const TOKEN_SECRET_SCOPES = ["environment", "global"];
29
28
  /** The env-var / secret name a profile's token is stored under: `CF_TOKEN_<PROFILE>`. */
30
29
  function tokenSecretName(profile) {
31
30
  return `CF_TOKEN_${profile.toUpperCase().replace(/[^A-Z0-9]+/g, "_")}`;
@@ -42,14 +41,23 @@ function defineTokenProfile(profile) {
42
41
  function assertPermissionKeys(keys, where) {
43
42
  for (const key of keys) if (!isPermissionKey(key)) throw new CloudflareNotConfiguredError({
44
43
  message: `${where} declares an unknown permission key: ${key}.`,
45
- action: "Use a known permission key (d1:read, d1:write, workers:write, secrets:read, secrets:write, email:routing, kv:write)."
44
+ action: `Use a known permission key: ${Object.keys(PERMISSION_GROUPS).join(", ")}.`
46
45
  });
47
46
  return keys;
48
47
  }
49
- /** Build the aggregating `ci-system` profile: the base permissions ∪ every capability's `ciPermissions`. */
48
+ /**
49
+ * Build the aggregating `ci-system` profile: the base permissions ∪ every capability's `ciPermissions` ∪
50
+ * `queues:write` where a composed capability binds a queue ∪ `artifacts:write` where one binds Artifacts.
51
+ *
52
+ * **The queue permission is derived from the binding, not declared (#662).** `pithy provision` creates the queue
53
+ * behind every `queue` binding it composes, the adopter's own app's included, and the app has no `ciPermissions`
54
+ * anyone would think to write. A permission read off the binding cannot be forgotten by whoever declares it.
55
+ */
50
56
  function ciSystemProfile(capabilities) {
51
57
  const permissions = [...CI_SYSTEM_BASE];
52
58
  for (const capability of capabilities) for (const key of assertPermissionKeys(capability.ciPermissions ?? [], `Capability "${capability.name}"`)) if (!permissions.includes(key)) permissions.push(key);
59
+ if (capabilities.some((capability) => capability.requiredBindings.some((binding) => binding.type === "queue")) && !permissions.includes("queues:write")) permissions.push("queues:write");
60
+ if (capabilities.some((capability) => capability.requiredBindings.some((binding) => binding.type === "artifacts")) && !permissions.includes("artifacts:write")) permissions.push("artifacts:write");
53
61
  return {
54
62
  name: CI_SYSTEM_PROFILE,
55
63
  permissions,
@@ -58,44 +66,44 @@ function ciSystemProfile(capabilities) {
58
66
  description: "The one CI pipeline credential: deploy Workers, migrate, and every composed capability's CI needs. Set it as CI's CLOUDFLARE_API_TOKEN."
59
67
  };
60
68
  }
61
- /** Build a concrete, validated worker-consumer {@link TokenProfile} from a capability's structural seam entry. */
62
- function profileFromSeam(name, seam, capability) {
63
- const store = seam.defaultStore;
64
- if (store !== void 0 && !TOKEN_STORES.includes(store)) throw new CloudflareNotConfiguredError({
65
- message: `Token profile "${name}" (capability "${capability}") declares an unknown store: ${store}.`,
66
- action: `Use one of: ${TOKEN_STORES.join(", ")}.`
67
- });
68
- const scope = seam.secretScope;
69
- if (scope !== void 0 && !TOKEN_SECRET_SCOPES.includes(scope)) throw new CloudflareNotConfiguredError({
70
- message: `Token profile "${name}" (capability "${capability}") declares an unknown secret scope: ${scope}.`,
71
- action: `Use one of: ${TOKEN_SECRET_SCOPES.join(", ")}.`
72
- });
69
+ /**
70
+ * Build the aggregating `runtime` profile: every composed capability's `runtimePermissions`, each once,
71
+ * in first-declared order — or nothing, when the composition declares none.
72
+ *
73
+ * **One token per deployed application, the way `ci-system` is one per pipeline.** A capability names
74
+ * what its Workers call Cloudflare with and never a token of its own, so a second credential is not
75
+ * something a capability can ask for. The token is minted per environment into the Secrets Store, so
76
+ * revoking staging's leaves production reading. A key whose group is zone-scoped is refused here: a
77
+ * Worker's token is account-scoped, and its zones would have to be chosen by hand.
78
+ */
79
+ function runtimeProfile(capabilities) {
80
+ const permissions = [];
81
+ for (const capability of capabilities) {
82
+ const where = `Capability "${capability.name}"`;
83
+ const keys = assertPermissionKeys(capability.runtimePermissions ?? [], where);
84
+ assertAccountScoped(keys, `${where}'s runtimePermissions`);
85
+ for (const key of keys) if (!permissions.includes(key)) permissions.push(key);
86
+ }
87
+ if (permissions.length === 0) return void 0;
73
88
  return {
74
- name,
75
- permissions: assertPermissionKeys(seam.permissions, `Token profile "${name}"`),
76
- resources: seam.resources,
77
- secret: seam.secret ?? tokenSecretName(name),
78
- secretScope: scope,
79
- defaultStore: store,
80
- description: seam.description ?? name
89
+ name: RUNTIME_PROFILE,
90
+ permissions,
91
+ secret: RUNTIME_TOKEN_SECRET,
92
+ defaultStore: "secrets-store",
93
+ description: "The one Cloudflare API token a deployed application holds: every composed capability's runtime needs, per environment. Analytics reads are account-wide by Cloudflare's grain."
81
94
  };
82
95
  }
83
96
  /**
84
- * The project's token-profile registry: the aggregating `ci-system` profile plus every composed
85
- * capability's worker-consumer {@link Capability.tokenProfiles} slice. A capability profile that
86
- * clashes with `ci-system` (or another capability's name) fails loudly at assembly. This is the one
87
- * registry `pithy token` reads.
97
+ * The project's token-profile registry: the aggregating `ci-system` profile and, when any composed
98
+ * capability declares {@link Capability.runtimePermissions}, the aggregating `runtime` profile. Nothing
99
+ * else — a capability cannot add a profile. This is the one registry `pithy token` reads.
88
100
  */
89
101
  function resolveTokenProfiles(capabilities) {
90
- const profiles = { [CI_SYSTEM_PROFILE]: ciSystemProfile(capabilities) };
91
- for (const capability of capabilities) for (const [name, seam] of Object.entries(capability.tokenProfiles ?? {})) {
92
- if (profiles[name]) throw new CloudflareNotConfiguredError({
93
- message: `Token profile "${name}" is declared more than once (capability "${capability.name}").`,
94
- action: "Give each token profile a unique name."
95
- });
96
- profiles[name] = profileFromSeam(name, seam, capability.name);
97
- }
98
- return profiles;
102
+ const runtime = runtimeProfile(capabilities);
103
+ return {
104
+ [CI_SYSTEM_PROFILE]: ciSystemProfile(capabilities),
105
+ ...runtime ? { [RUNTIME_PROFILE]: runtime } : {}
106
+ };
99
107
  }
100
108
  /**
101
109
  * Resolve a profile by name from the aggregated registry, applying an adopter override. An unknown
@@ -0,0 +1,16 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
3
+ /**
4
+ * **The names of the one Cloudflare API token a deployed application holds (#728)**, in a module of their
5
+ * own. A Worker reads the token by {@link RUNTIME_TOKEN_SECRET}, and importing the profile registry to
6
+ * learn a string would put the token-minting client on every Worker's bundle. Nothing here imports anything.
7
+ */
8
+ /** The name of the one runtime token — the single Cloudflare API token a deployed application holds (#728). */
9
+ export declare const RUNTIME_PROFILE = "runtime";
10
+ /**
11
+ * The secret the runtime token's value is stored under, and the binding every Worker that calls Cloudflare
12
+ * reads it through. Unscoped, because it is a binding name: the Secrets Store entry it resolves to carries
13
+ * the project and environment (`<project>-<env>-runtime-cf-api-token`).
14
+ */
15
+ export declare const RUNTIME_TOKEN_SECRET = "RUNTIME_CF_API_TOKEN";
16
+ //# sourceMappingURL=runtime.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../../src/tokens/runtime.ts"],"names":[],"mappings":"AAGA;;;;GAIG;AAEH,+GAA+G;AAC/G,eAAO,MAAM,eAAe,YAAY,CAAC;AAEzC;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,yBAAyB,CAAC"}
@@ -0,0 +1,18 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
3
+ //#region src/tokens/runtime.ts
4
+ /**
5
+ * **The names of the one Cloudflare API token a deployed application holds (#728)**, in a module of their
6
+ * own. A Worker reads the token by {@link RUNTIME_TOKEN_SECRET}, and importing the profile registry to
7
+ * learn a string would put the token-minting client on every Worker's bundle. Nothing here imports anything.
8
+ */
9
+ /** The name of the one runtime token — the single Cloudflare API token a deployed application holds (#728). */
10
+ const RUNTIME_PROFILE = "runtime";
11
+ /**
12
+ * The secret the runtime token's value is stored under, and the binding every Worker that calls Cloudflare
13
+ * reads it through. Unscoped, because it is a binding name: the Secrets Store entry it resolves to carries
14
+ * the project and environment (`<project>-<env>-runtime-cf-api-token`).
15
+ */
16
+ const RUNTIME_TOKEN_SECRET = "RUNTIME_CF_API_TOKEN";
17
+ //#endregion
18
+ export { RUNTIME_PROFILE, RUNTIME_TOKEN_SECRET };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pithy-sh/cloudflare",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -25,7 +25,7 @@
25
25
  },
26
26
  "scripts": {
27
27
  "build": "tsdown && tsc -p tsconfig.build.json && bun ../../tooling/build/src/stampDeclarations.ts",
28
- "typecheck": "tsc -p tsconfig.json",
28
+ "typecheck": "bun check -p tsconfig.json",
29
29
  "test": "vitest run",
30
30
  "test:node": "vitest run --project=node",
31
31
  "test:integration": "vitest run --config vitest.integration.config.ts",
@@ -40,7 +40,7 @@
40
40
  "js-base64": "^3.9.2"
41
41
  },
42
42
  "peerDependencies": {
43
- "@pithy-sh/core": "^0.10.0",
43
+ "@pithy-sh/core": "^0.12.0",
44
44
  "zod": "^4.4.0"
45
45
  },
46
46
  "devDependencies": {