@pikku/kysely 0.13.24 → 0.13.28

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 (80) hide show
  1. package/CHANGELOG.md +271 -0
  2. package/dist/src/index.d.ts +4 -1
  3. package/dist/src/index.js +3 -1
  4. package/dist/src/kysely-analytics-service.d.ts +30 -0
  5. package/dist/src/kysely-analytics-service.js +80 -0
  6. package/dist/src/kysely-feature-flag-store.d.ts +37 -0
  7. package/dist/src/kysely-feature-flag-store.js +239 -0
  8. package/dist/src/kysely-scope-service.d.ts +1 -0
  9. package/dist/src/kysely-scope-service.js +29 -0
  10. package/dist/src/kysely-tables.d.ts +45 -0
  11. package/dist/src/schema/analytics.schema.d.ts +17 -0
  12. package/dist/src/schema/analytics.schema.js +48 -0
  13. package/dist/src/schema/flag.schema.d.ts +16 -0
  14. package/dist/src/schema/flag.schema.js +62 -0
  15. package/dist/src/schema/index.d.ts +2 -0
  16. package/dist/src/schema/index.js +6 -0
  17. package/package.json +11 -6
  18. package/run-tests.sh +9 -1
  19. package/bin/pikku-kysely-pure.ts +0 -59
  20. package/dist/src/kysely-lock-vault.d.ts +0 -34
  21. package/dist/src/kysely-lock-vault.js +0 -77
  22. package/dist/src/schema/data-lock.schema.d.ts +0 -15
  23. package/dist/src/schema/data-lock.schema.js +0 -27
  24. package/dist/tsconfig.tsbuildinfo +0 -1
  25. package/src/classification-crypto.test.ts +0 -143
  26. package/src/classification-crypto.ts +0 -163
  27. package/src/classification-plugin.pglite.test.ts +0 -226
  28. package/src/classification-plugin.test.ts +0 -265
  29. package/src/classification-plugin.ts +0 -249
  30. package/src/coercion-plugin.test.ts +0 -160
  31. package/src/coercion-plugin.ts +0 -187
  32. package/src/create-audited-kysely.test.ts +0 -560
  33. package/src/create-audited-kysely.ts +0 -254
  34. package/src/index.ts +0 -81
  35. package/src/kysely-agent-run-scores.ts +0 -47
  36. package/src/kysely-agent-run-service.ts +0 -244
  37. package/src/kysely-agent-run-state-service.ts +0 -200
  38. package/src/kysely-agent-storage-service.ts +0 -632
  39. package/src/kysely-audit-service.test.ts +0 -352
  40. package/src/kysely-audit-service.ts +0 -237
  41. package/src/kysely-channel-store.ts +0 -107
  42. package/src/kysely-credential-service.ts +0 -286
  43. package/src/kysely-deployment-service.ts +0 -163
  44. package/src/kysely-eventhub-store.ts +0 -53
  45. package/src/kysely-json.ts +0 -5
  46. package/src/kysely-scope-service.test.ts +0 -418
  47. package/src/kysely-scope-service.ts +0 -425
  48. package/src/kysely-secret-service.ts +0 -242
  49. package/src/kysely-services.test.ts +0 -405
  50. package/src/kysely-session-store.ts +0 -57
  51. package/src/kysely-tables.test.ts +0 -110
  52. package/src/kysely-tables.ts +0 -434
  53. package/src/kysely-virtual-user-run-store.test.ts +0 -369
  54. package/src/kysely-virtual-user-run-store.ts +0 -235
  55. package/src/kysely-virtual-user-schedule-store.test.ts +0 -187
  56. package/src/kysely-virtual-user-schedule-store.ts +0 -187
  57. package/src/kysely-webhook-service.test.ts +0 -153
  58. package/src/kysely-webhook-service.ts +0 -139
  59. package/src/kysely-workflow-mirror.test.ts +0 -259
  60. package/src/kysely-workflow-mirror.ts +0 -374
  61. package/src/kysely-workflow-run-service.ts +0 -305
  62. package/src/kysely-workflow-service.test.ts +0 -544
  63. package/src/kysely-workflow-service.ts +0 -820
  64. package/src/schema/agent.schema.ts +0 -162
  65. package/src/schema/audit.schema.ts +0 -57
  66. package/src/schema/channel.schema.ts +0 -38
  67. package/src/schema/credential.schema.ts +0 -58
  68. package/src/schema/deployment.schema.ts +0 -47
  69. package/src/schema/index.ts +0 -441
  70. package/src/schema/pikku-schema.types.ts +0 -169
  71. package/src/schema/schema.test.ts +0 -591
  72. package/src/schema/scope.schema.ts +0 -110
  73. package/src/schema/secret.schema.ts +0 -49
  74. package/src/schema/session.schema.ts +0 -20
  75. package/src/schema/virtual-user-schedule.schema.ts +0 -48
  76. package/src/schema/virtual-user.schema.ts +0 -106
  77. package/src/schema/webhook.schema.ts +0 -58
  78. package/src/schema/workflow.schema.ts +0 -150
  79. package/src/serialize-plugin.ts +0 -233
  80. package/tsconfig.json +0 -17
