@pithy-sh/cloudflare 0.4.2 → 0.6.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 (95) hide show
  1. package/README.md +3 -3
  2. package/dist/ai/aiManager.d.ts +15 -1
  3. package/dist/ai/aiManager.d.ts.map +1 -1
  4. package/dist/ai/aiManager.js +39 -6
  5. package/dist/analytics/analyticsManager.d.ts +78 -0
  6. package/dist/analytics/analyticsManager.d.ts.map +1 -0
  7. package/dist/analytics/analyticsManager.js +206 -0
  8. package/dist/analytics/datasets.d.ts +26 -0
  9. package/dist/analytics/datasets.d.ts.map +1 -0
  10. package/dist/analytics/datasets.js +26 -0
  11. package/dist/analytics/window.d.ts +50 -0
  12. package/dist/analytics/window.d.ts.map +1 -0
  13. package/dist/analytics/window.js +67 -0
  14. package/dist/artifacts/artifactsManager.d.ts +52 -0
  15. package/dist/artifacts/artifactsManager.d.ts.map +1 -0
  16. package/dist/artifacts/artifactsManager.js +150 -0
  17. package/dist/client/clients.d.ts +13 -1
  18. package/dist/client/clients.d.ts.map +1 -1
  19. package/dist/client/clients.js +22 -1
  20. package/dist/client/manager.d.ts +13 -2
  21. package/dist/client/manager.d.ts.map +1 -1
  22. package/dist/client/manager.js +18 -5
  23. package/dist/d1/d1Manager.d.ts.map +1 -1
  24. package/dist/d1/d1Manager.js +3 -3
  25. package/dist/d1/d1PreparedStatement.d.ts +12 -0
  26. package/dist/d1/d1PreparedStatement.d.ts.map +1 -1
  27. package/dist/d1/d1PreparedStatement.js +13 -3
  28. package/dist/media/imageManager.d.ts +25 -0
  29. package/dist/media/imageManager.d.ts.map +1 -1
  30. package/dist/media/imageManager.js +58 -2
  31. package/dist/media/streamManager.d.ts +15 -0
  32. package/dist/media/streamManager.d.ts.map +1 -1
  33. package/dist/media/streamManager.js +28 -1
  34. package/dist/oauth/callback.d.ts +32 -0
  35. package/dist/oauth/callback.d.ts.map +1 -0
  36. package/dist/oauth/callback.js +58 -0
  37. package/dist/oauth/catalog.d.ts +26 -0
  38. package/dist/oauth/catalog.d.ts.map +1 -0
  39. package/dist/oauth/catalog.js +30 -0
  40. package/dist/oauth/client.d.ts +126 -0
  41. package/dist/oauth/client.d.ts.map +1 -0
  42. package/dist/oauth/client.js +200 -0
  43. package/dist/oauth/scopes.d.ts +62 -0
  44. package/dist/oauth/scopes.d.ts.map +1 -0
  45. package/dist/oauth/scopes.js +79 -0
  46. package/dist/queue/queueProvisioner.d.ts +30 -0
  47. package/dist/queue/queueProvisioner.d.ts.map +1 -0
  48. package/dist/queue/queueProvisioner.js +72 -0
  49. package/dist/r2/r2Manager.d.ts +11 -1
  50. package/dist/r2/r2Manager.d.ts.map +1 -1
  51. package/dist/r2/r2Manager.js +18 -1
  52. package/dist/secrets/secretsStoreManager.d.ts.map +1 -1
  53. package/dist/secrets/secretsStoreManager.js +3 -2
  54. package/dist/test-utils/settle.d.ts +79 -0
  55. package/dist/test-utils/settle.d.ts.map +1 -0
  56. package/dist/test-utils/settle.js +105 -0
  57. package/dist/tokens/accountTokensManager.d.ts +8 -7
  58. package/dist/tokens/accountTokensManager.d.ts.map +1 -1
  59. package/dist/tokens/accountTokensManager.js +2 -1
  60. package/dist/tokens/permissions.d.ts +49 -0
  61. package/dist/tokens/permissions.d.ts.map +1 -1
  62. package/dist/tokens/permissions.js +56 -2
  63. package/dist/tokens/profiles.d.ts +10 -30
  64. package/dist/tokens/profiles.d.ts.map +1 -1
  65. package/dist/tokens/profiles.js +48 -36
  66. package/dist/tokens/runtime.d.ts +16 -0
  67. package/dist/tokens/runtime.d.ts.map +1 -0
  68. package/dist/tokens/runtime.js +18 -0
  69. package/dist/workers/buildsManager.js +1 -1
  70. package/dist/workflows/stepFailure.js +1 -1
  71. package/dist/workflows/workflowsClient.js +1 -1
  72. package/package.json +3 -3
  73. package/src/ai/aiManager.ts +44 -5
  74. package/src/analytics/analyticsManager.ts +311 -0
  75. package/src/analytics/datasets.ts +30 -0
  76. package/src/analytics/window.ts +101 -0
  77. package/src/artifacts/artifactsManager.ts +199 -0
  78. package/src/client/clients.ts +28 -1
  79. package/src/client/manager.ts +24 -6
  80. package/src/d1/d1Manager.ts +7 -3
  81. package/src/d1/d1PreparedStatement.ts +21 -2
  82. package/src/media/imageManager.ts +93 -1
  83. package/src/media/streamManager.ts +34 -0
  84. package/src/oauth/callback.ts +70 -0
  85. package/src/oauth/catalog.ts +39 -0
  86. package/src/oauth/client.ts +265 -0
  87. package/src/oauth/scopes.ts +91 -0
  88. package/src/queue/queueProvisioner.ts +79 -0
  89. package/src/r2/r2Manager.ts +29 -2
  90. package/src/secrets/secretsStoreManager.ts +2 -1
  91. package/src/test-utils/settle.ts +139 -0
  92. package/src/tokens/accountTokensManager.ts +12 -0
  93. package/src/tokens/permissions.ts +81 -0
  94. package/src/tokens/profiles.ts +58 -70
  95. package/src/tokens/runtime.ts +18 -0
