@voltro/plugin-storage 0.84.0 → 0.86.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/avatar.js ADDED
@@ -0,0 +1,39 @@
1
+ import { StorageError as e } from "./types.js";
2
+ import { Effect as t } from "effect";
3
+ //#region src/avatar.ts
4
+ var n = {
5
+ "image/png": "png",
6
+ "image/jpeg": "jpg",
7
+ "image/jpg": "jpg",
8
+ "image/gif": "gif",
9
+ "image/webp": "webp",
10
+ "image/svg+xml": "svg"
11
+ }, r = (e) => {
12
+ let t = /^data:([^;,]+)(;base64)?,(.*)$/s.exec(e);
13
+ if (!t) throw Error("not a data URI");
14
+ let n = t[1] || "application/octet-stream", r = !!t[2], i = t[3] ?? "";
15
+ return {
16
+ bytes: r ? new Uint8Array(Buffer.from(i, "base64")) : new Uint8Array(Buffer.from(decodeURIComponent(i), "utf8")),
17
+ contentType: n
18
+ };
19
+ }, i = (i, a) => t.gen(function* () {
20
+ let o = yield* t.try({
21
+ try: () => r(a.dataUri),
22
+ catch: (t) => new e({
23
+ provider: "avatar",
24
+ message: String(t?.message ?? t),
25
+ transient: !1,
26
+ status: 400
27
+ })
28
+ }), s = n[o.contentType] ?? "bin";
29
+ return yield* i.put({
30
+ bytes: o.bytes,
31
+ contentType: o.contentType,
32
+ tenantId: a.tenantId,
33
+ ownerId: a.userId,
34
+ visibility: "public",
35
+ key: `avatars/${a.userId}/avatar.${s}`
36
+ });
37
+ });
38
+ //#endregion
39
+ export { r as decodeDataUri, i as putAvatar };
@@ -0,0 +1,93 @@
1
+ //#region src/constraints.ts
2
+ var e = (e, t) => {
3
+ if (!t || t.length === 0) return !0;
4
+ let n = e.toLowerCase();
5
+ return t.some((e) => {
6
+ let t = e.toLowerCase();
7
+ return t.endsWith("/*") ? n.startsWith(t.slice(0, -1)) : n === t;
8
+ });
9
+ }, t = [
10
+ {
11
+ ct: /^image\/png$/,
12
+ offset: 0,
13
+ magic: [
14
+ 137,
15
+ 80,
16
+ 78,
17
+ 71
18
+ ]
19
+ },
20
+ {
21
+ ct: /^image\/jpe?g$/,
22
+ offset: 0,
23
+ magic: [
24
+ 255,
25
+ 216,
26
+ 255
27
+ ]
28
+ },
29
+ {
30
+ ct: /^image\/gif$/,
31
+ offset: 0,
32
+ magic: [
33
+ 71,
34
+ 73,
35
+ 70,
36
+ 56
37
+ ]
38
+ },
39
+ {
40
+ ct: /^image\/webp$/,
41
+ offset: 0,
42
+ magic: [
43
+ 82,
44
+ 73,
45
+ 70,
46
+ 70
47
+ ]
48
+ },
49
+ {
50
+ ct: /^image\/bmp$/,
51
+ offset: 0,
52
+ magic: [66, 77]
53
+ },
54
+ {
55
+ ct: /^application\/pdf$/,
56
+ offset: 0,
57
+ magic: [
58
+ 37,
59
+ 80,
60
+ 68,
61
+ 70
62
+ ]
63
+ },
64
+ {
65
+ ct: /^application\/zip$/,
66
+ offset: 0,
67
+ magic: [
68
+ 80,
69
+ 75,
70
+ 3,
71
+ 4
72
+ ]
73
+ },
74
+ {
75
+ ct: /^application\/gzip$/,
76
+ offset: 0,
77
+ magic: [31, 139]
78
+ }
79
+ ], n = (e, n) => {
80
+ let r = n.toLowerCase(), i = t.find((e) => e.ct.test(r));
81
+ return i ? e.length < i.offset + i.magic.length ? !1 : i.magic.every((t, n) => e[i.offset + n] === t) : !0;
82
+ }, r = (t, r, i) => i ? i.maxBytes !== void 0 && t.byteLength > i.maxBytes ? {
83
+ reason: "too-large",
84
+ detail: `${t.byteLength} bytes exceeds the ${i.maxBytes}-byte limit`
85
+ } : e(r, i.allowedContentTypes) ? i.sniff && !n(t, r) ? {
86
+ reason: "content-mismatch",
87
+ detail: `bytes do not match the declared content type '${r}'`
88
+ } : null : {
89
+ reason: "content-type-not-allowed",
90
+ detail: `content type '${r}' is not in the allow-list`
91
+ } : null;
92
+ //#endregion
93
+ export { r as checkLimits, e as contentTypeAllowed, n as sniffMatches };
@@ -0,0 +1,30 @@
1
+ import { isTransformable as e, loadSharp as t } from "./transform.js";
2
+ //#region src/derivatives.ts
3
+ var n = async (n, r, i) => {
4
+ if (!e(r)) return { bytes: n };
5
+ let a = await t();
6
+ if (!a) return { bytes: n };
7
+ try {
8
+ let e = n;
9
+ if (i?.normalize) {
10
+ let t = await a(Buffer.from(n), { failOn: "none" }).rotate().toBuffer();
11
+ e = new Uint8Array(t);
12
+ }
13
+ let t = await a(Buffer.from(e), { failOn: "none" }).metadata(), r = await a(Buffer.from(e), { failOn: "none" }).resize({
14
+ width: 24,
15
+ height: 24,
16
+ fit: "inside",
17
+ withoutEnlargement: !0
18
+ }).webp({ quality: 40 }).toBuffer().catch(() => null);
19
+ return {
20
+ bytes: e,
21
+ ...typeof t.width == "number" ? { width: t.width } : {},
22
+ ...typeof t.height == "number" ? { height: t.height } : {},
23
+ ...r ? { placeholder: `data:image/webp;base64,${r.toString("base64")}` } : {}
24
+ };
25
+ } catch {
26
+ return { bytes: n };
27
+ }
28
+ };
29
+ //#endregion
30
+ export { n as extractImageMetadata };
package/dist/env.js ADDED
@@ -0,0 +1,156 @@
1
+ import { pluginEnv as e } from "@voltro/env";
2
+ //#region src/env.ts
3
+ var t = e([
4
+ {
5
+ name: "STORAGE_PROVIDER",
6
+ required: !1,
7
+ secret: !1,
8
+ description: "Provider name: 's3' | 'minio' | 'azure' | 'database' | 'filesystem' | 'memory'. Default 'memory'."
9
+ },
10
+ {
11
+ name: "S3_BUCKET",
12
+ required: !1,
13
+ secret: !1,
14
+ description: "S3/MinIO bucket name. Default STORAGE_BUCKET, else \"voltro\"."
15
+ },
16
+ {
17
+ name: "STORAGE_BUCKET",
18
+ required: !1,
19
+ secret: !1,
20
+ description: "Fallback bucket name when S3_BUCKET is unset."
21
+ },
22
+ {
23
+ name: "S3_REGION",
24
+ required: !1,
25
+ secret: !1,
26
+ description: "S3 region. Falls back to AWS_REGION."
27
+ },
28
+ {
29
+ name: "AWS_REGION",
30
+ required: !1,
31
+ secret: !1,
32
+ description: "AWS region fallback for the S3 provider."
33
+ },
34
+ {
35
+ name: "S3_ENDPOINT",
36
+ required: !1,
37
+ secret: !1,
38
+ description: "Custom S3 endpoint URL. Falls back to MINIO_ENDPOINT."
39
+ },
40
+ {
41
+ name: "MINIO_ENDPOINT",
42
+ required: !1,
43
+ secret: !1,
44
+ description: "MinIO endpoint URL fallback for the S3 provider."
45
+ },
46
+ {
47
+ name: "S3_ACCESS_KEY_ID",
48
+ required: !1,
49
+ secret: !0,
50
+ description: "S3 access key id. Falls back to AWS_ACCESS_KEY_ID."
51
+ },
52
+ {
53
+ name: "AWS_ACCESS_KEY_ID",
54
+ required: !1,
55
+ secret: !0,
56
+ description: "AWS access key id fallback for the S3 provider."
57
+ },
58
+ {
59
+ name: "S3_SECRET_ACCESS_KEY",
60
+ required: !1,
61
+ secret: !0,
62
+ description: "S3 secret access key. Falls back to AWS_SECRET_ACCESS_KEY."
63
+ },
64
+ {
65
+ name: "AWS_SECRET_ACCESS_KEY",
66
+ required: !1,
67
+ secret: !0,
68
+ description: "AWS secret access key fallback for the S3 provider."
69
+ },
70
+ {
71
+ name: "S3_FORCE_PATH_STYLE",
72
+ required: !1,
73
+ secret: !1,
74
+ description: "Set to \"1\" to force path-style S3 addressing."
75
+ },
76
+ {
77
+ name: "STORAGE_CDN_URL",
78
+ required: !1,
79
+ secret: !1,
80
+ description: "Public base URL (CDN) served in front of the bucket for public objects."
81
+ },
82
+ {
83
+ name: "VOLTRO_PUBLIC_URL",
84
+ required: !1,
85
+ secret: !1,
86
+ description: "Absolute public base URL of THIS api (scheme://host[:port]) — prepended to the /_voltro/storage/:id serve URL so getUrl/mintUrl/the upload route emit ABSOLUTE URLs. Required when the api is a different origin than the app."
87
+ },
88
+ {
89
+ name: "STORAGE_ALLOWED_ORIGINS",
90
+ required: !1,
91
+ secret: !1,
92
+ description: "Comma-separated origins allowed to read the serve route + POST the upload route cross-origin (CORS). Use '*' for any. Omit for same-origin only."
93
+ },
94
+ {
95
+ name: "AZURE_STORAGE_ACCOUNT",
96
+ required: !1,
97
+ secret: !1,
98
+ description: "Azure storage account name (provider \"azure\")."
99
+ },
100
+ {
101
+ name: "AZURE_STORAGE_KEY",
102
+ required: !1,
103
+ secret: !0,
104
+ description: "Azure account key — needed to mint SAS URLs."
105
+ },
106
+ {
107
+ name: "AZURE_STORAGE_CONNECTION_STRING",
108
+ required: !1,
109
+ secret: !0,
110
+ description: "Azure connection string (carries the account key)."
111
+ },
112
+ {
113
+ name: "AZURE_STORAGE_ENDPOINT",
114
+ required: !1,
115
+ secret: !1,
116
+ description: "Custom Azure blob endpoint (e.g. Azurite)."
117
+ },
118
+ {
119
+ name: "AZURE_STORAGE_CONTAINER",
120
+ required: !1,
121
+ secret: !1,
122
+ description: "Azure blob container name (the \"bucket\")."
123
+ },
124
+ {
125
+ name: "STORAGE_ROOT",
126
+ required: !1,
127
+ secret: !1,
128
+ description: "Filesystem provider root directory. Default \".voltro-storage\"."
129
+ },
130
+ {
131
+ name: "VOLTRO_STORAGE_SECRET",
132
+ required: !1,
133
+ secret: !0,
134
+ description: "HMAC secret for signed grant tokens. Falls back to the session secret."
135
+ },
136
+ {
137
+ name: "VOLTRO_SESSION_COOKIE",
138
+ required: !1,
139
+ secret: !1,
140
+ description: "Session cookie name used to resolve private-object access. Default \"voltro:session\"."
141
+ },
142
+ {
143
+ name: "CLAMAV_HOST",
144
+ required: !1,
145
+ secret: !1,
146
+ description: "clamd host for the optional clamav virus scanner. Default 127.0.0.1."
147
+ },
148
+ {
149
+ name: "CLAMAV_PORT",
150
+ required: !1,
151
+ secret: !1,
152
+ description: "clamd port for the optional clamav virus scanner. Default 3310."
153
+ }
154
+ ]);
155
+ //#endregion
156
+ export { t as storageEnv };
package/dist/grant.js ADDED
@@ -0,0 +1,112 @@
1
+ import { storageEnv as e } from "./env.js";
2
+ import { createHmac as t, timingSafeEqual as n } from "node:crypto";
3
+ import { Exit as r, Schema as i } from "effect";
4
+ import { StorageAccessPolicySchema as a } from "@voltro/protocol";
5
+ import { resolveSessionSecret as o } from "@voltro/protocol/session";
6
+ //#region src/grant.ts
7
+ var s = () => e.read("VOLTRO_STORAGE_SECRET") ?? o(), c = (e) => (typeof e == "string" ? Buffer.from(e, "utf8") : e).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""), l = (e) => {
8
+ let t = e.replace(/-/g, "+").replace(/_/g, "/") + "=".repeat((4 - e.length % 4) % 4);
9
+ return Buffer.from(t, "base64");
10
+ }, u = (e, n, r = s(), i = "/_voltro/storage") => {
11
+ if (!r) throw Error("signGrant: secret must be a non-empty string");
12
+ let a = {
13
+ refId: e,
14
+ audience: i,
15
+ exp: Math.floor(Date.now() / 1e3) + Math.floor(n)
16
+ }, o = c(JSON.stringify(a));
17
+ return `${o}.${c(t("sha256", r).update(o).digest())}`;
18
+ }, d = (e, r = s()) => {
19
+ if (!e || !r) return null;
20
+ let i = e.indexOf(".");
21
+ if (i <= 0 || i === e.length - 1) return null;
22
+ let a = e.slice(0, i), o = e.slice(i + 1), c = t("sha256", r).update(a).digest(), u = l(o);
23
+ if (u.length !== c.length || !n(u, c)) return null;
24
+ try {
25
+ let e = JSON.parse(l(a).toString("utf8"));
26
+ return typeof e.audience != "string" || !e.audience || typeof e.refId != "string" || typeof e.exp != "number" || e.exp <= Math.floor(Date.now() / 1e3) ? null : e;
27
+ } catch {
28
+ return null;
29
+ }
30
+ }, f = (e) => e.version !== 2 || typeof e.writeScope != "string" || !/^[a-f0-9]{64}$/.test(e.writeScope) || Object.hasOwn(e, "replicaId") || e.issuerReplicaId !== void 0 && (typeof e.issuerReplicaId != "string" || !e.issuerReplicaId.trim()) ? !1 : e.access === void 0 || e.vis === "private" && r.isSuccess(i.decodeUnknownExit(a, { onExcessProperty: "error" })(e.access)), p = (e, n, r = s()) => {
31
+ if (!r) throw Error("signUploadTicket: secret must be a non-empty string");
32
+ let i = {
33
+ ...e,
34
+ version: 2,
35
+ exp: Math.floor(Date.now() / 1e3) + Math.floor(n)
36
+ }, a = c(JSON.stringify(i));
37
+ return `${a}.${c(t("sha256", r).update(`u:${a}`).digest())}`;
38
+ }, m = (e, r = s()) => {
39
+ if (!e || !r) return null;
40
+ let i = e.indexOf(".");
41
+ if (i <= 0 || i === e.length - 1) return null;
42
+ let a = e.slice(0, i), o = e.slice(i + 1), c = t("sha256", r).update(`u:${a}`).digest(), u = l(o);
43
+ if (u.length !== c.length || !n(u, c)) return null;
44
+ try {
45
+ let e = JSON.parse(l(a).toString("utf8"));
46
+ return !f(e) || e.operationId !== void 0 && (typeof e.operationId != "string" || !e.operationId.trim() || e.operationId.length > 200) || typeof e.instance != "string" || !e.instance || typeof e.ct != "string" || typeof e.exp != "number" || e.vis !== "public" && e.vis !== "private" || e.exp <= Math.floor(Date.now() / 1e3) ? null : e;
47
+ } catch {
48
+ return null;
49
+ }
50
+ }, h = (e, n, r = s()) => {
51
+ if (!r) throw Error("signFinalizeTicket: secret must be a non-empty string");
52
+ let i = {
53
+ ...e,
54
+ version: 2,
55
+ exp: Math.floor(Date.now() / 1e3) + Math.floor(n)
56
+ }, a = c(JSON.stringify(i));
57
+ return `${a}.${c(t("sha256", r).update(`f:${a}`).digest())}`;
58
+ }, g = (e, r = s()) => {
59
+ if (!e || !r) return null;
60
+ let i = e.indexOf(".");
61
+ if (i <= 0 || i === e.length - 1) return null;
62
+ let a = e.slice(0, i), o = e.slice(i + 1), c = t("sha256", r).update(`f:${a}`).digest(), u = l(o);
63
+ if (u.length !== c.length || !n(u, c)) return null;
64
+ try {
65
+ let e = JSON.parse(l(a).toString("utf8"));
66
+ return !f(e) || typeof e.key != "string" || e.key.length === 0 || typeof e.instance != "string" || !e.instance || typeof e.ct != "string" || typeof e.exp != "number" || e.vis !== "public" && e.vis !== "private" || e.exp <= Math.floor(Date.now() / 1e3) ? null : e;
67
+ } catch {
68
+ return null;
69
+ }
70
+ }, _ = (e, n, r = s()) => {
71
+ if (!r) throw Error("signResumableTicket: secret must be a non-empty string");
72
+ let i = {
73
+ ...e,
74
+ version: 2,
75
+ exp: Math.floor(Date.now() / 1e3) + Math.floor(n)
76
+ }, a = c(JSON.stringify(i));
77
+ return `${a}.${c(t("sha256", r).update(`r:${a}`).digest())}`;
78
+ }, v = (e, r = s()) => {
79
+ if (!e || !r) return null;
80
+ let i = e.indexOf(".");
81
+ if (i <= 0 || i === e.length - 1) return null;
82
+ let a = e.slice(0, i), o = e.slice(i + 1), c = t("sha256", r).update(`r:${a}`).digest(), u = l(o);
83
+ if (u.length !== c.length || !n(u, c)) return null;
84
+ try {
85
+ let e = JSON.parse(l(a).toString("utf8"));
86
+ return !f(e) || typeof e.id != "string" || e.id.length === 0 || typeof e.size != "number" || e.size < 0 || typeof e.instance != "string" || !e.instance || typeof e.ct != "string" || typeof e.exp != "number" || e.vis !== "public" && e.vis !== "private" || e.exp <= Math.floor(Date.now() / 1e3) ? null : e;
87
+ } catch {
88
+ return null;
89
+ }
90
+ }, y = (e, n, r = s()) => {
91
+ if (!r) throw Error("signMultipartTicket: secret must be a non-empty string");
92
+ let i = {
93
+ ...e,
94
+ version: 2,
95
+ exp: Math.floor(Date.now() / 1e3) + Math.floor(n)
96
+ }, a = c(JSON.stringify(i));
97
+ return `${a}.${c(t("sha256", r).update(`m:${a}`).digest())}`;
98
+ }, b = (e, r = s()) => {
99
+ if (!e || !r) return null;
100
+ let i = e.indexOf(".");
101
+ if (i <= 0 || i === e.length - 1) return null;
102
+ let a = e.slice(0, i), o = e.slice(i + 1), c = t("sha256", r).update(`m:${a}`).digest(), u = l(o);
103
+ if (u.length !== c.length || !n(u, c)) return null;
104
+ try {
105
+ let e = JSON.parse(l(a).toString("utf8"));
106
+ return !f(e) || typeof e.key != "string" || e.key.length === 0 || typeof e.uploadId != "string" || e.uploadId.length === 0 || typeof e.instance != "string" || !e.instance || typeof e.ct != "string" || typeof e.exp != "number" || e.vis !== "public" && e.vis !== "private" || e.exp <= Math.floor(Date.now() / 1e3) ? null : e;
107
+ } catch {
108
+ return null;
109
+ }
110
+ };
111
+ //#endregion
112
+ export { s as resolveStorageSecret, h as signFinalizeTicket, u as signGrant, y as signMultipartTicket, _ as signResumableTicket, p as signUploadTicket, g as verifyFinalizeTicket, d as verifyGrant, b as verifyMultipartTicket, v as verifyResumableTicket, m as verifyUploadTicket };