@appsemble/lang-sdk 0.36.5-test.6 → 0.36.5

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/authorization.js CHANGED
@@ -3,6 +3,7 @@ function checkAppPermissions(acquiredPermissions, requiredPermissions) {
3
3
  const customAppResourcePermissionPattern = /^\$resource:[^:]+:(get|history:get|query|create|delete|patch|update)$/;
4
4
  const customAppOwnResourcePermissionPattern = /^\$resource:[^:]+:own:(get|query|delete|patch|update)$/;
5
5
  const customAppResourceViewPermissionPattern = /^\$resource:[^:]+:(get|query):[^:]+$/;
6
+ const customAppWebhookPermissionPattern = /^\$webhook:[^:]+:invoke$/;
6
7
  return requiredPermissions.every((p) => {
7
8
  if (acquiredPermissions.includes(p)) {
8
9
  return true;
@@ -23,6 +24,14 @@ function checkAppPermissions(acquiredPermissions, requiredPermissions) {
23
24
  acquiredPermissions.includes(`$resource:all:${resourceAction}`) ||
24
25
  acquiredPermissions.includes(`$resource:all:${resourceAction}:${view}`));
25
26
  }
27
+ if (customAppWebhookPermissionPattern.test(p)) {
28
+ // $webhook:all:invoke grants access to all webhooks
29
+ // $webhook:<name>:invoke grants access to specific webhook
30
+ const [, webhookName] = p.split(':');
31
+ return (acquiredPermissions.includes('$webhook:all:invoke') ||
32
+ (webhookName !== 'all' &&
33
+ acquiredPermissions.includes(`$webhook:${webhookName}:invoke`)));
34
+ }
26
35
  });
27
36
  }
28
37
  export function getAppInheritedRoles(appSecurityDefinition, roles, accumulatedRoles = []) {
@@ -113,6 +122,7 @@ export function getAppPossibleGuestPermissions(appDefinition) {
113
122
  `$resource:all:get:${view}`,
114
123
  ]),
115
124
  ]),
125
+ ...Object.keys(appDefinition.webhooks || {}).map((webhookName) => `$webhook:${webhookName}:invoke`),
116
126
  ];
117
127
  }
