@repo-toolkit/confluence 0.9.0 → 0.12.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.
Files changed (5) hide show
  1. package/README.md +140 -17
  2. package/cli.js +1381 -273
  3. package/index.d.ts +288 -34
  4. package/index.js +1165 -230
  5. package/package.json +2 -2
package/index.d.ts CHANGED
@@ -1,5 +1,22 @@
1
1
  import { FlagSpec } from '@repo-toolkit/publish-package';
2
- export { FlagSpec, isPlainObject, parseFlags, resolveCliOptions } from '@repo-toolkit/publish-package';
2
+
3
+ interface MermaidBlock {
4
+ id: string;
5
+ source: string;
6
+ }
7
+ interface MarkdownConvertResult {
8
+ html: string;
9
+ mermaidBlocks: MermaidBlock[];
10
+ }
11
+ interface MarkdownConvertOptions {
12
+ /** Render ```html fenced blocks as inline HTML via the Confluence `html` macro instead of a code box. Default: false. */
13
+ renderHtmlBlocks?: boolean;
14
+ }
15
+ declare function isAllowedUrl(url: string): boolean;
16
+ declare function markdownToStorage(markdown: string, options?: MarkdownConvertOptions): MarkdownConvertResult;
17
+ declare function isRemoteUrl(src: string): boolean;
18
+ declare function escapeXmlAttribute(text: string): string;
19
+ declare function escapeAttachmentFilename(filename: string): string;
3
20
 
