@agent-native/core 0.154.3 → 0.154.5

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 (64) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/content/.agents/skills/content/SKILL.md +2 -2
  3. package/corpus/templates/content/.agents/skills/document-editing/references/databases.md +20 -4
  4. package/corpus/templates/content/actions/_blocks-field-identity.ts +464 -0
  5. package/corpus/templates/content/actions/_database-row-mutation.ts +1490 -0
  6. package/corpus/templates/content/actions/_database-utils.ts +55 -3
  7. package/corpus/templates/content/actions/_property-utils.ts +141 -31
  8. package/corpus/templates/content/actions/add-database-item.ts +62 -240
  9. package/corpus/templates/content/actions/configure-document-property.ts +170 -1
  10. package/corpus/templates/content/actions/delete-document-property.ts +56 -17
  11. package/corpus/templates/content/actions/delete-document.ts +3 -0
  12. package/corpus/templates/content/actions/edit-document.ts +18 -1
  13. package/corpus/templates/content/actions/export-document.ts +41 -0
  14. package/corpus/templates/content/actions/remove-database-items.ts +10 -0
  15. package/corpus/templates/content/actions/set-document-property.ts +140 -3
  16. package/corpus/templates/content/actions/update-database-item.ts +62 -0
  17. package/corpus/templates/content/actions/update-document.ts +24 -0
  18. package/corpus/templates/content/actions/upsert-database-item-by-key.ts +43 -796
  19. package/corpus/templates/content/app/components/editor/DocumentBlockFields.tsx +99 -11
  20. package/corpus/templates/content/app/components/editor/blockFieldSaveController.ts +6 -0
  21. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +10 -8
  22. package/corpus/templates/content/app/hooks/use-content-database.ts +33 -32
  23. package/corpus/templates/content/app/hooks/use-document-properties.ts +4 -1
  24. package/corpus/templates/content/app/i18n-data.ts +20 -0
  25. package/corpus/templates/content/changelog/2026-08-10-database-blocks-fields-now-keep-logical-block-identity-throu.md +6 -0
  26. package/corpus/templates/content/changelog/2026-08-10-database-row-actions-now-validate-exact-schemas-and-safe-ret.md +6 -0
  27. package/corpus/templates/content/changelog/2026-08-13-blocks-field-conflicts-now-keep-the-newer-saved-version-visi.md +6 -0
  28. package/corpus/templates/content/docs/product/capabilities/content.object.block.md +5 -4
  29. package/corpus/templates/content/docs/product/capabilities/content.object.blocks-field.md +9 -4
  30. package/corpus/templates/content/docs/product/encyclopedia.md +4 -4
  31. package/corpus/templates/content/parity/matrix.md +1 -1
  32. package/corpus/templates/content/parity/matrix.ts +2 -0
  33. package/corpus/templates/content/server/db/schema.ts +100 -3
  34. package/corpus/templates/content/server/plugins/db.ts +85 -0
  35. package/corpus/templates/content/shared/api.ts +78 -2
  36. package/corpus/templates/content/shared/blocks-field-identity.ts +554 -0
  37. package/corpus/templates/content/shared/document-export.ts +33 -1
  38. package/dist/action.d.ts +22 -0
  39. package/dist/action.js +27 -0
  40. package/dist/collab/awareness.d.ts +2 -2
  41. package/dist/collab/routes.d.ts +1 -1
  42. package/dist/extensions/url-safety.d.ts +7 -2
  43. package/dist/extensions/url-safety.js +27 -12
  44. package/dist/index.browser.d.ts +1 -1
  45. package/dist/index.browser.js +1 -1
  46. package/dist/index.d.ts +1 -1
  47. package/dist/index.js +1 -1
  48. package/dist/mcp-client/oauth-client.d.ts +16 -1
  49. package/dist/mcp-client/oauth-client.js +177 -83
  50. package/dist/mcp-client/remote-store.js +46 -20
  51. package/dist/notifications/routes.d.ts +1 -1
  52. package/dist/oauth-tokens/index.d.ts +2 -1
  53. package/dist/oauth-tokens/index.js +2 -1
  54. package/dist/oauth-tokens/lifecycle.d.ts +87 -0
  55. package/dist/oauth-tokens/lifecycle.js +434 -0
  56. package/dist/oauth-tokens/migrations.js +10 -0
  57. package/dist/oauth-tokens/store.d.ts +22 -0
  58. package/dist/oauth-tokens/store.js +141 -18
  59. package/dist/observability/routes.d.ts +2 -2
  60. package/dist/progress/routes.d.ts +1 -1
  61. package/dist/secrets/routes.d.ts +6 -6
  62. package/dist/server/action-routes.js +10 -2
  63. package/dist/server/realtime-token.d.ts +1 -1
  64. package/package.json +2 -1
