@wafflebase/cli 0.3.7 → 0.4.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.
@@ -5,6 +5,7 @@ import { printDryRun } from '../client/dry-run.js';
5
5
  import { parseContentFormat, runDocsContent } from '../docs/content.js';
6
6
  import { exportPdf } from '../docs/pdf-export.js';
7
7
  import { exportDocx } from '../docs/docx-export.js';
8
+ import { createImageFetcher } from '../docs/image-fetcher.js';
8
9
  import { parsePageRange } from '../docs/page-range.js';
9
10
  import { writeBinary } from '../output/binary.js';
10
11
  import { runDocsImport } from '../docs/import.js';
@@ -224,6 +225,15 @@ export function registerDocsCommand(program) {
224
225
  throw new Error(`HTTP ${res.status}`);
225
226
  }
226
227
  const fetchedDoc = res.data;
228
+ // Image inlines reference URLs that PdfExporter and DocxExporter
229
+ // can't resolve on their own. The fetcher walks each unique
230
+ // `src`, GETs it (via `globalThis.fetch`), and hands back a
231
+ // Blob. Relative URLs (`/images/<id>`) resolve against the
232
+ // configured server base so the same doc round-trips between
233
+ // the editor (browser-relative) and the CLI.
234
+ const imageFetcher = createImageFetcher({
235
+ serverBase: getConfig(opts).server,
236
+ });
227
237
  let bytes;
228
238
  if (format === 'pdf') {
229
239
  // Pagination uses the FontkitMeasurer's coarse Latin estimate
@@ -238,6 +248,7 @@ export function registerDocsCommand(program) {
238
248
  // see `extractPages` in pdf-export.ts.
239
249
  const fullPdf = await exportPdf(fetchedDoc, {
240
250
  includeHeaderFooter: local.includeHeaderFooter,
251
+ imageFetcher,
241
252
  });
242
253
  const total = await pdfPageCount(fullPdf);
243
254
  pageRange = parsePageRange(local.pages, total);
@@ -248,11 +259,13 @@ export function registerDocsCommand(program) {
248
259
  bytes = await exportPdf(fetchedDoc, {
249
260
  includeHeaderFooter: local.includeHeaderFooter,
250
261
  pages: pageRange,
262
+ imageFetcher,
251
263
  });
252
264
  }
253
265
  else {
254
266
  bytes = await exportPdf(fetchedDoc, {
255
267
  includeHeaderFooter: local.includeHeaderFooter,
268
+ imageFetcher,
256
269
  });
257
270
  }
258
271
  }
@@ -262,6 +275,7 @@ export function registerDocsCommand(program) {
262
275
  }
263
276
  bytes = await exportDocx(fetchedDoc, {
264
277
  includeHeaderFooter: local.includeHeaderFooter,
278
+ imageFetcher,
265
279
  });
266
280
  }
267
281
  writeBinary(bytes, file, { force: local.force, quiet: opts.quiet });
@@ -23,7 +23,10 @@ export declare function getConfigPath(): string;
23
23
  *
24
24
  * Auth resolution order:
25
25
  * 1. Flag/env `--api-key` or `WAFFLEBASE_API_KEY` → API key auth
26
- * 2. Session `~/.wafflebase/session.json` exists and token valid → JWT auth
26
+ * 2. Session `~/.wafflebase/session.json` with both tokens → JWT auth
27
+ * (the access token may already be past its `expiresAt`; the HTTP
28
+ * client refreshes it on the first 401 and falls back to a clear
29
+ * SESSION_EXPIRED error when the refresh token is also dead)
27
30
  * 3. Config profile `api-key` → API key auth
28
31
  * 4. None → empty (commands will fail)
29
32
  */
@@ -2,7 +2,7 @@ import { readFileSync, existsSync, mkdirSync, copyFileSync } from 'node:fs';
2
2
  import { join, dirname } from 'node:path';
3
3
  import { homedir } from 'node:os';
4
4
  import { parse as parseYaml } from 'yaml';
5
- import { loadSession, isSessionExpired } from './session.js';
5
+ import { loadSession } from './session.js';
6
6
  export const DEFAULT_SERVER = 'https://api.wafflebase.io';
