@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
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,85 @@
|
|
|
1
|
+
## 0.13.37
|
|
2
|
+
|
|
3
|
+
### Patch Changes
|
|
4
|
+
|
|
5
|
+
- fdcc821: Fabric is gone from the open toolchain. The previous names are removed with no aliases.
|
|
6
|
+
|
|
7
|
+
The CLI group is `pikku cloud …` and the export is `@pikku/cli/cloud`. The project link is the `cloud` key of `pikku.config.json` (`cloud.projectId`), the CLI keeps its local state in `~/.pikkustudio` (run `pikku cloud login` again), and the project config file is `pikkustudio.config.json`.
|
|
8
|
+
|
|
9
|
+
Every environment variable drops the `FABRIC_` prefix for `PIKKUSTUDIO_` with the same suffix: `PIKKUSTUDIO_API_URL`, `PIKKUSTUDIO_PROJECT_ID`, `PIKKUSTUDIO_TOKEN`, `PIKKUSTUDIO_OPERATOR_TOKEN`, `PIKKUSTUDIO_AUTH_PUBLIC_KEY`, `PIKKUSTUDIO_STAGE_ID` and the rest. Rename them wherever you set them.
|
|
10
|
+
|
|
11
|
+
Wire ids: the operator sign-in route is `/sign-in/cloud`, the plugin and mode literal is `'cloud'`, the Better Auth plugin is `pikkuCloud`, its session field is `pikkustudioadmin`, and the signature header is `x-cloud-sig`. The cloud API calls are `getCloudLogs`, `getCloudLogsByStageKind`, `cloudCliProjects`, `cloudCliDeployments`, `providedByCloud` and `recordCloudAnalyticsEvents`. Addon packages are `@pikkustudio/*`, scaffolded files live under `scaffold/cloud` and `__cloud_*__`, and the API hosts are the `pikkustudio.app` and `pikkustudio.dev` domains.
|
|
12
|
+
|
|
13
|
+
`pikku skills install --fabric` is `--cloud`, the install group is `cloud`, and the skills are `pikku-cloud`, `pikku-cloud-debug`, `pikku-admin-to-cloud` and `pikku-blueprint-to-cloud`.
|
|
14
|
+
|
|
15
|
+
The stale analytics forwarder copies are removed from the online-shop example and the starter template.
|
|
16
|
+
|
|
17
|
+
The sealed-box salt stays `pikkufabric/sealed-box/v1`: it is a cryptographic constant that already-sealed secrets depend on.
|
|
18
|
+
|
|
19
|
+
- 33b102d: Virtual users now have one name everywhere. The id column of `virtual_user_run` and `virtual_user_schedule` is `virtual_user_id` (a migration renames it in place), the CLI group is `pikku virtual-user …`, and the declarations and imports live under `@pikku/core/virtual-user`. The previous names are removed with no aliases.
|
|
20
|
+
|
|
21
|
+
The default virtual user email domain is now `virtualuser.invalid`: apps that rely on the default domain get new accounts at the next provisioning (`pikku virtual-user sync`, deploy, or `pikku dev` start), and the old rows stay unused.
|
|
22
|
+
|
|
23
|
+
The sign-in surface carries the same name. The route is `/sign-in/virtual-user`, the environment variables are `PIKKU_ALLOW_VIRTUAL_USER_SIGN_IN` and the `PIKKU_DEV_VIRTUAL_USER_SIGN_IN` / `VITE_*` equivalents, the Better Auth plugin is `pikkuVirtualUser`, the scenario step option and wire member are `virtualUser` and `virtualUsers`, and the Better Auth user flag column is `virtual_user`. Rename the environment variables wherever you set them, and add a migration that renames the flag column on the `user` table. Nothing is aliased.
|
|
24
|
+
|
|
25
|
+
The conversation runner is now `@pikku/core/virtual-user/conversation`, replacing the old `-flow` subpath. Update the import; the old subpath is gone.
|
|
26
|
+
|
|
27
|
+
- Updated dependencies [fdcc821]
|
|
28
|
+
- Updated dependencies [33b102d]
|
|
29
|
+
- Updated dependencies [6a25c70]
|
|
30
|
+
- Updated dependencies [a084c3b]
|
|
31
|
+
- @pikku/core@0.12.142
|
|
32
|
+
|
|
33
|
+
## 0.13.36
|
|
34
|
+
|
|
35
|
+
### Patch Changes
|
|
36
|
+
|
|
37
|
+
- e0846b5: Saving an agent's tool messages skips a tool call whose result is undefined instead of writing it. When the browser driver for a scenario cannot be loaded, the error now includes the underlying cause and tells you to `bun add -D` it.
|
|
38
|
+
- 884a1b5: MySQL: the `@pikku/kysely` stores no longer emit postgres/sqlite-only SQL. Upserts use `on duplicate key update`, insert-or-ignore uses `insert ignore`, `returning` deletes read the rows first, the lease clock casts to `signed`, workflow JSON state uses `CAST(... AS JSON)`, the agent thread owner `LIKE` escapes with `!`, and timestamp columns are written as `Date`s instead of `Z`-suffixed ISO strings. `pikku db generate` now emits the `scope` schema when Better Auth's user model is a mapped table (for example a Rails `users` table), typing the key column from the real `users.id`.
|
|
39
|
+
- 51ec3ac: MySQL fixes found moving a Rails database onto pikku.
|
|
40
|
+
|
|
41
|
+
- The generated local services (`pikku db baseline`, `pikku dev`, `pikku all`, a local CLI) open MySQL: a `mysql://` `DATABASE_URL` or `mysqlUrl` in the config opens through a Kysely `MysqlDialect` on a `mysql2` pool (declare `mysql2` in the project), with `CamelCasePlugin` and the coercion map like the other dialects. A `mysql://` URL was read as a sqlite file path. `db/mysql` now counts as a project database, and `@pikku/kysely` re-exports `MysqlDialect`.
|
|
42
|
+
- MySQL migrations run with `FOREIGN_KEY_CHECKS` off, so a mysqldump whose tables reference ones created later applies as it is.
|
|
43
|
+
- `TINYINT(1)` is typed `boolean` and coerced at runtime (mysql2 returns 0/1); an explicit `tsType` in `db/annotations.ts` keeps it a number. `DECIMAL` is read as a number (`decimalNumbers`), as the generated types say. `BIGINT` stays `number`, exact to 2^53, which is mysql2's default.
|
|
44
|
+
- `pikku all` no longer throws "db/mysql exists but no mysqlUrl is configured": the server comes from createConfig's `mysqlUrl`, `db.mysqlUrl` or a `mysql://` `DATABASE_URL`, and with none the stub `DB` is written.
|
|
45
|
+
- Better Auth schema derivation follows the resolved database rather than the factory's `database.type`, so a factory declaring `type: 'sqlite'` no longer sends SQLite introspection (`pragma index_list`) to a MySQL server. Set `type: 'mysql'` in the factory too, so the runtime matches.
|
|
46
|
+
|
|
47
|
+
- Updated dependencies [f396538]
|
|
48
|
+
- @pikku/core@0.12.140
|
|
49
|
+
|
|
50
|
+
## 0.13.35
|
|
51
|
+
|
|
52
|
+
### Patch Changes
|
|
53
|
+
|
|
54
|
+
- 15c4968: The default runtime path no longer needs `node:*`, `Buffer` or an unguarded `process`, so it loads on edge runtimes (Web APIs only).
|
|
55
|
+
|
|
56
|
+
**Breaking:** `FileScenarioRunStore`, `scenarioArtifactContentType` and `scenarioRunSummary` leave the `@pikku/core/services` barrel because they use `node:fs`. Import them from `@pikku/core/services/file-scenario-run-store`. Pre-0.13 breaking changes still ship as a patch.
|
|
57
|
+
|
|
58
|
+
**Breaking:** the HMAC helpers in `@pikku/core/hmac` run on Web Crypto, which is async. `hmacSha256Hex`, `hmacDigest`, `verifyHmacSignature` and `verifyPublicKeySignature` now return a Promise; `timingSafeStringEqual` stays synchronous. `WebhookService.sign` and `WebhookService.verify` return a Promise, and so does `verifySlackSignature` in `@pikku/gateway-slack`: `await` them. A function-style `verify` on a webhook source may return a Promise as well. `verifyPublicKeySignature` accepts RSA and EC (P-256, P-384, P-521) PEM keys; Ed25519 and RSA-PSS keys verify as false.
|
|
59
|
+
|
|
60
|
+
`isProduction()` (and the `PIKKU_MODEL_ALIASES` and `PIKKU_ENV` reads) now ask the registered singleton services' `variables` service first, then `process.env` when `process` exists. With neither, `isProduction()` is true, so an edge deployment never exposes error details by accident. `LocalVariablesService` resolves its `process.env` default when constructed and falls back to an empty store without `process`.
|
|
61
|
+
|
|
62
|
+
Workflow invocation ids use a built-in SHA-1 and stay byte-identical. `@pikku/kysely` and the Cloudflare hibernation server use `globalThis.crypto.randomUUID()` and no `Buffer`; `@pikku/kysely` is marked `sideEffects: false`. `@pikku/ai-deepinfra` reads `DEEPINFRA_API_KEY` only when `process` exists and decodes base64 audio without `Buffer`.
|
|
63
|
+
|
|
64
|
+
Runtime tiers. A package declares where it can run in `package.json`: `"pikku": { "runtime": "edge" | "serverless" | "server" }`, with optional per-subpath overrides (`"exports": { "./dev": "server" }`). `server` is the default. `@pikku/core` and the default services are `edge` (core's Node-only subpaths are `server`), `@pikku/better-auth`, `@pikku/addon-admin` and `@pikku/cloudflare` are `serverless` (better-auth statically imports `node:crypto` scrypt), and `@pikku/addon-console` is `server` and must never land in a deployed unit.
|
|
65
|
+
|
|
66
|
+
`pikku deploy` now verifies each deployed unit before bundling it. The unit is bundled with esbuild (neutral platform, `workerd`/`worker`/`browser` conditions) under its tier's profile, ahead of the adapter's aliases and stubs so they cannot hide anything, and the build fails when the bundle holds a package declared above the unit's tier or a Node built-in the runtime does not provide. The error names the unit, package, import chain and the fix. A built-in the provider stubs is a warning. On an edge unit, a bundled `InMemory*` class also fails the build. The unit's tier is its `runtime`, else the project's own `pikku.runtime`, else the provider default (`serverless` on Cloudflare). Packages that declare nothing are judged by their imports only.
|
|
67
|
+
|
|
68
|
+
`ProviderAdapter.getRuntimeProfile(tier?)` returns the compat date, compat flags, the built-ins the runtime provides (keyed by compat date), the stubbed ones and the externals. The Cloudflare adapter derives `wrangler.toml`, the upload metadata and the bundle externals from it; `node:os` and `node:fs` are not provided before compat date 2025-09-15. The default is `nodejs_compat_v2` with compat date 2024-12-18, which is what `wrangler.toml` and Fabric's live workers already use, so Fabric deploys see no runtime change. Only the direct-API upload in `@pikku/deploy-cloudflare` (`createWorker`, used by its own `deploy()`) moves: it sent `nodejs_compat` with 2024-01-01 before. A newer date or flag is opt-in through the adapter option `new CloudflareProviderAdapter({ compatDate })` or `getCloudflareRuntimeProfile(tier, compatDate)`. From 2025-08-15 `node:http` and `node:https` exist, and from 2025-09-15 `node:os`, `node:fs` (stubbed by the build regardless) and `node:perf_hooks`; the verifier's allowed built-ins follow the effective date. A newer date changes the runtime of every worker deployed with it.
|
|
69
|
+
|
|
70
|
+
`declareScopes(['pikku:console'])` (from `@pikku/core/scope`) declares scopes by id without wiring anything. `wireAddon({ scopes })` only requires a scope of an addon's functions; the tree arrives with the addon's metadata, so a role granting `pikku:console` used to fail PKU124 unless the addon was wired, and a wired `@pikku/addon-console` lands in every unit carrying the `/rpc` catch-all. An app that grants the scope without the console now calls `declareScopes` and no longer needs the scaffold or the addon. When the addon is wired its tree and descriptions win and the bare id adds nothing.
|
|
71
|
+
|
|
72
|
+
More packages declare a tier after bundling each with the edge profile. `edge`: `@pikku/addon-graph`, `@pikku/pino`, `@pikku/jose`, `@pikku/ai-vercel`, `@pikku/ai-voice`. `serverless`: `@pikku/kysely-postgres` (postgres.js needs `node:buffer`, `stream`, `events`), `@pikku/backblaze` (`copyFile` reads a local file, which the build stubs), `@pikku/gateway-slack` (`@slack/web-api` imports `node:os` and `zlib`). `server`: `@pikku/schedule` (`cron` imports `child_process`), `@pikku/aws-services` (the AWS SDK's Node handler needs `node:http2`), `@pikku/browser` (puppeteer), and `@pikku/modelcontextprotocol` except its `./fetch` subpath, which is `edge`.
|
|
73
|
+
|
|
74
|
+
`@pikku/addon-graph` no longer needs `node:crypto` or `Buffer`: the Crypto function hashes and HMACs on Web Crypto (MD5 is built in, since Web Crypto has none) and encodes with `TextEncoder`/`atob`/`btoa`, with output unchanged.
|
|
75
|
+
|
|
76
|
+
- Updated dependencies [6f11bb5]
|
|
77
|
+
- Updated dependencies [0c67e33]
|
|
78
|
+
- Updated dependencies [15c4968]
|
|
79
|
+
- Updated dependencies [d75a23d]
|
|
80
|
+
- Updated dependencies [9b12c8a]
|
|
81
|
+
- @pikku/core@0.12.138
|
|
82
|
+
|
|
1
83
|
## 0.13.34
|
|
2
84
|
|
|
3
85
|
### Patch Changes
|
|
@@ -12,8 +12,22 @@ import { DEFAULT_KEY_ID } from '@pikku/core/classification';
|
|
|
12
12
|
*/
|
|
13
13
|
export { DEFAULT_KEY_ID };
|
|
14
14
|
const ENVELOPE_PREFIX = 'pikku1';
|
|
15
|
-
|
|
16
|
-
const
|
|
15
|
+
/** base64url of the UTF-8 bytes, unpadded: `Buffer`-free, so it runs on edge runtimes. */
|
|
16
|
+
const encodePart = (value) => {
|
|
17
|
+
let binary = '';
|
|
18
|
+
for (const byte of new TextEncoder().encode(value)) {
|
|
19
|
+
binary += String.fromCharCode(byte);
|
|
20
|
+
}
|
|
21
|
+
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
22
|
+
};
|
|
23
|
+
const decodePart = (value) => {
|
|
24
|
+
const base64 = value.replace(/-/g, '+').replace(/_/g, '/');
|
|
25
|
+
const binary = atob(base64 + '='.repeat((4 - (base64.length % 4)) % 4));
|
|
26
|
+
const bytes = new Uint8Array(binary.length);
|
|
27
|
+
for (let i = 0; i < binary.length; i++)
|
|
28
|
+
bytes[i] = binary.charCodeAt(i);
|
|
29
|
+
return new TextDecoder().decode(bytes);
|
|
30
|
+
};
|
|
17
31
|
const serializeEnvelope = (envelope) => [
|
|
18
32
|
ENVELOPE_PREFIX,
|
|
19
33
|
encodePart(envelope.keyId),
|
package/dist/src/index.d.ts
CHANGED
|
@@ -22,12 +22,12 @@ export { KyselyAuditService } from './kysely-audit-service.js';
|
|
|
22
22
|
export { KyselyAnalyticsService } from './kysely-analytics-service.js';
|
|
23
23
|
export { KyselyVirtualUserRunStore } from './kysely-virtual-user-run-store.js';
|
|
24
24
|
export { KyselyVirtualUserScheduleStore } from './kysely-virtual-user-schedule-store.js';
|
|
25
|
-
export { CamelCasePlugin, Kysely, PostgresDialect } from 'kysely';
|
|
25
|
+
export { CamelCasePlugin, Kysely, MysqlDialect, PostgresDialect } from 'kysely';
|
|
26
26
|
export { SerializePlugin, BaseSerializePlugin, type Serializer, type Deserializer, } from './serialize-plugin.js';
|
|
27
27
|
export { createCoercionPlugin, type ColumnKind, type CoercionMap, type CreateCoercionPluginOptions, } from './coercion-plugin.js';
|
|
28
28
|
export { createClassificationPlugin, type CreateClassificationPluginOptions, } from './classification-plugin.js';
|
|
29
29
|
export { ClassificationCrypto, DEFAULT_KEY_ID, isColumnEnvelope, parseColumnEnvelope, type ClassificationCryptoOptions, type ColumnEnvelope, type KEKResolver, type ResolvedKEK, } from './classification-crypto.js';
|
|
30
|
-
export { agentSchema, analyticsSchema, auditSchema, channelSchema, credentialSchema, deploymentSchema, flagSchema, incomingWebhookSchema, leaseSchema, scopeSchema, secretSchema, sessionSchema, virtualUserSchema, webhookSchema, workflowSchema, pikkuSchemas, requiredPikkuSchemas, applyPikkuSchemas, compilePikkuSchemas, requirePikkuSchema, resolveRequirements, type PikkuSchema, type RequiredTypes, type UnmetRequirement, type SchemaStatementFactory, } from './schema/index.js';
|
|
30
|
+
export { agentSchema, analyticsSchema, auditSchema, channelSchema, credentialSchema, deploymentSchema, flagSchema, incomingWebhookSchema, leaseSchema, scopeSchema, secretSchema, sessionSchema, triggerSourceSchema, virtualUserScheduleSchema, virtualUserSchema, webhookSchema, workflowSchema, pikkuSchemas, requiredPikkuSchemas, applyPikkuSchemas, compilePikkuSchemas, requirePikkuSchema, resolveRequirements, withUserTable, type PikkuSchema, type RequiredTypes, type UnmetRequirement, type SchemaStatementFactory, } from './schema/index.js';
|
|
31
31
|
export type { KyselyPikkuDB } from './kysely-tables.js';
|
|
32
32
|
export type { WorkflowRunService } from '@pikku/core/workflow';
|
|
33
33
|
export type { AgentRunService, AgentRunRow } from '@pikku/core/agent';
|
package/dist/src/index.js
CHANGED
|
@@ -24,9 +24,9 @@ export { KyselyVirtualUserScheduleStore } from './kysely-virtual-user-schedule-s
|
|
|
24
24
|
// Re-exported so a generated file that constructs the services above opens and
|
|
25
25
|
// types its database with the same copy of kysely those services were built
|
|
26
26
|
// against, rather than a second copy the project may resolve on its own.
|
|
27
|
-
export { CamelCasePlugin, Kysely, PostgresDialect } from 'kysely';
|
|
27
|
+
export { CamelCasePlugin, Kysely, MysqlDialect, PostgresDialect } from 'kysely';
|
|
28
28
|
export { SerializePlugin, BaseSerializePlugin, } from './serialize-plugin.js';
|
|
29
29
|
export { createCoercionPlugin, } from './coercion-plugin.js';
|
|
30
30
|
export { createClassificationPlugin, } from './classification-plugin.js';
|
|
31
31
|
export { ClassificationCrypto, DEFAULT_KEY_ID, isColumnEnvelope, parseColumnEnvelope, } from './classification-crypto.js';
|
|
32
|
-
export { agentSchema, analyticsSchema, auditSchema, channelSchema, credentialSchema, deploymentSchema, flagSchema, incomingWebhookSchema, leaseSchema, scopeSchema, secretSchema, sessionSchema, virtualUserSchema, webhookSchema, workflowSchema, pikkuSchemas, requiredPikkuSchemas, applyPikkuSchemas, compilePikkuSchemas, requirePikkuSchema, resolveRequirements, } from './schema/index.js';
|
|
32
|
+
export { agentSchema, analyticsSchema, auditSchema, channelSchema, credentialSchema, deploymentSchema, flagSchema, incomingWebhookSchema, leaseSchema, scopeSchema, secretSchema, sessionSchema, triggerSourceSchema, virtualUserScheduleSchema, virtualUserSchema, webhookSchema, workflowSchema, pikkuSchemas, requiredPikkuSchemas, applyPikkuSchemas, compilePikkuSchemas, requirePikkuSchema, resolveRequirements, withUserTable, } from './schema/index.js';
|
|
@@ -2,7 +2,7 @@ import { sql } from 'kysely';
|
|
|
2
2
|
import { parseJson } from './kysely-json.js';
|
|
3
3
|
// Owner ids routinely contain `_` (e.g. `user_123`), which LIKE treats as a
|
|
4
4
|
// single-character wildcard — so a raw prefix would match a foreign owner.
|
|
5
|
-
const escapeLikePattern = (value) => value.replace(/[
|
|
5
|
+
const escapeLikePattern = (value) => value.replace(/[!%_]/g, (char) => `!${char}`);
|
|
6
6
|
export class KyselyAgentRunService {
|
|
7
7
|
db;
|
|
8
8
|
constructor(db) {
|
|
@@ -37,7 +37,7 @@ export class KyselyAgentRunService {
|
|
|
37
37
|
if (owners) {
|
|
38
38
|
query = query.where((eb) => eb.or(owners.flatMap((owner) => [
|
|
39
39
|
eb('t.resourceId', '=', owner),
|
|
40
|
-
eb('t.resourceId', 'like', sql `${`${escapeLikePattern(owner)}:%`} escape '
|
|
40
|
+
eb('t.resourceId', 'like', sql `${`${escapeLikePattern(owner)}:%`} escape '!'`),
|
|
41
41
|
])));
|
|
42
42
|
}
|
|
43
43
|
const result = await query
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { parseJson } from './kysely-json.js';
|
|
2
2
|
import { getRunScores, saveRunScore } from './kysely-agent-run-scores.js';
|
|
3
3
|
import { requirePikkuSchema } from './schema/index.js';
|
|
4
|
+
import { upsert } from './kysely-upsert.js';
|
|
4
5
|
import { agentSchema } from './schema/agent.schema.js';
|
|
5
6
|
export class KyselyAgentStorageService {
|
|
6
7
|
db;
|
|
@@ -216,6 +217,8 @@ export class KyselyAgentStorageService {
|
|
|
216
217
|
if (!toolMsg.toolResults)
|
|
217
218
|
continue;
|
|
218
219
|
for (const tr of toolMsg.toolResults) {
|
|
220
|
+
if (tr.result === undefined)
|
|
221
|
+
continue;
|
|
219
222
|
await this.db
|
|
220
223
|
.updateTable('agentToolCall')
|
|
221
224
|
.set({ result: tr.result })
|
|
@@ -241,19 +244,12 @@ export class KyselyAgentStorageService {
|
|
|
241
244
|
return parseJson(row.data);
|
|
242
245
|
}
|
|
243
246
|
async saveWorkingMemory(id, scope, data) {
|
|
244
|
-
await this.db
|
|
245
|
-
.insertInto('agentWorkingMemory')
|
|
246
|
-
.values({
|
|
247
|
+
await upsert(this.db, this.db.insertInto('agentWorkingMemory').values({
|
|
247
248
|
id,
|
|
248
249
|
scope,
|
|
249
250
|
data: JSON.stringify(data),
|
|
250
251
|
updatedAt: new Date(),
|
|
251
|
-
})
|
|
252
|
-
.onConflict((oc) => oc.columns(['id', 'scope']).doUpdateSet({
|
|
253
|
-
data: JSON.stringify(data),
|
|
254
|
-
updatedAt: new Date(),
|
|
255
|
-
}))
|
|
256
|
-
.execute();
|
|
252
|
+
}), ['id', 'scope'], { data: JSON.stringify(data), updatedAt: new Date() }).execute();
|
|
257
253
|
}
|
|
258
254
|
async createRun(run) {
|
|
259
255
|
const runId = crypto.randomUUID();
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { requirePikkuSchema } from './schema/index.js';
|
|
2
|
+
import { insertOrIgnore } from './kysely-upsert.js';
|
|
2
3
|
import { analyticsSchema } from './schema/analytics.schema.js';
|
|
3
4
|
const jsonOrNull = (value) => value != null ? JSON.stringify(value) : null;
|
|
4
5
|
/**
|
|
@@ -71,10 +72,6 @@ export class KyselyAnalyticsService {
|
|
|
71
72
|
consent: jsonOrNull(record.userIdentity?.consent),
|
|
72
73
|
props: jsonOrNull(record.props),
|
|
73
74
|
}));
|
|
74
|
-
await this.db
|
|
75
|
-
.insertInto('pikkuAnalyticsEvents')
|
|
76
|
-
.values(rows)
|
|
77
|
-
.onConflict((oc) => oc.doNothing())
|
|
78
|
-
.execute();
|
|
75
|
+
await insertOrIgnore(this.db, this.db.insertInto('pikkuAnalyticsEvents').values(rows)).execute();
|
|
79
76
|
}
|
|
80
77
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { requirePikkuSchema } from './schema/index.js';
|
|
2
|
+
import { insertOrIgnore } from './kysely-upsert.js';
|
|
2
3
|
import { auditSchema } from './schema/audit.schema.js';
|
|
3
4
|
const jsonOrNull = (v) => v != null ? JSON.stringify(v) : null;
|
|
4
5
|
const parseJson = (v) => {
|
|
@@ -122,11 +123,7 @@ export class KyselyAuditService {
|
|
|
122
123
|
data: jsonOrNull(e.metadata),
|
|
123
124
|
};
|
|
124
125
|
});
|
|
125
|
-
await this.db
|
|
126
|
-
.insertInto('audit')
|
|
127
|
-
.values(rows)
|
|
128
|
-
.onConflict((oc) => oc.doNothing())
|
|
129
|
-
.execute();
|
|
126
|
+
await insertOrIgnore(this.db, this.db.insertInto('audit').values(rows)).execute();
|
|
130
127
|
}
|
|
131
128
|
/**
|
|
132
129
|
* A page of the trail, newest first.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { deriveKEK, envelopeDecrypt, envelopeEncrypt, envelopeRewrap, generateKEKSalt, } from '@pikku/core/crypto-utils';
|
|
2
2
|
import { requirePikkuSchema } from './schema/index.js';
|
|
3
|
+
import { insertOrIgnore, timestampParam } from './kysely-upsert.js';
|
|
3
4
|
import { credentialSchema } from './schema/credential.schema.js';
|
|
4
5
|
export class KyselyCredentialService {
|
|
5
6
|
db;
|
|
@@ -37,7 +38,7 @@ export class KyselyCredentialService {
|
|
|
37
38
|
credentialName: name,
|
|
38
39
|
userId: userId ?? null,
|
|
39
40
|
action,
|
|
40
|
-
performedAt:
|
|
41
|
+
performedAt: timestampParam(this.db),
|
|
41
42
|
})
|
|
42
43
|
.execute();
|
|
43
44
|
}
|
|
@@ -52,15 +53,11 @@ export class KyselyCredentialService {
|
|
|
52
53
|
.executeTakeFirst();
|
|
53
54
|
let salt = existing?.salt;
|
|
54
55
|
if (!salt) {
|
|
55
|
-
await this.db
|
|
56
|
-
.insertInto('credentialKekSalts')
|
|
57
|
-
.values({
|
|
56
|
+
await insertOrIgnore(this.db, this.db.insertInto('credentialKekSalts').values({
|
|
58
57
|
keyVersion: version,
|
|
59
58
|
salt: generateKEKSalt(),
|
|
60
|
-
createdAt:
|
|
61
|
-
})
|
|
62
|
-
.onConflict((oc) => oc.column('keyVersion').doNothing())
|
|
63
|
-
.execute();
|
|
59
|
+
createdAt: timestampParam(this.db),
|
|
60
|
+
}), ['keyVersion']).execute();
|
|
64
61
|
const row = await this.db
|
|
65
62
|
.selectFrom('credentialKekSalts')
|
|
66
63
|
.select('salt')
|
|
@@ -110,7 +107,7 @@ export class KyselyCredentialService {
|
|
|
110
107
|
async set(name, value, userId) {
|
|
111
108
|
const plaintext = JSON.stringify(value);
|
|
112
109
|
const { ciphertext, wrappedDEK } = await envelopeEncrypt(await this.getKEK(this.keyVersion), plaintext);
|
|
113
|
-
const now =
|
|
110
|
+
const now = timestampParam(this.db);
|
|
114
111
|
const exists = await this.has(name, userId);
|
|
115
112
|
if (exists) {
|
|
116
113
|
let qb = this.db
|
|
@@ -212,7 +209,7 @@ export class KyselyCredentialService {
|
|
|
212
209
|
.set({
|
|
213
210
|
wrappedDek: newWrappedDEK,
|
|
214
211
|
keyVersion: this.keyVersion,
|
|
215
|
-
updatedAt:
|
|
212
|
+
updatedAt: timestampParam(this.db),
|
|
216
213
|
})
|
|
217
214
|
.where('name', '=', row.name);
|
|
218
215
|
qb = this.whereUserId(qb, row.userId ?? undefined);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { buildRemoteHeaders } from '@pikku/core/remote';
|
|
2
2
|
import { getAllFunctionNames } from '@pikku/core/function';
|
|
3
3
|
import { requirePikkuSchema } from './schema/index.js';
|
|
4
|
+
import { upsert } from './kysely-upsert.js';
|
|
4
5
|
import { deploymentSchema } from './schema/deployment.schema.js';
|
|
5
6
|
export class KyselyDeploymentService {
|
|
6
7
|
db;
|
|
@@ -28,18 +29,11 @@ export class KyselyDeploymentService {
|
|
|
28
29
|
const functions = config.functions ?? getAllFunctionNames();
|
|
29
30
|
this.deploymentConfig = { ...config, functions };
|
|
30
31
|
await this.db.transaction().execute(async (trx) => {
|
|
31
|
-
await trx
|
|
32
|
-
.insertInto('pikkuDeployments')
|
|
33
|
-
.values({
|
|
32
|
+
await upsert(trx, trx.insertInto('pikkuDeployments').values({
|
|
34
33
|
deploymentId: config.deploymentId,
|
|
35
34
|
endpoint: config.endpoint,
|
|
36
35
|
lastHeartbeat: new Date(),
|
|
37
|
-
})
|
|
38
|
-
.onConflict((oc) => oc.column('deploymentId').doUpdateSet({
|
|
39
|
-
endpoint: config.endpoint,
|
|
40
|
-
lastHeartbeat: new Date(),
|
|
41
|
-
}))
|
|
42
|
-
.execute();
|
|
36
|
+
}), ['deploymentId'], { endpoint: config.endpoint, lastHeartbeat: new Date() }).execute();
|
|
43
37
|
await trx
|
|
44
38
|
.deleteFrom('pikkuDeploymentFunctions')
|
|
45
39
|
.where('deploymentId', '=', config.deploymentId)
|
|
@@ -54,7 +48,10 @@ export class KyselyDeploymentService {
|
|
|
54
48
|
.execute();
|
|
55
49
|
}
|
|
56
50
|
});
|
|
57
|
-
this.heartbeatTimer = setInterval(() =>
|
|
51
|
+
this.heartbeatTimer = setInterval(() => {
|
|
52
|
+
// sendHeartbeat catches its own errors; it retries on the next tick
|
|
53
|
+
void this.sendHeartbeat();
|
|
54
|
+
}, this.heartbeatInterval);
|
|
58
55
|
}
|
|
59
56
|
async stop() {
|
|
60
57
|
if (this.heartbeatTimer) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { EventHubStore } from '@pikku/core/channel';
|
|
2
|
+
import { insertOrIgnore } from './kysely-upsert.js';
|
|
2
3
|
export class KyselyEventHubStore extends EventHubStore {
|
|
3
4
|
db;
|
|
4
5
|
initialized = false;
|
|
@@ -22,11 +23,9 @@ export class KyselyEventHubStore extends EventHubStore {
|
|
|
22
23
|
}
|
|
23
24
|
async subscribe(topic, channelId) {
|
|
24
25
|
try {
|
|
25
|
-
await this.db
|
|
26
|
+
await insertOrIgnore(this.db, this.db
|
|
26
27
|
.insertInto('channelSubscriptions')
|
|
27
|
-
.values({ channelId: channelId, topic: topic })
|
|
28
|
-
.onConflict((oc) => oc.columns(['channelId', 'topic']).doNothing())
|
|
29
|
-
.execute();
|
|
28
|
+
.values({ channelId: channelId, topic: topic }), ['channelId', 'topic']).execute();
|
|
30
29
|
return true;
|
|
31
30
|
}
|
|
32
31
|
catch {
|
|
@@ -20,9 +20,9 @@ export declare class KyselyFeatureFlagStore extends CachedFlagSource implements
|
|
|
20
20
|
syncFlags(flags: DeclaredFlag[]): Promise<void>;
|
|
21
21
|
listFlags(): Promise<FlagRow[]>;
|
|
22
22
|
listOverrides(key: string): Promise<FlagOverrideRow[]>;
|
|
23
|
-
setEnabled(key: string, enabled: boolean,
|
|
24
|
-
setRollout(key: string, percent: number | null,
|
|
25
|
-
setOverride(key: string, subject: FlagSubject, enabled: boolean,
|
|
23
|
+
setEnabled(key: string, enabled: boolean, virtualUser?: string, note?: string): Promise<void>;
|
|
24
|
+
setRollout(key: string, percent: number | null, virtualUser?: string): Promise<void>;
|
|
25
|
+
setOverride(key: string, subject: FlagSubject, enabled: boolean, virtualUser?: string): Promise<void>;
|
|
26
26
|
clearOverride(key: string, subject: FlagSubject): Promise<void>;
|
|
27
27
|
findStaleFlags(): Promise<string[]>;
|
|
28
28
|
/**
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { CachedFlagSource, subjectIdOf } from '@pikku/core/flag';
|
|
2
2
|
import { requirePikkuSchema } from './schema/index.js';
|
|
3
|
+
import { INSERTED, isMysql, upsert } from './kysely-upsert.js';
|
|
3
4
|
import { flagSchema } from './schema/flag.schema.js';
|
|
4
5
|
/**
|
|
5
6
|
* Reads the stored `anyOf`, which arrives as either shape.
|
|
@@ -77,25 +78,16 @@ export class KyselyFeatureFlagStore extends CachedFlagSource {
|
|
|
77
78
|
this.setDeclared(flags);
|
|
78
79
|
await this.db.transaction().execute(async (trx) => {
|
|
79
80
|
for (const flag of flags) {
|
|
80
|
-
await trx
|
|
81
|
-
.insertInto('pikkuFeatureFlags')
|
|
82
|
-
.values({
|
|
83
|
-
name: flag.name,
|
|
84
|
-
description: flag.description ?? null,
|
|
85
|
-
anyOf: flag.anyOf ? JSON.stringify(flag.anyOf) : null,
|
|
86
|
-
declared: true,
|
|
87
|
-
})
|
|
88
|
-
.onConflict((oc) =>
|
|
89
81
|
// `enabled`, `rolloutPercent` and every override are the operator's
|
|
90
82
|
// and are never touched here. A deploy that re-enabled a flag
|
|
91
83
|
// somebody killed an hour earlier would make the kill switch
|
|
92
84
|
// useless exactly when it is being relied on.
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
85
|
+
await upsert(trx, trx.insertInto('pikkuFeatureFlags').values({
|
|
86
|
+
name: flag.name,
|
|
87
|
+
description: flag.description ?? null,
|
|
88
|
+
anyOf: flag.anyOf ? JSON.stringify(flag.anyOf) : null,
|
|
96
89
|
declared: true,
|
|
97
|
-
})))
|
|
98
|
-
.execute();
|
|
90
|
+
}), ['name'], { description: INSERTED, anyOf: INSERTED, declared: true }).execute();
|
|
99
91
|
}
|
|
100
92
|
const markStale = trx
|
|
101
93
|
.updateTable('pikkuFeatureFlags')
|
|
@@ -140,12 +132,12 @@ export class KyselyFeatureFlagStore extends CachedFlagSource {
|
|
|
140
132
|
: undefined,
|
|
141
133
|
}));
|
|
142
134
|
}
|
|
143
|
-
async setEnabled(key, enabled,
|
|
135
|
+
async setEnabled(key, enabled, virtualUser, note) {
|
|
144
136
|
await this.db
|
|
145
137
|
.updateTable('pikkuFeatureFlags')
|
|
146
138
|
.set({
|
|
147
139
|
enabled,
|
|
148
|
-
updatedBy:
|
|
140
|
+
updatedBy: virtualUser ?? null,
|
|
149
141
|
note: note ?? null,
|
|
150
142
|
updatedAt: new Date(),
|
|
151
143
|
})
|
|
@@ -153,7 +145,7 @@ export class KyselyFeatureFlagStore extends CachedFlagSource {
|
|
|
153
145
|
.execute();
|
|
154
146
|
this.invalidate();
|
|
155
147
|
}
|
|
156
|
-
async setRollout(key, percent,
|
|
148
|
+
async setRollout(key, percent, virtualUser) {
|
|
157
149
|
if (percent !== null && (percent < 0 || percent > 100)) {
|
|
158
150
|
throw new Error(`Feature flag '${key}': rollout must be between 0 and 100, got ${percent}.`);
|
|
159
151
|
}
|
|
@@ -161,38 +153,34 @@ export class KyselyFeatureFlagStore extends CachedFlagSource {
|
|
|
161
153
|
.updateTable('pikkuFeatureFlags')
|
|
162
154
|
.set({
|
|
163
155
|
rolloutPercent: percent === null ? null : Math.round(percent),
|
|
164
|
-
updatedBy:
|
|
156
|
+
updatedBy: virtualUser ?? null,
|
|
165
157
|
updatedAt: new Date(),
|
|
166
158
|
})
|
|
167
159
|
.where('name', '=', key)
|
|
168
160
|
.execute();
|
|
169
161
|
this.invalidate();
|
|
170
162
|
}
|
|
171
|
-
async setOverride(key, subject, enabled,
|
|
163
|
+
async setOverride(key, subject, enabled, virtualUser) {
|
|
172
164
|
const subjectId = subjectIdOf(subject);
|
|
173
165
|
if (!subjectId) {
|
|
174
166
|
throw new Error(`Feature flag '${key}': an override needs an organization or a user to be about.`);
|
|
175
167
|
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
168
|
+
// `grantedAt` is rewritten too: the column defaults on insert only, and the
|
|
169
|
+
// panel reads it as when the pin was last granted — not when the subject
|
|
170
|
+
// was first pinned to something else.
|
|
171
|
+
await upsert(this.db, this.db.insertInto('pikkuFeatureFlagOverrides').values({
|
|
179
172
|
flag: key,
|
|
180
173
|
subjectId,
|
|
181
174
|
subjectKind: subject.organizationId ? 'organization' : 'user',
|
|
182
175
|
enabled,
|
|
183
|
-
grantedBy:
|
|
176
|
+
grantedBy: virtualUser ?? null,
|
|
184
177
|
grantedAt: new Date(),
|
|
185
|
-
})
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
// when the pin was last granted — not when the subject was first
|
|
192
|
-
// pinned to something else.
|
|
193
|
-
grantedAt: eb.ref('excluded.grantedAt'),
|
|
194
|
-
})))
|
|
195
|
-
.execute();
|
|
178
|
+
}), ['flag', 'subjectId'], {
|
|
179
|
+
enabled: INSERTED,
|
|
180
|
+
subjectKind: INSERTED,
|
|
181
|
+
grantedBy: INSERTED,
|
|
182
|
+
grantedAt: INSERTED,
|
|
183
|
+
}).execute();
|
|
196
184
|
this.invalidate();
|
|
197
185
|
}
|
|
198
186
|
async clearOverride(key, subject) {
|
|
@@ -225,15 +213,33 @@ export class KyselyFeatureFlagStore extends CachedFlagSource {
|
|
|
225
213
|
* live flag and cascade away every override an operator had set on it.
|
|
226
214
|
*/
|
|
227
215
|
async pruneFlags() {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
216
|
+
// MySQL has no `returning`: the matching names are read under a lock in the
|
|
217
|
+
// same transaction as the delete, so what is reported is what was deleted.
|
|
218
|
+
const names = await this.db.transaction().execute(async (trx) => {
|
|
219
|
+
if (isMysql(trx)) {
|
|
220
|
+
const rows = await trx
|
|
221
|
+
.selectFrom('pikkuFeatureFlags')
|
|
222
|
+
.select('name')
|
|
223
|
+
.where('declared', '=', false)
|
|
224
|
+
.forUpdate()
|
|
225
|
+
.execute();
|
|
226
|
+
await trx
|
|
227
|
+
.deleteFrom('pikkuFeatureFlags')
|
|
228
|
+
.where('declared', '=', false)
|
|
229
|
+
.execute();
|
|
230
|
+
return rows.map((row) => row.name);
|
|
231
|
+
}
|
|
232
|
+
const deleted = await trx
|
|
233
|
+
.deleteFrom('pikkuFeatureFlags')
|
|
234
|
+
.where('declared', '=', false)
|
|
235
|
+
.returning('name')
|
|
236
|
+
.execute();
|
|
237
|
+
return deleted.map((row) => row.name);
|
|
238
|
+
});
|
|
239
|
+
if (names.length === 0) {
|
|
234
240
|
return [];
|
|
235
241
|
}
|
|
236
242
|
this.invalidate();
|
|
237
|
-
return
|
|
243
|
+
return names.sort();
|
|
238
244
|
}
|
|
239
245
|
}
|
|
@@ -9,6 +9,6 @@ import { type RawBuilder } from 'kysely';
|
|
|
9
9
|
* worker shares. This default binds the process clock, for SQLite, where the
|
|
10
10
|
* database lives in the process anyway.
|
|
11
11
|
*/
|
|
12
|
-
export declare const appNowMs: () => RawBuilder<number>;
|
|
12
|
+
export declare const appNowMs: (mysql?: boolean) => RawBuilder<number>;
|
|
13
13
|
/** `now + ms`, for a lease that runs `ms` from the clock's now. */
|
|
14
14
|
export declare const leaseUntil: (now: RawBuilder<number>, ms: number) => RawBuilder<number>;
|
|
@@ -9,6 +9,10 @@ import { sql } from 'kysely';
|
|
|
9
9
|
* worker shares. This default binds the process clock, for SQLite, where the
|
|
10
10
|
* database lives in the process anyway.
|
|
11
11
|
*/
|
|
12
|
-
export const appNowMs = (
|
|
12
|
+
export const appNowMs = (mysql = false) =>
|
|
13
|
+
// MySQL has no `bigint` to cast to; `signed` is its 64-bit integer.
|
|
14
|
+
mysql
|
|
15
|
+
? sql `cast(${Date.now()} as signed)`
|
|
16
|
+
: sql `cast(${Date.now()} as bigint)`;
|
|
13
17
|
/** `now + ms`, for a lease that runs `ms` from the clock's now. */
|
|
14
18
|
export const leaseUntil = (now, ms) => sql `${now} + ${ms}`;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { appNowMs, leaseUntil } from './kysely-lease-clock.js';
|
|
2
2
|
import { requirePikkuSchema } from './schema/index.js';
|
|
3
|
+
import { insertOrIgnore, isMysql } from './kysely-upsert.js';
|
|
3
4
|
import { leaseSchema } from './schema/lease.schema.js';
|
|
4
5
|
const toLease = (row) => ({
|
|
5
6
|
key: row.key,
|
|
@@ -31,15 +32,13 @@ export class KyselyLeaseService {
|
|
|
31
32
|
}
|
|
32
33
|
/** Now, in epoch milliseconds, on the clock leases are judged by. */
|
|
33
34
|
nowMs() {
|
|
34
|
-
return appNowMs();
|
|
35
|
+
return appNowMs(isMysql(this.db));
|
|
35
36
|
}
|
|
36
37
|
/** Create the key's row, lapsed, unless it already has one. */
|
|
37
38
|
async insertIfAbsent(key, holder) {
|
|
38
|
-
await this.db
|
|
39
|
+
await insertOrIgnore(this.db, this.db
|
|
39
40
|
.insertInto('pikkuLease')
|
|
40
|
-
.values({ key, holder, token: 0, expiresAt: 0 })
|
|
41
|
-
.onConflict((oc) => oc.column('key').doNothing())
|
|
42
|
-
.execute();
|
|
41
|
+
.values({ key, holder, token: 0, expiresAt: 0 }), ['key']).execute();
|
|
43
42
|
}
|
|
44
43
|
/**
|
|
45
44
|
* The row is created lapsed and then taken by the same guarded `UPDATE` as
|