@@ -46,12 +46,93 @@ export const PERMISSION_GROUPS = {
46
46
  //
47
47
  // Still not a group that can alter a zone. "Workers Routes Write" writes routes on a zone; changing
48
48
  // the zone itself would be "Zone Write", which this catalog does not have and will not get.
49
+ //
50
+ // Only this group is zone-limited. "Workers Scripts Write" is account-scoped and attaches a custom
51
+ // domain — a proxied DNS record and a certificate — on any zone the account holds (measured, #709;
52
+ // docs/TOKENS.md). So a token carrying the base can reach every zone's hostnames whatever this scopes.
49
53
  "routes:write": ["Workers Routes Write"],
54
+ // **Reading the account's analytics, and nothing else (#728).** The GraphQL Analytics API's account-level
55
+ // datasets — Stream minutes viewed, Images delivered — answer to this group. Cloudflare also has
56
+ // "Analytics Read", which is the **zone** group and reaches none of them.
57
+ //
58
+ // A second credential class for the kit: every other key here acts on resources the project owns,
59
+ // and this one reads the Cloudflare account. Its grain is the whole account, never one environment, so
60
+ // a capability reading it narrows the answer itself — media joins it to this environment's own assets.
61
+ "analytics:read": ["Account Analytics Read"],
62
+ // **Media's Cloudflare products, so the catalog can mint what an operator used to make by hand (#728).**
63
+ // A direct upload, a signing change and a delete on Cloudflare Images and on Stream. Both groups are
64
+ // account-scoped and edit only the account's Images and Stream libraries; they are carried by the one
65
+ // `runtime` token a deployed application holds, never by `ci-system`.
66
+ "images:write": ["Images Write"],
67
+ "stream:write": ["Stream Write"],
68
+ // **Creating and deleting the queue behind a `queue` binding (#662).** `pithy provision` creates one per
69
+ // binding and `pithy feature destroy` deletes a feature's, so `ci-system` carries it wherever a composition
70
+ // binds a queue. A Worker sends through its producer binding and needs no API token for it.
71
+ "queues:write": ["Queues Write"],
72
+ // **Listing and deleting the repos in a feature's Artifacts namespace, then the namespace (#673).** `pithy feature
73
+ // destroy` reaps them, because a repo costs storage and nothing else deletes one, so `ci-system` carries it wherever
74
+ // a composition binds `artifacts`. The account catalog names the group Write; the API schema's label says Edit. A Worker creates, reads and mints through its binding and needs no API token for any of it.
75
+ "artifacts:write": ["Artifacts Write"],
50
76
  } as const;
