@agent-native/core 0.154.2 → 0.154.4

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 (43) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/content/actions/_blocks-field-identity.ts +464 -0
  3. package/corpus/templates/content/actions/_database-utils.ts +11 -3
  4. package/corpus/templates/content/actions/_property-utils.ts +137 -30
  5. package/corpus/templates/content/actions/configure-document-property.ts +5 -0
  6. package/corpus/templates/content/actions/delete-document-property.ts +38 -17
  7. package/corpus/templates/content/actions/delete-document.ts +3 -0
  8. package/corpus/templates/content/actions/edit-document.ts +18 -1
  9. package/corpus/templates/content/actions/export-document.ts +41 -0
  10. package/corpus/templates/content/actions/remove-database-items.ts +10 -0
  11. package/corpus/templates/content/actions/set-document-property.ts +62 -2
  12. package/corpus/templates/content/actions/update-document.ts +24 -0
  13. package/corpus/templates/content/app/components/editor/DocumentBlockFields.tsx +99 -11
  14. package/corpus/templates/content/app/components/editor/blockFieldSaveController.ts +6 -0
  15. package/corpus/templates/content/app/hooks/use-document-properties.ts +4 -1
  16. package/corpus/templates/content/app/i18n-data.ts +20 -0
  17. package/corpus/templates/content/changelog/2026-08-10-database-blocks-fields-now-keep-logical-block-identity-throu.md +6 -0
  18. package/corpus/templates/content/changelog/2026-08-13-blocks-field-conflicts-now-keep-the-newer-saved-version-visi.md +6 -0
  19. package/corpus/templates/content/docs/product/capabilities/content.object.block.md +5 -4
  20. package/corpus/templates/content/docs/product/capabilities/content.object.blocks-field.md +9 -4
  21. package/corpus/templates/content/docs/product/encyclopedia.md +4 -4
  22. package/corpus/templates/content/server/db/schema.ts +61 -0
  23. package/corpus/templates/content/server/plugins/db.ts +55 -0
  24. package/corpus/templates/content/shared/api.ts +3 -0
  25. package/corpus/templates/content/shared/blocks-field-identity.ts +554 -0
  26. package/corpus/templates/content/shared/document-export.ts +33 -1
  27. package/dist/cli/create.js +3 -0
  28. package/dist/collab/awareness.d.ts +2 -2
  29. package/dist/extensions/url-safety.d.ts +7 -2
  30. package/dist/extensions/url-safety.js +27 -12
  31. package/dist/mcp-client/oauth-client.d.ts +16 -1
  32. package/dist/mcp-client/oauth-client.js +177 -83
  33. package/dist/mcp-client/remote-store.js +46 -20
  34. package/dist/oauth-tokens/index.d.ts +2 -1
  35. package/dist/oauth-tokens/index.js +2 -1
  36. package/dist/oauth-tokens/lifecycle.d.ts +87 -0
  37. package/dist/oauth-tokens/lifecycle.js +434 -0
  38. package/dist/oauth-tokens/migrations.js +10 -0
  39. package/dist/oauth-tokens/store.d.ts +22 -0
  40. package/dist/oauth-tokens/store.js +141 -18
  41. package/dist/observability/routes.d.ts +1 -1
  42. package/dist/resources/handlers.d.ts +1 -1
  43. package/package.json +2 -1
