@kelpie/server 0.10.0 → 0.11.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/dist/app.d.ts.map +1 -1
- package/dist/app.js +2 -0
- package/dist/app.js.map +1 -1
- package/dist/lib/actor.d.ts +3 -1
- package/dist/lib/actor.d.ts.map +1 -1
- package/dist/lib/actor.js.map +1 -1
- package/dist/lib/apiKeyScopes.d.ts +8 -0
- package/dist/lib/apiKeyScopes.d.ts.map +1 -0
- package/dist/lib/apiKeyScopes.js +166 -0
- package/dist/lib/apiKeyScopes.js.map +1 -0
- package/dist/modules/activities/repository.d.ts +2 -0
- package/dist/modules/activities/repository.d.ts.map +1 -1
- package/dist/modules/activities/repository.js +9 -0
- package/dist/modules/activities/repository.js.map +1 -1
- package/dist/modules/agent-tasks/repository.d.ts +2 -0
- package/dist/modules/agent-tasks/repository.d.ts.map +1 -1
- package/dist/modules/agent-tasks/repository.js +9 -0
- package/dist/modules/agent-tasks/repository.js.map +1 -1
- package/dist/modules/api-keys/routes.d.ts.map +1 -1
- package/dist/modules/api-keys/routes.js +4 -1
- package/dist/modules/api-keys/routes.js.map +1 -1
- package/dist/modules/api-keys/schema.d.ts +43 -0
- package/dist/modules/api-keys/schema.d.ts.map +1 -1
- package/dist/modules/api-keys/schema.js +2 -0
- package/dist/modules/api-keys/schema.js.map +1 -1
- package/dist/modules/api-keys/scopeMiddleware.d.ts +8 -0
- package/dist/modules/api-keys/scopeMiddleware.d.ts.map +1 -0
- package/dist/modules/api-keys/scopeMiddleware.js +21 -0
- package/dist/modules/api-keys/scopeMiddleware.js.map +1 -0
- package/dist/modules/api-keys/service.d.ts +3 -1
- package/dist/modules/api-keys/service.d.ts.map +1 -1
- package/dist/modules/api-keys/service.js +26 -1
- package/dist/modules/api-keys/service.js.map +1 -1
- package/dist/modules/auth/credentials.d.ts.map +1 -1
- package/dist/modules/auth/credentials.js +2 -0
- package/dist/modules/auth/credentials.js.map +1 -1
- package/dist/modules/consent-purposes/index.d.ts +1 -1
- package/dist/modules/consent-purposes/index.js +1 -1
- package/dist/modules/consent-purposes/schema.d.ts +3 -3
- package/dist/modules/consent-purposes/schema.js +3 -3
- package/dist/modules/consent-purposes/service.d.ts +1 -1
- package/dist/modules/consent-purposes/service.d.ts.map +1 -1
- package/dist/modules/consent-purposes/service.js +7 -16
- package/dist/modules/consent-purposes/service.js.map +1 -1
- package/dist/modules/consent-purposes/starters.d.ts +1 -1
- package/dist/modules/consent-purposes/tools.js +1 -1
- package/dist/modules/consent-purposes/tools.js.map +1 -1
- package/dist/modules/conversions/clearPointers.d.ts +10 -0
- package/dist/modules/conversions/clearPointers.d.ts.map +1 -0
- package/dist/modules/conversions/clearPointers.js +40 -0
- package/dist/modules/conversions/clearPointers.js.map +1 -0
- package/dist/modules/conversions/factory.d.ts +5 -0
- package/dist/modules/conversions/factory.d.ts.map +1 -0
- package/dist/modules/conversions/factory.js +18 -0
- package/dist/modules/conversions/factory.js.map +1 -0
- package/dist/modules/conversions/fieldMap.d.ts +55 -0
- package/dist/modules/conversions/fieldMap.d.ts.map +1 -0
- package/dist/modules/conversions/fieldMap.js +398 -0
- package/dist/modules/conversions/fieldMap.js.map +1 -0
- package/dist/modules/conversions/index.d.ts +7 -0
- package/dist/modules/conversions/index.d.ts.map +1 -0
- package/dist/modules/conversions/index.js +6 -0
- package/dist/modules/conversions/index.js.map +1 -0
- package/dist/modules/conversions/mcp.d.ts +7 -0
- package/dist/modules/conversions/mcp.d.ts.map +1 -0
- package/dist/modules/conversions/mcp.js +47 -0
- package/dist/modules/conversions/mcp.js.map +1 -0
- package/dist/modules/conversions/repoint.d.ts +19 -0
- package/dist/modules/conversions/repoint.d.ts.map +1 -0
- package/dist/modules/conversions/repoint.js +51 -0
- package/dist/modules/conversions/repoint.js.map +1 -0
- package/dist/modules/conversions/routes.d.ts +26 -0
- package/dist/modules/conversions/routes.d.ts.map +1 -0
- package/dist/modules/conversions/routes.js +23 -0
- package/dist/modules/conversions/routes.js.map +1 -0
- package/dist/modules/conversions/service.d.ts +28 -0
- package/dist/modules/conversions/service.d.ts.map +1 -0
- package/dist/modules/conversions/service.js +170 -0
- package/dist/modules/conversions/service.js.map +1 -0
- package/dist/modules/conversions/wire.d.ts +4 -0
- package/dist/modules/conversions/wire.d.ts.map +1 -0
- package/dist/modules/conversions/wire.js +28 -0
- package/dist/modules/conversions/wire.js.map +1 -0
- package/dist/modules/deals/events.d.ts +9 -0
- package/dist/modules/deals/events.d.ts.map +1 -1
- package/dist/modules/deals/events.js +4 -0
- package/dist/modules/deals/events.js.map +1 -1
- package/dist/modules/deals/index.d.ts.map +1 -1
- package/dist/modules/deals/index.js +4 -2
- package/dist/modules/deals/index.js.map +1 -1
- package/dist/modules/deals/routes.d.ts +2 -0
- package/dist/modules/deals/routes.d.ts.map +1 -1
- package/dist/modules/deals/routes.js +8 -1
- package/dist/modules/deals/routes.js.map +1 -1
- package/dist/modules/deals/schema.d.ts +34 -0
- package/dist/modules/deals/schema.d.ts.map +1 -1
- package/dist/modules/deals/schema.js +7 -2
- package/dist/modules/deals/schema.js.map +1 -1
- package/dist/modules/deals/service.d.ts.map +1 -1
- package/dist/modules/deals/service.js +2 -0
- package/dist/modules/deals/service.js.map +1 -1
- package/dist/modules/deals/tools.d.ts +2 -1
- package/dist/modules/deals/tools.d.ts.map +1 -1
- package/dist/modules/deals/tools.js +3 -1
- package/dist/modules/deals/tools.js.map +1 -1
- package/dist/modules/decisions/repository.d.ts +2 -1
- package/dist/modules/decisions/repository.d.ts.map +1 -1
- package/dist/modules/decisions/repository.js +9 -1
- package/dist/modules/decisions/repository.js.map +1 -1
- package/dist/modules/enquiries/events.d.ts +6 -2
- package/dist/modules/enquiries/events.d.ts.map +1 -1
- package/dist/modules/enquiries/events.js +5 -1
- package/dist/modules/enquiries/events.js.map +1 -1
- package/dist/modules/enquiries/index.d.ts.map +1 -1
- package/dist/modules/enquiries/index.js +4 -2
- package/dist/modules/enquiries/index.js.map +1 -1
- package/dist/modules/enquiries/routes.d.ts +2 -0
- package/dist/modules/enquiries/routes.d.ts.map +1 -1
- package/dist/modules/enquiries/routes.js +9 -9
- package/dist/modules/enquiries/routes.js.map +1 -1
- package/dist/modules/enquiries/schema.d.ts +36 -1
- package/dist/modules/enquiries/schema.d.ts.map +1 -1
- package/dist/modules/enquiries/schema.js +9 -3
- package/dist/modules/enquiries/schema.js.map +1 -1
- package/dist/modules/enquiries/service.d.ts +0 -8
- package/dist/modules/enquiries/service.d.ts.map +1 -1
- package/dist/modules/enquiries/service.js +3 -95
- package/dist/modules/enquiries/service.js.map +1 -1
- package/dist/modules/enquiries/tools.d.ts +2 -1
- package/dist/modules/enquiries/tools.d.ts.map +1 -1
- package/dist/modules/enquiries/tools.js +4 -16
- package/dist/modules/enquiries/tools.js.map +1 -1
- package/dist/modules/forms/applyMappedFields.d.ts +20 -0
- package/dist/modules/forms/applyMappedFields.d.ts.map +1 -0
- package/dist/modules/forms/applyMappedFields.js +169 -0
- package/dist/modules/forms/applyMappedFields.js.map +1 -0
- package/dist/modules/forms/applySubmissionFields.d.ts +18 -0
- package/dist/modules/forms/applySubmissionFields.d.ts.map +1 -0
- package/dist/modules/forms/applySubmissionFields.js +230 -0
- package/dist/modules/forms/applySubmissionFields.js.map +1 -0
- package/dist/modules/forms/fields.d.ts +7 -2
- package/dist/modules/forms/fields.d.ts.map +1 -1
- package/dist/modules/forms/fields.js +17 -6
- package/dist/modules/forms/fields.js.map +1 -1
- package/dist/modules/forms/mapping.d.ts +8 -2
- package/dist/modules/forms/mapping.d.ts.map +1 -1
- package/dist/modules/forms/mapping.js +11 -0
- package/dist/modules/forms/mapping.js.map +1 -1
- package/dist/modules/forms/repository.d.ts +0 -5
- package/dist/modules/forms/repository.d.ts.map +1 -1
- package/dist/modules/forms/repository.js +0 -2
- package/dist/modules/forms/repository.js.map +1 -1
- package/dist/modules/forms/routes.d.ts +2 -30
- package/dist/modules/forms/routes.d.ts.map +1 -1
- package/dist/modules/forms/routes.js +2 -2
- package/dist/modules/forms/routes.js.map +1 -1
- package/dist/modules/forms/schema.d.ts.map +1 -1
- package/dist/modules/forms/schema.js +1 -2
- package/dist/modules/forms/schema.js.map +1 -1
- package/dist/modules/forms/service.d.ts.map +1 -1
- package/dist/modules/forms/service.js +18 -3
- package/dist/modules/forms/service.js.map +1 -1
- package/dist/modules/forms/starters.d.ts +31 -0
- package/dist/modules/forms/starters.d.ts.map +1 -0
- package/dist/modules/forms/starters.js +94 -0
- package/dist/modules/forms/starters.js.map +1 -0
- package/dist/modules/forms/submission.d.ts +1 -0
- package/dist/modules/forms/submission.d.ts.map +1 -1
- package/dist/modules/forms/submission.js +58 -37
- package/dist/modules/forms/submission.js.map +1 -1
- package/dist/modules/import-export/customFieldCsv.d.ts +9 -0
- package/dist/modules/import-export/customFieldCsv.d.ts.map +1 -0
- package/dist/modules/import-export/customFieldCsv.js +39 -0
- package/dist/modules/import-export/customFieldCsv.js.map +1 -0
- package/dist/modules/import-export/customFieldImport.d.ts +19 -0
- package/dist/modules/import-export/customFieldImport.d.ts.map +1 -0
- package/dist/modules/import-export/customFieldImport.js +76 -0
- package/dist/modules/import-export/customFieldImport.js.map +1 -0
- package/dist/modules/import-export/drafts.d.ts +57 -3
- package/dist/modules/import-export/drafts.d.ts.map +1 -1
- package/dist/modules/import-export/drafts.js +65 -3
- package/dist/modules/import-export/drafts.js.map +1 -1
- package/dist/modules/import-export/exportRows.d.ts +15 -13
- package/dist/modules/import-export/exportRows.d.ts.map +1 -1
- package/dist/modules/import-export/exportRows.js +99 -26
- package/dist/modules/import-export/exportRows.js.map +1 -1
- package/dist/modules/import-export/plan.d.ts +42 -11
- package/dist/modules/import-export/plan.d.ts.map +1 -1
- package/dist/modules/import-export/plan.js +198 -56
- package/dist/modules/import-export/plan.js.map +1 -1
- package/dist/modules/import-export/repository.d.ts +58 -2
- package/dist/modules/import-export/repository.d.ts.map +1 -1
- package/dist/modules/import-export/repository.js +201 -12
- package/dist/modules/import-export/repository.js.map +1 -1
- package/dist/modules/import-export/routes.d.ts.map +1 -1
- package/dist/modules/import-export/routes.js +5 -5
- package/dist/modules/import-export/routes.js.map +1 -1
- package/dist/modules/import-export/service.d.ts +8 -3
- package/dist/modules/import-export/service.d.ts.map +1 -1
- package/dist/modules/import-export/service.js +133 -40
- package/dist/modules/import-export/service.js.map +1 -1
- package/dist/modules/import-export/streams.d.ts +2 -2
- package/dist/modules/import-export/streams.d.ts.map +1 -1
- package/dist/modules/import-export/streams.js +50 -9
- package/dist/modules/import-export/streams.js.map +1 -1
- package/dist/modules/import-export/tools.d.ts.map +1 -1
- package/dist/modules/import-export/tools.js +13 -7
- package/dist/modules/import-export/tools.js.map +1 -1
- package/dist/modules/import-export/validation.d.ts +5 -1
- package/dist/modules/import-export/validation.d.ts.map +1 -1
- package/dist/modules/import-export/validation.js +89 -2
- package/dist/modules/import-export/validation.js.map +1 -1
- package/dist/modules/import-export/writes.d.ts +2 -1
- package/dist/modules/import-export/writes.d.ts.map +1 -1
- package/dist/modules/import-export/writes.js +358 -25
- package/dist/modules/import-export/writes.js.map +1 -1
- package/dist/modules/lists/repository.d.ts +0 -1
- package/dist/modules/lists/repository.d.ts.map +1 -1
- package/dist/modules/lists/repository.js +0 -1
- package/dist/modules/lists/repository.js.map +1 -1
- package/dist/modules/lists/routes.d.ts +0 -2
- package/dist/modules/lists/routes.d.ts.map +1 -1
- package/dist/modules/lists/routes.js +0 -8
- package/dist/modules/lists/routes.js.map +1 -1
- package/dist/modules/lists/schema.d.ts +0 -17
- package/dist/modules/lists/schema.d.ts.map +1 -1
- package/dist/modules/lists/schema.js +0 -10
- package/dist/modules/lists/schema.js.map +1 -1
- package/dist/modules/lists/service.d.ts +2 -14
- package/dist/modules/lists/service.d.ts.map +1 -1
- package/dist/modules/lists/service.js +7 -53
- package/dist/modules/lists/service.js.map +1 -1
- package/dist/modules/lists/starters.d.ts +12 -0
- package/dist/modules/lists/starters.d.ts.map +1 -0
- package/dist/modules/lists/starters.js +4 -0
- package/dist/modules/lists/starters.js.map +1 -0
- package/dist/modules/mcp/protocol.d.ts.map +1 -1
- package/dist/modules/mcp/protocol.js +13 -0
- package/dist/modules/mcp/protocol.js.map +1 -1
- package/dist/modules/notes/repository.d.ts +2 -0
- package/dist/modules/notes/repository.d.ts.map +1 -1
- package/dist/modules/notes/repository.js +9 -0
- package/dist/modules/notes/repository.js.map +1 -1
- package/dist/modules/opportunities/events.d.ts +9 -0
- package/dist/modules/opportunities/events.d.ts.map +1 -1
- package/dist/modules/opportunities/events.js +4 -0
- package/dist/modules/opportunities/events.js.map +1 -1
- package/dist/modules/opportunities/index.d.ts.map +1 -1
- package/dist/modules/opportunities/index.js +4 -2
- package/dist/modules/opportunities/index.js.map +1 -1
- package/dist/modules/opportunities/routes.d.ts +2 -0
- package/dist/modules/opportunities/routes.d.ts.map +1 -1
- package/dist/modules/opportunities/routes.js +8 -1
- package/dist/modules/opportunities/routes.js.map +1 -1
- package/dist/modules/opportunities/schema.d.ts +34 -0
- package/dist/modules/opportunities/schema.d.ts.map +1 -1
- package/dist/modules/opportunities/schema.js +7 -2
- package/dist/modules/opportunities/schema.js.map +1 -1
- package/dist/modules/opportunities/service.d.ts.map +1 -1
- package/dist/modules/opportunities/service.js +2 -0
- package/dist/modules/opportunities/service.js.map +1 -1
- package/dist/modules/opportunities/tools.d.ts +2 -1
- package/dist/modules/opportunities/tools.d.ts.map +1 -1
- package/dist/modules/opportunities/tools.js +3 -1
- package/dist/modules/opportunities/tools.js.map +1 -1
- package/dist/modules/partnerships/events.d.ts +9 -0
- package/dist/modules/partnerships/events.d.ts.map +1 -1
- package/dist/modules/partnerships/events.js +4 -0
- package/dist/modules/partnerships/events.js.map +1 -1
- package/dist/modules/partnerships/index.d.ts.map +1 -1
- package/dist/modules/partnerships/index.js +4 -2
- package/dist/modules/partnerships/index.js.map +1 -1
- package/dist/modules/partnerships/routes.d.ts +2 -0
- package/dist/modules/partnerships/routes.d.ts.map +1 -1
- package/dist/modules/partnerships/routes.js +8 -1
- package/dist/modules/partnerships/routes.js.map +1 -1
- package/dist/modules/partnerships/schema.d.ts +34 -0
- package/dist/modules/partnerships/schema.d.ts.map +1 -1
- package/dist/modules/partnerships/schema.js +7 -2
- package/dist/modules/partnerships/schema.js.map +1 -1
- package/dist/modules/partnerships/service.d.ts.map +1 -1
- package/dist/modules/partnerships/service.js +2 -0
- package/dist/modules/partnerships/service.js.map +1 -1
- package/dist/modules/partnerships/tools.d.ts +2 -1
- package/dist/modules/partnerships/tools.d.ts.map +1 -1
- package/dist/modules/partnerships/tools.js +3 -1
- package/dist/modules/partnerships/tools.js.map +1 -1
- package/dist/modules/people/personConsentWrites.d.ts +1 -1
- package/dist/modules/people/schema.js +1 -1
- package/dist/modules/people/schema.js.map +1 -1
- package/dist/modules/plans/repository.d.ts +2 -0
- package/dist/modules/plans/repository.d.ts.map +1 -1
- package/dist/modules/plans/repository.js +9 -0
- package/dist/modules/plans/repository.js.map +1 -1
- package/dist/modules/raises/events.d.ts +9 -0
- package/dist/modules/raises/events.d.ts.map +1 -1
- package/dist/modules/raises/events.js +4 -0
- package/dist/modules/raises/events.js.map +1 -1
- package/dist/modules/raises/index.d.ts.map +1 -1
- package/dist/modules/raises/index.js +4 -2
- package/dist/modules/raises/index.js.map +1 -1
- package/dist/modules/raises/routes.d.ts +2 -0
- package/dist/modules/raises/routes.d.ts.map +1 -1
- package/dist/modules/raises/routes.js +8 -1
- package/dist/modules/raises/routes.js.map +1 -1
- package/dist/modules/raises/schema.d.ts +34 -0
- package/dist/modules/raises/schema.d.ts.map +1 -1
- package/dist/modules/raises/schema.js +7 -2
- package/dist/modules/raises/schema.js.map +1 -1
- package/dist/modules/raises/service.d.ts.map +1 -1
- package/dist/modules/raises/service.js +2 -0
- package/dist/modules/raises/service.js.map +1 -1
- package/dist/modules/raises/tools.d.ts +2 -1
- package/dist/modules/raises/tools.d.ts.map +1 -1
- package/dist/modules/raises/tools.js +3 -1
- package/dist/modules/raises/tools.js.map +1 -1
- package/dist/modules/workspace/routes.d.ts +0 -2
- package/dist/modules/workspace/routes.d.ts.map +1 -1
- package/dist/modules/workspace/routes.js +0 -6
- package/dist/modules/workspace/routes.js.map +1 -1
- package/dist/modules/workspace/schema.d.ts +0 -34
- package/dist/modules/workspace/schema.d.ts.map +1 -1
- package/dist/modules/workspace/schema.js +0 -2
- package/dist/modules/workspace/schema.js.map +1 -1
- package/dist/modules/workspace/seedStarterForms.d.ts +17 -0
- package/dist/modules/workspace/seedStarterForms.d.ts.map +1 -0
- package/dist/modules/workspace/seedStarterForms.js +71 -0
- package/dist/modules/workspace/seedStarterForms.js.map +1 -0
- package/dist/modules/workspace/service.d.ts +0 -4
- package/dist/modules/workspace/service.d.ts.map +1 -1
- package/dist/modules/workspace/service.js +20 -10
- package/dist/modules/workspace/service.js.map +1 -1
- package/dist/modules/workspace/tools.js +1 -1
- package/dist/modules/workspace/tools.js.map +1 -1
- package/dist/testing/fixtures.d.ts.map +1 -1
- package/dist/testing/fixtures.js +1 -0
- package/dist/testing/fixtures.js.map +1 -1
- package/migrations/0037_drop_lists_consent_purpose_id.sql +2 -0
- package/migrations/0038_smart_white_queen.sql +2 -0
- package/migrations/0039_form_map_to_open.sql +1 -0
- package/migrations/0040_pipeline_converted_to.sql +20 -0
- package/migrations/0041_api_key_scopes.sql +1 -0
- package/migrations/0042_drop_workspace_identity.sql +2 -0
- package/migrations/meta/0038_snapshot.json +6554 -0
- package/migrations/meta/_journal.json +42 -0
- package/package.json +2 -2
- package/src/app.ts +3 -0
- package/src/lib/actor.ts +3 -1
- package/src/lib/apiKeyScopes.ts +213 -0
- package/src/modules/activities/repository.ts +24 -0
- package/src/modules/agent-tasks/repository.ts +24 -0
- package/src/modules/api-keys/routes.ts +5 -0
- package/src/modules/api-keys/schema.ts +2 -0
- package/src/modules/api-keys/scopeMiddleware.ts +30 -0
- package/src/modules/api-keys/service.ts +36 -2
- package/src/modules/auth/credentials.ts +4 -0
- package/src/modules/consent-purposes/index.ts +1 -1
- package/src/modules/consent-purposes/schema.ts +3 -3
- package/src/modules/consent-purposes/service.ts +7 -16
- package/src/modules/consent-purposes/starters.ts +1 -1
- package/src/modules/consent-purposes/tools.ts +1 -1
- package/src/modules/conversions/clearPointers.ts +81 -0
- package/src/modules/conversions/factory.ts +20 -0
- package/src/modules/conversions/fieldMap.ts +548 -0
- package/src/modules/conversions/index.ts +6 -0
- package/src/modules/conversions/mcp.ts +71 -0
- package/src/modules/conversions/repoint.ts +146 -0
- package/src/modules/conversions/routes.ts +68 -0
- package/src/modules/conversions/service.ts +310 -0
- package/src/modules/conversions/wire.ts +42 -0
- package/src/modules/deals/events.ts +9 -0
- package/src/modules/deals/index.ts +4 -2
- package/src/modules/deals/routes.ts +11 -1
- package/src/modules/deals/schema.ts +10 -2
- package/src/modules/deals/service.ts +3 -0
- package/src/modules/deals/tools.ts +9 -1
- package/src/modules/decisions/repository.ts +23 -1
- package/src/modules/enquiries/events.ts +8 -2
- package/src/modules/enquiries/index.ts +4 -2
- package/src/modules/enquiries/routes.ts +11 -13
- package/src/modules/enquiries/schema.ts +12 -3
- package/src/modules/enquiries/service.ts +3 -150
- package/src/modules/enquiries/tools.ts +9 -18
- package/src/modules/forms/applyMappedFields.ts +248 -0
- package/src/modules/forms/applySubmissionFields.ts +435 -0
- package/src/modules/forms/fields.ts +31 -7
- package/src/modules/forms/mapping.ts +20 -3
- package/src/modules/forms/repository.ts +0 -7
- package/src/modules/forms/routes.ts +1 -2
- package/src/modules/forms/schema.ts +0 -2
- package/src/modules/forms/service.ts +31 -4
- package/src/modules/forms/starters.ts +129 -0
- package/src/modules/forms/submission.ts +135 -69
- package/src/modules/import-export/customFieldCsv.ts +55 -0
- package/src/modules/import-export/customFieldImport.ts +111 -0
- package/src/modules/import-export/drafts.ts +151 -3
- package/src/modules/import-export/exportRows.ts +222 -68
- package/src/modules/import-export/plan.ts +321 -67
- package/src/modules/import-export/repository.ts +356 -14
- package/src/modules/import-export/routes.ts +8 -8
- package/src/modules/import-export/service.ts +208 -45
- package/src/modules/import-export/streams.ts +95 -13
- package/src/modules/import-export/tools.ts +14 -6
- package/src/modules/import-export/validation.ts +128 -0
- package/src/modules/import-export/writes.ts +559 -27
- package/src/modules/lists/repository.ts +0 -2
- package/src/modules/lists/routes.ts +0 -8
- package/src/modules/lists/schema.ts +0 -10
- package/src/modules/lists/service.ts +9 -107
- package/src/modules/lists/starters.ts +16 -0
- package/src/modules/mcp/protocol.ts +18 -0
- package/src/modules/notes/repository.ts +24 -0
- package/src/modules/opportunities/events.ts +9 -0
- package/src/modules/opportunities/index.ts +4 -2
- package/src/modules/opportunities/routes.ts +11 -1
- package/src/modules/opportunities/schema.ts +10 -2
- package/src/modules/opportunities/service.ts +3 -0
- package/src/modules/opportunities/tools.ts +9 -1
- package/src/modules/partnerships/events.ts +9 -0
- package/src/modules/partnerships/index.ts +4 -2
- package/src/modules/partnerships/routes.ts +11 -1
- package/src/modules/partnerships/schema.ts +10 -2
- package/src/modules/partnerships/service.ts +3 -0
- package/src/modules/partnerships/tools.ts +9 -1
- package/src/modules/people/personConsentWrites.ts +1 -1
- package/src/modules/people/schema.ts +1 -1
- package/src/modules/plans/repository.ts +24 -0
- package/src/modules/raises/events.ts +9 -0
- package/src/modules/raises/index.ts +4 -2
- package/src/modules/raises/routes.ts +11 -1
- package/src/modules/raises/schema.ts +10 -2
- package/src/modules/raises/service.ts +3 -0
- package/src/modules/raises/tools.ts +9 -1
- package/src/modules/workspace/routes.ts +0 -6
- package/src/modules/workspace/schema.ts +0 -2
- package/src/modules/workspace/seedStarterForms.ts +98 -0
- package/src/modules/workspace/service.ts +23 -15
- package/src/modules/workspace/tools.ts +1 -1
- package/src/testing/fixtures.ts +1 -0
|
@@ -260,6 +260,48 @@
|
|
|
260
260
|
"when": 1788146891073,
|
|
261
261
|
"tag": "0036_form_field_notice_type",
|
|
262
262
|
"breakpoints": true
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
"idx": 37,
|
|
266
|
+
"version": "7",
|
|
267
|
+
"when": 1788150000000,
|
|
268
|
+
"tag": "0037_drop_lists_consent_purpose_id",
|
|
269
|
+
"breakpoints": true
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
"idx": 38,
|
|
273
|
+
"version": "7",
|
|
274
|
+
"when": 1788214656489,
|
|
275
|
+
"tag": "0038_smart_white_queen",
|
|
276
|
+
"breakpoints": true
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
"idx": 39,
|
|
280
|
+
"version": "7",
|
|
281
|
+
"when": 1788300000000,
|
|
282
|
+
"tag": "0039_form_map_to_open",
|
|
283
|
+
"breakpoints": true
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
"idx": 40,
|
|
287
|
+
"version": "7",
|
|
288
|
+
"when": 1788400000000,
|
|
289
|
+
"tag": "0040_pipeline_converted_to",
|
|
290
|
+
"breakpoints": true
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
"idx": 41,
|
|
294
|
+
"version": "7",
|
|
295
|
+
"when": 1788500000000,
|
|
296
|
+
"tag": "0041_api_key_scopes",
|
|
297
|
+
"breakpoints": true
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
"idx": 42,
|
|
301
|
+
"version": "7",
|
|
302
|
+
"when": 1788600000000,
|
|
303
|
+
"tag": "0042_drop_workspace_identity",
|
|
304
|
+
"breakpoints": true
|
|
263
305
|
}
|
|
264
306
|
]
|
|
265
307
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kelpie/server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "The Kelpie service as a library: module runtime, core CRM modules, REST API, MCP surface, and the shared migration pipeline.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"kelpie",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
},
|
|
53
53
|
"dependencies": {
|
|
54
54
|
"@hono/node-server": "^2.0.12",
|
|
55
|
-
"@kelpie/schemas": "^0.
|
|
55
|
+
"@kelpie/schemas": "^0.11.0",
|
|
56
56
|
"@node-rs/argon2": "^2.0.2",
|
|
57
57
|
"drizzle-orm": "^0.45.2",
|
|
58
58
|
"emittery": "^2.0.0",
|
package/src/app.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { securityHeadersMiddleware } from './lib/securityHeaders.ts'
|
|
|
14
14
|
import type { CredentialDependencies } from './modules/auth/credentials.ts'
|
|
15
15
|
import { MCP_INSTRUCTIONS, MCP_ROUTE_PREFIX, MCP_SERVER_INFO } from './modules/mcp/index.ts'
|
|
16
16
|
import { createMcpEndpoint } from './modules/mcp/router.ts'
|
|
17
|
+
import { createApiKeyScopeMiddleware } from './modules/api-keys/scopeMiddleware.ts'
|
|
17
18
|
import {
|
|
18
19
|
createAuthAndApiRateLimitMiddleware,
|
|
19
20
|
createFormSubmitRateLimitMiddleware,
|
|
@@ -145,6 +146,8 @@ export function createApp(dependencies: AppDependencies): Hono<AppBindings> {
|
|
|
145
146
|
|
|
146
147
|
app.use('/v1/*', workspaceAccessMiddleware)
|
|
147
148
|
|
|
149
|
+
app.use('/v1/*', createApiKeyScopeMiddleware(dependencies.credentials))
|
|
150
|
+
|
|
148
151
|
// Every module's `POST` gets this the same way, decided once here rather than
|
|
149
152
|
// per route (`api.md`). It skips `/v1/public/*` itself — a public request has
|
|
150
153
|
// no `Actor` to scope a key to — so it is mounted ahead of the public CORS
|
package/src/lib/actor.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { EventActor, MemberRole } from '@kelpie/schemas'
|
|
1
|
+
import type { ApiKeyScope, EventActor, MemberRole } from '@kelpie/schemas'
|
|
2
2
|
|
|
3
3
|
import { AppError } from './errors.ts'
|
|
4
4
|
|
|
@@ -39,6 +39,8 @@ export interface ApiKeyActor {
|
|
|
39
39
|
readonly userId: string | null
|
|
40
40
|
readonly workspaceId: string
|
|
41
41
|
readonly role: MemberRole
|
|
42
|
+
/** Empty means full access. Presets and granular tokens may be mixed. */
|
|
43
|
+
readonly scopes: readonly ApiKeyScope[]
|
|
42
44
|
/** Null for a workspace key: it belongs to the workspace, not to a member. */
|
|
43
45
|
readonly memberId: string | null
|
|
44
46
|
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import type { ApiKeyGranularScope, ApiKeyScope } from '@kelpie/schemas'
|
|
2
|
+
import { satisfiesApiKeyScope } from '@kelpie/schemas'
|
|
3
|
+
|
|
4
|
+
import type { Actor } from './actor.ts'
|
|
5
|
+
import { AppError } from './errors.ts'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* REST and MCP scope enforcement for API keys.
|
|
9
|
+
*
|
|
10
|
+
* Sessions are never scoped. An empty scope list on a key means full access.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
interface RouteScopeRule {
|
|
14
|
+
readonly methods: readonly string[]
|
|
15
|
+
readonly pattern: RegExp
|
|
16
|
+
readonly scope: ApiKeyGranularScope
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Paths that need no scope check (session-only or unauthenticated). */
|
|
20
|
+
function isExempt(method: string, path: string): boolean {
|
|
21
|
+
if (path.startsWith('/v1/public/')) {
|
|
22
|
+
return true
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (path.startsWith('/v1/auth/')) {
|
|
26
|
+
return true
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (path.startsWith('/v1/account')) {
|
|
30
|
+
return true
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (method === 'POST' && path === '/v1/workspaces') {
|
|
34
|
+
return true
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return method === 'POST' && path === '/v1/invites/accept'
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const ROUTE_SCOPE_RULES: readonly RouteScopeRule[] = [
|
|
41
|
+
{ methods: ['GET'], pattern: /^\/v1\/people(?:\/[^/]+)?$/u, scope: 'people:read' },
|
|
42
|
+
{ methods: ['POST', 'PATCH', 'DELETE'], pattern: /^\/v1\/people(?:\/[^/]+)?$/u, scope: 'people:write' },
|
|
43
|
+
{ methods: ['GET'], pattern: /^\/v1\/companies(?:\/[^/]+)?$/u, scope: 'companies:read' },
|
|
44
|
+
{ methods: ['POST', 'PATCH', 'DELETE'], pattern: /^\/v1\/companies(?:\/[^/]+)?$/u, scope: 'companies:write' },
|
|
45
|
+
{ methods: ['GET'], pattern: /^\/v1\/positions(?:\/[^/]+)?$/u, scope: 'positions:read' },
|
|
46
|
+
{ methods: ['POST', 'PATCH', 'DELETE'], pattern: /^\/v1\/positions(?:\/[^/]+)?$/u, scope: 'positions:write' },
|
|
47
|
+
{ methods: ['GET'], pattern: /^\/v1\/deals(?:\/[^/]+(?:\/convert)?)?$/u, scope: 'deals:read' },
|
|
48
|
+
{ methods: ['POST', 'PATCH', 'DELETE'], pattern: /^\/v1\/deals(?:\/[^/]+(?:\/convert)?)?$/u, scope: 'deals:write' },
|
|
49
|
+
{ methods: ['GET'], pattern: /^\/v1\/enquiries(?:\/[^/]+(?:\/convert)?)?$/u, scope: 'enquiries:read' },
|
|
50
|
+
{ methods: ['POST', 'PATCH', 'DELETE'], pattern: /^\/v1\/enquiries(?:\/[^/]+(?:\/convert)?)?$/u, scope: 'enquiries:write' },
|
|
51
|
+
{ methods: ['GET'], pattern: /^\/v1\/opportunities(?:\/[^/]+(?:\/convert)?)?$/u, scope: 'opportunities:read' },
|
|
52
|
+
{ methods: ['POST', 'PATCH', 'DELETE'], pattern: /^\/v1\/opportunities(?:\/[^/]+(?:\/convert)?)?$/u, scope: 'opportunities:write' },
|
|
53
|
+
{ methods: ['GET'], pattern: /^\/v1\/partnerships(?:\/[^/]+(?:\/convert)?)?$/u, scope: 'partnerships:read' },
|
|
54
|
+
{ methods: ['POST', 'PATCH', 'DELETE'], pattern: /^\/v1\/partnerships(?:\/[^/]+(?:\/convert)?)?$/u, scope: 'partnerships:write' },
|
|
55
|
+
{ methods: ['GET'], pattern: /^\/v1\/raises(?:\/[^/]+(?:\/convert)?)?$/u, scope: 'raises:read' },
|
|
56
|
+
{ methods: ['POST', 'PATCH', 'DELETE'], pattern: /^\/v1\/raises(?:\/[^/]+(?:\/convert)?)?$/u, scope: 'raises:write' },
|
|
57
|
+
{ methods: ['GET'], pattern: /^\/v1\/roles(?:\/[^/]+)?$/u, scope: 'roles:read' },
|
|
58
|
+
{ methods: ['POST', 'PATCH', 'DELETE'], pattern: /^\/v1\/roles(?:\/[^/]+)?$/u, scope: 'roles:write' },
|
|
59
|
+
{ methods: ['GET'], pattern: /^\/v1\/candidates(?:\/[^/]+)?$/u, scope: 'candidates:read' },
|
|
60
|
+
{ methods: ['POST', 'PATCH', 'DELETE'], pattern: /^\/v1\/candidates(?:\/[^/]+)?$/u, scope: 'candidates:write' },
|
|
61
|
+
{ methods: ['GET'], pattern: /^\/v1\/decisions(?:\/[^/]+)?$/u, scope: 'decisions:read' },
|
|
62
|
+
{ methods: ['POST', 'PATCH', 'DELETE'], pattern: /^\/v1\/decisions(?:\/[^/]+)?$/u, scope: 'decisions:write' },
|
|
63
|
+
{ methods: ['GET'], pattern: /^\/v1\/plan_items(?:\/[^/]+)?$/u, scope: 'plan_items:read' },
|
|
64
|
+
{ methods: ['POST', 'PATCH', 'DELETE'], pattern: /^\/v1\/plan_items(?:\/[^/]+)?$/u, scope: 'plan_items:write' },
|
|
65
|
+
{ methods: ['GET'], pattern: /^\/v1\/notes(?:\/[^/]+)?$/u, scope: 'notes:read' },
|
|
66
|
+
{ methods: ['POST', 'PATCH', 'DELETE'], pattern: /^\/v1\/notes(?:\/[^/]+)?$/u, scope: 'notes:write' },
|
|
67
|
+
{ methods: ['GET'], pattern: /^\/v1\/(?:lists(?:\/[^/]+(?:\/members(?:\/[^/]+)?)?)?|list-memberships)$/u, scope: 'lists:read' },
|
|
68
|
+
{ methods: ['POST', 'PATCH', 'DELETE'], pattern: /^\/v1\/(?:lists(?:\/[^/]+(?:\/members(?:\/[^/]+)?)?)?|list-memberships)$/u, scope: 'lists:write' },
|
|
69
|
+
{ methods: ['GET'], pattern: /^\/v1\/handbook_pages(?:\/[^/]+)?$/u, scope: 'handbook:read' },
|
|
70
|
+
{ methods: ['POST', 'PATCH', 'DELETE'], pattern: /^\/v1\/handbook_pages(?:\/[^/]+)?$/u, scope: 'handbook:write' },
|
|
71
|
+
{ methods: ['GET'], pattern: /^\/v1\/(?:forms(?:\/[^/]+(?:\/(?:submissions(?:\/[^/]+)?|embed))?)?|form-submissions)$/u, scope: 'forms:read' },
|
|
72
|
+
{ methods: ['POST', 'PATCH', 'DELETE'], pattern: /^\/v1\/forms(?:\/[^/]+)?$/u, scope: 'forms:write' },
|
|
73
|
+
{ methods: ['GET'], pattern: /^\/v1\/activities$/u, scope: 'activities:read' },
|
|
74
|
+
{ methods: ['GET'], pattern: /^\/v1\/search$/u, scope: 'search:read' },
|
|
75
|
+
{ methods: ['GET'], pattern: /^\/v1\/dashboard$/u, scope: 'dashboard:read' },
|
|
76
|
+
{ methods: ['GET'], pattern: /^\/v1\/custom_fields(?:\/[^/]+)?$/u, scope: 'custom_fields:read' },
|
|
77
|
+
{ methods: ['POST', 'PATCH', 'DELETE'], pattern: /^\/v1\/custom_fields(?:\/[^/]+)?$/u, scope: 'custom_fields:write' },
|
|
78
|
+
{ methods: ['GET'], pattern: /^\/v1\/pipeline_stages(?:\/[^/]+)?$/u, scope: 'pipeline_stages:read' },
|
|
79
|
+
{ methods: ['POST', 'PATCH', 'DELETE'], pattern: /^\/v1\/pipeline_stages(?:\/[^/]+)?$/u, scope: 'pipeline_stages:write' },
|
|
80
|
+
{ methods: ['GET'], pattern: /^\/v1\/consent_purposes(?:\/[^/]+)?$/u, scope: 'consent_purposes:read' },
|
|
81
|
+
{ methods: ['POST', 'PATCH', 'DELETE'], pattern: /^\/v1\/consent_purposes(?:\/[^/]+)?$/u, scope: 'consent_purposes:write' },
|
|
82
|
+
{ methods: ['GET'], pattern: /^\/v1\/export(?:\/|$)/u, scope: 'import_export:read' },
|
|
83
|
+
{ methods: ['GET'], pattern: /^\/v1\/import\/jobs(?:\/[^/]+)?$/u, scope: 'import_export:read' },
|
|
84
|
+
{ methods: ['POST', 'DELETE'], pattern: /^\/v1\/import\/jobs(?:\/[^/]+(?:\/commit)?)?$/u, scope: 'import_export:write' },
|
|
85
|
+
{ methods: ['GET'], pattern: /^\/v1\/webhooks(?:\/[^/]+(?:\/deliveries)?)?$/u, scope: 'webhooks:read' },
|
|
86
|
+
{ methods: ['POST', 'PATCH', 'DELETE'], pattern: /^\/v1\/webhooks(?:\/[^/]+(?:\/rotate_secret)?)?$/u, scope: 'webhooks:write' },
|
|
87
|
+
{ methods: ['GET'], pattern: /^\/v1\/api-keys(?:\/[^/]+)?$/u, scope: 'api_keys:read' },
|
|
88
|
+
{ methods: ['POST', 'DELETE'], pattern: /^\/v1\/api-keys(?:\/[^/]+)?$/u, scope: 'api_keys:write' },
|
|
89
|
+
{ methods: ['GET'], pattern: /^\/v1\/agent-tasks(?:\/[^/]+(?:\/(?:resolve|run))?)?$/u, scope: 'agent_tasks:read' },
|
|
90
|
+
{ methods: ['POST'], pattern: /^\/v1\/agent-tasks\/[^/]+(?:\/(?:resolve|run))?$/u, scope: 'agent_tasks:write' },
|
|
91
|
+
{ methods: ['GET'], pattern: /^\/v1\/agent-runs(?:\/[^/]+)?$/u, scope: 'agent_runs:read' },
|
|
92
|
+
{ methods: ['GET'], pattern: /^\/v1\/agents(?:\/[^/]+)?$/u, scope: 'agents:read' },
|
|
93
|
+
{ methods: ['POST', 'PATCH', 'DELETE'], pattern: /^\/v1\/agents(?:\/[^/]+)?$/u, scope: 'agents:write' },
|
|
94
|
+
{ methods: ['GET'], pattern: /^\/v1\/workspaces\/[^/]+$/u, scope: 'workspace:read' },
|
|
95
|
+
{ methods: ['PATCH'], pattern: /^\/v1\/workspaces\/[^/]+$/u, scope: 'workspace:write' },
|
|
96
|
+
{ methods: ['GET'], pattern: /^\/v1\/workspaces\/[^/]+\/members(?:\/[^/]+)?$/u, scope: 'workspace:read' },
|
|
97
|
+
{ methods: ['PATCH', 'DELETE'], pattern: /^\/v1\/workspaces\/[^/]+\/members(?:\/[^/]+)?$/u, scope: 'workspace:write' },
|
|
98
|
+
{ methods: ['GET'], pattern: /^\/v1\/workspaces\/[^/]+\/invites(?:\/[^/]+(?:\/resend)?)?$/u, scope: 'workspace:read' },
|
|
99
|
+
{ methods: ['POST', 'DELETE'], pattern: /^\/v1\/workspaces\/[^/]+\/invites(?:\/[^/]+(?:\/resend)?)?$/u, scope: 'workspace:write' },
|
|
100
|
+
{ methods: ['GET'], pattern: /^\/v1\/workspaces\/[^/]+\/modules(?:\/[^/]+)?$/u, scope: 'modules:read' },
|
|
101
|
+
{ methods: ['PATCH'], pattern: /^\/v1\/workspaces\/[^/]+\/modules(?:\/[^/]+)?$/u, scope: 'modules:write' },
|
|
102
|
+
{ methods: ['POST'], pattern: /^\/v1\/workspaces\/[^/]+\/sample-data$/u, scope: 'sample_data:write' },
|
|
103
|
+
{ methods: ['POST'], pattern: /^\/v1\/workspaces\/[^/]+\/relink-email-domains$/u, scope: 'workspace:write' },
|
|
104
|
+
{ methods: ['GET'], pattern: /^\/v1\/mcp\/tools$/u, scope: 'search:read' },
|
|
105
|
+
]
|
|
106
|
+
|
|
107
|
+
function storedScopes(actor: Actor): readonly ApiKeyScope[] {
|
|
108
|
+
if (actor.kind !== 'api_key') {
|
|
109
|
+
return []
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return actor.scopes
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function hasApiKeyScope(actor: Actor, required: ApiKeyGranularScope): boolean {
|
|
116
|
+
if (actor.kind !== 'api_key') {
|
|
117
|
+
return true
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return satisfiesApiKeyScope(actor.scopes, required)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function requireApiKeyScope(actor: Actor, required: ApiKeyGranularScope): void {
|
|
124
|
+
if (hasApiKeyScope(actor, required)) {
|
|
125
|
+
return
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
throw new AppError('forbidden', `This API key does not have the ${required} scope`)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function resolveRestScope(method: string, path: string): ApiKeyGranularScope | null {
|
|
132
|
+
if (isExempt(method, path)) {
|
|
133
|
+
return null
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (method === 'DELETE' && /^\/v1\/workspaces\/[^/]+$/u.test(path)) {
|
|
137
|
+
return 'workspace:write'
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
for (const rule of ROUTE_SCOPE_RULES) {
|
|
141
|
+
if (rule.methods.includes(method) && rule.pattern.test(path)) {
|
|
142
|
+
return rule.scope
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return null
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const MCP_READ_VERBS = new Set(['list', 'get', 'query'])
|
|
150
|
+
const MCP_WRITE_VERBS = new Set(['create', 'update', 'delete', 'commit', 'resolve', 'run', 'rotate_secret', 'install'])
|
|
151
|
+
|
|
152
|
+
/** Maps REST resource segments used in MCP tool names to scope resources. */
|
|
153
|
+
const MCP_RESOURCE_ALIASES: Readonly<Record<string, string>> = {
|
|
154
|
+
handbook_pages: 'handbook',
|
|
155
|
+
plan_items: 'plan_items',
|
|
156
|
+
pipeline_stages: 'pipeline_stages',
|
|
157
|
+
custom_fields: 'custom_fields',
|
|
158
|
+
consent_purposes: 'consent_purposes',
|
|
159
|
+
form_submissions: 'forms',
|
|
160
|
+
list_memberships: 'lists',
|
|
161
|
+
agent_tasks: 'agent_tasks',
|
|
162
|
+
agent_runs: 'agent_runs',
|
|
163
|
+
import_jobs: 'import_export',
|
|
164
|
+
export: 'import_export',
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function resolveMcpScope(toolName: string): ApiKeyGranularScope | null {
|
|
168
|
+
if (toolName === 'search_query') {
|
|
169
|
+
return 'search:read'
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const match = /^([a-z0-9_]+)_(list|get|create|update|delete|query|commit|resolve|run|rotate_secret|install)$/u.exec(
|
|
173
|
+
toolName,
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
if (match === null) {
|
|
177
|
+
const parts = toolName.split('_')
|
|
178
|
+
const verb = parts.at(-1)
|
|
179
|
+
|
|
180
|
+
if (verb === undefined) {
|
|
181
|
+
return null
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const resource = MCP_RESOURCE_ALIASES[parts.slice(0, -1).join('_')] ?? parts.slice(0, -1).join('_')
|
|
185
|
+
|
|
186
|
+
if (MCP_READ_VERBS.has(verb)) {
|
|
187
|
+
return `${resource}:read` as ApiKeyGranularScope
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (MCP_WRITE_VERBS.has(verb)) {
|
|
191
|
+
return `${resource}:write` as ApiKeyGranularScope
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return null
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const [, rawResource, verb] = match
|
|
198
|
+
const resource = MCP_RESOURCE_ALIASES[rawResource ?? ''] ?? rawResource ?? ''
|
|
199
|
+
|
|
200
|
+
if (MCP_READ_VERBS.has(verb ?? '')) {
|
|
201
|
+
return `${resource}:read` as ApiKeyGranularScope
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (MCP_WRITE_VERBS.has(verb ?? '')) {
|
|
205
|
+
return `${resource}:write` as ApiKeyGranularScope
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return null
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function actorHasStoredScopes(actor: Actor): boolean {
|
|
212
|
+
return storedScopes(actor).length > 0
|
|
213
|
+
}
|
|
@@ -193,3 +193,27 @@ export async function deleteForTarget(
|
|
|
193
193
|
|
|
194
194
|
return deleted.length
|
|
195
195
|
}
|
|
196
|
+
|
|
197
|
+
/** Moves every activity from one target to another inside a conversion transaction. */
|
|
198
|
+
export async function repointForTarget(
|
|
199
|
+
db: Queryable,
|
|
200
|
+
workspaceId: string,
|
|
201
|
+
fromType: string,
|
|
202
|
+
fromId: string,
|
|
203
|
+
toType: string,
|
|
204
|
+
toId: string,
|
|
205
|
+
): Promise<number> {
|
|
206
|
+
const updated = await db
|
|
207
|
+
.update(activities)
|
|
208
|
+
.set({ targetType: toType, targetId: toId })
|
|
209
|
+
.where(
|
|
210
|
+
and(
|
|
211
|
+
eq(activities.workspaceId, workspaceId),
|
|
212
|
+
eq(activities.targetType, fromType),
|
|
213
|
+
eq(activities.targetId, fromId),
|
|
214
|
+
),
|
|
215
|
+
)
|
|
216
|
+
.returning({ id: activities.id })
|
|
217
|
+
|
|
218
|
+
return updated.length
|
|
219
|
+
}
|
|
@@ -150,3 +150,27 @@ export async function updateRun(
|
|
|
150
150
|
): Promise<void> {
|
|
151
151
|
await db.update(agentRuns).set(changes).where(eq(agentRuns.id, id))
|
|
152
152
|
}
|
|
153
|
+
|
|
154
|
+
/** Moves every agent run from one target to another inside a conversion transaction. */
|
|
155
|
+
export async function repointForTarget(
|
|
156
|
+
db: Queryable,
|
|
157
|
+
workspaceId: string,
|
|
158
|
+
fromType: string,
|
|
159
|
+
fromId: string,
|
|
160
|
+
toType: string,
|
|
161
|
+
toId: string,
|
|
162
|
+
): Promise<number> {
|
|
163
|
+
const updated = await db
|
|
164
|
+
.update(agentRuns)
|
|
165
|
+
.set({ targetType: toType, targetId: toId })
|
|
166
|
+
.where(
|
|
167
|
+
and(
|
|
168
|
+
eq(agentRuns.workspaceId, workspaceId),
|
|
169
|
+
eq(agentRuns.targetType, fromType),
|
|
170
|
+
eq(agentRuns.targetId, fromId),
|
|
171
|
+
),
|
|
172
|
+
)
|
|
173
|
+
.returning({ id: agentRuns.id })
|
|
174
|
+
|
|
175
|
+
return updated.length
|
|
176
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { ApiKeyScope } from '@kelpie/schemas'
|
|
2
|
+
import { API_KEY_SCOPES } from '@kelpie/schemas'
|
|
1
3
|
import type { Context, Hono } from 'hono'
|
|
2
4
|
import { z } from 'zod'
|
|
3
5
|
|
|
@@ -13,6 +15,7 @@ import type { ApiKeyService, ApiKeyView, MintedApiKey } from './service.ts'
|
|
|
13
15
|
const createBody = z.object({
|
|
14
16
|
name: z.string().min(1),
|
|
15
17
|
kind: z.enum(KEY_KINDS),
|
|
18
|
+
scopes: z.array(z.enum(API_KEY_SCOPES)).optional(),
|
|
16
19
|
})
|
|
17
20
|
|
|
18
21
|
const listQuery = z.object({ kind: z.enum(KEY_KINDS) })
|
|
@@ -39,6 +42,7 @@ function keyResponse(key: ApiKeyView): Record<string, unknown> {
|
|
|
39
42
|
id: key.id,
|
|
40
43
|
name: key.name,
|
|
41
44
|
kind: key.kind,
|
|
45
|
+
scopes: key.scopes,
|
|
42
46
|
display_prefix: key.displayPrefix,
|
|
43
47
|
last_used_at: key.lastUsedAt === null ? null : key.lastUsedAt.toISOString(),
|
|
44
48
|
created_at: key.createdAt.toISOString(),
|
|
@@ -55,6 +59,7 @@ export function mountApiKeyRoutes(router: Hono, dependencies: ApiKeyRoutesDepend
|
|
|
55
59
|
await requireActor(context),
|
|
56
60
|
body.name,
|
|
57
61
|
body.kind,
|
|
62
|
+
body.scopes as ApiKeyScope[] | undefined,
|
|
58
63
|
)
|
|
59
64
|
|
|
60
65
|
return context.json({ ...keyResponse(minted), secret: minted.secret }, 201)
|
|
@@ -20,6 +20,8 @@ export const apiKeys = pgTable(
|
|
|
20
20
|
name: text('name').notNull(),
|
|
21
21
|
secretHash: text('secret_hash').notNull().unique(),
|
|
22
22
|
displayPrefix: text('display_prefix').notNull(),
|
|
23
|
+
/** Empty array means full access. Presets and granular tokens may be mixed. */
|
|
24
|
+
scopes: text('scopes').array().notNull().default([]),
|
|
23
25
|
lastUsedAt: moment('last_used_at'),
|
|
24
26
|
createdAt: createdAt(),
|
|
25
27
|
updatedAt: updatedAt(),
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { MiddlewareHandler } from 'hono'
|
|
2
|
+
|
|
3
|
+
import { requireApiKeyScope, resolveRestScope } from '../../lib/apiKeyScopes.ts'
|
|
4
|
+
import { resolveActorFrom } from '../auth/credentials.ts'
|
|
5
|
+
import type { CredentialDependencies } from '../auth/credentials.ts'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Blocks API-key requests that lack the scope a route needs. Sessions pass
|
|
9
|
+
* through unchanged.
|
|
10
|
+
*/
|
|
11
|
+
export function createApiKeyScopeMiddleware(
|
|
12
|
+
dependencies: CredentialDependencies,
|
|
13
|
+
): MiddlewareHandler {
|
|
14
|
+
return async (context, next) => {
|
|
15
|
+
const required = resolveRestScope(context.req.method, context.req.path)
|
|
16
|
+
|
|
17
|
+
if (required === null) {
|
|
18
|
+
await next()
|
|
19
|
+
return
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const actor = await resolveActorFrom(dependencies, context)
|
|
23
|
+
|
|
24
|
+
if (actor.kind === 'api_key') {
|
|
25
|
+
requireApiKeyScope(actor, required)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
await next()
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
import type { ApiKeyScope } from '@kelpie/schemas'
|
|
2
|
+
import { API_KEY_SCOPES, dedupeApiKeyScopes } from '@kelpie/schemas'
|
|
3
|
+
|
|
1
4
|
import type { Database } from '../../lib/database.ts'
|
|
2
5
|
import { AppError } from '../../lib/errors.ts'
|
|
6
|
+
import { hasApiKeyScope } from '../../lib/apiKeyScopes.ts'
|
|
3
7
|
import type { IdFactory } from '../../lib/ids.ts'
|
|
4
8
|
import type { Actor } from '../auth/actor.ts'
|
|
5
9
|
import { roleAllows } from '../workspace/roles.ts'
|
|
@@ -27,6 +31,7 @@ export interface ApiKeyView {
|
|
|
27
31
|
readonly id: string
|
|
28
32
|
readonly name: string
|
|
29
33
|
readonly kind: KeyKind
|
|
34
|
+
readonly scopes: readonly ApiKeyScope[]
|
|
30
35
|
readonly displayPrefix: string
|
|
31
36
|
readonly lastUsedAt: Date | null
|
|
32
37
|
readonly createdAt: Date
|
|
@@ -38,7 +43,7 @@ export interface MintedApiKey extends ApiKeyView {
|
|
|
38
43
|
}
|
|
39
44
|
|
|
40
45
|
export interface ApiKeyService {
|
|
41
|
-
create(actor: Actor, name: string, kind: KeyKind): Promise<MintedApiKey>
|
|
46
|
+
create(actor: Actor, name: string, kind: KeyKind, scopes?: readonly ApiKeyScope[]): Promise<MintedApiKey>
|
|
42
47
|
list(actor: Actor, kind: KeyKind): Promise<readonly ApiKeyView[]>
|
|
43
48
|
revoke(actor: Actor, id: string): Promise<void>
|
|
44
49
|
}
|
|
@@ -48,12 +53,31 @@ function toView(record: repository.ApiKeyRecord): ApiKeyView {
|
|
|
48
53
|
id: record.id,
|
|
49
54
|
name: record.name,
|
|
50
55
|
kind: record.userId === null ? 'workspace' : 'personal',
|
|
56
|
+
scopes: record.scopes as ApiKeyScope[],
|
|
51
57
|
displayPrefix: record.displayPrefix,
|
|
52
58
|
lastUsedAt: record.lastUsedAt,
|
|
53
59
|
createdAt: record.createdAt,
|
|
54
60
|
}
|
|
55
61
|
}
|
|
56
62
|
|
|
63
|
+
function parseScopes(scopes: readonly ApiKeyScope[] | undefined): ApiKeyScope[] {
|
|
64
|
+
if (scopes === undefined || scopes.length === 0) {
|
|
65
|
+
return []
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const deduped = dedupeApiKeyScopes(scopes)
|
|
69
|
+
const allowed = new Set<string>(API_KEY_SCOPES)
|
|
70
|
+
const invalid = deduped.filter((scope) => !allowed.has(scope))
|
|
71
|
+
|
|
72
|
+
if (invalid.length > 0) {
|
|
73
|
+
throw AppError.validationFailed('One or more scopes are not valid', [
|
|
74
|
+
{ field: 'scopes', message: `Unknown scopes: ${invalid.join(', ')}` },
|
|
75
|
+
])
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return deduped
|
|
79
|
+
}
|
|
80
|
+
|
|
57
81
|
export function createApiKeyService(dependencies: ApiKeyDependencies): ApiKeyService {
|
|
58
82
|
/**
|
|
59
83
|
* Keys are always workspace-bound, so an actor with no workspace cannot have
|
|
@@ -92,14 +116,19 @@ export function createApiKeyService(dependencies: ApiKeyDependencies): ApiKeySer
|
|
|
92
116
|
}
|
|
93
117
|
|
|
94
118
|
return {
|
|
95
|
-
async create(actor, name, kind) {
|
|
119
|
+
async create(actor, name, kind, scopes) {
|
|
96
120
|
const workspaceId = requireWorkspace(actor)
|
|
97
121
|
const userId = kind === 'workspace' ? null : requireUser(actor)
|
|
122
|
+
const storedScopes = parseScopes(scopes)
|
|
98
123
|
|
|
99
124
|
if (kind === 'workspace') {
|
|
100
125
|
requireAdmin(actor)
|
|
101
126
|
}
|
|
102
127
|
|
|
128
|
+
if (actor.kind === 'api_key' && !hasApiKeyScope(actor, 'api_keys:write')) {
|
|
129
|
+
throw new AppError('forbidden', 'This API key does not have the api_keys:write scope')
|
|
130
|
+
}
|
|
131
|
+
|
|
103
132
|
const minted = mintKey(kind, dependencies.newToken)
|
|
104
133
|
const record = await repository.insertApiKey(dependencies.db, {
|
|
105
134
|
id: dependencies.createId('apiKey'),
|
|
@@ -108,6 +137,7 @@ export function createApiKeyService(dependencies: ApiKeyDependencies): ApiKeySer
|
|
|
108
137
|
name,
|
|
109
138
|
secretHash: minted.secretHash,
|
|
110
139
|
displayPrefix: minted.displayPrefix,
|
|
140
|
+
scopes: storedScopes,
|
|
111
141
|
})
|
|
112
142
|
|
|
113
143
|
// The only time the secret leaves this process. Nothing stores it.
|
|
@@ -146,6 +176,10 @@ export function createApiKeyService(dependencies: ApiKeyDependencies): ApiKeySer
|
|
|
146
176
|
throw AppError.notFound('API key not found')
|
|
147
177
|
}
|
|
148
178
|
|
|
179
|
+
if (actor.kind === 'api_key' && !hasApiKeyScope(actor, 'api_keys:write')) {
|
|
180
|
+
throw new AppError('forbidden', 'This API key does not have the api_keys:write scope')
|
|
181
|
+
}
|
|
182
|
+
|
|
149
183
|
await repository.deleteApiKey(dependencies.db, workspaceId, id)
|
|
150
184
|
},
|
|
151
185
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { getCookie } from 'hono/cookie'
|
|
2
2
|
import type { Context } from 'hono'
|
|
3
3
|
|
|
4
|
+
import type { ApiKeyScope } from '@kelpie/schemas'
|
|
5
|
+
|
|
4
6
|
import type { Database } from '../../lib/database.ts'
|
|
5
7
|
import { AppError } from '../../lib/errors.ts'
|
|
6
8
|
import { hashToken } from '../../lib/tokens.ts'
|
|
@@ -100,6 +102,7 @@ async function resolveApiKeyActor(
|
|
|
100
102
|
userId: null,
|
|
101
103
|
workspaceId: record.workspaceId,
|
|
102
104
|
role: WORKSPACE_KEY_ROLE,
|
|
105
|
+
scopes: record.scopes as ApiKeyScope[],
|
|
103
106
|
memberId: null,
|
|
104
107
|
}
|
|
105
108
|
}
|
|
@@ -118,6 +121,7 @@ async function resolveApiKeyActor(
|
|
|
118
121
|
userId: record.userId,
|
|
119
122
|
workspaceId: record.workspaceId,
|
|
120
123
|
role: roleFromMembership(membership.role),
|
|
124
|
+
scopes: record.scopes as ApiKeyScope[],
|
|
121
125
|
memberId: membership.id,
|
|
122
126
|
}
|
|
123
127
|
}
|
|
@@ -10,7 +10,7 @@ import { registerConsentPurposesTools } from './tools.ts'
|
|
|
10
10
|
* Consent purposes: the workspace catalog every capture site names.
|
|
11
11
|
*
|
|
12
12
|
* Structural because Person consent rows carry `purpose_id`, and forms /
|
|
13
|
-
*
|
|
13
|
+
* forms / imports each reference one; a disabled module would leave those
|
|
14
14
|
* references without a definition to resolve.
|
|
15
15
|
*
|
|
16
16
|
* Registers before people, forms, lists, and import-export so their `_fk`
|
|
@@ -7,9 +7,9 @@ import { workspaces } from '../workspace/schema.ts'
|
|
|
7
7
|
/**
|
|
8
8
|
* Workspace-defined consent purposes.
|
|
9
9
|
*
|
|
10
|
-
* Every capture site — a form's consent field,
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* Every capture site — a form's consent field, an import job, the manual
|
|
11
|
+
* override on a Person — names one of these. `slug` is set at create and
|
|
12
|
+
* immutable after: a rename would strand every form/import that names it and
|
|
13
13
|
* names it and every `person_consents` row that carries it (identified by
|
|
14
14
|
* the purpose id). The strict PATCH body naturally refuses either as a `422`.
|
|
15
15
|
*
|
|
@@ -20,7 +20,6 @@ import type { Transaction, TransactionScope } from '../../runtime/transaction.ts
|
|
|
20
20
|
import type { Actor } from '../auth/actor.ts'
|
|
21
21
|
import { requireWorkspaceId } from '../auth/actor.ts'
|
|
22
22
|
import { formFields } from '../forms/schema.ts'
|
|
23
|
-
import { lists } from '../lists/schema.ts'
|
|
24
23
|
import { roleAllows } from '../workspace/roles.ts'
|
|
25
24
|
import { CONSENT_PURPOSES_LIMIT } from './capabilities.ts'
|
|
26
25
|
import './events.ts'
|
|
@@ -39,7 +38,7 @@ import type {
|
|
|
39
38
|
*
|
|
40
39
|
* Reads are open to any workspace member — every capture site needs to list
|
|
41
40
|
* them to render its purpose picker. Writes need the admin role: a purpose
|
|
42
|
-
* change touches every form
|
|
41
|
+
* change touches every form and import job that names it, and it is
|
|
43
42
|
* team-wide config rather than a per-user preference.
|
|
44
43
|
*/
|
|
45
44
|
|
|
@@ -143,17 +142,16 @@ function validateDefaultStatus(status: string): void {
|
|
|
143
142
|
}
|
|
144
143
|
|
|
145
144
|
/**
|
|
146
|
-
* A purpose still named by a form's consent field
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
* name one.
|
|
145
|
+
* A purpose still named by a form's consent field cannot be deleted. Counted
|
|
146
|
+
* here so the 409 names the referencing type; the FK on either side would
|
|
147
|
+
* refuse the delete on its own but only name one.
|
|
150
148
|
*/
|
|
151
149
|
async function referencingTypes(
|
|
152
150
|
db: Database | Transaction,
|
|
153
151
|
workspaceId: string,
|
|
154
152
|
id: string,
|
|
155
153
|
): Promise<readonly string[]> {
|
|
156
|
-
const [fieldRow
|
|
154
|
+
const [fieldRow] = await Promise.all([
|
|
157
155
|
db
|
|
158
156
|
.select({ count: sql<number>`count(*)::int` })
|
|
159
157
|
.from(formFields)
|
|
@@ -165,19 +163,12 @@ async function referencingTypes(
|
|
|
165
163
|
sql`${id} = ANY(${formFields.consentPurposeIds})`,
|
|
166
164
|
),
|
|
167
165
|
),
|
|
168
|
-
db
|
|
169
|
-
.select({ count: sql<number>`count(*)::int` })
|
|
170
|
-
.from(lists)
|
|
171
|
-
.where(and(eq(lists.workspaceId, workspaceId), eq(lists.consentPurposeId, id))),
|
|
172
166
|
])
|
|
173
167
|
|
|
174
168
|
const referencing: string[] = []
|
|
175
169
|
if (Number(fieldRow[0]?.count ?? 0) > 0) {
|
|
176
170
|
referencing.push('form_field')
|
|
177
171
|
}
|
|
178
|
-
if (Number(listRow[0]?.count ?? 0) > 0) {
|
|
179
|
-
referencing.push('list')
|
|
180
|
-
}
|
|
181
172
|
return referencing
|
|
182
173
|
}
|
|
183
174
|
|
|
@@ -390,7 +381,7 @@ export function createConsentPurposesService(
|
|
|
390
381
|
const referencing = await referencingTypes(tx, workspaceId, id)
|
|
391
382
|
if (referencing.length > 0) {
|
|
392
383
|
throw AppError.conflict(
|
|
393
|
-
'Remove this purpose from every form
|
|
384
|
+
'Remove this purpose from every form field that names it before deleting it',
|
|
394
385
|
referencing.map((type) => ({ field: type, message: 'still references this purpose' })),
|
|
395
386
|
)
|
|
396
387
|
}
|
|
@@ -400,7 +391,7 @@ export function createConsentPurposesService(
|
|
|
400
391
|
} catch (error: unknown) {
|
|
401
392
|
if (postgresErrorCode(error) === FOREIGN_KEY_VIOLATION) {
|
|
402
393
|
throw AppError.conflict(
|
|
403
|
-
'Remove this purpose from every form
|
|
394
|
+
'Remove this purpose from every form field that names it before deleting it',
|
|
404
395
|
[{ field: 'id', message: 'still referenced' }],
|
|
405
396
|
)
|
|
406
397
|
}
|