@agent-native/core 0.87.0 → 0.88.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +6 -0
  3. package/corpus/core/docs/content/agent-native-toolkit.mdx +162 -0
  4. package/corpus/core/docs/content/locales/ar-SA/agent-native-toolkit.mdx +73 -0
  5. package/corpus/core/docs/content/locales/ar-SA/sharing.mdx +15 -7
  6. package/corpus/core/docs/content/locales/de-DE/agent-native-toolkit.mdx +77 -0
  7. package/corpus/core/docs/content/locales/de-DE/sharing.mdx +15 -7
  8. package/corpus/core/docs/content/locales/es-ES/agent-native-toolkit.mdx +77 -0
  9. package/corpus/core/docs/content/locales/es-ES/sharing.mdx +15 -7
  10. package/corpus/core/docs/content/locales/fr-FR/agent-native-toolkit.mdx +79 -0
  11. package/corpus/core/docs/content/locales/fr-FR/sharing.mdx +15 -7
  12. package/corpus/core/docs/content/locales/hi-IN/agent-native-toolkit.mdx +74 -0
  13. package/corpus/core/docs/content/locales/hi-IN/sharing.mdx +15 -7
  14. package/corpus/core/docs/content/locales/ja-JP/agent-native-toolkit.mdx +67 -0
  15. package/corpus/core/docs/content/locales/ja-JP/sharing.mdx +15 -7
  16. package/corpus/core/docs/content/locales/ko-KR/agent-native-toolkit.mdx +63 -0
  17. package/corpus/core/docs/content/locales/ko-KR/sharing.mdx +15 -7
  18. package/corpus/core/docs/content/locales/pt-BR/agent-native-toolkit.mdx +77 -0
  19. package/corpus/core/docs/content/locales/pt-BR/sharing.mdx +15 -7
  20. package/corpus/core/docs/content/locales/zh-CN/agent-native-toolkit.mdx +61 -0
  21. package/corpus/core/docs/content/locales/zh-CN/sharing.mdx +15 -7
  22. package/corpus/core/docs/content/locales/zh-TW/agent-native-toolkit.mdx +61 -0
  23. package/corpus/core/docs/content/locales/zh-TW/sharing.mdx +15 -7
  24. package/corpus/core/docs/content/sharing.mdx +39 -7
  25. package/corpus/core/package.json +2 -1
  26. package/corpus/core/src/server/action-discovery.ts +4 -0
  27. package/corpus/core/src/server/action-routes.ts +8 -1
  28. package/corpus/core/src/server/index.ts +9 -0
  29. package/corpus/core/src/shared/agent-readable-resource.ts +111 -0
  30. package/corpus/core/src/shared/index.ts +9 -0
  31. package/corpus/core/src/sharing/actions/create-agent-resource-link.ts +91 -0
  32. package/corpus/core/src/sharing/registry.ts +19 -0
  33. package/corpus/core/src/templates/workspace-core/.agents/skills/agent-native-toolkit/SKILL.md +92 -0
  34. package/corpus/core/src/vite/client.ts +1 -0
  35. package/corpus/templates/analytics/actions/get-sql-dashboard.ts +9 -113
  36. package/corpus/templates/analytics/server/db/index.ts +14 -0
  37. package/corpus/templates/analytics/server/lib/agent-readable-resource-context.ts +156 -0
  38. package/corpus/templates/analytics/server/routes/[...page].get.ts +92 -25
  39. package/corpus/templates/analytics/server/routes/api/analysis-agent-context.json.get.ts +94 -0
  40. package/corpus/templates/analytics/server/routes/api/dashboard-agent-context.json.get.ts +109 -0
  41. package/corpus/templates/analytics/shared/resource-agent-access.ts +6 -0
  42. package/corpus/templates/content/app/routes/p.$id.tsx +114 -9
  43. package/corpus/templates/content/server/db/index.ts +9 -0
  44. package/corpus/templates/content/server/routes/api/document-agent-context.json.get.ts +90 -0
  45. package/corpus/templates/content/shared/agent-readable.ts +56 -0
  46. package/corpus/templates/design/server/db/index.ts +8 -0
  47. package/corpus/templates/design/server/routes/[...page].get.ts +94 -1
  48. package/corpus/templates/design/server/routes/api/design-agent-context.json.get.ts +84 -0
  49. package/corpus/templates/design/shared/agent-readable.ts +2 -0
  50. package/corpus/templates/plan/server/db/index.ts +12 -0
  51. package/corpus/templates/plan/server/plans.ts +25 -1
  52. package/corpus/templates/plan/server/routes/[...page].get.ts +107 -1
  53. package/corpus/templates/plan/server/routes/api/plan-agent-context.json.get.ts +65 -0
  54. package/corpus/templates/plan/shared/agent-readable.ts +2 -0
  55. package/corpus/templates/slides/app/routes/p.$id.tsx +126 -12
  56. package/corpus/templates/slides/app/routes/share.$token.tsx +55 -8
  57. package/corpus/templates/slides/server/db/index.ts +9 -0
  58. package/corpus/templates/slides/server/routes/api/deck-agent-context.json.get.ts +97 -0
  59. package/corpus/templates/slides/shared/agent-readable.ts +2 -0
  60. package/dist/collab/routes.d.ts +1 -1
  61. package/dist/collab/struct-routes.d.ts +1 -1
  62. package/dist/notifications/routes.d.ts +1 -1
  63. package/dist/observability/routes.d.ts +5 -5
  64. package/dist/progress/routes.d.ts +1 -1
  65. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  66. package/dist/resources/handlers.d.ts +1 -1
  67. package/dist/secrets/routes.d.ts +9 -9
  68. package/dist/server/action-discovery.d.ts.map +1 -1
  69. package/dist/server/action-discovery.js +4 -0
  70. package/dist/server/action-discovery.js.map +1 -1
  71. package/dist/server/action-routes.d.ts.map +1 -1
  72. package/dist/server/action-routes.js +8 -2
  73. package/dist/server/action-routes.js.map +1 -1
  74. package/dist/server/index.d.ts +1 -0
  75. package/dist/server/index.d.ts.map +1 -1
  76. package/dist/server/index.js +1 -0
  77. package/dist/server/index.js.map +1 -1
  78. package/dist/shared/agent-readable-resource.d.ts +29 -0
  79. package/dist/shared/agent-readable-resource.d.ts.map +1 -0
  80. package/dist/shared/agent-readable-resource.js +60 -0
  81. package/dist/shared/agent-readable-resource.js.map +1 -0
  82. package/dist/shared/index.d.ts +1 -0
  83. package/dist/shared/index.d.ts.map +1 -1
  84. package/dist/shared/index.js +1 -0
  85. package/dist/shared/index.js.map +1 -1
  86. package/dist/sharing/actions/create-agent-resource-link.d.ts +13 -0
  87. package/dist/sharing/actions/create-agent-resource-link.d.ts.map +1 -0
  88. package/dist/sharing/actions/create-agent-resource-link.js +72 -0
  89. package/dist/sharing/actions/create-agent-resource-link.js.map +1 -0
  90. package/dist/sharing/registry.d.ts +17 -0
  91. package/dist/sharing/registry.d.ts.map +1 -1
  92. package/dist/sharing/registry.js.map +1 -1
  93. package/dist/templates/workspace-core/.agents/skills/agent-native-toolkit/SKILL.md +92 -0
  94. package/dist/vite/client.d.ts.map +1 -1
  95. package/dist/vite/client.js +1 -0
  96. package/dist/vite/client.js.map +1 -1
  97. package/docs/content/agent-native-toolkit.mdx +162 -0
  98. package/docs/content/locales/ar-SA/agent-native-toolkit.mdx +73 -0
  99. package/docs/content/locales/ar-SA/sharing.mdx +15 -7
  100. package/docs/content/locales/de-DE/agent-native-toolkit.mdx +77 -0
  101. package/docs/content/locales/de-DE/sharing.mdx +15 -7
  102. package/docs/content/locales/es-ES/agent-native-toolkit.mdx +77 -0
  103. package/docs/content/locales/es-ES/sharing.mdx +15 -7
  104. package/docs/content/locales/fr-FR/agent-native-toolkit.mdx +79 -0
  105. package/docs/content/locales/fr-FR/sharing.mdx +15 -7
  106. package/docs/content/locales/hi-IN/agent-native-toolkit.mdx +74 -0
  107. package/docs/content/locales/hi-IN/sharing.mdx +15 -7
  108. package/docs/content/locales/ja-JP/agent-native-toolkit.mdx +67 -0
  109. package/docs/content/locales/ja-JP/sharing.mdx +15 -7
  110. package/docs/content/locales/ko-KR/agent-native-toolkit.mdx +63 -0
  111. package/docs/content/locales/ko-KR/sharing.mdx +15 -7
  112. package/docs/content/locales/pt-BR/agent-native-toolkit.mdx +77 -0
  113. package/docs/content/locales/pt-BR/sharing.mdx +15 -7
  114. package/docs/content/locales/zh-CN/agent-native-toolkit.mdx +61 -0
  115. package/docs/content/locales/zh-CN/sharing.mdx +15 -7
  116. package/docs/content/locales/zh-TW/agent-native-toolkit.mdx +61 -0
  117. package/docs/content/locales/zh-TW/sharing.mdx +15 -7
  118. package/docs/content/sharing.mdx +39 -7
  119. package/package.json +2 -1
  120. package/src/templates/workspace-core/.agents/skills/agent-native-toolkit/SKILL.md +92 -0
