@buildinternet/uploads 0.7.0 → 0.8.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 +14 -3
- package/dist/cli-args.d.ts +17 -2
- package/dist/cli-args.js +50 -5
- package/dist/cli.d.ts +5 -0
- package/dist/cli.js +67 -24
- package/dist/client.d.ts +69 -0
- package/dist/client.js +99 -5
- package/dist/commands/install.js +110 -52
- package/dist/commands/invite.d.ts +10 -0
- package/dist/commands/invite.js +102 -0
- package/dist/commands/login.d.ts +15 -0
- package/dist/commands/login.js +17 -9
- package/dist/commands.d.ts +4 -0
- package/dist/commands.js +206 -16
- package/dist/config.js +6 -3
- package/dist/errors.d.ts +1 -1
- package/dist/github.d.ts +15 -0
- package/dist/github.js +31 -7
- package/dist/index.d.ts +4 -2
- package/dist/index.js +3 -1
- package/dist/mcp/args.d.ts +17 -0
- package/dist/mcp/args.js +42 -0
- package/dist/mcp/server.d.ts +1 -1
- package/dist/mcp/server.js +1 -1
- package/dist/mcp/tools.js +121 -21
- package/dist/metadata.d.ts +32 -0
- package/dist/metadata.js +102 -0
- package/dist/public-urls.d.ts +18 -0
- package/dist/public-urls.js +68 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -21,8 +21,14 @@ uploads put ./shot.png --no-optimize
|
|
|
21
21
|
uploads put ./mobile.png --frame phone
|
|
22
22
|
uploads put ./ui.png --frame browser --frame-url "https://app.example"
|
|
23
23
|
uploads put ./after.png --pr 123 --comment
|
|
24
|
+
uploads put ./capture-2026-…Z.png --pr 123 --name hero.png # clean leaf, stable path
|
|
25
|
+
uploads put ./shot.png --pr 123 --name hero.png --dry-run --format url # preview URL, no upload
|
|
24
26
|
uploads gallery create --title "Release screenshots"
|
|
25
27
|
uploads put ./after.png --gallery gal_example
|
|
28
|
+
uploads put ./shot.png --meta app=myapp --meta page=settings # queryable custom metadata
|
|
29
|
+
uploads meta get screenshots/myapp/42/shot.png
|
|
30
|
+
uploads meta set screenshots/myapp/42/shot.png page=onboarding --delete device
|
|
31
|
+
uploads find app=myapp page=settings # or: list --meta app=myapp
|
|
26
32
|
uploads doctor
|
|
27
33
|
```
|
|
28
34
|
|
|
@@ -30,8 +36,8 @@ Inside this monorepo only, `pnpm uploads …` builds the package first so you pi
|
|
|
30
36
|
up local source; product docs and PR “how to try it” examples should use the
|
|
31
37
|
global `uploads` form above.
|
|
32
38
|
|
|
33
|
-
Commands: `attach`, `put`, `gallery`, `comment`, `list`, `
|
|
34
|
-
`purge-expired`, `setup`, `install`, `config`, `doctor`, `health`, `mcp`.
|
|
39
|
+
Commands: `attach`, `put`, `gallery`, `comment`, `list`, `find`, `meta`, `delete`, `usage`,
|
|
40
|
+
`reconcile`, `purge-expired`, `setup`, `install`, `config`, `doctor`, `health`, `mcp`.
|
|
35
41
|
|
|
36
42
|
**Globals (before the command):** `--api-url`, `--token`, `--workspace` / `-w`,
|
|
37
43
|
`--env-file`, `--json`, `--quiet`, `--version` / `-V`, `-h` / `--help`.
|
|
@@ -41,6 +47,11 @@ newer npm release is available (at most once/day, `~/.cache/uploads/`). Silence
|
|
|
41
47
|
with `--quiet`, `--json`, `UPLOADS_NO_UPDATE=1`, or `NO_UPDATE_NOTIFIER=1`. Not used
|
|
42
48
|
for `uploads mcp`.
|
|
43
49
|
|
|
50
|
+
**Exit codes:** `0` ok, `2` usage/token/file, `3` auth/policy, `4` network, `1` other.
|
|
51
|
+
Failures go to stderr; under `--format json|url|markdown` they also go to stdout so
|
|
52
|
+
piped runs stay self-diagnosing. Prefer JSON `code` over message text. `put --dry-run`
|
|
53
|
+
previews the key + public URL without uploading.
|
|
54
|
+
|
|
44
55
|
`attach` is the agent-friendly default for GitHub media. It accepts one or more files,
|
|
45
56
|
infers the pull request for the current branch via `gh`, uploads stable URLs, and creates
|
|
46
57
|
or updates one marker-owned GitHub comment. It keeps loose `gh/...` attachments and linked public galleries in distinct sections, shows up to three available gallery images inline, and updates that same comment in place on every sync. Use `--pr`, `--issue`, and `--repo` to select
|
|
@@ -95,7 +106,7 @@ Config layers (first match wins): CLI flags → env vars → `--env-file` → `~
|
|
|
95
106
|
|
|
96
107
|
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`.
|
|
97
108
|
|
|
98
|
-
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: file operations plus `gallery_create`, `gallery_get`, `gallery_add`, `gallery_link`, and `gallery_find_by_reference`; all use the same bearer-token workspace scopes and gallery URLs come from the API — see `apps/mcp` in the repo. `uploads install` registers
|
|
109
|
+
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: file operations plus `gallery_create`, `gallery_get`, `gallery_add`, `gallery_link`, and `gallery_find_by_reference`; all use the same bearer-token workspace scopes and gallery URLs come from the API — see `apps/mcp` in the repo. `uploads install` registers the skill + hosted MCP (short progress; `--verbose` for underlying output). 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.
|
|
99
110
|
|
|
100
111
|
## Programmatic use
|
|
101
112
|
|
package/dist/cli-args.d.ts
CHANGED
|
@@ -26,16 +26,31 @@ export declare class UsageError extends Error {
|
|
|
26
26
|
}
|
|
27
27
|
export interface CommandFlags {
|
|
28
28
|
positionals: string[];
|
|
29
|
-
|
|
29
|
+
/** Repeated string flags (e.g. `--meta k=v --meta k2=v2`) collapse into a string[]. */
|
|
30
|
+
flags: Map<string, string | boolean | string[]>;
|
|
30
31
|
help: boolean;
|
|
31
32
|
}
|
|
32
33
|
/**
|
|
33
34
|
* Parse command-specific args. Supports `--flag value`, `--flag=value`, and
|
|
34
|
-
* boolean `--flag` flags.
|
|
35
|
+
* boolean `--flag` flags. A flag repeated multiple times with string values
|
|
36
|
+
* (e.g. `--meta app=x --meta page=y`) collapses into a `string[]` — read it
|
|
37
|
+
* with `flagValues`, not `flagString`.
|
|
35
38
|
*/
|
|
36
39
|
export declare function parseCommandArgs(args: string[]): CommandFlags;
|
|
40
|
+
/**
|
|
41
|
+
* Single string value for a flag. A repeated single-value flag keeps the
|
|
42
|
+
* pre-repeatable-flags behavior: the last occurrence wins (e.g.
|
|
43
|
+
* `--repo a --repo b` → `"b"`). Genuinely repeatable flags should use
|
|
44
|
+
* `flagValues` instead.
|
|
45
|
+
*/
|
|
37
46
|
export declare function flagString(flags: CommandFlags["flags"], name: string): string | undefined;
|
|
38
47
|
export declare function flagBool(flags: CommandFlags["flags"], name: string): boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Every string value passed for a repeatable flag (e.g. `--meta k=v`), in
|
|
50
|
+
* argument order. Empty when the flag is absent; a single occurrence yields
|
|
51
|
+
* a one-element array.
|
|
52
|
+
*/
|
|
53
|
+
export declare function flagValues(flags: CommandFlags["flags"], name: string): string[];
|
|
39
54
|
/** Command-level workspace override (`--workspace` / `-w`). */
|
|
40
55
|
export declare function commandWorkspace(flags: CommandFlags["flags"]): string | undefined;
|
|
41
56
|
export declare function flagInt(flags: CommandFlags["flags"], name: string, label: string): number | undefined;
|
package/dist/cli-args.js
CHANGED
|
@@ -69,9 +69,29 @@ export class UsageError extends Error {
|
|
|
69
69
|
this.name = "UsageError";
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
|
+
/** Records a flag occurrence, turning a repeated string flag into an array. */
|
|
73
|
+
function setFlag(flags, name, value) {
|
|
74
|
+
const existing = flags.get(name);
|
|
75
|
+
if (existing === undefined) {
|
|
76
|
+
flags.set(name, value);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (Array.isArray(existing)) {
|
|
80
|
+
if (typeof value === "string")
|
|
81
|
+
existing.push(value);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (typeof existing === "string" && typeof value === "string") {
|
|
85
|
+
flags.set(name, [existing, value]);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
flags.set(name, value);
|
|
89
|
+
}
|
|
72
90
|
/**
|
|
73
91
|
* Parse command-specific args. Supports `--flag value`, `--flag=value`, and
|
|
74
|
-
* boolean `--flag` flags.
|
|
92
|
+
* boolean `--flag` flags. A flag repeated multiple times with string values
|
|
93
|
+
* (e.g. `--meta app=x --meta page=y`) collapses into a `string[]` — read it
|
|
94
|
+
* with `flagValues`, not `flagString`.
|
|
75
95
|
*/
|
|
76
96
|
export function parseCommandArgs(args) {
|
|
77
97
|
const positionals = [];
|
|
@@ -88,18 +108,18 @@ export function parseCommandArgs(args) {
|
|
|
88
108
|
if (arg.startsWith("--")) {
|
|
89
109
|
const eq = arg.indexOf("=");
|
|
90
110
|
if (eq !== -1) {
|
|
91
|
-
flags
|
|
111
|
+
setFlag(flags, arg.slice(0, eq), arg.slice(eq + 1));
|
|
92
112
|
i++;
|
|
93
113
|
continue;
|
|
94
114
|
}
|
|
95
115
|
const name = arg;
|
|
96
116
|
const next = args[i + 1];
|
|
97
117
|
if (next && !next.startsWith("-")) {
|
|
98
|
-
flags
|
|
118
|
+
setFlag(flags, name, next);
|
|
99
119
|
i += 2;
|
|
100
120
|
continue;
|
|
101
121
|
}
|
|
102
|
-
flags
|
|
122
|
+
setFlag(flags, name, true);
|
|
103
123
|
i++;
|
|
104
124
|
continue;
|
|
105
125
|
}
|
|
@@ -108,13 +128,38 @@ export function parseCommandArgs(args) {
|
|
|
108
128
|
}
|
|
109
129
|
return { positionals, flags, help };
|
|
110
130
|
}
|
|
131
|
+
/**
|
|
132
|
+
* Single string value for a flag. A repeated single-value flag keeps the
|
|
133
|
+
* pre-repeatable-flags behavior: the last occurrence wins (e.g.
|
|
134
|
+
* `--repo a --repo b` → `"b"`). Genuinely repeatable flags should use
|
|
135
|
+
* `flagValues` instead.
|
|
136
|
+
*/
|
|
111
137
|
export function flagString(flags, name) {
|
|
112
138
|
const value = flags.get(name);
|
|
113
|
-
|
|
139
|
+
if (typeof value === "string")
|
|
140
|
+
return value;
|
|
141
|
+
if (Array.isArray(value) && value.length > 0)
|
|
142
|
+
return value[value.length - 1];
|
|
143
|
+
return undefined;
|
|
114
144
|
}
|
|
115
145
|
export function flagBool(flags, name) {
|
|
116
146
|
return flags.get(name) === true;
|
|
117
147
|
}
|
|
148
|
+
/**
|
|
149
|
+
* Every string value passed for a repeatable flag (e.g. `--meta k=v`), in
|
|
150
|
+
* argument order. Empty when the flag is absent; a single occurrence yields
|
|
151
|
+
* a one-element array.
|
|
152
|
+
*/
|
|
153
|
+
export function flagValues(flags, name) {
|
|
154
|
+
const value = flags.get(name);
|
|
155
|
+
if (value === undefined)
|
|
156
|
+
return [];
|
|
157
|
+
if (Array.isArray(value))
|
|
158
|
+
return value;
|
|
159
|
+
if (typeof value === "string")
|
|
160
|
+
return [value];
|
|
161
|
+
return [];
|
|
162
|
+
}
|
|
118
163
|
/** Command-level workspace override (`--workspace` / `-w`). */
|
|
119
164
|
export function commandWorkspace(flags) {
|
|
120
165
|
return flagString(flags, "--workspace") ?? flagString(flags, "-w");
|
package/dist/cli.d.ts
CHANGED
|
@@ -1 +1,6 @@
|
|
|
1
|
+
/** Effective stdout format, so failures surface where the caller reads output. */
|
|
2
|
+
type OutputFormat = "json" | "url" | "markdown" | "human";
|
|
3
|
+
/** Global `--json` wins; else put-style `--format`. Drives where failures print. */
|
|
4
|
+
export declare function outputFormat(argv: string[]): OutputFormat;
|
|
1
5
|
export declare function runCli(argv: string[]): Promise<number>;
|
|
6
|
+
export {};
|
package/dist/cli.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { createUploadsClient } from "./client.js";
|
|
2
2
|
import { resolveApiUrl, resolveConfig } from "./config.js";
|
|
3
3
|
import { UploadsError } from "./errors.js";
|
|
4
|
-
import { commandWorkspace, isHelpFlag, parseArgv, parseCommandArgs, UsageError, } from "./cli-args.js";
|
|
5
|
-
import { runPut, runAttach, runList, runDelete, runHealth, runDoctor, runComment, runUsage, runReconcile, runPurgeExpired, runGallery, } from "./commands.js";
|
|
4
|
+
import { commandWorkspace, flagString, isHelpFlag, parseArgv, parseCommandArgs, UsageError, } from "./cli-args.js";
|
|
5
|
+
import { runPut, runAttach, runList, runFind, runMeta, runDelete, runHealth, runDoctor, runComment, runUsage, runReconcile, runPurgeExpired, runGallery, } from "./commands.js";
|
|
6
6
|
import { runConfig } from "./commands/config.js";
|
|
7
7
|
import { runSetup } from "./commands/setup.js";
|
|
8
8
|
import { runLogin } from "./commands/login.js";
|
|
9
|
+
import { runInvite } from "./commands/invite.js";
|
|
9
10
|
import { runAdmin } from "./commands/admin-enrollment.js";
|
|
10
11
|
import { runMcp } from "./commands/mcp.js";
|
|
11
12
|
import { runInstall } from "./commands/install.js";
|
|
@@ -41,7 +42,9 @@ Commands:
|
|
|
41
42
|
put <file> Upload (+ URL + markdown for GitHub)
|
|
42
43
|
gallery Create and organize public media galleries
|
|
43
44
|
comment Create/update a PR/issue attachments comment (via gh)
|
|
44
|
-
list List objects
|
|
45
|
+
list List objects (--meta k=v filters by queryable metadata)
|
|
46
|
+
find k=v... List objects matching metadata (alias for list --meta)
|
|
47
|
+
meta Get/set an object's queryable metadata
|
|
45
48
|
delete <key> Delete object
|
|
46
49
|
usage Workspace storage / upload counters
|
|
47
50
|
reconcile Rebuild usage ledger from storage
|
|
@@ -49,7 +52,8 @@ Commands:
|
|
|
49
52
|
setup Inspect/configure advanced CLI settings
|
|
50
53
|
install Install the agent skill + register the remote MCP server
|
|
51
54
|
login Sign in via browser (or an enrollment code) and save credentials
|
|
52
|
-
admin
|
|
55
|
+
invite Invite a teammate to a workspace (workspace admin; device login)
|
|
56
|
+
admin Site-operator invitation management (ADMIN_TOKEN)
|
|
53
57
|
config Show path, init, or set shared config
|
|
54
58
|
doctor Health + auth + workspace checks
|
|
55
59
|
health API liveness (no auth)
|
|
@@ -99,6 +103,7 @@ function exitCode(err) {
|
|
|
99
103
|
switch (err.code) {
|
|
100
104
|
case "MISSING_TOKEN":
|
|
101
105
|
case "USAGE":
|
|
106
|
+
case "FILE_NOT_FOUND":
|
|
102
107
|
return 2;
|
|
103
108
|
case "UNAUTHORIZED":
|
|
104
109
|
case "NOT_FOUND":
|
|
@@ -114,32 +119,58 @@ function exitCode(err) {
|
|
|
114
119
|
}
|
|
115
120
|
return 1;
|
|
116
121
|
}
|
|
117
|
-
|
|
122
|
+
/** Global `--json` wins; else put-style `--format`. Drives where failures print. */
|
|
123
|
+
export function outputFormat(argv) {
|
|
124
|
+
const flags = parseCommandArgs(argv.slice(2)).flags;
|
|
125
|
+
if (flags.has("--json"))
|
|
126
|
+
return "json";
|
|
127
|
+
const value = flagString(flags, "--format");
|
|
128
|
+
if (value === "json" || value === "url" || value === "markdown")
|
|
129
|
+
return value;
|
|
130
|
+
return "human";
|
|
131
|
+
}
|
|
132
|
+
const QUOTA_HINT = "hint: run `uploads usage` then delete objects or raise limits (`pnpm workspace:limits`)\n";
|
|
133
|
+
const ERROR_HINTS = {
|
|
134
|
+
STORAGE_QUOTA: QUOTA_HINT,
|
|
135
|
+
UPLOAD_BUDGET: QUOTA_HINT,
|
|
136
|
+
KEY_POLICY: "hint: use a typed destination (`--destination screenshots|gh`) or an allowed prefix; operators set allowlists with `pnpm workspace:limits --allowed-prefixes`\n",
|
|
137
|
+
UNAUTHORIZED: "hint: token rejected — run `uploads login` to sign in again, or check UPLOADS_TOKEN / --token\n",
|
|
138
|
+
};
|
|
139
|
+
function errorOut(err, format) {
|
|
118
140
|
const payload = err instanceof UploadsError
|
|
119
141
|
? { error: err.message, code: err.code, status: err.status }
|
|
120
142
|
: err instanceof UsageError
|
|
121
143
|
? { error: err.message, code: "USAGE" }
|
|
122
144
|
: { error: err instanceof Error ? err.message : String(err) };
|
|
123
|
-
if (json)
|
|
145
|
+
if (format === "json") {
|
|
124
146
|
process.stdout.write(JSON.stringify(payload, null, 2) + "\n");
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
if (
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const msg = payload.error;
|
|
150
|
+
// No token: onboarding nudge (no "error:" prefix). Exit stays non-zero; JSON keeps MISSING_TOKEN.
|
|
151
|
+
if (err instanceof UploadsError && err.code === "MISSING_TOKEN") {
|
|
152
|
+
process.stderr.write(`${msg}\n`);
|
|
153
|
+
if (format === "url" || format === "markdown") {
|
|
154
|
+
process.stdout.write("not signed in — run uploads login\n");
|
|
155
|
+
}
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (msg.includes("\n"))
|
|
159
|
+
process.stderr.write(`${msg}\n`);
|
|
160
|
+
else
|
|
161
|
+
process.stderr.write(`error: ${msg}\n`);
|
|
162
|
+
if (err instanceof UploadsError) {
|
|
163
|
+
const hint = ERROR_HINTS[err.code];
|
|
164
|
+
if (hint)
|
|
165
|
+
process.stderr.write(hint);
|
|
166
|
+
else if (err.status === 413 || err.message.toLowerCase().includes("too large")) {
|
|
167
|
+
process.stderr.write("hint: file exceeds workspace size policy (images vs video may differ); compress or raise --max-upload-bytes / --max-video-bytes\n");
|
|
141
168
|
}
|
|
142
169
|
}
|
|
170
|
+
// Scripted formats often drop stderr — mirror a one-line reason on stdout.
|
|
171
|
+
if (format === "url" || format === "markdown") {
|
|
172
|
+
process.stdout.write(`error: ${msg.replace(/\s*\n\s*/g, " ")}\n`);
|
|
173
|
+
}
|
|
143
174
|
}
|
|
144
175
|
/** Point agents at layered --help instead of dumping the full root manual. */
|
|
145
176
|
function usageHint(argv) {
|
|
@@ -180,6 +211,9 @@ export async function runCli(argv) {
|
|
|
180
211
|
case "login":
|
|
181
212
|
code = await runLogin(cmdArgs, { json, apiUrl: resolveApiUrl(parsed.globals) }, showHelp);
|
|
182
213
|
break;
|
|
214
|
+
case "invite":
|
|
215
|
+
code = await runInvite(cmdArgs, { json, apiUrl: resolveApiUrl(parsed.globals) }, showHelp);
|
|
216
|
+
break;
|
|
183
217
|
case "admin":
|
|
184
218
|
code = await runAdmin(cmdArgs, { json, apiUrl: resolveApiUrl(parsed.globals) }, showHelp);
|
|
185
219
|
break;
|
|
@@ -193,6 +227,8 @@ export async function runCli(argv) {
|
|
|
193
227
|
case "put":
|
|
194
228
|
case "gallery":
|
|
195
229
|
case "list":
|
|
230
|
+
case "find":
|
|
231
|
+
case "meta":
|
|
196
232
|
case "delete":
|
|
197
233
|
case "usage":
|
|
198
234
|
case "reconcile":
|
|
@@ -216,6 +252,12 @@ export async function runCli(argv) {
|
|
|
216
252
|
case "list":
|
|
217
253
|
code = await runList(ctx, cmdArgs, showHelp);
|
|
218
254
|
break;
|
|
255
|
+
case "find":
|
|
256
|
+
code = await runFind(ctx, cmdArgs, showHelp);
|
|
257
|
+
break;
|
|
258
|
+
case "meta":
|
|
259
|
+
code = await runMeta(ctx, cmdArgs, showHelp);
|
|
260
|
+
break;
|
|
219
261
|
case "delete":
|
|
220
262
|
code = await runDelete(ctx, cmdArgs, showHelp);
|
|
221
263
|
break;
|
|
@@ -245,8 +287,9 @@ export async function runCli(argv) {
|
|
|
245
287
|
return code;
|
|
246
288
|
}
|
|
247
289
|
catch (err) {
|
|
248
|
-
|
|
249
|
-
|
|
290
|
+
const format = outputFormat(argv);
|
|
291
|
+
errorOut(err, format);
|
|
292
|
+
if (err instanceof UsageError && format !== "json")
|
|
250
293
|
usageHint(argv);
|
|
251
294
|
return exitCode(err);
|
|
252
295
|
}
|
package/dist/client.d.ts
CHANGED
|
@@ -17,16 +17,47 @@ export interface PutOptions {
|
|
|
17
17
|
deriveRepoFromGit?: boolean;
|
|
18
18
|
/** Stored as R2 custom metadata; echoed on put/head. */
|
|
19
19
|
provenance?: ProvenanceInput;
|
|
20
|
+
/**
|
|
21
|
+
* Queryable custom metadata (D1 `file_metadata`), sent alongside provenance
|
|
22
|
+
* as more `X-Uploads-Meta-<key>` headers — the server routes each key to R2
|
|
23
|
+
* (provenance) or D1 (everything else) by name. See `metadata.ts` for the
|
|
24
|
+
* client-side validation callers should run before this.
|
|
25
|
+
*/
|
|
26
|
+
metadata?: Record<string, string>;
|
|
27
|
+
/** Validate key + resolve public URL without writing. `size` is local bytes only. */
|
|
28
|
+
dryRun?: boolean;
|
|
20
29
|
}
|
|
21
30
|
export interface ListOptions {
|
|
22
31
|
prefix?: string;
|
|
23
32
|
limit?: number;
|
|
24
33
|
cursor?: string;
|
|
25
34
|
}
|
|
35
|
+
export interface FindFilesOptions {
|
|
36
|
+
prefix?: string;
|
|
37
|
+
limit?: number;
|
|
38
|
+
}
|
|
39
|
+
export interface FindFilesItem {
|
|
40
|
+
key: string;
|
|
41
|
+
url: string | null;
|
|
42
|
+
metadata: Record<string, string>;
|
|
43
|
+
}
|
|
44
|
+
export interface FindFilesResult {
|
|
45
|
+
items: FindFilesItem[];
|
|
46
|
+
cursor: string | null;
|
|
47
|
+
}
|
|
48
|
+
export interface GetMetadataResult {
|
|
49
|
+
metadata: Record<string, string>;
|
|
50
|
+
}
|
|
51
|
+
export interface PatchMetadataOptions {
|
|
52
|
+
set?: Record<string, string>;
|
|
53
|
+
delete?: string[];
|
|
54
|
+
}
|
|
26
55
|
export interface PutResult {
|
|
27
56
|
workspace: string;
|
|
28
57
|
key: string;
|
|
29
58
|
url: string;
|
|
59
|
+
/** Same object on the embed host when dual-host applies; prefer for GitHub markdown. */
|
|
60
|
+
embedUrl: string | null;
|
|
30
61
|
size: number;
|
|
31
62
|
contentType: string;
|
|
32
63
|
metadata?: Record<string, string>;
|
|
@@ -34,6 +65,7 @@ export interface PutResult {
|
|
|
34
65
|
export interface ListItem {
|
|
35
66
|
key: string;
|
|
36
67
|
url: string | null;
|
|
68
|
+
embedUrl?: string | null;
|
|
37
69
|
size?: number;
|
|
38
70
|
uploaded?: string;
|
|
39
71
|
}
|
|
@@ -44,6 +76,7 @@ export interface ListResult {
|
|
|
44
76
|
export interface HeadResult {
|
|
45
77
|
key: string;
|
|
46
78
|
url: string | null;
|
|
79
|
+
embedUrl?: string | null;
|
|
47
80
|
size: number;
|
|
48
81
|
contentType: string;
|
|
49
82
|
uploaded?: string;
|
|
@@ -63,6 +96,8 @@ export interface GalleryItem {
|
|
|
63
96
|
createdAt: string;
|
|
64
97
|
status: "available" | "missing";
|
|
65
98
|
url: string | null;
|
|
99
|
+
/** Dual-host embed URL when available. */
|
|
100
|
+
embedUrl?: string | null;
|
|
66
101
|
/** Standalone web page for this item (gallery URL + item id). Absent on older API deployments. */
|
|
67
102
|
pageUrl?: string;
|
|
68
103
|
contentType: string | null;
|
|
@@ -193,6 +228,13 @@ export declare function createEnrollment(apiUrl: string, adminToken: string, inp
|
|
|
193
228
|
}): Promise<EnrollmentCreateResult>;
|
|
194
229
|
/** Static OAuth client id allowlisted by the auth worker's `validateClient`. */
|
|
195
230
|
export declare const DEVICE_CLIENT_ID = "uploads-cli";
|
|
231
|
+
/**
|
|
232
|
+
* User-Agent for device-flow requests. Stored on the Better Auth session row
|
|
233
|
+
* when `/device/token` creates the session, so the web account UI can tell a
|
|
234
|
+
* completed `uploads login` apart from a browser tab. Keep the
|
|
235
|
+
* `@buildinternet/uploads` prefix in sync with apps/web `CLI_USER_AGENT_RE`.
|
|
236
|
+
*/
|
|
237
|
+
export declare function cliUserAgent(purpose?: string): string;
|
|
196
238
|
export interface DeviceCodeResponse {
|
|
197
239
|
device_code: string;
|
|
198
240
|
user_code: string;
|
|
@@ -247,6 +289,24 @@ export interface MintTokenResult {
|
|
|
247
289
|
label: string | null;
|
|
248
290
|
expiresAt: string | null;
|
|
249
291
|
}
|
|
292
|
+
/**
|
|
293
|
+
* POST /me/workspaces/:name/invites — org invitation for a workspace.
|
|
294
|
+
* Requires a Better Auth session bearer (device flow), not a workspace token.
|
|
295
|
+
* Caller must be org admin|owner. `acceptUrl` is always returned so
|
|
296
|
+
* self-hosted deploys without email can still share the link.
|
|
297
|
+
*/
|
|
298
|
+
export declare function createWorkspaceInvite(apiUrl: string, accessToken: string, workspace: string, input: {
|
|
299
|
+
email: string;
|
|
300
|
+
role?: "member" | "admin";
|
|
301
|
+
}): Promise<{
|
|
302
|
+
invitation: {
|
|
303
|
+
id: string;
|
|
304
|
+
email: string;
|
|
305
|
+
role: string;
|
|
306
|
+
status: string;
|
|
307
|
+
};
|
|
308
|
+
acceptUrl?: string;
|
|
309
|
+
}>;
|
|
250
310
|
/**
|
|
251
311
|
* POST /v1/tokens — mint a `up_<workspace>_…` workspace token from a device-flow
|
|
252
312
|
* session (presented as a bearer). v1 sends exactly one grant.
|
|
@@ -267,6 +327,15 @@ export declare function createUploadsClient(config: UploadsClientConfig): {
|
|
|
267
327
|
cursor?: string;
|
|
268
328
|
}): Promise<ListItem[]>;
|
|
269
329
|
delete(key: string): Promise<DeleteResult>;
|
|
330
|
+
/** `GET /v1/:workspace/files/:key?metadata=1` — the object's queryable metadata. */
|
|
331
|
+
getMetadata(key: string): Promise<GetMetadataResult>;
|
|
332
|
+
/** `PATCH /v1/:workspace/files/:key` — merge `set`/`delete`; returns the merged map. */
|
|
333
|
+
patchMetadata(key: string, opts: PatchMetadataOptions): Promise<GetMetadataResult>;
|
|
334
|
+
/**
|
|
335
|
+
* `GET /v1/:workspace/files?meta.<k>=<v>&…` — ANDed equality filter over
|
|
336
|
+
* queryable metadata. `filters` must be pre-validated (see `metadata.ts`).
|
|
337
|
+
*/
|
|
338
|
+
findFiles(filters: Record<string, string>, opts?: FindFilesOptions): Promise<FindFilesResult>;
|
|
270
339
|
head(key: string): Promise<HeadResult>;
|
|
271
340
|
createGallery(opts: CreateGalleryOptions): Promise<Gallery>;
|
|
272
341
|
getGallery(id: string): Promise<Gallery>;
|
package/dist/client.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { inferContentType } from "./embed.js";
|
|
2
2
|
import { UploadsError } from "./errors.js";
|
|
3
3
|
import { buildScreenshotKey } from "./keys.js";
|
|
4
|
+
import { packageVersion } from "./package-version.js";
|
|
5
|
+
import { resolveEmbedUrl } from "./public-urls.js";
|
|
4
6
|
async function jsonRequest(url, init) {
|
|
5
7
|
let res;
|
|
6
8
|
try {
|
|
@@ -36,11 +38,23 @@ export function createEnrollment(apiUrl, adminToken, input) {
|
|
|
36
38
|
// below are what the worker expects.
|
|
37
39
|
/** Static OAuth client id allowlisted by the auth worker's `validateClient`. */
|
|
38
40
|
export const DEVICE_CLIENT_ID = "uploads-cli";
|
|
41
|
+
/**
|
|
42
|
+
* User-Agent for device-flow requests. Stored on the Better Auth session row
|
|
43
|
+
* when `/device/token` creates the session, so the web account UI can tell a
|
|
44
|
+
* completed `uploads login` apart from a browser tab. Keep the
|
|
45
|
+
* `@buildinternet/uploads` prefix in sync with apps/web `CLI_USER_AGENT_RE`.
|
|
46
|
+
*/
|
|
47
|
+
export function cliUserAgent(purpose = "device-login") {
|
|
48
|
+
return `@buildinternet/uploads/${packageVersion()} (${purpose})`;
|
|
49
|
+
}
|
|
39
50
|
/** POST /api/auth/device/code — start a device flow. Throws on a non-2xx. */
|
|
40
51
|
export function requestDeviceCode(authUrl, clientId = DEVICE_CLIENT_ID) {
|
|
41
52
|
return jsonRequest(`${authUrl.replace(/\/$/, "")}/api/auth/device/code`, {
|
|
42
53
|
method: "POST",
|
|
43
|
-
headers: {
|
|
54
|
+
headers: {
|
|
55
|
+
"Content-Type": "application/json",
|
|
56
|
+
"User-Agent": cliUserAgent("device-code"),
|
|
57
|
+
},
|
|
44
58
|
body: JSON.stringify({ client_id: clientId }),
|
|
45
59
|
});
|
|
46
60
|
}
|
|
@@ -49,7 +63,12 @@ export async function requestDeviceToken(authUrl, input) {
|
|
|
49
63
|
try {
|
|
50
64
|
res = await fetch(`${authUrl.replace(/\/$/, "")}/api/auth/device/token`, {
|
|
51
65
|
method: "POST",
|
|
52
|
-
headers: {
|
|
66
|
+
headers: {
|
|
67
|
+
"Content-Type": "application/json",
|
|
68
|
+
// Session user_agent is taken from this request when the token is
|
|
69
|
+
// exchanged — identify as the CLI so /account can surface it.
|
|
70
|
+
"User-Agent": cliUserAgent("device-token"),
|
|
71
|
+
},
|
|
53
72
|
body: JSON.stringify({
|
|
54
73
|
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
55
74
|
device_code: input.deviceCode,
|
|
@@ -93,6 +112,22 @@ export function listMintWorkspaces(apiUrl, accessToken) {
|
|
|
93
112
|
headers: { Authorization: `Bearer ${accessToken}` },
|
|
94
113
|
});
|
|
95
114
|
}
|
|
115
|
+
/**
|
|
116
|
+
* POST /me/workspaces/:name/invites — org invitation for a workspace.
|
|
117
|
+
* Requires a Better Auth session bearer (device flow), not a workspace token.
|
|
118
|
+
* Caller must be org admin|owner. `acceptUrl` is always returned so
|
|
119
|
+
* self-hosted deploys without email can still share the link.
|
|
120
|
+
*/
|
|
121
|
+
export function createWorkspaceInvite(apiUrl, accessToken, workspace, input) {
|
|
122
|
+
return jsonRequest(`${apiUrl.replace(/\/$/, "")}/me/workspaces/${encodeURIComponent(workspace)}/invites`, {
|
|
123
|
+
method: "POST",
|
|
124
|
+
headers: {
|
|
125
|
+
Authorization: `Bearer ${accessToken}`,
|
|
126
|
+
"Content-Type": "application/json",
|
|
127
|
+
},
|
|
128
|
+
body: JSON.stringify({ email: input.email, role: input.role ?? "member" }),
|
|
129
|
+
});
|
|
130
|
+
}
|
|
96
131
|
/**
|
|
97
132
|
* POST /v1/tokens — mint a `up_<workspace>_…` workspace token from a device-flow
|
|
98
133
|
* session (presented as a bearer). v1 sends exactly one grant.
|
|
@@ -206,7 +241,14 @@ export function createUploadsClient(config) {
|
|
|
206
241
|
if (opts.cursor)
|
|
207
242
|
params.set("cursor", opts.cursor);
|
|
208
243
|
const qs = params.toString();
|
|
209
|
-
|
|
244
|
+
const page = await request("GET", `${filesBase(config)}${qs ? `?${qs}` : ""}`);
|
|
245
|
+
return {
|
|
246
|
+
...page,
|
|
247
|
+
items: page.items.map((item) => ({
|
|
248
|
+
...item,
|
|
249
|
+
embedUrl: resolveEmbedUrl(item.url, item.embedUrl),
|
|
250
|
+
})),
|
|
251
|
+
};
|
|
210
252
|
}
|
|
211
253
|
async function getGallery(id) {
|
|
212
254
|
return request("GET", `${galleriesBase(config)}/${encodeURIComponent(id)}`);
|
|
@@ -223,6 +265,20 @@ export function createUploadsClient(config) {
|
|
|
223
265
|
deriveRepoFromGit: opts.deriveRepoFromGit,
|
|
224
266
|
}));
|
|
225
267
|
const contentType = opts.contentType ?? inferContentType(opts.filename);
|
|
268
|
+
if (opts.dryRun) {
|
|
269
|
+
const preview = await request("PUT", `${filesBase(config)}/${encodeKeyPath(key)}?dryRun=1`);
|
|
270
|
+
if (preview.url == null) {
|
|
271
|
+
throw new UploadsError("workspace has no publicBaseUrl (cannot resolve a public URL)", "NO_PUBLIC_URL");
|
|
272
|
+
}
|
|
273
|
+
return {
|
|
274
|
+
workspace: preview.workspace,
|
|
275
|
+
key: preview.key,
|
|
276
|
+
url: preview.url,
|
|
277
|
+
embedUrl: resolveEmbedUrl(preview.url, preview.embedUrl),
|
|
278
|
+
size: body.byteLength,
|
|
279
|
+
contentType,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
226
282
|
const headers = { "Content-Type": contentType };
|
|
227
283
|
if (opts.provenance) {
|
|
228
284
|
for (const [k, v] of Object.entries(opts.provenance)) {
|
|
@@ -230,6 +286,14 @@ export function createUploadsClient(config) {
|
|
|
230
286
|
headers[`X-Uploads-Meta-${k}`] = v;
|
|
231
287
|
}
|
|
232
288
|
}
|
|
289
|
+
// Same header prefix as provenance above; the server splits allowlisted
|
|
290
|
+
// provenance keys (R2) from everything else (D1 file_metadata) by name.
|
|
291
|
+
if (opts.metadata) {
|
|
292
|
+
for (const [k, v] of Object.entries(opts.metadata)) {
|
|
293
|
+
if (v !== undefined && v !== "")
|
|
294
|
+
headers[`X-Uploads-Meta-${k}`] = v;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
233
297
|
const result = await request("PUT", `${filesBase(config)}/${encodeKeyPath(key)}`, {
|
|
234
298
|
body,
|
|
235
299
|
headers,
|
|
@@ -237,7 +301,11 @@ export function createUploadsClient(config) {
|
|
|
237
301
|
if (result.url == null) {
|
|
238
302
|
throw new UploadsError("upload succeeded but workspace has no publicBaseUrl", "NO_PUBLIC_URL", 201);
|
|
239
303
|
}
|
|
240
|
-
return {
|
|
304
|
+
return {
|
|
305
|
+
...result,
|
|
306
|
+
url: result.url,
|
|
307
|
+
embedUrl: resolveEmbedUrl(result.url, result.embedUrl),
|
|
308
|
+
};
|
|
241
309
|
},
|
|
242
310
|
list,
|
|
243
311
|
/** Follow cursors (optionally starting from one) and return every remaining item. */
|
|
@@ -254,8 +322,34 @@ export function createUploadsClient(config) {
|
|
|
254
322
|
async delete(key) {
|
|
255
323
|
return request("DELETE", `${filesBase(config)}/${encodeKeyPath(key)}`);
|
|
256
324
|
},
|
|
325
|
+
/** `GET /v1/:workspace/files/:key?metadata=1` — the object's queryable metadata. */
|
|
326
|
+
async getMetadata(key) {
|
|
327
|
+
return request("GET", `${filesBase(config)}/${encodeKeyPath(key)}?metadata=1`);
|
|
328
|
+
},
|
|
329
|
+
/** `PATCH /v1/:workspace/files/:key` — merge `set`/`delete`; returns the merged map. */
|
|
330
|
+
async patchMetadata(key, opts) {
|
|
331
|
+
return request("PATCH", `${filesBase(config)}/${encodeKeyPath(key)}`, {
|
|
332
|
+
body: new TextEncoder().encode(JSON.stringify(opts)),
|
|
333
|
+
headers: { "Content-Type": "application/json" },
|
|
334
|
+
});
|
|
335
|
+
},
|
|
336
|
+
/**
|
|
337
|
+
* `GET /v1/:workspace/files?meta.<k>=<v>&…` — ANDed equality filter over
|
|
338
|
+
* queryable metadata. `filters` must be pre-validated (see `metadata.ts`).
|
|
339
|
+
*/
|
|
340
|
+
async findFiles(filters, opts = {}) {
|
|
341
|
+
const params = new URLSearchParams();
|
|
342
|
+
for (const [k, v] of Object.entries(filters))
|
|
343
|
+
params.append(`meta.${k}`, v);
|
|
344
|
+
if (opts.prefix)
|
|
345
|
+
params.set("prefix", opts.prefix);
|
|
346
|
+
if (opts.limit != null)
|
|
347
|
+
params.set("limit", String(opts.limit));
|
|
348
|
+
return request("GET", `${filesBase(config)}?${params.toString()}`);
|
|
349
|
+
},
|
|
257
350
|
async head(key) {
|
|
258
|
-
|
|
351
|
+
const result = await request("GET", `${filesBase(config)}/${encodeKeyPath(key)}`);
|
|
352
|
+
return { ...result, embedUrl: resolveEmbedUrl(result.url, result.embedUrl) };
|
|
259
353
|
},
|
|
260
354
|
async createGallery(opts) {
|
|
261
355
|
return request("POST", galleriesBase(config), {
|