@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.
Files changed (50) hide show
  1. package/CHANGELOG.md +82 -0
  2. package/dist/bin/pikku-kysely-pure.js +4 -1
  3. package/dist/src/classification-crypto.js +16 -2
  4. package/dist/src/index.d.ts +2 -2
  5. package/dist/src/index.js +2 -2
  6. package/dist/src/kysely-agent-run-service.js +2 -2
  7. package/dist/src/kysely-agent-storage-service.js +5 -9
  8. package/dist/src/kysely-analytics-service.js +2 -5
  9. package/dist/src/kysely-audit-service.js +2 -5
  10. package/dist/src/kysely-credential-service.js +7 -10
  11. package/dist/src/kysely-deployment-service.js +7 -10
  12. package/dist/src/kysely-eventhub-store.js +3 -4
  13. package/dist/src/kysely-feature-flag-store.d.ts +3 -3
  14. package/dist/src/kysely-feature-flag-store.js +47 -41
  15. package/dist/src/kysely-lease-clock.d.ts +1 -1
  16. package/dist/src/kysely-lease-clock.js +5 -1
  17. package/dist/src/kysely-lease-service.js +4 -5
  18. package/dist/src/kysely-scope-service.js +62 -42
  19. package/dist/src/kysely-secret-service.js +10 -17
  20. package/dist/src/kysely-session-store.js +3 -9
  21. package/dist/src/kysely-tables.d.ts +3 -3
  22. package/dist/src/kysely-trigger-source-store.js +3 -4
  23. package/dist/src/kysely-upsert.d.ts +37 -0
  24. package/dist/src/kysely-upsert.js +54 -0
  25. package/dist/src/kysely-virtual-user-run-store.d.ts +1 -1
  26. package/dist/src/kysely-virtual-user-run-store.js +8 -8
  27. package/dist/src/kysely-virtual-user-schedule-store.d.ts +4 -4
  28. package/dist/src/kysely-virtual-user-schedule-store.js +16 -19
  29. package/dist/src/kysely-workflow-mirror.js +2 -1
  30. package/dist/src/kysely-workflow-service.js +5 -1
  31. package/dist/src/schema/index.d.ts +11 -0
  32. package/dist/src/schema/index.js +29 -0
  33. package/dist/src/schema/virtual-user-schedule.schema.d.ts +3 -3
  34. package/dist/src/schema/virtual-user-schedule.schema.js +5 -5
  35. package/dist/src/schema/virtual-user.schema.d.ts +3 -3
  36. package/dist/src/schema/virtual-user.schema.js +9 -9
  37. package/knowledge/decisions/a-console-role-cannot-take-a-system-roles-name.md +25 -0
  38. package/knowledge/decisions/a-credential-with-no-user-is-one-row-per-name.md +22 -0
  39. package/knowledge/decisions/a-virtual-users-schedule-starts-disabled.md +23 -0
  40. package/knowledge/decisions/classification-driven-encryption-happens-in-a-plugin.md +1 -1
  41. package/knowledge/decisions/grants-reference-declared-scopes-and-users-by-foreign-key.md +29 -0
  42. package/knowledge/decisions/index.md +5 -2
  43. package/knowledge/decisions/the-secrets-audit-table-is-declared-even-when-auditing-is-off.md +22 -0
  44. package/knowledge/index.md +1 -0
  45. package/knowledge/questions/do-the-kysely-stores-check-who-calls-them.md +27 -0
  46. package/knowledge/questions/does-a-trigger-source-row-hold-only-provider-identifiers.md +26 -0
  47. package/knowledge/questions/index.md +17 -0
  48. package/knowledge/questions/should-pikku-user-sessions-expire-on-their-own.md +21 -0
  49. package/knowledge/questions/should-secret-and-credential-reads-be-audited-by-default.md +32 -0
  50. 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
  *
@@ -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 persona that keeps using the app on its own.
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 persona did and deciding that it
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
- * `persona` is the primary key: a persona has one cadence, and a second row for
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 persona that keeps using the app on its own.
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 persona did and deciding that it
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
- * `persona` is the primary key: a persona has one cadence, and a second row for
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('persona', 'varchar(255)', (col) => col.primaryKey())
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 persona does not keep an appointment.
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
- * `persona` is text rather than a foreign key: personas are declared in code
12
- * with `definePersonas()`, not rows, and a run has to outlive the deletion of
13
- * the persona it ran as.
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
- * `persona` is text rather than a foreign key: personas are declared in code
12
- * with `definePersonas()`, not rows, and a run has to outlive the deletion of
13
- * the persona it ran as.
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('persona', 'varchar(255)', (col) => col.notNull())
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 persona's history when
50
- // a finding needs comparing against what that persona found before.
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('idx_virtual_user_run_persona')
56
+ .createIndex('idx_virtual_user_run_virtual_user_id')
57
57
  .on('virtualUserRun')
58
- .columns(['persona', 'createdAt']),
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 `persona` is not one: this table is
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 — fabric has the same classified columns and
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.
@@ -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.34",
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.136"
30
+ "@pikku/core": "^0.12.142"
30
31
  },
31
32
  "dependencies": {
32
33
  "kysely": "^0.29.5"