7
7
  /**
8
8
  * Migrate config from old path (~/.config/wafflebase/config.yaml) to new path
@@ -40,7 +40,10 @@ export function getConfigPath() {
40
40
  *
41
41
  * Auth resolution order:
42
42
  * 1. Flag/env `--api-key` or `WAFFLEBASE_API_KEY` → API key auth
43
- * 2. Session `~/.wafflebase/session.json` exists and token valid → JWT auth
43
+ * 2. Session `~/.wafflebase/session.json` with both tokens → JWT auth
44
+ * (the access token may already be past its `expiresAt`; the HTTP
45
+ * client refreshes it on the first 401 and falls back to a clear
46
+ * SESSION_EXPIRED error when the refresh token is also dead)
44
47
  * 3. Config profile `api-key` → API key auth
45
48
  * 4. None → empty (commands will fail)
46
49
  */
@@ -62,9 +65,14 @@ export function resolveConfig(flags) {
62
65
  authMode: 'api-key',
63
66
  };
64
67
  }
65
- // Step 2: Try session
68
+ // Step 2: Try session — accept it whenever both tokens are present.
69
+ // Gating on `isSessionExpired` here would silently drop the session
70
+ // (and its `activeWorkspace`) the moment the access token expired,
71
+ // turning every command into a `/workspaces//...` 404. Letting the
72
+ // HTTP client see the expired access token + refresh token lets its
73
+ // 401 → refresh → retry path do its job.
66
74
  const session = loadSession();