118
128
  export function getAppPossiblePermissions(appDefinition) {
@@ -13,7 +13,8 @@ export const semver = new RegExp(`^${partialSemver.source}$`);
13
13
  /**
14
14
  * A pattern for extracting a normalized string.
15
15
  */
16
- export const partialNormalized = /([\da-z](?:(?!.*--)[\da-z-]*[\da-z])?)/;
16
+ // eslint-disable-next-line unicorn/better-regex
17
+ export const partialNormalized = /([0-9a-z](?:(?!.*--)[-0-9a-z]*[0-9a-z])?)/;
17
18
  /**
18
19
  * A pattern for exactly matching a lower case hyphen separated string.
19
20
  */
@@ -26,17 +27,20 @@ export const domainPattern = new RegExp(`^(${partialNormalized.source}+\\.)+[a-z
26
27
  /**
27
28
  * A pattern to match a Google Analytics ID or an empty string.
28
29
  */
29
- export const googleAnalyticsIDPattern = /^(|UA-\d{4,10}-\d{1,4}|G-[\dA-Z]{4,})$/;
30
+ // eslint-disable-next-line unicorn/better-regex
31
+ export const googleAnalyticsIDPattern = /^(|UA-[0-9]{4,10}-[0-9]{1,4}|G-[0-9A-Z]{4,})$/;
30
32
  /**
31
33
  * A pattern to match a Meta (Facebook) Pixel ID or an empty string.
32
34
  * Meta Pixel IDs are numeric strings, usually 5–20 digits.
33
35
  */
34
- export const metaPixelIDPattern = /^(|\d{5,20})$/;
36
+ // eslint-disable-next-line unicorn/better-regex
37
+ export const metaPixelIDPattern = /^(|[0-9]{5,20})$/;
35
38
  /**
36
39
  * A pattern to match a MS Clarity Project ID or an empty string.
37
40
  * Clarity IDs are alphanumeric strings, typically 8–20 characters long.
38
41
  */
39
- export const msClarityIDPattern = /^(|[\dA-Za-z]{8,20})$/;
42
+ // eslint-disable-next-line unicorn/better-regex
43
+ export const msClarityIDPattern = /^(|[0-9A-Za-z]{8,20})$/;
40
44
  /**
41
45
  * A pattern which matches a UUID 4.
42
46
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@appsemble/lang-sdk",
3
- "version": "0.36.5-test.6",
3
+ "version": "0.36.5",
4
4
  "description": "Language SDK for Appsemble",
5
5
  "keywords": [
6
6
  "app",
@@ -18,6 +18,7 @@ import { RequestActionDefinition } from '../../schemas/actions/RequestActionDefi
18
18
  import { ShareActionDefinition } from '../../schemas/actions/ShareActionDefinition.js';
19
19
  import { StaticActionDefinition } from '../../schemas/actions/StaticActionDefinition.js';
20
20
  import { ThrowActionDefinition } from '../../schemas/actions/ThrowActionDefinition.js';
21
+ import { WebhookActionDefinition } from '../../schemas/actions/WebhookActionDefinition.js';
21
22
  export const miscellaneousActions = {
22
23
  analytics: AnalyticsActionDefinition,
23
24
  condition: ConditionActionDefinition,
@@ -39,5 +40,6 @@ export const miscellaneousActions = {
39
40
  share: ShareActionDefinition,
40
41
  static: StaticActionDefinition,
41
42
  throw: ThrowActionDefinition,
43
+ webhook: WebhookActionDefinition,
42
44
  };
43
45
  //# sourceMappingURL=miscellaneous.js.map
@@ -91,6 +91,7 @@ export const ActionDefinition = {
91
91
  { $ref: '#/components/schemas/AppMemberPropertiesPatchActionDefinition' },
92
92
  { $ref: '#/components/schemas/AppMemberQueryActionDefinition' },
93
93
  { $ref: '#/components/schemas/AppMemberDeleteActionDefinition' },
94
+ { $ref: '#/components/schemas/WebhookActionDefinition' },
94
95
  ],
95
96
  },
96
97
  ],
@@ -0,0 +1 @@
1
+ export declare const WebhookActionDefinition: import("openapi-types").OpenAPIV3.SchemaObject;
@@ -0,0 +1,32 @@
1
+ import { BaseActionDefinition } from './BaseActionDefinition.js';
2
+ import { extendJSONSchema } from '../utils/extendJSONSchema.js';
3
+ export const WebhookActionDefinition = extendJSONSchema(BaseActionDefinition, {
4
+ type: 'object',
5
+ additionalProperties: false,
6
+ required: ['type', 'name'],
7
+ properties: {
8
+ type: {
9
+ enum: ['webhook'],
10
+ description: `Invoke a webhook defined in the app.
11
+
12
+ The webhook's action is executed server-side, allowing secure operations like
13
+ generating verification codes, sending emails, and modifying resources without
14
+ exposing sensitive logic to the client.
15
+
16
+ The user must have the \`$webhook:<webhookName>:invoke\` permission to call this action.
17
+ `,
18
+ },
19
+ name: {
20
+ type: 'string',
21
+ description: 'The name of the webhook to invoke. Must match a webhook defined in the app.',
22
+ },
23
+ body: {
24
+ $ref: '#/components/schemas/RemapperDefinition',
25
+ description: `A remapper for the webhook body.
26
+
27
+ If not specified, the raw input data is passed to the webhook.
28
+ `,
29
+ },
30
+ },
31
+ });
32
+ //# sourceMappingURL=WebhookActionDefinition.js.map
@@ -66,6 +66,7 @@ export * from './actions/StorageSubtractActionDefinition.js';
66
66
  export * from './actions/StorageUpdateActionDefinition.js';
67
67
  export * from './actions/StorageWriteActionDefinition.js';
68
68
  export * from './actions/ThrowActionDefinition.js';
69
+ export * from './actions/WebhookActionDefinition.js';
69
70
  export * from './AppDefinition.js';
70
71
  export * from './AppLayoutDefinition.js';
71
72
  export * from './AppMemberPropertyDefinition.js';
package/schemas/index.js CHANGED
@@ -66,6 +66,7 @@ export * from './actions/StorageSubtractActionDefinition.js';
66
66
  export * from './actions/StorageUpdateActionDefinition.js';
