@buildinternet/uploads 0.13.1 → 0.14.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/client.d.ts CHANGED
@@ -170,6 +170,17 @@ export interface FindGalleriesByReferenceOptions {
170
170
  limit?: number;
171
171
  cursor?: string;
172
172
  }
173
+ /** Reasons the bot did not post; the CLI treats any of them as "fall back to gh". */
174
+ export type GithubCommentDeclineReason = "app_unconfigured" | "not_installed" | "forbidden" | "unavailable";
175
+ export type GithubCommentResult = {
176
+ posted: true;
177
+ action: "created" | "updated" | "skipped";
178
+ count: number;
179
+ commentUrl?: string;
180
+ } | {
181
+ posted: false;
182
+ reason: GithubCommentDeclineReason;
183
+ };
173
184
  export interface HealthResult {
174
185
  ok: boolean;
175
186
  }
@@ -396,6 +407,11 @@ export declare function createUploadsClient(config: UploadsClientConfig): {
396
407
  id: string;
397
408
  }>;
398
409
  findGalleriesByReference(opts: FindGalleriesByReferenceOptions): Promise<GalleryListResult>;
410
+ upsertGithubComment(opts: {
411
+ repo: string;
412
+ num: number;
413
+ kind: "pull" | "issues";
414
+ }): Promise<GithubCommentResult>;
399
415
  health(): Promise<HealthResult>;
400
416
  /** Workspace storage / upload counters (+ limits when configured). */
401
417
  usage(): Promise<UsageResult>;
package/dist/client.js CHANGED
@@ -452,6 +452,12 @@ export function createUploadsClient(config) {
452
452
  params.set("cursor", opts.cursor);
453
453
  return request("GET", galleriesBase(config) + "/by-reference?" + params);
454
454
  },
455
+ async upsertGithubComment(opts) {
456
+ return request("POST", `${config.apiUrl}/v1/${encodeURIComponent(config.workspace)}/github/comment`, {
457
+ body: new TextEncoder().encode(JSON.stringify(opts)),
458
+ headers: { "Content-Type": "application/json" },
459
+ });
460
+ },
455
461
  async health() {
456
462
  return request("GET", `${config.apiUrl}/health`, { auth: false });
457
463
  },
@@ -2,12 +2,11 @@ import { readFileSync, writeFileSync } from "node:fs";
2
2
  import { basename } from "node:path";
3
3
  import { flagBool, flagInt, flagString, flagValues, parseCommandArgs, UsageError, } from "../cli-args.js";
4
4
  import { writeCommandHelp } from "../cli-style.js";
5
- import { frameOptionsFromFlags, ghTargetFromFlags, optimizeOptionsFromFlags, syncAttachmentsComment, uploadPreparedImage, } from "../commands.js";
5
+ import { frameOptionsFromFlags, ghTargetFromFlags, optimizeOptionsFromFlags, syncAttachmentsComment, commentViaSuffix, uploadPreparedImage, } 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
- import { ghMetadataFromTarget } from "../github.js";
10
- import { execRunner } from "../github-gh.js";
9
+ import { execRunner, ghMetadataFromTargetWithTitle } from "../github-gh.js";
11
10
  import { parseMetaFlags, validateMetaMap } from "../metadata.js";
12
11
  import { writeJson, writeStdout } from "../io.js";
13
12
  import { assertHideSelector, captureScreenshot, parseViewport, parseWaitUntil, } from "../screenshot.js";
@@ -67,7 +66,9 @@ Options:
67
66
  --keep-exif Keep EXIF/XMP/ICC when optimizing
68
67
  --pr <num> Attach to a pull request (stable URL, no hash)
69
68
  --issue <num> Attach to an issue
70
- --comment With --pr/--issue: update the managed attachments comment
69
+ --comment With --pr/--issue: update the managed attachments comment.
70
+ Posts as uploads-sh[bot] when the GitHub App is installed;
71
+ otherwise via local gh.
71
72
  --gallery <id> Add the uploaded object to this public gallery
72
73
  --meta <k=v> Queryable custom metadata (repeatable)
73
74
  --workspace, -w <name> Override workspace
