@buildinternet/uploads 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -13,6 +13,7 @@ npx @buildinternet/uploads --help
13
13
 
14
14
  ```bash
15
15
  uploads setup
16
+ uploads --version
16
17
  uploads attach ./before.png ./after.png
17
18
  uploads put ./shot.png
18
19
  uploads put ./shot.png --destination screenshots
@@ -20,6 +21,8 @@ uploads put ./shot.png --no-optimize
20
21
  uploads put ./mobile.png --frame phone
21
22
  uploads put ./ui.png --frame browser --frame-url "https://app.example"
22
23
  uploads put ./after.png --pr 123 --comment
24
+ uploads gallery create --title "Release screenshots"
25
+ uploads put ./after.png --gallery gal_example
23
26
  uploads doctor
24
27
  ```
25
28
 
@@ -27,12 +30,20 @@ Inside this monorepo only, `pnpm uploads …` builds the package first so you pi
27
30
  up local source; product docs and PR “how to try it” examples should use the
28
31
  global `uploads` form above.
29
32
 
30
- Commands: `attach`, `put`, `comment`, `list`, `delete`, `usage`, `reconcile`,
33
+ Commands: `attach`, `put`, `gallery`, `comment`, `list`, `delete`, `usage`, `reconcile`,
31
34
  `purge-expired`, `setup`, `install`, `config`, `doctor`, `health`, `mcp`.
32
35
 
36
+ **Globals (before the command):** `--api-url`, `--token`, `--workspace` / `-w`,
37
+ `--env-file`, `--json`, `--quiet`, `--version` / `-V`, `-h` / `--help`.
38
+
39
+ **Update hints:** after a successful run the CLI may print one stderr line when a
40
+ newer npm release is available (at most once/day, `~/.cache/uploads/`). Silence
41
+ with `--quiet`, `--json`, `UPLOADS_NO_UPDATE=1`, or `NO_UPDATE_NOTIFIER=1`. Not used
42
+ for `uploads mcp`.
43
+
33
44
  `attach` is the agent-friendly default for GitHub media. It accepts one or more files,
34
45
  infers the pull request for the current branch via `gh`, uploads stable URLs, and creates
35
- or updates one managed attachments comment. Use `--pr`, `--issue`, and `--repo` to select
46
+ 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
36
47
  the target explicitly, or `--no-comment` to upload without changing GitHub comments.
37
48
 
38
49
  **Keys / destinations:** default put uses the `screenshots` layout. Typed destinations
@@ -50,11 +61,33 @@ stripped**. Pass `--keep-exif` / `UPLOADS_KEEP_EXIF=1` to preserve image metadat
50
61
  community art from [device-frames-media](https://github.com/jonnyjackson26/device-frames-media)
51
62
  into `~/.cache/uploads/frames` (not bundled).
52
63
 
64
+ ## Public galleries
65
+
66
+ Create an ordered gallery, then add existing uploads by key. The API returns the canonical
67
+ public URL; the CLI never constructs it. **Anyone who knows that URL can view the gallery and
68
+ its media**—GitHub or repository visibility does not restrict it. Deleting a gallery removes
69
+ only the gallery record, not its uploaded objects or their retention policy. A workspace can hold up to 100 active galleries, each with up to 100 items and 20 external references.
70
+
71
+ ```bash
72
+ uploads gallery create --title "Release screenshots"
73
+ uploads gallery add gal_example screenshots/myapp/42/after.webp --alt "Updated dashboard"
74
+ uploads put ./before.png --gallery gal_example
75
+ uploads gallery show gal_example
76
+ uploads gallery link gal_example --github buildinternet/uploads#58
77
+ uploads gallery list --github https://github.com/buildinternet/uploads/pull/58
78
+ ```
79
+
80
+ When adding several keys, `uploads gallery add` processes them sequentially and reports any
81
+ individual failures in `--json` output. Gallery item updates use the API's current version to
82
+ avoid overwriting concurrent changes.
83
+
84
+ Link a gallery to a GitHub issue or pull request with `gallery link --github`. Run `uploads comment --pr <number>` (or use `put --comment`) to refresh that target’s one managed comment with every linked gallery and loose attachment. Coordinates and strict `https://github.com/<owner>/<repo>/issues|pull/<number>` URLs are accepted; `gallery list --github` performs the authenticated reverse lookup. Links never change gallery identity, and GitHub repository visibility does not make the public gallery private.
85
+
53
86
  Config layers (first match wins): CLI flags → env vars → `--env-file` → `~/.config/buildinternet/config`. See `config.example` for keys.
54
87
 
55
88
  ## MCP server
56
89
 
57
- `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).
90
+ `uploads mcp` serves the Model Context Protocol over stdio (newline-delimited JSON-RPC, no extra dependencies). Tools include file operations plus public gallery workflows: `gallery_create`, `gallery_get`, `gallery_add`, `gallery_link`, and `gallery_find_by_reference`. Gallery tools return API-provided canonical URLs and never need GitHub credentials. The remaining stdio tools are `put`, `attach`, `list`, `delete`, `usage`, `reconcile`, `purge_expired`, `comment`, `health`, and `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).
58
91
 
