@cyberismo/backend 0.0.21 → 0.0.23

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.
Files changed (152) hide show
  1. package/dist/app.d.ts +5 -2
  2. package/dist/app.js +25 -10
  3. package/dist/app.js.map +1 -1
  4. package/dist/auth/index.d.ts +16 -0
  5. package/dist/auth/index.js +15 -0
  6. package/dist/auth/index.js.map +1 -0
  7. package/dist/auth/keycloak.d.ts +27 -0
  8. package/dist/auth/keycloak.js +81 -0
  9. package/dist/auth/keycloak.js.map +1 -0
  10. package/dist/auth/mock.d.ts +23 -0
  11. package/dist/auth/mock.js +28 -0
  12. package/dist/auth/mock.js.map +1 -0
  13. package/dist/auth/types.d.ts +16 -0
  14. package/dist/auth/types.js +14 -0
  15. package/dist/auth/types.js.map +1 -0
  16. package/dist/domain/auth/index.d.ts +14 -0
  17. package/dist/domain/auth/index.js +30 -0
  18. package/dist/domain/auth/index.js.map +1 -0
  19. package/dist/domain/calculations/index.js +3 -1
  20. package/dist/domain/calculations/index.js.map +1 -1
  21. package/dist/domain/calculations/service.js +13 -11
  22. package/dist/domain/calculations/service.js.map +1 -1
  23. package/dist/domain/cardTypes/index.js +5 -3
  24. package/dist/domain/cardTypes/index.js.map +1 -1
  25. package/dist/domain/cardTypes/service.js +24 -72
  26. package/dist/domain/cardTypes/service.js.map +1 -1
  27. package/dist/domain/cards/index.js +124 -25
  28. package/dist/domain/cards/index.js.map +1 -1
  29. package/dist/domain/cards/lib.js +92 -93
  30. package/dist/domain/cards/lib.js.map +1 -1
  31. package/dist/domain/cards/presence.d.ts +50 -0
  32. package/dist/domain/cards/presence.js +93 -0
  33. package/dist/domain/cards/presence.js.map +1 -0
  34. package/dist/domain/cards/schema.d.ts +47 -0
  35. package/dist/domain/cards/schema.js +37 -0
  36. package/dist/domain/cards/schema.js.map +1 -0
  37. package/dist/domain/cards/service.d.ts +7 -3
  38. package/dist/domain/cards/service.js +81 -91
  39. package/dist/domain/cards/service.js.map +1 -1
  40. package/dist/domain/connectors/index.d.ts +15 -0
  41. package/dist/domain/connectors/index.js +37 -0
  42. package/dist/domain/connectors/index.js.map +1 -0
  43. package/dist/domain/connectors/service.d.ts +23 -0
  44. package/dist/domain/connectors/service.js +46 -0
  45. package/dist/domain/connectors/service.js.map +1 -0
  46. package/dist/domain/fieldTypes/index.js +4 -2
  47. package/dist/domain/fieldTypes/index.js.map +1 -1
  48. package/dist/domain/graphModels/index.js +3 -1
  49. package/dist/domain/graphModels/index.js.map +1 -1
  50. package/dist/domain/graphViews/index.js +3 -1
  51. package/dist/domain/graphViews/index.js.map +1 -1
  52. package/dist/domain/labels/index.js +4 -2
  53. package/dist/domain/labels/index.js.map +1 -1
  54. package/dist/domain/labels/service.d.ts +1 -1
  55. package/dist/domain/labels/service.js +2 -2
  56. package/dist/domain/labels/service.js.map +1 -1
  57. package/dist/domain/linkTypes/index.js +4 -2
  58. package/dist/domain/linkTypes/index.js.map +1 -1
  59. package/dist/domain/logicPrograms/index.js +3 -1
  60. package/dist/domain/logicPrograms/index.js.map +1 -1
  61. package/dist/domain/mcp/index.d.ts +15 -0
  62. package/dist/domain/mcp/index.js +127 -0
  63. package/dist/domain/mcp/index.js.map +1 -0
  64. package/dist/domain/project/index.js +19 -6
  65. package/dist/domain/project/index.js.map +1 -1
  66. package/dist/domain/project/schema.d.ts +3 -0
  67. package/dist/domain/project/schema.js +8 -0
  68. package/dist/domain/project/schema.js.map +1 -1
  69. package/dist/domain/project/service.d.ts +3 -1
  70. package/dist/domain/project/service.js +24 -14
  71. package/dist/domain/project/service.js.map +1 -1
  72. package/dist/domain/reports/index.js +3 -1
  73. package/dist/domain/reports/index.js.map +1 -1
  74. package/dist/domain/resources/index.js +6 -4
  75. package/dist/domain/resources/index.js.map +1 -1
  76. package/dist/domain/resources/service.js +66 -64
  77. package/dist/domain/resources/service.js.map +1 -1
  78. package/dist/domain/templates/index.js +5 -3
  79. package/dist/domain/templates/index.js.map +1 -1
  80. package/dist/domain/tree/index.js +3 -1
  81. package/dist/domain/tree/index.js.map +1 -1
  82. package/dist/domain/tree/service.js +0 -1
  83. package/dist/domain/tree/service.js.map +1 -1
  84. package/dist/domain/workflows/index.js +3 -1
  85. package/dist/domain/workflows/index.js.map +1 -1
  86. package/dist/export.d.ts +6 -5
  87. package/dist/export.js +16 -13
  88. package/dist/export.js.map +1 -1
  89. package/dist/index.d.ts +8 -2
  90. package/dist/index.js +12 -4
  91. package/dist/index.js.map +1 -1
  92. package/dist/main.js +29 -2
  93. package/dist/main.js.map +1 -1
  94. package/dist/middleware/auth.d.ts +40 -0
  95. package/dist/middleware/auth.js +68 -0
  96. package/dist/middleware/auth.js.map +1 -0
  97. package/dist/middleware/commandManager.d.ts +2 -2
  98. package/dist/middleware/commandManager.js +9 -11
  99. package/dist/middleware/commandManager.js.map +1 -1
  100. package/dist/public/THIRD-PARTY.txt +1212 -605
  101. package/dist/public/assets/index-Cdn_jRWy.js +720 -0
  102. package/dist/public/assets/index-ypsafPwV.css +1 -0
  103. package/dist/public/config.json +1 -0
  104. package/dist/public/images/broken_link.svg +7 -0
  105. package/dist/public/index.html +2 -2
  106. package/dist/types.d.ts +25 -0
  107. package/dist/types.js +13 -1
  108. package/dist/types.js.map +1 -1
  109. package/package.json +10 -7
  110. package/src/app.ts +37 -15
  111. package/src/auth/index.ts +17 -0
  112. package/src/auth/keycloak.ts +109 -0
  113. package/src/auth/mock.ts +38 -0
  114. package/src/auth/types.ts +18 -0
  115. package/src/domain/auth/index.ts +35 -0
  116. package/src/domain/calculations/index.ts +13 -6
  117. package/src/domain/calculations/service.ts +16 -14
  118. package/src/domain/cardTypes/index.ts +24 -16
  119. package/src/domain/cardTypes/service.ts +41 -95
  120. package/src/domain/cards/index.ts +258 -90
  121. package/src/domain/cards/lib.ts +102 -100
  122. package/src/domain/cards/presence.ts +124 -0
  123. package/src/domain/cards/schema.ts +41 -0
  124. package/src/domain/cards/service.ts +138 -93
  125. package/src/domain/connectors/index.ts +39 -0
  126. package/src/domain/connectors/service.ts +67 -0
  127. package/src/domain/fieldTypes/index.ts +23 -16
  128. package/src/domain/graphModels/index.ts +13 -6
  129. package/src/domain/graphViews/index.ts +13 -6
  130. package/src/domain/labels/index.ts +5 -2
  131. package/src/domain/labels/service.ts +2 -2
  132. package/src/domain/linkTypes/index.ts +14 -7
  133. package/src/domain/logicPrograms/index.ts +3 -0
  134. package/src/domain/mcp/index.ts +159 -0
  135. package/src/domain/project/index.ts +40 -9
  136. package/src/domain/project/schema.ts +9 -0
  137. package/src/domain/project/service.ts +37 -17
  138. package/src/domain/reports/index.ts +13 -6
  139. package/src/domain/resources/index.ts +6 -1
  140. package/src/domain/resources/service.ts +102 -97
  141. package/src/domain/templates/index.ts +31 -19
  142. package/src/domain/tree/index.ts +3 -1
  143. package/src/domain/tree/service.ts +0 -1
  144. package/src/domain/workflows/index.ts +13 -6
  145. package/src/export.ts +17 -15
  146. package/src/index.ts +18 -7
  147. package/src/main.ts +44 -2
  148. package/src/middleware/auth.ts +90 -0
  149. package/src/middleware/commandManager.ts +11 -14
  150. package/src/types.ts +27 -0
  151. package/dist/public/assets/index-CRSBseQM.css +0 -1
  152. package/dist/public/assets/index-Ca10XaMv.js +0 -164156