67
67
  export * from './actions/StorageWriteActionDefinition.js';
68
68
  export * from './actions/ThrowActionDefinition.js';
69
+ export * from './actions/WebhookActionDefinition.js';
69
70
  export * from './AppDefinition.js';
70
71
  export * from './AppLayoutDefinition.js';
71
72
  export * from './AppMemberPropertyDefinition.js';
package/types/action.d.ts CHANGED
@@ -48,5 +48,5 @@ export type ResourcePatchAction = RequestLikeAction<'resource.patch'>;
48
48
  /**
49
49
  * An action that can be called from within a block.
50
50
  */
51
- export type Action = BaseAction<'analytics'> | BaseAction<'app.member.current.patch'> | BaseAction<'app.member.delete'> | BaseAction<'app.member.invite'> | BaseAction<'app.member.login'> | BaseAction<'app.member.logout'> | BaseAction<'app.member.properties.patch'> | BaseAction<'app.member.query'> | BaseAction<'app.member.register'> | BaseAction<'app.member.role.update'> | BaseAction<'condition'> | BaseAction<'controller'> | BaseAction<'csv.parse'> | BaseAction<'dialog.error'> | BaseAction<'dialog.ok'> | BaseAction<'dialog'> | BaseAction<'download'> | BaseAction<'each'> | BaseAction<'email'> | BaseAction<'event'> | BaseAction<'flow.back'> | BaseAction<'flow.cancel'> | BaseAction<'flow.finish'> | BaseAction<'flow.next'> | BaseAction<'flow.to'> | BaseAction<'group.member.create'> | BaseAction<'group.member.delete'> | BaseAction<'group.member.invite'> | BaseAction<'group.member.query'> | BaseAction<'group.member.role.update'> | BaseAction<'group.query'> | BaseAction<'group.selected.update'> | BaseAction<'link.back'> | BaseAction<'link.next'> | BaseAction<'match'> | BaseAction<'message'> | BaseAction<'noop'> | BaseAction<'notify'> | BaseAction<'resource.subscription.status'> | BaseAction<'resource.subscription.subscribe'> | BaseAction<'resource.subscription.toggle'> | BaseAction<'resource.subscription.unsubscribe'> | BaseAction<'share'> | BaseAction<'static'> | BaseAction<'storage.append'> | BaseAction<'storage.delete'> | BaseAction<'storage.read'> | BaseAction<'storage.subtract'> | BaseAction<'storage.update'> | BaseAction<'storage.write'> | BaseAction<'throw'> | LinkAction | LogAction | RequestAction | ResourceCountAction | ResourceCreateAction | ResourceDeleteAction | ResourceDeleteAllAction | ResourceDeleteBulkAction | ResourceGetAction | ResourceHistoryGetAction | ResourcePatchAction | ResourceQueryAction | ResourceUpdateAction | ResourceUpdateGroupAction | ResourceUpdatePositionsAction;
51
+ export type Action = BaseAction<'analytics'> | BaseAction<'app.member.current.patch'> | BaseAction<'app.member.delete'> | BaseAction<'app.member.invite'> | BaseAction<'app.member.login'> | BaseAction<'app.member.logout'> | BaseAction<'app.member.properties.patch'> | BaseAction<'app.member.query'> | BaseAction<'app.member.register'> | BaseAction<'app.member.role.update'> | BaseAction<'condition'> | BaseAction<'controller'> | BaseAction<'csv.parse'> | BaseAction<'dialog.error'> | BaseAction<'dialog.ok'> | BaseAction<'dialog'> | BaseAction<'download'> | BaseAction<'each'> | BaseAction<'email'> | BaseAction<'event'> | BaseAction<'flow.back'> | BaseAction<'flow.cancel'> | BaseAction<'flow.finish'> | BaseAction<'flow.next'> | BaseAction<'flow.to'> | BaseAction<'group.member.create'> | BaseAction<'group.member.delete'> | BaseAction<'group.member.invite'> | BaseAction<'group.member.query'> | BaseAction<'group.member.role.update'> | BaseAction<'group.query'> | BaseAction<'group.selected.update'> | BaseAction<'link.back'> | BaseAction<'link.next'> | BaseAction<'match'> | BaseAction<'message'> | BaseAction<'noop'> | BaseAction<'notify'> | BaseAction<'resource.subscription.status'> | BaseAction<'resource.subscription.subscribe'> | BaseAction<'resource.subscription.toggle'> | BaseAction<'resource.subscription.unsubscribe'> | BaseAction<'share'> | BaseAction<'static'> | BaseAction<'storage.append'> | BaseAction<'storage.delete'> | BaseAction<'storage.read'> | BaseAction<'storage.subtract'> | BaseAction<'storage.update'> | BaseAction<'storage.write'> | BaseAction<'throw'> | BaseAction<'webhook'> | LinkAction | LogAction | RequestAction | ResourceCountAction | ResourceCreateAction | ResourceDeleteAction | ResourceDeleteAllAction | ResourceDeleteBulkAction | ResourceGetAction | ResourceHistoryGetAction | ResourcePatchAction | ResourceQueryAction | ResourceUpdateAction | ResourceUpdateGroupAction | ResourceUpdatePositionsAction;
52
52
  export {};
