@pithy-sh/cloudflare 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai/aiManager.d.ts +99 -0
- package/dist/ai/aiManager.d.ts.map +1 -0
- package/dist/ai/aiManager.js +148 -0
- package/dist/ai/vectorizeManager.d.ts +67 -0
- package/dist/ai/vectorizeManager.d.ts.map +1 -0
- package/dist/ai/vectorizeManager.js +101 -0
- package/dist/ai/vectorizeProvisioner.d.ts +123 -0
- package/dist/ai/vectorizeProvisioner.d.ts.map +1 -0
- package/dist/ai/vectorizeProvisioner.js +195 -0
- package/dist/client/accounts.d.ts +48 -0
- package/dist/client/accounts.d.ts.map +1 -0
- package/dist/client/accounts.js +55 -0
- package/dist/client/clients.d.ts +122 -0
- package/dist/client/clients.d.ts.map +1 -0
- package/dist/client/clients.js +216 -0
- package/dist/client/errors.d.ts +73 -0
- package/dist/client/errors.d.ts.map +1 -0
- package/dist/client/errors.js +98 -0
- package/dist/client/manager.d.ts +45 -0
- package/dist/client/manager.d.ts.map +1 -0
- package/dist/client/manager.js +51 -0
- package/dist/d1/d1Manager.d.ts +74 -0
- package/dist/d1/d1Manager.d.ts.map +1 -0
- package/dist/d1/d1Manager.js +128 -0
- package/dist/d1/d1PreparedStatement.d.ts +24 -0
- package/dist/d1/d1PreparedStatement.d.ts.map +1 -0
- package/dist/d1/d1PreparedStatement.js +91 -0
- package/dist/d1/d1Provisioner.d.ts +28 -0
- package/dist/d1/d1Provisioner.d.ts.map +1 -0
- package/dist/d1/d1Provisioner.js +67 -0
- package/dist/email/emailRoutingManager.d.ts +36 -0
- package/dist/email/emailRoutingManager.d.ts.map +1 -0
- package/dist/email/emailRoutingManager.js +124 -0
- package/dist/email/emailSendManager.d.ts +45 -0
- package/dist/email/emailSendManager.d.ts.map +1 -0
- package/dist/email/emailSendManager.js +52 -0
- package/dist/env/devVars.d.ts +65 -0
- package/dist/env/devVars.d.ts.map +1 -0
- package/dist/env/devVars.js +84 -0
- package/dist/hostnames/customHostnamesManager.d.ts +51 -0
- package/dist/hostnames/customHostnamesManager.d.ts.map +1 -0
- package/dist/hostnames/customHostnamesManager.js +84 -0
- package/dist/kv/kvManager.d.ts +76 -0
- package/dist/kv/kvManager.d.ts.map +1 -0
- package/dist/kv/kvManager.js +135 -0
- package/dist/kv/kvProvisioner.d.ts +28 -0
- package/dist/kv/kvProvisioner.d.ts.map +1 -0
- package/dist/kv/kvProvisioner.js +71 -0
- package/dist/media/assetSeeder.d.ts +31 -0
- package/dist/media/assetSeeder.d.ts.map +1 -0
- package/dist/media/assetSeeder.js +70 -0
- package/dist/media/imageManager.d.ts +34 -0
- package/dist/media/imageManager.d.ts.map +1 -0
- package/dist/media/imageManager.js +103 -0
- package/dist/media/ownership.d.ts +40 -0
- package/dist/media/ownership.d.ts.map +1 -0
- package/dist/media/ownership.js +46 -0
- package/dist/media/streamManager.d.ts +83 -0
- package/dist/media/streamManager.d.ts.map +1 -0
- package/dist/media/streamManager.js +145 -0
- package/dist/queue/queueManager.d.ts +84 -0
- package/dist/queue/queueManager.d.ts.map +1 -0
- package/dist/queue/queueManager.js +114 -0
- package/dist/r2/r2Credentials.d.ts +12 -0
- package/dist/r2/r2Credentials.d.ts.map +1 -0
- package/dist/r2/r2Credentials.js +13 -0
- package/dist/r2/r2Manager.d.ts +226 -0
- package/dist/r2/r2Manager.d.ts.map +1 -0
- package/dist/r2/r2Manager.js +405 -0
- package/dist/r2/r2Provisioner.d.ts +39 -0
- package/dist/r2/r2Provisioner.d.ts.map +1 -0
- package/dist/r2/r2Provisioner.js +86 -0
- package/dist/secrets/secretsStoreManager.d.ts +82 -0
- package/dist/secrets/secretsStoreManager.d.ts.map +1 -0
- package/dist/secrets/secretsStoreManager.js +147 -0
- package/dist/secrets/secretsStores.d.ts +47 -0
- package/dist/secrets/secretsStores.d.ts.map +1 -0
- package/dist/secrets/secretsStores.js +65 -0
- package/dist/test-utils/emailRoutingRules.d.ts +39 -0
- package/dist/test-utils/emailRoutingRules.d.ts.map +1 -0
- package/dist/test-utils/emailRoutingRules.js +89 -0
- package/dist/test-utils/fixtureReportSetup.d.ts +25 -0
- package/dist/test-utils/fixtureReportSetup.d.ts.map +1 -0
- package/dist/test-utils/fixtureReportSetup.js +30 -0
- package/dist/test-utils/fixtures.d.ts +224 -0
- package/dist/test-utils/fixtures.d.ts.map +1 -0
- package/dist/test-utils/fixtures.js +277 -0
- package/dist/test-utils/harness.d.ts +192 -0
- package/dist/test-utils/harness.d.ts.map +1 -0
- package/dist/test-utils/harness.js +351 -0
- package/dist/test-utils/inboundRecorder.d.ts +91 -0
- package/dist/test-utils/inboundRecorder.d.ts.map +1 -0
- package/dist/test-utils/inboundRecorder.js +141 -0
- package/dist/test-utils/integrationSetup.d.ts +22 -0
- package/dist/test-utils/integrationSetup.d.ts.map +1 -0
- package/dist/test-utils/integrationSetup.js +37 -0
- package/dist/test-utils/reap.d.ts +112 -0
- package/dist/test-utils/reap.d.ts.map +1 -0
- package/dist/test-utils/reap.js +185 -0
- package/dist/tokens/accountTokensManager.d.ts +157 -0
- package/dist/tokens/accountTokensManager.d.ts.map +1 -0
- package/dist/tokens/accountTokensManager.js +280 -0
- package/dist/tokens/permissions.d.ts +36 -0
- package/dist/tokens/permissions.d.ts.map +1 -0
- package/dist/tokens/permissions.js +50 -0
- package/dist/tokens/profiles.d.ts +102 -0
- package/dist/tokens/profiles.d.ts.map +1 -0
- package/dist/tokens/profiles.js +137 -0
- package/dist/turnstile/turnstileManager.d.ts +75 -0
- package/dist/turnstile/turnstileManager.d.ts.map +1 -0
- package/dist/turnstile/turnstileManager.js +138 -0
- package/dist/user/userManager.d.ts +36 -0
- package/dist/user/userManager.d.ts.map +1 -0
- package/dist/user/userManager.js +58 -0
- package/dist/workers/buildsManager.d.ts +116 -0
- package/dist/workers/buildsManager.d.ts.map +1 -0
- package/dist/workers/buildsManager.js +237 -0
- package/dist/workers/buildsTypes.d.ts +102 -0
- package/dist/workers/buildsTypes.d.ts.map +1 -0
- package/dist/workers/buildsTypes.js +87 -0
- package/dist/workers/workersBuildEvent.d.ts +64 -0
- package/dist/workers/workersBuildEvent.d.ts.map +1 -0
- package/dist/workers/workersBuildEvent.js +47 -0
- package/dist/workers/workersManager.d.ts +136 -0
- package/dist/workers/workersManager.d.ts.map +1 -0
- package/dist/workers/workersManager.js +312 -0
- package/dist/workers/workersProvisioner.d.ts +80 -0
- package/dist/workers/workersProvisioner.d.ts.map +1 -0
- package/dist/workers/workersProvisioner.js +106 -0
- package/dist/workflows/stepFailure.d.ts +180 -0
- package/dist/workflows/stepFailure.d.ts.map +1 -0
- package/dist/workflows/stepFailure.js +227 -0
- package/dist/workflows/workflowsClient.d.ts +87 -0
- package/dist/workflows/workflowsClient.d.ts.map +1 -0
- package/dist/workflows/workflowsClient.js +156 -0
- package/dist/zones/zonesManager.d.ts +50 -0
- package/dist/zones/zonesManager.d.ts.map +1 -0
- package/dist/zones/zonesManager.js +74 -0
- package/package.json +18 -9
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { Key } from "cloudflare/resources/kv/namespaces/keys";
|
|
2
|
+
import { CloudflareManager, type CloudflareManagerConfig } from "../client/manager";
|
|
3
|
+
/** Config for the KV manager: the shared client config plus the namespace it targets. */
|
|
4
|
+
export interface KVManagerConfig extends CloudflareManagerConfig {
|
|
5
|
+
/** The KV namespace id (the REST API addresses namespaces by id, not binding name). */
|
|
6
|
+
namespaceId: string;
|
|
7
|
+
}
|
|
8
|
+
/** Options for a single KV write. TTL is explicit (CLAUDE.md §KV). */
|
|
9
|
+
export interface KVPutOptions {
|
|
10
|
+
/** Seconds until the key expires. Omit for no expiry. */
|
|
11
|
+
expirationTtl?: number;
|
|
12
|
+
/** Arbitrary metadata stored alongside the value. */
|
|
13
|
+
metadata?: Record<string, unknown>;
|
|
14
|
+
}
|
|
15
|
+
/** One entry in a `batchSet` call. */
|
|
16
|
+
export interface KVOperation {
|
|
17
|
+
/** The key to write. */
|
|
18
|
+
key: string;
|
|
19
|
+
/** The value to store. */
|
|
20
|
+
value: string;
|
|
21
|
+
/** Seconds until the key expires. */
|
|
22
|
+
expirationTtl?: number;
|
|
23
|
+
/** Metadata to store with the value. */
|
|
24
|
+
metadata?: Record<string, unknown>;
|
|
25
|
+
}
|
|
26
|
+
/** The per-key outcome of a `batchSet`: a partial-failure report, never a thrown batch. */
|
|
27
|
+
export interface KVBatchResult {
|
|
28
|
+
/** The key this result is for. */
|
|
29
|
+
key: string;
|
|
30
|
+
/** Whether the write succeeded. */
|
|
31
|
+
success: boolean;
|
|
32
|
+
/** The failure reason, present only when `success` is false. */
|
|
33
|
+
error?: string;
|
|
34
|
+
}
|
|
35
|
+
/** A KV value together with its metadata, as returned by `getWithMetadata`. */
|
|
36
|
+
export interface KVValueWithMetadata<M = unknown> {
|
|
37
|
+
/** The stored value, or null if the key is absent. */
|
|
38
|
+
value: string | null;
|
|
39
|
+
/** The stored metadata, or null if absent. */
|
|
40
|
+
metadata: M | null;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Out-of-Worker KV access over the REST API: namespace management and key reads/writes from a
|
|
44
|
+
* CLI/CI/provisioning context. Inside a Worker you use the `KVNamespace` binding directly — this
|
|
45
|
+
* manager is the REST counterpart, addressed by namespace id.
|
|
46
|
+
*/
|
|
47
|
+
export declare class CloudflareKVManager extends CloudflareManager {
|
|
48
|
+
private readonly namespaceId;
|
|
49
|
+
constructor(config: KVManagerConfig);
|
|
50
|
+
/** Read a key's value as text. Returns null when the key is absent (the API 404s a missing key). */
|
|
51
|
+
get(key: string): Promise<string | null>;
|
|
52
|
+
/** Read a key and `JSON.parse` it. Returns null when absent; throws on malformed JSON. */
|
|
53
|
+
getJson<T = unknown>(key: string): Promise<T | null>;
|
|
54
|
+
/** Write a value, with optional TTL and metadata. */
|
|
55
|
+
set(key: string, value: string, options?: KVPutOptions): Promise<void>;
|
|
56
|
+
/** Delete a key. */
|
|
57
|
+
delete(key: string): Promise<void>;
|
|
58
|
+
/** List keys in the namespace, optionally filtered by prefix and bounded by limit. */
|
|
59
|
+
listKeys(prefix?: string, limit?: number): Promise<Key[]>;
|
|
60
|
+
/** Read a key's value and metadata together. Both are null when absent. */
|
|
61
|
+
getWithMetadata<M = unknown>(key: string): Promise<KVValueWithMetadata<M>>;
|
|
62
|
+
/**
|
|
63
|
+
* Write many keys, collecting a per-key result instead of failing the whole batch on the first
|
|
64
|
+
* error — the caller decides what to do with partial failure.
|
|
65
|
+
*/
|
|
66
|
+
batchSet(operations: KVOperation[]): Promise<KVBatchResult[]>;
|
|
67
|
+
/** The namespace this manager targets and the account it lives in. */
|
|
68
|
+
getKVInfo(): {
|
|
69
|
+
namespaceId: string;
|
|
70
|
+
accountId: string;
|
|
71
|
+
};
|
|
72
|
+
getServiceType(): string;
|
|
73
|
+
/** Prove access by reading the namespace record. Never throws. */
|
|
74
|
+
validateServiceAccess(): Promise<boolean>;
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=kvManager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"kvManager.d.ts","sourceRoot":"","sources":["../../src/kv/kvManager.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,yCAAyC,CAAC;AAQnE,OAAO,EAAE,iBAAiB,EAAE,KAAK,uBAAuB,EAAE,MAAM,mBAAmB,CAAC;AAEpF,yFAAyF;AACzF,MAAM,WAAW,eAAgB,SAAQ,uBAAuB;IAC9D,uFAAuF;IACvF,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,sEAAsE;AACtE,MAAM,WAAW,YAAY;IAC3B,yDAAyD;IACzD,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,qDAAqD;IACrD,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AAED,sCAAsC;AACtC,MAAM,WAAW,WAAW;IAC1B,wBAAwB;IACxB,GAAG,EAAE,MAAM,CAAC;IACZ,0BAA0B;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,qCAAqC;IACrC,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,wCAAwC;IACxC,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AAED,2FAA2F;AAC3F,MAAM,WAAW,aAAa;IAC5B,kCAAkC;IAClC,GAAG,EAAE,MAAM,CAAC;IACZ,mCAAmC;IACnC,OAAO,EAAE,OAAO,CAAC;IACjB,gEAAgE;IAChE,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,+EAA+E;AAC/E,MAAM,WAAW,mBAAmB,CAAC,CAAC,GAAG,OAAO;IAC9C,sDAAsD;IACtD,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,8CAA8C;IAC9C,QAAQ,EAAE,CAAC,GAAG,IAAI,CAAC;CACpB;AAED;;;;GAIG;AACH,qBAAa,mBAAoB,SAAQ,iBAAiB;IACxD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IAErC,YAAY,MAAM,EAAE,eAAe,EAMlC;IAED,oGAAoG;IAC9F,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAe7C;IAED,0FAA0F;IACpF,OAAO,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAWzD;IAED,qDAAqD;IAC/C,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAU3E;IAED,oBAAoB;IACd,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAOvC;IAED,sFAAsF;IAChF,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC,CAS9D;IAED,2EAA2E;IACrE,eAAe,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAAC,CAAC,CAAC,CAoB/E;IAED;;;OAGG;IACG,QAAQ,CAAC,UAAU,EAAE,WAAW,EAAE,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC,CAclE;IAED,sEAAsE;IACtE,SAAS,IAAI;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAEtD;IAED,cAAc,IAAI,MAAM,CAEvB;IAED,kEAAkE;IAC5D,qBAAqB,IAAI,OAAO,CAAC,OAAO,CAAC,CAO9C;CACF"}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { CloudflareInvalidResponseError, CloudflareNotConfiguredError, cloudflareRequest, isNotFoundError, reasonOf } from "../client/errors.js";
|
|
2
|
+
import { CloudflareManager } from "../client/manager.js";
|
|
3
|
+
//#region src/kv/kvManager.ts
|
|
4
|
+
/**
|
|
5
|
+
* Out-of-Worker KV access over the REST API: namespace management and key reads/writes from a
|
|
6
|
+
* CLI/CI/provisioning context. Inside a Worker you use the `KVNamespace` binding directly — this
|
|
7
|
+
* manager is the REST counterpart, addressed by namespace id.
|
|
8
|
+
*/
|
|
9
|
+
var CloudflareKVManager = class extends CloudflareManager {
|
|
10
|
+
namespaceId;
|
|
11
|
+
constructor(config) {
|
|
12
|
+
super(config);
|
|
13
|
+
if (!config.namespaceId) throw new CloudflareNotConfiguredError({ detail: "Missing namespaceId for KV REST access." });
|
|
14
|
+
this.namespaceId = config.namespaceId;
|
|
15
|
+
}
|
|
16
|
+
/** Read a key's value as text. Returns null when the key is absent (the API 404s a missing key). */
|
|
17
|
+
async get(key) {
|
|
18
|
+
return cloudflareRequest(`KV get for key '${key}'`, async () => {
|
|
19
|
+
try {
|
|
20
|
+
return await (await this.getClient().kv.namespaces.values.get(key, {
|
|
21
|
+
account_id: this.accountId,
|
|
22
|
+
namespace_id: this.namespaceId
|
|
23
|
+
})).text();
|
|
24
|
+
} catch (error) {
|
|
25
|
+
if (isNotFoundError(error)) return null;
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
/** Read a key and `JSON.parse` it. Returns null when absent; throws on malformed JSON. */
|
|
31
|
+
async getJson(key) {
|
|
32
|
+
const raw = await this.get(key);
|
|
33
|
+
if (raw === null) return null;
|
|
34
|
+
try {
|
|
35
|
+
return JSON.parse(raw);
|
|
36
|
+
} catch (error) {
|
|
37
|
+
throw new CloudflareInvalidResponseError({
|
|
38
|
+
message: `KV value for key '${key}' is not valid JSON.`,
|
|
39
|
+
detail: error instanceof Error ? error.message : String(error)
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/** Write a value, with optional TTL and metadata. */
|
|
44
|
+
async set(key, value, options) {
|
|
45
|
+
await cloudflareRequest(`KV set for key '${key}'`, async () => {
|
|
46
|
+
await this.getClient().kv.namespaces.values.update(key, {
|
|
47
|
+
account_id: this.accountId,
|
|
48
|
+
namespace_id: this.namespaceId,
|
|
49
|
+
value,
|
|
50
|
+
...options?.expirationTtl !== void 0 ? { expiration_ttl: options.expirationTtl } : {},
|
|
51
|
+
...options?.metadata !== void 0 ? { metadata: JSON.stringify(options.metadata) } : {}
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
/** Delete a key. */
|
|
56
|
+
async delete(key) {
|
|
57
|
+
await cloudflareRequest(`KV delete for key '${key}'`, async () => {
|
|
58
|
+
await this.getClient().kv.namespaces.values.delete(key, {
|
|
59
|
+
account_id: this.accountId,
|
|
60
|
+
namespace_id: this.namespaceId
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
/** List keys in the namespace, optionally filtered by prefix and bounded by limit. */
|
|
65
|
+
async listKeys(prefix, limit) {
|
|
66
|
+
return cloudflareRequest("KV list keys", async () => {
|
|
67
|
+
return (await this.getClient().kv.namespaces.keys.list(this.namespaceId, {
|
|
68
|
+
account_id: this.accountId,
|
|
69
|
+
...prefix !== void 0 ? { prefix } : {},
|
|
70
|
+
...limit !== void 0 ? { limit } : {}
|
|
71
|
+
})).result;
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
/** Read a key's value and metadata together. Both are null when absent. */
|
|
75
|
+
async getWithMetadata(key) {
|
|
76
|
+
return cloudflareRequest(`KV getWithMetadata for key '${key}'`, async () => {
|
|
77
|
+
const [value, metadata] = await Promise.all([this.get(key), this.getClient().kv.namespaces.metadata.get(key, {
|
|
78
|
+
account_id: this.accountId,
|
|
79
|
+
namespace_id: this.namespaceId
|
|
80
|
+
}).catch((error) => {
|
|
81
|
+
if (isNotFoundError(error)) return null;
|
|
82
|
+
throw error;
|
|
83
|
+
})]);
|
|
84
|
+
return {
|
|
85
|
+
value,
|
|
86
|
+
metadata: metadata ?? null
|
|
87
|
+
};
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Write many keys, collecting a per-key result instead of failing the whole batch on the first
|
|
92
|
+
* error — the caller decides what to do with partial failure.
|
|
93
|
+
*/
|
|
94
|
+
async batchSet(operations) {
|
|
95
|
+
const results = [];
|
|
96
|
+
for (const operation of operations) try {
|
|
97
|
+
await this.set(operation.key, operation.value, {
|
|
98
|
+
expirationTtl: operation.expirationTtl,
|
|
99
|
+
metadata: operation.metadata
|
|
100
|
+
});
|
|
101
|
+
results.push({
|
|
102
|
+
key: operation.key,
|
|
103
|
+
success: true
|
|
104
|
+
});
|
|
105
|
+
} catch (error) {
|
|
106
|
+
results.push({
|
|
107
|
+
key: operation.key,
|
|
108
|
+
success: false,
|
|
109
|
+
error: reasonOf(error)
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
return results;
|
|
113
|
+
}
|
|
114
|
+
/** The namespace this manager targets and the account it lives in. */
|
|
115
|
+
getKVInfo() {
|
|
116
|
+
return {
|
|
117
|
+
namespaceId: this.namespaceId,
|
|
118
|
+
accountId: this.accountId
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
getServiceType() {
|
|
122
|
+
return "KV Storage";
|
|
123
|
+
}
|
|
124
|
+
/** Prove access by reading the namespace record. Never throws. */
|
|
125
|
+
async validateServiceAccess() {
|
|
126
|
+
try {
|
|
127
|
+
await this.getClient().kv.namespaces.get(this.namespaceId, { account_id: this.accountId });
|
|
128
|
+
return true;
|
|
129
|
+
} catch {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
//#endregion
|
|
135
|
+
export { CloudflareKVManager };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { CloudflareManager } from "../client/manager";
|
|
3
|
+
/** A KV namespace's identity, decoded from the create response. */
|
|
4
|
+
export declare const KVNamespaceInfo: z.ZodObject<{
|
|
5
|
+
id: z.ZodString;
|
|
6
|
+
title: z.ZodString;
|
|
7
|
+
}, z.core.$strip>;
|
|
8
|
+
export type KVNamespaceInfo = z.output<typeof KVNamespaceInfo>;
|
|
9
|
+
/**
|
|
10
|
+
* Account-level KV control plane: **create and delete namespaces**. Customers use this to stand up
|
|
11
|
+
* and tear down per-environment KV namespaces (e.g. ephemeral staging), and `pithy add`-driven
|
|
12
|
+
* provisioning uses it to create each environment's namespaces. Addressed by account — unlike
|
|
13
|
+
* {@link CloudflareKVManager}, which targets one namespace id for key reads/writes.
|
|
14
|
+
*/
|
|
15
|
+
export declare class CloudflareKVProvisioner extends CloudflareManager {
|
|
16
|
+
getServiceType(): string;
|
|
17
|
+
/** Prove access by listing namespaces — a read, never a destructive create/delete. Never throws. */
|
|
18
|
+
validateServiceAccess(): Promise<boolean>;
|
|
19
|
+
/** Find a namespace by exact title in the account, or `null` — for idempotent provisioning. */
|
|
20
|
+
findNamespaceByTitle(title: string): Promise<KVNamespaceInfo | null>;
|
|
21
|
+
/** List every namespace in the account — for prefix-scan reconcile teardown. */
|
|
22
|
+
listNamespaces(): Promise<KVNamespaceInfo[]>;
|
|
23
|
+
/** Create a KV namespace by title; returns its id and title. */
|
|
24
|
+
createNamespace(title: string): Promise<KVNamespaceInfo>;
|
|
25
|
+
/** Delete a KV namespace by id. Idempotent — a missing namespace is not an error, so teardown can re-run safely. */
|
|
26
|
+
deleteNamespace(namespaceId: string): Promise<void>;
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=kvProvisioner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"kvProvisioner.d.ts","sourceRoot":"","sources":["../../src/kv/kvProvisioner.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAEtD,mEAAmE;AACnE,eAAO,MAAM,eAAe;;;iBAK4D,CAAC;AACzF,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,eAAe,CAAC,CAAC;AAE/D;;;;;GAKG;AACH,qBAAa,uBAAwB,SAAQ,iBAAiB;IAC5D,cAAc,IAAI,MAAM,CAEvB;IAED,oGAAoG;IAC9F,qBAAqB,IAAI,OAAO,CAAC,OAAO,CAAC,CAO9C;IAED,+FAA+F;IACzF,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC,CAQzE;IAED,gFAAgF;IAC1E,cAAc,IAAI,OAAO,CAAC,eAAe,EAAE,CAAC,CASjD;IAED,gEAAgE;IAC1D,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAK7D;IAED,oHAAoH;IAC9G,eAAe,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CASxD;CACF"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { cloudflareRequest, decodeResponse, isNotFoundError } from "../client/errors.js";
|
|
2
|
+
import { CloudflareManager } from "../client/manager.js";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
//#region src/kv/kvProvisioner.ts
|
|
5
|
+
/** A KV namespace's identity, decoded from the create response. */
|
|
6
|
+
const KVNamespaceInfo = z.object({
|
|
7
|
+
id: z.string().describe("The CF-assigned namespace id used to address the namespace."),
|
|
8
|
+
title: z.string().describe("The namespace title.")
|
|
9
|
+
}).describe("A Cloudflare KV namespace's identity, as returned by the create endpoint.");
|
|
10
|
+
/**
|
|
11
|
+
* Account-level KV control plane: **create and delete namespaces**. Customers use this to stand up
|
|
12
|
+
* and tear down per-environment KV namespaces (e.g. ephemeral staging), and `pithy add`-driven
|
|
13
|
+
* provisioning uses it to create each environment's namespaces. Addressed by account — unlike
|
|
14
|
+
* {@link CloudflareKVManager}, which targets one namespace id for key reads/writes.
|
|
15
|
+
*/
|
|
16
|
+
var CloudflareKVProvisioner = class extends CloudflareManager {
|
|
17
|
+
getServiceType() {
|
|
18
|
+
return "Cloudflare KV (control plane)";
|
|
19
|
+
}
|
|
20
|
+
/** Prove access by listing namespaces — a read, never a destructive create/delete. Never throws. */
|
|
21
|
+
async validateServiceAccess() {
|
|
22
|
+
try {
|
|
23
|
+
await this.getClient().kv.namespaces.list({ account_id: this.accountId });
|
|
24
|
+
return true;
|
|
25
|
+
} catch {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/** Find a namespace by exact title in the account, or `null` — for idempotent provisioning. */
|
|
30
|
+
async findNamespaceByTitle(title) {
|
|
31
|
+
return cloudflareRequest(`find KV namespace ${title}`, async () => {
|
|
32
|
+
for await (const ns of this.getClient().kv.namespaces.list({ account_id: this.accountId })) {
|
|
33
|
+
const parsed = KVNamespaceInfo.safeParse(ns);
|
|
34
|
+
if (parsed.success && parsed.data.title === title) return parsed.data;
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
/** List every namespace in the account — for prefix-scan reconcile teardown. */
|
|
40
|
+
async listNamespaces() {
|
|
41
|
+
return cloudflareRequest("list KV namespaces", async () => {
|
|
42
|
+
const namespaces = [];
|
|
43
|
+
for await (const ns of this.getClient().kv.namespaces.list({ account_id: this.accountId })) {
|
|
44
|
+
const parsed = KVNamespaceInfo.safeParse(ns);
|
|
45
|
+
if (parsed.success) namespaces.push(parsed.data);
|
|
46
|
+
}
|
|
47
|
+
return namespaces;
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
/** Create a KV namespace by title; returns its id and title. */
|
|
51
|
+
async createNamespace(title) {
|
|
52
|
+
const response = await cloudflareRequest(`create KV namespace ${title}`, () => this.getClient().kv.namespaces.create({
|
|
53
|
+
account_id: this.accountId,
|
|
54
|
+
title
|
|
55
|
+
}));
|
|
56
|
+
return decodeResponse(KVNamespaceInfo, response, "KV namespace create");
|
|
57
|
+
}
|
|
58
|
+
/** Delete a KV namespace by id. Idempotent — a missing namespace is not an error, so teardown can re-run safely. */
|
|
59
|
+
async deleteNamespace(namespaceId) {
|
|
60
|
+
await cloudflareRequest(`delete KV namespace ${namespaceId}`, async () => {
|
|
61
|
+
try {
|
|
62
|
+
await this.getClient().kv.namespaces.delete(namespaceId, { account_id: this.accountId });
|
|
63
|
+
} catch (error) {
|
|
64
|
+
if (isNotFoundError(error)) return;
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
//#endregion
|
|
71
|
+
export { CloudflareKVProvisioner, KVNamespaceInfo };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { CloudflareImageManager } from "./imageManager";
|
|
2
|
+
import { type AssetOwner } from "./ownership";
|
|
3
|
+
import type { CloudflareStreamManager } from "./streamManager";
|
|
4
|
+
/**
|
|
5
|
+
* Upload image bytes to Cloudflare Images, stamped with the owner, returning the minted id. Images
|
|
6
|
+
* supports a direct byte upload, so this is a single `uploadImage({ file, metadata })` call — the
|
|
7
|
+
* manager JSON-encodes the metadata bag for the wire. The fixture's own metadata rides along; the
|
|
8
|
+
* ownership keys are merged last, so a fixture cannot claim another project's assets.
|
|
9
|
+
*/
|
|
10
|
+
export declare function uploadImageBytes(manager: CloudflareImageManager, bytes: Uint8Array | Blob, opts: {
|
|
11
|
+
owner: AssetOwner;
|
|
12
|
+
metadata?: Record<string, string>;
|
|
13
|
+
}): Promise<{
|
|
14
|
+
id: string;
|
|
15
|
+
}>;
|
|
16
|
+
/**
|
|
17
|
+
* Upload video bytes to Cloudflare Stream, returning the minted uid. Stream has no single direct-bytes
|
|
18
|
+
* method, so this mints a one-time upload URL with `createDirectUpload` — carrying the stamped metadata
|
|
19
|
+
* as `meta` so ownership lands with the video — then POSTs the bytes to that URL as `multipart/form-data` (the
|
|
20
|
+
* basic direct-upload contract). Because the mint carries the metadata, no follow-up `updateVideo`
|
|
21
|
+
* stamp is needed; if a future Stream contract dropped mint-time `meta`, stamp with
|
|
22
|
+
* `updateVideo(uid, { meta })` after the upload.
|
|
23
|
+
*/
|
|
24
|
+
export declare function uploadStreamBytes(manager: CloudflareStreamManager, bytes: Uint8Array | Blob, opts: {
|
|
25
|
+
owner: AssetOwner;
|
|
26
|
+
metadata?: Record<string, string>;
|
|
27
|
+
maxDurationSeconds?: number;
|
|
28
|
+
}): Promise<{
|
|
29
|
+
uid: string;
|
|
30
|
+
}>;
|
|
31
|
+
//# sourceMappingURL=assetSeeder.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"assetSeeder.d.ts","sourceRoot":"","sources":["../../src/media/assetSeeder.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,gBAAgB,CAAC;AAC7D,OAAO,EAAE,KAAK,UAAU,EAAsB,MAAM,aAAa,CAAC;AAClE,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,iBAAiB,CAAC;AAyB/D;;;;;GAKG;AACH,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,sBAAsB,EAC/B,KAAK,EAAE,UAAU,GAAG,IAAI,EACxB,IAAI,EAAE;IAAE,KAAK,EAAE,UAAU,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,GAC7D,OAAO,CAAC;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,CAAC,CAMzB;AAED;;;;;;;GAOG;AACH,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,uBAAuB,EAChC,KAAK,EAAE,UAAU,GAAG,IAAI,EACxB,IAAI,EAAE;IAAE,KAAK,EAAE,UAAU,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAAC,kBAAkB,CAAC,EAAE,MAAM,CAAA;CAAE,GAC1F,OAAO,CAAC;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC,CAwB1B"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { CloudflareRequestError, cloudflareRequest, decodeResponse } from "../client/errors.js";
|
|
2
|
+
import { withAssetOwnership } from "./ownership.js";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
//#region src/media/assetSeeder.ts
|
|
5
|
+
/**
|
|
6
|
+
* Seed-time byte uploads to the account-flat Cloudflare Images and Stream stores. Seeding hands a
|
|
7
|
+
* manager raw bytes and a metadata bag and gets back the minted asset id/uid to record — the one
|
|
8
|
+
* place these two stores accept fixture bytes from a CLI/CI context. Every wire response is
|
|
9
|
+
* validated with a local Zod object so a shape drift fails loudly (`cloudflare/invalid_response`)
|
|
10
|
+
* instead of surfacing as `undefined`. These calls are always remote: neither store has a local
|
|
11
|
+
* emulation, so even a `pithy seed` against dev writes into the same store production shares.
|
|
12
|
+
*
|
|
13
|
+
* Which is why {@link AssetOwner} is **required**, not optional. An asset here is keyed by a
|
|
14
|
+
* Cloudflare-minted id, so its metadata is the only thing that says which project seeded it —
|
|
15
|
+
* see `ownership.ts`. Seed fixtures are the likeliest source of debris, and an unattributable
|
|
16
|
+
* fixture asset is one nobody can sweep.
|
|
17
|
+
*/
|
|
18
|
+
/** The default max video duration (seconds) CF Stream requires for a direct upload — 6 hours. */
|
|
19
|
+
const STREAM_MAX_DURATION_SECONDS = 21600;
|
|
20
|
+
/** The only field of an Images upload response the seeder needs: the minted image id. */
|
|
21
|
+
const ImageUploadResult = z.object({ id: z.string() });
|
|
22
|
+
/** The fields of a Stream direct-upload mint the seeder needs: the uid and the one-time upload URL. */
|
|
23
|
+
const StreamDirectUploadResult = z.object({
|
|
24
|
+
uid: z.string(),
|
|
25
|
+
uploadURL: z.string()
|
|
26
|
+
});
|
|
27
|
+
/**
|
|
28
|
+
* Upload image bytes to Cloudflare Images, stamped with the owner, returning the minted id. Images
|
|
29
|
+
* supports a direct byte upload, so this is a single `uploadImage({ file, metadata })` call — the
|
|
30
|
+
* manager JSON-encodes the metadata bag for the wire. The fixture's own metadata rides along; the
|
|
31
|
+
* ownership keys are merged last, so a fixture cannot claim another project's assets.
|
|
32
|
+
*/
|
|
33
|
+
async function uploadImageBytes(manager, bytes, opts) {
|
|
34
|
+
const file = bytes instanceof File ? bytes : new File([bytes], "seed");
|
|
35
|
+
const response = await manager.uploadImage({
|
|
36
|
+
file,
|
|
37
|
+
metadata: withAssetOwnership(opts.owner, opts.metadata)
|
|
38
|
+
});
|
|
39
|
+
return { id: decodeResponse(ImageUploadResult, response, "Images seed upload").id };
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Upload video bytes to Cloudflare Stream, returning the minted uid. Stream has no single direct-bytes
|
|
43
|
+
* method, so this mints a one-time upload URL with `createDirectUpload` — carrying the stamped metadata
|
|
44
|
+
* as `meta` so ownership lands with the video — then POSTs the bytes to that URL as `multipart/form-data` (the
|
|
45
|
+
* basic direct-upload contract). Because the mint carries the metadata, no follow-up `updateVideo`
|
|
46
|
+
* stamp is needed; if a future Stream contract dropped mint-time `meta`, stamp with
|
|
47
|
+
* `updateVideo(uid, { meta })` after the upload.
|
|
48
|
+
*/
|
|
49
|
+
async function uploadStreamBytes(manager, bytes, opts) {
|
|
50
|
+
const mint = decodeResponse(StreamDirectUploadResult, await manager.createDirectUpload({
|
|
51
|
+
maxDurationSeconds: opts.maxDurationSeconds ?? STREAM_MAX_DURATION_SECONDS,
|
|
52
|
+
meta: withAssetOwnership(opts.owner, opts.metadata)
|
|
53
|
+
}), "Stream seed direct upload");
|
|
54
|
+
await cloudflareRequest("Stream seed byte upload", async () => {
|
|
55
|
+
const file = bytes instanceof Blob ? bytes : new Blob([bytes]);
|
|
56
|
+
const form = new FormData();
|
|
57
|
+
form.append("file", file, "seed");
|
|
58
|
+
const response = await fetch(mint.uploadURL, {
|
|
59
|
+
method: "POST",
|
|
60
|
+
body: form
|
|
61
|
+
});
|
|
62
|
+
if (!response.ok) throw new CloudflareRequestError({
|
|
63
|
+
message: "The Stream direct upload failed.",
|
|
64
|
+
detail: `Stream direct upload returned ${response.status}: ${await response.text()}`
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
return { uid: mint.uid };
|
|
68
|
+
}
|
|
69
|
+
//#endregion
|
|
70
|
+
export { uploadImageBytes, uploadStreamBytes };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { Image, V1CreateParams, V1EditParams } from "cloudflare/resources/images/v1/v1";
|
|
2
|
+
import type { DirectUploadCreateParams, DirectUploadCreateResponse } from "cloudflare/resources/images/v2/direct-uploads";
|
|
3
|
+
import type { V2ListParams } from "cloudflare/resources/images/v2/v2";
|
|
4
|
+
import { CloudflareManager } from "../client/manager";
|
|
5
|
+
/**
|
|
6
|
+
* Out-of-Worker Cloudflare Images access over the REST API: upload, fetch, edit, delete, list, and
|
|
7
|
+
* direct-upload URLs from a CLI/CI/provisioning context. Image metadata is an arbitrary
|
|
8
|
+
* `Record<string, string>` passthrough at this layer — the manager imposes no shape.
|
|
9
|
+
*
|
|
10
|
+
* The ownership stamp is imposed one layer up, by `withAssetOwnership` in `ownership.ts`, which every
|
|
11
|
+
* Pithy path that *creates* an image goes through (the seeder here, and media's `imageMinter`). The
|
|
12
|
+
* split is deliberate: this class stays a faithful REST client usable for reading and sweeping
|
|
13
|
+
* anyone's images, while nothing Pithy writes can escape carrying `pithyProject`/`pithyEnv`.
|
|
14
|
+
*/
|
|
15
|
+
export declare class CloudflareImageManager extends CloudflareManager {
|
|
16
|
+
/** Upload an image via the V1 API. `account_id` is set from the manager's config. */
|
|
17
|
+
uploadImage(params: Omit<V1CreateParams, "account_id">): Promise<Image>;
|
|
18
|
+
/** Fetch details for one image by id. */
|
|
19
|
+
imageDetails(imageId: string): Promise<Image>;
|
|
20
|
+
/** Update an image's metadata and settings. */
|
|
21
|
+
updateImage(imageId: string, params: Omit<V1EditParams, "account_id">): Promise<Image>;
|
|
22
|
+
/** Delete an image by id. */
|
|
23
|
+
deleteImage(imageId: string): Promise<void>;
|
|
24
|
+
/** Create a direct-upload URL for a client-side upload (V2 API). */
|
|
25
|
+
createDirectUploadUrl(params: Omit<DirectUploadCreateParams, "account_id">): Promise<DirectUploadCreateResponse>;
|
|
26
|
+
/** List images (V2 API), first page only. Returns the page's images, never null. */
|
|
27
|
+
listImages(params?: Omit<V2ListParams, "account_id">): Promise<Image[]>;
|
|
28
|
+
/** List all images across every page, following the V2 continuation token. */
|
|
29
|
+
listAllImages(params?: Omit<V2ListParams, "account_id">): Promise<Image[]>;
|
|
30
|
+
getServiceType(): string;
|
|
31
|
+
/** Prove access by listing images. Never throws. */
|
|
32
|
+
validateServiceAccess(): Promise<boolean>;
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=imageManager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"imageManager.d.ts","sourceRoot":"","sources":["../../src/media/imageManager.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,KAAK,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,mCAAmC,CAAC;AAC7F,OAAO,KAAK,EACV,wBAAwB,EACxB,0BAA0B,EAC3B,MAAM,+CAA+C,CAAC;AACvD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mCAAmC,CAAC;AAEtE,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAoBtD;;;;;;;;;GASG;AACH,qBAAa,sBAAuB,SAAQ,iBAAiB;IAC3D,qFAAqF;IAC/E,WAAW,CAAC,MAAM,EAAE,IAAI,CAAC,cAAc,EAAE,YAAY,CAAC,GAAG,OAAO,CAAC,KAAK,CAAC,CAO5E;IAED,yCAAyC;IACnC,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC,CAIlD;IAED,+CAA+C;IACzC,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,CAAC,YAAY,EAAE,YAAY,CAAC,GAAG,OAAO,CAAC,KAAK,CAAC,CAI3F;IAED,6BAA6B;IACvB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAIhD;IAED,oEAAoE;IAC9D,qBAAqB,CACzB,MAAM,EAAE,IAAI,CAAC,wBAAwB,EAAE,YAAY,CAAC,GACnD,OAAO,CAAC,0BAA0B,CAAC,CAOrC;IAED,oFAAoF;IAC9E,UAAU,CAAC,MAAM,CAAC,EAAE,IAAI,CAAC,YAAY,EAAE,YAAY,CAAC,GAAG,OAAO,CAAC,KAAK,EAAE,CAAC,CAK5E;IAED,8EAA8E;IACxE,aAAa,CAAC,MAAM,CAAC,EAAE,IAAI,CAAC,YAAY,EAAE,YAAY,CAAC,GAAG,OAAO,CAAC,KAAK,EAAE,CAAC,CAe/E;IAED,cAAc,IAAI,MAAM,CAEvB;IAED,oDAAoD;IAC9C,qBAAqB,IAAI,OAAO,CAAC,OAAO,CAAC,CAO9C;CACF"}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { cloudflareRequest } from "../client/errors.js";
|
|
2
|
+
import { CloudflareManager } from "../client/manager.js";
|
|
3
|
+
//#region src/media/imageManager.ts
|
|
4
|
+
/**
|
|
5
|
+
* Per-request CF SDK options for every Images call: a 10s timeout and up to 3 retries. Uploads and
|
|
6
|
+
* list calls can be slow, so the timeout is generous and retries cover transient API blips.
|
|
7
|
+
*/
|
|
8
|
+
const requestOptions = {
|
|
9
|
+
timeout: 1e4,
|
|
10
|
+
maxRetries: 3
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Encode a user-metadata bag to the string the Images API expects. CF stores image metadata as a
|
|
14
|
+
* JSON string; we serialize the generic `Record<string, string>` passthrough before sending, and
|
|
15
|
+
* leave an absent bag untouched.
|
|
16
|
+
*/
|
|
17
|
+
function encodeMetadata(metadata) {
|
|
18
|
+
return metadata === void 0 || metadata === null ? metadata : JSON.stringify(metadata);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Out-of-Worker Cloudflare Images access over the REST API: upload, fetch, edit, delete, list, and
|
|
22
|
+
* direct-upload URLs from a CLI/CI/provisioning context. Image metadata is an arbitrary
|
|
23
|
+
* `Record<string, string>` passthrough at this layer — the manager imposes no shape.
|
|
24
|
+
*
|
|
25
|
+
* The ownership stamp is imposed one layer up, by `withAssetOwnership` in `ownership.ts`, which every
|
|
26
|
+
* Pithy path that *creates* an image goes through (the seeder here, and media's `imageMinter`). The
|
|
27
|
+
* split is deliberate: this class stays a faithful REST client usable for reading and sweeping
|
|
28
|
+
* anyone's images, while nothing Pithy writes can escape carrying `pithyProject`/`pithyEnv`.
|
|
29
|
+
*/
|
|
30
|
+
var CloudflareImageManager = class extends CloudflareManager {
|
|
31
|
+
/** Upload an image via the V1 API. `account_id` is set from the manager's config. */
|
|
32
|
+
async uploadImage(params) {
|
|
33
|
+
return cloudflareRequest("Images upload", () => this.getClient().images.v1.create({
|
|
34
|
+
account_id: this.accountId,
|
|
35
|
+
...params,
|
|
36
|
+
metadata: encodeMetadata(params.metadata)
|
|
37
|
+
}, requestOptions));
|
|
38
|
+
}
|
|
39
|
+
/** Fetch details for one image by id. */
|
|
40
|
+
async imageDetails(imageId) {
|
|
41
|
+
return cloudflareRequest(`Images get details for '${imageId}'`, () => this.getClient().images.v1.get(imageId, { account_id: this.accountId }, requestOptions));
|
|
42
|
+
}
|
|
43
|
+
/** Update an image's metadata and settings. */
|
|
44
|
+
async updateImage(imageId, params) {
|
|
45
|
+
return cloudflareRequest(`Images update '${imageId}'`, () => this.getClient().images.v1.edit(imageId, {
|
|
46
|
+
account_id: this.accountId,
|
|
47
|
+
...params
|
|
48
|
+
}, requestOptions));
|
|
49
|
+
}
|
|
50
|
+
/** Delete an image by id. */
|
|
51
|
+
async deleteImage(imageId) {
|
|
52
|
+
await cloudflareRequest(`Images delete '${imageId}'`, () => this.getClient().images.v1.delete(imageId, { account_id: this.accountId }, requestOptions));
|
|
53
|
+
}
|
|
54
|
+
/** Create a direct-upload URL for a client-side upload (V2 API). */
|
|
55
|
+
async createDirectUploadUrl(params) {
|
|
56
|
+
return cloudflareRequest("Images create direct upload URL", () => this.getClient().images.v2.directUploads.create({
|
|
57
|
+
account_id: this.accountId,
|
|
58
|
+
...params,
|
|
59
|
+
metadata: encodeMetadata(params.metadata)
|
|
60
|
+
}, requestOptions));
|
|
61
|
+
}
|
|
62
|
+
/** List images (V2 API), first page only. Returns the page's images, never null. */
|
|
63
|
+
async listImages(params) {
|
|
64
|
+
return cloudflareRequest("Images list", async () => {
|
|
65
|
+
return (await this.getClient().images.v2.list({
|
|
66
|
+
account_id: this.accountId,
|
|
67
|
+
...params
|
|
68
|
+
}, requestOptions)).images ?? [];
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
/** List all images across every page, following the V2 continuation token. */
|
|
72
|
+
async listAllImages(params) {
|
|
73
|
+
return cloudflareRequest("Images list all", async () => {
|
|
74
|
+
const allImages = [];
|
|
75
|
+
const client = this.getClient();
|
|
76
|
+
const fetchPage = async (token) => {
|
|
77
|
+
const response = await client.images.v2.list({
|
|
78
|
+
account_id: this.accountId,
|
|
79
|
+
...params,
|
|
80
|
+
continuation_token: token
|
|
81
|
+
}, requestOptions);
|
|
82
|
+
if (response.images) allImages.push(...response.images);
|
|
83
|
+
if (response.continuation_token) await fetchPage(response.continuation_token);
|
|
84
|
+
};
|
|
85
|
+
await fetchPage();
|
|
86
|
+
return allImages;
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
getServiceType() {
|
|
90
|
+
return "Cloudflare Images";
|
|
91
|
+
}
|
|
92
|
+
/** Prove access by listing images. Never throws. */
|
|
93
|
+
async validateServiceAccess() {
|
|
94
|
+
try {
|
|
95
|
+
await this.listImages();
|
|
96
|
+
return true;
|
|
97
|
+
} catch {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
//#endregion
|
|
103
|
+
export { CloudflareImageManager };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ownership stamp on every Cloudflare Images and Stream asset Pithy creates.
|
|
3
|
+
*
|
|
4
|
+
* Every other resource this toolset provisions is isolated by its **name** —
|
|
5
|
+
* `<project>-<env>-<thing>`, because those namespaces are flat and account-wide (CLAUDE.md
|
|
6
|
+
* §Resource naming). Images and Stream cannot use that lever: their stores are just as flat and
|
|
7
|
+
* account-wide, but an asset is keyed by a **Cloudflare-minted id**, not by a name we choose. So
|
|
8
|
+
* metadata is the only place ownership can live, and without it two projects' assets in one account
|
|
9
|
+
* are indistinguishable — you cannot tell which app owns an asset, cannot sweep one app's assets at
|
|
10
|
+
* teardown, and `pithy doctor` is blind to them.
|
|
11
|
+
*
|
|
12
|
+
* The **same two keys** are used for both stores (Images `metadata`, Stream `meta`), so one query
|
|
13
|
+
* answers "what does this project own" across them rather than two dialects answering half each.
|
|
14
|
+
* Nothing else is imposed: a caller's own metadata rides along untouched — but it can never displace
|
|
15
|
+
* the reserved keys, which are merged last on purpose.
|
|
16
|
+
*/
|
|
17
|
+
/** The metadata key naming the owning project — the root `pithy.config.ts` `name`. */
|
|
18
|
+
export declare const ASSET_PROJECT_KEY = "pithyProject";
|
|
19
|
+
/** The metadata key naming the owning environment (`dev` | `staging` | `production`). */
|
|
20
|
+
export declare const ASSET_ENV_KEY = "pithyEnv";
|
|
21
|
+
/** Who owns an asset: the project that created it, in the environment that created it. */
|
|
22
|
+
export interface AssetOwner {
|
|
23
|
+
/**
|
|
24
|
+
* The project name — the root `pithy.config.ts` `name`, resolved by `requireProjectName` and never
|
|
25
|
+
* guessed. It is the segment every sweep, listing, and teardown filters on.
|
|
26
|
+
*/
|
|
27
|
+
project: string;
|
|
28
|
+
/** The environment the asset was created in (`dev` | `staging` | `production`). */
|
|
29
|
+
env: string;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Merge the ownership stamp over a caller's metadata bag.
|
|
33
|
+
*
|
|
34
|
+
* Ownership is applied **last**, so a caller-supplied `pithyProject`/`pithyEnv` cannot displace the
|
|
35
|
+
* real one — a fixture or a client-supplied bag claiming another project's assets would defeat the
|
|
36
|
+
* whole point of the stamp. A blank project or environment is refused rather than written: an asset
|
|
37
|
+
* stamped with an empty string reads as owned and still sweeps to nothing.
|
|
38
|
+
*/
|
|
39
|
+
export declare function withAssetOwnership(owner: AssetOwner, metadata?: Record<string, string>): Record<string, string>;
|
|
40
|
+
//# sourceMappingURL=ownership.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ownership.d.ts","sourceRoot":"","sources":["../../src/media/ownership.ts"],"names":[],"mappings":"AAKA;;;;;;;;;;;;;;;GAeG;AAEH,sFAAsF;AACtF,eAAO,MAAM,iBAAiB,iBAAiB,CAAC;AAEhD,yFAAyF;AACzF,eAAO,MAAM,aAAa,aAAa,CAAC;AAExC,0FAA0F;AAC1F,MAAM,WAAW,UAAU;IACzB;;;OAGG;IACH,OAAO,EAAE,MAAM,CAAC;IAChB,mFAAmF;IACnF,GAAG,EAAE,MAAM,CAAC;CACb;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,UAAU,EAAE,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAW/G"}
|