@@ -0,0 +1,554 @@
1
+ import { docToNfm, nfmToDoc, type PMNode } from "./nfm.js";
2
+
3
+ export const BLOCKS_FIELD_IDENTITY_VERSION = 1;
4
+ export const BLOCK_TOMBSTONE_REVISION_WINDOW = 20;
5
+ export const MAX_BLOCK_TOMBSTONES_PER_FIELD = 500;
6
+
7
+ export type BlocksFieldIdentityStatus = "legacy" | "materialized" | "stale";
8
+
9
+ export interface BlocksFieldBlock {
10
+ id: string;
11
+ parentId: string | null;
12
+ kind: string;
13
+ position: number;
14
+ addressable: boolean;
15
+ }
16
+
17
+ export interface BlocksFieldTombstone {
18
+ id: string;
19
+ kind: string;
20
+ deletedAtRevision: number;
21
+ }
22
+
23
+ export interface BlocksFieldIdentity {
24
+ version: typeof BLOCKS_FIELD_IDENTITY_VERSION;
25
+ fieldId: string;
26
+ revision: number;
27
+ contentHash: string;
28
+ identityStatus: BlocksFieldIdentityStatus;
29
+ blocks: BlocksFieldBlock[];
30
+ tombstones: BlocksFieldTombstone[];
31
+ recoveryMode: "editor-undo-with-tombstones";
32
+ }
33
+
34
+ export interface StoredBlocksFieldBlock extends BlocksFieldBlock {
35
+ contentHash: string;
36
+ markdown: string;
37
+ deletedAtRevision: number | null;
38
+ recoveredAtRevision: number | null;
39
+ state: "live" | "deleted";
40
+ }
41
+
42
+ export interface StoredBlocksFieldIdentity {
43
+ fieldId: string;
44
+ revision: number;
45
+ contentHash: string;
46
+ blocks: StoredBlocksFieldBlock[];
47
+ }
48
+
49
+ interface BlockSnapshot {
50
+ path: string;
51
+ parentPath: string | null;
52
+ kind: string;
53
+ position: number;
54
+ addressable: boolean;
55
+ contentHash: string;
56
+ markdown: string;
57
+ preferredId: string | null;
58
+ }
59
+
60
+ const BLOCK_NODE_TYPES = new Set([
61
+ "paragraph",
62
+ "heading",
63
+ "horizontalRule",
64
+ "codeBlock",
65
+ "blockquote",
66
+ "bulletList",
67
+ "orderedList",
68
+ "listItem",
69
+ "taskList",
70
+ "taskItem",
71
+ "notionToggle",
72
+ "notionCallout",
73
+ "notionColumns",
74
+ "notionColumn",
75
+ "notionSyncedBlock",
76
+ "table",
77
+ "tableRow",
78
+ "tableHeader",
79
+ "tableCell",
80
+ "image",
81
+ "video",
82
+ "audio",
83
+ "notionBlockAtom",
84
+ "registryBlock",
85
+ "contentReference",
86
+ "localMdxComponent",
87
+ ]);
88
+
89
+ const NON_ADDRESSABLE_NODE_TYPES = new Set([
90
+ "bulletList",
91
+ "orderedList",
92
+ "taskList",
93
+ "tableRow",
94
+ "tableHeader",
95
+ "tableCell",
96
+ ]);
97
+
98
+ function hashString(value: string): string {
99
+ let first = 0x811c9dc5;
100
+ let second = 0x9e3779b9;
101
+ for (let index = 0; index < value.length; index++) {
102
+ const code = value.charCodeAt(index);
103
+ first ^= code;
104
+ first = Math.imul(first, 0x01000193);
105
+ second ^= code + index;
106
+ second = Math.imul(second, 0x85ebca6b);
107
+ }
108
+ return `${(first >>> 0).toString(16).padStart(8, "0")}${(second >>> 0)
109
+ .toString(16)
110
+ .padStart(8, "0")}`;
111
+ }
112
+
113
+ export function blocksFieldId(documentId: string, propertyId: string): string {
114
+ return `blocks_field_${hashString(`${documentId}\0${propertyId}`)}`;
115
+ }
116
+
117
+ export function blocksContentHash(markdown: string): string {
118
+ return `nfm_${hashString(markdown)}`;
119
+ }
120
+
121
+ function stableNodeValue(value: unknown): unknown {
122
+ if (Array.isArray(value)) return value.map(stableNodeValue);
123
+ if (!value || typeof value !== "object") return value;
124
+ return Object.fromEntries(
125
+ Object.entries(value as Record<string, unknown>)
126
+ .filter(([key]) => key !== "blockId")
127
+ .sort(([left], [right]) => left.localeCompare(right))
128
+ .map(([key, child]) => [key, stableNodeValue(child)]),
129
+ );
130
+ }
131
+
132
+ function nodeMarkdown(node: PMNode): string {
133
+ return docToNfm({ type: "doc", content: [node] });
134
+ }
135
+
136
+ function snapshotMarkdown(markdown: string): BlockSnapshot[] {
137
+ const snapshots: BlockSnapshot[] = [];
138
+ const doc = nfmToDoc(markdown);
139
+
140
+ function visit(nodes: PMNode[] | undefined, parentPath: string | null) {
141
+ let blockPosition = 0;
142
+ for (let index = 0; index < (nodes?.length ?? 0); index++) {
143
+ const node = nodes![index]!;
144
+ if (!BLOCK_NODE_TYPES.has(node.type)) continue;
145
+ const path =
146
+ parentPath === null
147
+ ? `${blockPosition}`
148
+ : `${parentPath}.${blockPosition}`;
149
+ const canonical = JSON.stringify(stableNodeValue(node));
150
+ snapshots.push({
151
+ path,
152
+ parentPath,
153
+ kind: node.type,
154
+ position: blockPosition,
155
+ addressable: !NON_ADDRESSABLE_NODE_TYPES.has(node.type),
156
+ contentHash: `block_${hashString(canonical)}`,
157
+ markdown: nodeMarkdown(node),
158
+ preferredId:
159
+ node.type === "registryBlock" &&
160
+ typeof node.attrs?.blockId === "string"
161
+ ? node.attrs.blockId
162
+ : null,
163
+ });
164
+ visit(node.content, path);
165
+ blockPosition++;
166
+ }
167
+ }
168
+
169
+ visit(doc.content, null);
170
+ return snapshots;
171
+ }
172
+
173
+ function deterministicBlockId(
174
+ fieldId: string,
175
+ snapshot: Pick<BlockSnapshot, "path" | "kind" | "contentHash">,
176
+ ): string {
177
+ return `block_${hashString(
178
+ `${fieldId}\0${snapshot.path}\0${snapshot.kind}\0${snapshot.contentHash}`,
179
+ )}`;
180
+ }
181
+
182
+ function fieldScopedBlockId(fieldId: string, candidateId: string): string {
183
+ return `block_${hashString(fieldId)}_${hashString(candidateId)}`;
184
+ }
185
+
186
+ function publicIdentity(
187
+ stored: StoredBlocksFieldIdentity,
188
+ identityStatus: BlocksFieldIdentityStatus,
189
+ ): BlocksFieldIdentity {
190
+ return {
191
+ version: BLOCKS_FIELD_IDENTITY_VERSION,
192
+ fieldId: stored.fieldId,
193
+ revision: stored.revision,
194
+ contentHash: stored.contentHash,
195
+ identityStatus,
196
+ blocks: stored.blocks
197
+ .filter((block) => block.state === "live")
198
+ .map(({ id, parentId, kind, position, addressable }) => ({
199
+ id,
200
+ parentId,
201
+ kind,
202
+ position,
203
+ addressable,
204
+ })),
205
+ tombstones: stored.blocks
206
+ .filter(
207
+ (
208
+ block,
209
+ ): block is StoredBlocksFieldBlock & {
210
+ deletedAtRevision: number;
211
+ } => block.state === "deleted" && block.deletedAtRevision !== null,
212
+ )
213
+ .map(({ id, kind, deletedAtRevision }) => ({
214
+ id,
215
+ kind,
216
+ deletedAtRevision,
217
+ })),
218
+ recoveryMode: "editor-undo-with-tombstones",
219
+ };
220
+ }
221
+
222
+ export function legacyBlocksFieldIdentity(args: {
223
+ documentId: string;
224
+ propertyId: string;
225
+ markdown: string;
226
+ }): BlocksFieldIdentity {
227
+ const fieldId = blocksFieldId(args.documentId, args.propertyId);
228
+ const snapshots = snapshotMarkdown(args.markdown);
229
+ const idByPath = new Map<string, string>();
230
+ const usedIds = new Set<string>();
231
+ const blocks: StoredBlocksFieldBlock[] = snapshots.map((snapshot) => {
232
+ const preferred = snapshot.preferredId
233
+ ? fieldScopedBlockId(fieldId, snapshot.preferredId)
234
+ : null;
235
+ const id =
236
+ preferred && !usedIds.has(preferred)
237
+ ? preferred
238
+ : deterministicBlockId(fieldId, snapshot);
239
+ usedIds.add(id);
240
+ idByPath.set(snapshot.path, id);
241
+ return {
242
+ id,
243
+ parentId: snapshot.parentPath
244
+ ? (idByPath.get(snapshot.parentPath) ?? null)
245
+ : null,
246
+ kind: snapshot.kind,
247
+ position: snapshot.position,
248
+ addressable: snapshot.addressable,
249
+ contentHash: snapshot.contentHash,
250
+ markdown: snapshot.markdown,
251
+ state: "live",
252
+ deletedAtRevision: null,
253
+ recoveredAtRevision: null,
254
+ };
255
+ });
256
+ return publicIdentity(
257
+ {
258
+ fieldId,
259
+ revision: 0,
260
+ contentHash: blocksContentHash(args.markdown),
261
+ blocks,
262
+ },
263
+ "legacy",
264
+ );
265
+ }
266
+
267
+ function uniqueIndexByKey<T>(
268
+ values: T[],
269
+ key: (value: T) => string,
270
+ ): Map<string, number> {
271
+ const counts = new Map<string, number>();
272
+ const index = new Map<string, number>();
273
+ values.forEach((value, valueIndex) => {
274
+ const valueKey = key(value);
275
+ counts.set(valueKey, (counts.get(valueKey) ?? 0) + 1);
276
+ index.set(valueKey, valueIndex);
277
+ });
278
+ for (const [valueKey, count] of counts) {
279
+ if (count !== 1) index.delete(valueKey);
280
+ }
281
+ return index;
282
+ }
283
+
284
+ function characterBigrams(value: string): Set<string> {
285
+ const normalized = value.toLocaleLowerCase().replace(/\s+/g, " ").trim();
286
+ if (normalized.length < 2) return new Set(normalized ? [normalized] : []);
287
+ const result = new Set<string>();
288
+ for (let index = 0; index < normalized.length - 1; index++) {
289
+ result.add(normalized.slice(index, index + 2));
290
+ }
291
+ return result;
292
+ }
293
+
294
+ function editSimilarity(left: string, right: string): number {
295
+ const leftBigrams = characterBigrams(left);
296
+ const rightBigrams = characterBigrams(right);
297
+ if (leftBigrams.size === 0 || rightBigrams.size === 0) return 0;
298
+ let shared = 0;
299
+ for (const bigram of leftBigrams) {
300
+ if (rightBigrams.has(bigram)) shared++;
301
+ }
302
+ return (2 * shared) / (leftBigrams.size + rightBigrams.size);
303
+ }
304
+
305
+ export function reconcileBlocksFieldIdentity(args: {
306
+ documentId: string;
307
+ propertyId: string;
308
+ previous: StoredBlocksFieldIdentity;
309
+ markdown: string;
310
+ createId: () => string;
311
+ }): StoredBlocksFieldIdentity {
312
+ const nextRevision = args.previous.revision + 1;
313
+ const snapshots = snapshotMarkdown(args.markdown);
314
+ const previousLive = args.previous.blocks.filter(
315
+ (block) => block.state === "live",
316
+ );
317
+ const previousDeleted = args.previous.blocks.filter(
318
+ (block) => block.state === "deleted",
319
+ );
320
+ const matchedPrevious = new Set<number>();
321
+ const assigned = new Map<number, StoredBlocksFieldBlock>();
322
+ const usedIds = new Set(args.previous.blocks.map((block) => block.id));
323
+
324
+ const previousExact = uniqueIndexByKey(
325
+ previousLive,
326
+ (block) => `${block.kind}\0${block.contentHash}`,
327
+ );
328
+ const nextExact = uniqueIndexByKey(
329
+ snapshots,
330
+ (snapshot) => `${snapshot.kind}\0${snapshot.contentHash}`,
331
+ );
332
+ for (const [key, previousIndex] of previousExact) {
333
+ const nextIndex = nextExact.get(key);
334
+ if (nextIndex === undefined) continue;
335
+ matchedPrevious.add(previousIndex);
336
+ assigned.set(nextIndex, previousLive[previousIndex]!);
337
+ }
338
+
339
+ // Split keeps the leading fragment's ID; merge keeps the receiving block's
340
+ // ID. These are the only cardinality-changing cases where position conveys
341
+ // more identity than text similarity.
342
+ const unmatchedKinds = new Set([
343
+ ...previousLive
344
+ .filter((_block, index) => !matchedPrevious.has(index))
345
+ .map((block) => block.kind),
346
+ ...snapshots
347
+ .filter((_snapshot, index) => !assigned.has(index))
348
+ .map((snapshot) => snapshot.kind),
349
+ ]);
350
+ for (const kind of unmatchedKinds) {
351
+ const previousIndexes = previousLive.flatMap((block, index) =>
352
+ !matchedPrevious.has(index) && block.kind === kind ? [index] : [],
353
+ );
354
+ const nextIndexes = snapshots.flatMap((snapshot, index) =>
355
+ !assigned.has(index) && snapshot.kind === kind ? [index] : [],
356
+ );
357
+ if (previousIndexes.length !== 1 && nextIndexes.length !== 1) {
358
+ continue;
359
+ }
360
+ if (previousIndexes.length === 0 || nextIndexes.length === 0) continue;
361
+ const samePosition = nextIndexes.find(
362
+ (nextIndex) =>
363
+ snapshots[nextIndex]!.position ===
364
+ previousLive[previousIndexes[0]!]!.position,
365
+ );
366
+ if (samePosition === undefined) continue;
367
+ matchedPrevious.add(previousIndexes[0]!);
368
+ assigned.set(samePosition, previousLive[previousIndexes[0]!]!);
369
+ }
370
+
371
+ // A reorder and a text edit can happen in one editor transaction. Pair only
372
+ // mutual, sufficiently similar best matches before considering position; an
373
+ // ambiguous match is left for the conservative positional rule below.
374
+ const bestNextForPrevious = new Map<
375
+ number,
376
+ { index: number; score: number }
377
+ >();
378
+ const bestPreviousForNext = new Map<
379
+ number,
380
+ { index: number; score: number }
381
+ >();
382
+ for (
383
+ let previousIndex = 0;
384
+ previousIndex < previousLive.length;
385
+ previousIndex++
386
+ ) {
387
+ if (matchedPrevious.has(previousIndex)) continue;
388
+ const previous = previousLive[previousIndex]!;
389
+ for (let nextIndex = 0; nextIndex < snapshots.length; nextIndex++) {
390
+ if (assigned.has(nextIndex)) continue;
391
+ const snapshot = snapshots[nextIndex]!;
392
+ if (previous.kind !== snapshot.kind) continue;
393
+ const score = editSimilarity(previous.markdown, snapshot.markdown);
394
+ if (score < 0.45) continue;
395
+ const previousBest = bestNextForPrevious.get(previousIndex);
396
+ if (!previousBest || score > previousBest.score) {
397
+ bestNextForPrevious.set(previousIndex, { index: nextIndex, score });
398
+ }
399
+ const nextBest = bestPreviousForNext.get(nextIndex);
400
+ if (!nextBest || score > nextBest.score) {
401
+ bestPreviousForNext.set(nextIndex, { index: previousIndex, score });
402
+ }
403
+ }
404
+ }
405
+ for (const [previousIndex, nextBest] of bestNextForPrevious) {
406
+ const previousBest = bestPreviousForNext.get(nextBest.index);
407
+ if (previousBest?.index !== previousIndex) continue;
408
+ matchedPrevious.add(previousIndex);
409
+ assigned.set(nextBest.index, previousLive[previousIndex]!);
410
+ }
411
+
412
+ for (let nextIndex = 0; nextIndex < snapshots.length; nextIndex++) {
413
+ if (assigned.has(nextIndex)) continue;
414
+ const snapshot = snapshots[nextIndex]!;
415
+ const samePosition = previousLive.findIndex(
416
+ (block, previousIndex) =>
417
+ !matchedPrevious.has(previousIndex) &&
418
+ block.kind === snapshot.kind &&
419
+ block.position === snapshot.position,
420
+ );
421
+ if (samePosition !== -1) {
422
+ matchedPrevious.add(samePosition);
423
+ assigned.set(nextIndex, previousLive[samePosition]!);
424
+ continue;
425
+ }
426
+ const sameKind = previousLive.findIndex(
427
+ (block, previousIndex) =>
428
+ !matchedPrevious.has(previousIndex) && block.kind === snapshot.kind,
429
+ );
430
+ if (sameKind !== -1) {
431
+ matchedPrevious.add(sameKind);
432
+ assigned.set(nextIndex, previousLive[sameKind]!);
433
+ }
434
+ }
435
+
436
+ const recoverable = uniqueIndexByKey(
437
+ previousDeleted,
438
+ (block) => `${block.kind}\0${block.contentHash}`,
439
+ );
440
+ const recoveredIds = new Set<string>();
441
+ const idByPath = new Map<string, string>();
442
+ const nextBlocks = snapshots.map((snapshot, nextIndex) => {
443
+ let previous = assigned.get(nextIndex);
444
+ if (!previous) {
445
+ const recoveredIndex = recoverable.get(
446
+ `${snapshot.kind}\0${snapshot.contentHash}`,
447
+ );
448
+ if (recoveredIndex !== undefined) {
449
+ previous = previousDeleted[recoveredIndex];
450
+ if (previous) {
451
+ recoveredIds.add(previous.id);
452
+ recoverable.delete(`${snapshot.kind}\0${snapshot.contentHash}`);
453
+ }
454
+ }
455
+ }
456
+ let id =
457
+ previous?.id ??
458
+ fieldScopedBlockId(
459
+ args.previous.fieldId,
460
+ snapshot.preferredId ?? args.createId(),
461
+ );
462
+ while (usedIds.has(id) && id !== previous?.id) {
463
+ id = fieldScopedBlockId(args.previous.fieldId, args.createId());
464
+ }
465
+ usedIds.add(id);
466
+ idByPath.set(snapshot.path, id);
467
+ return {
468
+ id,
469
+ parentId: snapshot.parentPath
470
+ ? (idByPath.get(snapshot.parentPath) ?? null)
471
+ : null,
472
+ kind: snapshot.kind,
473
+ position: snapshot.position,
474
+ addressable: snapshot.addressable,
475
+ contentHash: snapshot.contentHash,
476
+ markdown: snapshot.markdown,
477
+ state: "live" as const,
478
+ deletedAtRevision: null,
479
+ recoveredAtRevision: recoveredIds.has(id) ? nextRevision : null,
480
+ };
481
+ });
482
+
483
+ const deletedNow = previousLive
484
+ .filter((_block, index) => !matchedPrevious.has(index))
485
+ .map((block) => ({
486
+ ...block,
487
+ state: "deleted" as const,
488
+ deletedAtRevision: nextRevision,
489
+ recoveredAtRevision: null,
490
+ }));
491
+ const oldestRecoverableRevision =
492
+ nextRevision - BLOCK_TOMBSTONE_REVISION_WINDOW;
493
+ const retainedTombstones = [...previousDeleted, ...deletedNow]
494
+ .filter(
495
+ (block) =>
496
+ !recoveredIds.has(block.id) &&
497
+ block.deletedAtRevision !== null &&
498
+ block.deletedAtRevision > oldestRecoverableRevision,
499
+ )
500
+ .sort(
501
+ (left, right) =>
502
+ (right.deletedAtRevision ?? 0) - (left.deletedAtRevision ?? 0),
503
+ )
504
+ .slice(0, MAX_BLOCK_TOMBSTONES_PER_FIELD);
505
+
506
+ return {
507
+ fieldId: args.previous.fieldId,
508
+ revision: nextRevision,
509
+ contentHash: blocksContentHash(args.markdown),
510
+ blocks: [...nextBlocks, ...retainedTombstones],
511
+ };
512
+ }
513
+
514
+ export function exposeBlocksFieldIdentity(
515
+ stored: StoredBlocksFieldIdentity,
516
+ markdown: string,
517
+ ): BlocksFieldIdentity {
518
+ const currentHash = blocksContentHash(markdown);
519
+ if (currentHash === stored.contentHash) {
520
+ return publicIdentity(stored, "materialized");
521
+ }
522
+ let provisionalIndex = 0;
523
+ const reconciled = reconcileBlocksFieldIdentity({
524
+ documentId: "stale-read",
525
+ propertyId: stored.fieldId,
526
+ previous: stored,
527
+ markdown,
528
+ createId: () =>
529
+ `block_${hashString(`${stored.fieldId}\0${markdown}\0${provisionalIndex++}`)}`,
530
+ });
531
+ return publicIdentity({ ...reconciled, revision: stored.revision }, "stale");
532
+ }
533
+
534
+ export function materializeLegacyBlocksFieldIdentity(args: {
535
+ documentId: string;
536
+ propertyId: string;
537
+ markdown: string;
538
+ }): StoredBlocksFieldIdentity {
539
+ const publicState = legacyBlocksFieldIdentity(args);
540
+ const snapshots = snapshotMarkdown(args.markdown);
541
+ return {
542
+ fieldId: publicState.fieldId,
543
+ revision: 0,
544
+ contentHash: publicState.contentHash,
545
+ blocks: publicState.blocks.map((block, index) => ({
546
+ ...block,
547
+ contentHash: snapshots[index]!.contentHash,
548
+ markdown: snapshots[index]!.markdown,
549
+ state: "live",
550
+ deletedAtRevision: null,
551
+ recoveredAtRevision: null,
552
+ })),
553
+ };
554
+ }
@@ -1,3 +1,4 @@
1
+ import type { BlocksFieldIdentity } from "./blocks-field-identity.js";
1
2
  import { matchInlineMathAt } from "./inline-math.js";
