@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.
- package/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +25 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/client/frame.ts +55 -0
- package/corpus/core/src/mcp/build-server.ts +30 -4
- package/corpus/core/src/mcp/embed-app.ts +33 -18
- package/corpus/core/src/server/core-routes-plugin.ts +4 -5
- package/corpus/core/src/server/create-server.ts +3 -5
- package/corpus/core/src/server/google-oauth.ts +4 -0
- package/corpus/templates/calendar/actions/create-event.ts +9 -2
- package/corpus/templates/calendar/actions/event-action-helpers.ts +8 -2
- package/corpus/templates/calendar/app/components/calendar/GoogleConnectBanner.tsx +11 -0
- package/corpus/templates/calendar/app/components/layout/Sidebar.tsx +17 -0
- package/corpus/templates/calendar/app/hooks/use-google-auth.ts +70 -39
- package/corpus/templates/calendar/changelog/2026-06-30-google-calendar-connection-errors-now-stay-in-the-app-instea.md +6 -0
- package/corpus/templates/calendar/server/handlers/google-auth.ts +77 -21
- package/corpus/templates/calendar/server/lib/google-calendar.ts +19 -8
- package/corpus/templates/design/actions/apply-design-token-edit.ts +13 -6
- package/corpus/templates/design/actions/import-design-tokens.ts +17 -13
- package/corpus/templates/design/actions/preview-design-token-edit.ts +20 -3
- package/corpus/templates/design/actions/run-design-audit.ts +9 -2
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +8 -1
- package/corpus/templates/design/app/components/design/LayersPanel.tsx +11 -2
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +89 -16
- package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +21 -14
- package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +35 -14
- package/corpus/templates/design/app/components/design/inspector/ImageFillControls.tsx +20 -3
- package/corpus/templates/design/app/pages/DesignEditor.tsx +794 -262
- package/corpus/templates/design/app/pages/VisualEdit.tsx +1 -1
- package/corpus/templates/design/changelog/2026-06-30-design-editor-undo-drag-drop-style-editing-and-visual-edit-s.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-design-studio-canvas-layers-can-be-copied-pasted-and-reparen.md +6 -0
- package/corpus/templates/design/e2e/global-setup.ts +3 -1
- package/corpus/templates/design/e2e/helpers.ts +4 -1
- package/corpus/templates/design/playwright.config.ts +4 -1
- package/corpus/templates/design/shared/resolve-tweaks.ts +29 -6
- package/corpus/templates/plan/actions/consume-plan-feedback.ts +1 -9
- package/corpus/templates/plan/actions/convert-visual-plan-to-prototype.ts +1 -1
- package/corpus/templates/plan/actions/create-plan-design.ts +1 -1
- package/corpus/templates/plan/actions/create-prototype-plan.ts +1 -1
- package/corpus/templates/plan/actions/create-ui-plan.ts +1 -1
- package/corpus/templates/plan/actions/create-visual-plan.ts +36 -19
- package/corpus/templates/plan/actions/create-visual-questions.ts +1 -1
- package/corpus/templates/plan/actions/create-visual-recap.ts +1 -1
- package/corpus/templates/plan/actions/delete-plan-comment.ts +1 -9
- package/corpus/templates/plan/actions/delete-visual-plan.ts +1 -9
- package/corpus/templates/plan/actions/get-plan-feedback.ts +1 -9
- package/corpus/templates/plan/actions/get-visual-plan.ts +1 -9
- package/corpus/templates/plan/actions/import-visual-plan-source.ts +1 -1
- package/corpus/templates/plan/actions/list-visual-plans.ts +1 -9
- package/corpus/templates/plan/actions/reply-to-plan-comment.ts +1 -9
- package/corpus/templates/plan/actions/resolve-plan-comment.ts +1 -9
- package/corpus/templates/plan/actions/update-visual-plan.ts +1 -1
- package/corpus/templates/plan/actions/visual-answer.ts +1 -1
- package/corpus/templates/plan/app/components/layout/Layout.tsx +15 -0
- package/corpus/templates/plan/app/entry.client.tsx +10 -0
- package/corpus/templates/plan/app/global.css +28 -0
- package/corpus/templates/plan/shared/plan-content.ts +10 -6
- package/dist/client/frame.d.ts.map +1 -1
- package/dist/client/frame.js +59 -0
- package/dist/client/frame.js.map +1 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/mcp/build-server.d.ts.map +1 -1
- package/dist/mcp/build-server.js +31 -4
- package/dist/mcp/build-server.js.map +1 -1
- package/dist/mcp/embed-app.d.ts.map +1 -1
- package/dist/mcp/embed-app.js +33 -18
- package/dist/mcp/embed-app.js.map +1 -1
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +3 -3
- package/dist/progress/routes.d.ts +1 -1
- package/dist/resources/handlers.d.ts +3 -3
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/core-routes-plugin.d.ts.map +1 -1
- package/dist/server/core-routes-plugin.js +1 -2
- package/dist/server/core-routes-plugin.js.map +1 -1
- package/dist/server/create-server.d.ts.map +1 -1
- package/dist/server/create-server.js +1 -2
- package/dist/server/create-server.js.map +1 -1
- package/dist/server/google-oauth.d.ts +2 -0
- package/dist/server/google-oauth.d.ts.map +1 -1
- package/dist/server/google-oauth.js +3 -0
- package/dist/server/google-oauth.js.map +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- 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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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
|
|
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(
|
|
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 {
|
|
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
|
-
.
|
|
27
|
-
|
|
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
|
|
35
|
-
//
|
|
36
|
-
.refine(
|
|
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 {
|
|
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 (!
|
|
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 (!
|
|
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
|
-
|
|
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
|
|
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
|
|
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:
|
|
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:
|
|
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 {
|
|
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
|
|
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
|
|
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 =
|
|
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,
|
|
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(
|
|
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(
|
|
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(
|
|
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" ||
|