@buildinternet/uploads 0.3.0 → 0.5.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 +3 -3
- package/dist/agent.js +1 -1
- package/dist/cli.js +2 -2
- package/dist/client.d.ts +16 -0
- package/dist/client.js +38 -12
- package/dist/commands/admin-enrollment.d.ts +2 -0
- package/dist/commands/admin-enrollment.js +55 -8
- package/dist/commands.js +21 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/mcp/tools.js +21 -4
- package/dist/provenance.d.ts +12 -0
- package/dist/provenance.js +29 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
# @buildinternet/uploads
|
|
2
2
|
|
|
3
|
-
CLI and client for **uploads.sh** — upload files, get public URLs, and produce GitHub-ready markdown.
|
|
3
|
+
CLI and client for **uploads.sh** — upload files, get public URLs, and produce GitHub-ready markdown.
|
|
4
4
|
|
|
5
5
|
## CLI
|
|
6
6
|
|
|
7
|
-
Binary: **`uploads`**. Install globally (or use
|
|
7
|
+
Binary: **`uploads`**. Install globally (or use an `npx` one-shot):
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
10
|
npm install --global @buildinternet/uploads
|
|
11
|
-
npx @buildinternet/uploads
|
|
11
|
+
npx @buildinternet/uploads --help
|
|
12
12
|
```
|
|
13
13
|
|
|
14
14
|
```bash
|
package/dist/agent.js
CHANGED
|
@@ -15,7 +15,7 @@ export function createUploadsWorkerFileTools(files, opts = {}) {
|
|
|
15
15
|
},
|
|
16
16
|
overrides: {
|
|
17
17
|
uploadFile: {
|
|
18
|
-
description: "Upload a file for public hosting (e.g. GitHub embeds).
|
|
18
|
+
description: "Upload a non-sensitive file for public hosting (e.g. GitHub embeds). GitHub repository visibility does not restrict access; prefer keys under screenshots/.",
|
|
19
19
|
},
|
|
20
20
|
...overrides,
|
|
21
21
|
},
|
package/dist/cli.js
CHANGED
|
@@ -19,7 +19,7 @@ Config (first match wins, per key):
|
|
|
19
19
|
environment UPLOADS_API_URL, UPLOADS_TOKEN, UPLOADS_WORKSPACE
|
|
20
20
|
--env-file <path>
|
|
21
21
|
$BUILDINTERNET_CONFIG
|
|
22
|
-
~/.config/buildinternet/config
|
|
22
|
+
~/.config/buildinternet/config
|
|
23
23
|
|
|
24
24
|
Workspace (within config layers):
|
|
25
25
|
--workspace, -w override — global (before command) or per-command (after)
|
|
@@ -45,7 +45,7 @@ Commands:
|
|
|
45
45
|
setup Inspect/configure advanced CLI settings
|
|
46
46
|
install Install the agent skill + register the remote MCP server
|
|
47
47
|
login Exchange an enrollment code and configure credentials
|
|
48
|
-
admin Admin
|
|
48
|
+
admin Admin invitation management
|
|
49
49
|
config Show path, init, or set shared config
|
|
50
50
|
doctor Health + auth + workspace checks
|
|
51
51
|
health API liveness (no auth)
|
package/dist/client.d.ts
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
import type { UploadsClientConfig } from "./config.js";
|
|
2
|
+
/** Allowlisted object provenance (maps to X-Uploads-Meta-* on put). */
|
|
3
|
+
export type ProvenanceInput = {
|
|
4
|
+
client?: string;
|
|
5
|
+
"client-version"?: string;
|
|
6
|
+
"source-name"?: string;
|
|
7
|
+
optimized?: "0" | "1";
|
|
8
|
+
frame?: string;
|
|
9
|
+
"keep-exif"?: "0" | "1";
|
|
10
|
+
};
|
|
2
11
|
export interface PutOptions {
|
|
3
12
|
key?: string;
|
|
4
13
|
contentType?: string;
|
|
@@ -6,6 +15,8 @@ export interface PutOptions {
|
|
|
6
15
|
repo?: string;
|
|
7
16
|
ref?: string;
|
|
8
17
|
deriveRepoFromGit?: boolean;
|
|
18
|
+
/** Stored as R2 custom metadata; echoed on put/head. */
|
|
19
|
+
provenance?: ProvenanceInput;
|
|
9
20
|
}
|
|
10
21
|
export interface ListOptions {
|
|
11
22
|
prefix?: string;
|
|
@@ -18,6 +29,7 @@ export interface PutResult {
|
|
|
18
29
|
url: string;
|
|
19
30
|
size: number;
|
|
20
31
|
contentType: string;
|
|
32
|
+
metadata?: Record<string, string>;
|
|
21
33
|
}
|
|
22
34
|
export interface ListItem {
|
|
23
35
|
key: string;
|
|
@@ -35,6 +47,7 @@ export interface HeadResult {
|
|
|
35
47
|
size: number;
|
|
36
48
|
contentType: string;
|
|
37
49
|
uploaded?: string;
|
|
50
|
+
metadata?: Record<string, string>;
|
|
38
51
|
}
|
|
39
52
|
export interface DeleteResult {
|
|
40
53
|
key: string;
|
|
@@ -88,14 +101,17 @@ export interface EnrollmentExchangeResult {
|
|
|
88
101
|
expiresAt?: string;
|
|
89
102
|
}
|
|
90
103
|
export interface EnrollmentCreateResult {
|
|
104
|
+
pageId: string;
|
|
91
105
|
code: string;
|
|
92
106
|
expiresAt: string;
|
|
93
107
|
tokenExpiresAt: string;
|
|
108
|
+
emailed?: boolean;
|
|
94
109
|
}
|
|
95
110
|
export declare function exchangeEnrollment(apiUrl: string, code: string): Promise<EnrollmentExchangeResult>;
|
|
96
111
|
export declare function createEnrollment(apiUrl: string, adminToken: string, input: {
|
|
97
112
|
workspace?: string;
|
|
98
113
|
label?: string;
|
|
114
|
+
email?: string;
|
|
99
115
|
enrollmentSeconds?: number;
|
|
100
116
|
tokenExpiresInSeconds?: number;
|
|
101
117
|
scopes?: Array<"files:read" | "files:write" | "files:delete">;
|
package/dist/client.js
CHANGED
|
@@ -38,19 +38,19 @@ function usageBase(config) {
|
|
|
38
38
|
}
|
|
39
39
|
function mapApiError(status, error, code) {
|
|
40
40
|
const normalized = error.toLowerCase();
|
|
41
|
-
if (status === 401 || normalized === "unauthorized") {
|
|
41
|
+
if (status === 401 || code === "unauthorized" || normalized === "unauthorized") {
|
|
42
42
|
return new UploadsError(error, "UNAUTHORIZED", status);
|
|
43
43
|
}
|
|
44
|
-
if (status === 404 || normalized === "not found") {
|
|
44
|
+
if (status === 404 || code === "not_found" || normalized === "not found") {
|
|
45
45
|
return new UploadsError(error, "NOT_FOUND", status);
|
|
46
46
|
}
|
|
47
|
-
if (status === 400 && normalized === "invalid key") {
|
|
47
|
+
if (code === "invalid_key" || (status === 400 && normalized === "invalid key")) {
|
|
48
48
|
return new UploadsError(error, "INVALID_KEY", status);
|
|
49
49
|
}
|
|
50
50
|
if (code === "key_prefix_not_allowed" || code === "key_too_deep") {
|
|
51
51
|
return new UploadsError(error, "KEY_POLICY", status);
|
|
52
52
|
}
|
|
53
|
-
// Prefer stable body
|
|
53
|
+
// Prefer stable body code — bare 429 is also used for write rate limits.
|
|
54
54
|
if (status === 507 || code === "storage_quota_exceeded") {
|
|
55
55
|
return new UploadsError(error, "STORAGE_QUOTA", status);
|
|
56
56
|
}
|
|
@@ -59,15 +59,34 @@ function mapApiError(status, error, code) {
|
|
|
59
59
|
}
|
|
60
60
|
return new UploadsError(error, "API_ERROR", status);
|
|
61
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* Parse API error bodies. Prefers the nested envelope
|
|
64
|
+
* `{ error: { code, type, message, details? } }`; still accepts the legacy
|
|
65
|
+
* flat `{ error: string, code?: string }` shape.
|
|
66
|
+
*/
|
|
67
|
+
function extractErrorFields(body) {
|
|
68
|
+
if (typeof body === "object" && body && "error" in body) {
|
|
69
|
+
const err = body.error;
|
|
70
|
+
if (typeof err === "object" && err && "message" in err) {
|
|
71
|
+
const nested = err;
|
|
72
|
+
return {
|
|
73
|
+
message: typeof nested.message === "string" ? nested.message : "request failed",
|
|
74
|
+
code: typeof nested.code === "string" ? nested.code : undefined,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
if (typeof err === "string") {
|
|
78
|
+
const code = "code" in body && typeof body.code === "string"
|
|
79
|
+
? body.code
|
|
80
|
+
: undefined;
|
|
81
|
+
return { message: err, code };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return { message: "request failed" };
|
|
85
|
+
}
|
|
62
86
|
async function parseErrorResponse(res) {
|
|
63
87
|
const body = await res.json().catch(() => ({}));
|
|
64
|
-
const message
|
|
65
|
-
|
|
66
|
-
: res.statusText || "request failed";
|
|
67
|
-
const code = typeof body === "object" && body && "code" in body && typeof body.code === "string"
|
|
68
|
-
? body.code
|
|
69
|
-
: undefined;
|
|
70
|
-
return mapApiError(res.status, message, code);
|
|
88
|
+
const { message, code } = extractErrorFields(body);
|
|
89
|
+
return mapApiError(res.status, message || res.statusText || "request failed", code);
|
|
71
90
|
}
|
|
72
91
|
export function createUploadsClient(config) {
|
|
73
92
|
async function request(method, path, opts) {
|
|
@@ -117,9 +136,16 @@ export function createUploadsClient(config) {
|
|
|
117
136
|
deriveRepoFromGit: opts.deriveRepoFromGit,
|
|
118
137
|
}));
|
|
119
138
|
const contentType = opts.contentType ?? inferContentType(opts.filename);
|
|
139
|
+
const headers = { "Content-Type": contentType };
|
|
140
|
+
if (opts.provenance) {
|
|
141
|
+
for (const [k, v] of Object.entries(opts.provenance)) {
|
|
142
|
+
if (v !== undefined && v !== "")
|
|
143
|
+
headers[`X-Uploads-Meta-${k}`] = v;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
120
146
|
const result = await request("PUT", `${filesBase(config)}/${encodeKeyPath(key)}`, {
|
|
121
147
|
body,
|
|
122
|
-
headers
|
|
148
|
+
headers,
|
|
123
149
|
});
|
|
124
150
|
if (result.url == null) {
|
|
125
151
|
throw new UploadsError("upload succeeded but workspace has no publicBaseUrl", "NO_PUBLIC_URL", 201);
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
type FileScope = "files:read" | "files:write" | "files:delete";
|
|
2
|
+
export declare function invitePageUrl(apiUrl: string, pageId: string, webUrl?: string): string;
|
|
3
|
+
export declare function inviteMagicLink(pageUrl: string, code: string): string;
|
|
2
4
|
export declare function parseScopes(raw: string | undefined): FileScope[] | undefined;
|
|
3
5
|
export declare function runAdmin(args: string[], opts: {
|
|
4
6
|
json?: boolean;
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { createEnrollment } from "../client.js";
|
|
2
|
-
import { flagInt, flagString, parseCommandArgs, UsageError } from "../cli-args.js";
|
|
3
|
-
const HELP = `uploads admin
|
|
2
|
+
import { flagBool, flagInt, flagString, parseCommandArgs, UsageError } from "../cli-args.js";
|
|
3
|
+
const HELP = `uploads admin invite create [options]
|
|
4
4
|
|
|
5
|
-
Admin-only: create a short-lived
|
|
5
|
+
Admin-only: create a short-lived invitation for an existing workspace.
|
|
6
|
+
Prints one magic link whose URL fragment carries the single-use code — treat the
|
|
7
|
+
link like a password. Pass --separate-code for the legacy two-channel output (a
|
|
8
|
+
non-secret page URL plus a code you share separately). The legacy
|
|
9
|
+
"admin enrollment create" spelling is accepted.
|
|
6
10
|
|
|
7
11
|
Options:
|
|
8
12
|
--admin-token <token> Or ADMIN_TOKEN (UPLOADS_ADMIN_TOKEN is a legacy alias)
|
|
@@ -11,9 +15,34 @@ Options:
|
|
|
11
15
|
--expires-in <seconds> Default: server policy
|
|
12
16
|
--token-expires-in <seconds> Upload token lifetime (default: server policy)
|
|
13
17
|
--scopes <list> Comma-separated files:read,files:write,files:delete
|
|
18
|
+
--email <address> Email the invite link to this recipient (from invites@uploads.sh)
|
|
19
|
+
--separate-code Two-channel output: non-secret page URL + separate code
|
|
14
20
|
--api-url <url> Default: https://api.uploads.sh
|
|
21
|
+
--web-url <url> Invite-page origin (defaults from --api-url)
|
|
15
22
|
`;
|
|
16
23
|
const FILE_SCOPES = new Set(["files:read", "files:write", "files:delete"]);
|
|
24
|
+
export function invitePageUrl(apiUrl, pageId, webUrl) {
|
|
25
|
+
let url;
|
|
26
|
+
try {
|
|
27
|
+
url = new URL(webUrl ?? apiUrl);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
throw new UsageError("invalid invite web URL");
|
|
31
|
+
}
|
|
32
|
+
if (!webUrl && url.hostname.startsWith("api."))
|
|
33
|
+
url.hostname = url.hostname.slice(4);
|
|
34
|
+
url.pathname = "/invite";
|
|
35
|
+
url.search = "";
|
|
36
|
+
url.hash = "";
|
|
37
|
+
url.searchParams.set("id", pageId);
|
|
38
|
+
return url.toString();
|
|
39
|
+
}
|
|
40
|
+
// Compose the self-contained magic link. The one-time code rides in the URL
|
|
41
|
+
// fragment (#code=…), which browsers never send to the server, so opening the
|
|
42
|
+
// page neither leaks nor consumes it — only the CLI's exchange call redeems it.
|
|
43
|
+
export function inviteMagicLink(pageUrl, code) {
|
|
44
|
+
return `${pageUrl}#code=${encodeURIComponent(code)}`;
|
|
45
|
+
}
|
|
17
46
|
export function parseScopes(raw) {
|
|
18
47
|
if (raw === undefined)
|
|
19
48
|
return undefined;
|
|
@@ -34,26 +63,44 @@ export async function runAdmin(args, opts, help = false) {
|
|
|
34
63
|
process.stderr.write(HELP);
|
|
35
64
|
return 0;
|
|
36
65
|
}
|
|
37
|
-
if (
|
|
38
|
-
|
|
66
|
+
if (!["invite", "enrollment"].includes(parsed.positionals[0] ?? "") ||
|
|
67
|
+
parsed.positionals[1] !== "create")
|
|
68
|
+
throw new UsageError("expected: uploads admin invite create");
|
|
39
69
|
const adminToken = flagString(parsed.flags, "--admin-token") ??
|
|
40
70
|
process.env.ADMIN_TOKEN ??
|
|
41
71
|
process.env.UPLOADS_ADMIN_TOKEN;
|
|
42
72
|
if (!adminToken)
|
|
43
73
|
throw new UsageError("ADMIN_TOKEN is required for admin enrollment creation");
|
|
44
74
|
const apiUrl = flagString(parsed.flags, "--api-url") ?? opts.apiUrl ?? "https://api.uploads.sh";
|
|
75
|
+
const webUrl = flagString(parsed.flags, "--web-url");
|
|
45
76
|
const workspace = flagString(parsed.flags, "--workspace") ?? "default";
|
|
46
77
|
const label = flagString(parsed.flags, "--label");
|
|
78
|
+
const email = flagString(parsed.flags, "--email");
|
|
47
79
|
const result = await createEnrollment(apiUrl, adminToken, {
|
|
48
80
|
workspace,
|
|
49
81
|
label,
|
|
82
|
+
email,
|
|
50
83
|
enrollmentSeconds: flagInt(parsed.flags, "--expires-in", "--expires-in"),
|
|
51
84
|
tokenExpiresInSeconds: flagInt(parsed.flags, "--token-expires-in", "--token-expires-in"),
|
|
52
85
|
scopes: parseScopes(flagString(parsed.flags, "--scopes")),
|
|
53
86
|
});
|
|
54
|
-
|
|
55
|
-
|
|
87
|
+
const separateCode = flagBool(parsed.flags, "--separate-code");
|
|
88
|
+
const pageUrl = invitePageUrl(apiUrl, result.pageId, webUrl);
|
|
89
|
+
const link = separateCode ? pageUrl : inviteMagicLink(pageUrl, result.code);
|
|
90
|
+
const footer = `workspace: ${workspace}\nexpires: ${result.expiresAt}\n`;
|
|
91
|
+
if (opts.json) {
|
|
92
|
+
process.stdout.write(JSON.stringify({ workspace, label: label ?? null, url: link, ...result }, null, 2) + "\n");
|
|
93
|
+
return 0;
|
|
94
|
+
}
|
|
95
|
+
if (email && result.emailed) {
|
|
96
|
+
process.stdout.write(`Invite emailed to ${email}\n${footer}`);
|
|
97
|
+
return 0;
|
|
98
|
+
}
|
|
99
|
+
if (email && result.emailed === false)
|
|
100
|
+
process.stderr.write("warning: email delivery failed; share this link instead\n");
|
|
101
|
+
if (separateCode)
|
|
102
|
+
process.stdout.write(`Invite page: ${pageUrl}\nOne-time code (share separately): ${result.code}\n${footer}`);
|
|
56
103
|
else
|
|
57
|
-
process.stdout.write(`
|
|
104
|
+
process.stdout.write(`Invite link (contains the one-time code — treat like a password):\n${link}\n${footer}`);
|
|
58
105
|
return 0;
|
|
59
106
|
}
|
package/dist/commands.js
CHANGED
|
@@ -11,6 +11,7 @@ import { resolveRepo, resolveCurrentPullRequest, execRunner, upsertAttachmentsCo
|
|
|
11
11
|
import { resolvePutPrefix } from "./destinations.js";
|
|
12
12
|
import { optimizeImageForUpload, rewriteKeyExtension, } from "./optimize.js";
|
|
13
13
|
import { applyFrame, resolveFrameId } from "./frame.js";
|
|
14
|
+
import { buildCliProvenance } from "./provenance.js";
|
|
14
15
|
// --- put ---
|
|
15
16
|
const PUT_HELP = `uploads put <file> [options]
|
|
16
17
|
|
|
@@ -24,6 +25,10 @@ upload as-is, or --keep-exif when image metadata matters for the discussion.
|
|
|
24
25
|
Optional --frame wraps the image in a device/browser chrome before optimize
|
|
25
26
|
(default off). See: uploads put --help frames
|
|
26
27
|
|
|
28
|
+
Uploads are public. --pr/--issue keys include the repo, number, and filename and
|
|
29
|
+
remain public even for private/internal GitHub repositories. Upload only media
|
|
30
|
+
that is safe at a predictable public URL.
|
|
31
|
+
|
|
27
32
|
Options:
|
|
28
33
|
--key <key> Object key (default: <prefix>/<repo>/<ref>/<name>-<hash>.<ext>)
|
|
29
34
|
--destination <id> Typed root: screenshots | gh | f (sets --prefix)
|
|
@@ -165,6 +170,10 @@ const ATTACH_HELP = `uploads attach <file...> [options]
|
|
|
165
170
|
Upload one or more stable PR/issue attachments and maintain a single GitHub
|
|
166
171
|
comment. With no target, uses the pull request for the current branch.
|
|
167
172
|
|
|
173
|
+
Attachments are public and their repo/number/filename keys are predictable.
|
|
174
|
+
Private/internal GitHub repository visibility does not restrict access; upload
|
|
175
|
+
only media that is safe at a public URL.
|
|
176
|
+
|
|
168
177
|
Still images are optimized to WebP by default (same as put). Use --no-optimize
|
|
169
178
|
to upload originals. Optional --frame wraps images in device/browser chrome.
|
|
170
179
|
|
|
@@ -230,6 +239,12 @@ export async function runAttach(ctx, args, help = false, run = execRunner) {
|
|
|
230
239
|
filename: prepared.filename,
|
|
231
240
|
key: ghAttachmentKey(target, prepared.filename),
|
|
232
241
|
contentType: prepared.optimized ? prepared.contentType : contentTypeOverride,
|
|
242
|
+
provenance: buildCliProvenance({
|
|
243
|
+
sourceName,
|
|
244
|
+
optimized: prepared.optimized,
|
|
245
|
+
frameId: prepared.frame?.framed ? prepared.frame.frameId : undefined,
|
|
246
|
+
keepExif: optimizeOpts.keepExif === true,
|
|
247
|
+
}),
|
|
233
248
|
});
|
|
234
249
|
results.push({
|
|
235
250
|
...result,
|
|
@@ -363,6 +378,12 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
363
378
|
ref: flagString(parsed.flags, "--ref") ?? defaults.ref,
|
|
364
379
|
contentType: prepared.optimized ? prepared.contentType : contentTypeOverride,
|
|
365
380
|
deriveRepoFromGit: !noGit,
|
|
381
|
+
provenance: buildCliProvenance({
|
|
382
|
+
sourceName,
|
|
383
|
+
optimized: prepared.optimized,
|
|
384
|
+
frameId: prepared.frame?.framed ? prepared.frame.frameId : undefined,
|
|
385
|
+
keepExif: optimizeOpts.keepExif === true,
|
|
386
|
+
}),
|
|
366
387
|
});
|
|
367
388
|
const markdown = buildMarkdown(result.url, { alt, width });
|
|
368
389
|
const optimizeMeta = {
|
package/dist/index.d.ts
CHANGED
|
@@ -3,7 +3,8 @@ export { sanitizeKeySegment, sha256Short, deriveRepoFromGit, buildScreenshotKey
|
|
|
3
3
|
export { BUILTIN_DESTINATIONS, isBuiltinDestination, keyMatchesDestination, resolveDestinationRoot, resolvePutPrefix, type BuiltinDestinationId, } from "./destinations.js";
|
|
4
4
|
export { DEFAULT_API_URL, DEFAULT_WORKSPACE, UPLOADS_CONFIG_KEYS, defaultConfigPath, resolveConfigPath, loadConfigFile, loadEnvFile, resolveApiUrl, resolveConfig, describeConfigSources, redactToken, writeConfigKeys, configValuesFromClient, putDefaultsToConfigValues, resolvePutDefaults, mergePutDefaults, workspaceFromToken, workspaceMismatch, type UploadsClientConfig, type ResolvedConfig, type WorkspaceSource, type ConfigValueSource, type ConfigSources, type UploadsConfigKey, type UploadsConfigValues, type PutDefaults, } from "./config.js";
|
|
5
5
|
export { UploadsError, type UploadsErrorCode } from "./errors.js";
|
|
6
|
-
export { createUploadsClient, type UploadsClient, type PutOptions, type ListOptions, type PutResult, type ListItem, type ListResult, type HeadResult, type DeleteResult, type HealthResult, type UsageResult, type ReconcileResult, type PurgeExpiredResult, type PurgeExpiredResponse, } from "./client.js";
|
|
6
|
+
export { createUploadsClient, type UploadsClient, type PutOptions, type ProvenanceInput, type ListOptions, type PutResult, type ListItem, type ListResult, type HeadResult, type DeleteResult, type HealthResult, type UsageResult, type ReconcileResult, type PurgeExpiredResult, type PurgeExpiredResponse, } from "./client.js";
|
|
7
|
+
export { buildCliProvenance } from "./provenance.js";
|
|
7
8
|
export { ATTACHMENTS_MARKER, attachmentsCommentBody, ghAttachmentKey, ghKeyPrefix, isValidRepo, parseRepoFromRemoteUrl, type AttachmentItem, type GhTarget, type GhTargetKind, } from "./github.js";
|
|
8
9
|
export { DEFAULT_OPTIMIZE_MAX_EDGE, DEFAULT_OPTIMIZE_QUALITY, optimizeImageForUpload, rewriteKeyExtension, withImageExtension, type OptimizeImageOptions, type OptimizeImageResult, type OptimizeOutputFormat, } from "./optimize.js";
|
|
9
10
|
export { FRAME_PRESETS, applyFrame, listFramePresets, resolveFrameId, type FrameFit, type FrameOptions, type FrameResult, } from "./frame.js";
|
package/dist/index.js
CHANGED
|
@@ -4,6 +4,7 @@ export { BUILTIN_DESTINATIONS, isBuiltinDestination, keyMatchesDestination, reso
|
|
|
4
4
|
export { DEFAULT_API_URL, DEFAULT_WORKSPACE, UPLOADS_CONFIG_KEYS, defaultConfigPath, resolveConfigPath, loadConfigFile, loadEnvFile, resolveApiUrl, resolveConfig, describeConfigSources, redactToken, writeConfigKeys, configValuesFromClient, putDefaultsToConfigValues, resolvePutDefaults, mergePutDefaults, workspaceFromToken, workspaceMismatch, } from "./config.js";
|
|
5
5
|
export { UploadsError } from "./errors.js";
|
|
6
6
|
export { createUploadsClient, } from "./client.js";
|
|
7
|
+
export { buildCliProvenance } from "./provenance.js";
|
|
7
8
|
export { ATTACHMENTS_MARKER, attachmentsCommentBody, ghAttachmentKey, ghKeyPrefix, isValidRepo, parseRepoFromRemoteUrl, } from "./github.js";
|
|
8
9
|
export { DEFAULT_OPTIMIZE_MAX_EDGE, DEFAULT_OPTIMIZE_QUALITY, optimizeImageForUpload, rewriteKeyExtension, withImageExtension, } from "./optimize.js";
|
|
9
10
|
export { FRAME_PRESETS, applyFrame, listFramePresets, resolveFrameId, } from "./frame.js";
|
package/dist/mcp/tools.js
CHANGED
|
@@ -15,6 +15,7 @@ import { buildMarkdown } from "../embed.js";
|
|
|
15
15
|
import { resolvePutPrefix } from "../destinations.js";
|
|
16
16
|
import { ghAttachmentKey, ghKeyPrefix } from "../github.js";
|
|
17
17
|
import { rewriteKeyExtension } from "../optimize.js";
|
|
18
|
+
import { buildCliProvenance } from "../provenance.js";
|
|
18
19
|
import { execRunner, resolveCurrentPullRequest, resolveRepo, } from "../github-gh.js";
|
|
19
20
|
import { optPosInt, optString, usage } from "./args.js";
|
|
20
21
|
function optBool(args, name) {
|
|
@@ -137,7 +138,7 @@ export function createUploadsMcpTools(opts) {
|
|
|
137
138
|
return [
|
|
138
139
|
{
|
|
139
140
|
name: "put",
|
|
140
|
-
description: "Upload a file to uploads.sh and get a public URL plus GitHub-ready embed markdown (the returned `markdown` is ready to paste into a PR or issue). Pass `file` (a local path) or `contentBase64` + `filename` for in-memory content; with `pr`/`issue` the key is stable (same filename → same URL) and `comment` syncs the managed attachments comment.",
|
|
141
|
+
description: "Upload a file to uploads.sh and get a public URL plus GitHub-ready embed markdown (the returned `markdown` is ready to paste into a PR or issue). Pass `file` (a local path) or `contentBase64` + `filename` for in-memory content; with `pr`/`issue` the key is stable (same filename → same URL) and `comment` syncs the managed attachments comment. All uploads are public; pr/issue keys are predictable and remain public for private/internal GitHub repositories, so upload only non-sensitive media.",
|
|
141
142
|
inputSchema: {
|
|
142
143
|
type: "object",
|
|
143
144
|
properties: {
|
|
@@ -254,9 +255,11 @@ export function createUploadsMcpTools(opts) {
|
|
|
254
255
|
: new Uint8Array(Buffer.from(contentBase64, "base64"));
|
|
255
256
|
const sourceName = file !== undefined ? (filenameArg ?? basename(file)) : filenameArg;
|
|
256
257
|
const defaults = resolvePutDefaults({ envFile: globals.envFile });
|
|
258
|
+
const frameOpts = mcpFrameOptions(args);
|
|
259
|
+
const optimizeOpts = mcpOptimizeOptions(args, defaults);
|
|
257
260
|
const prepared = await prepareImageForUpload(bytes, sourceName, {
|
|
258
|
-
...
|
|
259
|
-
optimize:
|
|
261
|
+
...frameOpts,
|
|
262
|
+
optimize: optimizeOpts,
|
|
260
263
|
});
|
|
261
264
|
const filename = prepared.filename;
|
|
262
265
|
let key = target ? ghAttachmentKey(target, filename) : keyArg;
|
|
@@ -271,6 +274,13 @@ export function createUploadsMcpTools(opts) {
|
|
|
271
274
|
ref: refArg ?? defaults.ref,
|
|
272
275
|
contentType: prepared.optimized ? prepared.contentType : optString(args, "contentType"),
|
|
273
276
|
deriveRepoFromGit: !noGit,
|
|
277
|
+
provenance: buildCliProvenance({
|
|
278
|
+
sourceName,
|
|
279
|
+
client: "uploads-mcp",
|
|
280
|
+
optimized: prepared.optimized,
|
|
281
|
+
frameId: prepared.frame?.framed ? prepared.frame.frameId : undefined,
|
|
282
|
+
keepExif: optimizeOpts.keepExif === true,
|
|
283
|
+
}),
|
|
274
284
|
});
|
|
275
285
|
const markdown = buildMarkdown(result.url, {
|
|
276
286
|
alt: optString(args, "alt") ?? sourceName,
|
|
@@ -292,7 +302,7 @@ export function createUploadsMcpTools(opts) {
|
|
|
292
302
|
},
|
|
293
303
|
{
|
|
294
304
|
name: "attach",
|
|
295
|
-
description: "Upload one or more files as stable PR/issue attachments and maintain a single managed GitHub comment listing them (each upload's `markdown` is ready to paste into GitHub). With no pr/issue, targets the pull request for the current branch.",
|
|
305
|
+
description: "Upload one or more files as stable PR/issue attachments and maintain a single managed GitHub comment listing them (each upload's `markdown` is ready to paste into GitHub). With no pr/issue, targets the pull request for the current branch. Attachments are public and their repo/number/filename keys are predictable even for private/internal GitHub repositories; upload only non-sensitive media.",
|
|
296
306
|
inputSchema: {
|
|
297
307
|
type: "object",
|
|
298
308
|
properties: {
|
|
@@ -352,6 +362,13 @@ export function createUploadsMcpTools(opts) {
|
|
|
352
362
|
filename: prepared.filename,
|
|
353
363
|
key: ghAttachmentKey(target, prepared.filename),
|
|
354
364
|
contentType: prepared.optimized ? prepared.contentType : contentType,
|
|
365
|
+
provenance: buildCliProvenance({
|
|
366
|
+
sourceName,
|
|
367
|
+
client: "uploads-mcp",
|
|
368
|
+
optimized: prepared.optimized,
|
|
369
|
+
frameId: prepared.frame?.framed ? prepared.frame.frameId : undefined,
|
|
370
|
+
keepExif: optimizeOpts.keepExif === true,
|
|
371
|
+
}),
|
|
355
372
|
});
|
|
356
373
|
uploads.push({
|
|
357
374
|
...result,
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-side provenance headers for put (X-Uploads-Meta-*).
|
|
3
|
+
* API allowlists keys; secrets never go here.
|
|
4
|
+
*/
|
|
5
|
+
import type { ProvenanceInput } from "./client.js";
|
|
6
|
+
export declare function buildCliProvenance(opts: {
|
|
7
|
+
sourceName: string;
|
|
8
|
+
optimized?: boolean;
|
|
9
|
+
frameId?: string;
|
|
10
|
+
keepExif?: boolean;
|
|
11
|
+
client?: string;
|
|
12
|
+
}): ProvenanceInput;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
let cachedVersion;
|
|
3
|
+
function packageVersion() {
|
|
4
|
+
if (cachedVersion)
|
|
5
|
+
return cachedVersion;
|
|
6
|
+
try {
|
|
7
|
+
const require = createRequire(import.meta.url);
|
|
8
|
+
const pkg = require("../package.json");
|
|
9
|
+
cachedVersion = pkg.version ?? "0.0.0";
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
cachedVersion = "0.0.0";
|
|
13
|
+
}
|
|
14
|
+
return cachedVersion;
|
|
15
|
+
}
|
|
16
|
+
export function buildCliProvenance(opts) {
|
|
17
|
+
const provenance = {
|
|
18
|
+
client: opts.client ?? "uploads-cli",
|
|
19
|
+
"client-version": packageVersion(),
|
|
20
|
+
"source-name": opts.sourceName.slice(0, 128),
|
|
21
|
+
};
|
|
22
|
+
if (opts.optimized)
|
|
23
|
+
provenance.optimized = "1";
|
|
24
|
+
if (opts.frameId)
|
|
25
|
+
provenance.frame = opts.frameId.slice(0, 64);
|
|
26
|
+
if (opts.keepExif)
|
|
27
|
+
provenance["keep-exif"] = "1";
|
|
28
|
+
return provenance;
|
|
29
|
+
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@buildinternet/uploads",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "CLI and client for uploads.sh — workspace-scoped image hosting for GitHub embeds",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"sideEffects": false,
|
|
6
7
|
"license": "MIT",
|
|
7
8
|
"repository": {
|
|
8
9
|
"type": "git",
|