@buildinternet/uploads 0.4.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 CHANGED
@@ -4,11 +4,11 @@ CLI and client for **uploads.sh** — upload files, get public URLs, and produce
4
4
 
5
5
  ## CLI
6
6
 
7
- Binary: **`uploads`**. Install globally (or use a pinned `npx` one-shot):
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@0.1.0 --help
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). Prefer keys under screenshots/.",
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
@@ -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 enrollment management
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
@@ -101,14 +101,17 @@ export interface EnrollmentExchangeResult {
101
101
  expiresAt?: string;
102
102
  }
103
103
  export interface EnrollmentCreateResult {
104
+ pageId: string;
104
105
  code: string;
105
106
  expiresAt: string;
106
107
  tokenExpiresAt: string;
108
+ emailed?: boolean;
107
109
  }
108
110
  export declare function exchangeEnrollment(apiUrl: string, code: string): Promise<EnrollmentExchangeResult>;
109
111
  export declare function createEnrollment(apiUrl: string, adminToken: string, input: {
110
112
  workspace?: string;
111
113
  label?: string;
114
+ email?: string;
112
115
  enrollmentSeconds?: number;
113
116
  tokenExpiresInSeconds?: number;
114
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.code — bare 429 is also used for write rate limits.
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 = typeof body === "object" && body && "error" in body && typeof body.error === "string"
65
- ? body.error
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) {
@@ -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 enrollment create [options]
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, one-time enrollment code.
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 (parsed.positionals[0] !== "enrollment" || parsed.positionals[1] !== "create")
38
- throw new UsageError("expected: uploads admin enrollment create");
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
- if (opts.json)
55
- process.stdout.write(JSON.stringify({ workspace, label: label ?? null, ...result }, null, 2) + "\n");
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(`Enrollment code (share once): ${result.code}\nworkspace: ${workspace}\nexpires: ${result.expiresAt}\n`);
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
@@ -25,6 +25,10 @@ upload as-is, or --keep-exif when image metadata matters for the discussion.
25
25
  Optional --frame wraps the image in a device/browser chrome before optimize
26
26
  (default off). See: uploads put --help frames
27
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
+
28
32
  Options:
29
33
  --key <key> Object key (default: <prefix>/<repo>/<ref>/<name>-<hash>.<ext>)
30
34
  --destination <id> Typed root: screenshots | gh | f (sets --prefix)
@@ -166,6 +170,10 @@ const ATTACH_HELP = `uploads attach <file...> [options]
166
170
  Upload one or more stable PR/issue attachments and maintain a single GitHub
167
171
  comment. With no target, uses the pull request for the current branch.
168
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
+
169
177
  Still images are optimized to WebP by default (same as put). Use --no-optimize
170
178
  to upload originals. Optional --frame wraps images in device/browser chrome.
171
179
 
package/dist/mcp/tools.js CHANGED
@@ -138,7 +138,7 @@ export function createUploadsMcpTools(opts) {
138
138
  return [
139
139
  {
140
140
  name: "put",
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.",
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.",
142
142
  inputSchema: {
143
143
  type: "object",
144
144
  properties: {
@@ -302,7 +302,7 @@ export function createUploadsMcpTools(opts) {
302
302
  },
303
303
  {
304
304
  name: "attach",
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.",
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.",
306
306
  inputSchema: {
307
307
  type: "object",
308
308
  properties: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@buildinternet/uploads",
3
- "version": "0.4.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
6
  "sideEffects": false,