@buildinternet/uploads 0.17.0 → 0.19.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/dist/cli.js +1 -0
- package/dist/client.d.ts +4 -0
- package/dist/client.js +13 -3
- package/dist/commands/login.js +12 -2
- package/dist/commands.d.ts +2 -0
- package/dist/commands.js +8 -0
- package/dist/errors.d.ts +1 -1
- package/dist/github.js +5 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -105,6 +105,7 @@ const ERROR_HINTS = {
|
|
|
105
105
|
UPLOAD_BUDGET: QUOTA_HINT,
|
|
106
106
|
KEY_POLICY: "hint: use a typed destination (`--destination screenshots|gh`) or an allowed prefix; operators set allowlists with `pnpm workspace:limits --allowed-prefixes`\n",
|
|
107
107
|
UNAUTHORIZED: "hint: token rejected — run `uploads login` to sign in again, or check UPLOADS_TOKEN / --token\n",
|
|
108
|
+
INSUFFICIENT_SCOPE: "hint: re-run `uploads login` for a full-scope token (or mint one with --scopes)\n",
|
|
108
109
|
BROWSER_NOT_FOUND: "hint: no local browser found; try --via remote, or install Chrome / npx playwright install chromium\n",
|
|
109
110
|
RATE_LIMITED: "hint: transient rate limit — wait ~60s and retry\n",
|
|
110
111
|
};
|
package/dist/client.d.ts
CHANGED
|
@@ -256,6 +256,8 @@ export interface UsageResult {
|
|
|
256
256
|
storageRemainingBytes?: number;
|
|
257
257
|
maxUploadsPerPeriod?: number;
|
|
258
258
|
uploadsRemaining?: number;
|
|
259
|
+
/** File scopes of the presented token (servers ≥ this field's release). */
|
|
260
|
+
scopes?: Array<TokenScope>;
|
|
259
261
|
}
|
|
260
262
|
export interface ReconcileResult {
|
|
261
263
|
workspace: string;
|
|
@@ -427,11 +429,13 @@ export declare function mintWorkspaceToken(apiUrl: string, accessToken: string,
|
|
|
427
429
|
export declare function extractErrorFields(body: unknown, fallback?: string): {
|
|
428
430
|
message: string;
|
|
429
431
|
code?: string;
|
|
432
|
+
requiredScope?: string;
|
|
430
433
|
};
|
|
431
434
|
/** Fetch + parse an error-response body via {@link extractErrorFields}. */
|
|
432
435
|
export declare function parseErrorEnvelope(res: Response, fallback?: string): Promise<{
|
|
433
436
|
message: string;
|
|
434
437
|
code?: string;
|
|
438
|
+
requiredScope?: string;
|
|
435
439
|
}>;
|
|
436
440
|
export declare function createUploadsClient(config: UploadsClientConfig): {
|
|
437
441
|
put(body: Uint8Array, opts: PutOptions & {
|
package/dist/client.js
CHANGED
|
@@ -188,11 +188,17 @@ function usageBase(config) {
|
|
|
188
188
|
function galleriesBase(config) {
|
|
189
189
|
return `${config.apiUrl}/v1/${encodeURIComponent(config.workspace)}/galleries`;
|
|
190
190
|
}
|
|
191
|
-
function mapApiError(status, error, code) {
|
|
191
|
+
function mapApiError(status, error, code, requiredScope) {
|
|
192
192
|
const normalized = error.toLowerCase();
|
|
193
193
|
if (status === 401 || code === "unauthorized" || normalized === "unauthorized") {
|
|
194
194
|
return new UploadsError(error, "UNAUTHORIZED", status);
|
|
195
195
|
}
|
|
196
|
+
if (code === "insufficient_scope") {
|
|
197
|
+
// The server's message is a bare "forbidden" — name the missing scope so
|
|
198
|
+
// the failure is actionable without a doctor run.
|
|
199
|
+
const message = requiredScope ? `token lacks the ${requiredScope} scope` : error;
|
|
200
|
+
return new UploadsError(message, "INSUFFICIENT_SCOPE", status);
|
|
201
|
+
}
|
|
196
202
|
if (status === 404 || code === "not_found" || normalized === "not found") {
|
|
197
203
|
return new UploadsError(error, "NOT_FOUND", status);
|
|
198
204
|
}
|
|
@@ -226,9 +232,13 @@ export function extractErrorFields(body, fallback = "request failed") {
|
|
|
226
232
|
const err = body.error;
|
|
227
233
|
if (typeof err === "object" && err && "message" in err) {
|
|
228
234
|
const nested = err;
|
|
235
|
+
const details = nested.details;
|
|
229
236
|
return {
|
|
230
237
|
message: typeof nested.message === "string" ? nested.message : fallback,
|
|
231
238
|
code: typeof nested.code === "string" ? nested.code : undefined,
|
|
239
|
+
...(typeof details?.required_scope === "string"
|
|
240
|
+
? { requiredScope: details.required_scope }
|
|
241
|
+
: {}),
|
|
232
242
|
};
|
|
233
243
|
}
|
|
234
244
|
if (typeof err === "string") {
|
|
@@ -246,8 +256,8 @@ export async function parseErrorEnvelope(res, fallback = "request failed") {
|
|
|
246
256
|
return extractErrorFields(body, fallback);
|
|
247
257
|
}
|
|
248
258
|
async function parseErrorResponse(res) {
|
|
249
|
-
const { message, code } = await parseErrorEnvelope(res, res.statusText || "request failed");
|
|
250
|
-
return mapApiError(res.status, message, code);
|
|
259
|
+
const { message, code, requiredScope } = await parseErrorEnvelope(res, res.statusText || "request failed");
|
|
260
|
+
return mapApiError(res.status, message, code, requiredScope);
|
|
251
261
|
}
|
|
252
262
|
export function createUploadsClient(config) {
|
|
253
263
|
async function request(method, path, opts) {
|
package/dist/commands/login.js
CHANGED
|
@@ -19,7 +19,8 @@ Options:
|
|
|
19
19
|
--create With --workspace: create the workspace first if your
|
|
20
20
|
account doesn't have it yet (device flow only) — lets
|
|
21
21
|
scripted/agent logins provision without a prompt
|
|
22
|
-
--scopes <list> Comma-separated scopes (default:
|
|
22
|
+
--scopes <list> Comma-separated scopes (default:
|
|
23
|
+
files:read,files:write,files:delete)
|
|
23
24
|
--label <text> Token label (default: this machine's hostname)
|
|
24
25
|
--auth-url <url> Auth base (default: https://auth.uploads.sh)
|
|
25
26
|
--no-open Don't try to open a browser automatically
|
|
@@ -201,7 +202,16 @@ export async function obtainDeviceAccessToken(authUrl, opts = {}, io = defaultDe
|
|
|
201
202
|
* it in a browser, poll for the session token, then mint a workspace token.
|
|
202
203
|
*/
|
|
203
204
|
async function runDeviceLogin(parsed, opts, io) {
|
|
204
|
-
|
|
205
|
+
// Interactive login is the user's own credential: default to the full file
|
|
206
|
+
// scope set (including delete) so the CLI's own `delete` command works out
|
|
207
|
+
// of the box. Automation tokens minted elsewhere keep the server's
|
|
208
|
+
// conservative read+write default — narrowness there is a deliberate
|
|
209
|
+
// choice, not a surprise. `--scopes` still overrides.
|
|
210
|
+
const scopes = parseScopes(flagString(parsed.flags, "--scopes")) ?? [
|
|
211
|
+
"files:read",
|
|
212
|
+
"files:write",
|
|
213
|
+
"files:delete",
|
|
214
|
+
];
|
|
205
215
|
const label = flagString(parsed.flags, "--label") ?? safeHostname();
|
|
206
216
|
const requestedWorkspace = flagString(parsed.flags, "--workspace");
|
|
207
217
|
// Make the target explicit: a bare `uploads login` on a self-hosted install
|
package/dist/commands.d.ts
CHANGED
|
@@ -284,6 +284,8 @@ export interface DoctorReport {
|
|
|
284
284
|
uploadsInPeriod?: number;
|
|
285
285
|
error?: string;
|
|
286
286
|
};
|
|
287
|
+
/** File scopes of the presented token (absent against pre-scopes servers). */
|
|
288
|
+
scopes?: string[];
|
|
287
289
|
/** Workspace/token mismatch warning (also present in hints). */
|
|
288
290
|
warning?: string;
|
|
289
291
|
hints: string[];
|
package/dist/commands.js
CHANGED
|
@@ -2052,6 +2052,7 @@ export async function buildDoctorReport(config, client, detectRoots) {
|
|
|
2052
2052
|
}
|
|
2053
2053
|
}
|
|
2054
2054
|
let usage;
|
|
2055
|
+
let scopes;
|
|
2055
2056
|
if (authOk) {
|
|
2056
2057
|
try {
|
|
2057
2058
|
const snap = await client.usage();
|
|
@@ -2061,6 +2062,10 @@ export async function buildDoctorReport(config, client, detectRoots) {
|
|
|
2061
2062
|
objects: snap.objects,
|
|
2062
2063
|
uploadsInPeriod: snap.uploadsInPeriod,
|
|
2063
2064
|
};
|
|
2065
|
+
scopes = snap.scopes;
|
|
2066
|
+
if (scopes && !scopes.includes("files:delete")) {
|
|
2067
|
+
hints.push("token lacks files:delete (`uploads delete` will be forbidden) — re-run `uploads login` for a full-scope token");
|
|
2068
|
+
}
|
|
2064
2069
|
}
|
|
2065
2070
|
catch (err) {
|
|
2066
2071
|
usage = {
|
|
@@ -2084,6 +2089,7 @@ export async function buildDoctorReport(config, client, detectRoots) {
|
|
|
2084
2089
|
health,
|
|
2085
2090
|
auth: { ok: authOk, error: authError },
|
|
2086
2091
|
usage,
|
|
2092
|
+
scopes,
|
|
2087
2093
|
warning: mismatch,
|
|
2088
2094
|
hints,
|
|
2089
2095
|
browser,
|
|
@@ -2106,6 +2112,8 @@ export async function runDoctor(ctx, args, help = false) {
|
|
|
2106
2112
|
`workspace: ${report.workspace}`,
|
|
2107
2113
|
`auth: ${report.auth.ok ? "ok" : `failed — ${report.auth.error ?? "no token"}`}`,
|
|
2108
2114
|
];
|
|
2115
|
+
if (report.scopes)
|
|
2116
|
+
lines.push(`scopes: ${report.scopes.join(", ")}`);
|
|
2109
2117
|
if (report.usage) {
|
|
2110
2118
|
lines.push(report.usage.ok
|
|
2111
2119
|
? `usage: ${formatByteSize(report.usage.bytes ?? 0)}, ${formatCount(report.usage.objects ?? 0)} objects, ${formatCount(report.usage.uploadsInPeriod ?? 0)} uploads this period`
|
package/dist/errors.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type UploadsErrorCode = "MISSING_TOKEN" | "NO_PUBLIC_URL" | "FILE_NOT_FOUND" | "NOT_FOUND" | "UNAUTHORIZED" | "INVALID_KEY" | "KEY_POLICY" | "STORAGE_QUOTA" | "UPLOAD_BUDGET" | "GITHUB_REQUIRED" | "API_ERROR" | "NETWORK" | "USAGE" | "BROWSER_NOT_FOUND" | "RENDER_FAILED" | "RATE_LIMITED";
|
|
1
|
+
export type UploadsErrorCode = "MISSING_TOKEN" | "NO_PUBLIC_URL" | "FILE_NOT_FOUND" | "NOT_FOUND" | "UNAUTHORIZED" | "INSUFFICIENT_SCOPE" | "INVALID_KEY" | "KEY_POLICY" | "STORAGE_QUOTA" | "UPLOAD_BUDGET" | "GITHUB_REQUIRED" | "API_ERROR" | "NETWORK" | "USAGE" | "BROWSER_NOT_FOUND" | "RENDER_FAILED" | "RATE_LIMITED";
|
|
2
2
|
export declare class UploadsError extends Error {
|
|
3
3
|
readonly code: UploadsErrorCode;
|
|
4
4
|
readonly status?: number;
|
package/dist/github.js
CHANGED
|
@@ -99,6 +99,11 @@ export function ghMetadataForBranch(repo, branch, now = new Date()) {
|
|
|
99
99
|
"gh.kind": "branch",
|
|
100
100
|
"gh.branch": branchLower,
|
|
101
101
|
"gh.staged-at": now.toISOString().replace(/\.\d{3}Z$/, "Z"),
|
|
102
|
+
// Lifecycle tag (issue #339): flipped to "promoted" by server-side
|
|
103
|
+
// promotion, so "in-flight staged media" is a plain equality query
|
|
104
|
+
// (`meta.gh.status=staged`) — the metadata filter API can't express
|
|
105
|
+
// "gh.promoted-at absent".
|
|
106
|
+
"gh.status": "staged",
|
|
102
107
|
};
|
|
103
108
|
}
|
|
104
109
|
/**
|