@book.dev/sdk 1.65.1 → 1.67.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/import.js ADDED
@@ -0,0 +1,313 @@
1
+ /**
2
+ * The **format-agnostic import core** — the spine every importer (Markdown,
3
+ * HTML, Notion, …) targets. A parser's only job is to turn its source format
4
+ * into an {@link ImportedDoc}; this module then writes that IR into the
5
+ * workspace through the *existing* data paths, so importers never touch the
6
+ * store, the CRDT, or the wire protocol directly.
7
+ *
8
+ * Two writers, picked by the document's size/shape (see {@link chooseStrategy}):
9
+ *
10
+ * - **Strategy A — {@link writeViaCreateApis}** drives `savePage` /
11
+ * `createDatabase` / `createRow` directly, exactly like the template gallery
12
+ * (see `templates.ts`). Best for a single page / small tree: it streams
13
+ * creates and resolves parent links as it goes, with no whole-bundle staging.
14
+ *
15
+ * - **Strategy B — {@link writeViaBundle}** maps the whole IR into a backup
16
+ * bundle (`{pages, databases}` native records) and hands it to
17
+ * `importSpace(…, mode:'copy')`. That inherits — for free — the server's
18
+ * id-remap, `@`-mention link-rewrite, name de-dup, and idempotent-replay
19
+ * machinery (`remapBundle` / `importBundle`), plus the Restore dialog on the
20
+ * UI side. Best for a multi-page / database tree.
21
+ *
22
+ * **Assets (v1 decision).** A real asset store is a separate epic; until then an
23
+ * image becomes a *placeholder* that preserves the original `ref` + `alt` rather
24
+ * than being silently dropped — {@link imagePlaceholderBlock} for body content,
25
+ * {@link imagePlaceholderCell} for a database cell. The "never silently drop
26
+ * content" guarantee that the inbound HTML/EditorJS converters follow (see
27
+ * `ui/blockeditor/model.ts`) holds end-to-end through import.
28
+ *
29
+ * **Why the SDK.** The writers speak `DataClient` and the backup bundle, both
30
+ * SDK-owned; the block payload is the editor's plain JSON projection, which the
31
+ * SDK already handles structurally (`content.ts`, `mtime.ts`). {@link
32
+ * ImportedBlock} is the structural mirror of `@book.dev/ui`'s `BlockJSON`: a
33
+ * parser's real `BlockJSON` (built with the UI's `htmlToBlocks` / `htmlToRuns`
34
+ * helpers) is assignable straight into this IR — without the SDK taking a UI
35
+ * dependency. The assignment is one-way *by design*: `ImportedBlock.id` is
36
+ * OPTIONAL (import omits ids; the editor mints them on load via its
37
+ * `jsonToNewBlock`), so the reverse — treating an `ImportedBlock` as a
38
+ * `BlockJSON` — intentionally does not hold.
39
+ */
40
+ import { emptyPageSnapshot } from './types';
41
+ import { ICON_PROPERTY_ID } from './pageProperties';
42
+ import { contentHash } from './mtime';
43
+ // ── Image-placeholder shim (v1 assets decision) ──────────────────────────────
44
+ /**
45
+ * The `props.kind` marker on a placeholder block, and the `props` key the
46
+ * structured {@link ImportedAsset} is stashed under. A future real-asset epic
47
+ * can query for `props[IMAGE_PLACEHOLDER_PROP]` to find and rehydrate every
48
+ * placeholder a past import left behind.
49
+ */
50
+ export const IMAGE_PLACEHOLDER_KIND = 'import-image-placeholder';
51
+ export const IMAGE_PLACEHOLDER_PROP = 'importedAsset';
52
+ /**
53
+ * Turn an image asset into a real, visible **callout** block that preserves the
54
+ * original ref (as a clickable link run) and the alt/title text — never a
55
+ * silent drop. The structured {@link ImportedAsset} is stashed in `props` so the
56
+ * asset can be rehydrated later. Mirrors the "honest visible marker" pattern the
57
+ * EditorJS migration uses for not-yet-supported blocks (`model.ts`). Pure: the
58
+ * id is derived from the asset, so the same image always yields the same block
59
+ * id (deterministic re-imports, no module-global counter).
60
+ */
61
+ export function imagePlaceholderBlock(asset) {
62
+ const label = asset.alt?.trim() || asset.title?.trim() || asset.ref;
63
+ return {
64
+ id: `imp_img_${contentHash(`${asset.ref}|${asset.alt ?? ''}`)}`,
65
+ type: 'callout',
66
+ text: [{ t: '🖼 ' }, { t: label, a: { a: asset.ref } }],
67
+ props: {
68
+ variant: 'info',
69
+ [IMAGE_PLACEHOLDER_PROP]: {
70
+ kind: IMAGE_PLACEHOLDER_KIND,
71
+ assetKind: asset.kind,
72
+ ref: asset.ref,
73
+ ...(asset.alt ? { alt: asset.alt } : {}),
74
+ ...(asset.title ? { title: asset.title } : {}),
75
+ },
76
+ },
77
+ };
78
+ }
79
+ /**
80
+ * The placeholder *cell* value for a `files` / `url` database property: the
81
+ * original ref, so the cell still renders and resolves the image (`firstImageUrl`
82
+ * / `coverImageUrl` read it). Alt, when present, is preserved at block
83
+ * granularity via {@link imagePlaceholderBlock} on the row's body.
84
+ */
85
+ export function imagePlaceholderCell(asset) {
86
+ return asset.ref;
87
+ }
88
+ // ── Snapshot construction ────────────────────────────────────────────────────
89
+ /**
90
+ * Wrap a block list in a page snapshot the block editor reads — the same shape
91
+ * `templates.ts` ships (`editor:'blocks'`, blocks in `blockdoc.blocks`, NO CRDT
92
+ * `update`). The editor rebuilds the Y.Doc from the JSON projection on load
93
+ * (`decodeSnapshot` falls back to it), so importers never touch yjs.
94
+ */
95
+ export function importedBlocksToSnapshot(blocks) {
96
+ return {
97
+ editorjs: { blocks: [] },
98
+ values: [],
99
+ names: [],
100
+ editor: 'blocks',
101
+ blockdoc: { blocks },
102
+ };
103
+ }
104
+ const rowSnapshot = (row) => row.blocks && row.blocks.length > 0 ? importedBlocksToSnapshot(row.blocks) : emptyPageSnapshot();
105
+ const pageIsSimple = (page) => !page.database && !(page.children && page.children.length > 0);
106
+ /**
107
+ * Pick a writer by the document's shape: a lone, childless, database-less page
108
+ * → **Strategy A** (stream creates); anything bigger — multiple pages, nested
109
+ * children, or a hosted database — → **Strategy B** (stage a bundle and let the
110
+ * server remap/rewrite/dedup the whole tree at once).
111
+ */
112
+ export function chooseStrategy(doc) {
113
+ return doc.pages.length === 1 && pageIsSimple(doc.pages[0]) ? 'create' : 'bundle';
114
+ }
115
+ /**
116
+ * Does this block tree hold an image placeholder (a callout carrying
117
+ * {@link IMAGE_PLACEHOLDER_PROP})? Used by the writers to record which landed
118
+ * pages the post-import rehydration pass must revisit (see
119
+ * {@link ImportWriteResult.placeholderPageIds}). Recurses container children.
120
+ */
121
+ export function blocksHaveImagePlaceholder(blocks) {
122
+ for (const b of blocks ?? []) {
123
+ if (b.props?.[IMAGE_PLACEHOLDER_PROP])
124
+ return true;
125
+ if (b.children && blocksHaveImagePlaceholder(b.children))
126
+ return true;
127
+ }
128
+ return false;
129
+ }
130
+ /** How many `(imported)`-suffixed names to try before falling back to untitled. */
131
+ const NAME_SUFFIX_ATTEMPTS = 5;
132
+ /**
133
+ * Run a name-bearing create (page or row), suffixing the title on a uniqueness
134
+ * clash so an import **never hard-fails on a name collision** — page names are
135
+ * globally unique, so a raw `savePage`/`createRow` *throws* when the title is
136
+ * already taken (and Strategy A is chosen for exactly the lone-page case, the
137
+ * most collision-prone). Mirrors `templates.ts` (`availableName` + retry): try
138
+ * the title, then `"<title> (imported)"`, then numbered variants — consistent
139
+ * with the server's copy-mode ` (imported)` suffix so A and B converge — and
140
+ * finally an untitled record as a last resort. A genuine non-name failure
141
+ * surfaces once the untitled fallback also throws.
142
+ */
143
+ async function createDeduped(title, create) {
144
+ const candidates = [];
145
+ if (title) {
146
+ candidates.push(title, `${title} (imported)`);
147
+ for (let n = 2; n <= NAME_SUFFIX_ATTEMPTS; n += 1)
148
+ candidates.push(`${title} (imported) ${n}`);
149
+ }
150
+ for (const name of candidates) {
151
+ try {
152
+ return await create(name);
153
+ }
154
+ catch {
155
+ // Name taken (or transient) — step the suffix and retry the next candidate.
156
+ }
157
+ }
158
+ // Empty title, or every suffix exhausted: land it untitled rather than abort.
159
+ return create(null);
160
+ }
161
+ /**
162
+ * **Strategy A.** Write the tree by driving the create APIs directly, the way
163
+ * the template gallery does: each page is `savePage`d (parent resolved from the
164
+ * already-created ancestor), a hosted database is `createDatabase`d on it, and
165
+ * every row — including nested sub-items — is `createRow`d. Page/row names are
166
+ * deduped on the fly ({@link createDeduped}) since names are globally unique, so
167
+ * a title clash suffixes-and-lands instead of aborting the import. Returns the
168
+ * ids it minted, in creation order.
169
+ */
170
+ export async function writeViaCreateApis(client, doc, opts = {}) {
171
+ const result = { strategy: 'create', pageIds: [], databaseIds: [], rowIds: [], placeholderPageIds: [] };
172
+ const writeRows = async (databaseId, rows, parentRowId) => {
173
+ for (const row of rows) {
174
+ const data = row.blocks && row.blocks.length > 0 ? importedBlocksToSnapshot(row.blocks) : undefined;
175
+ const stored = await createDeduped(row.title, (name) => client.createRow(databaseId, { name, properties: row.properties, data, parentId: parentRowId }));
176
+ result.rowIds.push(stored.id);
177
+ if (blocksHaveImagePlaceholder(row.blocks))
178
+ result.placeholderPageIds.push(stored.id);
179
+ if (row.children && row.children.length > 0)
180
+ await writeRows(databaseId, row.children, stored.id);
181
+ }
182
+ };
183
+ const writePage = async (page, parentId) => {
184
+ const stored = await createDeduped(page.title, (name) => client.savePage({ name, data: importedBlocksToSnapshot(page.blocks), parentId }));
185
+ result.pageIds.push(stored.id);
186
+ if (blocksHaveImagePlaceholder(page.blocks))
187
+ result.placeholderPageIds.push(stored.id);
188
+ if (page.icon)
189
+ await client.setPageProperties(stored.id, { [ICON_PROPERTY_ID]: page.icon });
190
+ if (page.database) {
191
+ const db = await client.createDatabase({
192
+ pageId: stored.id,
193
+ name: page.database.name ?? page.title ?? null,
194
+ schema: page.database.schema,
195
+ });
196
+ result.databaseIds.push(db.id);
197
+ await writeRows(db.id, page.database.rows, null);
198
+ }
199
+ for (const child of page.children ?? [])
200
+ await writePage(child, stored.id);
201
+ };
202
+ for (const page of doc.pages)
203
+ await writePage(page, opts.parentId ?? null);
204
+ return result;
205
+ }
206
+ /**
207
+ * Map an {@link ImportedDoc} to a copy-mode backup bundle: native `StoredPage` /
208
+ * `StoredDatabase` records with internally-consistent **synthetic** ids (the
209
+ * server re-keys every id on import). The host page's `hostedDatabaseId`, the
210
+ * database's `pageId`, each row's `databaseId`, and sub-item / child `parentId`
211
+ * links are all wired up so `remapBundle` can rewrite them as one graph. Pure —
212
+ * no client, no I/O — so it is directly unit-testable.
213
+ */
214
+ export function buildImportBundle(doc, opts = {}) {
215
+ const now = opts.now ?? new Date().toISOString();
216
+ let counter = 0;
217
+ const newId = opts.newId ?? (() => `imp_${++counter}`);
218
+ const pages = [];
219
+ const databases = [];
220
+ const emitRows = (rows, databaseId, parentRowId) => {
221
+ for (const row of rows) {
222
+ // A parser may pin a stable id (so cross-page mentions resolve); else mint one.
223
+ const id = row.id ?? newId();
224
+ pages.push({
225
+ id,
226
+ name: row.title || null,
227
+ data: rowSnapshot(row),
228
+ hostedDatabaseId: null,
229
+ databaseId,
230
+ parentId: parentRowId,
231
+ properties: row.properties ?? {},
232
+ deletedAt: null,
233
+ createdAt: now,
234
+ updatedAt: now,
235
+ });
236
+ if (row.children && row.children.length > 0)
237
+ emitRows(row.children, databaseId, id);
238
+ }
239
+ };
240
+ const emitPage = (page, parentId) => {
241
+ // A parser may pin a stable id (so cross-page mentions resolve); else mint one.
242
+ const id = page.id ?? newId();
243
+ const hostedDatabaseId = page.database ? newId() : null;
244
+ pages.push({
245
+ id,
246
+ name: page.title || null,
247
+ data: importedBlocksToSnapshot(page.blocks),
248
+ hostedDatabaseId,
249
+ databaseId: null,
250
+ parentId,
251
+ properties: page.icon ? { [ICON_PROPERTY_ID]: page.icon } : {},
252
+ deletedAt: null,
253
+ createdAt: now,
254
+ updatedAt: now,
255
+ });
256
+ if (page.database && hostedDatabaseId) {
257
+ databases.push({
258
+ id: hostedDatabaseId,
259
+ pageId: id,
260
+ name: page.database.name ?? page.title ?? null,
261
+ schema: page.database.schema,
262
+ createdAt: now,
263
+ updatedAt: now,
264
+ });
265
+ emitRows(page.database.rows, hostedDatabaseId, null);
266
+ }
267
+ for (const child of page.children ?? [])
268
+ emitPage(child, id);
269
+ };
270
+ // Bundle roots land at the top level: copy-mode remap nulls any parent not in
271
+ // the bundle, so an external `parentId` would be dropped anyway — don't set it.
272
+ for (const page of doc.pages)
273
+ emitPage(page, null);
274
+ return { pages, databases };
275
+ }
276
+ /**
277
+ * **Strategy B.** Build the copy-mode bundle ({@link buildImportBundle}) and
278
+ * hand it to `importSpace`, inheriting the server's id-remap, link-rewrite,
279
+ * name-dedup, and idempotent-replay handling. Surfaces the server's
280
+ * {@link ImportResult} (and its `idMap` as the new page ids).
281
+ */
282
+ export async function writeViaBundle(client, doc, opts = {}) {
283
+ const { pages, databases } = buildImportBundle(doc, opts);
284
+ const req = { pages, databases, mode: 'copy' };
285
+ const importResult = await client.importSpace(req);
286
+ // Map the bundle pages that carry an image placeholder to their server-assigned
287
+ // ids (via the id-map) so the post-import rehydration pass revisits only those.
288
+ const placeholderPageIds = [];
289
+ for (const page of pages) {
290
+ const blocks = page.data.blockdoc?.blocks;
291
+ if (!blocksHaveImagePlaceholder(blocks))
292
+ continue;
293
+ const landed = importResult.idMap[page.id];
294
+ if (landed)
295
+ placeholderPageIds.push(landed);
296
+ }
297
+ return {
298
+ strategy: 'bundle',
299
+ pageIds: Object.values(importResult.idMap),
300
+ databaseIds: [],
301
+ rowIds: [],
302
+ importResult,
303
+ placeholderPageIds,
304
+ };
305
+ }
306
+ /**
307
+ * Import a document, picking the writer by {@link chooseStrategy}. The single
308
+ * entry point a parser calls once it has built its {@link ImportedDoc}.
309
+ */
310
+ export function importDoc(client, doc, opts = {}) {
311
+ return chooseStrategy(doc) === 'create' ? writeViaCreateApis(client, doc, opts) : writeViaBundle(client, doc, opts);
312
+ }
313
+ //# sourceMappingURL=import.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"import.js","sourceRoot":"","sources":["../src/import.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,167 @@
1
+ /**
2
+ * **Import asset rehydration (Assets A4).** Turns the image *placeholders* the
3
+ * importers emit (see `import.ts` {@link imagePlaceholderBlock}) into real,
4
+ * rendering `image` blocks — the payoff of OB-ASSETS: an imported picture comes
5
+ * through for real, not as a callout.
6
+ *
7
+ * Two seams, because the bytes arrive two very different ways:
8
+ *
9
+ * - **URL / `data:` images (Markdown · HTML).** These already carry a loadable
10
+ * source, so no upload is needed — {@link rehydrateImageUrls} rewrites the
11
+ * placeholder to an `image` block whose `src` is the original URL (kept as a
12
+ * link) or the inline `data:` URL (which the editor's A2 on-load migration
13
+ * then stores). Pure, DOM-free, no client — runnable *before* the doc is
14
+ * landed, so the page lands already-correct.
15
+ *
16
+ * - **Embedded bytes (Notion zip · opt-in URL download).** A Notion export holds
17
+ * every image's bytes inside the zip; there is no URL to render. So — *after*
18
+ * the page is created (we need its id to ref the asset's read-gate) —
19
+ * {@link rehydrateStoredImages} resolves each placeholder's `ref` to bytes,
20
+ * `putAsset`s them, and rewrites the placeholder to an `image` block holding
21
+ * the returned `assetId`. Content-addressed dedup makes a re-import idempotent.
22
+ *
23
+ * **Never lose the reference.** If the bytes are missing, over the 10 MiB cap, or
24
+ * the upload fails, the block degrades rather than dropping: a loadable URL is
25
+ * preserved as a URL `image` block; anything else keeps its original placeholder
26
+ * (ref + alt intact). A big import's uploads are processed with bounded
27
+ * concurrency so the UI is never blocked, and only pages that actually contain a
28
+ * placeholder are re-read/re-saved (the writers report them in
29
+ * `ImportWriteResult.placeholderPageIds`).
30
+ */
31
+ import { type ImportedBlock, type ImportedDoc } from './import';
32
+ import type { PageInput, StoredPage } from './types';
33
+ /** The `image` block type — the native picture block the editor ships (A0/A2). */
34
+ export declare const IMAGE_BLOCK_TYPE = "image";
35
+ /**
36
+ * Hard cap on an uploaded asset (10 MiB) — matches the server's asset bodyLimit
37
+ * (`app.ts` `ASSET_MAX_BYTES`). An import image over this stays a placeholder
38
+ * rather than failing the whole import.
39
+ */
40
+ export declare const DEFAULT_MAX_ASSET_BYTES: number;
41
+ /** Is this an `http(s)` URL — a picture the `image` block can render from `src`? */
42
+ export declare const isHttpUrl: (s: string) => boolean;
43
+ /** Is this a `data:` URL — inline bytes the editor can render (and A2 migrates)? */
44
+ export declare const isDataUrlRef: (s: string) => boolean;
45
+ /** Guess an image mime from a ref's file extension (defaults to octet-stream). */
46
+ export declare function mimeFromRef(ref: string): string;
47
+ /** The structured asset preserved on a placeholder block (see `imagePlaceholderBlock`). */
48
+ interface PlaceholderMeta {
49
+ ref?: string;
50
+ alt?: string;
51
+ title?: string;
52
+ }
53
+ /** Read the {@link PlaceholderMeta} off a block, or `undefined` if it isn't a placeholder. */
54
+ export declare function imagePlaceholderMeta(block: ImportedBlock): PlaceholderMeta | undefined;
55
+ /**
56
+ * Rewrite an image *placeholder* into a real `image` block, carrying either the
57
+ * uploaded `assetId` or a renderable `src`. The placeholder's `alt` becomes the
58
+ * image alt, its `title` the caption; the deterministic block id is kept so the
59
+ * rewrite is stable across re-imports.
60
+ */
61
+ export declare function importedImageBlock(placeholder: ImportedBlock, target: {
62
+ assetId: string;
63
+ } | {
64
+ src: string;
65
+ }): ImportedBlock;
66
+ /** Options for {@link rehydrateImageUrls}. */
67
+ export interface RehydrateUrlOptions {
68
+ /**
69
+ * Rewrite an `http(s)` image placeholder into a URL `image` block (default
70
+ * `true`). Set `false` to *keep* it a placeholder so a later
71
+ * {@link rehydrateStoredImages} pass can download the bytes into the store
72
+ * (the opt-in "download into workspace" flow).
73
+ */
74
+ preserveHttpUrls?: boolean;
75
+ /**
76
+ * Rewrite a `data:` placeholder into an inline `image` block when its decoded
77
+ * size is within this many bytes (default 10 MiB). An over-cap `data:` image
78
+ * stays a placeholder so the CRDT never carries a huge base64 blob.
79
+ */
80
+ maxDataBytes?: number;
81
+ }
82
+ /**
83
+ * **Pass 1 (pure).** Rewrite every image placeholder whose ref is a directly
84
+ * loadable source — an `http(s)` URL (preserved as a link) or an in-cap `data:`
85
+ * URL — into a real `image` block, throughout the doc (pages, children, database
86
+ * rows). No client, no I/O: run this on the IR *before* landing it so the page
87
+ * arrives already carrying real images. Idempotent (an `image` block isn't a
88
+ * placeholder, so a re-run is a no-op).
89
+ */
90
+ export declare function rehydrateImageUrls(doc: ImportedDoc, opts?: RehydrateUrlOptions): ImportedDoc;
91
+ /** An asset's raw bytes plus its mime, as resolved for upload. */
92
+ export interface AssetBytes {
93
+ bytes: Uint8Array;
94
+ mime: string;
95
+ }
96
+ /**
97
+ * Resolve a placeholder's `ref` to its bytes for upload, or `null` when the
98
+ * bytes are unavailable (missing / not fetchable). May be async (a network
99
+ * fetch). {@link notionAssetResolver} and {@link urlAssetResolver} build one.
100
+ */
101
+ export type ImportAssetResolver = (ref: string) => AssetBytes | null | Promise<AssetBytes | null>;
102
+ /** The slice of the data client {@link rehydrateStoredImages} drives. */
103
+ export interface RehydrateStoredClient {
104
+ getPage(id: string): Promise<StoredPage | null>;
105
+ savePage(input: PageInput): Promise<StoredPage>;
106
+ putAsset(bytes: Uint8Array, mime: string, pageId: string): Promise<{
107
+ id: string;
108
+ }>;
109
+ }
110
+ /** Options for {@link rehydrateStoredImages}. */
111
+ export interface RehydrateStoredOptions {
112
+ /** Skip (keep as placeholder) any image whose bytes exceed this (default 10 MiB). */
113
+ maxAssetBytes?: number;
114
+ /**
115
+ * Max total in-flight uploads at once (default 4) — a single shared budget
116
+ * across every page, so an image-dense page can't fan out N uploads and flood
117
+ * the socket. Also caps how many pages are walked concurrently.
118
+ */
119
+ concurrency?: number;
120
+ /** Progress callback: pages processed / total (so the UI can show a live count). */
121
+ onProgress?: (done: number, total: number) => void;
122
+ }
123
+ /** What a stored-image pass did, for honest reporting. */
124
+ export interface RehydrateStoredResult {
125
+ /** Placeholders uploaded to the store and rewritten to an `assetId` image block. */
126
+ uploaded: number;
127
+ /** Placeholders degraded to a URL `image` block (bytes unavailable but ref loadable). */
128
+ preservedUrls: number;
129
+ /** Placeholders left untouched (over-cap / no bytes / not loadable) — ref never lost. */
130
+ keptPlaceholders: number;
131
+ }
132
+ /**
133
+ * **Pass 2 (post-import).** For each page id carrying an image placeholder (the
134
+ * writers report these in `ImportWriteResult.placeholderPageIds`), resolve every
135
+ * placeholder's bytes and — within the size cap — `putAsset` + rewrite it to an
136
+ * `assetId` `image` block, then re-save the page. A single shared limiter caps
137
+ * total in-flight uploads (across all pages and all images) so a large,
138
+ * image-dense import streams rather than flooding the socket. Degrades safely
139
+ * (never loses a ref); idempotent (a re-run finds real images, not placeholders,
140
+ * and content-addressed dedup returns the same `assetId`).
141
+ */
142
+ export declare function rehydrateStoredImages(client: RehydrateStoredClient, pageIds: string[], resolve: ImportAssetResolver, opts?: RehydrateStoredOptions): Promise<RehydrateStoredResult>;
143
+ /**
144
+ * Build a resolver over a Notion export zip: a placeholder `ref` (rewritten by
145
+ * {@link notionExportToImportedDoc} to the absolute in-zip path) → its bytes +
146
+ * a mime guessed from the extension. Unzips lazily on first use (the import may
147
+ * land no images), and answers `null` for a ref with no matching entry.
148
+ */
149
+ export declare function notionAssetResolver(zipBytes: Uint8Array): ImportAssetResolver;
150
+ /** The `fetch`-shaped surface {@link urlAssetResolver} needs (so it's testable with a fake). */
151
+ export type FetchLike = (url: string) => Promise<{
152
+ ok: boolean;
153
+ status: number;
154
+ arrayBuffer(): Promise<ArrayBuffer>;
155
+ headers: {
156
+ get(name: string): string | null;
157
+ };
158
+ }>;
159
+ /**
160
+ * Build a resolver that downloads an `http(s)` image `ref` into bytes — the
161
+ * opt-in "store a copy of each linked image" path. Uses the ambient `fetch` when
162
+ * no impl is passed. Answers `null` (→ the URL is preserved as a link) for a
163
+ * non-http ref, a non-OK response, an empty body, or any fetch error — a slow /
164
+ * failing / cross-origin link degrades rather than breaking the import.
165
+ */
166
+ export declare function urlAssetResolver(fetchImpl?: FetchLike): ImportAssetResolver;
167
+ export {};