51
77
 
52
78
  /** A known permission key — one of {@link PERMISSION_GROUPS}'s keys. */
53
79
  export type PermissionKey = keyof typeof PERMISSION_GROUPS;
54
80
 
81
+ /**
82
+ * **The third column: the OAuth scope each key needs a signed-in session to hold (#759).**
83
+ *
84
+ * A session mints only what it could do itself. Cloudflare refuses a mint whose permission groups the
85
+ * session's scopes do not cover — *"The OAuth session does not include the scopes required for the
86
+ * requested permission groups"*, measured 2026-10-15 — so the scopes `pithy cloudflare connect` asks for
87
+ * are derived from the permissions the project's tokens carry, through this table, and never listed by
88
+ * hand. `satisfies` over {@link PermissionKey} makes a key added above without a scope here a type error.
89
+ *
90
+ * **The ids pair with the group names one to one.** `GET /oauth/scopes` names each scope exactly as the
91
+ * permission group it grants — `d1.write` is "D1 Write", `workers-routes.write` is "Workers Routes Write" —
92
+ * which is what lets the integration test check this column against Cloudflare's own catalog the way
93
+ * the group names are checked against the account. A write scope also covers its read: a session holding
94
+ * `d1.write` minted a "D1 Read" token, so a key never needs both.
95
+ *
96
+ * Dot-delimited, always. The colon form (`account:read`) is what a first-party client like wrangler asks
97
+ * for, and a third-party client can neither register one nor request it.
98
+ */
99
+ export const OAUTH_SCOPES = {
100
+ "d1:read": ["d1.read"],
101
+ "d1:write": ["d1.write"],
102
+ "workers:write": ["workers-scripts.write"],
103
+ "secrets:read": ["secrets-store.read"],
104
+ "secrets:write": ["secrets-store.write"],
105
+ "email:routing": ["email-routing-rule.write"],
106
+ "kv:write": ["workers-kv-storage.write"],
107
+ "r2:read": ["workers-r2.read"],
108
+ "r2:write": ["workers-r2.write"],
109
+ "vectorize:read": ["vectorize.read"],
110
+ "vectorize:write": ["vectorize.write"],
111
+ "ai:read": ["ai.read"],
112
+ "zone:read": ["zone.read"],
113
+ "routes:write": ["workers-routes.write"],
114
+ "analytics:read": ["account-analytics.read"],
115
+ "images:write": ["images.write"],
116
+ "stream:write": ["stream.write"],
117
+ "queues:write": ["queues.write"],
118
+ "artifacts:write": ["artifacts.write"],
119
+ } as const satisfies Record<PermissionKey, readonly string[]>;
120
+
121
+ /**
122
+ * Resolve permission keys to the OAuth scopes a session needs to act — or mint — with them, de-duped in
123
+ * first-seen order. An unknown key is refused exactly as {@link resolvePermissionKeys} refuses it.
124
+ */
125
+ export function oauthScopesFor(keys: readonly string[]): string[] {
126
+ resolvePermissionKeys([...keys]);
127
+ const scopes: string[] = [];
128
+ for (const key of keys) {
129
+ for (const scope of OAUTH_SCOPES[key as PermissionKey]) {
130
+ if (!scopes.includes(scope)) scopes.push(scope);
131
+ }
132
+ }
133
+ return scopes;
134
+ }
135
+
55
136
  /**
56
137
  * The keys whose Cloudflare permission group is **zone-scoped**: a policy carrying one must name zone
57
138
  * resources (`com.cloudflare.api.account.zone.<id>`), never the account resource.
@@ -1,7 +1,7 @@
1
1
  // SPDX-FileCopyrightText: 2026 Pithy
2
2
  // SPDX-License-Identifier: MIT
3
3
 
4
- import type { Capability, TokenProfileSeam } from "@pithy-sh/core/src/capability/capability";
4
+ import type { Capability } from "@pithy-sh/core/src/capability/capability";
5
5
  import { CloudflareNotConfiguredError } from "../client/errors";
6
6
  import { accountResource, type TokenPermission, zoneResources } from "./accountTokensManager";
7
7
  import {
@@ -11,12 +11,13 @@ import {
11
11
  type PermissionKey,
12
12
  resolvePermissionKeys,
13
13
  } from "./permissions";
14
+ import { RUNTIME_PROFILE, RUNTIME_TOKEN_SECRET } from "./runtime";
14
15
 
15
16
  /**
16
17
  * Where a minted token's value is written.
17
18
  *
18
- * - `secrets-store` — the CF Secrets Store; a Worker reads it via its binding. The destination for a
19
- * worker-consumer token; resolved from the token's declared secret (its registry backend) unless a
19
+ * - `secrets-store` — the CF Secrets Store; a Worker reads it via its binding. The destination for the
20
+ * `runtime` token; resolved from the token's declared secret (its registry backend) unless a
20
21
  * profile or `--store` names it directly.
21
22
  * - `dev-vars` — `<config>/<project>/tokens.json`, keyed by environment and outside every checkout.
22
23
  * **Read by an operator, never by a command** — nothing resolves a credential from it. The store for
@@ -62,26 +63,12 @@ export interface TokenProfile {
62
63
  resources?: "account" | Record<string, string>;
63
64
  /** The secret-registry name the minted value is stored under; its declared backend is the destination. */
