@krizaka/storage 0.2.0 → 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/README.md CHANGED
@@ -6,6 +6,8 @@ Files behind one port for every Krizaka product. The port is named by what it do
6
6
  - **Adapters.** `remoteStore` (an HTTP zone with the shape of Bunny Storage: `AccessKey` header, `PUT`/`GET`/`DELETE`, a directory listed by its path with a trailing slash, a pull zone with token-signed links), `bucketStore` (any S3-compatible service: Signature V4, presigned GET, `ListObjectsV2`, no SDK), `folderStore` (a folder, for development, tests and the Mac apps; a key never leaves the root), `memoryObjectStore`. All run `objectStoreContract` (`@krizaka/storage/testkit`).
7
7
  - **Rules every upload obeys** (`createStorage`): the size limit is passed in, the prefix must be declared in a `defineLayout` (an unknown prefix is refused), the bytes are checked by magic number (`sniffType`: png, jpeg, gif, webp, pdf, mp4, webm, svg), never by the extension, and a declared type that contradicts the bytes is refused. The stored name is content-hashed, `<name>.<sha10>.<ext>`, so a URL never changes content.
8
8
  - **The admin chooses** (`@krizaka/storage/settings`): `storage.provider` (`local`, `http`, `s3`), `storage.maxMegabytes`, `storage.signedUrlSeconds`, and the non-secret coordinates (`endpoint`, `region`, `bucket`, `zone`, `prefix`, `pullHostname`, `publicBaseUrl`). Keys stay in the environment; `storeFromSettings` takes them as parameters and refuses a provider chosen without what it needs, instead of falling back to another. An admin-typed S3 endpoint goes through `checkEndpoint` (https only, no credentials, no local or private address).
9
+ - **Encryption at rest, the vault, regions and `where`.** `encryptedStore(inner, key)` (AES-256-GCM over any `ObjectStore`; the key is a parameter; the provider holds ciphertext; no link is handed out), `seal`/`open` (credentials in the database, `v1.` envelope), `COUNTRY_OF_REGION` (the one region table: an unlisted region is never a country; `residencyOfConfig` says « canada » only when the region and the endpoint agree, never for a Bunny-shaped zone), `where(kind, active, { fixed, devOnly })` and `storageNotice` (the sentence key of the info panel, from the configuration).
10
+ - **Admin over environment** (`@krizaka/storage/resolve`). `resolveStorage(env, admin)` serves every kind of data (documents, avatars: pass each its own `DeploymentStorage`): the admin's `storage.*` wins, the environment gives the secrets and the fallback when the admin left `local`. If the environment says `none` and the admin turns keeping on, it throws `StorageError` `STORAGE_KEEPING_OFF` (call it when saving the setting to refuse early): keeping is turned on in the environment, never by a setting alone. `openResolved` builds the store, encrypted when a key is given.
9
11
  - **Signatures.** SigV4 is checked against AWS's published vectors. The token form of the HTTP zone's signed links (SHA-256 of key + path + expires, URL-safe base64) is the one Yomizaka and Orochia run against Bunny's pull zones; Bunny's documentation publishes no expected output for it.
10
12
 
