-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Add local frontend validation of app manifest fields #5997
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 3 commits
Commits
Show all changes
17 commits
Select commit
Hold shift + click to select a range
9752c2b
base validation of app manifest
lkostrowski a3c5c5d
validation logic
lkostrowski 0c540ab
wip
lkostrowski b11272c
fixes
lkostrowski 5f5a790
Update SDK and add test
lkostrowski 2878ec0
Fix jest config
lkostrowski 02f55f5
tests
lkostrowski 85239f0
tests
lkostrowski 3c1d14d
wip
lkostrowski 083033c
wip
lkostrowski 41304f3
display issues
lkostrowski 3980952
Print errors
lkostrowski 470e51b
test
lkostrowski 69439b1
fix test
lkostrowski 8759420
fix types
lkostrowski 278c2ba
fix import
lkostrowski 5b708a1
Merge branch 'main' into ENG-929-manifest-validation
lkostrowski File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
59 changes: 59 additions & 0 deletions
59
src/extensions/domain/app-extension-manifest-available-mounts.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| import { z } from "zod/index"; | ||
|
|
||
| const PRODUCT_MOUNTS = [ | ||
| "PRODUCT_OVERVIEW_CREATE", | ||
| "PRODUCT_OVERVIEW_MORE_ACTIONS", | ||
| "PRODUCT_DETAILS_MORE_ACTIONS", | ||
| "PRODUCT_DETAILS_WIDGETS", | ||
| ] as const; | ||
|
|
||
| const NAVIGATION_MOUNTS = [ | ||
| "NAVIGATION_CATALOG", | ||
| "NAVIGATION_ORDERS", | ||
| "NAVIGATION_CUSTOMERS", | ||
| "NAVIGATION_DISCOUNTS", | ||
| "NAVIGATION_TRANSLATIONS", | ||
| "NAVIGATION_PAGES", | ||
| ] as const; | ||
|
|
||
| const ORDER_MOUNTS = [ | ||
| "ORDER_DETAILS_MORE_ACTIONS", | ||
| "ORDER_OVERVIEW_CREATE", | ||
| "ORDER_DETAILS_WIDGETS", | ||
| "DRAFT_ORDER_DETAILS_WIDGETS", | ||
| "ORDER_OVERVIEW_MORE_ACTIONS", | ||
| ] as const; | ||
|
|
||
| const CUSTOMER_MOUNTS = [ | ||
| "CUSTOMER_OVERVIEW_CREATE", | ||
| "CUSTOMER_OVERVIEW_MORE_ACTIONS", | ||
| "CUSTOMER_DETAILS_MORE_ACTIONS", | ||
| "CUSTOMER_DETAILS_WIDGETS", | ||
| ] as const; | ||
|
|
||
| const COLLECTION_MOUNTS = ["COLLECTION_DETAILS_WIDGETS"] as const; | ||
|
|
||
| const GIFT_CARD_MOUNTS = ["GIFT_CARD_DETAILS_WIDGETS"] as const; | ||
|
|
||
| const VOUCHER_MOUNTS = ["VOUCHER_DETAILS_WIDGETS"] as const; | ||
|
|
||
| export const ALL_APP_EXTENSION_MOUNTS = z.enum([ | ||
| ...PRODUCT_MOUNTS, | ||
| ...NAVIGATION_MOUNTS, | ||
| ...ORDER_MOUNTS, | ||
| ...COLLECTION_MOUNTS, | ||
| ...GIFT_CARD_MOUNTS, | ||
| ...VOUCHER_MOUNTS, | ||
| ...CUSTOMER_MOUNTS, | ||
| ] as const); | ||
|
|
||
| // Subset of mounts available for WIDGET target | ||
| export const WIDGET_AVAILABLE_MOUNTS = [ | ||
| "ORDER_DETAILS_WIDGETS", | ||
| "PRODUCT_DETAILS_WIDGETS", | ||
| "VOUCHER_DETAILS_WIDGETS", | ||
| "DRAFT_ORDER_DETAILS_WIDGETS", | ||
| "GIFT_CARD_DETAILS_WIDGETS", | ||
| "CUSTOMER_DETAILS_WIDGETS", | ||
| "COLLECTION_DETAILS_WIDGETS", | ||
| ] as const; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| import { z } from "zod/index"; | ||
|
|
||
| const httpMethodSchema = z.enum(["GET", "POST"], { | ||
| // todo errorMap or message? | ||
| errorMap: () => ({ message: "Method must be either GET or POST" }), | ||
| }); | ||
|
|
||
| const newTabTargetOptionsSchema = z.object({ | ||
| method: httpMethodSchema, | ||
| }); | ||
|
|
||
| const widgetTargetOptionsSchema = z.object({ | ||
| method: httpMethodSchema, | ||
| }); | ||
|
|
||
| export const appExtensionManifestOptionsSchema = z | ||
| .object({ | ||
| newTabTarget: newTabTargetOptionsSchema.optional(), | ||
| widgetTarget: widgetTargetOptionsSchema.optional(), | ||
| }) | ||
| .refine( | ||
| data => { | ||
| // Only one of newTabTarget or widgetTarget can be set | ||
| return !(data.newTabTarget && data.widgetTarget); | ||
| }, | ||
| { | ||
| message: "Only one of 'newTabTarget' or 'widgetTarget' can be set.", | ||
| }, | ||
| ); | ||
|
|
||
| export type AppExtensionManifestOptions = z.infer<typeof appExtensionManifestOptionsSchema>; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| import { z } from "zod/index"; | ||
|
|
||
| export const AppExtensionManifestTarget = z.enum(["POPUP", "APP_PAGE", "NEW_TAB", "WIDGET"]); | ||
| export type AppExtensionManifestTarget = z.infer<typeof AppExtensionManifestTarget>; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| import { | ||
| ALL_APP_EXTENSION_MOUNTS, | ||
| WIDGET_AVAILABLE_MOUNTS, | ||
| } from "@dashboard/extensions/domain/app-extension-manifest-available-mounts"; | ||
| import { appExtensionManifestOptionsSchema } from "@dashboard/extensions/domain/app-extension-manifest-options"; | ||
| import { AppExtensionManifestTarget } from "@dashboard/extensions/domain/app-extension-manifest-target"; | ||
| import { z } from "zod/index"; | ||
|
|
||
| export const appExtensionManifest = z | ||
| .object({ | ||
| label: z.string().min(1), | ||
| url: z.string().min(1), | ||
| mount: ALL_APP_EXTENSION_MOUNTS, | ||
| target: AppExtensionManifestTarget.default("POPUP"), | ||
| permissions: z.array(z.string()).optional().default([]), | ||
| options: appExtensionManifestOptionsSchema.optional(), | ||
| }) | ||
| .refine( | ||
| data => { | ||
| // Validate that WIDGET target only uses widget-compatible mounts | ||
| if (data.target === "WIDGET") { | ||
| return WIDGET_AVAILABLE_MOUNTS.includes(data.mount as any); | ||
|
lkostrowski marked this conversation as resolved.
|
||
| } | ||
|
|
||
| return true; | ||
| }, | ||
| { | ||
| message: "Mount is not available for WIDGET target.", | ||
| }, | ||
| ) | ||
| .refine( | ||
| data => { | ||
| // Validate widgetTarget options only on WIDGET target | ||
| if (data.options?.widgetTarget && data.target !== "WIDGET") { | ||
| return false; | ||
| } | ||
|
|
||
| return true; | ||
| }, | ||
| { | ||
| message: "widgetTarget options must be set only on WIDGET target", | ||
| }, | ||
| ) | ||
| .refine( | ||
| data => { | ||
| // Validate newTabTarget options only on NEW_TAB target | ||
| if (data.options?.newTabTarget && data.target !== "NEW_TAB") { | ||
| return false; | ||
| } | ||
|
|
||
| return true; | ||
| }, | ||
| { | ||
| message: "newTabTarget options must be set only on NEW_TAB target", | ||
| }, | ||
| ) | ||
| .refine( | ||
| data => { | ||
| // URL validation based on target | ||
| const url = data.url; | ||
| const target = data.target; | ||
|
|
||
| // Relative URL validation | ||
| if (url.startsWith("/")) { | ||
| // APP_PAGE can use relative URLs | ||
| return target === "APP_PAGE"; | ||
| } | ||
|
|
||
| // APP_PAGE cannot use absolute URLs | ||
| if (target === "APP_PAGE") { | ||
| return false; | ||
| } | ||
|
|
||
| return true; | ||
| }, | ||
| { | ||
| message: "Incorrect relation between extension target and URL fields.", | ||
| }, | ||
| ); | ||
|
|
||
| export type AppExtensionManifest = z.infer<typeof appExtensionManifest>; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| import { appExtensionManifest } from "@dashboard/extensions/domain/app-extension-manifest"; | ||
| import { z } from "zod/index"; | ||
|
|
||
| // For now contains only partial fields, because Saleor is validating manifest anyway. | ||
| // Subset here serves only fields needed for dashboard extensions. | ||
| export const appManifestSchema = z | ||
| .object({ | ||
| appUrl: z.string().optional(), | ||
| permissions: z.array(z.string()).optional().default([]), | ||
| extensions: z.array(appExtensionManifest).optional().default([]), | ||
| }) | ||
| .refine( | ||
| data => { | ||
| // Validate extension URLs require appUrl for certain cases | ||
| return data.extensions.every(ext => { | ||
| if (ext.url.startsWith("/") && ext.target === "NEW_TAB") { | ||
| return !!data.appUrl; | ||
| } | ||
|
|
||
| return true; | ||
| }); | ||
| }, | ||
| { | ||
| message: "To use relative URL, you must specify appUrl.", | ||
| }, | ||
| ) | ||
| .refine( | ||
| data => { | ||
| // Validate extension permissions are subset of app permissions | ||
| return data.extensions.every(ext => { | ||
| return ext.permissions.every(perm => data.permissions.includes(perm)); | ||
| }); | ||
| }, | ||
| { | ||
| message: "Extension permission must be listed in App's permissions.", | ||
| }, | ||
| ); | ||
|
|
||
| export type AppManifest = z.infer<typeof appManifestSchema>; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| import { AppManifest, appManifestSchema } from "@dashboard/extensions/domain/app-manifest"; | ||
| import { ZodIssue } from "zod"; | ||
|
|
||
| //todo test | ||
|
lkostrowski marked this conversation as resolved.
Outdated
|
||
| export class ExtensionManifestValidator { | ||
| validateAppManifest(manifestJson: unknown): AppManifest | { issues: ZodIssue[] } { | ||
| const result = appManifestSchema.safeParse(manifestJson); | ||
|
|
||
| if (result.success) { | ||
| return result.data; | ||
| } | ||
|
|
||
| return { | ||
| issues: result.error.issues, | ||
| }; | ||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.