@buildinternet/uploads 0.21.0 → 0.22.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 +12 -1
- package/dist/client.js +11 -2
- package/dist/commands/login.d.ts +23 -1
- package/dist/commands/login.js +78 -10
- package/dist/commands.js +21 -6
- package/dist/github.d.ts +11 -0
- package/dist/github.js +52 -3
- package/package.json +1 -1
package/dist/client.d.ts
CHANGED
|
@@ -41,6 +41,8 @@ export interface ListOptions {
|
|
|
41
41
|
prefix?: string;
|
|
42
42
|
limit?: number;
|
|
43
43
|
cursor?: string;
|
|
44
|
+
/** Hydrate each row's queryable D1 metadata (`?metadata=1`). */
|
|
45
|
+
metadata?: boolean;
|
|
44
46
|
}
|
|
45
47
|
export interface FindFilesOptions {
|
|
46
48
|
prefix?: string;
|
|
@@ -90,6 +92,8 @@ export interface ListItem {
|
|
|
90
92
|
pageUrl?: string;
|
|
91
93
|
size?: number;
|
|
92
94
|
uploaded?: string;
|
|
95
|
+
/** Present only when listed with `metadata: true`, and only for keys that have rows. */
|
|
96
|
+
metadata?: Record<string, string>;
|
|
93
97
|
}
|
|
94
98
|
export interface ListResult {
|
|
95
99
|
items: ListItem[];
|
|
@@ -351,7 +355,14 @@ export interface DeviceCodeResponse {
|
|
|
351
355
|
interval: number;
|
|
352
356
|
}
|
|
353
357
|
/** POST /api/auth/device/code — start a device flow. Throws on a non-2xx. */
|
|
354
|
-
export declare function requestDeviceCode(authUrl: string, clientId?: string
|
|
358
|
+
export declare function requestDeviceCode(authUrl: string, clientId?: string,
|
|
359
|
+
/**
|
|
360
|
+
* RFC 8628 `scope`. Carries the requested workspace (`workspace:<slug>`,
|
|
361
|
+
* plus `create`) so the approval page can validate it before approving —
|
|
362
|
+
* issue #362. Stored on the device-code row and echoed back at token
|
|
363
|
+
* exchange, possibly rewritten by the page.
|
|
364
|
+
*/
|
|
365
|
+
scope?: string): Promise<DeviceCodeResponse>;
|
|
355
366
|
/**
|
|
356
367
|
* One poll of POST /api/auth/device/token. Unlike most calls, the "not ready
|
|
357
368
|
* yet" outcomes (`authorization_pending`, `slow_down`) are EXPECTED 400s, so
|
package/dist/client.js
CHANGED
|
@@ -56,14 +56,21 @@ export function cliUserAgent(purpose = "device-login") {
|
|
|
56
56
|
return `@buildinternet/uploads/${packageVersion()} (${purpose})`;
|
|
57
57
|
}
|
|
58
58
|
/** POST /api/auth/device/code — start a device flow. Throws on a non-2xx. */
|
|
59
|
-
export function requestDeviceCode(authUrl, clientId = DEVICE_CLIENT_ID
|
|
59
|
+
export function requestDeviceCode(authUrl, clientId = DEVICE_CLIENT_ID,
|
|
60
|
+
/**
|
|
61
|
+
* RFC 8628 `scope`. Carries the requested workspace (`workspace:<slug>`,
|
|
62
|
+
* plus `create`) so the approval page can validate it before approving —
|
|
63
|
+
* issue #362. Stored on the device-code row and echoed back at token
|
|
64
|
+
* exchange, possibly rewritten by the page.
|
|
65
|
+
*/
|
|
66
|
+
scope) {
|
|
60
67
|
return jsonRequest(`${authUrl.replace(/\/$/, "")}/api/auth/device/code`, {
|
|
61
68
|
method: "POST",
|
|
62
69
|
headers: {
|
|
63
70
|
"Content-Type": "application/json",
|
|
64
71
|
"User-Agent": cliUserAgent("device-code"),
|
|
65
72
|
},
|
|
66
|
-
body: JSON.stringify({ client_id: clientId }),
|
|
73
|
+
body: JSON.stringify({ client_id: clientId, ...(scope ? { scope } : {}) }),
|
|
67
74
|
});
|
|
68
75
|
}
|
|
69
76
|
export async function requestDeviceToken(authUrl, input) {
|
|
@@ -296,6 +303,8 @@ export function createUploadsClient(config) {
|
|
|
296
303
|
params.set("limit", String(opts.limit));
|
|
297
304
|
if (opts.cursor)
|
|
298
305
|
params.set("cursor", opts.cursor);
|
|
306
|
+
if (opts.metadata)
|
|
307
|
+
params.set("metadata", "1");
|
|
299
308
|
const qs = params.toString();
|
|
300
309
|
const page = await request("GET", `${filesBase(config)}${qs ? `?${qs}` : ""}`);
|
|
301
310
|
return {
|
package/dist/commands/login.d.ts
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
import { parseCommandArgs } from "../cli-args.js";
|
|
2
2
|
export declare function validateEnrollmentCode(raw: string): string;
|
|
3
|
+
/** The scope the CLI sends with its device-code request. No workspace requested → no scope. */
|
|
4
|
+
export declare function formatDeviceScope(workspace: string | undefined, create: boolean): string | undefined;
|
|
5
|
+
/**
|
|
6
|
+
* Read back what the approval page decided. A surviving `create` token means
|
|
7
|
+
* the page left the scope alone and deferred provisioning to the CLI; a bare
|
|
8
|
+
* `workspace:<slug>` means the browser recorded a choice and wins.
|
|
9
|
+
*/
|
|
10
|
+
export declare function parseDeviceScope(scope: string | undefined): {
|
|
11
|
+
workspace: string | undefined;
|
|
12
|
+
create: boolean;
|
|
13
|
+
};
|
|
3
14
|
export declare function resolveEnrollmentCode(parsed: ReturnType<typeof parseCommandArgs>, io?: {
|
|
4
15
|
isTTY: boolean;
|
|
5
16
|
readLine: () => Promise<string>;
|
|
@@ -23,10 +34,21 @@ export interface DeviceLoginIo {
|
|
|
23
34
|
promptWorkspaceName: () => Promise<string>;
|
|
24
35
|
}
|
|
25
36
|
export declare const defaultDeviceIo: DeviceLoginIo;
|
|
37
|
+
/** A completed device authorization: the session bearer plus the (possibly rewritten) scope. */
|
|
38
|
+
export interface DeviceSession {
|
|
39
|
+
accessToken: string;
|
|
40
|
+
scope: string;
|
|
41
|
+
}
|
|
26
42
|
/**
|
|
27
43
|
* Browser device-authorization session only (no workspace token mint).
|
|
28
44
|
* Shared by `uploads login` and `uploads invite create`.
|
|
29
45
|
*/
|
|
46
|
+
export declare function obtainDeviceSession(authUrl: string, opts?: {
|
|
47
|
+
noOpen?: boolean;
|
|
48
|
+
prompt?: string;
|
|
49
|
+
scope?: string;
|
|
50
|
+
}, io?: DeviceLoginIo): Promise<DeviceSession>;
|
|
51
|
+
/** Session bearer only — `invite create` has no workspace to resolve. */
|
|
30
52
|
export declare function obtainDeviceAccessToken(authUrl: string, opts?: {
|
|
31
53
|
noOpen?: boolean;
|
|
32
54
|
prompt?: string;
|
|
@@ -36,7 +58,7 @@ export declare function pollForDeviceToken(authUrl: string, code: {
|
|
|
36
58
|
device_code: string;
|
|
37
59
|
interval: number;
|
|
38
60
|
expires_in: number;
|
|
39
|
-
}, io: DeviceLoginIo): Promise<
|
|
61
|
+
}, io: DeviceLoginIo): Promise<DeviceSession>;
|
|
40
62
|
export declare function runLogin(args: string[], opts: {
|
|
41
63
|
json?: boolean;
|
|
42
64
|
apiUrl?: string;
|
package/dist/commands/login.js
CHANGED
|
@@ -7,15 +7,17 @@ import { createUploadsClient, createWorkspaceRequest, exchangeEnrollment, listMi
|
|
|
7
7
|
import { flagBool, flagString, parseCommandArgs, UsageError } from "../cli-args.js";
|
|
8
8
|
import { parseScopes } from "./admin-enrollment.js";
|
|
9
9
|
import { writeCommandHelp } from "../cli-style.js";
|
|
10
|
+
import { UploadsError } from "../errors.js";
|
|
10
11
|
const HELP = `uploads login [options]
|
|
11
12
|
|
|
12
13
|
Sign in and save workspace credentials. With no flags, opens a browser to
|
|
13
|
-
authorize this device — the recommended way to sign in.
|
|
14
|
-
|
|
14
|
+
authorize this device — the recommended way to sign in. The browser asks which
|
|
15
|
+
workspace to sign in to, so --workspace is optional. Pass an enrollment code
|
|
16
|
+
only if you were given one from before device login (fallback path).
|
|
15
17
|
|
|
16
18
|
Options:
|
|
17
|
-
--workspace <name>
|
|
18
|
-
|
|
19
|
+
--workspace <name> Preselect this workspace in the browser (device flow);
|
|
20
|
+
you can still change it there
|
|
19
21
|
--create With --workspace: create the workspace first if your
|
|
20
22
|
account doesn't have it yet (device flow only) — lets
|
|
21
23
|
scripted/agent logins provision without a prompt
|
|
@@ -46,6 +48,34 @@ export function validateEnrollmentCode(raw) {
|
|
|
46
48
|
throw new UsageError("invalid enrollment code");
|
|
47
49
|
return code;
|
|
48
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* Device-code scope vocabulary (issue #362). Mirrors `parseDeviceScope` /
|
|
53
|
+
* `workspaceScopeValue` in apps/auth/src/device-workspace.ts — this package
|
|
54
|
+
* ships with no workspace dependencies, so the two copies are deliberately
|
|
55
|
+
* independent. Keep the vocabulary in sync.
|
|
56
|
+
*/
|
|
57
|
+
const WORKSPACE_SCOPE_PREFIX = "workspace:";
|
|
58
|
+
const CREATE_SCOPE_TOKEN = "create";
|
|
59
|
+
/** The scope the CLI sends with its device-code request. No workspace requested → no scope. */
|
|
60
|
+
export function formatDeviceScope(workspace, create) {
|
|
61
|
+
if (!workspace)
|
|
62
|
+
return undefined;
|
|
63
|
+
return create
|
|
64
|
+
? `${WORKSPACE_SCOPE_PREFIX}${workspace} ${CREATE_SCOPE_TOKEN}`
|
|
65
|
+
: `${WORKSPACE_SCOPE_PREFIX}${workspace}`;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Read back what the approval page decided. A surviving `create` token means
|
|
69
|
+
* the page left the scope alone and deferred provisioning to the CLI; a bare
|
|
70
|
+
* `workspace:<slug>` means the browser recorded a choice and wins.
|
|
71
|
+
*/
|
|
72
|
+
export function parseDeviceScope(scope) {
|
|
73
|
+
const tokens = (scope ?? "").split(/\s+/).filter(Boolean);
|
|
74
|
+
const slug = tokens
|
|
75
|
+
.find((t) => t.startsWith(WORKSPACE_SCOPE_PREFIX))
|
|
76
|
+
?.slice(WORKSPACE_SCOPE_PREFIX.length) ?? "";
|
|
77
|
+
return { workspace: slug || undefined, create: tokens.includes(CREATE_SCOPE_TOKEN) };
|
|
78
|
+
}
|
|
49
79
|
async function readLine() {
|
|
50
80
|
let out = "";
|
|
51
81
|
for await (const chunk of stdin) {
|
|
@@ -187,8 +217,8 @@ export const defaultDeviceIo = {
|
|
|
187
217
|
* Browser device-authorization session only (no workspace token mint).
|
|
188
218
|
* Shared by `uploads login` and `uploads invite create`.
|
|
189
219
|
*/
|
|
190
|
-
export async function
|
|
191
|
-
const code = await requestDeviceCode(authUrl);
|
|
220
|
+
export async function obtainDeviceSession(authUrl, opts = {}, io = defaultDeviceIo) {
|
|
221
|
+
const code = await requestDeviceCode(authUrl, undefined, opts.scope);
|
|
192
222
|
const verifyUrl = code.verification_uri_complete ?? code.verification_uri;
|
|
193
223
|
const prompt = opts.prompt ?? "To sign in, open:";
|
|
194
224
|
io.write(`${prompt}\n\n ${verifyUrl}\n\nand confirm this code:\n\n ${code.user_code}\n\n`);
|
|
@@ -197,6 +227,31 @@ export async function obtainDeviceAccessToken(authUrl, opts = {}, io = defaultDe
|
|
|
197
227
|
io.write("Waiting for approval…\n");
|
|
198
228
|
return pollForDeviceToken(authUrl, code, io);
|
|
199
229
|
}
|
|
230
|
+
/** Session bearer only — `invite create` has no workspace to resolve. */
|
|
231
|
+
export async function obtainDeviceAccessToken(authUrl, opts = {}, io = defaultDeviceIo) {
|
|
232
|
+
return (await obtainDeviceSession(authUrl, opts, io)).accessToken;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Turn the API's deliberately opaque 403 (`no access to this workspace` — it
|
|
236
|
+
* refuses to distinguish "doesn't exist" from "you're not a member", see
|
|
237
|
+
* apps/api/src/routes/tokens.ts) into something the user can act on, by
|
|
238
|
+
* listing the workspaces their own account CAN reach. Backstop only: since
|
|
239
|
+
* #362 the approval page catches this before approving.
|
|
240
|
+
*/
|
|
241
|
+
async function describeMintFailure(apiUrl, accessToken, workspace, err) {
|
|
242
|
+
if (!(err instanceof UploadsError) || err.status !== 403)
|
|
243
|
+
throw err;
|
|
244
|
+
let names = [];
|
|
245
|
+
try {
|
|
246
|
+
names = (await listMintWorkspaces(apiUrl, accessToken)).workspaces.map((w) => w.workspace);
|
|
247
|
+
}
|
|
248
|
+
catch {
|
|
249
|
+
// Listing is best-effort — fall through to the generic hint below.
|
|
250
|
+
}
|
|
251
|
+
throw new UsageError(names.length
|
|
252
|
+
? `no access to workspace "${workspace}" — this account can use: ${names.join(", ")}`
|
|
253
|
+
: `no access to workspace "${workspace}" — this account has no workspaces yet; pass --workspace <name> --create to provision one`);
|
|
254
|
+
}
|
|
200
255
|
/**
|
|
201
256
|
* Device-authorization login (RFC 8628): request a code, have the user approve
|
|
202
257
|
* it in a browser, poll for the session token, then mint a workspace token.
|
|
@@ -217,9 +272,22 @@ async function runDeviceLogin(parsed, opts, io) {
|
|
|
217
272
|
// Make the target explicit: a bare `uploads login` on a self-hosted install
|
|
218
273
|
// would otherwise silently sign in to the cloud service.
|
|
219
274
|
io.write(`signing in to ${opts.authUrl} (self-hosted? pass --api-url or set UPLOADS_API_URL)\n\n`);
|
|
220
|
-
const
|
|
221
|
-
const
|
|
222
|
-
|
|
275
|
+
const create = flagBool(parsed.flags, "--create");
|
|
276
|
+
const session = await obtainDeviceSession(opts.authUrl, { noOpen: opts.noOpen, scope: formatDeviceScope(requestedWorkspace, create) }, io);
|
|
277
|
+
// The approval page is authoritative: it validated the workspace against the
|
|
278
|
+
// signed-in account's memberships (and may have created a new one) before
|
|
279
|
+
// approving. A scope that still carries `create` means the page deferred to
|
|
280
|
+
// the CLI, and an empty one means an older server that doesn't echo a
|
|
281
|
+
// choice — both fall back to the local resolution below.
|
|
282
|
+
const chosen = parseDeviceScope(session.scope);
|
|
283
|
+
const workspace = chosen.workspace && !chosen.create
|
|
284
|
+
? chosen.workspace
|
|
285
|
+
: await resolveMintWorkspace(opts.apiUrl, session.accessToken, requestedWorkspace, io, create);
|
|
286
|
+
const minted = await mintWorkspaceToken(opts.apiUrl, session.accessToken, {
|
|
287
|
+
workspace,
|
|
288
|
+
scopes,
|
|
289
|
+
label,
|
|
290
|
+
}).catch((err) => describeMintFailure(opts.apiUrl, session.accessToken, workspace, err));
|
|
223
291
|
return { workspace: minted.workspace, token: minted.token, apiUrl: opts.apiUrl };
|
|
224
292
|
}
|
|
225
293
|
function safeHostname() {
|
|
@@ -247,7 +315,7 @@ export async function pollForDeviceToken(authUrl, code, io) {
|
|
|
247
315
|
}
|
|
248
316
|
switch (result.status) {
|
|
249
317
|
case "ok":
|
|
250
|
-
return result.accessToken;
|
|
318
|
+
return { accessToken: result.accessToken, scope: result.scope };
|
|
251
319
|
case "pending":
|
|
252
320
|
continue;
|
|
253
321
|
case "slow_down":
|
package/dist/commands.js
CHANGED
|
@@ -464,12 +464,27 @@ export async function syncAttachmentsComment(client, target, run, workspace) {
|
|
|
464
464
|
}
|
|
465
465
|
}
|
|
466
466
|
// gh fallback: gather from this workspace's own data and post via local `gh`.
|
|
467
|
-
// Note (
|
|
468
|
-
// scope, so it cannot honor a workspace's
|
|
469
|
-
//
|
|
470
|
-
//
|
|
471
|
-
//
|
|
472
|
-
|
|
467
|
+
// Note (issues #304, #365): this CLI process has no server-side
|
|
468
|
+
// WorkspaceRecord in scope, so it cannot honor a workspace's
|
|
469
|
+
// githubCommentLinkToFilePage=false or githubCommentShowMetadata=false — it
|
|
470
|
+
// always links to the file page and always shows metadata here, matching the
|
|
471
|
+
// defaults. This only diverges from the bot-posted comment for a workspace
|
|
472
|
+
// that both sets one of those flags false and falls through to this path.
|
|
473
|
+
const items = (await client.listAll({ prefix: ghKeyPrefix(target), metadata: true })).map(({ key, url, embedUrl, pageUrl, metadata }) => {
|
|
474
|
+
// The list endpoint returns every metadata key; the comment renders only
|
|
475
|
+
// these two. Narrowing here keeps both render paths byte-identical.
|
|
476
|
+
const path = metadata?.path;
|
|
477
|
+
const state = metadata?.state;
|
|
478
|
+
return {
|
|
479
|
+
key,
|
|
480
|
+
url,
|
|
481
|
+
embedUrl,
|
|
482
|
+
pageUrl,
|
|
483
|
+
...(path || state
|
|
484
|
+
? { meta: { ...(path ? { path } : {}), ...(state ? { state } : {}) } }
|
|
485
|
+
: {}),
|
|
486
|
+
};
|
|
487
|
+
});
|
|
473
488
|
const galleries = [];
|
|
474
489
|
let cursor;
|
|
475
490
|
do {
|
package/dist/github.d.ts
CHANGED
|
@@ -75,6 +75,17 @@ export interface AttachmentItem {
|
|
|
75
75
|
embedUrl?: string | null;
|
|
76
76
|
/** Canonical `/f/` file-page URL (server-computed). Preferred click-through target; falls back to `url`. */
|
|
77
77
|
pageUrl?: string | null;
|
|
78
|
+
/**
|
|
79
|
+
* The only canonical metadata the managed comment renders (issue #365).
|
|
80
|
+
* Deliberately two named fields rather than `Record<string, string>`: the
|
|
81
|
+
* comment is posted publicly, and keeping the set narrow at the type level
|
|
82
|
+
* mirrors the server-side query filter that never fetches EXIF-derived
|
|
83
|
+
* keys like `device`/`software` for this path.
|
|
84
|
+
*/
|
|
85
|
+
meta?: {
|
|
86
|
+
path?: string;
|
|
87
|
+
state?: string;
|
|
88
|
+
};
|
|
78
89
|
}
|
|
79
90
|
/** A public gallery linked to the PR or issue whose managed comment is syncing. */
|
|
80
91
|
export interface GalleryCommentItem {
|
package/dist/github.js
CHANGED
|
@@ -177,6 +177,51 @@ function escapeHtmlAttr(s) {
|
|
|
177
177
|
function escapeHtmlText(s) {
|
|
178
178
|
return escapeHtmlAttr(s).replace(/'/g, "'").replace(/>/g, ">");
|
|
179
179
|
}
|
|
180
|
+
/**
|
|
181
|
+
* Backslash-escape the markdown metacharacters that can appear in a metadata
|
|
182
|
+
* value. `~` is in the set because GitHub's strikethrough extension treats a
|
|
183
|
+
* matching pair of ONE or two tildes as markup, so an unescaped `/a~b~c` would
|
|
184
|
+
* render with `b` struck through.
|
|
185
|
+
*/
|
|
186
|
+
function escapeMarkdownText(s) {
|
|
187
|
+
return s.replace(/([\\`*_[\]~])/g, "\\$1");
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* An attachment's caption parts — `path`, then `state` (issue #365). Empty
|
|
191
|
+
* when neither is usable, so callers emit nothing at all and a body with no
|
|
192
|
+
* metadata stays byte-identical to the pre-#365 render.
|
|
193
|
+
*
|
|
194
|
+
* Neither value is pre-sanitized: metadata values are printable ASCII up to
|
|
195
|
+
* 512 chars, and while the CLI validates `--state` against a closed enum,
|
|
196
|
+
* `PATCH /v1/:workspace/files/:key` can set any valid metadata value. A
|
|
197
|
+
* whitespace-only value passes that validation (length-1 printable ASCII), so
|
|
198
|
+
* treat it as absent rather than rendering a dangling separator.
|
|
199
|
+
*/
|
|
200
|
+
function metaCaptionParts(meta) {
|
|
201
|
+
const parts = [];
|
|
202
|
+
for (const value of [meta?.path, meta?.state]) {
|
|
203
|
+
const trimmed = value?.trim();
|
|
204
|
+
if (trimmed)
|
|
205
|
+
parts.push(trimmed);
|
|
206
|
+
}
|
|
207
|
+
return parts;
|
|
208
|
+
}
|
|
209
|
+
/** `<sub>` caption body for an inline image, or null when there is nothing to say. */
|
|
210
|
+
function metaCaptionHtml(meta) {
|
|
211
|
+
const parts = metaCaptionParts(meta);
|
|
212
|
+
return parts.length > 0 ? parts.map(escapeHtmlText).join(" · ") : null;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* ` · …` suffix for a markdown list row, or `""` when there is nothing to add.
|
|
216
|
+
* HTML-escapes first, then markdown-escapes: HTML escaping introduces no
|
|
217
|
+
* backslashes or brackets, so the markdown pass cannot corrupt its entities.
|
|
218
|
+
*/
|
|
219
|
+
function metaCaptionMarkdown(meta) {
|
|
220
|
+
const parts = metaCaptionParts(meta);
|
|
221
|
+
if (parts.length === 0)
|
|
222
|
+
return "";
|
|
223
|
+
return ` · ${parts.map((p) => escapeMarkdownText(escapeHtmlText(p))).join(" · ")}`;
|
|
224
|
+
}
|
|
180
225
|
/**
|
|
181
226
|
* Render the one marker-owned GitHub comment. When there are no galleries this
|
|
182
227
|
* intentionally preserves the legacy attachment-only body byte-for-byte.
|
|
@@ -224,13 +269,16 @@ export function attachmentsCommentBody(items, galleries = [], marker = ATTACHMEN
|
|
|
224
269
|
const href = escapeHtmlAttr(link ?? src);
|
|
225
270
|
const imgSrc = escapeHtmlAttr(src);
|
|
226
271
|
lines.push(`<a href="${href}"><img width="${w}" alt="${alt}" src="${imgSrc}"></a>`);
|
|
272
|
+
const caption = metaCaptionHtml(item.meta);
|
|
273
|
+
if (caption)
|
|
274
|
+
lines.push(`<sub>${caption}</sub>`);
|
|
227
275
|
lines.push("");
|
|
228
276
|
}
|
|
229
277
|
else if (link) {
|
|
230
|
-
lines.push(`- [${name}](${link})`);
|
|
278
|
+
lines.push(`- [${name}](${link})${metaCaptionMarkdown(item.meta)}`);
|
|
231
279
|
}
|
|
232
280
|
else {
|
|
233
|
-
lines.push(`- ${name}`);
|
|
281
|
+
lines.push(`- ${name}${metaCaptionMarkdown(item.meta)}`);
|
|
234
282
|
}
|
|
235
283
|
}
|
|
236
284
|
if (overflowImages.length > 0) {
|
|
@@ -239,7 +287,8 @@ export function attachmentsCommentBody(items, galleries = [], marker = ATTACHMEN
|
|
|
239
287
|
for (const item of overflowImages) {
|
|
240
288
|
const name = item.key.slice(item.key.lastIndexOf("/") + 1);
|
|
241
289
|
const link = item.pageUrl ?? item.url;
|
|
242
|
-
|
|
290
|
+
const suffix = metaCaptionMarkdown(item.meta);
|
|
291
|
+
lines.push(link ? `- [${name}](${link})${suffix}` : `- ${name}${suffix}`);
|
|
243
292
|
}
|
|
244
293
|
lines.push("", "</details>", "");
|
|
245
294
|
}
|