59
92
  ```json
60
93
  { "command": "uploads", "args": ["--env-file", "/path/to/.env", "mcp"] }
@@ -62,7 +95,7 @@ Config layers (first match wins): CLI flags → env vars → `--env-file` → `~
62
95
 
63
96
  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`.
64
97
 
65
- 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.
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 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.
66
99
 
67
100
  ## Programmatic use
68
101
 
@@ -76,14 +109,16 @@ Agent/MCP helpers: `@buildinternet/uploads/agent` (`createUploadsWorkerFileTools
76
109
 
77
110
  ```
78
111
  src/
79
- cli.ts Entry + help
80
- commands.ts put, list, delete, comment,
81
- commands/mcp.ts `mcp` command entry
82
- mcp/ Stdio MCP server (server.ts, tools.ts)
83
- client.ts HTTP client for the API
84
- github.ts PR/issue key paths + attachment comments
85
- embed.ts Markdown image output
86
- bin/uploads.js Bin shim
112
+ cli.ts Entry + help
113
+ package-version.ts Shared version for --version / doctor / headers
114
+ update-check.ts Optional npm update hint (stderr)
115
+ commands.ts put, list, delete, comment,
116
+ commands/mcp.ts `mcp` command entry
117
+ mcp/ Stdio MCP server (server.ts, tools.ts)
118
+ client.ts HTTP client for the API
119
+ github.ts PR/issue key paths + attachment comments
120
+ embed.ts Markdown image output
121
+ bin/uploads.js Bin shim
87
122
  ```
88
123
 
89
124
  ## Commands
@@ -5,6 +5,8 @@ export interface GlobalFlags {
5
5
  envFile?: string;
6
6
  json?: boolean;
7
7
  quiet?: boolean;
8
+ /** `--version` / `-V` — print package version and exit. */
9
+ version?: boolean;
8
10
  }
