@agent-native/core 0.135.0 → 0.135.2

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 (50) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +12 -0
  3. package/corpus/core/docs/content/getting-started-actions.mdx +235 -0
  4. package/corpus/core/docs/content/getting-started-database.mdx +253 -0
  5. package/corpus/core/docs/content/getting-started-pages.mdx +190 -0
  6. package/corpus/core/docs/content/getting-started.mdx +57 -613
  7. package/corpus/core/package.json +1 -1
  8. package/corpus/core/src/sharing/access.ts +44 -2
  9. package/corpus/templates/clips/actions/add-comment.ts +6 -2
  10. package/corpus/templates/clips/app/components/player/comments-panel.tsx +9 -2
  11. package/corpus/templates/clips/app/components/player/playback-comment-overlay.tsx +44 -27
  12. package/corpus/templates/clips/app/components/player/scrubber.tsx +10 -2
  13. package/corpus/templates/clips/app/components/player/video-player.tsx +4 -0
  14. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +22 -15
  15. package/corpus/templates/clips/app/routes/share.$shareId.tsx +1 -0
  16. package/corpus/templates/content/actions/_database-utils.ts +113 -4
  17. package/corpus/templates/content/actions/get-document.ts +84 -3
  18. package/corpus/templates/content/app/components/editor/BuilderBodySyncingNotice.tsx +9 -2
  19. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +29 -13
  20. package/corpus/templates/content/app/components/editor/DocumentToolbar.tsx +5 -12
  21. package/corpus/templates/content/app/components/editor/body-hydration.ts +12 -6
  22. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +2 -3
  23. package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +5 -12
  24. package/corpus/templates/content/app/hooks/use-content-database.ts +16 -27
  25. package/corpus/templates/content/app/hooks/use-create-page.ts +3 -6
  26. package/corpus/templates/content/app/hooks/use-document-properties.ts +9 -16
  27. package/corpus/templates/content/app/hooks/use-document-versions.ts +3 -3
  28. package/corpus/templates/content/app/hooks/use-documents.ts +85 -29
  29. package/corpus/templates/content/app/hooks/use-notion.ts +15 -13
  30. package/corpus/templates/content/app/i18n-data.ts +32 -1
  31. package/corpus/templates/content/app/lib/document-query.ts +36 -0
  32. package/corpus/templates/content/changelog/2026-08-02-pages-opened-from-a-database-now-keep-that-database-s-fields.md +6 -0
  33. package/corpus/templates/content/server/lib/document-context.ts +11 -6
  34. package/corpus/templates/content/shared/api.ts +8 -0
  35. package/dist/collab/struct-routes.d.ts +1 -1
  36. package/dist/notifications/routes.d.ts +3 -3
  37. package/dist/observability/routes.d.ts +1 -1
  38. package/dist/progress/routes.d.ts +1 -1
  39. package/dist/provider-api/actions/custom-provider-registration.d.ts +6 -6
  40. package/dist/provider-api/actions/provider-api.d.ts +4 -4
  41. package/dist/secrets/routes.d.ts +9 -9
  42. package/dist/sharing/access.d.ts.map +1 -1
  43. package/dist/sharing/access.js +32 -2
  44. package/dist/sharing/access.js.map +1 -1
  45. package/docs/content/getting-started-actions.mdx +235 -0
  46. package/docs/content/getting-started-database.mdx +253 -0
  47. package/docs/content/getting-started-pages.mdx +190 -0
  48. package/docs/content/getting-started.mdx +57 -613
  49. package/package.json +1 -1
  50. package/src/sharing/access.ts +44 -2
