@krizaka/storage 0.0.0-stage → 0.3.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/dist/index.js ADDED
@@ -0,0 +1,161 @@
1
+ import {
2
+ COUNTRY_OF_REGION,
3
+ STORE_KIND,
4
+ countryOf,
5
+ describeStore,
6
+ encryptedStore,
7
+ parseEncryptionKey,
8
+ parseSecretKey,
9
+ residencyOf,
10
+ residencyOfConfig,
11
+ storageNotice,
12
+ where
13
+ } from "./chunk-C3LEYKGC.js";
14
+ import {
15
+ STORAGE_CODE,
16
+ StorageError,
17
+ assertKey,
18
+ bucketStore,
19
+ checkEndpoint,
20
+ defineLayout,
21
+ folderStore,
22
+ hashOf,
23
+ hashedName,
24
+ remoteStore,
25
+ trimSlashes,
26
+ typeOfName
27
+ } from "./chunk-W56662PQ.js";
28
+
29
+ // src/memory-store.ts
30
+ function memoryObjectStore(options = {}) {
31
+ const files = /* @__PURE__ */ new Map();
32
+ return {
33
+ async put(key, bytes, meta) {
34
+ files.set(assertKey(key), { bytes: new Uint8Array(bytes), contentType: meta.contentType });
35
+ },
36
+ async get(key) {
37
+ const f = files.get(assertKey(key));
38
+ return f ? { bytes: new Uint8Array(f.bytes), contentType: f.contentType || typeOfName(key) } : null;
39
+ },
40
+ async head(key) {
41
+ const f = files.get(assertKey(key));
42
+ return f ? { key, size: f.bytes.byteLength } : null;
43
+ },
44
+ async delete(key) {
45
+ files.delete(assertKey(key));
46
+ },
47
+ async list(prefix) {
48
+ return [...files.entries()].filter(([key]) => key.startsWith(prefix)).map(([key, f]) => ({ key, size: f.bytes.byteLength })).sort((a, b) => a.key.localeCompare(b.key));
49
+ },
50
+ async signedUrl() {
51
+ return null;
52
+ },
53
+ publicUrl: (key) => options.publicBaseUrl ? `${trimSlashes(options.publicBaseUrl, "end")}/${assertKey(key)}` : null
54
+ };
55
+ }
56
+
57
+ // src/sniff.ts
58
+ var startsWith = (bytes, signature, offset = 0) => signature.every((b, i) => bytes[offset + i] === b);
59
+ var ascii = (bytes, from, to) => String.fromCharCode(...bytes.subarray(from, to));
60
+ function sniffType(bytes) {
61
+ if (startsWith(bytes, [137, 80, 78, 71, 13, 10, 26, 10])) return "image/png";
62
+ if (startsWith(bytes, [255, 216, 255])) return "image/jpeg";
63
+ if (ascii(bytes, 0, 4) === "GIF8") return "image/gif";
64
+ if (ascii(bytes, 0, 4) === "RIFF" && ascii(bytes, 8, 12) === "WEBP") return "image/webp";
65
+ if (ascii(bytes, 0, 5) === "%PDF-") return "application/pdf";
66
+ if (ascii(bytes, 4, 8) === "ftyp") return "video/mp4";
67
+ if (startsWith(bytes, [26, 69, 223, 163])) return "video/webm";
68
+ const head = new TextDecoder("utf-8", { fatal: false }).decode(bytes.subarray(0, 512)).replace("\uFEFF", "").trimStart();
69
+ if (startsSvg(head)) return "image/svg+xml";
70
+ return null;
71
+ }
72
+ function startsSvg(text) {
73
+ let rest = text;
74
+ if (rest.startsWith("<?xml")) {
75
+ const end = rest.indexOf("?>");
76
+ if (end < 0) return false;
77
+ rest = rest.slice(end + 2).trimStart();
78
+ }
79
+ while (rest.startsWith("<!--")) {
80
+ const end = rest.indexOf("-->");
81
+ if (end < 0) return false;
82
+ rest = rest.slice(end + 3).trimStart();
83
+ }
84
+ return /^<svg[\s>]/i.test(rest);
85
+ }
86
+
87
+ // src/storage.ts
88
+ function createStorage(deps) {
89
+ const limits = async () => typeof deps.limits === "function" ? await deps.limits() : deps.limits;
90
+ const urlFor = async (key) => deps.store.publicUrl(key) ?? await deps.store.signedUrl(key, (await limits()).signedUrlSeconds);
91
+ return {
92
+ async upload(input) {
93
+ const { maxBytes } = await limits();
94
+ if (input.bytes.byteLength === 0 || input.bytes.byteLength > maxBytes) throw new StorageError(STORAGE_CODE.tooBig, `a file is 1 to ${maxBytes} bytes`);
95
+ const actual = sniffType(input.bytes);
96
+ if (actual === null || !input.allow.includes(actual)) throw new StorageError(STORAGE_CODE.badType, `the bytes are ${actual ?? "of an unknown type"}, not one of ${input.allow.join(", ")}`);
97
+ if (input.declaredType !== void 0 && input.declaredType !== actual) throw new StorageError(STORAGE_CODE.typeMismatch, `declared ${input.declaredType}, the bytes are ${actual}`);
98
+ const key = deps.layout.check(`${input.prefix}${hashedName(input.name, input.bytes)}`);
99
+ if (deps.layout.prefixOf(key) !== input.prefix) throw new StorageError(STORAGE_CODE.badPrefix, `\xAB ${input.prefix} \xBB is not a declared prefix`);
100
+ await deps.store.put(key, input.bytes, { contentType: actual });
101
+ return { key, contentType: actual, size: input.bytes.byteLength, url: await urlFor(key) };
102
+ },
103
+ urlFor,
104
+ get: (key) => deps.store.get(key),
105
+ head: (key) => deps.store.head(key),
106
+ delete: (key) => deps.store.delete(key),
107
+ list: (prefix) => deps.store.list(prefix)
108
+ };
109
+ }
110
+
111
+ // src/vault.ts
112
+ import { createCipheriv, createDecipheriv, randomBytes } from "crypto";
113
+ var parseVaultKey = (base64) => parseSecretKey(base64, "The vault key");
114
+ function seal(secret, key) {
115
+ const iv = randomBytes(12);
116
+ const cipher = createCipheriv("aes-256-gcm", key, iv);
117
+ const body = Buffer.concat([cipher.update(secret, "utf8"), cipher.final()]);
118
+ return `v1.${Buffer.concat([iv, cipher.getAuthTag(), body]).toString("base64url")}`;
119
+ }
120
+ function open(sealed, key) {
121
+ if (!sealed.startsWith("v1.")) throw new StorageError(STORAGE_CODE.unreadable, "Unreadable sealed value");
122
+ try {
123
+ const raw = Buffer.from(sealed.slice(3), "base64url");
124
+ const decipher = createDecipheriv("aes-256-gcm", key, raw.subarray(0, 12));
125
+ decipher.setAuthTag(raw.subarray(12, 28));
126
+ return Buffer.concat([decipher.update(raw.subarray(28)), decipher.final()]).toString("utf8");
127
+ } catch {
128
+ throw new StorageError(STORAGE_CODE.unreadable, "The sealed value failed its integrity check");
129
+ }
130
+ }
131
+ export {
132
+ COUNTRY_OF_REGION,
133
+ STORAGE_CODE,
134
+ STORE_KIND,
135
+ StorageError,
136
+ assertKey,
137
+ bucketStore,
138
+ checkEndpoint,
139
+ countryOf,
140
+ createStorage,
141
+ defineLayout,
142
+ describeStore,
143
+ encryptedStore,
144
+ folderStore,
145
+ hashOf,
146
+ hashedName,
147
+ memoryObjectStore,
148
+ open,
149
+ parseEncryptionKey,
150
+ parseVaultKey,
151
+ remoteStore,
152
+ residencyOf,
153
+ residencyOfConfig,
154
+ seal,
155
+ sniffType,
156
+ storageNotice,
157
+ trimSlashes,
158
+ typeOfName,
159
+ where
160
+ };
161
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/memory-store.ts","../src/sniff.ts","../src/storage.ts","../src/vault.ts"],"sourcesContent":["import { assertKey, trimSlashes, typeOfName } from \"./keys\";\nimport type { ObjectStore } from \"./port\";\n\n/** An in-memory store, for unit tests and demos: the behaviour of the others (the contract kit runs on all). */\nexport function memoryObjectStore(options: { publicBaseUrl?: string } = {}): ObjectStore {\n const files = new Map<string, { bytes: Uint8Array; contentType: string }>();\n return {\n async put(key, bytes, meta) {\n files.set(assertKey(key), { bytes: new Uint8Array(bytes), contentType: meta.contentType });\n },\n async get(key) {\n const f = files.get(assertKey(key));\n return f ? { bytes: new Uint8Array(f.bytes), contentType: f.contentType || typeOfName(key) } : null;\n },\n async head(key) {\n const f = files.get(assertKey(key));\n return f ? { key, size: f.bytes.byteLength } : null;\n },\n async delete(key) {\n files.delete(assertKey(key));\n },\n async list(prefix) {\n return [...files.entries()]\n .filter(([key]) => key.startsWith(prefix))\n .map(([key, f]) => ({ key, size: f.bytes.byteLength }))\n .sort((a, b) => a.key.localeCompare(b.key));\n },\n async signedUrl() {\n return null;\n },\n publicUrl: (key) => (options.publicBaseUrl ? `${trimSlashes(options.publicBaseUrl, \"end\")}/${assertKey(key)}` : null),\n };\n}\n","const startsWith = (bytes: Uint8Array, signature: readonly number[], offset = 0) => signature.every((b, i) => bytes[offset + i] === b);\nconst ascii = (bytes: Uint8Array, from: number, to: number) => String.fromCharCode(...bytes.subarray(from, to));\n\n/**\n * The content type the BYTES say they are, by magic number, never by the file name: png, jpeg, gif, webp, pdf, mp4, webm and svg.\n * `null` when the bytes are none of these. An svg is text that opens with `<svg` (or an xml declaration followed by it).\n */\nexport function sniffType(bytes: Uint8Array): string | null {\n if (startsWith(bytes, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])) return \"image/png\";\n if (startsWith(bytes, [0xff, 0xd8, 0xff])) return \"image/jpeg\";\n if (ascii(bytes, 0, 4) === \"GIF8\") return \"image/gif\";\n if (ascii(bytes, 0, 4) === \"RIFF\" && ascii(bytes, 8, 12) === \"WEBP\") return \"image/webp\";\n if (ascii(bytes, 0, 5) === \"%PDF-\") return \"application/pdf\";\n if (ascii(bytes, 4, 8) === \"ftyp\") return \"video/mp4\";\n if (startsWith(bytes, [0x1a, 0x45, 0xdf, 0xa3])) return \"video/webm\";\n const head = new TextDecoder(\"utf-8\", { fatal: false }).decode(bytes.subarray(0, 512)).replace(\"\\uFEFF\", \"\").trimStart();\n if (startsSvg(head)) return \"image/svg+xml\";\n return null;\n}\n\n/** Whether text opens as an SVG document: an optional XML declaration and comments, then `<svg`. Read with indexOf, never a backtracking pattern. */\nfunction startsSvg(text: string): boolean {\n let rest = text;\n if (rest.startsWith(\"<?xml\")) {\n const end = rest.indexOf(\"?>\");\n if (end < 0) return false;\n rest = rest.slice(end + 2).trimStart();\n }\n while (rest.startsWith(\"<!--\")) {\n const end = rest.indexOf(\"-->\");\n if (end < 0) return false;\n rest = rest.slice(end + 3).trimStart();\n }\n return /^<svg[\\s>]/i.test(rest);\n}\n","import { hashedName, type Layout } from \"./keys\";\nimport { type ObjectStore, STORAGE_CODE, StorageError } from \"./port\";\nimport { sniffType } from \"./sniff\";\n\nexport interface StorageLimits {\n /** The largest file accepted, in bytes. */\n maxBytes: number;\n /** Seconds a signed link lives. */\n signedUrlSeconds: number;\n}\n\nexport interface StorageDeps {\n store: ObjectStore;\n layout: Layout;\n /** Read at each call so the admin's change applies at once. */\n limits: StorageLimits | (() => StorageLimits | Promise<StorageLimits>);\n}\n\nexport interface UploadInput {\n /** The layout prefix the file goes under (`site/home/`). */\n prefix: string;\n /** The name the file arrives with; the stored name is `<name>.<sha10>.<ext>`. */\n name: string;\n bytes: Uint8Array;\n /** The content types this upload may be: the BYTES must be one of them, whatever the name or the declared type say. */\n allow: readonly string[];\n /** The type the client declared, when it did: it must agree with the bytes. */\n declaredType?: string;\n}\n\nexport interface Storage {\n /** Checks the size, the prefix and the bytes' magic number, then stores the file under a content-hashed name. Answers its key, and a link when one exists. */\n upload(input: UploadInput): Promise<{ key: string; contentType: string; size: number; url: string | null }>;\n /** The public link when the zone has one, else a signed link, else `null`. */\n urlFor(key: string): Promise<string | null>;\n get: ObjectStore[\"get\"];\n head: ObjectStore[\"head\"];\n delete: ObjectStore[\"delete\"];\n list: ObjectStore[\"list\"];\n}\n\n/**\n * The storage of a product: an `ObjectStore` with the rules every upload obeys (a size limit passed in, a declared layout, bytes\n * checked by magic number, a content-hashed name).\n */\nexport function createStorage(deps: StorageDeps): Storage {\n const limits = async () => (typeof deps.limits === \"function\" ? await deps.limits() : deps.limits);\n const urlFor = async (key: string) => deps.store.publicUrl(key) ?? (await deps.store.signedUrl(key, (await limits()).signedUrlSeconds));\n return {\n async upload(input) {\n const { maxBytes } = await limits();\n if (input.bytes.byteLength === 0 || input.bytes.byteLength > maxBytes) throw new StorageError(STORAGE_CODE.tooBig, `a file is 1 to ${maxBytes} bytes`);\n const actual = sniffType(input.bytes);\n if (actual === null || !input.allow.includes(actual)) throw new StorageError(STORAGE_CODE.badType, `the bytes are ${actual ?? \"of an unknown type\"}, not one of ${input.allow.join(\", \")}`);\n if (input.declaredType !== undefined && input.declaredType !== actual) throw new StorageError(STORAGE_CODE.typeMismatch, `declared ${input.declaredType}, the bytes are ${actual}`);\n const key = deps.layout.check(`${input.prefix}${hashedName(input.name, input.bytes)}`);\n if (deps.layout.prefixOf(key) !== input.prefix) throw new StorageError(STORAGE_CODE.badPrefix, `« ${input.prefix} » is not a declared prefix`);\n await deps.store.put(key, input.bytes, { contentType: actual });\n return { key, contentType: actual, size: input.bytes.byteLength, url: await urlFor(key) };\n },\n urlFor,\n get: (key) => deps.store.get(key),\n head: (key) => deps.store.head(key),\n delete: (key) => deps.store.delete(key),\n list: (prefix) => deps.store.list(prefix),\n };\n}\n","import { createCipheriv, createDecipheriv, randomBytes } from \"node:crypto\";\n\nimport { STORAGE_CODE, StorageError } from \"./port\";\nimport { parseSecretKey } from \"./secret-key\";\n\n/** The 32-byte key of the vault, from its base64 form (the deployment's, passed in). */\nexport const parseVaultKey = (base64: string): Uint8Array => parseSecretKey(base64, \"The vault key\");\n\n/**\n * Seals a credential for the database (AES-256-GCM, a fresh IV, `v1.<iv|tag|ciphertext>` in base64url). A leaked table opens\n * nothing, and a sealed value is never sent back to the browser: the admin screen shows « set » or « not set ».\n */\nexport function seal(secret: string, key: Uint8Array): string {\n const iv = randomBytes(12);\n const cipher = createCipheriv(\"aes-256-gcm\", key, iv);\n const body = Buffer.concat([cipher.update(secret, \"utf8\"), cipher.final()]);\n return `v1.${Buffer.concat([iv, cipher.getAuthTag(), body]).toString(\"base64url\")}`;\n}\n\n/** @throws StorageError `STORAGE_UNREADABLE` for a value that is not sealed, was tampered with, or sealed under another key */\nexport function open(sealed: string, key: Uint8Array): string {\n if (!sealed.startsWith(\"v1.\")) throw new StorageError(STORAGE_CODE.unreadable, \"Unreadable sealed value\");\n try {\n const raw = Buffer.from(sealed.slice(3), \"base64url\");\n const decipher = createDecipheriv(\"aes-256-gcm\", key, raw.subarray(0, 12));\n decipher.setAuthTag(raw.subarray(12, 28));\n return Buffer.concat([decipher.update(raw.subarray(28)), decipher.final()]).toString(\"utf8\");\n } catch {\n throw new StorageError(STORAGE_CODE.unreadable, \"The sealed value failed its integrity check\");\n }\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,65 @@
1
+ /**
2
+ * The ports of « storage »: files behind one interface, named by what it does. Generic: no vendor SDK (adapters speak plain HTTP
3
+ * through an injected `fetch`), no environment variable (credentials come in as parameters), no wall clock (a `Clock` signs the links).
4
+ */
5
+
6
+ /** The part of `fetch` the adapters use: inject the global one (`fetch`), or a fake in a test. */
7
+ type FetchLike = (url: string, init: {
8
+ method: string;
9
+ headers: Record<string, string>;
10
+ body?: Uint8Array;
11
+ }) => Promise<{
12
+ ok: boolean;
13
+ status: number;
14
+ headers: {
15
+ get(name: string): string | null;
16
+ };
17
+ arrayBuffer(): Promise<ArrayBuffer>;
18
+ text(): Promise<string>;
19
+ }>;
20
+ interface StoredObject {
21
+ bytes: Uint8Array;
22
+ contentType: string;
23
+ }
24
+ interface ObjectInfo {
25
+ key: string;
26
+ size: number;
27
+ }
28
+ /** One place files live. Every adapter runs `objectStoreContract` (`@krizaka/storage/testkit`). Keys are `a/b/name.ext`, checked by `assertKey`. */
29
+ interface ObjectStore {
30
+ put(key: string, bytes: Uint8Array, meta: {
31
+ contentType: string;
32
+ }): Promise<void>;
33
+ /** `null` when there is no such file. */
34
+ get(key: string): Promise<StoredObject | null>;
35
+ head(key: string): Promise<ObjectInfo | null>;
36
+ /** Idempotent: deleting a missing file is not an error. */
37
+ delete(key: string): Promise<void>;
38
+ /** Every file whose key starts with `prefix`, in key order, directories flattened. */
39
+ list(prefix: string): Promise<ObjectInfo[]>;
40
+ /** A link valid for `ttlSeconds` for a private file, or `null` when the store cannot sign one. */
41
+ signedUrl(key: string, ttlSeconds: number): Promise<string | null>;
42
+ /** The permanent public link (a pull zone, a CDN), or `null` when the zone is private. */
43
+ publicUrl(key: string): string | null;
44
+ }
45
+ /** The refusals of this package: a stable `code`, and the HTTP status a route would answer. */
46
+ declare const STORAGE_CODE: {
47
+ readonly badKey: "STORAGE_BAD_KEY";
48
+ readonly badPrefix: "STORAGE_BAD_PREFIX";
49
+ readonly tooBig: "STORAGE_TOO_BIG";
50
+ readonly badType: "STORAGE_BAD_TYPE";
51
+ readonly typeMismatch: "STORAGE_TYPE_MISMATCH";
52
+ readonly provider: "STORAGE_PROVIDER_FAILED";
53
+ readonly badConfig: "STORAGE_BAD_CONFIG";
54
+ readonly badEndpoint: "STORAGE_BAD_ENDPOINT";
55
+ readonly badSecretKey: "STORAGE_BAD_SECRET_KEY";
56
+ readonly unreadable: "STORAGE_UNREADABLE";
57
+ readonly keepingOff: "STORAGE_KEEPING_OFF";
58
+ };
59
+ type StorageCode = (typeof STORAGE_CODE)[keyof typeof STORAGE_CODE];
60
+ declare class StorageError extends Error {
61
+ readonly code: StorageCode;
62
+ constructor(code: StorageCode, message: string);
63
+ }
64
+
65
+ export { type FetchLike as F, type ObjectStore as O, STORAGE_CODE as S, type ObjectInfo as a, type StorageCode as b, StorageError as c, type StoredObject as d };
@@ -0,0 +1,74 @@
1
+ import { F as FetchLike, O as ObjectStore } from './port-6VXXuSbc.js';
2
+ import { StorageSettings } from './settings.js';
3
+ import { a as StoreDescriptor } from './where-DKtS1O5l.js';
4
+ import { Clock } from '@krizaka/db';
5
+ import '@krizaka/settings';
6
+ import './storage-DurtrQKq.js';
7
+
8
+ /**
9
+ * What a deployment's environment says about one kind of kept data (documents, avatars, exports…), read by the product's edge and
10
+ * passed in: this module reads no environment. `provider: "none"` is « keeping is off here ».
11
+ */
12
+ interface DeploymentStorage {
13
+ environment: "dev" | "prod";
14
+ provider: "none" | "http" | "s3";
15
+ prefix?: string;
16
+ /** 32 bytes in base64: when set, everything is encrypted before it leaves. */
17
+ encryptionKey?: string;
18
+ http?: {
19
+ endpoint?: string;
20
+ zone?: string;
21
+ accessKey?: string;
22
+ pullHostname?: string;
23
+ tokenKey?: string;
24
+ };
25
+ s3?: {
26
+ endpoint?: string;
27
+ region?: string;
28
+ bucket?: string;
29
+ accessKeyId?: string;
30
+ secretAccessKey?: string;
31
+ };
32
+ }
33
+ /** A signed link never lives longer than this, whatever the admin or a caller asks (a kept file is personal data). */
34
+ declare const DEFAULT_MAX_SIGNED_SECONDS = 900;
35
+ type ResolvedStorage = {
36
+ provider: "none";
37
+ } | {
38
+ provider: "http" | "s3";
39
+ settings: StorageSettings;
40
+ secrets: {
41
+ accessKeyId?: string;
42
+ secretAccessKey?: string;
43
+ accessKey?: string;
44
+ tokenKey?: string;
45
+ };
46
+ descriptor: StoreDescriptor;
47
+ };
48
+ /**
49
+ * The provider that applies to a kind of data, from the admin's settings over the deployment's environment. The same function serves
50
+ * every kind (documents, avatars): the admin's choice in `storage.*` wins for each, the environment supplies the secrets and the
51
+ * fallback when the admin left `local`.
52
+ *
53
+ * The environment can switch keeping off (`provider: "none"`), and the admin cannot override that: an admin who picks `http` or `s3`
54
+ * while the environment says `none` is refused with `STORAGE_KEEPING_OFF`, never silently ignored (the screen would say « kept »
55
+ * while nothing is) and never silently turned on (a deployment that was told not to keep files would start to). Changing the
56
+ * environment is the way to turn keeping on. Call this when saving the settings to refuse early, and again to open the store.
57
+ * @throws StorageError `STORAGE_KEEPING_OFF`
58
+ */
59
+ declare function resolveStorage(env: DeploymentStorage, admin: StorageSettings | null, options?: {
60
+ maxSignedSeconds?: number;
61
+ }): ResolvedStorage;
62
+ /**
63
+ * The store of a resolved configuration, encrypted when the environment holds a key, with the descriptor the product says. `null` when
64
+ * keeping is off. A provider without its coordinates or keys is refused (`STORAGE_BAD_CONFIG`), never replaced by another.
65
+ */
66
+ declare function openResolved(resolved: ResolvedStorage, env: DeploymentStorage, deps: {
67
+ fetch: FetchLike;
68
+ clock: Clock;
69
+ }): {
70
+ store: ObjectStore;
71
+ descriptor: StoreDescriptor;
72
+ } | null;
73
+
74
+ export { DEFAULT_MAX_SIGNED_SECONDS, type DeploymentStorage, type ResolvedStorage, openResolved, resolveStorage };
@@ -0,0 +1,52 @@
1
+ import {
2
+ describeStore,
3
+ encryptedStore,
4
+ parseEncryptionKey
5
+ } from "./chunk-C3LEYKGC.js";
6
+ import {
7
+ storeFromSettings
8
+ } from "./chunk-ZWQMALDF.js";
9
+ import {
10
+ STORAGE_CODE,
11
+ StorageError
12
+ } from "./chunk-W56662PQ.js";
13
+
14
+ // src/resolve.ts
15
+ var DEFAULT_MAX_SIGNED_SECONDS = 900;
16
+ function resolveStorage(env, admin, options = {}) {
17
+ const adminProvider = admin && admin.provider !== "local" ? admin.provider : null;
18
+ if (env.provider === "none") {
19
+ if (adminProvider) throw new StorageError(STORAGE_CODE.keepingOff, `The admin chose storage.provider ${adminProvider}, but this deployment has keeping turned off (provider none): turn it on in the environment first`);
20
+ return { provider: "none" };
21
+ }
22
+ const provider = adminProvider ?? env.provider;
23
+ const pick = (own, fallback) => own ? own : fallback ?? "";
24
+ const http = provider === "http";
25
+ const settings = {
26
+ provider,
27
+ maxMegabytes: admin?.maxMegabytes ?? 25,
28
+ // @fixed: the settings default, the admin changes it
29
+ signedUrlSeconds: Math.min(admin?.signedUrlSeconds ?? Infinity, options.maxSignedSeconds ?? DEFAULT_MAX_SIGNED_SECONDS),
30
+ endpoint: http ? pick(admin?.endpoint, env.http?.endpoint) : pick(admin?.endpoint, env.s3?.endpoint),
31
+ region: pick(admin?.region, env.s3?.region),
32
+ bucket: pick(admin?.bucket, env.s3?.bucket),
33
+ zone: pick(admin?.zone, env.http?.zone),
34
+ prefix: pick(admin?.prefix, env.prefix),
35
+ pullHostname: pick(admin?.pullHostname, env.http?.pullHostname),
36
+ publicBaseUrl: admin?.publicBaseUrl ?? ""
37
+ };
38
+ const secrets = { ...env.s3, ...env.http };
39
+ return { provider, settings, secrets: { accessKeyId: secrets.accessKeyId, secretAccessKey: secrets.secretAccessKey, accessKey: secrets.accessKey, tokenKey: secrets.tokenKey }, descriptor: describeStore({ provider, endpoint: settings.endpoint, region: settings.region }, { encrypted: !!env.encryptionKey }) };
40
+ }
41
+ function openResolved(resolved, env, deps) {
42
+ if (resolved.provider === "none") return null;
43
+ const objects = storeFromSettings(resolved.settings, resolved.secrets, { ...deps, allowLocalEndpoint: env.environment === "dev" });
44
+ const store = env.encryptionKey ? encryptedStore(objects, parseEncryptionKey(env.encryptionKey)) : objects;
45
+ return { store, descriptor: resolved.descriptor };
46
+ }
47
+ export {
48
+ DEFAULT_MAX_SIGNED_SECONDS,
49
+ openResolved,
50
+ resolveStorage
51
+ };
52
+ //# sourceMappingURL=resolve.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/resolve.ts"],"sourcesContent":["import { encryptedStore, parseEncryptionKey } from \"./encrypted-store\";\nimport { type Clock, type FetchLike, type ObjectStore, STORAGE_CODE, StorageError } from \"./port\";\nimport { type StorageSettings, storeFromSettings } from \"./settings\";\nimport { describeStore, type StoreDescriptor } from \"./where\";\n\n/**\n * What a deployment's environment says about one kind of kept data (documents, avatars, exports…), read by the product's edge and\n * passed in: this module reads no environment. `provider: \"none\"` is « keeping is off here ».\n */\nexport interface DeploymentStorage {\n environment: \"dev\" | \"prod\";\n provider: \"none\" | \"http\" | \"s3\";\n prefix?: string;\n /** 32 bytes in base64: when set, everything is encrypted before it leaves. */\n encryptionKey?: string;\n http?: { endpoint?: string; zone?: string; accessKey?: string; pullHostname?: string; tokenKey?: string };\n s3?: { endpoint?: string; region?: string; bucket?: string; accessKeyId?: string; secretAccessKey?: string };\n}\n\n/** A signed link never lives longer than this, whatever the admin or a caller asks (a kept file is personal data). */\n// @fixed: a link to personal data that outlives a reading session is a leak\nexport const DEFAULT_MAX_SIGNED_SECONDS = 900;\n\nexport type ResolvedStorage = { provider: \"none\" } | { provider: \"http\" | \"s3\"; settings: StorageSettings; secrets: { accessKeyId?: string; secretAccessKey?: string; accessKey?: string; tokenKey?: string }; descriptor: StoreDescriptor };\n\n/**\n * The provider that applies to a kind of data, from the admin's settings over the deployment's environment. The same function serves\n * every kind (documents, avatars): the admin's choice in `storage.*` wins for each, the environment supplies the secrets and the\n * fallback when the admin left `local`.\n *\n * The environment can switch keeping off (`provider: \"none\"`), and the admin cannot override that: an admin who picks `http` or `s3`\n * while the environment says `none` is refused with `STORAGE_KEEPING_OFF`, never silently ignored (the screen would say « kept »\n * while nothing is) and never silently turned on (a deployment that was told not to keep files would start to). Changing the\n * environment is the way to turn keeping on. Call this when saving the settings to refuse early, and again to open the store.\n * @throws StorageError `STORAGE_KEEPING_OFF`\n */\nexport function resolveStorage(env: DeploymentStorage, admin: StorageSettings | null, options: { maxSignedSeconds?: number } = {}): ResolvedStorage {\n const adminProvider = admin && admin.provider !== \"local\" ? admin.provider : null;\n if (env.provider === \"none\") {\n if (adminProvider) throw new StorageError(STORAGE_CODE.keepingOff, `The admin chose storage.provider ${adminProvider}, but this deployment has keeping turned off (provider none): turn it on in the environment first`);\n return { provider: \"none\" };\n }\n const provider = adminProvider ?? env.provider;\n const pick = (own: string | undefined, fallback: string | undefined) => (own ? own : (fallback ?? \"\"));\n const http = provider === \"http\";\n const settings: StorageSettings = {\n provider,\n maxMegabytes: admin?.maxMegabytes ?? 25, // @fixed: the settings default, the admin changes it\n signedUrlSeconds: Math.min(admin?.signedUrlSeconds ?? Infinity, options.maxSignedSeconds ?? DEFAULT_MAX_SIGNED_SECONDS),\n endpoint: http ? pick(admin?.endpoint, env.http?.endpoint) : pick(admin?.endpoint, env.s3?.endpoint),\n region: pick(admin?.region, env.s3?.region),\n bucket: pick(admin?.bucket, env.s3?.bucket),\n zone: pick(admin?.zone, env.http?.zone),\n prefix: pick(admin?.prefix, env.prefix),\n pullHostname: pick(admin?.pullHostname, env.http?.pullHostname),\n publicBaseUrl: admin?.publicBaseUrl ?? \"\",\n };\n const secrets = { ...env.s3, ...env.http };\n return { provider, settings, secrets: { accessKeyId: secrets.accessKeyId, secretAccessKey: secrets.secretAccessKey, accessKey: secrets.accessKey, tokenKey: secrets.tokenKey }, descriptor: describeStore({ provider, endpoint: settings.endpoint, region: settings.region }, { encrypted: !!env.encryptionKey }) };\n}\n\n/**\n * The store of a resolved configuration, encrypted when the environment holds a key, with the descriptor the product says. `null` when\n * keeping is off. A provider without its coordinates or keys is refused (`STORAGE_BAD_CONFIG`), never replaced by another.\n */\nexport function openResolved(resolved: ResolvedStorage, env: DeploymentStorage, deps: { fetch: FetchLike; clock: Clock }): { store: ObjectStore; descriptor: StoreDescriptor } | null {\n if (resolved.provider === \"none\") return null;\n const objects = storeFromSettings(resolved.settings, resolved.secrets, { ...deps, allowLocalEndpoint: env.environment === \"dev\" });\n const store = env.encryptionKey ? encryptedStore(objects, parseEncryptionKey(env.encryptionKey)) : objects;\n return { store, descriptor: resolved.descriptor };\n}\n"],"mappings":";;;;;;;;;;;;;;AAqBO,IAAM,6BAA6B;AAenC,SAAS,eAAe,KAAwB,OAA+B,UAAyC,CAAC,GAAoB;AAClJ,QAAM,gBAAgB,SAAS,MAAM,aAAa,UAAU,MAAM,WAAW;AAC7E,MAAI,IAAI,aAAa,QAAQ;AAC3B,QAAI,cAAe,OAAM,IAAI,aAAa,aAAa,YAAY,oCAAoC,aAAa,mGAAmG;AACvN,WAAO,EAAE,UAAU,OAAO;AAAA,EAC5B;AACA,QAAM,WAAW,iBAAiB,IAAI;AACtC,QAAM,OAAO,CAAC,KAAyB,aAAkC,MAAM,MAAO,YAAY;AAClG,QAAM,OAAO,aAAa;AAC1B,QAAM,WAA4B;AAAA,IAChC;AAAA,IACA,cAAc,OAAO,gBAAgB;AAAA;AAAA,IACrC,kBAAkB,KAAK,IAAI,OAAO,oBAAoB,UAAU,QAAQ,oBAAoB,0BAA0B;AAAA,IACtH,UAAU,OAAO,KAAK,OAAO,UAAU,IAAI,MAAM,QAAQ,IAAI,KAAK,OAAO,UAAU,IAAI,IAAI,QAAQ;AAAA,IACnG,QAAQ,KAAK,OAAO,QAAQ,IAAI,IAAI,MAAM;AAAA,IAC1C,QAAQ,KAAK,OAAO,QAAQ,IAAI,IAAI,MAAM;AAAA,IAC1C,MAAM,KAAK,OAAO,MAAM,IAAI,MAAM,IAAI;AAAA,IACtC,QAAQ,KAAK,OAAO,QAAQ,IAAI,MAAM;AAAA,IACtC,cAAc,KAAK,OAAO,cAAc,IAAI,MAAM,YAAY;AAAA,IAC9D,eAAe,OAAO,iBAAiB;AAAA,EACzC;AACA,QAAM,UAAU,EAAE,GAAG,IAAI,IAAI,GAAG,IAAI,KAAK;AACzC,SAAO,EAAE,UAAU,UAAU,SAAS,EAAE,aAAa,QAAQ,aAAa,iBAAiB,QAAQ,iBAAiB,WAAW,QAAQ,WAAW,UAAU,QAAQ,SAAS,GAAG,YAAY,cAAc,EAAE,UAAU,UAAU,SAAS,UAAU,QAAQ,SAAS,OAAO,GAAG,EAAE,WAAW,CAAC,CAAC,IAAI,cAAc,CAAC,EAAE;AACpT;AAMO,SAAS,aAAa,UAA2B,KAAwB,MAAsG;AACpL,MAAI,SAAS,aAAa,OAAQ,QAAO;AACzC,QAAM,UAAU,kBAAkB,SAAS,UAAU,SAAS,SAAS,EAAE,GAAG,MAAM,oBAAoB,IAAI,gBAAgB,MAAM,CAAC;AACjI,QAAM,QAAQ,IAAI,gBAAgB,eAAe,SAAS,mBAAmB,IAAI,aAAa,CAAC,IAAI;AACnG,SAAO,EAAE,OAAO,YAAY,SAAS,WAAW;AAClD;","names":[]}
@@ -0,0 +1,68 @@
1
+ import * as _krizaka_settings from '@krizaka/settings';
2
+ import { F as FetchLike, O as ObjectStore } from './port-6VXXuSbc.js';
3
+ import { b as StorageLimits } from './storage-DurtrQKq.js';
4
+ import { Clock } from '@krizaka/db';
5
+
6
+ declare const STORAGE_PROVIDERS: readonly ["local", "http", "s3"];
7
+ declare const STORAGE_DEFAULTS: {
8
+ readonly provider: "local";
9
+ readonly maxMegabytes: 25;
10
+ readonly signedUrlSeconds: 300;
11
+ };
12
+ /**
13
+ * The settings of « storage », a fragment the product adds to its registry (`@krizaka/storage/settings`): the admin picks the provider
14
+ * (`local`, `http` for a Bunny-style zone, `s3` for any S3-compatible bucket) and its non-secret coordinates, without a redeploy.
15
+ * Access keys and the token key stay in the environment and are passed to `storeFromSettings`.
16
+ */
17
+ declare const storageSettings: {
18
+ readonly "storage.provider": _krizaka_settings.Def<"local" | "http" | "s3">;
19
+ readonly "storage.maxMegabytes": _krizaka_settings.Def<number>;
20
+ readonly "storage.signedUrlSeconds": _krizaka_settings.Def<number>;
21
+ readonly "storage.endpoint": _krizaka_settings.Def<string>;
22
+ readonly "storage.region": _krizaka_settings.Def<string>;
23
+ readonly "storage.bucket": _krizaka_settings.Def<string>;
24
+ readonly "storage.zone": _krizaka_settings.Def<string>;
25
+ readonly "storage.prefix": _krizaka_settings.Def<string>;
26
+ readonly "storage.pullHostname": _krizaka_settings.Def<string>;
27
+ readonly "storage.publicBaseUrl": _krizaka_settings.Def<string>;
28
+ };
29
+ interface StorageSettings {
30
+ provider: (typeof STORAGE_PROVIDERS)[number];
31
+ maxMegabytes: number;
32
+ signedUrlSeconds: number;
33
+ endpoint: string;
34
+ region: string;
35
+ bucket: string;
36
+ zone: string;
37
+ prefix: string;
38
+ pullHostname: string;
39
+ publicBaseUrl: string;
40
+ }
41
+ type Reader = (key: keyof typeof storageSettings) => string | number | undefined | Promise<string | number | undefined>;
42
+ /** The module's settings from the product's accessor (`(key) => service.get(key, ctx)`); an unknown key falls back to the default. */
43
+ declare function readStorageSettings(read: Reader): Promise<StorageSettings>;
44
+ declare const limitsOf: (s: StorageSettings) => StorageLimits;
45
+ /** The credentials the product reads from its environment and passes in. */
46
+ interface StorageSecrets {
47
+ accessKeyId?: string;
48
+ secretAccessKey?: string;
49
+ /** The zone's password (`http`). */
50
+ accessKey?: string;
51
+ /** The pull zone's token-authentication key (`http`). */
52
+ tokenKey?: string;
53
+ /** The folder for `local`. */
54
+ root?: string;
55
+ }
56
+ /**
57
+ * The store the admin chose. A provider chosen without its coordinates or keys is refused loudly, never replaced by another: a file
58
+ * must not land somewhere nobody expects. The `http` endpoint is the host only (`storage.bunnycdn.com`); an `s3` endpoint is checked
59
+ * against our own network (`checkEndpoint`) unless `allowLocalEndpoint` (development).
60
+ * @throws StorageError `STORAGE_BAD_CONFIG` or `STORAGE_BAD_ENDPOINT`
61
+ */
62
+ declare function storeFromSettings(s: StorageSettings, secrets: StorageSecrets, deps: {
63
+ fetch: FetchLike;
64
+ clock: Clock;
65
+ allowLocalEndpoint?: boolean;
66
+ }): ObjectStore;
67
+
68
+ export { STORAGE_DEFAULTS, STORAGE_PROVIDERS, type StorageSecrets, type StorageSettings, limitsOf, readStorageSettings, storageSettings, storeFromSettings };
@@ -0,0 +1,18 @@
1
+ import {
2
+ STORAGE_DEFAULTS,
3
+ STORAGE_PROVIDERS,
4
+ limitsOf,
5
+ readStorageSettings,
6
+ storageSettings,
7
+ storeFromSettings
8
+ } from "./chunk-ZWQMALDF.js";
9
+ import "./chunk-W56662PQ.js";
10
+ export {
11
+ STORAGE_DEFAULTS,
12
+ STORAGE_PROVIDERS,
13
+ limitsOf,
14
+ readStorageSettings,
15
+ storageSettings,
16
+ storeFromSettings
17
+ };
18
+ //# sourceMappingURL=settings.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,73 @@
1
+ import { O as ObjectStore } from './port-6VXXuSbc.js';
2
+
3
+ /** Trims slashes at the ends without a regular expression (a long run of slashes must not be quadratic). */
4
+ declare function trimSlashes(value: string, ends?: "both" | "end"): string;
5
+ /**
6
+ * A key is `a/b/name.ext`: no empty segment, no `.` or `..`, no leading or trailing slash, no backslash, no control character.
7
+ * @throws StorageError `STORAGE_BAD_KEY`
8
+ */
9
+ declare function assertKey(key: string): string;
10
+ /** The first 10 hex characters of the SHA-256: the rule of the Krizaka media library (`name.<hash>.ext`). */
11
+ declare const hashOf: (bytes: Uint8Array) => string;
12
+ /** `photo.jpg` and its bytes give `photo.<sha10>.jpg`: a URL never changes content, so it is cached for good. */
13
+ declare function hashedName(name: string, bytes: Uint8Array): string;
14
+ /**
15
+ * The prefixes a zone allows, as data: an unknown prefix is refused. A layout lists prefixes WITH a trailing slash (`site/home/`);
16
+ * a key belongs to the prefix it starts with.
17
+ */
18
+ interface Layout {
19
+ prefixes: readonly string[];
20
+ /** The prefix a key belongs to, or `null`. */
21
+ prefixOf(key: string): string | null;
22
+ /** @throws StorageError `STORAGE_BAD_PREFIX` when the key is under no declared prefix. */
23
+ check(key: string): string;
24
+ }
25
+ declare function defineLayout(prefixes: readonly string[]): Layout;
26
+ /** The content type a file name suggests, for stores that do not keep one. Never a security decision: see `sniffType`. */
27
+ declare const typeOfName: (key: string) => string;
28
+
29
+ interface StorageLimits {
30
+ /** The largest file accepted, in bytes. */
31
+ maxBytes: number;
32
+ /** Seconds a signed link lives. */
33
+ signedUrlSeconds: number;
34
+ }
35
+ interface StorageDeps {
36
+ store: ObjectStore;
37
+ layout: Layout;
38
+ /** Read at each call so the admin's change applies at once. */
39
+ limits: StorageLimits | (() => StorageLimits | Promise<StorageLimits>);
40
+ }
41
+ interface UploadInput {
42
+ /** The layout prefix the file goes under (`site/home/`). */
43
+ prefix: string;
44
+ /** The name the file arrives with; the stored name is `<name>.<sha10>.<ext>`. */
45
+ name: string;
46
+ bytes: Uint8Array;
47
+ /** The content types this upload may be: the BYTES must be one of them, whatever the name or the declared type say. */
48
+ allow: readonly string[];
49
+ /** The type the client declared, when it did: it must agree with the bytes. */
50
+ declaredType?: string;
51
+ }
52
+ interface Storage {
53
+ /** Checks the size, the prefix and the bytes' magic number, then stores the file under a content-hashed name. Answers its key, and a link when one exists. */
54
+ upload(input: UploadInput): Promise<{
55
+ key: string;
56
+ contentType: string;
57
+ size: number;
58
+ url: string | null;
59
+ }>;
60
+ /** The public link when the zone has one, else a signed link, else `null`. */
61
+ urlFor(key: string): Promise<string | null>;
62
+ get: ObjectStore["get"];
63
+ head: ObjectStore["head"];
64
+ delete: ObjectStore["delete"];
65
+ list: ObjectStore["list"];
66
+ }
67
+ /**
68
+ * The storage of a product: an `ObjectStore` with the rules every upload obeys (a size limit passed in, a declared layout, bytes
69
+ * checked by magic number, a content-hashed name).
70
+ */
71
+ declare function createStorage(deps: StorageDeps): Storage;
72
+
73
+ export { type Layout as L, type Storage as S, type UploadInput as U, type StorageDeps as a, type StorageLimits as b, assertKey as c, createStorage as d, defineLayout as e, hashedName as f, typeOfName as g, hashOf as h, trimSlashes as t };
@@ -0,0 +1,6 @@
1
+ import { O as ObjectStore } from './port-6VXXuSbc.js';
2
+
3
+ /** What every `ObjectStore` adapter must do: the same behaviour behind the same port. `make` gives an empty store. */
4
+ declare function objectStoreContract(name: string, make: () => ObjectStore | Promise<ObjectStore>): void;
5
+
6
+ export { objectStoreContract };
@@ -0,0 +1,44 @@
1
+ // src/testkit.ts
2
+ import { expect, it } from "vitest";
3
+ var bytes = (...n) => new Uint8Array(n);
4
+ function objectStoreContract(name, make) {
5
+ it(`${name}: stores a file and reads it back byte for byte`, async () => {
6
+ const store = await make();
7
+ await store.put("site/home/a.b1.png", bytes(1, 2, 3, 255), { contentType: "image/png" });
8
+ const got = await store.get("site/home/a.b1.png");
9
+ expect([...got.bytes]).toEqual([1, 2, 3, 255]);
10
+ expect(got.contentType).toBe("image/png");
11
+ expect(await store.head("site/home/a.b1.png")).toEqual({ key: "site/home/a.b1.png", size: 4 });
12
+ });
13
+ it(`${name}: answers null for a missing file, and deleting it twice is fine`, async () => {
14
+ const store = await make();
15
+ expect(await store.get("missing/file.txt")).toBeNull();
16
+ expect(await store.head("missing/file.txt")).toBeNull();
17
+ await store.put("x/y.txt", bytes(1), { contentType: "text/plain" });
18
+ await store.delete("x/y.txt");
19
+ await store.delete("x/y.txt");
20
+ expect(await store.get("x/y.txt")).toBeNull();
21
+ });
22
+ it(`${name}: overwrites a key`, async () => {
23
+ const store = await make();
24
+ await store.put("k/f.bin", bytes(1), { contentType: "application/octet-stream" });
25
+ await store.put("k/f.bin", bytes(9, 9), { contentType: "application/octet-stream" });
26
+ expect([...(await store.get("k/f.bin")).bytes]).toEqual([9, 9]);
27
+ });
28
+ it(`${name}: lists the files under a prefix, directories flattened, in key order`, async () => {
29
+ const store = await make();
30
+ for (const key of ["a/one.txt", "a/b/two.txt", "a/b/c/three.txt", "other/four.txt", "a-b/five.txt"]) await store.put(key, bytes(7), { contentType: "text/plain" });
31
+ expect((await store.list("a/")).map((f) => f.key)).toEqual(["a/b/c/three.txt", "a/b/two.txt", "a/one.txt"]);
32
+ expect((await store.list("a/b/")).map((f) => f.key)).toEqual(["a/b/c/three.txt", "a/b/two.txt"]);
33
+ expect(await store.list("nothing/")).toEqual([]);
34
+ expect((await store.list("a/o")).map((f) => f.key)).toEqual(["a/one.txt"]);
35
+ });
36
+ it(`${name}: refuses a key that climbs out, is empty or has a bad segment`, async () => {
37
+ const store = await make();
38
+ for (const key of ["../x.txt", "a/../b.txt", "/abs.txt", "a//b.txt", "a/b/", "", "a\\b.txt"]) await expect(store.put(key, bytes(1), { contentType: "text/plain" })).rejects.toMatchObject({ code: "STORAGE_BAD_KEY" });
39
+ });
40
+ }
41
+ export {
42
+ objectStoreContract
43
+ };
44
+ //# sourceMappingURL=testkit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/testkit.ts"],"sourcesContent":["import { expect, it } from \"vitest\";\n\nimport type { ObjectStore } from \"./port\";\n\nconst bytes = (...n: number[]) => new Uint8Array(n);\n\n/** What every `ObjectStore` adapter must do: the same behaviour behind the same port. `make` gives an empty store. */\nexport function objectStoreContract(name: string, make: () => ObjectStore | Promise<ObjectStore>) {\n it(`${name}: stores a file and reads it back byte for byte`, async () => {\n const store = await make();\n await store.put(\"site/home/a.b1.png\", bytes(1, 2, 3, 255), { contentType: \"image/png\" });\n const got = await store.get(\"site/home/a.b1.png\");\n expect([...got!.bytes]).toEqual([1, 2, 3, 255]);\n expect(got!.contentType).toBe(\"image/png\");\n expect(await store.head(\"site/home/a.b1.png\")).toEqual({ key: \"site/home/a.b1.png\", size: 4 });\n });\n\n it(`${name}: answers null for a missing file, and deleting it twice is fine`, async () => {\n const store = await make();\n expect(await store.get(\"missing/file.txt\")).toBeNull();\n expect(await store.head(\"missing/file.txt\")).toBeNull();\n await store.put(\"x/y.txt\", bytes(1), { contentType: \"text/plain\" });\n await store.delete(\"x/y.txt\");\n await store.delete(\"x/y.txt\");\n expect(await store.get(\"x/y.txt\")).toBeNull();\n });\n\n it(`${name}: overwrites a key`, async () => {\n const store = await make();\n await store.put(\"k/f.bin\", bytes(1), { contentType: \"application/octet-stream\" });\n await store.put(\"k/f.bin\", bytes(9, 9), { contentType: \"application/octet-stream\" });\n expect([...(await store.get(\"k/f.bin\"))!.bytes]).toEqual([9, 9]);\n });\n\n it(`${name}: lists the files under a prefix, directories flattened, in key order`, async () => {\n const store = await make();\n for (const key of [\"a/one.txt\", \"a/b/two.txt\", \"a/b/c/three.txt\", \"other/four.txt\", \"a-b/five.txt\"]) await store.put(key, bytes(7), { contentType: \"text/plain\" });\n expect((await store.list(\"a/\")).map((f) => f.key)).toEqual([\"a/b/c/three.txt\", \"a/b/two.txt\", \"a/one.txt\"]);\n expect((await store.list(\"a/b/\")).map((f) => f.key)).toEqual([\"a/b/c/three.txt\", \"a/b/two.txt\"]);\n expect((await store.list(\"nothing/\"))).toEqual([]);\n expect((await store.list(\"a/o\")).map((f) => f.key)).toEqual([\"a/one.txt\"]);\n });\n\n it(`${name}: refuses a key that climbs out, is empty or has a bad segment`, async () => {\n const store = await make();\n for (const key of [\"../x.txt\", \"a/../b.txt\", \"/abs.txt\", \"a//b.txt\", \"a/b/\", \"\", \"a\\\\b.txt\"]) await expect(store.put(key, bytes(1), { contentType: \"text/plain\" })).rejects.toMatchObject({ code: \"STORAGE_BAD_KEY\" });\n });\n}\n"],"mappings":";AAAA,SAAS,QAAQ,UAAU;AAI3B,IAAM,QAAQ,IAAI,MAAgB,IAAI,WAAW,CAAC;AAG3C,SAAS,oBAAoB,MAAc,MAAgD;AAChG,KAAG,GAAG,IAAI,mDAAmD,YAAY;AACvE,UAAM,QAAQ,MAAM,KAAK;AACzB,UAAM,MAAM,IAAI,sBAAsB,MAAM,GAAG,GAAG,GAAG,GAAG,GAAG,EAAE,aAAa,YAAY,CAAC;AACvF,UAAM,MAAM,MAAM,MAAM,IAAI,oBAAoB;AAChD,WAAO,CAAC,GAAG,IAAK,KAAK,CAAC,EAAE,QAAQ,CAAC,GAAG,GAAG,GAAG,GAAG,CAAC;AAC9C,WAAO,IAAK,WAAW,EAAE,KAAK,WAAW;AACzC,WAAO,MAAM,MAAM,KAAK,oBAAoB,CAAC,EAAE,QAAQ,EAAE,KAAK,sBAAsB,MAAM,EAAE,CAAC;AAAA,EAC/F,CAAC;AAED,KAAG,GAAG,IAAI,oEAAoE,YAAY;AACxF,UAAM,QAAQ,MAAM,KAAK;AACzB,WAAO,MAAM,MAAM,IAAI,kBAAkB,CAAC,EAAE,SAAS;AACrD,WAAO,MAAM,MAAM,KAAK,kBAAkB,CAAC,EAAE,SAAS;AACtD,UAAM,MAAM,IAAI,WAAW,MAAM,CAAC,GAAG,EAAE,aAAa,aAAa,CAAC;AAClE,UAAM,MAAM,OAAO,SAAS;AAC5B,UAAM,MAAM,OAAO,SAAS;AAC5B,WAAO,MAAM,MAAM,IAAI,SAAS,CAAC,EAAE,SAAS;AAAA,EAC9C,CAAC;AAED,KAAG,GAAG,IAAI,sBAAsB,YAAY;AAC1C,UAAM,QAAQ,MAAM,KAAK;AACzB,UAAM,MAAM,IAAI,WAAW,MAAM,CAAC,GAAG,EAAE,aAAa,2BAA2B,CAAC;AAChF,UAAM,MAAM,IAAI,WAAW,MAAM,GAAG,CAAC,GAAG,EAAE,aAAa,2BAA2B,CAAC;AACnF,WAAO,CAAC,IAAI,MAAM,MAAM,IAAI,SAAS,GAAI,KAAK,CAAC,EAAE,QAAQ,CAAC,GAAG,CAAC,CAAC;AAAA,EACjE,CAAC;AAED,KAAG,GAAG,IAAI,yEAAyE,YAAY;AAC7F,UAAM,QAAQ,MAAM,KAAK;AACzB,eAAW,OAAO,CAAC,aAAa,eAAe,mBAAmB,kBAAkB,cAAc,EAAG,OAAM,MAAM,IAAI,KAAK,MAAM,CAAC,GAAG,EAAE,aAAa,aAAa,CAAC;AACjK,YAAQ,MAAM,MAAM,KAAK,IAAI,GAAG,IAAI,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,QAAQ,CAAC,mBAAmB,eAAe,WAAW,CAAC;AAC1G,YAAQ,MAAM,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,QAAQ,CAAC,mBAAmB,aAAa,CAAC;AAC/F,WAAQ,MAAM,MAAM,KAAK,UAAU,CAAE,EAAE,QAAQ,CAAC,CAAC;AACjD,YAAQ,MAAM,MAAM,KAAK,KAAK,GAAG,IAAI,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,QAAQ,CAAC,WAAW,CAAC;AAAA,EAC3E,CAAC;AAED,KAAG,GAAG,IAAI,kEAAkE,YAAY;AACtF,UAAM,QAAQ,MAAM,KAAK;AACzB,eAAW,OAAO,CAAC,YAAY,cAAc,YAAY,YAAY,QAAQ,IAAI,UAAU,EAAG,OAAM,OAAO,MAAM,IAAI,KAAK,MAAM,CAAC,GAAG,EAAE,aAAa,aAAa,CAAC,CAAC,EAAE,QAAQ,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAAA,EACvN,CAAC;AACH;","names":[]}