@@ -54,126 +54,72 @@ export async function updateFieldVisibility(
54
54
  ): Promise<void> {
55
55
  const { fieldName, group: targetGroup, index: targetIndex } = body;
56
56
 
57
- // Get current card type data
58
- const cardType = await commands.showCmd.showResource(
59
- cardTypeName,
60
- 'cardTypes',
61
- );
62
- if (!cardType) {
63
- throw new Error(`Card type '${cardTypeName}' not found`);
64
- }
57
+ await commands.atomic(async () => {
58
+ // Read is now inside the write lock
59
+ const cardType = await commands.showCmd.showResource(
60
+ cardTypeName,
61
+ 'cardTypes',
62
+ );
63
+ if (!cardType) {
64
+ throw new Error(`Card type '${cardTypeName}' not found`);
65
+ }
65
66
 
66
- const customFields = cardType.customFields || [];
67
- const alwaysVisibleFields = cardType.alwaysVisibleFields || [];
68
- const optionallyVisibleFields = cardType.optionallyVisibleFields || [];
67
+ const customFields = cardType.customFields || [];
68
+ const alwaysVisibleFields = cardType.alwaysVisibleFields || [];
69
+ const optionallyVisibleFields = cardType.optionallyVisibleFields || [];
69
70
 
70
- // Validate that the field exists in customFields
71
- const fieldExists = customFields.some(
72
- (f: { name: string }) => f.name === fieldName,
73
- );
74
- if (!fieldExists) {
75
- throw new Error(
76
- `Field '${fieldName}' does not exist in card type '${cardTypeName}'. `,
71
+ // Validate that the field exists in customFields
72
+ const fieldExists = customFields.some(
73
+ (f: { name: string }) => f.name === fieldName,
77
74
  );
78
- }
75
+ if (!fieldExists) {
76
+ throw new Error(
77
+ `Field '${fieldName}' does not exist in card type '${cardTypeName}'. `,
78
+ );
79
+ }
79
80
 
80
- const currentGroup = getCurrentGroup(
81
- alwaysVisibleFields,
82
- optionallyVisibleFields,
83
- fieldName,
84
- );
81
+ const currentGroup = getCurrentGroup(
82
+ alwaysVisibleFields,
83
+ optionallyVisibleFields,
84
+ fieldName,
85
+ );
85
86
 
86
- // If same group, just handle reordering
87
- if (currentGroup === targetGroup) {
88
- if (targetGroup === 'hidden') {
89
- // Nothing to reorder in hidden group
87
+ // If same group, just handle reordering
88
+ if (currentGroup === targetGroup) {
89
+ if (targetGroup !== 'hidden' && targetIndex !== undefined) {
90
+ await commands.updateCmd.applyResourceOperation(
91
+ cardTypeName,
92
+ { key: groupToKey[targetGroup] },
93
+ { name: 'rank', target: fieldName, newIndex: targetIndex },
94
+ );
95
+ }
90
96
  return;
91
97
  }
92
98
 
93
- if (targetIndex !== undefined) {
94
- await commands.updateCmd.applyResourceOperation(
95
- cardTypeName,
96
- {
97
- key: groupToKey[targetGroup],
98
- },
99
- {
100
- name: 'rank',
101
- target: fieldName,
102
- newIndex: targetIndex,
103
- },
104
- );
105
- }
106
- return;
107
- }
108
-
109
- // Different group - need to remove from old and add to new
110
- let removedFromOld = false;
111
-
112
- try {
113
99
  // Remove from current group (if not hidden)
114
100
  if (currentGroup !== 'hidden') {
115
101
  await commands.updateCmd.applyResourceOperation(
116
102
  cardTypeName,
117
- {
118
- key: groupToKey[currentGroup],
119
- },
120
- {
121
- name: 'remove',
122
- target: fieldName,
123
- },
103
+ { key: groupToKey[currentGroup] },
104
+ { name: 'remove', target: fieldName },
124
105
  );
125
- removedFromOld = true;
126
106
  }
127
107
 
128
108
  // Add to new group (if not hidden)
129
109
  if (targetGroup !== 'hidden') {
130
110
  await commands.updateCmd.applyResourceOperation(
131
111
  cardTypeName,
132
- {
133
- key: groupToKey[targetGroup],
134
- },
135
- {
136
- name: 'add',
137
- target: fieldName,
138
- },
112
+ { key: groupToKey[targetGroup] },
113
+ { name: 'add', target: fieldName },
139
114
  );
140
115
 
141
- // Reorder if index specified
142
116
  if (targetIndex !== undefined) {
143
117
  await commands.updateCmd.applyResourceOperation(
144
118
  cardTypeName,
145
- {
146
- key: groupToKey[targetGroup],
147
- },
148
- {
149
- name: 'rank',
150
- target: fieldName,
151
- newIndex: targetIndex,
152
- },
153
- );
154
- }
155
- }
156
- } catch (error) {
157
- // Attempt rollback if we removed from old group but failed to add to new
158
- if (removedFromOld && currentGroup !== 'hidden') {
159
- try {
160
- await commands.updateCmd.applyResourceOperation(
161
- cardTypeName,
162
- {
163
- key: groupToKey[currentGroup],
164
- },
165
- {
166
- name: 'add',
167
- target: fieldName,
168
- },
169
- );
170
- } catch {
171
- // Rollback failed - log but throw original error
172
- console.error(
173
- `Rollback failed for field '${fieldName}' in card type '${cardTypeName}'`,
119
+ { key: groupToKey[targetGroup] },
120
+ { name: 'rank', target: fieldName, newIndex: targetIndex },
174
121
  );
175
122
  }
176
123
  }
177
- throw error;
178
- }
124
+ }, `Update field visibility for ${cardTypeName}`);
179
125
  }
@@ -13,10 +13,20 @@
13
13
 
14
14
  import { type Context, Hono } from 'hono';
15
15
  import type { ContentfulStatusCode } from 'hono/utils/http-status';
16
+ import { streamSSE } from 'hono/streaming';
16
17
  import { getCardDetails } from './lib.js';
17
18
  import * as cardService from './service.js';
18
19
  import { isSSGContext, ssgParams } from 'hono/ssg';
19
20
  import type { AppContext } from '../../types.js';
21
+ import { UserRole } from '../../types.js';
22
+ import { presenceStore } from './presence.js';
23
+ import { requireRole } from '../../middleware/auth.js';
24
+ import { zValidator } from '../../middleware/zvalidator.js';
25
+ import {
26
+ createLinkSchema,
27
+ removeLinkSchema,
28
+ updateLinkSchema,
29
+ } from './schema.js';
20
30
 
21
31
  const router = new Hono();
22
32
 
@@ -34,7 +44,7 @@ const router = new Hono();
34
44
  * 500:
35
45
  * description: project_path not set.
36
46
  */
37
- router.get('/', async (c) => {
47
+ router.get('/', requireRole(UserRole.Reader), async (c) => {
38
48
  const commands = c.get('commands');
39
49
 
40
50
  try {
@@ -75,6 +85,7 @@ router.get('/', async (c) => {
75
85
  */
76
86
  router.get(
77
87
  '/:key',
88
+ requireRole(UserRole.Reader),
78
89
  ssgParams(async (c: AppContext) => {
79
90
  const commands = c.get('commands');
80
91
  const opts = c.get('tree');
@@ -144,7 +155,7 @@ router.get(
144
155
  * 500:
145
156
  * description: project_path not set.
146
157
  */
147
- router.patch('/:key', async (c) => {
158
+ router.patch('/:key', requireRole(UserRole.Editor), async (c) => {
148
159
  const commands = c.get('commands');
149
160
  const key = c.req.param('key');
150
161
  if (!key) {
@@ -201,7 +212,7 @@ router.patch('/:key', async (c) => {
201
212
  * 500:
202
213
  * description: project_path not set.
203
214
  */
204
- router.delete('/:key', async (c) => {
215
+ router.delete('/:key', requireRole(UserRole.Editor), async (c) => {
205
216
  const commands = c.get('commands');
206
217
  const key = c.req.param('key');
207
218
  if (!key) {
@@ -244,7 +255,7 @@ router.delete('/:key', async (c) => {
244
255
  * 500:
245
256
  * description: project_path not set
246
257
  */
247
- router.post('/:key', async (c) => {
258
+ router.post('/:key', requireRole(UserRole.Editor), async (c) => {
248
259
  const key = c.req.param('key');
249
260
  if (!key) {
250
261
  return c.text('No search key', 400);
@@ -300,7 +311,7 @@ router.post('/:key', async (c) => {
300
311
  * 500:
301
312
  * description: Server error
302
313
  */
303
- router.post('/:key/attachments', async (c) => {
314
+ router.post('/:key/attachments', requireRole(UserRole.Editor), async (c) => {
304
315
  const commands = c.get('commands');
305
316
  const key = c.req.param('key');
306
317
 
@@ -354,25 +365,33 @@ router.post('/:key/attachments', async (c) => {
354
365
  * 500:
355
366
  * description: Server error
356
367
  */
357
- router.delete('/:key/attachments/:filename', async (c) => {
358
- const commands = c.get('commands');
359
- const { key, filename } = c.req.param();
368
+ router.delete(
369
+ '/:key/attachments/:filename',
370
+ requireRole(UserRole.Editor),
371
+ async (c) => {
372
+ const commands = c.get('commands');
373
+ const { key, filename } = c.req.param();
360
374
 
361
- try {
362
- const result = await cardService.removeAttachment(commands, key, filename);
363
- return c.json(result);
364
- } catch (error) {
365
- return c.json(
366
- {
367
- error:
368
- error instanceof Error
369
- ? error.message
370
- : 'Failed to remove attachment',
371
- },
372
- 500,
373
- );
374
- }
375
- });
375
+ try {
376
+ const result = await cardService.removeAttachment(
377
+ commands,
378
+ key,
379
+ filename,
380
+ );
381
+ return c.json(result);
382
+ } catch (error) {
383
+ return c.json(
384
+ {
385
+ error:
386
+ error instanceof Error
387
+ ? error.message
388
+ : 'Failed to remove attachment',
389
+ },
390
+ 500,
391
+ );
392
+ }
393
+ },
394
+ );
376
395
 
377
396
  /**
378
397
  * @swagger
@@ -398,23 +417,29 @@ router.delete('/:key/attachments/:filename', async (c) => {
398
417
  * 500:
399
418
  * description: Server error
400
419
  */
401
- router.post('/:key/attachments/:filename/open', async (c) => {
402
- const commands = c.get('commands');
403
- const { key, filename } = c.req.param();
420
+ router.post(
421
+ '/:key/attachments/:filename/open',
422
+ requireRole(UserRole.Reader),
423
+ async (c) => {
424
+ const commands = c.get('commands');
425
+ const { key, filename } = c.req.param();
404
426
 
405
- try {
406
- const result = await cardService.openAttachment(commands, key, filename);
407
- return c.json(result);
408
- } catch (error) {
409
- return c.json(
410
- {
411
- error:
412
- error instanceof Error ? error.message : 'Failed to open attachment',
413
- },
414
- 500,
415
- );
416
- }
417
- });
427
+ try {
428
+ const result = await cardService.openAttachment(commands, key, filename);
429
+ return c.json(result);
430
+ } catch (error) {
431
+ return c.json(
432
+ {
433
+ error:
434
+ error instanceof Error
435
+ ? error.message
436
+ : 'Failed to open attachment',
437
+ },
438
+ 500,
439
+ );
440
+ }
441
+ },
442
+ );
418
443
 
419
444
  /**
420
445
  * @swagger
@@ -443,7 +468,7 @@ router.post('/:key/attachments/:filename/open', async (c) => {
443
468
  * 500:
444
469
  * description: Server error
445
470
  */
446
- router.post('/:key/parse', async (c) => {
471
+ router.post('/:key/parse', requireRole(UserRole.Reader), async (c) => {
447
472
  const commands = c.get('commands');
448
473
  const key = c.req.param('key');
449
474
  const { content } = await c.req.json();
@@ -497,33 +522,36 @@ router.post('/:key/parse', async (c) => {
497
522
  * 500:
498
523
  * description: Server error
499
524
  */
500
- router.post('/:key/links', async (c) => {
501
- const commands = c.get('commands');
502
- const key = c.req.param('key');
503
- const { toCard, linkType, description } = await c.req.json();
504
-
505
- if (!toCard || !linkType) {
506
- return c.json({ error: 'toCard and linkType are required' }, 400);
507
- }
525
+ router.post(
526
+ '/:key/links',
527
+ requireRole(UserRole.Editor),
528
+ zValidator('json', createLinkSchema),
529
+ async (c) => {
530
+ const commands = c.get('commands');
531
+ const key = c.req.param('key');
532
+ const { toCard, linkType, description, direction } = c.req.valid('json');
508
533
 
509
- try {
510
- const result = await cardService.createLink(
511
- commands,
512
- key,
513
- toCard,
514
- linkType,
515
- description,
516
- );
517
- return c.json(result);
518
- } catch (error) {
519
- return c.json(
520
- {
521
- error: error instanceof Error ? error.message : 'Failed to create link',
522
- },
523
- 500,
524
- );
525
- }
526
- });
534
+ try {
535
+ const result = await cardService.createLink(
536
+ commands,
537
+ key,
538
+ toCard,
539
+ linkType,
540
+ direction,
541
+ description,
542
+ );
543
+ return c.json(result);
544
+ } catch (error) {
545
+ return c.json(
546
+ {
547
+ error:
548
+ error instanceof Error ? error.message : 'Failed to create link',
549
+ },
550
+ 500,
551
+ );
552
+ }
553
+ },
554
+ );
527
555
 
528
556
  /**
529
557
  * @swagger
@@ -556,33 +584,122 @@ router.post('/:key/links', async (c) => {
556
584
  * 500:
557
585
  * description: Server error
558
586
  */
559
- router.delete('/:key/links', async (c) => {
560
- const commands = c.get('commands');
561
- const key = c.req.param('key');
562
- const { toCard, linkType, description } = await c.req.json();
587
+ router.delete(
588
+ '/:key/links',
589
+ requireRole(UserRole.Editor),
590
+ zValidator('json', removeLinkSchema),
591
+ async (c) => {
592
+ const commands = c.get('commands');
593
+ const key = c.req.param('key');
594
+ const { toCard, linkType, description, direction } = c.req.valid('json');
563
595
 
564
- if (!toCard || !linkType) {
565
- return c.json({ error: 'toCard and linkType are required' }, 400);
566
- }
596
+ try {
597
+ const result = await cardService.removeLink(
598
+ commands,
599
+ key,
600
+ toCard,
601
+ linkType,
602
+ direction,
603
+ description,
604
+ );
605
+ return c.json(result);
606
+ } catch (error) {
607
+ return c.json(
608
+ {
609
+ error:
610
+ error instanceof Error ? error.message : 'Failed to remove link',
611
+ },
612
+ 500,
613
+ );
614
+ }
615
+ },
616
+ );
567
617
 
568
- try {
569
- const result = await cardService.removeLink(
570
- commands,
571
- key,
618
+ /**
619
+ * @swagger
620
+ * /api/cards/{key}/links:
621
+ * put:
622
+ * summary: Update a link between cards
623
+ * parameters:
624
+ * - name: key
625
+ * in: path
626
+ * required: true
627
+ * schema:
628
+ * type: string
629
+ * requestBody:
630
+ * content:
631
+ * application/json:
632
+ * schema:
633
+ * type: object
634
+ * properties:
635
+ * toCard:
636
+ * type: string
637
+ * linkType:
638
+ * type: string
639
+ * description:
640
+ * type: string
641
+ * direction:
642
+ * type: string
643
+ * previousToCard:
644
+ * type: string
645
+ * previousLinkType:
646
+ * type: string
647
+ * previousDirection:
648
+ * type: string
649
+ * previousDescription:
650
+ * type: string
651
+ * responses:
652
+ * 200:
653
+ * description: Link updated successfully
654
+ * 400:
655
+ * description: Invalid request
656
+ * 500:
657
+ * description: Server error
658
+ */
659
+ router.put(
660
+ '/:key/links',
661
+ requireRole(UserRole.Editor),
662
+ zValidator('json', updateLinkSchema),
663
+ async (c) => {
664
+ const commands = c.get('commands');
665
+ const key = c.req.param('key');
666
+
667
+ const {
572
668
  toCard,
573
669
  linkType,
574
670
  description,
575
- );
576
- return c.json(result);
577
- } catch (error) {
578
- return c.json(
579
- {
580
- error: error instanceof Error ? error.message : 'Failed to remove link',
581
- },
582
- 500,
583
- );
584
- }
585
- });
671
+ direction,
672
+ previousToCard,
673
+ previousLinkType,
674
+ previousDirection,
675
+ previousDescription,
676
+ } = c.req.valid('json');
677
+
678
+ try {
679
+ const result = await cardService.updateLink(
680
+ commands,
681
+ key,
682
+ toCard,
683
+ linkType,
684
+ direction,
685
+ previousToCard,
686
+ previousLinkType,
687
+ previousDirection,
688
+ description,
689
+ previousDescription,
690
+ );
691
+ return c.json(result);
692
+ } catch (error) {
693
+ return c.json(
694
+ {
695
+ error:
696
+ error instanceof Error ? error.message : 'Failed to update link',
697
+ },
698
+ 500,
699
+ );
700
+ }
701
+ },
702
+ );
586
703
 
587
704
  /**
588
705
  * @swagger
@@ -610,11 +727,12 @@ router.delete('/:key/links', async (c) => {
610
727
  */
611
728
  router.get(
612
729
  '/:key/a/:attachment',
730
+ requireRole(UserRole.Reader),
613
731
  ssgParams(async (c: Context) => {
614
732
  const commands = c.get('commands');
615
733
  return await cardService.findRelevantAttachments(commands, c.get('tree'));
616
734
  }),
617
- (c) => {
735
+ async (c) => {
618
736
  const commands = c.get('commands');
619
737
  const { key, attachment } = c.req.param();
620
738
  const filename = decodeURI(attachment);
@@ -624,7 +742,7 @@ router.get(
624
742
  }
625
743
 
626
744
  try {
627
- const attachmentResponse = cardService.getAttachment(
745
+ const attachmentResponse = await cardService.getAttachment(
628
746
  commands,
629
747
  key,
630
748
  filename,
@@ -652,5 +770,55 @@ router.get(
652
770
  }
653
771
  },
654
772
  );
773
+ /**
774
+ * @swagger
775
+ * /api/cards/{key}/presence:
776
+ * get:
777
+ * summary: SSE stream of users currently viewing or editing this card
778
+ * parameters:
779
+ * - name: key
780
+ * in: path
781
+ * required: true
782
+ * description: Card key (string)
783
+ * - name: mode
784
+ * in: query
785
+ * required: false
786
+ * schema:
787
+ * type: string
788
+ * enum: [viewing, editing]
789
+ * description: Whether the user is viewing or editing (default: viewing)
790
+ * responses:
791
+ * 200:
792
+ * description: SSE stream with presence events
793
+ */
794
+ router.get('/:key/presence', requireRole(UserRole.Reader), (c) => {
795
+ const key = c.req.param('key');
796
+ const mode = c.req.query('mode') === 'editing' ? 'editing' : 'viewing';
797
+ const user = c.get('user');
798
+
799
+ if (!key) {
800
+ return c.text('No card key', 400);
801
+ }
655
802
 
803
+ return streamSSE(c, async (stream) => {
804
+ const connId = presenceStore.add(
805
+ key,
806
+ user,
807
+ mode,
808
+ (data) => void stream.writeSSE(data),
809
+ );
810
+
811
+ let aborted = false;
812
+ stream.onAbort(() => {
813
+ aborted = true;
814
+ presenceStore.remove(key, connId);
815
+ });
816
+
817
+ // Keep connection alive with periodic heartbeat
818
+ while (!aborted) {
819
+ await stream.write(': hb\n\n');
820
+ await stream.sleep(30000);
821
+ }
822
+ });
823
+ });
656
824
  export default router;