@@ -1 +1 @@
1
- {"version":3,"file":"access.js","sourceRoot":"","sources":["../../src/sharing/access.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Access-control helpers for shareable resources.\n *\n * The access model combines:\n * 1. Direct ownership — `owner_email = currentUser`.\n * 2. Visibility — `'private' | 'org' | 'public'`. `org` grants read to anyone\n * in the same org; `public` grants read to any authenticated user.\n * 3. Share rows — per-user or per-org grants in the `{type}_shares` table\n * with a role (`viewer | editor | admin`).\n *\n * Use `applyAccessFilter()` on list/read queries to filter rows the current\n * user can see. Use `assertAccess()` at the top of write actions to reject\n * callers who lack the required role.\n */\n\nimport { and, eq, or, sql, type SQL } from \"drizzle-orm\";\n\nimport {\n getRequestAuthCapability,\n getRequestUserEmail,\n getRequestOrgId,\n} from \"../server/request-context.js\";\nimport {\n listShareableResources,\n requireShareableResource,\n type ShareableResourceRegistration,\n} from \"./registry.js\";\nimport { ROLE_RANK, type ShareRole, type Visibility } from \"./schema.js\";\n\n/**\n * Find a registration by its drizzle table identity. Used to look up\n * per-resource policy flags (e.g. `allowPublic`) inside `accessFilter`,\n * which receives only the table — not the resource-type name.\n *\n * Identity is stable within a single bundle (Vite dedupes module instances);\n * the SSR/server side is the only caller, so per-bundle identity is fine.\n */\nfunction findRegistrationByTable(\n resourceTable: any,\n): ShareableResourceRegistration | undefined {\n for (const reg of listShareableResources()) {\n if (reg.resourceTable === resourceTable) return reg;\n }\n return undefined;\n}\n\nexport class ForbiddenError extends Error {\n statusCode = 403;\n constructor(message = \"Forbidden\") {\n super(message);\n this.name = \"ForbiddenError\";\n }\n}\n\nexport interface AccessContext {\n userEmail?: string;\n orgId?: string;\n authCapability?: string;\n}\n\n/** Current request's access context. Pulls from request-context ALS. */\nexport function currentAccess(): AccessContext {\n return {\n userEmail: getRequestUserEmail(),\n orgId: getRequestOrgId(),\n authCapability: getRequestAuthCapability(),\n };\n}\n\nexport function resolveRegisteredAccessContext(\n reg: ShareableResourceRegistration | undefined,\n ctx: AccessContext,\n): AccessContext {\n if (!reg?.resolveAccessContext) return ctx;\n const resolved = reg.resolveAccessContext(ctx);\n return ctx.authCapability\n ? { ...resolved, authCapability: ctx.authCapability }\n : resolved;\n}\n\nfunction normalizeEmailForAccess(email: string | undefined): string | null {\n const normalized = email?.trim().toLowerCase();\n return normalized || null;\n}\n\nfunction emailColumnMatches(column: any, email: string): SQL {\n return sql`lower(${column}) = ${email}`;\n}\n\n/**\n * Build a Drizzle `WHERE` clause that admits rows the current user can see.\n * Pass the ownable resource table and its shares table; optional min role\n * (defaults to 'viewer') gates which share rows count.\n *\n * `visibility = 'public'` is intentionally NOT admitted by default. Public\n * means \"anyone with the link can view\" (still honoured by `resolveAccess`\n * for read-by-id), not \"appears in every signed-in user's list/sidebar.\"\n * Pass `{ includePublic: true }` for the rare list endpoint that wants\n * cross-user public discovery (a public template gallery, for example).\n *\n * Example:\n *\n * const rows = await db\n * .select()\n * .from(schema.documents)\n * .where(accessFilter(schema.documents, schema.documentShares));\n */\nexport function accessFilter(\n resourceTable: any,\n sharesTable: any,\n rawCtx: AccessContext = currentAccess(),\n minRole: ShareRole = \"viewer\",\n options: { includePublic?: boolean } = {},\n): SQL {\n // Defense in depth — resources registered with `allowPublic: false` must\n // never participate in cross-user \"public\" discovery, even if a caller\n // accidentally passes `includePublic: true` or if a stale public row sits\n // in the DB.\n const reg = findRegistrationByTable(resourceTable);\n const ctx = resolveRegisteredAccessContext(reg, rawCtx);\n const { userEmail, orgId } = ctx;\n const normalizedUserEmail = normalizeEmailForAccess(userEmail);\n const publicAllowed = reg?.allowPublic !== false;\n const includePublic = (options.includePublic ?? false) && publicAllowed;\n const clauses: SQL[] = [];\n\n if (normalizedUserEmail) {\n clauses.push(\n and(\n emailColumnMatches(resourceTable.ownerEmail, normalizedUserEmail),\n ownerScopeFilter(reg, resourceTable, ctx),\n )!,\n );\n }\n if (minRole === \"viewer\") {\n if (includePublic) {\n clauses.push(eq(resourceTable.visibility, \"public\"));\n }\n if (orgId) {\n clauses.push(\n and(\n eq(resourceTable.visibility, \"org\"),\n eq(resourceTable.orgId, orgId),\n )!,\n );\n }\n }\n if (normalizedUserEmail) {\n const shareScope = restrictedShareScopeSql(reg, resourceTable, ctx);\n clauses.push(\n sql`exists (select 1 from ${sharesTable}\n where ${sharesTable.resourceId} = ${resourceTable.id}\n and ${sharesTable.principalType} = 'user'\n and lower(${sharesTable.principalId}) = ${normalizedUserEmail}\n and ${shareScope}\n and ${minRoleSql(minRole)})`,\n );\n }\n if (orgId) {\n const shareScope = restrictedShareScopeSql(reg, resourceTable, ctx);\n clauses.push(\n sql`exists (select 1 from ${sharesTable}\n where ${sharesTable.resourceId} = ${resourceTable.id}\n and ${sharesTable.principalType} = 'org'\n and ${sharesTable.principalId} = ${orgId}\n and ${shareScope}\n and ${minRoleSql(minRole)})`,\n );\n }\n\n return or(...clauses) ?? sql`1=0`;\n}\n\nfunction ownerScopeFilter(\n reg: ShareableResourceRegistration | undefined,\n resourceTable: any,\n ctx: AccessContext,\n): SQL {\n if (reg?.ownerAccessIgnoresOrg === true) return sql`1=1`;\n if (ctx.orgId) {\n // Rows created before org-scoping, or in solo mode, have no org_id. Keep\n // them manageable by their owner after the owner joins or switches into an\n // organization, while still keeping rows from other orgs out of scope.\n return or(\n eq(resourceTable.orgId, ctx.orgId),\n sql`${resourceTable.orgId} IS NULL`,\n )!;\n }\n return sql`${resourceTable.orgId} IS NULL`;\n}\n\nfunction ownerMatchesActiveScope(\n reg: ShareableResourceRegistration | undefined,\n resource: any,\n ctx: AccessContext,\n): boolean {\n if (reg?.ownerAccessIgnoresOrg === true) return true;\n const resourceOrgId = resource?.orgId ?? null;\n if (!resourceOrgId) return true;\n return ctx.orgId === resourceOrgId;\n}\n\nfunction minRoleSql(minRole: ShareRole): SQL {\n if (minRole === \"viewer\") {\n // any role satisfies viewer\n return sql`1=1`;\n }\n if (minRole === \"editor\") {\n return sql`role in ('editor','admin')`;\n }\n return sql`role = 'admin'`;\n}\n\nfunction restrictedShareScopeSql(\n reg: ShareableResourceRegistration | undefined,\n resourceTable: any,\n ctx: AccessContext,\n): SQL {\n // Restricted resources (extensions) must stay inside their resource org even\n // if stale cross-org share rows already exist from older code or bad data.\n if (reg?.requireOrgMemberForUserShares !== true) return sql`1=1`;\n if (!ctx.orgId) return sql`1=0`;\n return eq(resourceTable.orgId, ctx.orgId);\n}\n\nfunction explicitSharesAllowedForResource(\n reg: ShareableResourceRegistration,\n resource: any,\n ctx: AccessContext,\n): boolean {\n if (reg.requireOrgMemberForUserShares !== true) return true;\n const resourceOrgId = resource?.orgId ?? null;\n return !!resourceOrgId && !!ctx.orgId && resourceOrgId === ctx.orgId;\n}\n\nexport interface ResolvedAccess {\n /** Effective role: 'owner' for the resource owner, or the share role. */\n role: \"owner\" | ShareRole;\n /** The resource row (already loaded). */\n resource: any;\n}\n\n/**\n * Minimal resource shape returned when a caller opts into a projected access\n * load via `{ skipResourceBody: true }`. Contains exactly the columns the\n * access-decision logic itself reads — identity, ownership, org scope, and\n * visibility — never a resource type's heavy body columns (`data`,\n * `content`, and similar blobs).\n */\nexport interface AccessProjectedResource {\n id: string;\n ownerEmail: string;\n orgId: string | null;\n visibility: Visibility;\n}\n\nexport interface ResolvedAccessProjected {\n /** Effective role: 'owner' for the resource owner, or the share role. */\n role: \"owner\" | ShareRole;\n /** Only the access-decision columns — not the full resource row. */\n resource: AccessProjectedResource;\n}\n\nexport interface ResolveAccessOptions {\n /**\n * When true, load only the columns the access decision itself needs\n * (`id`, `ownerEmail`, `orgId`, `visibility`) instead of the full resource\n * row. Use this when the caller only needs the access decision — not the\n * resource body — and wants to skip fetching heavy type-specific columns\n * (e.g. `data`/`content` blobs) on every `assertAccess`/`resolveAccess`\n * call.\n *\n * Silently ignored (falls back to a full row load) for any resource type\n * registered with a `publicAccessRole` *function* resolver, since that\n * callback can read arbitrary resource fields the projection would have\n * omitted — see `hasDynamicPublicAccessRoleResolver` below.\n *\n * Default: `false` — the full row is loaded, matching historical behavior.\n * Callers that need resource body fields (most call sites today — for\n * many resource types `resolveAccess().resource` doubles as the action's\n * primary data read) must NOT pass this.\n */\n skipResourceBody?: boolean;\n}\n\nasync function publicAccessRoleForResource(\n reg: ShareableResourceRegistration,\n resource: any,\n ctx: AccessContext,\n): Promise<ShareRole> {\n const roleResolver = reg.publicAccessRole;\n if (!roleResolver) return \"viewer\";\n return typeof roleResolver === \"function\"\n ? await roleResolver(resource, ctx)\n : roleResolver;\n}\n\nfunction higherShareRole(a: ShareRole, b: ShareRole | null): ShareRole {\n if (!b) return a;\n return ROLE_RANK[b] > ROLE_RANK[a] ? b : a;\n}\n\nfunction columnName(column: unknown): string | null {\n const candidate = column as\n | {\n name?: unknown;\n config?: { name?: unknown };\n _: { name?: unknown };\n }\n | undefined;\n const name = candidate?.name ?? candidate?.config?.name ?? candidate?._?.name;\n return typeof name === \"string\" && name ? name : null;\n}\n\nfunction missingColumnName(err: unknown): string | null {\n const error = err as { code?: string; message?: string } | undefined;\n const message = error?.message ?? \"\";\n if (\n error?.code !== \"42703\" &&\n !/no such column|does not exist/i.test(message)\n ) {\n return null;\n }\n const quoted = message.match(/column\\s+\"([^\"]+)\"\\s+does not exist/i)?.[1];\n if (quoted) return quoted;\n const sqlite = message.match(/no such column:\\s+[\"`]?([\\w.]+)[\"`]?/i)?.[1];\n if (sqlite) return sqlite.split(\".\").pop() ?? sqlite;\n return null;\n}\n\nfunction selectExistingColumns(\n columns: Record<string, unknown>,\n omittedColumnNames: Set<string>,\n): Record<string, unknown> {\n const selection: Record<string, unknown> = {};\n for (const [key, column] of Object.entries(columns)) {\n const name = columnName(column);\n if (!name || omittedColumnNames.has(name)) continue;\n selection[key] = column;\n }\n return selection;\n}\n\n/**\n * The fixed column set the access-decision logic in `resolveAccess` itself\n * reads: identity (`id`), ownership (`ownerEmail`, `orgId`), and the coarse\n * `visibility` flag. This is intentionally NOT caller-configurable — an\n * arbitrary caller-supplied column list could omit a column the access\n * checks below depend on and silently break authorization. Every ownable\n * resource table has these columns (`ownableColumns()` + a primary key\n * named `id`), so this projection is safe for any registration that\n * doesn't also read the row through a dynamic resolver (see below).\n */\nfunction projectedAccessColumns(resourceTable: any): Record<string, unknown> {\n return {\n id: resourceTable.id,\n ownerEmail: resourceTable.ownerEmail,\n orgId: resourceTable.orgId,\n visibility: resourceTable.visibility,\n };\n}\n\n/**\n * True when this registration's `publicAccessRole` is a callback rather than\n * a fixed role string. Callbacks receive the loaded `resource` and may read\n * arbitrary fields on it (see e.g. `templates/design`'s\n * `publicDesignAccessRole`, which inspects design-specific data). The\n * projected access load must never be used for these registrations — always\n * fall back to a full row load so the resolver sees the complete resource.\n */\nfunction hasDynamicPublicAccessRoleResolver(\n reg: ShareableResourceRegistration,\n): boolean {\n return typeof reg.publicAccessRole === \"function\";\n}\n\nasync function loadResourceForAccess(\n reg: ShareableResourceRegistration,\n resourceId: string,\n options: ResolveAccessOptions = {},\n): Promise<any | null> {\n const db = reg.getDb() as any;\n const useProjection =\n options.skipResourceBody === true &&\n !hasDynamicPublicAccessRoleResolver(reg);\n const projectedColumns = useProjection\n ? projectedAccessColumns(reg.resourceTable)\n : null;\n const omittedColumnNames = new Set<string>();\n\n for (let attempt = 0; attempt < 12; attempt++) {\n try {\n const query =\n !projectedColumns && omittedColumnNames.size === 0\n ? db.select()\n : db.select(\n selectExistingColumns(\n projectedColumns ?? reg.resourceTable,\n omittedColumnNames,\n ),\n );\n const [resource] = await query\n .from(reg.resourceTable)\n .where(eq(reg.resourceTable.id, resourceId));\n return resource ?? null;\n } catch (err) {\n const missing = missingColumnName(err);\n if (!missing || omittedColumnNames.has(missing)) throw err;\n omittedColumnNames.add(missing);\n console.warn(\n `[sharing] ${reg.type} access lookup omitted missing column ${missing}`,\n );\n }\n }\n\n throw new Error(\n `Could not load ${reg.type} ${resourceId}: too many missing resource columns`,\n );\n}\n\n/**\n * Return the effective role the current user has on a specific resource, or\n * null if they have no access. Loads the resource and relevant share rows.\n *\n * By default the full resource row is loaded (unchanged historical\n * behavior — for most resource types `.resource` here doubles as the\n * action's primary data read). Pass `{ skipResourceBody: true }` when the\n * caller only needs the access decision to load just the access-decision\n * columns instead — see `ResolveAccessOptions`.\n */\nexport async function resolveAccess(\n resourceType: string,\n resourceId: string,\n rawCtx?: AccessContext,\n options?: { skipResourceBody?: false },\n): Promise<ResolvedAccess | null>;\nexport async function resolveAccess(\n resourceType: string,\n resourceId: string,\n rawCtx: AccessContext | undefined,\n options: { skipResourceBody: true },\n): Promise<ResolvedAccessProjected | null>;\nexport async function resolveAccess(\n resourceType: string,\n resourceId: string,\n rawCtx: AccessContext = currentAccess(),\n options: ResolveAccessOptions = {},\n): Promise<ResolvedAccess | ResolvedAccessProjected | null> {\n return resolveAccessImpl(resourceType, resourceId, rawCtx, options);\n}\n\n/**\n * Un-overloaded implementation shared by the `resolveAccess` overloads and\n * called directly by `assertAccess` below. Kept separate from the exported\n * `resolveAccess` so internal callers passing a widened\n * `ResolveAccessOptions` (rather than a `{ skipResourceBody: true }` /\n * `{ skipResourceBody?: false }` literal) don't have to satisfy TypeScript's\n * overload resolution, which only matches against the declared overload\n * signatures, not the implementation signature.\n */\nasync function resolveAccessImpl(\n resourceType: string,\n resourceId: string,\n rawCtx: AccessContext = currentAccess(),\n options: ResolveAccessOptions = {},\n): Promise<ResolvedAccess | ResolvedAccessProjected | null> {\n const reg = requireShareableResource(resourceType);\n const ctx = resolveRegisteredAccessContext(reg, rawCtx);\n\n const resource = await loadResourceForAccess(reg, resourceId, options);\n if (!resource) return null;\n\n const { userEmail, orgId } = ctx;\n const normalizedUserEmail = normalizeEmailForAccess(userEmail);\n\n if (\n normalizedUserEmail &&\n normalizeEmailForAccess(resource.ownerEmail) === normalizedUserEmail &&\n ownerMatchesActiveScope(reg, resource, ctx)\n ) {\n return { role: \"owner\", resource };\n }\n if (resource.visibility === \"public\" && reg.allowPublic !== false) {\n // No share row needed; default viewer unless the resource registration\n // deliberately grants a stronger public-by-link role or explicit shares\n // upgrade the current principal.\n const publicRole = await publicAccessRoleForResource(reg, resource, ctx);\n const role = await highestShareRole(reg, resourceId, ctx, resource);\n return { role: higherShareRole(publicRole, role), resource };\n }\n // `visibility === \"public\"` on an `allowPublic: false` resource is treated\n // as private: only owner + explicit shares grant access. Falls through to\n // the explicit-share lookup below.\n if (resource.visibility === \"org\" && orgId && resource.orgId === orgId) {\n const role = await highestShareRole(reg, resourceId, ctx, resource);\n return { role: role ?? \"viewer\", resource };\n }\n const role = await highestShareRole(reg, resourceId, ctx, resource);\n if (role) return { role, resource };\n return null;\n}\n\nasync function highestShareRole(\n reg: ShareableResourceRegistration,\n resourceId: string,\n ctx: AccessContext,\n resource: any,\n): Promise<ShareRole | null> {\n const { userEmail, orgId } = ctx;\n const normalizedUserEmail = normalizeEmailForAccess(userEmail);\n if (!normalizedUserEmail && !orgId) return null;\n if (!explicitSharesAllowedForResource(reg, resource, ctx)) return null;\n const db = reg.getDb() as any;\n\n const principalClauses: ReturnType<typeof and>[] = [];\n if (normalizedUserEmail) {\n principalClauses.push(\n and(\n eq(reg.sharesTable.principalType, \"user\"),\n emailColumnMatches(reg.sharesTable.principalId, normalizedUserEmail),\n ),\n );\n }\n if (orgId) {\n principalClauses.push(\n and(\n eq(reg.sharesTable.principalType, \"org\"),\n eq(reg.sharesTable.principalId, orgId),\n ),\n );\n }\n\n const rows = await db\n .select({ role: reg.sharesTable.role })\n .from(reg.sharesTable)\n .where(\n and(eq(reg.sharesTable.resourceId, resourceId), or(...principalClauses)),\n )\n .limit(10);\n\n let best: ShareRole | null = null;\n for (const r of rows as Array<{ role: ShareRole }>) {\n if (!best || ROLE_RANK[r.role] > ROLE_RANK[best]) best = r.role;\n }\n return best;\n}\n\n/**\n * Throw ForbiddenError if the current user can't act on this resource with at\n * least the given role. Used at the top of update/delete actions.\n *\n * By default the full resource row is loaded (unchanged historical\n * behavior). Pass `{ skipResourceBody: true }` as the fifth argument when\n * the caller only needs the access decision — see `ResolveAccessOptions`.\n */\nexport async function assertAccess(\n resourceType: string,\n resourceId: string,\n minRole?: ShareRole | \"owner\",\n ctx?: AccessContext,\n options?: { skipResourceBody?: false },\n): Promise<ResolvedAccess>;\nexport async function assertAccess(\n resourceType: string,\n resourceId: string,\n minRole: ShareRole | \"owner\" | undefined,\n ctx: AccessContext | undefined,\n options: { skipResourceBody: true },\n): Promise<ResolvedAccessProjected>;\nexport async function assertAccess(\n resourceType: string,\n resourceId: string,\n minRole: ShareRole | \"owner\" = \"viewer\",\n ctx: AccessContext = currentAccess(),\n options: ResolveAccessOptions = {},\n): Promise<ResolvedAccess | ResolvedAccessProjected> {\n const access = await resolveAccessImpl(\n resourceType,\n resourceId,\n ctx,\n options,\n );\n if (!access) {\n throw new ForbiddenError(`No access to ${resourceType} ${resourceId}`);\n }\n if (ROLE_RANK[access.role] < ROLE_RANK[minRole]) {\n throw new ForbiddenError(\n `Requires ${minRole} role on ${resourceType} ${resourceId} (have ${access.role})`,\n );\n }\n return access;\n}\n"]}
1
+ {"version":3,"file":"access.js","sourceRoot":"","sources":["../../src/sharing/access.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Access-control helpers for shareable resources.\n *\n * The access model combines:\n * 1. Direct ownership — `owner_email = currentUser`.\n * 2. Visibility — `'private' | 'org' | 'public'`. `org` grants read to anyone\n * in the same org; `public` grants read to any authenticated user.\n * 3. Share rows — per-user or per-org grants in the `{type}_shares` table\n * with a role (`viewer | editor | admin`).\n *\n * Use `applyAccessFilter()` on list/read queries to filter rows the current\n * user can see. Use `assertAccess()` at the top of write actions to reject\n * callers who lack the required role.\n */\n\nimport { and, eq, or, sql, type SQL } from \"drizzle-orm\";\n\nimport { orgMembers } from \"../org/schema.js\";\nimport {\n getRequestAuthCapability,\n getRequestUserEmail,\n getRequestOrgId,\n} from \"../server/request-context.js\";\nimport {\n listShareableResources,\n requireShareableResource,\n type ShareableResourceRegistration,\n} from \"./registry.js\";\nimport { ROLE_RANK, type ShareRole, type Visibility } from \"./schema.js\";\n\n/**\n * Find a registration by its drizzle table identity. Used to look up\n * per-resource policy flags (e.g. `allowPublic`) inside `accessFilter`,\n * which receives only the table — not the resource-type name.\n *\n * Identity is stable within a single bundle (Vite dedupes module instances);\n * the SSR/server side is the only caller, so per-bundle identity is fine.\n */\nfunction findRegistrationByTable(\n resourceTable: any,\n): ShareableResourceRegistration | undefined {\n for (const reg of listShareableResources()) {\n if (reg.resourceTable === resourceTable) return reg;\n }\n return undefined;\n}\n\nexport class ForbiddenError extends Error {\n statusCode = 403;\n constructor(message = \"Forbidden\") {\n super(message);\n this.name = \"ForbiddenError\";\n }\n}\n\nexport interface AccessContext {\n userEmail?: string;\n orgId?: string;\n authCapability?: string;\n}\n\n/** Current request's access context. Pulls from request-context ALS. */\nexport function currentAccess(): AccessContext {\n return {\n userEmail: getRequestUserEmail(),\n orgId: getRequestOrgId(),\n authCapability: getRequestAuthCapability(),\n };\n}\n\nexport function resolveRegisteredAccessContext(\n reg: ShareableResourceRegistration | undefined,\n ctx: AccessContext,\n): AccessContext {\n if (!reg?.resolveAccessContext) return ctx;\n const resolved = reg.resolveAccessContext(ctx);\n return ctx.authCapability\n ? { ...resolved, authCapability: ctx.authCapability }\n : resolved;\n}\n\nfunction normalizeEmailForAccess(email: string | undefined): string | null {\n const normalized = email?.trim().toLowerCase();\n return normalized || null;\n}\n\nfunction emailColumnMatches(column: any, email: string): SQL {\n return sql`lower(${column}) = ${email}`;\n}\n\n/**\n * Real `org_members` membership, independent of the caller's currently\n * active org (`ctx.orgId`). `org`-visibility access must key off actual\n * membership — a user's active-org selection is a UI convenience, not a\n * statement of which orgs they belong to.\n *\n * Queries through the resource's own `reg.getDb()` — the same connection\n * every other lookup in this file uses — not the ambient `getDbExec()`,\n * since `org_members` lives in that same app database.\n */\nasync function isOrgMember(\n reg: ShareableResourceRegistration,\n memberOrgId: string,\n email: string,\n): Promise<boolean> {\n const db = reg.getDb() as any;\n const rows = await db\n .select({ id: orgMembers.id })\n .from(orgMembers)\n .where(\n and(\n eq(orgMembers.orgId, memberOrgId),\n emailColumnMatches(orgMembers.email, email),\n ),\n )\n .limit(1);\n return rows.length > 0;\n}\n\n/**\n * Build a Drizzle `WHERE` clause that admits rows the current user can see.\n * Pass the ownable resource table and its shares table; optional min role\n * (defaults to 'viewer') gates which share rows count.\n *\n * `visibility = 'public'` is intentionally NOT admitted by default. Public\n * means \"anyone with the link can view\" (still honoured by `resolveAccess`\n * for read-by-id), not \"appears in every signed-in user's list/sidebar.\"\n * Pass `{ includePublic: true }` for the rare list endpoint that wants\n * cross-user public discovery (a public template gallery, for example).\n *\n * Example:\n *\n * const rows = await db\n * .select()\n * .from(schema.documents)\n * .where(accessFilter(schema.documents, schema.documentShares));\n */\nexport function accessFilter(\n resourceTable: any,\n sharesTable: any,\n rawCtx: AccessContext = currentAccess(),\n minRole: ShareRole = \"viewer\",\n options: { includePublic?: boolean } = {},\n): SQL {\n // Defense in depth — resources registered with `allowPublic: false` must\n // never participate in cross-user \"public\" discovery, even if a caller\n // accidentally passes `includePublic: true` or if a stale public row sits\n // in the DB.\n const reg = findRegistrationByTable(resourceTable);\n const ctx = resolveRegisteredAccessContext(reg, rawCtx);\n const { userEmail, orgId } = ctx;\n const normalizedUserEmail = normalizeEmailForAccess(userEmail);\n const publicAllowed = reg?.allowPublic !== false;\n const includePublic = (options.includePublic ?? false) && publicAllowed;\n const clauses: SQL[] = [];\n\n if (normalizedUserEmail) {\n clauses.push(\n and(\n emailColumnMatches(resourceTable.ownerEmail, normalizedUserEmail),\n ownerScopeFilter(reg, resourceTable, ctx),\n )!,\n );\n }\n if (minRole === \"viewer\") {\n if (includePublic) {\n clauses.push(eq(resourceTable.visibility, \"public\"));\n }\n if (orgId) {\n clauses.push(\n and(\n eq(resourceTable.visibility, \"org\"),\n eq(resourceTable.orgId, orgId),\n )!,\n );\n }\n }\n if (normalizedUserEmail) {\n const shareScope = restrictedShareScopeSql(reg, resourceTable, ctx);\n clauses.push(\n sql`exists (select 1 from ${sharesTable}\n where ${sharesTable.resourceId} = ${resourceTable.id}\n and ${sharesTable.principalType} = 'user'\n and lower(${sharesTable.principalId}) = ${normalizedUserEmail}\n and ${shareScope}\n and ${minRoleSql(minRole)})`,\n );\n }\n if (orgId) {\n const shareScope = restrictedShareScopeSql(reg, resourceTable, ctx);\n clauses.push(\n sql`exists (select 1 from ${sharesTable}\n where ${sharesTable.resourceId} = ${resourceTable.id}\n and ${sharesTable.principalType} = 'org'\n and ${sharesTable.principalId} = ${orgId}\n and ${shareScope}\n and ${minRoleSql(minRole)})`,\n );\n }\n\n return or(...clauses) ?? sql`1=0`;\n}\n\nfunction ownerScopeFilter(\n reg: ShareableResourceRegistration | undefined,\n resourceTable: any,\n ctx: AccessContext,\n): SQL {\n if (reg?.ownerAccessIgnoresOrg === true) return sql`1=1`;\n if (ctx.orgId) {\n // Rows created before org-scoping, or in solo mode, have no org_id. Keep\n // them manageable by their owner after the owner joins or switches into an\n // organization, while still keeping rows from other orgs out of scope.\n return or(\n eq(resourceTable.orgId, ctx.orgId),\n sql`${resourceTable.orgId} IS NULL`,\n )!;\n }\n return sql`${resourceTable.orgId} IS NULL`;\n}\n\nfunction ownerMatchesActiveScope(\n reg: ShareableResourceRegistration | undefined,\n resource: any,\n ctx: AccessContext,\n): boolean {\n if (reg?.ownerAccessIgnoresOrg === true) return true;\n const resourceOrgId = resource?.orgId ?? null;\n if (!resourceOrgId) return true;\n return ctx.orgId === resourceOrgId;\n}\n\nfunction minRoleSql(minRole: ShareRole): SQL {\n if (minRole === \"viewer\") {\n // any role satisfies viewer\n return sql`1=1`;\n }\n if (minRole === \"editor\") {\n return sql`role in ('editor','admin')`;\n }\n return sql`role = 'admin'`;\n}\n\nfunction restrictedShareScopeSql(\n reg: ShareableResourceRegistration | undefined,\n resourceTable: any,\n ctx: AccessContext,\n): SQL {\n // Restricted resources (extensions) must stay inside their resource org even\n // if stale cross-org share rows already exist from older code or bad data.\n if (reg?.requireOrgMemberForUserShares !== true) return sql`1=1`;\n if (!ctx.orgId) return sql`1=0`;\n return eq(resourceTable.orgId, ctx.orgId);\n}\n\nfunction explicitSharesAllowedForResource(\n reg: ShareableResourceRegistration,\n resource: any,\n ctx: AccessContext,\n): boolean {\n if (reg.requireOrgMemberForUserShares !== true) return true;\n const resourceOrgId = resource?.orgId ?? null;\n return !!resourceOrgId && !!ctx.orgId && resourceOrgId === ctx.orgId;\n}\n\nexport interface ResolvedAccess {\n /** Effective role: 'owner' for the resource owner, or the share role. */\n role: \"owner\" | ShareRole;\n /** The resource row (already loaded). */\n resource: any;\n}\n\n/**\n * Minimal resource shape returned when a caller opts into a projected access\n * load via `{ skipResourceBody: true }`. Contains exactly the columns the\n * access-decision logic itself reads — identity, ownership, org scope, and\n * visibility — never a resource type's heavy body columns (`data`,\n * `content`, and similar blobs).\n */\nexport interface AccessProjectedResource {\n id: string;\n ownerEmail: string;\n orgId: string | null;\n visibility: Visibility;\n}\n\nexport interface ResolvedAccessProjected {\n /** Effective role: 'owner' for the resource owner, or the share role. */\n role: \"owner\" | ShareRole;\n /** Only the access-decision columns — not the full resource row. */\n resource: AccessProjectedResource;\n}\n\nexport interface ResolveAccessOptions {\n /**\n * When true, load only the columns the access decision itself needs\n * (`id`, `ownerEmail`, `orgId`, `visibility`) instead of the full resource\n * row. Use this when the caller only needs the access decision — not the\n * resource body — and wants to skip fetching heavy type-specific columns\n * (e.g. `data`/`content` blobs) on every `assertAccess`/`resolveAccess`\n * call.\n *\n * Silently ignored (falls back to a full row load) for any resource type\n * registered with a `publicAccessRole` *function* resolver, since that\n * callback can read arbitrary resource fields the projection would have\n * omitted — see `hasDynamicPublicAccessRoleResolver` below.\n *\n * Default: `false` — the full row is loaded, matching historical behavior.\n * Callers that need resource body fields (most call sites today — for\n * many resource types `resolveAccess().resource` doubles as the action's\n * primary data read) must NOT pass this.\n */\n skipResourceBody?: boolean;\n}\n\nasync function publicAccessRoleForResource(\n reg: ShareableResourceRegistration,\n resource: any,\n ctx: AccessContext,\n): Promise<ShareRole> {\n const roleResolver = reg.publicAccessRole;\n if (!roleResolver) return \"viewer\";\n return typeof roleResolver === \"function\"\n ? await roleResolver(resource, ctx)\n : roleResolver;\n}\n\nfunction higherShareRole(a: ShareRole, b: ShareRole | null): ShareRole {\n if (!b) return a;\n return ROLE_RANK[b] > ROLE_RANK[a] ? b : a;\n}\n\nfunction columnName(column: unknown): string | null {\n const candidate = column as\n | {\n name?: unknown;\n config?: { name?: unknown };\n _: { name?: unknown };\n }\n | undefined;\n const name = candidate?.name ?? candidate?.config?.name ?? candidate?._?.name;\n return typeof name === \"string\" && name ? name : null;\n}\n\nfunction missingColumnName(err: unknown): string | null {\n const error = err as { code?: string; message?: string } | undefined;\n const message = error?.message ?? \"\";\n if (\n error?.code !== \"42703\" &&\n !/no such column|does not exist/i.test(message)\n ) {\n return null;\n }\n const quoted = message.match(/column\\s+\"([^\"]+)\"\\s+does not exist/i)?.[1];\n if (quoted) return quoted;\n const sqlite = message.match(/no such column:\\s+[\"`]?([\\w.]+)[\"`]?/i)?.[1];\n if (sqlite) return sqlite.split(\".\").pop() ?? sqlite;\n return null;\n}\n\nfunction selectExistingColumns(\n columns: Record<string, unknown>,\n omittedColumnNames: Set<string>,\n): Record<string, unknown> {\n const selection: Record<string, unknown> = {};\n for (const [key, column] of Object.entries(columns)) {\n const name = columnName(column);\n if (!name || omittedColumnNames.has(name)) continue;\n selection[key] = column;\n }\n return selection;\n}\n\n/**\n * The fixed column set the access-decision logic in `resolveAccess` itself\n * reads: identity (`id`), ownership (`ownerEmail`, `orgId`), and the coarse\n * `visibility` flag. This is intentionally NOT caller-configurable — an\n * arbitrary caller-supplied column list could omit a column the access\n * checks below depend on and silently break authorization. Every ownable\n * resource table has these columns (`ownableColumns()` + a primary key\n * named `id`), so this projection is safe for any registration that\n * doesn't also read the row through a dynamic resolver (see below).\n */\nfunction projectedAccessColumns(resourceTable: any): Record<string, unknown> {\n return {\n id: resourceTable.id,\n ownerEmail: resourceTable.ownerEmail,\n orgId: resourceTable.orgId,\n visibility: resourceTable.visibility,\n };\n}\n\n/**\n * True when this registration's `publicAccessRole` is a callback rather than\n * a fixed role string. Callbacks receive the loaded `resource` and may read\n * arbitrary fields on it (see e.g. `templates/design`'s\n * `publicDesignAccessRole`, which inspects design-specific data). The\n * projected access load must never be used for these registrations — always\n * fall back to a full row load so the resolver sees the complete resource.\n */\nfunction hasDynamicPublicAccessRoleResolver(\n reg: ShareableResourceRegistration,\n): boolean {\n return typeof reg.publicAccessRole === \"function\";\n}\n\nasync function loadResourceForAccess(\n reg: ShareableResourceRegistration,\n resourceId: string,\n options: ResolveAccessOptions = {},\n): Promise<any | null> {\n const db = reg.getDb() as any;\n const useProjection =\n options.skipResourceBody === true &&\n !hasDynamicPublicAccessRoleResolver(reg);\n const projectedColumns = useProjection\n ? projectedAccessColumns(reg.resourceTable)\n : null;\n const omittedColumnNames = new Set<string>();\n\n for (let attempt = 0; attempt < 12; attempt++) {\n try {\n const query =\n !projectedColumns && omittedColumnNames.size === 0\n ? db.select()\n : db.select(\n selectExistingColumns(\n projectedColumns ?? reg.resourceTable,\n omittedColumnNames,\n ),\n );\n const [resource] = await query\n .from(reg.resourceTable)\n .where(eq(reg.resourceTable.id, resourceId));\n return resource ?? null;\n } catch (err) {\n const missing = missingColumnName(err);\n if (!missing || omittedColumnNames.has(missing)) throw err;\n omittedColumnNames.add(missing);\n console.warn(\n `[sharing] ${reg.type} access lookup omitted missing column ${missing}`,\n );\n }\n }\n\n throw new Error(\n `Could not load ${reg.type} ${resourceId}: too many missing resource columns`,\n );\n}\n\n/**\n * Return the effective role the current user has on a specific resource, or\n * null if they have no access. Loads the resource and relevant share rows.\n *\n * By default the full resource row is loaded (unchanged historical\n * behavior — for most resource types `.resource` here doubles as the\n * action's primary data read). Pass `{ skipResourceBody: true }` when the\n * caller only needs the access decision to load just the access-decision\n * columns instead — see `ResolveAccessOptions`.\n */\nexport async function resolveAccess(\n resourceType: string,\n resourceId: string,\n rawCtx?: AccessContext,\n options?: { skipResourceBody?: false },\n): Promise<ResolvedAccess | null>;\nexport async function resolveAccess(\n resourceType: string,\n resourceId: string,\n rawCtx: AccessContext | undefined,\n options: { skipResourceBody: true },\n): Promise<ResolvedAccessProjected | null>;\nexport async function resolveAccess(\n resourceType: string,\n resourceId: string,\n rawCtx: AccessContext = currentAccess(),\n options: ResolveAccessOptions = {},\n): Promise<ResolvedAccess | ResolvedAccessProjected | null> {\n return resolveAccessImpl(resourceType, resourceId, rawCtx, options);\n}\n\n/**\n * Un-overloaded implementation shared by the `resolveAccess` overloads and\n * called directly by `assertAccess` below. Kept separate from the exported\n * `resolveAccess` so internal callers passing a widened\n * `ResolveAccessOptions` (rather than a `{ skipResourceBody: true }` /\n * `{ skipResourceBody?: false }` literal) don't have to satisfy TypeScript's\n * overload resolution, which only matches against the declared overload\n * signatures, not the implementation signature.\n */\nasync function resolveAccessImpl(\n resourceType: string,\n resourceId: string,\n rawCtx: AccessContext = currentAccess(),\n options: ResolveAccessOptions = {},\n): Promise<ResolvedAccess | ResolvedAccessProjected | null> {\n const reg = requireShareableResource(resourceType);\n const ctx = resolveRegisteredAccessContext(reg, rawCtx);\n\n const resource = await loadResourceForAccess(reg, resourceId, options);\n if (!resource) return null;\n\n const { userEmail } = ctx;\n const normalizedUserEmail = normalizeEmailForAccess(userEmail);\n\n if (\n normalizedUserEmail &&\n normalizeEmailForAccess(resource.ownerEmail) === normalizedUserEmail &&\n ownerMatchesActiveScope(reg, resource, ctx)\n ) {\n return { role: \"owner\", resource };\n }\n if (resource.visibility === \"public\" && reg.allowPublic !== false) {\n // No share row needed; default viewer unless the resource registration\n // deliberately grants a stronger public-by-link role or explicit shares\n // upgrade the current principal.\n const publicRole = await publicAccessRoleForResource(reg, resource, ctx);\n const role = await highestShareRole(reg, resourceId, ctx, resource);\n return { role: higherShareRole(publicRole, role), resource };\n }\n // `visibility === \"public\"` on an `allowPublic: false` resource is treated\n // as private: only owner + explicit shares grant access. Falls through to\n // the explicit-share lookup below.\n //\n // Membership in the resource's own org, not equality with the caller's\n // currently active org: a caller can be a genuine member of the\n // resource's org while a *different* org is their active selection, and\n // `org` visibility should still admit them. Still requires some active\n // org to be set at all (`orgId`), matching the pre-existing behavior for\n // a caller with no active org.\n if (\n resource.visibility === \"org\" &&\n resource.orgId &&\n normalizedUserEmail &&\n (await isOrgMember(reg, resource.orgId, normalizedUserEmail))\n ) {\n const role = await highestShareRole(reg, resourceId, ctx, resource);\n return { role: role ?? \"viewer\", resource };\n }\n const role = await highestShareRole(reg, resourceId, ctx, resource);\n if (role) return { role, resource };\n return null;\n}\n\nasync function highestShareRole(\n reg: ShareableResourceRegistration,\n resourceId: string,\n ctx: AccessContext,\n resource: any,\n): Promise<ShareRole | null> {\n const { userEmail, orgId } = ctx;\n const normalizedUserEmail = normalizeEmailForAccess(userEmail);\n if (!normalizedUserEmail && !orgId) return null;\n if (!explicitSharesAllowedForResource(reg, resource, ctx)) return null;\n const db = reg.getDb() as any;\n\n const principalClauses: ReturnType<typeof and>[] = [];\n if (normalizedUserEmail) {\n principalClauses.push(\n and(\n eq(reg.sharesTable.principalType, \"user\"),\n emailColumnMatches(reg.sharesTable.principalId, normalizedUserEmail),\n ),\n );\n }\n if (orgId) {\n principalClauses.push(\n and(\n eq(reg.sharesTable.principalType, \"org\"),\n eq(reg.sharesTable.principalId, orgId),\n ),\n );\n }\n\n const rows = await db\n .select({ role: reg.sharesTable.role })\n .from(reg.sharesTable)\n .where(\n and(eq(reg.sharesTable.resourceId, resourceId), or(...principalClauses)),\n )\n .limit(10);\n\n let best: ShareRole | null = null;\n for (const r of rows as Array<{ role: ShareRole }>) {\n if (!best || ROLE_RANK[r.role] > ROLE_RANK[best]) best = r.role;\n }\n return best;\n}\n\n/**\n * Throw ForbiddenError if the current user can't act on this resource with at\n * least the given role. Used at the top of update/delete actions.\n *\n * By default the full resource row is loaded (unchanged historical\n * behavior). Pass `{ skipResourceBody: true }` as the fifth argument when\n * the caller only needs the access decision — see `ResolveAccessOptions`.\n */\nexport async function assertAccess(\n resourceType: string,\n resourceId: string,\n minRole?: ShareRole | \"owner\",\n ctx?: AccessContext,\n options?: { skipResourceBody?: false },\n): Promise<ResolvedAccess>;\nexport async function assertAccess(\n resourceType: string,\n resourceId: string,\n minRole: ShareRole | \"owner\" | undefined,\n ctx: AccessContext | undefined,\n options: { skipResourceBody: true },\n): Promise<ResolvedAccessProjected>;\nexport async function assertAccess(\n resourceType: string,\n resourceId: string,\n minRole: ShareRole | \"owner\" = \"viewer\",\n ctx: AccessContext = currentAccess(),\n options: ResolveAccessOptions = {},\n): Promise<ResolvedAccess | ResolvedAccessProjected> {\n const access = await resolveAccessImpl(\n resourceType,\n resourceId,\n ctx,\n options,\n );\n if (!access) {\n throw new ForbiddenError(`No access to ${resourceType} ${resourceId}`);\n }\n if (ROLE_RANK[access.role] < ROLE_RANK[minRole]) {\n throw new ForbiddenError(\n `Requires ${minRole} role on ${resourceType} ${resourceId} (have ${access.role})`,\n );\n }\n return access;\n}\n"]}
@@ -0,0 +1,235 @@
1
+ ---
2
+ title: "Add an Action"
3
+ description: "Define your first action and render its result as a UI component directly inside the chat transcript."
4
+ ---
5
+
6
+ # Add an Action
7
+
8
+ This is part two of the Getting Started series. In [Getting Started](/docs/getting-started) you created a Chat app and connected an AI engine. Here you'll define your first action and render its result inline in chat.
9
+
10
+ ## Add an action {#add-an-action}
11
+
12
+ An action is a typed operation that both your agent and your UI can call. It's
13
+ how the agent does things in your app. Actions live in the `actions/` directory
14
+ and can be triggered from chat, from React components, from the CLI, or on a
15
+ schedule. You define them once and call them from anywhere.
16
+
17
+ ### Try the starter action
18
+
19
+ The Chat template includes a `hello` action at `actions/hello.ts`:
20
+
21
+ ```ts filename="actions/hello.ts"
22
+ import { defineAction } from "@agent-native/core/action";
23
+ import { z } from "zod";
24
+
25
+ export default defineAction({
26
+ description: "Return a friendly greeting.",
27
+ schema: z.object({
28
+ name: z.string().default("world").describe("Name to greet"),
29
+ }),
30
+ http: { method: "GET" },
31
+ run: async ({ name }) => {
32
+ return { message: `Hello, ${name}!` };
33
+ },
34
+ });
35
+ ```
36
+
37
+ Run it from the terminal (inside your `my-app/` directory):
38
+
39
+ ```bash
40
+ pnpm action hello --name Alice
41
+ ```
42
+
43
+ Or open your app at `http://localhost:8080` and ask the agent in the chat there:
44
+
45
+ > Use the hello action with the name Alice.
46
+
47
+ ### Add your own action
48
+
49
+ Replace the starter action with the first real operation in your domain. This example
50
+ counts words, sentences, and paragraphs in any text you pass it. It computes
51
+ everything locally, so there's nothing to configure and no external service to connect.
52
+
53
+ Create a new file called `analyze-text.ts` in your `actions/` directory:
54
+
55
+ ```ts filename="actions/analyze-text.ts"
56
+ import { defineAction } from "@agent-native/core/action";
57
+ import { z } from "zod";
58
+
59
+ const textStatsSchema = z.object({
60
+ title: z.string(),
61
+ points: z.array(z.object({ label: z.string(), value: z.number() })),
62
+ });
63
+
64
+ export default defineAction({
65
+ description: "Count words, sentences, and paragraphs in a block of text.",
66
+ schema: z.object({
67
+ text: z
68
+ .string()
69
+ .default(
70
+ "The quick brown fox jumps over the lazy dog. Pack my box with five dozen liquor jugs.",
71
+ ),
72
+ }),
73
+ outputSchema: textStatsSchema,
74
+ chatUI: {
75
+ renderer: "text.stats-chart",
76
+ title: "Text stats",
77
+ },
78
+ readOnly: true,
79
+ run: async ({ text }) => ({
80
+ title: "Text statistics",
81
+ points: [
82
+ { label: "Characters", value: text.length },
83
+ { label: "Words", value: text.split(/\s+/).filter(Boolean).length },
84
+ {
85
+ label: "Sentences",
86
+ value: text.split(/[.!?]+/).filter(Boolean).length,
87
+ },
88
+ {
89
+ label: "Paragraphs",
90
+ value: text.split(/\n\n+/).filter(Boolean).length,
91
+ },
92
+ ],
93
+ }),
94
+ });
95
+ ```
96
+
97
+ Try it from the terminal:
98
+
99
+ ```bash
100
+ pnpm action analyze-text --text "Hello world. How are you today?"
101
+ ```
102
+
103
+ Or open your app at `http://localhost:8080` and ask the agent in the chat there:
104
+
105
+ > Run the analyze-text action on "Hello world. How are you today?"
106
+
107
+ #### Define once, call from anywhere
108
+
109
+ This action is now reachable from chat, React hooks, CLI, HTTP, MCP, A2A,
110
+ scheduled jobs, and webhooks.
111
+
112
+ TIP: Any time you want the agent to call a specific action without ambiguity, phrasing it as "Run the `<action-name>` action" is most reliable. Natural-language prompts work well once the agent has enough context about your app's domain. For a brand-new app with no data or context yet, explicit is safer.
113
+
114
+ ## Render the result inline {#render-inline}
115
+
116
+ When the agent runs `analyze-text`, it returns structured data: a title and an
117
+ array of counts. By default the agent will describe that data in prose: "The
118
+ text has 9 words, 2 sentences..." and so on. That works, but you can
119
+ also render the result as a real UI component (a bar chart, a table, a card)
120
+ directly inside the chat transcript, right where the agent responded.
121
+
122
+ This is what `chatUI.renderer` in the action does. It's a label that says "when
123
+ this action's result appears in chat, hand it to this React component instead of
124
+ summarizing it in text." The component receives the validated action output as
125
+ props and renders whatever you want.
126
+
127
+ In the next step, you'll create `app/chat-renderers.tsx`, but first, add one import line
128
+ to `app/root.tsx` so it runs on startup:
129
+
130
+ ```ts filename="app/root.tsx"
131
+ import "./chat-renderers";
132
+ ```
133
+
134
+ Add it alongside your other imports at the top of the file. That's the only
135
+ change to `root.tsx`. The import just ensures the file runs and registers the
136
+ renderer. Now create the renderer file:
137
+
138
+ ```tsx filename="app/chat-renderers.tsx"
139
+ import {
140
+ registerActionChatRenderer,
141
+ type ToolRendererProps,
142
+ } from "@agent-native/core/client/chat";
143
+
144
+ type TextStatsResult = {
145
+ title: string;
146
+ points: Array<{ label: string; value: number }>;
147
+ };
148
+
149
+ const MAX_BAR_PX = 80;
150
+
151
+ function TextStatsChart({ context }: ToolRendererProps) {
152
+ const result = context.resultJson as TextStatsResult;
153
+ const max = Math.max(...result.points.map((point) => point.value), 1);
154
+ return (
155
+ <section className="rounded-lg border bg-card p-4">
156
+ <h3 className="text-sm font-medium">{result.title}</h3>
157
+ <div className="mt-4 flex items-end gap-2">
158
+ {result.points.map((point) => (
159
+ <div
160
+ key={point.label}
161
+ className="flex flex-1 flex-col items-center gap-2"
162
+ >
163
+ <div
164
+ className="w-full rounded-t bg-blue-500"
165
+ style={{
166
+ height: `${Math.max(Math.round((point.value / max) * MAX_BAR_PX), 2)}px`,
167
+ }}
168
+ />
169
+ <span className="text-xs text-muted-foreground">{point.label}</span>
170
+ </div>
171
+ ))}
172
+ </div>
173
+ </section>
174
+ );
175
+ }
176
+
177
+ registerActionChatRenderer({
178
+ id: "text.stats-chart",
179
+ renderer: "text.stats-chart",
180
+ Component: TextStatsChart,
181
+ });
182
+ ```
183
+
184
+ Once the renderer is registered, the agent's response looks like this. Instead
185
+ of a paragraph of text, your React component renders directly inside the chat
186
+ transcript:
187
+
188
+ <WireframeBlock id="doc-block-inline-result-wireframe">
189
+ <Screen
190
+ surface="desktop"
191
+ html={
192
+ "<div style='min-height:340px;box-sizing:border-box;padding:24px;display:flex;justify-content:center;align-items:center;background:var(--wf-bg)'><div style='width:min(640px,100%);display:flex;flex-direction:column;gap:14px'><div class='wf-card' data-rough style='align-self:flex-end;max-width:70%;padding:12px 14px'><strong>User</strong><p style='margin:6px 0 0'>Run the analyze-text action on \"Hello world. How are you today?\"</p></div><div class='wf-card' data-rough style='align-self:flex-start;width:min(480px,100%);padding:14px'><strong>Agent</strong><p class='wf-muted' style='margin:6px 0 12px'>Rendered with text.stats-chart.</p><section class='wf-card' data-rough style='padding:14px'><h3 style='margin:0 0 12px;font-size:14px'>Text statistics</h3><div data-rough='line:bottom' style='height:104px;display:flex;align-items:end;gap:8px;border-bottom:1.4px solid var(--wf-line);padding-bottom:4px'><div style='flex:1;display:flex;flex-direction:column;align-items:center;gap:6px'><div data-rough style='height:80px;width:100%;background:color-mix(in srgb, var(--wf-accent) 36%, transparent);border:1.4px solid var(--wf-accent);border-radius:8px 8px 3px 3px'></div><span class='wf-muted'>Characters</span></div><div style='flex:1;display:flex;flex-direction:column;align-items:center;gap:6px'><div data-rough style='height:18px;width:100%;background:color-mix(in srgb, var(--wf-accent) 30%, transparent);border:1.4px solid var(--wf-accent);border-radius:8px 8px 3px 3px'></div><span class='wf-muted'>Words</span></div><div style='flex:1;display:flex;flex-direction:column;align-items:center;gap:6px'><div data-rough style='height:2px;width:100%;background:color-mix(in srgb, var(--wf-accent) 24%, transparent);border:1.4px solid var(--wf-accent);border-radius:8px 8px 3px 3px'></div><span class='wf-muted'>Sentences</span></div><div style='flex:1;display:flex;flex-direction:column;align-items:center;gap:6px'><div data-rough style='height:2px;width:100%;background:color-mix(in srgb, var(--wf-accent) 24%, transparent);border:1.4px solid var(--wf-accent);border-radius:8px 8px 3px 3px'></div><span class='wf-muted'>Paragraphs</span></div></div></section></div></div></div>"
193
+ }
194
+ />
195
+ </WireframeBlock>
196
+
197
+ Use this step when the result belongs where the agent is speaking:
198
+
199
+ - setup summaries
200
+ - short reports
201
+ - approvals
202
+ - tables or charts small enough to inspect inline
203
+ - links into durable app views
204
+
205
+ For reusable generic outputs, the framework also ships built-in
206
+ `data-chart` and `data-table` renderers, plus `data-insights` for combined
207
+ summary/chart/table cards. See [Native Chat UI](/docs/native-chat-ui). For
208
+ temporary controls the agent creates at runtime, see
209
+ [Generative UI](/docs/generative-ui).
210
+
211
+ ## What's next {#next}
212
+
213
+ <Cards>
214
+
215
+ ### [Persist Data in SQL](/docs/getting-started-database)
216
+
217
+ Save action results to a database so the agent can reference them across
218
+ conversations. Next in the series.
219
+
220
+ ### [Actions](/docs/actions)
221
+
222
+ Schemas, auth, approvals, hooks, and transport — the full reference for what
223
+ actions can do.
224
+
225
+ ### [Native Chat UI](/docs/native-chat-ui)
226
+
227
+ Built-in renderers for tables, charts, and typed cards — beyond the custom
228
+ renderer you just built.
229
+
230
+ ### [Key Concepts](/docs/key-concepts)
231
+
232
+ The architecture underneath this tutorial: SQL, actions, live sync, and context
233
+ awareness.
234
+
235
+ </Cards>
@@ -0,0 +1,253 @@
1
+ ---
2
+ title: "Persist Data in SQL"
3
+ description: "Save action results to a SQL database so the agent can reference them across conversations."
4
+ ---
5
+
6
+ # Persist Data in SQL
7
+
8
+ This is part three of the Getting Started series. In [Add an Action](/docs/getting-started-actions) you defined the `analyze-text` action and rendered its result inline in chat. Here you'll persist those results to a SQL database.
9
+
10
+ ## Persist data in SQL {#persist-data}
11
+
12
+ Right now, every time the agent runs `analyze-text` the result appears in chat
13
+ and then disappears. There's nothing to look back at, nothing the agent can
14
+ reference later, and no way to build a page around the data. Persisting to SQL
15
+ fixes that: the agent writes results to a table, and both the agent and your UI
16
+ can read them back at any time.
17
+
18
+ Agent-Native apps have a SQL database available by default: SQLite locally,
19
+ and your configured provider (Postgres, Turso/libSQL, Cloudflare D1) in
20
+ production.
21
+
22
+ ### Wire up the database plugin
23
+
24
+ The Chat template doesn't include a database plugin by default. Create
25
+ `server/plugins/db.ts` to initialize it. This is what runs migrations and
26
+ makes the database available to your actions:
27
+
28
+ ```ts filename="server/plugins/db.ts"
29
+ import { runMigrations } from "@agent-native/core/db";
30
+
31
+ export default runMigrations(
32
+ [
33
+ {
34
+ version: 1,
35
+ sql: `CREATE TABLE IF NOT EXISTS text_analyses (
36
+ id TEXT PRIMARY KEY,
37
+ input TEXT NOT NULL,
38
+ char_count INTEGER NOT NULL,
39
+ word_count INTEGER NOT NULL,
40
+ sentence_count INTEGER NOT NULL,
41
+ created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
42
+ )`,
43
+ },
44
+ ],
45
+ { table: "text_analyses_migrations" },
46
+ );
47
+ ```
48
+
49
+ Each entry in the array is an additive migration. When you add new columns or
50
+ tables later, append a new version object. Never edit existing ones.
51
+
52
+ ### Define the schema
53
+
54
+ Create `server/db/schema.ts`. The `server/db/` directory may not exist yet,
55
+ so create it if needed. This file describes your tables using typed helpers so
56
+ your actions get full TypeScript autocomplete:
57
+
58
+ ```ts filename="server/db/schema.ts"
59
+ import { integer, now, table, text } from "@agent-native/core/db/schema";
60
+
61
+ export const textAnalyses = table("text_analyses", {
62
+ id: text("id").primaryKey(),
63
+ input: text("input").notNull(),
64
+ charCount: integer("char_count").notNull(),
65
+ wordCount: integer("word_count").notNull(),
66
+ sentenceCount: integer("sentence_count").notNull(),
67
+ createdAt: text("created_at").notNull().default(now()),
68
+ });
69
+ ```
70
+
71
+ Use the framework schema helpers (`table`, `text`, `integer`, `now`) rather than
72
+ `sqliteTable`, `pgTable`, or dialect-specific imports. They pick the configured
73
+ SQL backend automatically, so the same schema runs locally on SQLite and in
74
+ production on any supported provider.
75
+
76
+ After adding both files, restart the dev server so the migration runs:
77
+
78
+ ```bash
79
+ pnpm dev
80
+ ```
81
+
82
+ Look for these two lines in the terminal output. They confirm the table was created:
83
+
84
+ ```
85
+ [db] Applying 1 migration(s) on SQLite/libsql…
86
+ [db] Applied migration v1 (1 statement)
87
+ ```
88
+
89
+ The `NitroViteError` lines, `BETTER_AUTH_SECRET` warning, and
90
+ `SECRETS_ENCRYPTION_KEY` warning that also appear are normal for local dev and
91
+ can be ignored.
92
+
93
+ ### Add actions for the table
94
+
95
+ Now create the action files that read and write the table. These go in your
96
+ `actions/` directory, the same place as `hello.ts` and `analyze-text.ts`. You
97
+ create them yourself, one file per operation. The agent and your UI will call
98
+ them the same way they call any other action.
99
+
100
+ **`actions/save-text-analysis.ts`** writes a result row to the database.
101
+ Call this after running `analyze-text` to make the result durable:
102
+
103
+ ```ts filename="actions/save-text-analysis.ts"
104
+ import { defineAction } from "@agent-native/core/action";
105
+ import { getDbExec } from "@agent-native/core/db";
106
+ import { z } from "zod";
107
+
108
+ export default defineAction({
109
+ description: "Save a text analysis result to the database.",
110
+ schema: z.object({
111
+ input: z.string(),
112
+ charCount: z.number(),
113
+ wordCount: z.number(),
114
+ sentenceCount: z.number(),
115
+ }),
116
+ run: async ({ input, charCount, wordCount, sentenceCount }) => {
117
+ const id = crypto.randomUUID();
118
+ await getDbExec().execute({
119
+ sql: `INSERT INTO text_analyses (id, input, char_count, word_count, sentence_count)
120
+ VALUES (?, ?, ?, ?, ?)`,
121
+ args: [id, input, charCount, wordCount, sentenceCount],
122
+ });
123
+ return { id };
124
+ },
125
+ });
126
+ ```
127
+
128
+ **`actions/list-text-analyses.ts`** reads all saved results. The agent can
129
+ call this to summarize past analyses, and your UI can use it to populate a page:
130
+
131
+ ```ts filename="actions/list-text-analyses.ts"
132
+ import { defineAction } from "@agent-native/core/action";
133
+ import { getDbExec } from "@agent-native/core/db";
134
+ import { z } from "zod";
135
+
136
+ export default defineAction({
137
+ description: "List all saved text analyses, newest first.",
138
+ schema: z.object({}),
139
+ run: async () => {
140
+ const result = await getDbExec().execute(
141
+ `SELECT id, input, char_count, word_count, sentence_count, created_at
142
+ FROM text_analyses
143
+ ORDER BY created_at DESC`,
144
+ );
145
+ return result.rows;
146
+ },
147
+ });
148
+ ```
149
+
150
+ **`actions/delete-text-analysis.ts`** removes a row by id:
151
+
152
+ ```ts filename="actions/delete-text-analysis.ts"
153
+ import { defineAction } from "@agent-native/core/action";
154
+ import { getDbExec } from "@agent-native/core/db";
155
+ import { z } from "zod";
156
+
157
+ export default defineAction({
158
+ description: "Delete a saved text analysis by id.",
159
+ schema: z.object({ id: z.string() }),
160
+ run: async ({ id }) => {
161
+ await getDbExec().execute({
162
+ sql: `DELETE FROM text_analyses WHERE id = ?`,
163
+ args: [id],
164
+ });
165
+ return { deleted: id };
166
+ },
167
+ });
168
+ ```
169
+
170
+ Once these files are saved the dev server picks them up automatically. No
171
+ restart needed. Try listing analyses from the terminal:
172
+
173
+ ```bash
174
+ pnpm action list-text-analyses
175
+ ```
176
+
177
+ You should see an empty array. The table exists and the action works; there's
178
+ just nothing saved yet:
179
+
180
+ ```
181
+ []
182
+ ```
183
+
184
+ Data is saved to `data/app.db`, a SQLite file in your project directory that
185
+ gets created automatically on first run. In production you'd point
186
+ `DATABASE_URL` at a hosted database instead, but locally this file is all you
187
+ need.
188
+
189
+ To save something, first run `analyze-text` to get the counts:
190
+
191
+ ```bash
192
+ pnpm action analyze-text --text "Hello world"
193
+ ```
194
+
195
+ You'll see output like:
196
+
197
+ ```
198
+ {
199
+ title: 'Text statistics',
200
+ points: [
201
+ { label: 'Characters', value: 11 },
202
+ { label: 'Words', value: 2 },
203
+ { label: 'Sentences', value: 1 },
204
+ { label: 'Paragraphs', value: 1 }
205
+ ]
206
+ }
207
+ ```
208
+
209
+ Then pass those values to `save-text-analysis`:
210
+
211
+ ```bash
212
+ pnpm action save-text-analysis \
213
+ --input "Hello world" \
214
+ --charCount 11 \
215
+ --wordCount 2 \
216
+ --sentenceCount 1
217
+ ```
218
+
219
+ Now run `list-text-analyses` again and you'll see the saved row:
220
+
221
+ ```bash
222
+ pnpm action list-text-analyses
223
+ ```
224
+
225
+ Or ask the agent in the chat at `http://localhost:8080` to do both steps at once:
226
+
227
+ > Run analyze-text on "Hello world", then save the result.
228
+
229
+ ## What's next {#next}
230
+
231
+ <Cards>
232
+
233
+ ### [Add a Page](/docs/getting-started-pages)
234
+
235
+ Build a React route that displays your saved analyses and wire it into the
236
+ sidebar. Next in the series.
237
+
238
+ ### [Database](/docs/database)
239
+
240
+ Migrations, schema helpers, and production database setup — the full reference
241
+ for SQL in Agent Native.
242
+
243
+ ### [Actions](/docs/actions)
244
+
245
+ Schemas, auth, approvals, hooks, and transport — the full reference for what
246
+ actions can do.
247
+
248
+ ### [Key Concepts](/docs/key-concepts)
249
+
250
+ The architecture underneath this tutorial: SQL, actions, live sync, and context
251
+ awareness.
252
+
253
+ </Cards>