@crewhaus/tool-objectstore 0.7.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 +190 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +256 -0
- package/dist/lib/clock.d.ts +29 -0
- package/dist/lib/clock.js +35 -0
- package/dist/lib/encode.d.ts +62 -0
- package/dist/lib/encode.js +106 -0
- package/dist/lib/request.d.ts +73 -0
- package/dist/lib/request.js +118 -0
- package/dist/lib/sigv4.d.ts +132 -0
- package/dist/lib/sigv4.js +203 -0
- package/dist/lib/store.d.ts +89 -0
- package/dist/lib/store.js +265 -0
- package/package.json +46 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where the object lives: endpoint, addressing style, bucket, key — resolved
|
|
3
|
+
* into the two strings the signature is computed over, the Host header and
|
|
4
|
+
* the canonical URI.
|
|
5
|
+
*
|
|
6
|
+
* The addressing style is an INPUT, never an inference. `https://minio.acme.io`
|
|
7
|
+
* and `https://s3.us-east-1.amazonaws.com` look the same to a parser, and R2
|
|
8
|
+
* answers to both styles depending on how the bucket was configured. Guessing
|
|
9
|
+
* produces a signature that is internally consistent and unusable: the Host
|
|
10
|
+
* header and the path both move together, so the wrong guess is not a typo
|
|
11
|
+
* the server can correct, it is a different request entirely.
|
|
12
|
+
*
|
|
13
|
+
* Everything here throws `PresignError` on a fact that would produce a URL
|
|
14
|
+
* that cannot work. A refusal now is worth an opaque 403 later, and it is the
|
|
15
|
+
* only feedback available — this package never talks to the store, so it can
|
|
16
|
+
* never learn that the bucket does not exist.
|
|
17
|
+
*/
|
|
18
|
+
import { CrewhausError } from "@crewhaus/errors";
|
|
19
|
+
/** A URL this tool would refuse to sign, with the reason in the message. */
|
|
20
|
+
export declare class PresignError extends CrewhausError {
|
|
21
|
+
readonly name = "PresignError";
|
|
22
|
+
constructor(message: string);
|
|
23
|
+
}
|
|
24
|
+
export declare const ADDRESSING_STYLES: readonly ["virtual-hosted", "path"];
|
|
25
|
+
export type AddressingStyle = (typeof ADDRESSING_STYLES)[number];
|
|
26
|
+
/** S3's own limit on a key: 1024 BYTES of UTF-8, not 1024 characters. */
|
|
27
|
+
export declare const MAX_KEY_BYTES = 1024;
|
|
28
|
+
export type Endpoint = {
|
|
29
|
+
scheme: "http" | "https";
|
|
30
|
+
/** Host header value: lowercased, port present only when non-default. */
|
|
31
|
+
host: string;
|
|
32
|
+
hostname: string;
|
|
33
|
+
isIpLiteral: boolean;
|
|
34
|
+
isLoopback: boolean;
|
|
35
|
+
};
|
|
36
|
+
export type Target = {
|
|
37
|
+
host: string;
|
|
38
|
+
/** `https://bucket.s3.amazonaws.com` — no path, no trailing slash. */
|
|
39
|
+
origin: string;
|
|
40
|
+
/** Percent-encoded, leading slash included. */
|
|
41
|
+
canonicalUri: string;
|
|
42
|
+
warnings: string[];
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Parse the store's base URL into the pieces the signature needs.
|
|
46
|
+
*
|
|
47
|
+
* The WHATWG parser does three things here that a hand-rolled split would
|
|
48
|
+
* get wrong: it lowercases the host, it converts an internationalized host to
|
|
49
|
+
* punycode (which is what the Host header must carry), and it drops the port
|
|
50
|
+
* when it is the scheme's default. That last one is load-bearing — a Host
|
|
51
|
+
* header of `s3.amazonaws.com:443` signs fine and then fails, because every
|
|
52
|
+
* HTTP client omits the default port and the server canonicalizes what it
|
|
53
|
+
* received, not what was meant.
|
|
54
|
+
*/
|
|
55
|
+
export declare function parseEndpoint(raw: string): Endpoint;
|
|
56
|
+
/**
|
|
57
|
+
* Check a bucket name, and say what is wrong in the terms of the style that
|
|
58
|
+
* will be used.
|
|
59
|
+
*
|
|
60
|
+
* Virtual-hosted style is strict because the name becomes a DNS label: an
|
|
61
|
+
* underscore or a capital cannot be a hostname, so the request would not even
|
|
62
|
+
* reach the store. Path style is permissive because the name is just a path
|
|
63
|
+
* segment — legacy us-east-1 buckets with capitals predate the modern rules
|
|
64
|
+
* and still work there — but it warns, because such a bucket can never be
|
|
65
|
+
* addressed the other way.
|
|
66
|
+
*/
|
|
67
|
+
export declare function checkBucket(bucket: string, style: AddressingStyle): string[];
|
|
68
|
+
/**
|
|
69
|
+
* Check an object key.
|
|
70
|
+
*
|
|
71
|
+
* The dot-segment refusal is the one that repays itself. S3 keys may legally
|
|
72
|
+
* contain `.` and `..` segments, and S3 does not normalize the request path —
|
|
73
|
+
* but nearly every HTTP client does, before sending. So a URL signed over
|
|
74
|
+
* `/a/../b.txt` is sent as `/b.txt`, the signature is computed over a path
|
|
75
|
+
* that never arrives, and the store answers 403 with no mention of paths.
|
|
76
|
+
* Signing it would be minting a URL that is broken by design.
|
|
77
|
+
*/
|
|
78
|
+
export declare function checkKey(key: string): void;
|
|
79
|
+
/**
|
|
80
|
+
* Resolve endpoint + style + bucket + key into the Host header and the
|
|
81
|
+
* canonical URI. These two strings are what the signature commits to; every
|
|
82
|
+
* other difference between the two styles follows from them.
|
|
83
|
+
*/
|
|
84
|
+
export declare function resolveTarget(input: {
|
|
85
|
+
endpoint: Endpoint;
|
|
86
|
+
style: AddressingStyle;
|
|
87
|
+
bucket: string;
|
|
88
|
+
key: string;
|
|
89
|
+
}): Target;
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where the object lives: endpoint, addressing style, bucket, key — resolved
|
|
3
|
+
* into the two strings the signature is computed over, the Host header and
|
|
4
|
+
* the canonical URI.
|
|
5
|
+
*
|
|
6
|
+
* The addressing style is an INPUT, never an inference. `https://minio.acme.io`
|
|
7
|
+
* and `https://s3.us-east-1.amazonaws.com` look the same to a parser, and R2
|
|
8
|
+
* answers to both styles depending on how the bucket was configured. Guessing
|
|
9
|
+
* produces a signature that is internally consistent and unusable: the Host
|
|
10
|
+
* header and the path both move together, so the wrong guess is not a typo
|
|
11
|
+
* the server can correct, it is a different request entirely.
|
|
12
|
+
*
|
|
13
|
+
* Everything here throws `PresignError` on a fact that would produce a URL
|
|
14
|
+
* that cannot work. A refusal now is worth an opaque 403 later, and it is the
|
|
15
|
+
* only feedback available — this package never talks to the store, so it can
|
|
16
|
+
* never learn that the bucket does not exist.
|
|
17
|
+
*/
|
|
18
|
+
import { CrewhausError } from "@crewhaus/errors";
|
|
19
|
+
import { encodePath, encodeRfc3986, utf8Length } from "./encode";
|
|
20
|
+
/** A URL this tool would refuse to sign, with the reason in the message. */
|
|
21
|
+
export class PresignError extends CrewhausError {
|
|
22
|
+
name = "PresignError";
|
|
23
|
+
constructor(message) {
|
|
24
|
+
super("tool", message);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export const ADDRESSING_STYLES = ["virtual-hosted", "path"];
|
|
28
|
+
/** S3's own limit on a key: 1024 BYTES of UTF-8, not 1024 characters. */
|
|
29
|
+
export const MAX_KEY_BYTES = 1024;
|
|
30
|
+
function isIpv4(hostname) {
|
|
31
|
+
return /^\d{1,3}(?:\.\d{1,3}){3}$/.test(hostname);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Is this host the local machine?
|
|
35
|
+
*
|
|
36
|
+
* Decided on the parsed ADDRESS, never on the text of the hostname. The trap
|
|
37
|
+
* this replaced was `/^127\./.test(hostname)`, which is a prefix match on a
|
|
38
|
+
* string: `127.0.0.1.evil.example` is an ordinary registered domain that
|
|
39
|
+
* somebody else controls, and it passes that test. The consequence is not
|
|
40
|
+
* cosmetic — loopback is the one place `parseEndpoint` permits plain http, on
|
|
41
|
+
* the grounds that there is no wire to listen on, and a presigned URL is a
|
|
42
|
+
* bearer credential for the object until it expires. A hostname that merely
|
|
43
|
+
* begins with "127." put that credential, and the object, on the open network
|
|
44
|
+
* in clear.
|
|
45
|
+
*
|
|
46
|
+
* Requiring an IPv4 literal first is exact rather than approximate, because
|
|
47
|
+
* the WHATWG parser has already normalized every form of one to dotted quad:
|
|
48
|
+
* `0x7f000001`, `127.1` and `127.0.0.1` all arrive here as `127.0.0.1`, and a
|
|
49
|
+
* host whose last label is not numeric is a domain, never an address.
|
|
50
|
+
*
|
|
51
|
+
* IPv4-mapped IPv6 (`[::ffff:7f00:1]`) is loopback in fact and is NOT
|
|
52
|
+
* accepted here. That direction is safe: it costs a clear refusal, where the
|
|
53
|
+
* opposite mistake costs a credential.
|
|
54
|
+
*/
|
|
55
|
+
function isLoopbackHost(hostname) {
|
|
56
|
+
if (hostname === "localhost" || hostname.endsWith(".localhost"))
|
|
57
|
+
return true;
|
|
58
|
+
if (hostname === "[::1]")
|
|
59
|
+
return true;
|
|
60
|
+
return isIpv4(hostname) && hostname.startsWith("127.");
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Quote an endpoint back in a refusal, WITHOUT anything credential-shaped.
|
|
64
|
+
*
|
|
65
|
+
* Two things a refused endpoint can carry, and both of them get quoted into
|
|
66
|
+
* an error message, which is the one string guaranteed to be copied onward
|
|
67
|
+
* into a transcript, a log line and a bug report:
|
|
68
|
+
*
|
|
69
|
+
* - USERINFO. Two of the checks in `parseEndpoint` run before the one that
|
|
70
|
+
* refuses `https://KEY:SECRET@host`: a bad port never reaches the URL
|
|
71
|
+
* parser at all, and a non-http scheme is rejected first. So those two
|
|
72
|
+
* quoted the secret straight back.
|
|
73
|
+
* - A QUERY STRING. The likeliest way an endpoint acquires one is somebody
|
|
74
|
+
* pasting a whole presigned URL into the field — which carries
|
|
75
|
+
* `X-Amz-Signature` and, on temporary credentials, a live
|
|
76
|
+
* `X-Amz-Security-Token`.
|
|
77
|
+
*
|
|
78
|
+
* Redacting at the point of echo rather than relying on check order, because
|
|
79
|
+
* check order is exactly what the next edit changes. The host and the path
|
|
80
|
+
* survive, which is all the message needs to identify what was refused.
|
|
81
|
+
*/
|
|
82
|
+
function quoteEndpoint(raw) {
|
|
83
|
+
const trimmed = raw.trim();
|
|
84
|
+
const scheme = /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.exec(trimmed);
|
|
85
|
+
const prefix = scheme === null ? "" : scheme[0];
|
|
86
|
+
const rest = trimmed.slice(prefix.length);
|
|
87
|
+
const cut = rest.search(/[/?#]/);
|
|
88
|
+
const authority = cut === -1 ? rest : rest.slice(0, cut);
|
|
89
|
+
const at = authority.lastIndexOf("@");
|
|
90
|
+
const shown = at === -1 ? authority : `<redacted>@${authority.slice(at + 1)}`;
|
|
91
|
+
if (cut === -1)
|
|
92
|
+
return `${prefix}${shown}`;
|
|
93
|
+
const tail = rest.slice(cut);
|
|
94
|
+
const mark = tail.search(/[?#]/);
|
|
95
|
+
const path = mark === -1 ? tail : tail.slice(0, mark);
|
|
96
|
+
const suffix = mark === -1 ? "" : `${tail[mark]}<redacted>`;
|
|
97
|
+
return `${prefix}${shown}${path}${suffix}`;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Parse the store's base URL into the pieces the signature needs.
|
|
101
|
+
*
|
|
102
|
+
* The WHATWG parser does three things here that a hand-rolled split would
|
|
103
|
+
* get wrong: it lowercases the host, it converts an internationalized host to
|
|
104
|
+
* punycode (which is what the Host header must carry), and it drops the port
|
|
105
|
+
* when it is the scheme's default. That last one is load-bearing — a Host
|
|
106
|
+
* header of `s3.amazonaws.com:443` signs fine and then fails, because every
|
|
107
|
+
* HTTP client omits the default port and the server canonicalizes what it
|
|
108
|
+
* received, not what was meant.
|
|
109
|
+
*/
|
|
110
|
+
export function parseEndpoint(raw) {
|
|
111
|
+
let url;
|
|
112
|
+
try {
|
|
113
|
+
url = new URL(raw.trim());
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
throw new PresignError(`endpoint "${quoteEndpoint(raw)}" is not a URL — write the store's origin, e.g. https://s3.us-east-1.amazonaws.com or http://127.0.0.1:9000`);
|
|
117
|
+
}
|
|
118
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") {
|
|
119
|
+
throw new PresignError(`endpoint "${quoteEndpoint(raw)}" uses ${url.protocol} — an object-store endpoint is http or https`);
|
|
120
|
+
}
|
|
121
|
+
if (url.username !== "" || url.password !== "") {
|
|
122
|
+
throw new PresignError("endpoint carries a username or password — SigV4 credentials go in accessKeyId/secretAccessKey, and a credential in the URL would be signed into the result");
|
|
123
|
+
}
|
|
124
|
+
if (url.search !== "" || url.hash !== "") {
|
|
125
|
+
throw new PresignError(`endpoint "${quoteEndpoint(raw)}" carries a query string or fragment; pass the origin only`);
|
|
126
|
+
}
|
|
127
|
+
if (url.pathname !== "/") {
|
|
128
|
+
// A path-prefixed gateway (`https://gw.acme.io/s3`) is a real deployment,
|
|
129
|
+
// and it is refused rather than guessed at: the prefix belongs in the
|
|
130
|
+
// canonical URI ahead of the bucket, and whether THIS gateway wants it
|
|
131
|
+
// signed is a fact about the gateway that nothing here can check.
|
|
132
|
+
throw new PresignError(`endpoint "${quoteEndpoint(raw)}" has a path ("${url.pathname}") — this tool signs a path built from the bucket and key, so a prefix on the endpoint would be signed twice or not at all. Pass the origin only`);
|
|
133
|
+
}
|
|
134
|
+
if (url.hostname === "")
|
|
135
|
+
throw new PresignError(`endpoint "${quoteEndpoint(raw)}" has no host`);
|
|
136
|
+
const hostname = url.hostname;
|
|
137
|
+
const isLoopback = isLoopbackHost(hostname);
|
|
138
|
+
const scheme = url.protocol === "https:" ? "https" : "http";
|
|
139
|
+
if (scheme === "http" && !isLoopback) {
|
|
140
|
+
// A presigned URL IS the credential for one object until it expires.
|
|
141
|
+
// Over http it, and the object, cross the network in clear to anyone on
|
|
142
|
+
// the path. Loopback is exempted because that is where MinIO runs in
|
|
143
|
+
// development and there is no network to listen on.
|
|
144
|
+
throw new PresignError(`endpoint "${quoteEndpoint(raw)}" is plain http and "${hostname}" is not loopback — a presigned URL is a bearer credential for the object, and http puts it and the object on the wire in clear. Use https, or point at localhost for a local MinIO`);
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
scheme,
|
|
148
|
+
host: url.host,
|
|
149
|
+
hostname,
|
|
150
|
+
isIpLiteral: isIpv4(hostname) || hostname.startsWith("["),
|
|
151
|
+
isLoopback,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Check a bucket name, and say what is wrong in the terms of the style that
|
|
156
|
+
* will be used.
|
|
157
|
+
*
|
|
158
|
+
* Virtual-hosted style is strict because the name becomes a DNS label: an
|
|
159
|
+
* underscore or a capital cannot be a hostname, so the request would not even
|
|
160
|
+
* reach the store. Path style is permissive because the name is just a path
|
|
161
|
+
* segment — legacy us-east-1 buckets with capitals predate the modern rules
|
|
162
|
+
* and still work there — but it warns, because such a bucket can never be
|
|
163
|
+
* addressed the other way.
|
|
164
|
+
*/
|
|
165
|
+
export function checkBucket(bucket, style) {
|
|
166
|
+
const warnings = [];
|
|
167
|
+
if (bucket.length === 0)
|
|
168
|
+
throw new PresignError("bucket is empty");
|
|
169
|
+
if (bucket.length > 255) {
|
|
170
|
+
throw new PresignError(`bucket name is ${bucket.length} characters; the ceiling is 255`);
|
|
171
|
+
}
|
|
172
|
+
if (bucket.includes("/")) {
|
|
173
|
+
throw new PresignError(`bucket "${bucket}" contains "/" — pass the bucket alone and put the rest in key`);
|
|
174
|
+
}
|
|
175
|
+
const modern = /^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$/.test(bucket) && !bucket.includes("..");
|
|
176
|
+
if (style === "virtual-hosted") {
|
|
177
|
+
if (!modern) {
|
|
178
|
+
throw new PresignError(`bucket "${bucket}" cannot be a DNS label, so virtual-hosted addressing cannot reach it: names must be 3-63 characters of lowercase letters, digits, dots and hyphens, start and end alphanumeric, and contain no "..". Use addressingStyle "path" for a legacy or non-conforming name`);
|
|
179
|
+
}
|
|
180
|
+
if (isIpv4(bucket)) {
|
|
181
|
+
throw new PresignError(`bucket "${bucket}" is shaped like an IPv4 address, which S3 forbids`);
|
|
182
|
+
}
|
|
183
|
+
if (bucket.includes(".")) {
|
|
184
|
+
warnings.push(`bucket "${bucket}" contains a dot: over https the wildcard certificate for the endpoint covers one label, so TLS verification typically fails for a dotted bucket in virtual-hosted style. Path style avoids it.`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
else if (!modern) {
|
|
188
|
+
// No length exemption here. There used to be a `bucket.length > 1` guard
|
|
189
|
+
// in front of this check, and it let a bucket of "." through entirely
|
|
190
|
+
// unvalidated: path style then signed "/./key" — the very dot segment
|
|
191
|
+
// `checkKey` refuses in a key, for the reason given there. Every HTTP
|
|
192
|
+
// client resolves it away before sending, so the signature would cover a
|
|
193
|
+
// path that never arrives and the store would answer 403 without
|
|
194
|
+
// mentioning paths. A one-character bucket is checked like any other.
|
|
195
|
+
if (!/^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/.test(bucket)) {
|
|
196
|
+
throw new PresignError(`bucket "${bucket}" is not a usable path segment: it must start and end alphanumeric and contain only letters, digits, dots, hyphens and underscores`);
|
|
197
|
+
}
|
|
198
|
+
warnings.push(`bucket "${bucket}" does not meet current S3 naming rules (3-63 lowercase alphanumerics, dots and hyphens), so it can only ever be addressed path-style.`);
|
|
199
|
+
}
|
|
200
|
+
return warnings;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Check an object key.
|
|
204
|
+
*
|
|
205
|
+
* The dot-segment refusal is the one that repays itself. S3 keys may legally
|
|
206
|
+
* contain `.` and `..` segments, and S3 does not normalize the request path —
|
|
207
|
+
* but nearly every HTTP client does, before sending. So a URL signed over
|
|
208
|
+
* `/a/../b.txt` is sent as `/b.txt`, the signature is computed over a path
|
|
209
|
+
* that never arrives, and the store answers 403 with no mention of paths.
|
|
210
|
+
* Signing it would be minting a URL that is broken by design.
|
|
211
|
+
*/
|
|
212
|
+
export function checkKey(key) {
|
|
213
|
+
if (key.length === 0)
|
|
214
|
+
throw new PresignError("key is empty");
|
|
215
|
+
const bytes = utf8Length(key);
|
|
216
|
+
if (bytes > MAX_KEY_BYTES) {
|
|
217
|
+
throw new PresignError(`key is ${bytes} bytes of UTF-8 and S3's limit is ${MAX_KEY_BYTES} bytes (the limit counts bytes, not characters, so non-ASCII keys reach it sooner)`);
|
|
218
|
+
}
|
|
219
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: refusing C0 controls is the point.
|
|
220
|
+
const control = key.match(/[\x00-\x1f\x7f]/);
|
|
221
|
+
if (control !== null) {
|
|
222
|
+
const code = control[0].charCodeAt(0).toString(16).padStart(4, "0").toUpperCase();
|
|
223
|
+
throw new PresignError(`key contains the control character U+${code} — S3 permits it, but it cannot survive a URL that a human or a log line passes along, and a bare CR or LF splits the request in a careless client`);
|
|
224
|
+
}
|
|
225
|
+
if (key.startsWith("/")) {
|
|
226
|
+
throw new PresignError(`key "${key}" starts with "/" — the canonical URI already supplies it, so this would sign "//…", a different object from the one meant`);
|
|
227
|
+
}
|
|
228
|
+
if (key.includes("//")) {
|
|
229
|
+
throw new PresignError(`key "${key}" contains an empty path segment ("//"). S3 would store it, but URL normalization in most clients collapses it before the request is sent, so the signature would be over a path that never arrives`);
|
|
230
|
+
}
|
|
231
|
+
const segments = key.split("/");
|
|
232
|
+
if (segments.includes(".") || segments.includes("..")) {
|
|
233
|
+
throw new PresignError(`key "${key}" contains a "." or ".." segment. HTTP clients resolve those before sending, so the request would arrive with a different path than the one signed and the store would answer 403 without explaining why`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Resolve endpoint + style + bucket + key into the Host header and the
|
|
238
|
+
* canonical URI. These two strings are what the signature commits to; every
|
|
239
|
+
* other difference between the two styles follows from them.
|
|
240
|
+
*/
|
|
241
|
+
export function resolveTarget(input) {
|
|
242
|
+
const warnings = checkBucket(input.bucket, input.style);
|
|
243
|
+
checkKey(input.key);
|
|
244
|
+
if (input.style === "path") {
|
|
245
|
+
return {
|
|
246
|
+
host: input.endpoint.host,
|
|
247
|
+
origin: `${input.endpoint.scheme}://${input.endpoint.host}`,
|
|
248
|
+
canonicalUri: `/${encodeRfc3986(input.bucket)}/${encodePath(input.key)}`,
|
|
249
|
+
warnings,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
if (input.endpoint.isIpLiteral) {
|
|
253
|
+
throw new PresignError(`endpoint host "${input.endpoint.hostname}" is an IP address, which cannot carry "${input.bucket}." as a subdomain. A store reached by IP — a local MinIO, most commonly — is addressed with addressingStyle "path"`);
|
|
254
|
+
}
|
|
255
|
+
if (input.endpoint.hostname.startsWith(`${input.bucket.toLowerCase()}.`)) {
|
|
256
|
+
throw new PresignError(`endpoint host "${input.endpoint.hostname}" already begins with the bucket name, so virtual-hosted addressing would sign "${input.bucket}.${input.endpoint.hostname}". Pass the store's own origin (e.g. https://s3.us-east-1.amazonaws.com) and let the bucket be added here`);
|
|
257
|
+
}
|
|
258
|
+
const host = `${input.bucket}.${input.endpoint.host}`;
|
|
259
|
+
return {
|
|
260
|
+
host,
|
|
261
|
+
origin: `${input.endpoint.scheme}://${host}`,
|
|
262
|
+
canonicalUri: `/${encodePath(input.key)}`,
|
|
263
|
+
warnings,
|
|
264
|
+
};
|
|
265
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@crewhaus/tool-objectstore",
|
|
3
|
+
"version": "0.7.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Deterministic object-store URL signing: SigV4 presigned GET and PUT, computed offline",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"test": "bun test src"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@crewhaus/errors": "0.7.0",
|
|
19
|
+
"@crewhaus/tool-builder": "0.7.0",
|
|
20
|
+
"@crewhaus/tool-catalog": "0.7.0",
|
|
21
|
+
"@crewhaus/tool-encode": "0.7.0",
|
|
22
|
+
"zod": "^3.23.8"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@crewhaus/tool-executor": "workspace:*"
|
|
26
|
+
},
|
|
27
|
+
"license": "Apache-2.0",
|
|
28
|
+
"author": {
|
|
29
|
+
"name": "Max Meier",
|
|
30
|
+
"email": "max@crewhaus.ai",
|
|
31
|
+
"url": "https://crewhaus.ai"
|
|
32
|
+
},
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "git+https://github.com/crewhaus/factory.git",
|
|
36
|
+
"directory": "packages/tool-objectstore"
|
|
37
|
+
},
|
|
38
|
+
"homepage": "https://github.com/crewhaus/factory/tree/main/packages/tool-objectstore#readme",
|
|
39
|
+
"bugs": {
|
|
40
|
+
"url": "https://github.com/crewhaus/factory/issues"
|
|
41
|
+
},
|
|
42
|
+
"publishConfig": {
|
|
43
|
+
"access": "public"
|
|
44
|
+
},
|
|
45
|
+
"files": ["dist", "README.md", "LICENSE", "NOTICE"]
|
|
46
|
+
}
|