@buildinternet/uploads 0.40.0 → 0.41.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-catalog.js +4 -0
- package/dist/client.d.ts +61 -4
- package/dist/client.js +87 -26
- package/dist/commands/screenshot.js +18 -6
- package/dist/commands.d.ts +45 -1
- package/dist/commands.js +222 -26
- package/dist/github.d.ts +62 -0
- package/dist/github.js +84 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +5 -1
- package/dist/mcp/tools.js +39 -8
- package/package.json +1 -1
package/dist/cli-catalog.js
CHANGED
|
@@ -164,6 +164,10 @@ export const ROOT_COMMANDS = [
|
|
|
164
164
|
subcommands: [
|
|
165
165
|
{ name: "link", summary: "Claim or inspect the repo binding" },
|
|
166
166
|
{ name: "doctor", summary: "Check the GitHub App's webhook event subscriptions" },
|
|
167
|
+
{
|
|
168
|
+
name: "rotate-prefix",
|
|
169
|
+
summary: "Rotate a private repo's randomized attachment URL prefix",
|
|
170
|
+
},
|
|
167
171
|
],
|
|
168
172
|
},
|
|
169
173
|
{
|
package/dist/client.d.ts
CHANGED
|
@@ -256,6 +256,41 @@ export type GithubCommentResult = {
|
|
|
256
256
|
fixUrl?: string;
|
|
257
257
|
required?: string[];
|
|
258
258
|
};
|
|
259
|
+
/** `POST /v1/workspaces/:workspace/github/private-prefix` request (server contract, issue #631/#613). */
|
|
260
|
+
export interface ResolveGhPrefixOptions {
|
|
261
|
+
repo: string;
|
|
262
|
+
branch?: string;
|
|
263
|
+
target?: {
|
|
264
|
+
kind: "pull" | "issues";
|
|
265
|
+
num: number;
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
/** `POST /v1/workspaces/:workspace/github/private-prefix` response (server contract, issue #631/#613). */
|
|
269
|
+
export type ResolveGhPrefixResult = {
|
|
270
|
+
mode: "plain";
|
|
271
|
+
} | {
|
|
272
|
+
mode: "private";
|
|
273
|
+
prefixId: string;
|
|
274
|
+
activePrefixIds?: string[];
|
|
275
|
+
};
|
|
276
|
+
/** `POST /v1/workspaces/:workspace/github/private-prefix/rotate` request (server contract, issue #631/#613). */
|
|
277
|
+
export interface RotateGhPrefixOptions {
|
|
278
|
+
repo: string;
|
|
279
|
+
/** Mutually exclusive with `repoLevel`: rotate one branch's id. */
|
|
280
|
+
branch?: string;
|
|
281
|
+
/** Mutually exclusive with `branch`: rotate the repo-level id shared by
|
|
282
|
+
* issue attachments and ingested assets. */
|
|
283
|
+
repoLevel?: boolean;
|
|
284
|
+
}
|
|
285
|
+
/** `POST /v1/workspaces/:workspace/github/private-prefix/rotate` response (server contract, issue #631/#613). */
|
|
286
|
+
export type RotateGhPrefixResult = {
|
|
287
|
+
rotated: false;
|
|
288
|
+
reason: string;
|
|
289
|
+
} | {
|
|
290
|
+
rotated: true;
|
|
291
|
+
prefixId: string;
|
|
292
|
+
moved: number;
|
|
293
|
+
};
|
|
259
294
|
/** `POST /v1/workspaces/:workspace/github/promote` request/response (server contract, PR #310). */
|
|
260
295
|
export interface PromoteBranchAttachmentsOptions {
|
|
261
296
|
repo: string;
|
|
@@ -567,10 +602,12 @@ export declare function createUploadsClient(config: UploadsClientConfig): {
|
|
|
567
602
|
/** `PATCH /v1/:workspace/files/:key` — merge `set`/`delete`; returns the merged map. */
|
|
568
603
|
patchMetadata(key: string, opts: PatchMetadataOptions): Promise<GetMetadataResult>;
|
|
569
604
|
/**
|
|
570
|
-
* `GET /v1/:workspace/files?meta.<k>=<v>&…&name=…` —
|
|
571
|
-
* over queryable metadata and/or a case-insensitive
|
|
572
|
-
* At least one of non-empty `filters` or `opts.name`
|
|
573
|
-
* `filters` must be pre-validated when present (see
|
|
605
|
+
* `GET /v1/workspaces/:workspace/files/search?meta.<k>=<v>&…&name=…` —
|
|
606
|
+
* ANDed equality filter over queryable metadata and/or a case-insensitive
|
|
607
|
+
* filename substring. At least one of non-empty `filters` or `opts.name`
|
|
608
|
+
* is required. `filters` must be pre-validated when present (see
|
|
609
|
+
* `metadata.ts`). The canonical search route is non-paginated (server cap
|
|
610
|
+
* 100, narrowable via `limit`), so `cursor` is always null.
|
|
574
611
|
*/
|
|
575
612
|
findFiles(filters?: Record<string, string>, opts?: FindFilesOptions): Promise<FindFilesResult>;
|
|
576
613
|
/** `GET /v1/:workspace/files/facets` — workspace metadata key vocabulary. */
|
|
@@ -606,6 +643,26 @@ export declare function createUploadsClient(config: UploadsClientConfig): {
|
|
|
606
643
|
kind: "pull" | "issues";
|
|
607
644
|
resync?: boolean;
|
|
608
645
|
}): Promise<GithubCommentResult>;
|
|
646
|
+
/**
|
|
647
|
+
* Resolve the GitHub-key mode (plain vs. randomized private prefix, issue
|
|
648
|
+
* #631) a caller should stage/list attachments under for `repo`. Fail-open:
|
|
649
|
+
* ANY failure — a 404 from an older/self-hosted server, a network error, a
|
|
650
|
+
* non-2xx response, or a malformed body — resolves to `{ mode: "plain" }`
|
|
651
|
+
* silently (no stderr noise), never throws. Cached per-process, keyed by
|
|
652
|
+
* repo+branch+target, so repeated calls for the same coordinate (e.g. the
|
|
653
|
+
* gh-fallback comment gather re-checking on every sync) cost one request.
|
|
654
|
+
*/
|
|
655
|
+
resolveGhPrefix(opts: ResolveGhPrefixOptions): Promise<ResolveGhPrefixResult>;
|
|
656
|
+
/**
|
|
657
|
+
* Rotate the active private-repo attachment prefix for `opts.repo` +
|
|
658
|
+
* (`opts.branch` or `opts.repoLevel`) (issue #631). Unlike
|
|
659
|
+
* `resolveGhPrefix`, this is NOT fail-open: it's an explicit, caller-
|
|
660
|
+
* initiated action, so a failure (including a 404 from an older/self-
|
|
661
|
+
* hosted server without this route) throws `UploadsError` — the CLI
|
|
662
|
+
* command decides how to present that, rather than this method silently
|
|
663
|
+
* degrading to a shape that would look like success.
|
|
664
|
+
*/
|
|
665
|
+
rotateGhPrefix(opts: RotateGhPrefixOptions): Promise<RotateGhPrefixResult>;
|
|
609
666
|
/**
|
|
610
667
|
* Promote a workspace's branch-staged attachments into a PR's stable
|
|
611
668
|
* attachment prefix (server contract, PR #310 — degrade-safe callers
|
package/dist/client.js
CHANGED
|
@@ -186,13 +186,12 @@ export function mintWorkspaceToken(apiUrl, accessToken, input) {
|
|
|
186
186
|
function encodeKeyPath(key) {
|
|
187
187
|
return key.split("/").map(encodeURIComponent).join("/");
|
|
188
188
|
}
|
|
189
|
-
//
|
|
190
|
-
//
|
|
191
|
-
//
|
|
192
|
-
//
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
return `${config.apiUrl}/v1/${encodeURIComponent(config.workspace)}/files`;
|
|
189
|
+
// Canonical files surface (issue #613): every file operation now goes to
|
|
190
|
+
// `/v1/workspaces/:workspace/files`. Per-key ops share handlers with the
|
|
191
|
+
// legacy wildcard (#636); list/find/facets adapt the canonical envelopes in
|
|
192
|
+
// their wrappers below (#613 shape reconciliation).
|
|
193
|
+
function canonicalFilesBase(config) {
|
|
194
|
+
return `${config.apiUrl}/v1/workspaces/${encodeURIComponent(config.workspace)}/files`;
|
|
196
195
|
}
|
|
197
196
|
function usageBase(config) {
|
|
198
197
|
return `${config.apiUrl}/v1/workspaces/${encodeURIComponent(config.workspace)}/usage`;
|
|
@@ -311,18 +310,30 @@ export function createUploadsClient(config) {
|
|
|
311
310
|
if (opts.metadata)
|
|
312
311
|
params.set("metadata", "1");
|
|
313
312
|
const qs = params.toString();
|
|
314
|
-
|
|
313
|
+
// Canonical list envelope is `{files, prefixes, cursor}` with queryable
|
|
314
|
+
// metadata always hydrated (issue #613 — the session shape won the
|
|
315
|
+
// reconciliation). This client keeps its historical `{items, cursor}`
|
|
316
|
+
// contract: rename the array and honor `opts.metadata` by stripping the
|
|
317
|
+
// hydrated maps when the caller didn't ask for them.
|
|
318
|
+
const page = await request("GET", `${canonicalFilesBase(config)}${qs ? `?${qs}` : ""}`);
|
|
315
319
|
return {
|
|
316
|
-
|
|
317
|
-
items: page.
|
|
318
|
-
...item
|
|
319
|
-
|
|
320
|
-
|
|
320
|
+
cursor: page.cursor,
|
|
321
|
+
items: page.files.map((item) => {
|
|
322
|
+
const { metadata, ...rest } = item;
|
|
323
|
+
return {
|
|
324
|
+
...rest,
|
|
325
|
+
...(opts.metadata && metadata !== undefined ? { metadata } : {}),
|
|
326
|
+
embedUrl: resolveEmbedUrl(item.url, item.embedUrl),
|
|
327
|
+
};
|
|
328
|
+
}),
|
|
321
329
|
};
|
|
322
330
|
}
|
|
323
331
|
async function getGallery(id) {
|
|
324
332
|
return request("GET", `${galleriesBase(config)}/${encodeURIComponent(id)}`);
|
|
325
333
|
}
|
|
334
|
+
// Per-process cache for resolveGhPrefix, keyed by repo+branch+target — see
|
|
335
|
+
// that method's doc.
|
|
336
|
+
const resolveGhPrefixCache = new Map();
|
|
326
337
|
return {
|
|
327
338
|
async put(body, opts) {
|
|
328
339
|
const key = opts.key ??
|
|
@@ -337,7 +348,7 @@ export function createUploadsClient(config) {
|
|
|
337
348
|
const contentType = opts.contentType ?? inferContentType(opts.filename);
|
|
338
349
|
if (opts.dryRun) {
|
|
339
350
|
const qs = opts.replace ? "dryRun=1&replace=1" : "dryRun=1";
|
|
340
|
-
const preview = await request("PUT", `${
|
|
351
|
+
const preview = await request("PUT", `${canonicalFilesBase(config)}/${encodeKeyPath(key)}?${qs}`);
|
|
341
352
|
if (preview.url == null) {
|
|
342
353
|
throw new UploadsError("workspace has no publicBaseUrl (cannot resolve a public URL)", "NO_PUBLIC_URL");
|
|
343
354
|
}
|
|
@@ -369,7 +380,7 @@ export function createUploadsClient(config) {
|
|
|
369
380
|
headers[`X-Uploads-Meta-${k}`] = v;
|
|
370
381
|
}
|
|
371
382
|
}
|
|
372
|
-
const result = await request("PUT", `${
|
|
383
|
+
const result = await request("PUT", `${canonicalFilesBase(config)}/${encodeKeyPath(key)}`, {
|
|
373
384
|
body,
|
|
374
385
|
headers,
|
|
375
386
|
});
|
|
@@ -396,24 +407,26 @@ export function createUploadsClient(config) {
|
|
|
396
407
|
return items;
|
|
397
408
|
},
|
|
398
409
|
async delete(key) {
|
|
399
|
-
return request("DELETE", `${
|
|
410
|
+
return request("DELETE", `${canonicalFilesBase(config)}/${encodeKeyPath(key)}`);
|
|
400
411
|
},
|
|
401
412
|
/** `GET /v1/:workspace/files/:key?metadata=1` — the object's queryable metadata. */
|
|
402
413
|
async getMetadata(key) {
|
|
403
|
-
return request("GET", `${
|
|
414
|
+
return request("GET", `${canonicalFilesBase(config)}/${encodeKeyPath(key)}?metadata=1`);
|
|
404
415
|
},
|
|
405
416
|
/** `PATCH /v1/:workspace/files/:key` — merge `set`/`delete`; returns the merged map. */
|
|
406
417
|
async patchMetadata(key, opts) {
|
|
407
|
-
return request("PATCH", `${
|
|
418
|
+
return request("PATCH", `${canonicalFilesBase(config)}/${encodeKeyPath(key)}`, {
|
|
408
419
|
body: new TextEncoder().encode(JSON.stringify(opts)),
|
|
409
420
|
headers: { "Content-Type": "application/json" },
|
|
410
421
|
});
|
|
411
422
|
},
|
|
412
423
|
/**
|
|
413
|
-
* `GET /v1/:workspace/files?meta.<k>=<v>&…&name=…` —
|
|
414
|
-
* over queryable metadata and/or a case-insensitive
|
|
415
|
-
* At least one of non-empty `filters` or `opts.name`
|
|
416
|
-
* `filters` must be pre-validated when present (see
|
|
424
|
+
* `GET /v1/workspaces/:workspace/files/search?meta.<k>=<v>&…&name=…` —
|
|
425
|
+
* ANDed equality filter over queryable metadata and/or a case-insensitive
|
|
426
|
+
* filename substring. At least one of non-empty `filters` or `opts.name`
|
|
427
|
+
* is required. `filters` must be pre-validated when present (see
|
|
428
|
+
* `metadata.ts`). The canonical search route is non-paginated (server cap
|
|
429
|
+
* 100, narrowable via `limit`), so `cursor` is always null.
|
|
417
430
|
*/
|
|
418
431
|
async findFiles(filters = {}, opts = {}) {
|
|
419
432
|
const params = new URLSearchParams();
|
|
@@ -425,18 +438,19 @@ export function createUploadsClient(config) {
|
|
|
425
438
|
params.set("prefix", opts.prefix);
|
|
426
439
|
if (opts.limit != null)
|
|
427
440
|
params.set("limit", String(opts.limit));
|
|
428
|
-
|
|
441
|
+
const result = await request("GET", `${canonicalFilesBase(config)}/search?${params.toString()}`);
|
|
442
|
+
return { items: result.items, cursor: null, truncated: result.truncated };
|
|
429
443
|
},
|
|
430
444
|
/** `GET /v1/:workspace/files/facets` — workspace metadata key vocabulary. */
|
|
431
445
|
async listMetadataKeys() {
|
|
432
|
-
return request("GET", `${
|
|
446
|
+
return request("GET", `${canonicalFilesBase(config)}/facets`);
|
|
433
447
|
},
|
|
434
448
|
/** `GET /v1/:workspace/files/facets?key=` — distinct values for one key. */
|
|
435
449
|
async listMetadataValues(key) {
|
|
436
|
-
return request("GET", `${
|
|
450
|
+
return request("GET", `${canonicalFilesBase(config)}/facets?${new URLSearchParams({ key })}`);
|
|
437
451
|
},
|
|
438
452
|
async head(key) {
|
|
439
|
-
const result = await request("GET", `${
|
|
453
|
+
const result = await request("GET", `${canonicalFilesBase(config)}/${encodeKeyPath(key)}`);
|
|
440
454
|
return { ...result, embedUrl: resolveEmbedUrl(result.url, result.embedUrl) };
|
|
441
455
|
},
|
|
442
456
|
async createGallery(opts) {
|
|
@@ -509,6 +523,53 @@ export function createUploadsClient(config) {
|
|
|
509
523
|
headers: { "Content-Type": "application/json" },
|
|
510
524
|
});
|
|
511
525
|
},
|
|
526
|
+
/**
|
|
527
|
+
* Resolve the GitHub-key mode (plain vs. randomized private prefix, issue
|
|
528
|
+
* #631) a caller should stage/list attachments under for `repo`. Fail-open:
|
|
529
|
+
* ANY failure — a 404 from an older/self-hosted server, a network error, a
|
|
530
|
+
* non-2xx response, or a malformed body — resolves to `{ mode: "plain" }`
|
|
531
|
+
* silently (no stderr noise), never throws. Cached per-process, keyed by
|
|
532
|
+
* repo+branch+target, so repeated calls for the same coordinate (e.g. the
|
|
533
|
+
* gh-fallback comment gather re-checking on every sync) cost one request.
|
|
534
|
+
*/
|
|
535
|
+
async resolveGhPrefix(opts) {
|
|
536
|
+
const cacheKey = JSON.stringify([
|
|
537
|
+
opts.repo.toLowerCase(),
|
|
538
|
+
opts.branch ?? "",
|
|
539
|
+
opts.target ?? null,
|
|
540
|
+
]);
|
|
541
|
+
const cached = resolveGhPrefixCache.get(cacheKey);
|
|
542
|
+
if (cached)
|
|
543
|
+
return cached;
|
|
544
|
+
const resolved = await (async () => {
|
|
545
|
+
try {
|
|
546
|
+
return await request("POST", `${config.apiUrl}/v1/workspaces/${encodeURIComponent(config.workspace)}/github/private-prefix`, {
|
|
547
|
+
body: new TextEncoder().encode(JSON.stringify(opts)),
|
|
548
|
+
headers: { "Content-Type": "application/json" },
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
catch {
|
|
552
|
+
return { mode: "plain" };
|
|
553
|
+
}
|
|
554
|
+
})();
|
|
555
|
+
resolveGhPrefixCache.set(cacheKey, resolved);
|
|
556
|
+
return resolved;
|
|
557
|
+
},
|
|
558
|
+
/**
|
|
559
|
+
* Rotate the active private-repo attachment prefix for `opts.repo` +
|
|
560
|
+
* (`opts.branch` or `opts.repoLevel`) (issue #631). Unlike
|
|
561
|
+
* `resolveGhPrefix`, this is NOT fail-open: it's an explicit, caller-
|
|
562
|
+
* initiated action, so a failure (including a 404 from an older/self-
|
|
563
|
+
* hosted server without this route) throws `UploadsError` — the CLI
|
|
564
|
+
* command decides how to present that, rather than this method silently
|
|
565
|
+
* degrading to a shape that would look like success.
|
|
566
|
+
*/
|
|
567
|
+
async rotateGhPrefix(opts) {
|
|
568
|
+
return request("POST", `${config.apiUrl}/v1/workspaces/${encodeURIComponent(config.workspace)}/github/private-prefix/rotate`, {
|
|
569
|
+
body: new TextEncoder().encode(JSON.stringify(opts)),
|
|
570
|
+
headers: { "Content-Type": "application/json" },
|
|
571
|
+
});
|
|
572
|
+
},
|
|
512
573
|
/**
|
|
513
574
|
* Promote a workspace's branch-staged attachments into a PR's stable
|
|
514
575
|
* attachment prefix (server contract, PR #310 — degrade-safe callers
|
|
@@ -2,12 +2,11 @@ import { readFileSync, writeFileSync } from "node:fs";
|
|
|
2
2
|
import { basename } from "node:path";
|
|
3
3
|
import { extractDashValue, flagBool, flagInt, flagString, flagValues, parseCommandArgs, UsageError, } from "../cli-args.js";
|
|
4
4
|
import { writeCommandHelp } from "../cli-style.js";
|
|
5
|
-
import { branchFromFlags, derivedMetaEnabled, frameOptionsFromFlags, ghTargetFromFlags, optimizeOptionsFromFlags, stateAppMetaFromFlags, warnNearMissMeta, syncAttachmentsComment, commentViaSuffix, uploadPreparedImage, resolvePutStagingTarget, putStagingNoteText, resolveStageBindingWarning, mergeStagingMeta, writeReplacedNote, } from "../commands.js";
|
|
5
|
+
import { branchFromFlags, derivedMetaEnabled, frameOptionsFromFlags, ghTargetFromFlags, optimizeOptionsFromFlags, stateAppMetaFromFlags, warnNearMissMeta, syncAttachmentsComment, commentViaSuffix, uploadPreparedImage, resolvePutStagingTarget, putStagingNoteText, resolveStageBindingWarning, mergeStagingMeta, writeReplacedNote, resolveGhPrefixSafe, } from "../commands.js";
|
|
6
6
|
import { resolvePutDefaults } from "../config.js";
|
|
7
7
|
import { loadDefaultsRaw, resolveScreenshotDefaults } from "../config-file.js";
|
|
8
8
|
import { resolvePutPrefix } from "../destinations.js";
|
|
9
9
|
import { execRunner, ghMetadataFromTargetWithTitle, resolveRepo, } from "../github-gh.js";
|
|
10
|
-
import { ghBranchAttachmentKey } from "../github.js";
|
|
11
10
|
import { deriveRepoSlugFromGit } from "../keys.js";
|
|
12
11
|
import { safeCaptureFacts } from "../capture-facts.js";
|
|
13
12
|
import { parseMetaFlags, validateMetaMap } from "../metadata.js";
|
|
@@ -448,15 +447,28 @@ loadAnnotateModule = () => import("../annotate/index.js")) {
|
|
|
448
447
|
}
|
|
449
448
|
const repo = flagString(parsed.flags, "--repo") ?? putDefaults.repo;
|
|
450
449
|
const ref = flagString(parsed.flags, "--ref") ?? putDefaults.ref;
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
450
|
+
// Resolved once (issue #631), only when it's actually needed for the
|
|
451
|
+
// upload about to happen (never for the noUpload/no-target bailouts
|
|
452
|
+
// above) — never per file (screenshot only ever uploads one).
|
|
453
|
+
const ghPrefix = ghTarget
|
|
454
|
+
? await resolveGhPrefixSafe(ctx.client, {
|
|
455
|
+
repo: ghTarget.repo,
|
|
456
|
+
target: { kind: ghTarget.kind, num: ghTarget.num },
|
|
457
|
+
})
|
|
458
|
+
: stagingTarget !== undefined
|
|
459
|
+
? await resolveGhPrefixSafe(ctx.client, {
|
|
460
|
+
repo: stagingTarget.repo,
|
|
461
|
+
branch: stagingTarget.branch,
|
|
462
|
+
})
|
|
463
|
+
: undefined;
|
|
454
464
|
const alt = altFlag ?? basename(captured.filename);
|
|
455
465
|
const { result, prepared, markdown } = await uploadPreparedImage(ctx.client, finalPng, captured.filename, {
|
|
456
466
|
frame: frameOpts,
|
|
457
467
|
optimize: optimizeOpts,
|
|
458
468
|
ghTarget,
|
|
459
|
-
|
|
469
|
+
ghBranchTarget: stagingTarget,
|
|
470
|
+
ghPrefix,
|
|
471
|
+
key: keyHint,
|
|
460
472
|
prefix: resolvedPrefix ?? putDefaults.prefix,
|
|
461
473
|
repo,
|
|
462
474
|
ref,
|
package/dist/commands.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type PutResult, type UploadsClient } from "./client.js";
|
|
1
|
+
import { type PutResult, type ResolveGhPrefixOptions, type ResolveGhPrefixResult, type UploadsClient } from "./client.js";
|
|
2
2
|
import { type CommandFlags } from "./cli-args.js";
|
|
3
3
|
import { type ResolvedConfig } from "./config.js";
|
|
4
4
|
import { type GhTarget } from "./github.js";
|
|
@@ -11,6 +11,42 @@ import type { DetectRoots } from "./screenshot-local.js";
|
|
|
11
11
|
export declare const UPLOAD_BATCH_CONCURRENCY = 8;
|
|
12
12
|
/** @deprecated Use UPLOAD_BATCH_CONCURRENCY. */
|
|
13
13
|
export declare const ATTACH_CONCURRENCY = 8;
|
|
14
|
+
/**
|
|
15
|
+
* Fail-open wrapper around `client.resolveGhPrefix` (issue #631): resolves to
|
|
16
|
+
* `{ mode: "plain" }` on ANY failure — an HTTP/network error (already handled
|
|
17
|
+
* inside `resolveGhPrefix` itself), or a self-hosted/older server or test
|
|
18
|
+
* double that lacks the method entirely (the outer try/catch here). Never
|
|
19
|
+
* blocks an upload or a read-back, and never logs — this is not an error.
|
|
20
|
+
* Call once per command invocation and thread the resolved mode through
|
|
21
|
+
* (uploadPuts/uploadAttachments/uploadBranchAttachments each do this once
|
|
22
|
+
* internally, ahead of their per-file loop); `resolveGhPrefix` itself also
|
|
23
|
+
* caches per-process by repo+branch+target, so repeat callers in the same
|
|
24
|
+
* process (e.g. attach's promote + comment-sync + upload, all for the same
|
|
25
|
+
* target) cost one request total.
|
|
26
|
+
*/
|
|
27
|
+
export declare function resolveGhPrefixSafe(client: UploadsClient, opts: ResolveGhPrefixOptions): Promise<ResolveGhPrefixResult>;
|
|
28
|
+
/**
|
|
29
|
+
* The list of prefixes to fan a multi-prefix list/gather across (issue
|
|
30
|
+
* #631): the plain prefix plus every active private prefix, if any — a
|
|
31
|
+
* repo's history can be split across the plain shape and MULTIPLE private
|
|
32
|
+
* prefixes (e.g. a prefix rotation, or the repo went private after some
|
|
33
|
+
* files were uploaded), not just the currently-resolved one. Falls back to
|
|
34
|
+
* `[prefixId]` when the server omits `activePrefixIds` (optional field — an
|
|
35
|
+
* older/self-hosted worker), so a private repo is never listed as zero
|
|
36
|
+
* private prefixes. Collapses to `[plainPrefix]` in plain mode, so callers
|
|
37
|
+
* that special-case a single-prefix array stay byte-identical to pre-#631.
|
|
38
|
+
*/
|
|
39
|
+
export declare function ghListPrefixes(plainPrefix: string, ghPrefix: ResolveGhPrefixResult, privatePrefixFor: (prefixId: string) => string): string[];
|
|
40
|
+
/**
|
|
41
|
+
* Merge-list helper for a multi-prefix fan-out: runs `fetchItems` per prefix
|
|
42
|
+
* concurrently and concatenates in prefix order. Encodes the first-prefix-
|
|
43
|
+
* only cursor rule once — a cursor is opaque and scoped to the prefix it was
|
|
44
|
+
* minted against, so a multi-prefix merge only ever hands it to the FIRST
|
|
45
|
+
* prefix; every other prefix always starts from its own beginning (undefined
|
|
46
|
+
* cursor), or a cursor minted for one prefix's keyspace would get replayed
|
|
47
|
+
* against a different one.
|
|
48
|
+
*/
|
|
49
|
+
export declare function ghMergedList<T>(prefixes: readonly string[], cursor: string | undefined, fetchItems: (prefix: string, cursor: string | undefined) => Promise<T[]>): Promise<T[]>;
|
|
14
50
|
export { formatUsageHuman } from "./format-usage.js";
|
|
15
51
|
export interface CliContext {
|
|
16
52
|
config: ResolvedConfig;
|
|
@@ -90,6 +126,14 @@ export interface UploadPreparedImageOptions {
|
|
|
90
126
|
* --branch` for the same filename via `ghBranchAttachmentKey`.
|
|
91
127
|
*/
|
|
92
128
|
ghBranchTarget?: BranchTarget;
|
|
129
|
+
/**
|
|
130
|
+
* Resolved GitHub-key mode (issue #631), from a single upstream
|
|
131
|
+
* `resolveGhPrefixSafe` call — never resolved here. Passed straight to
|
|
132
|
+
* `ghAttachmentKeyForMode`/`ghBranchAttachmentKeyForMode`, which own the
|
|
133
|
+
* plain-vs-private branch. Ignored when neither `ghTarget` nor
|
|
134
|
+
* `ghBranchTarget` is set.
|
|
135
|
+
*/
|
|
136
|
+
ghPrefix?: ResolveGhPrefixResult;
|
|
93
137
|
key?: string;
|
|
94
138
|
prefix?: string;
|
|
95
139
|
repo?: string;
|
package/dist/commands.js
CHANGED
|
@@ -13,7 +13,7 @@ import { imageFactsFromBytes } from "./image-facts.js";
|
|
|
13
13
|
import { parseMetaFlags, validateMetaMap } from "./metadata.js";
|
|
14
14
|
import { mergeDerivedMeta, nearMissMetaWarnings, validateStateValue } from "./metadata-vocab.js";
|
|
15
15
|
import { mergeSidecarMeta } from "./sidecar.js";
|
|
16
|
-
import {
|
|
16
|
+
import { ghAttachmentKeyForMode, ghBranchAttachmentKeyForMode, ghBranchKeyPrefix, ghKeyPrefix, ghPrivateKeyPrefix, ghPrivateBranchKeyPrefix, ghMetadataFromTarget, parseGhKey, parseGhPrivateKey, ghMetadataForBranch, attachmentsCommentBody, attachmentsMarker, AUTO_RENDER_OPTIONS, GH_FALLBACK_AUTHOR_NOTE, normalizeGithubCoordinate, } from "./github.js";
|
|
17
17
|
import { resolveRepo, resolveCurrentPullRequest, resolveCurrentBranch, resolveDefaultBranch, classifyGhNumber, execRunner, timedExecRunner, ghMetadataFromTargetWithTitle, upsertAttachmentsComment, } from "./github-gh.js";
|
|
18
18
|
import { deriveRepoFromGit, deriveRepoSlugFromGit } from "./keys.js";
|
|
19
19
|
import { resolvePutPrefix } from "./destinations.js";
|
|
@@ -28,6 +28,56 @@ import { colorEnabled, writeCommandHelp } from "./cli-style.js";
|
|
|
28
28
|
export const UPLOAD_BATCH_CONCURRENCY = 8;
|
|
29
29
|
/** @deprecated Use UPLOAD_BATCH_CONCURRENCY. */
|
|
30
30
|
export const ATTACH_CONCURRENCY = UPLOAD_BATCH_CONCURRENCY;
|
|
31
|
+
/**
|
|
32
|
+
* Fail-open wrapper around `client.resolveGhPrefix` (issue #631): resolves to
|
|
33
|
+
* `{ mode: "plain" }` on ANY failure — an HTTP/network error (already handled
|
|
34
|
+
* inside `resolveGhPrefix` itself), or a self-hosted/older server or test
|
|
35
|
+
* double that lacks the method entirely (the outer try/catch here). Never
|
|
36
|
+
* blocks an upload or a read-back, and never logs — this is not an error.
|
|
37
|
+
* Call once per command invocation and thread the resolved mode through
|
|
38
|
+
* (uploadPuts/uploadAttachments/uploadBranchAttachments each do this once
|
|
39
|
+
* internally, ahead of their per-file loop); `resolveGhPrefix` itself also
|
|
40
|
+
* caches per-process by repo+branch+target, so repeat callers in the same
|
|
41
|
+
* process (e.g. attach's promote + comment-sync + upload, all for the same
|
|
42
|
+
* target) cost one request total.
|
|
43
|
+
*/
|
|
44
|
+
export async function resolveGhPrefixSafe(client, opts) {
|
|
45
|
+
try {
|
|
46
|
+
return await client.resolveGhPrefix(opts);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return { mode: "plain" };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* The list of prefixes to fan a multi-prefix list/gather across (issue
|
|
54
|
+
* #631): the plain prefix plus every active private prefix, if any — a
|
|
55
|
+
* repo's history can be split across the plain shape and MULTIPLE private
|
|
56
|
+
* prefixes (e.g. a prefix rotation, or the repo went private after some
|
|
57
|
+
* files were uploaded), not just the currently-resolved one. Falls back to
|
|
58
|
+
* `[prefixId]` when the server omits `activePrefixIds` (optional field — an
|
|
59
|
+
* older/self-hosted worker), so a private repo is never listed as zero
|
|
60
|
+
* private prefixes. Collapses to `[plainPrefix]` in plain mode, so callers
|
|
61
|
+
* that special-case a single-prefix array stay byte-identical to pre-#631.
|
|
62
|
+
*/
|
|
63
|
+
export function ghListPrefixes(plainPrefix, ghPrefix, privatePrefixFor) {
|
|
64
|
+
if (ghPrefix.mode !== "private")
|
|
65
|
+
return [plainPrefix];
|
|
66
|
+
return [plainPrefix, ...(ghPrefix.activePrefixIds ?? [ghPrefix.prefixId]).map(privatePrefixFor)];
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Merge-list helper for a multi-prefix fan-out: runs `fetchItems` per prefix
|
|
70
|
+
* concurrently and concatenates in prefix order. Encodes the first-prefix-
|
|
71
|
+
* only cursor rule once — a cursor is opaque and scoped to the prefix it was
|
|
72
|
+
* minted against, so a multi-prefix merge only ever hands it to the FIRST
|
|
73
|
+
* prefix; every other prefix always starts from its own beginning (undefined
|
|
74
|
+
* cursor), or a cursor minted for one prefix's keyspace would get replayed
|
|
75
|
+
* against a different one.
|
|
76
|
+
*/
|
|
77
|
+
export async function ghMergedList(prefixes, cursor, fetchItems) {
|
|
78
|
+
const pages = await Promise.all(prefixes.map((prefix, i) => fetchItems(prefix, i === 0 ? cursor : undefined)));
|
|
79
|
+
return pages.flat();
|
|
80
|
+
}
|
|
31
81
|
export { formatUsageHuman } from "./format-usage.js";
|
|
32
82
|
/** Read a local file (or `-` for stdin). Missing path → FILE_NOT_FOUND (exit 2). */
|
|
33
83
|
export function readFileArg(fileArg) {
|
|
@@ -385,10 +435,11 @@ export async function uploadPreparedImage(client, bytes, sourceName, opts) {
|
|
|
385
435
|
frameFit: opts.frame.frameFit,
|
|
386
436
|
optimize: opts.optimize,
|
|
387
437
|
});
|
|
438
|
+
const ghMode = opts.ghPrefix ?? { mode: "plain" };
|
|
388
439
|
let key = opts.ghTarget
|
|
389
|
-
?
|
|
440
|
+
? ghAttachmentKeyForMode(ghMode, opts.ghTarget, prepared.filename)
|
|
390
441
|
: opts.ghBranchTarget
|
|
391
|
-
?
|
|
442
|
+
? ghBranchAttachmentKeyForMode(ghMode, opts.ghBranchTarget.repo, opts.ghBranchTarget.branch, prepared.filename)
|
|
392
443
|
: opts.key;
|
|
393
444
|
if (key && prepared.optimized)
|
|
394
445
|
key = rewriteKeyExtension(key, prepared.filename);
|
|
@@ -510,9 +561,21 @@ export async function syncAttachmentsComment(client, target, run, workspace, opt
|
|
|
510
561
|
// always links to the file page and always shows metadata here, matching the
|
|
511
562
|
// defaults. This only diverges from the bot-posted comment for a workspace
|
|
512
563
|
// that both sets one of those flags false and falls through to this path.
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
564
|
+
//
|
|
565
|
+
// Also list every active private prefix for this repo (issue #631): a
|
|
566
|
+
// private repo's attachments can live under a randomized prefix instead of
|
|
567
|
+
// the plain one. `resolveGhPrefixSafe` is fail-open (any resolve failure,
|
|
568
|
+
// including a client that lacks the method entirely) — plain-only listing,
|
|
569
|
+
// silently, matching pre-#631 behavior exactly.
|
|
570
|
+
const ghPrefix = await resolveGhPrefixSafe(client, {
|
|
571
|
+
repo: target.repo,
|
|
572
|
+
target: { kind: target.kind, num: target.num },
|
|
573
|
+
});
|
|
574
|
+
const prefixes = ghListPrefixes(ghKeyPrefix(target), ghPrefix, (id) => ghPrivateKeyPrefix(id, target));
|
|
575
|
+
const items = await ghMergedList(prefixes, undefined, async (prefix) => (await client.listAll({ prefix, metadata: true })).map(({ key, url, embedUrl, pageUrl, metadata }) => {
|
|
576
|
+
// The list endpoint returns every metadata key; the comment
|
|
577
|
+
// renders only these two. Narrowing here keeps both render paths
|
|
578
|
+
// byte-identical.
|
|
516
579
|
const path = metadata?.path;
|
|
517
580
|
const state = metadata?.state;
|
|
518
581
|
return {
|
|
@@ -524,7 +587,7 @@ export async function syncAttachmentsComment(client, target, run, workspace, opt
|
|
|
524
587
|
? { meta: { ...(path ? { path } : {}), ...(state ? { state } : {}) } }
|
|
525
588
|
: {}),
|
|
526
589
|
};
|
|
527
|
-
});
|
|
590
|
+
}));
|
|
528
591
|
const galleries = [];
|
|
529
592
|
let cursor;
|
|
530
593
|
do {
|
|
@@ -791,9 +854,14 @@ async function uploadAttachmentBatch(opts) {
|
|
|
791
854
|
* original cause of the first failure — for rethrowing single-file CLI paths.
|
|
792
855
|
*/
|
|
793
856
|
export async function uploadAttachments(opts) {
|
|
857
|
+
// Resolved once for the whole batch (issue #631) — never per file.
|
|
858
|
+
const ghPrefix = await resolveGhPrefixSafe(opts.client, {
|
|
859
|
+
repo: opts.target.repo,
|
|
860
|
+
target: { kind: opts.target.kind, num: opts.target.num },
|
|
861
|
+
});
|
|
794
862
|
return uploadAttachmentBatch({
|
|
795
863
|
...opts,
|
|
796
|
-
keyFor: (filename) =>
|
|
864
|
+
keyFor: (filename) => ghAttachmentKeyForMode(ghPrefix, opts.target, filename),
|
|
797
865
|
});
|
|
798
866
|
}
|
|
799
867
|
/**
|
|
@@ -804,9 +872,14 @@ export async function uploadAttachments(opts) {
|
|
|
804
872
|
* `syncAttachmentsComment` for a branch target.
|
|
805
873
|
*/
|
|
806
874
|
export async function uploadBranchAttachments(opts) {
|
|
875
|
+
// Resolved once for the whole batch (issue #631) — never per file.
|
|
876
|
+
const ghPrefix = await resolveGhPrefixSafe(opts.client, {
|
|
877
|
+
repo: opts.target.repo,
|
|
878
|
+
branch: opts.target.branch,
|
|
879
|
+
});
|
|
807
880
|
return uploadAttachmentBatch({
|
|
808
881
|
...opts,
|
|
809
|
-
keyFor: (filename) =>
|
|
882
|
+
keyFor: (filename) => ghBranchAttachmentKeyForMode(ghPrefix, opts.target.repo, opts.target.branch, filename),
|
|
810
883
|
});
|
|
811
884
|
}
|
|
812
885
|
function errorDetail(err) {
|
|
@@ -828,6 +901,18 @@ export async function uploadPuts(opts) {
|
|
|
828
901
|
if (opts.files.length > 1 && opts.nameOverride) {
|
|
829
902
|
throw new UsageError("--name cannot be combined with multiple files");
|
|
830
903
|
}
|
|
904
|
+
// Resolved once for the whole batch (issue #631) — never per file.
|
|
905
|
+
const ghPrefix = opts.ghTarget
|
|
906
|
+
? await resolveGhPrefixSafe(opts.client, {
|
|
907
|
+
repo: opts.ghTarget.repo,
|
|
908
|
+
target: { kind: opts.ghTarget.kind, num: opts.ghTarget.num },
|
|
909
|
+
})
|
|
910
|
+
: opts.ghBranchTarget
|
|
911
|
+
? await resolveGhPrefixSafe(opts.client, {
|
|
912
|
+
repo: opts.ghBranchTarget.repo,
|
|
913
|
+
branch: opts.ghBranchTarget.branch,
|
|
914
|
+
})
|
|
915
|
+
: undefined;
|
|
831
916
|
const slots = await mapBounded(opts.files, opts.concurrency ?? UPLOAD_BATCH_CONCURRENCY, async (file) => {
|
|
832
917
|
try {
|
|
833
918
|
const sourceName = opts.nameOverride ??
|
|
@@ -845,6 +930,7 @@ export async function uploadPuts(opts) {
|
|
|
845
930
|
optimize: opts.optimize,
|
|
846
931
|
ghTarget: opts.ghTarget,
|
|
847
932
|
ghBranchTarget: opts.ghBranchTarget,
|
|
933
|
+
ghPrefix,
|
|
848
934
|
key: opts.explicitKey,
|
|
849
935
|
prefix: opts.prefix,
|
|
850
936
|
repo: opts.repo,
|
|
@@ -1447,18 +1533,28 @@ async function resolveStagedBinding(client, repo) {
|
|
|
1447
1533
|
*/
|
|
1448
1534
|
export async function resolveStaged(opts) {
|
|
1449
1535
|
const { client, repo, branch } = opts;
|
|
1450
|
-
const
|
|
1451
|
-
|
|
1452
|
-
|
|
1536
|
+
const plainPrefix = ghBranchKeyPrefix(repo, branch);
|
|
1537
|
+
// Also list every active private prefix, if any (issue #631) — mirrors
|
|
1538
|
+
// syncAttachmentsComment's gh-fallback gather above: a repo's staged
|
|
1539
|
+
// history can be split across the plain shape and MULTIPLE private
|
|
1540
|
+
// prefixes (e.g. a prefix rotation, or the repo went private after some
|
|
1541
|
+
// files were staged), not just the currently-resolved one. Fail-open: any
|
|
1542
|
+
// resolve failure degrades to plain-only, byte-identical to pre-#631.
|
|
1543
|
+
const ghPrefix = await resolveGhPrefixSafe(client, { repo, branch });
|
|
1544
|
+
const prefixes = ghListPrefixes(plainPrefix, ghPrefix, (id) => ghPrivateBranchKeyPrefix(id));
|
|
1545
|
+
const [files, binding] = await Promise.all([
|
|
1546
|
+
ghMergedList(prefixes, undefined, async (prefix) => {
|
|
1547
|
+
const list = await client.list({ prefix, metadata: true });
|
|
1548
|
+
return list.items.map((item) => ({
|
|
1549
|
+
key: item.key,
|
|
1550
|
+
filename: item.key.slice(prefix.length),
|
|
1551
|
+
size: item.size,
|
|
1552
|
+
stagedAt: item.metadata?.["gh.staged-at"],
|
|
1553
|
+
url: item.url,
|
|
1554
|
+
}));
|
|
1555
|
+
}),
|
|
1453
1556
|
resolveStagedBinding(client, repo),
|
|
1454
1557
|
]);
|
|
1455
|
-
const files = list.items.map((item) => ({
|
|
1456
|
-
key: item.key,
|
|
1457
|
-
filename: item.key.slice(prefix.length),
|
|
1458
|
-
size: item.size,
|
|
1459
|
-
stagedAt: item.metadata?.["gh.staged-at"],
|
|
1460
|
-
url: item.url,
|
|
1461
|
-
}));
|
|
1462
1558
|
return { repo, branch, files, binding };
|
|
1463
1559
|
}
|
|
1464
1560
|
const STAGED_HELP = `uploads staged [--branch <name>] [--repo <owner/name>] [--format json] [--workspace <name>]
|
|
@@ -2236,16 +2332,31 @@ export async function runList(ctx, args, help = false, run = execRunner) {
|
|
|
2236
2332
|
const prefixFlag = flagString(parsed.flags, "--prefix");
|
|
2237
2333
|
let prefix = prefixFlag ?? (defaults.prefix ? `${defaults.prefix}/` : undefined);
|
|
2238
2334
|
const ghTarget = ghTargetFromFlags(parsed.flags, run);
|
|
2335
|
+
// Also list every active private prefix, if any (issue #631) — mirrors
|
|
2336
|
+
// syncAttachmentsComment's gh-fallback gather: a repo's attachment
|
|
2337
|
+
// history can be split across the plain shape and MULTIPLE private
|
|
2338
|
+
// prefixes, not just the currently-resolved one. `prefixes` stays
|
|
2339
|
+
// undefined outside --pr/--issue (unchanged behavior); when defined it
|
|
2340
|
+
// collapses to just `[prefix]` in plain mode, so the single-request path
|
|
2341
|
+
// below is byte-identical to pre-#631 output there.
|
|
2342
|
+
let prefixes;
|
|
2239
2343
|
if (ghTarget) {
|
|
2240
2344
|
if (prefixFlag)
|
|
2241
2345
|
throw new UsageError("--prefix cannot be combined with --pr/--issue");
|
|
2242
2346
|
prefix = ghKeyPrefix(ghTarget);
|
|
2347
|
+
const ghPrefix = await resolveGhPrefixSafe(ctx.client, {
|
|
2348
|
+
repo: ghTarget.repo,
|
|
2349
|
+
target: { kind: ghTarget.kind, num: ghTarget.num },
|
|
2350
|
+
});
|
|
2351
|
+
prefixes = ghListPrefixes(prefix, ghPrefix, (id) => ghPrivateKeyPrefix(id, ghTarget));
|
|
2243
2352
|
}
|
|
2244
2353
|
const limit = flagInt(parsed.flags, "--limit", "--limit");
|
|
2245
2354
|
const cursor = flagString(parsed.flags, "--cursor");
|
|
2246
2355
|
if (flagBool(parsed.flags, "--all")) {
|
|
2247
2356
|
// --all may start from a caller-provided --cursor and drains from there.
|
|
2248
|
-
const items =
|
|
2357
|
+
const items = prefixes && prefixes.length > 1
|
|
2358
|
+
? await ghMergedList(prefixes, cursor, (p, c) => ctx.client.listAll({ prefix: p, limit, cursor: c }))
|
|
2359
|
+
: await ctx.client.listAll({ prefix, limit, cursor });
|
|
2249
2360
|
if (ctx.json)
|
|
2250
2361
|
await writeJson({ items, cursor: null });
|
|
2251
2362
|
else
|
|
@@ -2253,7 +2364,16 @@ export async function runList(ctx, args, help = false, run = execRunner) {
|
|
|
2253
2364
|
await writeStdout(`${item.key}${item.url ? ` ${item.url}` : ""}\n`);
|
|
2254
2365
|
return 0;
|
|
2255
2366
|
}
|
|
2256
|
-
|
|
2367
|
+
// Merged multi-prefix pages don't have a meaningful combined cursor —
|
|
2368
|
+
// dropped (null) only when there's more than one prefix to merge; the
|
|
2369
|
+
// single-prefix path (every non-private call, plus every call before
|
|
2370
|
+
// #631) is untouched. Same first-prefix-only cursor guard as --all above.
|
|
2371
|
+
const result = prefixes && prefixes.length > 1
|
|
2372
|
+
? {
|
|
2373
|
+
items: await ghMergedList(prefixes, cursor, async (p, c) => (await ctx.client.list({ prefix: p, limit, cursor: c })).items),
|
|
2374
|
+
cursor: null,
|
|
2375
|
+
}
|
|
2376
|
+
: await ctx.client.list({ prefix, limit, cursor });
|
|
2257
2377
|
if (ctx.json)
|
|
2258
2378
|
await writeJson(result);
|
|
2259
2379
|
else {
|
|
@@ -2451,10 +2571,37 @@ const COMMENT_RENDERED_META_KEYS = ["path", "state"];
|
|
|
2451
2571
|
* metadata tweak, not an explicit comment command); any failure degrades to
|
|
2452
2572
|
* a stderr hint instead of failing the metadata write that already landed.
|
|
2453
2573
|
*/
|
|
2574
|
+
/**
|
|
2575
|
+
* Resolve the `{repo, kind, num}` target for a key, whether plain
|
|
2576
|
+
* (`parseGhKey`) or private-prefixed (issue #631, `parseGhPrivateKey` —
|
|
2577
|
+
* cannot recover the repo from the key alone, since the randomized prefix
|
|
2578
|
+
* deliberately omits it). For a private key, reads `gh.repo` metadata — the
|
|
2579
|
+
* attach/put that created this key already wrote it — via the same metadata
|
|
2580
|
+
* client call `meta get` uses. Fail-open: any read failure, or a key that
|
|
2581
|
+
* isn't gh-managed at all, resolves to undefined (nothing to resync).
|
|
2582
|
+
*/
|
|
2583
|
+
async function resolveGhTargetForResync(client, key) {
|
|
2584
|
+
const plain = parseGhKey(key);
|
|
2585
|
+
if (plain)
|
|
2586
|
+
return plain;
|
|
2587
|
+
const priv = parseGhPrivateKey(key);
|
|
2588
|
+
if (!priv)
|
|
2589
|
+
return undefined;
|
|
2590
|
+
try {
|
|
2591
|
+
const { metadata } = await client.getMetadata(key);
|
|
2592
|
+
const repo = metadata["gh.repo"];
|
|
2593
|
+
if (!repo)
|
|
2594
|
+
return undefined;
|
|
2595
|
+
return { repo, kind: priv.kind, num: priv.num };
|
|
2596
|
+
}
|
|
2597
|
+
catch {
|
|
2598
|
+
return undefined;
|
|
2599
|
+
}
|
|
2600
|
+
}
|
|
2454
2601
|
async function resyncCommentAfterMetaSet(ctx, key, touchedKeys) {
|
|
2455
2602
|
if (!touchedKeys.some((k) => COMMENT_RENDERED_META_KEYS.includes(k)))
|
|
2456
2603
|
return;
|
|
2457
|
-
const target =
|
|
2604
|
+
const target = await resolveGhTargetForResync(ctx.client, key);
|
|
2458
2605
|
if (!target)
|
|
2459
2606
|
return;
|
|
2460
2607
|
try {
|
|
@@ -2614,6 +2761,7 @@ export async function runIngest(ctx, args, help = false, run = execRunner) {
|
|
|
2614
2761
|
const GITHUB_HELP = `uploads github link [--repo <owner/name>] [--status] [--workspace <name>]
|
|
2615
2762
|
uploads github unlink [--repo <owner/name>] [--workspace <name>]
|
|
2616
2763
|
uploads github doctor [--workspace <name>]
|
|
2764
|
+
uploads github rotate-prefix [--repo <owner/name>] [--branch <name> | --repo-level] [--workspace <name>]
|
|
2617
2765
|
|
|
2618
2766
|
Claim, inspect, or release this workspace's binding to a GitHub repo (see the
|
|
2619
2767
|
managed attachments comment / webhook auto-promotion, which use this
|
|
@@ -2635,12 +2783,22 @@ subscription is the classic silent failure: the App's ping stays green
|
|
|
2635
2783
|
while webhook auto-promotion and title-cache invalidation quietly do
|
|
2636
2784
|
nothing.
|
|
2637
2785
|
|
|
2786
|
+
\`rotate-prefix\` mints a fresh randomized URL prefix for a private repo's
|
|
2787
|
+
attachments and moves everything under the old one to it, so the old URLs
|
|
2788
|
+
404 at origin immediately (see docs/private-attachments.md). --branch
|
|
2789
|
+
defaults to the current git branch; --repo-level rotates the id shared by
|
|
2790
|
+
issue attachments and ingested assets instead of a branch's id. Rotation
|
|
2791
|
+
is an explicit action — an unauthorized caller gets an error, not a silent
|
|
2792
|
+
no-op.
|
|
2793
|
+
|
|
2638
2794
|
Examples:
|
|
2639
2795
|
uploads github link
|
|
2640
2796
|
uploads github link --repo buildinternet/uploads
|
|
2641
2797
|
uploads github link --status
|
|
2642
2798
|
uploads github unlink --repo buildinternet/uploads
|
|
2643
2799
|
uploads github doctor
|
|
2800
|
+
uploads github rotate-prefix --branch feature-x
|
|
2801
|
+
uploads github rotate-prefix --repo-level
|
|
2644
2802
|
`;
|
|
2645
2803
|
/** Older servers' health payload predates recommendedEvents/missingRecommendedEvents — treat as no recommendations rather than crashing. */
|
|
2646
2804
|
function missingRecommendedEventsOf(result) {
|
|
@@ -2760,6 +2918,34 @@ async function runGithubUnlink(ctx, repo) {
|
|
|
2760
2918
|
: `${repo} was not bound to any workspace — nothing to unlink\n`);
|
|
2761
2919
|
return 0;
|
|
2762
2920
|
}
|
|
2921
|
+
function formatGithubRotatePrefix(repo, branchLabel, result) {
|
|
2922
|
+
if (!result.rotated) {
|
|
2923
|
+
return `nothing to rotate for ${repo} (${branchLabel}): ${result.reason}\n`;
|
|
2924
|
+
}
|
|
2925
|
+
return `rotated ${repo} (${branchLabel}): moved ${result.moved} object${result.moved === 1 ? "" : "s"} to a new prefix (${result.prefixId})\n`;
|
|
2926
|
+
}
|
|
2927
|
+
async function runGithubRotatePrefix(ctx, repo, branch, repoLevel) {
|
|
2928
|
+
let result;
|
|
2929
|
+
try {
|
|
2930
|
+
result = await ctx.client.rotateGhPrefix(repoLevel ? { repo, repoLevel: true } : { repo, branch });
|
|
2931
|
+
}
|
|
2932
|
+
catch (err) {
|
|
2933
|
+
if (err instanceof UploadsError && err.status === 404) {
|
|
2934
|
+
throw new UsageError("server does not support private-prefix rotation yet (404) — upgrade the uploads.sh API/self-hosted worker");
|
|
2935
|
+
}
|
|
2936
|
+
if (err instanceof UploadsError && err.status === 403) {
|
|
2937
|
+
throw new UsageError(`not authorized to rotate ${repo}'s attachment prefix (${err.message})`);
|
|
2938
|
+
}
|
|
2939
|
+
throw err;
|
|
2940
|
+
}
|
|
2941
|
+
if (ctx.json) {
|
|
2942
|
+
await writeJson(result);
|
|
2943
|
+
return result.rotated ? 0 : 1;
|
|
2944
|
+
}
|
|
2945
|
+
const branchLabel = repoLevel ? "repo-level" : (branch ?? "");
|
|
2946
|
+
await writeStdout(formatGithubRotatePrefix(repo, branchLabel, result));
|
|
2947
|
+
return result.rotated ? 0 : 1;
|
|
2948
|
+
}
|
|
2763
2949
|
export async function runGithub(ctx, args, help = false, run = execRunner) {
|
|
2764
2950
|
const parsed = parseCommandArgs(args);
|
|
2765
2951
|
const action = parsed.positionals[0];
|
|
@@ -2767,14 +2953,24 @@ export async function runGithub(ctx, args, help = false, run = execRunner) {
|
|
|
2767
2953
|
writeCommandHelp(GITHUB_HELP);
|
|
2768
2954
|
return help || parsed.help ? 0 : 2;
|
|
2769
2955
|
}
|
|
2770
|
-
if (action !== "link" &&
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2956
|
+
if (action !== "link" &&
|
|
2957
|
+
action !== "unlink" &&
|
|
2958
|
+
action !== "doctor" &&
|
|
2959
|
+
action !== "rotate-prefix") {
|
|
2960
|
+
throw new UsageError(`unknown github subcommand: ${action} (expected link, unlink, doctor, or rotate-prefix)`, { example: "uploads github link" });
|
|
2774
2961
|
}
|
|
2775
2962
|
if (action === "doctor")
|
|
2776
2963
|
return runGithubDoctor(ctx);
|
|
2777
2964
|
const repo = resolveRepo(flagString(parsed.flags, "--repo"), run);
|
|
2965
|
+
if (action === "rotate-prefix") {
|
|
2966
|
+
const repoLevel = flagBool(parsed.flags, "--repo-level");
|
|
2967
|
+
const branchFlag = flagString(parsed.flags, "--branch");
|
|
2968
|
+
if (repoLevel && branchFlag !== undefined) {
|
|
2969
|
+
throw new UsageError("pass either --branch or --repo-level, not both");
|
|
2970
|
+
}
|
|
2971
|
+
const branch = repoLevel ? undefined : (branchFlag ?? resolveCurrentBranch(run));
|
|
2972
|
+
return runGithubRotatePrefix(ctx, repo, branch, repoLevel);
|
|
2973
|
+
}
|
|
2778
2974
|
if (action === "unlink")
|
|
2779
2975
|
return runGithubUnlink(ctx, repo);
|
|
2780
2976
|
const statusOnly = flagBool(parsed.flags, "--status");
|
package/dist/github.d.ts
CHANGED
|
@@ -19,8 +19,47 @@ export declare function normalizeGithubCoordinate(value: string): GithubCoordina
|
|
|
19
19
|
* Inverse of `ghKeyPrefix`: parse the PR/issue coordinate back out of a
|
|
20
20
|
* stable attachment key (`gh/<owner>/<name>/<kind>/<num>/<filename>`), or
|
|
21
21
|
* undefined for any other key shape.
|
|
22
|
+
*
|
|
23
|
+
* A real GitHub owner CAN be named `private`, so the strict private-repo
|
|
24
|
+
* shape (`gh/private/<32-hex-id>/...`, see `parseGhPrivateKey`) is checked
|
|
25
|
+
* first and rejected here — otherwise a private-prefixed key would
|
|
26
|
+
* misparse as an ordinary key with owner "private". The accepted ambiguity:
|
|
27
|
+
* a key whose second segment is NOT 32-lowercase-hex (e.g.
|
|
28
|
+
* `gh/private/realrepo/pull/5/x.png`) still parses here as owner "private",
|
|
29
|
+
* repo "private/realrepo" — that's an ordinary public-repo key for a repo
|
|
30
|
+
* actually named "private", not a private-prefix key.
|
|
22
31
|
*/
|
|
23
32
|
export declare function parseGhKey(key: string): GhTarget | undefined;
|
|
33
|
+
/** Literal root under which every private-repo attachment key lives. */
|
|
34
|
+
export declare const GH_PRIVATE_ROOT = "gh/private/";
|
|
35
|
+
/**
|
|
36
|
+
* Private-repo key prefix: `gh/private/<32-hex-id>/<kind>/<num>/`.
|
|
37
|
+
* Deliberately omits the repo (unlike `ghKeyPrefix`) — the id is a random,
|
|
38
|
+
* unguessable per-repo prefix rather than an owner/name path, so callers
|
|
39
|
+
* that need the repo back must read `gh.repo` metadata (see
|
|
40
|
+
* `parseGhPrivateKey`, which cannot recover it from the key alone).
|
|
41
|
+
*/
|
|
42
|
+
export declare function ghPrivateKeyPrefix(prefixId: string, target: GhTarget): string;
|
|
43
|
+
/** Private-repo attachment key: `ghPrivateKeyPrefix` + the sanitized filename. */
|
|
44
|
+
export declare function ghPrivateAttachmentKey(prefixId: string, target: GhTarget, filename: string): string;
|
|
45
|
+
/**
|
|
46
|
+
* Private-repo branch-staged key prefix: `gh/private/<32-hex-id>/branch/`.
|
|
47
|
+
* Unlike `ghBranchKeyPrefix`, there is deliberately NO branch-name segment —
|
|
48
|
+
* the branch name itself is not embedded in a private-repo key.
|
|
49
|
+
*/
|
|
50
|
+
export declare function ghPrivateBranchKeyPrefix(prefixId: string): string;
|
|
51
|
+
/** Private-repo branch-staged attachment key: `ghPrivateBranchKeyPrefix` + the sanitized filename. */
|
|
52
|
+
export declare function ghPrivateBranchAttachmentKey(prefixId: string, filename: string): string;
|
|
53
|
+
/**
|
|
54
|
+
* Inverse of `ghPrivateKeyPrefix`: parse the prefix id/kind/number back out
|
|
55
|
+
* of a private-repo attachment key, or undefined for any other key shape.
|
|
56
|
+
* Cannot recover the repo — callers that need it read `gh.repo` metadata.
|
|
57
|
+
*/
|
|
58
|
+
export declare function parseGhPrivateKey(key: string): {
|
|
59
|
+
prefixId: string;
|
|
60
|
+
kind: GhTargetKind;
|
|
61
|
+
num: number;
|
|
62
|
+
} | undefined;
|
|
24
63
|
export declare function ghKeyPrefix(target: GhTarget): string;
|
|
25
64
|
/**
|
|
26
65
|
* Stable attachment key: same filename → same key → same public URL, so
|
|
@@ -37,6 +76,29 @@ export declare function ghAttachmentKey(target: GhTarget, filename: string): str
|
|
|
37
76
|
export declare function ghBranchKeyPrefix(repo: string, branch: string): string;
|
|
38
77
|
/** Branch-staged attachment key: `ghBranchKeyPrefix` + the sanitized filename. */
|
|
39
78
|
export declare function ghBranchAttachmentKey(repo: string, branch: string, filename: string): string;
|
|
79
|
+
/**
|
|
80
|
+
* Structural stand-in for `ResolveGhPrefixResult` (defined in client.ts) —
|
|
81
|
+
* kept local so these key builders don't need to import client types just
|
|
82
|
+
* to own the plain-vs-private branch.
|
|
83
|
+
*/
|
|
84
|
+
export type GhKeyMode = {
|
|
85
|
+
mode: "plain";
|
|
86
|
+
} | {
|
|
87
|
+
mode: "private";
|
|
88
|
+
prefixId: string;
|
|
89
|
+
};
|
|
90
|
+
/**
|
|
91
|
+
* Mode-owning attachment key builder: collapses the
|
|
92
|
+
* `mode === "private" ? ghPrivateAttachmentKey(...) : ghAttachmentKey(...)`
|
|
93
|
+
* ternary repeated across call sites into one place.
|
|
94
|
+
*/
|
|
95
|
+
export declare function ghAttachmentKeyForMode(mode: GhKeyMode, target: GhTarget, filename: string): string;
|
|
96
|
+
/**
|
|
97
|
+
* Mode-owning branch-staged attachment key builder. The private form
|
|
98
|
+
* ignores `repo`/`branch` (a private-repo key has no branch-name segment,
|
|
99
|
+
* see `ghPrivateBranchKeyPrefix`) and uses the prefix id instead.
|
|
100
|
+
*/
|
|
101
|
+
export declare function ghBranchAttachmentKeyForMode(mode: GhKeyMode, repo: string, branch: string, filename: string): string;
|
|
40
102
|
/**
|
|
41
103
|
* `gh.*` metadata for a branch-staged attach: `gh.repo`, `gh.kind=branch`,
|
|
42
104
|
* `gh.branch` (lowercased), and `gh.staged-at` (ISO 8601 UTC, no fractional
|
package/dist/github.js
CHANGED
|
@@ -57,14 +57,78 @@ export function normalizeGithubCoordinate(value) {
|
|
|
57
57
|
* Inverse of `ghKeyPrefix`: parse the PR/issue coordinate back out of a
|
|
58
58
|
* stable attachment key (`gh/<owner>/<name>/<kind>/<num>/<filename>`), or
|
|
59
59
|
* undefined for any other key shape.
|
|
60
|
+
*
|
|
61
|
+
* A real GitHub owner CAN be named `private`, so the strict private-repo
|
|
62
|
+
* shape (`gh/private/<32-hex-id>/...`, see `parseGhPrivateKey`) is checked
|
|
63
|
+
* first and rejected here — otherwise a private-prefixed key would
|
|
64
|
+
* misparse as an ordinary key with owner "private". The accepted ambiguity:
|
|
65
|
+
* a key whose second segment is NOT 32-lowercase-hex (e.g.
|
|
66
|
+
* `gh/private/realrepo/pull/5/x.png`) still parses here as owner "private",
|
|
67
|
+
* repo "private/realrepo" — that's an ordinary public-repo key for a repo
|
|
68
|
+
* actually named "private", not a private-prefix key.
|
|
60
69
|
*/
|
|
61
70
|
export function parseGhKey(key) {
|
|
71
|
+
if (parseGhPrivateKey(key))
|
|
72
|
+
return undefined;
|
|
62
73
|
const match = /^gh\/([^/]+)\/([^/]+)\/(pull|issues)\/([1-9][0-9]*)\/./.exec(key);
|
|
63
74
|
if (!match)
|
|
64
75
|
return undefined;
|
|
65
76
|
const [, owner, name, kind, num] = match;
|
|
66
77
|
return { repo: `${owner}/${name}`, kind: kind, num: Number(num) };
|
|
67
78
|
}
|
|
79
|
+
/** Literal root under which every private-repo attachment key lives. */
|
|
80
|
+
export const GH_PRIVATE_ROOT = "gh/private/";
|
|
81
|
+
/** Strict shape for a randomized private-repo prefix id: 32 lowercase hex chars. */
|
|
82
|
+
const PRIVATE_PREFIX_ID_RE = /^[0-9a-f]{32}$/;
|
|
83
|
+
/**
|
|
84
|
+
* Guard every private-key builder against a malformed `prefixId` — a
|
|
85
|
+
* caller bug here must never silently produce a guessable-ish key.
|
|
86
|
+
*/
|
|
87
|
+
function assertPrivatePrefixId(prefixId) {
|
|
88
|
+
if (!PRIVATE_PREFIX_ID_RE.test(prefixId)) {
|
|
89
|
+
throw new Error(`invalid private prefix id: "${prefixId}" must be 32 lowercase hex characters`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Private-repo key prefix: `gh/private/<32-hex-id>/<kind>/<num>/`.
|
|
94
|
+
* Deliberately omits the repo (unlike `ghKeyPrefix`) — the id is a random,
|
|
95
|
+
* unguessable per-repo prefix rather than an owner/name path, so callers
|
|
96
|
+
* that need the repo back must read `gh.repo` metadata (see
|
|
97
|
+
* `parseGhPrivateKey`, which cannot recover it from the key alone).
|
|
98
|
+
*/
|
|
99
|
+
export function ghPrivateKeyPrefix(prefixId, target) {
|
|
100
|
+
assertPrivatePrefixId(prefixId);
|
|
101
|
+
return `${GH_PRIVATE_ROOT}${prefixId}/${target.kind}/${target.num}/`;
|
|
102
|
+
}
|
|
103
|
+
/** Private-repo attachment key: `ghPrivateKeyPrefix` + the sanitized filename. */
|
|
104
|
+
export function ghPrivateAttachmentKey(prefixId, target, filename) {
|
|
105
|
+
return `${ghPrivateKeyPrefix(prefixId, target)}${sanitizeKeySegment(filename)}`;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Private-repo branch-staged key prefix: `gh/private/<32-hex-id>/branch/`.
|
|
109
|
+
* Unlike `ghBranchKeyPrefix`, there is deliberately NO branch-name segment —
|
|
110
|
+
* the branch name itself is not embedded in a private-repo key.
|
|
111
|
+
*/
|
|
112
|
+
export function ghPrivateBranchKeyPrefix(prefixId) {
|
|
113
|
+
assertPrivatePrefixId(prefixId);
|
|
114
|
+
return `${GH_PRIVATE_ROOT}${prefixId}/branch/`;
|
|
115
|
+
}
|
|
116
|
+
/** Private-repo branch-staged attachment key: `ghPrivateBranchKeyPrefix` + the sanitized filename. */
|
|
117
|
+
export function ghPrivateBranchAttachmentKey(prefixId, filename) {
|
|
118
|
+
return `${ghPrivateBranchKeyPrefix(prefixId)}${sanitizeKeySegment(filename)}`;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Inverse of `ghPrivateKeyPrefix`: parse the prefix id/kind/number back out
|
|
122
|
+
* of a private-repo attachment key, or undefined for any other key shape.
|
|
123
|
+
* Cannot recover the repo — callers that need it read `gh.repo` metadata.
|
|
124
|
+
*/
|
|
125
|
+
export function parseGhPrivateKey(key) {
|
|
126
|
+
const match = /^gh\/private\/([0-9a-f]{32})\/(pull|issues)\/([1-9][0-9]*)\/./.exec(key);
|
|
127
|
+
if (!match)
|
|
128
|
+
return undefined;
|
|
129
|
+
const [, prefixId, kind, num] = match;
|
|
130
|
+
return { prefixId, kind: kind, num: Number(num) };
|
|
131
|
+
}
|
|
68
132
|
export function ghKeyPrefix(target) {
|
|
69
133
|
const [owner, name] = target.repo.split("/");
|
|
70
134
|
return `gh/${sanitizeKeySegment(owner)}/${sanitizeKeySegment(name)}/${target.kind}/${target.num}/`;
|
|
@@ -91,6 +155,26 @@ export function ghBranchKeyPrefix(repo, branch) {
|
|
|
91
155
|
export function ghBranchAttachmentKey(repo, branch, filename) {
|
|
92
156
|
return `${ghBranchKeyPrefix(repo, branch)}${sanitizeKeySegment(filename)}`;
|
|
93
157
|
}
|
|
158
|
+
/**
|
|
159
|
+
* Mode-owning attachment key builder: collapses the
|
|
160
|
+
* `mode === "private" ? ghPrivateAttachmentKey(...) : ghAttachmentKey(...)`
|
|
161
|
+
* ternary repeated across call sites into one place.
|
|
162
|
+
*/
|
|
163
|
+
export function ghAttachmentKeyForMode(mode, target, filename) {
|
|
164
|
+
return mode.mode === "private"
|
|
165
|
+
? ghPrivateAttachmentKey(mode.prefixId, target, filename)
|
|
166
|
+
: ghAttachmentKey(target, filename);
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Mode-owning branch-staged attachment key builder. The private form
|
|
170
|
+
* ignores `repo`/`branch` (a private-repo key has no branch-name segment,
|
|
171
|
+
* see `ghPrivateBranchKeyPrefix`) and uses the prefix id instead.
|
|
172
|
+
*/
|
|
173
|
+
export function ghBranchAttachmentKeyForMode(mode, repo, branch, filename) {
|
|
174
|
+
return mode.mode === "private"
|
|
175
|
+
? ghPrivateBranchAttachmentKey(mode.prefixId, filename)
|
|
176
|
+
: ghBranchAttachmentKey(repo, branch, filename);
|
|
177
|
+
}
|
|
94
178
|
/**
|
|
95
179
|
* `gh.*` metadata for a branch-staged attach: `gh.repo`, `gh.kind=branch`,
|
|
96
180
|
* `gh.branch` (lowercased), and `gh.staged-at` (ISO 8601 UTC, no fractional
|
package/dist/index.d.ts
CHANGED
|
@@ -4,10 +4,10 @@ export { sanitizeKeySegment, sha256Short, deriveRepoFromGit, buildScreenshotKey
|
|
|
4
4
|
export { BUILTIN_DESTINATIONS, isBuiltinDestination, keyMatchesDestination, resolveDestinationRoot, resolvePutPrefix, type BuiltinDestinationId, } from "./destinations.js";
|
|
5
5
|
export { DEFAULT_API_URL, DEFAULT_WORKSPACE, UPLOADS_CONFIG_KEYS, defaultConfigPath, resolveConfigPath, loadConfigFile, loadEnvFile, resolveApiUrl, resolveConfig, describeConfigSources, redactToken, writeConfigKeys, configValuesFromClient, putDefaultsToConfigValues, resolvePutDefaults, mergePutDefaults, workspaceFromToken, workspaceMismatch, type UploadsClientConfig, type ResolvedConfig, type WorkspaceSource, type ConfigValueSource, type ConfigSources, type UploadsConfigKey, type UploadsConfigValues, type PutDefaults, } from "./config.js";
|
|
6
6
|
export { UploadsError, type UploadsErrorCode } from "./errors.js";
|
|
7
|
-
export { createUploadsClient, type UploadsClient, type PutOptions, type ProvenanceInput, type ListOptions, type PutResult, type ListItem, type ListResult, type HeadResult, type DeleteResult, type GalleryItem, type Gallery, type GallerySummary, type GalleryListOptions, type GalleryListResult, type CreateGalleryOptions, type AddGalleryItemOptions, type DeleteGalleryOptions, type HealthResult, type UsageResult, type ReconcileResult, type PurgeExpiredResult, type PurgeExpiredResponse, type FindFilesOptions, type FindFilesItem, type FindFilesResult, type MetadataKeysResult, type MetadataValuesResult, type GetMetadataResult, type PatchMetadataOptions, } from "./client.js";
|
|
7
|
+
export { createUploadsClient, type UploadsClient, type PutOptions, type ProvenanceInput, type ListOptions, type PutResult, type ListItem, type ListResult, type HeadResult, type DeleteResult, type GalleryItem, type Gallery, type GallerySummary, type GalleryListOptions, type GalleryListResult, type CreateGalleryOptions, type AddGalleryItemOptions, type DeleteGalleryOptions, type HealthResult, type UsageResult, type ReconcileResult, type PurgeExpiredResult, type PurgeExpiredResponse, type FindFilesOptions, type FindFilesItem, type FindFilesResult, type MetadataKeysResult, type MetadataValuesResult, type GetMetadataResult, type PatchMetadataOptions, type ResolveGhPrefixOptions, type ResolveGhPrefixResult, } from "./client.js";
|
|
8
8
|
export { buildCliProvenance } from "./provenance.js";
|
|
9
9
|
export { META_KEY_RE, META_VALUE_MAX, META_MAX_KEYS, META_MAX_TOTAL_BYTES, validateMetaEntry, parseMetaPair, parseMetaFlags, } from "./metadata.js";
|
|
10
|
-
export { ATTACHMENTS_MARKER, attachmentsCommentBody, GH_FALLBACK_AUTHOR_NOTE, ghAttachmentKey, ghKeyPrefix, ghMetadataFromTarget, ghBranchAttachmentKey, ghBranchKeyPrefix, ghMetadataForBranch, isValidRepo, parseRepoFromRemoteUrl, type AttachmentItem, type GhTarget, type GhTargetKind, } from "./github.js";
|
|
10
|
+
export { ATTACHMENTS_MARKER, attachmentsCommentBody, GH_FALLBACK_AUTHOR_NOTE, ghAttachmentKey, ghKeyPrefix, ghMetadataFromTarget, ghBranchAttachmentKey, ghBranchKeyPrefix, ghMetadataForBranch, isValidRepo, parseRepoFromRemoteUrl, GH_PRIVATE_ROOT, ghPrivateKeyPrefix, ghPrivateAttachmentKey, ghPrivateBranchKeyPrefix, ghPrivateBranchAttachmentKey, ghAttachmentKeyForMode, ghBranchAttachmentKeyForMode, parseGhKey, parseGhPrivateKey, type AttachmentItem, type GhTarget, type GhTargetKind, type GhKeyMode, } from "./github.js";
|
|
11
11
|
export { DEFAULT_OPTIMIZE_MAX_EDGE, DEFAULT_OPTIMIZE_QUALITY, optimizeImageForUpload, rewriteKeyExtension, withImageExtension, type OptimizeImageOptions, type OptimizeImageResult, type OptimizeOutputFormat, } from "./optimize.js";
|
|
12
12
|
export { FRAME_PRESETS, applyFrame, listFramePresets, resolveFrameId, type FrameFit, type FrameOptions, type FrameResult, } from "./frame.js";
|
|
13
13
|
export { execRunner, resolveRepo, upsertAttachmentsComment, type CommandRunner, } from "./github-gh.js";
|
package/dist/index.js
CHANGED
|
@@ -7,7 +7,11 @@ export { UploadsError } from "./errors.js";
|
|
|
7
7
|
export { createUploadsClient, } from "./client.js";
|
|
8
8
|
export { buildCliProvenance } from "./provenance.js";
|
|
9
9
|
export { META_KEY_RE, META_VALUE_MAX, META_MAX_KEYS, META_MAX_TOTAL_BYTES, validateMetaEntry, parseMetaPair, parseMetaFlags, } from "./metadata.js";
|
|
10
|
-
export { ATTACHMENTS_MARKER, attachmentsCommentBody, GH_FALLBACK_AUTHOR_NOTE, ghAttachmentKey, ghKeyPrefix, ghMetadataFromTarget, ghBranchAttachmentKey, ghBranchKeyPrefix, ghMetadataForBranch, isValidRepo, parseRepoFromRemoteUrl,
|
|
10
|
+
export { ATTACHMENTS_MARKER, attachmentsCommentBody, GH_FALLBACK_AUTHOR_NOTE, ghAttachmentKey, ghKeyPrefix, ghMetadataFromTarget, ghBranchAttachmentKey, ghBranchKeyPrefix, ghMetadataForBranch, isValidRepo, parseRepoFromRemoteUrl,
|
|
11
|
+
// Private-repo randomized-prefix builders (issue #631) — needed by the
|
|
12
|
+
// hosted MCP (apps/mcp), which builds keys in-process rather than via
|
|
13
|
+
// the CLI's own commands.ts.
|
|
14
|
+
GH_PRIVATE_ROOT, ghPrivateKeyPrefix, ghPrivateAttachmentKey, ghPrivateBranchKeyPrefix, ghPrivateBranchAttachmentKey, ghAttachmentKeyForMode, ghBranchAttachmentKeyForMode, parseGhKey, parseGhPrivateKey, } from "./github.js";
|
|
11
15
|
export { DEFAULT_OPTIMIZE_MAX_EDGE, DEFAULT_OPTIMIZE_QUALITY, optimizeImageForUpload, rewriteKeyExtension, withImageExtension, } from "./optimize.js";
|
|
12
16
|
export { FRAME_PRESETS, applyFrame, listFramePresets, resolveFrameId, } from "./frame.js";
|
|
13
17
|
export { execRunner, resolveRepo, upsertAttachmentsComment, } from "./github-gh.js";
|
package/dist/mcp/tools.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { createUploadsClient } from "../client.js";
|
|
2
|
-
import { buildDoctorReport, makeGhTarget, mergeStagingMeta, resolvePutStagingTarget, resolveStaged, syncAttachmentsComment, uploadAttachments, uploadPreparedImage, uploadPuts, } from "../commands.js";
|
|
2
|
+
import { buildDoctorReport, ghListPrefixes, ghMergedList, makeGhTarget, mergeStagingMeta, resolveGhPrefixSafe, resolvePutStagingTarget, resolveStaged, syncAttachmentsComment, uploadAttachments, uploadPreparedImage, uploadPuts, } from "../commands.js";
|
|
3
3
|
import { resolveFrameId } from "../frame.js";
|
|
4
4
|
import { resolveConfig, resolvePutDefaults, } from "../config.js";
|
|
5
5
|
import { resolvePutPrefix } from "../destinations.js";
|
|
6
|
-
import {
|
|
6
|
+
import { ghKeyPrefix, ghPrivateKeyPrefix } from "../github.js";
|
|
7
7
|
import { safeCaptureFacts } from "../capture-facts.js";
|
|
8
8
|
import { deriveRepoSlugFromGit } from "../keys.js";
|
|
9
9
|
import { validateMetaMap } from "../metadata.js";
|
|
@@ -801,14 +801,26 @@ export function createUploadsMcpTools(opts) {
|
|
|
801
801
|
}
|
|
802
802
|
throw err;
|
|
803
803
|
}
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
804
|
+
// Resolved once (issue #631), only now that upload is actually
|
|
805
|
+
// about to happen — never per file (screenshot uploads exactly one).
|
|
806
|
+
const ghPrefix = target
|
|
807
|
+
? await resolveGhPrefixSafe(client, {
|
|
808
|
+
repo: target.repo,
|
|
809
|
+
target: { kind: target.kind, num: target.num },
|
|
810
|
+
})
|
|
811
|
+
: stagingTarget
|
|
812
|
+
? await resolveGhPrefixSafe(client, {
|
|
813
|
+
repo: stagingTarget.repo,
|
|
814
|
+
branch: stagingTarget.branch,
|
|
815
|
+
})
|
|
816
|
+
: undefined;
|
|
807
817
|
const { result, prepared, markdown } = await uploadPreparedImage(client, captured.png, captured.filename, {
|
|
808
818
|
frame: frameOpts,
|
|
809
819
|
optimize: optimizeOpts,
|
|
810
820
|
ghTarget: target,
|
|
811
|
-
|
|
821
|
+
ghBranchTarget: stagingTarget,
|
|
822
|
+
ghPrefix,
|
|
823
|
+
key: keyArg,
|
|
812
824
|
prefix: resolvedPrefix ?? defaults.prefix,
|
|
813
825
|
repo: optString(args, "repo") ?? defaults.repo,
|
|
814
826
|
ref: refArg ?? defaults.ref,
|
|
@@ -980,16 +992,35 @@ export function createUploadsMcpTools(opts) {
|
|
|
980
992
|
const prefixArg = optString(args, "prefix");
|
|
981
993
|
let prefix = prefixArg ?? (defaults.prefix ? `${defaults.prefix}/` : undefined);
|
|
982
994
|
const target = ghTargetFromArgs(args, run);
|
|
995
|
+
const { client } = clientFor(args);
|
|
996
|
+
// Also list every active private prefix, if any (issue #631) —
|
|
997
|
+
// mirrors syncAttachmentsComment's gh-fallback gather: a repo's
|
|
998
|
+
// attachment history can be split across the plain shape and
|
|
999
|
+
// MULTIPLE private prefixes, not just the currently-resolved one.
|
|
1000
|
+
// `prefixes` stays undefined outside pr/issue (unchanged behavior);
|
|
1001
|
+
// collapses to `[prefix]` in plain mode, so the single-request path
|
|
1002
|
+
// below is byte-identical to pre-#631.
|
|
1003
|
+
let prefixes;
|
|
983
1004
|
if (target) {
|
|
984
1005
|
if (prefixArg)
|
|
985
1006
|
usage("prefix cannot be combined with pr/issue");
|
|
986
1007
|
prefix = ghKeyPrefix(target);
|
|
1008
|
+
const ghPrefix = await resolveGhPrefixSafe(client, {
|
|
1009
|
+
repo: target.repo,
|
|
1010
|
+
target: { kind: target.kind, num: target.num },
|
|
1011
|
+
});
|
|
1012
|
+
prefixes = ghListPrefixes(prefix, ghPrefix, (id) => ghPrivateKeyPrefix(id, target));
|
|
987
1013
|
}
|
|
988
1014
|
const limit = optPosInt(args, "limit");
|
|
989
1015
|
const cursor = optString(args, "cursor");
|
|
990
|
-
const { client } = clientFor(args);
|
|
991
1016
|
if (optBool(args, "all")) {
|
|
992
|
-
const items =
|
|
1017
|
+
const items = prefixes && prefixes.length > 1
|
|
1018
|
+
? await ghMergedList(prefixes, cursor, (p, c) => client.listAll({ prefix: p, limit, cursor: c }))
|
|
1019
|
+
: await client.listAll({ prefix, limit, cursor });
|
|
1020
|
+
return { items, cursor: null };
|
|
1021
|
+
}
|
|
1022
|
+
if (prefixes && prefixes.length > 1) {
|
|
1023
|
+
const items = await ghMergedList(prefixes, cursor, async (p, c) => (await client.list({ prefix: p, limit, cursor: c })).items);
|
|
993
1024
|
return { items, cursor: null };
|
|
994
1025
|
}
|
|
995
1026
|
return client.list({ prefix, limit, cursor });
|