@buildinternet/uploads 0.1.0 → 0.2.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/LICENSE +21 -0
- package/README.md +79 -64
- package/bin/uploads.js +0 -0
- package/dist/cli.js +81 -48
- package/dist/client.d.ts +68 -1
- package/dist/client.js +75 -12
- package/dist/commands/admin-enrollment.d.ts +7 -0
- package/dist/commands/admin-enrollment.js +59 -0
- package/dist/commands/install.d.ts +8 -0
- package/dist/commands/install.js +133 -0
- package/dist/commands/login.d.ts +11 -0
- package/dist/commands/login.js +160 -0
- package/dist/commands/mcp.d.ts +4 -0
- package/dist/commands/mcp.js +39 -0
- package/dist/commands/setup.js +6 -16
- package/dist/commands.d.ts +47 -0
- package/dist/commands.js +226 -133
- package/dist/config-file.js +14 -2
- package/dist/config.js +1 -0
- package/dist/errors.d.ts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/io.d.ts +3 -0
- package/dist/io.js +9 -0
- package/dist/mcp/args.d.ts +4 -0
- package/dist/mcp/args.js +26 -0
- package/dist/mcp/server.d.ts +19 -0
- package/dist/mcp/server.js +109 -0
- package/dist/mcp/stdio.d.ts +3 -0
- package/dist/mcp/stdio.js +14 -0
- package/dist/mcp/tools.d.ts +10 -0
- package/dist/mcp/tools.js +386 -0
- package/package.json +63 -60
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Build Internet
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,64 +1,79 @@
|
|
|
1
|
-
# @buildinternet/uploads
|
|
2
|
-
|
|
3
|
-
CLI and client for **uploads.sh** — upload files, get public URLs, and produce GitHub-ready markdown. Successor to the R2 scripts in `buildinternet-skills/github-screenshots`.
|
|
4
|
-
|
|
5
|
-
## CLI
|
|
6
|
-
|
|
7
|
-
Binary: `uploads` (also `pnpm uploads` from repo root after `pnpm install`).
|
|
8
|
-
|
|
9
|
-
Install globally or run a pinned version without installing:
|
|
10
|
-
|
|
11
|
-
```bash
|
|
12
|
-
npm install --global @buildinternet/uploads
|
|
13
|
-
npx @buildinternet/uploads@0.1.0 --help
|
|
14
|
-
```
|
|
15
|
-
|
|
16
|
-
```bash
|
|
17
|
-
pnpm uploads setup --env-file .env
|
|
18
|
-
pnpm uploads attach ./before.png ./after.png --env-file .env
|
|
19
|
-
pnpm uploads put ./shot.png --env-file .env
|
|
20
|
-
pnpm uploads put ./after.png --pr 123 --comment --env-file .env
|
|
21
|
-
pnpm uploads doctor --env-file .env
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
Commands: `attach`, `put`, `comment`, `list`, `delete`, `
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
1
|
+
# @buildinternet/uploads
|
|
2
|
+
|
|
3
|
+
CLI and client for **uploads.sh** — upload files, get public URLs, and produce GitHub-ready markdown. Successor to the R2 scripts in `buildinternet-skills/github-screenshots`.
|
|
4
|
+
|
|
5
|
+
## CLI
|
|
6
|
+
|
|
7
|
+
Binary: `uploads` (also `pnpm uploads` from repo root after `pnpm install`).
|
|
8
|
+
|
|
9
|
+
Install globally or run a pinned version without installing:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install --global @buildinternet/uploads
|
|
13
|
+
npx @buildinternet/uploads@0.1.0 --help
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pnpm uploads setup --env-file .env
|
|
18
|
+
pnpm uploads attach ./before.png ./after.png --env-file .env
|
|
19
|
+
pnpm uploads put ./shot.png --env-file .env
|
|
20
|
+
pnpm uploads put ./after.png --pr 123 --comment --env-file .env
|
|
21
|
+
pnpm uploads doctor --env-file .env
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Commands: `attach`, `put`, `comment`, `list`, `delete`, `usage`, `reconcile`,
|
|
25
|
+
`purge-expired`, `setup`, `install`, `config`, `doctor`, `health`, `mcp`.
|
|
26
|
+
|
|
27
|
+
`attach` is the agent-friendly default for GitHub media. It accepts one or more files,
|
|
28
|
+
infers the pull request for the current branch via `gh`, uploads stable URLs, and creates
|
|
29
|
+
or updates one managed attachments comment. Use `--pr`, `--issue`, and `--repo` to select
|
|
30
|
+
the target explicitly, or `--no-comment` to upload without changing GitHub comments.
|
|
31
|
+
|
|
32
|
+
Config layers (first match wins): CLI flags → env vars → `--env-file` → `~/.config/buildinternet/config`. See `config.example` for keys.
|
|
33
|
+
|
|
34
|
+
## MCP server
|
|
35
|
+
|
|
36
|
+
`uploads mcp` serves the Model Context Protocol over stdio (newline-delimited JSON-RPC, no extra dependencies). Tools mirror the CLI commands one-to-one — `put`, `attach`, `list`, `delete`, `usage`, `reconcile`, `purge_expired`, `comment`, `health`, `doctor` — with the same config resolution and defaults, plus a per-call `workspace` argument. Interactive/credential commands (`setup`, `login`, `admin`, `config`) are not exposed. A token isn't required to start the server; auth errors surface per tool call (`health` needs no auth).
|
|
37
|
+
|
|
38
|
+
```json
|
|
39
|
+
{ "command": "uploads", "args": ["--env-file", "/path/to/.env", "mcp"] }
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Or with `UPLOADS_TOKEN`/`UPLOADS_WORKSPACE` in the environment or user config. Claude Code: `claude mcp add uploads -- uploads --env-file /path/to/.env mcp`.
|
|
43
|
+
|
|
44
|
+
For HTTP clients there's also a hosted variant at `https://agents.uploads.sh/mcp` — the workspace is inferred from the bearer token, so only the URL and token are needed (`https://agents.uploads.sh/<workspace>/mcp` and the `mcp.uploads.sh` hostname also work). Tools: put/list/delete/health, same bearer tokens as the REST API — see `apps/mcp` in the repo. `uploads install` registers it with Claude Code (and installs the agent skill) in one step. Its `put` takes no content type: the stored type is sniffed server-side from the bytes and checked against the workspace allowlist, and writes are rate limited per workspace.
|
|
45
|
+
|
|
46
|
+
## Programmatic use
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
import { createUploadsClient } from "@buildinternet/uploads";
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Agent/MCP helpers: `@buildinternet/uploads/agent` (`createUploadsWorkerFileTools` for Workers); for local stdio MCP, use `uploads mcp` (above).
|
|
53
|
+
|
|
54
|
+
## Layout
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
src/
|
|
58
|
+
cli.ts Entry + help
|
|
59
|
+
commands.ts put, list, delete, comment, …
|
|
60
|
+
commands/mcp.ts `mcp` command entry
|
|
61
|
+
mcp/ Stdio MCP server (server.ts, tools.ts)
|
|
62
|
+
client.ts HTTP client for the API
|
|
63
|
+
github.ts PR/issue key paths + attachment comments
|
|
64
|
+
embed.ts Markdown image output
|
|
65
|
+
bin/uploads.js Bin shim
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Commands
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
pnpm build # tsc → dist/
|
|
72
|
+
pnpm typecheck
|
|
73
|
+
pnpm test
|
|
74
|
+
pnpm pack:check # verify the npm tarball contents
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Maintainer release instructions: [`docs/releasing.md`](../../docs/releasing.md).
|
|
78
|
+
|
|
79
|
+
Agent-oriented usage: [`skills/uploads-cli/SKILL.md`](../../skills/uploads-cli/SKILL.md). REST details: [`docs/api.md`](../../docs/api.md).
|
package/bin/uploads.js
CHANGED
|
File without changes
|
package/dist/cli.js
CHANGED
|
@@ -2,56 +2,70 @@ import { createUploadsClient } from "./client.js";
|
|
|
2
2
|
import { resolveApiUrl, resolveConfig } from "./config.js";
|
|
3
3
|
import { UploadsError } from "./errors.js";
|
|
4
4
|
import { commandWorkspace, isHelpFlag, parseArgv, parseCommandArgs, UsageError, } from "./cli-args.js";
|
|
5
|
-
import { runPut, runAttach, runList, runDelete, runHealth, runDoctor, runComment, } from "./commands.js";
|
|
5
|
+
import { runPut, runAttach, runList, runDelete, runHealth, runDoctor, runComment, runUsage, runReconcile, runPurgeExpired, } from "./commands.js";
|
|
6
6
|
import { runConfig } from "./commands/config.js";
|
|
7
7
|
import { runSetup } from "./commands/setup.js";
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
--
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
8
|
+
import { runLogin } from "./commands/login.js";
|
|
9
|
+
import { runAdmin } from "./commands/admin-enrollment.js";
|
|
10
|
+
import { runMcp } from "./commands/mcp.js";
|
|
11
|
+
import { runInstall } from "./commands/install.js";
|
|
12
|
+
const ROOT_HELP = `uploads — CLI for uploads.sh (GitHub image embeds)
|
|
13
|
+
|
|
14
|
+
Usage:
|
|
15
|
+
uploads [globals] <command> [args]
|
|
16
|
+
|
|
17
|
+
Config (first match wins, per key):
|
|
18
|
+
CLI flags --api-url, --token, --workspace
|
|
19
|
+
environment UPLOADS_API_URL, UPLOADS_TOKEN, UPLOADS_WORKSPACE
|
|
20
|
+
--env-file <path>
|
|
21
|
+
$BUILDINTERNET_CONFIG
|
|
22
|
+
~/.config/buildinternet/config (shared with github-screenshots)
|
|
23
|
+
|
|
24
|
+
Workspace (within config layers):
|
|
25
|
+
--workspace, -w override — global (before command) or per-command (after)
|
|
26
|
+
UPLOADS_WORKSPACE env / config file
|
|
27
|
+
(else inferred from token up_<name>_…, else "default")
|
|
28
|
+
|
|
29
|
+
Other globals (before command):
|
|
30
|
+
--api-url <url> default: https://api.uploads.sh
|
|
31
|
+
--token <token> or UPLOADS_TOKEN
|
|
32
|
+
--env-file <path>
|
|
33
|
+
--json JSON on stdout
|
|
34
|
+
--quiet
|
|
35
|
+
|
|
36
|
+
Commands:
|
|
37
|
+
attach <file...> Attach media to the current PR (stable URLs + managed comment)
|
|
38
|
+
put <file> Upload (+ URL + markdown for GitHub)
|
|
39
|
+
comment Create/update a PR/issue attachments comment (via gh)
|
|
40
|
+
list List objects
|
|
41
|
+
delete <key> Delete object
|
|
42
|
+
usage Workspace storage / upload counters
|
|
43
|
+
reconcile Rebuild usage ledger from storage
|
|
44
|
+
purge-expired Delete objects past retentionDays
|
|
45
|
+
setup Inspect/configure advanced CLI settings
|
|
46
|
+
install Install the agent skill + register the remote MCP server
|
|
47
|
+
login Exchange an enrollment code and configure credentials
|
|
48
|
+
admin Admin enrollment management
|
|
49
|
+
config Show path, init, or set shared config
|
|
50
|
+
doctor Health + auth + workspace checks
|
|
51
|
+
health API liveness (no auth)
|
|
52
|
+
mcp Serve MCP over stdio (tools mirror the CLI)
|
|
53
|
+
|
|
54
|
+
Put/list defaults (config file or env):
|
|
55
|
+
UPLOADS_DEFAULT_PREFIX, UPLOADS_DEFAULT_REPO, UPLOADS_DEFAULT_REF
|
|
56
|
+
UPLOADS_DEFAULT_WIDTH, UPLOADS_NO_GIT
|
|
57
|
+
|
|
58
|
+
Examples:
|
|
59
|
+
uploads setup
|
|
60
|
+
uploads setup --token up_default_… --repo myorg/myapp
|
|
61
|
+
uploads attach ./before.png ./after.png
|
|
62
|
+
uploads put ./shot.png --ref 42
|
|
63
|
+
uploads doctor
|
|
64
|
+
|
|
65
|
+
Agent/MCP: \`uploads install\` sets up the agent skill and the hosted MCP server
|
|
66
|
+
(https://agents.uploads.sh/mcp, workspace inferred from the token). Run
|
|
67
|
+
\`uploads mcp\` for local stdio, or use createUploadsWorkerFileTools()
|
|
68
|
+
from @buildinternet/uploads/agent on the Worker.
|
|
55
69
|
`;
|
|
56
70
|
function createContext(globals, requireToken, commandArgs) {
|
|
57
71
|
const cmdWorkspace = commandWorkspace(parseCommandArgs(commandArgs).flags);
|
|
@@ -80,6 +94,8 @@ function exitCode(err) {
|
|
|
80
94
|
return 2;
|
|
81
95
|
case "UNAUTHORIZED":
|
|
82
96
|
case "NOT_FOUND":
|
|
97
|
+
case "STORAGE_QUOTA":
|
|
98
|
+
case "UPLOAD_BUDGET":
|
|
83
99
|
return 3;
|
|
84
100
|
case "NETWORK":
|
|
85
101
|
return 4;
|
|
@@ -122,10 +138,21 @@ export async function runCli(argv) {
|
|
|
122
138
|
return runConfig(cmdArgs, { json, envFile: parsed.globals.envFile }, showHelp);
|
|
123
139
|
case "setup":
|
|
124
140
|
return runSetup(cmdArgs, { json, envFile: parsed.globals.envFile }, showHelp);
|
|
141
|
+
case "login":
|
|
142
|
+
return runLogin(cmdArgs, { json, apiUrl: resolveApiUrl(parsed.globals) }, showHelp);
|
|
143
|
+
case "admin":
|
|
144
|
+
return runAdmin(cmdArgs, { json, apiUrl: resolveApiUrl(parsed.globals) }, showHelp);
|
|
145
|
+
case "mcp":
|
|
146
|
+
return runMcp(cmdArgs, { globals: parsed.globals }, showHelp);
|
|
147
|
+
case "install":
|
|
148
|
+
return runInstall(cmdArgs, { globals: parsed.globals, json }, showHelp);
|
|
125
149
|
case "attach":
|
|
126
150
|
case "put":
|
|
127
151
|
case "list":
|
|
128
152
|
case "delete":
|
|
153
|
+
case "usage":
|
|
154
|
+
case "reconcile":
|
|
155
|
+
case "purge-expired":
|
|
129
156
|
case "doctor":
|
|
130
157
|
case "comment": {
|
|
131
158
|
const ctx = createContext(parsed.globals, !showHelp, cmdArgs);
|
|
@@ -140,6 +167,12 @@ export async function runCli(argv) {
|
|
|
140
167
|
return runList(ctx, cmdArgs, showHelp);
|
|
141
168
|
case "delete":
|
|
142
169
|
return runDelete(ctx, cmdArgs, showHelp);
|
|
170
|
+
case "usage":
|
|
171
|
+
return runUsage(ctx, cmdArgs, showHelp);
|
|
172
|
+
case "reconcile":
|
|
173
|
+
return runReconcile(ctx, cmdArgs, showHelp);
|
|
174
|
+
case "purge-expired":
|
|
175
|
+
return runPurgeExpired(ctx, cmdArgs, showHelp);
|
|
143
176
|
case "doctor":
|
|
144
177
|
return runDoctor(ctx, cmdArgs, showHelp);
|
|
145
178
|
}
|
package/dist/client.d.ts
CHANGED
|
@@ -43,13 +43,80 @@ export interface DeleteResult {
|
|
|
43
43
|
export interface HealthResult {
|
|
44
44
|
ok: boolean;
|
|
45
45
|
}
|
|
46
|
+
export interface UsageResult {
|
|
47
|
+
workspace: string;
|
|
48
|
+
bytes: number;
|
|
49
|
+
objects: number;
|
|
50
|
+
uploadsInPeriod: number;
|
|
51
|
+
periodStart: string;
|
|
52
|
+
updatedAt: string;
|
|
53
|
+
maxStorageBytes?: number;
|
|
54
|
+
storageRemainingBytes?: number;
|
|
55
|
+
maxUploadsPerPeriod?: number;
|
|
56
|
+
uploadsRemaining?: number;
|
|
57
|
+
}
|
|
58
|
+
export interface ReconcileResult {
|
|
59
|
+
workspace: string;
|
|
60
|
+
bytes: number;
|
|
61
|
+
objects: number;
|
|
62
|
+
previous: {
|
|
63
|
+
bytes: number;
|
|
64
|
+
objects: number;
|
|
65
|
+
};
|
|
66
|
+
changed: boolean;
|
|
67
|
+
usage: UsageResult;
|
|
68
|
+
}
|
|
69
|
+
export interface PurgeExpiredResult {
|
|
70
|
+
workspace: string;
|
|
71
|
+
retentionDays: number;
|
|
72
|
+
cutoff: string;
|
|
73
|
+
deleted: number;
|
|
74
|
+
freedBytes: number;
|
|
75
|
+
keys: string[];
|
|
76
|
+
keysTruncated: boolean;
|
|
77
|
+
reconcile: ReconcileResult;
|
|
78
|
+
}
|
|
79
|
+
export type PurgeExpiredResponse = PurgeExpiredResult | {
|
|
80
|
+
skipped: true;
|
|
81
|
+
reason: string;
|
|
82
|
+
};
|
|
83
|
+
export interface EnrollmentExchangeResult {
|
|
84
|
+
apiUrl?: string;
|
|
85
|
+
workspace: string;
|
|
86
|
+
token: string;
|
|
87
|
+
scopes?: Array<"files:read" | "files:write" | "files:delete">;
|
|
88
|
+
expiresAt?: string;
|
|
89
|
+
}
|
|
90
|
+
export interface EnrollmentCreateResult {
|
|
91
|
+
code: string;
|
|
92
|
+
expiresAt: string;
|
|
93
|
+
tokenExpiresAt: string;
|
|
94
|
+
}
|
|
95
|
+
export declare function exchangeEnrollment(apiUrl: string, code: string): Promise<EnrollmentExchangeResult>;
|
|
96
|
+
export declare function createEnrollment(apiUrl: string, adminToken: string, input: {
|
|
97
|
+
workspace?: string;
|
|
98
|
+
label?: string;
|
|
99
|
+
enrollmentSeconds?: number;
|
|
100
|
+
tokenExpiresInSeconds?: number;
|
|
101
|
+
scopes?: Array<"files:read" | "files:write" | "files:delete">;
|
|
102
|
+
}): Promise<EnrollmentCreateResult>;
|
|
46
103
|
export declare function createUploadsClient(config: UploadsClientConfig): {
|
|
47
104
|
put(body: Uint8Array, opts: PutOptions & {
|
|
48
105
|
filename: string;
|
|
49
106
|
}): Promise<PutResult>;
|
|
50
|
-
list(opts?: ListOptions)
|
|
107
|
+
list: (opts?: ListOptions) => Promise<ListResult>;
|
|
108
|
+
/** Follow cursors (optionally starting from one) and return every remaining item. */
|
|
109
|
+
listAll(opts?: Omit<ListOptions, "cursor"> & {
|
|
110
|
+
cursor?: string;
|
|
111
|
+
}): Promise<ListItem[]>;
|
|
51
112
|
delete(key: string): Promise<DeleteResult>;
|
|
52
113
|
head(key: string): Promise<HeadResult>;
|
|
53
114
|
health(): Promise<HealthResult>;
|
|
115
|
+
/** Workspace storage / upload counters (+ limits when configured). */
|
|
116
|
+
usage(): Promise<UsageResult>;
|
|
117
|
+
/** Rebuild ledger bytes/objects from storage (source of truth). */
|
|
118
|
+
reconcile(): Promise<ReconcileResult>;
|
|
119
|
+
/** Delete objects past retentionDays (if set), then reconcile. */
|
|
120
|
+
purgeExpired(): Promise<PurgeExpiredResponse>;
|
|
54
121
|
};
|
|
55
122
|
export type UploadsClient = ReturnType<typeof createUploadsClient>;
|
package/dist/client.js
CHANGED
|
@@ -1,13 +1,42 @@
|
|
|
1
1
|
import { inferContentType } from "./embed.js";
|
|
2
2
|
import { UploadsError } from "./errors.js";
|
|
3
3
|
import { buildScreenshotKey } from "./keys.js";
|
|
4
|
+
async function jsonRequest(url, init) {
|
|
5
|
+
let res;
|
|
6
|
+
try {
|
|
7
|
+
res = await fetch(url, init);
|
|
8
|
+
}
|
|
9
|
+
catch (err) {
|
|
10
|
+
throw new UploadsError(err instanceof Error ? err.message : "network request failed", "NETWORK");
|
|
11
|
+
}
|
|
12
|
+
if (!res.ok)
|
|
13
|
+
throw await parseErrorResponse(res);
|
|
14
|
+
return (await res.json());
|
|
15
|
+
}
|
|
16
|
+
export function exchangeEnrollment(apiUrl, code) {
|
|
17
|
+
return jsonRequest(`${apiUrl.replace(/\/$/, "")}/auth/enrollments/exchange`, {
|
|
18
|
+
method: "POST",
|
|
19
|
+
headers: { "Content-Type": "application/json" },
|
|
20
|
+
body: JSON.stringify({ code }),
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
export function createEnrollment(apiUrl, adminToken, input) {
|
|
24
|
+
return jsonRequest(`${apiUrl.replace(/\/$/, "")}/admin/enrollments`, {
|
|
25
|
+
method: "POST",
|
|
26
|
+
headers: { Authorization: `Bearer ${adminToken}`, "Content-Type": "application/json" },
|
|
27
|
+
body: JSON.stringify(input),
|
|
28
|
+
});
|
|
29
|
+
}
|
|
4
30
|
function encodeKeyPath(key) {
|
|
5
31
|
return key.split("/").map(encodeURIComponent).join("/");
|
|
6
32
|
}
|
|
7
33
|
function filesBase(config) {
|
|
8
34
|
return `${config.apiUrl}/v1/${encodeURIComponent(config.workspace)}/files`;
|
|
9
35
|
}
|
|
10
|
-
function
|
|
36
|
+
function usageBase(config) {
|
|
37
|
+
return `${config.apiUrl}/v1/${encodeURIComponent(config.workspace)}/usage`;
|
|
38
|
+
}
|
|
39
|
+
function mapApiError(status, error, code) {
|
|
11
40
|
const normalized = error.toLowerCase();
|
|
12
41
|
if (status === 401 || normalized === "unauthorized") {
|
|
13
42
|
return new UploadsError(error, "UNAUTHORIZED", status);
|
|
@@ -18,6 +47,13 @@ function mapApiError(status, error) {
|
|
|
18
47
|
if (status === 400 && normalized === "invalid key") {
|
|
19
48
|
return new UploadsError(error, "INVALID_KEY", status);
|
|
20
49
|
}
|
|
50
|
+
// Prefer stable body.code — bare 429 is also used for write rate limits.
|
|
51
|
+
if (status === 507 || code === "storage_quota_exceeded") {
|
|
52
|
+
return new UploadsError(error, "STORAGE_QUOTA", status);
|
|
53
|
+
}
|
|
54
|
+
if (code === "upload_budget_exceeded") {
|
|
55
|
+
return new UploadsError(error, "UPLOAD_BUDGET", status);
|
|
56
|
+
}
|
|
21
57
|
return new UploadsError(error, "API_ERROR", status);
|
|
22
58
|
}
|
|
23
59
|
async function parseErrorResponse(res) {
|
|
@@ -25,7 +61,10 @@ async function parseErrorResponse(res) {
|
|
|
25
61
|
const message = typeof body === "object" && body && "error" in body && typeof body.error === "string"
|
|
26
62
|
? body.error
|
|
27
63
|
: res.statusText || "request failed";
|
|
28
|
-
|
|
64
|
+
const code = typeof body === "object" && body && "code" in body && typeof body.code === "string"
|
|
65
|
+
? body.code
|
|
66
|
+
: undefined;
|
|
67
|
+
return mapApiError(res.status, message, code);
|
|
29
68
|
}
|
|
30
69
|
export function createUploadsClient(config) {
|
|
31
70
|
async function request(method, path, opts) {
|
|
@@ -52,6 +91,17 @@ export function createUploadsClient(config) {
|
|
|
52
91
|
return undefined;
|
|
53
92
|
return (await res.json());
|
|
54
93
|
}
|
|
94
|
+
async function list(opts = {}) {
|
|
95
|
+
const params = new URLSearchParams();
|
|
96
|
+
if (opts.prefix)
|
|
97
|
+
params.set("prefix", opts.prefix);
|
|
98
|
+
if (opts.limit != null)
|
|
99
|
+
params.set("limit", String(opts.limit));
|
|
100
|
+
if (opts.cursor)
|
|
101
|
+
params.set("cursor", opts.cursor);
|
|
102
|
+
const qs = params.toString();
|
|
103
|
+
return request("GET", `${filesBase(config)}${qs ? `?${qs}` : ""}`);
|
|
104
|
+
}
|
|
55
105
|
return {
|
|
56
106
|
async put(body, opts) {
|
|
57
107
|
const key = opts.key ??
|
|
@@ -73,16 +123,17 @@ export function createUploadsClient(config) {
|
|
|
73
123
|
}
|
|
74
124
|
return { ...result, url: result.url };
|
|
75
125
|
},
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
126
|
+
list,
|
|
127
|
+
/** Follow cursors (optionally starting from one) and return every remaining item. */
|
|
128
|
+
async listAll(opts = {}) {
|
|
129
|
+
const items = [];
|
|
130
|
+
let cursor = opts.cursor;
|
|
131
|
+
do {
|
|
132
|
+
const page = await list({ ...opts, cursor });
|
|
133
|
+
items.push(...page.items);
|
|
134
|
+
cursor = page.cursor ?? undefined;
|
|
135
|
+
} while (cursor);
|
|
136
|
+
return items;
|
|
86
137
|
},
|
|
87
138
|
async delete(key) {
|
|
88
139
|
return request("DELETE", `${filesBase(config)}/${encodeKeyPath(key)}`);
|
|
@@ -93,5 +144,17 @@ export function createUploadsClient(config) {
|
|
|
93
144
|
async health() {
|
|
94
145
|
return request("GET", `${config.apiUrl}/health`, { auth: false });
|
|
95
146
|
},
|
|
147
|
+
/** Workspace storage / upload counters (+ limits when configured). */
|
|
148
|
+
async usage() {
|
|
149
|
+
return request("GET", usageBase(config));
|
|
150
|
+
},
|
|
151
|
+
/** Rebuild ledger bytes/objects from storage (source of truth). */
|
|
152
|
+
async reconcile() {
|
|
153
|
+
return request("POST", `${usageBase(config)}/reconcile`);
|
|
154
|
+
},
|
|
155
|
+
/** Delete objects past retentionDays (if set), then reconcile. */
|
|
156
|
+
async purgeExpired() {
|
|
157
|
+
return request("POST", `${usageBase(config)}/purge-expired`);
|
|
158
|
+
},
|
|
96
159
|
};
|
|
97
160
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
type FileScope = "files:read" | "files:write" | "files:delete";
|
|
2
|
+
export declare function parseScopes(raw: string | undefined): FileScope[] | undefined;
|
|
3
|
+
export declare function runAdmin(args: string[], opts: {
|
|
4
|
+
json?: boolean;
|
|
5
|
+
apiUrl?: string;
|
|
6
|
+
}, help?: boolean): Promise<number>;
|
|
7
|
+
export {};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { createEnrollment } from "../client.js";
|
|
2
|
+
import { flagInt, flagString, parseCommandArgs, UsageError } from "../cli-args.js";
|
|
3
|
+
const HELP = `uploads admin enrollment create [options]
|
|
4
|
+
|
|
5
|
+
Admin-only: create a short-lived, one-time enrollment code.
|
|
6
|
+
|
|
7
|
+
Options:
|
|
8
|
+
--admin-token <token> Or ADMIN_TOKEN (UPLOADS_ADMIN_TOKEN is a legacy alias)
|
|
9
|
+
--workspace <name> Default: default
|
|
10
|
+
--label <label>
|
|
11
|
+
--expires-in <seconds> Default: server policy
|
|
12
|
+
--token-expires-in <seconds> Upload token lifetime (default: server policy)
|
|
13
|
+
--scopes <list> Comma-separated files:read,files:write,files:delete
|
|
14
|
+
--api-url <url> Default: https://api.uploads.sh
|
|
15
|
+
`;
|
|
16
|
+
const FILE_SCOPES = new Set(["files:read", "files:write", "files:delete"]);
|
|
17
|
+
export function parseScopes(raw) {
|
|
18
|
+
if (raw === undefined)
|
|
19
|
+
return undefined;
|
|
20
|
+
const scopes = raw
|
|
21
|
+
.split(",")
|
|
22
|
+
.map((scope) => scope.trim())
|
|
23
|
+
.filter(Boolean);
|
|
24
|
+
if (scopes.length === 0)
|
|
25
|
+
throw new UsageError("--scopes must contain at least one scope");
|
|
26
|
+
const invalid = scopes.find((scope) => !FILE_SCOPES.has(scope));
|
|
27
|
+
if (invalid)
|
|
28
|
+
throw new UsageError(`invalid scope: ${invalid} (expected files:read, files:write, or files:delete)`);
|
|
29
|
+
return [...new Set(scopes)];
|
|
30
|
+
}
|
|
31
|
+
export async function runAdmin(args, opts, help = false) {
|
|
32
|
+
const parsed = parseCommandArgs(args);
|
|
33
|
+
if (help || parsed.help) {
|
|
34
|
+
process.stderr.write(HELP);
|
|
35
|
+
return 0;
|
|
36
|
+
}
|
|
37
|
+
if (parsed.positionals[0] !== "enrollment" || parsed.positionals[1] !== "create")
|
|
38
|
+
throw new UsageError("expected: uploads admin enrollment create");
|
|
39
|
+
const adminToken = flagString(parsed.flags, "--admin-token") ??
|
|
40
|
+
process.env.ADMIN_TOKEN ??
|
|
41
|
+
process.env.UPLOADS_ADMIN_TOKEN;
|
|
42
|
+
if (!adminToken)
|
|
43
|
+
throw new UsageError("ADMIN_TOKEN is required for admin enrollment creation");
|
|
44
|
+
const apiUrl = flagString(parsed.flags, "--api-url") ?? opts.apiUrl ?? "https://api.uploads.sh";
|
|
45
|
+
const workspace = flagString(parsed.flags, "--workspace") ?? "default";
|
|
46
|
+
const label = flagString(parsed.flags, "--label");
|
|
47
|
+
const result = await createEnrollment(apiUrl, adminToken, {
|
|
48
|
+
workspace,
|
|
49
|
+
label,
|
|
50
|
+
enrollmentSeconds: flagInt(parsed.flags, "--expires-in", "--expires-in"),
|
|
51
|
+
tokenExpiresInSeconds: flagInt(parsed.flags, "--token-expires-in", "--token-expires-in"),
|
|
52
|
+
scopes: parseScopes(flagString(parsed.flags, "--scopes")),
|
|
53
|
+
});
|
|
54
|
+
if (opts.json)
|
|
55
|
+
process.stdout.write(JSON.stringify({ workspace, label: label ?? null, ...result }, null, 2) + "\n");
|
|
56
|
+
else
|
|
57
|
+
process.stdout.write(`Enrollment code (share once): ${result.code}\nworkspace: ${workspace}\nexpires: ${result.expiresAt}\n`);
|
|
58
|
+
return 0;
|
|
59
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type GlobalFlags } from "../cli-args.js";
|
|
2
|
+
import { type CommandRunner } from "../github-gh.js";
|
|
3
|
+
export declare const DEFAULT_MCP_URL = "https://agents.uploads.sh/mcp";
|
|
4
|
+
export declare function runInstall(args: string[], opts: {
|
|
5
|
+
globals: GlobalFlags;
|
|
6
|
+
json?: boolean;
|
|
7
|
+
runner?: CommandRunner;
|
|
8
|
+
}, help?: boolean): Promise<number>;
|