@kelpie/server 0.5.1 → 0.6.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 +5 -2
- package/dist/index.d.ts +17 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -4
- package/dist/index.js.map +1 -1
- package/dist/lib/actor.d.ts +9 -1
- package/dist/lib/actor.d.ts.map +1 -1
- package/dist/lib/actor.js +13 -0
- package/dist/lib/actor.js.map +1 -1
- package/dist/lib/config.d.ts +44 -6
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +9 -5
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/email.d.ts +15 -63
- package/dist/lib/email.d.ts.map +1 -1
- package/dist/lib/email.js +13 -65
- package/dist/lib/email.js.map +1 -1
- package/dist/lib/fromEnv.d.ts +60 -0
- package/dist/lib/fromEnv.d.ts.map +1 -0
- package/dist/lib/fromEnv.js +94 -0
- package/dist/lib/fromEnv.js.map +1 -0
- package/dist/lib/ids.d.ts +6 -0
- package/dist/lib/ids.d.ts.map +1 -1
- package/dist/lib/ids.js +6 -0
- package/dist/lib/ids.js.map +1 -1
- package/dist/lib/kelpieConfigFile.d.ts +123 -0
- package/dist/lib/kelpieConfigFile.d.ts.map +1 -0
- package/dist/lib/kelpieConfigFile.js +111 -0
- package/dist/lib/kelpieConfigFile.js.map +1 -0
- package/dist/lib/logger.d.ts +38 -7
- package/dist/lib/logger.d.ts.map +1 -1
- package/dist/lib/logger.js +58 -20
- package/dist/lib/logger.js.map +1 -1
- package/dist/modules/agent-tasks/index.d.ts.map +1 -1
- package/dist/modules/agent-tasks/index.js +4 -1
- package/dist/modules/agent-tasks/index.js.map +1 -1
- package/dist/modules/agent-tasks/prompt.d.ts +36 -6
- package/dist/modules/agent-tasks/prompt.d.ts.map +1 -1
- package/dist/modules/agent-tasks/prompt.js +33 -4
- package/dist/modules/agent-tasks/prompt.js.map +1 -1
- package/dist/modules/agent-tasks/service.d.ts.map +1 -1
- package/dist/modules/agent-tasks/service.js +3 -2
- package/dist/modules/agent-tasks/service.js.map +1 -1
- package/dist/modules/agent-tasks/wire.d.ts +11 -0
- package/dist/modules/agent-tasks/wire.d.ts.map +1 -1
- package/dist/modules/agent-tasks/wire.js +1 -0
- package/dist/modules/agent-tasks/wire.js.map +1 -1
- package/dist/modules/auth/index.d.ts.map +1 -1
- package/dist/modules/auth/index.js +8 -7
- package/dist/modules/auth/index.js.map +1 -1
- package/dist/modules/companies/events.d.ts +22 -0
- package/dist/modules/companies/events.d.ts.map +1 -0
- package/dist/modules/companies/events.js +8 -0
- package/dist/modules/companies/events.js.map +1 -0
- package/dist/modules/companies/index.d.ts.map +1 -1
- package/dist/modules/companies/index.js +2 -0
- package/dist/modules/companies/index.js.map +1 -1
- package/dist/modules/companies/service.d.ts +1 -0
- package/dist/modules/companies/service.d.ts.map +1 -1
- package/dist/modules/companies/service.js +8 -11
- package/dist/modules/companies/service.js.map +1 -1
- package/dist/modules/core.d.ts.map +1 -1
- package/dist/modules/core.js +4 -0
- package/dist/modules/core.js.map +1 -1
- package/dist/modules/deals/events.d.ts +31 -0
- package/dist/modules/deals/events.d.ts.map +1 -0
- package/dist/modules/deals/events.js +12 -0
- package/dist/modules/deals/events.js.map +1 -0
- package/dist/modules/deals/index.d.ts.map +1 -1
- package/dist/modules/deals/index.js +2 -0
- package/dist/modules/deals/index.js.map +1 -1
- package/dist/modules/deals/service.d.ts +1 -0
- package/dist/modules/deals/service.d.ts.map +1 -1
- package/dist/modules/deals/service.js +9 -18
- package/dist/modules/deals/service.js.map +1 -1
- package/dist/modules/decisions/events.d.ts +21 -0
- package/dist/modules/decisions/events.d.ts.map +1 -0
- package/dist/modules/decisions/events.js +11 -0
- package/dist/modules/decisions/events.js.map +1 -0
- package/dist/modules/decisions/index.d.ts.map +1 -1
- package/dist/modules/decisions/index.js +2 -0
- package/dist/modules/decisions/index.js.map +1 -1
- package/dist/modules/decisions/service.d.ts +1 -0
- package/dist/modules/decisions/service.d.ts.map +1 -1
- package/dist/modules/decisions/service.js +4 -7
- package/dist/modules/decisions/service.js.map +1 -1
- package/dist/modules/forms/events.d.ts +37 -0
- package/dist/modules/forms/events.d.ts.map +1 -0
- package/dist/modules/forms/events.js +15 -0
- package/dist/modules/forms/events.js.map +1 -0
- package/dist/modules/forms/index.d.ts.map +1 -1
- package/dist/modules/forms/index.js +2 -0
- package/dist/modules/forms/index.js.map +1 -1
- package/dist/modules/forms/service.d.ts +1 -0
- package/dist/modules/forms/service.d.ts.map +1 -1
- package/dist/modules/forms/service.js +8 -11
- package/dist/modules/forms/service.js.map +1 -1
- package/dist/modules/forms/submission.d.ts +5 -0
- package/dist/modules/forms/submission.d.ts.map +1 -1
- package/dist/modules/forms/submission.js +54 -28
- package/dist/modules/forms/submission.js.map +1 -1
- package/dist/modules/handbook/events.d.ts +28 -0
- package/dist/modules/handbook/events.d.ts.map +1 -0
- package/dist/modules/handbook/events.js +14 -0
- package/dist/modules/handbook/events.js.map +1 -0
- package/dist/modules/handbook/index.d.ts.map +1 -1
- package/dist/modules/handbook/index.js +2 -0
- package/dist/modules/handbook/index.js.map +1 -1
- package/dist/modules/handbook/service.d.ts +1 -0
- package/dist/modules/handbook/service.d.ts.map +1 -1
- package/dist/modules/handbook/service.js +8 -15
- package/dist/modules/handbook/service.js.map +1 -1
- package/dist/modules/hiring/candidates.d.ts +1 -0
- package/dist/modules/hiring/candidates.d.ts.map +1 -1
- package/dist/modules/hiring/candidates.js +8 -11
- package/dist/modules/hiring/candidates.js.map +1 -1
- package/dist/modules/hiring/events.d.ts +35 -0
- package/dist/modules/hiring/events.d.ts.map +1 -0
- package/dist/modules/hiring/events.js +11 -0
- package/dist/modules/hiring/events.js.map +1 -0
- package/dist/modules/hiring/index.d.ts.map +1 -1
- package/dist/modules/hiring/index.js +2 -0
- package/dist/modules/hiring/index.js.map +1 -1
- package/dist/modules/hiring/roles.d.ts +1 -0
- package/dist/modules/hiring/roles.d.ts.map +1 -1
- package/dist/modules/hiring/roles.js +8 -11
- package/dist/modules/hiring/roles.js.map +1 -1
- package/dist/modules/import-export/events.d.ts +22 -0
- package/dist/modules/import-export/events.d.ts.map +1 -0
- package/dist/modules/import-export/events.js +12 -0
- package/dist/modules/import-export/events.js.map +1 -0
- package/dist/modules/import-export/index.d.ts.map +1 -1
- package/dist/modules/import-export/index.js +2 -0
- package/dist/modules/import-export/index.js.map +1 -1
- package/dist/modules/import-export/service.d.ts +5 -0
- package/dist/modules/import-export/service.d.ts.map +1 -1
- package/dist/modules/import-export/service.js +53 -14
- package/dist/modules/import-export/service.js.map +1 -1
- package/dist/modules/notes/events.d.ts +22 -0
- package/dist/modules/notes/events.d.ts.map +1 -0
- package/dist/modules/notes/events.js +12 -0
- package/dist/modules/notes/events.js.map +1 -0
- package/dist/modules/notes/index.d.ts.map +1 -1
- package/dist/modules/notes/index.js +2 -0
- package/dist/modules/notes/index.js.map +1 -1
- package/dist/modules/notes/service.d.ts +1 -0
- package/dist/modules/notes/service.d.ts.map +1 -1
- package/dist/modules/notes/service.js +4 -7
- package/dist/modules/notes/service.js.map +1 -1
- package/dist/modules/opportunities/events.d.ts +31 -0
- package/dist/modules/opportunities/events.d.ts.map +1 -0
- package/dist/modules/opportunities/events.js +12 -0
- package/dist/modules/opportunities/events.js.map +1 -0
- package/dist/modules/opportunities/index.d.ts.map +1 -1
- package/dist/modules/opportunities/index.js +2 -0
- package/dist/modules/opportunities/index.js.map +1 -1
- package/dist/modules/opportunities/service.d.ts +1 -0
- package/dist/modules/opportunities/service.d.ts.map +1 -1
- package/dist/modules/opportunities/service.js +9 -18
- package/dist/modules/opportunities/service.js.map +1 -1
- package/dist/modules/partnerships/events.d.ts +31 -0
- package/dist/modules/partnerships/events.d.ts.map +1 -0
- package/dist/modules/partnerships/events.js +12 -0
- package/dist/modules/partnerships/events.js.map +1 -0
- package/dist/modules/partnerships/index.d.ts.map +1 -1
- package/dist/modules/partnerships/index.js +2 -0
- package/dist/modules/partnerships/index.js.map +1 -1
- package/dist/modules/partnerships/service.d.ts +1 -0
- package/dist/modules/partnerships/service.d.ts.map +1 -1
- package/dist/modules/partnerships/service.js +9 -18
- package/dist/modules/partnerships/service.js.map +1 -1
- package/dist/modules/people/events.d.ts +28 -0
- package/dist/modules/people/events.d.ts.map +1 -0
- package/dist/modules/people/events.js +18 -0
- package/dist/modules/people/events.js.map +1 -0
- package/dist/modules/people/index.d.ts.map +1 -1
- package/dist/modules/people/index.js +2 -0
- package/dist/modules/people/index.js.map +1 -1
- package/dist/modules/people/service.d.ts +1 -0
- package/dist/modules/people/service.d.ts.map +1 -1
- package/dist/modules/people/service.js +8 -11
- package/dist/modules/people/service.js.map +1 -1
- package/dist/modules/pipelines/service.d.ts +4 -0
- package/dist/modules/pipelines/service.d.ts.map +1 -1
- package/dist/modules/pipelines/service.js +31 -8
- package/dist/modules/pipelines/service.js.map +1 -1
- package/dist/modules/plans/events.d.ts +21 -0
- package/dist/modules/plans/events.d.ts.map +1 -0
- package/dist/modules/plans/events.js +11 -0
- package/dist/modules/plans/events.js.map +1 -0
- package/dist/modules/plans/index.d.ts.map +1 -1
- package/dist/modules/plans/index.js +2 -0
- package/dist/modules/plans/index.js.map +1 -1
- package/dist/modules/plans/service.d.ts +1 -0
- package/dist/modules/plans/service.d.ts.map +1 -1
- package/dist/modules/plans/service.js +6 -14
- package/dist/modules/plans/service.js.map +1 -1
- package/dist/modules/positions/events.d.ts +22 -0
- package/dist/modules/positions/events.d.ts.map +1 -0
- package/dist/modules/positions/events.js +8 -0
- package/dist/modules/positions/events.js.map +1 -0
- package/dist/modules/positions/index.d.ts.map +1 -1
- package/dist/modules/positions/index.js +2 -0
- package/dist/modules/positions/index.js.map +1 -1
- package/dist/modules/positions/service.d.ts +1 -0
- package/dist/modules/positions/service.d.ts.map +1 -1
- package/dist/modules/positions/service.js +8 -11
- package/dist/modules/positions/service.js.map +1 -1
- package/dist/modules/raises/events.d.ts +31 -0
- package/dist/modules/raises/events.d.ts.map +1 -0
- package/dist/modules/raises/events.js +12 -0
- package/dist/modules/raises/events.js.map +1 -0
- package/dist/modules/raises/index.d.ts.map +1 -1
- package/dist/modules/raises/index.js +2 -0
- package/dist/modules/raises/index.js.map +1 -1
- package/dist/modules/raises/service.d.ts +1 -0
- package/dist/modules/raises/service.d.ts.map +1 -1
- package/dist/modules/raises/service.js +9 -18
- package/dist/modules/raises/service.js.map +1 -1
- package/dist/modules/reseal.d.ts +55 -0
- package/dist/modules/reseal.d.ts.map +1 -1
- package/dist/modules/reseal.js +90 -1
- package/dist/modules/reseal.js.map +1 -1
- package/dist/modules/smtp-email/index.d.ts +69 -0
- package/dist/modules/smtp-email/index.d.ts.map +1 -0
- package/dist/modules/smtp-email/index.js +82 -0
- package/dist/modules/smtp-email/index.js.map +1 -0
- package/dist/modules/webhooks/index.d.ts.map +1 -1
- package/dist/modules/webhooks/index.js +5 -1
- package/dist/modules/webhooks/index.js.map +1 -1
- package/dist/modules/webhooks/payloads.d.ts +22 -7
- package/dist/modules/webhooks/payloads.d.ts.map +1 -1
- package/dist/modules/webhooks/payloads.js +63 -44
- package/dist/modules/webhooks/payloads.js.map +1 -1
- package/dist/modules/workspace/events.d.ts +52 -0
- package/dist/modules/workspace/events.d.ts.map +1 -0
- package/dist/modules/workspace/events.js +16 -0
- package/dist/modules/workspace/events.js.map +1 -0
- package/dist/modules/workspace/index.d.ts.map +1 -1
- package/dist/modules/workspace/index.js +6 -2
- package/dist/modules/workspace/index.js.map +1 -1
- package/dist/modules/workspace/service.d.ts +1 -0
- package/dist/modules/workspace/service.d.ts.map +1 -1
- package/dist/modules/workspace/service.js +13 -15
- package/dist/modules/workspace/service.js.map +1 -1
- package/dist/modules/workspace/workspaceAccessMiddleware.d.ts.map +1 -1
- package/dist/modules/workspace/workspaceAccessMiddleware.js +11 -3
- package/dist/modules/workspace/workspaceAccessMiddleware.js.map +1 -1
- package/dist/runtime/events.d.ts +86 -108
- package/dist/runtime/events.d.ts.map +1 -1
- package/dist/runtime/events.js +123 -52
- package/dist/runtime/events.js.map +1 -1
- package/dist/runtime/module.d.ts +54 -2
- package/dist/runtime/module.d.ts.map +1 -1
- package/dist/runtime/registry.d.ts +23 -0
- package/dist/runtime/registry.d.ts.map +1 -1
- package/dist/runtime/registry.js +102 -2
- package/dist/runtime/registry.js.map +1 -1
- package/dist/runtime/transaction.d.ts +29 -4
- package/dist/runtime/transaction.d.ts.map +1 -1
- package/dist/runtime/transaction.js +48 -7
- package/dist/runtime/transaction.js.map +1 -1
- package/dist/testing/app.d.ts.map +1 -1
- package/dist/testing/app.js +11 -3
- package/dist/testing/app.js.map +1 -1
- package/dist/testing/database.d.ts.map +1 -1
- package/dist/testing/database.js +6 -3
- package/dist/testing/database.js.map +1 -1
- package/dist/testing/services.d.ts +21 -1
- package/dist/testing/services.d.ts.map +1 -1
- package/dist/testing/services.js +20 -5
- package/dist/testing/services.js.map +1 -1
- package/package.json +5 -2
- package/src/index.ts +44 -9
- package/src/lib/actor.ts +15 -1
- package/src/lib/config.ts +56 -7
- package/src/lib/email.ts +22 -92
- package/src/lib/fromEnv.ts +143 -0
- package/src/lib/ids.ts +6 -0
- package/src/lib/kelpieConfigFile.ts +283 -0
- package/src/lib/logger.ts +92 -34
- package/src/modules/agent-tasks/index.ts +4 -1
- package/src/modules/agent-tasks/prompt.ts +57 -9
- package/src/modules/agent-tasks/service.ts +3 -2
- package/src/modules/agent-tasks/wire.ts +12 -0
- package/src/modules/auth/index.ts +11 -9
- package/src/modules/companies/events.ts +25 -0
- package/src/modules/companies/index.ts +2 -0
- package/src/modules/companies/service.ts +8 -11
- package/src/modules/core.ts +4 -0
- package/src/modules/deals/events.ts +34 -0
- package/src/modules/deals/index.ts +2 -0
- package/src/modules/deals/service.ts +17 -18
- package/src/modules/decisions/events.ts +24 -0
- package/src/modules/decisions/index.ts +2 -0
- package/src/modules/decisions/service.ts +8 -7
- package/src/modules/forms/events.ts +37 -0
- package/src/modules/forms/index.ts +2 -0
- package/src/modules/forms/service.ts +12 -11
- package/src/modules/forms/submission.ts +80 -29
- package/src/modules/handbook/events.ts +31 -0
- package/src/modules/handbook/index.ts +2 -0
- package/src/modules/handbook/service.ts +8 -15
- package/src/modules/hiring/candidates.ts +8 -11
- package/src/modules/hiring/events.ts +36 -0
- package/src/modules/hiring/index.ts +2 -0
- package/src/modules/hiring/roles.ts +8 -11
- package/src/modules/import-export/events.ts +25 -0
- package/src/modules/import-export/index.ts +2 -0
- package/src/modules/import-export/service.ts +83 -15
- package/src/modules/notes/events.ts +25 -0
- package/src/modules/notes/index.ts +2 -0
- package/src/modules/notes/service.ts +8 -7
- package/src/modules/opportunities/events.ts +34 -0
- package/src/modules/opportunities/index.ts +2 -0
- package/src/modules/opportunities/service.ts +17 -18
- package/src/modules/partnerships/events.ts +34 -0
- package/src/modules/partnerships/index.ts +2 -0
- package/src/modules/partnerships/service.ts +17 -18
- package/src/modules/people/events.ts +36 -0
- package/src/modules/people/index.ts +2 -0
- package/src/modules/people/service.ts +97 -87
- package/src/modules/pipelines/service.ts +47 -8
- package/src/modules/plans/events.ts +24 -0
- package/src/modules/plans/index.ts +2 -0
- package/src/modules/plans/service.ts +14 -14
- package/src/modules/positions/events.ts +25 -0
- package/src/modules/positions/index.ts +2 -0
- package/src/modules/positions/service.ts +8 -11
- package/src/modules/raises/events.ts +34 -0
- package/src/modules/raises/index.ts +2 -0
- package/src/modules/raises/service.ts +17 -18
- package/src/modules/reseal.ts +144 -1
- package/src/modules/smtp-email/index.ts +119 -0
- package/src/modules/webhooks/index.ts +5 -1
- package/src/modules/webhooks/payloads.ts +94 -48
- package/src/modules/workspace/events.ts +46 -0
- package/src/modules/workspace/index.ts +6 -2
- package/src/modules/workspace/service.ts +37 -15
- package/src/modules/workspace/workspaceAccessMiddleware.ts +12 -3
- package/src/runtime/events.ts +268 -107
- package/src/runtime/module.ts +55 -2
- package/src/runtime/registry.ts +169 -1
- package/src/runtime/transaction.ts +94 -17
- package/src/testing/app.ts +11 -3
- package/src/testing/database.ts +6 -3
- package/src/testing/services.ts +34 -5
|
@@ -14,8 +14,10 @@ import {
|
|
|
14
14
|
describeUpdate,
|
|
15
15
|
} from '../activities/wording.ts'
|
|
16
16
|
import type { FieldLabels } from '../activities/wording.ts'
|
|
17
|
+
import { toEventActor } from '../../lib/actor.ts'
|
|
17
18
|
import type { Actor } from '../auth/actor.ts'
|
|
18
19
|
import { actorMemberId, requireWorkspaceId } from '../auth/actor.ts'
|
|
20
|
+
import './events.ts'
|
|
19
21
|
import { deleteRecordsAttachedTo } from '../attachedRecords.ts'
|
|
20
22
|
import * as companyRepository from '../companies/repository.ts'
|
|
21
23
|
import * as pipelineRepository from '../pipelines/repository.ts'
|
|
@@ -301,10 +303,10 @@ export function createRaisesService(dependencies: RaisesDependencies): RaisesSer
|
|
|
301
303
|
})
|
|
302
304
|
}
|
|
303
305
|
|
|
304
|
-
events.emit('
|
|
306
|
+
events.emit('raises.raise.created', { type: 'raise', id }, {})
|
|
305
307
|
|
|
306
308
|
return toView(created, personIds)
|
|
307
|
-
})
|
|
309
|
+
}, { workspaceId, actor: toEventActor(actor) })
|
|
308
310
|
},
|
|
309
311
|
|
|
310
312
|
async update(actor, id, changes) {
|
|
@@ -402,25 +404,22 @@ export function createRaisesService(dependencies: RaisesDependencies): RaisesSer
|
|
|
402
404
|
})
|
|
403
405
|
}
|
|
404
406
|
|
|
405
|
-
events.emit(
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
})
|
|
407
|
+
events.emit(
|
|
408
|
+
'raises.raise.updated',
|
|
409
|
+
{ type: 'raise', id },
|
|
410
|
+
{ changed: linksChanged ? [...changed, 'personIds'] : changed },
|
|
411
|
+
)
|
|
411
412
|
|
|
412
413
|
if (stageMove !== undefined) {
|
|
413
|
-
events.emit(
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
toStageId: stageMove.to.id,
|
|
419
|
-
})
|
|
414
|
+
events.emit(
|
|
415
|
+
'raises.raise.stage_changed',
|
|
416
|
+
{ type: 'raise', id },
|
|
417
|
+
{ fromStageId: stageMove.from.id, toStageId: stageMove.to.id },
|
|
418
|
+
)
|
|
420
419
|
}
|
|
421
420
|
|
|
422
421
|
return toView(updated, nextPeople ?? currentPeople)
|
|
423
|
-
})
|
|
422
|
+
}, { workspaceId, actor: toEventActor(actor) })
|
|
424
423
|
},
|
|
425
424
|
|
|
426
425
|
async remove(actor, id) {
|
|
@@ -435,8 +434,8 @@ export function createRaisesService(dependencies: RaisesDependencies): RaisesSer
|
|
|
435
434
|
await deleteRecordsAttachedTo(tx, workspaceId, 'raise', id)
|
|
436
435
|
await repository.deleteRaise(tx, workspaceId, id)
|
|
437
436
|
|
|
438
|
-
events.emit('
|
|
439
|
-
})
|
|
437
|
+
events.emit('raises.raise.deleted', { type: 'raise', id }, {})
|
|
438
|
+
}, { workspaceId, actor: toEventActor(actor) })
|
|
440
439
|
},
|
|
441
440
|
}
|
|
442
441
|
}
|
package/src/modules/reseal.ts
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { eq } from 'drizzle-orm'
|
|
2
2
|
|
|
3
|
+
import { ConfigurationError } from '../lib/config.ts'
|
|
4
|
+
import type { KelpieConfig } from '../lib/config.ts'
|
|
5
|
+
import { connectDatabase } from '../lib/database.ts'
|
|
3
6
|
import type { Database } from '../lib/database.ts'
|
|
4
|
-
import {
|
|
7
|
+
import type { Logger } from '../lib/logger.ts'
|
|
8
|
+
import { createSecretCipher, secretEncryptionConfigSchema, SecretDecryptionError } from '../lib/secrets.ts'
|
|
5
9
|
import type { SecretCipher } from '../lib/secrets.ts'
|
|
6
10
|
import { agentRegistrations } from './agent-tasks/schema.ts'
|
|
7
11
|
import { webhooks } from './webhooks/schema.ts'
|
|
@@ -164,3 +168,142 @@ export async function resealStoredSecrets(
|
|
|
164
168
|
unreadable: columns.reduce((total, column) => total + column.unreadable.length, 0),
|
|
165
169
|
}
|
|
166
170
|
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* One reseal pass over some set of columns. Core's own pass is one; a module
|
|
174
|
+
* with its own sealed table exports one too, and the assembly wires it in as
|
|
175
|
+
* an `extraPasses` entry.
|
|
176
|
+
*
|
|
177
|
+
* The shape matches `resealStoredSecrets` on purpose, so a pass covering a
|
|
178
|
+
* single column returns a one-element `columns` and `runReseal` can print
|
|
179
|
+
* every pass the same way.
|
|
180
|
+
*/
|
|
181
|
+
export type ResealPass = (db: Database, cipher: SecretCipher) => Promise<ResealOutcome>
|
|
182
|
+
|
|
183
|
+
export interface RunResealOptions {
|
|
184
|
+
readonly config: KelpieConfig
|
|
185
|
+
readonly logger: Logger
|
|
186
|
+
/**
|
|
187
|
+
* Extra passes to run after core's own. An assembly wires its modules'
|
|
188
|
+
* sealed columns in here; each pass covers columns core cannot see.
|
|
189
|
+
*/
|
|
190
|
+
readonly extraPasses?: readonly ResealPass[]
|
|
191
|
+
/** Where the per-column report goes. Defaults to stdout. */
|
|
192
|
+
readonly report?: (message: string) => void
|
|
193
|
+
/** Where unreadable-row ids and the trailing failure summary go. Defaults to stderr. */
|
|
194
|
+
readonly reportFatal?: (message: string) => void
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function defaultReport(message: string): void {
|
|
198
|
+
process.stdout.write(`${message}\n`)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function defaultReportFatal(message: string): void {
|
|
202
|
+
process.stderr.write(`${message}\n`)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Runs every reseal pass under the current `SECRET_ENCRYPTION_KEY` and
|
|
207
|
+
* prints a report. Returns the exit code a script should return: 0 if every
|
|
208
|
+
* examined value is now under the current key, 1 if any row was unreadable.
|
|
209
|
+
*
|
|
210
|
+
* The rotation procedure, in full:
|
|
211
|
+
*
|
|
212
|
+
* 1. Keep the current key. Add the new one as `SECRET_ENCRYPTION_KEY` and
|
|
213
|
+
* move the old value to `SECRET_ENCRYPTION_KEY_PREVIOUS`.
|
|
214
|
+
* 2. Deploy. Deliveries keep signing: new secrets seal under the new key,
|
|
215
|
+
* and existing ones still open under the previous one.
|
|
216
|
+
* 3. Run the reseal script. It rewrites every row still sealed under the
|
|
217
|
+
* old key.
|
|
218
|
+
* 4. Remove `SECRET_ENCRYPTION_KEY_PREVIOUS` and deploy again.
|
|
219
|
+
*
|
|
220
|
+
* Safe to run at any point, including with no previous key set, where it
|
|
221
|
+
* reports that everything is already current and writes nothing.
|
|
222
|
+
*
|
|
223
|
+
* Prefers `config.secretEncryption` (the top-level field an assembly's
|
|
224
|
+
* `kelpie.config.ts` declares). Falls back to parsing `config.env` for
|
|
225
|
+
* older assemblies and for `loadConfig`-based callers that never populate
|
|
226
|
+
* the top-level field. Either path validates before the pass opens a row,
|
|
227
|
+
* so a mistyped key throws `ConfigurationError` at boot rather than
|
|
228
|
+
* reporting every row as unreadable.
|
|
229
|
+
*
|
|
230
|
+
* @throws ConfigurationError if neither `config.secretEncryption` nor the
|
|
231
|
+
* fallback env parse produces a valid `SecretEncryptionConfig`. The
|
|
232
|
+
* database is closed before the throw.
|
|
233
|
+
*/
|
|
234
|
+
export async function runReseal(options: RunResealOptions): Promise<number> {
|
|
235
|
+
const report = options.report ?? defaultReport
|
|
236
|
+
const reportFatal = options.reportFatal ?? defaultReportFatal
|
|
237
|
+
const database = connectDatabase(options.config.databaseUrl, options.logger)
|
|
238
|
+
|
|
239
|
+
let secretConfig = options.config.secretEncryption
|
|
240
|
+
|
|
241
|
+
if (secretConfig === undefined) {
|
|
242
|
+
const parsed = secretEncryptionConfigSchema.safeParse(options.config.env)
|
|
243
|
+
|
|
244
|
+
if (!parsed.success) {
|
|
245
|
+
await database.close()
|
|
246
|
+
|
|
247
|
+
throw new ConfigurationError(
|
|
248
|
+
parsed.error.issues.map((issue) => `${issue.path.join('.')}: ${issue.message}`),
|
|
249
|
+
)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
secretConfig = parsed.data
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const hasPrevious =
|
|
256
|
+
secretConfig.SECRET_ENCRYPTION_KEY_PREVIOUS !== undefined &&
|
|
257
|
+
secretConfig.SECRET_ENCRYPTION_KEY_PREVIOUS.trim().length > 0
|
|
258
|
+
|
|
259
|
+
if (!hasPrevious) {
|
|
260
|
+
report('SECRET_ENCRYPTION_KEY_PREVIOUS is not set. Nothing sealed under an older key can be read.')
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const cipher = createSecretCipher(secretConfig)
|
|
264
|
+
const passes: readonly ResealPass[] = [resealStoredSecrets, ...(options.extraPasses ?? [])]
|
|
265
|
+
|
|
266
|
+
try {
|
|
267
|
+
// Sequential rather than `Promise.all`, so two full-table passes never
|
|
268
|
+
// contend for the pool at once. Each pass is idempotent; a run that
|
|
269
|
+
// dies halfway leaves the finished rows current and re-running
|
|
270
|
+
// completes the rest.
|
|
271
|
+
const outcomes: ResealOutcome[] = []
|
|
272
|
+
for (const pass of passes) {
|
|
273
|
+
outcomes.push(await pass(database.db, cipher))
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const columns = outcomes.flatMap((outcome) => outcome.columns)
|
|
277
|
+
const examined = outcomes.reduce((total, outcome) => total + outcome.examined, 0)
|
|
278
|
+
const resealed = outcomes.reduce((total, outcome) => total + outcome.resealed, 0)
|
|
279
|
+
const unreadable = outcomes.reduce((total, outcome) => total + outcome.unreadable, 0)
|
|
280
|
+
|
|
281
|
+
for (const column of columns) {
|
|
282
|
+
report(
|
|
283
|
+
`${column.label}: ${String(column.examined)} examined, ${String(column.resealed)} re-sealed, ${String(column.unreadable.length)} unreadable`,
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
for (const id of column.unreadable) {
|
|
287
|
+
reportFatal(` unreadable: ${id}`)
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (unreadable > 0) {
|
|
292
|
+
reportFatal(
|
|
293
|
+
`\n${String(unreadable)} value(s) opened under neither key. They were sealed under a key that is not configured, or the rows have been altered. Nothing here can recover them: set SECRET_ENCRYPTION_KEY_PREVIOUS to the key they were sealed with and run this again, or have those records re-created.`,
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
return 1
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
report(
|
|
300
|
+
resealed === 0
|
|
301
|
+
? `\nNothing to do: all ${String(examined)} value(s) are already sealed under the current key.`
|
|
302
|
+
: `\nRe-sealed ${String(resealed)} of ${String(examined)} value(s). You can now remove SECRET_ENCRYPTION_KEY_PREVIOUS.`,
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
return 0
|
|
306
|
+
} finally {
|
|
307
|
+
await database.close()
|
|
308
|
+
}
|
|
309
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import nodemailer from 'nodemailer'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
|
|
4
|
+
import { describeThrown } from '../../lib/errors.ts'
|
|
5
|
+
import type { EmailMessage, EmailSender } from '../../lib/email.ts'
|
|
6
|
+
import type { Logger } from '../../lib/logger.ts'
|
|
7
|
+
import type { KelpieModule } from '../../runtime/module.ts'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The built-in SMTP email module.
|
|
11
|
+
*
|
|
12
|
+
* Registers a provider named `'smtp'` with core's email runtime. An assembly
|
|
13
|
+
* that sets `email.provider: 'smtp'` in its `kelpie.config.ts` uses this
|
|
14
|
+
* sender; any other value picks a different provider (`'log'` for the built-in
|
|
15
|
+
* or another provider module's name). Reads `EMAIL_FROM` and `SMTP_*` from the
|
|
16
|
+
* environment; missing or malformed values fail boot.
|
|
17
|
+
*
|
|
18
|
+
* A commercial email integration (Resend, Postmark) belongs in its own module
|
|
19
|
+
* following the same shape, registered under a different name.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The name this module registers under. An assembly puts `provider: 'smtp'`
|
|
24
|
+
* in its `email` block to pick it.
|
|
25
|
+
*/
|
|
26
|
+
export const SMTP_EMAIL_PROVIDER = 'smtp'
|
|
27
|
+
|
|
28
|
+
export type { EmailMessage } from '../../lib/email.ts'
|
|
29
|
+
|
|
30
|
+
const smtpEmailConfigSchema = z.object({
|
|
31
|
+
EMAIL_FROM: z.string().min(1),
|
|
32
|
+
SMTP_HOST: z.string().min(1),
|
|
33
|
+
SMTP_PORT: z.coerce.number().int().positive().max(65535),
|
|
34
|
+
SMTP_SECURE: z.enum(['true', 'false']).transform((value) => value === 'true'),
|
|
35
|
+
SMTP_USER: z.string().min(1),
|
|
36
|
+
SMTP_PASSWORD: z.string().min(1),
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
export type SmtpEmailConfig = z.infer<typeof smtpEmailConfigSchema>
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* What `createSmtpEmailSender` sends a message through. A real deployment gets
|
|
43
|
+
* one backed by `nodemailer`; a test injects one that records calls, so the
|
|
44
|
+
* sender is verifiable without a container running a real SMTP conversation.
|
|
45
|
+
*/
|
|
46
|
+
export interface SmtpTransport {
|
|
47
|
+
sendMail(message: { from: string; to: string; subject: string; text: string }): Promise<unknown>
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function createNodemailerTransport(config: SmtpEmailConfig): SmtpTransport {
|
|
51
|
+
return nodemailer.createTransport({
|
|
52
|
+
host: config.SMTP_HOST,
|
|
53
|
+
port: config.SMTP_PORT,
|
|
54
|
+
secure: config.SMTP_SECURE,
|
|
55
|
+
auth: { user: config.SMTP_USER, pass: config.SMTP_PASSWORD },
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Sends over SMTP. `transport` defaults to a real connection built from `config`. */
|
|
60
|
+
export function createSmtpEmailSender(
|
|
61
|
+
config: SmtpEmailConfig,
|
|
62
|
+
logger: Logger,
|
|
63
|
+
transport: SmtpTransport = createNodemailerTransport(config),
|
|
64
|
+
): EmailSender {
|
|
65
|
+
return {
|
|
66
|
+
async send(message: EmailMessage) {
|
|
67
|
+
try {
|
|
68
|
+
await transport.sendMail({
|
|
69
|
+
from: config.EMAIL_FROM,
|
|
70
|
+
to: message.to,
|
|
71
|
+
subject: message.subject,
|
|
72
|
+
text: message.body,
|
|
73
|
+
})
|
|
74
|
+
} catch (cause) {
|
|
75
|
+
const reason = describeThrown(cause)
|
|
76
|
+
logger.error('smtp send failed', { to: message.to, reason })
|
|
77
|
+
throw new Error(`Failed to send email to ${message.to} over SMTP: ${reason}`, { cause })
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Options a caller may pass when building the module. `transport` is here for
|
|
85
|
+
* tests; a real deployment omits it and lets the module open a nodemailer
|
|
86
|
+
* connection from the resolved config.
|
|
87
|
+
*/
|
|
88
|
+
export interface SmtpEmailModuleOptions {
|
|
89
|
+
readonly transport?: SmtpTransport
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* The built-in SMTP module. Registers a factory under `SMTP_EMAIL_PROVIDER`;
|
|
94
|
+
* the factory reads the SMTP environment and builds the nodemailer sender, but
|
|
95
|
+
* only if the assembly's `email.provider` picks this module. Boot fails loudly
|
|
96
|
+
* if any SMTP variable is missing or malformed at that point.
|
|
97
|
+
*
|
|
98
|
+
* The module stays in `coreModules` unconditionally: on any other provider
|
|
99
|
+
* (e.g. `EMAIL_PROVIDER=log`) the factory never runs, so the SMTP environment
|
|
100
|
+
* is not checked.
|
|
101
|
+
*/
|
|
102
|
+
export function createSmtpEmailModule(options: SmtpEmailModuleOptions = {}): KelpieModule {
|
|
103
|
+
return {
|
|
104
|
+
id: 'smtp-email',
|
|
105
|
+
structural: true,
|
|
106
|
+
|
|
107
|
+
register(context) {
|
|
108
|
+
context.provideEmailSender(SMTP_EMAIL_PROVIDER, () => {
|
|
109
|
+
const config = context.config(smtpEmailConfigSchema)
|
|
110
|
+
|
|
111
|
+
return options.transport === undefined
|
|
112
|
+
? createSmtpEmailSender(config, context.log)
|
|
113
|
+
: createSmtpEmailSender(config, context.log, options.transport)
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
return Promise.resolve()
|
|
117
|
+
},
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -48,7 +48,11 @@ export function createWebhooksModule(
|
|
|
48
48
|
// when a customer's endpoint goes quiet is far too late. The retention
|
|
49
49
|
// window is validated the same way for the same reason — a bad value
|
|
50
50
|
// should stop boot, not prune by the wrong window.
|
|
51
|
-
|
|
51
|
+
//
|
|
52
|
+
// Prefer the top-level `secretEncryption` from the assembly's
|
|
53
|
+
// kelpie.config.ts; fall back to the schema read for older assemblies
|
|
54
|
+
// that don't declare it.
|
|
55
|
+
const cipher = createSecretCipher(context.secretEncryption ?? context.config(secretEncryptionConfigSchema))
|
|
52
56
|
const retention = context.config(deliveryRetentionConfigSchema)
|
|
53
57
|
const egress = createEgressGuard(context.config(egressConfigSchema))
|
|
54
58
|
|
|
@@ -1,13 +1,21 @@
|
|
|
1
|
-
import type { WebhookEvent } from '@kelpie/schemas'
|
|
1
|
+
import type { KelpieEvent, WebhookEvent } from '@kelpie/schemas'
|
|
2
2
|
|
|
3
|
-
import type {
|
|
3
|
+
import type { EventBus } from '../../runtime/events.ts'
|
|
4
|
+
import { RECORD_OBJECT_TYPES } from '../../runtime/events.ts'
|
|
5
|
+
import type { RecordObjectType } from '../../runtime/events.ts'
|
|
4
6
|
|
|
5
7
|
/**
|
|
6
8
|
* The bridge from the internal event catalog to what a webhook receiver sees.
|
|
7
9
|
*
|
|
8
10
|
* `modules.md` makes the webhooks engine a consumer of the bus, so this is the
|
|
9
11
|
* only place that knows both vocabularies. Everything downstream works with a
|
|
10
|
-
* `WebhookEventPayload` and never sees
|
|
12
|
+
* `WebhookEventPayload` and never sees an internal event key.
|
|
13
|
+
*
|
|
14
|
+
* A single prefix subscribe (`''`) sees every envelope event, and the
|
|
15
|
+
* translator below decides whether the event corresponds to a deliverable wire
|
|
16
|
+
* event. Unrecognised names are dropped, so a module's private events (a
|
|
17
|
+
* `.stage_changed` on a deal, a `.added` on a note) never reach a receiver
|
|
18
|
+
* that never subscribed to them.
|
|
11
19
|
*
|
|
12
20
|
* Payload keys are `snake_case`, matching `api.md`, because a receiver has no
|
|
13
21
|
* reason to expect a different convention from a webhook than from the REST
|
|
@@ -24,61 +32,99 @@ export interface WebhookEventPayload {
|
|
|
24
32
|
readonly data: Record<string, unknown>
|
|
25
33
|
}
|
|
26
34
|
|
|
35
|
+
const recordObjectTypes = new Set<string>(RECORD_OBJECT_TYPES)
|
|
36
|
+
|
|
37
|
+
interface RecordUpdatedData {
|
|
38
|
+
readonly changed?: readonly string[]
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface FormSubmittedData {
|
|
42
|
+
readonly formId?: string
|
|
43
|
+
readonly submissionId?: string
|
|
44
|
+
}
|
|
45
|
+
|
|
27
46
|
/**
|
|
28
|
-
*
|
|
47
|
+
* Translates an envelope event into a `WebhookEventPayload`, or `undefined` if
|
|
48
|
+
* the event does not correspond to any deliverable wire event.
|
|
29
49
|
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
50
|
+
* The last segment of the event name (`.created`, `.updated`, `.deleted`,
|
|
51
|
+
* `.submitted`) picks the wire event. A `.created`/`.updated`/`.deleted` event
|
|
52
|
+
* is only translated when its target type is one of the CRM record objects
|
|
53
|
+
* (`RECORD_OBJECT_TYPES`), because those are the only ones the wire catalog
|
|
54
|
+
* offers. Everything else is skipped.
|
|
33
55
|
*/
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
data: {
|
|
45
|
-
object_type: payload.objectType,
|
|
46
|
-
record_id: payload.recordId,
|
|
47
|
-
// The values stay as the emitting service named them, which is camelCase
|
|
48
|
-
// (`parentId`, `sortOrder`) even though the keys around them are not.
|
|
49
|
-
// Translating them to the wire spelling would mean a second copy of every
|
|
50
|
-
// module's column naming, kept in step by hand.
|
|
51
|
-
changed_fields: [...payload.changedFields],
|
|
52
|
-
},
|
|
53
|
-
}),
|
|
56
|
+
export function translateEnvelopeEvent(
|
|
57
|
+
event: KelpieEvent<string, unknown>,
|
|
58
|
+
): WebhookEventPayload | undefined {
|
|
59
|
+
const suffix = event.name.slice(event.name.lastIndexOf('.') + 1)
|
|
60
|
+
const objectType = event.target.type
|
|
61
|
+
|
|
62
|
+
if (suffix === 'created' || suffix === 'deleted') {
|
|
63
|
+
if (!recordObjectTypes.has(objectType)) {
|
|
64
|
+
return undefined
|
|
65
|
+
}
|
|
54
66
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
67
|
+
return {
|
|
68
|
+
workspaceId: event.workspaceId,
|
|
69
|
+
event: suffix === 'created' ? 'record.created' : 'record.deleted',
|
|
70
|
+
data: { object_type: objectType as RecordObjectType, record_id: event.target.id },
|
|
71
|
+
}
|
|
72
|
+
}
|
|
60
73
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
74
|
+
if (suffix === 'updated') {
|
|
75
|
+
if (!recordObjectTypes.has(objectType)) {
|
|
76
|
+
return undefined
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const data = event.data as RecordUpdatedData
|
|
80
|
+
return {
|
|
81
|
+
workspaceId: event.workspaceId,
|
|
82
|
+
event: 'record.updated',
|
|
83
|
+
data: {
|
|
84
|
+
object_type: objectType as RecordObjectType,
|
|
85
|
+
record_id: event.target.id,
|
|
86
|
+
changed_fields: data.changed !== undefined ? [...data.changed] : [],
|
|
87
|
+
},
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (suffix === 'submitted') {
|
|
92
|
+
// Form submission is the only `.submitted` wire event today. The forms
|
|
93
|
+
// module emits its envelope with `formId` and `submissionId` in `data`.
|
|
94
|
+
const data = event.data as FormSubmittedData
|
|
95
|
+
if (data.formId === undefined || data.submissionId === undefined) {
|
|
96
|
+
return undefined
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
workspaceId: event.workspaceId,
|
|
101
|
+
event: 'form.submitted',
|
|
102
|
+
data: { form_id: data.formId, submission_id: data.submissionId },
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return undefined
|
|
107
|
+
}
|
|
67
108
|
|
|
68
109
|
/**
|
|
69
|
-
* Subscribes the engine to every deliverable event
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
* would need a cast, and the cast is exactly what would let a wrong payload
|
|
74
|
-
* through. A literal key indexes `builders` at its precise signature.
|
|
110
|
+
* Subscribes the engine to every deliverable event through one prefix match on
|
|
111
|
+
* `''`, which every envelope event starts with. The translator inside decides
|
|
112
|
+
* whether the event corresponds to a deliverable wire event; unrecognised
|
|
113
|
+
* names are dropped.
|
|
75
114
|
*/
|
|
76
115
|
export function subscribeDeliverableEvents(
|
|
77
116
|
bus: EventBus,
|
|
78
117
|
deliver: (payload: WebhookEventPayload) => Promise<void>,
|
|
79
118
|
): void {
|
|
80
|
-
bus.
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
119
|
+
bus.subscribePrefix(
|
|
120
|
+
'',
|
|
121
|
+
async (event) => {
|
|
122
|
+
const built = translateEnvelopeEvent(event)
|
|
123
|
+
if (built === undefined) {
|
|
124
|
+
return
|
|
125
|
+
}
|
|
126
|
+
await deliver(built)
|
|
127
|
+
},
|
|
128
|
+
{ label: 'webhooks:envelope-bridge' },
|
|
129
|
+
)
|
|
84
130
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
import type { ModuleEventCatalog } from '../../runtime/module.ts'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Domain events published by the workspace module.
|
|
7
|
+
*
|
|
8
|
+
* Workspace-level events (`workspace.workspace.*`) target the workspace itself.
|
|
9
|
+
* Membership events target the `workspace_members` row, with the user id in
|
|
10
|
+
* `data` so a consumer that watches identity does not need a second lookup.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export const workspaceEvents = {
|
|
14
|
+
'workspace.workspace.created': z.object({ slug: z.string() }),
|
|
15
|
+
'workspace.workspace.deleted': z.object({ slug: z.string() }),
|
|
16
|
+
'workspace.member.invited': z.object({ inviteId: z.string(), email: z.string() }),
|
|
17
|
+
'workspace.member.joined': z.object({ userId: z.string() }),
|
|
18
|
+
'workspace.member.removed': z.object({ userId: z.string() }),
|
|
19
|
+
} satisfies ModuleEventCatalog
|
|
20
|
+
|
|
21
|
+
export interface WorkspaceCreatedData {
|
|
22
|
+
readonly slug: string
|
|
23
|
+
}
|
|
24
|
+
export interface WorkspaceDeletedData {
|
|
25
|
+
readonly slug: string
|
|
26
|
+
}
|
|
27
|
+
export interface MemberInvitedData {
|
|
28
|
+
readonly inviteId: string
|
|
29
|
+
readonly email: string
|
|
30
|
+
}
|
|
31
|
+
export interface MemberJoinedData {
|
|
32
|
+
readonly userId: string
|
|
33
|
+
}
|
|
34
|
+
export interface MemberRemovedData {
|
|
35
|
+
readonly userId: string
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
declare module '../../runtime/events.ts' {
|
|
39
|
+
interface KelpieEventMap {
|
|
40
|
+
'workspace.workspace.created': WorkspaceCreatedData
|
|
41
|
+
'workspace.workspace.deleted': WorkspaceDeletedData
|
|
42
|
+
'workspace.member.invited': MemberInvitedData
|
|
43
|
+
'workspace.member.joined': MemberJoinedData
|
|
44
|
+
'workspace.member.removed': MemberRemovedData
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -2,6 +2,7 @@ import { appUrlConfigSchema } from '../../lib/appUrl.ts'
|
|
|
2
2
|
import type { KelpieModule } from '../../runtime/module.ts'
|
|
3
3
|
import { parseModuleCapability } from '../../runtime/moduleConfig.ts'
|
|
4
4
|
import { SEATS_LIMIT, WORKSPACE_ACCESS } from './capabilities.ts'
|
|
5
|
+
import { workspaceEvents } from './events.ts'
|
|
5
6
|
import * as repository from './repository.ts'
|
|
6
7
|
import { mountWorkspaceRoutes } from './routes.ts'
|
|
7
8
|
import * as schema from './schema.ts'
|
|
@@ -19,9 +20,12 @@ export function createWorkspaceModule(migrationsDirectory: string): KelpieModule
|
|
|
19
20
|
id: 'workspace',
|
|
20
21
|
requires: ['auth'],
|
|
21
22
|
structural: true,
|
|
23
|
+
events: workspaceEvents,
|
|
22
24
|
|
|
23
25
|
register(context) {
|
|
24
|
-
|
|
26
|
+
// Prefer the top-level `appBaseUrl` from the assembly's kelpie.config.ts;
|
|
27
|
+
// fall back to the schema read for older assemblies that don't declare it.
|
|
28
|
+
const appBaseUrl = context.appBaseUrl ?? context.config(appUrlConfigSchema).APP_BASE_URL
|
|
25
29
|
|
|
26
30
|
context.entitlements.declare(SEATS_LIMIT)
|
|
27
31
|
context.entitlements.declare(WORKSPACE_ACCESS)
|
|
@@ -48,7 +52,7 @@ export function createWorkspaceModule(migrationsDirectory: string): KelpieModule
|
|
|
48
52
|
createId: context.createId,
|
|
49
53
|
now: context.now,
|
|
50
54
|
entitlements: context.entitlements,
|
|
51
|
-
appBaseUrl
|
|
55
|
+
appBaseUrl,
|
|
52
56
|
toggleableModuleIds: context.moduleCatalog
|
|
53
57
|
.filter((entry) => !entry.structural)
|
|
54
58
|
.map((entry) => entry.id),
|