@pikku/kysely 0.13.34 → 0.13.37
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/CHANGELOG.md +82 -0
- package/dist/bin/pikku-kysely-pure.js +4 -1
- package/dist/src/classification-crypto.js +16 -2
- package/dist/src/index.d.ts +2 -2
- package/dist/src/index.js +2 -2
- package/dist/src/kysely-agent-run-service.js +2 -2
- package/dist/src/kysely-agent-storage-service.js +5 -9
- package/dist/src/kysely-analytics-service.js +2 -5
- package/dist/src/kysely-audit-service.js +2 -5
- package/dist/src/kysely-credential-service.js +7 -10
- package/dist/src/kysely-deployment-service.js +7 -10
- package/dist/src/kysely-eventhub-store.js +3 -4
- package/dist/src/kysely-feature-flag-store.d.ts +3 -3
- package/dist/src/kysely-feature-flag-store.js +47 -41
- package/dist/src/kysely-lease-clock.d.ts +1 -1
- package/dist/src/kysely-lease-clock.js +5 -1
- package/dist/src/kysely-lease-service.js +4 -5
- package/dist/src/kysely-scope-service.js +62 -42
- package/dist/src/kysely-secret-service.js +10 -17
- package/dist/src/kysely-session-store.js +3 -9
- package/dist/src/kysely-tables.d.ts +3 -3
- package/dist/src/kysely-trigger-source-store.js +3 -4
- package/dist/src/kysely-upsert.d.ts +37 -0
- package/dist/src/kysely-upsert.js +54 -0
- package/dist/src/kysely-virtual-user-run-store.d.ts +1 -1
- package/dist/src/kysely-virtual-user-run-store.js +8 -8
- package/dist/src/kysely-virtual-user-schedule-store.d.ts +4 -4
- package/dist/src/kysely-virtual-user-schedule-store.js +16 -19
- package/dist/src/kysely-workflow-mirror.js +2 -1
- package/dist/src/kysely-workflow-service.js +5 -1
- package/dist/src/schema/index.d.ts +11 -0
- package/dist/src/schema/index.js +29 -0
- package/dist/src/schema/virtual-user-schedule.schema.d.ts +3 -3
- package/dist/src/schema/virtual-user-schedule.schema.js +5 -5
- package/dist/src/schema/virtual-user.schema.d.ts +3 -3
- package/dist/src/schema/virtual-user.schema.js +9 -9
- package/knowledge/decisions/a-console-role-cannot-take-a-system-roles-name.md +25 -0
- package/knowledge/decisions/a-credential-with-no-user-is-one-row-per-name.md +22 -0
- package/knowledge/decisions/a-virtual-users-schedule-starts-disabled.md +23 -0
- package/knowledge/decisions/classification-driven-encryption-happens-in-a-plugin.md +1 -1
- package/knowledge/decisions/grants-reference-declared-scopes-and-users-by-foreign-key.md +29 -0
- package/knowledge/decisions/index.md +5 -2
- package/knowledge/decisions/the-secrets-audit-table-is-declared-even-when-auditing-is-off.md +22 -0
- package/knowledge/index.md +1 -0
- package/knowledge/questions/do-the-kysely-stores-check-who-calls-them.md +27 -0
- package/knowledge/questions/does-a-trigger-source-row-hold-only-provider-identifiers.md +26 -0
- package/knowledge/questions/index.md +17 -0
- package/knowledge/questions/should-pikku-user-sessions-expire-on-their-own.md +21 -0
- package/knowledge/questions/should-secret-and-credential-reads-be-audited-by-default.md +32 -0
- package/package.json +3 -2
|
@@ -2,6 +2,17 @@ import { type Kysely } from 'kysely';
|
|
|
2
2
|
import { type PikkuSchema, type RequiredTypes, type SchemaRequirement } from './pikku-schema.types.js';
|
|
3
3
|
export type { PikkuSchema, RequiredTypes, SchemaContext, SchemaRequirement, SchemaStatement, SchemaStatementFactory, } from './pikku-schema.types.js';
|
|
4
4
|
export { rawStatement, requiredType, schemaContext, unqualifiedContext, } from './pikku-schema.types.js';
|
|
5
|
+
/**
|
|
6
|
+
* Re-point the schemas that reference Better Auth's `user` table at the table
|
|
7
|
+
* the project actually maps that model to.
|
|
8
|
+
*
|
|
9
|
+
* Better Auth's `user.modelName` decides the physical name: an app on a legacy
|
|
10
|
+
* `users` table has no `user` for the declaration to point at, so the scope
|
|
11
|
+
* schema read as unmet and was silently left out. The requirement, the column
|
|
12
|
+
* type lookup and every foreign key target are rewritten together, because they
|
|
13
|
+
* name the table three different ways.
|
|
14
|
+
*/
|
|
15
|
+
export declare const withUserTable: (schemas: PikkuSchema[], userTable: string) => PikkuSchema[];
|
|
5
16
|
/**
|
|
6
17
|
* Every table any part of the pikku runtime can need, in dependency order.
|
|
7
18
|
*
|
package/dist/src/schema/index.js
CHANGED
|
@@ -18,6 +18,35 @@ import { workflowSchema } from './workflow.schema.js';
|
|
|
18
18
|
import { virtualUserSchema } from './virtual-user.schema.js';
|
|
19
19
|
import { virtualUserScheduleSchema } from './virtual-user-schedule.schema.js';
|
|
20
20
|
export { rawStatement, requiredType, schemaContext, unqualifiedContext, } from './pikku-schema.types.js';
|
|
21
|
+
/**
|
|
22
|
+
* Re-point the schemas that reference Better Auth's `user` table at the table
|
|
23
|
+
* the project actually maps that model to.
|
|
24
|
+
*
|
|
25
|
+
* Better Auth's `user.modelName` decides the physical name: an app on a legacy
|
|
26
|
+
* `users` table has no `user` for the declaration to point at, so the scope
|
|
27
|
+
* schema read as unmet and was silently left out. The requirement, the column
|
|
28
|
+
* type lookup and every foreign key target are rewritten together, because they
|
|
29
|
+
* name the table three different ways.
|
|
30
|
+
*/
|
|
31
|
+
export const withUserTable = (schemas, userTable) => {
|
|
32
|
+
if (userTable === 'user')
|
|
33
|
+
return schemas;
|
|
34
|
+
const retarget = (target) => target.startsWith('user.') ? `${userTable}.${target.slice(5)}` : target;
|
|
35
|
+
return schemas.map((schema) => ({
|
|
36
|
+
...schema,
|
|
37
|
+
requires: schema.requires?.map((r) => r.table === 'user' ? { ...r, table: userTable } : r),
|
|
38
|
+
statements: schema.statements.map((statement) => (db, types, ctx) => statement(db, Object.fromEntries(Object.entries(types).flatMap(([key, value]) => key.startsWith(`${userTable}.`)
|
|
39
|
+
? [
|
|
40
|
+
[key, value],
|
|
41
|
+
[`user.${key.slice(userTable.length + 1)}`, value],
|
|
42
|
+
]
|
|
43
|
+
: [[key, value]])), {
|
|
44
|
+
...ctx,
|
|
45
|
+
references: (column, target) => ctx.references(column, retarget(target)),
|
|
46
|
+
foreignKeys: (table, keys) => ctx.foreignKeys(table, Object.fromEntries(Object.entries(keys).map(([k, v]) => [k, retarget(v)]))),
|
|
47
|
+
})),
|
|
48
|
+
}));
|
|
49
|
+
};
|
|
21
50
|
/**
|
|
22
51
|
* Every table any part of the pikku runtime can need, in dependency order.
|
|
23
52
|
*
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import type { PikkuSchema } from './pikku-schema.types.js';
|
|
2
2
|
/**
|
|
3
3
|
* The `virtualUserSchedule` table {@link KyselyVirtualUserScheduleStore} writes
|
|
4
|
-
* to: one row per
|
|
4
|
+
* to: one row per virtual user that keeps using the app on its own.
|
|
5
5
|
*
|
|
6
6
|
* Its own schema rather than a third table in {@link virtualUserSchema}, and
|
|
7
|
-
* owned by its own store: recording what a
|
|
7
|
+
* owned by its own store: recording what a virtual user did and deciding that it
|
|
8
8
|
* keeps going on its own are separate opt-ins, and a project that only wants
|
|
9
9
|
* the first should not be handed a cadence table it never writes to.
|
|
10
10
|
*
|
|
11
|
-
* `
|
|
11
|
+
* `virtualUserId` is the primary key: a virtual user has one cadence, and a second row for
|
|
12
12
|
* the same one would mean two copies of the same user acting at once — not a
|
|
13
13
|
* heavier test, a different and unreproducible one.
|
|
14
14
|
*/
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The `virtualUserSchedule` table {@link KyselyVirtualUserScheduleStore} writes
|
|
3
|
-
* to: one row per
|
|
3
|
+
* to: one row per virtual user that keeps using the app on its own.
|
|
4
4
|
*
|
|
5
5
|
* Its own schema rather than a third table in {@link virtualUserSchema}, and
|
|
6
|
-
* owned by its own store: recording what a
|
|
6
|
+
* owned by its own store: recording what a virtual user did and deciding that it
|
|
7
7
|
* keeps going on its own are separate opt-ins, and a project that only wants
|
|
8
8
|
* the first should not be handed a cadence table it never writes to.
|
|
9
9
|
*
|
|
10
|
-
* `
|
|
10
|
+
* `virtualUserId` is the primary key: a virtual user has one cadence, and a second row for
|
|
11
11
|
* the same one would mean two copies of the same user acting at once — not a
|
|
12
12
|
* heavier test, a different and unreproducible one.
|
|
13
13
|
*/
|
|
@@ -17,7 +17,7 @@ export const virtualUserScheduleSchema = {
|
|
|
17
17
|
statements: [
|
|
18
18
|
(db, _types, ctx) => db.schema
|
|
19
19
|
.createTable('virtualUserSchedule')
|
|
20
|
-
.addColumn('
|
|
20
|
+
.addColumn('virtualUserId', 'varchar(255)', (col) => col.primaryKey())
|
|
21
21
|
// 0 or 1 rather than a boolean, matching the run tables: a bare sqlite
|
|
22
22
|
// driver cannot bind one, and `SerializePlugin` is not installed
|
|
23
23
|
// everywhere. Off by default, so creating the table spends nothing.
|
|
@@ -26,7 +26,7 @@ export const virtualUserScheduleSchema = {
|
|
|
26
26
|
.addColumn('goals', ctx.text, (col) => col.defaultTo(ctx.defaultText('[]')).notNull())
|
|
27
27
|
.addColumn('budget', ctx.text)
|
|
28
28
|
// The gap to the next run is drawn between these rather than fixed, so
|
|
29
|
-
// the
|
|
29
|
+
// the virtual user does not keep an appointment.
|
|
30
30
|
.addColumn('minIntervalMs', 'bigint', (col) => col.notNull())
|
|
31
31
|
.addColumn('maxIntervalMs', 'bigint', (col) => col.notNull())
|
|
32
32
|
// The entire schedule. Written before a run is dispatched, so a tick
|
|
@@ -8,9 +8,9 @@ import type { PikkuSchema } from './pikku-schema.types.js';
|
|
|
8
8
|
* put an empty table in every database. `KyselyVirtualUserRunStore.init()`
|
|
9
9
|
* applies it, so it arrives exactly when the thing that fills it does.
|
|
10
10
|
*
|
|
11
|
-
* `
|
|
12
|
-
* with `
|
|
13
|
-
* the
|
|
11
|
+
* `virtualUserId` is text rather than a foreign key: virtual users are declared in code
|
|
12
|
+
* with `defineVirtualUsers()`, not rows, and a run has to outlive the deletion of
|
|
13
|
+
* the virtual user it ran as.
|
|
14
14
|
*
|
|
15
15
|
* The JSON columns are text on every engine, like the audit table's, because
|
|
16
16
|
* the store serialises them itself — a project on SQLite and one on Postgres
|
|
@@ -8,9 +8,9 @@ import { sql } from 'kysely';
|
|
|
8
8
|
* put an empty table in every database. `KyselyVirtualUserRunStore.init()`
|
|
9
9
|
* applies it, so it arrives exactly when the thing that fills it does.
|
|
10
10
|
*
|
|
11
|
-
* `
|
|
12
|
-
* with `
|
|
13
|
-
* the
|
|
11
|
+
* `virtualUserId` is text rather than a foreign key: virtual users are declared in code
|
|
12
|
+
* with `defineVirtualUsers()`, not rows, and a run has to outlive the deletion of
|
|
13
|
+
* the virtual user it ran as.
|
|
14
14
|
*
|
|
15
15
|
* The JSON columns are text on every engine, like the audit table's, because
|
|
16
16
|
* the store serialises them itself — a project on SQLite and one on Postgres
|
|
@@ -24,7 +24,7 @@ export const virtualUserSchema = {
|
|
|
24
24
|
(db, _types, ctx) => db.schema
|
|
25
25
|
.createTable('virtualUserRun')
|
|
26
26
|
.addColumn('runId', 'varchar(36)', (col) => col.primaryKey())
|
|
27
|
-
.addColumn('
|
|
27
|
+
.addColumn('virtualUserId', 'varchar(255)', (col) => col.notNull())
|
|
28
28
|
.addColumn('disposition', 'varchar(50)', (col) => col.notNull())
|
|
29
29
|
.addColumn('seed', 'bigint', (col) => col.notNull())
|
|
30
30
|
// running | completed | failed. Not derived from `finishedAt` being
|
|
@@ -46,19 +46,19 @@ export const virtualUserSchema = {
|
|
|
46
46
|
.addColumn('startedBy', 'varchar(255)')
|
|
47
47
|
.addColumn('createdAt', 'timestamp', (col) => col.defaultTo(sql `CURRENT_TIMESTAMP`).notNull())
|
|
48
48
|
.addColumn('finishedAt', 'timestamp'),
|
|
49
|
-
// The two reads: the run list newest-first, and one
|
|
50
|
-
// a finding needs comparing against what that
|
|
49
|
+
// The two reads: the run list newest-first, and one virtual user's history when
|
|
50
|
+
// a finding needs comparing against what that virtual user found before.
|
|
51
51
|
(db, _types, ctx) => db.schema
|
|
52
52
|
.createIndex('idx_virtual_user_run_created')
|
|
53
53
|
.on('virtualUserRun')
|
|
54
54
|
.column('createdAt'),
|
|
55
55
|
(db, _types, ctx) => db.schema
|
|
56
|
-
.createIndex('
|
|
56
|
+
.createIndex('idx_virtual_user_run_virtual_user_id')
|
|
57
57
|
.on('virtualUserRun')
|
|
58
|
-
.columns(['
|
|
58
|
+
.columns(['virtualUserId', 'createdAt']),
|
|
59
59
|
// One row per turn, kept out of `virtualUserRun` so that listing runs does
|
|
60
60
|
// not drag a budget's worth of transcript along with it. No foreign key
|
|
61
|
-
// onto the run for the same reason `
|
|
61
|
+
// onto the run for the same reason `virtualUserId` is not one: this table is
|
|
62
62
|
// created by the store at boot on whatever engine the project uses, and a
|
|
63
63
|
// cascade that half the engines enforce differently is worse than the read
|
|
64
64
|
// rule that a step with no run is orphaned.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: decision
|
|
3
|
+
title: A console role cannot take a system role's name
|
|
4
|
+
description: createRole refuses any name the store holds as a system role, and asks the pikkuRoles table rather than the declared list, so one name never answers to two roles
|
|
5
|
+
tags: [roles, scopes, kysely, system-roles]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# A console role cannot take a system role's name
|
|
9
|
+
|
|
10
|
+
`KyselyScopeService.createRole` in `packages/services/kysely/src/kysely-scope-service.ts`
|
|
11
|
+
throws `SystemRoleShadowedError` when `isSystemRole(role.name)` is true
|
|
12
|
+
(`kysely-scope-service.ts:167-170`). `isSystemRole` reads the `system` flag from the
|
|
13
|
+
`pikkuRoles` table for that name (`kysely-scope-service.ts:221-228`).
|
|
14
|
+
|
|
15
|
+
The method's doc comment gives the reason for asking the store rather than the generated
|
|
16
|
+
declaration list: the store is where a shadow would collide, and a role whose declaration
|
|
17
|
+
was deleted stays immutable until it is pruned (`kysely-scope-service.ts:214-220`).
|
|
18
|
+
|
|
19
|
+
The schema comment says the same refusal exists because "two rows answering to one name
|
|
20
|
+
would make 'does Susan hold `buyer`?' depend on which one the store returned"
|
|
21
|
+
(`packages/services/kysely/src/schema/scope.schema.ts:39-43`).
|
|
22
|
+
|
|
23
|
+
**What this rules out:** a console role sharing a system role's name, and checking the
|
|
24
|
+
name against the code's declaration list instead of the store. `deleteRole` and
|
|
25
|
+
`setRoleScopes` apply the same `isSystemRole` guard (`kysely-scope-service.ts:191-201`).
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: decision
|
|
3
|
+
title: A credential with no user is one row per name
|
|
4
|
+
description: The database enforces one app-wide credential per name by indexing COALESCE(user_id, ''), and the service maps a missing user to IS NULL
|
|
5
|
+
tags: [credentials, schema, kysely, uniqueness]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# A credential with no user is one row per name
|
|
9
|
+
|
|
10
|
+
`credentialSchema` in `packages/services/kysely/src/schema/credential.schema.ts` creates
|
|
11
|
+
a unique index `credentials_name_user_id_unique` on `(name, COALESCE(user_id, ''))`
|
|
12
|
+
(`credential.schema.ts:33-36`). Its comment says `user_id` is nullable and that two rows
|
|
13
|
+
with a null user must still collide on name, which is why the rule is an expression
|
|
14
|
+
index (`credential.schema.ts:7-9`).
|
|
15
|
+
|
|
16
|
+
`KyselyCredentialService.whereUserId` turns a call with no user into `userId IS NULL`
|
|
17
|
+
(`packages/services/kysely/src/kysely-credential-service.ts:118-122`), so `get`, `set`,
|
|
18
|
+
`has` and `delete` without a user all address that single row. `getUsersWithCredential`
|
|
19
|
+
and `getAllUsers` leave the null-user row out (`kysely-credential-service.ts:233-253`).
|
|
20
|
+
|
|
21
|
+
**What this rules out:** two app-wide rows for the same credential name, and treating a
|
|
22
|
+
null user as a distinct owner from every other null user.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: decision
|
|
3
|
+
title: A virtual user's schedule starts disabled
|
|
4
|
+
description: A virtual-user schedule is written with enabled off unless the caller says otherwise, and only enabled rows come due, so unattended runs need an explicit enable
|
|
5
|
+
tags: [virtual-users, schedules, kysely, defaults]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# A virtual user's schedule starts disabled
|
|
9
|
+
|
|
10
|
+
The `virtualUserSchedule` table gives `enabled` a default of 0 and marks it not null
|
|
11
|
+
(`packages/services/kysely/src/schema/virtual-user-schedule.schema.ts:24-27`). The
|
|
12
|
+
schema comment says "Off by default, so creating the table spends nothing"
|
|
13
|
+
(`virtual-user-schedule.schema.ts:24-26`).
|
|
14
|
+
|
|
15
|
+
`KyselyVirtualUserScheduleStore.set` resolves the flag as
|
|
16
|
+
`schedule.enabled ?? existing?.enabled ?? false`
|
|
17
|
+
(`packages/services/kysely/src/kysely-virtual-user-schedule-store.ts:55`). `due()` reads
|
|
18
|
+
only rows with `enabled = 1` (`kysely-virtual-user-schedule-store.ts:131`). The store
|
|
19
|
+
comment says enabling a schedule "is a deliberate act" (`kysely-virtual-user-schedule-store.ts:70-71`).
|
|
20
|
+
|
|
21
|
+
**What this rules out:** a virtual user that runs on its own from the moment its schedule row
|
|
22
|
+
is created. Who may write the enabled flag is a separate question, recorded in the Kysely
|
|
23
|
+
questions.
|
|
@@ -53,7 +53,7 @@ path is the thing actually hurting.
|
|
|
53
53
|
|
|
54
54
|
That plugin lives here, in `@pikku/kysely`, and not in a SQLite-specific
|
|
55
55
|
package. Nothing about wrapping a column value is dialect-specific, and the same
|
|
56
|
-
plugin is wanted against Postgres —
|
|
56
|
+
plugin is wanted against Postgres — cloud has the same classified columns and
|
|
57
57
|
the same reason to protect them. Putting it beside the dialect would mean
|
|
58
58
|
writing it twice.
|
|
59
59
|
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: decision
|
|
3
|
+
title: Grants reference declared scopes and users by foreign key
|
|
4
|
+
description: The scope tables tie role and user grants to pikkuScopes and to Better Auth's user table with foreign keys, so the database refuses a grant of an undeclared scope
|
|
5
|
+
tags: [scopes, roles, schema, kysely, foreign-keys]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Grants reference declared scopes and users by foreign key
|
|
9
|
+
|
|
10
|
+
`scopeSchema` in `packages/services/kysely/src/schema/scope.schema.ts` sets these
|
|
11
|
+
references:
|
|
12
|
+
|
|
13
|
+
- `pikkuRoleScopes.scope` references `pikkuScopes.name` (`scope.schema.ts:63-72`).
|
|
14
|
+
- `pikkuUserScope.scope` references `pikkuScopes.name` (`scope.schema.ts:101-113`).
|
|
15
|
+
- `pikkuUserRole.role` references `pikkuRoles.name` (`scope.schema.ts:80-82`).
|
|
16
|
+
- `pikkuUserRole.userId` and `pikkuUserScope.userId` reference `user.id`, which Better
|
|
17
|
+
Auth owns (`scope.schema.ts:77-79`, `scope.schema.ts:98-100`). The schema declares that
|
|
18
|
+
requirement at `scope.schema.ts:23`.
|
|
19
|
+
|
|
20
|
+
The file's header comment explains the choice: a grant is made to a user, so the user
|
|
21
|
+
table must exist first (`scope.schema.ts:7-11`). The `KyselyScopeService` class comment
|
|
22
|
+
states that `pikku_role_scopes` FKs into `pikku_scopes`, "so the database itself refuses
|
|
23
|
+
to grant a scope that was never declared" (`packages/services/kysely/src/kysely-scope-service.ts:24-25`).
|
|
24
|
+
|
|
25
|
+
`syncScopes` marks scopes that are no longer declared rather than deleting them
|
|
26
|
+
(`kysely-scope-service.ts:41-46`), so a foreign key never loses a grant during a deploy.
|
|
27
|
+
|
|
28
|
+
**What this rules out:** storing a grant for a scope name that was never declared, and
|
|
29
|
+
storing a user grant that has no user row.
|
|
@@ -7,12 +7,15 @@ description: How encrypted columns are wrapped, and which key wraps them
|
|
|
7
7
|
# Decisions
|
|
8
8
|
|
|
9
9
|
<!-- pikku:knowledge-index -->
|
|
10
|
-
|
|
10
|
+
- [A console role cannot take a system role's name](a-console-role-cannot-take-a-system-roles-name.md) — createRole refuses any name the store holds as a system role, and asks the pikkuRoles table rather than the declared list, so one name never answers to two roles
|
|
11
|
+
- [A credential with no user is one row per name](a-credential-with-no-user-is-one-row-per-name.md) — The database enforces one app-wide credential per name by indexing COALESCE(user_id, ''), and the service maps a missing user to IS NULL
|
|
11
12
|
- [A lease is taken by one guarded UPDATE](a-lease-is-taken-by-one-guarded-update.md) — Acquire inserts the key's row already lapsed, takes it with a guarded UPDATE, and reads the row back, because that is the one shape that behaves the same on SQLite, Postgres and MySQL
|
|
13
|
+
- [A virtual user's schedule starts disabled](a-virtual-users-schedule-starts-disabled.md) — A virtual-user schedule is written with enabled off unless the caller says otherwise, and only enabled rows come due, so unattended runs need an explicit enable
|
|
12
14
|
- [Classification-driven encryption happens in a Kysely plugin](classification-driven-encryption-happens-in-a-plugin.md) — The existing column classification manifest drives a transparent query plugin, rather than encrypting the whole database file or asking call sites to encrypt
|
|
15
|
+
- [Grants reference declared scopes and users by foreign key](grants-reference-declared-scopes-and-users-by-foreign-key.md) — The scope tables tie role and user grants to pikkuScopes and to Better Auth's user table with foreign keys, so the database refuses a grant of an undeclared scope
|
|
13
16
|
- [KEKs are scoped by purpose as well as version](keks-are-scoped-by-purpose-as-well-as-version.md) — Rows carry a keyId, and the plugin resolves KEKs through a seam rather than holding one; v1 ships a single key, but scoping never becomes a migration
|
|
14
17
|
- [Leases are judged by the database's clock](leases-are-judged-by-the-database-clock.md) — A lease and a step lease are written and compared in SQL against the server's own clock, stored as epoch milliseconds, so no worker's clock decides who holds anything
|
|
15
18
|
- [The plugin is given a resolver, never a key](the-plugin-is-given-a-resolver-never-a-key.md) — Assembly order for classified columns — a plain Kysely handle, then a plugin that asks a `KEKResolver` per operation rather than being handed a key it holds for the life of the process
|
|
19
|
+
- [The secrets audit table is declared even when auditing is off](the-secrets-audit-table-is-declared-even-when-auditing-is-off.md) — secretsAudit is created by the secret migration unconditionally, because a migration cannot depend on runtime config and an unused table costs nothing
|
|
16
20
|
- [Workflows own the lease table too](workflows-own-the-lease-table-too.md) — `leaseSchema` is owned by `workflowService` as well as `leaseService`, because the engine takes the run lease without any function ever naming leaseService
|
|
17
|
-
|
|
18
21
|
<!-- /pikku:knowledge-index -->
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: decision
|
|
3
|
+
title: The secrets audit table is declared even when auditing is off
|
|
4
|
+
description: secretsAudit is created by the secret migration unconditionally, because a migration cannot depend on runtime config and an unused table costs nothing
|
|
5
|
+
tags: [secrets, audit, migrations, kysely]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# The secrets audit table is declared even when auditing is off
|
|
9
|
+
|
|
10
|
+
`secretSchema` in `packages/services/kysely/src/schema/secret.schema.ts` creates the
|
|
11
|
+
`secretsAudit` table as one of its statements (`secret.schema.ts:39-47`), with no
|
|
12
|
+
condition on the audit setting. The comment gives the reason: a migration cannot depend
|
|
13
|
+
on runtime configuration, and an unused table costs nothing, while turning auditing on
|
|
14
|
+
against a database that never got the table fails on the first write
|
|
15
|
+
(`secret.schema.ts:7-11`).
|
|
16
|
+
|
|
17
|
+
Whether audit rows are written is a separate setting. `KyselySecretService` defaults
|
|
18
|
+
`audit` to false (`packages/services/kysely/src/kysely-secret-service.ts:42`), and its
|
|
19
|
+
`logAudit` returns early when auditing is off (`kysely-secret-service.ts:56`).
|
|
20
|
+
|
|
21
|
+
**What this rules out:** generating the audit table only when the service is configured
|
|
22
|
+
with auditing on, which would make the schema depend on runtime configuration.
|
package/knowledge/index.md
CHANGED
|
@@ -15,4 +15,5 @@ These notes record how this package answers them.
|
|
|
15
15
|
|
|
16
16
|
<!-- pikku:knowledge-index -->
|
|
17
17
|
- [decisions](decisions/index.md) — a rule that was chosen, and what it rules out
|
|
18
|
+
- [questions](questions/index.md) — something asked and not yet answered
|
|
18
19
|
<!-- /pikku:knowledge-index -->
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: question
|
|
3
|
+
title: Do the Kysely stores check who calls them?
|
|
4
|
+
description: The virtual-user run, schedule and audit stores take no identity or scope argument, so their authorisation rests on callers, and the run store's comment says reads are scope-gated somewhere else
|
|
5
|
+
tags: [authorization, virtual-users, audit, kysely]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Do the Kysely stores check who calls them?
|
|
9
|
+
|
|
10
|
+
Three Kysely stores take no identity or scope argument, and the files do not check one.
|
|
11
|
+
|
|
12
|
+
- **Virtual-user run store.** `get(runId)` and `list({ virtualUserId, limit, offset })` run
|
|
13
|
+
queries with no caller check (`packages/services/kysely/src/kysely-virtual-user-run-store.ts:125-151`).
|
|
14
|
+
The class comment says the store is privileged because an adversarial run's findings
|
|
15
|
+
are "working exploits carrying live ids", and that "the scaffolded reads are
|
|
16
|
+
scope-gated" (`kysely-virtual-user-run-store.ts:37-39`). The gate is not in this file.
|
|
17
|
+
- **Virtual-user schedule store.** `set(schedule)` takes no identity, and an enabled
|
|
18
|
+
flag written through it makes a virtual user due (`packages/services/kysely/src/kysely-virtual-user-schedule-store.ts:45-48`,
|
|
19
|
+
`:55`, `:131`). `remove(virtualUserId)` takes none either (`:161-167`).
|
|
20
|
+
- **Audit service.** `query` applies the `userIds`, `types` and `orgId` filters the
|
|
21
|
+
caller passes and runs them (`packages/services/kysely/src/kysely-audit-service.ts:161-182`,
|
|
22
|
+
`:211-236`). Nothing in those lines checks who is asking.
|
|
23
|
+
|
|
24
|
+
Current behaviour: authorisation for these stores is entirely in the callers.
|
|
25
|
+
|
|
26
|
+
Open: which layer is meant to check identity and scope for each store, and is the
|
|
27
|
+
run-store scope gate the comment describes implemented anywhere in the repository?
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: question
|
|
3
|
+
title: Does a trigger-source row hold only provider identifiers?
|
|
4
|
+
description: The trigger-source schema says state holds provider identifiers and that signing secrets go to the credential service, but the store writes whatever state the caller passes as JSON
|
|
5
|
+
tags: [kysely, trigger-source, secrets]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Does a trigger-source row hold only provider identifiers?
|
|
9
|
+
|
|
10
|
+
The doc comment on the table says "`state` holds provider identifiers only: a signing
|
|
11
|
+
secret a provider issues goes to the credential service"
|
|
12
|
+
(`packages/services/kysely/src/schema/trigger-source.schema.ts:4-7`).
|
|
13
|
+
|
|
14
|
+
The table has a `state` text column and a `detail` text column
|
|
15
|
+
(`trigger-source.schema.ts:26-27`). Nothing in the schema restricts what is written to
|
|
16
|
+
them.
|
|
17
|
+
|
|
18
|
+
The store writes the value it is given. `recordTriggerSource` sets
|
|
19
|
+
`state` to `JSON.stringify(result.state)` whenever `result.state` is set
|
|
20
|
+
(`packages/services/kysely/src/kysely-trigger-source-store.ts:102-107`). Its test
|
|
21
|
+
writes `{ id: 'we_1' }` (`kysely-trigger-source-store.test.ts:41`), which is an
|
|
22
|
+
identifier. The test does not check that a secret is refused.
|
|
23
|
+
|
|
24
|
+
This note did not read the code that builds `result.state`, so it cannot say whether
|
|
25
|
+
any caller puts a secret there. The question is whether the comment describes a rule
|
|
26
|
+
the code enforces, or only the convention the callers are expected to follow.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: overview
|
|
3
|
+
title: Questions
|
|
4
|
+
description: Things asked of the Kysely stores and not yet answered
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Questions
|
|
8
|
+
|
|
9
|
+
Something asked and not yet answered. The code makes a choice nobody has stated, so the
|
|
10
|
+
doubt is recorded with the current behaviour, not a guessed rationale.
|
|
11
|
+
|
|
12
|
+
<!-- pikku:knowledge-index -->
|
|
13
|
+
- [Do the Kysely stores check who calls them?](do-the-kysely-stores-check-who-calls-them.md) — The virtual-user run, schedule and audit stores take no identity or scope argument, so their authorisation rests on callers, and the run store's comment says reads are scope-gated somewhere else
|
|
14
|
+
- [Does a trigger-source row hold only provider identifiers?](does-a-trigger-source-row-hold-only-provider-identifiers.md) — The trigger-source schema says state holds provider identifiers and that signing secrets go to the credential service, but the store writes whatever state the caller passes as JSON
|
|
15
|
+
- [Should pikku user sessions expire on their own?](should-pikku-user-sessions-expire-on-their-own.md) — The Kysely session store writes a session with no expiry, reads it back at any age, and its table has no expiry column
|
|
16
|
+
- [Should secret and credential reads be audited by default?](should-secret-and-credential-reads-be-audited-by-default.md) — The Kysely secret and credential stores leave auditing off, leave reads unaudited unless asked, and the secret store never records bulk reads; the generic query audit goes the other way
|
|
17
|
+
<!-- /pikku:knowledge-index -->
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: question
|
|
3
|
+
title: Should pikku user sessions expire on their own?
|
|
4
|
+
description: The Kysely session store writes a session with no expiry, reads it back at any age, and its table has no expiry column
|
|
5
|
+
tags: [sessions, expiry, kysely]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Should pikku user sessions expire on their own?
|
|
9
|
+
|
|
10
|
+
`KyselySessionStore` (`packages/services/kysely/src/kysely-session-store.ts`) writes a
|
|
11
|
+
session as JSON with an `updatedAt` stamp and nothing else (`kysely-session-store.ts:35-46`).
|
|
12
|
+
`get` returns the stored session whatever its age, and has no expiry check
|
|
13
|
+
(`kysely-session-store.ts:21-33`). `clear` deletes a user's row (`kysely-session-store.ts:48-53`).
|
|
14
|
+
|
|
15
|
+
The table has `pikkuUserId`, `session`, `createdAt` and `updatedAt`, and no expiry column
|
|
16
|
+
(`packages/services/kysely/src/schema/session.schema.ts:8-18`).
|
|
17
|
+
|
|
18
|
+
Current behaviour: a stored session stays readable until something calls `clear`.
|
|
19
|
+
|
|
20
|
+
Open: should a session expire in this store, or is its lifetime bounded somewhere else? The
|
|
21
|
+
files do not say.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: question
|
|
3
|
+
title: Should secret and credential reads be audited by default?
|
|
4
|
+
description: The Kysely secret and credential stores leave auditing off, leave reads unaudited unless asked, and the secret store never records bulk reads; the generic query audit goes the other way
|
|
5
|
+
tags: [audit, secrets, credentials, kysely]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Should secret and credential reads be audited by default?
|
|
9
|
+
|
|
10
|
+
`KyselySecretService` (`packages/services/kysely/src/kysely-secret-service.ts`) defaults
|
|
11
|
+
`audit` and `auditReads` to false (`kysely-secret-service.ts:42-43`). `logAudit` returns
|
|
12
|
+
early when auditing is off, and for reads when `auditReads` is off
|
|
13
|
+
(`kysely-secret-service.ts:56-57`). A single `getSecret` records a read
|
|
14
|
+
(`kysely-secret-service.ts:129`). `getSecrets` records nothing, even with auditing on
|
|
15
|
+
(`kysely-secret-service.ts:178-209`). Writes, deletes and rotations are recorded when
|
|
16
|
+
auditing is on (`kysely-secret-service.ts:170`, `:175`, `:237`).
|
|
17
|
+
|
|
18
|
+
`KyselyCredentialService` (`packages/services/kysely/src/kysely-credential-service.ts`)
|
|
19
|
+
has the same defaults (`kysely-credential-service.ts:40-41`) and the same gate
|
|
20
|
+
(`kysely-credential-service.ts:50-56`). `get` records a read (`:140`), and `getAll`
|
|
21
|
+
records one row per credential, still under the same flag (`:207-230`, `:227`).
|
|
22
|
+
|
|
23
|
+
`createAuditedKysely` goes the other way. It skips selects only when `auditReads` is
|
|
24
|
+
explicitly `false` (`packages/services/kysely/src/create-audited-kysely.ts:196-198`), so
|
|
25
|
+
database selects are audited by default.
|
|
26
|
+
|
|
27
|
+
Current behaviour: secret and credential reads leave no record until the app turns
|
|
28
|
+
auditing on, and bulk secret reads never leave one. The code does not say whether that is
|
|
29
|
+
meant.
|
|
30
|
+
|
|
31
|
+
Open: should secret and credential reads be audited by default, and should `getSecrets`
|
|
32
|
+
record a row per key? The files do not say.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pikku/kysely",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.37",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/pikkujs/pikku.git",
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"pikku-kysely-pure": "dist/bin/pikku-kysely-pure.js"
|
|
16
16
|
},
|
|
17
17
|
"type": "module",
|
|
18
|
+
"sideEffects": false,
|
|
18
19
|
"scripts": {
|
|
19
20
|
"tsc": "tsc",
|
|
20
21
|
"ncu": "npx npm-check-updates",
|
|
@@ -26,7 +27,7 @@
|
|
|
26
27
|
"prepublishOnly": "bun run build"
|
|
27
28
|
},
|
|
28
29
|
"peerDependencies": {
|
|
29
|
-
"@pikku/core": "^0.12.
|
|
30
|
+
"@pikku/core": "^0.12.142"
|
|
30
31
|
},
|
|
31
32
|
"dependencies": {
|
|
32
33
|
"kysely": "^0.29.5"
|