@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 +10 -0
- package/constants/patterns.js +8 -4
- package/package.json +1 -1
- package/reference-schemas/actions/miscellaneous.js +2 -0
- package/schemas/actions/ActionDefinition.js +1 -0
- package/schemas/actions/WebhookActionDefinition.d.ts +1 -0
- package/schemas/actions/WebhookActionDefinition.js +32 -0
- package/schemas/index.d.ts +1 -0
- package/schemas/index.js +1 -0
- package/types/action.d.ts +1 -1
- package/types/actionDefinition.d.ts +16 -2
- package/types/index.d.ts +4 -0
- package/types/permission.d.ts +5 -1
- package/types/permission.js +4 -0
- package/types/security.d.ts +2 -1
- package/validation.js +76 -36
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) {
|
package/constants/patterns.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
@@ -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
|
package/schemas/index.d.ts
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/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
package/types/permission.d.ts
CHANGED
|
@@ -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
|
}
|
package/types/permission.js
CHANGED
|
@@ -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
|
package/types/security.d.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
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
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
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}`, []);
|