@@ -3,8 +3,8 @@ import { type BlockDefinition } from './block.js';
3
3
  import { type BulmaColor } from './bulma.js';
4
4
  import { type HTTPMethods } from './http.js';
5
5
  import { type Remapper } from './remapper.js';
6
- export type ActionDefinition = AnalyticsAction | AppMemberCurrentPatchAction | AppMemberDeleteAction | AppMemberInviteAction | AppMemberLoginAction | AppMemberLogoutAction | AppMemberPropertiesPatchAction | AppMemberQueryAction | AppMemberRegisterAction | AppMemberRoleUpdateAction | BaseActionDefinition<'dialog.error'> | BaseActionDefinition<'dialog.ok'> | BaseActionDefinition<'flow.back'> | BaseActionDefinition<'flow.cancel'> | BaseActionDefinition<'flow.finish'> | BaseActionDefinition<'flow.next'> | BaseActionDefinition<'group.query'> | BaseActionDefinition<'link.back'> | BaseActionDefinition<'link.next'> | BaseActionDefinition<'noop'> | BaseActionDefinition<'throw'> | ConditionActionDefinition | ControllerActionDefinition | CsvParserActionDefinition | DialogActionDefinition | DownloadActionDefinition | EachActionDefinition | EmailActionDefinition | EventActionDefinition | FlowToActionDefinition | GroupMemberCreateActionDefinition | GroupMemberDeleteActionDefinition | GroupMemberInviteActionDefinition | GroupMemberQueryActionDefinition | GroupMemberRoleUpdateActionDefinition | GroupSelectedUpdateActionDefinition | LinkActionDefinition | LogActionDefinition | MatchActionDefinition | MessageActionDefinition | NotifyActionDefinition | RequestActionDefinition | ResourceCountActionDefinition | ResourceCreateActionDefinition | ResourceDeleteActionDefinition | ResourceDeleteAllActionDefinition | ResourceDeleteBulkActionDefinition | ResourceGetActionDefinition | ResourceHistoryGetActionDefinition | ResourcePatchActionDefinition | ResourceQueryActionDefinition | ResourceSubscriptionStatusActionDefinition | ResourceSubscriptionSubscribeActionDefinition | ResourceSubscriptionToggleActionDefinition | ResourceSubscriptionUnsubscribeActionDefinition | ResourceUpdateActionDefinition | ResourceUpdateGroupActionDefinition | ResourceUpdatePositionsActionDefinition | ShareActionDefinition | StaticActionDefinition | StorageAppendActionDefinition | StorageDeleteActionDefinition | StorageReadActionDefinition | StorageSubtractActionDefinition | StorageUpdateActionDefinition | StorageWriteActionDefinition;
7
- export type ActionName = 'analytics' | 'app.member.current.patch' | 'app.member.delete' | 'app.member.invite' | 'app.member.login' | 'app.member.logout' | 'app.member.properties.patch' | 'app.member.query' | 'app.member.register' | 'app.member.role.update' | 'condition' | 'controller' | 'csv.parse' | 'dialog.error' | 'dialog.ok' | 'dialog' | 'download' | 'each' | 'email' | 'event' | 'flow.back' | 'flow.cancel' | 'flow.finish' | 'flow.next' | 'flow.to' | 'group.member.create' | 'group.member.delete' | 'group.member.invite' | 'group.member.query' | 'group.member.role.update' | 'group.query' | 'group.selected.update' | 'link.back' | 'link.next' | 'link' | 'log' | 'match' | 'message' | 'noop' | 'notify' | 'request' | 'resource.count' | 'resource.create' | 'resource.delete.all' | 'resource.delete.bulk' | 'resource.delete' | 'resource.get' | 'resource.history.get' | 'resource.patch' | 'resource.query' | 'resource.subscription.status' | 'resource.subscription.subscribe' | 'resource.subscription.toggle' | 'resource.subscription.unsubscribe' | 'resource.update.group' | 'resource.update.positions' | 'resource.update' | 'share' | 'static' | 'storage.append' | 'storage.delete' | 'storage.read' | 'storage.subtract' | 'storage.update' | 'storage.write' | 'throw';
6
+ export type ActionDefinition = AnalyticsAction | AppMemberCurrentPatchAction | AppMemberDeleteAction | AppMemberInviteAction | AppMemberLoginAction | AppMemberLogoutAction | AppMemberPropertiesPatchAction | AppMemberQueryAction | AppMemberRegisterAction | AppMemberRoleUpdateAction | BaseActionDefinition<'dialog.error'> | BaseActionDefinition<'dialog.ok'> | BaseActionDefinition<'flow.back'> | BaseActionDefinition<'flow.cancel'> | BaseActionDefinition<'flow.finish'> | BaseActionDefinition<'flow.next'> | BaseActionDefinition<'group.query'> | BaseActionDefinition<'link.back'> | BaseActionDefinition<'link.next'> | BaseActionDefinition<'noop'> | BaseActionDefinition<'throw'> | ConditionActionDefinition | ControllerActionDefinition | CsvParserActionDefinition | DialogActionDefinition | DownloadActionDefinition | EachActionDefinition | EmailActionDefinition | EventActionDefinition | FlowToActionDefinition | GroupMemberCreateActionDefinition | GroupMemberDeleteActionDefinition | GroupMemberInviteActionDefinition | GroupMemberQueryActionDefinition | GroupMemberRoleUpdateActionDefinition | GroupSelectedUpdateActionDefinition | LinkActionDefinition | LogActionDefinition | MatchActionDefinition | MessageActionDefinition | NotifyActionDefinition | RequestActionDefinition | ResourceCountActionDefinition | ResourceCreateActionDefinition | ResourceDeleteActionDefinition | ResourceDeleteAllActionDefinition | ResourceDeleteBulkActionDefinition | ResourceGetActionDefinition | ResourceHistoryGetActionDefinition | ResourcePatchActionDefinition | ResourceQueryActionDefinition | ResourceSubscriptionStatusActionDefinition | ResourceSubscriptionSubscribeActionDefinition | ResourceSubscriptionToggleActionDefinition | ResourceSubscriptionUnsubscribeActionDefinition | ResourceUpdateActionDefinition | ResourceUpdateGroupActionDefinition | ResourceUpdatePositionsActionDefinition | ShareActionDefinition | StaticActionDefinition | StorageAppendActionDefinition | StorageDeleteActionDefinition | StorageReadActionDefinition | StorageSubtractActionDefinition | StorageUpdateActionDefinition | StorageWriteActionDefinition | WebhookActionDefinition;
7
+ export type ActionName = 'analytics' | 'app.member.current.patch' | 'app.member.delete' | 'app.member.invite' | 'app.member.login' | 'app.member.logout' | 'app.member.properties.patch' | 'app.member.query' | 'app.member.register' | 'app.member.role.update' | 'condition' | 'controller' | 'csv.parse' | 'dialog.error' | 'dialog.ok' | 'dialog' | 'download' | 'each' | 'email' | 'event' | 'flow.back' | 'flow.cancel' | 'flow.finish' | 'flow.next' | 'flow.to' | 'group.member.create' | 'group.member.delete' | 'group.member.invite' | 'group.member.query' | 'group.member.role.update' | 'group.query' | 'group.selected.update' | 'link.back' | 'link.next' | 'link' | 'log' | 'match' | 'message' | 'noop' | 'notify' | 'request' | 'resource.count' | 'resource.create' | 'resource.delete.all' | 'resource.delete.bulk' | 'resource.delete' | 'resource.get' | 'resource.history.get' | 'resource.patch' | 'resource.query' | 'resource.subscription.status' | 'resource.subscription.subscribe' | 'resource.subscription.toggle' | 'resource.subscription.unsubscribe' | 'resource.update.group' | 'resource.update.positions' | 'resource.update' | 'share' | 'static' | 'storage.append' | 'storage.delete' | 'storage.read' | 'storage.subtract' | 'storage.update' | 'storage.write' | 'throw' | 'webhook';
8
8
  export interface BaseActionDefinition<T extends ActionName> {
9
9
  /**
10
10
  * The type of the action.
@@ -638,4 +638,18 @@ export interface CsvParserActionDefinition extends BaseActionDefinition<'csv.par
638
638
  */
639
639
  delimiter?: Remapper;
640
640
  }