@@ -0,0 +1,6 @@
1
+ export const ANALYTICS_DASHBOARD_AGENT_RESOURCE_KIND = "analytics:dashboard";
2
+ export const ANALYTICS_ANALYSIS_AGENT_RESOURCE_KIND = "analytics:analysis";
3
+ export const ANALYTICS_DASHBOARD_AGENT_CONTEXT_ENDPOINT =
4
+ "/api/dashboard-agent-context.json";
5
+ export const ANALYTICS_ANALYSIS_AGENT_CONTEXT_ENDPOINT =
6
+ "/api/analysis-agent-context.json";
@@ -1,23 +1,82 @@
1
1
  import { agentNativePath, useT } from "@agent-native/core/client";
2
+ import {
3
+ AGENT_ACCESS_PARAM,
4
+ verifyScopedAgentAccessToken,
5
+ } from "@agent-native/core/server";
2
6
  import {
3
7
  getConfiguredAppBasePath,
4
8
  getRequestUserEmail,
5
9
  } from "@agent-native/core/server";
10
+ import {
11
+ AGENT_READABLE_RESOURCE_SCRIPT_TYPE,
12
+ safeJsonForHtml,
13
+ } from "@agent-native/core/shared";
6
14
  import { resolveAccess } from "@agent-native/core/sharing";