@@ -1,286 +0,0 @@
1
- import {
2
- deriveKEK,
3
- envelopeDecrypt,
4
- envelopeEncrypt,
5
- envelopeRewrap,
6
- generateKEKSalt,
7
- } from '@pikku/core/crypto-utils'
8
- import type { CredentialService } from '@pikku/core/services'
9
- import type { Kysely } from 'kysely'
10
- import type { KyselyPikkuDB } from './kysely-tables.js'
11
- import { requirePikkuSchema } from './schema/index.js'
12
- import { credentialSchema } from './schema/credential.schema.js'
13
-
14
- export interface KyselyCredentialServiceConfig {
15
- key: string
16
- keyVersion?: number
17
- previousKey?: string
18
- audit?: boolean
19
- auditReads?: boolean
20
- }
21
-
22
- export class KyselyCredentialService implements CredentialService {
23
- private initialized = false
24
- private kekSalts = new Map<number, string>()
25
- private keks = new Map<number, CryptoKey>()
26
- private key: string
27
- private keyVersion: number
28
- private previousKey?: string
29
- private audit: boolean
30
- private auditReads: boolean
31
-
32
- constructor(
33
- private db: Kysely<KyselyPikkuDB>,
34
- config: KyselyCredentialServiceConfig
35
- ) {
36
- this.key = config.key
37
- this.keyVersion = config.keyVersion ?? 1
38
- this.previousKey = config.previousKey
39
- this.audit = config.audit ?? false
40
- this.auditReads = config.auditReads ?? false
41
- }
42
-
43
- public async init(): Promise<void> {
44
- if (this.initialized) return
45
- await requirePikkuSchema(this.db, credentialSchema)
46
- this.initialized = true
47
- }
48
-
49
- private async logAudit(
50
- name: string,
51
- userId: string | undefined,
52
- action: 'read' | 'write' | 'delete' | 'rotate'
53
- ): Promise<void> {
54
- if (!this.audit) return
55
- if (action === 'read' && !this.auditReads) return
56
-
57
- await this.db
58
- .insertInto('credentialsAudit')
59
- .values({
60
- id: crypto.randomUUID(),
61
- credentialName: name,
62
- userId: userId ?? null,
63
- action,
64
- performedAt: new Date().toISOString() as unknown as Date,
65
- })
66
- .execute()
67
- }
68
-
69
- private async getKEKSalt(version: number): Promise<string> {
70
- const cached = this.kekSalts.get(version)
71
- if (cached) return cached
72
-
73
- const existing = await this.db
74
- .selectFrom('credentialKekSalts')
75
- .select('salt')
76
- .where('keyVersion', '=', version)
77
- .executeTakeFirst()
78
-
79
- let salt = existing?.salt
80
- if (!salt) {
81
- await this.db
82
- .insertInto('credentialKekSalts')
83
- .values({
84
- keyVersion: version,
85
- salt: generateKEKSalt(),
86
- createdAt: new Date().toISOString() as unknown as Date,
87
- })
88
- .onConflict((oc) => oc.column('keyVersion').doNothing())
89
- .execute()
90
-
91
- const row = await this.db
92
- .selectFrom('credentialKekSalts')
93
- .select('salt')
94
- .where('keyVersion', '=', version)
95
- .executeTakeFirstOrThrow()
96
- salt = row.salt
97
- }
98
-
99
- this.kekSalts.set(version, salt)
100
- return salt
101
- }
102
-
103
- private async getKEK(version: number): Promise<CryptoKey> {
104
- const cached = this.keks.get(version)
105
- if (cached) return cached
106
-
107
- const passphrase = version === this.keyVersion ? this.key : this.previousKey
108
- if (!passphrase) {
109
- throw new Error(`No KEK available for key_version ${version}`)
110
- }
111
-
112
- const kek = await deriveKEK(passphrase, await this.getKEKSalt(version))
113
- this.keks.set(version, kek)
114
- return kek
115
- }
116
-
117
- private whereUserId(qb: any, userId?: string) {
118
- return userId
119
- ? qb.where('userId', '=', userId)
120
- : qb.where('userId', 'is', null)
121
- }
122
-
123
- async get<T = unknown>(name: string, userId?: string): Promise<T | null> {
124
- let qb = this.db
125
- .selectFrom('credentials')
126
- .select(['ciphertext', 'wrappedDek', 'keyVersion'])
127
- .where('name', '=', name)
128
- qb = this.whereUserId(qb, userId)
129
-
130
- const row = await qb.executeTakeFirst()
131
- if (!row) return null
132
-
133
- const kek = await this.getKEK(row.keyVersion)
134
- const plaintext = await envelopeDecrypt<string>(
135
- kek,
136
- row.ciphertext,
137
- row.wrappedDek
138
- )
139
- await this.logAudit(name, userId, 'read')
140
-
141
- try {
142
- return JSON.parse(plaintext) as T
143
- } catch {
144
- throw new Error(`Credential '${name}' contains invalid data`)
145
- }
146
- }
147
-
148
- async set(name: string, value: unknown, userId?: string): Promise<void> {
149
- const plaintext = JSON.stringify(value)
150
- const { ciphertext, wrappedDEK } = await envelopeEncrypt(
151
- await this.getKEK(this.keyVersion),
152
- plaintext
153
- )
154
- const now = new Date().toISOString()
155
- const exists = await this.has(name, userId)
156
-
157
- if (exists) {
158
- let qb = this.db
159
- .updateTable('credentials')
160
- .set({
161
- ciphertext,
162
- wrappedDek: wrappedDEK,
163
- keyVersion: this.keyVersion,
164
- updatedAt: now as unknown as Date,
165
- })
166
- .where('name', '=', name)
167
- qb = this.whereUserId(qb, userId)
168
- await qb.execute()
169
- } else {
170
- await this.db
171
- .insertInto('credentials')
172
- .values({
173
- name,
174
- userId: userId ?? null,
175
- ciphertext,
176
- wrappedDek: wrappedDEK,
177
- keyVersion: this.keyVersion,
178
- createdAt: now as unknown as Date,
179
- updatedAt: now as unknown as Date,
180
- })
181
- .execute()
182
- }
183
-
184
- await this.logAudit(name, userId, 'write')
185
- }
186
-
187
- async delete(name: string, userId?: string): Promise<void> {
188
- let qb = this.db.deleteFrom('credentials').where('name', '=', name)
189
- qb = this.whereUserId(qb, userId)
190
- await qb.execute()
191
-
192
- await this.logAudit(name, userId, 'delete')
193
- }
194
-
195
- async has(name: string, userId?: string): Promise<boolean> {
196
- let qb = this.db
197
- .selectFrom('credentials')
198
- .select('name')
199
- .where('name', '=', name)
200
- qb = this.whereUserId(qb, userId)
201
-
202
- const row = await qb.executeTakeFirst()
203
- return !!row
204
- }
205
-
206
- async getAll(userId: string): Promise<Record<string, unknown>> {
207
- const rows = await this.db
208
- .selectFrom('credentials')
209
- .select(['name', 'ciphertext', 'wrappedDek', 'keyVersion'])
210
- .where('userId', '=', userId)
211
- .execute()
212
-
213
- const result: Record<string, unknown> = {}
214
- for (const row of rows) {
215
- const kek = await this.getKEK(row.keyVersion)
216
- const plaintext = await envelopeDecrypt<string>(
217
- kek,
218
- row.ciphertext,
219
- row.wrappedDek
220
- )
221
- try {
222
- result[row.name] = JSON.parse(plaintext)
223
- } catch {
224
- throw new Error(`Credential '${row.name}' contains invalid data`)
225
- }
226
- await this.logAudit(row.name, userId, 'read')
227
- }
228
-
229
- return result
230
- }
231
-
232
- async getUsersWithCredential(name: string): Promise<string[]> {
233
- const rows = await this.db
234
- .selectFrom('credentials')
235
- .select('userId')
236
- .where('name', '=', name)
237
- .where('userId', 'is not', null)
238
- .execute()
239
-
240
- return rows.map((row) => row.userId!).filter(Boolean)
241
- }
242
-
243
- async getAllUsers(): Promise<string[]> {
244
- const rows = await this.db
245
- .selectFrom('credentials')
246
- .select('userId')
247
- .distinct()
248
- .where('userId', 'is not', null)
249
- .execute()
250
-
251
- return rows.map((row) => row.userId!).filter(Boolean)
252
- }
253
-
254
- async rotateKEK(): Promise<number> {
255
- if (!this.previousKey) {
256
- throw new Error('No previousKey configured — nothing to rotate from')
257
- }
258
-
259
- const rows = await this.db
260
- .selectFrom('credentials')
261
- .select(['name', 'userId', 'wrappedDek'])
262
- .where('keyVersion', '<', this.keyVersion)
263
- .execute()
264
-
265
- const oldKEK = await this.getKEK(this.keyVersion - 1)
266
- const newKEK = await this.getKEK(this.keyVersion)
267
-
268
- for (const row of rows) {
269
- const newWrappedDEK = await envelopeRewrap(oldKEK, newKEK, row.wrappedDek)
270
- let qb = this.db
271
- .updateTable('credentials')
272
- .set({
273
- wrappedDek: newWrappedDEK,
274
- keyVersion: this.keyVersion,
275
- updatedAt: new Date().toISOString() as unknown as Date,
276
- })
277
- .where('name', '=', row.name)
278
- qb = this.whereUserId(qb, row.userId ?? undefined)
279
- await qb.execute()
280
-
281
- await this.logAudit(row.name, row.userId ?? undefined, 'rotate')
282
- }
283
-
284
- return rows.length
285
- }
286
- }
@@ -1,163 +0,0 @@
1
- import { buildRemoteHeaders } from '@pikku/core/remote'
2
- import type {
3
- DeploymentService,
4
- DeploymentServiceConfig,
5
- DeploymentConfig,
6
- } from '@pikku/core/services'
7
- import type { JWTService } from '@pikku/core/services'
8
- import type { SecretService } from '@pikku/core/services'
9
- import { getAllFunctionNames } from '@pikku/core/function'
10
- import type { Kysely } from 'kysely'
11
- import type { KyselyPikkuDB } from './kysely-tables.js'
12
- import { requirePikkuSchema } from './schema/index.js'
13
- import { deploymentSchema } from './schema/deployment.schema.js'
14
-
15
- export class KyselyDeploymentService implements DeploymentService {
16
- private initialized = false
17
- private heartbeatTimer?: ReturnType<typeof setInterval>
18
- private deploymentConfig?: DeploymentConfig
19
- private heartbeatInterval: number
20
- private heartbeatTtl: number
21
-
22
- constructor(
23
- config: DeploymentServiceConfig,
24
- protected db: Kysely<KyselyPikkuDB>,
25
- private jwt?: JWTService,
26
- private secrets?: SecretService
27
- ) {
28
- this.heartbeatInterval = config.heartbeatInterval ?? 10000
29
- this.heartbeatTtl = config.heartbeatTtl ?? 30000
30
- }
31
-
32
- public async init(): Promise<void> {
33
- if (this.initialized) return
34
- await requirePikkuSchema(this.db, deploymentSchema)
35
- this.initialized = true
36
- }
37
-
38
- async start(config: DeploymentConfig): Promise<void> {
39
- const functions = config.functions ?? getAllFunctionNames()
40
- this.deploymentConfig = { ...config, functions }
41
-
42
- await this.db.transaction().execute(async (trx) => {
43
- await trx
44
- .insertInto('pikkuDeployments')
45
- .values({
46
- deploymentId: config.deploymentId,
47
- endpoint: config.endpoint,
48
- lastHeartbeat: new Date(),
49
- })
50
- .onConflict((oc) =>
51
- oc.column('deploymentId').doUpdateSet({
52
- endpoint: config.endpoint,
53
- lastHeartbeat: new Date(),
54
- })
55
- )
56
- .execute()
57
-
58
- await trx
59
- .deleteFrom('pikkuDeploymentFunctions')
60
- .where('deploymentId', '=', config.deploymentId)
61
- .execute()
62
-
63
- if (functions.length > 0) {
64
- await trx
65
- .insertInto('pikkuDeploymentFunctions')
66
- .values(
67
- functions.map((fn) => ({
68
- deploymentId: config.deploymentId,
69
- functionName: fn,
70
- }))
71
- )
72
- .execute()
73
- }
74
- })
75
-
76
- this.heartbeatTimer = setInterval(
77
- () => this.sendHeartbeat(),
78
- this.heartbeatInterval
79
- )
80
- }
81
-
82
- async stop(): Promise<void> {
83
- if (this.heartbeatTimer) {
84
- clearInterval(this.heartbeatTimer)
85
- this.heartbeatTimer = undefined
86
- }
87
-
88
- if (this.deploymentConfig) {
89
- await this.db
90
- .deleteFrom('pikkuDeployments')
91
- .where('deploymentId', '=', this.deploymentConfig.deploymentId)
92
- .execute()
93
- }
94
- }
95
-
96
- async invoke(
97
- funcName: string,
98
- data: unknown,
99
- session?: unknown,
100
- traceId?: string
101
- ): Promise<unknown> {
102
- const headers = await buildRemoteHeaders(
103
- this.jwt,
104
- this.secrets,
105
- funcName,
106
- session,
107
- traceId
108
- )
109
- const ttlMs = this.heartbeatTtl
110
- const cutoff = new Date(Date.now() - ttlMs)
111
-
112
- const result = await this.db
113
- .selectFrom('pikkuDeployments as d')
114
- .innerJoin(
115
- 'pikkuDeploymentFunctions as f',
116
- 'f.deploymentId',
117
- 'd.deploymentId'
118
- )
119
- .select(['d.deploymentId', 'd.endpoint'])
120
- .where('f.functionName', '=', funcName)
121
- .where('d.lastHeartbeat', '>', cutoff)
122
- .orderBy('d.lastHeartbeat', 'desc')
123
- .limit(1)
124
- .execute()
125
-
126
- if (result.length === 0) {
127
- throw new Error(`No deployment found for function '${funcName}'`)
128
- }
129
-
130
- const endpoint = result[0].endpoint
131
- const url = `${endpoint}/remote/rpc/${encodeURIComponent(funcName)}`
132
- const response = await fetch(url, {
133
- method: 'POST',
134
- headers: {
135
- 'content-type': 'application/json',
136
- ...headers,
137
- },
138
- body: JSON.stringify({ data }),
139
- })
140
-
141
- if (!response.ok) {
142
- throw new Error(
143
- `Remote RPC call to '${funcName}' failed: ${response.status}`
144
- )
145
- }
146
-
147
- return response.json()
148
- }
149
-
150
- private async sendHeartbeat(): Promise<void> {
151
- if (!this.deploymentConfig) return
152
-
153
- try {
154
- await this.db
155
- .updateTable('pikkuDeployments')
156
- .set({ lastHeartbeat: new Date() })
157
- .where('deploymentId', '=', this.deploymentConfig.deploymentId)
158
- .execute()
159
- } catch {
160
- // Heartbeat failed, will retry on next interval
161
- }
162
- }
163
- }
@@ -1,53 +0,0 @@
1
- import { EventHubStore } from '@pikku/core/channel'
2
- import type { Kysely } from 'kysely'
3
- import type { KyselyPikkuDB } from './kysely-tables.js'
4
-
5
- export class KyselyEventHubStore extends EventHubStore {
6
- private initialized = false
7
-
8
- constructor(private db: Kysely<KyselyPikkuDB>) {
9
- super()
10
- }
11
-
12
- public async init(): Promise<void> {
13
- if (this.initialized) {
14
- return
15
- }
16
- this.initialized = true
17
- }
18
-
19
- public async getChannelIdsForTopic(topic: string): Promise<string[]> {
20
- const result = await this.db
21
- .selectFrom('channelSubscriptions')
22
- .select('channelId')
23
- .where('topic', '=', topic)
24
- .execute()
25
-
26
- return result.map((row) => row.channelId)
27
- }
28
-
29
- public async subscribe(topic: string, channelId: string): Promise<boolean> {
30
- try {
31
- await this.db
32
- .insertInto('channelSubscriptions')
33
- .values({ channelId: channelId, topic: topic as string })
34
- .onConflict((oc) => oc.columns(['channelId', 'topic']).doNothing())
35
- .execute()
36
- return true
37
- } catch {
38
- return false
39
- }
40
- }
41
-
42
- public async unsubscribe(topic: string, channelId: string): Promise<boolean> {
43
- const result = await this.db
44
- .deleteFrom('channelSubscriptions')
45
- .where('channelId', '=', channelId)
46
- .where('topic', '=', topic as string)
47
- .executeTakeFirst()
48
-
49
- return BigInt(result.numDeletedRows) > 0n
50
- }
51
-
52
- public async close(): Promise<void> {}
53
- }
@@ -1,5 +0,0 @@
1
- export function parseJson(val: unknown): any {
2
- if (val == null) return undefined
3
- if (typeof val === 'string') return JSON.parse(val)
4
- return val
5
- }