@agent-native/core 0.81.1 → 0.81.3

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 (86) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +25 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/client/frame.ts +55 -0
  5. package/corpus/core/src/mcp/build-server.ts +30 -4
  6. package/corpus/core/src/mcp/embed-app.ts +33 -18
  7. package/corpus/core/src/server/core-routes-plugin.ts +4 -5
  8. package/corpus/core/src/server/create-server.ts +3 -5
  9. package/corpus/core/src/server/google-oauth.ts +4 -0
  10. package/corpus/templates/calendar/actions/create-event.ts +9 -2
  11. package/corpus/templates/calendar/actions/event-action-helpers.ts +8 -2
  12. package/corpus/templates/calendar/app/components/calendar/GoogleConnectBanner.tsx +11 -0
  13. package/corpus/templates/calendar/app/components/layout/Sidebar.tsx +17 -0
  14. package/corpus/templates/calendar/app/hooks/use-google-auth.ts +70 -39
  15. package/corpus/templates/calendar/changelog/2026-06-30-google-calendar-connection-errors-now-stay-in-the-app-instea.md +6 -0
  16. package/corpus/templates/calendar/server/handlers/google-auth.ts +77 -21
  17. package/corpus/templates/calendar/server/lib/google-calendar.ts +19 -8
  18. package/corpus/templates/design/actions/apply-design-token-edit.ts +13 -6
  19. package/corpus/templates/design/actions/import-design-tokens.ts +17 -13
  20. package/corpus/templates/design/actions/preview-design-token-edit.ts +20 -3
  21. package/corpus/templates/design/actions/run-design-audit.ts +9 -2
  22. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +8 -1
  23. package/corpus/templates/design/app/components/design/LayersPanel.tsx +11 -2
  24. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +89 -16
  25. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +21 -14
  26. package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +35 -14
  27. package/corpus/templates/design/app/components/design/inspector/ImageFillControls.tsx +20 -3
  28. package/corpus/templates/design/app/pages/DesignEditor.tsx +794 -262
  29. package/corpus/templates/design/app/pages/VisualEdit.tsx +1 -1
  30. package/corpus/templates/design/changelog/2026-06-30-design-editor-undo-drag-drop-style-editing-and-visual-edit-s.md +6 -0
  31. package/corpus/templates/design/changelog/2026-06-30-design-studio-canvas-layers-can-be-copied-pasted-and-reparen.md +6 -0
  32. package/corpus/templates/design/e2e/global-setup.ts +3 -1
  33. package/corpus/templates/design/e2e/helpers.ts +4 -1
  34. package/corpus/templates/design/playwright.config.ts +4 -1
  35. package/corpus/templates/design/shared/resolve-tweaks.ts +29 -6
  36. package/corpus/templates/plan/actions/consume-plan-feedback.ts +1 -9
  37. package/corpus/templates/plan/actions/convert-visual-plan-to-prototype.ts +1 -1
  38. package/corpus/templates/plan/actions/create-plan-design.ts +1 -1
  39. package/corpus/templates/plan/actions/create-prototype-plan.ts +1 -1
  40. package/corpus/templates/plan/actions/create-ui-plan.ts +1 -1
  41. package/corpus/templates/plan/actions/create-visual-plan.ts +36 -19
  42. package/corpus/templates/plan/actions/create-visual-questions.ts +1 -1
  43. package/corpus/templates/plan/actions/create-visual-recap.ts +1 -1
  44. package/corpus/templates/plan/actions/delete-plan-comment.ts +1 -9
  45. package/corpus/templates/plan/actions/delete-visual-plan.ts +1 -9
  46. package/corpus/templates/plan/actions/get-plan-feedback.ts +1 -9
  47. package/corpus/templates/plan/actions/get-visual-plan.ts +1 -9
  48. package/corpus/templates/plan/actions/import-visual-plan-source.ts +1 -1
  49. package/corpus/templates/plan/actions/list-visual-plans.ts +1 -9
  50. package/corpus/templates/plan/actions/reply-to-plan-comment.ts +1 -9
  51. package/corpus/templates/plan/actions/resolve-plan-comment.ts +1 -9
  52. package/corpus/templates/plan/actions/update-visual-plan.ts +1 -1
  53. package/corpus/templates/plan/actions/visual-answer.ts +1 -1
  54. package/corpus/templates/plan/app/components/layout/Layout.tsx +15 -0
  55. package/corpus/templates/plan/app/entry.client.tsx +10 -0
  56. package/corpus/templates/plan/app/global.css +28 -0
  57. package/corpus/templates/plan/shared/plan-content.ts +10 -6
  58. package/dist/client/frame.d.ts.map +1 -1
  59. package/dist/client/frame.js +59 -0
  60. package/dist/client/frame.js.map +1 -1
  61. package/dist/collab/awareness.d.ts +2 -2
  62. package/dist/collab/awareness.d.ts.map +1 -1
  63. package/dist/collab/routes.d.ts +1 -1
  64. package/dist/mcp/build-server.d.ts.map +1 -1
  65. package/dist/mcp/build-server.js +31 -4
  66. package/dist/mcp/build-server.js.map +1 -1
  67. package/dist/mcp/embed-app.d.ts.map +1 -1
  68. package/dist/mcp/embed-app.js +33 -18
  69. package/dist/mcp/embed-app.js.map +1 -1
  70. package/dist/notifications/routes.d.ts +3 -3
  71. package/dist/observability/routes.d.ts +3 -3
  72. package/dist/progress/routes.d.ts +1 -1
  73. package/dist/resources/handlers.d.ts +3 -3
  74. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  75. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  76. package/dist/server/core-routes-plugin.js +1 -2
  77. package/dist/server/core-routes-plugin.js.map +1 -1
  78. package/dist/server/create-server.d.ts.map +1 -1
  79. package/dist/server/create-server.js +1 -2
  80. package/dist/server/create-server.js.map +1 -1
  81. package/dist/server/google-oauth.d.ts +2 -0
  82. package/dist/server/google-oauth.d.ts.map +1 -1
  83. package/dist/server/google-oauth.js +3 -0
  84. package/dist/server/google-oauth.js.map +1 -1
  85. package/dist/server/transcribe-voice.d.ts +1 -1
  86. package/package.json +1 -1