641
+ export interface WebhookActionDefinition extends BaseActionDefinition<'webhook'> {
642
+ /**
643
+ * The name of the webhook to invoke.
644
+ *
645
+ * This must be a webhook defined in the app's `webhooks` section.
646
+ */
647
+ name: string;
648
+ /**
649
+ * A remapper for the webhook body.
650
+ *
651
+ * If this isn't specified, the raw input data is passed to the webhook.
652
+ */
653
+ body?: Remapper;
654
+ }
641
655
  export {};
package/types/index.d.ts CHANGED
@@ -54,6 +54,10 @@ export interface ProjectConfig {
54
54
  * ^\d+\.\d+\.\d+$
55
55
  */
56
56
  version: string;
57
+ /**
58
+ * The URL to the source code repository of the project.
59
+ */
60
+ repositoryUrl?: string;
57
61
  }
58
62
  export interface ProjectImplementations {
59
63
  /**
@@ -103,5 +103,9 @@ export declare enum AppPermission {
103
103
  /**
104
104
  * The permission to delete own app resources.
105
105
  */
106
- DeleteOwnResources = "$resource:all:own:delete"
106
+ DeleteOwnResources = "$resource:all:own:delete",
107
+ /**
108
+ * The permission to invoke any app webhook.
109
+ */
110
+ InvokeWebhooks = "$webhook:all:invoke"
107
111
  }
