@crabcrop/url 0.1.0 → 0.1.1
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 +3 -0
- package/dist/index.d.ts +16 -4
- package/dist/index.js +28 -8
- package/dist/operations.js +13 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -48,7 +48,10 @@ const group = purgeGroupSignature("your-secret", "thumb");
|
|
|
48
48
|
|
|
49
49
|
// Upload signatures for the server's PUT /upload/<signature>/<name>
|
|
50
50
|
// endpoint live in a third domain, under their own derived subkey.
|
|
51
|
+
// Pass an expiry (unix seconds) for a token that stops working
|
|
52
|
+
// then; its path is /upload/<signature>/exp:<expiry>/<name>.
|
|
51
53
|
const upload = uploadSignature("your-secret", "heroes/photo.jpg");
|
|
54
|
+
const expiring = uploadSignature("your-secret", "heroes/photo.jpg", 1893456000);
|
|
52
55
|
```
|
|
53
56
|
|
|
54
57
|
Pass `null` as the secret to produce the literal `unsafe` signature segment, for servers running with `CRABCROP_ALLOW_UNSAFE` or in presets-only mode.
|
package/dist/index.d.ts
CHANGED
|
@@ -16,9 +16,12 @@ export declare function sign(secret: string, remainder: string): string;
|
|
|
16
16
|
*/
|
|
17
17
|
export declare function purgeSignature(secret: string, target: string): string;
|
|
18
18
|
/**
|
|
19
|
-
* The purge-group signature for a cache group:
|
|
20
|
-
*
|
|
21
|
-
*
|
|
19
|
+
* The purge-group signature for a cache group: HMAC-SHA256
|
|
20
|
+
* of the group name under a subkey derived from the secret
|
|
21
|
+
* (the raw HMAC of the secret over the label
|
|
22
|
+
* `crabcrop/purge-group-key`), its own domain beside the
|
|
23
|
+
* purge subkey, so a per-target purge signature can never
|
|
24
|
+
* be transplanted to the purge-group endpoint. The helper
|
|
22
25
|
* exists so nobody has to know the signed string. This is
|
|
23
26
|
* the signature segment for
|
|
24
27
|
* `POST /purge-group/<signature>/<group>`.
|
|
@@ -31,8 +34,17 @@ export declare function purgeGroupSignature(secret: string, group: string): stri
|
|
|
31
34
|
* so neither an image signature nor a purge signature can
|
|
32
35
|
* ever be replayed as an upload signature. This is the
|
|
33
36
|
* signature segment for `PUT /upload/<signature>/<name>`.
|
|
37
|
+
*
|
|
38
|
+
* With `expires` (unix seconds), the signature covers
|
|
39
|
+
* `exp:<expires>/<name>` instead, and the request path is
|
|
40
|
+
* `PUT /upload/<signature>/exp:<expires>/<name>`: the
|
|
41
|
+
* expiry sits inside the signed string, so it can neither
|
|
42
|
+
* be stripped nor extended, and the server refuses the
|
|
43
|
+
* upload once that second is reached. Delegated upload
|
|
44
|
+
* tokens should always use the expiring form; without it
|
|
45
|
+
* a token replays until the secret rotates.
|
|
34
46
|
*/
|
|
35
|
-
export declare function uploadSignature(secret: string, name: string): string;
|
|
47
|
+
export declare function uploadSignature(secret: string, name: string, expires?: number): string;
|
|
36
48
|
export interface BuildOptions {
|
|
37
49
|
/** Unix-seconds expiry, emitted as a leading `exp:` segment. */
|
|
38
50
|
expires?: number;
|
package/dist/index.js
CHANGED
|
@@ -26,15 +26,19 @@ export function purgeSignature(secret, target) {
|
|
|
26
26
|
return createHmac("sha256", subkey).update(target, "utf8").digest("hex");
|
|
27
27
|
}
|
|
28
28
|
/**
|
|
29
|
-
* The purge-group signature for a cache group:
|
|
30
|
-
*
|
|
31
|
-
*
|
|
29
|
+
* The purge-group signature for a cache group: HMAC-SHA256
|
|
30
|
+
* of the group name under a subkey derived from the secret
|
|
31
|
+
* (the raw HMAC of the secret over the label
|
|
32
|
+
* `crabcrop/purge-group-key`), its own domain beside the
|
|
33
|
+
* purge subkey, so a per-target purge signature can never
|
|
34
|
+
* be transplanted to the purge-group endpoint. The helper
|
|
32
35
|
* exists so nobody has to know the signed string. This is
|
|
33
36
|
* the signature segment for
|
|
34
37
|
* `POST /purge-group/<signature>/<group>`.
|
|
35
38
|
*/
|
|
36
39
|
export function purgeGroupSignature(secret, group) {
|
|
37
|
-
|
|
40
|
+
const subkey = Buffer.from(sign(secret, "crabcrop/purge-group-key"), "hex");
|
|
41
|
+
return createHmac("sha256", subkey).update(group, "utf8").digest("hex");
|
|
38
42
|
}
|
|
39
43
|
/**
|
|
40
44
|
* The upload signature for an object name: HMAC-SHA256 of
|
|
@@ -43,10 +47,20 @@ export function purgeGroupSignature(secret, group) {
|
|
|
43
47
|
* so neither an image signature nor a purge signature can
|
|
44
48
|
* ever be replayed as an upload signature. This is the
|
|
45
49
|
* signature segment for `PUT /upload/<signature>/<name>`.
|
|
50
|
+
*
|
|
51
|
+
* With `expires` (unix seconds), the signature covers
|
|
52
|
+
* `exp:<expires>/<name>` instead, and the request path is
|
|
53
|
+
* `PUT /upload/<signature>/exp:<expires>/<name>`: the
|
|
54
|
+
* expiry sits inside the signed string, so it can neither
|
|
55
|
+
* be stripped nor extended, and the server refuses the
|
|
56
|
+
* upload once that second is reached. Delegated upload
|
|
57
|
+
* tokens should always use the expiring form; without it
|
|
58
|
+
* a token replays until the secret rotates.
|
|
46
59
|
*/
|
|
47
|
-
export function uploadSignature(secret, name) {
|
|
60
|
+
export function uploadSignature(secret, name, expires) {
|
|
48
61
|
const subkey = Buffer.from(sign(secret, "crabcrop/upload-key"), "hex");
|
|
49
|
-
|
|
62
|
+
const signed = expires === undefined ? name : `exp:${expires}/${name}`;
|
|
63
|
+
return createHmac("sha256", subkey).update(signed, "utf8").digest("hex");
|
|
50
64
|
}
|
|
51
65
|
/** Whether `id` is a well-formed key ID (1-32 of `[A-Za-z0-9_-]`). */
|
|
52
66
|
export function isValidKeyId(id) {
|
|
@@ -175,8 +189,14 @@ export function verifyUrlSignature(secret, input) {
|
|
|
175
189
|
return matches(secret);
|
|
176
190
|
}
|
|
177
191
|
if (signature.keyId !== undefined) {
|
|
178
|
-
|
|
179
|
-
|
|
192
|
+
// Own properties only: key IDs are attacker-shaped text
|
|
193
|
+
// (`constructor`, `toString`, `__proto__` are all legal
|
|
194
|
+
// IDs), and an inherited lookup would hand a non-string
|
|
195
|
+
// to the HMAC and throw instead of answering false.
|
|
196
|
+
const candidate = Object.hasOwn(secret, signature.keyId)
|
|
197
|
+
? secret[signature.keyId]
|
|
198
|
+
: undefined;
|
|
199
|
+
return typeof candidate === "string" ? matches(candidate) : false;
|
|
180
200
|
}
|
|
181
201
|
return Object.values(secret).some(matches);
|
|
182
202
|
}
|
package/dist/operations.js
CHANGED
|
@@ -89,10 +89,13 @@ function encodeArg(text) {
|
|
|
89
89
|
}
|
|
90
90
|
function className(what, name) {
|
|
91
91
|
const trimmed = name.trim();
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
92
|
+
// The server's bound is on UTF-8 bytes, not UTF-16 code
|
|
93
|
+
// units: a name of multibyte characters can pass a
|
|
94
|
+
// length check here and still be rejected there, so
|
|
95
|
+
// measure the encoded form, as the explainer does.
|
|
96
|
+
const bytes = new TextEncoder().encode(trimmed).length;
|
|
97
|
+
if (bytes < 1 || bytes > 64 || !/^[\p{L}\p{N} _.\-/]+$/u.test(trimmed)) {
|
|
98
|
+
fail(what, name, "1 to 64 UTF-8 bytes of letters, digits, space, `_`, `.`, `-`, `/`");
|
|
96
99
|
}
|
|
97
100
|
return encodeArg(name);
|
|
98
101
|
}
|
|
@@ -175,8 +178,12 @@ export const ops = {
|
|
|
175
178
|
},
|
|
176
179
|
/** `preset:thumb`; expands server-side. */
|
|
177
180
|
preset(name) {
|
|
178
|
-
|
|
179
|
-
|
|
181
|
+
// The server can only configure names from this
|
|
182
|
+
// charset (its preset file validation), so anything
|
|
183
|
+
// outside it is a reference that can only ever answer
|
|
184
|
+
// unknown_preset; refuse it here instead.
|
|
185
|
+
if (!/^[A-Za-z0-9_-]+$/.test(name)) {
|
|
186
|
+
fail("preset name", name, "letters, digits, `_`, `-` (the server's preset name charset)");
|
|
180
187
|
}
|
|
181
188
|
return `preset:${name}`;
|
|
182
189
|
},
|