67
- if (session && !isSessionExpired(session)) {
75
+ if (session && session.accessToken && session.refreshToken) {
68
76
  return {
69
77
  server: flags.server ??
70
78
  process.env.WAFFLEBASE_SERVER ??
@@ -0,0 +1,37 @@
1
+ import type { DocxImageFetcher } from '@wafflebase/docs';
2
+ export interface ImageFetcherOptions {
3
+ /**
4
+ * Base URL of the Wafflebase API server. Used to resolve image
5
+ * inlines whose `src` is a server-relative path (`/images/<id>`).
6
+ * Absolute URLs (`http(s)://...`, `data:`, `blob:`, ...) pass through
7
+ * untouched so docs imported from external sources keep working.
8
+ */
9
+ serverBase: string;
10
+ /**
11
+ * Optional `fetch` override — kept as a seam for tests. Defaults to
12
+ * the global `fetch`. The signature matches the WHATWG fetch so a
13
+ * stub can be a plain async function without pulling DOM types in.
14
+ */
15
+ fetch?: typeof globalThis.fetch;
16
+ }
17
+ /**
18
+ * Resolve a possibly-relative image URL against the configured server
19
+ * base. Mirrors `resolveImageUrl` in
20
+ * `packages/frontend/src/app/docs/export-utils.ts` so the CLI and
21
+ * browser agree on what counts as "absolute". The shared rule: any URL
22
+ * that starts with a scheme (`http:`, `https:`, `data:`, `blob:`,
23
+ * `file:`, ...) is left alone; everything else gets prefixed.
24
+ */
25
+ export declare function resolveImageUrl(url: string, serverBase: string): string;
26
+ /**
27
+ * Build an `ImageFetcher` for the CLI export pipelines. The fetcher
28
+ * downloads each unique image inline `src` once, returning a Blob the
29
+ * exporter can embed verbatim (PDF) or stream into the DOCX zip.
30
+ *
31
+ * Backend's `GET /images/:id` is publicly readable, so we don't attach
32
+ * an Authorization header — sending JWT cookies via the CLI isn't
33
+ * possible anyway. Relative URLs resolve against `serverBase`; absolute
34
+ * URLs (e.g., the canonical `https://api.wafflebase.io/images/...`
35
+ * surfaced by `imageFetcher required` errors) pass through.
36
+ */
37
+ export declare function createImageFetcher(opts: ImageFetcherOptions): DocxImageFetcher;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Resolve a possibly-relative image URL against the configured server
3
+ * base. Mirrors `resolveImageUrl` in
4
+ * `packages/frontend/src/app/docs/export-utils.ts` so the CLI and
5
+ * browser agree on what counts as "absolute". The shared rule: any URL
6
+ * that starts with a scheme (`http:`, `https:`, `data:`, `blob:`,
7
+ * `file:`, ...) is left alone; everything else gets prefixed.
8
+ */
9
+ export function resolveImageUrl(url, serverBase) {
10
+ if (/^[a-z][a-z0-9+.-]*:/i.test(url))
11
+ return url;
12
+ const base = serverBase.replace(/\/$/, '');
13
+ if (!base)
14
+ return url;
15
+ return `${base}${url.startsWith('/') ? url : `/${url}`}`;
16
+ }
17
+ /**
18
+ * Build an `ImageFetcher` for the CLI export pipelines. The fetcher
19
+ * downloads each unique image inline `src` once, returning a Blob the
20
+ * exporter can embed verbatim (PDF) or stream into the DOCX zip.
21
+ *
22
+ * Backend's `GET /images/:id` is publicly readable, so we don't attach
23
+ * an Authorization header — sending JWT cookies via the CLI isn't
24
+ * possible anyway. Relative URLs resolve against `serverBase`; absolute
25
+ * URLs (e.g., the canonical `https://api.wafflebase.io/images/...`
26
+ * surfaced by `imageFetcher required` errors) pass through.
27
+ */
28
+ export function createImageFetcher(opts) {
29
+ const fetchImpl = opts.fetch ?? globalThis.fetch;
30
+ return async (url) => {
31
+ const resolved = resolveImageUrl(url, opts.serverBase);
32
+ const res = await fetchImpl(resolved);
33
+ if (!res.ok) {
34
+ throw new Error(`Image fetch failed: ${res.status} ${res.statusText} for ${resolved}`);
35
+ }
36
+ return res.blob();
37
+ };
38
+ }
@@ -1,4 +1,4 @@
1
- import { type Document, type PdfFontsOptions } from '@wafflebase/docs';
1
+ import { type Document, type DocxImageFetcher, type PdfFontsOptions } from '@wafflebase/docs';
2
2
  import type { PageRange } from './page-range.js';
3
3
  /**
4
4
  * Options for the CLI PDF export pipeline.
@@ -21,6 +21,15 @@ export interface CliPdfExportOptions {
21
21
  * and to leave room for a future "strip header/footer" mode. */
22
22
  includeHeaderFooter?: boolean;
23
23
  fontSources?: PdfFontsOptions['sources'];
24
+ /** Resolve and download image inlines so they can be embedded in the
25
+ * PDF. Required when the document contains image inlines —
26
+ * `PdfExporter` throws otherwise to avoid silently dropping content.
27
+ * The CLI command builds one via `createImageFetcher` from the
28
+ * configured server base; tests inject a stub that returns
29
+ * in-memory bytes. Typed as `DocxImageFetcher` (a structural
30
+ * `(url) => Promise<Blob>` alias) since the bare `ImageFetcher`
31
+ * symbol from `pdf-image-painter` isn't re-exported. */
32
+ imageFetcher?: DocxImageFetcher;
24
33
  }
25
34
  /**
26
35
  * Render `doc` to a PDF byte buffer using the same `PdfExporter` the
@@ -19,7 +19,11 @@ export async function exportPdf(doc, opts = {}) {
19
19
  const fonts = new PdfFonts({ sources: opts.fontSources });
20
20
  const measurer = new FontkitMeasurer();
21
21
  await preloadKoreanFonts(fonts, measurer, usage);
22
- const blob = await PdfExporter.export(doc, { measurer, fonts });
22
+ const blob = await PdfExporter.export(doc, {
23
+ measurer,
24
+ fonts,
25
+ imageFetcher: opts.imageFetcher,
26
+ });
23
27
  const fullBytes = new Uint8Array(await blob.arrayBuffer());
24
28
  if (!opts.pages || opts.pages.pages.size === 0) {
25
29
  return fullBytes;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wafflebase/cli",
3
- "version": "0.3.7",
3
+ "version": "0.4.0",
4
4
  "description": "CLI for Wafflebase spreadsheet API",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -20,7 +20,7 @@
20
20
  "open": "^11.0.0",
21
21
  "pdf-lib": "^1.17.1",
22
22
  "yaml": "^2.0.0",
23
- "@wafflebase/docs": "0.3.7"
23
+ "@wafflebase/docs": "0.4.0"
24
24
  },
25
25
  "devDependencies": {
26
26
  "@types/fontkit": "^2.0.0",