7
15
  import { buildPublicDocumentDescription } from "@shared/og-description";
8
16
  import { IconLock, IconMessageCircle } from "@tabler/icons-react";
9
17
  import { eq } from "drizzle-orm";
10
18
  import { useEffect, useState } from "react";
11
- import type { LoaderFunctionArgs, MetaFunction } from "react-router";
12
- import { redirect, useLoaderData } from "react-router";
19
+ import type {
20
+ HeadersArgs,
21
+ LoaderFunctionArgs,
22
+ MetaFunction,
23
+ } from "react-router";
24
+ import { data, redirect, useLoaderData } from "react-router";
13
25
 
14
26
  import { VisualEditor } from "@/components/editor/VisualEditor";
15
27
 
16
28
  import { getDb, schema } from "../../server/db";
29
+ import {
30
+ buildContentDocumentAgentDiscovery,
31
+ buildContentPublicDocumentUrl,
32
+ DOCUMENT_AGENT_RESOURCE_KIND,
33
+ } from "../../shared/agent-readable";
34
+
35
+ type PublicDocumentLoaderData =
36
+ | {
37
+ document: {
38
+ id: string;
39
+ title: string;
40
+ content: string;
41
+ updatedAt: string;
42
+ visibility: string;
43
+ };
44
+ agentAccessToken: string | null;
45
+ basePath: string;
46
+ unavailable?: undefined;
47
+ }
48
+ | {
49
+ document: null;
50
+ agentAccessToken: null;
51
+ basePath: string;
52
+ unavailable: { reason: "private"; id: string; basePath: string };
53
+ };
54
+
55
+ const PRIVATE_AGENT_DOCUMENT_HEADERS = {
56
+ "Cache-Control": "private, max-age=0, no-store",
57
+ "Referrer-Policy": "no-referrer",
58
+ };
17
59
 
