@buildinternet/uploads 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -4
- package/dist/cli-args.d.ts +17 -2
- package/dist/cli-args.js +50 -5
- package/dist/cli.d.ts +5 -0
- package/dist/cli.js +67 -24
- package/dist/client.d.ts +69 -0
- package/dist/client.js +99 -5
- package/dist/commands/install.js +110 -52
- package/dist/commands/invite.d.ts +10 -0
- package/dist/commands/invite.js +102 -0
- package/dist/commands/login.d.ts +15 -0
- package/dist/commands/login.js +17 -9
- package/dist/commands.d.ts +4 -0
- package/dist/commands.js +287 -17
- package/dist/config-file.d.ts +3 -1
- package/dist/config-file.js +10 -0
- package/dist/config.js +6 -3
- package/dist/errors.d.ts +1 -1
- package/dist/github-gh.d.ts +7 -0
- package/dist/github-gh.js +24 -0
- package/dist/github.d.ts +15 -0
- package/dist/github.js +31 -7
- package/dist/index.d.ts +4 -2
- package/dist/index.js +3 -1
- package/dist/mcp/args.d.ts +17 -0
- package/dist/mcp/args.js +42 -0
- package/dist/mcp/server.d.ts +1 -1
- package/dist/mcp/server.js +1 -1
- package/dist/mcp/tools.js +121 -21
- package/dist/metadata.d.ts +32 -0
- package/dist/metadata.js +102 -0
- package/dist/public-urls.d.ts +18 -0
- package/dist/public-urls.js +68 -0
- package/package.json +1 -1
package/dist/github.d.ts
CHANGED
|
@@ -22,11 +22,25 @@ export declare function ghKeyPrefix(target: GhTarget): string;
|
|
|
22
22
|
* (unlike buildScreenshotKey).
|
|
23
23
|
*/
|
|
24
24
|
export declare function ghAttachmentKey(target: GhTarget, filename: string): string;
|
|
25
|
+
/**
|
|
26
|
+
* The four `gh.*` queryable-metadata pairs `uploads attach` writes
|
|
27
|
+
* automatically (`.context/2026-07-13-file-metadata-design.md`). `gh.kind`
|
|
28
|
+
* uses the API's singular vocabulary (`pull`/`issue`), distinct from
|
|
29
|
+
* `GhTarget.kind`'s URL-segment spelling (`pull`/`issues`). `gh.repo` and
|
|
30
|
+
* `gh.ref` are both lowercased so exact-match metadata search has one
|
|
31
|
+
* canonical spelling regardless of source casing (`--repo`, git remote, and
|
|
32
|
+
* `gh` output vary); `gh.ref` uses the same lowercased `owner/repo#number`
|
|
33
|
+
* coordinate as gallery GitHub references, so both surfaces resolve the same
|
|
34
|
+
* lookup key.
|
|
35
|
+
*/
|
|
36
|
+
export declare function ghMetadataFromTarget(target: GhTarget): Record<string, string>;
|
|
25
37
|
/** Hidden marker identifying the one comment this CLI manages. Never change it — existing comments are found by exact match. */
|
|
26
38
|
export declare const ATTACHMENTS_MARKER = "<!-- uploads.sh:attachments -->";
|
|
27
39
|
export interface AttachmentItem {
|
|
28
40
|
key: string;
|
|
29
41
|
url: string | null;
|
|
42
|
+
/** Prefer for `<img src>` on GitHub (Camo-friendly host). Falls back to `url`. */
|
|
43
|
+
embedUrl?: string | null;
|
|
30
44
|
}
|
|
31
45
|
/** A public gallery linked to the PR or issue whose managed comment is syncing. */
|
|
32
46
|
export interface GalleryCommentItem {
|
|
@@ -37,6 +51,7 @@ export interface GalleryCommentItem {
|
|
|
37
51
|
previews?: {
|
|
38
52
|
url: string;
|
|
39
53
|
alt: string;
|
|
54
|
+
embedUrl?: string | null;
|
|
40
55
|
itemUrl?: string;
|
|
41
56
|
}[];
|
|
42
57
|
}
|
package/dist/github.js
CHANGED
|
@@ -63,6 +63,26 @@ export function ghKeyPrefix(target) {
|
|
|
63
63
|
export function ghAttachmentKey(target, filename) {
|
|
64
64
|
return `${ghKeyPrefix(target)}${sanitizeKeySegment(filename)}`;
|
|
65
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* The four `gh.*` queryable-metadata pairs `uploads attach` writes
|
|
68
|
+
* automatically (`.context/2026-07-13-file-metadata-design.md`). `gh.kind`
|
|
69
|
+
* uses the API's singular vocabulary (`pull`/`issue`), distinct from
|
|
70
|
+
* `GhTarget.kind`'s URL-segment spelling (`pull`/`issues`). `gh.repo` and
|
|
71
|
+
* `gh.ref` are both lowercased so exact-match metadata search has one
|
|
72
|
+
* canonical spelling regardless of source casing (`--repo`, git remote, and
|
|
73
|
+
* `gh` output vary); `gh.ref` uses the same lowercased `owner/repo#number`
|
|
74
|
+
* coordinate as gallery GitHub references, so both surfaces resolve the same
|
|
75
|
+
* lookup key.
|
|
76
|
+
*/
|
|
77
|
+
export function ghMetadataFromTarget(target) {
|
|
78
|
+
const repo = target.repo.toLowerCase();
|
|
79
|
+
return {
|
|
80
|
+
"gh.repo": repo,
|
|
81
|
+
"gh.kind": target.kind === "issues" ? "issue" : "pull",
|
|
82
|
+
"gh.number": String(target.num),
|
|
83
|
+
"gh.ref": `${repo}#${target.num}`,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
66
86
|
/** Hidden marker identifying the one comment this CLI manages. Never change it — existing comments are found by exact match. */
|
|
67
87
|
export const ATTACHMENTS_MARKER = "<!-- uploads.sh:attachments -->";
|
|
68
88
|
/** Default max width for images in the managed attachments comment (HTML img). */
|
|
@@ -107,7 +127,8 @@ export function attachmentsCommentBody(items, galleries = []) {
|
|
|
107
127
|
lines.push(`#### <a href="${href}">${escapeHtmlText(gallery.title)}</a>`);
|
|
108
128
|
for (const preview of gallery.previews ?? []) {
|
|
109
129
|
const previewHref = preview.itemUrl ? escapeHtmlAttr(preview.itemUrl) : href;
|
|
110
|
-
|
|
130
|
+
const previewSrc = escapeHtmlAttr(preview.embedUrl ?? preview.url);
|
|
131
|
+
lines.push(`<a href="${previewHref}"><img width="320" alt="${escapeHtmlAttr(preview.alt)}" src="${previewSrc}"></a>`);
|
|
111
132
|
}
|
|
112
133
|
lines.push(`<sub><a href="${href}">Open gallery</a></sub>`, "");
|
|
113
134
|
}
|
|
@@ -117,17 +138,20 @@ export function attachmentsCommentBody(items, galleries = []) {
|
|
|
117
138
|
lines.push("### 📎 Attachments", "");
|
|
118
139
|
for (const item of sorted) {
|
|
119
140
|
const name = item.key.slice(item.key.lastIndexOf("/") + 1);
|
|
120
|
-
|
|
141
|
+
const stable = item.url;
|
|
142
|
+
const src = item.embedUrl ?? item.url;
|
|
143
|
+
if (src && inferContentType(name).startsWith("image/")) {
|
|
121
144
|
// Markdown ![]() has no width control — phone frames become full-column giants.
|
|
122
|
-
//
|
|
145
|
+
// img src uses embed host when available (Camo revalidates); click-through keeps stable URL.
|
|
123
146
|
const w = attachmentImageWidth(name);
|
|
124
147
|
const alt = escapeHtmlAttr(name);
|
|
125
|
-
const href = escapeHtmlAttr(
|
|
126
|
-
|
|
148
|
+
const href = escapeHtmlAttr(stable ?? src);
|
|
149
|
+
const imgSrc = escapeHtmlAttr(src);
|
|
150
|
+
lines.push(`<a href="${href}"><img width="${w}" alt="${alt}" src="${imgSrc}"></a>`);
|
|
127
151
|
lines.push("");
|
|
128
152
|
}
|
|
129
|
-
else if (
|
|
130
|
-
lines.push(`- [${name}](${
|
|
153
|
+
else if (stable) {
|
|
154
|
+
lines.push(`- [${name}](${stable})`);
|
|
131
155
|
}
|
|
132
156
|
else {
|
|
133
157
|
lines.push(`- ${name}`);
|
package/dist/index.d.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
export { inferContentType, buildMarkdown } from "./embed.js";
|
|
2
|
+
export { DEFAULT_EMBED_PUBLIC_BASE_URL, embedBaseUrlFromEnv, embedUrlFromPublic, resolveEmbedBaseUrl, resolveEmbedUrl, urlForGithubEmbed, } from "./public-urls.js";
|
|
2
3
|
export { sanitizeKeySegment, sha256Short, deriveRepoFromGit, buildScreenshotKey } from "./keys.js";
|
|
3
4
|
export { BUILTIN_DESTINATIONS, isBuiltinDestination, keyMatchesDestination, resolveDestinationRoot, resolvePutPrefix, type BuiltinDestinationId, } from "./destinations.js";
|
|
4
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";
|
|
5
6
|
export { UploadsError, type UploadsErrorCode } from "./errors.js";
|
|
6
|
-
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, } 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 GetMetadataResult, type PatchMetadataOptions, } from "./client.js";
|
|
7
8
|
export { buildCliProvenance } from "./provenance.js";
|
|
8
|
-
export {
|
|
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, ghAttachmentKey, ghKeyPrefix, ghMetadataFromTarget, isValidRepo, parseRepoFromRemoteUrl, type AttachmentItem, type GhTarget, type GhTargetKind, } from "./github.js";
|
|
9
11
|
export { DEFAULT_OPTIMIZE_MAX_EDGE, DEFAULT_OPTIMIZE_QUALITY, optimizeImageForUpload, rewriteKeyExtension, withImageExtension, type OptimizeImageOptions, type OptimizeImageResult, type OptimizeOutputFormat, } from "./optimize.js";
|
|
10
12
|
export { FRAME_PRESETS, applyFrame, listFramePresets, resolveFrameId, type FrameFit, type FrameOptions, type FrameResult, } from "./frame.js";
|
|
11
13
|
export { execRunner, resolveRepo, upsertAttachmentsComment, type CommandRunner, } from "./github-gh.js";
|
package/dist/index.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
export { inferContentType, buildMarkdown } from "./embed.js";
|
|
2
|
+
export { DEFAULT_EMBED_PUBLIC_BASE_URL, embedBaseUrlFromEnv, embedUrlFromPublic, resolveEmbedBaseUrl, resolveEmbedUrl, urlForGithubEmbed, } from "./public-urls.js";
|
|
2
3
|
export { sanitizeKeySegment, sha256Short, deriveRepoFromGit, buildScreenshotKey } from "./keys.js";
|
|
3
4
|
export { BUILTIN_DESTINATIONS, isBuiltinDestination, keyMatchesDestination, resolveDestinationRoot, resolvePutPrefix, } from "./destinations.js";
|
|
4
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, } from "./config.js";
|
|
5
6
|
export { UploadsError } from "./errors.js";
|
|
6
7
|
export { createUploadsClient, } from "./client.js";
|
|
7
8
|
export { buildCliProvenance } from "./provenance.js";
|
|
8
|
-
export {
|
|
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, ghAttachmentKey, ghKeyPrefix, ghMetadataFromTarget, isValidRepo, parseRepoFromRemoteUrl, } from "./github.js";
|
|
9
11
|
export { DEFAULT_OPTIMIZE_MAX_EDGE, DEFAULT_OPTIMIZE_QUALITY, optimizeImageForUpload, rewriteKeyExtension, withImageExtension, } from "./optimize.js";
|
|
10
12
|
export { FRAME_PRESETS, applyFrame, listFramePresets, resolveFrameId, } from "./frame.js";
|
|
11
13
|
export { execRunner, resolveRepo, upsertAttachmentsComment, } from "./github-gh.js";
|
package/dist/mcp/args.d.ts
CHANGED
|
@@ -2,3 +2,20 @@ export type ToolArgs = Record<string, unknown>;
|
|
|
2
2
|
export declare function usage(msg: string): never;
|
|
3
3
|
export declare function optString(args: ToolArgs, name: string): string | undefined;
|
|
4
4
|
export declare function optPosInt(args: ToolArgs, name: string): number | undefined;
|
|
5
|
+
/** A JSON-object argument of string→string pairs (e.g. a `metadata` or `filters` param). */
|
|
6
|
+
export declare function optStringRecord(args: ToolArgs, name: string): Record<string, string> | undefined;
|
|
7
|
+
/** A JSON-array argument of strings (e.g. a `delete` or `files` param). */
|
|
8
|
+
export declare function optStringArray(args: ToolArgs, name: string): string[] | undefined;
|
|
9
|
+
/**
|
|
10
|
+
* Shared tool-description text for the metadata-shaped `metadata`/`set`/
|
|
11
|
+
* `filters` params across the CLI/local MCP (put/attach/set_metadata/
|
|
12
|
+
* find_files) and the remote MCP worker (set_metadata/find_files).
|
|
13
|
+
*/
|
|
14
|
+
export declare const METADATA_DESCRIPTION = "Queryable custom metadata (key\u2192value), separate from provenance. Omit to leave any metadata already stored for this key untouched; pass an object (even {}) to fully replace it. Keys: lowercase, ^[a-z][a-z0-9._-]{0,63}$. Values: 1-512 printable ASCII characters. Caps: at most 24 keys, at most 8192 total key+value bytes. Suggested keys: app, url, page, device, resolution, commit, branch. `gh.*` is reserved by convention for GitHub PR/issue attachment context (repo/kind/number/ref).";
|
|
15
|
+
export declare const metadataProp: {
|
|
16
|
+
type: string;
|
|
17
|
+
additionalProperties: {
|
|
18
|
+
type: string;
|
|
19
|
+
};
|
|
20
|
+
description: string;
|
|
21
|
+
};
|
package/dist/mcp/args.js
CHANGED
|
@@ -24,3 +24,45 @@ export function optPosInt(args, name) {
|
|
|
24
24
|
}
|
|
25
25
|
return v;
|
|
26
26
|
}
|
|
27
|
+
/** A JSON-object argument of string→string pairs (e.g. a `metadata` or `filters` param). */
|
|
28
|
+
export function optStringRecord(args, name) {
|
|
29
|
+
const v = args[name];
|
|
30
|
+
if (v === undefined || v === null)
|
|
31
|
+
return undefined;
|
|
32
|
+
if (typeof v !== "object" || Array.isArray(v)) {
|
|
33
|
+
usage(`${name} must be an object of string values`);
|
|
34
|
+
}
|
|
35
|
+
// Object.create(null): a plain `{}` would silently drop a `__proto__` key
|
|
36
|
+
// (it hits the inherited setter instead of becoming an own property),
|
|
37
|
+
// turning a malicious/malformed key into a no-op rather than a rejected
|
|
38
|
+
// input. A null-prototype object makes every key a real own property so
|
|
39
|
+
// downstream validation (e.g. META_KEY_RE) sees and rejects it.
|
|
40
|
+
const result = Object.create(null);
|
|
41
|
+
for (const [key, value] of Object.entries(v)) {
|
|
42
|
+
if (typeof value !== "string")
|
|
43
|
+
usage(`${name}.${key} must be a string`);
|
|
44
|
+
result[key] = value;
|
|
45
|
+
}
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
/** A JSON-array argument of strings (e.g. a `delete` or `files` param). */
|
|
49
|
+
export function optStringArray(args, name) {
|
|
50
|
+
const v = args[name];
|
|
51
|
+
if (v === undefined || v === null)
|
|
52
|
+
return undefined;
|
|
53
|
+
if (!Array.isArray(v) || v.some((item) => typeof item !== "string")) {
|
|
54
|
+
usage(`${name} must be an array of strings`);
|
|
55
|
+
}
|
|
56
|
+
return v;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Shared tool-description text for the metadata-shaped `metadata`/`set`/
|
|
60
|
+
* `filters` params across the CLI/local MCP (put/attach/set_metadata/
|
|
61
|
+
* find_files) and the remote MCP worker (set_metadata/find_files).
|
|
62
|
+
*/
|
|
63
|
+
export const METADATA_DESCRIPTION = "Queryable custom metadata (key→value), separate from provenance. Omit to leave any metadata already stored for this key untouched; pass an object (even {}) to fully replace it. Keys: lowercase, ^[a-z][a-z0-9._-]{0,63}$. Values: 1-512 printable ASCII characters. Caps: at most 24 keys, at most 8192 total key+value bytes. Suggested keys: app, url, page, device, resolution, commit, branch. `gh.*` is reserved by convention for GitHub PR/issue attachment context (repo/kind/number/ref).";
|
|
64
|
+
export const metadataProp = {
|
|
65
|
+
type: "object",
|
|
66
|
+
additionalProperties: { type: "string" },
|
|
67
|
+
description: METADATA_DESCRIPTION,
|
|
68
|
+
};
|
package/dist/mcp/server.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { optPosInt, optString, usage, type ToolArgs } from "./args.js";
|
|
1
|
+
export { METADATA_DESCRIPTION, metadataProp, optPosInt, optString, optStringArray, optStringRecord, usage, type ToolArgs, } from "./args.js";
|
|
2
2
|
export interface McpTool {
|
|
3
3
|
name: string;
|
|
4
4
|
description: string;
|
package/dist/mcp/server.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* stdio transport lives in ./stdio.ts; logs must never go to stdout.
|
|
9
9
|
*/
|
|
10
10
|
import { UploadsError } from "../errors.js";
|
|
11
|
-
export { optPosInt, optString, usage } from "./args.js";
|
|
11
|
+
export { METADATA_DESCRIPTION, metadataProp, optPosInt, optString, optStringArray, optStringRecord, usage, } from "./args.js";
|
|
12
12
|
const SUPPORTED_PROTOCOL_VERSIONS = new Set(["2025-06-18", "2025-03-26", "2024-11-05"]);
|
|
13
13
|
const LATEST_PROTOCOL_VERSION = "2025-06-18";
|
|
14
14
|
function response(id, result) {
|
package/dist/mcp/tools.js
CHANGED
|
@@ -5,19 +5,20 @@
|
|
|
5
5
|
* per-call `workspace` argument behaves like the CLI's --workspace flag, and
|
|
6
6
|
* a missing token surfaces as a tool error rather than a startup failure.
|
|
7
7
|
*/
|
|
8
|
-
import { readFileSync } from "node:fs";
|
|
9
8
|
import { basename } from "node:path";
|
|
10
9
|
import { createUploadsClient } from "../client.js";
|
|
11
|
-
import { buildDoctorReport, makeGhTarget, prepareImageForUpload, syncAttachmentsComment, } from "../commands.js";
|
|
10
|
+
import { buildDoctorReport, makeGhTarget, prepareImageForUpload, readFileArg, syncAttachmentsComment, } from "../commands.js";
|
|
12
11
|
import { resolveFrameId } from "../frame.js";
|
|
13
12
|
import { resolveConfig, resolvePutDefaults, } from "../config.js";
|
|
14
13
|
import { buildMarkdown } from "../embed.js";
|
|
14
|
+
import { urlForGithubEmbed } from "../public-urls.js";
|
|
15
15
|
import { resolvePutPrefix } from "../destinations.js";
|
|
16
|
-
import { ghAttachmentKey, ghKeyPrefix } from "../github.js";
|
|
16
|
+
import { ghAttachmentKey, ghKeyPrefix, ghMetadataFromTarget } from "../github.js";
|
|
17
|
+
import { validateMetaMap } from "../metadata.js";
|
|
17
18
|
import { rewriteKeyExtension } from "../optimize.js";
|
|
18
19
|
import { buildCliProvenance } from "../provenance.js";
|
|
19
20
|
import { execRunner, resolveCurrentPullRequest, resolveRepo, } from "../github-gh.js";
|
|
20
|
-
import { optPosInt, optString, usage } from "./args.js";
|
|
21
|
+
import { METADATA_DESCRIPTION, metadataProp, optPosInt, optString, optStringArray, optStringRecord, usage, } from "./args.js";
|
|
21
22
|
function optBool(args, name) {
|
|
22
23
|
const v = args[name];
|
|
23
24
|
if (v === undefined || v === null)
|
|
@@ -26,15 +27,6 @@ function optBool(args, name) {
|
|
|
26
27
|
usage(`${name} must be a boolean`);
|
|
27
28
|
return v;
|
|
28
29
|
}
|
|
29
|
-
function optStringArray(args, name) {
|
|
30
|
-
const v = args[name];
|
|
31
|
-
if (v === undefined || v === null)
|
|
32
|
-
return undefined;
|
|
33
|
-
if (!Array.isArray(v) || v.some((item) => typeof item !== "string")) {
|
|
34
|
-
usage(`${name} must be an array of strings`);
|
|
35
|
-
}
|
|
36
|
-
return v;
|
|
37
|
-
}
|
|
38
30
|
function mcpOptimizeOptions(args, defaults) {
|
|
39
31
|
const quality = optPosInt(args, "optimizeQuality");
|
|
40
32
|
if (quality !== undefined && quality > 100)
|
|
@@ -276,7 +268,7 @@ export function createUploadsMcpTools(opts) {
|
|
|
276
268
|
},
|
|
277
269
|
{
|
|
278
270
|
name: "put",
|
|
279
|
-
description: "Upload a file to uploads.sh and get a public URL plus GitHub-ready embed markdown (
|
|
271
|
+
description: "Upload a file to uploads.sh and get a public URL plus GitHub-ready embed markdown. Returns `url` (durable CDN) and `embedUrl` (same object, freshness-oriented host for GitHub Camo — prefer this in PR/issue markdown). The returned `markdown` already uses embedUrl when available. Pass `file` or `contentBase64` + `filename`; with `pr`/`issue` the key is stable and `comment` syncs the managed attachments comment. All uploads are public; pr/issue keys are predictable, so upload only non-sensitive media.",
|
|
280
272
|
inputSchema: {
|
|
281
273
|
type: "object",
|
|
282
274
|
properties: {
|
|
@@ -290,7 +282,7 @@ export function createUploadsMcpTools(opts) {
|
|
|
290
282
|
},
|
|
291
283
|
filename: {
|
|
292
284
|
type: "string",
|
|
293
|
-
description: "Filename for contentBase64 content (drives the key and content type).",
|
|
285
|
+
description: "Filename for contentBase64 content (drives the key and content type). With `file`, overrides the key's leaf (clean name) while keeping the pr/default path.",
|
|
294
286
|
},
|
|
295
287
|
key: {
|
|
296
288
|
type: "string",
|
|
@@ -345,6 +337,11 @@ export function createUploadsMcpTools(opts) {
|
|
|
345
337
|
type: "boolean",
|
|
346
338
|
description: "With pr/issue: create or update the managed attachments comment via local gh auth (best-effort).",
|
|
347
339
|
},
|
|
340
|
+
dryRun: {
|
|
341
|
+
type: "boolean",
|
|
342
|
+
description: "Resolve key + public URL without uploading. Not with comment.",
|
|
343
|
+
},
|
|
344
|
+
metadata: metadataProp,
|
|
348
345
|
workspace: workspaceProp,
|
|
349
346
|
},
|
|
350
347
|
additionalProperties: false,
|
|
@@ -361,12 +358,15 @@ export function createUploadsMcpTools(opts) {
|
|
|
361
358
|
}
|
|
362
359
|
const target = ghTargetFromArgs(args, run);
|
|
363
360
|
const wantComment = optBool(args, "comment");
|
|
361
|
+
const dryRun = optBool(args, "dryRun");
|
|
364
362
|
const keyArg = optString(args, "key");
|
|
365
363
|
const destArg = optString(args, "destination");
|
|
366
364
|
const prefixArg = optString(args, "prefix");
|
|
367
365
|
const refArg = optString(args, "ref");
|
|
368
366
|
if (wantComment && !target)
|
|
369
367
|
usage("comment requires pr or issue");
|
|
368
|
+
if (dryRun && wantComment)
|
|
369
|
+
usage("dryRun cannot be combined with comment");
|
|
370
370
|
if (target) {
|
|
371
371
|
if (keyArg)
|
|
372
372
|
usage("key cannot be combined with pr/issue");
|
|
@@ -375,6 +375,12 @@ export function createUploadsMcpTools(opts) {
|
|
|
375
375
|
if (prefixArg)
|
|
376
376
|
usage("prefix cannot be combined with pr/issue");
|
|
377
377
|
}
|
|
378
|
+
// Validate up front (fail fast, before reading/optimizing the file).
|
|
379
|
+
// undefined leaves existing metadata untouched; an object (even {})
|
|
380
|
+
// fully replaces it — see metadataProp's description.
|
|
381
|
+
const metadata = optStringRecord(args, "metadata");
|
|
382
|
+
if (metadata)
|
|
383
|
+
validateMetaMap(metadata);
|
|
378
384
|
let resolvedPrefix;
|
|
379
385
|
try {
|
|
380
386
|
resolvedPrefix = resolvePutPrefix({
|
|
@@ -389,7 +395,7 @@ export function createUploadsMcpTools(opts) {
|
|
|
389
395
|
}
|
|
390
396
|
const { client } = clientFor(args);
|
|
391
397
|
const bytes = file !== undefined
|
|
392
|
-
?
|
|
398
|
+
? readFileArg(file)
|
|
393
399
|
: new Uint8Array(Buffer.from(contentBase64, "base64"));
|
|
394
400
|
const sourceName = file !== undefined ? (filenameArg ?? basename(file)) : filenameArg;
|
|
395
401
|
const defaults = resolvePutDefaults({ envFile: globals.envFile });
|
|
@@ -412,6 +418,7 @@ export function createUploadsMcpTools(opts) {
|
|
|
412
418
|
ref: refArg ?? defaults.ref,
|
|
413
419
|
contentType: prepared.optimized ? prepared.contentType : optString(args, "contentType"),
|
|
414
420
|
deriveRepoFromGit: !noGit,
|
|
421
|
+
dryRun,
|
|
415
422
|
provenance: buildCliProvenance({
|
|
416
423
|
sourceName,
|
|
417
424
|
client: "uploads-mcp",
|
|
@@ -419,8 +426,9 @@ export function createUploadsMcpTools(opts) {
|
|
|
419
426
|
frameId: prepared.frame?.framed ? prepared.frame.frameId : undefined,
|
|
420
427
|
keepExif: optimizeOpts.keepExif === true,
|
|
421
428
|
}),
|
|
429
|
+
metadata,
|
|
422
430
|
});
|
|
423
|
-
const markdown = buildMarkdown(result.url, {
|
|
431
|
+
const markdown = buildMarkdown(urlForGithubEmbed(result.url, result.embedUrl), {
|
|
424
432
|
alt: optString(args, "alt") ?? sourceName,
|
|
425
433
|
width: optPosInt(args, "width") ?? defaults.width,
|
|
426
434
|
});
|
|
@@ -435,12 +443,18 @@ export function createUploadsMcpTools(opts) {
|
|
|
435
443
|
const { comment, commentError } = await syncComment(client, target);
|
|
436
444
|
return { ...result, markdown, optimize, frame: prepared.frame, comment, commentError };
|
|
437
445
|
}
|
|
438
|
-
return {
|
|
446
|
+
return {
|
|
447
|
+
...result,
|
|
448
|
+
markdown,
|
|
449
|
+
optimize,
|
|
450
|
+
frame: prepared.frame,
|
|
451
|
+
...(dryRun ? { dryRun: true } : {}),
|
|
452
|
+
};
|
|
439
453
|
},
|
|
440
454
|
},
|
|
441
455
|
{
|
|
442
456
|
name: "attach",
|
|
443
|
-
description: "Upload one or more files as stable PR/issue attachments and maintain a single managed GitHub comment listing them
|
|
457
|
+
description: "Upload one or more files as stable PR/issue attachments and maintain a single managed GitHub comment listing them. Each upload returns `url`, `embedUrl` (when dual-host applies), and `markdown` (uses embedUrl for GitHub). With no pr/issue, targets the pull request for the current branch. Attachments are public and keys are predictable; upload only non-sensitive media.",
|
|
444
458
|
inputSchema: {
|
|
445
459
|
type: "object",
|
|
446
460
|
properties: {
|
|
@@ -475,6 +489,11 @@ export function createUploadsMcpTools(opts) {
|
|
|
475
489
|
description: "Keep EXIF/XMP/ICC when optimizing (default: strip for privacy on public embeds).",
|
|
476
490
|
},
|
|
477
491
|
...frameProps,
|
|
492
|
+
metadata: {
|
|
493
|
+
...metadataProp,
|
|
494
|
+
description: "Extra queryable metadata (key→value), merged with the automatic gh.repo/gh.kind/gh.number/gh.ref pairs — a gh.* pair here loses to the resolved target's own gh.* value. " +
|
|
495
|
+
METADATA_DESCRIPTION,
|
|
496
|
+
},
|
|
478
497
|
workspace: workspaceProp,
|
|
479
498
|
},
|
|
480
499
|
required: ["files"],
|
|
@@ -492,10 +511,23 @@ export function createUploadsMcpTools(opts) {
|
|
|
492
511
|
const defaults = resolvePutDefaults({ envFile: globals.envFile });
|
|
493
512
|
const frameOpts = mcpFrameOptions(args);
|
|
494
513
|
const optimizeOpts = mcpOptimizeOptions(args, defaults);
|
|
514
|
+
// User-supplied extras first, then the resolved target's gh.* —
|
|
515
|
+
// explicit target pairs always win over a same-named metadata extra
|
|
516
|
+
// (mirrors runAttach in ../commands.js). Validate the merged map (not
|
|
517
|
+
// just the extras) so the 24-key/8KB caps are enforced client-side —
|
|
518
|
+
// extras alone might pass while extras + the 4 gh.* pairs exceed the
|
|
519
|
+
// cap, which would otherwise only be caught server-side after upload.
|
|
520
|
+
const metaExtras = optStringRecord(args, "metadata") ?? {};
|
|
521
|
+
const metadata = { ...metaExtras, ...ghMetadataFromTarget(target) };
|
|
522
|
+
if (Object.keys(metadata).length > 0)
|
|
523
|
+
validateMetaMap(metadata);
|
|
495
524
|
const uploads = [];
|
|
496
525
|
for (const file of files) {
|
|
497
526
|
const sourceName = basename(file);
|
|
498
|
-
const prepared = await prepareImageForUpload(
|
|
527
|
+
const prepared = await prepareImageForUpload(readFileArg(file), sourceName, {
|
|
528
|
+
...frameOpts,
|
|
529
|
+
optimize: optimizeOpts,
|
|
530
|
+
});
|
|
499
531
|
const result = await client.put(prepared.bytes, {
|
|
500
532
|
filename: prepared.filename,
|
|
501
533
|
key: ghAttachmentKey(target, prepared.filename),
|
|
@@ -507,10 +539,13 @@ export function createUploadsMcpTools(opts) {
|
|
|
507
539
|
frameId: prepared.frame?.framed ? prepared.frame.frameId : undefined,
|
|
508
540
|
keepExif: optimizeOpts.keepExif === true,
|
|
509
541
|
}),
|
|
542
|
+
metadata,
|
|
510
543
|
});
|
|
511
544
|
uploads.push({
|
|
512
545
|
...result,
|
|
513
|
-
markdown: buildMarkdown(result.url,
|
|
546
|
+
markdown: buildMarkdown(urlForGithubEmbed(result.url, result.embedUrl), {
|
|
547
|
+
alt: sourceName,
|
|
548
|
+
}),
|
|
514
549
|
frame: prepared.frame,
|
|
515
550
|
optimize: {
|
|
516
551
|
optimized: prepared.optimized,
|
|
@@ -591,6 +626,71 @@ export function createUploadsMcpTools(opts) {
|
|
|
591
626
|
return client.delete(key);
|
|
592
627
|
},
|
|
593
628
|
},
|
|
629
|
+
{
|
|
630
|
+
name: "set_metadata",
|
|
631
|
+
description: "Merge-set and/or delete an object's queryable custom metadata (D1-backed key-value pairs; distinct from the R2 provenance headers put on upload). `set` pairs win over `delete` when a key appears in both. " +
|
|
632
|
+
METADATA_DESCRIPTION +
|
|
633
|
+
" Requires at least one of `set` or `delete`. Same as `uploads meta set`.",
|
|
634
|
+
inputSchema: {
|
|
635
|
+
type: "object",
|
|
636
|
+
properties: {
|
|
637
|
+
key: { type: "string", description: "Object key to update." },
|
|
638
|
+
set: { ...metadataProp, description: "Keys to set/overwrite. " + METADATA_DESCRIPTION },
|
|
639
|
+
delete: {
|
|
640
|
+
type: "array",
|
|
641
|
+
items: { type: "string" },
|
|
642
|
+
description: "Keys to remove.",
|
|
643
|
+
},
|
|
644
|
+
workspace: workspaceProp,
|
|
645
|
+
},
|
|
646
|
+
required: ["key"],
|
|
647
|
+
additionalProperties: false,
|
|
648
|
+
},
|
|
649
|
+
async handler(args) {
|
|
650
|
+
const key = optString(args, "key");
|
|
651
|
+
if (!key)
|
|
652
|
+
usage("key is required");
|
|
653
|
+
const set = optStringRecord(args, "set");
|
|
654
|
+
const del = optStringArray(args, "delete");
|
|
655
|
+
if ((!set || Object.keys(set).length === 0) && (!del || del.length === 0)) {
|
|
656
|
+
usage("set_metadata requires set and/or delete");
|
|
657
|
+
}
|
|
658
|
+
if (set)
|
|
659
|
+
validateMetaMap(set);
|
|
660
|
+
const { client } = clientFor(args);
|
|
661
|
+
return client.patchMetadata(key, { set, delete: del });
|
|
662
|
+
},
|
|
663
|
+
},
|
|
664
|
+
{
|
|
665
|
+
name: "find_files",
|
|
666
|
+
description: "Find objects in the workspace whose queryable custom metadata matches ALL of `filters` (ANDed equality). Returns each match's key, public URL, and full metadata map. Same as `uploads find k=v...` / `uploads list --meta k=v`.",
|
|
667
|
+
inputSchema: {
|
|
668
|
+
type: "object",
|
|
669
|
+
properties: {
|
|
670
|
+
filters: {
|
|
671
|
+
...metadataProp,
|
|
672
|
+
description: "Metadata equality filters (at least one pair). " + METADATA_DESCRIPTION,
|
|
673
|
+
},
|
|
674
|
+
prefix: { type: "string", description: "Key prefix filter, combinable with filters." },
|
|
675
|
+
limit: { type: "number", description: "Page size (default 50, max 500)." },
|
|
676
|
+
workspace: workspaceProp,
|
|
677
|
+
},
|
|
678
|
+
required: ["filters"],
|
|
679
|
+
additionalProperties: false,
|
|
680
|
+
},
|
|
681
|
+
async handler(args) {
|
|
682
|
+
const filters = optStringRecord(args, "filters");
|
|
683
|
+
if (!filters || Object.keys(filters).length === 0) {
|
|
684
|
+
usage("filters must have at least one key");
|
|
685
|
+
}
|
|
686
|
+
validateMetaMap(filters);
|
|
687
|
+
const { client } = clientFor(args);
|
|
688
|
+
return client.findFiles(filters, {
|
|
689
|
+
prefix: optString(args, "prefix"),
|
|
690
|
+
limit: optPosInt(args, "limit"),
|
|
691
|
+
});
|
|
692
|
+
},
|
|
693
|
+
},
|
|
594
694
|
{
|
|
595
695
|
name: "usage",
|
|
596
696
|
description: "Workspace storage and monthly upload counters (and remaining headroom when budgets are configured). Same as `uploads usage`.",
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** Lowercase key, optionally dot-namespaced (e.g. `gh.repo`). Mirrors META_KEY_RE server-side. */
|
|
2
|
+
export declare const META_KEY_RE: RegExp;
|
|
3
|
+
/** Max value length in characters (mirrors META_VALUE_MAX server-side). */
|
|
4
|
+
export declare const META_VALUE_MAX = 512;
|
|
5
|
+
/** Cap on keys per request (mirrors META_MAX_KEYS server-side). */
|
|
6
|
+
export declare const META_MAX_KEYS = 24;
|
|
7
|
+
/** Cap on total UTF-8 key+value bytes per request (mirrors META_MAX_TOTAL_BYTES server-side). */
|
|
8
|
+
export declare const META_MAX_TOTAL_BYTES = 8192;
|
|
9
|
+
/** Throws `UsageError` with a readable message if `key`/`value` violate the metadata rules. */
|
|
10
|
+
export declare function validateMetaEntry(key: string, value: string): void;
|
|
11
|
+
/**
|
|
12
|
+
* Split `k=v` on the FIRST "=" (so values may themselves contain "="), then
|
|
13
|
+
* validate the pair. Throws `UsageError` on malformed input.
|
|
14
|
+
*/
|
|
15
|
+
export declare function parseMetaPair(raw: string): [string, string];
|
|
16
|
+
/**
|
|
17
|
+
* Parse and validate a batch of `k=v` pairs (e.g. every `--meta` occurrence,
|
|
18
|
+
* or `meta set`'s positional pairs) into a map. Fails fast on the first
|
|
19
|
+
* invalid pair or when the batch exceeds `META_MAX_KEYS` or
|
|
20
|
+
* `META_MAX_TOTAL_BYTES`. Later duplicate keys in the same batch win
|
|
21
|
+
* (last write).
|
|
22
|
+
*/
|
|
23
|
+
export declare function parseMetaFlags(pairs: string[]): Record<string, string>;
|
|
24
|
+
/**
|
|
25
|
+
* Validates a pre-parsed key→value metadata map (e.g. an MCP tool's object
|
|
26
|
+
* argument) against the same rules as {@link parseMetaFlags} — the same
|
|
27
|
+
* per-entry validation (`validateMetaEntry`, which `parseMetaPair` also
|
|
28
|
+
* uses), the same key-count cap, and the same aggregate byte cap — without
|
|
29
|
+
* round-tripping through "k=v" string reconstruction and re-parsing first.
|
|
30
|
+
* Throws `UsageError`.
|
|
31
|
+
*/
|
|
32
|
+
export declare function validateMetaMap(meta: Record<string, string>): void;
|
package/dist/metadata.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-side validation for `--meta k=v` / `meta set` pairs, mirroring the
|
|
3
|
+
* server rules in `apps/api/src/file-metadata.ts` (`.context/2026-07-13-file-metadata-design.md`).
|
|
4
|
+
* Validating here lets the CLI fail fast with a readable message instead of a
|
|
5
|
+
* round-trip 400 — the server remains the source of truth and re-validates on
|
|
6
|
+
* every write.
|
|
7
|
+
*/
|
|
8
|
+
import { UsageError } from "./cli-args.js";
|
|
9
|
+
/** Lowercase key, optionally dot-namespaced (e.g. `gh.repo`). Mirrors META_KEY_RE server-side. */
|
|
10
|
+
export const META_KEY_RE = /^[a-z][a-z0-9._-]{0,63}$/;
|
|
11
|
+
/** Max value length in characters (mirrors META_VALUE_MAX server-side). */
|
|
12
|
+
export const META_VALUE_MAX = 512;
|
|
13
|
+
/** Cap on keys per request (mirrors META_MAX_KEYS server-side). */
|
|
14
|
+
export const META_MAX_KEYS = 24;
|
|
15
|
+
/** Cap on total UTF-8 key+value bytes per request (mirrors META_MAX_TOTAL_BYTES server-side). */
|
|
16
|
+
export const META_MAX_TOTAL_BYTES = 8192;
|
|
17
|
+
// Printable ASCII only — same rule as the server's file-metadata.ts.
|
|
18
|
+
const VALUE_SAFE_RE = /^[\x20-\x7E]+$/;
|
|
19
|
+
const encoder = new TextEncoder();
|
|
20
|
+
/**
|
|
21
|
+
* Server-computed/server-reserved keys the API rejects as custom metadata:
|
|
22
|
+
* `content-sha256` (server-computed provenance) and `visibility` (names the
|
|
23
|
+
* R2-backed public/private gate, not a piece of D1 custom metadata — mirrors
|
|
24
|
+
* apps/api/src/file-metadata.ts's RESERVED_META_KEYS). `gh.*` is NOT reserved
|
|
25
|
+
* here: it's system-managed by convention (attach flow), not blocked — the
|
|
26
|
+
* server happily accepts user-supplied `gh.*` extras via `--meta`.
|
|
27
|
+
*/
|
|
28
|
+
const RESERVED_META_KEYS = new Set(["content-sha256", "visibility"]);
|
|
29
|
+
/** Throws `UsageError` with a readable message if `key`/`value` violate the metadata rules. */
|
|
30
|
+
export function validateMetaEntry(key, value) {
|
|
31
|
+
if (!META_KEY_RE.test(key)) {
|
|
32
|
+
throw new UsageError(`invalid metadata key: "${key}" (must match ^[a-z][a-z0-9._-]{0,63}$)`);
|
|
33
|
+
}
|
|
34
|
+
if (RESERVED_META_KEYS.has(key)) {
|
|
35
|
+
throw new UsageError(`invalid metadata key: "${key}" is reserved (server-computed)`);
|
|
36
|
+
}
|
|
37
|
+
if (value.length < 1 || value.length > META_VALUE_MAX || !VALUE_SAFE_RE.test(value)) {
|
|
38
|
+
throw new UsageError(`invalid metadata value for key "${key}": must be 1-${META_VALUE_MAX} printable ASCII characters`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Split `k=v` on the FIRST "=" (so values may themselves contain "="), then
|
|
43
|
+
* validate the pair. Throws `UsageError` on malformed input.
|
|
44
|
+
*/
|
|
45
|
+
export function parseMetaPair(raw) {
|
|
46
|
+
const eq = raw.indexOf("=");
|
|
47
|
+
if (eq === -1) {
|
|
48
|
+
throw new UsageError(`invalid --meta value: "${raw}" (expected key=value)`);
|
|
49
|
+
}
|
|
50
|
+
const key = raw.slice(0, eq);
|
|
51
|
+
const value = raw.slice(eq + 1);
|
|
52
|
+
validateMetaEntry(key, value);
|
|
53
|
+
return [key, value];
|
|
54
|
+
}
|
|
55
|
+
/** Throws `UsageError` if the aggregate UTF-8 key+value bytes of `entries` exceed `META_MAX_TOTAL_BYTES`. */
|
|
56
|
+
function enforceMetaByteCap(entries) {
|
|
57
|
+
let totalBytes = 0;
|
|
58
|
+
for (const [key, value] of entries) {
|
|
59
|
+
totalBytes += encoder.encode(key).byteLength + encoder.encode(value).byteLength;
|
|
60
|
+
}
|
|
61
|
+
if (totalBytes > META_MAX_TOTAL_BYTES) {
|
|
62
|
+
throw new UsageError(`metadata too large: ${totalBytes} bytes of keys+values exceeds the ${META_MAX_TOTAL_BYTES}-byte limit per request`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Parse and validate a batch of `k=v` pairs (e.g. every `--meta` occurrence,
|
|
67
|
+
* or `meta set`'s positional pairs) into a map. Fails fast on the first
|
|
68
|
+
* invalid pair or when the batch exceeds `META_MAX_KEYS` or
|
|
69
|
+
* `META_MAX_TOTAL_BYTES`. Later duplicate keys in the same batch win
|
|
70
|
+
* (last write).
|
|
71
|
+
*/
|
|
72
|
+
export function parseMetaFlags(pairs) {
|
|
73
|
+
if (pairs.length > META_MAX_KEYS) {
|
|
74
|
+
throw new UsageError(`too many --meta pairs: at most ${META_MAX_KEYS} per request`);
|
|
75
|
+
}
|
|
76
|
+
const result = {};
|
|
77
|
+
for (const pair of pairs) {
|
|
78
|
+
const [key, value] = parseMetaPair(pair);
|
|
79
|
+
result[key] = value;
|
|
80
|
+
}
|
|
81
|
+
// Aggregate byte cap over the deduplicated map — same accounting as the
|
|
82
|
+
// server (sum of UTF-8 key+value bytes, META_MAX_TOTAL_BYTES).
|
|
83
|
+
enforceMetaByteCap(Object.entries(result));
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Validates a pre-parsed key→value metadata map (e.g. an MCP tool's object
|
|
88
|
+
* argument) against the same rules as {@link parseMetaFlags} — the same
|
|
89
|
+
* per-entry validation (`validateMetaEntry`, which `parseMetaPair` also
|
|
90
|
+
* uses), the same key-count cap, and the same aggregate byte cap — without
|
|
91
|
+
* round-tripping through "k=v" string reconstruction and re-parsing first.
|
|
92
|
+
* Throws `UsageError`.
|
|
93
|
+
*/
|
|
94
|
+
export function validateMetaMap(meta) {
|
|
95
|
+
const entries = Object.entries(meta);
|
|
96
|
+
if (entries.length > META_MAX_KEYS) {
|
|
97
|
+
throw new UsageError(`too many --meta pairs: at most ${META_MAX_KEYS} per request`);
|
|
98
|
+
}
|
|
99
|
+
for (const [key, value] of entries)
|
|
100
|
+
validateMetaEntry(key, value);
|
|
101
|
+
enforceMetaByteCap(entries);
|
|
102
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-side dual-host helpers (published package; no @uploads/storage dep).
|
|
3
|
+
* Keep behavior aligned with packages/storage.
|
|
4
|
+
*
|
|
5
|
+
* `UPLOADS_EMBED_PUBLIC_BASE_URL`: unset = default twin; empty = disable; URL = override.
|
|
6
|
+
*/
|
|
7
|
+
export declare const DEFAULT_EMBED_PUBLIC_BASE_URL = "https://embed.uploads.sh";
|
|
8
|
+
export type ClientEmbedUrlOptions = {
|
|
9
|
+
embedBaseUrl?: string | null;
|
|
10
|
+
publicBaseUrl?: string | null;
|
|
11
|
+
};
|
|
12
|
+
export declare function embedBaseUrlFromEnv(env?: NodeJS.ProcessEnv): string | null | undefined;
|
|
13
|
+
export declare function resolveEmbedBaseUrl(publicBaseUrl?: string | null, embedBaseUrl?: string | null): string | null;
|
|
14
|
+
export declare function embedUrlFromPublic(publicObjectUrl: string | null | undefined, opts?: ClientEmbedUrlOptions): string | null;
|
|
15
|
+
/** Prefer API `embedUrl`; otherwise derive (incl. env override). */
|
|
16
|
+
export declare function resolveEmbedUrl(publicObjectUrl: string | null | undefined, apiEmbedUrl?: string | null, opts?: ClientEmbedUrlOptions): string | null;
|
|
17
|
+
/** Prefer embed host for GitHub markdown; fall back to stable public URL. */
|
|
18
|
+
export declare function urlForGithubEmbed(publicObjectUrl: string | null | undefined, apiEmbedUrl?: string | null, opts?: ClientEmbedUrlOptions): string | null;
|