@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
package/corpus/README.md CHANGED
@@ -31,4 +31,4 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
31
31
 
32
32
  ## Generated Counts
33
33
 
34
- - template files: 8208
34
+ - template files: 8212
@@ -0,0 +1,464 @@
1
+ import { and, asc, eq, inArray } from "drizzle-orm";
2
+
3
+ import { getDb, schema } from "../server/db/index.js";
4
+ import {
5
+ blocksContentHash,
6
+ blocksFieldId,
7
+ exposeBlocksFieldIdentity,
8
+ legacyBlocksFieldIdentity,
9
+ materializeLegacyBlocksFieldIdentity,
10
+ reconcileBlocksFieldIdentity,
11
+ type BlocksFieldIdentity,
12
+ type StoredBlocksFieldIdentity,
13
+ } from "../shared/blocks-field-identity.js";
14
+ import { lockDatabaseMemberships } from "./_database-membership-lock.js";
15
+
16
+ type ContentDb = ReturnType<typeof getDb>;
17
+
18
+ export interface PrimaryBlocksField {
19
+ propertyId: string;
20
+ ownerEmail: string;
21
+ }
22
+
23
+ export class BlocksFieldRevisionConflictError extends Error {
24
+ readonly statusCode = 409;
25
+
26
+ constructor(message: string) {
27
+ super(message);
28
+ this.name = "BlocksFieldRevisionConflictError";
29
+ }
30
+ }
31
+
32
+ function nanoid(size = 12): string {
33
+ const chars =
34
+ "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
35
+ let id = "";
36
+ const bytes = crypto.getRandomValues(new Uint8Array(size));
37
+ for (const byte of bytes) id += chars[byte % chars.length];
38
+ return id;
39
+ }
40
+
41
+ function groups<T>(values: T[], size: number): T[][] {
42
+ const result: T[][] = [];
43
+ for (let index = 0; index < values.length; index += size) {
44
+ result.push(values.slice(index, index + size));
45
+ }
46
+ return result;
47
+ }
48
+
49
+ export async function lockPrimaryBlocksFields(
50
+ db: ContentDb,
51
+ documentId: string,
52
+ ): Promise<PrimaryBlocksField[]> {
53
+ return (
54
+ (await lockPrimaryBlocksFieldsForDocuments(db, [documentId])).get(
55
+ documentId,
56
+ ) ?? []
57
+ );
58
+ }
59
+
60
+ export async function lockPrimaryBlocksFieldsForDocuments(
61
+ db: ContentDb,
62
+ documentIds: string[],
63
+ ): Promise<Map<string, PrimaryBlocksField[]>> {
64
+ const uniqueDocumentIds = [...new Set(documentIds)];
65
+ const memberships: Array<{
66
+ id: string;
67
+ documentId: string;
68
+ }> = [];
69
+ for (const documentIdGroup of groups(uniqueDocumentIds, 90)) {
70
+ memberships.push(
71
+ ...(await db
72
+ .select({
73
+ id: schema.contentDatabaseItems.id,
74
+ documentId: schema.contentDatabaseItems.documentId,
75
+ })
76
+ .from(schema.contentDatabaseItems)
77
+ .where(
78
+ inArray(schema.contentDatabaseItems.documentId, documentIdGroup),
79
+ )),
80
+ );
81
+ }
82
+ const membershipIds = memberships.map((membership) => membership.id);
83
+ await lockDatabaseMemberships(db, membershipIds);
84
+ if (membershipIds.length === 0) return new Map();
85
+
86
+ const fields: Array<{
87
+ documentId: string;
88
+ propertyId: string | null;
89
+ ownerEmail: string;
90
+ }> = [];
91
+ for (const membershipIdGroup of groups(membershipIds, 90)) {
92
+ fields.push(
93
+ ...(await db
94
+ .select({
95
+ documentId: schema.contentDatabaseItems.documentId,
96
+ propertyId: schema.contentDatabases.primaryBlocksPropertyId,
97
+ ownerEmail: schema.contentDatabases.ownerEmail,
98
+ })
99
+ .from(schema.contentDatabaseItems)
100
+ .innerJoin(
101
+ schema.contentDatabases,
102
+ eq(
103
+ schema.contentDatabases.id,
104
+ schema.contentDatabaseItems.databaseId,
105
+ ),
106
+ )
107
+ .where(inArray(schema.contentDatabaseItems.id, membershipIdGroup))),
108
+ );
109
+ }
110
+ const fieldsByDocument = new Map<string, Map<string, string>>();
111
+ for (const field of fields) {
112
+ if (!field.propertyId) continue;
113
+ const documentFields =
114
+ fieldsByDocument.get(field.documentId) ?? new Map<string, string>();
115
+ documentFields.set(field.propertyId, field.ownerEmail);
116
+ fieldsByDocument.set(field.documentId, documentFields);
117
+ }
118
+ return new Map(
119
+ [...fieldsByDocument].map(([id, documentFields]) => [
120
+ id,
121
+ [...documentFields].map(([propertyId, ownerEmail]) => ({
122
+ propertyId,
123
+ ownerEmail,
124
+ })),
125
+ ]),
126
+ );
127
+ }
128
+
129
+ async function loadStoredIdentity(
130
+ db: ContentDb,
131
+ fieldId: string,
132
+ ): Promise<StoredBlocksFieldIdentity | null> {
133
+ const [field] = await db
134
+ .select()
135
+ .from(schema.documentBlockFields)
136
+ .where(eq(schema.documentBlockFields.id, fieldId));
137
+ if (!field) return null;
138
+ const blocks = await db
139
+ .select()
140
+ .from(schema.documentBlocks)
141
+ .where(eq(schema.documentBlocks.fieldId, fieldId))
142
+ .orderBy(asc(schema.documentBlocks.sortIndex));
143
+ return {
144
+ fieldId,
145
+ revision: field.revision,
146
+ contentHash: field.contentHash,
147
+ blocks: blocks.map((block) => ({
148
+ id: block.id,
149
+ parentId: block.parentId,
150
+ kind: block.kind,
151
+ position: block.position,
152
+ addressable: block.addressable,
153
+ contentHash: block.contentHash,
154
+ markdown: block.markdown,
155
+ state: block.state === "deleted" ? "deleted" : "live",
156
+ deletedAtRevision: block.deletedAtRevision,
157
+ recoveredAtRevision: block.recoveredAtRevision,
158
+ })),
159
+ };
160
+ }
161
+
162
+ export async function readBlocksFieldIdentity(args: {
163
+ db?: ContentDb;
164
+ documentId: string;
165
+ propertyId: string;
166
+ markdown: string;
167
+ }): Promise<BlocksFieldIdentity> {
168
+ const fieldId = blocksFieldId(args.documentId, args.propertyId);
169
+ const stored = await loadStoredIdentity(args.db ?? getDb(), fieldId);
170
+ return stored
171
+ ? exposeBlocksFieldIdentity(stored, args.markdown)
172
+ : legacyBlocksFieldIdentity(args);
173
+ }
174
+
175
+ export async function readBlocksFieldIdentities(args: {
176
+ db?: ContentDb;
177
+ fields: Array<{
178
+ documentId: string;
179
+ propertyId: string;
180
+ markdown: string;
181
+ }>;
182
+ }): Promise<Map<string, BlocksFieldIdentity>> {
183
+ const db = args.db ?? getDb();
184
+ const inputs = new Map(
185
+ args.fields.map((field) => [
186
+ blocksFieldId(field.documentId, field.propertyId),
187
+ field,
188
+ ]),
189
+ );
190
+ const storedFields: Array<typeof schema.documentBlockFields.$inferSelect> =
191
+ [];
192
+ for (const ids of groups([...inputs.keys()], 200)) {
193
+ if (ids.length === 0) continue;
194
+ storedFields.push(
195
+ ...(await db
196
+ .select()
197
+ .from(schema.documentBlockFields)
198
+ .where(inArray(schema.documentBlockFields.id, ids))),
199
+ );
200
+ }
201
+ const storedById = new Map(storedFields.map((field) => [field.id, field]));
202
+ const storedBlocks: Array<typeof schema.documentBlocks.$inferSelect> = [];
203
+ for (const ids of groups(
204
+ storedFields.map((field) => field.id),
205
+ 200,
206
+ )) {
207
+ if (ids.length === 0) continue;
208
+ storedBlocks.push(
209
+ ...(await db
210
+ .select()
211
+ .from(schema.documentBlocks)
212
+ .where(inArray(schema.documentBlocks.fieldId, ids))
213
+ .orderBy(asc(schema.documentBlocks.sortIndex))),
214
+ );
215
+ }
216
+ const blocksByField = new Map<
217
+ string,
218
+ Array<typeof schema.documentBlocks.$inferSelect>
219
+ >();
220
+ for (const block of storedBlocks) {
221
+ const values = blocksByField.get(block.fieldId) ?? [];
222
+ values.push(block);
223
+ blocksByField.set(block.fieldId, values);
224
+ }
225
+
226
+ const result = new Map<string, BlocksFieldIdentity>();
227
+ for (const [fieldId, input] of inputs) {
228
+ const field = storedById.get(fieldId);
229
+ if (!field) {
230
+ result.set(fieldId, legacyBlocksFieldIdentity(input));
231
+ continue;
232
+ }
233
+ result.set(
234
+ fieldId,
235
+ exposeBlocksFieldIdentity(
236
+ {
237
+ fieldId,
238
+ revision: field.revision,
239
+ contentHash: field.contentHash,
240
+ blocks: (blocksByField.get(fieldId) ?? []).map((block) => ({
241
+ id: block.id,
242
+ parentId: block.parentId,
243
+ kind: block.kind,
244
+ position: block.position,
245
+ addressable: block.addressable,
246
+ contentHash: block.contentHash,
247
+ markdown: block.markdown,
248
+ state: block.state === "deleted" ? "deleted" : "live",
249
+ deletedAtRevision: block.deletedAtRevision,
250
+ recoveredAtRevision: block.recoveredAtRevision,
251
+ })),
252
+ },
253
+ input.markdown,
254
+ ),
255
+ );
256
+ }
257
+ return result;
258
+ }
259
+
260
+ export async function persistBlocksFieldIdentity(args: {
261
+ db: ContentDb;
262
+ ownerEmail: string;
263
+ documentId: string;
264
+ propertyId: string;
265
+ previousMarkdown: string;
266
+ markdown: string;
267
+ expectedRevision?: number;
268
+ now: string;
269
+ }): Promise<StoredBlocksFieldIdentity> {
270
+ const fieldId = blocksFieldId(args.documentId, args.propertyId);
271
+ const stored = await loadStoredIdentity(args.db, fieldId);
272
+ const actualRevision = stored?.revision ?? 0;
273
+ if (
274
+ args.expectedRevision !== undefined &&
275
+ args.expectedRevision !== actualRevision
276
+ ) {
277
+ throw new BlocksFieldRevisionConflictError(
278
+ `Blocks field revision conflict: expected ${args.expectedRevision}, current ${actualRevision}`,
279
+ );
280
+ }
281
+
282
+ let previous =
283
+ stored ??
284
+ materializeLegacyBlocksFieldIdentity({
285
+ documentId: args.documentId,
286
+ propertyId: args.propertyId,
287
+ markdown: args.previousMarkdown,
288
+ });
289
+
290
+ // A legacy whole-field writer may have changed Markdown without updating the
291
+ // sidecar. Reconcile exact unique blocks first and report the resulting extra
292
+ // revision instead of silently pretending continuity was complete.
293
+ if (previous.contentHash !== blocksContentHash(args.previousMarkdown)) {
294
+ previous = reconcileBlocksFieldIdentity({
295
+ documentId: args.documentId,
296
+ propertyId: args.propertyId,
297
+ previous,
298
+ markdown: args.previousMarkdown,
299
+ createId: () => `block_${nanoid(16)}`,
300
+ });
301
+ }
302
+
303
+ let next = reconcileBlocksFieldIdentity({
304
+ documentId: args.documentId,
305
+ propertyId: args.propertyId,
306
+ previous,
307
+ markdown: args.markdown,
308
+ createId: () => `block_${nanoid(16)}`,
309
+ });
310
+
311
+ if (next.blocks.length > 0) {
312
+ const existingOwners = await args.db
313
+ .select({
314
+ id: schema.documentBlocks.id,
315
+ fieldId: schema.documentBlocks.fieldId,
316
+ })
317
+ .from(schema.documentBlocks)
318
+ .where(
319
+ inArray(
320
+ schema.documentBlocks.id,
321
+ next.blocks.map((block) => block.id),
322
+ ),
323
+ );
324
+ const remappedIds = new Map<string, string>();
325
+ const reserved = new Set(next.blocks.map((block) => block.id));
326
+ for (const existing of existingOwners) {
327
+ if (existing.fieldId === fieldId) continue;
328
+ let replacement = `block_${nanoid(16)}`;
329
+ while (reserved.has(replacement)) replacement = `block_${nanoid(16)}`;
330
+ reserved.add(replacement);
331
+ remappedIds.set(existing.id, replacement);
332
+ }
333
+ if (remappedIds.size > 0) {
334
+ next = {
335
+ ...next,
336
+ blocks: next.blocks.map((block) => ({
337
+ ...block,
338
+ id: remappedIds.get(block.id) ?? block.id,
339
+ parentId: block.parentId
340
+ ? (remappedIds.get(block.parentId) ?? block.parentId)
341
+ : null,
342
+ })),
343
+ };
344
+ }
345
+ }
346
+
347
+ if (stored) {
348
+ const applied = await args.db
349
+ .update(schema.documentBlockFields)
350
+ .set({
351
+ revision: next.revision,
352
+ contentHash: next.contentHash,
353
+ updatedAt: args.now,
354
+ })
355
+ .where(
356
+ and(
357
+ eq(schema.documentBlockFields.id, fieldId),
358
+ eq(schema.documentBlockFields.revision, actualRevision),
359
+ ),
360
+ )
361
+ .returning({ id: schema.documentBlockFields.id });
362
+ if (applied.length === 0) {
363
+ throw new BlocksFieldRevisionConflictError(
364
+ `Blocks field revision conflict: expected ${actualRevision}, current revision changed`,
365
+ );
366
+ }
367
+ } else {
368
+ const inserted = await args.db
369
+ .insert(schema.documentBlockFields)
370
+ .values({
371
+ id: fieldId,
372
+ ownerEmail: args.ownerEmail,
373
+ documentId: args.documentId,
374
+ propertyId: args.propertyId,
375
+ revision: next.revision,
376
+ contentHash: next.contentHash,
377
+ createdAt: args.now,
378
+ updatedAt: args.now,
379
+ })
380
+ .onConflictDoNothing()
381
+ .returning({ id: schema.documentBlockFields.id });
382
+ if (inserted.length === 0) {
383
+ throw new BlocksFieldRevisionConflictError(
384
+ "Blocks field revision conflict: concurrent first materialization",
385
+ );
386
+ }
387
+ }
388
+ await args.db
389
+ .delete(schema.documentBlocks)
390
+ .where(eq(schema.documentBlocks.fieldId, fieldId));
391
+ if (next.blocks.length > 0) {
392
+ await args.db.insert(schema.documentBlocks).values(
393
+ next.blocks.map((block, sortIndex) => ({
394
+ id: block.id,
395
+ ownerEmail: args.ownerEmail,
396
+ fieldId,
397
+ parentId: block.parentId,
398
+ kind: block.kind,
399
+ position: block.position,
400
+ sortIndex,
401
+ addressable: block.addressable,
402
+ contentHash: block.contentHash,
403
+ markdown: block.markdown,
404
+ state: block.state,
405
+ deletedAtRevision: block.deletedAtRevision,
406
+ recoveredAtRevision: block.recoveredAtRevision,
407
+ createdAt: args.now,
408
+ updatedAt: args.now,
409
+ })),
410
+ );
411
+ }
412
+ return next;
413
+ }
414
+
415
+ export async function deleteBlocksFieldIdentity(args: {
416
+ db: ContentDb;
417
+ documentId?: string;
418
+ documentIds?: string[];
419
+ propertyId?: string;
420
+ propertyIds?: string[];
421
+ }): Promise<void> {
422
+ if (
423
+ !args.documentId &&
424
+ !args.documentIds?.length &&
425
+ !args.propertyId &&
426
+ !args.propertyIds?.length
427
+ ) {
428
+ return;
429
+ }
430
+ const clauses = [];
431
+ if (args.documentId) {
432
+ clauses.push(eq(schema.documentBlockFields.documentId, args.documentId));
433
+ }
434
+ if (args.documentIds?.length) {
435
+ clauses.push(
436
+ inArray(schema.documentBlockFields.documentId, args.documentIds),
437
+ );
438
+ }
439
+ if (args.propertyId) {
440
+ clauses.push(eq(schema.documentBlockFields.propertyId, args.propertyId));
441
+ }
442
+ if (args.propertyIds?.length) {
443
+ clauses.push(
444
+ inArray(schema.documentBlockFields.propertyId, args.propertyIds),
445
+ );
446
+ }
447
+ const fields = await args.db
448
+ .select({ id: schema.documentBlockFields.id })
449
+ .from(schema.documentBlockFields)
450
+ .where(clauses.length === 1 ? clauses[0] : and(...clauses));
451
+ if (fields.length === 0) return;
452
+ await args.db.delete(schema.documentBlocks).where(
453
+ inArray(
454
+ schema.documentBlocks.fieldId,
455
+ fields.map((field) => field.id),
456
+ ),
457
+ );
458
+ await args.db.delete(schema.documentBlockFields).where(
459
+ inArray(
460
+ schema.documentBlockFields.id,
461
+ fields.map((field) => field.id),
462
+ ),
463
+ );
464
+ }
@@ -38,6 +38,7 @@ import {
38
38
  parsePropertyValue,
39
39
  type DocumentPropertyType,
40
40
  } from "../shared/properties.js";
