@moxn/kb-migrate 0.5.0 → 0.6.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/api-error.d.ts +7 -0
- package/dist/api-error.js +21 -0
- package/dist/client-http.test.d.ts +1 -0
- package/dist/client-http.test.js +89 -0
- package/dist/client.d.ts +61 -0
- package/dist/client.js +111 -38
- package/dist/export.js +94 -4
- package/dist/export.test.js +121 -0
- package/dist/http.d.ts +1 -0
- package/dist/http.js +16 -0
- package/dist/import-local.js +1 -0
- package/dist/import-local.test.js +96 -0
- package/dist/index.js +39 -12
- package/dist/output.d.ts +17 -0
- package/dist/output.js +65 -0
- package/dist/output.test.d.ts +1 -0
- package/dist/output.test.js +59 -0
- package/dist/targets/notion.js +3 -2
- package/dist/types.d.ts +6 -1
- package/package.json +2 -1
package/dist/api-error.d.ts
CHANGED
|
@@ -12,3 +12,10 @@ export declare function formatApiError(status: number, body: {
|
|
|
12
12
|
details?: unknown;
|
|
13
13
|
[key: string]: unknown;
|
|
14
14
|
} | null | undefined): string;
|
|
15
|
+
/**
|
|
16
|
+
* Parse a response body that should be JSON. When it isn't — typically the
|
|
17
|
+
* Vercel Auth wall's HTML page on a protected deployment — say what arrived
|
|
18
|
+
* (status, content-type) instead of surfacing a raw `JSON.parse` error like
|
|
19
|
+
* `Unexpected token '<', "<!DOCTYPE "... is not valid JSON`.
|
|
20
|
+
*/
|
|
21
|
+
export declare function readJson<T = any>(response: Response): Promise<T>;
|
package/dist/api-error.js
CHANGED
|
@@ -17,3 +17,24 @@ export function formatApiError(status, body) {
|
|
|
17
17
|
: '';
|
|
18
18
|
return detail ? `API error ${status}: ${detail}` : `API error: ${status}`;
|
|
19
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* Parse a response body that should be JSON. When it isn't — typically the
|
|
22
|
+
* Vercel Auth wall's HTML page on a protected deployment — say what arrived
|
|
23
|
+
* (status, content-type) instead of surfacing a raw `JSON.parse` error like
|
|
24
|
+
* `Unexpected token '<', "<!DOCTYPE "... is not valid JSON`.
|
|
25
|
+
*/
|
|
26
|
+
export async function readJson(response) {
|
|
27
|
+
try {
|
|
28
|
+
return (await response.json());
|
|
29
|
+
}
|
|
30
|
+
catch (cause) {
|
|
31
|
+
if (!(cause instanceof SyntaxError))
|
|
32
|
+
throw cause;
|
|
33
|
+
const contentType = response.headers?.get?.('content-type') ?? 'no content-type';
|
|
34
|
+
const where = response.url ? ` from ${response.url}` : '';
|
|
35
|
+
const hint = /html/i.test(contentType)
|
|
36
|
+
? ' — an HTML page usually means the Vercel protection wall: set MOXN_VERCEL_BYPASS, with MOXN_BASE_URL on this origin'
|
|
37
|
+
: '';
|
|
38
|
+
throw new Error(`Expected JSON${where} but got a non-JSON response (HTTP ${response.status}, ${contentType})${hint}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* kb-migrate's HTTP client against a Vercel-protected deployment (staging).
|
|
3
|
+
*
|
|
4
|
+
* `export-local` against https://staging.moxn.dev failed with
|
|
5
|
+
* `Unexpected token '<', "<!DOCTYPE "... is not valid JSON`: kb-migrate never
|
|
6
|
+
* sent the protection-bypass header, and the auth wall's HTML page reached a
|
|
7
|
+
* bare `response.json()`. The client now attaches the SAME
|
|
8
|
+
* `vercelBypassHeaders` kb-cli uses (@moxn/auth — env-gated, scoped to
|
|
9
|
+
* MOXN_BASE_URL's origin, so signed storage URLs never see the secret), and a
|
|
10
|
+
* non-JSON body is reported as what it is.
|
|
11
|
+
*/
|
|
12
|
+
import { describe, it, expect, vi, afterEach, beforeEach } from 'vitest';
|
|
13
|
+
import * as fs from 'fs';
|
|
14
|
+
import * as os from 'os';
|
|
15
|
+
import * as path from 'path';
|
|
16
|
+
import { MoxnClient } from './client.js';
|
|
17
|
+
const STAGING = 'https://staging.moxn.dev';
|
|
18
|
+
const HEADER = 'x-vercel-protection-bypass';
|
|
19
|
+
function jsonResponse(body) {
|
|
20
|
+
return new Response(JSON.stringify(body), {
|
|
21
|
+
status: 200,
|
|
22
|
+
headers: { 'content-type': 'application/json' },
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
function headersOf(init) {
|
|
26
|
+
return new Headers(init?.headers).has(HEADER)
|
|
27
|
+
? { [HEADER]: new Headers(init?.headers).get(HEADER) }
|
|
28
|
+
: {};
|
|
29
|
+
}
|
|
30
|
+
describe('MoxnClient — Vercel protection bypass', () => {
|
|
31
|
+
const saved = { ...process.env };
|
|
32
|
+
let fetchSpy;
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
fetchSpy = vi.spyOn(globalThis, 'fetch');
|
|
35
|
+
});
|
|
36
|
+
afterEach(() => {
|
|
37
|
+
vi.restoreAllMocks();
|
|
38
|
+
process.env = { ...saved };
|
|
39
|
+
});
|
|
40
|
+
function client() {
|
|
41
|
+
return new MoxnClient({
|
|
42
|
+
apiUrl: STAGING,
|
|
43
|
+
apiKey: 'k',
|
|
44
|
+
basePath: '/',
|
|
45
|
+
onConflict: 'skip',
|
|
46
|
+
dryRun: false,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
it('attaches the bypass header to requests for the MOXN_BASE_URL origin only', async () => {
|
|
50
|
+
process.env.MOXN_VERCEL_BYPASS = 'bypass-secret';
|
|
51
|
+
process.env.MOXN_BASE_URL = STAGING;
|
|
52
|
+
fetchSpy.mockImplementation(async (url) => String(url).startsWith(STAGING)
|
|
53
|
+
? jsonResponse({ documents: [], pagination: { hasMore: false } })
|
|
54
|
+
: new Response('blob', { status: 200 }));
|
|
55
|
+
await client().listDocuments('/qa');
|
|
56
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'kb-bypass-'));
|
|
57
|
+
await client().downloadFile('https://storage.example/signed/f.png', path.join(dir, 'f.png'));
|
|
58
|
+
const [apiCall, storageCall] = fetchSpy.mock.calls;
|
|
59
|
+
expect(String(apiCall[0])).toContain(`${STAGING}/api/v1/kb/documents`);
|
|
60
|
+
expect(headersOf(apiCall[1])).toEqual({ [HEADER]: 'bypass-secret' });
|
|
61
|
+
// The API key still rides alongside.
|
|
62
|
+
expect(new Headers(apiCall[1].headers).get('x-api-key')).toBe('k');
|
|
63
|
+
// A different origin (signed storage URL) never receives the secret.
|
|
64
|
+
expect(headersOf(storageCall[1])).toEqual({});
|
|
65
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
66
|
+
});
|
|
67
|
+
it('sends no bypass header when MOXN_VERCEL_BYPASS is unset', async () => {
|
|
68
|
+
delete process.env.MOXN_VERCEL_BYPASS;
|
|
69
|
+
process.env.MOXN_BASE_URL = STAGING;
|
|
70
|
+
fetchSpy.mockResolvedValue(jsonResponse({ documents: [], pagination: { hasMore: false } }));
|
|
71
|
+
await client().listDocuments('/qa');
|
|
72
|
+
expect(headersOf(fetchSpy.mock.calls[0][1])).toEqual({});
|
|
73
|
+
});
|
|
74
|
+
it('reports a non-JSON (auth-wall HTML) body by status + content-type, not a raw JSON.parse error', async () => {
|
|
75
|
+
fetchSpy.mockResolvedValue(new Response('<!DOCTYPE html><html>Vercel Authentication</html>', {
|
|
76
|
+
status: 200,
|
|
77
|
+
headers: { 'content-type': 'text/html; charset=utf-8' },
|
|
78
|
+
}));
|
|
79
|
+
const err = await client()
|
|
80
|
+
.listDocuments('/qa')
|
|
81
|
+
.then(() => null, (e) => e);
|
|
82
|
+
expect(err).toBeInstanceOf(Error);
|
|
83
|
+
expect(err.message).not.toMatch(/Unexpected token/);
|
|
84
|
+
expect(err.message).toMatch(/non-JSON/i);
|
|
85
|
+
expect(err.message).toContain('200');
|
|
86
|
+
expect(err.message).toContain('text/html');
|
|
87
|
+
expect(err.message).toMatch(/MOXN_VERCEL_BYPASS/);
|
|
88
|
+
});
|
|
89
|
+
});
|
package/dist/client.d.ts
CHANGED
|
@@ -36,12 +36,59 @@ export interface ImportMarkdownResult {
|
|
|
36
36
|
sectionIds?: string[];
|
|
37
37
|
/** The branch the upsert landed on (for branch-scoped follow-ups like tags). */
|
|
38
38
|
branchId?: string;
|
|
39
|
+
/**
|
|
40
|
+
* The permissions a CREATED document got. Absent on update/skip (an import
|
|
41
|
+
* never re-permissions an existing document) and from servers that predate
|
|
42
|
+
* the importer permission fields.
|
|
43
|
+
*/
|
|
44
|
+
permissions?: {
|
|
45
|
+
defaultPermission: 'edit' | 'read' | 'none';
|
|
46
|
+
aiAccess: 'edit' | 'read' | 'none';
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The warning (if any) for an import whose `--default-permission` /
|
|
51
|
+
* `--ai-access` did not take effect: an existing document is never
|
|
52
|
+
* re-permissioned by a re-import, and a server that predates the fields
|
|
53
|
+
* creates documents with its defaults without echoing `permissions`.
|
|
54
|
+
*/
|
|
55
|
+
export declare function importPermissionWarning(requested: {
|
|
56
|
+
defaultPermission?: string;
|
|
57
|
+
aiAccess?: string;
|
|
58
|
+
}, result: Pick<ImportMarkdownResult, 'outcome' | 'permissions'>): string | undefined;
|
|
59
|
+
/**
|
|
60
|
+
* One item of a `read` tool response, narrowed to the fields the export uses.
|
|
61
|
+
* `kind` is absent on an error item and on older md reads.
|
|
62
|
+
*/
|
|
63
|
+
export interface ReadDocumentItem {
|
|
64
|
+
type: string;
|
|
65
|
+
documentId: string;
|
|
66
|
+
error?: string;
|
|
67
|
+
kind?: string;
|
|
68
|
+
/** report: the HTML; slides: the deck-marker HTML. */
|
|
69
|
+
content?: Array<{
|
|
70
|
+
type: string;
|
|
71
|
+
text?: string;
|
|
72
|
+
}>;
|
|
73
|
+
/** kind='file' only. */
|
|
74
|
+
file?: {
|
|
75
|
+
filename: string;
|
|
76
|
+
download: {
|
|
77
|
+
url: string;
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
/** kind='slides' only. */
|
|
81
|
+
deck?: {
|
|
82
|
+
themeCss?: string | null;
|
|
83
|
+
};
|
|
39
84
|
}
|
|
40
85
|
export declare class MoxnClient {
|
|
41
86
|
private apiUrl;
|
|
42
87
|
private apiKey;
|
|
43
88
|
private defaultPermission?;
|
|
44
89
|
private aiAccess?;
|
|
90
|
+
/** Every request goes through {@link moxnFetch} (bypass-header aware). */
|
|
91
|
+
private fetch;
|
|
45
92
|
constructor(options: MigrationOptions | ExportOptions);
|
|
46
93
|
/**
|
|
47
94
|
* Migrate a single document
|
|
@@ -58,6 +105,13 @@ export declare class MoxnClient {
|
|
|
58
105
|
* update by the server.
|
|
59
106
|
*/
|
|
60
107
|
migrateDocument(doc: ExtractedDocument, basePath: string, onConflict: 'skip' | 'update', dryRun: boolean): Promise<MigrationResult>;
|
|
108
|
+
/**
|
|
109
|
+
* {@link importPermissionWarning} for this client's requested permissions,
|
|
110
|
+
* logged once and returned as a spreadable `{ warning }`.
|
|
111
|
+
*/
|
|
112
|
+
permissionWarningFor(result: Pick<ImportMarkdownResult, 'outcome' | 'permissions'>, sourcePath: string): {
|
|
113
|
+
warning?: string;
|
|
114
|
+
};
|
|
61
115
|
/**
|
|
62
116
|
* List all documents, optionally filtered by path prefix.
|
|
63
117
|
* Handles pagination automatically.
|
|
@@ -79,6 +133,13 @@ export declare class MoxnClient {
|
|
|
79
133
|
getDocumentMarkdown(documentId: string, opts?: {
|
|
80
134
|
forEdit?: boolean;
|
|
81
135
|
}): Promise<DocumentMarkdownResponse | null>;
|
|
136
|
+
/**
|
|
137
|
+
* Read one document through the `read` tool (`/api/v1/kb/tools`) — the same
|
|
138
|
+
* serialization agents and the CLI see. The export uses it for the kinds the
|
|
139
|
+
* grammar export has no body for (report / file / slides). Returns the single
|
|
140
|
+
* read item: a document shape carrying `kind`, or an `{ error }` item.
|
|
141
|
+
*/
|
|
142
|
+
readDocument(documentId: string): Promise<ReadDocumentItem>;
|
|
82
143
|
/**
|
|
83
144
|
* Build a `storageKey → signedDownloadUrl` map for a document's media, by
|
|
84
145
|
* reusing the `get_document_content` export action (the blocks read, which
|
package/dist/client.js
CHANGED
|
@@ -2,13 +2,40 @@
|
|
|
2
2
|
* API client for Moxn KB
|
|
3
3
|
*/
|
|
4
4
|
import * as fs from 'fs/promises';
|
|
5
|
-
import { formatApiError } from './api-error.js';
|
|
5
|
+
import { formatApiError, readJson } from './api-error.js';
|
|
6
|
+
import { moxnFetch } from './http.js';
|
|
6
7
|
import { sectionsToGrammarMarkdown, buildImportFrontMatter, } from './blocks-to-grammar.js';
|
|
8
|
+
/**
|
|
9
|
+
* The warning (if any) for an import whose `--default-permission` /
|
|
10
|
+
* `--ai-access` did not take effect: an existing document is never
|
|
11
|
+
* re-permissioned by a re-import, and a server that predates the fields
|
|
12
|
+
* creates documents with its defaults without echoing `permissions`.
|
|
13
|
+
*/
|
|
14
|
+
export function importPermissionWarning(requested, result) {
|
|
15
|
+
const asked = [
|
|
16
|
+
requested.defaultPermission &&
|
|
17
|
+
`--default-permission ${requested.defaultPermission}`,
|
|
18
|
+
requested.aiAccess && `--ai-access ${requested.aiAccess}`,
|
|
19
|
+
].filter(Boolean);
|
|
20
|
+
if (asked.length === 0 || result.outcome === 'skipped')
|
|
21
|
+
return undefined;
|
|
22
|
+
if (result.outcome === 'updated') {
|
|
23
|
+
return `${asked.join(', ')} not applied: this updated an existing document, whose permissions are left as they were (change them in the app)`;
|
|
24
|
+
}
|
|
25
|
+
if (!result.permissions) {
|
|
26
|
+
return `${asked.join(', ')}: the server did not apply them (it predates importer permissions) — the document was created with the server defaults`;
|
|
27
|
+
}
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
7
30
|
export class MoxnClient {
|
|
8
31
|
apiUrl;
|
|
9
32
|
apiKey;
|
|
10
33
|
defaultPermission;
|
|
11
34
|
aiAccess;
|
|
35
|
+
/** Every request goes through {@link moxnFetch} (bypass-header aware). */
|
|
36
|
+
fetch(url, init = {}) {
|
|
37
|
+
return moxnFetch(url, init);
|
|
38
|
+
}
|
|
12
39
|
constructor(options) {
|
|
13
40
|
this.apiUrl = options.apiUrl.replace(/\/$/, '');
|
|
14
41
|
this.apiKey = options.apiKey;
|
|
@@ -72,6 +99,7 @@ export class MoxnClient {
|
|
|
72
99
|
sectionIds: result.sectionIds,
|
|
73
100
|
references: doc.references,
|
|
74
101
|
sourcePageId: doc.metadata?.notionPageId,
|
|
102
|
+
...this.permissionWarningFor(result, doc.sourcePath),
|
|
75
103
|
duration: Date.now() - startTime,
|
|
76
104
|
};
|
|
77
105
|
}
|
|
@@ -85,6 +113,17 @@ export class MoxnClient {
|
|
|
85
113
|
};
|
|
86
114
|
}
|
|
87
115
|
}
|
|
116
|
+
/**
|
|
117
|
+
* {@link importPermissionWarning} for this client's requested permissions,
|
|
118
|
+
* logged once and returned as a spreadable `{ warning }`.
|
|
119
|
+
*/
|
|
120
|
+
permissionWarningFor(result, sourcePath) {
|
|
121
|
+
const warning = importPermissionWarning({ defaultPermission: this.defaultPermission, aiAccess: this.aiAccess }, result);
|
|
122
|
+
if (!warning)
|
|
123
|
+
return {};
|
|
124
|
+
console.warn(` ⚠ ${warning}: ${sourcePath}`);
|
|
125
|
+
return { warning };
|
|
126
|
+
}
|
|
88
127
|
// ──────────────────────────────────────────────
|
|
89
128
|
// Export methods
|
|
90
129
|
// ──────────────────────────────────────────────
|
|
@@ -112,14 +151,14 @@ export class MoxnClient {
|
|
|
112
151
|
params.set('modifiedAfter', dateFilter.modifiedAfter);
|
|
113
152
|
if (dateFilter?.modifiedBefore)
|
|
114
153
|
params.set('modifiedBefore', dateFilter.modifiedBefore);
|
|
115
|
-
const response = await fetch(`${this.apiUrl}/api/v1/kb/documents?${params}`, {
|
|
154
|
+
const response = await this.fetch(`${this.apiUrl}/api/v1/kb/documents?${params}`, {
|
|
116
155
|
headers: { 'x-api-key': this.apiKey },
|
|
117
156
|
});
|
|
118
157
|
if (!response.ok) {
|
|
119
158
|
const error = await response.text();
|
|
120
159
|
throw new Error(`Failed to list documents: ${response.status} ${error}`);
|
|
121
160
|
}
|
|
122
|
-
const data = await response
|
|
161
|
+
const data = await readJson(response);
|
|
123
162
|
allDocs.push(...data.documents);
|
|
124
163
|
if (!data.pagination.hasMore)
|
|
125
164
|
break;
|
|
@@ -131,14 +170,14 @@ export class MoxnClient {
|
|
|
131
170
|
* Get full document detail with sections and content.
|
|
132
171
|
*/
|
|
133
172
|
async getDocument(documentId) {
|
|
134
|
-
const response = await fetch(`${this.apiUrl}/api/v1/kb/documents/${documentId}`, {
|
|
173
|
+
const response = await this.fetch(`${this.apiUrl}/api/v1/kb/documents/${documentId}`, {
|
|
135
174
|
headers: { 'x-api-key': this.apiKey },
|
|
136
175
|
});
|
|
137
176
|
if (!response.ok) {
|
|
138
177
|
const error = await response.text();
|
|
139
178
|
throw new Error(`Failed to get document ${documentId}: ${response.status} ${error}`);
|
|
140
179
|
}
|
|
141
|
-
return response
|
|
180
|
+
return readJson(response);
|
|
142
181
|
}
|
|
143
182
|
/**
|
|
144
183
|
* Get a document's GRAMMAR markdown (front-matter + body + `:::image/:::csv/
|
|
@@ -150,7 +189,7 @@ export class MoxnClient {
|
|
|
150
189
|
* address sections stably.
|
|
151
190
|
*/
|
|
152
191
|
async getDocumentMarkdown(documentId, opts) {
|
|
153
|
-
const response = await fetch(`${this.apiUrl}/api/v1/kb/export`, {
|
|
192
|
+
const response = await this.fetch(`${this.apiUrl}/api/v1/kb/export`, {
|
|
154
193
|
method: 'POST',
|
|
155
194
|
headers: {
|
|
156
195
|
'Content-Type': 'application/json',
|
|
@@ -166,9 +205,38 @@ export class MoxnClient {
|
|
|
166
205
|
const body = await response.text();
|
|
167
206
|
throw new Error(`Failed to get document markdown ${documentId}: ${response.status} ${body}`);
|
|
168
207
|
}
|
|
169
|
-
const data = await response
|
|
208
|
+
const data = await readJson(response);
|
|
170
209
|
return (data.result?.document ?? null);
|
|
171
210
|
}
|
|
211
|
+
/**
|
|
212
|
+
* Read one document through the `read` tool (`/api/v1/kb/tools`) — the same
|
|
213
|
+
* serialization agents and the CLI see. The export uses it for the kinds the
|
|
214
|
+
* grammar export has no body for (report / file / slides). Returns the single
|
|
215
|
+
* read item: a document shape carrying `kind`, or an `{ error }` item.
|
|
216
|
+
*/
|
|
217
|
+
async readDocument(documentId) {
|
|
218
|
+
const response = await this.fetch(`${this.apiUrl}/api/v1/kb/tools`, {
|
|
219
|
+
method: 'POST',
|
|
220
|
+
headers: {
|
|
221
|
+
'Content-Type': 'application/json',
|
|
222
|
+
'x-api-key': this.apiKey,
|
|
223
|
+
},
|
|
224
|
+
body: JSON.stringify({
|
|
225
|
+
tool: 'read',
|
|
226
|
+
params: { items: [{ type: 'document', documentId }] },
|
|
227
|
+
}),
|
|
228
|
+
});
|
|
229
|
+
if (!response.ok) {
|
|
230
|
+
const body = await response.json().catch(() => ({}));
|
|
231
|
+
throw new Error(formatApiError(response.status, body));
|
|
232
|
+
}
|
|
233
|
+
const data = await readJson(response);
|
|
234
|
+
const item = data.result?.items?.[0];
|
|
235
|
+
if (!item) {
|
|
236
|
+
throw new Error(`read returned no item for document ${documentId}`);
|
|
237
|
+
}
|
|
238
|
+
return item;
|
|
239
|
+
}
|
|
172
240
|
/**
|
|
173
241
|
* Build a `storageKey → signedDownloadUrl` map for a document's media, by
|
|
174
242
|
* reusing the `get_document_content` export action (the blocks read, which
|
|
@@ -178,7 +246,7 @@ export class MoxnClient {
|
|
|
178
246
|
* Returns an empty map when the document has no resolvable media.
|
|
179
247
|
*/
|
|
180
248
|
async getDocumentMediaUrls(documentId) {
|
|
181
|
-
const response = await fetch(`${this.apiUrl}/api/v1/kb/export`, {
|
|
249
|
+
const response = await this.fetch(`${this.apiUrl}/api/v1/kb/export`, {
|
|
182
250
|
method: 'POST',
|
|
183
251
|
headers: {
|
|
184
252
|
'Content-Type': 'application/json',
|
|
@@ -190,7 +258,7 @@ export class MoxnClient {
|
|
|
190
258
|
const body = await response.text();
|
|
191
259
|
throw new Error(`Failed to get document content ${documentId}: ${response.status} ${body}`);
|
|
192
260
|
}
|
|
193
|
-
const data = await response
|
|
261
|
+
const data = await readJson(response);
|
|
194
262
|
const doc = data.result?.document;
|
|
195
263
|
const map = new Map();
|
|
196
264
|
if (!doc)
|
|
@@ -215,7 +283,7 @@ export class MoxnClient {
|
|
|
215
283
|
* onConflict 'update' → replace_document (full-set PUT)
|
|
216
284
|
*/
|
|
217
285
|
async importMarkdown(input) {
|
|
218
|
-
const response = await fetch(`${this.apiUrl}/api/v1/kb/import`, {
|
|
286
|
+
const response = await this.fetch(`${this.apiUrl}/api/v1/kb/import`, {
|
|
219
287
|
method: 'POST',
|
|
220
288
|
headers: {
|
|
221
289
|
'Content-Type': 'application/json',
|
|
@@ -226,20 +294,25 @@ export class MoxnClient {
|
|
|
226
294
|
markdown: input.markdown,
|
|
227
295
|
path: input.path,
|
|
228
296
|
onConflict: input.onConflict,
|
|
297
|
+
// Applied by the server to a document this import CREATES.
|
|
298
|
+
...(this.defaultPermission
|
|
299
|
+
? { defaultPermission: this.defaultPermission }
|
|
300
|
+
: {}),
|
|
301
|
+
...(this.aiAccess ? { aiAccess: this.aiAccess } : {}),
|
|
229
302
|
}),
|
|
230
303
|
});
|
|
231
304
|
if (!response.ok) {
|
|
232
305
|
const body = await response.json().catch(() => ({}));
|
|
233
306
|
throw new Error(formatApiError(response.status, body));
|
|
234
307
|
}
|
|
235
|
-
const data = await response
|
|
308
|
+
const data = await readJson(response);
|
|
236
309
|
return data.result;
|
|
237
310
|
}
|
|
238
311
|
/**
|
|
239
312
|
* Download a file from a URL to a local path.
|
|
240
313
|
*/
|
|
241
314
|
async downloadFile(url, destPath) {
|
|
242
|
-
const response = await fetch(url);
|
|
315
|
+
const response = await this.fetch(url);
|
|
243
316
|
if (!response.ok) {
|
|
244
317
|
throw new Error(`Failed to download ${url}: ${response.status}`);
|
|
245
318
|
}
|
|
@@ -338,7 +411,7 @@ export class MoxnClient {
|
|
|
338
411
|
block.type === 'url' &&
|
|
339
412
|
block.url) {
|
|
340
413
|
try {
|
|
341
|
-
const response = await fetch(block.url);
|
|
414
|
+
const response = await this.fetch(block.url);
|
|
342
415
|
if (!response.ok)
|
|
343
416
|
throw new Error(`HTTP ${response.status}`);
|
|
344
417
|
const data = Buffer.from(await response.arrayBuffer());
|
|
@@ -364,7 +437,7 @@ export class MoxnClient {
|
|
|
364
437
|
// 1. Get presigned upload URL
|
|
365
438
|
const { key, uploadUrl } = await this.getUploadUrl(mimeType, filename);
|
|
366
439
|
// 2. PUT file to presigned URL
|
|
367
|
-
const response = await fetch(uploadUrl, {
|
|
440
|
+
const response = await this.fetch(uploadUrl, {
|
|
368
441
|
method: 'PUT',
|
|
369
442
|
headers: { 'Content-Type': mimeType },
|
|
370
443
|
body: new Uint8Array(data),
|
|
@@ -375,7 +448,7 @@ export class MoxnClient {
|
|
|
375
448
|
return { key };
|
|
376
449
|
}
|
|
377
450
|
async getUploadUrl(type, filename) {
|
|
378
|
-
const response = await fetch(`${this.apiUrl}/api/v1/kb/upload`, {
|
|
451
|
+
const response = await this.fetch(`${this.apiUrl}/api/v1/kb/upload`, {
|
|
379
452
|
method: 'POST',
|
|
380
453
|
headers: {
|
|
381
454
|
'Content-Type': 'application/json',
|
|
@@ -387,7 +460,7 @@ export class MoxnClient {
|
|
|
387
460
|
const body = await response.text();
|
|
388
461
|
throw new Error(`Upload URL request failed: ${response.status} ${body}`);
|
|
389
462
|
}
|
|
390
|
-
const data = await response
|
|
463
|
+
const data = await readJson(response);
|
|
391
464
|
return { key: data.key, uploadUrl: data.uploadUrl };
|
|
392
465
|
}
|
|
393
466
|
// ──────────────────────────────────────────────
|
|
@@ -397,7 +470,7 @@ export class MoxnClient {
|
|
|
397
470
|
* Create a KB database.
|
|
398
471
|
*/
|
|
399
472
|
async createDatabase(input) {
|
|
400
|
-
const response = await fetch(`${this.apiUrl}/api/v1/kb/databases`, {
|
|
473
|
+
const response = await this.fetch(`${this.apiUrl}/api/v1/kb/databases`, {
|
|
401
474
|
method: 'POST',
|
|
402
475
|
headers: {
|
|
403
476
|
'Content-Type': 'application/json',
|
|
@@ -409,13 +482,13 @@ export class MoxnClient {
|
|
|
409
482
|
const body = await response.json().catch(() => ({}));
|
|
410
483
|
throw new Error(body.error || `Failed to create database: ${response.status}`);
|
|
411
484
|
}
|
|
412
|
-
return response
|
|
485
|
+
return readJson(response);
|
|
413
486
|
}
|
|
414
487
|
/**
|
|
415
488
|
* Add a column to a KB database.
|
|
416
489
|
*/
|
|
417
490
|
async addDatabaseColumn(databaseId, input) {
|
|
418
|
-
const response = await fetch(`${this.apiUrl}/api/v1/kb/databases/${databaseId}/columns`, {
|
|
491
|
+
const response = await this.fetch(`${this.apiUrl}/api/v1/kb/databases/${databaseId}/columns`, {
|
|
419
492
|
method: 'POST',
|
|
420
493
|
headers: {
|
|
421
494
|
'Content-Type': 'application/json',
|
|
@@ -427,13 +500,13 @@ export class MoxnClient {
|
|
|
427
500
|
const body = await response.json().catch(() => ({}));
|
|
428
501
|
throw new Error(body.error || `Failed to add column: ${response.status}`);
|
|
429
502
|
}
|
|
430
|
-
return response
|
|
503
|
+
return readJson(response);
|
|
431
504
|
}
|
|
432
505
|
/**
|
|
433
506
|
* Add a document to a KB database.
|
|
434
507
|
*/
|
|
435
508
|
async addDocumentToDatabase(databaseId, documentId) {
|
|
436
|
-
const response = await fetch(`${this.apiUrl}/api/v1/kb/databases/${databaseId}/documents`, {
|
|
509
|
+
const response = await this.fetch(`${this.apiUrl}/api/v1/kb/databases/${databaseId}/documents`, {
|
|
437
510
|
method: 'POST',
|
|
438
511
|
headers: {
|
|
439
512
|
'Content-Type': 'application/json',
|
|
@@ -453,7 +526,7 @@ export class MoxnClient {
|
|
|
453
526
|
* Create a tag (with automatic ancestor creation).
|
|
454
527
|
*/
|
|
455
528
|
async createTag(input) {
|
|
456
|
-
const response = await fetch(`${this.apiUrl}/api/v1/kb/tags`, {
|
|
529
|
+
const response = await this.fetch(`${this.apiUrl}/api/v1/kb/tags`, {
|
|
457
530
|
method: 'POST',
|
|
458
531
|
headers: {
|
|
459
532
|
'Content-Type': 'application/json',
|
|
@@ -473,13 +546,13 @@ export class MoxnClient {
|
|
|
473
546
|
}
|
|
474
547
|
throw new Error(body.error || `Failed to create tag: ${response.status}`);
|
|
475
548
|
}
|
|
476
|
-
return response
|
|
549
|
+
return readJson(response);
|
|
477
550
|
}
|
|
478
551
|
/**
|
|
479
552
|
* Assign a tag to a document.
|
|
480
553
|
*/
|
|
481
554
|
async assignTag(documentId, tagId, branchId) {
|
|
482
|
-
const response = await fetch(`${this.apiUrl}/api/v1/kb/documents/${documentId}/tags`, {
|
|
555
|
+
const response = await this.fetch(`${this.apiUrl}/api/v1/kb/documents/${documentId}/tags`, {
|
|
483
556
|
method: 'POST',
|
|
484
557
|
headers: {
|
|
485
558
|
'Content-Type': 'application/json',
|
|
@@ -497,7 +570,7 @@ export class MoxnClient {
|
|
|
497
570
|
* Returns { created, skipped, errors }.
|
|
498
571
|
*/
|
|
499
572
|
async createReferences(documentId, references) {
|
|
500
|
-
const response = await fetch(`${this.apiUrl}/api/v1/kb/documents/${documentId}/references`, {
|
|
573
|
+
const response = await this.fetch(`${this.apiUrl}/api/v1/kb/documents/${documentId}/references`, {
|
|
501
574
|
method: 'POST',
|
|
502
575
|
headers: {
|
|
503
576
|
'Content-Type': 'application/json',
|
|
@@ -509,51 +582,51 @@ export class MoxnClient {
|
|
|
509
582
|
const body = await response.json().catch(() => ({}));
|
|
510
583
|
throw new Error(body.error || `Failed to create references: ${response.status}`);
|
|
511
584
|
}
|
|
512
|
-
return response
|
|
585
|
+
return readJson(response);
|
|
513
586
|
}
|
|
514
587
|
/**
|
|
515
588
|
* List all KB databases for the tenant.
|
|
516
589
|
*/
|
|
517
590
|
async listDatabases() {
|
|
518
|
-
const response = await fetch(`${this.apiUrl}/api/v1/kb/databases`, {
|
|
591
|
+
const response = await this.fetch(`${this.apiUrl}/api/v1/kb/databases`, {
|
|
519
592
|
headers: { 'x-api-key': this.apiKey },
|
|
520
593
|
});
|
|
521
594
|
if (!response.ok) {
|
|
522
595
|
const body = await response.json().catch(() => ({}));
|
|
523
596
|
throw new Error(body.error || `Failed to list databases: ${response.status}`);
|
|
524
597
|
}
|
|
525
|
-
const data = await response
|
|
598
|
+
const data = await readJson(response);
|
|
526
599
|
return data.databases;
|
|
527
600
|
}
|
|
528
601
|
/**
|
|
529
602
|
* Get fully resolved database with columns, options, and document property values.
|
|
530
603
|
*/
|
|
531
604
|
async resolveDatabase(databaseId) {
|
|
532
|
-
const response = await fetch(`${this.apiUrl}/api/v1/kb/databases/${databaseId}/resolve`, {
|
|
605
|
+
const response = await this.fetch(`${this.apiUrl}/api/v1/kb/databases/${databaseId}/resolve`, {
|
|
533
606
|
headers: { 'x-api-key': this.apiKey },
|
|
534
607
|
});
|
|
535
608
|
if (!response.ok) {
|
|
536
609
|
const body = await response.json().catch(() => ({}));
|
|
537
610
|
throw new Error(body.error || `Failed to resolve database: ${response.status}`);
|
|
538
611
|
}
|
|
539
|
-
return response
|
|
612
|
+
return readJson(response);
|
|
540
613
|
}
|
|
541
614
|
/**
|
|
542
615
|
* Get Notion database mapping for a KB database.
|
|
543
616
|
*/
|
|
544
617
|
async getNotionDatabaseMapping(kbDatabaseId) {
|
|
545
|
-
const response = await fetch(`${this.apiUrl}/api/v1/kb/notion-mappings/databases/by-kb-database/${kbDatabaseId}`, { headers: { 'x-api-key': this.apiKey } });
|
|
618
|
+
const response = await this.fetch(`${this.apiUrl}/api/v1/kb/notion-mappings/databases/by-kb-database/${kbDatabaseId}`, { headers: { 'x-api-key': this.apiKey } });
|
|
546
619
|
if (!response.ok) {
|
|
547
620
|
const body = await response.json().catch(() => ({}));
|
|
548
621
|
throw new Error(body.error || `Failed to get database mapping: ${response.status}`);
|
|
549
622
|
}
|
|
550
|
-
return response
|
|
623
|
+
return readJson(response);
|
|
551
624
|
}
|
|
552
625
|
/**
|
|
553
626
|
* Create or upsert a Notion database mapping.
|
|
554
627
|
*/
|
|
555
628
|
async createNotionDatabaseMapping(input) {
|
|
556
|
-
const response = await fetch(`${this.apiUrl}/api/v1/kb/notion-mappings/databases`, {
|
|
629
|
+
const response = await this.fetch(`${this.apiUrl}/api/v1/kb/notion-mappings/databases`, {
|
|
557
630
|
method: 'POST',
|
|
558
631
|
headers: {
|
|
559
632
|
'Content-Type': 'application/json',
|
|
@@ -565,13 +638,13 @@ export class MoxnClient {
|
|
|
565
638
|
const body = await response.json().catch(() => ({}));
|
|
566
639
|
throw new Error(body.error || `Failed to create database mapping: ${response.status}`);
|
|
567
640
|
}
|
|
568
|
-
return response
|
|
641
|
+
return readJson(response);
|
|
569
642
|
}
|
|
570
643
|
/**
|
|
571
644
|
* Set a scalar property value on a document in a database.
|
|
572
645
|
*/
|
|
573
646
|
async setPropertyValue(databaseId, documentId, columnName, value) {
|
|
574
|
-
const response = await fetch(`${this.apiUrl}/api/v1/kb/databases/${databaseId}/properties`, {
|
|
647
|
+
const response = await this.fetch(`${this.apiUrl}/api/v1/kb/databases/${databaseId}/properties`, {
|
|
575
648
|
method: 'POST',
|
|
576
649
|
headers: {
|
|
577
650
|
'Content-Type': 'application/json',
|
|
@@ -591,7 +664,7 @@ export class MoxnClient {
|
|
|
591
664
|
* Create or upsert a Notion page → KB document mapping.
|
|
592
665
|
*/
|
|
593
666
|
async createNotionPageMapping(input) {
|
|
594
|
-
const response = await fetch(`${this.apiUrl}/api/v1/kb/notion-mappings`, {
|
|
667
|
+
const response = await this.fetch(`${this.apiUrl}/api/v1/kb/notion-mappings`, {
|
|
595
668
|
method: 'POST',
|
|
596
669
|
headers: {
|
|
597
670
|
'Content-Type': 'application/json',
|
|
@@ -609,14 +682,14 @@ export class MoxnClient {
|
|
|
609
682
|
* Returns a Map of notionDatabaseId → kbDatabaseId.
|
|
610
683
|
*/
|
|
611
684
|
async getAllDatabaseMappings() {
|
|
612
|
-
const response = await fetch(`${this.apiUrl}/api/v1/kb/notion-mappings/databases`, {
|
|
685
|
+
const response = await this.fetch(`${this.apiUrl}/api/v1/kb/notion-mappings/databases`, {
|
|
613
686
|
headers: { 'x-api-key': this.apiKey },
|
|
614
687
|
});
|
|
615
688
|
if (!response.ok) {
|
|
616
689
|
const body = await response.json().catch(() => ({}));
|
|
617
690
|
throw new Error(body.error || `Failed to get database mappings: ${response.status}`);
|
|
618
691
|
}
|
|
619
|
-
const data = await response
|
|
692
|
+
const data = await readJson(response);
|
|
620
693
|
const map = new Map();
|
|
621
694
|
for (const mapping of data.mappings) {
|
|
622
695
|
map.set(mapping.notionDatabaseId, mapping.kbDatabaseId);
|