@kelpie/server 0.3.0 → 0.4.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/lib/ids.d.ts +0 -1
- package/dist/lib/ids.d.ts.map +1 -1
- package/dist/lib/ids.js +0 -1
- package/dist/lib/ids.js.map +1 -1
- package/dist/modules/core.d.ts +0 -5
- package/dist/modules/core.d.ts.map +1 -1
- package/dist/modules/core.js +0 -17
- package/dist/modules/core.js.map +1 -1
- package/dist/modules/import-export/drafts.d.ts.map +1 -1
- package/dist/modules/import-export/drafts.js +0 -1
- package/dist/modules/import-export/drafts.js.map +1 -1
- package/dist/modules/reseal.d.ts.map +1 -1
- package/dist/modules/reseal.js +10 -21
- package/dist/modules/reseal.js.map +1 -1
- package/dist/modules/workspace/repository.d.ts +14 -1
- package/dist/modules/workspace/repository.d.ts.map +1 -1
- package/dist/modules/workspace/repository.js.map +1 -1
- package/dist/modules/workspace/service.d.ts +15 -5
- package/dist/modules/workspace/service.d.ts.map +1 -1
- package/dist/modules/workspace/service.js.map +1 -1
- package/dist/runtime/registry.d.ts +14 -4
- package/dist/runtime/registry.d.ts.map +1 -1
- package/dist/runtime/registry.js.map +1 -1
- package/dist/schema/index.d.ts +0 -1
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema/index.js +0 -1
- package/dist/schema/index.js.map +1 -1
- package/migrations/0017_drop_integration_connections.sql +1 -0
- package/migrations/meta/0017_snapshot.json +5035 -0
- package/migrations/meta/_journal.json +8 -1
- package/package.json +2 -2
- package/src/lib/ids.ts +0 -1
- package/src/modules/core.ts +0 -25
- package/src/modules/import-export/drafts.ts +15 -2
- package/src/modules/reseal.ts +10 -22
- package/src/modules/workspace/repository.ts +12 -1
- package/src/modules/workspace/service.ts +15 -5
- package/src/runtime/registry.ts +14 -4
- package/src/schema/index.ts +0 -1
- package/src/modules/integrations/schema.ts +0 -31
|
@@ -120,6 +120,13 @@
|
|
|
120
120
|
"when": 1786410884983,
|
|
121
121
|
"tag": "0016_misty_mentor",
|
|
122
122
|
"breakpoints": true
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
"idx": 17,
|
|
126
|
+
"version": "7",
|
|
127
|
+
"when": 1786490413323,
|
|
128
|
+
"tag": "0017_drop_integration_connections",
|
|
129
|
+
"breakpoints": true
|
|
123
130
|
}
|
|
124
131
|
]
|
|
125
|
-
}
|
|
132
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kelpie/server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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.4.0",
|
|
56
56
|
"@node-rs/argon2": "^2.0.2",
|
|
57
57
|
"drizzle-orm": "^0.45.2",
|
|
58
58
|
"hono": "^4.12.32",
|
package/src/lib/ids.ts
CHANGED
package/src/modules/core.ts
CHANGED
|
@@ -13,7 +13,6 @@ import { createFormsModule } from './forms/index.ts'
|
|
|
13
13
|
import { createHandbookModule } from './handbook/index.ts'
|
|
14
14
|
import { createHiringModule } from './hiring/index.ts'
|
|
15
15
|
import { createImportExportModule } from './import-export/index.ts'
|
|
16
|
-
import * as integrations from './integrations/schema.ts'
|
|
17
16
|
import { createNotesModule } from './notes/index.ts'
|
|
18
17
|
import { createOpportunitiesModule } from './opportunities/index.ts'
|
|
19
18
|
import { createPartnershipsModule } from './partnerships/index.ts'
|
|
@@ -40,21 +39,6 @@ import { createWorkspaceModule } from './workspace/index.ts'
|
|
|
40
39
|
*/
|
|
41
40
|
export const coreMigrationsDirectory = fileURLToPath(new URL('../../migrations', import.meta.url))
|
|
42
41
|
|
|
43
|
-
interface CoreModuleDefinition {
|
|
44
|
-
readonly id: string
|
|
45
|
-
readonly requires?: readonly string[]
|
|
46
|
-
readonly tables: Readonly<Record<string, unknown>>
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const definitions: readonly CoreModuleDefinition[] = [
|
|
50
|
-
{ id: 'integrations', requires: ['workspace'], tables: integrations },
|
|
51
|
-
]
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Modules with behaviour are written out; the rest contribute only tables so far
|
|
55
|
-
* and are generated from the table above. As each grows routes and services it
|
|
56
|
-
* moves out of `definitions` into its own module file, like `auth` has.
|
|
57
|
-
*/
|
|
58
42
|
export const coreModules: readonly KelpieModule[] = [
|
|
59
43
|
createAuthModule(coreMigrationsDirectory),
|
|
60
44
|
createWorkspaceModule(coreMigrationsDirectory),
|
|
@@ -81,13 +65,4 @@ export const coreModules: readonly KelpieModule[] = [
|
|
|
81
65
|
createImportExportModule(coreMigrationsDirectory),
|
|
82
66
|
createAgentTasksModule(coreMigrationsDirectory),
|
|
83
67
|
createWebhooksModule(coreMigrationsDirectory),
|
|
84
|
-
...definitions.map((definition): KelpieModule => ({
|
|
85
|
-
id: definition.id,
|
|
86
|
-
...(definition.requires === undefined ? {} : { requires: definition.requires }),
|
|
87
|
-
register(context) {
|
|
88
|
-
context.schema(definition.tables, coreMigrationsDirectory)
|
|
89
|
-
|
|
90
|
-
return Promise.resolve()
|
|
91
|
-
},
|
|
92
|
-
})),
|
|
93
68
|
]
|
|
@@ -120,8 +120,21 @@ function enumeration(
|
|
|
120
120
|
return canonicalEnum(values, mapped[column]) ?? undefined
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
-
/**
|
|
124
|
-
|
|
123
|
+
/**
|
|
124
|
+
* Drops the keys whose value is undefined, so a draft holds only what it sets.
|
|
125
|
+
*
|
|
126
|
+
* The parameter is not `T`. Under `exactOptionalPropertyTypes` an optional key
|
|
127
|
+
* declared `name?: string` may be absent but may not be present holding
|
|
128
|
+
* `undefined`, and every literal below spells out all of its keys and lets the
|
|
129
|
+
* extractors return `undefined` for the columns the row did not carry. So the
|
|
130
|
+
* input is `T` with every key required and `undefined` allowed, and the return
|
|
131
|
+
* is `T`, where the absences this function creates are the only ones. Requiring
|
|
132
|
+
* the keys is the useful half: a field left out of a literal is a compile error
|
|
133
|
+
* rather than a column that silently never imports.
|
|
134
|
+
*/
|
|
135
|
+
type Supplied<T> = { [K in keyof T]-?: T[K] | undefined }
|
|
136
|
+
|
|
137
|
+
function present<T extends object>(draft: Supplied<T>): T {
|
|
125
138
|
return Object.fromEntries(Object.entries(draft).filter(([, value]) => value !== undefined)) as T
|
|
126
139
|
}
|
|
127
140
|
|
package/src/modules/reseal.ts
CHANGED
|
@@ -4,7 +4,6 @@ import type { Database } from '../lib/database.ts'
|
|
|
4
4
|
import { SecretDecryptionError } from '../lib/secrets.ts'
|
|
5
5
|
import type { SecretCipher } from '../lib/secrets.ts'
|
|
6
6
|
import { agentRegistrations } from './agent-tasks/schema.ts'
|
|
7
|
-
import { integrationConnections } from './integrations/schema.ts'
|
|
8
7
|
import { webhooks } from './webhooks/schema.ts'
|
|
9
8
|
|
|
10
9
|
/**
|
|
@@ -37,18 +36,20 @@ interface SealedColumn {
|
|
|
37
36
|
}
|
|
38
37
|
|
|
39
38
|
/**
|
|
40
|
-
* Every column holding a value sealed by `lib/secrets.ts`.
|
|
39
|
+
* Every column in core's schema holding a value sealed by `lib/secrets.ts`.
|
|
41
40
|
*
|
|
42
|
-
* All three that exist are here, including
|
|
43
|
-
* a column before its first write is the cheap half of
|
|
44
|
-
* is noticing, a year later, that a rotation reported
|
|
45
|
-
* customer's stored
|
|
46
|
-
* list against the schema, so a fourth `_encrypted` column
|
|
47
|
-
* it is added rather than at the next rotation.
|
|
41
|
+
* All three that exist are here, including `agent_registrations`, which nothing
|
|
42
|
+
* writes yet. Covering a column before its first write is the cheap half of
|
|
43
|
+
* this: the expensive half is noticing, a year later, that a rotation reported
|
|
44
|
+
* success while stranding a customer's stored credential with no way back. The
|
|
45
|
+
* test asserts this list against the schema, so a fourth `_encrypted` column
|
|
46
|
+
* fails the day it is added rather than at the next rotation.
|
|
48
47
|
*
|
|
49
48
|
* There is no registry for modules to declare these through, on purpose. One
|
|
50
49
|
* caller does not need an extension point, and a sealed column that nothing
|
|
51
|
-
* re-seals is a bug whichever way it was registered.
|
|
50
|
+
* re-seals is a bug whichever way it was registered. A module outside core
|
|
51
|
+
* seals under the same key through `createSecretCipher` and brings its own pass
|
|
52
|
+
* over its own tables, which this one cannot see.
|
|
52
53
|
*/
|
|
53
54
|
const SEALED_COLUMNS: readonly SealedColumn[] = [
|
|
54
55
|
{
|
|
@@ -79,19 +80,6 @@ const SEALED_COLUMNS: readonly SealedColumn[] = [
|
|
|
79
80
|
.where(eq(agentRegistrations.id, id))
|
|
80
81
|
},
|
|
81
82
|
},
|
|
82
|
-
{
|
|
83
|
-
label: 'integration_connections.secrets_encrypted',
|
|
84
|
-
read: async (db) =>
|
|
85
|
-
db
|
|
86
|
-
.select({ id: integrationConnections.id, sealed: integrationConnections.secretsEncrypted })
|
|
87
|
-
.from(integrationConnections),
|
|
88
|
-
write: async (db, id, sealed) => {
|
|
89
|
-
await db
|
|
90
|
-
.update(integrationConnections)
|
|
91
|
-
.set({ secretsEncrypted: sealed })
|
|
92
|
-
.where(eq(integrationConnections.id, id))
|
|
93
|
-
},
|
|
94
|
-
},
|
|
95
83
|
]
|
|
96
84
|
|
|
97
85
|
/** The columns this pass covers. Exported for the test that checks none is missing. */
|
|
@@ -41,10 +41,21 @@ export async function findWorkspace(db: Queryable, id: string): Promise<Workspac
|
|
|
41
41
|
return found
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* A PATCH's changes, as they arrive: every key optional, and a key may be
|
|
46
|
+
* present holding `undefined` because that is what a parsed request body looks
|
|
47
|
+
* like. `Partial<T>` alone permits only the absence, not the `undefined`.
|
|
48
|
+
*
|
|
49
|
+
* Passing `undefined` through is safe rather than merely tolerated. Drizzle's
|
|
50
|
+
* `mapUpdateSet` filters those entries out before building the `set` clause, so
|
|
51
|
+
* a field the caller did not send is left alone instead of being written null.
|
|
52
|
+
*/
|
|
53
|
+
type Changes<T> = { [K in keyof T]?: T[K] | undefined }
|
|
54
|
+
|
|
44
55
|
export async function updateWorkspace(
|
|
45
56
|
db: Queryable,
|
|
46
57
|
id: string,
|
|
47
|
-
changes:
|
|
58
|
+
changes: Changes<typeof workspaces.$inferInsert>,
|
|
48
59
|
): Promise<WorkspaceRecord | undefined> {
|
|
49
60
|
const [updated] = await db.update(workspaces).set(changes).where(eq(workspaces.id, id)).returning()
|
|
50
61
|
|
|
@@ -83,12 +83,22 @@ export interface CreateWorkspaceInput {
|
|
|
83
83
|
readonly timezone: string
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
/**
|
|
87
|
+
* A partial update, on `api.md`'s rule: an absent field is not being changed,
|
|
88
|
+
* and `null` clears one.
|
|
89
|
+
*
|
|
90
|
+
* Every field spells out `| undefined` because a caller reaches this with the
|
|
91
|
+
* parsed request body, and Zod produces a key holding `undefined` rather than no
|
|
92
|
+
* key at all. Under `exactOptionalPropertyTypes` those are different types, and
|
|
93
|
+
* they are not different behaviour here: `update` spreads this into Drizzle's
|
|
94
|
+
* `set`, which skips undefined values.
|
|
95
|
+
*/
|
|
86
96
|
export interface UpdateWorkspaceInput {
|
|
87
|
-
readonly name?: string
|
|
88
|
-
readonly slug?: string
|
|
89
|
-
readonly timezone?: string
|
|
90
|
-
readonly tagline?: string | null
|
|
91
|
-
readonly oneLiner?: string | null
|
|
97
|
+
readonly name?: string | undefined
|
|
98
|
+
readonly slug?: string | undefined
|
|
99
|
+
readonly timezone?: string | undefined
|
|
100
|
+
readonly tagline?: string | null | undefined
|
|
101
|
+
readonly oneLiner?: string | null | undefined
|
|
92
102
|
}
|
|
93
103
|
|
|
94
104
|
export interface WorkspaceService {
|
package/src/runtime/registry.ts
CHANGED
|
@@ -43,22 +43,32 @@ export interface ModuleRouter {
|
|
|
43
43
|
readonly router: Hono
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
/**
|
|
47
|
+
* What `registerModules` needs.
|
|
48
|
+
*
|
|
49
|
+
* Every optional field spells out `| undefined`, which is not noise. Under
|
|
50
|
+
* `exactOptionalPropertyTypes` an omitted key and a key holding `undefined` are
|
|
51
|
+
* different types, and an assembly building these options has the second:
|
|
52
|
+
* `readModuleConfigFile` returns `undefined` when no override file is
|
|
53
|
+
* configured. Without it, core hands a caller a value its own runtime refuses.
|
|
54
|
+
* None of the four fields tells the two cases apart.
|
|
55
|
+
*/
|
|
46
56
|
export interface ModuleRuntimeOptions {
|
|
47
57
|
readonly modules: readonly KelpieModule[]
|
|
48
58
|
/** Raw variables. Each module validates the slice it needs via `context.config`. */
|
|
49
59
|
readonly environment: Environment
|
|
50
60
|
readonly logger: Logger
|
|
51
61
|
/** Injected so a test can watch what core modules subscribe to. Defaults to a fresh bus. */
|
|
52
|
-
readonly events?: EventBus
|
|
62
|
+
readonly events?: EventBus | undefined
|
|
53
63
|
/** Injected so a test can grant or deny before core modules register. */
|
|
54
|
-
readonly entitlements?: EntitlementRegistry
|
|
64
|
+
readonly entitlements?: EntitlementRegistry | undefined
|
|
55
65
|
/** The database, transaction scope, and collaborators every module builds on. */
|
|
56
66
|
readonly services: ModuleServices
|
|
57
67
|
/**
|
|
58
68
|
* The deploy-time module override (`lib/moduleConfig.ts`), parsed. A locked
|
|
59
69
|
* module id wins over whatever a workspace's own settings say.
|
|
60
70
|
*/
|
|
61
|
-
readonly moduleConfig?: Readonly<Record<string, boolean>>
|
|
71
|
+
readonly moduleConfig?: Readonly<Record<string, boolean>> | undefined
|
|
62
72
|
/**
|
|
63
73
|
* Resolves the caller of a REST request, the same way every route already
|
|
64
74
|
* does (`modules/auth/credentials.ts`), so a non-structural module's router
|
|
@@ -70,7 +80,7 @@ export interface ModuleRuntimeOptions {
|
|
|
70
80
|
* every test that does not exercise module toggling wants without having to
|
|
71
81
|
* say so.
|
|
72
82
|
*/
|
|
73
|
-
readonly resolveActor?: (context: Context) => Promise<Actor>
|
|
83
|
+
readonly resolveActor?: ((context: Context) => Promise<Actor>) | undefined
|
|
74
84
|
}
|
|
75
85
|
|
|
76
86
|
/** Contributions accumulate here, one mutable set per registration pass. */
|
package/src/schema/index.ts
CHANGED
|
@@ -27,5 +27,4 @@ export * from '../modules/forms/schema.ts'
|
|
|
27
27
|
export * from '../modules/import-export/schema.ts'
|
|
28
28
|
export * from '../modules/agent-tasks/schema.ts'
|
|
29
29
|
export * from '../modules/webhooks/schema.ts'
|
|
30
|
-
export * from '../modules/integrations/schema.ts'
|
|
31
30
|
export * from '../modules/rate-limit/schema.ts'
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import { index, jsonb, pgTable, text } from 'drizzle-orm/pg-core'
|
|
2
|
-
|
|
3
|
-
import { createdAt, moment, primaryId, updatedAt } from '../../lib/columns.ts'
|
|
4
|
-
import { users } from '../auth/schema.ts'
|
|
5
|
-
import { workspaces } from '../workspace/schema.ts'
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Core owns the connection lifecycle; the provider itself ships as a module and
|
|
9
|
-
* declares `provider_id`. Module-owned provider tables reference this row.
|
|
10
|
-
*
|
|
11
|
-
* `user_id` is set for personal connections such as a Gmail mailbox, null for
|
|
12
|
-
* workspace-wide ones.
|
|
13
|
-
*/
|
|
14
|
-
export const integrationConnections = pgTable(
|
|
15
|
-
'integration_connections',
|
|
16
|
-
{
|
|
17
|
-
id: primaryId(),
|
|
18
|
-
workspaceId: text('workspace_id')
|
|
19
|
-
.notNull()
|
|
20
|
-
.references(() => workspaces.id, { onDelete: 'cascade' }),
|
|
21
|
-
providerId: text('provider_id').notNull(),
|
|
22
|
-
userId: text('user_id').references(() => users.id, { onDelete: 'cascade' }),
|
|
23
|
-
status: text('status').notNull(),
|
|
24
|
-
config: jsonb('config').notNull().default({}),
|
|
25
|
-
secretsEncrypted: text('secrets_encrypted'),
|
|
26
|
-
lastSyncAt: moment('last_sync_at'),
|
|
27
|
-
createdAt: createdAt(),
|
|
28
|
-
updatedAt: updatedAt(),
|
|
29
|
-
},
|
|
30
|
-
(table) => [index('integration_connections_workspace_idx').on(table.workspaceId)],
|
|
31
|
-
)
|