@remit/storage-service 0.0.11 → 0.0.13

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/src/s3.ts DELETED
@@ -1,36 +0,0 @@
1
- import { S3Client } from "@aws-sdk/client-s3";
2
- import { createFilesystemStorageService } from "./backends/filesystem.js";
3
- import { createS3StorageService } from "./backends/s3.js";
4
- import type { StorageService } from "./storage.js";
5
-
6
- export { createS3StorageService } from "./backends/s3.js";
7
-
8
- export const createStorageService = (): StorageService => {
9
- const bucketName = process.env.S3_BUCKET_NAME;
10
-
11
- if (bucketName) {
12
- const client = new S3Client({
13
- endpoint: process.env.S3_ENDPOINT,
14
- // The SDK default (WHEN_SUPPORTED) hangs a CRC32 of an empty body off
15
- // every presigned PUT as signed query params. Real S3 tolerates it;
16
- // MinIO and LocalStack reject the request, and S3_ENDPOINT exists to
17
- // point at exactly those.
18
- requestChecksumCalculation: "WHEN_REQUIRED",
19
- });
20
- return createS3StorageService(client, bucketName);
21
- }
22
-
23
- const basePath = process.env.STORAGE_LOCAL_PATH ?? ".remit/storage";
24
- // BETTER_AUTH_URL is the deployment's own public base — the value the token
25
- // issuer and the JWKS discovery document already agree on — so a minted
26
- // upload URL resolves from wherever the browser reached the API. Both it and
27
- // the signing secret are required on the self-host stack; without them a
28
- // mint fails loud inside the backend rather than handing out a URL that
29
- // cannot be verified.
30
- const origin = process.env.BETTER_AUTH_URL;
31
- const signingSecret = process.env.BETTER_AUTH_SECRET;
32
- return createFilesystemStorageService(
33
- basePath,
34
- origin && signingSecret ? { origin, signingSecret } : undefined,
35
- );
36
- };
@@ -1,86 +0,0 @@
1
- import { createHmac, timingSafeEqual } from "node:crypto";
2
-
3
- /**
4
- * The HMAC scheme behind every signed storage URL this stack mints.
5
- *
6
- * It started as the read side — `/content/*` is served against a signature
7
- * because a bearer token cannot ride on an `<img src>` inside email HTML — and
8
- * the write side needs exactly the same capability: a URL that grants access to
9
- * one path, for a while, and is unforgeable without the key. Rather than a
10
- * second scheme, both use this one with a different derivation label, so a
11
- * signature minted to read content can never be replayed as permission to
12
- * write, and vice versa.
13
- *
14
- * The signed message is the storage path followed by every claim in a fixed
15
- * order. Because the path is part of the message, a signature minted for one
16
- * account's object cannot be presented for another's.
17
- */
18
-
19
- const deriveSigningKey = (masterSecret: string, label: string): Buffer =>
20
- createHmac("sha256", masterSecret).update(label).digest();
21
-
22
- const canonicalMessage = (
23
- relativePath: string,
24
- claims: readonly number[],
25
- ): string => [relativePath, ...claims].join("\n");
26
-
27
- const computeSignature = (
28
- key: Buffer,
29
- relativePath: string,
30
- claims: readonly number[],
31
- ): string =>
32
- createHmac("sha256", key)
33
- .update(canonicalMessage(relativePath, claims))
34
- .digest("base64url");
35
-
36
- export const signStoragePath = (
37
- masterSecret: string,
38
- label: string,
39
- relativePath: string,
40
- claims: readonly number[],
41
- ): string =>
42
- computeSignature(deriveSigningKey(masterSecret, label), relativePath, claims);
43
-
44
- export type SignedPathFailure =
45
- | "missing"
46
- | "malformed"
47
- | "expired"
48
- | "bad-signature";
49
-
50
- export type SignedPathResult =
51
- | { valid: true }
52
- | { valid: false; reason: SignedPathFailure };
53
-
54
- /**
55
- * Verify a presented signature against the path and claims it must cover. The
56
- * first claim is always the expiry, in unix seconds. Pure, so the decision can
57
- * be unit-tested without a live server.
58
- */
59
- export const verifyStoragePath = (
60
- masterSecret: string,
61
- label: string,
62
- relativePath: string,
63
- claims: readonly number[],
64
- sig: string | undefined,
65
- nowSeconds: number,
66
- ): SignedPathResult => {
67
- if (!sig || claims.length === 0) return { valid: false, reason: "missing" };
68
-
69
- const [exp] = claims;
70
- if (!claims.every((claim) => Number.isInteger(claim) && claim > 0)) {
71
- return { valid: false, reason: "malformed" };
72
- }
73
- if (exp < nowSeconds) return { valid: false, reason: "expired" };
74
-
75
- const expected = Buffer.from(
76
- signStoragePath(masterSecret, label, relativePath, claims),
77
- );
78
- const presented = Buffer.from(sig);
79
- if (expected.length !== presented.length) {
80
- return { valid: false, reason: "bad-signature" };
81
- }
82
- if (!timingSafeEqual(expected, presented)) {
83
- return { valid: false, reason: "bad-signature" };
84
- }
85
- return { valid: true };
86
- };