@voltro/plugin-storage 0.85.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/CHANGELOG.md +666 -0
- package/README.md +50 -0
- package/THIRD-PARTY-NOTICES.md +5 -33
- package/dist/access.js +47 -0
- package/dist/assetRef.js +13 -0
- package/dist/avatar.js +39 -0
- package/dist/constraints.js +93 -0
- package/dist/derivatives.js +30 -0
- package/dist/env.js +156 -0
- package/dist/grant.js +112 -0
- package/dist/index.d.ts +93 -9
- package/dist/index.js +581 -3185
- package/dist/ingest.js +81 -0
- package/dist/ingestPolicy.js +1 -0
- package/dist/metadataError.js +19 -0
- package/dist/objectLifecycle.js +209 -0
- package/dist/objectMetadata.js +150 -0
- package/dist/objectWriter.js +58 -0
- package/dist/pathDurability.js +16 -0
- package/dist/providers.js +493 -0
- package/dist/referenceCatalogue.js +31 -0
- package/dist/rpc.d.ts +2 -0
- package/dist/rpc.js +3 -1
- package/dist/scanners.js +46 -0
- package/dist/service.js +1084 -0
- package/dist/storageNamespaces.js +56 -0
- package/dist/storageNumbers.js +13 -0
- package/dist/storageObjectNamespaces.js +194 -0
- package/dist/storageReferenceInstances.js +52 -0
- package/dist/streamBridge.js +30 -0
- package/dist/table.js +53 -0
- package/dist/transcodeVideo.js +53 -0
- package/dist/transform.js +90 -0
- package/dist/transformCache.js +31 -0
- package/dist/types.d.ts +4 -0
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -20,7 +20,57 @@ Installation, guides, the full API reference, and examples all live in the
|
|
|
20
20
|
**[Voltro documentation](https://docs.voltro.dev/docs/intro/getting-started)** — the single source of truth. This README is
|
|
21
21
|
deliberately short so there is nothing here to drift out of date.
|
|
22
22
|
|
|
23
|
+
### Bounded HTTP source reader
|
|
24
|
+
|
|
25
|
+
Server integrations may use `fetchIngestBody(url, maxBytes?, policy?)` to collect
|
|
26
|
+
bounded, interruptible HTTP bytes before validating an expected digest or size.
|
|
27
|
+
The default limit is 64 MiB. Redirects and internal addresses use `IngestPolicy`;
|
|
28
|
+
`allowHosts` exempts named internal hosts, not an exclusive source allow-list.
|
|
29
|
+
The reader creates no references and performs no authorization, scanning or
|
|
30
|
+
persistence. Use `StorageService.put` after validation for the complete storage
|
|
31
|
+
pipeline, bound request duration, and protect network egress against DNS rebinding.
|
|
32
|
+
Redact capability URLs from errors. Bounded buffering is not a process-RSS limit.
|
|
33
|
+
|
|
23
34
|
## License
|
|
24
35
|
|
|
36
|
+
|
|
25
37
|
Proprietary. © 2026 Voltro UG. Your use is governed by the
|
|
26
38
|
[Terms of Service](https://voltro.cloud/legal/terms). See the bundled `LICENSE`.
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
### Reference catalogues and combined replica inspection
|
|
42
|
+
|
|
43
|
+
A physical provider namespace identifies object bytes. A reference catalogue identifies the metadata view that lists those objects. They are separate: independent reference stores may use the same provider, and one database may contain views for independent providers or named storage instances.
|
|
44
|
+
|
|
45
|
+
`RefStore.catalogueId` is a required `Effect<string, StorageError>` with a stable SHA-256 identity for its actual catalogue view. Replicas reading the same view must agree; independent catalogues must differ even if their reference IDs or contents match. Custom stores must retain this identity with their metadata. Wrappers forward the underlying effect. `memoryRefStore()` creates one identity per actual map, not per read.
|
|
46
|
+
|
|
47
|
+
Configured default stores initialize `_voltro_storage_reference_catalogue` after normal schema reconciliation. Production starts still require applying the normal schema first. Preserve the catalogue marker on restart, backup and restore. If database copies will run concurrently as independent catalogues, assign them distinct markers through a reviewed stopped-service data migration. Do not treat two independent copies as one catalogue merely because a backup copied its marker.
|
|
48
|
+
|
|
49
|
+
Direct `dataStoreRefStore` consumers register `storageReferenceCatalogueTable`, reconcile their schema and call `ensureStorageReferenceCatalogue(store)` once at boot before inspection. The adapter's `catalogueId` read never writes or repairs missing or corrupt identity. Its view identity additionally includes the configured storage instance and physical provider namespace.
|
|
50
|
+
|
|
51
|
+
The `/refs` and `/stats` inspect reads report their catalogue, provider namespace and storage instance in `source`. Combined reads expose `sources` and attach source identity and answering replica IDs to each reference. Shared catalogue rows and counters appear once; independent catalogues remain separate, including equal reference IDs. The observation envelope retains unavailable-replica evidence. Counts describe reference metadata, not a complete inventory of unindexed provider objects.
|
|
52
|
+
|
|
53
|
+
Each catalogue contributes up to 50 references for the requested page offset. Combined pages can therefore contain more than 50 files. Page advancement moves the same 50-reference window across all catalogues; ordering is newest first within the combined window. This is not global pagination across a single merged index. Divergent snapshots from the same shared catalogue are refused with an explicit comparison/retry message; no responder wins silently and counters are not guessed.
|
|
54
|
+
|
|
55
|
+
The shared Storage browser displays file provenance and catalogue page semantics. Choose one replica before uploading, downloading, sharing, revoking access or deleting. Multi-replica browsing does not fan out file operations. Custom hosts must pass `StoragePage.operationTargetAvailable={false}` while multiple replicas or all replicas are selected, and use a distinct upload scope when the target changes. Server authorization and explicit mutation-target checks remain authoritative.
|
|
56
|
+
|
|
57
|
+
Combined instance choosers offer only identical plugin declarations from every answering replica. Differing or absent installations are named explicitly; select a single replica to inspect those instances. Missing replica answers remain visible in the observation notice.
|
|
58
|
+
|
|
59
|
+
## Through-app ticket retries
|
|
60
|
+
|
|
61
|
+
With the application's DataStore and default reference/grant stores bound, ordinary and Inspect through-app upload tickets carry one native object operation. The ordinary ticket response exposes its optional `operationId`. Keep the issued upload URL to retry the same transfer: repeated or concurrent POSTs with the same bytes converge on one reference in that physical storage namespace.
|
|
62
|
+
|
|
63
|
+
A provider failure retains the operation's intent and reservation until it is resumed or explicitly retired through the server's object-operation lifecycle. Ticket expiry alone does not release that reservation. Its reference becomes visible after the bytes are confirmed; retrying the same ticket resumes that intent. Different bytes or immutable inputs under that operation are refused with HTTP 409. Upload and resumable HTTP responses retain a provider's declared error status.
|
|
64
|
+
|
|
65
|
+
Pass the returned `operationId` in a fresh authorized `storage.mintUploadTicket` request with the same metadata to renew an expired ticket and resume its retained transfer. A caller can also choose a stable `operationId` before the first request to recover a lost ticket acknowledgement. This identity is scoped to the current upload owner, tenant and configured storage instance; it is not an authorization credential. Omitting `operationId` starts another operation. Custom reference or grant stores do not bind the native object-operation boundary and do not advertise its operation ID. Presigned, resumable and multipart transfers retain their separate transport contracts. Provider I/O stays outside retryable metadata transactions; the operation does not add a later business-authorization check.
|
|
66
|
+
|
|
67
|
+
`useUpload` retains an advertised ticket `operationId` across its automatic through-app retries. To recover a lost first mint reply or an explicit later UI retry, choose `operationId` before the first `upload(file, { operationId, prefer: 'through-app' })` call and retain it with that same file and metadata. A new file or business scope needs a new identity. `uploadMany` rejects one explicit identity shared by multiple files before dispatch. Without native operation support, omit the explicit identity; custom stores do not acquire an exactly-once guarantee. This does not persist a browser transfer across a page reload or extend presigned, resumable or multipart contracts.
|
|
68
|
+
|
|
69
|
+
Object keys are physical provider keys. Native operation-backed writes use their existing content-addressed namespace; use the reference ID and approved ticket binding instead of inferring authority from a logical path inside a physical key.
|
|
70
|
+
|
|
71
|
+
## Signed token deadlines
|
|
72
|
+
|
|
73
|
+
Native signed read grants and upload, finalize, resumable and multipart tickets expire at their signed Unix-second deadline. Verification refuses the exact deadline and every later instant; a zero-second lifetime grants no usable interval. Obtain a new token through the authorized issuer when another transfer or read is needed.
|
|
74
|
+
|
|
75
|
+
The ticket keeps its approved owner, tenant, key and access policy. Its deadline does not turn redemption into a session login or rerun the application's authorization function. Provider-presigned URLs retain their provider's expiry contract; this rule concerns Voltro's native signed tokens.
|
|
76
|
+
|
package/THIRD-PARTY-NOTICES.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# Third-Party Notices — @voltro/plugin-storage
|
|
2
2
|
|
|
3
|
-
This package
|
|
3
|
+
This package uses the open-source software listed below. Each is the
|
|
4
4
|
property of its respective copyright holders and is used under the terms of
|
|
5
5
|
its license. This file is provided for attribution; it grants no rights in
|
|
6
6
|
@voltro/plugin-storage itself, which is proprietary (see LICENSE).
|
|
7
7
|
|
|
8
|
-
Generated from the resolved runtime dependency closure (
|
|
8
|
+
Generated from the resolved runtime dependency closure (108 packages).
|
|
9
9
|
|
|
10
10
|
---
|
|
11
11
|
|
|
@@ -9624,34 +9624,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
|
9624
9624
|
SOFTWARE.
|
|
9625
9625
|
```
|
|
9626
9626
|
|
|
9627
|
-
## layerr@3.0.0
|
|
9628
|
-
|
|
9629
|
-
License: MIT
|
|
9630
|
-
|
|
9631
|
-
```
|
|
9632
|
-
MIT License
|
|
9633
|
-
|
|
9634
|
-
Copyright (c) 2020 Perry Mitchell
|
|
9635
|
-
|
|
9636
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
9637
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
9638
|
-
in the Software without restriction, including without limitation the rights
|
|
9639
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9640
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
9641
|
-
furnished to do so, subject to the following conditions:
|
|
9642
|
-
|
|
9643
|
-
The above copyright notice and this permission notice shall be included in all
|
|
9644
|
-
copies or substantial portions of the Software.
|
|
9645
|
-
|
|
9646
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
9647
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
9648
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
9649
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
9650
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
9651
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
9652
|
-
SOFTWARE.
|
|
9653
|
-
```
|
|
9654
|
-
|
|
9655
9627
|
## lib0@0.2.117
|
|
9656
9628
|
|
|
9657
9629
|
License: MIT
|
|
@@ -10459,14 +10431,14 @@ Apache License
|
|
|
10459
10431
|
limitations under the License.
|
|
10460
10432
|
```
|
|
10461
10433
|
|
|
10462
|
-
##
|
|
10434
|
+
## ulid@3.0.2
|
|
10463
10435
|
|
|
10464
10436
|
License: MIT
|
|
10465
10437
|
|
|
10466
10438
|
```
|
|
10467
|
-
MIT License
|
|
10439
|
+
The MIT License (MIT)
|
|
10468
10440
|
|
|
10469
|
-
Copyright (c)
|
|
10441
|
+
Copyright (c) 2017 Alizain Feerasta
|
|
10470
10442
|
|
|
10471
10443
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
10472
10444
|
of this software and associated documentation files (the "Software"), to deal
|
package/dist/access.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { randomBytes as e, scryptSync as t, timingSafeEqual as n } from "node:crypto";
|
|
2
|
+
import { Effect as r } from "effect";
|
|
3
|
+
//#region src/access.ts
|
|
4
|
+
var i = (n) => {
|
|
5
|
+
let r = e(16).toString("hex");
|
|
6
|
+
return `${r}:${t(n, r, 32).toString("hex")}`;
|
|
7
|
+
}, a = (e, r) => {
|
|
8
|
+
if (!e || !r) return !1;
|
|
9
|
+
let [i, a] = r.split(":");
|
|
10
|
+
if (!i || !a) return !1;
|
|
11
|
+
let o = Buffer.from(a, "hex"), s = t(e, i, o.length);
|
|
12
|
+
return s.length === o.length && n(s, o);
|
|
13
|
+
}, o = (e) => Array.isArray(e) ? e.filter((e) => typeof e == "string") : [], s = (e) => o(e.metadata?.roles), c = (e) => o(e.scopes), l = (e, t) => e.some((e) => t.includes(e)), u = (e) => o(e.metadata?.groups), d = (e) => typeof e == "boolean" ? r.succeed(e) : r.isEffect(e) ? e : r.promise(() => e), f = (e, t, n = {}) => r.gen(function* () {
|
|
14
|
+
if (t.id && e.ownerId && t.id === e.ownerId) return !0;
|
|
15
|
+
let i = e.accessPolicy ?? [];
|
|
16
|
+
if (i.length === 0) return !1;
|
|
17
|
+
let a = n.resolveGroups ? yield* r.promise(async () => await n.resolveGroups(t)) : u(t);
|
|
18
|
+
for (let r of i) if (yield* p(r, e, t, a, n)) return !0;
|
|
19
|
+
return !1;
|
|
20
|
+
}), p = (e, t, n, i, o) => r.gen(function* () {
|
|
21
|
+
let r = !1, u = (e) => (r = !0, e);
|
|
22
|
+
if (e.owner !== void 0 && e.owner && !u(!!n.id && !!t.ownerId && n.id === t.ownerId) || e.tenant !== void 0 && e.tenant && !u(!!n.tenantId && n.tenantId === t.tenantId) || e.apiKey !== void 0 && e.apiKey && !u(n.type === "apiKey") || e.roles && e.roles.length > 0 && !u(l(s(n), e.roles)) || e.groups && e.groups.length > 0 && !u(l(i, e.groups)) || e.scopes && e.scopes.length > 0 && !u(l(c(n), e.scopes)) || e.password !== void 0 && e.password && !u(a(o.password, t.passwordHash))) return !1;
|
|
23
|
+
if (e.guard) {
|
|
24
|
+
let r = o.guards?.[e.guard];
|
|
25
|
+
if (!u(r ? yield* d(r({
|
|
26
|
+
subject: n,
|
|
27
|
+
ref: t
|
|
28
|
+
})) : !1)) return !1;
|
|
29
|
+
}
|
|
30
|
+
return r;
|
|
31
|
+
}), m = (e, t) => {
|
|
32
|
+
let n = [];
|
|
33
|
+
e.id && n.push({
|
|
34
|
+
type: e.type === "apiKey" ? "apiKey" : "user",
|
|
35
|
+
id: e.id
|
|
36
|
+
});
|
|
37
|
+
for (let e of t) n.push({
|
|
38
|
+
type: "group",
|
|
39
|
+
id: e
|
|
40
|
+
});
|
|
41
|
+
return n;
|
|
42
|
+
}, h = (e, t, n, r, i) => {
|
|
43
|
+
let a = m(t, n);
|
|
44
|
+
return e.some((e) => e.expiresAt && e.expiresAt.getTime() < i || r === "write" && e.permission !== "write" ? !1 : a.some((t) => t.type === e.principalType && t.id === e.principalId));
|
|
45
|
+
}, g = (e, t) => t.resolveGroups ? r.promise(async () => await t.resolveGroups(e)) : r.succeed(u(e));
|
|
46
|
+
//#endregion
|
|
47
|
+
export { f as evaluatePolicy, h as grantsAllow, i as hashPassword, g as resolveSubjectGroups, m as subjectPrincipals, a as verifyPassword };
|
package/dist/assetRef.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { _voltroStorageRefsTable as e } from "./table.js";
|
|
2
|
+
import { mixin as t, reference as n, text as r } from "@voltro/database";
|
|
3
|
+
//#region src/assetRef.ts
|
|
4
|
+
function i(t = {}) {
|
|
5
|
+
let i = t.nullable ?? !0, a = t.fk === !1 ? r().externalId() : n(() => e, { onDelete: t.onDelete ?? "setNull" });
|
|
6
|
+
return i ? a.nullable() : a;
|
|
7
|
+
}
|
|
8
|
+
var a = () => t({
|
|
9
|
+
id: "@voltro/plugin-storage/withStorage",
|
|
10
|
+
fields: { asset: i() }
|
|
11
|
+
});
|
|
12
|
+
//#endregion
|
|
13
|
+
export { i as assetRef, a as withStorage };
|
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 };
|