@checkstack/notification-backend 0.0.4 → 0.1.1
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/CHANGELOG.md +92 -0
- package/package.json +1 -1
- package/src/index.ts +39 -40
- package/src/router.ts +4 -4
- package/src/strategy-service.test.ts +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,97 @@
|
|
|
1
1
|
# @checkstack/notification-backend
|
|
2
2
|
|
|
3
|
+
## 0.1.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [9a27800]
|
|
8
|
+
- @checkstack/queue-api@0.0.6
|
|
9
|
+
- @checkstack/backend-api@0.3.1
|
|
10
|
+
- @checkstack/auth-backend@0.2.1
|
|
11
|
+
|
|
12
|
+
## 0.1.0
|
|
13
|
+
|
|
14
|
+
### Minor Changes
|
|
15
|
+
|
|
16
|
+
- 9faec1f: # Unified AccessRule Terminology Refactoring
|
|
17
|
+
|
|
18
|
+
This release completes a comprehensive terminology refactoring from "permission" to "accessRule" across the entire codebase, establishing a consistent and modern access control vocabulary.
|
|
19
|
+
|
|
20
|
+
## Changes
|
|
21
|
+
|
|
22
|
+
### Core Infrastructure (`@checkstack/common`)
|
|
23
|
+
|
|
24
|
+
- Introduced `AccessRule` interface as the primary access control type
|
|
25
|
+
- Added `accessPair()` helper for creating read/manage access rule pairs
|
|
26
|
+
- Added `access()` builder for individual access rules
|
|
27
|
+
- Replaced `Permission` type with `AccessRule` throughout
|
|
28
|
+
|
|
29
|
+
### API Changes
|
|
30
|
+
|
|
31
|
+
- `env.registerPermissions()` → `env.registerAccessRules()`
|
|
32
|
+
- `meta.permissions` → `meta.access` in RPC contracts
|
|
33
|
+
- `usePermission()` → `useAccess()` in frontend hooks
|
|
34
|
+
- Route `permission:` field → `accessRule:` field
|
|
35
|
+
|
|
36
|
+
### UI Changes
|
|
37
|
+
|
|
38
|
+
- "Roles & Permissions" tab → "Roles & Access Rules"
|
|
39
|
+
- "You don't have permission..." → "You don't have access..."
|
|
40
|
+
- All permission-related UI text updated
|
|
41
|
+
|
|
42
|
+
### Documentation & Templates
|
|
43
|
+
|
|
44
|
+
- Updated 18 documentation files with AccessRule terminology
|
|
45
|
+
- Updated 7 scaffolding templates with `accessPair()` pattern
|
|
46
|
+
- All code examples use new AccessRule API
|
|
47
|
+
|
|
48
|
+
## Migration Guide
|
|
49
|
+
|
|
50
|
+
### Backend Plugins
|
|
51
|
+
|
|
52
|
+
```diff
|
|
53
|
+
- import { permissionList } from "./permissions";
|
|
54
|
+
- env.registerPermissions(permissionList);
|
|
55
|
+
+ import { accessRules } from "./access";
|
|
56
|
+
+ env.registerAccessRules(accessRules);
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### RPC Contracts
|
|
60
|
+
|
|
61
|
+
```diff
|
|
62
|
+
- .meta({ userType: "user", permissions: [permissions.read.id] })
|
|
63
|
+
+ .meta({ userType: "user", access: [access.read] })
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Frontend Hooks
|
|
67
|
+
|
|
68
|
+
```diff
|
|
69
|
+
- const canRead = accessApi.usePermission(permissions.read.id);
|
|
70
|
+
+ const canRead = accessApi.useAccess(access.read);
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Routes
|
|
74
|
+
|
|
75
|
+
```diff
|
|
76
|
+
- permission: permissions.entityRead.id,
|
|
77
|
+
+ accessRule: access.read,
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Patch Changes
|
|
81
|
+
|
|
82
|
+
- Updated dependencies [9faec1f]
|
|
83
|
+
- Updated dependencies [827b286]
|
|
84
|
+
- Updated dependencies [95eeec7]
|
|
85
|
+
- Updated dependencies [f533141]
|
|
86
|
+
- Updated dependencies [aa4a8ab]
|
|
87
|
+
- @checkstack/auth-backend@0.2.0
|
|
88
|
+
- @checkstack/auth-common@0.2.0
|
|
89
|
+
- @checkstack/backend-api@0.3.0
|
|
90
|
+
- @checkstack/common@0.2.0
|
|
91
|
+
- @checkstack/notification-common@0.1.0
|
|
92
|
+
- @checkstack/signal-common@0.1.0
|
|
93
|
+
- @checkstack/queue-api@0.0.5
|
|
94
|
+
|
|
3
95
|
## 0.0.4
|
|
4
96
|
|
|
5
97
|
### Patch Changes
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -8,11 +8,15 @@ import {
|
|
|
8
8
|
type NotificationStrategyRegistry,
|
|
9
9
|
} from "@checkstack/backend-api";
|
|
10
10
|
import {
|
|
11
|
-
|
|
11
|
+
notificationAccessRules,
|
|
12
12
|
pluginMetadata,
|
|
13
13
|
notificationContract,
|
|
14
14
|
} from "@checkstack/notification-common";
|
|
15
|
-
import
|
|
15
|
+
import {
|
|
16
|
+
access,
|
|
17
|
+
type PluginMetadata,
|
|
18
|
+
type AccessRule,
|
|
19
|
+
} from "@checkstack/common";
|
|
16
20
|
import { eq } from "drizzle-orm";
|
|
17
21
|
|
|
18
22
|
import * as schema from "./schema";
|
|
@@ -49,9 +53,8 @@ export const notificationStrategyExtensionPoint =
|
|
|
49
53
|
* Create a new notification strategy registry instance.
|
|
50
54
|
*/
|
|
51
55
|
function createNotificationStrategyRegistry(): NotificationStrategyRegistry & {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
description: string;
|
|
56
|
+
getNewAccessRules: () => Array<{
|
|
57
|
+
accessRule: AccessRule;
|
|
55
58
|
ownerPluginId: string;
|
|
56
59
|
}>;
|
|
57
60
|
} {
|
|
@@ -59,9 +62,8 @@ function createNotificationStrategyRegistry(): NotificationStrategyRegistry & {
|
|
|
59
62
|
string,
|
|
60
63
|
RegisteredNotificationStrategy<unknown, unknown, unknown>
|
|
61
64
|
>();
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
description: string;
|
|
65
|
+
const newAccessRules: Array<{
|
|
66
|
+
accessRule: AccessRule;
|
|
65
67
|
ownerPluginId: string;
|
|
66
68
|
}> = [];
|
|
67
69
|
|
|
@@ -71,7 +73,7 @@ function createNotificationStrategyRegistry(): NotificationStrategyRegistry & {
|
|
|
71
73
|
metadata: PluginMetadata
|
|
72
74
|
): void {
|
|
73
75
|
const qualifiedId = `${metadata.pluginId}.${strategy.id}`;
|
|
74
|
-
const
|
|
76
|
+
const accessRuleId = `${metadata.pluginId}.strategy.${strategy.id}.use`;
|
|
75
77
|
|
|
76
78
|
// Cast to unknown for storage - registry stores heterogeneous strategies
|
|
77
79
|
const registered: RegisteredNotificationStrategy<
|
|
@@ -82,15 +84,18 @@ function createNotificationStrategyRegistry(): NotificationStrategyRegistry & {
|
|
|
82
84
|
...(strategy as NotificationStrategy<unknown, unknown, unknown>),
|
|
83
85
|
qualifiedId,
|
|
84
86
|
ownerPluginId: metadata.pluginId,
|
|
85
|
-
|
|
87
|
+
accessRuleId,
|
|
86
88
|
};
|
|
87
89
|
|
|
88
90
|
strategies.set(qualifiedId, registered);
|
|
89
91
|
|
|
90
|
-
// Track new
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
92
|
+
// Track new access rule for later registration
|
|
93
|
+
newAccessRules.push({
|
|
94
|
+
accessRule: access(
|
|
95
|
+
`strategy.${strategy.id}`,
|
|
96
|
+
"manage",
|
|
97
|
+
`Use ${strategy.displayName} notification channel`
|
|
98
|
+
),
|
|
94
99
|
ownerPluginId: metadata.pluginId,
|
|
95
100
|
});
|
|
96
101
|
},
|
|
@@ -110,15 +115,15 @@ function createNotificationStrategyRegistry(): NotificationStrategyRegistry & {
|
|
|
110
115
|
},
|
|
111
116
|
|
|
112
117
|
getStrategiesForUser(
|
|
113
|
-
|
|
118
|
+
userAccessRules: Set<string>
|
|
114
119
|
): RegisteredNotificationStrategy<unknown, unknown, unknown>[] {
|
|
115
120
|
return [...strategies.values()].filter((s) =>
|
|
116
|
-
|
|
121
|
+
userAccessRules.has(s.accessRuleId)
|
|
117
122
|
);
|
|
118
123
|
},
|
|
119
124
|
|
|
120
|
-
|
|
121
|
-
return
|
|
125
|
+
getNewAccessRules() {
|
|
126
|
+
return newAccessRules;
|
|
122
127
|
},
|
|
123
128
|
};
|
|
124
129
|
}
|
|
@@ -134,8 +139,8 @@ export default createBackendPlugin({
|
|
|
134
139
|
// Create the strategy registry
|
|
135
140
|
const strategyRegistry = createNotificationStrategyRegistry();
|
|
136
141
|
|
|
137
|
-
// Register static
|
|
138
|
-
env.
|
|
142
|
+
// Register static access rules
|
|
143
|
+
env.registerAccessRules(notificationAccessRules);
|
|
139
144
|
|
|
140
145
|
// Register the extension point
|
|
141
146
|
env.registerExtensionPoint(notificationStrategyExtensionPoint, {
|
|
@@ -214,32 +219,26 @@ export default createBackendPlugin({
|
|
|
214
219
|
.join(", ")}`
|
|
215
220
|
);
|
|
216
221
|
|
|
217
|
-
// Emit dynamic
|
|
218
|
-
const
|
|
219
|
-
if (
|
|
222
|
+
// Emit dynamic access rules for strategies
|
|
223
|
+
const newAccessRules = strategyRegistry.getNewAccessRules();
|
|
224
|
+
if (newAccessRules.length > 0) {
|
|
220
225
|
logger.debug(
|
|
221
|
-
`🔐 Registering ${
|
|
226
|
+
`🔐 Registering ${newAccessRules.length} dynamic strategy access rules`
|
|
222
227
|
);
|
|
223
228
|
|
|
224
|
-
// Group
|
|
225
|
-
const byPlugin = new Map<
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
const existing = byPlugin.get(perm.ownerPluginId) ?? [];
|
|
231
|
-
existing.push({ id: perm.id, description: perm.description });
|
|
232
|
-
byPlugin.set(perm.ownerPluginId, existing);
|
|
229
|
+
// Group access rules by owner plugin and emit hooks
|
|
230
|
+
const byPlugin = new Map<string, AccessRule[]>();
|
|
231
|
+
for (const item of newAccessRules) {
|
|
232
|
+
const existing = byPlugin.get(item.ownerPluginId) ?? [];
|
|
233
|
+
existing.push(item.accessRule);
|
|
234
|
+
byPlugin.set(item.ownerPluginId, existing);
|
|
233
235
|
}
|
|
234
236
|
|
|
235
|
-
// Emit
|
|
236
|
-
for (const [ownerPluginId,
|
|
237
|
-
await emitHook(coreHooks.
|
|
237
|
+
// Emit access rules registered hook for each plugin's rules
|
|
238
|
+
for (const [ownerPluginId, accessRules] of byPlugin) {
|
|
239
|
+
await emitHook(coreHooks.accessRulesRegistered, {
|
|
238
240
|
pluginId: ownerPluginId,
|
|
239
|
-
|
|
240
|
-
id: p.id,
|
|
241
|
-
description: p.description,
|
|
242
|
-
})),
|
|
241
|
+
accessRules,
|
|
243
242
|
});
|
|
244
243
|
}
|
|
245
244
|
}
|
package/src/router.ts
CHANGED
|
@@ -85,8 +85,8 @@ function resolveContact({
|
|
|
85
85
|
/**
|
|
86
86
|
* Creates the notification router using contract-based implementation.
|
|
87
87
|
*
|
|
88
|
-
* Auth and
|
|
89
|
-
* based on the contract's meta.userType and meta.
|
|
88
|
+
* Auth and access rules are automatically enforced via autoAuthMiddleware
|
|
89
|
+
* based on the contract's meta.userType and meta.access.
|
|
90
90
|
*/
|
|
91
91
|
export const createNotificationRouter = (
|
|
92
92
|
database: NodePgDatabase<typeof schema>,
|
|
@@ -260,7 +260,7 @@ export const createNotificationRouter = (
|
|
|
260
260
|
return os.router({
|
|
261
261
|
// ==========================================================================
|
|
262
262
|
// USER NOTIFICATION ENDPOINTS
|
|
263
|
-
// Contract meta: userType: "user",
|
|
263
|
+
// Contract meta: userType: "user", accessRules: [notificationRead]
|
|
264
264
|
// ==========================================================================
|
|
265
265
|
|
|
266
266
|
getNotifications: os.getNotifications.handler(
|
|
@@ -364,7 +364,7 @@ export const createNotificationRouter = (
|
|
|
364
364
|
|
|
365
365
|
// ==========================================================================
|
|
366
366
|
// ADMIN SETTINGS ENDPOINTS
|
|
367
|
-
// Contract meta: userType: "user",
|
|
367
|
+
// Contract meta: userType: "user", accessRules: [notificationAdmin]
|
|
368
368
|
// ==========================================================================
|
|
369
369
|
|
|
370
370
|
getRetentionSchema: os.getRetentionSchema.handler(() => {
|
|
@@ -85,7 +85,7 @@ function createMockRegistry(): NotificationStrategyRegistry & {
|
|
|
85
85
|
id: "smtp",
|
|
86
86
|
qualifiedId: "test-plugin.smtp",
|
|
87
87
|
ownerPluginId: "test-plugin",
|
|
88
|
-
|
|
88
|
+
accessRuleId: "test-plugin.strategy.smtp.use",
|
|
89
89
|
displayName: "SMTP Email",
|
|
90
90
|
description: "Send emails via SMTP",
|
|
91
91
|
contactResolution: { type: "auth-email" },
|
|
@@ -97,7 +97,7 @@ function createMockRegistry(): NotificationStrategyRegistry & {
|
|
|
97
97
|
id: "sms",
|
|
98
98
|
qualifiedId: "test-plugin.sms",
|
|
99
99
|
ownerPluginId: "test-plugin",
|
|
100
|
-
|
|
100
|
+
accessRuleId: "test-plugin.strategy.sms.use",
|
|
101
101
|
displayName: "SMS",
|
|
102
102
|
description: "Send SMS messages",
|
|
103
103
|
contactResolution: { type: "user-config", field: "phoneNumber" },
|