64
65
  secret: string;
65
- /**
66
- * Whether the value behind {@link secret} differs per environment (the default) or is one value every
67
- * environment shares. It decides the environment segment of the **CF Secrets Store entry name** the
68
- * minted value is written to, and nothing else — never the variable key, which is a variable name
69
- * and stays verbatim. A `global` profile writes one entry with the literal `global` in that slot,
70
- * matching what provisioning wrote; an `environment` profile writes one entry per environment.
71
- */
72
- secretScope?: TokenSecretScope;
73
66
  /** A built-in destination override (`dev-vars`/`ephemeral`/`secrets-store`); else the secret's backend. */
74
67
  defaultStore?: TokenStore;
75
68
  /** Why this profile exists / what job consumes it. */
76
69
  description: string;
77
70
  }
78
71
 
79
- /** Whether a profile's secret is one value per environment or one value shared by all of them. */
80
- export type TokenSecretScope = "environment" | "global";
81
-
82
- /** The valid secret scopes, for validating a capability's declaration. */
83
- const TOKEN_SECRET_SCOPES: readonly TokenSecretScope[] = ["environment", "global"];
84
-
85
72
  /** An adopter's override of a profile's defaults (from `pithy.config.ts` or CLI flags). Every field optional. */
86
73
  export interface ProfileOverride {
87
74
  /** Replace the profile's permission keys. */
@@ -112,15 +99,21 @@ function assertPermissionKeys(keys: readonly string[], where: string): Permissio
112
99
  if (!isPermissionKey(key)) {
113
100
  throw new CloudflareNotConfiguredError({
114
101
  message: `${where} declares an unknown permission key: ${key}.`,
115
- action:
116
- "Use a known permission key (d1:read, d1:write, workers:write, secrets:read, secrets:write, email:routing, kv:write).",
102
+ action: `Use a known permission key: ${Object.keys(PERMISSION_GROUPS).join(", ")}.`,
117
103
  });
118
104
  }
119
105
  }
120
106
  return keys as PermissionKey[];
121
107
  }
122
108
 
