canopycms-auth-dev 0.0.0

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/README.md ADDED
@@ -0,0 +1,324 @@
1
+ # canopycms-auth-dev
2
+
3
+ Development-only authentication provider for CanopyCMS that allows testing the CMS without setting up a real auth provider like Clerk.
4
+
5
+ **⚠️ Development Only**: This package throws an error if `NODE_ENV === 'production'`. Never use it in production.
6
+
7
+ ## Features
8
+
9
+ - **Zero configuration**: Works out of the box with 5 default users
10
+ - **UI-based user switching**: Click avatar to switch between users in the editor
11
+ - **Test compatibility**: Supports `X-Test-User` header for Playwright tests
12
+ - **Group-based permissions**: Test team-based access control with external groups
13
+ - **Internal groups**: Configure reserved groups (Admins, Reviewers) via `.canopycms/groups.json`
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ npm install canopycms-auth-dev
19
+ ```
20
+
21
+ ## Quick Start
22
+
23
+ ### 1. Server-side Setup
24
+
25
+ Replace your auth plugin in the API route:
26
+
27
+ ```ts
28
+ // app/lib/canopy.ts
29
+ import { createNextCanopyContext } from 'canopycms-next'
30
+ import { createDevAuthPlugin } from 'canopycms-auth-dev'
31
+ import configBundle from '../../canopycms.config'
32
+
33
+ const { handler } = createNextCanopyContext({
34
+ config: configBundle.server,
35
+ authPlugin: createDevAuthPlugin(),
36
+ })
37
+
38
+ export { handler }
39
+ ```
40
+
41
+ ### 2. Client-side Setup
42
+
43
+ Use the dev auth hook in your edit page:
44
+
45
+ ```tsx
46
+ // app/edit/page.tsx
47
+ 'use client'
48
+
49
+ import { useDevAuthConfig } from 'canopycms-auth-dev/client'
50
+ import { NextCanopyEditorPage } from 'canopycms-next/client'
51
+ import config from '../../canopycms.config'
52
+
53
+ export default function EditPage() {
54
+ const devAuth = useDevAuthConfig()
55
+ const clientConfig = config.client(devAuth)
56
+ const EditorPage = NextCanopyEditorPage(clientConfig)
57
+ return <EditorPage />
58
+ }
59
+ ```
60
+
61
+ ### 3. Configure Bootstrap Admins
62
+
63
+ Add admin1's user ID to your config:
64
+
65
+ ```ts
66
+ // canopycms.config.ts
67
+ export default defineCanopyConfig({
68
+ // ... other config
69
+ bootstrapAdminIds: ['dev_admin_3xY6zW1qR5'], // admin1
70
+ })
71
+ ```
72
+
73
+ ### 4. Create Internal Groups (Optional)
74
+
75
+ For Reviewers and other internal groups, create `.canopycms/groups.json`:
76
+
77
+ ```json
78
+ {
79
+ "version": 1,
80
+ "updatedAt": "2024-01-01T00:00:00.000Z",
81
+ "updatedBy": "canopycms-system",
82
+ "groups": [
83
+ {
84
+ "id": "Reviewers",
85
+ "name": "Reviewers",
86
+ "description": "Users who can review and approve branches",
87
+ "members": ["dev_reviewer_9aB4cD2eF7"]
88
+ }
89
+ ]
90
+ }
91
+ ```
92
+
93
+ ## Default Users
94
+
95
+ The plugin comes with 5 pre-configured users:
96
+
97
+ | User | ID | Email | External Groups |
98
+ | ------------ | ------------------------- | ----------------------- | ---------------------- |
99
+ | User One | `dev_user1_2nK8mP4xL9` | user1@localhost.dev | team-a, team-b |
100
+ | User Two | `dev_user2_7qR3tY6wN2` | user2@localhost.dev | team-b |
101
+ | User Three | `dev_user3_5vS1pM8kJ4` | user3@localhost.dev | team-c |
102
+ | Reviewer One | `dev_reviewer_9aB4cD2eF7` | reviewer1@localhost.dev | team-a |
103
+ | Admin One | `dev_admin_3xY6zW1qR5` | admin1@localhost.dev | team-a, team-b, team-c |
104
+
105
+ **Note**: admin1 gets the 'Admins' group via `bootstrapAdminIds` config, not from external groups.
106
+
107
+ ## User Switching
108
+
109
+ ### In the UI
110
+
111
+ 1. Open the editor (`/edit`)
112
+ 2. Click the avatar button in the top-right
113
+ 3. Select a user from the modal
114
+ 4. Page reloads with the new user
115
+
116
+ ### In Tests (Playwright)
117
+
118
+ Send the `X-Test-User` header with one of these values:
119
+
120
+ ```ts
121
+ // In your test
122
+ await page.setExtraHTTPHeaders({
123
+ 'X-Test-User': 'admin', // Maps to admin1 (dev_admin_3xY6zW1qR5)
124
+ })
125
+ ```
126
+
127
+ Test user mappings:
128
+
129
+ - `admin` → admin1 (dev_admin_3xY6zW1qR5)
130
+ - `editor` → user1 (dev_user1_2nK8mP4xL9)
131
+ - `viewer` → user2 (dev_user2_7qR3tY6wN2)
132
+ - `reviewer` → reviewer1 (dev_reviewer_9aB4cD2eF7)
133
+
134
+ ## Configuration
135
+
136
+ Customize users and groups:
137
+
138
+ ```ts
139
+ import { createDevAuthPlugin } from 'canopycms-auth-dev'
140
+
141
+ const authPlugin = createDevAuthPlugin({
142
+ defaultUserId: 'dev_user1_2nK8mP4xL9', // user1
143
+ users: [
144
+ {
145
+ userId: 'custom_user1',
146
+ name: 'Custom User',
147
+ email: 'custom@example.com',
148
+ externalGroups: ['team-x'],
149
+ },
150
+ ],
151
+ groups: [{ id: 'team-x', name: 'Team X', description: 'Custom team' }],
152
+ })
153
+ ```
154
+
155
+ ## How It Works
156
+
157
+ ### Authentication Flow
158
+
159
+ 1. **Request arrives** → Plugin checks for user identifier
160
+ 2. **Priority order**:
161
+ - `X-Test-User` header (for tests)
162
+ - `x-dev-user-id` header (custom)
163
+ - `canopy-dev-user` cookie (from UI)
164
+ - Default user (user1)
165
+ 3. **User lookup** → Find user in config
166
+ 4. **Groups assigned**:
167
+ - External groups (from auth plugin)
168
+ - Bootstrap admin groups (from config)
169
+ - Internal groups (from `.canopycms/groups.json`)
170
+
171
+ ### Group Types
172
+
173
+ - **External groups**: Returned by auth plugin (e.g., team-a, team-b, team-c)
174
+ - **Bootstrap admins**: Added automatically from `bootstrapAdminIds` config
175
+ - **Internal groups**: Loaded from `.canopycms/groups.json` (managed by admins via UI)
176
+
177
+ ### Reserved Groups
178
+
179
+ - **`Admins`**: Full access to all CMS operations
180
+ - **`Reviewers`**: Can review branches, request changes, approve PRs
181
+
182
+ ## API
183
+
184
+ ### `createDevAuthPlugin(config?)`
185
+
186
+ Factory function that creates a dev auth plugin.
187
+
188
+ **Parameters:**
189
+
190
+ - `config.users?` - Custom user list
191
+ - `config.groups?` - Custom group list
192
+ - `config.defaultUserId?` - Default user when none specified
193
+
194
+ **Returns:** `AuthPlugin`
195
+
196
+ ### `useDevAuthConfig()`
197
+
198
+ React hook that provides editor configuration with user switcher.
199
+
200
+ **Returns:** `Pick<CanopyClientConfig, 'editor'>`
201
+
202
+ ### Exports
203
+
204
+ ```ts
205
+ // Server-side
206
+ export { createDevAuthPlugin, DevAuthPlugin, DEFAULT_USERS, DEFAULT_GROUPS }
207
+ export type { DevAuthConfig, DevUser, DevGroup }
208
+
209
+ // Client-side (import from 'canopycms-auth-dev/client')
210
+ export { useDevAuthConfig }
211
+ ```
212
+
213
+ ## Switching Between Auth Providers
214
+
215
+ You can configure your app to switch between dev auth and production auth (like Clerk) using environment variables:
216
+
217
+ ### Server-side (app/lib/canopy.ts)
218
+
219
+ ```ts
220
+ import { createNextCanopyContext } from 'canopycms-next'
221
+ import { createClerkAuthPlugin } from 'canopycms-auth-clerk'
222
+ import { createDevAuthPlugin } from 'canopycms-auth-dev'
223
+ import type { AuthPlugin } from 'canopycms/auth'
224
+ import config from '../../canopycms.config'
225
+
226
+ function getAuthPlugin(): AuthPlugin {
227
+ const authMode = process.env.CANOPY_AUTH_MODE || 'dev'
228
+
229
+ if (authMode === 'dev') {
230
+ return createDevAuthPlugin()
231
+ }
232
+
233
+ if (authMode === 'clerk') {
234
+ return createClerkAuthPlugin({
235
+ useOrganizationsAsGroups: true,
236
+ })
237
+ }
238
+
239
+ throw new Error(`Invalid CANOPY_AUTH_MODE: "${authMode}". Must be "dev" or "clerk".`)
240
+ }
241
+
242
+ const canopyContext = createNextCanopyContext({
243
+ config: config.server,
244
+ authPlugin: getAuthPlugin(),
245
+ })
246
+
247
+ export const getCanopy = canopyContext.getCanopy
248
+ export const handler = canopyContext.handler
249
+ ```
250
+
251
+ ### Client-side (app/edit/page.tsx)
252
+
253
+ ```tsx
254
+ 'use client'
255
+
256
+ import { useClerkAuthConfig } from 'canopycms-auth-clerk/client'
257
+ import { useDevAuthConfig } from 'canopycms-auth-dev/client'
258
+ import { NextCanopyEditorPage } from 'canopycms-next/client'
259
+ import config from '../../canopycms.config'
260
+
261
+ function useAuthConfig() {
262
+ const authMode = process.env.NEXT_PUBLIC_CANOPY_AUTH_MODE || 'dev'
263
+
264
+ if (authMode === 'dev') {
265
+ return useDevAuthConfig()
266
+ }
267
+
268
+ if (authMode === 'clerk') {
269
+ return useClerkAuthConfig()
270
+ }
271
+
272
+ throw new Error(`Invalid NEXT_PUBLIC_CANOPY_AUTH_MODE: "${authMode}". Must be "dev" or "clerk".`)
273
+ }
274
+
275
+ export default function EditPage() {
276
+ const authConfig = useAuthConfig()
277
+ const clientConfig = config.client(authConfig)
278
+ const EditorPage = NextCanopyEditorPage(clientConfig)
279
+ return <EditorPage />
280
+ }
281
+ ```
282
+
283
+ ### Environment Configuration
284
+
285
+ **Default: Dev auth is enabled by default** (no configuration needed)
286
+
287
+ To switch to Clerk, create `.env.local`:
288
+
289
+ ```bash
290
+ # Use Clerk authentication
291
+ CANOPY_AUTH_MODE=clerk
292
+ NEXT_PUBLIC_CANOPY_AUTH_MODE=clerk
293
+
294
+ # Clerk configuration
295
+ NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=your_key
296
+ CLERK_SECRET_KEY=your_secret
297
+
298
+ # Bootstrap admin (use your Clerk user ID)
299
+ CANOPY_BOOTSTRAP_ADMIN_IDS=user_xxxxxxxxxxxxx
300
+ ```
301
+
302
+ To switch back to dev auth, just remove `.env.local` or set the mode to `dev`.
303
+
304
+ **Optional dev auth configuration** (`.env.local`):
305
+
306
+ ```bash
307
+ # Bootstrap admin for dev mode
308
+ CANOPY_BOOTSTRAP_ADMIN_IDS=dev_admin_3xY6zW1qR5
309
+ ```
310
+
311
+ ### Benefits
312
+
313
+ - **No code changes**: Switch auth modes by changing environment variables
314
+ - **Team flexibility**: Developers can use dev auth locally while staging/production uses Clerk
315
+ - **Easy testing**: Quickly test features without auth provider setup
316
+ - **Clean separation**: Same codebase works with multiple auth providers
317
+
318
+ ## Production Safety
319
+
320
+ ⚠️ **WARNING**: This plugin is for development and testing only. Do not use in production environments.
321
+
322
+ ## License
323
+
324
+ MIT
@@ -0,0 +1,4 @@
1
+ /**
2
+ * User switcher button component that shows current user avatar and opens modal
3
+ */
4
+ export declare function UserSwitcherButton(): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,23 @@
1
+ 'use client';
2
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useState, useEffect } from 'react';
4
+ import { ActionIcon, Avatar } from '@mantine/core';
5
+ import { UserSwitcherModal } from './UserSwitcherModal';
6
+ import { DEFAULT_USERS } from './dev-plugin';
7
+ import { getDevUserCookie, DEFAULT_USER_ID } from './cookie-utils';
8
+ /**
9
+ * User switcher button component that shows current user avatar and opens modal
10
+ */
11
+ export function UserSwitcherButton() {
12
+ const [opened, setOpened] = useState(false);
13
+ const [mounted, setMounted] = useState(false);
14
+ // Only read cookie after mount to avoid hydration mismatch
15
+ useEffect(() => {
16
+ setMounted(true);
17
+ }, []);
18
+ // Read current user from cookie (only on client)
19
+ const currentUserId = mounted ? (getDevUserCookie() ?? DEFAULT_USER_ID) : DEFAULT_USER_ID;
20
+ const currentUser = DEFAULT_USERS.find((u) => u.userId === currentUserId);
21
+ return (_jsxs(_Fragment, { children: [_jsx(ActionIcon, { variant: "subtle", size: "lg", radius: "md", onClick: () => setOpened(true), "aria-label": "Switch user", children: _jsx(Avatar, { size: "sm", color: "blue", children: currentUser?.name[0] ?? 'U' }) }), _jsx(UserSwitcherModal, { opened: opened, onClose: () => setOpened(false), currentUserId: currentUserId })] }));
22
+ }
23
+ //# sourceMappingURL=UserSwitcherButton.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"UserSwitcherButton.js","sourceRoot":"","sources":["../src/UserSwitcherButton.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAA;;AAEZ,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAC3C,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,eAAe,CAAA;AAClD,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAA;AACvD,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA;AAC5C,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAA;AAElE;;GAEG;AACH,MAAM,UAAU,kBAAkB;IAChC,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAA;IAC3C,MAAM,CAAC,OAAO,EAAE,UAAU,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAA;IAE7C,2DAA2D;IAC3D,SAAS,CAAC,GAAG,EAAE;QACb,UAAU,CAAC,IAAI,CAAC,CAAA;IAClB,CAAC,EAAE,EAAE,CAAC,CAAA;IAEN,iDAAiD;IACjD,MAAM,aAAa,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC,gBAAgB,EAAE,IAAI,eAAe,CAAC,CAAC,CAAC,CAAC,eAAe,CAAA;IACzF,MAAM,WAAW,GAAG,aAAa,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,aAAa,CAAC,CAAA;IAEzE,OAAO,CACL,8BACE,KAAC,UAAU,IACT,OAAO,EAAC,QAAQ,EAChB,IAAI,EAAC,IAAI,EACT,MAAM,EAAC,IAAI,EACX,OAAO,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,gBACnB,aAAa,YAExB,KAAC,MAAM,IAAC,IAAI,EAAC,IAAI,EAAC,KAAK,EAAC,MAAM,YAC3B,WAAW,EAAE,IAAI,CAAC,CAAC,CAAC,IAAI,GAAG,GACrB,GACE,EAEb,KAAC,iBAAiB,IAChB,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,KAAK,CAAC,EAC/B,aAAa,EAAE,aAAa,GAC5B,IACD,CACJ,CAAA;AACH,CAAC"}
@@ -0,0 +1,10 @@
1
+ interface Props {
2
+ opened: boolean;
3
+ onClose: () => void;
4
+ currentUserId: string;
5
+ }
6
+ /**
7
+ * User switcher modal component that displays all available dev users
8
+ */
9
+ export declare function UserSwitcherModal({ opened, onClose, currentUserId }: Props): import("react/jsx-runtime").JSX.Element;
10
+ export {};
@@ -0,0 +1,19 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Modal, Stack, Paper, Group, Avatar, Text, Badge } from '@mantine/core';
4
+ import { MdCheck } from 'react-icons/md';
5
+ import { DEFAULT_USERS } from './dev-plugin';
6
+ import { setDevUserCookie } from './cookie-utils';
7
+ /**
8
+ * User switcher modal component that displays all available dev users
9
+ */
10
+ export function UserSwitcherModal({ opened, onClose, currentUserId }) {
11
+ const switchUser = (userId) => {
12
+ // Set cookie for 7 days
13
+ setDevUserCookie(userId);
14
+ // Reload to apply new user
15
+ window.location.reload();
16
+ };
17
+ return (_jsx(Modal, { opened: opened, onClose: onClose, title: "Switch Development User", children: _jsx(Stack, { gap: "sm", children: DEFAULT_USERS.map((user) => (_jsxs(Paper, { p: "md", withBorder: true, style: { cursor: 'pointer' }, onClick: () => switchUser(user.userId), children: [_jsxs(Group, { justify: "space-between", mb: "xs", children: [_jsxs(Group, { children: [_jsx(Avatar, { color: "blue", children: user.name[0] }), _jsxs("div", { children: [_jsx(Text, { fw: 500, children: user.name }), _jsx(Text, { size: "sm", c: "dimmed", children: user.email })] })] }), user.userId === currentUserId && _jsx(MdCheck, { size: 20 })] }), user.externalGroups.length > 0 && (_jsx(Group, { gap: "xs", children: user.externalGroups.map((g) => (_jsx(Badge, { variant: "outline", size: "sm", children: g }, g))) }))] }, user.userId))) }) }));
18
+ }
19
+ //# sourceMappingURL=UserSwitcherModal.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"UserSwitcherModal.js","sourceRoot":"","sources":["../src/UserSwitcherModal.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAA;;AAEZ,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,eAAe,CAAA;AAC/E,OAAO,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAA;AACxC,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA;AAC5C,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAA;AAQjD;;GAEG;AACH,MAAM,UAAU,iBAAiB,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,EAAS;IACzE,MAAM,UAAU,GAAG,CAAC,MAAc,EAAE,EAAE;QACpC,wBAAwB;QACxB,gBAAgB,CAAC,MAAM,CAAC,CAAA;QACxB,2BAA2B;QAC3B,MAAM,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAA;IAC1B,CAAC,CAAA;IAED,OAAO,CACL,KAAC,KAAK,IAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAC,yBAAyB,YACtE,KAAC,KAAK,IAAC,GAAG,EAAC,IAAI,YACZ,aAAa,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAC3B,MAAC,KAAK,IAEJ,CAAC,EAAC,IAAI,EACN,UAAU,QACV,KAAK,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,EAC5B,OAAO,EAAE,GAAG,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,MAAM,CAAC,aAEtC,MAAC,KAAK,IAAC,OAAO,EAAC,eAAe,EAAC,EAAE,EAAC,IAAI,aACpC,MAAC,KAAK,eACJ,KAAC,MAAM,IAAC,KAAK,EAAC,MAAM,YAAE,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,GAAU,EAC5C,0BACE,KAAC,IAAI,IAAC,EAAE,EAAE,GAAG,YAAG,IAAI,CAAC,IAAI,GAAQ,EACjC,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,CAAC,EAAC,QAAQ,YACvB,IAAI,CAAC,KAAK,GACN,IACH,IACA,EACP,IAAI,CAAC,MAAM,KAAK,aAAa,IAAI,KAAC,OAAO,IAAC,IAAI,EAAE,EAAE,GAAI,IACjD,EAEP,IAAI,CAAC,cAAc,CAAC,MAAM,GAAG,CAAC,IAAI,CACjC,KAAC,KAAK,IAAC,GAAG,EAAC,IAAI,YACZ,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAC9B,KAAC,KAAK,IAAS,OAAO,EAAC,SAAS,EAAC,IAAI,EAAC,IAAI,YACvC,CAAC,IADQ,CAAC,CAEL,CACT,CAAC,GACI,CACT,KA3BI,IAAI,CAAC,MAAM,CA4BV,CACT,CAAC,GACI,GACF,CACT,CAAA;AACH,CAAC"}
@@ -0,0 +1,22 @@
1
+ import type { DevUser, DevGroup } from './dev-plugin';
2
+ export interface RefreshDevCacheOptions {
3
+ /** Directory to write cache files to (e.g., .canopy-prod-sim/.cache) */
4
+ cachePath: string;
5
+ /** Custom users (defaults to DEFAULT_USERS) */
6
+ users?: DevUser[];
7
+ /** Custom groups (defaults to DEFAULT_GROUPS) */
8
+ groups?: DevGroup[];
9
+ }
10
+ /**
11
+ * Write dev users/groups to cache files for FileBasedAuthCache.
12
+ *
13
+ * This is the dev-auth equivalent of refreshClerkCache() — it populates
14
+ * the same JSON files that CachingAuthPlugin reads. Since dev users are
15
+ * hardcoded, no API calls are needed.
16
+ *
17
+ * Used by the worker's `run-once` command in prod-sim mode with dev auth.
18
+ */
19
+ export declare function refreshDevCache(options: RefreshDevCacheOptions): Promise<{
20
+ userCount: number;
21
+ groupCount: number;
22
+ }>;
@@ -0,0 +1,42 @@
1
+ import { writeAuthCacheSnapshot } from 'canopycms/auth/cache';
2
+ import { DEFAULT_USERS, DEFAULT_GROUPS } from './dev-plugin';
3
+ /**
4
+ * Write dev users/groups to cache files for FileBasedAuthCache.
5
+ *
6
+ * This is the dev-auth equivalent of refreshClerkCache() — it populates
7
+ * the same JSON files that CachingAuthPlugin reads. Since dev users are
8
+ * hardcoded, no API calls are needed.
9
+ *
10
+ * Used by the worker's `run-once` command in prod-sim mode with dev auth.
11
+ */
12
+ export async function refreshDevCache(options) {
13
+ const { cachePath } = options;
14
+ const users = options.users ?? DEFAULT_USERS;
15
+ const groups = options.groups ?? DEFAULT_GROUPS;
16
+ const usersData = {
17
+ users: users.map((u) => ({
18
+ id: u.userId,
19
+ name: u.name,
20
+ email: u.email,
21
+ avatarUrl: u.avatarUrl,
22
+ })),
23
+ };
24
+ const groupsData = {
25
+ groups: groups.map((g) => ({
26
+ id: g.id,
27
+ name: g.name,
28
+ description: g.description,
29
+ })),
30
+ };
31
+ const membershipsData = {
32
+ memberships: Object.fromEntries(users.filter((u) => u.externalGroups.length > 0).map((u) => [u.userId, u.externalGroups])),
33
+ };
34
+ // Write cache files atomically via snapshot directory + symlink swap
35
+ await writeAuthCacheSnapshot(cachePath, {
36
+ 'users.json': usersData,
37
+ 'orgs.json': groupsData,
38
+ 'memberships.json': membershipsData,
39
+ });
40
+ return { userCount: users.length, groupCount: groups.length };
41
+ }
42
+ //# sourceMappingURL=cache-writer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache-writer.js","sourceRoot":"","sources":["../src/cache-writer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAA;AAC7D,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAY5D;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,OAA+B;IAE/B,MAAM,EAAE,SAAS,EAAE,GAAG,OAAO,CAAA;IAC7B,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,aAAa,CAAA;IAC5C,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,cAAc,CAAA;IAE/C,MAAM,SAAS,GAAG;QAChB,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YACvB,EAAE,EAAE,CAAC,CAAC,MAAM;YACZ,IAAI,EAAE,CAAC,CAAC,IAAI;YACZ,KAAK,EAAE,CAAC,CAAC,KAAK;YACd,SAAS,EAAE,CAAC,CAAC,SAAS;SACvB,CAAC,CAAC;KACJ,CAAA;IAED,MAAM,UAAU,GAAG;QACjB,MAAM,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YACzB,EAAE,EAAE,CAAC,CAAC,EAAE;YACR,IAAI,EAAE,CAAC,CAAC,IAAI;YACZ,WAAW,EAAE,CAAC,CAAC,WAAW;SAC3B,CAAC,CAAC;KACJ,CAAA;IAED,MAAM,eAAe,GAAG;QACtB,WAAW,EAAE,MAAM,CAAC,WAAW,CAC7B,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,cAAc,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,cAAc,CAAC,CAAC,CAC1F;KACF,CAAA;IAED,qEAAqE;IACrE,MAAM,sBAAsB,CAAC,SAAS,EAAE;QACtC,YAAY,EAAE,SAAS;QACvB,WAAW,EAAE,UAAU;QACvB,kBAAkB,EAAE,eAAe;KACpC,CAAC,CAAA;IAEF,OAAO,EAAE,SAAS,EAAE,KAAK,CAAC,MAAM,EAAE,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,CAAA;AAC/D,CAAC"}
@@ -0,0 +1,18 @@
1
+ import type { CanopyClientConfig } from 'canopycms/client';
2
+ /**
3
+ * Hook that provides dev auth handlers and components for CanopyCMS editor.
4
+ * Model after: packages/canopycms-auth-clerk/src/client.ts
5
+ *
6
+ * @example
7
+ * ```tsx
8
+ * import { useDevAuthConfig } from 'canopycms-auth-dev/client'
9
+ * import config from '../../canopycms.config'
10
+ *
11
+ * export default function EditPage() {
12
+ * const devAuth = useDevAuthConfig()
13
+ * const editorConfig = config.client(devAuth)
14
+ * return <CanopyEditorPage config={editorConfig} />
15
+ * }
16
+ * ```
17
+ */
18
+ export declare function useDevAuthConfig(): Pick<CanopyClientConfig, 'editor'>;
package/dist/client.js ADDED
@@ -0,0 +1,32 @@
1
+ 'use client';
2
+ import { UserSwitcherButton } from './UserSwitcherButton';
3
+ import { clearDevUserCookie } from './cookie-utils';
4
+ /**
5
+ * Hook that provides dev auth handlers and components for CanopyCMS editor.
6
+ * Model after: packages/canopycms-auth-clerk/src/client.ts
7
+ *
8
+ * @example
9
+ * ```tsx
10
+ * import { useDevAuthConfig } from 'canopycms-auth-dev/client'
11
+ * import config from '../../canopycms.config'
12
+ *
13
+ * export default function EditPage() {
14
+ * const devAuth = useDevAuthConfig()
15
+ * const editorConfig = config.client(devAuth)
16
+ * return <CanopyEditorPage config={editorConfig} />
17
+ * }
18
+ * ```
19
+ */
20
+ export function useDevAuthConfig() {
21
+ return {
22
+ editor: {
23
+ AccountComponent: UserSwitcherButton,
24
+ onLogoutClick: () => {
25
+ // Reset to default user
26
+ clearDevUserCookie();
27
+ window.location.reload();
28
+ },
29
+ },
30
+ };
31
+ }
32
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,YAAY,CAAA;AAGZ,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAA;AACzD,OAAO,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAA;AAEnD;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,gBAAgB;IAC9B,OAAO;QACL,MAAM,EAAE;YACN,gBAAgB,EAAE,kBAAkB;YACpC,aAAa,EAAE,GAAG,EAAE;gBAClB,wBAAwB;gBACxB,kBAAkB,EAAE,CAAA;gBACpB,MAAM,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAA;YAC1B,CAAC;SACF;KACF,CAAA;AACH,CAAC"}
@@ -0,0 +1,20 @@
1
+ import type { HeadersLike } from 'canopycms/auth';
2
+ export declare const DEV_USER_COOKIE_NAME = "canopy-dev-user";
3
+ export declare const DEV_USER_COOKIE_MAX_AGE: number;
4
+ export declare const DEFAULT_USER_ID = "dev_user1_2nK8mP4xL9";
5
+ /**
6
+ * Server-side: Extract cookie value from HTTP headers
7
+ */
8
+ export declare function getDevUserCookieFromHeaders(headers: HeadersLike): string | null;
9
+ /**
10
+ * Client-side: Read cookie from document.cookie
11
+ */
12
+ export declare function getDevUserCookie(): string | null;
13
+ /**
14
+ * Client-side: Set dev user cookie
15
+ */
16
+ export declare function setDevUserCookie(userId: string): void;
17
+ /**
18
+ * Client-side: Clear dev user cookie (logout)
19
+ */
20
+ export declare function clearDevUserCookie(): void;
@@ -0,0 +1,39 @@
1
+ export const DEV_USER_COOKIE_NAME = 'canopy-dev-user';
2
+ export const DEV_USER_COOKIE_MAX_AGE = 60 * 60 * 24 * 7; // 7 days
3
+ export const DEFAULT_USER_ID = 'dev_user1_2nK8mP4xL9';
4
+ /**
5
+ * Server-side: Extract cookie value from HTTP headers
6
+ */
7
+ export function getDevUserCookieFromHeaders(headers) {
8
+ const cookie = headers.get('Cookie');
9
+ if (!cookie)
10
+ return null;
11
+ const match = cookie.match(new RegExp(`${DEV_USER_COOKIE_NAME}=([^;]+)`));
12
+ return match?.[1] ?? null;
13
+ }
14
+ /**
15
+ * Client-side: Read cookie from document.cookie
16
+ */
17
+ export function getDevUserCookie() {
18
+ if (typeof document === 'undefined')
19
+ return null;
20
+ const match = document.cookie.match(new RegExp(`${DEV_USER_COOKIE_NAME}=([^;]+)`));
21
+ return match?.[1] ?? null;
22
+ }
23
+ /**
24
+ * Client-side: Set dev user cookie
25
+ */
26
+ export function setDevUserCookie(userId) {
27
+ if (typeof document === 'undefined')
28
+ return;
29
+ document.cookie = `${DEV_USER_COOKIE_NAME}=${userId}; path=/; max-age=${DEV_USER_COOKIE_MAX_AGE}; SameSite=Lax`;
30
+ }
31
+ /**
32
+ * Client-side: Clear dev user cookie (logout)
33
+ */
34
+ export function clearDevUserCookie() {
35
+ if (typeof document === 'undefined')
36
+ return;
37
+ document.cookie = `${DEV_USER_COOKIE_NAME}=; path=/; max-age=0`;
38
+ }
39
+ //# sourceMappingURL=cookie-utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cookie-utils.js","sourceRoot":"","sources":["../src/cookie-utils.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,oBAAoB,GAAG,iBAAiB,CAAA;AACrD,MAAM,CAAC,MAAM,uBAAuB,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAA,CAAC,SAAS;AACjE,MAAM,CAAC,MAAM,eAAe,GAAG,sBAAsB,CAAA;AAErD;;GAEG;AACH,MAAM,UAAU,2BAA2B,CAAC,OAAoB;IAC9D,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;IACpC,IAAI,CAAC,MAAM;QAAE,OAAO,IAAI,CAAA;IAExB,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,MAAM,CAAC,GAAG,oBAAoB,UAAU,CAAC,CAAC,CAAA;IACzE,OAAO,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,IAAI,CAAA;AAC3B,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,gBAAgB;IAC9B,IAAI,OAAO,QAAQ,KAAK,WAAW;QAAE,OAAO,IAAI,CAAA;IAEhD,MAAM,KAAK,GAAG,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,MAAM,CAAC,GAAG,oBAAoB,UAAU,CAAC,CAAC,CAAA;IAClF,OAAO,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,IAAI,CAAA;AAC3B,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,gBAAgB,CAAC,MAAc;IAC7C,IAAI,OAAO,QAAQ,KAAK,WAAW;QAAE,OAAM;IAE3C,QAAQ,CAAC,MAAM,GAAG,GAAG,oBAAoB,IAAI,MAAM,qBAAqB,uBAAuB,gBAAgB,CAAA;AACjH,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,kBAAkB;IAChC,IAAI,OAAO,QAAQ,KAAK,WAAW;QAAE,OAAM;IAE3C,QAAQ,CAAC,MAAM,GAAG,GAAG,oBAAoB,sBAAsB,CAAA;AACjE,CAAC"}
@@ -0,0 +1,71 @@
1
+ import type { AuthPlugin } from 'canopycms/auth';
2
+ import type { UserSearchResult, GroupMetadata, AuthenticationResult } from 'canopycms/auth';
3
+ import type { CanopyUserId, CanopyGroupId } from 'canopycms';
4
+ /**
5
+ * WARNING: This plugin is for development and testing only!
6
+ * Do not use in production environments.
7
+ */
8
+ export interface DevUser {
9
+ userId: CanopyUserId;
10
+ name: string;
11
+ email: string;
12
+ avatarUrl?: string;
13
+ externalGroups: CanopyGroupId[];
14
+ }
15
+ export interface DevGroup {
16
+ id: CanopyGroupId;
17
+ name: string;
18
+ description?: string;
19
+ }
20
+ export declare const DEV_ADMIN_USER_ID: CanopyUserId;
21
+ export interface DevAuthConfig {
22
+ /**
23
+ * Custom mock users. If not provided, uses default users.
24
+ */
25
+ users?: DevUser[];
26
+ /**
27
+ * Custom mock groups. If not provided, uses default groups.
28
+ */
29
+ groups?: DevGroup[];
30
+ /**
31
+ * Default user ID when no user is selected.
32
+ * @default 'dev_user1_2nK8mP4xL9' (user1)
33
+ */
34
+ defaultUserId?: CanopyUserId;
35
+ /**
36
+ * Whether to auto-set CANOPY_BOOTSTRAP_ADMIN_IDS for the admin dev user
37
+ * when the env var is not already set. Defaults to true.
38
+ */
39
+ autoBootstrapAdmin?: boolean;
40
+ }
41
+ export declare const DEFAULT_USERS: DevUser[];
42
+ export declare const DEFAULT_GROUPS: DevGroup[];
43
+ /**
44
+ * Dev authentication plugin implementation for CanopyCMS.
45
+ * Supports both cookie-based (UI) and header-based (tests) authentication.
46
+ */
47
+ export declare class DevAuthPlugin implements AuthPlugin {
48
+ private users;
49
+ private groups;
50
+ private defaultUserId;
51
+ constructor(config?: DevAuthConfig);
52
+ authenticate(context: unknown): Promise<AuthenticationResult>;
53
+ /**
54
+ * Map test-app user keys to dev user IDs for backward compatibility
55
+ */
56
+ private mapTestUserKey;
57
+ searchUsers(query: string, limit?: number): Promise<UserSearchResult[]>;
58
+ getUserMetadata(userId: CanopyUserId): Promise<UserSearchResult | null>;
59
+ getGroupMetadata(groupId: CanopyGroupId): Promise<GroupMetadata | null>;
60
+ listGroups(limit?: number): Promise<GroupMetadata[]>;
61
+ searchExternalGroups(query: string): Promise<Array<{
62
+ id: CanopyGroupId;
63
+ name: string;
64
+ }>>;
65
+ }
66
+ /**
67
+ * Factory function for creating dev auth plugin.
68
+ * By default, auto-sets CANOPY_BOOTSTRAP_ADMIN_IDS to the admin dev user
69
+ * if the env var is not already set. Disable with { autoBootstrapAdmin: false }.
70
+ */
71
+ export declare function createDevAuthPlugin(config?: DevAuthConfig): AuthPlugin;