41
+ import { deleteBlocksFieldIdentity } from "./_blocks-field-identity.js";
41
42
  import { favoriteDocumentIds } from "./_content-favorites.js";
42
43
  import {
43
44
  listContentOrganizationMemberships,
@@ -1417,15 +1418,21 @@ export async function deleteDatabaseDataForDocument(
1417
1418
  .from(schema.documentPropertyDefinitions)
1418
1419
  .where(eq(schema.documentPropertyDefinitions.databaseId, database.id));
1419
1420
 
1420
- for (const definition of definitions) {
1421
+ const definitionIds = definitions.map((definition) => definition.id);
1422
+ if (definitionIds.length > 0) {
1423
+ await deleteBlocksFieldIdentity({ db, propertyIds: definitionIds });
1421
1424
  await db
1422
1425
  .delete(schema.documentPropertyValues)
1423
- .where(eq(schema.documentPropertyValues.propertyId, definition.id));
1426
+ .where(
1427
+ inArray(schema.documentPropertyValues.propertyId, definitionIds),
1428
+ );
1424
1429
  // Independent Blocks-field content is keyed by property id; drop it so
1425
1430
  // deleting a database leaves no orphaned document_block_field_contents.
1426
1431
  await db
1427
1432
  .delete(schema.documentBlockFieldContents)
1428
- .where(eq(schema.documentBlockFieldContents.propertyId, definition.id));
1433
+ .where(
1434
+ inArray(schema.documentBlockFieldContents.propertyId, definitionIds),
1435
+ );
1429
1436
  }
1430
1437
  const sources = await db
1431
1438
  .select({ id: schema.contentDatabaseSources.id })
@@ -1482,6 +1489,7 @@ export async function deleteDatabaseDataForDocument(
1482
1489
  db,
1483
1490
  );
1484
1491
  if (item) {
1492
+ await deleteBlocksFieldIdentity({ db, documentId });
1485
1493
  await db
1486
1494
  .delete(schema.contentDatabaseItemKeyClaims)
1487
1495
  .where(eq(schema.contentDatabaseItemKeyClaims.documentId, documentId));