@pithy-sh/cloudflare 0.2.3 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/secrets/secretsStoreManager.d.ts +26 -1
- package/dist/secrets/secretsStoreManager.d.ts.map +1 -1
- package/dist/secrets/secretsStoreManager.js +58 -10
- package/dist/workers/workersManager.d.ts +13 -2
- package/dist/workers/workersManager.d.ts.map +1 -1
- package/dist/workers/workersManager.js +15 -3
- package/dist/workflows/workflowsClient.d.ts +17 -0
- package/dist/workflows/workflowsClient.d.ts.map +1 -1
- package/dist/workflows/workflowsClient.js +37 -1
- package/package.json +2 -2
- package/src/secrets/secretsStoreManager.ts +67 -7
- package/src/workers/workersManager.ts +16 -3
- package/src/workflows/workflowsClient.ts +42 -0
|
@@ -19,6 +19,8 @@ export declare const CfSecretEntry: z.ZodObject<{
|
|
|
19
19
|
modified: z.ZodCodec<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodDate]>, z.ZodDate>;
|
|
20
20
|
}, z.core.$strip>;
|
|
21
21
|
export type CfSecretEntry = z.output<typeof CfSecretEntry>;
|
|
22
|
+
/** What {@link CloudflareSecretsStoreManager.createSecretIfAbsent} did — see it for each value. */
|
|
23
|
+
export type CreateSecretOutcome = "created" | "present" | "unconfirmed";
|
|
22
24
|
/** Config for the Secrets Store manager: the shared client config plus the store it targets. */
|
|
23
25
|
export interface SecretsStoreManagerConfig extends CloudflareManagerConfig {
|
|
24
26
|
/** The CF Secrets Store id (the REST API addresses stores by id). */
|
|
@@ -48,6 +50,23 @@ export declare class CloudflareSecretsStoreManager extends CloudflareManager {
|
|
|
48
50
|
* Scopes are re-sent so an entry converges on the same shape whichever branch wrote it.
|
|
49
51
|
*/
|
|
50
52
|
putSecret(name: string, value: string): Promise<void>;
|
|
53
|
+
/**
|
|
54
|
+
* Create a secret only if no entry of that name is there, and say what happened — never "somebody else did"
|
|
55
|
+
* when this call cannot know that.
|
|
56
|
+
*
|
|
57
|
+
* - `created` — this call's create landed, and no older entry of the name exists beside it.
|
|
58
|
+
* - `present` — an entry of the name was there before this call, or appeared beside this one's and is older;
|
|
59
|
+
* in the second case this call's own entry is deleted, so exactly one entry of the name remains.
|
|
60
|
+
* - `unconfirmed` — the create threw, and an entry of the name is there now. It may be this call's own, landed
|
|
61
|
+
* server-side before the response was lost, or another run's. Nothing can tell which (F1 of #643's review), so
|
|
62
|
+
* the answer says so rather than guess, and a caller must not make correctness depend on who created it.
|
|
63
|
+
*
|
|
64
|
+
* **Never an overwrite, even in a race (#643)**, and **never reliant on the store refusing a duplicate name**:
|
|
65
|
+
* Cloudflare does not document that it does. So after a create lands, the entries of that name are listed, and
|
|
66
|
+
* if another is older this one is the loser and removes itself. The oldest wins, deterministically, whichever
|
|
67
|
+
* run looks. A failure with no entry there afterwards — an outage, a refused token — is thrown as it came.
|
|
68
|
+
*/
|
|
69
|
+
createSecretIfAbsent(name: string, value: string): Promise<CreateSecretOutcome>;
|
|
51
70
|
/** Delete a secret by name. Resolves the id via `listSecrets`, then issues DELETE by id. */
|
|
52
71
|
deleteSecret(name: string): Promise<void>;
|
|
53
72
|
/**
|
|
@@ -77,8 +96,14 @@ export declare class CloudflareSecretsStoreManager extends CloudflareManager {
|
|
|
77
96
|
storeId: string;
|
|
78
97
|
accountId: string;
|
|
79
98
|
};
|
|
80
|
-
/** Create a single secret with the default scopes. */
|
|
99
|
+
/** Create a single secret with the default scopes, and return the id Cloudflare gave it when it says one. */
|
|
81
100
|
private createSecret;
|
|
101
|
+
/**
|
|
102
|
+
* The entry of this name, or `undefined`. **A `deleted` entry is not one** (#643): Cloudflare lists a secret's
|
|
103
|
+
* lifecycle status as `pending`, `active` or `deleted` without saying what each means, and a deleted entry is
|
|
104
|
+
* the one reading that cannot be "there". `pending` is taken as there, since creating over it would duplicate
|
|
105
|
+
* it. Of several entries of one name, the oldest, which is the one {@link createSecretIfAbsent} leaves standing.
|
|
106
|
+
*/
|
|
82
107
|
private findByName;
|
|
83
108
|
}
|
|
84
109
|
//# sourceMappingURL=secretsStoreManager.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"secretsStoreManager.d.ts","sourceRoot":"","sources":["../../src/secrets/secretsStoreManager.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,iBAAiB,EAAE,KAAK,uBAAuB,EAAE,MAAM,mBAAmB,CAAC;AAKpF;;;;GAIG;AACH,eAAO,MAAM,aAAa;;;;;;;;;;iBAQqE,CAAC;AAChG,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,aAAa,CAAC,CAAC;AAE3D,gGAAgG;AAChG,MAAM,WAAW,yBAA0B,SAAQ,uBAAuB;IACxE,qEAAqE;IACrE,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;GAOG;AACH,qBAAa,6BAA8B,SAAQ,iBAAiB;IAClE,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IAEjC,YAAY,MAAM,EAAE,yBAAyB,EAM5C;IAED,cAAc,IAAI,MAAM,CAEvB;IAED,iEAAiE;IAC3D,qBAAqB,IAAI,OAAO,CAAC,OAAO,CAAC,CAO9C;IAED;;;;;;;;OAQG;IACG,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAe1D;IAED,4FAA4F;IACtF,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAc9C;IAED;;;;;;;;;;;;;OAaG;IACG,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAU1D;IAED;;;OAGG;IACG,WAAW,IAAI,OAAO,CAAC,aAAa,EAAE,CAAC,CAU5C;IAED,0EAA0E;IACpE,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAE3C;IAED,kEAAkE;IAClE,mBAAmB,IAAI;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAE5D;IAED,
|
|
1
|
+
{"version":3,"file":"secretsStoreManager.d.ts","sourceRoot":"","sources":["../../src/secrets/secretsStoreManager.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,iBAAiB,EAAE,KAAK,uBAAuB,EAAE,MAAM,mBAAmB,CAAC;AAKpF;;;;GAIG;AACH,eAAO,MAAM,aAAa;;;;;;;;;;iBAQqE,CAAC;AAChG,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,aAAa,CAAC,CAAC;AAE3D,mGAAmG;AACnG,MAAM,MAAM,mBAAmB,GAAG,SAAS,GAAG,SAAS,GAAG,aAAa,CAAC;AAOxE,gGAAgG;AAChG,MAAM,WAAW,yBAA0B,SAAQ,uBAAuB;IACxE,qEAAqE;IACrE,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;GAOG;AACH,qBAAa,6BAA8B,SAAQ,iBAAiB;IAClE,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IAEjC,YAAY,MAAM,EAAE,yBAAyB,EAM5C;IAED,cAAc,IAAI,MAAM,CAEvB;IAED,iEAAiE;IAC3D,qBAAqB,IAAI,OAAO,CAAC,OAAO,CAAC,CAO9C;IAED;;;;;;;;OAQG;IACG,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAe1D;IAED;;;;;;;;;;;;;;;OAeG;IACG,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAoBpF;IAED,4FAA4F;IACtF,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAc9C;IAED;;;;;;;;;;;;;OAaG;IACG,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAU1D;IAED;;;OAGG;IACG,WAAW,IAAI,OAAO,CAAC,aAAa,EAAE,CAAC,CAU5C;IAED,0EAA0E;IACpE,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAE3C;IAED,kEAAkE;IAClE,mBAAmB,IAAI;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAE5D;IAED,6GAA6G;YAC/F,YAAY;IAiB1B;;;;;OAKG;YACW,UAAU;CAIzB"}
|
|
@@ -24,6 +24,10 @@ const CfSecretEntry = z.object({
|
|
|
24
24
|
created: JsonDate.describe("When the secret was created (ISO string on the wire, Date in app)."),
|
|
25
25
|
modified: JsonDate.describe("When the secret was last modified (ISO string on the wire, Date in app).")
|
|
26
26
|
}).describe("A single Cloudflare Secrets Store secret's metadata (never its plaintext value).");
|
|
27
|
+
/** The oldest of some entries by creation time, the smaller id breaking a tie — one answer whoever asks. */
|
|
28
|
+
function oldestEntry(entries) {
|
|
29
|
+
return [...entries].sort((a, b) => a.created.getTime() - b.created.getTime() || a.id.localeCompare(b.id))[0];
|
|
30
|
+
}
|
|
27
31
|
/**
|
|
28
32
|
* Out-of-Worker access to the account-level Cloudflare Secrets Store over the REST API: provisioning
|
|
29
33
|
* and audit from a CLI/CI context. Inside a Worker, secret values resolve via bindings — this manager
|
|
@@ -73,6 +77,39 @@ var CloudflareSecretsStoreManager = class extends CloudflareManager {
|
|
|
73
77
|
scopes: [...DEFAULT_SCOPES]
|
|
74
78
|
}));
|
|
75
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* Create a secret only if no entry of that name is there, and say what happened — never "somebody else did"
|
|
82
|
+
* when this call cannot know that.
|
|
83
|
+
*
|
|
84
|
+
* - `created` — this call's create landed, and no older entry of the name exists beside it.
|
|
85
|
+
* - `present` — an entry of the name was there before this call, or appeared beside this one's and is older;
|
|
86
|
+
* in the second case this call's own entry is deleted, so exactly one entry of the name remains.
|
|
87
|
+
* - `unconfirmed` — the create threw, and an entry of the name is there now. It may be this call's own, landed
|
|
88
|
+
* server-side before the response was lost, or another run's. Nothing can tell which (F1 of #643's review), so
|
|
89
|
+
* the answer says so rather than guess, and a caller must not make correctness depend on who created it.
|
|
90
|
+
*
|
|
91
|
+
* **Never an overwrite, even in a race (#643)**, and **never reliant on the store refusing a duplicate name**:
|
|
92
|
+
* Cloudflare does not document that it does. So after a create lands, the entries of that name are listed, and
|
|
93
|
+
* if another is older this one is the loser and removes itself. The oldest wins, deterministically, whichever
|
|
94
|
+
* run looks. A failure with no entry there afterwards — an outage, a refused token — is thrown as it came.
|
|
95
|
+
*/
|
|
96
|
+
async createSecretIfAbsent(name, value) {
|
|
97
|
+
if (await this.findByName(name)) return "present";
|
|
98
|
+
let ownId;
|
|
99
|
+
try {
|
|
100
|
+
ownId = await this.createSecret(name, value);
|
|
101
|
+
} catch (error) {
|
|
102
|
+
if (await this.findByName(name)) return "unconfirmed";
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
const oldest = oldestEntry((await this.listSecrets()).filter((entry) => entry.name === name && entry.status !== "deleted"));
|
|
106
|
+
if (oldest === void 0 || ownId === void 0 || oldest.id === ownId) return "created";
|
|
107
|
+
await cloudflareRequest(`delete duplicate secret ${name}`, () => this.getClient().secretsStore.stores.secrets.delete(ownId, {
|
|
108
|
+
account_id: this.accountId,
|
|
109
|
+
store_id: this.storeId
|
|
110
|
+
}));
|
|
111
|
+
return "present";
|
|
112
|
+
}
|
|
76
113
|
/** Delete a secret by name. Resolves the id via `listSecrets`, then issues DELETE by id. */
|
|
77
114
|
async deleteSecret(name) {
|
|
78
115
|
const existing = await this.findByName(name);
|
|
@@ -130,19 +167,30 @@ var CloudflareSecretsStoreManager = class extends CloudflareManager {
|
|
|
130
167
|
accountId: this.accountId
|
|
131
168
|
};
|
|
132
169
|
}
|
|
133
|
-
/** Create a single secret with the default scopes. */
|
|
170
|
+
/** Create a single secret with the default scopes, and return the id Cloudflare gave it when it says one. */
|
|
134
171
|
async createSecret(name, value) {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
172
|
+
return cloudflareRequest(`create secret ${name}`, async () => {
|
|
173
|
+
const response = await this.getClient().secretsStore.stores.secrets.create(this.storeId, {
|
|
174
|
+
account_id: this.accountId,
|
|
175
|
+
body: [{
|
|
176
|
+
name,
|
|
177
|
+
value,
|
|
178
|
+
scopes: [...DEFAULT_SCOPES]
|
|
179
|
+
}]
|
|
180
|
+
});
|
|
181
|
+
const items = response?.result ?? response;
|
|
182
|
+
if (!Array.isArray(items)) return void 0;
|
|
183
|
+
for (const created of items) if (typeof created?.id === "string" && created.name === name) return created.id;
|
|
184
|
+
});
|
|
143
185
|
}
|
|
186
|
+
/**
|
|
187
|
+
* The entry of this name, or `undefined`. **A `deleted` entry is not one** (#643): Cloudflare lists a secret's
|
|
188
|
+
* lifecycle status as `pending`, `active` or `deleted` without saying what each means, and a deleted entry is
|
|
189
|
+
* the one reading that cannot be "there". `pending` is taken as there, since creating over it would duplicate
|
|
190
|
+
* it. Of several entries of one name, the oldest, which is the one {@link createSecretIfAbsent} leaves standing.
|
|
191
|
+
*/
|
|
144
192
|
async findByName(name) {
|
|
145
|
-
return (await this.listSecrets()).
|
|
193
|
+
return oldestEntry((await this.listSecrets()).filter((entry) => entry.name === name && entry.status !== "deleted"));
|
|
146
194
|
}
|
|
147
195
|
};
|
|
148
196
|
//#endregion
|
|
@@ -109,8 +109,19 @@ export declare class CloudflareWorkersManager extends CloudflareManager {
|
|
|
109
109
|
getWorkerVars(scriptName: string): Promise<Record<string, string> | null>;
|
|
110
110
|
/** Edit a Worker's settings (observability, logpush, tags, …). */
|
|
111
111
|
updateSettings(scriptName: string, settings: WorkerSettings): Promise<void>;
|
|
112
|
-
/**
|
|
113
|
-
|
|
112
|
+
/**
|
|
113
|
+
* Delete a Worker script.
|
|
114
|
+
*
|
|
115
|
+
* **Cloudflare refuses the delete while another Worker still binds the script** — as a service, a Durable
|
|
116
|
+
* Object namespace, or a dispatch outbound — unless it is forced. `force` sends that flag. It is the
|
|
117
|
+
* caller's to decide, because only the caller knows whether what binds the script is going too: a set of
|
|
118
|
+
* Workers that call each other can only be deleted forced, and one Worker another project still calls
|
|
119
|
+
* should not be. Forcing also removes what Cloudflare associates with the script, its Durable Objects
|
|
120
|
+
* included. wrangler's own `delete` forces only after a person has read the list of what will break.
|
|
121
|
+
*/
|
|
122
|
+
deleteWorker(scriptName: string, options?: {
|
|
123
|
+
force?: boolean;
|
|
124
|
+
}): Promise<void>;
|
|
114
125
|
/** List every version of a Worker script. */
|
|
115
126
|
listVersions(scriptName: string): Promise<VersionListResponse[]>;
|
|
116
127
|
/** Get one version of a Worker script. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"workersManager.d.ts","sourceRoot":"","sources":["../../src/workers/workersManager.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,qCAAqC,CAAC;AAClG,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kDAAkD,CAAC;AAEnF,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,8CAA8C,CAAC;AAC3E,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,8CAA8C,CAAC;AACvF,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,+CAA+C,CAAC;AACvF,OAAO,KAAK,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,+CAA+C,CAAC;AAE7G,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAuBtD,uGAAuG;AACvG,MAAM,MAAM,cAAc,GAAG,IAAI,CAAC,iBAAiB,EAAE,YAAY,CAAC,CAAC;AAEnE;;;;;;;;;GASG;AACH,MAAM,WAAW,YAAY;IAC3B,oGAAoG;IACpG,IAAI,EAAE,MAAM,CAAC;IACb,2FAA2F;IAC3F,IAAI,EAAE,MAAM,CAAC;CACd;AAKD;;;;;GAKG;AACH,qBAAa,wBAAyB,SAAQ,iBAAiB;IAC7D,+CAA+C;IACzC,WAAW,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC,CAQrC;IAED,oEAAoE;IAC9D,SAAS,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAG1D;IAED;;;;OAIG;IACG,mBAAmB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAU7D;IAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAuCG;IACG,YAAY,CAChB,UAAU,EAAE,MAAM,EAClB,iBAAiB,EAAE,MAAM,EACzB,QAAQ,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,EACtC,MAAM,GAAE,YAA0B,GACjC,OAAO,CAAC,MAAM,CAAC,CAsDjB;IAED;;;OAGG;IACG,oBAAoB,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,eAAe,UAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAQvG;IAED;;;;;;;;;;;;;;;;;OAiBG;IACG,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAAC,CAmB9E;IAED,kEAAkE;IAC5D,cAAc,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAIhF;IAED
|
|
1
|
+
{"version":3,"file":"workersManager.d.ts","sourceRoot":"","sources":["../../src/workers/workersManager.ts"],"names":[],"mappings":"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"}
|
|
@@ -186,9 +186,21 @@ var CloudflareWorkersManager = class extends CloudflareManager {
|
|
|
186
186
|
...settings
|
|
187
187
|
}));
|
|
188
188
|
}
|
|
189
|
-
/**
|
|
190
|
-
|
|
191
|
-
|
|
189
|
+
/**
|
|
190
|
+
* Delete a Worker script.
|
|
191
|
+
*
|
|
192
|
+
* **Cloudflare refuses the delete while another Worker still binds the script** — as a service, a Durable
|
|
193
|
+
* Object namespace, or a dispatch outbound — unless it is forced. `force` sends that flag. It is the
|
|
194
|
+
* caller's to decide, because only the caller knows whether what binds the script is going too: a set of
|
|
195
|
+
* Workers that call each other can only be deleted forced, and one Worker another project still calls
|
|
196
|
+
* should not be. Forcing also removes what Cloudflare associates with the script, its Durable Objects
|
|
197
|
+
* included. wrangler's own `delete` forces only after a person has read the list of what will break.
|
|
198
|
+
*/
|
|
199
|
+
async deleteWorker(scriptName, options = {}) {
|
|
200
|
+
await cloudflareRequest(`delete worker '${scriptName}'`, () => this.getClient().workers.scripts.delete(scriptName, {
|
|
201
|
+
account_id: this.accountId,
|
|
202
|
+
...options.force === true ? { force: true } : {}
|
|
203
|
+
}, requestOptions));
|
|
192
204
|
}
|
|
193
205
|
/** List every version of a Worker script. */
|
|
194
206
|
async listVersions(scriptName) {
|
|
@@ -41,6 +41,12 @@ export declare const WorkflowInstance: z.ZodObject<{
|
|
|
41
41
|
steps: z.ZodOptional<z.ZodArray<z.ZodUnknown>>;
|
|
42
42
|
}, z.core.$strip>;
|
|
43
43
|
export type WorkflowInstance = z.output<typeof WorkflowInstance>;
|
|
44
|
+
/** One deployed Workflow definition — its name and the script that hosts it — from the account's list. */
|
|
45
|
+
export declare const WorkflowDefinition: z.ZodObject<{
|
|
46
|
+
name: z.ZodString;
|
|
47
|
+
script_name: z.ZodString;
|
|
48
|
+
}, z.core.$strip>;
|
|
49
|
+
export type WorkflowDefinition = z.output<typeof WorkflowDefinition>;
|
|
44
50
|
/**
|
|
45
51
|
* A client over the Cloudflare Workflows API — the only sanctioned way to reach CF Workflows from
|
|
46
52
|
* outside a Worker (CLAUDE.md: no hand-rolled CF `fetch` outside `@pithy-sh/cloudflare`). The
|
|
@@ -58,6 +64,17 @@ export declare class CloudflareWorkflowsClient extends CloudflareManager {
|
|
|
58
64
|
getServiceType(): string;
|
|
59
65
|
/** Prove reach by listing the account's Workflows; never throws. */
|
|
60
66
|
validateServiceAccess(): Promise<boolean>;
|
|
67
|
+
/**
|
|
68
|
+
* Every Workflow definition on the account, validated at the wire boundary. What `pithy feature destroy` reads
|
|
69
|
+
* to find the Workflows a feature's scripts host, by exact script name (#643).
|
|
70
|
+
*/
|
|
71
|
+
listWorkflows(): Promise<WorkflowDefinition[]>;
|
|
72
|
+
/**
|
|
73
|
+
* Delete a Workflow definition by name, resolving `false` when there was none (#643). Cloudflare's own
|
|
74
|
+
* reference says this deletes the Workflow and leaves the Worker script alone; whether deleting the script
|
|
75
|
+
* deletes its Workflows it does not say, so teardown deletes them explicitly rather than rely on either.
|
|
76
|
+
*/
|
|
77
|
+
deleteWorkflow(workflowName: string): Promise<boolean>;
|
|
61
78
|
/** Create (trigger) a Workflow instance and return its id. */
|
|
62
79
|
createInstance(workflowName: string, params: unknown, instanceId?: string): Promise<string>;
|
|
63
80
|
/** Fetch an instance's status. Returns `null` on a 404 (a just-created instance can briefly lag). */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"workflowsClient.d.ts","sourceRoot":"","sources":["../../src/workflows/workflowsClient.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,iBAAiB,EAAE,KAAK,uBAAuB,EAAE,MAAM,mBAAmB,CAAC;AAGpF,gGAAgG;AAChG,MAAM,MAAM,OAAO,GAAG,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAEpD,MAAM,WAAW,+BAAgC,SAAQ,uBAAuB;IAC9E,wEAAwE;IACxE,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,eAAO,MAAM,sBAAsB;;;;;;;;;;;EAawD,CAAC;AAC5F,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAE7E,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;iBAqBuE,CAAC;AACrG,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,gBAAgB,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"workflowsClient.d.ts","sourceRoot":"","sources":["../../src/workflows/workflowsClient.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,iBAAiB,EAAE,KAAK,uBAAuB,EAAE,MAAM,mBAAmB,CAAC;AAGpF,gGAAgG;AAChG,MAAM,MAAM,OAAO,GAAG,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAEpD,MAAM,WAAW,+BAAgC,SAAQ,uBAAuB;IAC9E,wEAAwE;IACxE,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,eAAO,MAAM,sBAAsB;;;;;;;;;;;EAawD,CAAC;AAC5F,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAE7E,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;iBAqBuE,CAAC;AACrG,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAEjE,0GAA0G;AAC1G,eAAO,MAAM,kBAAkB;;;iBAKuF,CAAC;AACvH,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAOrE;;;;;;;;;;GAUG;AACH,qBAAa,yBAA0B,SAAQ,iBAAiB;;IAG9D,YAAY,MAAM,EAAE,+BAA+B,EAGlD;IAED,cAAc,IAAI,MAAM,CAEvB;IAED,oEAAoE;IAC9D,qBAAqB,IAAI,OAAO,CAAC,OAAO,CAAC,CAO9C;IAED;;;OAGG;IACG,aAAa,IAAI,OAAO,CAAC,kBAAkB,EAAE,CAAC,CAUnD;IAED;;;;OAIG;IACG,cAAc,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAU3D;IAED,8DAA8D;IACxD,cAAc,CAAC,YAAY,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAYhG;IAED,qGAAqG;IAC/F,iBAAiB,CAAC,YAAY,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAoBlG;IAED;;;;;;;;;;;;;;;;;OAiBG;IACG,eAAe,CACnB,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,OAAO,EACf,OAAO,GAAE;QAAE,cAAc,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAO,GAChF,OAAO,CAAC,OAAO,CAAC,CAmClB;CACF"}
|
|
@@ -28,6 +28,11 @@ const WorkflowInstance = z.object({
|
|
|
28
28
|
]).optional().describe("The instance-level failure, present when `status` is `errored` or `terminated`."),
|
|
29
29
|
steps: z.array(z.unknown()).optional().describe("The instance's steps, each validated where it is read (`stepFailure`). Held as `unknown` here on purpose: the API has four step shapes, this client reads one field of one of them, and a shape nobody anticipated must not turn a failed Workflow into a parse error about the failure report.")
|
|
30
30
|
}).describe("A Cloudflare Workflow instance's status, as returned by the instance-detail endpoint.");
|
|
31
|
+
/** One deployed Workflow definition — its name and the script that hosts it — from the account's list. */
|
|
32
|
+
const WorkflowDefinition = z.object({
|
|
33
|
+
name: z.string().describe("The Workflow's account-wide name, e.g. `acme-prod-email-send`."),
|
|
34
|
+
script_name: z.string().describe("The Worker script whose exported class runs the Workflow.")
|
|
35
|
+
}).describe("A Workflow definition on the account, as far as teardown needs one: what it is called and who hosts it.");
|
|
31
36
|
/** Terminal states a poll stops on. */
|
|
32
37
|
const TERMINAL = /* @__PURE__ */ new Set([
|
|
33
38
|
"complete",
|
|
@@ -64,6 +69,37 @@ var CloudflareWorkflowsClient = class extends CloudflareManager {
|
|
|
64
69
|
return false;
|
|
65
70
|
}
|
|
66
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* Every Workflow definition on the account, validated at the wire boundary. What `pithy feature destroy` reads
|
|
74
|
+
* to find the Workflows a feature's scripts host, by exact script name (#643).
|
|
75
|
+
*/
|
|
76
|
+
async listWorkflows() {
|
|
77
|
+
return cloudflareRequest("list Workflows", async () => {
|
|
78
|
+
const out = [];
|
|
79
|
+
for await (const workflow of this.getClient().workflows.list({ account_id: this.accountId })) {
|
|
80
|
+
const parsed = WorkflowDefinition.safeParse(workflow);
|
|
81
|
+
if (!parsed.success) throw new CloudflareInvalidResponseError({ detail: "list Workflows: unexpected shape" });
|
|
82
|
+
out.push(parsed.data);
|
|
83
|
+
}
|
|
84
|
+
return out;
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Delete a Workflow definition by name, resolving `false` when there was none (#643). Cloudflare's own
|
|
89
|
+
* reference says this deletes the Workflow and leaves the Worker script alone; whether deleting the script
|
|
90
|
+
* deletes its Workflows it does not say, so teardown deletes them explicitly rather than rely on either.
|
|
91
|
+
*/
|
|
92
|
+
async deleteWorkflow(workflowName) {
|
|
93
|
+
return cloudflareRequest(`delete Workflow ${workflowName}`, async () => {
|
|
94
|
+
try {
|
|
95
|
+
await this.getClient().workflows.delete(workflowName, { account_id: this.accountId });
|
|
96
|
+
return true;
|
|
97
|
+
} catch (error) {
|
|
98
|
+
if (isNotFoundError(error)) return false;
|
|
99
|
+
throw error;
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
}
|
|
67
103
|
/** Create (trigger) a Workflow instance and return its id. */
|
|
68
104
|
async createInstance(workflowName, params, instanceId) {
|
|
69
105
|
return cloudflareRequest(`create instance of ${workflowName}`, async () => {
|
|
@@ -155,4 +191,4 @@ function describeFailure(failure, error) {
|
|
|
155
191
|
return `${failure.step === void 0 ? "step" : `step '${failure.step}'`} raised${failure.code === void 0 ? "" : ` [${failure.code}]`} ${failure.raw} — instance error: ${instance}`;
|
|
156
192
|
}
|
|
157
193
|
//#endregion
|
|
158
|
-
export { CloudflareWorkflowsClient, WorkflowInstance, WorkflowInstanceStatus };
|
|
194
|
+
export { CloudflareWorkflowsClient, WorkflowDefinition, WorkflowInstance, WorkflowInstanceStatus };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pithy-sh/cloudflare",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"js-base64": "^3.9.2"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
|
-
"@pithy-sh/core": "^0.
|
|
43
|
+
"@pithy-sh/core": "^0.7.4",
|
|
44
44
|
"zod": "^4.4.0"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
@@ -26,6 +26,14 @@ export const CfSecretEntry = z
|
|
|
26
26
|
.describe("A single Cloudflare Secrets Store secret's metadata (never its plaintext value).");
|
|
27
27
|
export type CfSecretEntry = z.output<typeof CfSecretEntry>;
|
|
28
28
|
|
|
29
|
+
/** What {@link CloudflareSecretsStoreManager.createSecretIfAbsent} did — see it for each value. */
|
|
30
|
+
export type CreateSecretOutcome = "created" | "present" | "unconfirmed";
|
|
31
|
+
|
|
32
|
+
/** The oldest of some entries by creation time, the smaller id breaking a tie — one answer whoever asks. */
|
|
33
|
+
function oldestEntry(entries: readonly CfSecretEntry[]): CfSecretEntry | undefined {
|
|
34
|
+
return [...entries].sort((a, b) => a.created.getTime() - b.created.getTime() || a.id.localeCompare(b.id))[0];
|
|
35
|
+
}
|
|
36
|
+
|
|
29
37
|
/** Config for the Secrets Store manager: the shared client config plus the store it targets. */
|
|
30
38
|
export interface SecretsStoreManagerConfig extends CloudflareManagerConfig {
|
|
31
39
|
/** The CF Secrets Store id (the REST API addresses stores by id). */
|
|
@@ -91,6 +99,44 @@ export class CloudflareSecretsStoreManager extends CloudflareManager {
|
|
|
91
99
|
);
|
|
92
100
|
}
|
|
93
101
|
|
|
102
|
+
/**
|
|
103
|
+
* Create a secret only if no entry of that name is there, and say what happened — never "somebody else did"
|
|
104
|
+
* when this call cannot know that.
|
|
105
|
+
*
|
|
106
|
+
* - `created` — this call's create landed, and no older entry of the name exists beside it.
|
|
107
|
+
* - `present` — an entry of the name was there before this call, or appeared beside this one's and is older;
|
|
108
|
+
* in the second case this call's own entry is deleted, so exactly one entry of the name remains.
|
|
109
|
+
* - `unconfirmed` — the create threw, and an entry of the name is there now. It may be this call's own, landed
|
|
110
|
+
* server-side before the response was lost, or another run's. Nothing can tell which (F1 of #643's review), so
|
|
111
|
+
* the answer says so rather than guess, and a caller must not make correctness depend on who created it.
|
|
112
|
+
*
|
|
113
|
+
* **Never an overwrite, even in a race (#643)**, and **never reliant on the store refusing a duplicate name**:
|
|
114
|
+
* Cloudflare does not document that it does. So after a create lands, the entries of that name are listed, and
|
|
115
|
+
* if another is older this one is the loser and removes itself. The oldest wins, deterministically, whichever
|
|
116
|
+
* run looks. A failure with no entry there afterwards — an outage, a refused token — is thrown as it came.
|
|
117
|
+
*/
|
|
118
|
+
async createSecretIfAbsent(name: string, value: string): Promise<CreateSecretOutcome> {
|
|
119
|
+
if (await this.findByName(name)) return "present";
|
|
120
|
+
let ownId: string | undefined;
|
|
121
|
+
try {
|
|
122
|
+
ownId = await this.createSecret(name, value);
|
|
123
|
+
} catch (error) {
|
|
124
|
+
if (await this.findByName(name)) return "unconfirmed";
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
127
|
+
const same = (await this.listSecrets()).filter((entry) => entry.name === name && entry.status !== "deleted");
|
|
128
|
+
const oldest = oldestEntry(same);
|
|
129
|
+
if (oldest === undefined || ownId === undefined || oldest.id === ownId) return "created";
|
|
130
|
+
// Another run's entry of the same name is older: it is the one that stands.
|
|
131
|
+
await cloudflareRequest(`delete duplicate secret ${name}`, () =>
|
|
132
|
+
this.getClient().secretsStore.stores.secrets.delete(ownId as string, {
|
|
133
|
+
account_id: this.accountId,
|
|
134
|
+
store_id: this.storeId,
|
|
135
|
+
}),
|
|
136
|
+
);
|
|
137
|
+
return "present";
|
|
138
|
+
}
|
|
139
|
+
|
|
94
140
|
/** Delete a secret by name. Resolves the id via `listSecrets`, then issues DELETE by id. */
|
|
95
141
|
async deleteSecret(name: string): Promise<void> {
|
|
96
142
|
const existing = await this.findByName(name);
|
|
@@ -160,18 +206,32 @@ export class CloudflareSecretsStoreManager extends CloudflareManager {
|
|
|
160
206
|
return { storeId: this.storeId, accountId: this.accountId };
|
|
161
207
|
}
|
|
162
208
|
|
|
163
|
-
/** Create a single secret with the default scopes. */
|
|
164
|
-
private async createSecret(name: string, value: string): Promise<
|
|
165
|
-
|
|
166
|
-
this.getClient().secretsStore.stores.secrets.create(this.storeId, {
|
|
209
|
+
/** Create a single secret with the default scopes, and return the id Cloudflare gave it when it says one. */
|
|
210
|
+
private async createSecret(name: string, value: string): Promise<string | undefined> {
|
|
211
|
+
return cloudflareRequest(`create secret ${name}`, async () => {
|
|
212
|
+
const response: unknown = await this.getClient().secretsStore.stores.secrets.create(this.storeId, {
|
|
167
213
|
account_id: this.accountId,
|
|
168
214
|
body: [{ name, value, scopes: [...DEFAULT_SCOPES] }],
|
|
169
|
-
})
|
|
170
|
-
|
|
215
|
+
});
|
|
216
|
+
// The SDK hands back a page of what was created. Read defensively: an id is a convenience here, and a
|
|
217
|
+
// response without one leaves `createSecretIfAbsent` answering `created` as it would have before.
|
|
218
|
+
const items = (response as { result?: unknown })?.result ?? response;
|
|
219
|
+
if (!Array.isArray(items)) return undefined;
|
|
220
|
+
for (const created of items as { id?: unknown; name?: unknown }[]) {
|
|
221
|
+
if (typeof created?.id === "string" && created.name === name) return created.id;
|
|
222
|
+
}
|
|
223
|
+
return undefined;
|
|
224
|
+
});
|
|
171
225
|
}
|
|
172
226
|
|
|
227
|
+
/**
|
|
228
|
+
* The entry of this name, or `undefined`. **A `deleted` entry is not one** (#643): Cloudflare lists a secret's
|
|
229
|
+
* lifecycle status as `pending`, `active` or `deleted` without saying what each means, and a deleted entry is
|
|
230
|
+
* the one reading that cannot be "there". `pending` is taken as there, since creating over it would duplicate
|
|
231
|
+
* it. Of several entries of one name, the oldest, which is the one {@link createSecretIfAbsent} leaves standing.
|
|
232
|
+
*/
|
|
173
233
|
private async findByName(name: string): Promise<CfSecretEntry | undefined> {
|
|
174
234
|
const all = await this.listSecrets();
|
|
175
|
-
return all.
|
|
235
|
+
return oldestEntry(all.filter((entry) => entry.name === name && entry.status !== "deleted"));
|
|
176
236
|
}
|
|
177
237
|
}
|
|
@@ -259,10 +259,23 @@ export class CloudflareWorkersManager extends CloudflareManager {
|
|
|
259
259
|
);
|
|
260
260
|
}
|
|
261
261
|
|
|
262
|
-
/**
|
|
263
|
-
|
|
262
|
+
/**
|
|
263
|
+
* Delete a Worker script.
|
|
264
|
+
*
|
|
265
|
+
* **Cloudflare refuses the delete while another Worker still binds the script** — as a service, a Durable
|
|
266
|
+
* Object namespace, or a dispatch outbound — unless it is forced. `force` sends that flag. It is the
|
|
267
|
+
* caller's to decide, because only the caller knows whether what binds the script is going too: a set of
|
|
268
|
+
* Workers that call each other can only be deleted forced, and one Worker another project still calls
|
|
269
|
+
* should not be. Forcing also removes what Cloudflare associates with the script, its Durable Objects
|
|
270
|
+
* included. wrangler's own `delete` forces only after a person has read the list of what will break.
|
|
271
|
+
*/
|
|
272
|
+
async deleteWorker(scriptName: string, options: { force?: boolean } = {}): Promise<void> {
|
|
264
273
|
await cloudflareRequest(`delete worker '${scriptName}'`, () =>
|
|
265
|
-
this.getClient().workers.scripts.delete(
|
|
274
|
+
this.getClient().workers.scripts.delete(
|
|
275
|
+
scriptName,
|
|
276
|
+
{ account_id: this.accountId, ...(options.force === true ? { force: true } : {}) },
|
|
277
|
+
requestOptions,
|
|
278
|
+
),
|
|
266
279
|
);
|
|
267
280
|
}
|
|
268
281
|
|
|
@@ -55,6 +55,15 @@ export const WorkflowInstance = z
|
|
|
55
55
|
.describe("A Cloudflare Workflow instance's status, as returned by the instance-detail endpoint.");
|
|
56
56
|
export type WorkflowInstance = z.output<typeof WorkflowInstance>;
|
|
57
57
|
|
|
58
|
+
/** One deployed Workflow definition — its name and the script that hosts it — from the account's list. */
|
|
59
|
+
export const WorkflowDefinition = z
|
|
60
|
+
.object({
|
|
61
|
+
name: z.string().describe("The Workflow's account-wide name, e.g. `acme-prod-email-send`."),
|
|
62
|
+
script_name: z.string().describe("The Worker script whose exported class runs the Workflow."),
|
|
63
|
+
})
|
|
64
|
+
.describe("A Workflow definition on the account, as far as teardown needs one: what it is called and who hosts it.");
|
|
65
|
+
export type WorkflowDefinition = z.output<typeof WorkflowDefinition>;
|
|
66
|
+
|
|
58
67
|
/** Terminal states a poll stops on. */
|
|
59
68
|
const TERMINAL: ReadonlySet<WorkflowInstanceStatus> = new Set(["complete", "errored", "terminated"]);
|
|
60
69
|
|
|
@@ -93,6 +102,39 @@ export class CloudflareWorkflowsClient extends CloudflareManager {
|
|
|
93
102
|
}
|
|
94
103
|
}
|
|
95
104
|
|
|
105
|
+
/**
|
|
106
|
+
* Every Workflow definition on the account, validated at the wire boundary. What `pithy feature destroy` reads
|
|
107
|
+
* to find the Workflows a feature's scripts host, by exact script name (#643).
|
|
108
|
+
*/
|
|
109
|
+
async listWorkflows(): Promise<WorkflowDefinition[]> {
|
|
110
|
+
return cloudflareRequest("list Workflows", async () => {
|
|
111
|
+
const out: WorkflowDefinition[] = [];
|
|
112
|
+
for await (const workflow of this.getClient().workflows.list({ account_id: this.accountId })) {
|
|
113
|
+
const parsed = WorkflowDefinition.safeParse(workflow);
|
|
114
|
+
if (!parsed.success) throw new CloudflareInvalidResponseError({ detail: "list Workflows: unexpected shape" });
|
|
115
|
+
out.push(parsed.data);
|
|
116
|
+
}
|
|
117
|
+
return out;
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Delete a Workflow definition by name, resolving `false` when there was none (#643). Cloudflare's own
|
|
123
|
+
* reference says this deletes the Workflow and leaves the Worker script alone; whether deleting the script
|
|
124
|
+
* deletes its Workflows it does not say, so teardown deletes them explicitly rather than rely on either.
|
|
125
|
+
*/
|
|
126
|
+
async deleteWorkflow(workflowName: string): Promise<boolean> {
|
|
127
|
+
return cloudflareRequest(`delete Workflow ${workflowName}`, async () => {
|
|
128
|
+
try {
|
|
129
|
+
await this.getClient().workflows.delete(workflowName, { account_id: this.accountId });
|
|
130
|
+
return true;
|
|
131
|
+
} catch (error) {
|
|
132
|
+
if (isNotFoundError(error)) return false;
|
|
133
|
+
throw error;
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
96
138
|
/** Create (trigger) a Workflow instance and return its id. */
|
|
97
139
|
async createInstance(workflowName: string, params: unknown, instanceId?: string): Promise<string> {
|
|
98
140
|
return cloudflareRequest(`create instance of ${workflowName}`, async () => {
|