@@ -105,5 +105,9 @@ export var AppPermission;
105
105
  * The permission to delete own app resources.
106
106
  */
107
107
  AppPermission["DeleteOwnResources"] = "$resource:all:own:delete";
108
+ /**
109
+ * The permission to invoke any app webhook.
110
+ */
111
+ AppPermission["InvokeWebhooks"] = "$webhook:all:invoke";
108
112
  })(AppPermission || (AppPermission = {}));
109
113
  //# sourceMappingURL=permission.js.map
@@ -4,7 +4,8 @@ import { type AppRole, type PredefinedAppRole } from './roles.js';
4
4
  export type CustomAppResourcePermission = `$resource:${string}:${ResourceAction}`;
5
5
  export type CustomAppOwnResourcePermission = `$resource:${string}:own:${OwnResourceAction}`;
6
6
  export type CustomAppResourceViewPermission = `$resource:${string}:${ResourceViewAction}:${string}`;
7
- export type CustomAppGuestPermission = AppPermission | CustomAppResourcePermission | CustomAppResourceViewPermission;
7
+ export type CustomAppWebhookPermission = `$webhook:${string}:invoke`;
8
+ export type CustomAppGuestPermission = AppPermission | CustomAppResourcePermission | CustomAppResourceViewPermission | CustomAppWebhookPermission;
8
9
  export type CustomAppPermission = CustomAppGuestPermission | CustomAppOwnResourcePermission;