2
3
  import { KATEX_STYLESHEET_URL, renderMathToHtml } from "./math-rendering.js";
3
4
 
@@ -9,6 +10,16 @@ export interface DocumentExportInput {
9
10
  content?: string | null;
10
11
  updatedAt?: string | null;
11
12
  format: DocumentExportFormat;
13
+ blocksFields?: BlocksFieldExport[];
14
+ }
15
+
16
+ export interface BlocksFieldExport {
17
+ databaseId: string;
18
+ propertyId: string;
19
+ name: string;
20
+ position: number;
21
+ markdown: string;
22
+ identity: BlocksFieldIdentity;
12
23
  }
13
24
 
14
25
  export interface DocumentExportPayload {
@@ -19,6 +30,15 @@ export interface DocumentExportPayload {
19
30
  mimeType: string;
20
31
  content: string;
21
32
  print: boolean;
33
+ blocksFields?: BlocksFieldExport[];
34
+ }
35
+
36
+ function serializeBlocksFieldsManifest(fields: BlocksFieldExport[]): string {
37
+ return JSON.stringify({ version: 1, fields })
38
+ .replace(/</g, "\\u003c")
39
+ .replace(/>/g, "\\u003e")
40
+ .replace(/&/g, "\\u0026")
41
+ .replace(/--/g, "\\u002d\\u002d");
22
42
  }
23
43
 
24
44
  const EXTENSION_BY_FORMAT: Record<DocumentExportFormat, string> = {
@@ -638,7 +658,10 @@ export function buildDocumentExport(
638
658
  const filename = exportFilename(title, input.format);
639
659
  const markdown = markdownWithTitle(title, input.content);
640
660
  const isHtmlLike = input.format === "html" || input.format === "pdf";
641
- const content = isHtmlLike
661
+ const manifest = input.blocksFields?.length
662
+ ? serializeBlocksFieldsManifest(input.blocksFields)
663
+ : null;
664
+ let content = isHtmlLike
642
665
  ? buildHtmlDocument({
643
666
  title,
644
667
  content: input.content ?? "",
@@ -646,6 +669,14 @@ export function buildDocumentExport(
646
669
  print: input.format === "pdf",
647
670
  })
648
671
  : markdown;
672
+ if (manifest) {
673
+ content = isHtmlLike
674
+ ? content.replace(
675
+ "</body>",
676
+ `<script type="application/json" id="agent-native-blocks">${manifest}</script>\n</body>`,
677
+ )
678
+ : `${content.trimEnd()}\n\n<!-- agent-native-blocks:${manifest} -->\n`;
679
+ }
649
680
 
650
681
  return {
651
682
  id: input.id,
@@ -655,5 +686,6 @@ export function buildDocumentExport(
655
686
  mimeType: MIME_BY_FORMAT[input.format],
656
687
  content,
657
688
  print: input.format === "pdf",
689
+ ...(input.blocksFields?.length ? { blocksFields: input.blocksFields } : {}),
658
690
  };
659
691
  }
@@ -81,6 +81,7 @@ const FIRST_PARTY_TARBALL_SYMLINK_EXCLUDES = [
81
81
  "*/CLAUDE.md",
82
82
  "*/.claude/skills",
83
83
  ];
84
+ const TAR_LISTING_MAX_BUFFER = 100 * 1024 * 1024;
84
85
  const localPackageTarballs = new Map();
85
86
  /** VCS/editor files that don't count as "not empty" for an in-place scaffold. */
86
87
  const IN_PLACE_ALLOWLIST = new Set([
@@ -1737,6 +1738,7 @@ function archiveSymlinksForExtraction(tarPath) {
1737
1738
  const listing = execFileSync("tar", ["tvzf", tarPath], {
1738
1739
  encoding: "utf-8",
1739
1740
  stdio: ["ignore", "pipe", "pipe"],
1741
+ maxBuffer: TAR_LISTING_MAX_BUFFER,
1740
1742
  });
1741
1743
  return listing.split(/\r?\n/).flatMap((line) => {
1742
1744
  if (!line.startsWith("l"))
@@ -1866,6 +1868,7 @@ function validateCommunityArchive(tarPath) {
1866
1868
  const listing = execFileSync("tar", ["tvzf", tarPath], {
1867
1869
  encoding: "utf-8",
1868
1870
  stdio: ["ignore", "pipe", "pipe"],
1871
+ maxBuffer: TAR_LISTING_MAX_BUFFER,
1869
1872
  });
1870
1873
  assertSafeCommunityArchiveListing(listing);
1871
1874
  }
@@ -62,11 +62,11 @@ export declare const postAwareness: import("h3").EventHandlerWithFetch<import("h
62
62
  error: string;
63
63
  states?: undefined;
64
64
  } | {
65
- error?: undefined;
66
65
  states: {
67
66
  clientId: number;
68
67
  state: string;
69
68
  }[];
69
+ error?: undefined;
70
70
  }>>;
71
71
  /**
72
72
  * GET /_agent-native/collab/:docId/users
@@ -77,9 +77,9 @@ export declare const getActiveUsers: import("h3").EventHandlerWithFetch<import("
77
77
  error: string;
78
78
  users?: undefined;
79
79
  } | {
80
- error?: undefined;
81
80
  users: {
82
81
  clientId: number;
83
82
  lastSeen: number;
84
83
  }[];
84
+ error?: undefined;
85
85
  }>>;
@@ -5,7 +5,9 @@ export declare function isBlockedExtensionUrl(url: string): boolean;
5
5
  * "public hostname resolves to a private address" gap before dispatch.
6
6
  */
7
7
  export declare function isBlockedExtensionUrlWithDns(url: string): Promise<boolean>;
8
- export declare function createSsrfSafeDispatcher(allowedPrivateOrigins?: readonly string[]): Promise<unknown | null>;
8
+ export declare function createSsrfSafeDispatcher(allowedPrivateOrigins?: readonly string[], destinationUrl?: string, options?: {
9
+ required?: boolean;
10
+ }): Promise<unknown>;
9
11
  /**
10
12
  * SSRF-safe `fetch` for any server-side request to a user/agent-supplied URL.
11
13
  *
@@ -14,7 +16,9 @@ export declare function createSsrfSafeDispatcher(allowedPrivateOrigins?: readonl
14
16
  * 1. Pre-flight DNS-aware private-address check (isBlockedExtensionUrlWithDns)
15
17
  * on the initial URL and on every redirect hop.
16
18
  * 2. A connect-time dispatcher that re-checks the resolved IP at TCP-connect
17
- * time (closes the DNS-rebinding TOCTOU) when undici is available.
19
+ * time (closes the DNS-rebinding TOCTOU) when the runtime supports the
20
+ * Node dispatcher. Edge runtimes retain preflight DNS checks, literal and
21
+ * rebinding-domain rejection, and per-hop redirect validation.
18
22
  * 3. Manual redirect handling — a public URL cannot 30x-redirect into the
19
23
  * private network because each hop is re-validated before it is followed.
20
24
  *
@@ -33,6 +37,7 @@ export declare function createSsrfSafeDispatcher(allowedPrivateOrigins?: readonl
33
37
  */
34
38
  export declare function ssrfSafeFetch(url: string, init?: RequestInit, options?: {
35
39
  maxRedirects?: number;
40
+ followRedirects?: boolean;
36
41
  httpsOnly?: boolean;
37
42
  assertUrlAllowed?: (url: string) => void | Promise<void>;
38
43
  /**