11
13
  ```ts
@@ -0,0 +1,149 @@
1
+ import {
2
+ STORAGE_CODE,
3
+ StorageError,
4
+ assertKey
5
+ } from "./chunk-W56662PQ.js";
6
+
7
+ // src/encrypted-store.ts
8
+ import { createCipheriv, createDecipheriv, randomBytes } from "crypto";
9
+
10
+ // src/secret-key.ts
11
+ var KEY_BYTES = 32;
12
+ function parseSecretKey(base64, what) {
13
+ const key = Uint8Array.from(Buffer.from(base64, "base64"));
14
+ if (key.length !== KEY_BYTES) throw new StorageError(STORAGE_CODE.badSecretKey, `${what} must be ${KEY_BYTES} bytes in base64`);
15
+ return key;
16
+ }
17
+
18
+ // src/encrypted-store.ts
19
+ var VERSION = 1;
20
+ var IV_BYTES = 12;
21
+ var TAG_BYTES = 16;
22
+ var HEADER_BYTES = 1 + IV_BYTES + TAG_BYTES;
23
+ var parseEncryptionKey = (base64) => parseSecretKey(base64, "The encryption key");
24
+ function encryptedStore(inner, key) {
25
+ if (key.length !== 32) throw new StorageError(STORAGE_CODE.badSecretKey, "The encryption key must be 32 bytes");
26
+ const store = {
27
+ async put(k, bytes, meta) {
28
+ assertKey(k);
29
+ const iv = randomBytes(IV_BYTES);
30
+ const cipher = createCipheriv("aes-256-gcm", key, iv);
31
+ cipher.setAAD(Buffer.from(k));
32
+ const type = Buffer.from(meta.contentType);
33
+ const head = Buffer.alloc(2);
34
+ head.writeUInt16BE(type.length);
35
+ const body = Buffer.concat([cipher.update(Buffer.concat([head, type, bytes])), cipher.final()]);
36
+ await inner.put(k, Buffer.concat([Buffer.from([VERSION]), iv, cipher.getAuthTag(), body]), { contentType: "application/octet-stream" });
37
+ },
38
+ async get(k) {
39
+ assertKey(k);
40
+ const found = await inner.get(k);
41
+ if (!found) return null;
42
+ const raw = Buffer.from(found.bytes);
43
+ if (raw.length < HEADER_BYTES || raw[0] !== VERSION) throw new StorageError(STORAGE_CODE.unreadable, "Unreadable stored object");
44
+ try {
45
+ const decipher = createDecipheriv("aes-256-gcm", key, raw.subarray(1, 1 + IV_BYTES));
46
+ decipher.setAAD(Buffer.from(k));
47
+ decipher.setAuthTag(raw.subarray(1 + IV_BYTES, HEADER_BYTES));
48
+ const plain = Buffer.concat([decipher.update(raw.subarray(HEADER_BYTES)), decipher.final()]);
49
+ const typeLength = plain.readUInt16BE(0);
50
+ return { contentType: plain.subarray(2, 2 + typeLength).toString(), bytes: new Uint8Array(plain.subarray(2 + typeLength)) };
51
+ } catch {
52
+ throw new StorageError(STORAGE_CODE.unreadable, "The stored object failed its integrity check");
53
+ }
54
+ },
55
+ // The size of the plain file, read by opening it; `list` shows what the provider holds (ciphertext sizes).
56
+ async head(k) {
57
+ const found = await store.get(k);
58
+ return found ? { key: k, size: found.bytes.byteLength } : null;
59
+ },
60
+ delete: (k) => inner.delete(assertKey(k)),
61
+ list: (prefix) => inner.list(prefix),
62
+ signedUrl: async () => null,
63
+ publicUrl: () => null
64
+ };
65
+ return store;
66
+ }
67
+
68
+ // src/regions.ts
69
+ var COUNTRY_OF_REGION = {
70
+ // DigitalOcean (App Platform `tor`, managed databases and Spaces `tor1`): Toronto.
71
+ tor: "CA",
72
+ tor1: "CA",
73
+ // AWS, Oracle and Azure Canadian regions.
74
+ "ca-central-1": "CA",
75
+ "ca-west-1": "CA",
76
+ "ca-central": "CA",
77
+ "ca-east": "CA",
78
+ "ca-toronto-1": "CA",
79
+ "ca-montreal-1": "CA",
80
+ "canada-central": "CA",
81
+ canadacentral: "CA",
82
+ canadaeast: "CA",
83
+ // OVHcloud Beauharnois (Québec).
84
+ bhs: "CA",
85
+ // Bunny Storage zones (`<region>.storage.bunnycdn.com`): none is Canadian.
86
+ ny: "US",
87
+ la: "US",
88
+ de: "DE",
89
+ uk: "GB",
90
+ sg: "SG",
91
+ se: "SE",
92
+ // DigitalOcean regions a product might use later.
93
+ nyc3: "US",
94
+ sfo3: "US",
95
+ ams3: "NL",
96
+ fra1: "DE",
97
+ lon1: "GB",
98
+ sgp1: "SG",
99
+ blr1: "IN",
100
+ syd1: "AU"
101
+ };
102
+ var countryOf = (region) => COUNTRY_OF_REGION[region.trim().toLowerCase()];
103
+ var residencyOf = (region) => countryOf(region) === "CA" ? "canada" : "elsewhere";
104
+ function residencyOfConfig(config) {
105
+ if (config.provider === "http") return "elsewhere";
106
+ const host = config.endpoint.replace(/^[a-z]+:\/\//i, "").split("/")[0]?.toLowerCase() ?? "";
107
+ const labels = host.split(".").filter((label) => countryOf(label) !== void 0);
108
+ if (labels.some((label) => countryOf(label) !== "CA")) return "elsewhere";
109
+ return residencyOf(config.region);
110
+ }
111
+
112
+ // src/where.ts
113
+ var STORE_KIND = { none: "none", device: "device" };
114
+ function describeStore(config, options) {
115
+ const owner = options.owner ?? "krizaka";
116
+ if (config.provider === "http") return { kind: options.httpKind ?? "http", endpoint: config.endpoint, region: null, residency: "elsewhere", encrypted: options.encrypted, owner, devOnly: true };
117
+ return { kind: "s3", endpoint: new URL(config.endpoint).host, region: config.region, residency: residencyOfConfig(config), encrypted: options.encrypted, owner, devOnly: false };
118
+ }
119
+ function where(kind, active, options = {}) {
120
+ const fixed = options.fixed?.[kind];
121
+ if (fixed) return fixed;
122
+ if (kind === STORE_KIND.none) return { ...active, kind, endpoint: null, region: null, residency: "unknown", encrypted: false, devOnly: false };
123
+ if (kind === STORE_KIND.device) return { kind, endpoint: null, region: null, residency: "device", encrypted: false, owner: "device", devOnly: false };
124
+ if (kind === active.kind) return active;
125
+ return { kind, endpoint: null, region: null, residency: "unknown", encrypted: false, owner: "krizaka", devOnly: options.devOnly?.includes(kind) ?? false };
126
+ }
127
+ function storageNotice(d) {
128
+ if (d.kind === STORE_KIND.none) return { key: "none", vars: {} };
129
+ if (d.kind === STORE_KIND.device) return { key: "device", vars: {} };
130
+ if (d.devOnly) return { key: "devOnly", vars: {} };
131
+ if (d.owner === "workspace") return { key: "workspace", vars: { endpoint: d.endpoint ?? "", region: d.region ?? "" } };
132
+ if (d.residency === "canada") return { key: d.encrypted ? "krizakaCanadaEncrypted" : "krizakaCanada", vars: {} };
133
+ return { key: "krizakaElsewhere", vars: { region: d.region ?? "" } };
134
+ }
135
+
136
+ export {
137
+ parseSecretKey,
138
+ parseEncryptionKey,
139
+ encryptedStore,
140
+ COUNTRY_OF_REGION,
141
+ countryOf,
142
+ residencyOf,
143
+ residencyOfConfig,
144
+ STORE_KIND,
145
+ describeStore,
146
+ where,
147
+ storageNotice
148
+ };
149
+ //# sourceMappingURL=chunk-C3LEYKGC.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/encrypted-store.ts","../src/secret-key.ts","../src/regions.ts","../src/where.ts"],"sourcesContent":["import { createCipheriv, createDecipheriv, randomBytes } from \"node:crypto\";\n\nimport { assertKey } from \"./keys\";\nimport { type ObjectStore, STORAGE_CODE, StorageError } from \"./port\";\nimport { parseSecretKey } from \"./secret-key\";\n\nconst VERSION = 1;\nconst IV_BYTES = 12;\nconst TAG_BYTES = 16;\nconst HEADER_BYTES = 1 + IV_BYTES + TAG_BYTES;\n\n/** The 32-byte AES key of the encryption at rest, from its base64 form (the owner's key, passed in as a parameter). */\nexport const parseEncryptionKey = (base64: string): Uint8Array => parseSecretKey(base64, \"The encryption key\");\n\n/**\n * Encryption at rest over any `ObjectStore` (AES-256-GCM, a fresh IV per object, the object's key as authenticated data, so a file\n * moved under another name does not open). Envelope: `1 | iv(12) | tag(16) | ciphertext(contentTypeLength(2) | contentType | bytes)`.\n * The key comes in as a parameter (from the settings or the environment, read by the product's edge); the provider only ever holds\n * ciphertext. A link to ciphertext is useless to a browser, so `signedUrl` and `publicUrl` answer `null`: the application decrypts and serves.\n * @throws StorageError `STORAGE_UNREADABLE` when an object is not an envelope or fails its integrity check\n */\nexport function encryptedStore(inner: ObjectStore, key: Uint8Array): ObjectStore {\n if (key.length !== 32) throw new StorageError(STORAGE_CODE.badSecretKey, \"The encryption key must be 32 bytes\");\n const store: ObjectStore = {\n async put(k, bytes, meta) {\n assertKey(k);\n const iv = randomBytes(IV_BYTES);\n const cipher = createCipheriv(\"aes-256-gcm\", key, iv);\n cipher.setAAD(Buffer.from(k));\n const type = Buffer.from(meta.contentType);\n const head = Buffer.alloc(2);\n head.writeUInt16BE(type.length);\n const body = Buffer.concat([cipher.update(Buffer.concat([head, type, bytes])), cipher.final()]);\n await inner.put(k, Buffer.concat([Buffer.from([VERSION]), iv, cipher.getAuthTag(), body]), { contentType: \"application/octet-stream\" });\n },\n async get(k) {\n assertKey(k);\n const found = await inner.get(k);\n if (!found) return null;\n const raw = Buffer.from(found.bytes);\n if (raw.length < HEADER_BYTES || raw[0] !== VERSION) throw new StorageError(STORAGE_CODE.unreadable, \"Unreadable stored object\");\n try {\n const decipher = createDecipheriv(\"aes-256-gcm\", key, raw.subarray(1, 1 + IV_BYTES));\n decipher.setAAD(Buffer.from(k));\n decipher.setAuthTag(raw.subarray(1 + IV_BYTES, HEADER_BYTES));\n const plain = Buffer.concat([decipher.update(raw.subarray(HEADER_BYTES)), decipher.final()]);\n const typeLength = plain.readUInt16BE(0);\n return { contentType: plain.subarray(2, 2 + typeLength).toString(), bytes: new Uint8Array(plain.subarray(2 + typeLength)) };\n } catch {\n throw new StorageError(STORAGE_CODE.unreadable, \"The stored object failed its integrity check\");\n }\n },\n // The size of the plain file, read by opening it; `list` shows what the provider holds (ciphertext sizes).\n async head(k) {\n const found = await store.get(k);\n return found ? { key: k, size: found.bytes.byteLength } : null;\n },\n delete: (k) => inner.delete(assertKey(k)),\n list: (prefix) => inner.list(prefix),\n signedUrl: async () => null,\n publicUrl: () => null,\n };\n return store;\n}\n","import { STORAGE_CODE, StorageError } from \"./port\";\n\nconst KEY_BYTES = 32;\n\n/** A 32-byte key given in base64 (the form an environment variable or a secret manager holds). Anything else is refused: no weaker key is ever made up. */\nexport function parseSecretKey(base64: string, what: string): Uint8Array {\n const key = Uint8Array.from(Buffer.from(base64, \"base64\"));\n if (key.length !== KEY_BYTES) throw new StorageError(STORAGE_CODE.badSecretKey, `${what} must be ${KEY_BYTES} bytes in base64`);\n return key;\n}\n","/**\n * The country of each provider region (data: a new region is a line here, one table for every product, ADR-010). A region not listed\n * is unknown: nothing claims a country for it, and the generated residency text never says « Canada » for it.\n */\nexport const COUNTRY_OF_REGION: Readonly<Record<string, string>> = {\n // DigitalOcean (App Platform `tor`, managed databases and Spaces `tor1`): Toronto.\n tor: \"CA\",\n tor1: \"CA\",\n // AWS, Oracle and Azure Canadian regions.\n \"ca-central-1\": \"CA\",\n \"ca-west-1\": \"CA\",\n \"ca-central\": \"CA\",\n \"ca-east\": \"CA\",\n \"ca-toronto-1\": \"CA\",\n \"ca-montreal-1\": \"CA\",\n \"canada-central\": \"CA\",\n canadacentral: \"CA\",\n canadaeast: \"CA\",\n // OVHcloud Beauharnois (Québec).\n bhs: \"CA\",\n // Bunny Storage zones (`<region>.storage.bunnycdn.com`): none is Canadian.\n ny: \"US\",\n la: \"US\",\n de: \"DE\",\n uk: \"GB\",\n sg: \"SG\",\n se: \"SE\",\n // DigitalOcean regions a product might use later.\n nyc3: \"US\",\n sfo3: \"US\",\n ams3: \"NL\",\n fra1: \"DE\",\n lon1: \"GB\",\n sgp1: \"SG\",\n blr1: \"IN\",\n syd1: \"AU\",\n};\n\nexport type Residency = \"canada\" | \"elsewhere\";\n\n/** The ISO country of a provider region, or undefined when the region is not mapped. */\nexport const countryOf = (region: string): string | undefined => COUNTRY_OF_REGION[region.trim().toLowerCase()];\n\n/** Canada only when the table says so; an unmapped region is « elsewhere », never a guess in Canada's favour. */\nexport const residencyOf = (region: string): Residency => (countryOf(region) === \"CA\" ? \"canada\" : \"elsewhere\");\n\n/**\n * Residency of a configured store: the region AND the endpoint have to agree. A host that names a known region\n * (`nyc3.digitaloceanspaces.com`, `ny.storage.bunnycdn.com`) in another country than the region setting is the contradiction that\n * would let a text claim Canada falsely, so any known region label of the host that is not Canadian makes the answer « elsewhere ».\n * A Bunny-shaped zone (`http`) has no Canadian region, so it is never « canada ».\n */\nexport function residencyOfConfig(config: { provider: \"http\" | \"s3\"; endpoint: string; region: string }): Residency {\n if (config.provider === \"http\") return \"elsewhere\";\n const host = config.endpoint.replace(/^[a-z]+:\\/\\//i, \"\").split(\"/\")[0]?.toLowerCase() ?? \"\";\n const labels = host.split(\".\").filter((label) => countryOf(label) !== undefined);\n if (labels.some((label) => countryOf(label) !== \"CA\")) return \"elsewhere\";\n return residencyOf(config.region);\n}\n","import { type Residency, residencyOfConfig } from \"./regions\";\n\n/** The two kinds every product shares; the others are the product's. */\nexport const STORE_KIND = { none: \"none\", device: \"device\" } as const;\n\n/** Whose store it is: Krizaka's (the deployment's), the workspace's own, or the person's device. */\nexport type StoreOwner = \"krizaka\" | \"workspace\" | \"device\";\n\n/** Where objects live, as the product says it: computed from the configuration, never typed. */\nexport interface StoreDescriptor {\n /** The product's name for the store (`s3`, `bunny`, `database`, `none`, `device`…). */\n kind: string;\n endpoint: string | null;\n region: string | null;\n residency: Residency | \"unknown\" | \"device\";\n encrypted: boolean;\n owner: StoreOwner;\n /** For fictional and development data only: the notice says so instead of claiming a place. */\n devOnly: boolean;\n}\n\n/** The descriptor of a configured store, from its configuration alone (see `residencyOfConfig`: a Bunny-shaped zone is dev only and never Canadian). */\nexport function describeStore(config: { provider: \"http\" | \"s3\"; endpoint: string; region: string }, options: { encrypted: boolean; owner?: StoreOwner; httpKind?: string }): StoreDescriptor {\n const owner = options.owner ?? \"krizaka\";\n if (config.provider === \"http\") return { kind: options.httpKind ?? \"http\", endpoint: config.endpoint, region: null, residency: \"elsewhere\", encrypted: options.encrypted, owner, devOnly: true };\n return { kind: \"s3\", endpoint: new URL(config.endpoint).host, region: config.region, residency: residencyOfConfig(config), encrypted: options.encrypted, owner, devOnly: false };\n}\n\nexport interface WhereOptions {\n /** Stores that are not configurable but fixed places (Yomizaka's managed database in Toronto), by kind. */\n fixed?: Readonly<Record<string, StoreDescriptor>>;\n /** Kinds that exist only for development data. */\n devOnly?: readonly string[];\n}\n\n/**\n * Where an object lives, from the store kind recorded with it and the store configured now. True at every moment, per object:\n * a fixed place says what it is; `none` and `device` say so; the active store answers for its own kind; any other kind (a store\n * no longer configured) says only its kind, with an unknown residency, so nothing is claimed that nothing proves.\n */\nexport function where(kind: string, active: StoreDescriptor, options: WhereOptions = {}): StoreDescriptor {\n const fixed = options.fixed?.[kind];\n if (fixed) return fixed;\n if (kind === STORE_KIND.none) return { ...active, kind, endpoint: null, region: null, residency: \"unknown\", encrypted: false, devOnly: false };\n if (kind === STORE_KIND.device) return { kind, endpoint: null, region: null, residency: \"device\", encrypted: false, owner: \"device\", devOnly: false };\n if (kind === active.kind) return active;\n return { kind, endpoint: null, region: null, residency: \"unknown\", encrypted: false, owner: \"krizaka\", devOnly: options.devOnly?.includes(kind) ?? false };\n}\n\nexport type NoticeKey = \"none\" | \"krizakaCanadaEncrypted\" | \"krizakaCanada\" | \"krizakaElsewhere\" | \"workspace\" | \"devOnly\" | \"device\";\n\n/** Which sentence says where this object lives (« Not kept », « Kept: Krizaka (Canada, encrypted) »…), with its variables; the product's messages hold the words. */\nexport function storageNotice(d: StoreDescriptor): { key: NoticeKey; vars: Record<string, string> } {\n if (d.kind === STORE_KIND.none) return { key: \"none\", vars: {} };\n if (d.kind === STORE_KIND.device) return { key: \"device\", vars: {} };\n if (d.devOnly) return { key: \"devOnly\", vars: {} };\n if (d.owner === \"workspace\") return { key: \"workspace\", vars: { endpoint: d.endpoint ?? \"\", region: d.region ?? \"\" } };\n if (d.residency === \"canada\") return { key: d.encrypted ? \"krizakaCanadaEncrypted\" : \"krizakaCanada\", vars: {} };\n return { key: \"krizakaElsewhere\", vars: { region: d.region ?? \"\" } };\n}\n"],"mappings":";;;;;;;AAAA,SAAS,gBAAgB,kBAAkB,mBAAmB;;;ACE9D,IAAM,YAAY;AAGX,SAAS,eAAe,QAAgB,MAA0B;AACvE,QAAM,MAAM,WAAW,KAAK,OAAO,KAAK,QAAQ,QAAQ,CAAC;AACzD,MAAI,IAAI,WAAW,UAAW,OAAM,IAAI,aAAa,aAAa,cAAc,GAAG,IAAI,YAAY,SAAS,kBAAkB;AAC9H,SAAO;AACT;;;ADHA,IAAM,UAAU;AAChB,IAAM,WAAW;AACjB,IAAM,YAAY;AAClB,IAAM,eAAe,IAAI,WAAW;AAG7B,IAAM,qBAAqB,CAAC,WAA+B,eAAe,QAAQ,oBAAoB;AAStG,SAAS,eAAe,OAAoB,KAA8B;AAC/E,MAAI,IAAI,WAAW,GAAI,OAAM,IAAI,aAAa,aAAa,cAAc,qCAAqC;AAC9G,QAAM,QAAqB;AAAA,IACzB,MAAM,IAAI,GAAG,OAAO,MAAM;AACxB,gBAAU,CAAC;AACX,YAAM,KAAK,YAAY,QAAQ;AAC/B,YAAM,SAAS,eAAe,eAAe,KAAK,EAAE;AACpD,aAAO,OAAO,OAAO,KAAK,CAAC,CAAC;AAC5B,YAAM,OAAO,OAAO,KAAK,KAAK,WAAW;AACzC,YAAM,OAAO,OAAO,MAAM,CAAC;AAC3B,WAAK,cAAc,KAAK,MAAM;AAC9B,YAAM,OAAO,OAAO,OAAO,CAAC,OAAO,OAAO,OAAO,OAAO,CAAC,MAAM,MAAM,KAAK,CAAC,CAAC,GAAG,OAAO,MAAM,CAAC,CAAC;AAC9F,YAAM,MAAM,IAAI,GAAG,OAAO,OAAO,CAAC,OAAO,KAAK,CAAC,OAAO,CAAC,GAAG,IAAI,OAAO,WAAW,GAAG,IAAI,CAAC,GAAG,EAAE,aAAa,2BAA2B,CAAC;AAAA,IACxI;AAAA,IACA,MAAM,IAAI,GAAG;AACX,gBAAU,CAAC;AACX,YAAM,QAAQ,MAAM,MAAM,IAAI,CAAC;AAC/B,UAAI,CAAC,MAAO,QAAO;AACnB,YAAM,MAAM,OAAO,KAAK,MAAM,KAAK;AACnC,UAAI,IAAI,SAAS,gBAAgB,IAAI,CAAC,MAAM,QAAS,OAAM,IAAI,aAAa,aAAa,YAAY,0BAA0B;AAC/H,UAAI;AACF,cAAM,WAAW,iBAAiB,eAAe,KAAK,IAAI,SAAS,GAAG,IAAI,QAAQ,CAAC;AACnF,iBAAS,OAAO,OAAO,KAAK,CAAC,CAAC;AAC9B,iBAAS,WAAW,IAAI,SAAS,IAAI,UAAU,YAAY,CAAC;AAC5D,cAAM,QAAQ,OAAO,OAAO,CAAC,SAAS,OAAO,IAAI,SAAS,YAAY,CAAC,GAAG,SAAS,MAAM,CAAC,CAAC;AAC3F,cAAM,aAAa,MAAM,aAAa,CAAC;AACvC,eAAO,EAAE,aAAa,MAAM,SAAS,GAAG,IAAI,UAAU,EAAE,SAAS,GAAG,OAAO,IAAI,WAAW,MAAM,SAAS,IAAI,UAAU,CAAC,EAAE;AAAA,MAC5H,QAAQ;AACN,cAAM,IAAI,aAAa,aAAa,YAAY,8CAA8C;AAAA,MAChG;AAAA,IACF;AAAA;AAAA,IAEA,MAAM,KAAK,GAAG;AACZ,YAAM,QAAQ,MAAM,MAAM,IAAI,CAAC;AAC/B,aAAO,QAAQ,EAAE,KAAK,GAAG,MAAM,MAAM,MAAM,WAAW,IAAI;AAAA,IAC5D;AAAA,IACA,QAAQ,CAAC,MAAM,MAAM,OAAO,UAAU,CAAC,CAAC;AAAA,IACxC,MAAM,CAAC,WAAW,MAAM,KAAK,MAAM;AAAA,IACnC,WAAW,YAAY;AAAA,IACvB,WAAW,MAAM;AAAA,EACnB;AACA,SAAO;AACT;;;AE3DO,IAAM,oBAAsD;AAAA;AAAA,EAEjE,KAAK;AAAA,EACL,MAAM;AAAA;AAAA,EAEN,gBAAgB;AAAA,EAChB,aAAa;AAAA,EACb,cAAc;AAAA,EACd,WAAW;AAAA,EACX,gBAAgB;AAAA,EAChB,iBAAiB;AAAA,EACjB,kBAAkB;AAAA,EAClB,eAAe;AAAA,EACf,YAAY;AAAA;AAAA,EAEZ,KAAK;AAAA;AAAA,EAEL,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA;AAAA,EAEJ,MAAM;AAAA,EACN,MAAM;AAAA,EACN,MAAM;AAAA,EACN,MAAM;AAAA,EACN,MAAM;AAAA,EACN,MAAM;AAAA,EACN,MAAM;AAAA,EACN,MAAM;AACR;AAKO,IAAM,YAAY,CAAC,WAAuC,kBAAkB,OAAO,KAAK,EAAE,YAAY,CAAC;AAGvG,IAAM,cAAc,CAAC,WAA+B,UAAU,MAAM,MAAM,OAAO,WAAW;AAQ5F,SAAS,kBAAkB,QAAkF;AAClH,MAAI,OAAO,aAAa,OAAQ,QAAO;AACvC,QAAM,OAAO,OAAO,SAAS,QAAQ,iBAAiB,EAAE,EAAE,MAAM,GAAG,EAAE,CAAC,GAAG,YAAY,KAAK;AAC1F,QAAM,SAAS,KAAK,MAAM,GAAG,EAAE,OAAO,CAAC,UAAU,UAAU,KAAK,MAAM,MAAS;AAC/E,MAAI,OAAO,KAAK,CAAC,UAAU,UAAU,KAAK,MAAM,IAAI,EAAG,QAAO;AAC9D,SAAO,YAAY,OAAO,MAAM;AAClC;;;ACvDO,IAAM,aAAa,EAAE,MAAM,QAAQ,QAAQ,SAAS;AAmBpD,SAAS,cAAc,QAAuE,SAAyF;AAC5L,QAAM,QAAQ,QAAQ,SAAS;AAC/B,MAAI,OAAO,aAAa,OAAQ,QAAO,EAAE,MAAM,QAAQ,YAAY,QAAQ,UAAU,OAAO,UAAU,QAAQ,MAAM,WAAW,aAAa,WAAW,QAAQ,WAAW,OAAO,SAAS,KAAK;AAC/L,SAAO,EAAE,MAAM,MAAM,UAAU,IAAI,IAAI,OAAO,QAAQ,EAAE,MAAM,QAAQ,OAAO,QAAQ,WAAW,kBAAkB,MAAM,GAAG,WAAW,QAAQ,WAAW,OAAO,SAAS,MAAM;AACjL;AAcO,SAAS,MAAM,MAAc,QAAyB,UAAwB,CAAC,GAAoB;AACxG,QAAM,QAAQ,QAAQ,QAAQ,IAAI;AAClC,MAAI,MAAO,QAAO;AAClB,MAAI,SAAS,WAAW,KAAM,QAAO,EAAE,GAAG,QAAQ,MAAM,UAAU,MAAM,QAAQ,MAAM,WAAW,WAAW,WAAW,OAAO,SAAS,MAAM;AAC7I,MAAI,SAAS,WAAW,OAAQ,QAAO,EAAE,MAAM,UAAU,MAAM,QAAQ,MAAM,WAAW,UAAU,WAAW,OAAO,OAAO,UAAU,SAAS,MAAM;AACpJ,MAAI,SAAS,OAAO,KAAM,QAAO;AACjC,SAAO,EAAE,MAAM,UAAU,MAAM,QAAQ,MAAM,WAAW,WAAW,WAAW,OAAO,OAAO,WAAW,SAAS,QAAQ,SAAS,SAAS,IAAI,KAAK,MAAM;AAC3J;AAKO,SAAS,cAAc,GAAsE;AAClG,MAAI,EAAE,SAAS,WAAW,KAAM,QAAO,EAAE,KAAK,QAAQ,MAAM,CAAC,EAAE;AAC/D,MAAI,EAAE,SAAS,WAAW,OAAQ,QAAO,EAAE,KAAK,UAAU,MAAM,CAAC,EAAE;AACnE,MAAI,EAAE,QAAS,QAAO,EAAE,KAAK,WAAW,MAAM,CAAC,EAAE;AACjD,MAAI,EAAE,UAAU,YAAa,QAAO,EAAE,KAAK,aAAa,MAAM,EAAE,UAAU,EAAE,YAAY,IAAI,QAAQ,EAAE,UAAU,GAAG,EAAE;AACrH,MAAI,EAAE,cAAc,SAAU,QAAO,EAAE,KAAK,EAAE,YAAY,2BAA2B,iBAAiB,MAAM,CAAC,EAAE;AAC/G,SAAO,EAAE,KAAK,oBAAoB,MAAM,EAAE,QAAQ,EAAE,UAAU,GAAG,EAAE;AACrE;","names":[]}
@@ -7,7 +7,10 @@ var STORAGE_CODE = {
7
7
  typeMismatch: "STORAGE_TYPE_MISMATCH",
8
8
  provider: "STORAGE_PROVIDER_FAILED",
9
9
  badConfig: "STORAGE_BAD_CONFIG",
10
- badEndpoint: "STORAGE_BAD_ENDPOINT"
10
+ badEndpoint: "STORAGE_BAD_ENDPOINT",
11
+ badSecretKey: "STORAGE_BAD_SECRET_KEY",
12
+ unreadable: "STORAGE_UNREADABLE",
13
+ keepingOff: "STORAGE_KEEPING_OFF"
11
14
  };
12
15
  var StorageError = class extends Error {
13
16
  constructor(code, message) {
@@ -340,4 +343,4 @@ export {
340
343
  folderStore,
341
344
  remoteStore
342
345
  };
343
- //# sourceMappingURL=chunk-3E2PZNBG.js.map
346
+ //# sourceMappingURL=chunk-W56662PQ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/port.ts","../src/keys.ts","../src/request-signing.ts","../src/bucket-store.ts","../src/endpoint.ts","../src/folder-store.ts","../src/remote-store.ts"],"sourcesContent":["/**\n * The ports of « storage »: files behind one interface, named by what it does. Generic: no vendor SDK (adapters speak plain HTTP\n * through an injected `fetch`), no environment variable (credentials come in as parameters), no wall clock (a `Clock` signs the links).\n */\nexport type { Clock } from \"@krizaka/db\";\n\n/** The part of `fetch` the adapters use: inject the global one (`fetch`), or a fake in a test. */\nexport type FetchLike = (\n url: string,\n init: { method: string; headers: Record<string, string>; body?: Uint8Array },\n) => Promise<{ ok: boolean; status: number; headers: { get(name: string): string | null }; arrayBuffer(): Promise<ArrayBuffer>; text(): Promise<string> }>;\n\nexport interface StoredObject {\n bytes: Uint8Array;\n contentType: string;\n}\n\nexport interface ObjectInfo {\n key: string;\n size: number;\n}\n\n/** One place files live. Every adapter runs `objectStoreContract` (`@krizaka/storage/testkit`). Keys are `a/b/name.ext`, checked by `assertKey`. */\nexport interface ObjectStore {\n put(key: string, bytes: Uint8Array, meta: { contentType: string }): Promise<void>;\n /** `null` when there is no such file. */\n get(key: string): Promise<StoredObject | null>;\n head(key: string): Promise<ObjectInfo | null>;\n /** Idempotent: deleting a missing file is not an error. */\n delete(key: string): Promise<void>;\n /** Every file whose key starts with `prefix`, in key order, directories flattened. */\n list(prefix: string): Promise<ObjectInfo[]>;\n /** A link valid for `ttlSeconds` for a private file, or `null` when the store cannot sign one. */\n signedUrl(key: string, ttlSeconds: number): Promise<string | null>;\n /** The permanent public link (a pull zone, a CDN), or `null` when the zone is private. */\n publicUrl(key: string): string | null;\n}\n\n/** The refusals of this package: a stable `code`, and the HTTP status a route would answer. */\nexport const STORAGE_CODE = {\n badKey: \"STORAGE_BAD_KEY\",\n badPrefix: \"STORAGE_BAD_PREFIX\",\n tooBig: \"STORAGE_TOO_BIG\",\n badType: \"STORAGE_BAD_TYPE\",\n typeMismatch: \"STORAGE_TYPE_MISMATCH\",\n provider: \"STORAGE_PROVIDER_FAILED\",\n badConfig: \"STORAGE_BAD_CONFIG\",\n badEndpoint: \"STORAGE_BAD_ENDPOINT\",\n badSecretKey: \"STORAGE_BAD_SECRET_KEY\",\n unreadable: \"STORAGE_UNREADABLE\",\n keepingOff: \"STORAGE_KEEPING_OFF\",\n} as const;\nexport type StorageCode = (typeof STORAGE_CODE)[keyof typeof STORAGE_CODE];\n\nexport class StorageError extends Error {\n constructor(\n readonly code: StorageCode,\n message: string,\n ) {\n super(message);\n this.name = \"StorageError\";\n }\n}\n","import { createHash } from \"node:crypto\";\n\nimport { STORAGE_CODE, StorageError } from \"./port\";\n\n// @fixed: S3's own limit on a key; a longer one is refused by every provider\nconst MAX_KEY = 1024;\nconst SEGMENT = /^[A-Za-z0-9][A-Za-z0-9._ ()@+=,~-]*$/;\n\n/** Trims slashes at the ends without a regular expression (a long run of slashes must not be quadratic). */\nexport function trimSlashes(value: string, ends: \"both\" | \"end\" = \"both\"): string {\n let start = 0;\n let end = value.length;\n if (ends === \"both\") while (start < end && value[start] === \"/\") start += 1;\n while (end > start && value[end - 1] === \"/\") end -= 1;\n return value.slice(start, end);\n}\n\n/**\n * A key is `a/b/name.ext`: no empty segment, no `.` or `..`, no leading or trailing slash, no backslash, no control character.\n * @throws StorageError `STORAGE_BAD_KEY`\n */\nexport function assertKey(key: string): string {\n const bad = (why: string) => new StorageError(STORAGE_CODE.badKey, `${why}: « ${key.slice(0, 80)} »`);\n if (typeof key !== \"string\" || key === \"\" || key.length > MAX_KEY) throw bad(\"a key is 1 to 1024 characters\");\n for (const segment of key.split(\"/\")) {\n if (segment === \".\" || segment === \"..\") throw bad(\"a key has no . or .. segment\");\n if (!SEGMENT.test(segment)) throw bad(\"a key segment is empty or has a character storage services refuse\");\n }\n return key;\n}\n\n/** The first 10 hex characters of the SHA-256: the rule of the Krizaka media library (`name.<hash>.ext`). */\nexport const hashOf = (bytes: Uint8Array): string => createHash(\"sha256\").update(bytes).digest(\"hex\").slice(0, 10);\n\n/** `photo.jpg` and its bytes give `photo.<sha10>.jpg`: a URL never changes content, so it is cached for good. */\nexport function hashedName(name: string, bytes: Uint8Array): string {\n const dot = name.lastIndexOf(\".\");\n const base = dot > 0 ? name.slice(0, dot) : name;\n const ext = dot > 0 ? name.slice(dot) : \"\";\n return `${base}.${hashOf(bytes)}${ext}`;\n}\n\n/**\n * The prefixes a zone allows, as data: an unknown prefix is refused. A layout lists prefixes WITH a trailing slash (`site/home/`);\n * a key belongs to the prefix it starts with.\n */\nexport interface Layout {\n prefixes: readonly string[];\n /** The prefix a key belongs to, or `null`. */\n prefixOf(key: string): string | null;\n /** @throws StorageError `STORAGE_BAD_PREFIX` when the key is under no declared prefix. */\n check(key: string): string;\n}\n\nexport function defineLayout(prefixes: readonly string[]): Layout {\n for (const p of prefixes) if (!p.endsWith(\"/\")) throw new StorageError(STORAGE_CODE.badPrefix, `layout prefix « ${p} » must end with a slash`);\n const sorted = [...prefixes].sort((a, b) => b.length - a.length);\n const prefixOf = (key: string) => sorted.find((p) => key.startsWith(p)) ?? null;\n return {\n prefixes,\n prefixOf,\n check(key) {\n assertKey(key);\n if (prefixOf(key) === null) throw new StorageError(STORAGE_CODE.badPrefix, `« ${key.slice(0, 80)} » is under no declared prefix`);\n return key;\n },\n };\n}\n\nconst TYPES: Record<string, string> = {\n png: \"image/png\",\n jpg: \"image/jpeg\",\n jpeg: \"image/jpeg\",\n gif: \"image/gif\",\n webp: \"image/webp\",\n svg: \"image/svg+xml\",\n pdf: \"application/pdf\",\n mp4: \"video/mp4\",\n webm: \"video/webm\",\n json: \"application/json\",\n txt: \"text/plain\",\n vtt: \"text/vtt\",\n};\n\n/** The content type a file name suggests, for stores that do not keep one. Never a security decision: see `sniffType`. */\nexport const typeOfName = (key: string): string => TYPES[key.split(\".\").pop()?.toLowerCase() ?? \"\"] ?? \"application/octet-stream\";\n","import crypto from \"node:crypto\";\n\n/** AWS Signature Version 4, the part of it any S3-compatible provider needs (header signing and presigned URLs). Dependency-free. */\nconst sha256 = (data: string | Uint8Array) => crypto.createHash(\"sha256\").update(data).digest(\"hex\");\nconst hmac = (key: crypto.BinaryLike | crypto.KeyObject, data: string) => crypto.createHmac(\"sha256\", key).update(data).digest();\nexport const EMPTY_SHA256 = sha256(\"\");\nexport const hashPayload = (bytes: Uint8Array) => sha256(bytes);\n\nexport interface Credentials {\n accessKeyId: string;\n secretAccessKey: string;\n region: string;\n service: string;\n}\n\nconst stamp = (now: Date) => now.toISOString().replace(/[:-]|\\.\\d{3}/g, \"\");\nconst encode = (s: string) => encodeURIComponent(s).replace(/[!'()*]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`);\nconst canonicalPath = (url: URL) =>\n url.pathname\n .split(\"/\")\n .map((seg) => encode(decodeURIComponent(seg)))\n .join(\"/\") || \"/\";\nconst byNameThenValue = ([a, av]: readonly [string, string], [b, bv]: readonly [string, string]) => {\n if (a === b) return av < bv ? -1 : 1;\n return a < b ? -1 : 1;\n};\nconst canonicalQuery = (url: URL) =>\n [...url.searchParams.entries()]\n .map(([k, v]) => [encode(k), encode(v)] as const)\n .sort(byNameThenValue)\n .map(([k, v]) => `${k}=${v}`)\n .join(\"&\");\n\nfunction signingKey(c: Credentials, date: string) {\n return hmac(hmac(hmac(hmac(`AWS4${c.secretAccessKey}`, date), c.region), c.service), \"aws4_request\");\n}\n\nfunction signature(c: Credentials, method: string, url: URL, headers: Record<string, string>, payloadHash: string, amzDate: string) {\n const names = Object.keys(headers)\n .map((h) => h.toLowerCase())\n .sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)); // code-unit order, as the signature requires\n const lower = Object.fromEntries(Object.entries(headers).map(([k, v]) => [k.toLowerCase(), v.trim().replace(/\\s+/g, \" \")]));\n const canonical = [method, canonicalPath(url), canonicalQuery(url), names.map((n) => `${n}:${lower[n]}\\n`).join(\"\"), names.join(\";\"), payloadHash].join(\"\\n\");\n const scope = `${amzDate.slice(0, 8)}/${c.region}/${c.service}/aws4_request`;\n const toSign = [\"AWS4-HMAC-SHA256\", amzDate, scope, sha256(canonical)].join(\"\\n\");\n return { signed: names.join(\";\"), scope, value: crypto.createHmac(\"sha256\", signingKey(c, amzDate.slice(0, 8))).update(toSign).digest(\"hex\") };\n}\n\n/** The headers of a signed request: `authorization`, `x-amz-date` and, for S3, `x-amz-content-sha256` (give the headers you send, without host). */\nexport function signRequest(c: Credentials, req: { method: string; url: string; headers?: Record<string, string>; payloadHash: string; now: Date }) {\n const url = new URL(req.url);\n const amzDate = stamp(req.now);\n const extra: Record<string, string> = c.service === \"s3\" ? { \"x-amz-content-sha256\": req.payloadHash } : {};\n const s = signature(c, req.method, url, { ...req.headers, host: url.host, \"x-amz-date\": amzDate, ...extra }, req.payloadHash, amzDate);\n return { ...req.headers, \"x-amz-date\": amzDate, ...extra, authorization: `AWS4-HMAC-SHA256 Credential=${c.accessKeyId}/${s.scope}, SignedHeaders=${s.signed}, Signature=${s.value}` };\n}\n\n/** A presigned GET URL, valid for `ttlSeconds`. */\nexport function presignGet(c: Credentials, url: string, ttlSeconds: number, now: Date): string {\n const u = new URL(url);\n const amzDate = stamp(now);\n u.searchParams.set(\"X-Amz-Algorithm\", \"AWS4-HMAC-SHA256\");\n u.searchParams.set(\"X-Amz-Credential\", `${c.accessKeyId}/${amzDate.slice(0, 8)}/${c.region}/${c.service}/aws4_request`);\n u.searchParams.set(\"X-Amz-Date\", amzDate);\n u.searchParams.set(\"X-Amz-Expires\", String(ttlSeconds));\n u.searchParams.set(\"X-Amz-SignedHeaders\", \"host\");\n const s = signature(c, \"GET\", u, { host: u.host }, \"UNSIGNED-PAYLOAD\", amzDate);\n u.searchParams.set(\"X-Amz-Signature\", s.value);\n return u.toString();\n}\n","import { assertKey, trimSlashes, typeOfName } from \"./keys\";\nimport { type Clock, type FetchLike, type ObjectInfo, type ObjectStore, STORAGE_CODE, StorageError } from \"./port\";\nimport { type Credentials, hashPayload, presignGet, signRequest } from \"./request-signing\";\n\nexport interface BucketStoreOptions {\n /** The provider's origin: `https://tor1.digitaloceanspaces.com`, `https://s3.example.com`, `http://localhost:9000`. */\n endpoint: string;\n region: string;\n bucket: string;\n accessKeyId: string;\n secretAccessKey: string;\n /** Every object lives under this prefix (no slashes at the ends), or none. */\n prefix?: string;\n /** Where the bucket is served publicly (a CDN in front of it), or none: the bucket is private. */\n publicBaseUrl?: string;\n /** Ask the provider to encrypt at rest (`AES256`). */\n serverSideEncryption?: boolean;\n fetch: FetchLike;\n clock: Clock;\n}\n\nconst fail = (what: string, status: number) => new StorageError(STORAGE_CODE.provider, `the bucket refused the ${what} (${status})`);\nconst unescapeXml = (s: string) => s.replaceAll(\"&lt;\", \"<\").replaceAll(\"&gt;\", \">\").replaceAll(\"&quot;\", '\"').replaceAll(\"&apos;\", \"'\").replaceAll(\"&amp;\", \"&\");\nconst tag = (xml: string, name: string) => new RegExp(`<${name}>([\\\\s\\\\S]*?)</${name}>`).exec(xml)?.[1];\n\n/**\n * Any S3-compatible service (Spaces, a customer's bucket, MinIO) over plain HTTP: path-style URLs and Signature V4, no SDK.\n * `signedUrl` is a presigned GET.\n */\nexport function bucketStore(options: BucketStoreOptions): ObjectStore {\n const creds: Credentials = { accessKeyId: options.accessKeyId, secretAccessKey: options.secretAccessKey, region: options.region, service: \"s3\" };\n const prefix = trimSlashes(options.prefix ?? \"\");\n const inBucket = (key: string) => (prefix === \"\" ? \"\" : `${prefix}/`) + key;\n const origin = trimSlashes(options.endpoint, \"end\");\n const encodePath = (path: string) => path.split(\"/\").map(encodeURIComponent).join(\"/\");\n const urlOf = (key: string) => `${origin}/${options.bucket}/${encodePath(inBucket(assertKey(key)))}`;\n const send = (method: string, url: string, body?: Uint8Array, extra: Record<string, string> = {}) => {\n const headers = signRequest(creds, { method, url, headers: extra, payloadHash: hashPayload(body ?? new Uint8Array()), now: options.clock.now() });\n return options.fetch(url, { method, headers, ...(body ? { body } : {}) });\n };\n const list = async (p: string): Promise<ObjectInfo[]> => {\n const out: ObjectInfo[] = [];\n let token: string | undefined;\n do {\n const q = new URL(`${origin}/${options.bucket}`);\n q.searchParams.set(\"list-type\", \"2\");\n q.searchParams.set(\"prefix\", inBucket(p));\n if (token) q.searchParams.set(\"continuation-token\", token);\n const res = await send(\"GET\", q.toString());\n if (!res.ok) throw fail(\"listing\", res.status);\n const xml = await res.text();\n for (const block of xml.split(\"<Contents>\").slice(1)) {\n const key = unescapeXml(tag(block, \"Key\") ?? \"\");\n out.push({ key: prefix === \"\" ? key : key.slice(prefix.length + 1), size: Number(tag(block, \"Size\") ?? 0) });\n }\n token = tag(xml, \"IsTruncated\") === \"true\" ? unescapeXml(tag(xml, \"NextContinuationToken\") ?? \"\") || undefined : undefined;\n } while (token);\n return out.sort((a, b) => a.key.localeCompare(b.key));\n };\n return {\n async put(key, bytes, meta) {\n const res = await send(\"PUT\", urlOf(key), bytes, { \"content-type\": meta.contentType, ...(options.serverSideEncryption ? { \"x-amz-server-side-encryption\": \"AES256\" } : {}) });\n if (!res.ok) throw fail(\"write\", res.status);\n },\n async get(key) {\n const res = await send(\"GET\", urlOf(key));\n if (res.status === 404) return null;\n if (!res.ok) throw fail(\"read\", res.status);\n return { bytes: new Uint8Array(await res.arrayBuffer()), contentType: res.headers.get(\"content-type\") ?? typeOfName(key) };\n },\n async head(key) {\n const res = await send(\"HEAD\", urlOf(key));\n if (res.status === 404) return null;\n if (!res.ok) throw fail(\"head\", res.status);\n return { key, size: Number(res.headers.get(\"content-length\") ?? 0) };\n },\n async delete(key) {\n const res = await send(\"DELETE\", urlOf(key));\n if (!res.ok && res.status !== 404) throw fail(\"delete\", res.status);\n },\n list,\n async signedUrl(key, ttlSeconds) {\n return presignGet(creds, urlOf(key), ttlSeconds, options.clock.now());\n },\n publicUrl: (key) => (options.publicBaseUrl ? `${trimSlashes(options.publicBaseUrl, \"end\")}/${assertKey(key)}` : null),\n };\n}\n","import { STORAGE_CODE, StorageError } from \"./port\";\n\nconst PRIVATE_V4 = [/^10\\./, /^127\\./, /^169\\.254\\./, /^172\\.(1[6-9]|2\\d|3[01])\\./, /^192\\.168\\./, /^0\\./];\n\n/**\n * An endpoint an administrator typed (their own bucket) is called by our server: it must not reach inside our network.\n * Refuses a non-https scheme, credentials in the URL, `localhost`, and literal private, loopback and link-local addresses.\n * A name that RESOLVES to a private address is not caught here: the product's network egress rules are the second guard.\n * `allowLocal` (development, MinIO on the Mac) lets `http` and local addresses through.\n * @returns the origin, without a trailing slash\n * @throws StorageError `STORAGE_BAD_ENDPOINT`\n */\nexport function checkEndpoint(endpoint: string, options: { allowLocal?: boolean } = {}): string {\n const bad = (why: string) => new StorageError(STORAGE_CODE.badEndpoint, `endpoint ${why}`);\n let url: URL;\n try {\n url = new URL(endpoint);\n } catch {\n throw bad(\"is not a URL\");\n }\n if (url.username !== \"\" || url.password !== \"\") throw bad(\"must not carry credentials\");\n if (options.allowLocal) return url.origin;\n if (url.protocol !== \"https:\") throw bad(\"must be https\");\n const host = url.hostname.toLowerCase();\n if (host === \"localhost\" || host.endsWith(\".localhost\") || host.endsWith(\".internal\") || host.endsWith(\".local\")) throw bad(\"must not be a local name\");\n if (PRIVATE_V4.some((re) => re.test(host))) throw bad(\"must not be a private address\");\n if (host.startsWith(\"[\") && (host === \"[::1]\" || host.startsWith(\"[fc\") || host.startsWith(\"[fd\") || host.startsWith(\"[fe80\") || host.startsWith(\"[::ffff:\"))) throw bad(\"must not be a private address\");\n return url.origin;\n}\n","import { mkdir, readdir, readFile, rm, stat, writeFile } from \"node:fs/promises\";\nimport { dirname, join, resolve, sep } from \"node:path\";\n\nimport { assertKey, trimSlashes, typeOfName } from \"./keys\";\nimport type { ObjectInfo, ObjectStore } from \"./port\";\n\nexport interface FolderOptions {\n /** The folder every file lives under. */\n root: string;\n /** Where the folder is served from, when it is (a dev server): the public link. */\n publicBaseUrl?: string;\n}\n\n/** A folder on disk, for development, tests and the Mac apps. A key can never leave the root. */\nexport function folderStore(options: FolderOptions): ObjectStore {\n const root = resolve(options.root);\n const pathOf = (key: string) => {\n const path = resolve(join(root, assertKey(key)));\n if (!path.startsWith(root + sep)) throw new Error(\"a key cannot leave the root\");\n return path;\n };\n const walk = async (dir: string, base: string): Promise<ObjectInfo[]> => {\n let entries;\n try {\n entries = await readdir(dir, { withFileTypes: true });\n } catch {\n return [];\n }\n const out: ObjectInfo[] = [];\n for (const entry of entries) {\n const key = base === \"\" ? entry.name : `${base}/${entry.name}`;\n if (entry.isDirectory()) out.push(...(await walk(join(dir, entry.name), key)));\n else out.push({ key, size: (await stat(join(dir, entry.name))).size });\n }\n return out;\n };\n return {\n async put(key, bytes) {\n const path = pathOf(key);\n await mkdir(dirname(path), { recursive: true });\n await writeFile(path, bytes);\n },\n async get(key) {\n try {\n return { bytes: new Uint8Array(await readFile(pathOf(key))), contentType: typeOfName(key) };\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === \"ENOENT\" || (error as NodeJS.ErrnoException).code === \"EISDIR\") return null;\n throw error;\n }\n },\n async head(key) {\n try {\n const s = await stat(pathOf(key));\n return s.isFile() ? { key, size: s.size } : null;\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === \"ENOENT\") return null;\n throw error;\n }\n },\n async delete(key) {\n await rm(pathOf(key), { force: true });\n },\n async list(prefix) {\n const slash = prefix.lastIndexOf(\"/\");\n const dir = slash < 0 ? \"\" : prefix.slice(0, slash);\n const found = await walk(dir === \"\" ? root : join(root, dir), dir);\n return found.filter((f) => f.key.startsWith(prefix)).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","import { createHash } from \"node:crypto\";\n\nimport { assertKey, trimSlashes, typeOfName } from \"./keys\";\nimport { type Clock, type FetchLike, type ObjectInfo, type ObjectStore, STORAGE_CODE, StorageError } from \"./port\";\n\nexport interface RemoteStoreOptions {\n /** The storage API host (`storage.bunnycdn.com`, or a region's). */\n endpoint: string;\n /** The zone's name, the first segment of every path. */\n zone: string;\n /** The zone's password, sent as the `AccessKey` header. Never logged, never in a link we hand out. */\n accessKey: string;\n /** Every object lives under this prefix (no slashes at the ends), or none. */\n prefix?: string;\n /** The pull zone: with it, `publicUrl` answers; with its token key too, `signedUrl` does. */\n pullHostname?: string;\n tokenKey?: string;\n fetch: FetchLike;\n clock: Clock;\n}\n\ninterface Entry {\n ObjectName: string;\n IsDirectory: boolean;\n Length?: number;\n}\n\nconst fail = (what: string, status: number) => new StorageError(STORAGE_CODE.provider, `the storage refused the ${what} (${status})`);\n\n/**\n * An HTTP object store with the shape of Bunny Storage: `PUT`/`GET`/`DELETE` on `https://<endpoint>/<zone>/<prefix>/<key>` with an\n * `AccessKey` header, a directory listed by its path with a trailing slash (a JSON array of `ObjectName`, `IsDirectory`, `Length`),\n * and a pull zone for public and token-signed links. Reads and writes go through the authenticated API, never the public zone.\n */\nexport function remoteStore(options: RemoteStoreOptions): ObjectStore {\n const prefix = trimSlashes(options.prefix ?? \"\");\n const inZone = (key: string) => (prefix === \"\" ? \"\" : `${prefix}/`) + key;\n const base = `https://${options.endpoint}/${options.zone}`;\n const headers = { AccessKey: options.accessKey };\n const url = (key: string) => `${base}/${inZone(assertKey(key))}`;\n const listDir = async (dir: string): Promise<ObjectInfo[]> => {\n const root = prefix === \"\" ? \"\" : `${prefix}/`;\n const res = await options.fetch(dir === \"\" ? `${base}/${root}` : `${base}/${inZone(dir)}/`, { method: \"GET\", headers });\n if (res.status === 404) return [];\n if (!res.ok) throw fail(\"listing\", res.status);\n const entries = JSON.parse(await res.text()) as Entry[];\n const out: ObjectInfo[] = [];\n for (const e of entries) {\n const key = dir === \"\" ? e.ObjectName : `${dir}/${e.ObjectName}`;\n if (e.IsDirectory) out.push(...(await listDir(key)));\n else out.push({ key, size: e.Length ?? 0 });\n }\n return out;\n };\n const list = async (p: string) => {\n const slash = p.lastIndexOf(\"/\");\n return (await listDir(slash < 0 ? \"\" : p.slice(0, slash))).filter((f) => f.key.startsWith(p)).sort((a, b) => a.key.localeCompare(b.key));\n };\n return {\n async put(key, bytes, meta) {\n const res = await options.fetch(url(key), { method: \"PUT\", headers: { ...headers, \"content-type\": meta.contentType }, body: bytes });\n if (!res.ok) throw fail(\"write\", res.status);\n },\n async get(key) {\n const res = await options.fetch(url(key), { method: \"GET\", headers });\n if (res.status === 404) return null;\n if (!res.ok) throw fail(\"read\", res.status);\n return { bytes: new Uint8Array(await res.arrayBuffer()), contentType: res.headers.get(\"content-type\") ?? typeOfName(key) };\n },\n async head(key) {\n assertKey(key);\n return (await list(key)).find((f) => f.key === key) ?? null;\n },\n async delete(key) {\n const res = await options.fetch(url(key), { method: \"DELETE\", headers });\n if (!res.ok && res.status !== 404) throw fail(\"delete\", res.status);\n },\n list,\n async signedUrl(key, ttlSeconds) {\n if (!options.pullHostname || !options.tokenKey) return null;\n const path = `/${inZone(assertKey(key))}`;\n const expires = Math.floor(options.clock.now().valueOf() / 1000) + ttlSeconds;\n const token = createHash(\"sha256\").update(`${options.tokenKey}${path}${expires}`).digest(\"base64\").replaceAll(\"+\", \"-\").replaceAll(\"/\", \"_\").replaceAll(\"=\", \"\");\n return `https://${options.pullHostname}${path}?token=${token}&expires=${expires}`;\n },\n publicUrl: (key) => (options.pullHostname ? `https://${options.pullHostname}/${inZone(assertKey(key))}` : null),\n };\n}\n"],"mappings":"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","names":["createHash","fail","createHash"]}
@@ -0,0 +1,81 @@
1
+ import {
2
+ STORAGE_CODE,
3
+ StorageError,
4
+ bucketStore,
5
+ checkEndpoint,
6
+ folderStore,
7
+ remoteStore
8
+ } from "./chunk-W56662PQ.js";
9
+
10
+ // src/settings.ts
11
+ import { choice, integer, text } from "@krizaka/settings";
12
+ var STORAGE_PROVIDERS = ["local", "http", "s3"];
13
+ var STORAGE_DEFAULTS = { provider: "local", maxMegabytes: 25, signedUrlSeconds: 300 };
14
+ var MB = 1024 * 1024;
15
+ var storageSettings = {
16
+ "storage.provider": choice("storage", STORAGE_PROVIDERS, STORAGE_DEFAULTS.provider),
17
+ "storage.maxMegabytes": integer("storage", STORAGE_DEFAULTS.maxMegabytes, { min: 1, max: 2048, unit: "MB" }),
18
+ "storage.signedUrlSeconds": integer("storage", STORAGE_DEFAULTS.signedUrlSeconds, { min: 30, max: 86400, unit: "seconds" }),
19
+ "storage.endpoint": text("storage", "", { empty: true }),
20
+ "storage.region": text("storage", "", { empty: true }),
21
+ "storage.bucket": text("storage", "", { empty: true }),
22
+ "storage.zone": text("storage", "", { empty: true }),
23
+ "storage.prefix": text("storage", "", { empty: true }),
24
+ "storage.pullHostname": text("storage", "", { empty: true }),
25
+ "storage.publicBaseUrl": text("storage", "", { empty: true })
26
+ };
27
+ async function readStorageSettings(read) {
28
+ const str = async (k) => {
29
+ const v = await read(k);
30
+ return typeof v === "string" ? v : "";
31
+ };
32
+ const provider = await read("storage.provider");
33
+ const maxMegabytes = await read("storage.maxMegabytes");
34
+ const signed = await read("storage.signedUrlSeconds");
35
+ return {
36
+ provider: STORAGE_PROVIDERS.find((p) => p === provider) ?? STORAGE_DEFAULTS.provider,
37
+ maxMegabytes: typeof maxMegabytes === "number" ? maxMegabytes : STORAGE_DEFAULTS.maxMegabytes,
38
+ signedUrlSeconds: typeof signed === "number" ? signed : STORAGE_DEFAULTS.signedUrlSeconds,
39
+ endpoint: await str("storage.endpoint"),
40
+ region: await str("storage.region"),
41
+ bucket: await str("storage.bucket"),
42
+ zone: await str("storage.zone"),
43
+ prefix: await str("storage.prefix"),
44
+ pullHostname: await str("storage.pullHostname"),
45
+ publicBaseUrl: await str("storage.publicBaseUrl")
46
+ };
47
+ }
48
+ var limitsOf = (s) => ({ maxBytes: s.maxMegabytes * MB, signedUrlSeconds: s.signedUrlSeconds });
49
+ function storeFromSettings(s, secrets, deps) {
50
+ const need = (value, what) => {
51
+ if (!value) throw new StorageError(STORAGE_CODE.badConfig, `storage.provider is ${s.provider} but ${what} is not set`);
52
+ return value;
53
+ };
54
+ const optional = (key, value) => value === "" ? {} : { [key]: value };
55
+ if (s.provider === "local") return folderStore({ root: need(secrets.root, "the folder"), ...optional("publicBaseUrl", s.publicBaseUrl) });
56
+ if (s.provider === "http") {
57
+ const endpoint = need(s.endpoint, "storage.endpoint");
58
+ if (/[/:]/.test(endpoint)) throw new StorageError(STORAGE_CODE.badEndpoint, "the http endpoint is a host name only");
59
+ return remoteStore({ endpoint, zone: need(s.zone, "storage.zone"), accessKey: need(secrets.accessKey, "the zone password"), ...optional("prefix", s.prefix), ...optional("pullHostname", s.pullHostname), ...optional("tokenKey", secrets.tokenKey ?? ""), ...deps });
60
+ }
61
+ return bucketStore({
62
+ endpoint: checkEndpoint(need(s.endpoint, "storage.endpoint"), deps.allowLocalEndpoint ? { allowLocal: true } : {}),
63
+ region: need(s.region, "storage.region"),
64
+ bucket: need(s.bucket, "storage.bucket"),
65
+ accessKeyId: need(secrets.accessKeyId, "the access key id"),
66
+ secretAccessKey: need(secrets.secretAccessKey, "the secret access key"),
67
+ ...optional("prefix", s.prefix),
68
+ ...optional("publicBaseUrl", s.publicBaseUrl),
69
+ ...deps
70
+ });
71
+ }
72
+
73
+ export {
74
+ STORAGE_PROVIDERS,
75
+ STORAGE_DEFAULTS,
76
+ storageSettings,
77
+ readStorageSettings,
78
+ limitsOf,
79
+ storeFromSettings
80
+ };
81
+ //# sourceMappingURL=chunk-ZWQMALDF.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/settings.ts"],"sourcesContent":["import { choice, integer, type Registry, text } from \"@krizaka/settings\";\n\nimport { bucketStore } from \"./bucket-store\";\nimport { checkEndpoint } from \"./endpoint\";\nimport { folderStore } from \"./folder-store\";\nimport { type Clock, type FetchLike, type ObjectStore, STORAGE_CODE, StorageError } from \"./port\";\nimport { remoteStore } from \"./remote-store\";\nimport type { StorageLimits } from \"./storage\";\n\nexport const STORAGE_PROVIDERS = [\"local\", \"http\", \"s3\"] as const;\nexport const STORAGE_DEFAULTS = { provider: \"local\", maxMegabytes: 25, signedUrlSeconds: 300 } as const;\nconst MB = 1024 * 1024;\n\n/**\n * The settings of « storage », a fragment the product adds to its registry (`@krizaka/storage/settings`): the admin picks the provider\n * (`local`, `http` for a Bunny-style zone, `s3` for any S3-compatible bucket) and its non-secret coordinates, without a redeploy.\n * Access keys and the token key stay in the environment and are passed to `storeFromSettings`.\n */\nexport const storageSettings = {\n \"storage.provider\": choice(\"storage\", STORAGE_PROVIDERS, STORAGE_DEFAULTS.provider),\n \"storage.maxMegabytes\": integer(\"storage\", STORAGE_DEFAULTS.maxMegabytes, { min: 1, max: 2048, unit: \"MB\" }),\n \"storage.signedUrlSeconds\": integer(\"storage\", STORAGE_DEFAULTS.signedUrlSeconds, { min: 30, max: 86_400, unit: \"seconds\" }),\n \"storage.endpoint\": text(\"storage\", \"\", { empty: true }),\n \"storage.region\": text(\"storage\", \"\", { empty: true }),\n \"storage.bucket\": text(\"storage\", \"\", { empty: true }),\n \"storage.zone\": text(\"storage\", \"\", { empty: true }),\n \"storage.prefix\": text(\"storage\", \"\", { empty: true }),\n \"storage.pullHostname\": text(\"storage\", \"\", { empty: true }),\n \"storage.publicBaseUrl\": text(\"storage\", \"\", { empty: true }),\n} as const satisfies Registry;\n\nexport interface StorageSettings {\n provider: (typeof STORAGE_PROVIDERS)[number];\n maxMegabytes: number;\n signedUrlSeconds: number;\n endpoint: string;\n region: string;\n bucket: string;\n zone: string;\n prefix: string;\n pullHostname: string;\n publicBaseUrl: string;\n}\n\ntype Reader = (key: keyof typeof storageSettings) => string | number | undefined | Promise<string | number | undefined>;\n\n/** The module's settings from the product's accessor (`(key) => service.get(key, ctx)`); an unknown key falls back to the default. */\nexport async function readStorageSettings(read: Reader): Promise<StorageSettings> {\n const str = async (k: keyof typeof storageSettings) => {\n const v = await read(k);\n return typeof v === \"string\" ? v : \"\";\n };\n const provider = await read(\"storage.provider\");\n const maxMegabytes = await read(\"storage.maxMegabytes\");\n const signed = await read(\"storage.signedUrlSeconds\");\n return {\n provider: STORAGE_PROVIDERS.find((p) => p === provider) ?? STORAGE_DEFAULTS.provider,\n maxMegabytes: typeof maxMegabytes === \"number\" ? maxMegabytes : STORAGE_DEFAULTS.maxMegabytes,\n signedUrlSeconds: typeof signed === \"number\" ? signed : STORAGE_DEFAULTS.signedUrlSeconds,\n endpoint: await str(\"storage.endpoint\"),\n region: await str(\"storage.region\"),\n bucket: await str(\"storage.bucket\"),\n zone: await str(\"storage.zone\"),\n prefix: await str(\"storage.prefix\"),\n pullHostname: await str(\"storage.pullHostname\"),\n publicBaseUrl: await str(\"storage.publicBaseUrl\"),\n };\n}\n\nexport const limitsOf = (s: StorageSettings): StorageLimits => ({ maxBytes: s.maxMegabytes * MB, signedUrlSeconds: s.signedUrlSeconds });\n\n/** The credentials the product reads from its environment and passes in. */\nexport interface StorageSecrets {\n accessKeyId?: string;\n secretAccessKey?: string;\n /** The zone's password (`http`). */\n accessKey?: string;\n /** The pull zone's token-authentication key (`http`). */\n tokenKey?: string;\n /** The folder for `local`. */\n root?: string;\n}\n\n/**\n * The store the admin chose. A provider chosen without its coordinates or keys is refused loudly, never replaced by another: a file\n * must not land somewhere nobody expects. The `http` endpoint is the host only (`storage.bunnycdn.com`); an `s3` endpoint is checked\n * against our own network (`checkEndpoint`) unless `allowLocalEndpoint` (development).\n * @throws StorageError `STORAGE_BAD_CONFIG` or `STORAGE_BAD_ENDPOINT`\n */\nexport function storeFromSettings(s: StorageSettings, secrets: StorageSecrets, deps: { fetch: FetchLike; clock: Clock; allowLocalEndpoint?: boolean }): ObjectStore {\n const need = (value: string | undefined, what: string) => {\n if (!value) throw new StorageError(STORAGE_CODE.badConfig, `storage.provider is ${s.provider} but ${what} is not set`);\n return value;\n };\n const optional = <K extends string>(key: K, value: string): Partial<Record<K, string>> => (value === \"\" ? {} : ({ [key]: value } as Record<K, string>));\n if (s.provider === \"local\") return folderStore({ root: need(secrets.root, \"the folder\"), ...optional(\"publicBaseUrl\", s.publicBaseUrl) });\n if (s.provider === \"http\") {\n const endpoint = need(s.endpoint, \"storage.endpoint\");\n if (/[/:]/.test(endpoint)) throw new StorageError(STORAGE_CODE.badEndpoint, \"the http endpoint is a host name only\");\n return remoteStore({ endpoint, zone: need(s.zone, \"storage.zone\"), accessKey: need(secrets.accessKey, \"the zone password\"), ...optional(\"prefix\", s.prefix), ...optional(\"pullHostname\", s.pullHostname), ...optional(\"tokenKey\", secrets.tokenKey ?? \"\"), ...deps });\n }\n return bucketStore({\n endpoint: checkEndpoint(need(s.endpoint, \"storage.endpoint\"), deps.allowLocalEndpoint ? { allowLocal: true } : {}),\n region: need(s.region, \"storage.region\"),\n bucket: need(s.bucket, \"storage.bucket\"),\n accessKeyId: need(secrets.accessKeyId, \"the access key id\"),\n secretAccessKey: need(secrets.secretAccessKey, \"the secret access key\"),\n ...optional(\"prefix\", s.prefix),\n ...optional(\"publicBaseUrl\", s.publicBaseUrl),\n ...deps,\n });\n}\n"],"mappings":"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","names":[]}
package/dist/index.d.ts CHANGED
@@ -1,8 +1,9 @@
1
- import { F as FetchLike, O as ObjectStore } from './port-CUTAx9CD.js';
2
- export { a as ObjectInfo, S as STORAGE_CODE, b as StorageCode, c as StorageError, d as StoredObject } from './port-CUTAx9CD.js';
1
+ import { F as FetchLike, O as ObjectStore } from './port-6VXXuSbc.js';
2
+ export { a as ObjectInfo, S as STORAGE_CODE, b as StorageCode, c as StorageError, d as StoredObject } from './port-6VXXuSbc.js';
3
3
  import { Clock } from '@krizaka/db';
