@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.
Files changed (40) hide show
  1. package/dist/lib/ids.d.ts +0 -1
  2. package/dist/lib/ids.d.ts.map +1 -1
  3. package/dist/lib/ids.js +0 -1
  4. package/dist/lib/ids.js.map +1 -1
  5. package/dist/modules/core.d.ts +0 -5
  6. package/dist/modules/core.d.ts.map +1 -1
  7. package/dist/modules/core.js +0 -17
  8. package/dist/modules/core.js.map +1 -1
  9. package/dist/modules/import-export/drafts.d.ts.map +1 -1
  10. package/dist/modules/import-export/drafts.js +0 -1
  11. package/dist/modules/import-export/drafts.js.map +1 -1
  12. package/dist/modules/reseal.d.ts.map +1 -1
  13. package/dist/modules/reseal.js +10 -21
  14. package/dist/modules/reseal.js.map +1 -1
  15. package/dist/modules/workspace/repository.d.ts +14 -1
  16. package/dist/modules/workspace/repository.d.ts.map +1 -1
  17. package/dist/modules/workspace/repository.js.map +1 -1
  18. package/dist/modules/workspace/service.d.ts +15 -5
  19. package/dist/modules/workspace/service.d.ts.map +1 -1
  20. package/dist/modules/workspace/service.js.map +1 -1
  21. package/dist/runtime/registry.d.ts +14 -4
  22. package/dist/runtime/registry.d.ts.map +1 -1
  23. package/dist/runtime/registry.js.map +1 -1
  24. package/dist/schema/index.d.ts +0 -1
  25. package/dist/schema/index.d.ts.map +1 -1
  26. package/dist/schema/index.js +0 -1
  27. package/dist/schema/index.js.map +1 -1
  28. package/migrations/0017_drop_integration_connections.sql +1 -0
  29. package/migrations/meta/0017_snapshot.json +5035 -0
  30. package/migrations/meta/_journal.json +8 -1
  31. package/package.json +2 -2
  32. package/src/lib/ids.ts +0 -1
  33. package/src/modules/core.ts +0 -25
  34. package/src/modules/import-export/drafts.ts +15 -2
  35. package/src/modules/reseal.ts +10 -22
  36. package/src/modules/workspace/repository.ts +12 -1
  37. package/src/modules/workspace/service.ts +15 -5
  38. package/src/runtime/registry.ts +14 -4
  39. package/src/schema/index.ts +0 -1
  40. 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.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.3.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
@@ -36,7 +36,6 @@ export const idPrefixes = {
36
36
  agentRegistration: 'ag',
37
37
  agentRun: 'run',
38
38
  importJob: 'imp',
39
- integrationConnection: 'int',
40
39
  /** Never returns over the wire either: identified by `(workspace_id, module_id)` instead. */
41
40
  moduleSetting: 'mset',
42
41
  /**
@@ -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
- /** Drops the keys whose value is undefined, so a draft holds only what it sets. */
124
- function present<T extends object>(draft: T): T {
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
 
@@ -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 the two nothing writes yet. Covering
43
- * a column before its first write is the cheap half of this: the expensive half
44
- * is noticing, a year later, that a rotation reported success while stranding a
45
- * customer's stored OAuth credential with no way back. `resealTest` asserts this
46
- * list against the schema, so a fourth `_encrypted` column fails a test the day
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: Partial<typeof workspaces.$inferInsert>,
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 {
@@ -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. */
@@ -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
- )