@@ -214,7 +215,7 @@ captureImpl = captureScreenshot) {
214
215
  const metaExtras = parseMetaFlags(flagValues(parsed.flags, "--meta"));
215
216
  let metadata = metaExtras;
216
217
  if (ghTarget) {
217
- metadata = { ...metaExtras, ...ghMetadataFromTarget(ghTarget) };
218
+ metadata = { ...metaExtras, ...ghMetadataFromTargetWithTitle(ghTarget, run) };
218
219
  validateMetaMap(metadata);
219
220
  }
220
221
  else if (Object.keys(metaExtras).length > 0) {
@@ -296,7 +297,7 @@ captureImpl = captureScreenshot) {
296
297
  try {
297
298
  comment = await syncAttachmentsComment(ctx.client, ghTarget, run);
298
299
  if (logHuman)
299
- process.stderr.write(`>> attachments comment ${comment.action}\n`);
300
+ process.stderr.write(`>> attachments comment ${comment.action}${commentViaSuffix(comment.via)}\n`);
300
301
  }
301
302
  catch (err) {
302
303
  commentError = err instanceof Error ? err.message : String(err);
@@ -89,13 +89,22 @@ export declare function frameOptionsFromFlags(flags: CommandFlags["flags"]): {
89
89
  };
90
90
  /**
91
91
  * List every attachment under the target's prefix and create/update the
92
- * managed comment. Throws on gh failure callers decide whether that is
93
- * fatal (`comment` command) or a warning (`put --comment`).
92
+ * managed comment. Prefers the server-side bot endpoint (`uploads-sh[bot]`,
93
+ * rendered from this workspace's own data); any failure to post that way —
94
+ * not installed, declined, self-hosted 404, network error — falls through to
95
+ * the local-`gh` path so self-hosters keep working unchanged. Throws on gh
96
+ * failure — callers decide whether that is fatal (`comment` command) or a
97
+ * warning (`put --comment`).
94
98
  */
95
- export declare function syncAttachmentsComment(client: UploadsClient, target: GhTarget, run: CommandRunner): Promise<{
99
+ export interface AttachmentsCommentResult {
96
100
  action: "created" | "updated" | "skipped";
97
101
  count: number;
98
- }>;
102
+ /** Who posted the comment: the GitHub App bot, or the local `gh` fallback. */
103
+ via: "bot" | "gh";
104
+ }
105
+ /** Human-mode suffix noting who posted the managed comment. */
106
+ export declare function commentViaSuffix(via: AttachmentsCommentResult["via"]): string;
107
+ export declare function syncAttachmentsComment(client: UploadsClient, target: GhTarget, run: CommandRunner): Promise<AttachmentsCommentResult>;
99
108
  export type AttachUploadItem = PutResult & {
100
109
  file: string;
101
110
  markdown: string;
package/dist/commands.js CHANGED
@@ -10,7 +10,7 @@ import { UploadsError } from "./errors.js";
10
10
  import { writeJson, writeStdout } from "./io.js";
11
11
  import { parseMetaFlags, validateMetaMap } from "./metadata.js";
12
12
  import { ghAttachmentKey, ghKeyPrefix, ghMetadataFromTarget, attachmentsCommentBody, normalizeGithubCoordinate, } from "./github.js";
13
- import { resolveRepo, resolveCurrentPullRequest, classifyGhNumber, execRunner, upsertAttachmentsComment, } from "./github-gh.js";
13
+ import { resolveRepo, resolveCurrentPullRequest, classifyGhNumber, execRunner, ghMetadataFromTargetWithTitle, upsertAttachmentsComment, } from "./github-gh.js";
14
14
  import { resolvePutPrefix } from "./destinations.js";
15
15
  import { optimizeImageForUpload, rewriteKeyExtension, } from "./optimize.js";
16
16
  import { applyFrame, resolveFrameId } from "./frame.js";
@@ -89,7 +89,9 @@ Options:
89
89
  --format human|url|markdown|json
90
90
  --pr <num> Attach to a pull request: key gh/<owner>/<repo>/pull/<num>/<name> (stable URL, no hash)
91
91
  --issue <num> Attach to an issue: key gh/<owner>/<repo>/issues/<num>/<name>
92
- --comment With --pr/--issue: update one managed comment with attachments and linked galleries via local gh auth
92
+ --comment With --pr/--issue: update one managed comment with
93
+ attachments and linked galleries. Posts as uploads-sh[bot]
94
+ when the GitHub App is installed; otherwise via local gh.
93
95
  --gallery <id> Add the uploaded object(s) to this public gallery
94
96
  --meta <k=v> Queryable custom metadata (repeatable; value may contain "="): key ^[a-z][a-z0-9._-]{0,63}$, value 1-512 printable ASCII, max 24 pairs
95
97
  Re-uploading to an existing key WITH --meta replaces that file's
@@ -272,12 +274,25 @@ export function frameOptionsFromFlags(flags) {
272
274
  throw new UsageError("--frame-url requires --frame");
273
275
  return { frameId, frameUrl, frameFit };
274
276
  }
275
- /**
276
- * List every attachment under the target's prefix and create/update the
277
- * managed comment. Throws on gh failure callers decide whether that is
278
- * fatal (`comment` command) or a warning (`put --comment`).
279
- */
277
+ /** Human-mode suffix noting who posted the managed comment. */
278
+ export function commentViaSuffix(via) {
279
+ return via === "bot" ? " (uploads-sh[bot])" : " (via gh)";
280
+ }
280
281
  export async function syncAttachmentsComment(client, target, run) {
282
+ try {
283
+ const bot = await client.upsertGithubComment({
284
+ repo: target.repo,
285
+ num: target.num,
286
+ kind: target.kind,
287
+ });
288
+ if (bot.posted)
289
+ return { action: bot.action, count: bot.count, via: "bot" };
290
+ }
291
+ catch {
292
+ // Endpoint absent/unreachable (self-hosted, network, older worker) — fall
293
+ // through to the gh path below.
294
+ }
295
+ // gh fallback: gather from this workspace's own data and post via local `gh`.
281
296
  const items = (await client.listAll({ prefix: ghKeyPrefix(target) })).map(({ key, url, embedUrl }) => ({ key, url, embedUrl }));
282
297
  const galleries = [];
283
298
  let cursor;
@@ -313,10 +328,14 @@ export async function syncAttachmentsComment(client, target, run) {
313
328
  }
314
329
  }));
315
330
  if (items.length === 0 && previewGalleries.length === 0)
316
- return { action: "skipped", count: 0 };
331
+ return { action: "skipped", count: 0, via: "gh" };
317
332
  const body = attachmentsCommentBody(items, previewGalleries);
318
333
  const { created } = upsertAttachmentsComment(target, body, run);
319
- return { action: created ? "created" : "updated", count: items.length + previewGalleries.length };
334
+ return {
335
+ action: created ? "created" : "updated",
336
+ count: items.length + previewGalleries.length,
337
+ via: "gh",
338
+ };
320
339
  }
321
340
  // --- attach ---
322
341
  const ATTACH_HELP = `uploads attach <file...> [options]
@@ -532,9 +551,9 @@ export async function runAttach(ctx, args, help = false, run = execRunner) {
532
551
  // target pairs always win over a same-named --meta extra (documented above).
533
552
  // Validate the merged map (not just the extras) so the 24-key/8KB caps are
534
553
  // enforced client-side even when extras alone are under the cap but extras
535
- // + the 4 gh.* pairs push the merged map over it.
554
+ // + the gh.* pairs push the merged map over it.
536
555
  const metaExtras = parseMetaFlags(flagValues(parsed.flags, "--meta"));
537
- const metadata = { ...metaExtras, ...ghMetadataFromTarget(target) };
556
+ const metadata = { ...metaExtras, ...ghMetadataFromTargetWithTitle(target, run) };
538
557
  if (Object.keys(metadata).length > 0)
539
558
  validateMetaMap(metadata);
540
559
  const logHuman = !ctx.quiet && !ctx.json;
@@ -588,7 +607,7 @@ export async function runAttach(ctx, args, help = false, run = execRunner) {
588
607
  process.stderr.write(`warning: could not upload ${failure.file}: ${failure.error.message}\n`);
589
608
  }
590
609
  if (!ctx.quiet && comment)
591
- process.stderr.write(`>> attachments comment ${comment.action}\n`);
610
+ process.stderr.write(`>> attachments comment ${comment.action}${commentViaSuffix(comment.via)}\n`);
592
611
  if (!ctx.quiet && uploads.length > 0) {
593
612
  const ref = ghMetadataFromTarget(target)["gh.ref"];
594
613
  process.stderr.write(`>> find these later: uploads find gh.ref=${ref}\n`);
@@ -711,7 +730,7 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
711
730
  let metadata = userMeta;
712
731
  let attachedRef;
713
732
  if (ghTarget) {
714
- const merged = { ...userMeta, ...ghMetadataFromTarget(ghTarget) };
733
+ const merged = { ...userMeta, ...ghMetadataFromTargetWithTitle(ghTarget, run) };
715
734
  validateMetaMap(merged); // enforce 24-key/8KB caps on the merged map (matches attach)
716
735
  metadata = merged;
717
736
  attachedRef = merged["gh.ref"];
@@ -723,7 +742,7 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
723
742
  if (autoEnabled) {
724
743
  const autoTarget = resolveAutoGhTarget(flagString(parsed.flags, "--repo") ?? defaults.repo, flagString(parsed.flags, "--ref") ?? defaults.ref, run);
725
744
  if (autoTarget) {
726
- const autoMeta = ghMetadataFromTarget(autoTarget);
745
+ const autoMeta = ghMetadataFromTargetWithTitle(autoTarget, run);
727
746
  const merged = { ...autoMeta, ...userMeta };
728
747
  // Auto resolution must never fail the upload: if merging the gh.* pairs
729
748
  // would exceed the metadata caps, drop them and upload with --meta only.
@@ -798,7 +817,7 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
798
817
  try {
799
818
  comment = await syncAttachmentsComment(ctx.client, ghTarget, run);
800
819
  if (logHuman)
801
- process.stderr.write(`>> attachments comment ${comment.action}\n`);
820
+ process.stderr.write(`>> attachments comment ${comment.action}${commentViaSuffix(comment.via)}\n`);
802
821
  }
803
822
  catch (err) {
804
823
  commentError = err instanceof Error ? err.message : String(err);
@@ -1322,7 +1341,8 @@ export async function runDelete(ctx, args, help = false) {
1322
1341
  const COMMENT_HELP = `uploads comment (--pr <num> | --issue <num>) [--repo <owner/name>] [--workspace <name>]
1323
1342
 
1324
1343
  Create or update the managed attachments comment on a GitHub PR or issue,
1325
- listing everything uploaded for it. Uses your local gh auth. Finds its own
1344
+ listing everything uploaded for it. Posts as uploads-sh[bot] when the GitHub
1345
+ App is installed on the repo; otherwise via your local gh auth. Finds its own
1326
1346
  prior comment via a hidden marker and edits it in place; never touches other
1327
1347
  comments or the description.
1328
1348
 
@@ -1344,9 +1364,10 @@ export async function runComment(ctx, args, help = false, run = execRunner) {
1344
1364
  await writeJson({ ...target, ...result });
1345
1365
  }
1346
1366
  else if (!ctx.quiet) {
1367
+ const via = commentViaSuffix(result.via);
1347
1368
  process.stderr.write(result.action === "skipped"
1348
1369
  ? `no attachments under ${ghKeyPrefix(target)} — nothing to do\n`
1349
- : `${result.action} attachments comment on ${target.repo}#${target.num} (${result.count} file${result.count === 1 ? "" : "s"})\n`);
1370
+ : `${result.action} attachments comment on ${target.repo}#${target.num} (${result.count} file${result.count === 1 ? "" : "s"})${via}\n`);
1350
1371
  }
