@checkstack/auth-common 0.0.2

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 ADDED
@@ -0,0 +1,114 @@
1
+ # @checkstack/auth-common
2
+
3
+ ## 0.0.2
4
+
5
+ ### Patch Changes
6
+
7
+ - d20d274: Initial release of all @checkstack packages. Rebranded from Checkmate to Checkstack with new npm organization @checkstack and domain checkstack.dev.
8
+ - Updated dependencies [d20d274]
9
+ - @checkstack/common@0.0.2
10
+
11
+ ## 0.2.1
12
+
13
+ ### Patch Changes
14
+
15
+ - a65e002: Add compile-time type safety for Lucide icon names
16
+
17
+ - Add `LucideIconName` type and `lucideIconSchema` Zod schema to `@checkstack/common`
18
+ - Update backend interfaces (`AuthStrategy`, `NotificationStrategy`, `IntegrationProvider`, `CommandDefinition`) to use `LucideIconName`
19
+ - Update RPC contracts to use `lucideIconSchema` for proper type inference across RPC boundaries
20
+ - Simplify `SocialProviderButton` to use `DynamicIcon` directly (removes 30+ lines of pascalCase conversion)
21
+ - Replace static `iconMap` in `SearchDialog` with `DynamicIcon` for dynamic icon rendering
22
+ - Add fallback handling in `DynamicIcon` when icon name isn't found
23
+ - Fix legacy kebab-case icon names to PascalCase: `mail`→`Mail`, `send`→`Send`, `github`→`Github`, `key-round`→`KeyRound`, `network`→`Network`, `AlertCircle`→`CircleAlert`
24
+
25
+ - Updated dependencies [a65e002]
26
+ - @checkstack/common@0.2.0
27
+
28
+ ## 0.2.0
29
+
30
+ ### Minor Changes
31
+
32
+ - e26c08e: Add password change functionality for credential-authenticated users
33
+
34
+ - Add `changePassword` route to auth-common
35
+ - Create `ChangePasswordPage.tsx` component with password validation, current password verification, and session revocation option
36
+ - Add "Change Password" menu item in User Menu
37
+ - Reuses patterns from existing password reset flow for consistency
38
+
39
+ ## 0.1.0
40
+
41
+ ### Minor Changes
42
+
43
+ - 32f2535: Refactor application role assignment
44
+
45
+ - Removed role selection from the application creation dialog
46
+ - New applications now automatically receive the "Applications" role
47
+ - Roles are now manageable inline in the Applications table (similar to user role management)
48
+ - Added informational alert in create dialog explaining default role behavior
49
+
50
+ - b354ab3: # Strategy Instructions Support & Telegram Notification Plugin
51
+
52
+ ## Strategy Instructions Interface
53
+
54
+ Added `adminInstructions` and `userInstructions` optional fields to the `NotificationStrategy` interface. These allow strategies to export markdown-formatted setup guides that are displayed in the configuration UI:
55
+
56
+ - **`adminInstructions`**: Shown when admins configure platform-wide strategy settings (e.g., how to create API keys)
57
+ - **`userInstructions`**: Shown when users configure their personal settings (e.g., how to link their account)
58
+
59
+ ### Updated Components
60
+
61
+ - `StrategyConfigCard` now accepts an `instructions` prop and renders it before config sections
62
+ - `StrategyCard` passes `adminInstructions` to `StrategyConfigCard`
63
+ - `UserChannelCard` renders `userInstructions` when users need to connect
64
+
65
+ ## New Telegram Notification Plugin
66
+
67
+ Added `@checkstack/notification-telegram-backend` plugin for sending notifications via Telegram:
68
+
69
+ - Uses [grammY](https://grammy.dev/) framework for Telegram Bot API integration
70
+ - Sends messages with MarkdownV2 formatting and inline keyboard buttons for actions
71
+ - Includes comprehensive admin instructions for bot setup via @BotFather
72
+ - Includes user instructions for account linking
73
+
74
+ ### Configuration
75
+
76
+ Admins need to configure a Telegram Bot Token obtained from @BotFather.
77
+
78
+ ### User Linking
79
+
80
+ The strategy uses `contactResolution: { type: "custom" }` for Telegram Login Widget integration. Full frontend integration for the Login Widget is pending future work.
81
+
82
+ ### Patch Changes
83
+
84
+ - ffc28f6: ### Anonymous Role and Public Access
85
+
86
+ Introduces a configurable "anonymous" role for managing permissions available to unauthenticated users.
87
+
88
+ **Core Changes:**
89
+
90
+ - Added `userType: "public"` - endpoints accessible by both authenticated users (with their permissions) and anonymous users (with anonymous role permissions)
91
+ - Renamed `userType: "both"` to `"authenticated"` for clarity
92
+ - Renamed `isDefault` to `isAuthenticatedDefault` on Permission interface
93
+ - Added `isPublicDefault` flag for permissions that should be granted to the anonymous role by default
94
+
95
+ **Backend Infrastructure:**
96
+
97
+ - New `anonymous` system role created during auth-backend initialization
98
+ - New `disabled_public_default_permission` table tracks admin-disabled public defaults
99
+ - `autoAuthMiddleware` now checks anonymous role permissions for unauthenticated public endpoint access
100
+ - `AuthService.getAnonymousPermissions()` with 1-minute caching for performance
101
+ - Anonymous role filtered from `getRoles` endpoint (not assignable to users)
102
+ - Validation prevents assigning anonymous role to users
103
+
104
+ **Catalog Integration:**
105
+
106
+ - `catalog.read` permission now has both `isAuthenticatedDefault` and `isPublicDefault`
107
+ - Read endpoints (`getSystems`, `getGroups`, `getEntities`) now use `userType: "public"`
108
+
109
+ **UI:**
110
+
111
+ - New `PermissionGate` component for conditionally rendering content based on permissions
112
+
113
+ - Updated dependencies [ffc28f6]
114
+ - @checkstack/common@0.1.0
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@checkstack/auth-common",
3
+ "version": "0.0.2",
4
+ "type": "module",
5
+ "exports": {
6
+ ".": {
7
+ "types": "./src/index.ts",
8
+ "import": "./src/index.ts"
9
+ }
10
+ },
11
+ "dependencies": {
12
+ "@checkstack/common": "workspace:*",
13
+ "@orpc/contract": "^1.13.2",
14
+ "zod": "^4.0.0"
15
+ },
16
+ "devDependencies": {
17
+ "@checkstack/tsconfig": "workspace:*",
18
+ "typescript": "^5.7.2",
19
+ "@checkstack/scripts": "workspace:*"
20
+ },
21
+ "scripts": {
22
+ "typecheck": "tsc --noEmit",
23
+ "lint": "bun run lint:code",
24
+ "lint:code": "eslint . --max-warnings 0"
25
+ }
26
+ }
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ export * from "./permissions";
2
+ export * from "./rpc-contract";
3
+ export * from "./plugin-metadata";
4
+ export * from "./schemas";
5
+ export { authRoutes } from "./routes";
@@ -0,0 +1,50 @@
1
+ import type { Permission } from "@checkstack/common";
2
+
3
+ export const permissions = {
4
+ usersRead: {
5
+ id: "users.read",
6
+ description: "List all users",
7
+ },
8
+ usersCreate: {
9
+ id: "users.create",
10
+ description: "Create new users (credential strategy)",
11
+ },
12
+ usersManage: {
13
+ id: "users.manage",
14
+ description: "Delete users",
15
+ },
16
+ rolesRead: {
17
+ id: "roles.read",
18
+ description: "Read and list roles",
19
+ },
20
+ rolesCreate: {
21
+ id: "roles.create",
22
+ description: "Create new roles",
23
+ },
24
+ rolesUpdate: {
25
+ id: "roles.update",
26
+ description: "Update role names and permissions",
27
+ },
28
+ rolesDelete: {
29
+ id: "roles.delete",
30
+ description: "Delete roles",
31
+ },
32
+ rolesManage: {
33
+ id: "roles.manage",
34
+ description: "Assign roles to users",
35
+ },
36
+ strategiesManage: {
37
+ id: "strategies.manage",
38
+ description: "Manage authentication strategies and settings",
39
+ },
40
+ registrationManage: {
41
+ id: "registration.manage",
42
+ description: "Manage user registration settings",
43
+ },
44
+ applicationsManage: {
45
+ id: "applications.manage",
46
+ description: "Create, update, delete, and view external applications",
47
+ },
48
+ } satisfies Record<string, Permission>;
49
+
50
+ export const permissionList = Object.values(permissions);
@@ -0,0 +1,9 @@
1
+ import { definePluginMetadata } from "@checkstack/common";
2
+
3
+ /**
4
+ * Plugin metadata for the auth plugin.
5
+ * Exported from the common package so both backend and frontend can reference it.
6
+ */
7
+ export const pluginMetadata = definePluginMetadata({
8
+ pluginId: "auth",
9
+ });
package/src/routes.ts ADDED
@@ -0,0 +1,14 @@
1
+ import { createRoutes } from "@checkstack/common";
2
+
3
+ /**
4
+ * Route definitions for the auth plugin.
5
+ */
6
+ export const authRoutes = createRoutes("auth", {
7
+ login: "/login",
8
+ register: "/register",
9
+ error: "/error",
10
+ settings: "/settings",
11
+ forgotPassword: "/forgot-password",
12
+ resetPassword: "/reset-password",
13
+ changePassword: "/change-password",
14
+ });
@@ -0,0 +1,395 @@
1
+ import { oc } from "@orpc/contract";
2
+ import {
3
+ createClientDefinition,
4
+ type ProcedureMetadata,
5
+ lucideIconSchema,
6
+ } from "@checkstack/common";
7
+ import { z } from "zod";
8
+ import { permissions } from "./permissions";
9
+ import { pluginMetadata } from "./plugin-metadata";
10
+
11
+ // Base builder with full metadata support
12
+ const _base = oc.$meta<ProcedureMetadata>({});
13
+
14
+ // Zod schemas for return types
15
+ const UserDtoSchema = z.object({
16
+ id: z.string(),
17
+ email: z.string(),
18
+ name: z.string(),
19
+ roles: z.array(z.string()),
20
+ });
21
+
22
+ const RoleDtoSchema = z.object({
23
+ id: z.string(),
24
+ name: z.string(),
25
+ description: z.string().optional().nullable(),
26
+ permissions: z.array(z.string()),
27
+ isSystem: z.boolean().optional(),
28
+ isAssignable: z.boolean().optional(), // False for anonymous role
29
+ });
30
+
31
+ const PermissionDtoSchema = z.object({
32
+ id: z.string(),
33
+ description: z.string().optional(),
34
+ });
35
+
36
+ const StrategyDtoSchema = z.object({
37
+ id: z.string(),
38
+ displayName: z.string(),
39
+ description: z.string().optional(),
40
+ icon: lucideIconSchema.optional(),
41
+ enabled: z.boolean(),
42
+ configVersion: z.number(),
43
+ configSchema: z.record(z.string(), z.unknown()), // JSON Schema representation
44
+ config: z.record(z.string(), z.unknown()).optional(), // VersionedConfig.data (secrets redacted)
45
+ adminInstructions: z.string().optional(), // Markdown instructions for admins
46
+ });
47
+
48
+ const EnabledStrategyDtoSchema = z.object({
49
+ id: z.string(),
50
+ displayName: z.string(),
51
+ description: z.string().optional(),
52
+ type: z.enum(["credential", "social"]),
53
+ icon: lucideIconSchema.optional(),
54
+ requiresManualRegistration: z.boolean(),
55
+ });
56
+
57
+ const RegistrationStatusSchema = z.object({
58
+ allowRegistration: z.boolean(),
59
+ });
60
+
61
+ // ==========================================================================
62
+ // SERVICE-TO-SERVICE SCHEMAS (for auth provider plugins like LDAP)
63
+ // ==========================================================================
64
+
65
+ const FindUserByEmailInputSchema = z.object({
66
+ email: z.string().email(),
67
+ });
68
+
69
+ const FindUserByEmailOutputSchema = z
70
+ .object({
71
+ id: z.string(),
72
+ })
73
+ .optional();
74
+
75
+ const UpsertExternalUserInputSchema = z.object({
76
+ email: z.string().email(),
77
+ name: z.string(),
78
+ providerId: z.string(), // e.g., "ldap"
79
+ accountId: z.string(), // Provider-specific account ID (e.g., LDAP username)
80
+ password: z.string(), // Hashed password
81
+ autoUpdateUser: z.boolean().optional(), // Update existing user's name
82
+ });
83
+
84
+ const UpsertExternalUserOutputSchema = z.object({
85
+ userId: z.string(),
86
+ created: z.boolean(), // true if new user was created, false if existing
87
+ });
88
+
89
+ const CreateSessionInputSchema = z.object({
90
+ userId: z.string(),
91
+ token: z.string(),
92
+ expiresAt: z.coerce.date(),
93
+ });
94
+
95
+ const CreateCredentialUserInputSchema = z.object({
96
+ email: z.string().email(),
97
+ name: z.string().min(1),
98
+ password: z.string(), // Validated against passwordSchema on backend
99
+ });
100
+
101
+ const CreateCredentialUserOutputSchema = z.object({
102
+ userId: z.string(),
103
+ });
104
+
105
+ // Auth RPC Contract with full metadata
106
+ export const authContract = {
107
+ // ==========================================================================
108
+ // ANONYMOUS ENDPOINTS (userType: "anonymous")
109
+ // These can be called without authentication (login/registration pages)
110
+ // ==========================================================================
111
+
112
+ getEnabledStrategies: _base
113
+ .meta({ userType: "anonymous" })
114
+ .output(z.array(EnabledStrategyDtoSchema)),
115
+
116
+ getRegistrationStatus: _base
117
+ .meta({ userType: "anonymous" })
118
+ .output(RegistrationStatusSchema),
119
+
120
+ // ==========================================================================
121
+ // AUTHENTICATED ENDPOINTS (userType: "authenticated" - no specific permission)
122
+ // ==========================================================================
123
+
124
+ permissions: _base
125
+ .meta({ userType: "authenticated" }) // Any authenticated user can check their own permissions
126
+ .output(z.object({ permissions: z.array(z.string()) })),
127
+
128
+ // ==========================================================================
129
+ // USER MANAGEMENT (userType: "user" with permissions)
130
+ // ==========================================================================
131
+
132
+ getUsers: _base
133
+ .meta({ userType: "user", permissions: [permissions.usersRead.id] })
134
+ .output(z.array(UserDtoSchema)),
135
+
136
+ deleteUser: _base
137
+ .meta({ userType: "user", permissions: [permissions.usersManage.id] })
138
+ .input(z.string())
139
+ .output(z.void()),
140
+
141
+ createCredentialUser: _base
142
+ .meta({ userType: "user", permissions: [permissions.usersCreate.id] })
143
+ .input(CreateCredentialUserInputSchema)
144
+ .output(CreateCredentialUserOutputSchema),
145
+
146
+ updateUserRoles: _base
147
+ .meta({ userType: "user", permissions: [permissions.usersManage.id] })
148
+ .input(
149
+ z.object({
150
+ userId: z.string(),
151
+ roles: z.array(z.string()),
152
+ })
153
+ )
154
+ .output(z.void()),
155
+
156
+ // ==========================================================================
157
+ // ROLE MANAGEMENT (userType: "user" with permissions)
158
+ // ==========================================================================
159
+
160
+ getRoles: _base
161
+ .meta({ userType: "user", permissions: [permissions.rolesRead.id] })
162
+ .output(z.array(RoleDtoSchema)),
163
+
164
+ getPermissions: _base
165
+ .meta({ userType: "user", permissions: [permissions.rolesRead.id] })
166
+ .output(z.array(PermissionDtoSchema)),
167
+
168
+ createRole: _base
169
+ .meta({ userType: "user", permissions: [permissions.rolesCreate.id] })
170
+ .input(
171
+ z.object({
172
+ name: z.string(),
173
+ description: z.string().optional(),
174
+ permissions: z.array(z.string()),
175
+ })
176
+ )
177
+ .output(z.void()),
178
+
179
+ updateRole: _base
180
+ .meta({ userType: "user", permissions: [permissions.rolesUpdate.id] })
181
+ .input(
182
+ z.object({
183
+ id: z.string(),
184
+ name: z.string().optional(),
185
+ description: z.string().optional(),
186
+ permissions: z.array(z.string()),
187
+ })
188
+ )
189
+ .output(z.void()),
190
+
191
+ deleteRole: _base
192
+ .meta({ userType: "user", permissions: [permissions.rolesDelete.id] })
193
+ .input(z.string())
194
+ .output(z.void()),
195
+
196
+ // ==========================================================================
197
+ // STRATEGY MANAGEMENT (userType: "user" with permissions)
198
+ // ==========================================================================
199
+
200
+ getStrategies: _base
201
+ .meta({ userType: "user", permissions: [permissions.strategiesManage.id] })
202
+ .output(z.array(StrategyDtoSchema)),
203
+
204
+ updateStrategy: _base
205
+ .meta({ userType: "user", permissions: [permissions.strategiesManage.id] })
206
+ .input(
207
+ z.object({
208
+ id: z.string(),
209
+ enabled: z.boolean(),
210
+ config: z.record(z.string(), z.unknown()).optional(),
211
+ })
212
+ )
213
+ .output(z.object({ success: z.boolean() })),
214
+
215
+ reloadAuth: _base
216
+ .meta({ userType: "user", permissions: [permissions.strategiesManage.id] })
217
+ .output(z.object({ success: z.boolean() })),
218
+
219
+ // ==========================================================================
220
+ // REGISTRATION MANAGEMENT (userType: "user" with permissions)
221
+ // ==========================================================================
222
+
223
+ getRegistrationSchema: _base
224
+ .meta({
225
+ userType: "user",
226
+ permissions: [permissions.registrationManage.id],
227
+ })
228
+ .output(z.record(z.string(), z.unknown())),
229
+
230
+ setRegistrationStatus: _base
231
+ .meta({
232
+ userType: "user",
233
+ permissions: [permissions.registrationManage.id],
234
+ })
235
+ .input(RegistrationStatusSchema)
236
+ .output(z.object({ success: z.boolean() })),
237
+
238
+ // ==========================================================================
239
+ // INTERNAL SERVICE ENDPOINTS (userType: "service")
240
+ // ==========================================================================
241
+
242
+ /**
243
+ * Get permissions assigned to the anonymous role.
244
+ * Used by core AuthService for permission checks on public endpoints.
245
+ */
246
+ getAnonymousPermissions: _base
247
+ .meta({ userType: "service" })
248
+ .output(z.array(z.string())),
249
+
250
+ /**
251
+ * Find a user by email address.
252
+ * Used by external auth providers (e.g., LDAP) to check if a user exists.
253
+ */
254
+ findUserByEmail: _base
255
+ .meta({ userType: "service" })
256
+ .input(FindUserByEmailInputSchema)
257
+ .output(FindUserByEmailOutputSchema),
258
+
259
+ /**
260
+ * Upsert a user from an external auth provider.
261
+ * Creates user + account if new, or updates user if autoUpdateUser is true.
262
+ * Used by external auth providers (e.g., LDAP) to sync users.
263
+ */
264
+ upsertExternalUser: _base
265
+ .meta({ userType: "service" })
266
+ .input(UpsertExternalUserInputSchema)
267
+ .output(UpsertExternalUserOutputSchema),
268
+
269
+ /**
270
+ * Create a session for a user.
271
+ * Used by external auth providers (e.g., LDAP) after successful authentication.
272
+ */
273
+ createSession: _base
274
+ .meta({ userType: "service" })
275
+ .input(CreateSessionInputSchema)
276
+ .output(z.object({ sessionId: z.string() })),
277
+
278
+ /**
279
+ * Get a user by their ID.
280
+ * Used by notification backend to fetch user email for contact resolution.
281
+ */
282
+ getUserById: _base
283
+ .meta({ userType: "service" })
284
+ .input(z.object({ userId: z.string() }))
285
+ .output(
286
+ z
287
+ .object({
288
+ id: z.string(),
289
+ email: z.string(),
290
+ name: z.string().nullable(),
291
+ })
292
+ .optional()
293
+ ),
294
+
295
+ /**
296
+ * Filter a list of user IDs to only those who have a specific permission.
297
+ * Used by SignalService to send signals only to authorized users.
298
+ */
299
+ filterUsersByPermission: _base
300
+ .meta({ userType: "service" })
301
+ .input(
302
+ z.object({
303
+ userIds: z.array(z.string()),
304
+ permission: z.string(), // Fully-qualified permission ID
305
+ })
306
+ )
307
+ .output(z.array(z.string())), // Returns filtered user IDs
308
+
309
+ // ==========================================================================
310
+ // APPLICATION MANAGEMENT (userType: "user" with permissions)
311
+ // External API applications (API keys) with RBAC integration
312
+ // ==========================================================================
313
+
314
+ getApplications: _base
315
+ .meta({
316
+ userType: "user",
317
+ permissions: [permissions.applicationsManage.id],
318
+ })
319
+ .output(
320
+ z.array(
321
+ z.object({
322
+ id: z.string(),
323
+ name: z.string(),
324
+ description: z.string().optional().nullable(),
325
+ roles: z.array(z.string()),
326
+ createdById: z.string(),
327
+ createdAt: z.coerce.date(),
328
+ lastUsedAt: z.coerce.date().optional().nullable(),
329
+ })
330
+ )
331
+ ),
332
+
333
+ createApplication: _base
334
+ .meta({
335
+ userType: "user",
336
+ permissions: [permissions.applicationsManage.id],
337
+ })
338
+ .input(
339
+ z.object({
340
+ name: z.string().min(1).max(100),
341
+ description: z.string().max(500).optional(),
342
+ })
343
+ )
344
+ .output(
345
+ z.object({
346
+ application: z.object({
347
+ id: z.string(),
348
+ name: z.string(),
349
+ description: z.string().optional().nullable(),
350
+ roles: z.array(z.string()),
351
+ createdById: z.string(),
352
+ createdAt: z.coerce.date(),
353
+ }),
354
+ secret: z.string(), // Full secret - ONLY shown once!
355
+ })
356
+ ),
357
+
358
+ updateApplication: _base
359
+ .meta({
360
+ userType: "user",
361
+ permissions: [permissions.applicationsManage.id],
362
+ })
363
+ .input(
364
+ z.object({
365
+ id: z.string(),
366
+ name: z.string().optional(),
367
+ description: z.string().optional().nullable(),
368
+ roles: z.array(z.string()).optional(),
369
+ })
370
+ )
371
+ .output(z.void()),
372
+
373
+ deleteApplication: _base
374
+ .meta({
375
+ userType: "user",
376
+ permissions: [permissions.applicationsManage.id],
377
+ })
378
+ .input(z.string())
379
+ .output(z.void()),
380
+
381
+ regenerateApplicationSecret: _base
382
+ .meta({
383
+ userType: "user",
384
+ permissions: [permissions.applicationsManage.id],
385
+ })
386
+ .input(z.string())
387
+ .output(z.object({ secret: z.string() })), // New secret - shown once
388
+ };
389
+
390
+ // Export contract type
391
+ export type AuthContract = typeof authContract;
392
+
393
+ // Export client definition for type-safe forPlugin usage
394
+ // Use: const client = rpcApi.forPlugin(AuthApi);
395
+ export const AuthApi = createClientDefinition(authContract, pluginMetadata);
package/src/schemas.ts ADDED
@@ -0,0 +1,20 @@
1
+ import { z } from "zod";
2
+
3
+ /**
4
+ * Password validation schema with security requirements.
5
+ * Used for both registration and password reset flows.
6
+ *
7
+ * Requirements:
8
+ * - Minimum 8 characters
9
+ * - At least one uppercase letter
10
+ * - At least one lowercase letter
11
+ * - At least one number
12
+ */
13
+ export const passwordSchema = z
14
+ .string()
15
+ .min(8, "Password must be at least 8 characters")
16
+ .regex(/[A-Z]/, "Password must contain at least one uppercase letter")
17
+ .regex(/[a-z]/, "Password must contain at least one lowercase letter")
18
+ .regex(/[0-9]/, "Password must contain at least one number");
19
+
20
+ export type Password = z.infer<typeof passwordSchema>;
package/tsconfig.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "extends": "@checkstack/tsconfig/common.json",
3
+ "include": [
4
+ "src"
5
+ ]
6
+ }