4
21
  interface ConfluenceClientOptions {
5
22
  baseUrl: string;
@@ -7,6 +24,41 @@ interface ConfluenceClientOptions {
7
24
  apiToken: string;
8
25
  fetch?: typeof fetch;
9
26
  userAgent?: string;
27
+ /** Timeout (ms) applied to every HTTP request. Default 30000. */
28
+ requestTimeoutMs?: number;
29
+ /** Retries for 429 / 5xx on safe methods (GET/HEAD/OPTIONS). Default 3; never applied to writes. */
30
+ maxRetries?: number;
31
+ /** Per-file maximum attachment upload size in bytes. Default 50 MB. */
32
+ maxUploadBytes?: number;
33
+ }
34
+ /**
35
+ * Narrow gateway covering only the attachment-list and binary upload methods the
36
+ * image/mermaid rewriters need. `ConfluenceClient` implements this interface;
37
+ * tests inject fakes that implement just these three methods.
38
+ */
39
+ interface AttachmentGateway {
40
+ getAttachments(pageId: string): Promise<Attachment[]>;
41
+ uploadAttachment(pageId: string, filePath: string, comment?: string, filename?: string): Promise<Attachment>;
42
+ updateAttachmentData(pageId: string, attachmentId: string, filePath: string, comment?: string, filename?: string): Promise<Attachment>;
43
+ }
44
+ /**
45
+ * Narrow remote-mutation boundary consumed by the sync orchestrator and the
46
+ * image/mermaid rewriters. `ConfluenceClient` implements this interface; tests
47
+ * may inject any object whose method shapes match (typed fakes — no `unknown`
48
+ * cast required). Replacing the client also replaces the credentials/baseUrl
49
+ * requirement: the sync orchestrator relies solely on this gateway for remote
50
+ * work, so callers that supply their own gateway never need dummy credentials.
51
+ *
52
+ * The interface is the supported contract for custom clients: every method
53
+ * listed here is one the sync actually calls, and the signatures match the v1
54
+ * multipart + v2 JSON contract the bundled `ConfluenceClient` honors.
55
+ */
56
+ interface ConfluenceGateway extends AttachmentGateway {
57
+ getSpaceIdByKey(spaceKey: string): Promise<string>;
58
+ getPagesByTitle(spaceId: string, title: string): Promise<Page[]>;
59
+ getPage(pageId: string): Promise<Page>;
60
+ createPage(input: CreatePageInput): Promise<Page>;
61
+ updatePage(input: UpdatePageInput): Promise<Page>;
10
62
  }
11
63
  interface PageBody {
12
64
  representation: 'storage' | 'atlas_doc' | 'wiki' | 'view' | 'export_view';
@@ -78,22 +130,27 @@ declare class ConfluenceApiError extends Error {
78
130
  readonly responseBody: string;
79
131
  constructor(message: string, status: number, endpoint: string, responseBody: string);
80
132
  }
81
- declare class ConfluenceClient {
133
+ declare class ConfluenceClient implements ConfluenceGateway {
82
134
  private readonly baseUrl;
135
+ private readonly baseUrlOrigin;
83
136
  private readonly authHeader;
84
137
  private readonly fetchFn;
85
138
  private readonly userAgent;
139
+ private readonly requestTimeoutMs;
140
+ private readonly maxRetries;
141
+ private readonly maxUploadBytes;
86
142
  constructor(options: ConfluenceClientOptions);
87
143
  getSpaceIdByKey(spaceKey: string): Promise<string>;
88
- getPageByTitle(spaceId: string, title: string): Promise<Page | undefined>;
144
+ getPagesByTitle(spaceId: string, title: string): Promise<Page[]>;
89
145
  getPage(pageId: string): Promise<Page>;
90
146
  createPage(input: CreatePageInput): Promise<Page>;
91
147
  updatePage(input: UpdatePageInput): Promise<Page>;
92
148
  getAttachments(pageId: string): Promise<Attachment[]>;
93
- uploadAttachment(pageId: string, filePath: string, comment?: string): Promise<Attachment>;
94
- updateAttachmentData(pageId: string, attachmentId: string, filePath: string, comment?: string): Promise<Attachment>;
149
+ uploadAttachment(pageId: string, filePath: string, comment?: string, filename?: string): Promise<Attachment>;
150
+ updateAttachmentData(pageId: string, attachmentId: string, filePath: string, comment?: string, filename?: string): Promise<Attachment>;
95
151
  private sendAttachmentMultipart;
96
152
  private requestJson;
153
+ private makeTimeoutSignal;
97
154
  private v2Url;
98
155
  private v1Url;
99
156
  }
@@ -111,26 +168,6 @@ declare function readDocTree(root: string, depth?: number): Promise<DocTree>;
111
168
  declare function isMarkdownName(name: string): boolean;
112
169
  declare function titleFromSegment(segment: string): string;
113
170
 
114
- interface MermaidBlock {
115
- id: string;
116
- source: string;
117
- }
118
- interface MarkdownConvertResult {
119
- html: string;
120
- mermaidBlocks: MermaidBlock[];
121
- }
122
- interface MarkdownConvertOptions {
123
- /** Render ```html fenced blocks as inline HTML via the Confluence `html` macro instead of a code box. Default: false. */
124
- renderHtmlBlocks?: boolean;
125
- }
126
- declare function markdownToStorage(markdown: string, options?: MarkdownConvertOptions): MarkdownConvertResult;
127
- declare function renderHtmlBlock(code: string): string;
128
- declare function renderInline(text: string): string;
129
- declare const LOCAL_IMAGE_PLACEHOLDER_RE: RegExp;
130
- declare function isRemoteUrl(src: string): boolean;
131
- declare function escapeXmlAttribute(text: string): string;
132
- declare function escapeAttachmentFilename(filename: string): string;
133
-
134
171
  interface RewriteResult {
135
172
  html: string;
136
173
  uploaded: ReadonlyArray<{
@@ -138,21 +175,112 @@ interface RewriteResult {
138
175
  attachment: Attachment;
139
176
  }>;
140
177
  }
178
+ /**
179
+ * Equivalent of {@link RewriteResult} for a preflight pass: it produces the
180
+ * final HTML that would be emitted if uploads were allowed, plus the set of
181
+ * local sources that would require either a fresh upload or a content update.
182
+ *
183
+ * A preflight is `predictable` when every placeholder resolves to an existing
184
+ * attachment whose stored content hash matches the local source hash; in that
185
+ * state no upload is required and the produced HTML is byte-identical to a full
186
+ * rewrite.
187
+ */
188
+ interface PreflightResult {
189
+ html: string;
190
+ /** Sources that would upload or update their attachment on a real rewrite. */
191
+ pending: ReadonlyArray<{
192
+ src: string;
193
+ filename: string;
194
+ hash: string;
195
+ }>;
196
+ /** Sources that already have a matching-content attachment and would be reused. */
197
+ reused: ReadonlyArray<{
198
+ src: string;
199
+ attachment: Attachment;
200
+ }>;
201
+ }
141
202
  interface RewriteOptions {
142
203
  /** Directory used to resolve relative image src values. The markdown file's own dir. */
143
204
  markdownDir: string;
205
+ /** Documentation root used to confine local file reads. */
206
+ allowedRoot: string;
207
+ /** Maximum size accepted for an individual attachment source. */
208
+ maxAttachmentBytes?: number;
209
+ /** Maximum total size accepted across all uploaded sources in one document. */
210
+ maxTotalAttachmentBytes?: number;
211
+ }
212
+ /**
213
+ * Resolved view of a local image source after the same root-confined,
214
+ * regular-file, and size validation that {@link rewriteImagesToAttachments}
215
+ * applies at upload time. Used by the CFARC-03 local preflight so the dry
216
+ * validation pass before any API mutation never relaxes the trust boundary.
217
+ */
218
+ interface ValidatedAttachmentSource {
219
+ /** Original `src` as it appeared in the markdown. */
220
+ src: string;
221
+ /** Real, root-confined absolute path on disk. */
222
+ abs: string;
223
+ /** Resolved content-addressed attachment filename (`<stem>-<hash16>.<ext>`). */
224
+ filename: string;
144
225
  }
145
- declare function rewriteImagesToAttachments(html: string, pageId: string, client: ConfluenceClient, options: RewriteOptions): Promise<RewriteResult>;
226
+ /**
227
+ * Validate every local image placeholder in `html` against the same physical
228
+ * resolution, root confinement, regular-file, and size checks performed at
229
+ * upload time, returning the content-addressed filename each source would get.
230
+ * Throws on the first validation failure with the same message the upload path
231
+ * would produce — so a pre-mutation pass cannot relax the trust boundary.
232
+ *
233
+ * Performs no client work and no upload; reads file metadata and short-hashes
234
+ * file contents only.
235
+ */
236
+ declare function validateAttachmentSources(html: string, options: RewriteOptions): ValidatedAttachmentSource[];
237
+ declare function rewriteImagesToAttachments(html: string, pageId: string, client: AttachmentGateway, options: RewriteOptions): Promise<RewriteResult>;
238
+ /**
239
+ * Compute the final HTML and a pending/reused plan without performing any
240
+ * upload, attachment mutation, or renderer spawn. Used by `skipUnchanged` to
241
+ * compare the would-be body byte-equal against the current page body before
242
+ * any remote mutation is considered.
243
+ *
244
+ * The preflight validates every local source (root confinement, regular
245
+ * file, size limits, missing/escaping) the same way {@link rewriteImagesToAttachments}
246
+ * does, so a dry-validation pass before a page PUT never relaxes the trust
247
+ * boundary established by CFSEC-02.
248
+ */
249
+ declare function preflightImagesToAttachments(html: string, pageId: string, client: AttachmentGateway, options: RewriteOptions): Promise<PreflightResult>;
146
250
 
147
251
  interface MermaidRewriteResult {
148
252
  html: string;
149
- /** Placeholders that could not be rendered (mmdc missing or render failure). Fallback source is in the original code macro. */
150
253
  fallbacks: string[];
151
254
  uploaded: ReadonlyArray<{
152
255
  id: string;
153
256
  attachment: Attachment;
154
257
  }>;
155
258
  }
259
+ /**
260
+ * Equivalent of {@link MermaidRewriteResult} for a preflight pass: it produces
261
+ * the final HTML that would be emitted if rendering and uploads were allowed,
262
+ * plus the set of mermaid blocks that would require a fresh render/upload.
263
+ *
264
+ * A preflight is `predictable` when every mermaid block resolves to an existing
265
+ * attachment whose stored content hash matches the local source hash; in that
266
+ * state no `mmdc` spawn, attachment mutation, or render work is required.
267
+ */
268
+ interface MermaidPreflightResult {
269
+ html: string;
270
+ /** Blocks that would render and/or upload on a real rewrite. */
271
+ pending: ReadonlyArray<{
272
+ id: string;
273
+ filename: string;
274
+ hash: string;
275
+ }>;
276
+ /** Blocks already matching an existing attachment by content hash. */
277
+ reused: ReadonlyArray<{
278
+ id: string;
279
+ attachment: Attachment;
280
+ }>;
281
+ /** Blocks that would fall back to a code macro when `mmdc` is unavailable. */
282
+ fallbacks: string[];
283
+ }
156
284
  interface MermaidRewriteOptions {
157
285
  /** Override the mmdc binary path; otherwise discovered via PATH. */
158
286
  mmdcPath?: string;
@@ -160,8 +288,25 @@ interface MermaidRewriteOptions {
160
288
  renderHook?: (source: string, outFile: string) => Promise<void>;
161
289
  /** Force-enable or force-disable rendering regardless of PATH detection. When true, skips the mmdc probe. */
162
290
  available?: boolean;
291
+ /** Timeout (ms) for the mmdc subprocess and stream accumulation. Default 30000. */
292
+ renderTimeoutMs?: number;
293
+ /** Maximum bytes accumulated from stdout/stderr before rejecting. Default 1 MiB. */
294
+ maxStreamBytes?: number;
163
295
  }
164
- declare function rewriteMermaidBlocks(html: string, blocks: MermaidBlock[], pageId: string, client: ConfluenceClient, options?: MermaidRewriteOptions): Promise<MermaidRewriteResult>;
296
+ declare function rewriteMermaidBlocks(html: string, blocks: MermaidBlock[], pageId: string, client: AttachmentGateway, options?: MermaidRewriteOptions): Promise<MermaidRewriteResult>;
297
+ /**
298
+ * Compute the final HTML and a pending/reused plan without performing any
299
+ * `mmdc` spawn, attachment mutation, or upload. Used by `skipUnchanged` to
300
+ * compare the would-be body byte-equal against the current page body before
301
+ * any remote mutation or renderer work is considered.
302
+ *
303
+ * The preflight is conservative: a block that would fall back to a code macro
304
+ * (because `mmdc` would be unavailable when run) is reported in `fallbacks`
305
+ * and rendered into the predicted HTML as a code macro, matching the real
306
+ * rewrite path. Blocks already present as a code macro in the page body must
307
+ * remain a code macro to keep the comparison stable.
308
+ */
309
+ declare function preflightMermaidBlocks(html: string, blocks: MermaidBlock[], pageId: string, client: AttachmentGateway, options?: MermaidRewriteOptions): Promise<MermaidPreflightResult>;
165
310
 
166
311
  declare const INTERACTIVE_FLAG: FlagSpec;
167
312
  interface ConfluenceSyncOptions {
@@ -181,14 +326,32 @@ interface ConfluenceSyncOptions {
181
326
  cwd?: string;
182
327
  /** Version-message suffix appended to every page/attachment PUT. */
183
328
  versionMessage?: string;
184
- /** Skip uploads that would have no markdown changes (default: true). */
329
+ /**
330
+ * Skip uploads that would have no markdown changes (default: true).
331
+ *
332
+ * When true, a second identical sync performs no page PUT, no attachment
333
+ * mutation, and no Mermaid `mmdc` spawn: attachment and Mermaid names are
334
+ * content-addressed (sha256 of the source), so detecting an unchanged body
335
+ * produces byte-equal storage HTML to the current page body before any
336
+ * render/upload work is performed.
337
+ */
185
338
  skipUnchanged?: boolean;
186
339
  /** Render ```html fenced blocks as inline HTML via the Confluence `html` macro instead of a code box (default: false). */
187
340
  renderHtmlBlocks?: boolean;
188
- /** Dry-run: walk the tree and print the plan but make no API calls. */
341
+ /**
342
+ * Dry-run: walk the tree and validate every markdown file and local image
343
+ * source (same preflight as a real sync) then print the plan, but make no
344
+ * API mutation calls. Credentials are not required under `--dry-run`.
345
+ */
189
346
  dryRun?: boolean;
190
- /** Custom Confluence client instance (testing). When supplied, `username`/`apiToken`/`baseUrl` are ignored. */
191
- client?: ConfluenceClient;
347
+ /**
348
+ * Custom Confluence gateway. Any object whose method shapes match
349
+ * {@link ConfluenceGateway} is accepted (typed fakes — no `unknown` cast).
350
+ * When supplied, `username`/`apiToken`/`baseUrl`/`spaceKey`/`parentPageId`
351
+ * are ignored by the orchestrator and the credential/baseUrl required-field
352
+ * checks are skipped — the gateway owns all remote work.
353
+ */
354
+ client?: ConfluenceGateway;
192
355
  /** Logger sink; defaults to `console`. */
193
356
  log?: (message: string) => void;
194
357
  }
@@ -205,7 +368,98 @@ interface ConfluenceSyncPlan {
205
368
  dryRun: boolean;
206
369
  renderHtmlBlocks: boolean;
207
370
  }
371
+ /** A single markdown entry's locally-validated sync plan. */
372
+ interface LocalSyncEntryPlan {
373
+ /** The original doc-tree entry this plan covers. */
374
+ entry: DocEntry;
375
+ /** Rendered storage HTML from `markdownToStorage` (placeholder macros intact). */
376
+ html: string;
377
+ /** Mermaid blocks parsed from the markdown source. */
378
+ mermaidBlocks: ReadonlyArray<MermaidBlock>;
379
+ /** Markdown file's directory, used to resolve relative image src values. */
380
+ markdownDir: string;
381
+ /** Whether the rendered body contains any local-image placeholders (`<ac:image data-local-src>`). */
382
+ hasLocalImages: boolean;
383
+ /** Whether the rendered body contains any Mermaid placeholder macros. */
384
+ hasMermaidBlocks: boolean;
385
+ /**
386
+ * Resolved content-addressed attachment sources for every local image
387
+ * placeholder, validated (root confinement, regular file, size limits) by
388
+ * the local preflight pass. Empty when the body has no local images.
389
+ */
390
+ attachments: ReadonlyArray<ValidatedAttachmentSource>;
391
+ }
392
+ /** Aggregate result of the local preflight pass over every doc-tree entry. */
393
+ interface LocalSyncPlan {
394
+ entries: ReadonlyArray<LocalSyncEntryPlan>;
395
+ }
396
+ /**
397
+ * Structured report returned from {@link validateLocalSync}. Each defect names
398
+ * the doc-tree path that failed and the error message the upload path would
399
+ * have produced, so callers can fix local inputs without any remote mutation.
400
+ */
401
+ interface LocalSyncValidationError {
402
+ entry: DocEntry;
403
+ error: Error;
404
+ }
405
+ declare class LocalSyncValidationAggregateError extends Error {
406
+ readonly defects: ReadonlyArray<LocalSyncValidationError>;
407
+ constructor(defects: ReadonlyArray<LocalSyncValidationError>);
408
+ }
409
+ /**
410
+ * Pre-read the document tree, convert every Markdown file to storage HTML,
411
+ * and validate every local image source (root confinement, regular file,
412
+ * size limits) using the same {@link validateAttachmentSources} path that
413
+ * {@link rewriteImagesToAttachments} applies at upload time.
414
+ *
415
+ * Runs against local files only — no client, no network, and no API mutation.
416
+ * Throws {@link LocalSyncValidationAggregateError} listing every defect so a
417
+ * caller can fix all local inputs in one pass instead of failing entry by
418
+ * entry after orphan pages or partial uploads have already occurred.
419
+ */
420
+ declare function validateLocalSync(entries: ReadonlyArray<DocEntry>, plan: ConfluenceSyncPlan): LocalSyncPlan;
421
+ /** A page successfully created or updated during the remote mutation phase. */
422
+ interface SyncChange {
423
+ /** Doc-tree entry that was synced. */
424
+ entry: DocEntry;
425
+ /** Confluence page id that was created or updated. */
426
+ pageId: string;
427
+ /** Outcome: `created` (page POSTed, optionally with final body), `updated` (page PUT), or `unchanged` (skipUnchanged no-op). */
428
+ kind: 'created' | 'updated' | 'unchanged';
429
+ }
430
+ /** A single remote-mutation failure during the sync loop. */
431
+ interface SyncFailure {
432
+ /** Doc-tree entry that failed. */
433
+ entry: DocEntry;
434
+ /** Underlying error from the remote call. */
435
+ error: Error;
436
+ }
437
+ /**
438
+ * Structured partial-mutation report. Thrown when the remote mutation phase
439
+ * fails after at least one page has been created/updated. Carries every
440
+ * successful {@link SyncChange}, the {@link SyncFailure} that aborted the run,
441
+ * and the doc-tree entries that remained unprocessed after the abort.
442
+ *
443
+ * The error message echoes the underlying failure message so existing
444
+ * `.rejects.toThrowError(/regex/)` assertions on remote errors keep matching.
445
+ */
446
+ declare class SyncMutationError extends Error {
447
+ readonly changes: ReadonlyArray<SyncChange>;
448
+ readonly failure: SyncFailure;
449
+ readonly unprocessed: ReadonlyArray<DocEntry>;
450
+ constructor(input: {
451
+ changes: ReadonlyArray<SyncChange>;
452
+ failure: SyncFailure;
453
+ unprocessed: ReadonlyArray<DocEntry>;
454
+ });
455
+ }
456
+ /** Result shape returned by {@link syncConfluenceToDocs}. `void` is preserved
457
+ * for callers that ignore the return value; structured evidence is available
458
+ * when a full run completes without error. */
459
+ interface SyncResult {
460
+ changes: ReadonlyArray<SyncChange>;
461
+ }
208
462
  declare function resolveConfluenceSyncPlan(options?: ConfluenceSyncOptions): ConfluenceSyncPlan;
209
- declare function syncConfluenceToDocs(options?: ConfluenceSyncOptions): Promise<void>;
463
+ declare function syncConfluenceToDocs(options?: ConfluenceSyncOptions): Promise<SyncResult | void>;
210
464
 
211
- export { type Attachment, ConfluenceApiError, ConfluenceClient, type ConfluenceClientOptions, type ConfluenceSyncOptions, type ConfluenceSyncPlan, type DocEntry, type DocTree, INTERACTIVE_FLAG, LOCAL_IMAGE_PLACEHOLDER_RE, type MarkdownConvertOptions, type MarkdownConvertResult, type MermaidBlock, type MermaidRewriteOptions, type MermaidRewriteResult, type Page, type PageBody, type PageVersion, escapeAttachmentFilename, escapeXmlAttribute, isMarkdownName, isRemoteUrl, markdownToStorage, readDocTree, renderHtmlBlock, renderInline, resolveConfluenceSyncPlan, resolveConfluenceSyncPlan as resolveSyncPlan, rewriteImagesToAttachments, rewriteMermaidBlocks, syncConfluenceToDocs, titleFromSegment };
465
+ export { type Attachment, type AttachmentGateway, type PreflightResult as AttachmentPreflightResult, ConfluenceApiError, ConfluenceClient, type ConfluenceClientOptions, type ConfluenceGateway, type ConfluenceSyncOptions, type ConfluenceSyncPlan, type CreatePageInput, type DocEntry, type DocTree, INTERACTIVE_FLAG, type LocalSyncEntryPlan, type LocalSyncPlan, LocalSyncValidationAggregateError, type LocalSyncValidationError, type MarkdownConvertOptions, type MarkdownConvertResult, type MermaidBlock, type MermaidPreflightResult, type MermaidRewriteOptions, type MermaidRewriteResult, type Page, type PageBody, type PageVersion, type RewriteOptions, type RewriteResult, type SyncChange, type SyncFailure, SyncMutationError, type SyncResult, type UpdatePageInput, type ValidatedAttachmentSource, escapeAttachmentFilename, escapeXmlAttribute, isAllowedUrl, isMarkdownName, isRemoteUrl, markdownToStorage, preflightImagesToAttachments, preflightMermaidBlocks, readDocTree, resolveConfluenceSyncPlan, resolveConfluenceSyncPlan as resolveSyncPlan, rewriteImagesToAttachments, rewriteMermaidBlocks, syncConfluenceToDocs, titleFromSegment, validateAttachmentSources, validateLocalSync };