@knowledgefutures/sdk 0.1.0 → 0.2.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/README.md +47 -0
- package/dist/whistle/hono.d.ts +42 -0
- package/dist/whistle/hono.js +89 -0
- package/dist/whistle/index.d.ts +135 -0
- package/dist/whistle/index.js +249 -0
- package/dist/whistle/wire.d.ts +64 -0
- package/dist/whistle/wire.js +98 -0
- package/package.json +5 -1
- package/dist/analytics/beacon.d.ts +0 -25
- package/dist/analytics/beacon.js +0 -86
- package/dist/analytics/binding.d.ts +0 -24
- package/dist/analytics/binding.js +0 -46
- package/dist/analytics/collector.d.ts +0 -10
- package/dist/analytics/collector.js +0 -10
- package/dist/analytics/cubes.d.ts +0 -96
- package/dist/analytics/cubes.js +0 -213
- package/dist/analytics/envelope.d.ts +0 -135
- package/dist/analytics/envelope.js +0 -231
- package/dist/analytics/errors.d.ts +0 -24
- package/dist/analytics/errors.js +0 -20
- package/dist/analytics/index.d.ts +0 -38
- package/dist/analytics/index.js +0 -98
- package/dist/analytics/memory.d.ts +0 -35
- package/dist/analytics/memory.js +0 -66
- package/dist/analytics/testing.d.ts +0 -6
- package/dist/analytics/testing.js +0 -6
- package/dist/analytics/types.d.ts +0 -22
- package/dist/analytics/types.js +0 -1
- package/dist/storage/conformance.d.ts +0 -24
- package/dist/storage/conformance.js +0 -311
- package/dist/storage/local.d.ts +0 -26
- package/dist/storage/local.js +0 -57
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import type { Cube, Query, QueryResult } from './cubes.js';
|
|
2
|
-
import type { ServerHint } from './envelope.js';
|
|
3
|
-
/** What project code holds. Every implementation — the collector, the memory double, `kf dev`'s log — is this. */
|
|
4
|
-
export interface Analytics {
|
|
5
|
-
/**
|
|
6
|
-
* A server event, signed with the project's secret, so it counts as
|
|
7
|
-
* `source = 'server'`. For what matters: a download the project's Worker
|
|
8
|
-
* served, a sign-up. Pass the reader's `country` and `userAgent` when the
|
|
9
|
-
* server knows them.
|
|
10
|
-
*/
|
|
11
|
-
track(event: ServerHint): Promise<void>;
|
|
12
|
-
/** The project's own numbers. Server-side only: the secret signs it. */
|
|
13
|
-
query<C extends Cube>(q: Query<C>): Promise<QueryResult>;
|
|
14
|
-
/**
|
|
15
|
-
* The `<script>` tag for the project's pages. `keyed` adds the public site
|
|
16
|
-
* id, for a project whose domains the console can't list. Empty under
|
|
17
|
-
* `kf dev` and in the memory double: nothing collects there.
|
|
18
|
-
*/
|
|
19
|
-
beaconTag(options?: {
|
|
20
|
-
keyed?: boolean;
|
|
21
|
-
}): string;
|
|
22
|
-
}
|
package/dist/analytics/types.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import type { Storage } from './types.js';
|
|
2
|
-
/**
|
|
3
|
-
* The storage conformance suite: what "same interface" means, as tests.
|
|
4
|
-
* Every implementation — `memoryStorage`, RustFS under `kf dev`, R2 behind the
|
|
5
|
-
* console — must pass all of it, or it is not offered. Project code written
|
|
6
|
-
* against one therefore runs the same against the others.
|
|
7
|
-
*
|
|
8
|
-
* describeStorageConformance('rustfs', async () => ({ storage, fetch }))
|
|
9
|
-
*
|
|
10
|
-
* Everything is written under a fresh `conformance/<uuid>/` prefix and
|
|
11
|
-
* deleted afterwards, so the suite is safe against a real, shared bucket.
|
|
12
|
-
*/
|
|
13
|
-
export interface ConformanceTarget {
|
|
14
|
-
storage: Storage;
|
|
15
|
-
/**
|
|
16
|
-
* How a browser would reach the URLs the store hands out (signed uploads,
|
|
17
|
-
* signed downloads, public objects). The global fetch for a real backend;
|
|
18
|
-
* `memoryStorage().fetch` for the in-memory store.
|
|
19
|
-
*/
|
|
20
|
-
fetch: (input: string, init?: RequestInit) => Promise<Response>;
|
|
21
|
-
/** false when the environment doesn't declare `public`: the public tests then check it refuses. */
|
|
22
|
-
hasPublic?: boolean;
|
|
23
|
-
}
|
|
24
|
-
export declare function describeStorageConformance(name: string, setup: () => Promise<ConformanceTarget>): void;
|
|
@@ -1,311 +0,0 @@
|
|
|
1
|
-
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
|
2
|
-
import { isStorageError } from './errors.js';
|
|
3
|
-
export function describeStorageConformance(name, setup) {
|
|
4
|
-
describe(`storage conformance: ${name}`, () => {
|
|
5
|
-
let t;
|
|
6
|
-
const run = `conformance/${crypto.randomUUID()}/`;
|
|
7
|
-
const k = (key) => `${run}${key}`;
|
|
8
|
-
beforeAll(async () => {
|
|
9
|
-
t = await setup();
|
|
10
|
-
});
|
|
11
|
-
afterAll(async () => {
|
|
12
|
-
if (!t)
|
|
13
|
-
return;
|
|
14
|
-
const stores = [t.storage];
|
|
15
|
-
if (t.hasPublic !== false)
|
|
16
|
-
stores.push(t.storage.public);
|
|
17
|
-
for (const store of stores) {
|
|
18
|
-
let cursor;
|
|
19
|
-
do {
|
|
20
|
-
const page = await store.list({ prefix: run, ...(cursor ? { cursor } : {}) });
|
|
21
|
-
for (const o of page.objects)
|
|
22
|
-
await store.delete(o.key);
|
|
23
|
-
cursor = page.cursor;
|
|
24
|
-
} while (cursor);
|
|
25
|
-
}
|
|
26
|
-
});
|
|
27
|
-
describe('objects', () => {
|
|
28
|
-
it('put then get returns the bytes, type and metadata', async () => {
|
|
29
|
-
const put = await t.storage.put(k('a.bin'), new Uint8Array([0, 1, 2, 255]), {
|
|
30
|
-
contentType: 'application/x-test',
|
|
31
|
-
metadata: { owner: 'atlas', 'page-count': '3' },
|
|
32
|
-
cacheControl: 'private, max-age=60',
|
|
33
|
-
});
|
|
34
|
-
expect(put).toMatchObject({ key: k('a.bin'), size: 4, contentType: 'application/x-test' });
|
|
35
|
-
expect(put.etag).not.toBe('');
|
|
36
|
-
const got = await t.storage.get(k('a.bin'));
|
|
37
|
-
expect(got).not.toBeNull();
|
|
38
|
-
expect([...new Uint8Array(await got.arrayBuffer())]).toEqual([0, 1, 2, 255]);
|
|
39
|
-
expect(got).toMatchObject({
|
|
40
|
-
size: 4,
|
|
41
|
-
contentType: 'application/x-test',
|
|
42
|
-
cacheControl: 'private, max-age=60',
|
|
43
|
-
metadata: { owner: 'atlas', 'page-count': '3' },
|
|
44
|
-
etag: put.etag,
|
|
45
|
-
});
|
|
46
|
-
});
|
|
47
|
-
it('head agrees with put', async () => {
|
|
48
|
-
const put = await t.storage.put(k('head.txt'), 'hello');
|
|
49
|
-
const head = await t.storage.head(k('head.txt'));
|
|
50
|
-
expect(head).toMatchObject({
|
|
51
|
-
key: k('head.txt'),
|
|
52
|
-
size: 5,
|
|
53
|
-
etag: put.etag,
|
|
54
|
-
contentType: 'text/plain; charset=utf-8',
|
|
55
|
-
});
|
|
56
|
-
expect(Math.abs(head.uploaded.getTime() - put.uploaded.getTime())).toBeLessThan(5000);
|
|
57
|
-
});
|
|
58
|
-
it('accepts a Blob, taking its type, and a stream', async () => {
|
|
59
|
-
await t.storage.put(k('blob.json'), new Blob(['{"a":1}'], { type: 'application/json' }));
|
|
60
|
-
expect((await t.storage.head(k('blob.json')))?.contentType).toBe('application/json');
|
|
61
|
-
const stream = new Blob(['streamed']).stream();
|
|
62
|
-
await t.storage.put(k('stream.txt'), stream, { contentType: 'text/plain' });
|
|
63
|
-
expect(await (await t.storage.get(k('stream.txt'))).text()).toBe('streamed');
|
|
64
|
-
});
|
|
65
|
-
it('get and head of a missing key are null; deleting it is success', async () => {
|
|
66
|
-
expect(await t.storage.get(k('missing'))).toBeNull();
|
|
67
|
-
expect(await t.storage.head(k('missing'))).toBeNull();
|
|
68
|
-
await t.storage.delete(k('missing'));
|
|
69
|
-
});
|
|
70
|
-
it('delete removes; overwrite changes the etag', async () => {
|
|
71
|
-
const first = await t.storage.put(k('over.txt'), 'one');
|
|
72
|
-
const second = await t.storage.put(k('over.txt'), 'two!');
|
|
73
|
-
expect(second.etag).not.toBe(first.etag);
|
|
74
|
-
expect(await (await t.storage.get(k('over.txt'))).text()).toBe('two!');
|
|
75
|
-
await t.storage.delete(k('over.txt'));
|
|
76
|
-
expect(await t.storage.head(k('over.txt'))).toBeNull();
|
|
77
|
-
});
|
|
78
|
-
it('keys that need encoding round-trip exactly', async () => {
|
|
79
|
-
const keys = [
|
|
80
|
-
'sp ace.txt',
|
|
81
|
-
'plus+sign',
|
|
82
|
-
'pct%20lit',
|
|
83
|
-
'q?mark#hash',
|
|
84
|
-
'ünï/cødé/文字',
|
|
85
|
-
"quote'(paren)*",
|
|
86
|
-
];
|
|
87
|
-
for (const key of keys)
|
|
88
|
-
await t.storage.put(k(`enc/${key}`), key);
|
|
89
|
-
for (const key of keys)
|
|
90
|
-
expect(await (await t.storage.get(k(`enc/${key}`))).text()).toBe(key);
|
|
91
|
-
const listed = (await t.storage.list({ prefix: k('enc/') })).objects.map((o) => o.key);
|
|
92
|
-
expect(listed.sort()).toEqual(keys.map((key) => k(`enc/${key}`)).sort());
|
|
93
|
-
});
|
|
94
|
-
});
|
|
95
|
-
describe('list', () => {
|
|
96
|
-
it('lists a prefix in key order, paged by cursor', async () => {
|
|
97
|
-
const names = ['e', 'a', 'c', 'b', 'd'];
|
|
98
|
-
for (const n of names)
|
|
99
|
-
await t.storage.put(k(`list/${n}`), n);
|
|
100
|
-
await t.storage.put(k('listing-not-this'), 'x');
|
|
101
|
-
const seen = [];
|
|
102
|
-
let cursor;
|
|
103
|
-
let pages = 0;
|
|
104
|
-
do {
|
|
105
|
-
const page = await t.storage.list({
|
|
106
|
-
prefix: k('list/'),
|
|
107
|
-
limit: 2,
|
|
108
|
-
...(cursor ? { cursor } : {}),
|
|
109
|
-
});
|
|
110
|
-
expect(page.objects.length).toBeLessThanOrEqual(2);
|
|
111
|
-
seen.push(...page.objects.map((o) => o.key));
|
|
112
|
-
cursor = page.cursor;
|
|
113
|
-
pages++;
|
|
114
|
-
} while (cursor && pages < 10);
|
|
115
|
-
expect(seen).toEqual(['a', 'b', 'c', 'd', 'e'].map((n) => k(`list/${n}`)));
|
|
116
|
-
expect(pages).toBe(3);
|
|
117
|
-
});
|
|
118
|
-
it('reports size and etag', async () => {
|
|
119
|
-
const put = await t.storage.put(k('sized/one'), '12345');
|
|
120
|
-
const [o] = (await t.storage.list({ prefix: k('sized/') })).objects;
|
|
121
|
-
expect(o).toMatchObject({ key: k('sized/one'), size: 5, etag: put.etag });
|
|
122
|
-
expect(o.uploaded).toBeInstanceOf(Date);
|
|
123
|
-
});
|
|
124
|
-
});
|
|
125
|
-
describe('refusals are the same everywhere', () => {
|
|
126
|
-
it.each([
|
|
127
|
-
['empty', ''],
|
|
128
|
-
['a leading slash', '/abs'],
|
|
129
|
-
['a .. segment', 'a/../b'],
|
|
130
|
-
['a . segment', 'a/./b'],
|
|
131
|
-
['a control character', 'a\u0001b'],
|
|
132
|
-
['more than 1024 bytes', 'x'.repeat(1025)],
|
|
133
|
-
])('a key with %s is invalid', async (_why, key) => {
|
|
134
|
-
await expect(t.storage.put(key, 'x')).rejects.toSatisfy((e) => isStorageError(e, 'invalid'));
|
|
135
|
-
await expect(t.storage.get(key)).rejects.toSatisfy((e) => isStorageError(e, 'invalid'));
|
|
136
|
-
});
|
|
137
|
-
it('every failure is a rejected promise, never a synchronous throw', async () => {
|
|
138
|
-
const calls = [
|
|
139
|
-
() => t.storage.put('', 'x'),
|
|
140
|
-
() => t.storage.upload(new Blob(['x']), { prefix: '/abs' }),
|
|
141
|
-
() => t.storage.signUpload('', { contentType: 'text/plain', size: 1 }),
|
|
142
|
-
() => t.storage.list({ limit: 0 }),
|
|
143
|
-
];
|
|
144
|
-
for (const call of calls) {
|
|
145
|
-
let promise;
|
|
146
|
-
expect(() => {
|
|
147
|
-
promise = call();
|
|
148
|
-
}).not.toThrow();
|
|
149
|
-
await expect(promise).rejects.toSatisfy((e) => isStorageError(e, 'invalid'));
|
|
150
|
-
}
|
|
151
|
-
});
|
|
152
|
-
it('a metadata name outside [a-z0-9-] is invalid', async () => {
|
|
153
|
-
await expect(t.storage.put(k('meta'), 'x', { metadata: { Bad_Name: 'x' } })).rejects.toSatisfy((e) => isStorageError(e, 'invalid'));
|
|
154
|
-
});
|
|
155
|
-
});
|
|
156
|
-
describe('upload', () => {
|
|
157
|
-
it('generates a key under the prefix, keeping the extension and type', async () => {
|
|
158
|
-
const file = new File(['cover'], 'Cover Art.JPG', { type: 'image/jpeg' });
|
|
159
|
-
const o = await t.storage.upload(file, { prefix: k('covers') });
|
|
160
|
-
expect(o.key).toMatch(new RegExp(`^${k('covers/')}[0-9a-f-]{36}\\.jpg$`));
|
|
161
|
-
expect(o.contentType).toBe('image/jpeg');
|
|
162
|
-
expect(await (await t.storage.get(o.key)).text()).toBe('cover');
|
|
163
|
-
});
|
|
164
|
-
});
|
|
165
|
-
describe('signed URLs', () => {
|
|
166
|
-
it('signUpload: a PUT with the signed headers and exact size lands', async () => {
|
|
167
|
-
const body = 'uploaded from a browser';
|
|
168
|
-
const up = await t.storage.signUpload(k('signed/ok.txt'), {
|
|
169
|
-
contentType: 'text/plain',
|
|
170
|
-
size: new TextEncoder().encode(body).length,
|
|
171
|
-
});
|
|
172
|
-
expect(up.method).toBe('PUT');
|
|
173
|
-
const res = await t.fetch(up.url, { method: 'PUT', headers: up.headers, body });
|
|
174
|
-
expect(res.status).toBeLessThan(300);
|
|
175
|
-
const got = await t.storage.get(k('signed/ok.txt'));
|
|
176
|
-
expect(await got.text()).toBe(body);
|
|
177
|
-
expect(got.contentType).toBe('text/plain');
|
|
178
|
-
});
|
|
179
|
-
it('signUpload: a different size or type is refused', async () => {
|
|
180
|
-
const up = await t.storage.signUpload(k('signed/strict.txt'), {
|
|
181
|
-
contentType: 'text/plain',
|
|
182
|
-
size: 5,
|
|
183
|
-
});
|
|
184
|
-
const tooBig = await t.fetch(up.url, { method: 'PUT', headers: up.headers, body: '123456' });
|
|
185
|
-
expect(tooBig.ok).toBe(false);
|
|
186
|
-
const wrongType = await t.fetch(up.url, {
|
|
187
|
-
method: 'PUT',
|
|
188
|
-
headers: { ...up.headers, 'content-type': 'text/html' },
|
|
189
|
-
body: '12345',
|
|
190
|
-
});
|
|
191
|
-
expect(wrongType.ok).toBe(false);
|
|
192
|
-
expect(await t.storage.head(k('signed/strict.txt'))).toBeNull();
|
|
193
|
-
});
|
|
194
|
-
it('signDownload: GET returns the object, as an attachment when named', async () => {
|
|
195
|
-
await t.storage.put(k('signed/dl.txt'), 'download me');
|
|
196
|
-
const res = await t.fetch(await t.storage.signDownload(k('signed/dl.txt')));
|
|
197
|
-
expect(res.status).toBe(200);
|
|
198
|
-
expect(await res.text()).toBe('download me');
|
|
199
|
-
const named = await t.fetch(await t.storage.signDownload(k('signed/dl.txt'), { filename: 'Réport.txt' }));
|
|
200
|
-
expect(named.headers.get('content-disposition')).toContain('attachment');
|
|
201
|
-
expect(named.headers.get('content-disposition')).toContain('R%C3%A9port.txt');
|
|
202
|
-
});
|
|
203
|
-
it('an expired signed URL is refused', async () => {
|
|
204
|
-
await t.storage.put(k('signed/expiring.txt'), 'soon gone');
|
|
205
|
-
const url = await t.storage.signDownload(k('signed/expiring.txt'), { expiresIn: 2 });
|
|
206
|
-
expect((await t.fetch(url)).ok).toBe(true);
|
|
207
|
-
// The server judges expiry by its own clock, which can drift from
|
|
208
|
-
// ours (a container's VM, say): poll for the refusal rather than
|
|
209
|
-
// assume exactly when it comes.
|
|
210
|
-
let refused = false;
|
|
211
|
-
for (let waited = 0; !refused && waited < 10_000; waited += 500) {
|
|
212
|
-
await new Promise((r) => setTimeout(r, 500));
|
|
213
|
-
refused = !(await t.fetch(url)).ok;
|
|
214
|
-
}
|
|
215
|
-
expect(refused).toBe(true);
|
|
216
|
-
}, 15_000);
|
|
217
|
-
it('expiresIn outside 1–604800 seconds is invalid', async () => {
|
|
218
|
-
await expect(t.storage.signDownload(k('x'), { expiresIn: 604_801 })).rejects.toSatisfy((e) => isStorageError(e, 'invalid'));
|
|
219
|
-
});
|
|
220
|
-
});
|
|
221
|
-
describe('public store', () => {
|
|
222
|
-
it('is refused with not_configured when the environment has none', async () => {
|
|
223
|
-
if (t.hasPublic !== false)
|
|
224
|
-
return;
|
|
225
|
-
expect(() => t.storage.public).toThrow(expect.objectContaining({ code: 'not_configured' }));
|
|
226
|
-
});
|
|
227
|
-
it('url(key) serves the object to anyone', async () => {
|
|
228
|
-
if (t.hasPublic === false)
|
|
229
|
-
return;
|
|
230
|
-
await t.storage.public.put(k('pub/hello.txt'), 'public bytes', {
|
|
231
|
-
contentType: 'text/plain',
|
|
232
|
-
});
|
|
233
|
-
const res = await t.fetch(t.storage.public.url(k('pub/hello.txt')));
|
|
234
|
-
expect(res.status).toBe(200);
|
|
235
|
-
expect(await res.text()).toBe('public bytes');
|
|
236
|
-
expect(res.headers.get('cache-control')).toBe('public, max-age=300');
|
|
237
|
-
});
|
|
238
|
-
it('upload() writes an immutable object at a fresh key', async () => {
|
|
239
|
-
if (t.hasPublic === false)
|
|
240
|
-
return;
|
|
241
|
-
const o = await t.storage.public.upload(new Blob(['img'], { type: 'image/png' }), {
|
|
242
|
-
prefix: k('pub/covers'),
|
|
243
|
-
});
|
|
244
|
-
expect(o.cacheControl).toBe('public, max-age=31536000, immutable');
|
|
245
|
-
const res = await t.fetch(t.storage.public.url(o.key));
|
|
246
|
-
expect(await res.text()).toBe('img');
|
|
247
|
-
expect(res.headers.get('cache-control')).toBe('public, max-age=31536000, immutable');
|
|
248
|
-
});
|
|
249
|
-
it('serves anything but raster images, audio and video as an attachment', async () => {
|
|
250
|
-
if (t.hasPublic === false)
|
|
251
|
-
return;
|
|
252
|
-
const page = '<html><body>not a scam</body></html>';
|
|
253
|
-
const html = await t.storage.public.put(k('pub/page.html'), page, {
|
|
254
|
-
contentType: 'text/html',
|
|
255
|
-
});
|
|
256
|
-
expect(html.contentDisposition).toMatch(/^attachment/);
|
|
257
|
-
const res = await t.fetch(t.storage.public.url(k('pub/page.html')));
|
|
258
|
-
expect(res.headers.get('content-disposition')).toMatch(/^attachment/);
|
|
259
|
-
expect(await res.text()).toBe(page);
|
|
260
|
-
// Asking for inline is not honoured for a type outside the list.
|
|
261
|
-
const svg = await t.storage.public.put(k('pub/logo.svg'), '<svg/>', {
|
|
262
|
-
contentType: 'image/svg+xml',
|
|
263
|
-
contentDisposition: 'inline',
|
|
264
|
-
});
|
|
265
|
-
expect(svg.contentDisposition).toMatch(/^attachment/);
|
|
266
|
-
// A caller's own attachment, with a file name, is kept.
|
|
267
|
-
const named = await t.storage.public.put(k('pub/r.pdf'), '%PDF', {
|
|
268
|
-
contentType: 'application/pdf',
|
|
269
|
-
contentDisposition: 'attachment; filename="report.pdf"',
|
|
270
|
-
});
|
|
271
|
-
expect(named.contentDisposition).toBe('attachment; filename="report.pdf"');
|
|
272
|
-
const png = await t.storage.public.put(k('pub/cover.png'), new Uint8Array([137, 80]), {
|
|
273
|
-
contentType: 'image/png',
|
|
274
|
-
});
|
|
275
|
-
expect(png.contentDisposition).toBeUndefined();
|
|
276
|
-
const img = await t.fetch(t.storage.public.url(k('pub/cover.png')));
|
|
277
|
-
expect(img.headers.get('content-disposition')).toBeNull();
|
|
278
|
-
});
|
|
279
|
-
it('signs the attachment into a public upload, so a browser cannot drop it', async () => {
|
|
280
|
-
if (t.hasPublic === false)
|
|
281
|
-
return;
|
|
282
|
-
const body = '<html></html>';
|
|
283
|
-
const up = await t.storage.public.signUpload(k('pub/up.html'), {
|
|
284
|
-
contentType: 'text/html',
|
|
285
|
-
size: body.length,
|
|
286
|
-
});
|
|
287
|
-
expect(up.headers['content-disposition']).toBe('attachment');
|
|
288
|
-
const { 'content-disposition': _dropped, ...without } = up.headers;
|
|
289
|
-
expect((await t.fetch(up.url, { method: 'PUT', headers: without, body })).ok).toBe(false);
|
|
290
|
-
expect((await t.fetch(up.url, { method: 'PUT', headers: up.headers, body })).ok).toBe(true);
|
|
291
|
-
expect((await t.storage.public.head(k('pub/up.html')))?.contentDisposition).toBe('attachment');
|
|
292
|
-
const img = await t.storage.public.signUpload(k('pub/up.webp'), {
|
|
293
|
-
contentType: 'image/webp',
|
|
294
|
-
size: 3,
|
|
295
|
-
});
|
|
296
|
-
expect(img.headers['content-disposition']).toBeUndefined();
|
|
297
|
-
});
|
|
298
|
-
it('leaves the private store alone', async () => {
|
|
299
|
-
const o = await t.storage.put(k('priv.html'), '<p>', { contentType: 'text/html' });
|
|
300
|
-
expect(o.contentDisposition).toBeUndefined();
|
|
301
|
-
});
|
|
302
|
-
it('the private and public stores are separate', async () => {
|
|
303
|
-
if (t.hasPublic === false)
|
|
304
|
-
return;
|
|
305
|
-
await t.storage.put(k('pub/secret.txt'), 'private');
|
|
306
|
-
expect(await t.storage.public.head(k('pub/secret.txt'))).toBeNull();
|
|
307
|
-
expect((await t.fetch(t.storage.public.url(k('pub/secret.txt')))).ok).toBe(false);
|
|
308
|
-
});
|
|
309
|
-
});
|
|
310
|
-
});
|
|
311
|
-
}
|
package/dist/storage/local.d.ts
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Setting up a LOCAL S3 server — the one `kf dev` runs, or one in a test —
|
|
3
|
-
* over the plain S3 API: create buckets, and let anyone read the public one.
|
|
4
|
-
* No vendor CLI, so it works against any server that passes the conformance
|
|
5
|
-
* suite (RustFS today). The console never uses this: deployed buckets are
|
|
6
|
-
* provisioned by kf/storage@1's adapter through the provider's own API.
|
|
7
|
-
*/
|
|
8
|
-
export interface LocalServer {
|
|
9
|
-
/** `http://localhost:<port>` */
|
|
10
|
-
endpoint: string;
|
|
11
|
-
accessKeyId: string;
|
|
12
|
-
secretAccessKey: string;
|
|
13
|
-
region?: string;
|
|
14
|
-
}
|
|
15
|
-
export interface LocalBucket {
|
|
16
|
-
name: string;
|
|
17
|
-
/** Anonymous GET on every object: the public store. */
|
|
18
|
-
publicRead?: boolean;
|
|
19
|
-
}
|
|
20
|
-
export interface EnsureOptions {
|
|
21
|
-
/** How long to wait for the server to accept connections. Default 30s. */
|
|
22
|
-
timeoutMs?: number;
|
|
23
|
-
fetch?: typeof fetch;
|
|
24
|
-
}
|
|
25
|
-
/** Idempotent: buckets that exist are left alone, and the policy is simply set again. */
|
|
26
|
-
export declare function ensureBuckets(server: LocalServer, buckets: LocalBucket[], options?: EnsureOptions): Promise<void>;
|
package/dist/storage/local.js
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import { StorageError } from './errors.js';
|
|
2
|
-
import { sha256Hex, signRequest } from './sigv4.js';
|
|
3
|
-
/** Idempotent: buckets that exist are left alone, and the policy is simply set again. */
|
|
4
|
-
export async function ensureBuckets(server, buckets, options = {}) {
|
|
5
|
-
const doFetch = options.fetch ?? ((input, init) => fetch(input, init));
|
|
6
|
-
const credentials = {
|
|
7
|
-
accessKeyId: server.accessKeyId,
|
|
8
|
-
secretAccessKey: server.secretAccessKey,
|
|
9
|
-
region: server.region ?? 'us-east-1',
|
|
10
|
-
};
|
|
11
|
-
const deadline = Date.now() + (options.timeoutMs ?? 30_000);
|
|
12
|
-
const put = async (url, body = '') => {
|
|
13
|
-
for (;;) {
|
|
14
|
-
const headers = await signRequest({
|
|
15
|
-
method: 'PUT',
|
|
16
|
-
url,
|
|
17
|
-
payloadHash: await sha256Hex(body),
|
|
18
|
-
credentials,
|
|
19
|
-
});
|
|
20
|
-
try {
|
|
21
|
-
return await doFetch(url, { method: 'PUT', headers, ...(body ? { body } : {}) });
|
|
22
|
-
}
|
|
23
|
-
catch (e) {
|
|
24
|
-
// Not listening yet: the container has started, the server hasn't.
|
|
25
|
-
if (Date.now() > deadline) {
|
|
26
|
-
throw new StorageError('unavailable', `${server.endpoint} never answered: ${e.message}`);
|
|
27
|
-
}
|
|
28
|
-
await new Promise((r) => setTimeout(r, 250));
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
};
|
|
32
|
-
for (const b of buckets) {
|
|
33
|
-
const res = await put(new URL(`${server.endpoint}/${b.name}`));
|
|
34
|
-
const text = await res.text();
|
|
35
|
-
if (!res.ok && !/BucketAlreadyOwnedByYou|BucketAlreadyExists/.test(text)) {
|
|
36
|
-
throw new StorageError('unavailable', `creating bucket ${b.name}: ${res.status} ${text.slice(0, 200)}`);
|
|
37
|
-
}
|
|
38
|
-
if (!b.publicRead)
|
|
39
|
-
continue;
|
|
40
|
-
const policy = JSON.stringify({
|
|
41
|
-
Version: '2012-10-17',
|
|
42
|
-
Statement: [
|
|
43
|
-
{
|
|
44
|
-
Effect: 'Allow',
|
|
45
|
-
Principal: { AWS: ['*'] },
|
|
46
|
-
Action: ['s3:GetObject'],
|
|
47
|
-
Resource: [`arn:aws:s3:::${b.name}/*`],
|
|
48
|
-
},
|
|
49
|
-
],
|
|
50
|
-
});
|
|
51
|
-
const set = await put(new URL(`${server.endpoint}/${b.name}?policy=`), policy);
|
|
52
|
-
if (!set.ok) {
|
|
53
|
-
throw new StorageError('unavailable', `making ${b.name} publicly readable: ${set.status} ${(await set.text()).slice(0, 200)}`);
|
|
54
|
-
}
|
|
55
|
-
await set.body?.cancel();
|
|
56
|
-
}
|
|
57
|
-
}
|