9
11
  export interface ParsedArgv {
10
12
  globals: GlobalFlags;
package/dist/cli-args.js CHANGED
@@ -28,6 +28,11 @@ export function parseArgv(argv) {
28
28
  i++;
29
29
  continue;
30
30
  }
31
+ if (arg === "--version" || arg === "-V") {
32
+ globals.version = true;
33
+ i++;
34
+ continue;
35
+ }
31
36
  if (VALUE_GLOBALS.has(arg)) {
32
37
  const value = args[i + 1];
33
38
  if (!value || value.startsWith("-")) {
package/dist/cli.js CHANGED
@@ -2,13 +2,15 @@ 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, runUsage, runReconcile, runPurgeExpired, } from "./commands.js";
5
+ import { runPut, runAttach, runList, 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
9
  import { runAdmin } from "./commands/admin-enrollment.js";
10
10
  import { runMcp } from "./commands/mcp.js";
11
11
  import { runInstall } from "./commands/install.js";
12
+ import { packageVersion } from "./package-version.js";
13
+ import { maybeHintUpdate } from "./update-check.js";
12
14
  const ROOT_HELP = `uploads — CLI for uploads.sh (GitHub image embeds)
13
15
 
14
16
  Usage:
@@ -31,11 +33,13 @@ Other globals (before command):
31
33
  --token <token> or UPLOADS_TOKEN
32
34
  --env-file <path>
33
35
  --json JSON on stdout
34
- --quiet
36
+ --quiet Suppress stderr progress and update hints
37
+ --version, -V Print package version and exit
35
38
 
36
39
  Commands:
37
40
  attach <file...> Attach media to the current PR (stable URLs + managed comment)
38
41
  put <file> Upload (+ URL + markdown for GitHub)
42
+ gallery Create and organize public media galleries
39
43
  comment Create/update a PR/issue attachments comment (via gh)
40
44
  list List objects
41
45
  delete <key> Delete object
@@ -44,7 +48,7 @@ Commands:
44
48
  purge-expired Delete objects past retentionDays
45
49
  setup Inspect/configure advanced CLI settings
46
50
  install Install the agent skill + register the remote MCP server
47
- login Exchange an enrollment code and configure credentials
51
+ login Sign in via browser (or an enrollment code) and save credentials
48
52
  admin Admin invitation management
49
53
  config Show path, init, or set shared config
50
54
  doctor Health + auth + workspace checks
@@ -55,12 +59,16 @@ Put/list defaults (config file or env):
55
59
  UPLOADS_DEFAULT_PREFIX, UPLOADS_DEFAULT_REPO, UPLOADS_DEFAULT_REF
56
60
  UPLOADS_DEFAULT_WIDTH, UPLOADS_NO_GIT
57
61
 
62
+ Update hints (stderr, once/day): silence with --quiet / UPLOADS_NO_UPDATE=1 / NO_UPDATE_NOTIFIER=1
63
+
58
64
  Examples:
59
65
  uploads setup
60
66
  uploads setup --token up_default_… --repo myorg/myapp
61
67
  uploads attach ./before.png ./after.png
62
68
  uploads put ./shot.png --ref 42
69
+ uploads gallery create --title "Release screenshots"
63
70
  uploads doctor
71
+ uploads --version
64
72
 
65
73
  Agent/MCP: \`uploads install\` sets up the agent skill and the hosted MCP server
66
74
  (https://agents.uploads.sh/mcp, workspace inferred from the token). Run
@@ -133,33 +141,57 @@ function errorOut(err, json) {
133
141
  }
134
142
  }
135
143
  }
144
+ /** Point agents at layered --help instead of dumping the full root manual. */
145
+ function usageHint(argv) {
146
+ try {
147
+ const cmd = parseArgv(argv).command;
148
+ process.stderr.write(cmd ? `hint: uploads ${cmd} --help\n` : "hint: uploads --help\n");
149
+ }
150
+ catch {
151
+ process.stderr.write("hint: uploads --help\n");
152
+ }
153
+ }
136
154
  export async function runCli(argv) {
137
155
  try {
138
156
  const parsed = parseArgv(argv);
139
157
  const json = parsed.globals.json ?? false;
158
+ const quiet = parsed.globals.quiet ?? false;
159
+ if (parsed.globals.version) {
160
+ process.stdout.write(`${packageVersion()}\n`);
161
+ return 0;
162
+ }
140
163
  if (!parsed.command) {
141
164
  process.stderr.write(ROOT_HELP);
142
165
  return parsed.help ? 0 : 2;
143
166
  }
144
167
  const cmdArgs = parsed.rest.slice(1);
145
168
  const showHelp = parsed.help || cmdArgs.some(isHelpFlag);
169
+ let code;
146
170
  switch (parsed.command) {
147
171
  case "health":
148
- return runHealth({ apiUrl: resolveApiUrl(parsed.globals), json }, cmdArgs, showHelp);
172
+ code = await runHealth({ apiUrl: resolveApiUrl(parsed.globals), json }, cmdArgs, showHelp);
173
+ break;
149
174
  case "config":
150
- return runConfig(cmdArgs, { json, envFile: parsed.globals.envFile }, showHelp);
175
+ code = await runConfig(cmdArgs, { json, envFile: parsed.globals.envFile }, showHelp);
176
+ break;
151
177
  case "setup":
152
- return runSetup(cmdArgs, { json, envFile: parsed.globals.envFile }, showHelp);
178
+ code = await runSetup(cmdArgs, { json, envFile: parsed.globals.envFile }, showHelp);
179
+ break;
153
180
  case "login":
154
- return runLogin(cmdArgs, { json, apiUrl: resolveApiUrl(parsed.globals) }, showHelp);
181
+ code = await runLogin(cmdArgs, { json, apiUrl: resolveApiUrl(parsed.globals) }, showHelp);
182
+ break;
155
183
  case "admin":
156
- return runAdmin(cmdArgs, { json, apiUrl: resolveApiUrl(parsed.globals) }, showHelp);
184
+ code = await runAdmin(cmdArgs, { json, apiUrl: resolveApiUrl(parsed.globals) }, showHelp);
185
+ break;
157
186
  case "mcp":
158
- return runMcp(cmdArgs, { globals: parsed.globals }, showHelp);
187
+ code = await runMcp(cmdArgs, { globals: parsed.globals }, showHelp);
188
+ break;
159
189
  case "install":
160
- return runInstall(cmdArgs, { globals: parsed.globals, json }, showHelp);
190
+ code = await runInstall(cmdArgs, { globals: parsed.globals, json }, showHelp);
191
+ break;
161
192
  case "attach":
162
193
  case "put":
194
+ case "gallery":
163
195
  case "list":
164
196
  case "delete":
165
197
  case "usage":
@@ -170,34 +202,52 @@ export async function runCli(argv) {
170
202
  const ctx = createContext(parsed.globals, !showHelp, cmdArgs);
171
203
  switch (parsed.command) {
172
204
  case "attach":
173
- return runAttach(ctx, cmdArgs, showHelp);
205
+ code = await runAttach(ctx, cmdArgs, showHelp);
206
+ break;
174
207
  case "put":
175
- return runPut(ctx, cmdArgs, showHelp);
208
+ code = await runPut(ctx, cmdArgs, showHelp);
209
+ break;
210
+ case "gallery":
211
+ code = await runGallery(ctx, cmdArgs, showHelp);
212
+ break;
176
213
  case "comment":
177
- return runComment(ctx, cmdArgs, showHelp);
214
+ code = await runComment(ctx, cmdArgs, showHelp);
215
+ break;
178
216
  case "list":
179
- return runList(ctx, cmdArgs, showHelp);
217
+ code = await runList(ctx, cmdArgs, showHelp);
218
+ break;
180
219
  case "delete":
181
- return runDelete(ctx, cmdArgs, showHelp);
220
+ code = await runDelete(ctx, cmdArgs, showHelp);
221
+ break;
182
222
  case "usage":
183
- return runUsage(ctx, cmdArgs, showHelp);
223
+ code = await runUsage(ctx, cmdArgs, showHelp);
224
+ break;
184
225
  case "reconcile":
185
- return runReconcile(ctx, cmdArgs, showHelp);
226
+ code = await runReconcile(ctx, cmdArgs, showHelp);
227
+ break;
186
228
  case "purge-expired":
187
- return runPurgeExpired(ctx, cmdArgs, showHelp);
229
+ code = await runPurgeExpired(ctx, cmdArgs, showHelp);
230
+ break;
188
231
  case "doctor":
189
- return runDoctor(ctx, cmdArgs, showHelp);
232
+ code = await runDoctor(ctx, cmdArgs, showHelp);
233
+ break;
190
234
  }
235
+ break;
191
236
  }
192
237
  default:
193
238
  process.stderr.write(`unknown command: ${parsed.command}\n\n${ROOT_HELP}`);
194
239
  return 2;
195
240
  }
241
+ // Best-effort; skipped for mcp, --quiet/--json, and opt-out env vars.
242
+ if (code === 0 && !showHelp) {
243
+ await maybeHintUpdate({ quiet: quiet || json, command: parsed.command });
244
+ }
245
+ return code;
196
246
  }
197
247
  catch (err) {
198
248
  errorOut(err, argv.includes("--json"));
199
249
  if (err instanceof UsageError && !argv.includes("--json"))
200
- process.stderr.write(`\n${ROOT_HELP}`);
250
+ usageHint(argv);
201
251
  return exitCode(err);
202
252
  }
203
253
  }
package/dist/client.d.ts CHANGED
@@ -53,6 +53,81 @@ export interface DeleteResult {
53
53
  key: string;
54
54
  deleted: boolean;
55
55
  }
56
+ /** A workspace-owned, publicly visible ordered media gallery. */
57
+ export interface GalleryItem {
58
+ id: string;
59
+ objectKey: string;
60
+ position: number;
61
+ caption: string | null;
62
+ altText: string | null;
63
+ createdAt: string;
64
+ status: "available" | "missing";
65
+ url: string | null;
66
+ /** Standalone web page for this item (gallery URL + item id). Absent on older API deployments. */
67
+ pageUrl?: string;
68
+ contentType: string | null;
69
+ size: number | null;
70
+ }
71
+ export interface Gallery {
72
+ id: string;
73
+ /** Canonical public URL returned by the API; clients must not construct it. */
74
+ url: string;
75
+ workspace: string;
76
+ title: string;
77
+ description: string | null;
78
+ visibility: "public";
79
+ coverItemId: string | null;
80
+ version: number;
81
+ createdAt: string;
82
+ updatedAt: string;
83
+ items: GalleryItem[];
84
+ }
85
+ export type GallerySummary = Omit<Gallery, "items">;
86
+ export interface GalleryListOptions {
87
+ limit?: number;
88
+ cursor?: string;
89
+ }
90
+ export interface GalleryListResult {
91
+ galleries: GallerySummary[];
92
+ nextCursor: string | null;
93
+ }
94
+ export interface CreateGalleryOptions {
95
+ title: string;
96
+ description?: string | null;
97
+ }
98
+ export interface AddGalleryItemOptions {
99
+ expectedVersion: number;
100
+ caption?: string | null;
101
+ altText?: string | null;
102
+ }
103
+ export interface DeleteGalleryOptions {
104
+ expectedVersion: number;
105
+ }
106
+ export interface GalleryExternalReference {
107
+ id: string;
108
+ provider: "github";
109
+ resourceType: "item";
110
+ coordinate: string;
111
+ canonicalUrl: string | null;
112
+ createdAt: string;
113
+ }
114
+ export interface GalleryExternalReferenceListResult {
115
+ references: GalleryExternalReference[];
116
+ }
117
+ export interface LinkGalleryExternalReferenceOptions {
118
+ expectedVersion: number;
119
+ provider: "github";
120
+ coordinate: string;
121
+ }
122
+ export interface UnlinkGalleryExternalReferenceOptions {
123
+ expectedVersion: number;
124
+ }
125
+ export interface FindGalleriesByReferenceOptions {
126
+ provider: "github";
127
+ coordinate: string;
128
+ limit?: number;
129
+ cursor?: string;
130
+ }
56
131
  export interface HealthResult {
57
132
  ok: boolean;
58
133
  }
@@ -116,6 +191,72 @@ export declare function createEnrollment(apiUrl: string, adminToken: string, inp
116
191
  tokenExpiresInSeconds?: number;
117
192
  scopes?: Array<"files:read" | "files:write" | "files:delete">;
118
193
  }): Promise<EnrollmentCreateResult>;
194
+ /** Static OAuth client id allowlisted by the auth worker's `validateClient`. */
195
+ export declare const DEVICE_CLIENT_ID = "uploads-cli";
196
+ export interface DeviceCodeResponse {
197
+ device_code: string;
198
+ user_code: string;
199
+ verification_uri: string;
200
+ verification_uri_complete?: string;
201
+ expires_in: number;
202
+ interval: number;
203
+ }
204
+ /** POST /api/auth/device/code — start a device flow. Throws on a non-2xx. */
205
+ export declare function requestDeviceCode(authUrl: string, clientId?: string): Promise<DeviceCodeResponse>;
206
+ /**
207
+ * One poll of POST /api/auth/device/token. Unlike most calls, the "not ready
208
+ * yet" outcomes (`authorization_pending`, `slow_down`) are EXPECTED 400s, so
209
+ * this returns a discriminated result instead of throwing — the caller's poll
210
+ * loop branches on `status`.
211
+ */
212
+ export type DeviceTokenResult = {
213
+ status: "ok";
214
+ accessToken: string;
215
+ tokenType: string;
216
+ expiresIn: number;
217
+ scope: string;
218
+ } | {
219
+ status: "pending";
220
+ } | {
221
+ status: "slow_down";
222
+ } | {
223
+ status: "expired";
224
+ } | {
225
+ status: "denied";
226
+ } | {
227
+ status: "error";
228
+ error: string;
229
+ description?: string;
230
+ };
231
+ export declare function requestDeviceToken(authUrl: string, input: {
232
+ deviceCode: string;
233
+ clientId?: string;
234
+ }): Promise<DeviceTokenResult>;
235
+ export interface MintWorkspaceSummary {
236
+ workspace: string;
237
+ role: string;
238
+ }
239
+ /** GET /v1/tokens — workspaces the signed-in user can mint tokens for. */
240
+ export declare function listMintWorkspaces(apiUrl: string, accessToken: string): Promise<{
241
+ workspaces: MintWorkspaceSummary[];
242
+ }>;
243
+ export interface MintTokenResult {
244
+ token: string;
245
+ workspace: string;
246
+ scopes: Array<"files:read" | "files:write" | "files:delete">;
247
+ label: string | null;
248
+ expiresAt: string | null;
249
+ }
250
+ /**
251
+ * POST /v1/tokens — mint a `up_<workspace>_…` workspace token from a device-flow
252
+ * session (presented as a bearer). v1 sends exactly one grant.
253
+ */
254
+ export declare function mintWorkspaceToken(apiUrl: string, accessToken: string, input: {
255
+ workspace: string;
256
+ scopes?: Array<"files:read" | "files:write" | "files:delete">;
257
+ label?: string;
258
+ ttlSeconds?: number;
259
+ }): Promise<MintTokenResult>;
119
260
  export declare function createUploadsClient(config: UploadsClientConfig): {
120
261
  put(body: Uint8Array, opts: PutOptions & {
121
262
  filename: string;
@@ -127,6 +268,21 @@ export declare function createUploadsClient(config: UploadsClientConfig): {
127
268
  }): Promise<ListItem[]>;
128
269
  delete(key: string): Promise<DeleteResult>;
129
270
  head(key: string): Promise<HeadResult>;
271
+ createGallery(opts: CreateGalleryOptions): Promise<Gallery>;
272
+ getGallery(id: string): Promise<Gallery>;
273
+ listGalleries(opts?: GalleryListOptions): Promise<GalleryListResult>;
274
+ deleteGallery(id: string, opts: DeleteGalleryOptions): Promise<{
275
+ deleted: boolean;
276
+ id: string;
277
+ }>;
278
+ addGalleryItem(id: string, objectKey: string, opts: AddGalleryItemOptions): Promise<GalleryItem>;
279
+ listGalleryExternalReferences(id: string): Promise<GalleryExternalReferenceListResult>;
280
+ linkGalleryExternalReference(id: string, opts: LinkGalleryExternalReferenceOptions): Promise<GalleryExternalReference>;
281
+ unlinkGalleryExternalReference(id: string, referenceId: string, opts: UnlinkGalleryExternalReferenceOptions): Promise<{
282
+ deleted: boolean;
283
+ id: string;
284
+ }>;
285
+ findGalleriesByReference(opts: FindGalleriesByReferenceOptions): Promise<GalleryListResult>;
130
286
  health(): Promise<HealthResult>;
131
287
  /** Workspace storage / upload counters (+ limits when configured). */
132
288
  usage(): Promise<UsageResult>;
package/dist/client.js CHANGED
@@ -27,6 +27,87 @@ export function createEnrollment(apiUrl, adminToken, input) {
27
27
  body: JSON.stringify(input),
28
28
  });
29
29
  }
30
+ // --- Device authorization (RFC 8628) — the `uploads login` device flow ---
31
+ //
32
+ // The CLI speaks the auth worker's OAuth-shaped endpoints directly with plain
33
+ // `fetch` (no better-auth client dependency in the published package, per plan
34
+ // D5). Better Auth's `device.code`/`device.token` endpoints take
35
+ // `application/json` bodies, NOT the RFC's form-encoding — the JSON shapes
36
+ // below are what the worker expects.
37
+ /** Static OAuth client id allowlisted by the auth worker's `validateClient`. */
38
+ export const DEVICE_CLIENT_ID = "uploads-cli";
39
+ /** POST /api/auth/device/code — start a device flow. Throws on a non-2xx. */
40
+ export function requestDeviceCode(authUrl, clientId = DEVICE_CLIENT_ID) {
41
+ return jsonRequest(`${authUrl.replace(/\/$/, "")}/api/auth/device/code`, {
42
+ method: "POST",
43
+ headers: { "Content-Type": "application/json" },
44
+ body: JSON.stringify({ client_id: clientId }),
45
+ });
46
+ }
47
+ export async function requestDeviceToken(authUrl, input) {
48
+ let res;
49
+ try {
50
+ res = await fetch(`${authUrl.replace(/\/$/, "")}/api/auth/device/token`, {
51
+ method: "POST",
52
+ headers: { "Content-Type": "application/json" },
53
+ body: JSON.stringify({
54
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code",
55
+ device_code: input.deviceCode,
56
+ client_id: input.clientId ?? DEVICE_CLIENT_ID,
57
+ }),
58
+ });
59
+ }
60
+ catch (err) {
61
+ throw new UploadsError(err instanceof Error ? err.message : "network request failed", "NETWORK");
62
+ }
63
+ const body = (await res.json().catch(() => null));
64
+ if (res.ok && body?.access_token) {
65
+ return {
66
+ status: "ok",
67
+ accessToken: body.access_token,
68
+ tokenType: body.token_type ?? "Bearer",
69
+ expiresIn: typeof body.expires_in === "number" ? body.expires_in : 0,
70
+ scope: body.scope ?? "",
71
+ };
72
+ }
73
+ switch (body?.error) {
74
+ case "authorization_pending":
75
+ return { status: "pending" };
76
+ case "slow_down":
77
+ return { status: "slow_down" };
78
+ case "expired_token":
79
+ return { status: "expired" };
80
+ case "access_denied":
81
+ return { status: "denied" };
82
+ default:
83
+ return {
84
+ status: "error",
85
+ error: body?.error ?? "unknown",
86
+ description: body?.error_description,
87
+ };
88
+ }
89
+ }
90
+ /** GET /v1/tokens — workspaces the signed-in user can mint tokens for. */
91
+ export function listMintWorkspaces(apiUrl, accessToken) {
92
+ return jsonRequest(`${apiUrl.replace(/\/$/, "")}/v1/tokens`, {
93
+ headers: { Authorization: `Bearer ${accessToken}` },
94
+ });
95
+ }
96
+ /**
97
+ * POST /v1/tokens — mint a `up_<workspace>_…` workspace token from a device-flow
98
+ * session (presented as a bearer). v1 sends exactly one grant.
99
+ */
100
+ export function mintWorkspaceToken(apiUrl, accessToken, input) {
101
+ return jsonRequest(`${apiUrl.replace(/\/$/, "")}/v1/tokens`, {
102
+ method: "POST",
103
+ headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json" },
104
+ body: JSON.stringify({
105
+ grants: [{ workspace: input.workspace, ...(input.scopes ? { scopes: input.scopes } : {}) }],
106
+ ...(input.label ? { label: input.label } : {}),
107
+ ...(input.ttlSeconds ? { ttlSeconds: input.ttlSeconds } : {}),
108
+ }),
109
+ });
110
+ }
30
111
  function encodeKeyPath(key) {
31
112
  return key.split("/").map(encodeURIComponent).join("/");
32
113
  }
@@ -36,6 +117,9 @@ function filesBase(config) {
36
117
  function usageBase(config) {
37
118
  return `${config.apiUrl}/v1/${encodeURIComponent(config.workspace)}/usage`;
38
119
  }
120
+ function galleriesBase(config) {
121
+ return `${config.apiUrl}/v1/${encodeURIComponent(config.workspace)}/galleries`;
122
+ }
39
123
  function mapApiError(status, error, code) {
40
124
  const normalized = error.toLowerCase();
41
125
  if (status === 401 || code === "unauthorized" || normalized === "unauthorized") {
@@ -124,6 +208,9 @@ export function createUploadsClient(config) {
124
208
  const qs = params.toString();
125
209
  return request("GET", `${filesBase(config)}${qs ? `?${qs}` : ""}`);
126
210
  }
211
+ async function getGallery(id) {
212
+ return request("GET", `${galleriesBase(config)}/${encodeURIComponent(id)}`);
213
+ }
127
214
  return {
128
215
  async put(body, opts) {
129
216
  const key = opts.key ??
@@ -170,6 +257,63 @@ export function createUploadsClient(config) {
170
257
  async head(key) {
171
258
  return request("GET", `${filesBase(config)}/${encodeKeyPath(key)}`);
172
259
  },
260
+ async createGallery(opts) {
261
+ return request("POST", galleriesBase(config), {
262
+ body: new TextEncoder().encode(JSON.stringify(opts)),
263
+ headers: { "Content-Type": "application/json" },
264
+ });
265
+ },
266
+ async getGallery(id) {
267
+ return getGallery(id);
268
+ },
269
+ async listGalleries(opts = {}) {
270
+ const params = new URLSearchParams();
271
+ if (opts.limit != null)
272
+ params.set("limit", String(opts.limit));
273
+ if (opts.cursor)
274
+ params.set("cursor", opts.cursor);
275
+ const qs = params.toString();
276
+ return request("GET", `${galleriesBase(config)}${qs ? `?${qs}` : ""}`);
277
+ },
278
+ async deleteGallery(id, opts) {
279
+ return request("DELETE", `${galleriesBase(config)}/${encodeURIComponent(id)}`, {
280
+ body: new TextEncoder().encode(JSON.stringify(opts)),
281
+ headers: { "Content-Type": "application/json" },
282
+ });
283
+ },
284
+ async addGalleryItem(id, objectKey, opts) {
285
+ return request("POST", `${galleriesBase(config)}/${encodeURIComponent(id)}/items`, {
286
+ body: new TextEncoder().encode(JSON.stringify({ objectKey, ...opts })),
287
+ headers: { "Content-Type": "application/json" },
288
+ });
289
+ },
290
+ async listGalleryExternalReferences(id) {
291
+ return request("GET", galleriesBase(config) + "/" + encodeURIComponent(id) + "/external-references");
292
+ },
293
+ async linkGalleryExternalReference(id, opts) {
294
+ return request("POST", galleriesBase(config) + "/" + encodeURIComponent(id) + "/external-references", {
295
+ body: new TextEncoder().encode(JSON.stringify(opts)),
296
+ headers: { "Content-Type": "application/json" },
297
+ });
298
+ },
299
+ async unlinkGalleryExternalReference(id, referenceId, opts) {
300
+ return request("DELETE", galleriesBase(config) +
301
+ "/" +
302
+ encodeURIComponent(id) +
303
+ "/external-references/" +
304
+ encodeURIComponent(referenceId), {
305
+ body: new TextEncoder().encode(JSON.stringify(opts)),
306
+ headers: { "Content-Type": "application/json" },
307
+ });
308
+ },
309
+ async findGalleriesByReference(opts) {
310
+ const params = new URLSearchParams({ provider: opts.provider, coordinate: opts.coordinate });
311
+ if (opts.limit != null)
312
+ params.set("limit", String(opts.limit));
313
+ if (opts.cursor)
314
+ params.set("cursor", opts.cursor);
315
+ return request("GET", galleriesBase(config) + "/by-reference?" + params);
316
+ },
173
317
  async health() {
174
318
  return request("GET", `${config.apiUrl}/health`, { auth: false });
175
319
  },