@pellux/goodvibes-sdk 0.25.8 → 0.25.10
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/README.md +4 -0
- package/dist/_internal/contracts/artifacts/operator-contract.json +1 -1
- package/dist/_internal/contracts/generated/foundation-metadata.d.ts +1 -1
- package/dist/_internal/contracts/generated/foundation-metadata.js +1 -1
- package/dist/_internal/contracts/generated/operator-contract.js +1 -1
- package/dist/_internal/platform/batch/manager.d.ts.map +1 -1
- package/dist/_internal/platform/batch/manager.js +4 -0
- package/dist/_internal/platform/batch/types.d.ts +4 -0
- package/dist/_internal/platform/batch/types.d.ts.map +1 -1
- package/dist/_internal/platform/cloudflare/client.d.ts +3 -0
- package/dist/_internal/platform/cloudflare/client.d.ts.map +1 -0
- package/dist/_internal/platform/cloudflare/client.js +9 -0
- package/dist/_internal/platform/cloudflare/config.d.ts +8 -0
- package/dist/_internal/platform/cloudflare/config.d.ts.map +1 -0
- package/dist/_internal/platform/cloudflare/config.js +48 -0
- package/dist/_internal/platform/cloudflare/constants.d.ts +18 -0
- package/dist/_internal/platform/cloudflare/constants.d.ts.map +1 -0
- package/dist/_internal/platform/cloudflare/constants.js +36 -0
- package/dist/_internal/platform/cloudflare/discovery.d.ts +23 -0
- package/dist/_internal/platform/cloudflare/discovery.d.ts.map +1 -0
- package/dist/_internal/platform/cloudflare/discovery.js +57 -0
- package/dist/_internal/platform/cloudflare/index.d.ts +6 -0
- package/dist/_internal/platform/cloudflare/index.d.ts.map +1 -0
- package/dist/_internal/platform/cloudflare/index.js +4 -0
- package/dist/_internal/platform/cloudflare/manager.d.ts +30 -0
- package/dist/_internal/platform/cloudflare/manager.d.ts.map +1 -0
- package/dist/_internal/platform/cloudflare/manager.js +671 -0
- package/dist/_internal/platform/cloudflare/resources.d.ts +79 -0
- package/dist/_internal/platform/cloudflare/resources.d.ts.map +1 -0
- package/dist/_internal/platform/cloudflare/resources.js +353 -0
- package/dist/_internal/platform/cloudflare/types.d.ts +689 -0
- package/dist/_internal/platform/cloudflare/types.d.ts.map +1 -0
- package/dist/_internal/platform/cloudflare/types.js +10 -0
- package/dist/_internal/platform/cloudflare/utils.d.ts +14 -0
- package/dist/_internal/platform/cloudflare/utils.d.ts.map +1 -0
- package/dist/_internal/platform/cloudflare/utils.js +222 -0
- package/dist/_internal/platform/cloudflare/worker-source.d.ts +2 -0
- package/dist/_internal/platform/cloudflare/worker-source.d.ts.map +1 -0
- package/dist/_internal/platform/cloudflare/worker-source.js +165 -0
- package/dist/_internal/platform/config/schema-domain-runtime.d.ts +24 -0
- package/dist/_internal/platform/config/schema-domain-runtime.d.ts.map +1 -1
- package/dist/_internal/platform/config/schema-domain-runtime.js +169 -0
- package/dist/_internal/platform/config/schema-types.d.ts +26 -2
- package/dist/_internal/platform/config/schema-types.d.ts.map +1 -1
- package/dist/_internal/platform/daemon/http/cloudflare-routes.d.ts +7 -0
- package/dist/_internal/platform/daemon/http/cloudflare-routes.d.ts.map +1 -0
- package/dist/_internal/platform/daemon/http/cloudflare-routes.js +201 -0
- package/dist/_internal/platform/daemon/http/router.d.ts +1 -1
- package/dist/_internal/platform/daemon/http/router.d.ts.map +1 -1
- package/dist/_internal/platform/daemon/http/router.js +15 -0
- package/dist/_internal/platform/version.js +1 -1
- package/dist/workers.d.ts +3 -0
- package/dist/workers.d.ts.map +1 -1
- package/dist/workers.js +13 -1
- package/package.json +2 -1
|
@@ -0,0 +1,671 @@
|
|
|
1
|
+
import { resolveSecretInput } from '../config/secret-refs.js';
|
|
2
|
+
import { summarizeError } from '../utils/error-display.js';
|
|
3
|
+
import { createCloudflareApiClient } from './client.js';
|
|
4
|
+
import { readCloudflareConfig } from './config.js';
|
|
5
|
+
import { CLOUDFLARE_API_TOKEN_KEY, CLOUDFLARE_WORKER_CLIENT_TOKEN_KEY, CLOUDFLARE_WORKER_OPERATOR_TOKEN_KEY, DEFAULT_DLQ_NAME, DEFAULT_DO_NAMESPACE_NAME, DEFAULT_KV_NAMESPACE_NAME, DEFAULT_QUEUE_NAME, DEFAULT_R2_BUCKET_NAME, DEFAULT_SECRETS_STORE_NAME, DEFAULT_TUNNEL_NAME, DEFAULT_WORKER_CRON, DEFAULT_WORKER_NAME, } from './constants.js';
|
|
6
|
+
import { discoverZones, resolveZone, selectDiscoveredZone, tryDiscover, } from './discovery.js';
|
|
7
|
+
import { configureDns, configureWorkerSubdomain, ensureAccess, ensureKvNamespace, ensureQueue, ensureQueueConsumer, ensureR2Bucket, ensureSecretsStore, ensureTunnel, findDurableObjectNamespace, uploadWorker, } from './resources.js';
|
|
8
|
+
import { CloudflareControlPlaneError } from './types.js';
|
|
9
|
+
import { buildTokenRequirements, buildTokenResources, clean, collectAsync, collectSingleAccount, hostnameFromUrl, requireKvNamespaceId, requireQueueId, resolveComponents, safeResponseText, selectPermissionGroups, stripTrailingSlash, } from './utils.js';
|
|
10
|
+
export class CloudflareControlPlaneManager {
|
|
11
|
+
options;
|
|
12
|
+
createClient;
|
|
13
|
+
fetchImpl;
|
|
14
|
+
constructor(options) {
|
|
15
|
+
this.options = options;
|
|
16
|
+
this.createClient = options.createClient ?? createCloudflareApiClient;
|
|
17
|
+
this.fetchImpl = options.fetch ?? fetch;
|
|
18
|
+
}
|
|
19
|
+
async describeStatus() {
|
|
20
|
+
const config = this.readConfig();
|
|
21
|
+
const apiToken = await this.resolveApiToken({});
|
|
22
|
+
const workerToken = await this.resolveOperatorToken({});
|
|
23
|
+
const workerClientToken = await this.resolveWorkerClientToken({});
|
|
24
|
+
const configured = {
|
|
25
|
+
accountId: config.accountId.length > 0,
|
|
26
|
+
apiToken: apiToken.value !== null,
|
|
27
|
+
zone: config.zoneId.length > 0 || config.zoneName.length > 0,
|
|
28
|
+
workerName: config.workerName.length > 0,
|
|
29
|
+
daemonBaseUrl: config.daemonBaseUrl.length > 0,
|
|
30
|
+
daemonHostname: config.daemonHostname.length > 0,
|
|
31
|
+
workerBaseUrl: config.workerBaseUrl.length > 0,
|
|
32
|
+
workerHostname: config.workerHostname.length > 0,
|
|
33
|
+
queueName: config.queueName.length > 0,
|
|
34
|
+
deadLetterQueueName: config.deadLetterQueueName.length > 0,
|
|
35
|
+
workerToken: workerToken.value !== null,
|
|
36
|
+
workerClientToken: workerClientToken.value !== null,
|
|
37
|
+
tunnel: config.tunnelId.length > 0 || config.tunnelName.length > 0,
|
|
38
|
+
access: config.accessAppId.length > 0 || config.accessServiceTokenId.length > 0 || config.accessServiceTokenRef.length > 0,
|
|
39
|
+
kv: config.kvNamespaceId.length > 0 || config.kvNamespaceName.length > 0,
|
|
40
|
+
durableObjects: config.durableObjectNamespaceId.length > 0 || config.durableObjectNamespaceName.length > 0,
|
|
41
|
+
r2: config.r2BucketName.length > 0,
|
|
42
|
+
secretsStore: config.secretsStoreId.length > 0 || config.secretsStoreName.length > 0,
|
|
43
|
+
};
|
|
44
|
+
const warnings = [];
|
|
45
|
+
if (config.enabled && !configured.apiToken)
|
|
46
|
+
warnings.push('Cloudflare is enabled but no API token is configured.');
|
|
47
|
+
if (config.enabled && !configured.daemonBaseUrl)
|
|
48
|
+
warnings.push('Cloudflare is enabled but no daemonBaseUrl is configured for Worker-to-daemon calls.');
|
|
49
|
+
if (config.enabled && !configured.workerToken)
|
|
50
|
+
warnings.push('Cloudflare is enabled but no Worker-to-daemon operator token is configured.');
|
|
51
|
+
if (config.enabled && !configured.workerClientToken)
|
|
52
|
+
warnings.push('Cloudflare Worker client auth is not configured; provisioning will generate one.');
|
|
53
|
+
const ready = config.enabled &&
|
|
54
|
+
configured.accountId &&
|
|
55
|
+
configured.apiToken &&
|
|
56
|
+
configured.workerName &&
|
|
57
|
+
configured.daemonBaseUrl &&
|
|
58
|
+
configured.workerBaseUrl &&
|
|
59
|
+
configured.queueName &&
|
|
60
|
+
configured.deadLetterQueueName &&
|
|
61
|
+
configured.workerToken;
|
|
62
|
+
return { enabled: config.enabled, ready, configured, config, warnings };
|
|
63
|
+
}
|
|
64
|
+
tokenRequirements(input = {}) {
|
|
65
|
+
const components = resolveComponents(input.components);
|
|
66
|
+
return {
|
|
67
|
+
ok: true,
|
|
68
|
+
components,
|
|
69
|
+
permissions: buildTokenRequirements(components, input.includeBootstrap === true),
|
|
70
|
+
bootstrapToken: {
|
|
71
|
+
requiredForSdkCreation: true,
|
|
72
|
+
storeInGoodVibes: false,
|
|
73
|
+
instructions: [
|
|
74
|
+
'Create a temporary Cloudflare API token in the dashboard with Account API Tokens Write, Account Settings Read, and the GoodVibes operational permissions listed here.',
|
|
75
|
+
'Pass that temporary token as bootstrapToken to POST /api/cloudflare/token/create.',
|
|
76
|
+
'The SDK uses the bootstrap token once to create a narrower operational token, stores only the operational token as a goodvibes:// secret when requested, and never persists the bootstrap token.',
|
|
77
|
+
],
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
async createOperationalToken(input) {
|
|
82
|
+
const bootstrapToken = clean(input.bootstrapToken);
|
|
83
|
+
if (!bootstrapToken) {
|
|
84
|
+
throw new CloudflareControlPlaneError('bootstrapToken is required to create a Cloudflare operational token. The bootstrap token is used once and is not stored.', 'CLOUDFLARE_BOOTSTRAP_TOKEN_REQUIRED', 400);
|
|
85
|
+
}
|
|
86
|
+
const persist = input.persistConfig !== false;
|
|
87
|
+
const config = this.readConfig();
|
|
88
|
+
const accountId = this.resolveAccountId(input.accountId);
|
|
89
|
+
const components = resolveComponents(input.components);
|
|
90
|
+
const requirements = buildTokenRequirements(components, false);
|
|
91
|
+
const client = await this.createClient(bootstrapToken);
|
|
92
|
+
await client.accounts.get({ account_id: accountId });
|
|
93
|
+
const zone = await resolveZone(client, {
|
|
94
|
+
accountId,
|
|
95
|
+
zoneId: input.zoneId,
|
|
96
|
+
zoneName: input.zoneName,
|
|
97
|
+
configuredZoneId: config.zoneId,
|
|
98
|
+
configuredZoneName: config.zoneName,
|
|
99
|
+
required: components.dns,
|
|
100
|
+
});
|
|
101
|
+
const groups = await this.collectPermissionGroups(client, accountId);
|
|
102
|
+
const permissionIds = selectPermissionGroups(requirements, groups);
|
|
103
|
+
const resources = buildTokenResources(accountId, zone?.id, components);
|
|
104
|
+
const tokenName = clean(input.tokenName) || 'GoodVibes Cloudflare Operational';
|
|
105
|
+
const token = await this.requireAccountTokens(client).create({
|
|
106
|
+
account_id: accountId,
|
|
107
|
+
name: tokenName,
|
|
108
|
+
policies: [
|
|
109
|
+
{
|
|
110
|
+
effect: 'allow',
|
|
111
|
+
permission_groups: permissionIds.map((id) => ({ id })),
|
|
112
|
+
resources,
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
...(clean(input.expiresOn) ? { expires_on: clean(input.expiresOn) } : {}),
|
|
116
|
+
});
|
|
117
|
+
if (!token.value) {
|
|
118
|
+
throw new CloudflareControlPlaneError('Cloudflare did not return a token value for the newly-created operational token.', 'CLOUDFLARE_TOKEN_VALUE_MISSING', 502);
|
|
119
|
+
}
|
|
120
|
+
let apiTokenRef;
|
|
121
|
+
if (input.storeApiToken !== false) {
|
|
122
|
+
await this.storeSecret(CLOUDFLARE_API_TOKEN_KEY, token.value);
|
|
123
|
+
apiTokenRef = `goodvibes://secrets/goodvibes/${CLOUDFLARE_API_TOKEN_KEY}`;
|
|
124
|
+
this.setConfig('cloudflare.apiTokenRef', apiTokenRef, persist);
|
|
125
|
+
this.setConfig('cloudflare.accountId', accountId, persist);
|
|
126
|
+
if (zone) {
|
|
127
|
+
this.setConfig('cloudflare.zoneId', zone.id, persist);
|
|
128
|
+
this.setConfig('cloudflare.zoneName', zone.name, persist);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
ok: true,
|
|
133
|
+
...(token.id ? { tokenId: token.id } : {}),
|
|
134
|
+
tokenName,
|
|
135
|
+
tokenSource: 'bootstrap',
|
|
136
|
+
...(apiTokenRef ? { apiTokenRef } : {}),
|
|
137
|
+
...(input.returnGeneratedToken ? { generatedToken: token.value } : {}),
|
|
138
|
+
accountId,
|
|
139
|
+
...(zone ? { zoneId: zone.id } : {}),
|
|
140
|
+
permissions: requirements,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
async discover(input = {}) {
|
|
144
|
+
const apiToken = await this.resolveApiToken(input);
|
|
145
|
+
if (!apiToken.value) {
|
|
146
|
+
throw new CloudflareControlPlaneError('Cloudflare API token is required. Set CLOUDFLARE_API_TOKEN, configure cloudflare.apiTokenRef, or pass apiToken.', 'CLOUDFLARE_API_TOKEN_REQUIRED', 400);
|
|
147
|
+
}
|
|
148
|
+
const client = await this.createClient(apiToken.value);
|
|
149
|
+
const config = this.readConfig();
|
|
150
|
+
const warnings = [];
|
|
151
|
+
const accounts = client.accounts.list
|
|
152
|
+
? await collectAsync(client.accounts.list())
|
|
153
|
+
: await collectSingleAccount(client, clean(input.accountId) || config.accountId, warnings);
|
|
154
|
+
const selectedAccountId = clean(input.accountId) || config.accountId || (accounts.length === 1 ? accounts[0]?.id ?? '' : '');
|
|
155
|
+
const selectedAccount = selectedAccountId ? accounts.find((account) => account.id === selectedAccountId) ?? await client.accounts.get({ account_id: selectedAccountId }) : undefined;
|
|
156
|
+
const zones = await discoverZones(client, {
|
|
157
|
+
accountId: selectedAccount?.id ?? '',
|
|
158
|
+
zoneName: input.zoneName,
|
|
159
|
+
warnings,
|
|
160
|
+
});
|
|
161
|
+
const selectedZone = await selectDiscoveredZone(client, zones, {
|
|
162
|
+
zoneId: input.zoneId,
|
|
163
|
+
zoneName: input.zoneName,
|
|
164
|
+
configuredZoneId: config.zoneId,
|
|
165
|
+
configuredZoneName: config.zoneName,
|
|
166
|
+
warnings,
|
|
167
|
+
});
|
|
168
|
+
let workerSubdomain;
|
|
169
|
+
let queues;
|
|
170
|
+
let kvNamespaces;
|
|
171
|
+
let durableObjectNamespaces;
|
|
172
|
+
let r2Buckets;
|
|
173
|
+
let secretsStores;
|
|
174
|
+
let tunnels;
|
|
175
|
+
let accessApplications;
|
|
176
|
+
if (input.includeResources !== false && selectedAccount) {
|
|
177
|
+
workerSubdomain = await tryDiscover('worker-subdomain', warnings, async () => (await client.workers.subdomains.get({ account_id: selectedAccount.id })).subdomain);
|
|
178
|
+
queues = await tryDiscover('queues', warnings, async () => collectAsync(client.queues.list({ account_id: selectedAccount.id })));
|
|
179
|
+
if (client.kv)
|
|
180
|
+
kvNamespaces = await tryDiscover('kv-namespaces', warnings, async () => collectAsync(client.kv.namespaces.list({ account_id: selectedAccount.id })));
|
|
181
|
+
if (client.durableObjects)
|
|
182
|
+
durableObjectNamespaces = await tryDiscover('durable-object-namespaces', warnings, async () => collectAsync(client.durableObjects.namespaces.list({ account_id: selectedAccount.id })));
|
|
183
|
+
if (client.r2)
|
|
184
|
+
r2Buckets = await tryDiscover('r2-buckets', warnings, async () => (await client.r2.buckets.list({ account_id: selectedAccount.id })).buckets ?? []);
|
|
185
|
+
if (client.secretsStore)
|
|
186
|
+
secretsStores = await tryDiscover('secrets-stores', warnings, async () => collectAsync(client.secretsStore.stores.list({ account_id: selectedAccount.id })));
|
|
187
|
+
if (client.zeroTrust?.tunnels)
|
|
188
|
+
tunnels = await tryDiscover('zero-trust-tunnels', warnings, async () => collectAsync(client.zeroTrust.tunnels.cloudflared.list({ account_id: selectedAccount.id, is_deleted: false })));
|
|
189
|
+
if (client.zeroTrust?.access)
|
|
190
|
+
accessApplications = await tryDiscover('access-applications', warnings, async () => collectAsync(client.zeroTrust.access.applications.list({ account_id: selectedAccount.id })));
|
|
191
|
+
}
|
|
192
|
+
else if (!selectedAccount) {
|
|
193
|
+
warnings.push('Select a Cloudflare account before discovering account-scoped resources.');
|
|
194
|
+
}
|
|
195
|
+
return {
|
|
196
|
+
ok: true,
|
|
197
|
+
tokenSource: apiToken.source,
|
|
198
|
+
accounts,
|
|
199
|
+
...(selectedAccount ? { selectedAccount } : {}),
|
|
200
|
+
zones,
|
|
201
|
+
...(selectedZone ? { selectedZone } : {}),
|
|
202
|
+
...(workerSubdomain ? { workerSubdomain } : {}),
|
|
203
|
+
...(queues ? { queues } : {}),
|
|
204
|
+
...(kvNamespaces ? { kvNamespaces } : {}),
|
|
205
|
+
...(durableObjectNamespaces ? { durableObjectNamespaces } : {}),
|
|
206
|
+
...(r2Buckets ? { r2Buckets } : {}),
|
|
207
|
+
...(secretsStores ? { secretsStores } : {}),
|
|
208
|
+
...(tunnels ? { tunnels } : {}),
|
|
209
|
+
...(accessApplications ? { accessApplications } : {}),
|
|
210
|
+
warnings,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
async validate(input) {
|
|
214
|
+
const accountId = this.resolveAccountId(input.accountId);
|
|
215
|
+
const apiToken = await this.resolveApiToken(input);
|
|
216
|
+
if (!apiToken.value) {
|
|
217
|
+
throw new CloudflareControlPlaneError('Cloudflare API token is required. Set CLOUDFLARE_API_TOKEN, configure cloudflare.apiTokenRef, or pass apiToken.', 'CLOUDFLARE_API_TOKEN_REQUIRED', 400);
|
|
218
|
+
}
|
|
219
|
+
const client = await this.createClient(apiToken.value);
|
|
220
|
+
const account = await client.accounts.get({ account_id: accountId });
|
|
221
|
+
return {
|
|
222
|
+
ok: true,
|
|
223
|
+
account: {
|
|
224
|
+
id: account.id,
|
|
225
|
+
name: account.name,
|
|
226
|
+
...(account.type ? { type: account.type } : {}),
|
|
227
|
+
},
|
|
228
|
+
tokenSource: apiToken.source,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
async provision(input) {
|
|
232
|
+
const steps = [];
|
|
233
|
+
const persist = input.persistConfig !== false;
|
|
234
|
+
const config = this.readConfig();
|
|
235
|
+
const components = resolveComponents(input.components);
|
|
236
|
+
const accountId = this.resolveAccountId(input.accountId);
|
|
237
|
+
const workerName = this.resolveWorkerName(input.workerName);
|
|
238
|
+
const queueName = clean(input.queueName) || config.queueName || DEFAULT_QUEUE_NAME;
|
|
239
|
+
const deadLetterQueueName = clean(input.deadLetterQueueName) || config.deadLetterQueueName || DEFAULT_DLQ_NAME;
|
|
240
|
+
const daemonBaseUrl = stripTrailingSlash(clean(input.daemonBaseUrl) || config.daemonBaseUrl);
|
|
241
|
+
const daemonHostname = clean(input.daemonHostname) || config.daemonHostname || hostnameFromUrl(daemonBaseUrl);
|
|
242
|
+
const workerHostname = clean(input.workerHostname) || config.workerHostname;
|
|
243
|
+
const workerCron = clean(input.workerCron) || config.workerCron || DEFAULT_WORKER_CRON;
|
|
244
|
+
const queueJobPayloads = input.queueJobPayloads === true;
|
|
245
|
+
if (components.workers && !daemonBaseUrl) {
|
|
246
|
+
throw new CloudflareControlPlaneError('cloudflare.daemonBaseUrl is required so the deployed Worker can reach the GoodVibes daemon.', 'CLOUDFLARE_DAEMON_URL_REQUIRED', 400);
|
|
247
|
+
}
|
|
248
|
+
const apiToken = await this.resolveApiToken(input);
|
|
249
|
+
if (!apiToken.value) {
|
|
250
|
+
throw new CloudflareControlPlaneError('Cloudflare API token is required. Set CLOUDFLARE_API_TOKEN, configure cloudflare.apiTokenRef, or pass apiToken.', 'CLOUDFLARE_API_TOKEN_REQUIRED', 400);
|
|
251
|
+
}
|
|
252
|
+
if (input.storeApiToken && input.apiToken) {
|
|
253
|
+
await this.storeSecret(CLOUDFLARE_API_TOKEN_KEY, input.apiToken);
|
|
254
|
+
this.setConfig('cloudflare.apiTokenRef', `goodvibes://secrets/goodvibes/${CLOUDFLARE_API_TOKEN_KEY}`, persist);
|
|
255
|
+
steps.push({ name: 'store-api-token', status: 'ok', message: 'Stored Cloudflare API token in the GoodVibes secret store.' });
|
|
256
|
+
}
|
|
257
|
+
const client = await this.createClient(apiToken.value);
|
|
258
|
+
const resourceContext = this.createProvisioningContext();
|
|
259
|
+
const account = await client.accounts.get({ account_id: accountId });
|
|
260
|
+
steps.push({ name: 'validate-account', status: 'ok', message: `Validated Cloudflare account ${account.name}.`, resourceId: account.id });
|
|
261
|
+
const zone = await resolveZone(client, {
|
|
262
|
+
accountId,
|
|
263
|
+
zoneId: input.zoneId,
|
|
264
|
+
zoneName: input.zoneName,
|
|
265
|
+
configuredZoneId: config.zoneId,
|
|
266
|
+
configuredZoneName: config.zoneName,
|
|
267
|
+
required: components.dns || components.zeroTrustAccess,
|
|
268
|
+
});
|
|
269
|
+
if (zone) {
|
|
270
|
+
this.setConfig('cloudflare.zoneId', zone.id, persist);
|
|
271
|
+
this.setConfig('cloudflare.zoneName', zone.name, persist);
|
|
272
|
+
steps.push({ name: 'zone', status: 'ok', message: `Using Cloudflare zone ${zone.name}.`, resourceId: zone.id });
|
|
273
|
+
}
|
|
274
|
+
else if (components.dns || components.zeroTrustAccess) {
|
|
275
|
+
steps.push({ name: 'zone', status: 'warning', message: 'No Cloudflare zone was selected; DNS and Access hostname automation were skipped.' });
|
|
276
|
+
}
|
|
277
|
+
let deadLetterQueue;
|
|
278
|
+
let queue;
|
|
279
|
+
let deadLetterQueueId = '';
|
|
280
|
+
let queueId = '';
|
|
281
|
+
if (components.queues) {
|
|
282
|
+
deadLetterQueue = await ensureQueue(client, accountId, deadLetterQueueName, steps, 'dead-letter-queue');
|
|
283
|
+
queue = await ensureQueue(client, accountId, queueName, steps, 'queue');
|
|
284
|
+
deadLetterQueueId = requireQueueId(deadLetterQueue, deadLetterQueueName);
|
|
285
|
+
queueId = requireQueueId(queue, queueName);
|
|
286
|
+
}
|
|
287
|
+
const kv = components.kv ? await ensureKvNamespace(resourceContext, client, accountId, clean(input.kvNamespaceName) || config.kvNamespaceName || DEFAULT_KV_NAMESPACE_NAME, persist, steps) : undefined;
|
|
288
|
+
const r2 = components.r2 ? await ensureR2Bucket(resourceContext, client, accountId, clean(input.r2BucketName) || config.r2BucketName || DEFAULT_R2_BUCKET_NAME, persist, steps) : undefined;
|
|
289
|
+
const secretsStore = components.secretsStore ? await ensureSecretsStore(resourceContext, client, accountId, clean(input.secretsStoreName) || config.secretsStoreName || DEFAULT_SECRETS_STORE_NAME, persist, steps) : undefined;
|
|
290
|
+
const tunnel = components.zeroTrustTunnel
|
|
291
|
+
? await ensureTunnel(resourceContext, client, {
|
|
292
|
+
accountId,
|
|
293
|
+
tunnelName: clean(input.tunnelName) || config.tunnelName || DEFAULT_TUNNEL_NAME,
|
|
294
|
+
tunnelId: clean(input.tunnelId) || config.tunnelId,
|
|
295
|
+
daemonHostname,
|
|
296
|
+
tunnelServiceUrl: stripTrailingSlash(clean(input.tunnelServiceUrl) || daemonBaseUrl),
|
|
297
|
+
persist,
|
|
298
|
+
returnGeneratedSecrets: input.returnGeneratedSecrets === true,
|
|
299
|
+
steps,
|
|
300
|
+
})
|
|
301
|
+
: undefined;
|
|
302
|
+
let generatedWorkerClientToken;
|
|
303
|
+
let effectiveWorkerClientToken = '';
|
|
304
|
+
let subdomain = '';
|
|
305
|
+
let workerBaseUrl = stripTrailingSlash(clean(input.workerBaseUrl) || config.workerBaseUrl);
|
|
306
|
+
if (components.workers) {
|
|
307
|
+
await uploadWorker(client, {
|
|
308
|
+
accountId,
|
|
309
|
+
workerName,
|
|
310
|
+
queueName: components.queues ? queueName : '',
|
|
311
|
+
daemonBaseUrl,
|
|
312
|
+
queueJobPayloads,
|
|
313
|
+
kvNamespaceId: kv?.id ?? '',
|
|
314
|
+
r2BucketName: r2?.name ?? '',
|
|
315
|
+
durableObject: components.durableObjects,
|
|
316
|
+
});
|
|
317
|
+
steps.push({ name: 'deploy-worker', status: 'ok', message: `Uploaded Worker ${workerName}.`, resourceId: workerName });
|
|
318
|
+
const operatorToken = await this.resolveOperatorToken(input);
|
|
319
|
+
if (!operatorToken.value) {
|
|
320
|
+
throw new CloudflareControlPlaneError('A Worker-to-daemon operator token is required. Pass operatorToken, configure cloudflare.workerTokenRef, or ensure the daemon has an operator token.', 'CLOUDFLARE_OPERATOR_TOKEN_REQUIRED', 400);
|
|
321
|
+
}
|
|
322
|
+
if (input.storeOperatorToken && input.operatorToken) {
|
|
323
|
+
await this.storeSecret(CLOUDFLARE_WORKER_OPERATOR_TOKEN_KEY, input.operatorToken);
|
|
324
|
+
this.setConfig('cloudflare.workerTokenRef', `goodvibes://secrets/goodvibes/${CLOUDFLARE_WORKER_OPERATOR_TOKEN_KEY}`, persist);
|
|
325
|
+
steps.push({ name: 'store-worker-token', status: 'ok', message: 'Stored Worker-to-daemon token in the GoodVibes secret store.' });
|
|
326
|
+
}
|
|
327
|
+
await client.workers.scripts.secrets.update(workerName, {
|
|
328
|
+
account_id: accountId,
|
|
329
|
+
name: 'GOODVIBES_OPERATOR_TOKEN',
|
|
330
|
+
text: operatorToken.value,
|
|
331
|
+
type: 'secret_text',
|
|
332
|
+
});
|
|
333
|
+
steps.push({ name: 'set-worker-daemon-secret', status: 'ok', message: 'Configured Worker-to-daemon bearer token secret.' });
|
|
334
|
+
const workerClientToken = await this.resolveWorkerClientToken(input);
|
|
335
|
+
effectiveWorkerClientToken = workerClientToken.value ?? '';
|
|
336
|
+
if (!effectiveWorkerClientToken) {
|
|
337
|
+
effectiveWorkerClientToken = this.generateToken();
|
|
338
|
+
generatedWorkerClientToken = effectiveWorkerClientToken;
|
|
339
|
+
await this.storeSecret(CLOUDFLARE_WORKER_CLIENT_TOKEN_KEY, effectiveWorkerClientToken);
|
|
340
|
+
this.setConfig('cloudflare.workerClientTokenRef', `goodvibes://secrets/goodvibes/${CLOUDFLARE_WORKER_CLIENT_TOKEN_KEY}`, persist);
|
|
341
|
+
steps.push({ name: 'generate-worker-client-token', status: 'ok', message: 'Generated and stored Worker client bearer token.' });
|
|
342
|
+
}
|
|
343
|
+
else if (input.storeWorkerClientToken && input.workerClientToken) {
|
|
344
|
+
await this.storeSecret(CLOUDFLARE_WORKER_CLIENT_TOKEN_KEY, input.workerClientToken);
|
|
345
|
+
this.setConfig('cloudflare.workerClientTokenRef', `goodvibes://secrets/goodvibes/${CLOUDFLARE_WORKER_CLIENT_TOKEN_KEY}`, persist);
|
|
346
|
+
steps.push({ name: 'store-worker-client-token', status: 'ok', message: 'Stored Worker client bearer token in the GoodVibes secret store.' });
|
|
347
|
+
}
|
|
348
|
+
await client.workers.scripts.secrets.update(workerName, {
|
|
349
|
+
account_id: accountId,
|
|
350
|
+
name: 'GOODVIBES_WORKER_TOKEN',
|
|
351
|
+
text: effectiveWorkerClientToken,
|
|
352
|
+
type: 'secret_text',
|
|
353
|
+
});
|
|
354
|
+
steps.push({ name: 'set-worker-client-secret', status: 'ok', message: 'Configured Worker client bearer token secret.' });
|
|
355
|
+
subdomain = await configureWorkerSubdomain(resourceContext, client, {
|
|
356
|
+
accountId,
|
|
357
|
+
workerName,
|
|
358
|
+
requestedSubdomain: input.workerSubdomain,
|
|
359
|
+
enableWorkersDev: input.enableWorkersDev !== false,
|
|
360
|
+
steps,
|
|
361
|
+
persist,
|
|
362
|
+
});
|
|
363
|
+
const inferredWorkerBaseUrl = subdomain ? `https://${workerName}.${subdomain}.workers.dev` : '';
|
|
364
|
+
workerBaseUrl = workerBaseUrl || inferredWorkerBaseUrl;
|
|
365
|
+
if (workerBaseUrl) {
|
|
366
|
+
this.setConfig('cloudflare.workerBaseUrl', workerBaseUrl, persist);
|
|
367
|
+
}
|
|
368
|
+
else {
|
|
369
|
+
steps.push({
|
|
370
|
+
name: 'worker-base-url',
|
|
371
|
+
status: 'warning',
|
|
372
|
+
message: 'Could not infer workerBaseUrl. Configure cloudflare.workerBaseUrl after assigning a custom route or workers.dev subdomain.',
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
if (workerCron) {
|
|
376
|
+
await client.workers.scripts.schedules.update(workerName, {
|
|
377
|
+
account_id: accountId,
|
|
378
|
+
body: [{ cron: workerCron }],
|
|
379
|
+
});
|
|
380
|
+
steps.push({ name: 'configure-cron', status: 'ok', message: `Configured Worker cron ${workerCron}.` });
|
|
381
|
+
}
|
|
382
|
+
else {
|
|
383
|
+
steps.push({ name: 'configure-cron', status: 'skipped', message: 'No Worker cron configured.' });
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
const consumer = components.queues && components.workers
|
|
387
|
+
? await ensureQueueConsumer(client, {
|
|
388
|
+
accountId,
|
|
389
|
+
queueId,
|
|
390
|
+
workerName,
|
|
391
|
+
deadLetterQueueName,
|
|
392
|
+
steps,
|
|
393
|
+
})
|
|
394
|
+
: undefined;
|
|
395
|
+
if (components.queues && !components.workers) {
|
|
396
|
+
steps.push({ name: 'queue-consumer', status: 'warning', message: 'Cloudflare Queues were provisioned without a Worker consumer.' });
|
|
397
|
+
}
|
|
398
|
+
const durableObject = components.durableObjects
|
|
399
|
+
? await findDurableObjectNamespace(resourceContext, client, accountId, clean(input.durableObjectNamespaceName) || config.durableObjectNamespaceName || DEFAULT_DO_NAMESPACE_NAME, persist, steps)
|
|
400
|
+
: undefined;
|
|
401
|
+
const dnsRecords = await configureDns(client, {
|
|
402
|
+
enabled: components.dns,
|
|
403
|
+
zone,
|
|
404
|
+
daemonHostname,
|
|
405
|
+
tunnelId: tunnel?.id ?? '',
|
|
406
|
+
workerHostname,
|
|
407
|
+
workerBaseUrl,
|
|
408
|
+
steps,
|
|
409
|
+
});
|
|
410
|
+
const access = components.zeroTrustAccess
|
|
411
|
+
? await ensureAccess(resourceContext, client, {
|
|
412
|
+
accountId,
|
|
413
|
+
zone,
|
|
414
|
+
daemonHostname,
|
|
415
|
+
accessAppId: clean(input.accessAppId) || config.accessAppId,
|
|
416
|
+
accessServiceTokenId: clean(input.accessServiceTokenId) || config.accessServiceTokenId,
|
|
417
|
+
accessServiceTokenRef: clean(input.accessServiceTokenRef) || config.accessServiceTokenRef,
|
|
418
|
+
persist,
|
|
419
|
+
returnGeneratedSecrets: input.returnGeneratedSecrets === true,
|
|
420
|
+
steps,
|
|
421
|
+
})
|
|
422
|
+
: undefined;
|
|
423
|
+
this.setConfig('cloudflare.enabled', true, persist);
|
|
424
|
+
this.setConfig('cloudflare.accountId', accountId, persist);
|
|
425
|
+
if (components.workers) {
|
|
426
|
+
this.setConfig('cloudflare.workerName', workerName, persist);
|
|
427
|
+
this.setConfig('cloudflare.daemonBaseUrl', daemonBaseUrl, persist);
|
|
428
|
+
if (daemonHostname)
|
|
429
|
+
this.setConfig('cloudflare.daemonHostname', daemonHostname, persist);
|
|
430
|
+
if (workerHostname)
|
|
431
|
+
this.setConfig('cloudflare.workerHostname', workerHostname, persist);
|
|
432
|
+
this.setConfig('cloudflare.workerCron', workerCron, persist);
|
|
433
|
+
}
|
|
434
|
+
if (components.queues) {
|
|
435
|
+
this.setConfig('cloudflare.queueName', queueName, persist);
|
|
436
|
+
this.setConfig('cloudflare.deadLetterQueueName', deadLetterQueueName, persist);
|
|
437
|
+
this.setConfig('batch.queueBackend', 'cloudflare', persist);
|
|
438
|
+
}
|
|
439
|
+
if (input.batchMode)
|
|
440
|
+
this.setConfig('batch.mode', input.batchMode, persist);
|
|
441
|
+
const verification = components.workers && input.verify !== false && workerBaseUrl
|
|
442
|
+
? await this.verify({ workerBaseUrl, workerClientToken: effectiveWorkerClientToken })
|
|
443
|
+
: undefined;
|
|
444
|
+
if (verification) {
|
|
445
|
+
steps.push({
|
|
446
|
+
name: 'verify-worker',
|
|
447
|
+
status: verification.ok ? 'ok' : 'warning',
|
|
448
|
+
message: verification.ok ? 'Verified Worker health and daemon batch proxy.' : 'Worker verification completed with warnings.',
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
const generatedSecrets = {
|
|
452
|
+
...(generatedWorkerClientToken && input.returnGeneratedSecrets ? { workerClientToken: generatedWorkerClientToken } : {}),
|
|
453
|
+
...(tunnel?.token && input.returnGeneratedSecrets ? { tunnelToken: tunnel.token } : {}),
|
|
454
|
+
...(access?.clientId && input.returnGeneratedSecrets ? { accessServiceTokenClientId: access.clientId } : {}),
|
|
455
|
+
...(access?.clientSecret && input.returnGeneratedSecrets ? { accessServiceTokenClientSecret: access.clientSecret } : {}),
|
|
456
|
+
};
|
|
457
|
+
return {
|
|
458
|
+
ok: true,
|
|
459
|
+
dryRun: false,
|
|
460
|
+
steps,
|
|
461
|
+
account: { id: account.id, name: account.name },
|
|
462
|
+
...(components.queues ? {
|
|
463
|
+
queues: {
|
|
464
|
+
queueName,
|
|
465
|
+
queueId,
|
|
466
|
+
deadLetterQueueName,
|
|
467
|
+
deadLetterQueueId,
|
|
468
|
+
...(consumer?.consumer_id ? { consumerId: consumer.consumer_id } : {}),
|
|
469
|
+
},
|
|
470
|
+
} : {}),
|
|
471
|
+
...(components.workers ? {
|
|
472
|
+
worker: {
|
|
473
|
+
name: workerName,
|
|
474
|
+
...(workerBaseUrl ? { baseUrl: workerBaseUrl } : {}),
|
|
475
|
+
...(subdomain ? { subdomain } : {}),
|
|
476
|
+
...(workerHostname ? { hostname: workerHostname } : {}),
|
|
477
|
+
...(workerCron ? { cron: workerCron } : {}),
|
|
478
|
+
},
|
|
479
|
+
} : {}),
|
|
480
|
+
...(tunnel ? {
|
|
481
|
+
tunnel: {
|
|
482
|
+
id: tunnel.id,
|
|
483
|
+
name: tunnel.name,
|
|
484
|
+
...(daemonHostname ? { hostname: daemonHostname } : {}),
|
|
485
|
+
...(tunnel.tokenRef ? { tokenRef: tunnel.tokenRef } : {}),
|
|
486
|
+
},
|
|
487
|
+
} : {}),
|
|
488
|
+
...(access ? {
|
|
489
|
+
access: {
|
|
490
|
+
...(access.appId ? { appId: access.appId } : {}),
|
|
491
|
+
...(access.serviceTokenId ? { serviceTokenId: access.serviceTokenId } : {}),
|
|
492
|
+
...(access.serviceTokenRef ? { serviceTokenRef: access.serviceTokenRef } : {}),
|
|
493
|
+
},
|
|
494
|
+
} : {}),
|
|
495
|
+
...(dnsRecords.length > 0 && zone ? { dns: { zoneId: zone.id, zoneName: zone.name, records: dnsRecords } } : {}),
|
|
496
|
+
...(kv ? { kv: { namespaceName: kv.title ?? DEFAULT_KV_NAMESPACE_NAME, namespaceId: requireKvNamespaceId(kv) } } : {}),
|
|
497
|
+
...(durableObject ? { durableObjects: { namespaceName: durableObject.name ?? durableObject.class ?? DEFAULT_DO_NAMESPACE_NAME, ...(durableObject.id ? { namespaceId: durableObject.id } : {}) } } : {}),
|
|
498
|
+
...(r2 ? { r2: { bucketName: r2.name ?? DEFAULT_R2_BUCKET_NAME, storageClass: 'Standard' } } : {}),
|
|
499
|
+
...(secretsStore ? { secretsStore: { storeName: secretsStore.name, storeId: secretsStore.id } } : {}),
|
|
500
|
+
...(verification ? { verification } : {}),
|
|
501
|
+
...(Object.keys(generatedSecrets).length > 0 ? { generatedSecrets } : {}),
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
async verify(input = {}) {
|
|
505
|
+
const workerBaseUrl = stripTrailingSlash(clean(input.workerBaseUrl) || this.readConfig().workerBaseUrl);
|
|
506
|
+
if (!workerBaseUrl) {
|
|
507
|
+
throw new CloudflareControlPlaneError('cloudflare.workerBaseUrl is required for verification.', 'CLOUDFLARE_WORKER_URL_REQUIRED', 400);
|
|
508
|
+
}
|
|
509
|
+
const workerClientToken = await this.resolveWorkerClientToken(input);
|
|
510
|
+
const health = await this.fetchWorker(`${workerBaseUrl}/batch/health`);
|
|
511
|
+
const proxy = await this.fetchWorker(`${workerBaseUrl}/api/batch/config`, workerClientToken.value ?? undefined);
|
|
512
|
+
return {
|
|
513
|
+
ok: health.ok && proxy.ok,
|
|
514
|
+
workerHealth: health,
|
|
515
|
+
daemonBatchProxy: proxy,
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
async disable(input = {}) {
|
|
519
|
+
const steps = [];
|
|
520
|
+
const persist = input.persistConfig !== false;
|
|
521
|
+
const accountId = clean(input.accountId) || this.readConfig().accountId;
|
|
522
|
+
const workerName = clean(input.workerName) || this.readConfig().workerName || DEFAULT_WORKER_NAME;
|
|
523
|
+
const apiToken = await this.resolveApiToken(input);
|
|
524
|
+
if (apiToken.value && accountId) {
|
|
525
|
+
const client = await this.createClient(apiToken.value);
|
|
526
|
+
if (input.disableCron !== false) {
|
|
527
|
+
await client.workers.scripts.schedules.update(workerName, { account_id: accountId, body: [] });
|
|
528
|
+
steps.push({ name: 'disable-cron', status: 'ok', message: `Removed Worker cron schedules from ${workerName}.` });
|
|
529
|
+
}
|
|
530
|
+
if (input.disableWorkerSubdomain) {
|
|
531
|
+
await client.workers.scripts.subdomain.delete(workerName, { account_id: accountId });
|
|
532
|
+
steps.push({ name: 'disable-worker-subdomain', status: 'ok', message: `Disabled workers.dev route for ${workerName}.` });
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
else {
|
|
536
|
+
steps.push({ name: 'cloudflare-api', status: 'skipped', message: 'No Cloudflare API token/account configured; only local config was disabled.' });
|
|
537
|
+
}
|
|
538
|
+
this.setConfig('cloudflare.enabled', false, persist);
|
|
539
|
+
this.setConfig('batch.queueBackend', 'local', persist);
|
|
540
|
+
return { ok: true, steps };
|
|
541
|
+
}
|
|
542
|
+
readConfig() {
|
|
543
|
+
return readCloudflareConfig(this.options.configManager);
|
|
544
|
+
}
|
|
545
|
+
resolveAccountId(inputAccountId) {
|
|
546
|
+
const accountId = clean(inputAccountId) || this.readConfig().accountId;
|
|
547
|
+
if (!accountId) {
|
|
548
|
+
throw new CloudflareControlPlaneError('Cloudflare account id is required. Configure cloudflare.accountId or pass accountId.', 'CLOUDFLARE_ACCOUNT_REQUIRED', 400);
|
|
549
|
+
}
|
|
550
|
+
return accountId;
|
|
551
|
+
}
|
|
552
|
+
resolveWorkerName(inputWorkerName) {
|
|
553
|
+
const workerName = clean(inputWorkerName) || this.readConfig().workerName || DEFAULT_WORKER_NAME;
|
|
554
|
+
if (!/^[a-z0-9][a-z0-9-]{0,62}$/i.test(workerName)) {
|
|
555
|
+
throw new CloudflareControlPlaneError('Cloudflare workerName must be 1-63 characters using letters, numbers, and dashes.', 'CLOUDFLARE_WORKER_NAME_INVALID', 400);
|
|
556
|
+
}
|
|
557
|
+
return workerName;
|
|
558
|
+
}
|
|
559
|
+
async resolveApiToken(input) {
|
|
560
|
+
const bodyToken = clean(input.apiToken);
|
|
561
|
+
if (bodyToken)
|
|
562
|
+
return { value: bodyToken, source: 'body' };
|
|
563
|
+
const ref = clean(input.apiTokenRef) || this.readConfig().apiTokenRef;
|
|
564
|
+
const fromRef = await this.resolveSecretRef(ref);
|
|
565
|
+
if (fromRef.value)
|
|
566
|
+
return { value: fromRef.value, source: 'config-ref' };
|
|
567
|
+
const envToken = clean(process.env['CLOUDFLARE_API_TOKEN']);
|
|
568
|
+
if (envToken)
|
|
569
|
+
return { value: envToken, source: 'env' };
|
|
570
|
+
const stored = await this.options.secretsManager?.get(CLOUDFLARE_API_TOKEN_KEY) ?? null;
|
|
571
|
+
if (stored)
|
|
572
|
+
return { value: stored, source: 'goodvibes-secret' };
|
|
573
|
+
return { value: null, source: 'missing' };
|
|
574
|
+
}
|
|
575
|
+
async resolveOperatorToken(input) {
|
|
576
|
+
const bodyToken = clean(input.operatorToken);
|
|
577
|
+
if (bodyToken)
|
|
578
|
+
return { value: bodyToken, source: 'body' };
|
|
579
|
+
const ref = clean(input.operatorTokenRef) || this.readConfig().workerTokenRef;
|
|
580
|
+
const fromRef = await this.resolveSecretRef(ref);
|
|
581
|
+
if (fromRef.value)
|
|
582
|
+
return { value: fromRef.value, source: 'config-ref' };
|
|
583
|
+
const stored = await this.options.secretsManager?.get(CLOUDFLARE_WORKER_OPERATOR_TOKEN_KEY) ?? null;
|
|
584
|
+
if (stored)
|
|
585
|
+
return { value: stored, source: 'goodvibes-secret' };
|
|
586
|
+
const authToken = clean(this.options.authToken?.() ?? undefined);
|
|
587
|
+
if (authToken)
|
|
588
|
+
return { value: authToken, source: 'auth-token' };
|
|
589
|
+
return { value: null, source: 'missing' };
|
|
590
|
+
}
|
|
591
|
+
async resolveWorkerClientToken(input) {
|
|
592
|
+
const bodyToken = clean(input.workerClientToken);
|
|
593
|
+
if (bodyToken)
|
|
594
|
+
return { value: bodyToken, source: 'body' };
|
|
595
|
+
const ref = clean(input.workerClientTokenRef) || this.readConfig().workerClientTokenRef;
|
|
596
|
+
const fromRef = await this.resolveSecretRef(ref);
|
|
597
|
+
if (fromRef.value)
|
|
598
|
+
return { value: fromRef.value, source: 'config-ref' };
|
|
599
|
+
const envToken = clean(process.env['GOODVIBES_CLOUDFLARE_WORKER_TOKEN']);
|
|
600
|
+
if (envToken)
|
|
601
|
+
return { value: envToken, source: 'env' };
|
|
602
|
+
const stored = await this.options.secretsManager?.get(CLOUDFLARE_WORKER_CLIENT_TOKEN_KEY) ?? null;
|
|
603
|
+
if (stored)
|
|
604
|
+
return { value: stored, source: 'goodvibes-secret' };
|
|
605
|
+
return { value: null, source: 'missing' };
|
|
606
|
+
}
|
|
607
|
+
async resolveSecretRef(ref) {
|
|
608
|
+
if (!ref)
|
|
609
|
+
return { value: null, source: 'missing' };
|
|
610
|
+
try {
|
|
611
|
+
const value = await resolveSecretInput(ref, {
|
|
612
|
+
resolveLocalSecret: async (key) => await this.options.secretsManager?.get(key) ?? null,
|
|
613
|
+
homeDirectory: this.options.secretsManager?.getGlobalHome(),
|
|
614
|
+
});
|
|
615
|
+
return value ? { value, source: 'config-ref' } : { value: null, source: 'missing' };
|
|
616
|
+
}
|
|
617
|
+
catch {
|
|
618
|
+
return { value: null, source: 'missing' };
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
async storeSecret(key, value) {
|
|
622
|
+
if (!this.options.secretsManager?.set) {
|
|
623
|
+
throw new CloudflareControlPlaneError('SecretsManager is required to store Cloudflare tokens.', 'SECRETS_MANAGER_REQUIRED', 500);
|
|
624
|
+
}
|
|
625
|
+
await this.options.secretsManager.set(key, value, { scope: 'user', medium: 'secure' });
|
|
626
|
+
}
|
|
627
|
+
requireAccountTokens(client) {
|
|
628
|
+
if (!client.accounts.tokens) {
|
|
629
|
+
throw new CloudflareControlPlaneError('The Cloudflare client does not expose account token creation APIs.', 'CLOUDFLARE_TOKEN_API_UNAVAILABLE', 500);
|
|
630
|
+
}
|
|
631
|
+
return client.accounts.tokens;
|
|
632
|
+
}
|
|
633
|
+
async collectPermissionGroups(client, accountId) {
|
|
634
|
+
const tokenApi = this.requireAccountTokens(client);
|
|
635
|
+
const groups = tokenApi.permissionGroups.get
|
|
636
|
+
? await tokenApi.permissionGroups.get({ account_id: accountId })
|
|
637
|
+
: await collectAsync(tokenApi.permissionGroups.list({ account_id: accountId }));
|
|
638
|
+
return groups;
|
|
639
|
+
}
|
|
640
|
+
createProvisioningContext() {
|
|
641
|
+
return {
|
|
642
|
+
readConfig: () => this.readConfig(),
|
|
643
|
+
setConfig: (key, value, persist) => this.setConfig(key, value, persist),
|
|
644
|
+
storeSecret: async (key, value) => await this.storeSecret(key, value),
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
async fetchWorker(url, bearerToken) {
|
|
648
|
+
try {
|
|
649
|
+
const headers = new Headers();
|
|
650
|
+
if (bearerToken)
|
|
651
|
+
headers.set('Authorization', `Bearer ${bearerToken}`);
|
|
652
|
+
const response = await this.fetchImpl(url, { headers });
|
|
653
|
+
return response.ok
|
|
654
|
+
? { ok: true, status: response.status }
|
|
655
|
+
: { ok: false, status: response.status, error: await safeResponseText(response) };
|
|
656
|
+
}
|
|
657
|
+
catch (error) {
|
|
658
|
+
return { ok: false, status: 0, error: summarizeError(error) };
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
generateToken() {
|
|
662
|
+
const id = this.options.randomUUID?.() ?? globalThis.crypto?.randomUUID?.() ?? String(Math.random()).slice(2);
|
|
663
|
+
return `gv-cf-${id.replace(/[^a-zA-Z0-9]/g, '')}`;
|
|
664
|
+
}
|
|
665
|
+
setConfig(key, value, persist) {
|
|
666
|
+
if (!persist)
|
|
667
|
+
return;
|
|
668
|
+
const set = this.options.configManager.set;
|
|
669
|
+
set.call(this.options.configManager, key, value);
|
|
670
|
+
}
|
|
671
|
+
}
|