1351
1372
  return 0;
1352
1373
  }
@@ -16,6 +16,25 @@ export declare function resolveCurrentPullRequest(repo: string, run?: CommandRun
16
16
  * uploads without metadata.
17
17
  */
18
18
  export declare function classifyGhNumber(repo: string, num: number, run?: CommandRunner): GhTarget | undefined;
19
+ /**
20
+ * Best-effort PR/issue title lookup via local `gh`. Returns undefined on any
21
+ * failure (gh missing, unauthenticated, network, 404) — mirrors
22
+ * `resolveCurrentPullRequest`/`classifyGhNumber`'s degrade-don't-throw
23
+ * pattern. A title is a nice-to-have annotation, never a blocker: callers
24
+ * must never let this failure abort an upload.
25
+ */
26
+ export declare function resolveGhTitle(target: GhTarget, run?: CommandRunner): string | undefined;
27
+ /**
28
+ * `ghMetadataFromTarget`'s 4 pairs, plus a best-effort `gh.title` (issue #267)
29
+ * when `resolveGhTitle` yields one that also satisfies the metadata-value
30
+ * rule every other pair follows (1-512 printable ASCII — `metadata.ts`'s
31
+ * `META_VALUE_MAX`/`isMetaValueSafe`). Truncated to `META_VALUE_MAX` first;
32
+ * a title left empty or unsafe by truncation (e.g. non-ASCII — real titles
33
+ * often contain emoji or curly quotes) is silently omitted rather than
34
+ * sanitized, matching `resolveGhTitle`'s own "degrade, don't fail the
35
+ * upload" contract.
36
+ */
37
+ export declare function ghMetadataFromTargetWithTitle(target: GhTarget, run?: CommandRunner): Record<string, string>;
19
38
  /**
20
39
  * Create the managed attachments comment, or edit it in place if it already
21
40
  * exists. Never touches any other comment. Body is passed via stdin
package/dist/github-gh.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { execFileSync } from "node:child_process";
2
2
  import { UsageError } from "./cli-args.js";
3
- import { ATTACHMENTS_MARKER, isValidRepo, parseRepoFromRemoteUrl, } from "./github.js";
3
+ import { ATTACHMENTS_MARKER, ghMetadataFromTarget, isValidRepo, parseRepoFromRemoteUrl, } from "./github.js";
4
+ import { META_VALUE_MAX, isMetaValueSafe } from "./metadata.js";
4
5
  export const execRunner = (cmd, args, input) => execFileSync(cmd, args, { encoding: "utf8", input, stdio: ["pipe", "pipe", "pipe"] });
5
6
  /**
6
7
  * Resolve "owner/name". Order: explicit --repo (validated) → `gh repo view`
@@ -91,12 +92,62 @@ export function classifyGhNumber(repo, num, run = execRunner) {
91
92
  }
92
93
  return undefined;
93
94
  }
95
+ /**
96
+ * Best-effort PR/issue title lookup via local `gh`. Returns undefined on any
97
+ * failure (gh missing, unauthenticated, network, 404) — mirrors
98
+ * `resolveCurrentPullRequest`/`classifyGhNumber`'s degrade-don't-throw
99
+ * pattern. A title is a nice-to-have annotation, never a blocker: callers
100
+ * must never let this failure abort an upload.
101
+ */
102
+ export function resolveGhTitle(target, run = execRunner) {
103
+ try {
104
+ const out = run("gh", [
105
+ target.kind === "pull" ? "pr" : "issue",
106
+ "view",
107
+ String(target.num),
108
+ "--repo",
109
+ target.repo,
110
+ "--json",
111
+ "title",
112
+ "--jq",
113
+ ".title",
114
+ ]).trim();
115
+ return out.length > 0 ? out : undefined;
116
+ }
117
+ catch {
118
+ // gh missing / unauthenticated / not found / network — caller skips
119
+ return undefined;
120
+ }
121
+ }
122
+ /**
123
+ * `ghMetadataFromTarget`'s 4 pairs, plus a best-effort `gh.title` (issue #267)
124
+ * when `resolveGhTitle` yields one that also satisfies the metadata-value
125
+ * rule every other pair follows (1-512 printable ASCII — `metadata.ts`'s
126
+ * `META_VALUE_MAX`/`isMetaValueSafe`). Truncated to `META_VALUE_MAX` first;
127
+ * a title left empty or unsafe by truncation (e.g. non-ASCII — real titles
128
+ * often contain emoji or curly quotes) is silently omitted rather than
129
+ * sanitized, matching `resolveGhTitle`'s own "degrade, don't fail the
130
+ * upload" contract.
131
+ */
132
+ export function ghMetadataFromTargetWithTitle(target, run = execRunner) {
133
+ const base = ghMetadataFromTarget(target);
134
+ const title = resolveGhTitle(target, run);
135
+ if (title === undefined)
136
+ return base;
137
+ const truncated = title.length > META_VALUE_MAX ? title.slice(0, META_VALUE_MAX) : title;
138
+ return isMetaValueSafe(truncated) ? { ...base, "gh.title": truncated } : base;
139
+ }
94
140
  /**
95
141
  * PR comments live on the issues endpoint, so one path covers PRs and issues.
96
- * Only the first 100 comments are searched (accepted v1 limitation).
142
+ * `--paginate` follows Link headers and merges every page into one array, so the
143
+ * marker comment is found even on threads past 100 comments.
97
144
  */
98
145
  function findManagedComment(target, run) {
99
- const raw = run("gh", ["api", `repos/${target.repo}/issues/${target.num}/comments?per_page=100`]);
146
+ const raw = run("gh", [
147
+ "api",
148
+ `repos/${target.repo}/issues/${target.num}/comments?per_page=100`,
149
+ "--paginate",
150
+ ]);
100
151
  const comments = JSON.parse(raw);
101
152
  return comments.find((c) => typeof c.body === "string" && c.body.includes(ATTACHMENTS_MARKER));
102
153
  }
package/dist/mcp/tools.js CHANGED
@@ -3,9 +3,9 @@ import { buildDoctorReport, makeGhTarget, syncAttachmentsComment, uploadAttachme
3
3
  import { resolveFrameId } from "../frame.js";
4
4
  import { resolveConfig, resolvePutDefaults, } from "../config.js";
5
5
  import { resolvePutPrefix } from "../destinations.js";
6
- import { ghKeyPrefix, ghMetadataFromTarget } from "../github.js";
6
+ import { ghKeyPrefix } from "../github.js";
7
7
  import { validateMetaMap } from "../metadata.js";
8
- import { execRunner, resolveCurrentPullRequest, resolveRepo, } from "../github-gh.js";
8
+ import { execRunner, ghMetadataFromTargetWithTitle, resolveCurrentPullRequest, resolveRepo, } from "../github-gh.js";
9
9
  import { METADATA_DESCRIPTION, metadataProp, optPosInt, optString, optStringArray, optStringRecord, usage, } from "./args.js";
10
10
  import { batchFailureMessage, ToolBatchError } from "./server.js";
11
11
  import { attachmentFromText, buildReportPayload, parseReportType, REPORT_TYPES, submitReport, validateReportMessage, } from "../report.js";
@@ -830,10 +830,10 @@ export function createUploadsMcpTools(opts) {
830
830
  // explicit target pairs always win over a same-named metadata extra
831
831
  // (mirrors runAttach in ../commands.js). Validate the merged map (not
832
832
  // just the extras) so the 24-key/8KB caps are enforced client-side —
833
- // extras alone might pass while extras + the 4 gh.* pairs exceed the
833
+ // extras alone might pass while extras + the gh.* pairs exceed the
834
834
  // cap, which would otherwise only be caught server-side after upload.
835
835
  const metaExtras = optStringRecord(args, "metadata") ?? {};
836
- const metadata = { ...metaExtras, ...ghMetadataFromTarget(target) };
836
+ const metadata = { ...metaExtras, ...ghMetadataFromTargetWithTitle(target, run) };
837
837
  if (Object.keys(metadata).length > 0)
838
838
  validateMetaMap(metadata);
839
839
  const { uploads, failures } = await uploadAttachments({
@@ -8,6 +8,14 @@ export declare const META_MAX_KEYS = 24;
8
8
  export declare const META_MAX_TOTAL_BYTES = 8192;
9
9
  /** Throws `UsageError` with a readable message if `key`/`value` violate the metadata rules. */
10
10
  export declare function validateMetaEntry(key: string, value: string): void;
11
+ /**
12
+ * Non-throwing version of `validateMetaEntry`'s value check (length +
13
+ * printable-ASCII), for callers that want to silently drop a value that
14
+ * doesn't fit rather than reject the whole request — e.g. a best-effort
15
+ * `gh.title` derived from a real-world PR/issue title, which may contain
16
+ * unicode (emoji, curly quotes) that the metadata value rule disallows.
17
+ */
18
+ export declare function isMetaValueSafe(value: string): boolean;
11
19
  /**
12
20
  * Split `k=v` on the FIRST "=" (so values may themselves contain "="), then
13
21
  * validate the pair. Throws `UsageError` on malformed input.
package/dist/metadata.js CHANGED
@@ -38,6 +38,16 @@ export function validateMetaEntry(key, value) {
38
38
  throw new UsageError(`invalid metadata value for key "${key}": must be 1-${META_VALUE_MAX} printable ASCII characters`);
39
39
  }
40
40
  }
41
+ /**
42
+ * Non-throwing version of `validateMetaEntry`'s value check (length +
43
+ * printable-ASCII), for callers that want to silently drop a value that
44
+ * doesn't fit rather than reject the whole request — e.g. a best-effort
45
+ * `gh.title` derived from a real-world PR/issue title, which may contain
46
+ * unicode (emoji, curly quotes) that the metadata value rule disallows.
47
+ */
48
+ export function isMetaValueSafe(value) {
49
+ return value.length >= 1 && value.length <= META_VALUE_MAX && VALUE_SAFE_RE.test(value);
50
+ }
41
51
  /**
42
52
  * Split `k=v` on the FIRST "=" (so values may themselves contain "="), then
43
53
  * validate the pair. Throws `UsageError` on malformed input.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@buildinternet/uploads",
3
- "version": "0.13.1",
3
+ "version": "0.14.0",
4
4
  "description": "CLI and client for uploads.sh — workspace-scoped image hosting for GitHub embeds",
5
5
  "type": "module",
6
6
  "sideEffects": false,