ldap-rest 0.1.8 → 0.2.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.
@@ -0,0 +1,76 @@
1
+ import TwakePlugin from '../../abstract/twakePlugin';
2
+ import { type Role } from '../../abstract/plugin';
3
+ import type { DM } from '../../bin';
4
+ import { Hooks } from '../../hooks';
5
+ export default class Drive extends TwakePlugin {
6
+ name: string;
7
+ roles: Role[];
8
+ dependencies: {
9
+ onLdapChange: string;
10
+ };
11
+ private cozyDomainAttr;
12
+ private defaultDomainTemplate;
13
+ private templateAttrs;
14
+ private driveQuotaAttr;
15
+ constructor(server: DM);
16
+ /**
17
+ * Validate Cozy domain format to prevent URL injection attacks
18
+ * @param domain Cozy domain to validate
19
+ * @returns true if domain format is valid
20
+ */
21
+ private isValidCozyDomain;
22
+ /**
23
+ * Get Cozy domain for a user from LDAP
24
+ * Falls back to template-based domain if attribute not found
25
+ * @param dn User's LDAP DN
26
+ * @returns Cozy domain or null if not found
27
+ */
28
+ getCozyDomain(dn: string): Promise<string | null>;
29
+ /**
30
+ * Update Cozy instance via Admin API
31
+ * @param hookname Hook name for logging
32
+ * @param cozyDomain Cozy instance domain
33
+ * @param dn User's LDAP DN
34
+ * @param params Query parameters to update
35
+ */
36
+ private updateCozyInstance;
37
+ hooks: Hooks;
38
+ /**
39
+ * Extract display name from LDAP attributes
40
+ * Fallback logic: displayName → cn → givenName+sn
41
+ */
42
+ private getDisplayNameFromAttributes;
43
+ /**
44
+ * Public method to get display name from DN
45
+ * Useful for external integrations
46
+ */
47
+ getDisplayNameFromDN(dn: string): Promise<string | null>;
48
+ /**
49
+ * Public method to get mail from DN
50
+ * Useful for external integrations
51
+ */
52
+ getMailFromDN(dn: string): Promise<string | null>;
53
+ /**
54
+ * Public method to get drive quota from DN (in bytes)
55
+ * Useful for external integrations
56
+ */
57
+ getDriveQuotaFromDN(dn: string): Promise<number | null>;
58
+ /**
59
+ * Public method to manually sync a user to Twake Drive
60
+ * Useful for initial sync or manual refresh
61
+ */
62
+ syncUserToCozy(dn: string): Promise<boolean>;
63
+ /**
64
+ * Block a Cozy instance
65
+ * @param dn User's LDAP DN
66
+ * @param reason Optional blocking reason (e.g., 'PAYMENT_FAILED', 'LOGIN_FAILED', 'SUSPENDED')
67
+ * @returns true if successful, false otherwise
68
+ */
69
+ blockInstance(dn: string, reason?: string): Promise<boolean>;
70
+ /**
71
+ * Unblock a Cozy instance
72
+ * @param dn User's LDAP DN
73
+ * @returns true if successful, false otherwise
74
+ */
75
+ unblockInstance(dn: string): Promise<boolean>;
76
+ }
@@ -0,0 +1,646 @@
1
+ # Twake Drive Plugin
2
+
3
+ Synchronize LDAP user changes with Twake Drive via Admin API.
4
+
5
+ ## Overview
6
+
7
+ The `twake/drive` plugin automatically synchronizes email address, display name, and disk quota changes from LDAP to [Twake Drive](https://twake.app/).
8
+ It listens to `onChange` hooks and updates Twake Drive instances via the Admin API.
9
+
10
+ ## Prerequisites
11
+
12
+ 1. **Twake Drive / Cozy** instance with Admin API enabled
13
+ 2. **onChange plugin** loaded to detect LDAP changes
14
+ 3. **Cozy domain** - either:
15
+ - Per-user attribute in LDAP schema (e.g., `twakeCozyDomain`), or
16
+ - Default domain template configured (e.g., `{uid}.mycompany.cloud`)
17
+
18
+ ## Configuration
19
+
20
+ ```bash
21
+ npx ldap-rest \
22
+ --plugin core/ldap/onChange \
23
+ --plugin core/ldap/flatGeneric \
24
+ --plugin twake/drive \
25
+ --ldap-flat-schema ./static/schemas/twake/users.json \
26
+ --mail-attribute mail \
27
+ --twake-drive-domain-attribute twakeCozyDomain \
28
+ --twake-drive-webadmin-url http://cozy-admin:6060 \
29
+ --twake-drive-webadmin-token "your-admin-token" \
30
+ ...
31
+ ```
32
+
33
+ **Environment Variables:**
34
+
35
+ ```bash
36
+ DM_TWAKE_DRIVE_WEBADMIN_URL="http://cozy-admin:6060"
37
+ DM_TWAKE_DRIVE_WEBADMIN_TOKEN="your-admin-token"
38
+ DM_TWAKE_DRIVE_DOMAIN_ATTRIBUTE="twakeCozyDomain"
39
+ ```
40
+
41
+ ### Parameters
42
+
43
+ | Parameter | Description | Default |
44
+ | --------------------------------------- | ------------------------------------------------------------- | ----------------- |
45
+ | `--twake-drive-webadmin-url` | Twake Drive Admin API base URL (required) | - |
46
+ | `--twake-drive-webadmin-token` | Bearer token for Admin authentication | - |
47
+ | `--mail-attribute` | LDAP attribute for email | `mail` |
48
+ | `--display-name-attribute` | LDAP attribute for display name | `displayName` |
49
+ | `--drive-quota-attribute` | LDAP attribute for disk quota (in bytes) | `twakeDriveQuota` |
50
+ | `--twake-drive-domain-attribute` | LDAP attribute storing user's Twake Drive domain | `twakeCozyDomain` |
51
+ | `--twake-drive-default-domain-template` | Template for Twake Drive domain (e.g., `{uid}.company.cloud`) | - |
52
+ | `--twake-drive-concurrency` | Maximum concurrent API requests | `10` |
53
+
54
+ ## Cozy Domain Resolution
55
+
56
+ The plugin determines each user's Cozy domain using the following logic:
57
+
58
+ 1. **Per-user attribute** (if present): Use the `twakeCozyDomain` LDAP attribute value
59
+ 2. **Default template** (fallback): Build domain from template with LDAP attribute placeholders
60
+
61
+ ### Per-user Cozy domain attribute
62
+
63
+ Users can have an explicit `twakeCozyDomain` attribute for custom domains:
64
+
65
+ ```ldif
66
+ dn: uid=jdoe,ou=users,dc=example,dc=com
67
+ uid: jdoe
68
+ mail: jdoe@example.com
69
+ twakeCozyDomain: john-doe.partner.cloud
70
+ ```
71
+
72
+ ### Default domain template
73
+
74
+ Configure a template for users without an explicit attribute. Use `{attribute}` placeholders:
75
+
76
+ ```bash
77
+ --twake-drive-default-domain-template "{uid}.mycompany.cloud"
78
+ ```
79
+
80
+ Users without `twakeCozyDomain` will have their domain built from the template:
81
+
82
+ - User `uid=jdoe` → Cozy domain `jdoe.mycompany.cloud`
83
+
84
+ Other template examples:
85
+
86
+ - `"{mail}"` → use mail attribute directly as domain
87
+ - `"{cn}.cloud.example.com"` → use common name
88
+
89
+ ### Combined usage
90
+
91
+ Both can be used together. The per-user attribute takes precedence:
92
+
93
+ ```ldif
94
+ # User with custom domain (uses twakeCozyDomain)
95
+ dn: uid=partner1,ou=users,dc=example,dc=com
96
+ uid: partner1
97
+ twakeCozyDomain: partner1.external.cloud
98
+
99
+ # Regular user (uses default suffix → jdoe.mycompany.cloud)
100
+ dn: uid=jdoe,ou=users,dc=example,dc=com
101
+ uid: jdoe
102
+ ```
103
+
104
+ ## How It Works
105
+
106
+ ### Mail Address Changes
107
+
108
+ When a user's mail attribute changes:
109
+
110
+ 1. **LDAP modify operation**
111
+
112
+ ```bash
113
+ PUT /api/v1/ldap/users/jdoe
114
+ {
115
+ "replace": {
116
+ "mail": "john.doe@company.com"
117
+ }
118
+ }
119
+ ```
120
+
121
+ 2. **onChange plugin** detects mail change and triggers `onLdapMailChange` hook
122
+
123
+ 3. **Drive plugin** receives hook and calls Cozy Admin API:
124
+
125
+ ```http
126
+ PATCH /instances/jdoe.mycompany.cloud?Email=john.doe@company.com&FromCloudery=true
127
+ ```
128
+
129
+ 4. **Cozy** updates the instance owner's email address
130
+
131
+ ### Display Name Changes
132
+
133
+ When a user's display name changes:
134
+
135
+ 1. **LDAP modify operation**
136
+
137
+ ```bash
138
+ PUT /api/v1/ldap/users/jdoe
139
+ {
140
+ "replace": {
141
+ "displayName": "John M. Doe"
142
+ }
143
+ }
144
+ ```
145
+
146
+ 2. **onChange plugin** triggers `onLdapDisplayNameChange` hook
147
+
148
+ 3. **Drive plugin** updates Cozy instance:
149
+
150
+ ```http
151
+ PATCH /instances/jdoe.mycompany.cloud?PublicName=John%20M.%20Doe&FromCloudery=true
152
+ ```
153
+
154
+ ### Display Name Fallback Logic
155
+
156
+ If `displayName` is not set, the plugin uses fallback logic:
157
+
158
+ 1. `displayName` attribute (first choice)
159
+ 2. `cn` (Common Name)
160
+ 3. `givenName` + `sn` (First Name + Last Name)
161
+
162
+ ### Disk Quota Changes
163
+
164
+ When a user's drive quota changes:
165
+
166
+ 1. **LDAP modify operation**
167
+
168
+ ```bash
169
+ PUT /api/v1/ldap/users/jdoe
170
+ {
171
+ "replace": {
172
+ "twakeDriveQuota": "5368709120"
173
+ }
174
+ }
175
+ ```
176
+
177
+ 2. **onChange plugin** triggers `onLdapDriveQuotaChange` hook
178
+
179
+ 3. **Drive plugin** updates Twake Drive instance:
180
+
181
+ ```http
182
+ PATCH /instances/jdoe.company.cloud?DiskQuota=5368709120&FromCloudery=true
183
+ ```
184
+
185
+ 4. **Twake Drive** updates the instance disk quota
186
+
187
+ **Note:** The quota value is in bytes. Common values:
188
+
189
+ - 1 GB = `1073741824`
190
+ - 5 GB = `5368709120`
191
+ - 10 GB = `10737418240`
192
+
193
+ ## Public Methods
194
+
195
+ Public methods available on the Drive plugin instance:
196
+
197
+ ### Usage
198
+
199
+ ```typescript
200
+ import type { DM } from 'ldap-rest';
201
+ import type Drive from 'ldap-rest/plugin-twake-drive';
202
+
203
+ // Get the Drive plugin instance
204
+ const drive = server.getPlugin('drive') as Drive;
205
+ ```
206
+
207
+ ### getCozyDomain(dn)
208
+
209
+ Retrieve the Cozy domain for a user given their LDAP DN.
210
+
211
+ **Signature:**
212
+
213
+ ```typescript
214
+ async getCozyDomain(dn: string): Promise<string | null>
215
+ ```
216
+
217
+ **Example:**
218
+
219
+ ```typescript
220
+ const domain = await drive.getCozyDomain('uid=jdoe,ou=users,dc=example,dc=com');
221
+ // Returns: "jdoe.mycompany.cloud"
222
+ ```
223
+
224
+ ---
225
+
226
+ ### getDisplayNameFromDN(dn)
227
+
228
+ Retrieve the display name for a user using fallback logic.
229
+
230
+ **Signature:**
231
+
232
+ ```typescript
233
+ async getDisplayNameFromDN(dn: string): Promise<string | null>
234
+ ```
235
+
236
+ **Example:**
237
+
238
+ ```typescript
239
+ const name = await drive.getDisplayNameFromDN(
240
+ 'uid=jdoe,ou=users,dc=example,dc=com'
241
+ );
242
+ // Returns: "John Doe"
243
+ ```
244
+
245
+ ---
246
+
247
+ ### getMailFromDN(dn)
248
+
249
+ Retrieve the email address for a user.
250
+
251
+ **Signature:**
252
+
253
+ ```typescript
254
+ async getMailFromDN(dn: string): Promise<string | null>
255
+ ```
256
+
257
+ **Example:**
258
+
259
+ ```typescript
260
+ const mail = await drive.getMailFromDN('uid=jdoe,ou=users,dc=example,dc=com');
261
+ // Returns: "jdoe@example.com"
262
+ ```
263
+
264
+ ---
265
+
266
+ ### getDriveQuotaFromDN(dn)
267
+
268
+ Retrieve the drive quota for a user (in bytes).
269
+
270
+ **Signature:**
271
+
272
+ ```typescript
273
+ async getDriveQuotaFromDN(dn: string): Promise<number | null>
274
+ ```
275
+
276
+ **Example:**
277
+
278
+ ```typescript
279
+ const quota = await drive.getDriveQuotaFromDN(
280
+ 'uid=jdoe,ou=users,dc=example,dc=com'
281
+ );
282
+ // Returns: 5368709120 (5 GB)
283
+ ```
284
+
285
+ ---
286
+
287
+ ### syncUserToCozy(dn)
288
+
289
+ Manually synchronize a user's attributes (email, display name, disk quota) to their Twake Drive instance. Useful for initial sync or manual refresh.
290
+
291
+ **Signature:**
292
+
293
+ ```typescript
294
+ async syncUserToCozy(dn: string): Promise<boolean>
295
+ ```
296
+
297
+ **Example:**
298
+
299
+ ```typescript
300
+ const success = await drive.syncUserToCozy(
301
+ 'uid=jdoe,ou=users,dc=example,dc=com'
302
+ );
303
+ if (success) {
304
+ console.log('User synced successfully');
305
+ }
306
+ ```
307
+
308
+ ---
309
+
310
+ ### blockInstance(dn, reason?)
311
+
312
+ Block a user's Cozy instance with an optional reason.
313
+
314
+ **Signature:**
315
+
316
+ ```typescript
317
+ async blockInstance(dn: string, reason?: string): Promise<boolean>
318
+ ```
319
+
320
+ **Parameters:**
321
+
322
+ - `dn` - User's LDAP DN
323
+ - `reason` - Optional blocking reason. Common values:
324
+ - `PAYMENT_FAILED` - Payment issue
325
+ - `LOGIN_FAILED` - Too many failed login attempts
326
+ - `SUSPENDED` - Account suspended by admin
327
+ - Any custom string
328
+
329
+ **Example:**
330
+
331
+ ```typescript
332
+ // Block with reason
333
+ await drive.blockInstance(
334
+ 'uid=jdoe,ou=users,dc=example,dc=com',
335
+ 'PAYMENT_FAILED'
336
+ );
337
+
338
+ // Block without reason
339
+ await drive.blockInstance('uid=jdoe,ou=users,dc=example,dc=com');
340
+ ```
341
+
342
+ ---
343
+
344
+ ### unblockInstance(dn)
345
+
346
+ Unblock a user's Cozy instance.
347
+
348
+ **Signature:**
349
+
350
+ ```typescript
351
+ async unblockInstance(dn: string): Promise<boolean>
352
+ ```
353
+
354
+ **Example:**
355
+
356
+ ```typescript
357
+ await drive.unblockInstance('uid=jdoe,ou=users,dc=example,dc=com');
358
+ ```
359
+
360
+ ## Cozy Admin API
361
+
362
+ ### Endpoints Used
363
+
364
+ | Operation | Endpoint | Method |
365
+ | --------------- | ----------------------------------------------------------------------------------------- | ------ |
366
+ | Update email | `/instances/{domain}?Email={email}&FromCloudery=true` | PATCH |
367
+ | Update name | `/instances/{domain}?PublicName={name}&FromCloudery=true` | PATCH |
368
+ | Update quota | `/instances/{domain}?DiskQuota={bytes}&FromCloudery=true` | PATCH |
369
+ | Combined update | `/instances/{domain}?Email={email}&PublicName={name}&DiskQuota={bytes}&FromCloudery=true` | PATCH |
370
+
371
+ ### Available Cozy Parameters
372
+
373
+ The Cozy Admin API supports the following parameters (query string):
374
+
375
+ | Parameter | Type | Description |
376
+ | ---------------- | ------- | ---------------------------------------------------- |
377
+ | `Email` | string | Owner email address (supported by this plugin) |
378
+ | `PublicName` | string | Display name (supported by this plugin) |
379
+ | `Locale` | string | Locale (e.g. `fr`, `en`) |
380
+ | `Timezone` | string | Timezone (e.g. `Europe/Paris`) |
381
+ | `Phone` | string | Phone number |
382
+ | `DiskQuota` | integer | Disk quota in bytes |
383
+ | `Blocked` | bool | Block or unblock the instance |
384
+ | `BlockingReason` | string | Reason code (e.g. `PAYMENT_FAILED`, `LOGIN_FAILED`) |
385
+ | `FromCloudery` | bool | Skip Cloudery callback (always true for this plugin) |
386
+
387
+ See the [Cozy Admin Documentation](https://docs.cozy.io/en/cozy-stack/admin/#patch-instancesdomain) for the complete list.
388
+
389
+ ### FromCloudery Parameter
390
+
391
+ The `FromCloudery=true` query parameter is always included to:
392
+
393
+ - Prevent the Cozy stack from sending callback notifications
394
+ - Avoid notification loops when changes originate from the LDAP directory
395
+ - Indicate the change is administrative (from the cloudery/provisioning system)
396
+
397
+ ### Authentication
398
+
399
+ The Cozy Admin API uses bearer token authentication:
400
+
401
+ ```bash
402
+ --twake-drive-webadmin-token "your-admin-token"
403
+ ```
404
+
405
+ The plugin automatically adds the `Authorization: Bearer {token}` header to all requests.
406
+
407
+ **Note:** The Cozy Admin API typically runs on port 6060 and should be protected at the network level.
408
+
409
+ ## Examples
410
+
411
+ ### Example 1: Basic Setup
412
+
413
+ ```bash
414
+ --plugin core/ldap/onChange \
415
+ --plugin core/ldap/flatGeneric \
416
+ --plugin twake/drive \
417
+ --ldap-flat-schema ./schemas/twake/users.json \
418
+ --mail-attribute mail \
419
+ --twake-drive-webadmin-url http://cozy-admin:6060
420
+ ```
421
+
422
+ ### Example 2: With Authentication
423
+
424
+ ```bash
425
+ --plugin core/ldap/onChange \
426
+ --plugin core/ldap/flatGeneric \
427
+ --plugin twake/drive \
428
+ --ldap-flat-schema ./schemas/twake/users.json \
429
+ --mail-attribute mail \
430
+ --twake-drive-webadmin-url http://cozy-admin:6060 \
431
+ --twake-drive-webadmin-token "cozy-admin-secret"
432
+ ```
433
+
434
+ ### Example 3: Complete Twake Setup (James + Drive)
435
+
436
+ ```bash
437
+ --plugin core/auth/token \
438
+ --plugin core/ldap/onChange \
439
+ --plugin core/ldap/flatGeneric \
440
+ --plugin core/ldap/groups \
441
+ --plugin twake/james \
442
+ --plugin twake/drive \
443
+ --ldap-flat-schema ./schemas/twake/users.json \
444
+ --mail-attribute mail \
445
+ --quota-attribute mailQuotaSize \
446
+ --james-webadmin-url http://james:8000 \
447
+ --james-webadmin-token "james-admin-token" \
448
+ --twake-drive-webadmin-url http://cozy-admin:6060 \
449
+ --twake-drive-webadmin-token "cozy-admin-token" \
450
+ --twake-drive-domain-attribute twakeCozyDomain \
451
+ --auth-token "api-admin-token"
452
+ ```
453
+
454
+ ## Logging
455
+
456
+ The plugin logs all operations:
457
+
458
+ ### Successful Operations
459
+
460
+ ```json
461
+ {
462
+ "level": "info",
463
+ "plugin": "drive",
464
+ "event": "onLdapMailChange",
465
+ "result": "success",
466
+ "http_status": 200,
467
+ "dn": "uid=jdoe,ou=users,dc=example,dc=com",
468
+ "cozyDomain": "jdoe.mycompany.cloud",
469
+ "Email": "john.doe@company.com"
470
+ }
471
+ ```
472
+
473
+ ### Skipped Operations
474
+
475
+ ```json
476
+ {
477
+ "level": "debug",
478
+ "message": "Skipping mail change for uid=jdoe,...: no Cozy domain attribute (twakeCozyDomain)"
479
+ }
480
+ ```
481
+
482
+ ### Failed Operations
483
+
484
+ ```json
485
+ {
486
+ "level": "error",
487
+ "plugin": "drive",
488
+ "event": "onLdapMailChange",
489
+ "result": "error",
490
+ "http_status": 500,
491
+ "http_status_text": "Internal Server Error",
492
+ "dn": "uid=jdoe,ou=users,dc=example,dc=com",
493
+ "cozyDomain": "jdoe.mycompany.cloud"
494
+ }
495
+ ```
496
+
497
+ ### Instance Not Found
498
+
499
+ ```json
500
+ {
501
+ "level": "debug",
502
+ "plugin": "drive",
503
+ "event": "onLdapMailChange",
504
+ "result": "ignored",
505
+ "http_status": 404,
506
+ "message": "Cozy instance not found (may not be provisioned yet)"
507
+ }
508
+ ```
509
+
510
+ ## Error Handling
511
+
512
+ ### Non-Existent Instance (404)
513
+
514
+ If the Cozy instance doesn't exist yet:
515
+
516
+ - Error is logged at DEBUG level (not ERROR)
517
+ - LDAP operation succeeds
518
+ - This is normal during user provisioning flow
519
+
520
+ ### Cozy Server Down
521
+
522
+ If the Cozy Admin API is unreachable:
523
+
524
+ - Error is logged
525
+ - LDAP operation succeeds
526
+ - Manual sync may be required later using `syncUserToCozy()`
527
+
528
+ ### Invalid Token
529
+
530
+ If the admin token is invalid:
531
+
532
+ - 401 or 403 error is logged
533
+ - LDAP operation succeeds
534
+ - Check token configuration
535
+
536
+ ## Synchronization Scenarios
537
+
538
+ ### Scenario 1: User Creation
539
+
540
+ 1. Create user in LDAP with `twakeCozyDomain` attribute
541
+ 2. Provision Cozy instance separately (via cloudery or API)
542
+ 3. Future mail/name changes are synced automatically
543
+ 4. Use `syncUserToCozy()` for initial sync if needed
544
+
545
+ ### Scenario 2: Email Change
546
+
547
+ 1. User changes email in LDAP: `old@company.com` → `new@company.com`
548
+ 2. Drive plugin updates Cozy instance email
549
+ 3. Cozy updates instance owner's email
550
+ 4. User receives notifications at new email
551
+
552
+ ### Scenario 3: Name Change
553
+
554
+ 1. Admin updates user's display name in LDAP
555
+ 2. Drive plugin updates Cozy instance PublicName
556
+ 3. User's name is updated across Cozy apps
557
+
558
+ ### Scenario 4: Manual Sync
559
+
560
+ ```typescript
561
+ // Sync all attributes for a user
562
+ const drive = server.getPlugin('drive') as Drive;
563
+ await drive.syncUserToCozy('uid=jdoe,ou=users,dc=example,dc=com');
564
+ ```
565
+
566
+ ## Limitations
567
+
568
+ 1. **One-way sync**: LDAP → Cozy only. Cozy changes don't sync back to LDAP.
569
+ 2. **No instance creation**: Plugin doesn't create Cozy instances, only updates existing ones.
570
+ 3. **No user deletion**: Plugin doesn't delete Cozy instances when LDAP users are deleted.
571
+ 4. **Domain required**: Users must have the `twakeCozyDomain` attribute to be synced.
572
+
573
+ ## Troubleshooting
574
+
575
+ ### Problem: Changes Not Syncing
576
+
577
+ **Solutions:**
578
+
579
+ 1. Verify onChange plugin is loaded:
580
+
581
+ ```bash
582
+ --plugin core/ldap/onChange
583
+ ```
584
+
585
+ 2. Check Cozy domain attribute is set for the user:
586
+
587
+ ```bash
588
+ ldapsearch -b "uid=jdoe,ou=users,dc=example,dc=com" twakeCozyDomain
589
+ ```
590
+
591
+ 3. Enable debug logging:
592
+
593
+ ```bash
594
+ --log-level debug
595
+ ```
596
+
597
+ 4. Verify Cozy Admin API is reachable:
598
+
599
+ ```bash
600
+ curl http://cozy-admin:6060/instances
601
+ ```
602
+
603
+ ### Problem: 404 Errors
604
+
605
+ **Symptoms:**
606
+
607
+ ```json
608
+ { "level": "debug", "message": "Cozy instance not found" }
609
+ ```
610
+
611
+ **Solutions:**
612
+
613
+ 1. Provision Cozy instance for the user first
614
+ 2. Verify `twakeCozyDomain` attribute matches the actual Cozy domain
615
+ 3. Check Cozy logs for instance existence
616
+
617
+ ### Problem: Authentication Errors
618
+
619
+ **Symptoms:**
620
+
621
+ ```json
622
+ { "http_status": 401, "http_status_text": "Unauthorized" }
623
+ ```
624
+
625
+ **Solutions:**
626
+
627
+ 1. Verify admin token is correct
628
+ 2. Check token hasn't expired
629
+ 3. Ensure Admin API is configured to accept bearer tokens
630
+
631
+ ## Integration with James
632
+
633
+ When using both James and Drive plugins, email changes are propagated to both services:
634
+
635
+ ```bash
636
+ --plugin twake/james \
637
+ --plugin twake/drive
638
+ ```
639
+
640
+ Both plugins listen to `onLdapMailChange` and update their respective services in parallel.
641
+
642
+ ## See Also
643
+
644
+ - [onChange.md](onChange.md) - LDAP change detection
645
+ - [james-mail.md](james-mail.md) - James mail server integration
646
+ - [Cozy Admin Documentation](https://docs.cozy.io/en/cozy-stack/admin/) - Admin API reference