9
10
  export interface GuestDefinition {
10
11
  permissions?: CustomAppPermission[];
package/validation.js CHANGED
@@ -15,6 +15,8 @@ const allOwnResourcePermissionPattern = /^\$resource:all:own:(get|query|delete|p
15
15
  const ownResourcePermissionPattern = /^\$resource:[^:]+:own:(get|query|delete|patch|update)$/;
16
16
  const allResourceViewPermissionPattern = /^\$resource:all:(get|query):[^:]+$/;
17
17
  const resourceViewPermissionPattern = /^\$resource:[^:]+:(get|query):[^:]+$/;
18
+ const allWebhookPermissionPattern = /^\$webhook:all:invoke$/;
19
+ const webhookPermissionPattern = /^\$webhook:[^:]+:invoke$/;
18
20
  /**
19
21
  * Check whether or not the given link represents a link related to the Appsemble core.
20
22
  *
@@ -558,6 +560,13 @@ function validatePermissions(appDefinition, permissions, inheritedPermissions, p
558
560
  }
559
561
  }
560
562
  }
563
+ if (webhookPermissionPattern.test(permission)) {
564
+ const [, webhookName] = permission.split(':');
565
+ if (webhookName !== 'all' && !appDefinition.webhooks?.[webhookName]) {
566
+ report(appDefinition, `webhook ${webhookName} does not exist in the app's webhooks definition`, [...path, 'permissions', index]);
567
+ return;
568
+ }
569
+ }
561
570
  report(appDefinition, 'invalid permission', [...path, 'permissions', index]);
562
571
  return;
563
572
  }
@@ -1064,7 +1073,8 @@ function validateActions(definition, report) {
1064
1073
  report(action.type, 'missing security definition', [...path, 'resource']);
1065
1074
  return;
1066
1075
  }
1067
- const allPermissions = definition.security.guest?.permissions || [];
1076
+ // IMPORTANT: Create a COPY of the permissions array to avoid mutating the definition
1077
+ const allPermissions = [...(definition.security.guest?.permissions || [])];
1068
1078
  if (definition.security?.cron) {
1069
1079
  allPermissions.push(...(definition.security.cron.permissions ?? []));
1070
1080
  }
@@ -1086,22 +1096,47 @@ function validateActions(definition, report) {
1086
1096
  (permissionResourceAction === resourceAction ||
1087
1097
  (resourceAction === 'count' && permissionResourceAction === 'query')));
1088
1098
  }
1099
+ if (view && resourceViewPermissionPattern.test(permission)) {
1100
+ const [, permissionResourceName, permissionResourceAction, permissionResourceView] = permission.split(':');
1101
+ return (['all', resourceName].includes(permissionResourceName) &&
1102
+ permissionResourceAction === resourceAction &&
1103
+ (!permissionResourceView || permissionResourceView === view));
1104
+ }
1089
1105
  return false;
1090
1106
  })) {
1091
1107
  report(action.type, 'there is no one in the app who has permissions to use this action', [...path, 'resource']);
1092
1108
  return;
1093
1109
  }
1094
- if (view &&
1095
- !allPermissions.some((permission) => {
1096
- if (resourceViewPermissionPattern.test(permission)) {
1097
- const [, permissionResourceName, permissionResourceAction, permissionResourceView] = permission.split(':');
1098
- return (['all', resourceName].includes(permissionResourceName) &&
1099
- permissionResourceAction === resourceAction &&
1100
- (!permissionResourceView || permissionResourceView === view));
1101
- }
1102
- return false;
1103
- })) {
1104
- report(action.type, 'there is no one in the app who has permissions to use this action', [...path, 'resource']);
1110
+ }
1111
+ }
1112
+ if (action.type === 'webhook') {
1113
+ const { name: webhookName } = action;
1114
+ if (!definition.webhooks?.[webhookName]) {
1115
+ report(action.type, `webhook '${webhookName}' does not exist in the app's webhooks definition`, [...path, 'name']);
1116
+ return;
1117
+ }
1118
+ // Check if anyone has permission to invoke this webhook
1119
+ if (definition.security) {
1120
+ // IMPORTANT: Create a COPY of the permissions array to avoid mutating the definition
1121
+ const webhookAllPermissions = [
1122
+ ...(definition.security.guest?.permissions || []),
1123
+ ];
1124
+ if (definition.security?.cron) {
1125
+ webhookAllPermissions.push(...(definition.security.cron.permissions ?? []));
1126
+ }
1127
+ if (definition.security.roles) {
1128
+ const allRolePermissions = getAppRolePermissions(definition.security, Object.keys(definition.security.roles));
1129
+ webhookAllPermissions.push(...allRolePermissions);
1130
+ }
1131
+ if (!webhookAllPermissions.some((permission) => {
1132
+ if (webhookPermissionPattern.test(permission) ||
1133
+ allWebhookPermissionPattern.test(permission)) {
1134
+ const [, permissionWebhookName] = permission.split(':');
1135
+ return permissionWebhookName === 'all' || permissionWebhookName === webhookName;
1136
+ }
1137
+ return false;
1138
+ })) {
1139
+ report(action.type, 'there is no one in the app who has permissions to invoke this webhook', [...path, 'name']);
1105
1140
  return;
1106
1141
  }
1107
1142
  }
@@ -1357,34 +1392,39 @@ export async function validateAppDefinition(definition, getBlockVersions, contro
1357
1392
  result.addError('App definition can not be null');
1358
1393
  return result;
1359
1394
  }
1360
- const blocks = getAppBlocks(definition);
1361
- const blockVersions = await getBlockVersions(blocks);
1362
- const blockVersionMap = new Map();
1363
- for (const version of blockVersions) {
1364
- if (!blockVersionMap.has(version.name)) {
1365
- blockVersionMap.set(version.name, new Map());
1366
- }
1367
- blockVersionMap.get(version.name).set(version.version, version);
1368
- }
1369
1395
  const report = (instance, message, path) => {
1370
1396
  result.errors.push(new ValidationError(message, instance, undefined, path));
1371
1397
  };
1372
1398
  try {
1373
- validateController(definition, controllerImplementations, report);
1374
- validateCronJobs(definition, report);
1375
- validateDefaultPage(definition, report);
1376
- validateHooks(definition, report);
1377
- validateLanguage(definition, report);
1378
- validateResourceReferences(definition, report);
1379
- validateMembersSchema(definition, report);
1380
- validatePhoneNumberDefinition(definition, report);
1381
- validateResourceSchemas(definition, report);
1382
- validateSecurity(definition, report);
1383
- validateBlocks(definition, blockVersionMap, report);
1384
- validateActions(definition, report);
1385
- validateEvents(definition, blockVersionMap, report);
1386
- validateUniquePageNames(definition, report);
1387
- validateGridLayout(definition, report);
1399
+ // IMPORTANT: Create a deep copy to prevent validation logic from mutating the original definition.
1400
+ // The imperative/procedural-style algorithms below (e.g., in validateActions) build up arrays
1401
+ // using .push(), which can accidentally mutate the original definition if passed by reference.
1402
+ // This clone acts as a safeguard against such side effects.
1403
+ const clonedDefinition = structuredClone(definition);
1404
+ const blocks = getAppBlocks(clonedDefinition);
1405
+ const blockVersions = await getBlockVersions(blocks);
1406
+ const blockVersionMap = new Map();
1407
+ for (const version of blockVersions) {
1408
+ if (!blockVersionMap.has(version.name)) {
1409
+ blockVersionMap.set(version.name, new Map());
1410
+ }
1411
+ blockVersionMap.get(version.name).set(version.version, version);
1412
+ }
1413
+ validateController(clonedDefinition, controllerImplementations, report);
1414
+ validateCronJobs(clonedDefinition, report);
1415
+ validateDefaultPage(clonedDefinition, report);
1416
+ validateHooks(clonedDefinition, report);
1417
+ validateLanguage(clonedDefinition, report);
1418
+ validateResourceReferences(clonedDefinition, report);
1419
+ validateMembersSchema(clonedDefinition, report);
1420
+ validatePhoneNumberDefinition(clonedDefinition, report);
1421
+ validateResourceSchemas(clonedDefinition, report);
1422
+ validateSecurity(clonedDefinition, report);
1423
+ validateBlocks(clonedDefinition, blockVersionMap, report);
1424
+ validateActions(clonedDefinition, report);
1425
+ validateEvents(clonedDefinition, blockVersionMap, report);
1426
+ validateUniquePageNames(clonedDefinition, report);
1427
+ validateGridLayout(clonedDefinition, report);
1388
1428
  }
1389
1429
  catch (error) {
1390
1430
  report(null, `Unexpected error: ${error instanceof Error ? error.message : error}`, []);