123
- /** Build the aggregating `ci-system` profile: the base permissions ∪ every capability's `ciPermissions`. */
109
+ /**
110
+ * Build the aggregating `ci-system` profile: the base permissions ∪ every capability's `ciPermissions` ∪
111
+ * `queues:write` where a composed capability binds a queue ∪ `artifacts:write` where one binds Artifacts.
112
+ *
113
+ * **The queue permission is derived from the binding, not declared (#662).** `pithy provision` creates the queue
114
+ * behind every `queue` binding it composes, the adopter's own app's included, and the app has no `ciPermissions`
115
+ * anyone would think to write. A permission read off the binding cannot be forgotten by whoever declares it.
116
+ */
124
117
  function ciSystemProfile(capabilities: readonly Capability[]): TokenProfile {
125
118
  const permissions: PermissionKey[] = [...CI_SYSTEM_BASE];
126
119
  for (const capability of capabilities) {
@@ -128,6 +121,16 @@ function ciSystemProfile(capabilities: readonly Capability[]): TokenProfile {
128
121
  if (!permissions.includes(key)) permissions.push(key);
129
122
  }
130
123
  }
124
+ const bindsQueue = capabilities.some((capability) =>
125
+ capability.requiredBindings.some((binding) => binding.type === "queue"),
126
+ );
127
+ if (bindsQueue && !permissions.includes("queues:write")) permissions.push("queues:write");
128
+ // **And `artifacts:write` where one binds Artifacts (#673)**, for the same reason: `pithy feature destroy` reaps
129
+ // every repo in the feature's namespace under this token, and the app that binds it declares no `ciPermissions`.
130
+ const bindsArtifacts = capabilities.some((capability) =>
131
+ capability.requiredBindings.some((binding) => binding.type === "artifacts"),
132
+ );
133
+ if (bindsArtifacts && !permissions.includes("artifacts:write")) permissions.push("artifacts:write");
131
134
  return {
132
135
  name: CI_SYSTEM_PROFILE,
133
136
  permissions,
@@ -139,66 +142,47 @@ function ciSystemProfile(capabilities: readonly Capability[]): TokenProfile {
139
142
  }
140
143
 
141
144
  /**
142
- * The seam fields this package reads. Core's {@link TokenProfileSeam} is the structural contract every
143
- * capability declares against; `secretScope` is additive on top of it, so it is named here rather than
144
- * in core — a seam value that omits it is still assignable, and one that carries it is read as declared.
145
+ * Build the aggregating `runtime` profile: every composed capability's `runtimePermissions`, each once,
146
+ * in first-declared order — or nothing, when the composition declares none.
145
147
  *
146
- * Exported so a capability can type its `tokenProfiles` entry against it. Declaring `secretScope` as an
147
- * inline object literal directly in `tokenProfiles` still trips TypeScript's excess-property check
148
- * against core's narrower `TokenProfileSeam`; declare the entry as its own `const` (with `as const` or
149
- * `satisfies TokenProfileSeamInput`) and it flows through.
148
+ * **One token per deployed application, the way `ci-system` is one per pipeline.** A capability names
149
+ * what its Workers call Cloudflare with and never a token of its own, so a second credential is not
150
+ * something a capability can ask for. The token is minted per environment into the Secrets Store, so
151
+ * revoking staging's leaves production reading. A key whose group is zone-scoped is refused here: a
152
+ * Worker's token is account-scoped, and its zones would have to be chosen by hand.
150
153
  */
151
- export interface TokenProfileSeamInput extends TokenProfileSeam {
152
- readonly secretScope?: string;
153
- }
154
-
155
- /** Build a concrete, validated worker-consumer {@link TokenProfile} from a capability's structural seam entry. */
156
- function profileFromSeam(name: string, seam: TokenProfileSeamInput, capability: string): TokenProfile {
157
- const store = seam.defaultStore;
158
- if (store !== undefined && !(TOKEN_STORES as readonly string[]).includes(store)) {
159
- throw new CloudflareNotConfiguredError({
160
- message: `Token profile "${name}" (capability "${capability}") declares an unknown store: ${store}.`,
161
- action: `Use one of: ${TOKEN_STORES.join(", ")}.`,
162
- });
163
- }
164
- const scope = seam.secretScope;
165
- if (scope !== undefined && !(TOKEN_SECRET_SCOPES as readonly string[]).includes(scope)) {
166
- throw new CloudflareNotConfiguredError({
167
- message: `Token profile "${name}" (capability "${capability}") declares an unknown secret scope: ${scope}.`,
168
- action: `Use one of: ${TOKEN_SECRET_SCOPES.join(", ")}.`,
169
- });
154
+ function runtimeProfile(capabilities: readonly Capability[]): TokenProfile | undefined {
155
+ const permissions: PermissionKey[] = [];
156
+ for (const capability of capabilities) {
157
+ const where = `Capability "${capability.name}"`;
158
+ const keys = assertPermissionKeys(capability.runtimePermissions ?? [], where);
159
+ assertAccountScoped(keys, `${where}'s runtimePermissions`);
160
+ for (const key of keys) {
161
+ if (!permissions.includes(key)) permissions.push(key);
162
+ }
170
163
  }
164
+ if (permissions.length === 0) return undefined;
171
165
  return {
172
- name,
173
- permissions: assertPermissionKeys(seam.permissions, `Token profile "${name}"`),
174
- resources: seam.resources,
175
- secret: seam.secret ?? tokenSecretName(name),
176
- secretScope: scope as TokenSecretScope | undefined,
177
- defaultStore: store as TokenStore | undefined,
178
- description: seam.description ?? name,
166
+ name: RUNTIME_PROFILE,
167
+ permissions,
168
+ secret: RUNTIME_TOKEN_SECRET,
169
+ defaultStore: "secrets-store",
170
+ description:
171
+ "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.",
179
172
  };
180
173
  }
181
174
 
182
175
  /**
183
- * The project's token-profile registry: the aggregating `ci-system` profile plus every composed
184
- * capability's worker-consumer {@link Capability.tokenProfiles} slice. A capability profile that
185
- * clashes with `ci-system` (or another capability's name) fails loudly at assembly. This is the one
186
- * registry `pithy token` reads.
176
+ * The project's token-profile registry: the aggregating `ci-system` profile and, when any composed
177
+ * capability declares {@link Capability.runtimePermissions}, the aggregating `runtime` profile. Nothing
178
+ * else — a capability cannot add a profile. This is the one registry `pithy token` reads.
187
179
  */
188
180
  export function resolveTokenProfiles(capabilities: readonly Capability[]): Record<string, TokenProfile> {
189
- const profiles: Record<string, TokenProfile> = { [CI_SYSTEM_PROFILE]: ciSystemProfile(capabilities) };
190
- for (const capability of capabilities) {
191
- for (const [name, seam] of Object.entries(capability.tokenProfiles ?? {})) {
192
- if (profiles[name]) {
193
- throw new CloudflareNotConfiguredError({
194
- message: `Token profile "${name}" is declared more than once (capability "${capability.name}").`,
195
- action: "Give each token profile a unique name.",
196
- });
197
- }
198
- profiles[name] = profileFromSeam(name, seam, capability.name);
199
- }
200
- }
201
- return profiles;
181
+ const runtime = runtimeProfile(capabilities);
182
+ return {
183
+ [CI_SYSTEM_PROFILE]: ciSystemProfile(capabilities),
184
+ ...(runtime ? { [RUNTIME_PROFILE]: runtime } : {}),
185
+ };
202
186
  }
203
187
 
204
188
  /**
@@ -263,6 +247,10 @@ export function permissionsForKeys(keys: PermissionKey[], accountId: string): To
263
247
  * it is a policy of its own rather than a permission on the profile because its resources are zones,
264
248
  * which a profile cannot name: they are resolved from the domains at mint time.
265
249
  *
250
+ * **This limits the route, not the token.** The account policy beside it carries Workers Scripts Write,
251
+ * which attaches a custom domain on any zone the account holds (measured, #709; `docs/TOKENS.md`). Only
252
+ * the route calls are held to the declared zones.
253
+ *
266
254
  * **Empty for an empty zone list, deliberately.** A project that declares no domain attaches no route,
267
255
  * so it mints exactly the token it minted before this existed. No new permission for a project that
268
256
  * needs none.
@@ -0,0 +1,18 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
3
+
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
+
10
+ /** The name of the one runtime token — the single Cloudflare API token a deployed application holds (#728). */
11
+ export const RUNTIME_PROFILE = "runtime";
12
+
13
+ /**
14
+ * The secret the runtime token's value is stored under, and the binding every Worker that calls Cloudflare
15
+ * reads it through. Unscoped, because it is a binding name: the Secrets Store entry it resolves to carries
16
+ * the project and environment (`<project>-<env>-runtime-cf-api-token`).
17
+ */
18
+ export const RUNTIME_TOKEN_SECRET = "RUNTIME_CF_API_TOKEN";