@@ -12,6 +12,10 @@ import {
12
12
  parsePropertyOptions,
13
13
  type DocumentPropertyType,
14
14
  } from "../shared/properties.js";
15
+ import {
16
+ lockPrimaryBlocksFields,
17
+ persistBlocksFieldIdentity,
18
+ } from "./_blocks-field-identity.js";
15
19
  import { lockContentDatabaseMutation } from "./_content-database-mutation-lock.js";
16
20
  import { resolveContentDocumentAccess } from "./_content-document-access.js";
17
21
  import { lockDatabaseMemberships } from "./_database-membership-lock.js";
@@ -34,8 +38,15 @@ export default defineAction({
34
38
  ),
35
39
  propertyId: z.string().describe("Property definition ID"),
36
40
  value: z.unknown().describe("Value for the property type"),
41
+ expectedBlocksFieldRevision: z.number().int().nonnegative().optional(),
37
42
  }),
38
- run: async ({ documentId, databaseId, propertyId, value }) => {
43
+ run: async ({
44
+ documentId,
45
+ databaseId,
46
+ propertyId,
47
+ value,
48
+ expectedBlocksFieldRevision,
49
+ }) => {
39
50
  const db = getDb();
40
51
  const [definition] = await db
41
52
  .select()
@@ -85,7 +96,10 @@ export default defineAction({
85
96
  parsePropertyOptions(definition.optionsJson),
86
97
  );
87
98
  await db.transaction(async (tx) => {
88
- await lockDatabaseMemberships(tx, [membership.id]);
99
+ const primaryBlocksFields = await lockPrimaryBlocksFields(
100
+ tx as unknown as ReturnType<typeof getDb>,
101
+ documentId,
102
+ );
89
103
  const [lockedDefinition] = await tx
90
104
  .select()
91
105
  .from(schema.documentPropertyDefinitions)
@@ -116,12 +130,31 @@ export default defineAction({
116
130
  target = blocksStorageTarget(
117
131
  parsePropertyOptions(lockedDefinition.optionsJson),
118
132
  );
133
+ let previousContent = "";
119
134
  if (target === "document_body") {
135
+ const [currentDocument] = await tx
136
+ .select({ content: schema.documents.content })
137
+ .from(schema.documents)
138
+ .where(eq(schema.documents.id, documentId));
139
+ if (!currentDocument) {
140
+ throw new Error(`Document "${documentId}" not found`);
141
+ }
142
+ previousContent = currentDocument.content;
120
143
  await tx
121
144
  .update(schema.documents)
122
145
  .set({ content, updatedAt: now })
123
146
  .where(eq(schema.documents.id, documentId));
124
147
  } else {
148
+ const [currentField] = await tx
149
+ .select({ content: schema.documentBlockFieldContents.content })
150
+ .from(schema.documentBlockFieldContents)
151
+ .where(
152
+ and(
153
+ eq(schema.documentBlockFieldContents.documentId, documentId),
154
+ eq(schema.documentBlockFieldContents.propertyId, propertyId),
155
+ ),
156
+ );
157
+ previousContent = currentField?.content ?? "";
125
158
  await tx
126
159
  .insert(schema.documentBlockFieldContents)
127
160
  .values({
@@ -141,6 +174,33 @@ export default defineAction({
141
174
  set: { content, updatedAt: now },
142
175
  });
143
176
  }
177
+ const identityFields =
178
+ target === "document_body"
179
+ ? primaryBlocksFields
180
+ : [{ propertyId, ownerEmail: database.ownerEmail }];
181
+ if (
182
+ target === "document_body" &&
183
+ !identityFields.some((field) => field.propertyId === propertyId)
184
+ ) {
185
+ throw new Error(
186
+ "Primary Blocks membership changed before the operation completed.",
187
+ );
188
+ }
189
+ for (const field of identityFields) {
190
+ await persistBlocksFieldIdentity({
191
+ db: tx as unknown as ReturnType<typeof getDb>,
192
+ ownerEmail: field.ownerEmail,
193
+ documentId,
194
+ propertyId: field.propertyId,
195
+ previousMarkdown: previousContent,
196
+ markdown: content,
197
+ expectedRevision:
198
+ field.propertyId === propertyId
199
+ ? expectedBlocksFieldRevision
200
+ : undefined,
201
+ now,
202
+ });
203
+ }
144
204
  });
145
205
  return {
146
206
  documentId,
@@ -166,7 +226,10 @@ export default defineAction({
166
226
  database.id,
167
227
  );
168
228
  const [lockedDatabase] = await tx
169
- .select({ id: schema.contentDatabases.id })
229
+ .select({
230
+ id: schema.contentDatabases.id,
231
+ naturalKeyPropertyId: schema.contentDatabases.naturalKeyPropertyId,
232
+ })
170
233
  .from(schema.contentDatabases)
171
234
  .where(
172
235
  and(
@@ -212,6 +275,41 @@ export default defineAction({
212
275
  if (isComputedPropertyType(lockedType)) {
213
276
  throw new Error("Computed properties cannot be edited.");
214
277
  }
278
+ const isNaturalKey = lockedDatabase.naturalKeyPropertyId === propertyId;
279
+ if (isNaturalKey) {
280
+ let parsed: unknown;
281
+ try {
282
+ parsed = JSON.parse(valueJson);
283
+ } catch {
284
+ parsed = null;
285
+ }
286
+ if (typeof parsed !== "string" || !parsed.trim()) {
287
+ throw new Error(
288
+ "A database natural key must remain a non-empty string.",
289
+ );
290
+ }
291
+ const [existingNaturalKeyClaim] = await tx
292
+ .select({
293
+ keyValueJson: schema.contentDatabaseItemKeyClaims.keyValueJson,
294
+ })
295
+ .from(schema.contentDatabaseItemKeyClaims)
296
+ .where(
297
+ and(
298
+ eq(schema.contentDatabaseItemKeyClaims.databaseId, database.id),
299
+ eq(schema.contentDatabaseItemKeyClaims.propertyId, propertyId),
300
+ eq(schema.contentDatabaseItemKeyClaims.documentId, documentId),
301
+ ),
302
+ )
303
+ .limit(1);
304
+ if (
305
+ existingNaturalKeyClaim &&
306
+ existingNaturalKeyClaim.keyValueJson !== valueJson
307
+ ) {
308
+ throw new Error(
309
+ "A claimed database natural key cannot be changed. Create a new row instead.",
310
+ );
311
+ }
312
+ }
215
313
  const [conflictingClaim] = await tx
216
314
  .select({ id: schema.contentDatabaseItemKeyClaims.id })
217
315
  .from(schema.contentDatabaseItemKeyClaims)
@@ -254,6 +352,45 @@ export default defineAction({
254
352
  updatedAt: now,
255
353
  });
256
354
  }
355
+ if (isNaturalKey) {
356
+ await tx
357
+ .insert(schema.contentDatabaseItemKeyClaims)
358
+ .values({
359
+ id: nanoid(),
360
+ ownerEmail: database.ownerEmail,
361
+ orgId: database.orgId,
362
+ databaseId: database.id,
363
+ propertyId,
364
+ keyValueJson: valueJson,
365
+ itemId: membership.id,
366
+ documentId,
367
+ createdAt: now,
368
+ updatedAt: now,
369
+ })
370
+ .onConflictDoNothing();
371
+ const [claim] = await tx
372
+ .select({
373
+ itemId: schema.contentDatabaseItemKeyClaims.itemId,
374
+ documentId: schema.contentDatabaseItemKeyClaims.documentId,
375
+ })
376
+ .from(schema.contentDatabaseItemKeyClaims)
377
+ .where(
378
+ and(
379
+ eq(schema.contentDatabaseItemKeyClaims.databaseId, database.id),
380
+ eq(schema.contentDatabaseItemKeyClaims.propertyId, propertyId),
381
+ eq(schema.contentDatabaseItemKeyClaims.keyValueJson, valueJson),
382
+ ),
383
+ );
384
+ if (
385
+ !claim ||
386
+ claim.itemId !== membership.id ||
387
+ claim.documentId !== documentId
388
+ ) {
389
+ throw new Error(
390
+ "This natural key is already claimed by another database row.",
391
+ );
392
+ }
393
+ }
257
394
  await tx
258
395
  .delete(schema.contentDatabaseItemKeyClaims)
259
396
  .where(
@@ -0,0 +1,62 @@
1
+ import { defineAction } from "@agent-native/core";
2
+ import { buildDeepLink } from "@agent-native/core/server";
3
+ import { z } from "zod";
4
+
5
+ import type { ContentDatabaseRowMutationResult } from "../shared/api.js";
6
+ import {
7
+ databaseMutationEnvelopeSchema,
8
+ updateDatabaseRow,
9
+ } from "./_database-row-mutation.js";
10
+
11
+ const schema = databaseMutationEnvelopeSchema.extend({
12
+ itemId: z.string().min(1).describe("Exact database membership row ID"),
13
+ documentId: z.string().min(1).describe("Exact row page ID"),
14
+ expectedRowRevision: z
15
+ .string()
16
+ .min(1)
17
+ .describe("Row revision returned by get-content-database"),
18
+ title: z.string().trim().min(1).max(500).optional(),
19
+ propertyValues: z
20
+ .record(z.string(), z.unknown())
21
+ .optional()
22
+ .describe(
23
+ "Sparse strict patch keyed by property definition ID; omitted fields are preserved and explicit null clears a value",
24
+ ),
25
+ });
26
+
27
+ export default defineAction({
28
+ description:
29
+ "Sparsely update one exact Content database row by stable item and document IDs. Requires schema and row revisions, validates every non-Blocks property, and returns a verified idempotent receipt.",
30
+ schema,
31
+ http: { method: "PUT" },
32
+ audit: {
33
+ recordInputs: false,
34
+ target: (args) => ({
35
+ type: "document",
36
+ id: args.documentId,
37
+ visibility: "private",
38
+ }),
39
+ summary: (_args, result) => {
40
+ const receipt = (result as ContentDatabaseRowMutationResult | null)
41
+ ?.receipt;
42
+ return receipt
43
+ ? `${receipt.outcome === "unchanged" ? "Checked" : "Updated"} Content database row ${receipt.row.itemId}`
44
+ : "Updated Content database row";
45
+ },
46
+ },
47
+ run: updateDatabaseRow,
48
+ link: ({ result }) => {
49
+ const documentId = (result as ContentDatabaseRowMutationResult | null)
50
+ ?.receipt.row.documentId;
51
+ if (!documentId) return null;
52
+ return {
53
+ url: buildDeepLink({
54
+ app: "content",
55
+ view: "editor",
56
+ params: { documentId },
57
+ }),
58
+ label: "Open database row",
59
+ view: "editor",
60
+ };
61
+ },
62
+ });
@@ -19,6 +19,10 @@ import {
19
19
  parseDocumentHideFromSearch,
20
20
  } from "../server/lib/documents.js";
21
21
  import type { DocumentUpdateResponse } from "../shared/api.js";
22
+ import {
23
+ lockPrimaryBlocksFields,
24
+ persistBlocksFieldIdentity,
25
+ } from "./_blocks-field-identity.js";
22
26
  import { BUILDER_CMS_BODY_CONTENT_KEY } from "./_builder-cms-source-adapter.js";
23
27
  import { reconcileInlineDatabasesForDocument } from "./_content-database-lifecycle.js";
24
28
  import { resolveContentDocumentAccess } from "./_content-document-access.js";
@@ -510,6 +514,12 @@ export default defineAction({
510
514
  if (iconChanged) updates.icon = args.icon;
511
515
  let contentCasConflict = false;
512
516
  await db.transaction(async (tx) => {
517
+ const primaryBlocksFields = contentChanged
518
+ ? await lockPrimaryBlocksFields(
519
+ tx as unknown as ReturnType<typeof getDb>,
520
+ id,
521
+ )
522
+ : [];
513
523
  if (useContentCas) {
514
524
  const applied = await tx
515
525
  .update(schema.documents)
@@ -532,6 +542,20 @@ export default defineAction({
532
542
  .where(eq(schema.documents.id, id));
533
543
  }
534
544
 
545
+ if (contentChanged && content !== undefined) {
546
+ for (const field of primaryBlocksFields) {
547
+ await persistBlocksFieldIdentity({
548
+ db: tx as unknown as ReturnType<typeof getDb>,
549
+ ownerEmail: field.ownerEmail,
550
+ documentId: id,
551
+ propertyId: field.propertyId,
552
+ previousMarkdown: existing.content,
553
+ markdown: content,
554
+ now: updates.updatedAt as string,
555
+ });
556
+ }
557
+ }
558
+
535
559
  if (titleChanged || contentChanged) {
536
560
  const [latestVersion] = await tx
537
561
  .select({ createdAt: schema.documentVersions.createdAt })