4
4
  export { Clock } from '@krizaka/db';
5
- export { L as Layout, S as Storage, a as StorageDeps, b as StorageLimits, U as UploadInput, c as assertKey, d as createStorage, e as defineLayout, h as hashOf, f as hashedName, t as trimSlashes, g as typeOfName } from './storage-7cUuR_Z5.js';
5
+ export { L as Layout, S as Storage, a as StorageDeps, b as StorageLimits, U as UploadInput, c as assertKey, d as createStorage, e as defineLayout, h as hashOf, f as hashedName, t as trimSlashes, g as typeOfName } from './storage-DurtrQKq.js';
6
+ export { C as COUNTRY_OF_REGION, N as NoticeKey, R as Residency, S as STORE_KIND, a as StoreDescriptor, b as StoreOwner, W as WhereOptions, c as countryOf, d as describeStore, r as residencyOf, e as residencyOfConfig, s as storageNotice, w as where } from './where-DKtS1O5l.js';
6
7
 
7
8
  interface BucketStoreOptions {
8
9
  /** The provider's origin: `https://tor1.digitaloceanspaces.com`, `https://s3.example.com`, `http://localhost:9000`. */
@@ -26,6 +27,17 @@ interface BucketStoreOptions {
26
27
  */
27
28
  declare function bucketStore(options: BucketStoreOptions): ObjectStore;
28
29
 
30
+ /** The 32-byte AES key of the encryption at rest, from its base64 form (the owner's key, passed in as a parameter). */
31
+ declare const parseEncryptionKey: (base64: string) => Uint8Array;
32
+ /**
33
+ * Encryption at rest over any `ObjectStore` (AES-256-GCM, a fresh IV per object, the object's key as authenticated data, so a file
34
+ * moved under another name does not open). Envelope: `1 | iv(12) | tag(16) | ciphertext(contentTypeLength(2) | contentType | bytes)`.
35
+ * The key comes in as a parameter (from the settings or the environment, read by the product's edge); the provider only ever holds
36
+ * ciphertext. A link to ciphertext is useless to a browser, so `signedUrl` and `publicUrl` answer `null`: the application decrypts and serves.
37
+ * @throws StorageError `STORAGE_UNREADABLE` when an object is not an envelope or fails its integrity check
38
+ */
39
+ declare function encryptedStore(inner: ObjectStore, key: Uint8Array): ObjectStore;
40
+
29
41
  /**
30
42
  * An endpoint an administrator typed (their own bucket) is called by our server: it must not reach inside our network.
31
43
  * Refuses a non-https scheme, credentials in the URL, `localhost`, and literal private, loopback and link-local addresses.
@@ -80,4 +92,14 @@ declare function remoteStore(options: RemoteStoreOptions): ObjectStore;
80
92
  */
81
93
  declare function sniffType(bytes: Uint8Array): string | null;
82
94
 
83
- export { type BucketStoreOptions, FetchLike, type FolderOptions, ObjectStore, type RemoteStoreOptions, bucketStore, checkEndpoint, folderStore, memoryObjectStore, remoteStore, sniffType };
95
+ /** The 32-byte key of the vault, from its base64 form (the deployment's, passed in). */
96
+ declare const parseVaultKey: (base64: string) => Uint8Array;
97
+ /**
98
+ * Seals a credential for the database (AES-256-GCM, a fresh IV, `v1.<iv|tag|ciphertext>` in base64url). A leaked table opens
99
+ * nothing, and a sealed value is never sent back to the browser: the admin screen shows « set » or « not set ».
100
+ */
101
+ declare function seal(secret: string, key: Uint8Array): string;
102
+ /** @throws StorageError `STORAGE_UNREADABLE` for a value that is not sealed, was tampered with, or sealed under another key */
103
+ declare function open(sealed: string, key: Uint8Array): string;
104
+
105
+ export { type BucketStoreOptions, FetchLike, type FolderOptions, ObjectStore, type RemoteStoreOptions, bucketStore, checkEndpoint, encryptedStore, folderStore, memoryObjectStore, open, parseEncryptionKey, parseVaultKey, remoteStore, seal, sniffType };
package/dist/index.js CHANGED
@@ -1,3 +1,16 @@
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";
1
14
  import {
2
15
  STORAGE_CODE,
3
16
  StorageError,
@@ -11,7 +24,7 @@ import {
11
24
  remoteStore,
12
25
  trimSlashes,
13
26
  typeOfName
14
- } from "./chunk-3E2PZNBG.js";
27
+ } from "./chunk-W56662PQ.js";
15
28
 
16
29
  // src/memory-store.ts
17
30
  function memoryObjectStore(options = {}) {
@@ -94,21 +107,55 @@ function createStorage(deps) {
94
107
  list: (prefix) => deps.store.list(prefix)
95
108
  };
96
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
+ }
97
131
  export {
132
+ COUNTRY_OF_REGION,
98
133
  STORAGE_CODE,
134
+ STORE_KIND,
99
135
  StorageError,
100
136
  assertKey,
101
137
  bucketStore,
102
138
  checkEndpoint,
139
+ countryOf,
103
140
  createStorage,
104
141
  defineLayout,
142
+ describeStore,
143
+ encryptedStore,
105
144
  folderStore,
106
145
  hashOf,
107
146
  hashedName,
108
147
  memoryObjectStore,
148
+ open,
149
+ parseEncryptionKey,
150
+ parseVaultKey,
109
151
  remoteStore,
152
+ residencyOf,
153
+ residencyOfConfig,
154
+ seal,
110
155
  sniffType,
156
+ storageNotice,
111
157
  trimSlashes,
112
- typeOfName
158
+ typeOfName,
159
+ where
113
160
  };
114
161
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/memory-store.ts","../src/sniff.ts","../src/storage.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"],"mappings":"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","names":[]}
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":[]}
@@ -52,6 +52,9 @@ declare const STORAGE_CODE: {
52
52
  readonly provider: "STORAGE_PROVIDER_FAILED";
53
53
  readonly badConfig: "STORAGE_BAD_CONFIG";
54
54
  readonly badEndpoint: "STORAGE_BAD_ENDPOINT";
55
+ readonly badSecretKey: "STORAGE_BAD_SECRET_KEY";
56
+ readonly unreadable: "STORAGE_UNREADABLE";
57
+ readonly keepingOff: "STORAGE_KEEPING_OFF";
55
58
  };
56
59
  type StorageCode = (typeof STORAGE_CODE)[keyof typeof STORAGE_CODE];
57
60
  declare class StorageError extends Error {
@@ -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":[]}
@@ -1,6 +1,6 @@
1
1
  import * as _krizaka_settings from '@krizaka/settings';
2
- import { F as FetchLike, O as ObjectStore } from './port-CUTAx9CD.js';
3
- import { b as StorageLimits } from './storage-7cUuR_Z5.js';
2
+ import { F as FetchLike, O as ObjectStore } from './port-6VXXuSbc.js';
3
+ import { b as StorageLimits } from './storage-DurtrQKq.js';
4
4
  import { Clock } from '@krizaka/db';
5
5
 
6
6
  declare const STORAGE_PROVIDERS: readonly ["local", "http", "s3"];
package/dist/settings.js CHANGED
@@ -1,74 +1,12 @@
1
1
  import {
2
- STORAGE_CODE,
3
- StorageError,
4
- bucketStore,
5
- checkEndpoint,
6
- folderStore,
7
- remoteStore
8
- } from "./chunk-3E2PZNBG.js";
9
-
10
- // src/settings.ts
11
- import { choice, integer, text } from "@krizaka/settings";
12
- var STORAGE_PROVIDERS = ["local", "http", "s3"];
13
- var STORAGE_DEFAULTS = { provider: "local", maxMegabytes: 25, signedUrlSeconds: 300 };
14
- var MB = 1024 * 1024;
15
- var storageSettings = {
16
- "storage.provider": choice("storage", STORAGE_PROVIDERS, STORAGE_DEFAULTS.provider),
17
- "storage.maxMegabytes": integer("storage", STORAGE_DEFAULTS.maxMegabytes, { min: 1, max: 2048, unit: "MB" }),
18
- "storage.signedUrlSeconds": integer("storage", STORAGE_DEFAULTS.signedUrlSeconds, { min: 30, max: 86400, unit: "seconds" }),
19
- "storage.endpoint": text("storage", "", { empty: true }),
20
- "storage.region": text("storage", "", { empty: true }),
21
- "storage.bucket": text("storage", "", { empty: true }),
22
- "storage.zone": text("storage", "", { empty: true }),
23
- "storage.prefix": text("storage", "", { empty: true }),
24
- "storage.pullHostname": text("storage", "", { empty: true }),
25
- "storage.publicBaseUrl": text("storage", "", { empty: true })
26
- };
27
- async function readStorageSettings(read) {
28
- const str = async (k) => {
29
- const v = await read(k);
30
- return typeof v === "string" ? v : "";
31
- };
32
- const provider = await read("storage.provider");
33
- const maxMegabytes = await read("storage.maxMegabytes");
34
- const signed = await read("storage.signedUrlSeconds");
35
- return {
36
- provider: STORAGE_PROVIDERS.find((p) => p === provider) ?? STORAGE_DEFAULTS.provider,
37
- maxMegabytes: typeof maxMegabytes === "number" ? maxMegabytes : STORAGE_DEFAULTS.maxMegabytes,
38
- signedUrlSeconds: typeof signed === "number" ? signed : STORAGE_DEFAULTS.signedUrlSeconds,
39
- endpoint: await str("storage.endpoint"),
40
- region: await str("storage.region"),
41
- bucket: await str("storage.bucket"),
42
- zone: await str("storage.zone"),
43
- prefix: await str("storage.prefix"),
44
- pullHostname: await str("storage.pullHostname"),
45
- publicBaseUrl: await str("storage.publicBaseUrl")
46
- };
47
- }
48
- var limitsOf = (s) => ({ maxBytes: s.maxMegabytes * MB, signedUrlSeconds: s.signedUrlSeconds });
49
- function storeFromSettings(s, secrets, deps) {
50
- const need = (value, what) => {
51
- if (!value) throw new StorageError(STORAGE_CODE.badConfig, `storage.provider is ${s.provider} but ${what} is not set`);
52
- return value;
53
- };
54
- const optional = (key, value) => value === "" ? {} : { [key]: value };
55
- if (s.provider === "local") return folderStore({ root: need(secrets.root, "the folder"), ...optional("publicBaseUrl", s.publicBaseUrl) });
56
- if (s.provider === "http") {
57
- const endpoint = need(s.endpoint, "storage.endpoint");
58
- if (/[/:]/.test(endpoint)) throw new StorageError(STORAGE_CODE.badEndpoint, "the http endpoint is a host name only");
59
- return remoteStore({ endpoint, zone: need(s.zone, "storage.zone"), accessKey: need(secrets.accessKey, "the zone password"), ...optional("prefix", s.prefix), ...optional("pullHostname", s.pullHostname), ...optional("tokenKey", secrets.tokenKey ?? ""), ...deps });
60
- }
61
- return bucketStore({
62
- endpoint: checkEndpoint(need(s.endpoint, "storage.endpoint"), deps.allowLocalEndpoint ? { allowLocal: true } : {}),
63
- region: need(s.region, "storage.region"),
64
- bucket: need(s.bucket, "storage.bucket"),
65
- accessKeyId: need(secrets.accessKeyId, "the access key id"),
66
- secretAccessKey: need(secrets.secretAccessKey, "the secret access key"),
67
- ...optional("prefix", s.prefix),
68
- ...optional("publicBaseUrl", s.publicBaseUrl),
69
- ...deps
70
- });
71
- }
2
+ STORAGE_DEFAULTS,
3
+ STORAGE_PROVIDERS,
4
+ limitsOf,
5
+ readStorageSettings,
6
+ storageSettings,
7
+ storeFromSettings
8
+ } from "./chunk-ZWQMALDF.js";
9
+ import "./chunk-W56662PQ.js";
72
10
  export {
73
11
  STORAGE_DEFAULTS,
74
12
  STORAGE_PROVIDERS,
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/settings.ts"],"sourcesContent":["import { choice, integer, type Registry, text } from \"@krizaka/settings\";\n\nimport { bucketStore } from \"./bucket-store\";\nimport { checkEndpoint } from \"./endpoint\";\nimport { folderStore } from \"./folder-store\";\nimport { type Clock, type FetchLike, type ObjectStore, STORAGE_CODE, StorageError } from \"./port\";\nimport { remoteStore } from \"./remote-store\";\nimport type { StorageLimits } from \"./storage\";\n\nexport const STORAGE_PROVIDERS = [\"local\", \"http\", \"s3\"] as const;\nexport const STORAGE_DEFAULTS = { provider: \"local\", maxMegabytes: 25, signedUrlSeconds: 300 } as const;\nconst MB = 1024 * 1024;\n\n/**\n * The settings of « storage », a fragment the product adds to its registry (`@krizaka/storage/settings`): the admin picks the provider\n * (`local`, `http` for a Bunny-style zone, `s3` for any S3-compatible bucket) and its non-secret coordinates, without a redeploy.\n * Access keys and the token key stay in the environment and are passed to `storeFromSettings`.\n */\nexport const storageSettings = {\n \"storage.provider\": choice(\"storage\", STORAGE_PROVIDERS, STORAGE_DEFAULTS.provider),\n \"storage.maxMegabytes\": integer(\"storage\", STORAGE_DEFAULTS.maxMegabytes, { min: 1, max: 2048, unit: \"MB\" }),\n \"storage.signedUrlSeconds\": integer(\"storage\", STORAGE_DEFAULTS.signedUrlSeconds, { min: 30, max: 86_400, unit: \"seconds\" }),\n \"storage.endpoint\": text(\"storage\", \"\", { empty: true }),\n \"storage.region\": text(\"storage\", \"\", { empty: true }),\n \"storage.bucket\": text(\"storage\", \"\", { empty: true }),\n \"storage.zone\": text(\"storage\", \"\", { empty: true }),\n \"storage.prefix\": text(\"storage\", \"\", { empty: true }),\n \"storage.pullHostname\": text(\"storage\", \"\", { empty: true }),\n \"storage.publicBaseUrl\": text(\"storage\", \"\", { empty: true }),\n} as const satisfies Registry;\n\nexport interface StorageSettings {\n provider: (typeof STORAGE_PROVIDERS)[number];\n maxMegabytes: number;\n signedUrlSeconds: number;\n endpoint: string;\n region: string;\n bucket: string;\n zone: string;\n prefix: string;\n pullHostname: string;\n publicBaseUrl: string;\n}\n\ntype Reader = (key: keyof typeof storageSettings) => string | number | undefined | Promise<string | number | undefined>;\n\n/** The module's settings from the product's accessor (`(key) => service.get(key, ctx)`); an unknown key falls back to the default. */\nexport async function readStorageSettings(read: Reader): Promise<StorageSettings> {\n const str = async (k: keyof typeof storageSettings) => {\n const v = await read(k);\n return typeof v === \"string\" ? v : \"\";\n };\n const provider = await read(\"storage.provider\");\n const maxMegabytes = await read(\"storage.maxMegabytes\");\n const signed = await read(\"storage.signedUrlSeconds\");\n return {\n provider: STORAGE_PROVIDERS.find((p) => p === provider) ?? STORAGE_DEFAULTS.provider,\n maxMegabytes: typeof maxMegabytes === \"number\" ? maxMegabytes : STORAGE_DEFAULTS.maxMegabytes,\n signedUrlSeconds: typeof signed === \"number\" ? signed : STORAGE_DEFAULTS.signedUrlSeconds,\n endpoint: await str(\"storage.endpoint\"),\n region: await str(\"storage.region\"),\n bucket: await str(\"storage.bucket\"),\n zone: await str(\"storage.zone\"),\n prefix: await str(\"storage.prefix\"),\n pullHostname: await str(\"storage.pullHostname\"),\n publicBaseUrl: await str(\"storage.publicBaseUrl\"),\n };\n}\n\nexport const limitsOf = (s: StorageSettings): StorageLimits => ({ maxBytes: s.maxMegabytes * MB, signedUrlSeconds: s.signedUrlSeconds });\n\n/** The credentials the product reads from its environment and passes in. */\nexport interface StorageSecrets {\n accessKeyId?: string;\n secretAccessKey?: string;\n /** The zone's password (`http`). */\n accessKey?: string;\n /** The pull zone's token-authentication key (`http`). */\n tokenKey?: string;\n /** The folder for `local`. */\n root?: string;\n}\n\n/**\n * The store the admin chose. A provider chosen without its coordinates or keys is refused loudly, never replaced by another: a file\n * must not land somewhere nobody expects. The `http` endpoint is the host only (`storage.bunnycdn.com`); an `s3` endpoint is checked\n * against our own network (`checkEndpoint`) unless `allowLocalEndpoint` (development).\n * @throws StorageError `STORAGE_BAD_CONFIG` or `STORAGE_BAD_ENDPOINT`\n */\nexport function storeFromSettings(s: StorageSettings, secrets: StorageSecrets, deps: { fetch: FetchLike; clock: Clock; allowLocalEndpoint?: boolean }): ObjectStore {\n const need = (value: string | undefined, what: string) => {\n if (!value) throw new StorageError(STORAGE_CODE.badConfig, `storage.provider is ${s.provider} but ${what} is not set`);\n return value;\n };\n const optional = <K extends string>(key: K, value: string): Partial<Record<K, string>> => (value === \"\" ? {} : ({ [key]: value } as Record<K, string>));\n if (s.provider === \"local\") return folderStore({ root: need(secrets.root, \"the folder\"), ...optional(\"publicBaseUrl\", s.publicBaseUrl) });\n if (s.provider === \"http\") {\n const endpoint = need(s.endpoint, \"storage.endpoint\");\n if (/[/:]/.test(endpoint)) throw new StorageError(STORAGE_CODE.badEndpoint, \"the http endpoint is a host name only\");\n return remoteStore({ endpoint, zone: need(s.zone, \"storage.zone\"), accessKey: need(secrets.accessKey, \"the zone password\"), ...optional(\"prefix\", s.prefix), ...optional(\"pullHostname\", s.pullHostname), ...optional(\"tokenKey\", secrets.tokenKey ?? \"\"), ...deps });\n }\n return bucketStore({\n endpoint: checkEndpoint(need(s.endpoint, \"storage.endpoint\"), deps.allowLocalEndpoint ? { allowLocal: true } : {}),\n region: need(s.region, \"storage.region\"),\n bucket: need(s.bucket, \"storage.bucket\"),\n accessKeyId: need(secrets.accessKeyId, \"the access key id\"),\n secretAccessKey: need(secrets.secretAccessKey, \"the secret access key\"),\n ...optional(\"prefix\", s.prefix),\n ...optional(\"publicBaseUrl\", s.publicBaseUrl),\n ...deps,\n });\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -1,4 +1,4 @@
1
- import { O as ObjectStore } from './port-CUTAx9CD.js';
1
+ import { O as ObjectStore } from './port-6VXXuSbc.js';
2
2
 
3
3
  /** Trims slashes at the ends without a regular expression (a long run of slashes must not be quadratic). */
4
4
  declare function trimSlashes(value: string, ends?: "both" | "end"): string;
package/dist/testkit.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { O as ObjectStore } from './port-CUTAx9CD.js';
1
+ import { O as ObjectStore } from './port-6VXXuSbc.js';
2
2
 
3
3
  /** What every `ObjectStore` adapter must do: the same behaviour behind the same port. `make` gives an empty store. */
4
4
  declare function objectStoreContract(name: string, make: () => ObjectStore | Promise<ObjectStore>): void;
@@ -0,0 +1,71 @@
1
+ /**
2
+ * The country of each provider region (data: a new region is a line here, one table for every product, ADR-010). A region not listed
3
+ * is unknown: nothing claims a country for it, and the generated residency text never says « Canada » for it.
4
+ */
5
+ declare const COUNTRY_OF_REGION: Readonly<Record<string, string>>;
6
+ type Residency = "canada" | "elsewhere";
7
+ /** The ISO country of a provider region, or undefined when the region is not mapped. */
8
+ declare const countryOf: (region: string) => string | undefined;
9
+ /** Canada only when the table says so; an unmapped region is « elsewhere », never a guess in Canada's favour. */
10
+ declare const residencyOf: (region: string) => Residency;
11
+ /**
12
+ * Residency of a configured store: the region AND the endpoint have to agree. A host that names a known region
13
+ * (`nyc3.digitaloceanspaces.com`, `ny.storage.bunnycdn.com`) in another country than the region setting is the contradiction that
14
+ * would let a text claim Canada falsely, so any known region label of the host that is not Canadian makes the answer « elsewhere ».
15
+ * A Bunny-shaped zone (`http`) has no Canadian region, so it is never « canada ».
16
+ */
17
+ declare function residencyOfConfig(config: {
18
+ provider: "http" | "s3";
19
+ endpoint: string;
20
+ region: string;
21
+ }): Residency;
22
+
23
+ /** The two kinds every product shares; the others are the product's. */
24
+ declare const STORE_KIND: {
25
+ readonly none: "none";
26
+ readonly device: "device";
27
+ };
28
+ /** Whose store it is: Krizaka's (the deployment's), the workspace's own, or the person's device. */
29
+ type StoreOwner = "krizaka" | "workspace" | "device";
30
+ /** Where objects live, as the product says it: computed from the configuration, never typed. */
31
+ interface StoreDescriptor {
32
+ /** The product's name for the store (`s3`, `bunny`, `database`, `none`, `device`…). */
33
+ kind: string;
34
+ endpoint: string | null;
35
+ region: string | null;
36
+ residency: Residency | "unknown" | "device";
37
+ encrypted: boolean;
38
+ owner: StoreOwner;
39
+ /** For fictional and development data only: the notice says so instead of claiming a place. */
40
+ devOnly: boolean;
41
+ }
42
+ /** The descriptor of a configured store, from its configuration alone (see `residencyOfConfig`: a Bunny-shaped zone is dev only and never Canadian). */
43
+ declare function describeStore(config: {
44
+ provider: "http" | "s3";
45
+ endpoint: string;
46
+ region: string;
47
+ }, options: {
48
+ encrypted: boolean;
49
+ owner?: StoreOwner;
50
+ httpKind?: string;
51
+ }): StoreDescriptor;
52
+ interface WhereOptions {
53
+ /** Stores that are not configurable but fixed places (Yomizaka's managed database in Toronto), by kind. */
54
+ fixed?: Readonly<Record<string, StoreDescriptor>>;
55
+ /** Kinds that exist only for development data. */
56
+ devOnly?: readonly string[];
57
+ }
58
+ /**
59
+ * Where an object lives, from the store kind recorded with it and the store configured now. True at every moment, per object:
60
+ * a fixed place says what it is; `none` and `device` say so; the active store answers for its own kind; any other kind (a store
61
+ * no longer configured) says only its kind, with an unknown residency, so nothing is claimed that nothing proves.
62
+ */
63
+ declare function where(kind: string, active: StoreDescriptor, options?: WhereOptions): StoreDescriptor;
64
+ type NoticeKey = "none" | "krizakaCanadaEncrypted" | "krizakaCanada" | "krizakaElsewhere" | "workspace" | "devOnly" | "device";
65
+ /** Which sentence says where this object lives (« Not kept », « Kept: Krizaka (Canada, encrypted) »…), with its variables; the product's messages hold the words. */
66
+ declare function storageNotice(d: StoreDescriptor): {
67
+ key: NoticeKey;
68
+ vars: Record<string, string>;
69
+ };
70
+
71
+ export { COUNTRY_OF_REGION as C, type NoticeKey as N, type Residency as R, STORE_KIND as S, type WhereOptions as W, type StoreDescriptor as a, type StoreOwner as b, countryOf as c, describeStore as d, residencyOfConfig as e, residencyOf as r, storageNotice as s, where as w };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@krizaka/storage",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Files behind one port for every Krizaka product: put with a content-hashed name, get, head, delete, list, signed and public URLs; adapters over an injected fetch for an HTTP object store and any S3-compatible bucket (Signature V4, no SDK), and a local folder; layout as data, bytes checked by magic number, limits passed in. The admin chooses the provider through settings.",
5
5
  "keywords": [
6
6
  "krizaka",
@@ -33,6 +33,10 @@
33
33
  "types": "./dist/settings.d.ts",
34
34
  "import": "./dist/settings.js"
35
35
  },
36
+ "./resolve": {
37
+ "types": "./dist/resolve.d.ts",
38
+ "import": "./dist/resolve.js"
39
+ },
36
40
  "./testkit": {
37
41
  "types": "./dist/testkit.d.ts",
38
42
  "import": "./dist/testkit.js"
@@ -57,11 +61,11 @@
57
61
  "typescript": "^5.9.3",
58
62
  "vitest": "^4.1.11",
59
63
  "zod": "^4.6.7",
60
- "@krizaka/config": "^0.8.1"
64
+ "@krizaka/config": "^0.9.1"
61
65
  },
62
66
  "dependencies": {
63
- "@krizaka/db": "^0.2.0",
64
- "@krizaka/settings": "^0.5.0"
67
+ "@krizaka/settings": "^0.5.0",
68
+ "@krizaka/db": "^0.2.0"
65
69
  },
66
70
  "peerDependencies": {
67
71
  "vitest": ">=4",
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/port.ts","../src/keys.ts","../src/request-signing.ts","../src/bucket-store.ts","../src/endpoint.ts","../src/folder-store.ts","../src/remote-store.ts"],"sourcesContent":["/**\n * The ports of « storage »: files behind one interface, named by what it does. Generic: no vendor SDK (adapters speak plain HTTP\n * through an injected `fetch`), no environment variable (credentials come in as parameters), no wall clock (a `Clock` signs the links).\n */\nexport type { Clock } from \"@krizaka/db\";\n\n/** The part of `fetch` the adapters use: inject the global one (`fetch`), or a fake in a test. */\nexport type FetchLike = (\n url: string,\n init: { method: string; headers: Record<string, string>; body?: Uint8Array },\n) => Promise<{ ok: boolean; status: number; headers: { get(name: string): string | null }; arrayBuffer(): Promise<ArrayBuffer>; text(): Promise<string> }>;\n\nexport interface StoredObject {\n bytes: Uint8Array;\n contentType: string;\n}\n\nexport interface ObjectInfo {\n key: string;\n size: number;\n}\n\n/** One place files live. Every adapter runs `objectStoreContract` (`@krizaka/storage/testkit`). Keys are `a/b/name.ext`, checked by `assertKey`. */\nexport interface ObjectStore {\n put(key: string, bytes: Uint8Array, meta: { contentType: string }): Promise<void>;\n /** `null` when there is no such file. */\n get(key: string): Promise<StoredObject | null>;\n head(key: string): Promise<ObjectInfo | null>;\n /** Idempotent: deleting a missing file is not an error. */\n delete(key: string): Promise<void>;\n /** Every file whose key starts with `prefix`, in key order, directories flattened. */\n list(prefix: string): Promise<ObjectInfo[]>;\n /** A link valid for `ttlSeconds` for a private file, or `null` when the store cannot sign one. */\n signedUrl(key: string, ttlSeconds: number): Promise<string | null>;\n /** The permanent public link (a pull zone, a CDN), or `null` when the zone is private. */\n publicUrl(key: string): string | null;\n}\n\n/** The refusals of this package: a stable `code`, and the HTTP status a route would answer. */\nexport const STORAGE_CODE = {\n badKey: \"STORAGE_BAD_KEY\",\n badPrefix: \"STORAGE_BAD_PREFIX\",\n tooBig: \"STORAGE_TOO_BIG\",\n badType: \"STORAGE_BAD_TYPE\",\n typeMismatch: \"STORAGE_TYPE_MISMATCH\",\n provider: \"STORAGE_PROVIDER_FAILED\",\n badConfig: \"STORAGE_BAD_CONFIG\",\n badEndpoint: \"STORAGE_BAD_ENDPOINT\",\n} as const;\nexport type StorageCode = (typeof STORAGE_CODE)[keyof typeof STORAGE_CODE];\n\nexport class StorageError extends Error {\n constructor(\n readonly code: StorageCode,\n message: string,\n ) {\n super(message);\n this.name = \"StorageError\";\n }\n}\n","import { createHash } from \"node:crypto\";\n\nimport { STORAGE_CODE, StorageError } from \"./port\";\n\n// @fixed: S3's own limit on a key; a longer one is refused by every provider\nconst MAX_KEY = 1024;\nconst SEGMENT = /^[A-Za-z0-9][A-Za-z0-9._ ()@+=,~-]*$/;\n\n/** Trims slashes at the ends without a regular expression (a long run of slashes must not be quadratic). */\nexport function trimSlashes(value: string, ends: \"both\" | \"end\" = \"both\"): string {\n let start = 0;\n let end = value.length;\n if (ends === \"both\") while (start < end && value[start] === \"/\") start += 1;\n while (end > start && value[end - 1] === \"/\") end -= 1;\n return value.slice(start, end);\n}\n\n/**\n * A key is `a/b/name.ext`: no empty segment, no `.` or `..`, no leading or trailing slash, no backslash, no control character.\n * @throws StorageError `STORAGE_BAD_KEY`\n */\nexport function assertKey(key: string): string {\n const bad = (why: string) => new StorageError(STORAGE_CODE.badKey, `${why}: « ${key.slice(0, 80)} »`);\n if (typeof key !== \"string\" || key === \"\" || key.length > MAX_KEY) throw bad(\"a key is 1 to 1024 characters\");\n for (const segment of key.split(\"/\")) {\n if (segment === \".\" || segment === \"..\") throw bad(\"a key has no . or .. segment\");\n if (!SEGMENT.test(segment)) throw bad(\"a key segment is empty or has a character storage services refuse\");\n }\n return key;\n}\n\n/** The first 10 hex characters of the SHA-256: the rule of the Krizaka media library (`name.<hash>.ext`). */\nexport const hashOf = (bytes: Uint8Array): string => createHash(\"sha256\").update(bytes).digest(\"hex\").slice(0, 10);\n\n/** `photo.jpg` and its bytes give `photo.<sha10>.jpg`: a URL never changes content, so it is cached for good. */\nexport function hashedName(name: string, bytes: Uint8Array): string {\n const dot = name.lastIndexOf(\".\");\n const base = dot > 0 ? name.slice(0, dot) : name;\n const ext = dot > 0 ? name.slice(dot) : \"\";\n return `${base}.${hashOf(bytes)}${ext}`;\n}\n\n/**\n * The prefixes a zone allows, as data: an unknown prefix is refused. A layout lists prefixes WITH a trailing slash (`site/home/`);\n * a key belongs to the prefix it starts with.\n */\nexport interface Layout {\n prefixes: readonly string[];\n /** The prefix a key belongs to, or `null`. */\n prefixOf(key: string): string | null;\n /** @throws StorageError `STORAGE_BAD_PREFIX` when the key is under no declared prefix. */\n check(key: string): string;\n}\n\nexport function defineLayout(prefixes: readonly string[]): Layout {\n for (const p of prefixes) if (!p.endsWith(\"/\")) throw new StorageError(STORAGE_CODE.badPrefix, `layout prefix « ${p} » must end with a slash`);\n const sorted = [...prefixes].sort((a, b) => b.length - a.length);\n const prefixOf = (key: string) => sorted.find((p) => key.startsWith(p)) ?? null;\n return {\n prefixes,\n prefixOf,\n check(key) {\n assertKey(key);\n if (prefixOf(key) === null) throw new StorageError(STORAGE_CODE.badPrefix, `« ${key.slice(0, 80)} » is under no declared prefix`);\n return key;\n },\n };\n}\n\nconst TYPES: Record<string, string> = {\n png: \"image/png\",\n jpg: \"image/jpeg\",\n jpeg: \"image/jpeg\",\n gif: \"image/gif\",\n webp: \"image/webp\",\n svg: \"image/svg+xml\",\n pdf: \"application/pdf\",\n mp4: \"video/mp4\",\n webm: \"video/webm\",\n json: \"application/json\",\n txt: \"text/plain\",\n vtt: \"text/vtt\",\n};\n\n/** The content type a file name suggests, for stores that do not keep one. Never a security decision: see `sniffType`. */\nexport const typeOfName = (key: string): string => TYPES[key.split(\".\").pop()?.toLowerCase() ?? \"\"] ?? \"application/octet-stream\";\n","import crypto from \"node:crypto\";\n\n/** AWS Signature Version 4, the part of it any S3-compatible provider needs (header signing and presigned URLs). Dependency-free. */\nconst sha256 = (data: string | Uint8Array) => crypto.createHash(\"sha256\").update(data).digest(\"hex\");\nconst hmac = (key: crypto.BinaryLike | crypto.KeyObject, data: string) => crypto.createHmac(\"sha256\", key).update(data).digest();\nexport const EMPTY_SHA256 = sha256(\"\");\nexport const hashPayload = (bytes: Uint8Array) => sha256(bytes);\n\nexport interface Credentials {\n accessKeyId: string;\n secretAccessKey: string;\n region: string;\n service: string;\n}\n\nconst stamp = (now: Date) => now.toISOString().replace(/[:-]|\\.\\d{3}/g, \"\");\nconst encode = (s: string) => encodeURIComponent(s).replace(/[!'()*]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`);\nconst canonicalPath = (url: URL) =>\n url.pathname\n .split(\"/\")\n .map((seg) => encode(decodeURIComponent(seg)))\n .join(\"/\") || \"/\";\nconst byNameThenValue = ([a, av]: readonly [string, string], [b, bv]: readonly [string, string]) => {\n if (a === b) return av < bv ? -1 : 1;\n return a < b ? -1 : 1;\n};\nconst canonicalQuery = (url: URL) =>\n [...url.searchParams.entries()]\n .map(([k, v]) => [encode(k), encode(v)] as const)\n .sort(byNameThenValue)\n .map(([k, v]) => `${k}=${v}`)\n .join(\"&\");\n\nfunction signingKey(c: Credentials, date: string) {\n return hmac(hmac(hmac(hmac(`AWS4${c.secretAccessKey}`, date), c.region), c.service), \"aws4_request\");\n}\n\nfunction signature(c: Credentials, method: string, url: URL, headers: Record<string, string>, payloadHash: string, amzDate: string) {\n const names = Object.keys(headers)\n .map((h) => h.toLowerCase())\n .sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)); // code-unit order, as the signature requires\n const lower = Object.fromEntries(Object.entries(headers).map(([k, v]) => [k.toLowerCase(), v.trim().replace(/\\s+/g, \" \")]));\n const canonical = [method, canonicalPath(url), canonicalQuery(url), names.map((n) => `${n}:${lower[n]}\\n`).join(\"\"), names.join(\";\"), payloadHash].join(\"\\n\");\n const scope = `${amzDate.slice(0, 8)}/${c.region}/${c.service}/aws4_request`;\n const toSign = [\"AWS4-HMAC-SHA256\", amzDate, scope, sha256(canonical)].join(\"\\n\");\n return { signed: names.join(\";\"), scope, value: crypto.createHmac(\"sha256\", signingKey(c, amzDate.slice(0, 8))).update(toSign).digest(\"hex\") };\n}\n\n/** The headers of a signed request: `authorization`, `x-amz-date` and, for S3, `x-amz-content-sha256` (give the headers you send, without host). */\nexport function signRequest(c: Credentials, req: { method: string; url: string; headers?: Record<string, string>; payloadHash: string; now: Date }) {\n const url = new URL(req.url);\n const amzDate = stamp(req.now);\n const extra: Record<string, string> = c.service === \"s3\" ? { \"x-amz-content-sha256\": req.payloadHash } : {};\n const s = signature(c, req.method, url, { ...req.headers, host: url.host, \"x-amz-date\": amzDate, ...extra }, req.payloadHash, amzDate);\n return { ...req.headers, \"x-amz-date\": amzDate, ...extra, authorization: `AWS4-HMAC-SHA256 Credential=${c.accessKeyId}/${s.scope}, SignedHeaders=${s.signed}, Signature=${s.value}` };\n}\n\n/** A presigned GET URL, valid for `ttlSeconds`. */\nexport function presignGet(c: Credentials, url: string, ttlSeconds: number, now: Date): string {\n const u = new URL(url);\n const amzDate = stamp(now);\n u.searchParams.set(\"X-Amz-Algorithm\", \"AWS4-HMAC-SHA256\");\n u.searchParams.set(\"X-Amz-Credential\", `${c.accessKeyId}/${amzDate.slice(0, 8)}/${c.region}/${c.service}/aws4_request`);\n u.searchParams.set(\"X-Amz-Date\", amzDate);\n u.searchParams.set(\"X-Amz-Expires\", String(ttlSeconds));\n u.searchParams.set(\"X-Amz-SignedHeaders\", \"host\");\n const s = signature(c, \"GET\", u, { host: u.host }, \"UNSIGNED-PAYLOAD\", amzDate);\n u.searchParams.set(\"X-Amz-Signature\", s.value);\n return u.toString();\n}\n","import { assertKey, trimSlashes, typeOfName } from \"./keys\";\nimport { type Clock, type FetchLike, type ObjectInfo, type ObjectStore, STORAGE_CODE, StorageError } from \"./port\";\nimport { type Credentials, hashPayload, presignGet, signRequest } from \"./request-signing\";\n\nexport interface BucketStoreOptions {\n /** The provider's origin: `https://tor1.digitaloceanspaces.com`, `https://s3.example.com`, `http://localhost:9000`. */\n endpoint: string;\n region: string;\n bucket: string;\n accessKeyId: string;\n secretAccessKey: string;\n /** Every object lives under this prefix (no slashes at the ends), or none. */\n prefix?: string;\n /** Where the bucket is served publicly (a CDN in front of it), or none: the bucket is private. */\n publicBaseUrl?: string;\n /** Ask the provider to encrypt at rest (`AES256`). */\n serverSideEncryption?: boolean;\n fetch: FetchLike;\n clock: Clock;\n}\n\nconst fail = (what: string, status: number) => new StorageError(STORAGE_CODE.provider, `the bucket refused the ${what} (${status})`);\nconst unescapeXml = (s: string) => s.replaceAll(\"&lt;\", \"<\").replaceAll(\"&gt;\", \">\").replaceAll(\"&quot;\", '\"').replaceAll(\"&apos;\", \"'\").replaceAll(\"&amp;\", \"&\");\nconst tag = (xml: string, name: string) => new RegExp(`<${name}>([\\\\s\\\\S]*?)</${name}>`).exec(xml)?.[1];\n\n/**\n * Any S3-compatible service (Spaces, a customer's bucket, MinIO) over plain HTTP: path-style URLs and Signature V4, no SDK.\n * `signedUrl` is a presigned GET.\n */\nexport function bucketStore(options: BucketStoreOptions): ObjectStore {\n const creds: Credentials = { accessKeyId: options.accessKeyId, secretAccessKey: options.secretAccessKey, region: options.region, service: \"s3\" };\n const prefix = trimSlashes(options.prefix ?? \"\");\n const inBucket = (key: string) => (prefix === \"\" ? \"\" : `${prefix}/`) + key;\n const origin = trimSlashes(options.endpoint, \"end\");\n const encodePath = (path: string) => path.split(\"/\").map(encodeURIComponent).join(\"/\");\n const urlOf = (key: string) => `${origin}/${options.bucket}/${encodePath(inBucket(assertKey(key)))}`;\n const send = (method: string, url: string, body?: Uint8Array, extra: Record<string, string> = {}) => {\n const headers = signRequest(creds, { method, url, headers: extra, payloadHash: hashPayload(body ?? new Uint8Array()), now: options.clock.now() });\n return options.fetch(url, { method, headers, ...(body ? { body } : {}) });\n };\n const list = async (p: string): Promise<ObjectInfo[]> => {\n const out: ObjectInfo[] = [];\n let token: string | undefined;\n do {\n const q = new URL(`${origin}/${options.bucket}`);\n q.searchParams.set(\"list-type\", \"2\");\n q.searchParams.set(\"prefix\", inBucket(p));\n if (token) q.searchParams.set(\"continuation-token\", token);\n const res = await send(\"GET\", q.toString());\n if (!res.ok) throw fail(\"listing\", res.status);\n const xml = await res.text();\n for (const block of xml.split(\"<Contents>\").slice(1)) {\n const key = unescapeXml(tag(block, \"Key\") ?? \"\");\n out.push({ key: prefix === \"\" ? key : key.slice(prefix.length + 1), size: Number(tag(block, \"Size\") ?? 0) });\n }\n token = tag(xml, \"IsTruncated\") === \"true\" ? unescapeXml(tag(xml, \"NextContinuationToken\") ?? \"\") || undefined : undefined;\n } while (token);\n return out.sort((a, b) => a.key.localeCompare(b.key));\n };\n return {\n async put(key, bytes, meta) {\n const res = await send(\"PUT\", urlOf(key), bytes, { \"content-type\": meta.contentType, ...(options.serverSideEncryption ? { \"x-amz-server-side-encryption\": \"AES256\" } : {}) });\n if (!res.ok) throw fail(\"write\", res.status);\n },\n async get(key) {\n const res = await send(\"GET\", urlOf(key));\n if (res.status === 404) return null;\n if (!res.ok) throw fail(\"read\", res.status);\n return { bytes: new Uint8Array(await res.arrayBuffer()), contentType: res.headers.get(\"content-type\") ?? typeOfName(key) };\n },\n async head(key) {\n const res = await send(\"HEAD\", urlOf(key));\n if (res.status === 404) return null;\n if (!res.ok) throw fail(\"head\", res.status);\n return { key, size: Number(res.headers.get(\"content-length\") ?? 0) };\n },\n async delete(key) {\n const res = await send(\"DELETE\", urlOf(key));\n if (!res.ok && res.status !== 404) throw fail(\"delete\", res.status);\n },\n list,\n async signedUrl(key, ttlSeconds) {\n return presignGet(creds, urlOf(key), ttlSeconds, options.clock.now());\n },\n publicUrl: (key) => (options.publicBaseUrl ? `${trimSlashes(options.publicBaseUrl, \"end\")}/${assertKey(key)}` : null),\n };\n}\n","import { STORAGE_CODE, StorageError } from \"./port\";\n\nconst PRIVATE_V4 = [/^10\\./, /^127\\./, /^169\\.254\\./, /^172\\.(1[6-9]|2\\d|3[01])\\./, /^192\\.168\\./, /^0\\./];\n\n/**\n * An endpoint an administrator typed (their own bucket) is called by our server: it must not reach inside our network.\n * Refuses a non-https scheme, credentials in the URL, `localhost`, and literal private, loopback and link-local addresses.\n * A name that RESOLVES to a private address is not caught here: the product's network egress rules are the second guard.\n * `allowLocal` (development, MinIO on the Mac) lets `http` and local addresses through.\n * @returns the origin, without a trailing slash\n * @throws StorageError `STORAGE_BAD_ENDPOINT`\n */\nexport function checkEndpoint(endpoint: string, options: { allowLocal?: boolean } = {}): string {\n const bad = (why: string) => new StorageError(STORAGE_CODE.badEndpoint, `endpoint ${why}`);\n let url: URL;\n try {\n url = new URL(endpoint);\n } catch {\n throw bad(\"is not a URL\");\n }\n if (url.username !== \"\" || url.password !== \"\") throw bad(\"must not carry credentials\");\n if (options.allowLocal) return url.origin;\n if (url.protocol !== \"https:\") throw bad(\"must be https\");\n const host = url.hostname.toLowerCase();\n if (host === \"localhost\" || host.endsWith(\".localhost\") || host.endsWith(\".internal\") || host.endsWith(\".local\")) throw bad(\"must not be a local name\");\n if (PRIVATE_V4.some((re) => re.test(host))) throw bad(\"must not be a private address\");\n if (host.startsWith(\"[\") && (host === \"[::1]\" || host.startsWith(\"[fc\") || host.startsWith(\"[fd\") || host.startsWith(\"[fe80\") || host.startsWith(\"[::ffff:\"))) throw bad(\"must not be a private address\");\n return url.origin;\n}\n","import { mkdir, readdir, readFile, rm, stat, writeFile } from \"node:fs/promises\";\nimport { dirname, join, resolve, sep } from \"node:path\";\n\nimport { assertKey, trimSlashes, typeOfName } from \"./keys\";\nimport type { ObjectInfo, ObjectStore } from \"./port\";\n\nexport interface FolderOptions {\n /** The folder every file lives under. */\n root: string;\n /** Where the folder is served from, when it is (a dev server): the public link. */\n publicBaseUrl?: string;\n}\n\n/** A folder on disk, for development, tests and the Mac apps. A key can never leave the root. */\nexport function folderStore(options: FolderOptions): ObjectStore {\n const root = resolve(options.root);\n const pathOf = (key: string) => {\n const path = resolve(join(root, assertKey(key)));\n if (!path.startsWith(root + sep)) throw new Error(\"a key cannot leave the root\");\n return path;\n };\n const walk = async (dir: string, base: string): Promise<ObjectInfo[]> => {\n let entries;\n try {\n entries = await readdir(dir, { withFileTypes: true });\n } catch {\n return [];\n }\n const out: ObjectInfo[] = [];\n for (const entry of entries) {\n const key = base === \"\" ? entry.name : `${base}/${entry.name}`;\n if (entry.isDirectory()) out.push(...(await walk(join(dir, entry.name), key)));\n else out.push({ key, size: (await stat(join(dir, entry.name))).size });\n }\n return out;\n };\n return {\n async put(key, bytes) {\n const path = pathOf(key);\n await mkdir(dirname(path), { recursive: true });\n await writeFile(path, bytes);\n },\n async get(key) {\n try {\n return { bytes: new Uint8Array(await readFile(pathOf(key))), contentType: typeOfName(key) };\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === \"ENOENT\" || (error as NodeJS.ErrnoException).code === \"EISDIR\") return null;\n throw error;\n }\n },\n async head(key) {\n try {\n const s = await stat(pathOf(key));\n return s.isFile() ? { key, size: s.size } : null;\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === \"ENOENT\") return null;\n throw error;\n }\n },\n async delete(key) {\n await rm(pathOf(key), { force: true });\n },\n async list(prefix) {\n const slash = prefix.lastIndexOf(\"/\");\n const dir = slash < 0 ? \"\" : prefix.slice(0, slash);\n const found = await walk(dir === \"\" ? root : join(root, dir), dir);\n return found.filter((f) => f.key.startsWith(prefix)).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","import { createHash } from \"node:crypto\";\n\nimport { assertKey, trimSlashes, typeOfName } from \"./keys\";\nimport { type Clock, type FetchLike, type ObjectInfo, type ObjectStore, STORAGE_CODE, StorageError } from \"./port\";\n\nexport interface RemoteStoreOptions {\n /** The storage API host (`storage.bunnycdn.com`, or a region's). */\n endpoint: string;\n /** The zone's name, the first segment of every path. */\n zone: string;\n /** The zone's password, sent as the `AccessKey` header. Never logged, never in a link we hand out. */\n accessKey: string;\n /** Every object lives under this prefix (no slashes at the ends), or none. */\n prefix?: string;\n /** The pull zone: with it, `publicUrl` answers; with its token key too, `signedUrl` does. */\n pullHostname?: string;\n tokenKey?: string;\n fetch: FetchLike;\n clock: Clock;\n}\n\ninterface Entry {\n ObjectName: string;\n IsDirectory: boolean;\n Length?: number;\n}\n\nconst fail = (what: string, status: number) => new StorageError(STORAGE_CODE.provider, `the storage refused the ${what} (${status})`);\n\n/**\n * An HTTP object store with the shape of Bunny Storage: `PUT`/`GET`/`DELETE` on `https://<endpoint>/<zone>/<prefix>/<key>` with an\n * `AccessKey` header, a directory listed by its path with a trailing slash (a JSON array of `ObjectName`, `IsDirectory`, `Length`),\n * and a pull zone for public and token-signed links. Reads and writes go through the authenticated API, never the public zone.\n */\nexport function remoteStore(options: RemoteStoreOptions): ObjectStore {\n const prefix = trimSlashes(options.prefix ?? \"\");\n const inZone = (key: string) => (prefix === \"\" ? \"\" : `${prefix}/`) + key;\n const base = `https://${options.endpoint}/${options.zone}`;\n const headers = { AccessKey: options.accessKey };\n const url = (key: string) => `${base}/${inZone(assertKey(key))}`;\n const listDir = async (dir: string): Promise<ObjectInfo[]> => {\n const root = prefix === \"\" ? \"\" : `${prefix}/`;\n const res = await options.fetch(dir === \"\" ? `${base}/${root}` : `${base}/${inZone(dir)}/`, { method: \"GET\", headers });\n if (res.status === 404) return [];\n if (!res.ok) throw fail(\"listing\", res.status);\n const entries = JSON.parse(await res.text()) as Entry[];\n const out: ObjectInfo[] = [];\n for (const e of entries) {\n const key = dir === \"\" ? e.ObjectName : `${dir}/${e.ObjectName}`;\n if (e.IsDirectory) out.push(...(await listDir(key)));\n else out.push({ key, size: e.Length ?? 0 });\n }\n return out;\n };\n const list = async (p: string) => {\n const slash = p.lastIndexOf(\"/\");\n return (await listDir(slash < 0 ? \"\" : p.slice(0, slash))).filter((f) => f.key.startsWith(p)).sort((a, b) => a.key.localeCompare(b.key));\n };\n return {\n async put(key, bytes, meta) {\n const res = await options.fetch(url(key), { method: \"PUT\", headers: { ...headers, \"content-type\": meta.contentType }, body: bytes });\n if (!res.ok) throw fail(\"write\", res.status);\n },\n async get(key) {\n const res = await options.fetch(url(key), { method: \"GET\", headers });\n if (res.status === 404) return null;\n if (!res.ok) throw fail(\"read\", res.status);\n return { bytes: new Uint8Array(await res.arrayBuffer()), contentType: res.headers.get(\"content-type\") ?? typeOfName(key) };\n },\n async head(key) {\n assertKey(key);\n return (await list(key)).find((f) => f.key === key) ?? null;\n },\n async delete(key) {\n const res = await options.fetch(url(key), { method: \"DELETE\", headers });\n if (!res.ok && res.status !== 404) throw fail(\"delete\", res.status);\n },\n list,\n async signedUrl(key, ttlSeconds) {\n if (!options.pullHostname || !options.tokenKey) return null;\n const path = `/${inZone(assertKey(key))}`;\n const expires = Math.floor(options.clock.now().valueOf() / 1000) + ttlSeconds;\n const token = createHash(\"sha256\").update(`${options.tokenKey}${path}${expires}`).digest(\"base64\").replaceAll(\"+\", \"-\").replaceAll(\"/\", \"_\").replaceAll(\"=\", \"\");\n return `https://${options.pullHostname}${path}?token=${token}&expires=${expires}`;\n },\n publicUrl: (key) => (options.pullHostname ? `https://${options.pullHostname}/${inZone(assertKey(key))}` : null),\n };\n}\n"],"mappings":"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","names":["createHash","fail","createHash"]}