@forgezero/providers 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +75 -0
- package/dist/binance.d.ts +27 -0
- package/dist/binance.js +493 -0
- package/dist/chain.d.ts +99 -0
- package/dist/chain.js +279 -0
- package/dist/database.d.ts +49 -0
- package/dist/database.js +209 -0
- package/dist/email.d.ts +68 -0
- package/dist/email.js +278 -0
- package/dist/http.d.ts +100 -0
- package/dist/http.js +283 -0
- package/dist/index.d.ts +155 -0
- package/dist/index.js +139 -0
- package/dist/pool.d.ts +112 -0
- package/dist/pool.js +376 -0
- package/dist/storage.d.ts +137 -0
- package/dist/storage.js +441 -0
- package/package.json +82 -0
package/dist/pool.js
ADDED
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
class ProviderError extends Error {
|
|
3
|
+
code;
|
|
4
|
+
details;
|
|
5
|
+
constructor(code, message, details) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.code = code;
|
|
8
|
+
this.details = details;
|
|
9
|
+
this.name = "ProviderError";
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
function envCredentials(env) {
|
|
13
|
+
return {
|
|
14
|
+
name: "env",
|
|
15
|
+
async get(reference, field) {
|
|
16
|
+
const key = `${reference}_${field}`.replace(/[.-]/g, "_").toUpperCase();
|
|
17
|
+
const value = env[key];
|
|
18
|
+
if (value === undefined) {
|
|
19
|
+
throw new ProviderError("CREDENTIAL_MISSING", `Set ${key} in the environment.`);
|
|
20
|
+
}
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function chainCredentials(...sources) {
|
|
26
|
+
return {
|
|
27
|
+
name: sources.map((source) => source.name).join("+"),
|
|
28
|
+
async get(reference, field) {
|
|
29
|
+
let last;
|
|
30
|
+
for (const source of sources) {
|
|
31
|
+
try {
|
|
32
|
+
return await source.get(reference, field);
|
|
33
|
+
} catch (error) {
|
|
34
|
+
last = error;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
throw last instanceof Error ? last : new ProviderError("CREDENTIAL_MISSING", `No source held ${reference}.${field}.`);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function staticConfig(services) {
|
|
42
|
+
const health = new Map;
|
|
43
|
+
return {
|
|
44
|
+
name: "static",
|
|
45
|
+
async list(serviceKey) {
|
|
46
|
+
return (services[serviceKey] ?? []).map((provider) => ({
|
|
47
|
+
...provider,
|
|
48
|
+
health: health.get(`${serviceKey}:${provider.providerId}`) ?? provider.health
|
|
49
|
+
}));
|
|
50
|
+
},
|
|
51
|
+
async recordHealth(serviceKey, providerId, next) {
|
|
52
|
+
health.set(`${serviceKey}:${providerId}`, next);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function defineProvider(spec) {
|
|
57
|
+
return spec;
|
|
58
|
+
}
|
|
59
|
+
var STRIKES_TO_OFFLINE = 3;
|
|
60
|
+
function nextHealth(current, kind) {
|
|
61
|
+
if (kind === "success")
|
|
62
|
+
return { strikes: 0, status: "ok" };
|
|
63
|
+
if (kind === "backoff")
|
|
64
|
+
return current ?? { strikes: 0, status: "ok" };
|
|
65
|
+
const strikes = (current?.strikes ?? 0) + 1;
|
|
66
|
+
return {
|
|
67
|
+
strikes,
|
|
68
|
+
status: strikes >= STRIKES_TO_OFFLINE ? "offline" : "degraded",
|
|
69
|
+
lastFailureAtTs: Date.now()
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function createRegistry(options) {
|
|
73
|
+
const byId = new Map(options.providers.map((provider) => [provider.id, provider]));
|
|
74
|
+
async function call(serviceKey, args) {
|
|
75
|
+
const configured = [...await options.config.list(serviceKey)].filter((provider) => provider.enabled).sort((a, b) => a.priority - b.priority);
|
|
76
|
+
const attempts = [];
|
|
77
|
+
for (const entry of configured) {
|
|
78
|
+
const spec = byId.get(entry.providerId);
|
|
79
|
+
if (!spec) {
|
|
80
|
+
attempts.push({ providerId: entry.providerId, outcome: "skipped", error: "not registered" });
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (entry.health?.status === "offline") {
|
|
84
|
+
attempts.push({ providerId: entry.providerId, outcome: "skipped", error: "offline" });
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
options.before?.({ service: serviceKey, provider: entry.providerId });
|
|
88
|
+
try {
|
|
89
|
+
const result = await spec.invoke({
|
|
90
|
+
config: entry.config,
|
|
91
|
+
secret: (field) => options.credentials.get(entry.secretRef, field)
|
|
92
|
+
}, args);
|
|
93
|
+
attempts.push({ providerId: entry.providerId, outcome: "sent" });
|
|
94
|
+
await options.config.recordHealth(serviceKey, entry.providerId, nextHealth(entry.health, "success"));
|
|
95
|
+
const sent = { ok: true, result, provider: entry.providerId, attempts };
|
|
96
|
+
options.after?.(sent);
|
|
97
|
+
return sent;
|
|
98
|
+
} catch (error) {
|
|
99
|
+
const kind = spec.classify(error);
|
|
100
|
+
attempts.push({
|
|
101
|
+
providerId: entry.providerId,
|
|
102
|
+
outcome: "failed",
|
|
103
|
+
kind,
|
|
104
|
+
error: error instanceof Error ? error.message : String(error)
|
|
105
|
+
});
|
|
106
|
+
await options.config.recordHealth(serviceKey, entry.providerId, nextHealth(entry.health, kind));
|
|
107
|
+
if (kind === "terminal") {
|
|
108
|
+
const refused = {
|
|
109
|
+
ok: false,
|
|
110
|
+
attempts,
|
|
111
|
+
error: new ProviderError("PAYLOAD_REJECTED", "The request was refused as malformed; no provider will accept it.")
|
|
112
|
+
};
|
|
113
|
+
options.after?.(refused);
|
|
114
|
+
return refused;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const failed = {
|
|
119
|
+
ok: false,
|
|
120
|
+
attempts,
|
|
121
|
+
error: new ProviderError(attempts.length === 0 ? "NO_PROVIDER" : "ALL_PROVIDERS_FAILED", attempts.length === 0 ? `No provider is configured for "${serviceKey}".` : `Every provider for "${serviceKey}" failed or was skipped.`)
|
|
122
|
+
};
|
|
123
|
+
options.after?.(failed);
|
|
124
|
+
return failed;
|
|
125
|
+
}
|
|
126
|
+
return { call };
|
|
127
|
+
}
|
|
128
|
+
var VERSION = "0.1.0";
|
|
129
|
+
|
|
130
|
+
// src/http.ts
|
|
131
|
+
class BudgetExhausted extends ProviderError {
|
|
132
|
+
host;
|
|
133
|
+
retryAfterMs;
|
|
134
|
+
constructor(host, retryAfterMs) {
|
|
135
|
+
super("RATE_BUDGET_EXHAUSTED", `The ${host} budget is spent. Retry in ${retryAfterMs}ms.`);
|
|
136
|
+
this.host = host;
|
|
137
|
+
this.retryAfterMs = retryAfterMs;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
var windows = new Map;
|
|
141
|
+
function resetBudgets() {
|
|
142
|
+
windows.clear();
|
|
143
|
+
}
|
|
144
|
+
function spend(budget, cost, nowMs) {
|
|
145
|
+
const ceiling = Math.floor(budget.limit * (budget.headroom ?? 0.9));
|
|
146
|
+
const current = windows.get(budget.host);
|
|
147
|
+
if (!current || current.resetAtMs <= nowMs) {
|
|
148
|
+
windows.set(budget.host, { spent: cost, resetAtMs: nowMs + budget.windowMs });
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (current.spent + cost > ceiling) {
|
|
152
|
+
throw new BudgetExhausted(budget.host, current.resetAtMs - nowMs);
|
|
153
|
+
}
|
|
154
|
+
current.spent += cost;
|
|
155
|
+
}
|
|
156
|
+
function settle(host, reserved, actual) {
|
|
157
|
+
const current = windows.get(host);
|
|
158
|
+
if (!current)
|
|
159
|
+
return;
|
|
160
|
+
current.spent = Math.max(0, current.spent - reserved + actual);
|
|
161
|
+
}
|
|
162
|
+
var budgetState = (host) => windows.get(host);
|
|
163
|
+
function createHttpClient(config) {
|
|
164
|
+
const doFetch = config.fetch ?? globalThis.fetch;
|
|
165
|
+
const timeoutMs = config.timeoutMs ?? 1e4;
|
|
166
|
+
return {
|
|
167
|
+
async call(request) {
|
|
168
|
+
const reserved = request.weight ?? config.budget.defaultCost;
|
|
169
|
+
spend(config.budget, reserved, Date.now());
|
|
170
|
+
const url = new URL(config.baseUrl + request.path);
|
|
171
|
+
for (const [key, value] of Object.entries(request.query ?? {})) {
|
|
172
|
+
url.searchParams.set(key, String(value));
|
|
173
|
+
}
|
|
174
|
+
const controller = new AbortController;
|
|
175
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
176
|
+
try {
|
|
177
|
+
const response = await doFetch(url.toString(), {
|
|
178
|
+
method: request.method ?? "GET",
|
|
179
|
+
signal: controller.signal,
|
|
180
|
+
headers: {
|
|
181
|
+
...request.body === undefined ? {} : { "content-type": "application/json" },
|
|
182
|
+
...request.headers
|
|
183
|
+
},
|
|
184
|
+
...request.body === undefined ? {} : { body: JSON.stringify(request.body) }
|
|
185
|
+
});
|
|
186
|
+
const cost = config.costOf?.(response);
|
|
187
|
+
if (cost !== undefined)
|
|
188
|
+
settle(config.budget.host, reserved, cost);
|
|
189
|
+
const text = await response.text();
|
|
190
|
+
let body;
|
|
191
|
+
try {
|
|
192
|
+
body = text ? JSON.parse(text) : null;
|
|
193
|
+
} catch {
|
|
194
|
+
body = text;
|
|
195
|
+
}
|
|
196
|
+
if (!response.ok) {
|
|
197
|
+
throw Object.assign(new Error(`${config.budget.host} ${response.status}`), {
|
|
198
|
+
status: response.status,
|
|
199
|
+
body,
|
|
200
|
+
retryAfter: response.headers.get("retry-after")
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
return { status: response.status, body, cost };
|
|
204
|
+
} finally {
|
|
205
|
+
clearTimeout(timer);
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
budget: () => budgetState(config.budget.host)
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
var http = defineProvider({
|
|
212
|
+
id: "http",
|
|
213
|
+
service: "http",
|
|
214
|
+
label: "HTTP",
|
|
215
|
+
multiInstance: true,
|
|
216
|
+
credentials: {
|
|
217
|
+
type: "object",
|
|
218
|
+
additionalProperties: false,
|
|
219
|
+
properties: {
|
|
220
|
+
apiKey: { type: "string", title: "API key", writeOnly: true },
|
|
221
|
+
apiSecret: { type: "string", title: "API secret", writeOnly: true }
|
|
222
|
+
}
|
|
223
|
+
},
|
|
224
|
+
config: {
|
|
225
|
+
type: "object",
|
|
226
|
+
additionalProperties: false,
|
|
227
|
+
required: ["baseUrl"],
|
|
228
|
+
properties: {
|
|
229
|
+
baseUrl: { type: "string", title: "Base URL" },
|
|
230
|
+
limit: { type: "integer", default: 6000, title: "Units per window" },
|
|
231
|
+
windowMs: { type: "integer", default: 60000 },
|
|
232
|
+
headroom: {
|
|
233
|
+
type: "number",
|
|
234
|
+
default: 0.9,
|
|
235
|
+
description: "Stop at this fraction of the limit. The venue's window boundary is not ours, and the penalty for crossing is a ban rather than a rejection."
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
async invoke(context, request) {
|
|
240
|
+
const config = context.config;
|
|
241
|
+
const client = createHttpClient({
|
|
242
|
+
baseUrl: config.baseUrl,
|
|
243
|
+
fetch: config.fetch,
|
|
244
|
+
costOf: config.costOf,
|
|
245
|
+
budget: {
|
|
246
|
+
host: new URL(config.baseUrl).host,
|
|
247
|
+
limit: config.limit ?? 6000,
|
|
248
|
+
windowMs: config.windowMs ?? 60000,
|
|
249
|
+
defaultCost: 1,
|
|
250
|
+
headroom: config.headroom
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
return client.call(request);
|
|
254
|
+
},
|
|
255
|
+
classify(error) {
|
|
256
|
+
const status = error.status;
|
|
257
|
+
if (status === 418)
|
|
258
|
+
return "backoff";
|
|
259
|
+
if (status === 429)
|
|
260
|
+
return "backoff";
|
|
261
|
+
if (status === 400 || status === 422)
|
|
262
|
+
return "terminal";
|
|
263
|
+
if (status === 401 || status === 403)
|
|
264
|
+
return "retryable";
|
|
265
|
+
return "retryable";
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
var binanceWeight = (response) => {
|
|
269
|
+
const header = response.headers.get("x-mbx-used-weight-1m");
|
|
270
|
+
return header === null ? undefined : Number(header);
|
|
271
|
+
};
|
|
272
|
+
var httpProviders = [http];
|
|
273
|
+
|
|
274
|
+
// src/pool.ts
|
|
275
|
+
class PoolExhausted extends Error {
|
|
276
|
+
addresses;
|
|
277
|
+
retryAfterMs;
|
|
278
|
+
constructor(addresses, retryAfterMs) {
|
|
279
|
+
super(`Every one of the ${addresses} outbound addresses is at its limit. Retry in ${retryAfterMs}ms, or add an address.`);
|
|
280
|
+
this.addresses = addresses;
|
|
281
|
+
this.retryAfterMs = retryAfterMs;
|
|
282
|
+
this.name = "PoolExhausted";
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
class PoolEmpty extends Error {
|
|
287
|
+
constructor() {
|
|
288
|
+
super("The address pool is enabled with no addresses configured.");
|
|
289
|
+
this.name = "PoolEmpty";
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
function createAddressPool(options) {
|
|
293
|
+
const now = options.now ?? Date.now;
|
|
294
|
+
const enabled = options.enabled ?? true;
|
|
295
|
+
const budgets = new Map;
|
|
296
|
+
const budgetFor = (address) => {
|
|
297
|
+
const existing = budgets.get(address.id);
|
|
298
|
+
if (existing)
|
|
299
|
+
return existing;
|
|
300
|
+
const budget = {
|
|
301
|
+
host: `pool:${address.id}`,
|
|
302
|
+
limit: options.perAddress,
|
|
303
|
+
windowMs: options.windowMs,
|
|
304
|
+
defaultCost: 1,
|
|
305
|
+
headroom: options.headroom ?? 0.9
|
|
306
|
+
};
|
|
307
|
+
budgets.set(address.id, budget);
|
|
308
|
+
return budget;
|
|
309
|
+
};
|
|
310
|
+
const usable = (address) => address.enabled && (address.blockedUntilMs ?? 0) <= now();
|
|
311
|
+
function orderFor(key, candidates) {
|
|
312
|
+
if (candidates.length === 0)
|
|
313
|
+
return [];
|
|
314
|
+
if (!key)
|
|
315
|
+
return [...candidates];
|
|
316
|
+
let hash = 0;
|
|
317
|
+
for (let index = 0;index < key.length; index += 1) {
|
|
318
|
+
hash = hash * 31 + key.charCodeAt(index) >>> 0;
|
|
319
|
+
}
|
|
320
|
+
const start = hash % candidates.length;
|
|
321
|
+
return [...candidates.slice(start), ...candidates.slice(0, start)];
|
|
322
|
+
}
|
|
323
|
+
return {
|
|
324
|
+
take(args = {}) {
|
|
325
|
+
if (!enabled) {
|
|
326
|
+
return { address: null, settle: () => {} };
|
|
327
|
+
}
|
|
328
|
+
const candidates = options.addresses.filter(usable);
|
|
329
|
+
if (candidates.length === 0) {
|
|
330
|
+
if (options.addresses.length === 0)
|
|
331
|
+
throw new PoolEmpty;
|
|
332
|
+
throw new PoolExhausted(options.addresses.length, 60000);
|
|
333
|
+
}
|
|
334
|
+
const cost = args.cost ?? 1;
|
|
335
|
+
let soonest = Number.MAX_SAFE_INTEGER;
|
|
336
|
+
for (const address of orderFor(args.key, candidates)) {
|
|
337
|
+
const budget = budgetFor(address);
|
|
338
|
+
try {
|
|
339
|
+
spend(budget, cost, now());
|
|
340
|
+
return {
|
|
341
|
+
address,
|
|
342
|
+
settle: (actual) => settle(budget.host, cost, actual)
|
|
343
|
+
};
|
|
344
|
+
} catch (cause) {
|
|
345
|
+
if (cause instanceof BudgetExhausted) {
|
|
346
|
+
soonest = Math.min(soonest, cause.retryAfterMs);
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
throw cause;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
throw new PoolExhausted(candidates.length, soonest === Number.MAX_SAFE_INTEGER ? 1000 : soonest);
|
|
353
|
+
},
|
|
354
|
+
block(id, forMs) {
|
|
355
|
+
const address = options.addresses.find((entry) => entry.id === id);
|
|
356
|
+
if (address)
|
|
357
|
+
address.blockedUntilMs = now() + forMs;
|
|
358
|
+
},
|
|
359
|
+
state() {
|
|
360
|
+
return options.addresses.map((address) => ({
|
|
361
|
+
id: address.id,
|
|
362
|
+
spent: budgetState(`pool:${address.id}`)?.spent ?? 0,
|
|
363
|
+
limit: options.perAddress,
|
|
364
|
+
blocked: (address.blockedUntilMs ?? 0) > now(),
|
|
365
|
+
enabled: address.enabled
|
|
366
|
+
}));
|
|
367
|
+
},
|
|
368
|
+
capacity: () => enabled ? options.addresses.filter(usable).length * Math.floor(options.perAddress * (options.headroom ?? 0.9)) : Number.POSITIVE_INFINITY,
|
|
369
|
+
enabled
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
export {
|
|
373
|
+
createAddressPool,
|
|
374
|
+
PoolExhausted,
|
|
375
|
+
PoolEmpty
|
|
376
|
+
};
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { ProviderError } from './index';
|
|
2
|
+
export declare class StorageError extends ProviderError {
|
|
3
|
+
readonly status?: number | undefined;
|
|
4
|
+
constructor(code: string, message: string, status?: number | undefined);
|
|
5
|
+
}
|
|
6
|
+
/** Single-PUT ceiling. Above this a real implementation needs multipart. */
|
|
7
|
+
export declare const MAX_SINGLE_PUT_BYTES: number;
|
|
8
|
+
export interface S3Config {
|
|
9
|
+
endpoint: string;
|
|
10
|
+
region: string;
|
|
11
|
+
bucket: string;
|
|
12
|
+
/**
|
|
13
|
+
* `path` → `host/bucket/key`, `virtual` → `bucket.host/key`.
|
|
14
|
+
*
|
|
15
|
+
* Defaults to `path` because that is what every S3-compatible store other
|
|
16
|
+
* than AWS expects, and getting it wrong presents as a 404 rather than as a
|
|
17
|
+
* configuration error.
|
|
18
|
+
*/
|
|
19
|
+
addressing?: 'path' | 'virtual';
|
|
20
|
+
fetch?: typeof globalThis.fetch;
|
|
21
|
+
timeoutMs?: number;
|
|
22
|
+
}
|
|
23
|
+
export interface S3Credentials {
|
|
24
|
+
accessKeyId: string;
|
|
25
|
+
secretAccessKey: string;
|
|
26
|
+
/** STS / temporary credentials. Carried in `x-amz-security-token` when present. */
|
|
27
|
+
sessionToken?: string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Percent-encode a path segment per SigV4's rules.
|
|
31
|
+
*
|
|
32
|
+
* `encodeURIComponent` leaves `!'()*` alone and S3 does not, so a key containing
|
|
33
|
+
* any of them signs correctly on our side and is rejected as a signature
|
|
34
|
+
* mismatch by the vendor — an error that points at credentials rather than at
|
|
35
|
+
* the filename that actually caused it.
|
|
36
|
+
*/
|
|
37
|
+
export declare function encodeSegment(segment: string): string;
|
|
38
|
+
/** `20260731T140233Z` and `20260731`, the two forms every part of SigV4 wants. */
|
|
39
|
+
export declare function amzDate(at: Date): {
|
|
40
|
+
full: string;
|
|
41
|
+
short: string;
|
|
42
|
+
};
|
|
43
|
+
export interface SignedRequest {
|
|
44
|
+
url: string;
|
|
45
|
+
method: string;
|
|
46
|
+
headers: Record<string, string>;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* AWS Signature Version 4.
|
|
50
|
+
*
|
|
51
|
+
* `at` is a parameter rather than read from the clock so the test suite can
|
|
52
|
+
* check the signature against AWS's own published example vectors — the only
|
|
53
|
+
* way to know the implementation is correct rather than merely self-consistent.
|
|
54
|
+
*/
|
|
55
|
+
export declare function signRequest(args: {
|
|
56
|
+
method: string;
|
|
57
|
+
url: URL;
|
|
58
|
+
region: string;
|
|
59
|
+
service?: string;
|
|
60
|
+
credentials: S3Credentials;
|
|
61
|
+
payloadHash: string;
|
|
62
|
+
headers?: Record<string, string>;
|
|
63
|
+
at: Date;
|
|
64
|
+
}): Promise<SignedRequest>;
|
|
65
|
+
export interface PutObjectArgs {
|
|
66
|
+
key: string;
|
|
67
|
+
body: Uint8Array | string;
|
|
68
|
+
contentType?: string;
|
|
69
|
+
/** Written as `x-amz-meta-*`. Never a place for anything secret — see below. */
|
|
70
|
+
metadata?: Record<string, string>;
|
|
71
|
+
}
|
|
72
|
+
export interface ObjectSummary {
|
|
73
|
+
key: string;
|
|
74
|
+
size: number;
|
|
75
|
+
etag?: string;
|
|
76
|
+
lastModified?: string;
|
|
77
|
+
}
|
|
78
|
+
export declare function createS3Client(config: S3Config, credentials: S3Credentials): {
|
|
79
|
+
putObject(args: PutObjectArgs): Promise<{
|
|
80
|
+
etag?: string;
|
|
81
|
+
}>;
|
|
82
|
+
getObject(key: string): Promise<Uint8Array>;
|
|
83
|
+
headObject(key: string): Promise<ObjectSummary | null>;
|
|
84
|
+
deleteObject(key: string): Promise<void>;
|
|
85
|
+
/**
|
|
86
|
+
* List, following continuation tokens.
|
|
87
|
+
*
|
|
88
|
+
* S3 caps a response at 1000 keys and says so only by returning a token.
|
|
89
|
+
* A client that ignores it silently lists the first thousand — which is
|
|
90
|
+
* correct in every test and wrong in every production bucket.
|
|
91
|
+
*/
|
|
92
|
+
listObjects(prefix?: string, limit?: number): Promise<ObjectSummary[]>;
|
|
93
|
+
/**
|
|
94
|
+
* A URL that works without our credentials, for a bounded time.
|
|
95
|
+
*
|
|
96
|
+
* The signature is in the query string, so anyone holding the URL can use
|
|
97
|
+
* it — it is a bearer token that survives a browser history, a referrer
|
|
98
|
+
* header and a support screenshot. Expiries are therefore short by default
|
|
99
|
+
* and the caller has to ask for longer.
|
|
100
|
+
*/
|
|
101
|
+
presign(args: {
|
|
102
|
+
key: string;
|
|
103
|
+
expiresInSec?: number;
|
|
104
|
+
method?: "GET" | "PUT";
|
|
105
|
+
}): Promise<string>;
|
|
106
|
+
bucket: string;
|
|
107
|
+
endpoint: string;
|
|
108
|
+
};
|
|
109
|
+
export type S3Client = ReturnType<typeof createS3Client>;
|
|
110
|
+
export declare function parseListXml(xml: string): ObjectSummary[];
|
|
111
|
+
export type StorageRequest = {
|
|
112
|
+
op: 'put';
|
|
113
|
+
key: string;
|
|
114
|
+
body: Uint8Array | string;
|
|
115
|
+
contentType?: string;
|
|
116
|
+
metadata?: Record<string, string>;
|
|
117
|
+
} | {
|
|
118
|
+
op: 'get';
|
|
119
|
+
key: string;
|
|
120
|
+
} | {
|
|
121
|
+
op: 'head';
|
|
122
|
+
key: string;
|
|
123
|
+
} | {
|
|
124
|
+
op: 'delete';
|
|
125
|
+
key: string;
|
|
126
|
+
} | {
|
|
127
|
+
op: 'list';
|
|
128
|
+
prefix?: string;
|
|
129
|
+
limit?: number;
|
|
130
|
+
} | {
|
|
131
|
+
op: 'presign';
|
|
132
|
+
key: string;
|
|
133
|
+
expiresInSec?: number;
|
|
134
|
+
method?: 'GET' | 'PUT';
|
|
135
|
+
};
|
|
136
|
+
export declare const s3: import("./index").ProviderSpec<StorageRequest, unknown>;
|
|
137
|
+
export declare const storageProviders: readonly [import("./index").ProviderSpec<StorageRequest, unknown>];
|