@@ -43,6 +43,28 @@ const GOOGLE_IDENTITY_SCOPES = [
43
43
  "https://www.googleapis.com/auth/userinfo.profile",
44
44
  ];
45
45
 
46
+ type CalendarOAuthStateOptions = {
47
+ redirectUri: string;
48
+ owner?: string;
49
+ orgId?: string;
50
+ desktop?: boolean;
51
+ addAccount?: boolean;
52
+ app?: string;
53
+ returnUrl?: string;
54
+ flowId?: string;
55
+ };
56
+
57
+ function encodeCalendarOAuthState(options: CalendarOAuthStateOptions): string {
58
+ return encodeOAuthState(options as any);
59
+ }
60
+
61
+ function getCalendarOAuthStateOrgId(
62
+ state: ReturnType<typeof decodeOAuthState>,
63
+ ): string | undefined {
64
+ return (state as ReturnType<typeof decodeOAuthState> & { orgId?: string })
65
+ .orgId;
66
+ }
67
+
46
68
  async function resolveCalendarOAuthCredentials(event: H3Event) {
47
69
  const session = await getSession(event).catch(() => null);
48
70
  const { clientId, clientSecret } = await runWithRequestContext(
@@ -182,6 +204,28 @@ function googleOAuthErrorResponse(
182
204
  return oauthErrorPage(payload.message);
183
205
  }
184
206
 
207
+ function missingCredentialsResponse(
208
+ event: H3Event,
209
+ message: string,
210
+ opts: { desktop?: boolean; flowId?: string; redirect?: boolean } = {},
211
+ ) {
212
+ if (opts.desktop && opts.flowId) {
213
+ setDesktopExchangeError(opts.flowId, {
214
+ message,
215
+ code: "missing_credentials",
216
+ });
217
+ return oauthDesktopExchangePage("Returning to Calendar...");
218
+ }
219
+ if (opts.redirect) {
220
+ return oauthErrorPage(message);
221
+ }
222
+ setResponseStatus(event, 422);
223
+ return {
224
+ error: "missing_credentials",
225
+ message,
226
+ };
227
+ }
228
+
185
229
  export const getGoogleAuthUrl = defineEventHandler(async (event: H3Event) => {
186
230
  try {
187
231
  const q = getQuery(event);
@@ -195,6 +239,7 @@ export const getGoogleAuthUrl = defineEventHandler(async (event: H3Event) => {
195
239
  }
196
240
  const session = await getSession(event);
197
241
  const owner = session?.email;
242
+ const orgId = session?.orgId;
198
243
  const desktop =
199
244
  isElectron(event) || q.desktop === "1" || q.desktop === "true";
200
245
  const flowId = desktop ? (q.flow_id as string) || undefined : undefined;
@@ -204,13 +249,13 @@ export const getGoogleAuthUrl = defineEventHandler(async (event: H3Event) => {
204
249
  : resolveGoogleSignInCredentials();
205
250
 
206
251
  if (!credentials) {
207
- setResponseStatus(event, 422);
208
- return {
209
- error: "missing_credentials",
210
- message: calendarConnect
252
+ return missingCredentialsResponse(
253
+ event,
254
+ calendarConnect
211
255
  ? "Google Calendar OAuth credentials are not configured. Save GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET in settings."
212
256
  : "Google sign-in credentials are not configured. Set GOOGLE_SIGN_IN_CLIENT_ID and GOOGLE_SIGN_IN_CLIENT_SECRET.",
213
- };
257
+ { desktop, flowId, redirect: q.redirect === "1" },
258
+ );
214
259
  }
215
260
 
216
261
  if (calendarConnect && !owner) {
@@ -226,9 +271,10 @@ export const getGoogleAuthUrl = defineEventHandler(async (event: H3Event) => {
226
271
  const returnUrl = requestedReturn !== "/" ? requestedReturn : undefined;
227
272
  // Use the named-arg overload — the positional form previously passed
228
273
  // `flowId` in the `returnUrl` slot, breaking desktop completion.
229
- const state = encodeOAuthState({
274
+ const state = encodeCalendarOAuthState({
230
275
  redirectUri,
231
276
  owner,
277
+ orgId,
232
278
  desktop,
233
279
  addAccount: calendarConnect,
234
280
  app: OAUTH_STATE_APP_ID,
@@ -237,7 +283,7 @@ export const getGoogleAuthUrl = defineEventHandler(async (event: H3Event) => {
237
283
  });
238
284
 
239
285
  const url = calendarConnect
240
- ? await getAuthUrl(undefined, redirectUri, state, owner)
286
+ ? await getAuthUrl(undefined, redirectUri, state, owner, orgId)
241
287
  : `${GOOGLE_AUTH_URL}?${new URLSearchParams({
242
288
  client_id: credentials.clientId,
243
289
  redirect_uri: redirectUri,
@@ -293,6 +339,7 @@ export const handleGoogleCallback = defineEventHandler(
293
339
  }
294
340
 
295
341
  const { redirectUri, owner: stateOwner, addAccount, returnUrl } = state;
342
+ const stateOrgId = getCalendarOAuthStateOrgId(state);
296
343
 
297
344
  // 1. Resolve owner (needs session context, before exchangeCode)
298
345
  const { owner, hasProductionSession } = await resolveOAuthOwner(
@@ -330,7 +377,13 @@ export const handleGoogleCallback = defineEventHandler(
330
377
  }
331
378
 
332
379
  // 2. Exchange code with Google (template-specific Calendar connect)
333
- const email = await exchangeCode(code, undefined, redirectUri, owner);
380
+ const email = await exchangeCode(
381
+ code,
382
+ undefined,
383
+ redirectUri,
384
+ owner,
385
+ stateOrgId,
386
+ );
334
387
 
335
388
  // 3. Create session token (after we have the email)
336
389
  // Skip for add-account flows — adding a second account must not switch
@@ -372,16 +425,18 @@ export const getGoogleAddAccountUrl = defineEventHandler(
372
425
  setResponseStatus(event, 401);
373
426
  return { error: "Must be logged in to add an account" };
374
427
  }
428
+ const q = getQuery(event);
429
+ const desktop =
430
+ isElectron(event) || q.desktop === "1" || q.desktop === "true";
431
+ const flowId = desktop ? (q.flow_id as string) || undefined : undefined;
375
432
  if (!(await resolveCalendarOAuthCredentials(event))) {
376
- setResponseStatus(event, 422);
377
- return {
378
- error: "missing_credentials",
379
- message:
380
- "Google OAuth credentials are not configured. Save GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET in settings.",
381
- };
433
+ return missingCredentialsResponse(
434
+ event,
435
+ "Google OAuth credentials are not configured. Save GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET in settings.",
436
+ { desktop, flowId, redirect: q.redirect === "1" },
437
+ );
382
438
  }
383
439
  try {
384
- const q = getQuery(event);
385
440
  const redirectUri = resolveOAuthRedirectUri(event);
386
441
  if (!redirectUri) {
387
442
  setResponseStatus(event, 400);
@@ -390,12 +445,10 @@ export const getGoogleAddAccountUrl = defineEventHandler(
390
445
  message: "redirect_uri must stay on this app's _agent-native routes.",
391
446
  };
392
447
  }
393
- const desktop =
394
- isElectron(event) || q.desktop === "1" || q.desktop === "true";
395
- const flowId = desktop ? (q.flow_id as string) || undefined : undefined;
396
- const state = encodeOAuthState({
448
+ const state = encodeCalendarOAuthState({
397
449
  redirectUri,
398
450
  owner: session.email,
451
+ orgId: session.orgId,
399
452
  desktop,
400
453
  addAccount: true,
401
454
  app: OAUTH_STATE_APP_ID,
@@ -406,6 +459,7 @@ export const getGoogleAddAccountUrl = defineEventHandler(
406
459
  redirectUri,
407
460
  state,
408
461
  session.email,
462
+ session.orgId,
409
463
  );
410
464
  if (q.redirect === "1") {
411
465
  return oauthRedirectResponse(url);
@@ -449,6 +503,7 @@ export const handleGoogleAddAccountCallback = defineEventHandler(
449
503
  }
450
504
 
451
505
  const { redirectUri, owner: stateOwner } = state;
506
+ const stateOrgId = getCalendarOAuthStateOrgId(state);
452
507
 
453
508
  const ownerEmail = session?.email || stateOwner;
454
509
  if (!ownerEmail) {
@@ -466,6 +521,7 @@ export const handleGoogleAddAccountCallback = defineEventHandler(
466
521
  undefined,
467
522
  redirectUri,
468
523
  ownerEmail,
524
+ session?.orgId ?? stateOrgId,
469
525
  );
470
526
 
471
527
  return oauthCallbackResponse(event, addedEmail, {
@@ -486,7 +542,7 @@ export const handleGoogleAddAccountCallback = defineEventHandler(
486
542
  export const getGoogleStatus = defineEventHandler(async (event: H3Event) => {
487
543
  try {
488
544
  const session = await getSession(event);
489
- return await getAuthStatus(session?.email);
545
+ return await getAuthStatus(session?.email, session?.orgId);
490
546
  } catch (error: any) {
491
547
  setResponseStatus(event, 500);
492
548
  return { error: error.message };
@@ -506,7 +562,7 @@ export const disconnectGoogle = defineEventHandler(async (event: H3Event) => {
506
562
  setResponseStatus(event, 400);
507
563
  return { error: "email is required" };
508
564
  }
509
- const owned = await getAuthStatus(session.email);
565
+ const owned = await getAuthStatus(session.email, session.orgId);
510
566
  const isOwned = owned.accounts.some((a) => a.email === targetEmail);
511
567
  if (!isOwned) {
512
568
  setResponseStatus(event, 403);
@@ -7,6 +7,7 @@ import {
7
7
  import {
8
8
  isOAuthConnected,
9
9
  getOAuthAccounts,
10
+ getRequestOrgId,
10
11
  resolveSecret,
11
12
  runWithRequestContext,
12
13
  } from "@agent-native/core/server";
@@ -61,9 +62,9 @@ function optionalString(value: unknown): string | undefined {
61
62
  return typeof value === "string" && value.length > 0 ? value : undefined;
62
63
  }
63
64
 
64
- async function getOAuth2Credentials(owner?: string) {
65
+ async function getOAuth2Credentials(owner?: string, orgId?: string) {
65
66
  const credentials = (
66
- await resolveGoogleProviderCredentialCandidates(owner)
67
+ await resolveGoogleProviderCredentialCandidates(owner, orgId)
67
68
  )[0];
68
69
  if (!credentials) {
69
70
  throw new Error(
@@ -73,8 +74,11 @@ async function getOAuth2Credentials(owner?: string) {
73
74
  return credentials;
74
75
  }
75
76
 
76
- async function getOAuth2RefreshCredentials(owner?: string) {
77
- const candidates = await resolveGoogleProviderCredentialCandidates(owner);
77
+ async function getOAuth2RefreshCredentials(owner?: string, orgId?: string) {
78
+ const candidates = await resolveGoogleProviderCredentialCandidates(
79
+ owner,
80
+ orgId,
81
+ );
78
82
  if (!candidates.length) {
79
83
  throw new Error(
80
84
  "GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET must be saved in settings",
@@ -97,6 +101,7 @@ async function readCredentialPair(
97
101
 
98
102
  async function resolveGoogleProviderCredentialCandidates(
99
103
  owner?: string,
104
+ orgId?: string,
100
105
  ): Promise<GoogleOAuthCredentials[]> {
101
106
  const resolve = async () => {
102
107
  const primary = await readCredentialPair(
@@ -111,8 +116,9 @@ async function resolveGoogleProviderCredentialCandidates(
111
116
  if (!legacy || legacy.clientId === primary.clientId) return [primary];
112
117
  return [primary, legacy];
113
118
  };
119
+ const resolvedOrgId = orgId ?? getRequestOrgId();
114
120
  return owner
115
- ? runWithRequestContext({ userEmail: owner }, resolve)
121
+ ? runWithRequestContext({ userEmail: owner, orgId: resolvedOrgId }, resolve)
116
122
  : await resolve();
117
123
  }
118
124
 
@@ -359,6 +365,7 @@ async function getValidAccessToken(
359
365
  accountId: string,
360
366
  tokens: GoogleTokens,
361
367
  owner?: string,
368
+ orgId?: string,
362
369
  ): Promise<string> {
363
370
  // Check if token is expired (with 5-minute buffer)
364
371
  if (tokens.expiry_date && tokens.expiry_date < Date.now() + 5 * 60 * 1000) {
@@ -375,7 +382,7 @@ async function getValidAccessToken(
375
382
  for (const {
376
383
  clientId,
377
384
  clientSecret,
378
- } of await getOAuth2RefreshCredentials(owner)) {
385
+ } of await getOAuth2RefreshCredentials(owner, orgId)) {
379
386
  try {
380
387
  const oauth2 = createOAuth2Client(clientId, clientSecret, "");
381
388
  const newTokens = await oauth2.refreshToken(tokens.refresh_token);
@@ -420,8 +427,9 @@ export async function getAuthUrl(
420
427
  redirectUri?: string,
421
428
  state?: string,
422
429
  owner?: string,
430
+ orgId?: string,
423
431
  ): Promise<string> {
424
- const { clientId, clientSecret } = await getOAuth2Credentials(owner);
432
+ const { clientId, clientSecret } = await getOAuth2Credentials(owner, orgId);
425
433
  const uri =
426
434
  redirectUri ||
427
435
  (origin ? `${origin}/_agent-native/google/callback` : undefined);
@@ -439,8 +447,9 @@ export async function exchangeCode(
439
447
  origin?: string,
440
448
  redirectUri?: string,
441
449
  owner?: string,
450
+ orgId?: string,
442
451
  ): Promise<string> {
443
- const { clientId, clientSecret } = await getOAuth2Credentials(owner);
452
+ const { clientId, clientSecret } = await getOAuth2Credentials(owner, orgId);
444
453
  const uri =
445
454
  redirectUri ||
446
455
  (origin ? `${origin}/_agent-native/google/callback` : undefined);
@@ -621,6 +630,7 @@ export async function getPrimaryAccountPhotoUrl(
621
630
 
622
631
  export async function getAuthStatus(
623
632
  forEmail?: string,
633
+ orgId?: string,
624
634
  ): Promise<GoogleAuthStatus> {
625
635
  const oauthAccounts = await getOAuthAccounts("google", forEmail);
626
636
 
@@ -642,6 +652,7 @@ export async function getAuthStatus(
642
652
  account.accountId,
643
653
  tokens,
644
654
  forEmail,
655
+ orgId,
645
656
  );
646
657
  tokenValid = true;
647
658
  photoUrl = await resolveAccountPhotoUrl(accessToken, photoUrl);
@@ -10,7 +10,11 @@ import {
10
10
  INLINE_DEFAULT_CAPABILITIES,
11
11
  hasCapability,
12
12
  } from "../shared/design-source-capabilities.js";
13
- import { resolveTweaksToCssVars } from "../shared/resolve-tweaks.js";
13
+ import {
14
+ isSafeCssTokenValue,
15
+ isSafeCssVarName,
16
+ resolveTweaksToCssVars,
17
+ } from "../shared/resolve-tweaks.js";
14
18
 
15
19
  // ---------------------------------------------------------------------------
16
20
  // Token-edit patch schema (mirrors preview-design-token-edit)
@@ -23,17 +27,20 @@ const tokenEditSchema = z.object({
23
27
  .startsWith("--")
24
28
  // The token name is spliced raw into a `:root { … }` rule, so constrain it
25
29
  // to a safe CSS custom-property ident (leading "--" plus ident chars only).
26
- .regex(
27
- /^--[-_a-zA-Z0-9]+$/,
30
+ .refine(
31
+ isSafeCssVarName,
28
32
  "cssVar must be a valid CSS custom property name (-- followed by letters, digits, hyphens, or underscores).",
29
33
  )
30
34
  .describe("CSS custom property to edit"),
31
35
  /** New value string, e.g. "#3B82F6" or "0.75rem". */
32
36
  value: z
33
37
  .string()
34
- // The value is spliced raw into the `:root { … }` block; reject characters
35
- // that could terminate the rule (`}`) or break out of a `<style>` (`<`).
36
- .refine((v) => !/[}<]/.test(v), 'Token value may not contain "}" or "<".')
38
+ // The value is rendered into CSS custom-property declarations; reject
39
+ // declaration terminators and style-tag breakout characters.
40
+ .refine(
41
+ isSafeCssTokenValue,
42
+ 'Token value may not contain ";", "{", "}", "<", ">", CSS comments, or control characters.',
43
+ )
37
44
  .describe("New value for the token"),
38
45
  });
39
46
 
@@ -17,7 +17,11 @@ import { z } from "zod";
17
17
 
18
18
  import { getDb, schema } from "../server/db/index.js";
19
19
  import "../server/db/index.js"; // ensure registerShareableResource runs
20
- import { resolveTweaksToCssVars } from "../shared/resolve-tweaks.js";
20
+ import {
21
+ isSafeCssTokenValue,
22
+ isSafeCssVarName,
23
+ resolveTweaksToCssVars,
24
+ } from "../shared/resolve-tweaks.js";
21
25
 
22
26
  type ImportedTokenType =
23
27
  | "color"
@@ -174,10 +178,6 @@ function normalizeValue(value: string): string {
174
178
  .trim();
175
179
  }
176
180
 
177
- function isSafeTokenValue(value: string): boolean {
178
- return value.length > 0 && value.length <= 300 && !/[}<]/.test(value);
179
- }
180
-
181
181
  function tokenVar(prefix: string, key: string): string {
182
182
  if (key.startsWith("--")) return key;
183
183
  const stem = toKebab(key);
@@ -224,7 +224,7 @@ function parseNamedLines(
224
224
 
225
225
  const label = match[1].replace(/\*\*/g, "").trim();
226
226
  const value = normalizeValue(match[2]);
227
- if (!isSafeTokenValue(value)) continue;
227
+ if (!isSafeCssTokenValue(value)) continue;
228
228
 
229
229
  const lower = label.toLowerCase();
230
230
  let cssVar: string | null = null;
@@ -261,7 +261,7 @@ function uniqueTokenList(tokens: ImportedDesignToken[]): ImportedDesignToken[] {
261
261
 
262
262
  for (const token of tokens) {
263
263
  if (!/^--[-_a-zA-Z0-9]+$/.test(token.cssVar)) continue;
264
- if (!isSafeTokenValue(token.value)) continue;
264
+ if (!isSafeCssTokenValue(token.value)) continue;
265
265
  const previous = byVar.get(token.cssVar);
266
266
  byVar.set(token.cssVar, token);
267
267
  if (!previous && !/^--color-imported-\d+$/.test(token.cssVar)) {
@@ -404,7 +404,11 @@ export default defineAction({
404
404
  }
405
405
 
406
406
  const { filesAnalyzed, tokens } = tokensFromFiles(importFiles);
407
- if (tokens.length === 0) {
407
+ const safeTokens = tokens.filter(
408
+ (token) =>
409
+ isSafeCssVarName(token.cssVar) && isSafeCssTokenValue(token.value),
410
+ );
411
+ if (safeTokens.length === 0) {
408
412
  return {
409
413
  designId,
410
414
  source,
@@ -452,7 +456,7 @@ export default defineAction({
452
456
  : {};
453
457
  const nextSelections = { ...prevSelections };
454
458
 
455
- for (const token of tokens) {
459
+ for (const token of safeTokens) {
456
460
  const tweakId = cssVarToTweakId.get(token.cssVar);
457
461
  nextSelections[tweakId ?? token.cssVar] = token.value;
458
462
  }
@@ -464,7 +468,7 @@ export default defineAction({
464
468
  ? (prevData.tokenImportSources as Record<string, string>)
465
469
  : {};
466
470
  const tokenImportSources = { ...previousSources };
467
- for (const token of tokens) {
471
+ for (const token of safeTokens) {
468
472
  tokenImportSources[token.cssVar] =
469
473
  token.source === "CSS variables" || token.source === "Colors"
470
474
  ? sourceLabel
@@ -484,7 +488,7 @@ export default defineAction({
484
488
  {
485
489
  source,
486
490
  sourceLabel,
487
- tokenCount: tokens.length,
491
+ tokenCount: safeTokens.length,
488
492
  filesAnalyzed,
489
493
  importedAt: now,
490
494
  },
@@ -500,10 +504,10 @@ export default defineAction({
500
504
  return {
501
505
  designId,
502
506
  source,
503
- importedCount: tokens.length,
507
+ importedCount: safeTokens.length,
504
508
  skippedCount: importFiles.length - filesAnalyzed.length,
505
509
  filesAnalyzed,
506
- tokens,
510
+ tokens: safeTokens,
507
511
  resolvedCssVars: resolveTweaksToCssVars(tweaks, nextSelections),
508
512
  deepLink: designDeepLink(designId),
509
513
  };
@@ -3,7 +3,11 @@ import { resolveAccess } from "@agent-native/core/sharing";
3
3
  import { z } from "zod";
4
4
 
5
5
  import "../server/db/index.js"; // ensure registerShareableResource runs
6
- import { resolveTweaksToCssVars } from "../shared/resolve-tweaks.js";
6
+ import {
7
+ isSafeCssTokenValue,
8
+ isSafeCssVarName,
9
+ resolveTweaksToCssVars,
10
+ } from "../shared/resolve-tweaks.js";
7
11
 
8
12
  // ---------------------------------------------------------------------------
9
13
  // Token-edit patch schema
@@ -11,9 +15,22 @@ import { resolveTweaksToCssVars } from "../shared/resolve-tweaks.js";
11
15
 
12
16
  const tokenEditSchema = z.object({
13
17
  /** The CSS custom property to update, e.g. "--primary-color". */
14
- cssVar: z.string().startsWith("--").describe("CSS custom property to edit"),
18
+ cssVar: z
19
+ .string()
20
+ .startsWith("--")
21
+ .refine(
22
+ isSafeCssVarName,
23
+ "cssVar must be a valid CSS custom property name (-- followed by letters, digits, hyphens, or underscores).",
24
+ )
25
+ .describe("CSS custom property to edit"),
15
26
  /** New value string, e.g. "#3B82F6" or "0.75rem". */
16
- value: z.string().describe("New value for the token"),
27
+ value: z
28
+ .string()
29
+ .refine(
30
+ isSafeCssTokenValue,
31
+ 'Token value may not contain ";", "{", "}", "<", ">", CSS comments, or control characters.',
32
+ )
33
+ .describe("New value for the token"),
17
34
  });
18
35
 
19
36
  // ---------------------------------------------------------------------------
@@ -196,13 +196,20 @@ export function checkTapTargets(html: string): A11yFinding[] {
196
196
  const findings: A11yFinding[] = [];
197
197
  // Heuristic: buttons/links with explicit tiny size classes (h-4, h-5, w-4, w-5, size-4, size-5)
198
198
  // and no explicit larger override or sr-only are flagged.
199
- const interactivePattern = /<(?:button|a)\b[^>]*>/gi;
199
+ const interactivePattern =
200
+ /<(button|a|input|select|textarea)\b[^>]*(?:\/>|>)/gi;
200
201
  const tinyPattern = /\b(?:h|w|size)-[345]\b/;
201
202
  const largePattern = /\b(?:h|w|size)-(?:[6-9]|[1-9]\d)/;
202
203
  const srOnlyPattern = /\bsr-only\b/;
203
204
  let idx = 0;
204
205
  for (const m of html.matchAll(interactivePattern)) {
205
206
  const tag = m[0];
207
+ const tagName = (m[1] ?? "button").toLowerCase();
208
+ const typeMatch = tag.match(/\btype\s*=\s*(?:"([^"]*?)"|'([^']*?)')/i);
209
+ if (tagName === "input") {
210
+ const type = (typeMatch?.[1] ?? typeMatch?.[2] ?? "text").toLowerCase();
211
+ if (type === "hidden") continue;
212
+ }
206
213
  if (
207
214
  tinyPattern.test(tag) &&
208
215
  !largePattern.test(tag) &&
@@ -217,7 +224,7 @@ export function checkTapTargets(html: string): A11yFinding[] {
217
224
  detail:
218
225
  "Minimum recommended tap target size is 44×44 px (WCAG 2.5.5). Consider increasing padding or size.",
219
226
  nodeId: extractNodeId(tag),
220
- selector: extractSelector(tag, "button"),
227
+ selector: extractSelector(tag, tagName),
221
228
  wcag: "2.5.5",
222
229
  fixAvailable: true,
223
230
  });
@@ -374,6 +374,8 @@ interface DesignCanvasProps {
374
374
  designTitle?: string;
375
375
  /** Stable id for comment pins, usually scoped to the active screen. */
376
376
  commentContextId?: string;
377
+ /** Stable id of the screen containing this canvas when rendered in overview. */
378
+ screenId?: string;
377
379
  /** Human-readable label for comment-pin prompts. */
378
380
  commentContextLabel?: string;
379
381
  /**
@@ -506,6 +508,7 @@ export function DesignCanvas({
506
508
  designId,
507
509
  designTitle,
508
510
  commentContextId,
511
+ screenId,
509
512
  commentContextLabel,
510
513
  onPrototypeNavigate,
511
514
  motionTracks,
@@ -1098,7 +1101,10 @@ export function DesignCanvas({
1098
1101
  const beginTextEdit = useCallback((nodeId: string) => {
1099
1102
  const iframe = iframeRef.current;
1100
1103
  if (!iframe?.contentWindow || !nodeId) return;
1101
- iframe.contentWindow.postMessage({ type: "begin-text-edit", nodeId }, "*");
1104
+ iframe.contentWindow.postMessage(
1105
+ { type: "begin-text-edit", nodeId, force: true },
1106
+ "*",
1107
+ );
1102
1108
  }, []);
1103
1109
 
1104
1110
  const sendStyleChange = useCallback(
@@ -1342,6 +1348,7 @@ export function DesignCanvas({
1342
1348
  : "allow-scripts allow-popups allow-popups-to-escape-sandbox allow-same-origin"
1343
1349
  }
1344
1350
  data-design-preview-iframe
1351
+ data-screen-iframe-id={screenId ?? undefined}
1345
1352
  data-design-source-type={
1346
1353
  sourceType ??
1347
1354
  (externalPreviewUrl
@@ -1000,7 +1000,11 @@ function LayerRow({
1000
1000
  const hideable = node.hideable !== false && Boolean(onToggleHidden);
1001
1001
  const dragEligible = selectable && !node.locked && !node.hidden;
1002
1002
  const draggable = dragEligible && Boolean(onMoveLayer);
1003
- const canDropInside = layerCanDropInside(node, hasChildren);
1003
+ const canDropInside = layerCanDropInside(
1004
+ node,
1005
+ hasChildren,
1006
+ canAcceptChildren,
1007
+ );
1004
1008
  const activeDrop =
1005
1009
  dropIndicator?.targetId === node.id ? dropIndicator.placement : null;
1006
1010
  // Tracks whether the user pressed Escape to cancel rename so that the
@@ -1868,9 +1872,14 @@ function ElementLayerGlyph({ className }: { className?: string }) {
1868
1872
  );
1869
1873
  }
1870
1874
 
1871
- function layerCanDropInside(node: LayersPanelNode, hasChildren: boolean) {
1875
+ function layerCanDropInside(
1876
+ node: LayersPanelNode,
1877
+ hasChildren: boolean,
1878
+ canAcceptChildren: boolean,
1879
+ ) {
1872
1880
  return (
1873
1881
  hasChildren ||
1882
+ canAcceptChildren ||
1874
1883
  Boolean(node.layout?.isFlexContainer || node.layout?.isGridContainer) ||
1875
1884
  node.type === "file" ||
1876
1885
  node.type === "screen" ||