18
- export async function loader({ params }: LoaderFunctionArgs) {
60
+ function publicDocumentLoaderData(
61
+ payload: PublicDocumentLoaderData,
62
+ privateAgentAccess = false,
63
+ ) {
64
+ if (!privateAgentAccess) return payload;
65
+ return data(payload, {
66
+ headers: PRIVATE_AGENT_DOCUMENT_HEADERS,
67
+ });
68
+ }
69
+
70
+ export function headers({ loaderHeaders }: HeadersArgs) {
71
+ return loaderHeaders;
72
+ }
73
+
74
+ export async function loader({ params, request }: LoaderFunctionArgs) {
19
75
  const id = params.id;
20
76
  if (!id) throw new Response("Not found", { status: 404 });
77
+ const agentAccessToken = new URL(request.url).searchParams.get(
78
+ AGENT_ACCESS_PARAM,
79
+ );
21
80
 
22
81
  // This is a server loader; use the server-side base-path helper
23
82
  // (reads APP_BASE_PATH / VITE_APP_BASE_PATH at request time)
@@ -45,7 +104,22 @@ export async function loader({ params }: LoaderFunctionArgs) {
45
104
  .limit(1);
46
105
 
47
106
  if (!doc) throw new Response("Not found", { status: 404 });
48
- if (doc.visibility === "public") return { document: doc };
107
+ const tokenAccess = agentAccessToken
108
+ ? verifyScopedAgentAccessToken(agentAccessToken, {
109
+ resourceKind: DOCUMENT_AGENT_RESOURCE_KIND,
110
+ resourceId: id,
111
+ }).ok
112
+ : false;
113
+ if (doc.visibility === "public" || tokenAccess) {
114
+ return publicDocumentLoaderData(
115
+ {
116
+ document: doc,
117
+ agentAccessToken: tokenAccess ? agentAccessToken : null,
118
+ basePath,
119
+ },
120
+ tokenAccess,
121
+ );
122
+ }
49
123
 
50
124
  // Doc exists but isn't public. SSR renders impersonally (no session is read
51
125
  // server-side, so the page can be CDN-cached for everyone), which means we
@@ -56,10 +130,12 @@ export async function loader({ params }: LoaderFunctionArgs) {
56
130
  // routes the viewer to the auth-guarded `/page/<id>` editor, where the real
57
131
  // per-user access check runs (signed-in-with-access sees the doc; everyone
58
132
  // else gets the standard sign-in / no-access handling).
59
- return {
133
+ return publicDocumentLoaderData({
60
134
  document: null,
135
+ agentAccessToken: null,
136
+ basePath,
61
137
  unavailable: { reason: "private" as const, id, basePath },
62
- };
138
+ });
63
139
  }
64
140
 
65
141
  export const meta: MetaFunction<typeof loader> = ({ loaderData }) => {
@@ -139,6 +215,7 @@ function renderMarkdownBlocks(content: string) {
139
215
 
140
216
  function PublicDocumentContextSync({
141
217
  document,
218
+ basePath,
142
219
  }: {
143
220
  document: {
144
221
  id: string;
@@ -146,6 +223,7 @@ function PublicDocumentContextSync({
146
223
  content: string;
147
224
  updatedAt: string;
148
225
  };
226
+ basePath?: string;
149
227
  }) {
150
228
  useEffect(() => {
151
229
  fetch(agentNativePath("/_agent-native/application-state/navigation"), {
@@ -156,10 +234,10 @@ function PublicDocumentContextSync({
156
234
  view: "public-document",
157
235
  documentId: document.id,
158
236
  title: document.title,
159
- publicUrl: `/p/${document.id}`,
237
+ publicUrl: buildContentPublicDocumentUrl(document.id, { basePath }),
160
238
  }),
161
239
  }).catch(() => {});
162
- }, [document.id, document.title]);
240
+ }, [basePath, document.id, document.title]);
163
241
 
164
242
  return null;
165
243
  }
@@ -178,6 +256,28 @@ function ReadOnlyMarkdownContent({ content }: { content: string }) {
178
256
  );
179
257
  }
180
258
 
259
+ function AgentReadableDocumentDiscovery({
260
+ document,
261
+ token,
262
+ basePath,
263
+ }: {
264
+ document: { id: string; title: string };
265
+ token?: string | null;
266
+ basePath?: string;
267
+ }) {
268
+ const discovery = buildContentDocumentAgentDiscovery({
269
+ document,
270
+ token,
271
+ basePath,
272
+ });
273
+ return (
274
+ <script
275
+ type={AGENT_READABLE_RESOURCE_SCRIPT_TYPE}
276
+ dangerouslySetInnerHTML={{ __html: safeJsonForHtml(discovery) }}
277
+ />
278
+ );
279
+ }
280
+
181
281
  function PrivateDocumentNotice({
182
282
  id,
183
283
  basePath,
@@ -230,7 +330,12 @@ export default function PublicDocumentPage() {
230
330
 
231
331
  return (
232
332
  <main className="min-h-screen bg-background text-foreground">
233
- <PublicDocumentContextSync document={document} />
333
+ <PublicDocumentContextSync document={document} basePath={data.basePath} />
334
+ <AgentReadableDocumentDiscovery
335
+ document={document}
336
+ token={data.agentAccessToken}
337
+ basePath={data.basePath}
338
+ />
234
339
  <div className="mx-auto flex max-w-3xl justify-end px-6 pt-5 sm:px-8">
235
340
  <button
236
341
  type="button"
@@ -1,6 +1,10 @@
1
1
  import { createGetDb } from "@agent-native/core/db";
2
2
  import { registerShareableResource } from "@agent-native/core/sharing";
3
3
 
4
+ import {
5
+ DOCUMENT_AGENT_CONTEXT_ENDPOINT,
6
+ DOCUMENT_AGENT_RESOURCE_KIND,
7
+ } from "../../shared/agent-readable.js";
4
8
  import * as schema from "./schema.js";
5
9
 
6
10
  export const getDb = createGetDb(schema);
@@ -13,5 +17,10 @@ registerShareableResource({
13
17
  displayName: "Document",
14
18
  titleColumn: "title",
15
19
  getResourcePath: (document) => `/page/${document.id}`,
20
+ agentReadable: {
21
+ resourceKind: DOCUMENT_AGENT_RESOURCE_KIND,
22
+ getContextPath: () => DOCUMENT_AGENT_CONTEXT_ENDPOINT,
23
+ getPagePath: (document) => `/p/${document.id}`,
24
+ },
16
25
  getDb,
17
26
  });
@@ -0,0 +1,90 @@
1
+ import {
2
+ AGENT_ACCESS_PARAM,
3
+ getConfiguredAppBasePath,
4
+ verifyScopedAgentAccessToken,
5
+ } from "@agent-native/core/server";
6
+ import { eq } from "drizzle-orm";
7
+ import {
8
+ defineEventHandler,
9
+ getQuery,
10
+ setResponseHeader,
11
+ setResponseStatus,
12
+ } from "h3";
13
+
14
+ import {
15
+ buildContentPublicDocumentUrl,
16
+ DOCUMENT_AGENT_RESOURCE_KIND,
17
+ } from "../../../shared/agent-readable.js";
18
+ import { getDb, schema } from "../../db/index.js";
19
+
20
+ function queryString(value: unknown): string {
21
+ if (typeof value === "string") return value;
22
+ if (Array.isArray(value) && typeof value[0] === "string") return value[0];
23
+ return "";
24
+ }
25
+
26
+ function deny(statusCode: number, message: string) {
27
+ return { statusCode, body: { error: message } };
28
+ }
29
+
30
+ export default defineEventHandler(async (event) => {
31
+ setResponseHeader(event, "Cache-Control", "no-store");
32
+ setResponseHeader(event, "Referrer-Policy", "no-referrer");
33
+ setResponseHeader(event, "X-Content-Type-Options", "nosniff");
34
+
35
+ const query = getQuery(event);
36
+ const id = queryString(query.id);
37
+ if (!id) {
38
+ setResponseStatus(event, 400);
39
+ return { error: "Document id is required" };
40
+ }
41
+
42
+ const token = queryString(query[AGENT_ACCESS_PARAM]);
43
+ const db = getDb();
44
+ // guard:allow-unscoped -- this endpoint returns a document only when it is public or a document-scoped agent_access token verifies for this id.
45
+ const [document] = await db
46
+ .select({
47
+ id: schema.documents.id,
48
+ title: schema.documents.title,
49
+ content: schema.documents.content,
50
+ icon: schema.documents.icon,
51
+ visibility: schema.documents.visibility,
52
+ updatedAt: schema.documents.updatedAt,
53
+ createdAt: schema.documents.createdAt,
54
+ })
55
+ .from(schema.documents)
56
+ .where(eq(schema.documents.id, id))
57
+ .limit(1);
58
+
59
+ if (!document) {
60
+ setResponseStatus(event, 404);
61
+ return { error: "Document not found" };
62
+ }
63
+
64
+ const tokenAccess = token
65
+ ? verifyScopedAgentAccessToken(token, {
66
+ resourceKind: DOCUMENT_AGENT_RESOURCE_KIND,
67
+ resourceId: id,
68
+ }).ok
69
+ : false;
70
+ if (document.visibility !== "public" && !tokenAccess) {
71
+ const denied = deny(403, "Invalid or expired agent access token");
72
+ setResponseStatus(event, denied.statusCode);
73
+ return denied.body;
74
+ }
75
+
76
+ return {
77
+ resourceType: "document",
78
+ id: document.id,
79
+ title: document.title,
80
+ icon: document.icon,
81
+ content: document.content,
82
+ visibility: document.visibility,
83
+ createdAt: document.createdAt,
84
+ updatedAt: document.updatedAt,
85
+ url: buildContentPublicDocumentUrl(document.id, {
86
+ basePath: getConfiguredAppBasePath(),
87
+ token: tokenAccess ? token : null,
88
+ }),
89
+ };
90
+ });
@@ -0,0 +1,56 @@
1
+ import {
2
+ AGENT_ACCESS_PARAM,
3
+ buildAgentAccessUrl,
4
+ buildAgentReadableResourceDiscovery,
5
+ normalizeAgentAccessBasePath,
6
+ toAgentAccessUrl,
7
+ type AgentReadableResourceDiscovery,
8
+ } from "@agent-native/core/shared";
9
+
10
+ export const DOCUMENT_AGENT_RESOURCE_KIND = "content:document";
11
+ export const DOCUMENT_AGENT_CONTEXT_ENDPOINT =
12
+ "/api/document-agent-context.json";
13
+ export const DOCUMENT_AGENT_READABLE_INSTRUCTIONS =
14
+ "Use contextUrl to read this shared Content document as structured JSON. The page is read-only for token and public viewers.";
15
+
16
+ export function buildContentPublicDocumentPath(documentId: string): string {
17
+ return `/p/${documentId}`;
18
+ }
19
+
20
+ export function buildContentPublicDocumentUrl(
21
+ documentId: string,
22
+ options: { basePath?: string; token?: string | null } = {},
23
+ ): string {
24
+ const path = buildContentPublicDocumentPath(documentId);
25
+ const basePath = normalizeAgentAccessBasePath(options.basePath);
26
+ if (options.token) {
27
+ return buildAgentAccessUrl({
28
+ path,
29
+ basePath,
30
+ token: options.token,
31
+ tokenParam: AGENT_ACCESS_PARAM,
32
+ });
33
+ }
34
+ return toAgentAccessUrl(path, { basePath });
35
+ }
36
+
37
+ export function buildContentDocumentAgentDiscovery({
38
+ document,
39
+ token,
40
+ basePath,
41
+ }: {
42
+ document: { id: string; title: string };
43
+ token?: string | null;
44
+ basePath?: string;
45
+ }): AgentReadableResourceDiscovery {
46
+ return buildAgentReadableResourceDiscovery({
47
+ resourceType: "document",
48
+ resourceId: document.id,
49
+ title: document.title,
50
+ path: buildContentPublicDocumentPath(document.id),
51
+ contextEndpoint: DOCUMENT_AGENT_CONTEXT_ENDPOINT,
52
+ token,
53
+ basePath,
54
+ instructions: DOCUMENT_AGENT_READABLE_INSTRUCTIONS,
55
+ });
56
+ }
@@ -1,6 +1,10 @@
1
1
  import { createGetDb } from "@agent-native/core/db";
2
2
  import { registerShareableResource } from "@agent-native/core/sharing";
3
3
 
4
+ import {
5
+ DESIGN_AGENT_CONTEXT_ENDPOINT,
6
+ DESIGN_AGENT_RESOURCE_KIND,
7
+ } from "../../shared/agent-readable.js";
4
8
  import * as schema from "./schema.js";
5
9
 
6
10
  export const getDb = createGetDb(schema);
@@ -63,6 +67,10 @@ registerShareableResource({
63
67
  displayName: "Design",
64
68
  titleColumn: "title",
65
69
  getResourcePath: (design) => `/design/${design.id}`,
70
+ agentReadable: {
71
+ resourceKind: DESIGN_AGENT_RESOURCE_KIND,
72
+ getContextPath: () => DESIGN_AGENT_CONTEXT_ENDPOINT,
73
+ },
66
74
  getDb,
67
75
  publicAccessRole: (design) =>
68
76
  isLoopbackLocalhostDesign(design) ? "editor" : "viewer",
@@ -1,5 +1,98 @@
1
+ import {
2
+ AGENT_ACCESS_PARAM,
3
+ getConfiguredAppBasePath,
4
+ } from "@agent-native/core/server";
1
5
  import { createH3SSRHandler } from "@agent-native/core/server/ssr-handler";
6
+ import {
7
+ buildAgentReadableResourceDiscovery,
8
+ renderAgentReadableResourceDiscoveryScript,
9
+ } from "@agent-native/core/shared";
10
+ import {
11
+ defineEventHandler,
12
+ getQuery,
13
+ getRequestURL,
14
+ setResponseHeader,
15
+ } from "h3";
2
16
 
3
- export default createH3SSRHandler(
17
+ import { DESIGN_AGENT_CONTEXT_ENDPOINT } from "../../shared/agent-readable.js";
18
+
19
+ const ssrHandler = createH3SSRHandler(
4
20
  () => import("virtual:react-router/server-build"),
5
21
  );
22
+
23
+ function stripBasePath(pathname: string): string {
24
+ const basePath = getConfiguredAppBasePath();
25
+ if (!basePath) return pathname;
26
+ if (pathname === basePath) return "/";
27
+ if (pathname.startsWith(`${basePath}/`)) {
28
+ return pathname.slice(basePath.length) || "/";
29
+ }
30
+ return pathname;
31
+ }
32
+
33
+ function designIdFromPath(pathname: string): string | null {
34
+ const match = stripBasePath(pathname).match(/^\/design\/([^/]+)\/?$/);
35
+ if (!match?.[1]) return null;
36
+ try {
37
+ return decodeURIComponent(match[1]);
38
+ } catch {
39
+ return match[1];
40
+ }
41
+ }
42
+
43
+ function queryString(value: unknown): string {
44
+ if (typeof value === "string") return value;
45
+ if (Array.isArray(value) && typeof value[0] === "string") return value[0];
46
+ return "";
47
+ }
48
+
49
+ function injectScript(html: string, script: string): string {
50
+ if (html.includes("agent-native-design-agent-context")) return html;
51
+ if (html.includes("</head>"))
52
+ return html.replace("</head>", `${script}</head>`);
53
+ if (html.includes("</body>"))
54
+ return html.replace("</body>", `${script}</body>`);
55
+ return `${html}${script}`;
56
+ }
57
+
58
+ export default defineEventHandler(async (event) => {
59
+ const response = (await ssrHandler(event)) as Response;
60
+ const requestUrl = getRequestURL(event);
61
+ const designId = designIdFromPath(requestUrl.pathname);
62
+ if (!designId) return response;
63
+
64
+ const token = queryString(getQuery(event)[AGENT_ACCESS_PARAM]);
65
+ const script = renderAgentReadableResourceDiscoveryScript(
66
+ buildAgentReadableResourceDiscovery({
67
+ resourceType: "design",
68
+ resourceId: designId,
69
+ path: `/design/${designId}`,
70
+ contextEndpoint: DESIGN_AGENT_CONTEXT_ENDPOINT,
71
+ origin: requestUrl.origin,
72
+ basePath: getConfiguredAppBasePath(),
73
+ token,
74
+ instructions:
75
+ "Use contextUrl to read the current design handoff JSON. Token links are read-only and do not grant edit access.",
76
+ }),
77
+ { id: "agent-native-design-agent-context" },
78
+ );
79
+
80
+ const contentType = response.headers.get("content-type") ?? "";
81
+ if (!contentType.includes("text/html")) return response;
82
+
83
+ const html = await response.text();
84
+ const headers = new Headers(response.headers);
85
+ headers.delete("content-length");
86
+ if (token) {
87
+ headers.set("Cache-Control", "private, max-age=0, no-store");
88
+ headers.set("Referrer-Policy", "no-referrer");
89
+ setResponseHeader(event, "Cache-Control", "private, max-age=0, no-store");
90
+ setResponseHeader(event, "Referrer-Policy", "no-referrer");
91
+ }
92
+
93
+ return new Response(injectScript(html, script), {
94
+ status: response.status,
95
+ statusText: response.statusText,
96
+ headers,
97
+ });
98
+ });
@@ -0,0 +1,84 @@
1
+ import {
2
+ AGENT_ACCESS_PARAM,
3
+ verifyScopedAgentAccessToken,
4
+ } from "@agent-native/core/server";
5
+ import { eq } from "drizzle-orm";
6
+ import {
7
+ defineEventHandler,
8
+ getQuery,
9
+ setResponseHeader,
10
+ setResponseStatus,
11
+ } from "h3";
12
+
13
+ import { DESIGN_AGENT_RESOURCE_KIND } from "../../../shared/agent-readable.js";
14
+ import { getDb, schema } from "../../db/index.js";
15
+ import { buildDesignHandoffPayload } from "../../lib/coding-handoff.js";
16
+ import { buildDesignSnapshot } from "../../lib/design-snapshot.js";
17
+
18
+ function queryString(value: unknown): string {
19
+ if (typeof value === "string") return value;
20
+ if (Array.isArray(value) && typeof value[0] === "string") return value[0];
21
+ return "";
22
+ }
23
+
24
+ export default defineEventHandler(async (event) => {
25
+ setResponseHeader(event, "Cache-Control", "no-store");
26
+ setResponseHeader(event, "Referrer-Policy", "no-referrer");
27
+ setResponseHeader(event, "X-Content-Type-Options", "nosniff");
28
+
29
+ const query = getQuery(event);
30
+ const id = queryString(query.id);
31
+ if (!id) {
32
+ setResponseStatus(event, 400);
33
+ return { error: "Design id is required" };
34
+ }
35
+
36
+ const token = queryString(query[AGENT_ACCESS_PARAM]);
37
+ const db = getDb();
38
+ // guard:allow-unscoped -- this endpoint returns a design handoff only when it is public or a design-scoped agent_access token verifies for this id.
39
+ const [design] = await db
40
+ .select()
41
+ .from(schema.designs)
42
+ .where(eq(schema.designs.id, id))
43
+ .limit(1);
44
+
45
+ if (!design) {
46
+ setResponseStatus(event, 404);
47
+ return { error: "Design not found" };
48
+ }
49
+
50
+ const tokenAccess = token
51
+ ? verifyScopedAgentAccessToken(token, {
52
+ resourceKind: DESIGN_AGENT_RESOURCE_KIND,
53
+ resourceId: id,
54
+ }).ok
55
+ : false;
56
+ if (design.visibility !== "public" && !tokenAccess) {
57
+ setResponseStatus(event, 403);
58
+ return { error: "Invalid or expired agent access token" };
59
+ }
60
+
61
+ const snapshot = await buildDesignSnapshot(id, design.data);
62
+ if (snapshot.files.length === 0) {
63
+ setResponseStatus(event, 404);
64
+ return { error: "This design has no files to hand off yet" };
65
+ }
66
+
67
+ return {
68
+ resourceType: "design",
69
+ id: design.id,
70
+ visibility: design.visibility,
71
+ url: `/design/${design.id}`,
72
+ handoff: buildDesignHandoffPayload({
73
+ design,
74
+ files: snapshot.files.map((f) => ({
75
+ filename: f.filename,
76
+ fileType: f.fileType,
77
+ content: f.content,
78
+ })),
79
+ resolvedCssVars: snapshot.resolvedCssVars,
80
+ }),
81
+ appliedTweaks: snapshot.appliedTweaks,
82
+ resolvedCssVars: snapshot.resolvedCssVars,
83
+ };
84
+ });
@@ -0,0 +1,2 @@
1
+ export const DESIGN_AGENT_RESOURCE_KIND = "design:design";
2
+ export const DESIGN_AGENT_CONTEXT_ENDPOINT = "/api/design-agent-context.json";
@@ -1,6 +1,10 @@
1
1
  import { createGetDb } from "@agent-native/core/db";
2
2
  import { registerShareableResource } from "@agent-native/core/sharing";
3
3
 
4
+ import {
5
+ PLAN_AGENT_CONTEXT_ENDPOINT,
6
+ PLAN_AGENT_RESOURCE_KIND,
7
+ } from "../../shared/agent-readable.js";
4
8
  import { resolvePlanAccessContext } from "../lib/local-identity.js";
5
9
  import * as schema from "./schema.js";
6
10
 
@@ -17,6 +21,14 @@ registerShareableResource({
17
21
  (plan as { kind?: string }).kind === "recap"
18
22
  ? `/recaps/${plan.id}`
19
23
  : `/plans/${plan.id}`,
24
+ agentReadable: {
25
+ resourceKind: PLAN_AGENT_RESOURCE_KIND,
26
+ getContextPath: () => PLAN_AGENT_CONTEXT_ENDPOINT,
27
+ getPagePath: (plan) =>
28
+ (plan as { kind?: string }).kind === "recap"
29
+ ? `/recaps/${plan.id}`
30
+ : `/plans/${plan.id}`,
31
+ },
20
32
  getDb,
21
33
  resolveAccessContext: resolvePlanAccessContext,
22
34
  });
@@ -740,6 +740,30 @@ export async function loadPlanBundle(planId: string): Promise<PlanBundle> {
740
740
  if (plan.deletedAt) {
741
741
  throw new ForbiddenError(`Plan ${planId} not found`);
742
742
  }
743
+ return loadPlanBundleForAuthorizedPlan(planId, plan, access.role);
744
+ }
745
+
746
+ export async function loadPlanBundleForAgentAccess(
747
+ planId: string,
748
+ ): Promise<PlanBundle> {
749
+ const db = getDb();
750
+ // guard:allow-unscoped -- callers must verify a plan-scoped agent_access token before using this helper; it intentionally returns a read-only viewer bundle.
751
+ const [plan] = await db
752
+ .select()
753
+ .from(schema.plans)
754
+ .where(eq(schema.plans.id, planId))
755
+ .limit(1);
756
+ if (!plan || plan.deletedAt) {
757
+ throw new ForbiddenError(`Plan ${planId} not found`);
758
+ }
759
+ return loadPlanBundleForAuthorizedPlan(planId, plan, "viewer");
760
+ }
761
+
762
+ async function loadPlanBundleForAuthorizedPlan(
763
+ planId: string,
764
+ plan: typeof schema.plans.$inferSelect,
765
+ role: PlanBundle["access"]["role"],
766
+ ): Promise<PlanBundle> {
743
767
  const db = getDb();
744
768
  const [sectionRows, commentRows, eventRows] = await Promise.all([
745
769
  db
@@ -802,7 +826,7 @@ export async function loadPlanBundle(planId: string): Promise<PlanBundle> {
802
826
  deletedBy: plan.deletedBy,
803
827
  },
804
828
  access: {
805
- role: access.role,
829
+ role,
806
830
  ownerEmail: plan.ownerEmail ?? null,
807
831
  orgId: plan.orgId ?? null,
808
832
  visibility: plan.visibility ?? "private",