@crabcrop/url 0.0.0-stage → 0.1.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 +80 -2
- package/dist/explain.d.ts +42 -0
- package/dist/explain.js +654 -0
- package/dist/index.d.ts +84 -0
- package/dist/index.js +182 -0
- package/dist/operations.d.ts +178 -0
- package/dist/operations.js +491 -0
- package/package.json +29 -4
package/README.md
CHANGED
|
@@ -1,3 +1,81 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @crabcrop/url
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Build and sign [crabcrop](https://github.com/dayvson/crabcrop) URLs from TypeScript or JavaScript. Standard library only (`node:crypto`); the signing scheme is specified in [`spec/url-signing.md`](../../spec/url-signing.md). Signing is pinned by the canonical [`spec/test-vectors.json`](../../spec/test-vectors.json), which this package runs as tests, and the explainer's verdicts are pinned by a grammar corpus mirrored in the playground's test suite.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { buildURL, buildSrcSet, purgeGroupSignature, purgeSignature, sign, uploadSignature, verifyUrlSignature } from "@crabcrop/url";
|
|
9
|
+
|
|
10
|
+
// A signed URL. The operation string is passed verbatim and may
|
|
11
|
+
// contain several segments and preset: references.
|
|
12
|
+
const url = buildURL(
|
|
13
|
+
"https://images.example.com",
|
|
14
|
+
"your-secret",
|
|
15
|
+
"crop:1200x630:smart/filters:format(webp)",
|
|
16
|
+
"https://example.com/photo.jpg",
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
// An expiring URL (unix seconds), emitted as a leading exp: segment.
|
|
20
|
+
const expiring = buildURL(
|
|
21
|
+
"https://images.example.com",
|
|
22
|
+
"your-secret",
|
|
23
|
+
"300x200",
|
|
24
|
+
"https://example.com/photo.jpg",
|
|
25
|
+
{ expires: 1893456000 },
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
// A responsive srcset: one {width}x0 URL per width.
|
|
29
|
+
const srcset = buildSrcSet(
|
|
30
|
+
"https://images.example.com",
|
|
31
|
+
"your-secret",
|
|
32
|
+
"https://example.com/photo.jpg",
|
|
33
|
+
[400, 800, 1200],
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
// The raw signature for a remainder, if you assemble URLs yourself.
|
|
37
|
+
const signature = sign("your-secret", "300x200/https://example.com/photo.jpg");
|
|
38
|
+
|
|
39
|
+
// Verify a signed URL's signature locally, in constant time
|
|
40
|
+
// (unsigned or malformed URLs return null).
|
|
41
|
+
const ok = verifyUrlSignature("your-secret", url);
|
|
42
|
+
|
|
43
|
+
// Purge signatures for the server's /purge and /purge-group endpoints
|
|
44
|
+
// are domain-separated under a derived subkey; the helpers sign the
|
|
45
|
+
// right strings so callers never assemble them by hand.
|
|
46
|
+
const purge = purgeSignature("your-secret", "300x200/https://example.com/photo.jpg");
|
|
47
|
+
const group = purgeGroupSignature("your-secret", "thumb");
|
|
48
|
+
|
|
49
|
+
// Upload signatures for the server's PUT /upload/<signature>/<name>
|
|
50
|
+
// endpoint live in a third domain, under their own derived subkey.
|
|
51
|
+
const upload = uploadSignature("your-secret", "heroes/photo.jpg");
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Pass `null` as the secret to produce the literal `unsafe` signature segment, for servers running with `CRABCROP_ALLOW_UNSAFE` or in presets-only mode.
|
|
55
|
+
|
|
56
|
+
## Typed operations
|
|
57
|
+
|
|
58
|
+
The operation string is the spec, and passing it verbatim stays the primary API. For callers who would rather build it, `ops` and `filter` are typed builders that validate each argument against the server's grammar and return the canonical spelling, so the typed and hand-written forms sign byte-identical URLs (the test suite pins this against the canonical corpus):
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
import { buildURL, filter, joinOps, ops } from "@crabcrop/url";
|
|
62
|
+
|
|
63
|
+
const operations = joinOps(
|
|
64
|
+
ops.crop(1200, 630, "smart"),
|
|
65
|
+
ops.filters(filter.format("webp"), filter.quality(80)),
|
|
66
|
+
);
|
|
67
|
+
// operations === "crop:1200x630:smart/filters:format(webp):quality(80)"
|
|
68
|
+
const url = buildURL("https://images.example.com", "your-secret", operations, "https://example.com/photo.jpg");
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
A value the server would reject (`filter.quality(0)`, `filter.brighten(256)`, `ops.crop(0, 100)`) throws at build time instead of minting a URL that fails at the edge. The closed sets are exported as types (`ImageFormat`, `CropAlign`, `Anchor`, `BlendMode`, `MaskShape`, `Dimension`), and `FILTER_DESCRIPTIONS` is the filter metadata table the explainer uses.
|
|
72
|
+
|
|
73
|
+
## Runtime
|
|
74
|
+
|
|
75
|
+
The package is TypeScript and runs directly on Node >= 22 (type stripping), which is also how the tests run:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
node --test
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
In this repository the framework loaders and the MCP server resolve `@crabcrop/url` to these sources; a standalone install comes from npm once published, or from the repository with `"@crabcrop/url": "file:../typescript"` as the MCP package in this repo does.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse a crabcrop URL (or bare path) back into its pieces and
|
|
3
|
+
* explain each segment, mirroring the server's grammar. This is
|
|
4
|
+
* the canonical implementation: the playground keeps a plain-JS
|
|
5
|
+
* copy for its no-build static page, and both are pinned to the
|
|
6
|
+
* same verdict corpus (audit PL-10, P-3).
|
|
7
|
+
*/
|
|
8
|
+
/** The segment kinds an explanation is made of. */
|
|
9
|
+
export type SegmentKind = "expiry" | "maxage" | "debug" | "preset" | "operation";
|
|
10
|
+
/** The signature kinds an explanation can report. */
|
|
11
|
+
export type SignatureKind = "hmac" | "unsafe" | "invalid";
|
|
12
|
+
export interface ExplainedSegment {
|
|
13
|
+
raw: string;
|
|
14
|
+
kind: SegmentKind;
|
|
15
|
+
title: string;
|
|
16
|
+
detail?: string;
|
|
17
|
+
error?: string;
|
|
18
|
+
expired?: boolean;
|
|
19
|
+
}
|
|
20
|
+
export interface Explanation {
|
|
21
|
+
signature: {
|
|
22
|
+
kind: SignatureKind;
|
|
23
|
+
value: string;
|
|
24
|
+
detail: string;
|
|
25
|
+
keyId?: string;
|
|
26
|
+
} | null;
|
|
27
|
+
segments: ExplainedSegment[];
|
|
28
|
+
source: string;
|
|
29
|
+
remainder: string;
|
|
30
|
+
errors: string[];
|
|
31
|
+
}
|
|
32
|
+
export declare const FILTER_DESCRIPTIONS: Record<string, string>;
|
|
33
|
+
/**
|
|
34
|
+
* Parse a crabcrop URL (or bare path) back into its pieces,
|
|
35
|
+
* explaining each segment. Mirrors the server's dispatch:
|
|
36
|
+
* segments are operations until the first segment that is not
|
|
37
|
+
* one; everything from there is the source.
|
|
38
|
+
*
|
|
39
|
+
* Returns { signature, segments, source, remainder, errors }
|
|
40
|
+
* where segments carry { raw, kind, title, detail?, error? }.
|
|
41
|
+
*/
|
|
42
|
+
export declare function explainUrl(input: string, nowSeconds?: number): Explanation;
|