@sanoma-ai/workflows 0.11.11 → 0.11.13
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/CLOUD-IDENTITY.md +12 -4
- package/KMS-SECRETS.md +3 -1
- package/README.md +6 -2
- package/dist/bundled.d.ts +2 -2
- package/dist/chats.d.ts +3 -3
- package/dist/chats.js +34 -20
- package/dist/chats.js.map +1 -1
- package/dist/client.d.ts +17 -4
- package/dist/client.js +74 -63
- package/dist/client.js.map +1 -1
- package/dist/cloud-identity.js +14 -12
- package/dist/cloud-identity.js.map +1 -1
- package/dist/collection.d.ts +2 -2
- package/dist/collection.js +53 -42
- package/dist/collection.js.map +1 -1
- package/dist/config.d.ts +11 -6
- package/dist/config.js +15 -13
- package/dist/config.js.map +1 -1
- package/dist/connection.d.ts +2 -0
- package/dist/connection.js +112 -40
- package/dist/connection.js.map +1 -1
- package/dist/credentials.d.ts +1 -1
- package/dist/credentials.js +33 -29
- package/dist/credentials.js.map +1 -1
- package/dist/database.js +4 -1
- package/dist/database.js.map +1 -1
- package/dist/db/company.d.ts +1152 -0
- package/dist/db/company.js +118 -0
- package/dist/db/company.js.map +1 -0
- package/dist/db/dbos.d.ts +111 -0
- package/dist/db/dbos.js +15 -0
- package/dist/db/dbos.js.map +1 -0
- package/dist/db/identity.d.ts +367 -0
- package/dist/db/identity.js +48 -0
- package/dist/db/identity.js.map +1 -0
- package/dist/db/index.d.ts +6 -0
- package/dist/db/index.js +10 -0
- package/dist/db/index.js.map +1 -0
- package/dist/db/kms.d.ts +224 -0
- package/dist/db/kms.js +29 -0
- package/dist/db/kms.js.map +1 -0
- package/dist/db/migrate.d.ts +63 -0
- package/dist/db/migrate.js +41 -0
- package/dist/db/migrate.js.map +1 -0
- package/dist/db/wakeup.d.ts +271 -0
- package/dist/db/wakeup.js +34 -0
- package/dist/db/wakeup.js.map +1 -0
- package/dist/db.d.ts +42 -39
- package/dist/db.js +129 -169
- package/dist/db.js.map +1 -1
- package/dist/deadlines.js +8 -4
- package/dist/deadlines.js.map +1 -1
- package/dist/deployment-key.d.ts +4 -1
- package/dist/deployment-key.js +10 -3
- package/dist/deployment-key.js.map +1 -1
- package/dist/host-derive.d.ts +12 -3
- package/dist/host-derive.js +21 -8
- package/dist/host-derive.js.map +1 -1
- package/dist/identity-provisioning.d.ts +2 -2
- package/dist/identity-provisioning.js +75 -42
- package/dist/identity-provisioning.js.map +1 -1
- package/dist/identity-service.js +18 -15
- package/dist/identity-service.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/kms-secrets.d.ts +9 -0
- package/dist/kms-secrets.js +59 -27
- package/dist/kms-secrets.js.map +1 -1
- package/dist/ledger.d.ts +1 -1
- package/dist/ledger.js +22 -14
- package/dist/ledger.js.map +1 -1
- package/dist/op.d.ts +27 -6
- package/dist/op.js.map +1 -1
- package/dist/run-status.js +7 -3
- package/dist/run-status.js.map +1 -1
- package/dist/schedules.d.ts +1 -1
- package/dist/schedules.js +5 -7
- package/dist/schedules.js.map +1 -1
- package/dist/schema.d.ts +6 -5
- package/dist/schema.js +11 -7
- package/dist/schema.js.map +1 -1
- package/dist/shared.d.ts +6 -23
- package/dist/shared.js +6 -5
- package/dist/shared.js.map +1 -1
- package/dist/storage-provisioning.d.ts +2 -0
- package/dist/storage-provisioning.js +51 -50
- package/dist/storage-provisioning.js.map +1 -1
- package/dist/tokens.d.ts +2 -2
- package/dist/tokens.js +31 -20
- package/dist/tokens.js.map +1 -1
- package/dist/version.d.ts +4 -4
- package/dist/version.js +5 -5
- package/dist/version.js.map +1 -1
- package/dist/wakeup-service.d.ts +0 -4
- package/dist/wakeup-service.js +102 -93
- package/dist/wakeup-service.js.map +1 -1
- package/dist/worker.d.ts +7 -0
- package/dist/worker.js +26 -21
- package/dist/worker.js.map +1 -1
- package/drizzle/company/0000_tables.sql +104 -0
- package/drizzle/company/0001_wakeup_row.sql +2 -0
- package/drizzle/company/meta/0000_snapshot.json +652 -0
- package/drizzle/company/meta/0001_snapshot.json +652 -0
- package/drizzle/company/meta/_journal.json +20 -0
- package/drizzle/wakeup/0000_tables.sql +26 -0
- package/drizzle/wakeup/meta/0000_snapshot.json +200 -0
- package/drizzle/wakeup/meta/_journal.json +13 -0
- package/oxlint.json +2 -1
- package/package.json +11 -2
- package/src/bundled.ts +2 -2
- package/src/chats.ts +41 -42
- package/src/client.ts +81 -64
- package/src/cloud-identity.ts +23 -24
- package/src/collection.ts +58 -50
- package/src/config.ts +29 -19
- package/src/connection.ts +132 -102
- package/src/credentials.ts +37 -33
- package/src/database.ts +4 -2
- package/src/db/company.ts +173 -0
- package/src/db/dbos.ts +17 -0
- package/src/db/identity.ts +61 -0
- package/src/db/index.ts +10 -0
- package/src/db/kms.ts +41 -0
- package/src/db/migrate.ts +70 -0
- package/src/db/wakeup.ts +41 -0
- package/src/db.ts +151 -182
- package/src/deadlines.ts +10 -4
- package/src/deployment-key.ts +11 -3
- package/src/host-derive.ts +26 -9
- package/src/identity-provisioning.ts +85 -57
- package/src/identity-service.ts +19 -19
- package/src/index.ts +3 -0
- package/src/kms-secrets.ts +64 -56
- package/src/ledger.ts +23 -18
- package/src/op.ts +28 -6
- package/src/run-status.ts +7 -6
- package/src/schedules.ts +6 -8
- package/src/schema.ts +11 -8
- package/src/shared.ts +6 -17
- package/src/storage-provisioning.ts +47 -52
- package/src/tokens.ts +43 -33
- package/src/version.ts +5 -5
- package/src/wakeup-service.ts +117 -131
- package/src/worker.ts +38 -21
package/CLOUD-IDENTITY.md
CHANGED
|
@@ -75,8 +75,10 @@ Trusted composition that already owns a writer pool may instead supply the low-l
|
|
|
75
75
|
|
|
76
76
|
```ts
|
|
77
77
|
import { postgresIdentityProvisioning } from "@sanoma-ai/workflows/cloud-identity";
|
|
78
|
+
import { databaseOn } from "@sanoma-ai/workflows/db";
|
|
78
79
|
|
|
79
|
-
const
|
|
80
|
+
const enrollmentWriter = databaseOn(enrollmentWriterPool);
|
|
81
|
+
const identityProvisioning = postgresIdentityProvisioning(async () => enrollmentWriter, {
|
|
80
82
|
maxValidityMs: 24 * 60 * 60 * 1000,
|
|
81
83
|
});
|
|
82
84
|
// Supply this adapter as defineConfig's identityProvisioning value.
|
|
@@ -111,9 +113,11 @@ import {
|
|
|
111
113
|
postgresIdentityEnrollments,
|
|
112
114
|
attachedGoogleTokenIssuer,
|
|
113
115
|
} from "@sanoma-ai/workflows/cloud-identity";
|
|
116
|
+
import { databaseOn } from "@sanoma-ai/workflows/db";
|
|
114
117
|
|
|
118
|
+
const brokerReader = databaseOn(brokerReadOnlyPool);
|
|
115
119
|
const handle = identityBroker({
|
|
116
|
-
enrollments: postgresIdentityEnrollments(async () =>
|
|
120
|
+
enrollments: postgresIdentityEnrollments(async () => brokerReader),
|
|
117
121
|
issue: attachedGoogleTokenIssuer(),
|
|
118
122
|
audit: writeDurableIdentityAudit,
|
|
119
123
|
});
|
|
@@ -131,8 +135,10 @@ The following composition joins D1 to the separate [#470 KMS adapter](https://gi
|
|
|
131
135
|
import { readFile } from "node:fs/promises";
|
|
132
136
|
import { KeyManagementServiceClient } from "@google-cloud/kms";
|
|
133
137
|
import { cloudIdentityAuth } from "@sanoma-ai/workflows/cloud-identity";
|
|
138
|
+
import { databaseOn } from "@sanoma-ai/workflows/db";
|
|
134
139
|
import { kmsPostgresStore } from "@sanoma-ai/workflows/kms-secrets";
|
|
135
140
|
|
|
141
|
+
const companyDatabase = databaseOn(companyScopedPool);
|
|
136
142
|
const authClient = cloudIdentityAuth({
|
|
137
143
|
endpoint: "https://identity.example.test/token",
|
|
138
144
|
credential: async () => (await readFile("/run/sanoma/deployment-credential", "utf8")).trim(),
|
|
@@ -142,7 +148,7 @@ const store = kmsPostgresStore({
|
|
|
142
148
|
company: "example-company", // Fixed by trusted bootstrap.
|
|
143
149
|
keyName: "projects/example-project/locations/us-central1/keyRings/production/cryptoKeys/example-company",
|
|
144
150
|
kms,
|
|
145
|
-
database: async () =>
|
|
151
|
+
database: async () => companyDatabase,
|
|
146
152
|
});
|
|
147
153
|
// Supply store to defineConfig's secrets configuration.
|
|
148
154
|
// The deployment owns kms.close() and the database pool lifecycle.
|
|
@@ -208,7 +214,9 @@ import {
|
|
|
208
214
|
attachedGoogleIdTokenIssuer,
|
|
209
215
|
postgresIdentityEnrollments,
|
|
210
216
|
} from "@sanoma-ai/workflows/cloud-identity";
|
|
217
|
+
import { databaseOn } from "@sanoma-ai/workflows/db";
|
|
211
218
|
|
|
219
|
+
const provisioningReader = databaseOn(provisioningReadOnlyPool);
|
|
212
220
|
const identity = {
|
|
213
221
|
serviceAccount: "provision-source@example-project.iam.gserviceaccount.com",
|
|
214
222
|
subject: "123456789012345678901",
|
|
@@ -217,7 +225,7 @@ const identity = {
|
|
|
217
225
|
};
|
|
218
226
|
const handle = provisioningIdentityBroker({
|
|
219
227
|
identity,
|
|
220
|
-
enrollments: postgresIdentityEnrollments(async () =>
|
|
228
|
+
enrollments: postgresIdentityEnrollments(async () => provisioningReader),
|
|
221
229
|
issue: attachedGoogleIdTokenIssuer(identity),
|
|
222
230
|
audit: writeDurableProvisioningAudit,
|
|
223
231
|
});
|
package/KMS-SECRETS.md
CHANGED
|
@@ -48,8 +48,10 @@ For standalone use or an explicitly supplied KMS client, use the lower-level `km
|
|
|
48
48
|
|
|
49
49
|
```ts
|
|
50
50
|
import { KeyManagementServiceClient } from "@google-cloud/kms";
|
|
51
|
+
import { databaseOn } from "@sanoma-ai/workflows/db";
|
|
51
52
|
import { kmsPostgresStore } from "@sanoma-ai/workflows/kms-secrets";
|
|
52
53
|
|
|
54
|
+
const companyDatabase = databaseOn(companyPool);
|
|
53
55
|
const kms = new KeyManagementServiceClient({ authClient: companyAuthClient });
|
|
54
56
|
const store = kmsPostgresStore({
|
|
55
57
|
company: companyIdFromTrustedBootstrap,
|
|
@@ -60,7 +62,7 @@ const store = kmsPostgresStore({
|
|
|
60
62
|
await store.validate("production");
|
|
61
63
|
```
|
|
62
64
|
|
|
63
|
-
An explicit `database` returns a `SecretStore`; omitting it returns a factory that uses the runtime's database. In both forms of `kmsPostgresStore`, the caller owns the supplied KMS client, including `kms.close()`. An explicit database
|
|
65
|
+
An explicit `database` returns a `SecretStore`; omitting it returns a factory that uses the runtime's database. In both forms of `kmsPostgresStore`, the caller owns the supplied KMS client, including `kms.close()`. An explicit database (`databaseOn` over the caller's `pg` pool) is also caller-owned, its pool included. `managedKmsStore` supplies both the runtime binding and SDK lifecycle instead.
|
|
64
66
|
|
|
65
67
|
`SANOMA_SIGNING_KEY` stays independent. This store does not use `SANOMA_SECRETS_KEY`. Workflow code cannot import either backend entry. Secret intake must remain outside durable workflow inputs/outputs. Runtime binding does not establish deployed Cloud readiness; provisioning, bootstrap delivery and live company isolation remain #532 verification work.
|
|
66
68
|
|
package/README.md
CHANGED
|
@@ -53,7 +53,9 @@ export const shop = withSetup(defineConnector("shop", specs, info), {
|
|
|
53
53
|
|
|
54
54
|
`withSetup` lays what it is given over what the connector already carries, and keeps it on the connector's vendor record, so a spread copy of the connector keeps it too. A config that needs a driver made otherwise (another bridge, other options) swaps the driver and keeps the fake: `withSetup(github, { driver: () => githubDriver({ bridge }) })`. It is for connector packages and configs, not for workflows, which the lint does not let import it. Both are optional: a connector without a driver declares its operations and implements none, so a workflow using one is refused; a connector without a fake has none for sandbox runs, which the app's connector page says.
|
|
55
55
|
|
|
56
|
-
A connector that
|
|
56
|
+
A connector that keeps tables on the company's database (a login's) attaches how to make them, as `prepare`: `withSetup(connector, { prepare: async (databaseUrl) => … })`, a `DatabasePreparation` the worker runs at its start, after the runtime's own tables, with the database's direct URL (never a pooler's: an advisory lock is one connection's); idempotent, and safe for two workers at once. The app never makes a connector's tables, since on a host it may run where it must not migrate: a login opened before the worker prepared them refuses. `startWorker`'s `prepare` option adds preparations no config names, as the `sanoma` bin adds the staff login's.
|
|
57
|
+
|
|
58
|
+
A connector that signs visitors in to a public site attaches a login half too, as `login`: `withSetup(connector, { login: (context) => half })`. The app calls it once per public site whose `site.ts` names the connector (`defineSite({ public: true, login: "<vendor>" })`), when it starts, with a `LoginContext` (`database()`, the client's pool, the app's one on Node and the request's own on a Worker, so a half keeps an instance per pool and nothing else across requests; a key of its own derived from `SANOMA_SIGNING_KEY` to sign its cookies with, a vendor's secrets, the site's origins, `sendCode(email, code)` when the config names a `login.sender`, and `social`, the company's GitHub App's OAuth client when the app is given it), on the tables its `prepare` made. The `LoginHalf` it makes (see its type) answers the provider's stable subject for a request, with the email and the GitHub login when it verified them, never a principal: the app makes the visitor's principal from it, so a connector cannot set a visitor's groups. A visitor's address, for rate limits, is the `CLIENT_IP_HEADER` header the app sets, never `X-Forwarded-For`. One vendor's connectors attach one `login`, as for a driver. [`@sanoma-ai/connector-better-auth`](https://www.npmjs.com/package/@sanoma-ai/connector-better-auth) is one, email and password (and an email code, with a sender) on the company's database; it also exports `betterAuthStaffLogin`, the staff sign-in the app uses on a host (`LoginContext.staff`: who is declared, the GitHub client, and where a sign-in is recorded). The app makes it as it starts and sends it every request to the site's own host ([public sites](https://www.npmjs.com/package/@sanoma-ai/app#public-sites)).
|
|
57
59
|
|
|
58
60
|
A model connector attaches a model half, as `model`: `withSetup(connector, { model: (context) => provider })`, an AI SDK provider (`ProviderV4` from `@ai-sdk/provider`) with the vendor's adapter as the transport. The app makes it for its model registry, once per resolved config and client (again after one that throws), where its models are `<vendor>/<model-id>`; it answers `languageModel("default")` with the connector's own default. Its `ModelContext` gives it the secrets as a login's does (the selected store and explicit bindings), which it reads when a model is called; its driver declares the key in `env`, with no operations if it needs none. One vendor's connectors attach one `model`. [`@sanoma-ai/connector-anthropic`](https://www.npmjs.com/package/@sanoma-ai/connector-anthropic) is one.
|
|
59
61
|
|
|
@@ -669,6 +671,8 @@ A stopped worker cannot launch again in its process, so whoever supervises it st
|
|
|
669
671
|
|
|
670
672
|
A database is a resource type that holds the company's database (a Neon project, a Supabase project): `defineResource({ …, writable: true, afterCreate, database })`, where `database` (`DatabaseSpec`) says what it can do (`capabilities: { pauses, branch }`: its compute scales to zero and wakes on a connection; it makes copy-on-write branches that carry the data), where it takes going live's choices, by dotted path into its fields (`inputs: { region, size? }`), the regions it takes when known (`regions`), the operation that stores its URL (`storeUrl`) and how its state reads (`outputs(state) → { healthy }`). `storeUrl` is an operation of the type's own vendor, in the type's own group (`neon.project.storeUrl`) or another (`supabase.database.storeUrl`), never one of a resource type's own: it takes `{ ref | id, export }` and stores the URL that carries `LISTEN` as `SANOMA_DATABASE_URL_<EXPORT>` (`databaseUrlSecret(export)`; `storeDatabaseUrl` stores it so, refusing a secret that holds another), never returning it, and the type's `afterCreate` calls it. A database type is not `stateless`: apply asks before it deletes one. `defineResource` refuses a `database` that breaks any of that; `resolveConfig` refuses one whose `storeUrl` is not among the config's operations, or does not take a binding id and an export. Its conformance kit is [`@sanoma-ai/testing/database`](../testing/README.md#a-databases-conformance). After retiring a database, a workflow can remove its exported URL with `sanoma.secret.delete` under the [runtime operation contract](#the-runtimes-own-operations).
|
|
671
673
|
|
|
674
|
+
`databaseUrlSecret` converts camel-case exports to uppercase words (`mainStore` → `SANOMA_DATABASE_URL_MAIN_STORE`) and preserves exports that are already uppercase, including digits and underscores (`CLOUD_ACME_97F9` → `SANOMA_DATABASE_URL_CLOUD_ACME_97F9`). Use the same export when storing, binding and retiring its URL.
|
|
675
|
+
|
|
672
676
|
### Secrets
|
|
673
677
|
|
|
674
678
|
Never put a secret in a workflow's input or an operation's input or output. They are persisted verbatim: DBOS keeps every step's input and output in Postgres, the ledger records each call's input and output, an approval a policy asks for carries the held call's input, and the app shows all of it to anyone who can reach it. A driver reads its credentials when it is called (from the environment or a secret store), and an operation that creates a secret (an API key, a password reset link) returns a reference to where it is stored, not the secret.
|
|
@@ -679,7 +683,7 @@ A secret a resource needs is named, never written. A data file gives `secret("NA
|
|
|
679
683
|
|
|
680
684
|
Every run is stamped with the application version of the worker that runs it, `<appName>@<version>`, and only a worker on that version recovers it after a restart. Versioning is automatic: the version is a hash of each workflow's name, the source of its `run` function, its operations and its input schema, which is DBOS's own scheme applied to the workflow code rather than to the runtime's registration wrapper (DBOS would otherwise see one identical function for every workflow and never change). The hash cannot see functions `run` calls that live elsewhere, op schemas, drivers or the policy: edit those with no run in flight, or restart on the old code first. To name a version instead (a git commit, say), set DBOS's `DBOS__APPVERSION` environment variable; it is prefixed with the app name the same way. A new version becomes the app's latest when it first starts, and runs queued without a version go to the latest; a worker started on an older version (a rollback) warns instead, unless started with `{ promote: true }`. The hash is of the code as it runs, so TypeScript source and the compiled JavaScript of the same workflow have different versions.
|
|
681
685
|
|
|
682
|
-
|
|
686
|
+
Built-in runs are the exception: a deploy restarts the worker on the merged code, a new version, and the `apply` or `change` run that made the deploy has to outlive its version. Each run of a built-in workflow (`apply`, `change`, `drift`, `call`, `login-code`) carries a hash of that workflow's own code (its `builtInCode` attribute: what the version hashes of the workflow, with the runtime's and DBOS's versions, never the company's workflows), and a worker that starts as the app's latest version takes over the unfinished built-in runs of other versions whose hash is its own, keeping each run's id, its completed steps and the messages it has not read yet (an approval decided, an event told while no worker ran). It says so in one line at `info`. Runs of the company's workflows stay on their version; so does a built-in run whose hash differs, after an upgrade of the runtime (or, for `apply`, a change of the resource types it may call).
|
|
683
687
|
|
|
684
688
|
When a worker starts, it warns about unfinished runs it will not pick up, naming them: runs started on another version, and runs queued on another queue. Run the version that started them, or fork each onto the current version with `DBOSClient.forkWorkflow(id, step, { applicationVersion, queueName })` and cancel the original.
|
|
685
689
|
|
package/dist/bundled.d.ts
CHANGED
|
@@ -5,8 +5,8 @@ export interface RootSnapshot {
|
|
|
5
5
|
appName: string;
|
|
6
6
|
/** `ResolvedConfig.version`: a hash of the workflows' source, which a bundle's code is not, so a run it queues is its worker's. */
|
|
7
7
|
version: string;
|
|
8
|
-
/** `ResolvedConfig.
|
|
9
|
-
|
|
8
|
+
/** `ResolvedConfig.builtInCodes`, for the same reason. */
|
|
9
|
+
builtInCodes: Readonly<Record<string, string>>;
|
|
10
10
|
/** `readPolicyFiles`: each `.cedar` file under `policies/` (and an app's), by path from the root. */
|
|
11
11
|
policies: {
|
|
12
12
|
file: string;
|
package/dist/chats.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { UIMessage } from "ai";
|
|
2
|
-
import type {
|
|
2
|
+
import type { Db } from "./db.ts";
|
|
3
3
|
/** A chat as `SanomaClient.chats.list` reads it. */
|
|
4
4
|
export interface Chat {
|
|
5
5
|
id: string;
|
|
@@ -39,5 +39,5 @@ export interface Chats {
|
|
|
39
39
|
/** The person's chats, the newest first: the latest 50. */
|
|
40
40
|
list(person: string): Promise<Chat[]>;
|
|
41
41
|
}
|
|
42
|
-
/** The chats' store on the
|
|
43
|
-
export declare function chatStore(db: () => Promise<
|
|
42
|
+
/** The chats' store on the database `db` gives, the config's database once it is prepared. */
|
|
43
|
+
export declare function chatStore(db: () => Promise<Db>): Chats;
|
package/dist/chats.js
CHANGED
|
@@ -1,21 +1,25 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { asc, desc, eq, sql } from "drizzle-orm";
|
|
2
|
+
import { chatMessages, chats } from "./db/company.js";
|
|
2
3
|
import { SanomaError } from "./errors.js";
|
|
3
|
-
const
|
|
4
|
-
const chatOf = ({ draft_id, created_at, ...row }) => ({
|
|
4
|
+
const chatOf = ({ draftId, createdAt, ...row }) => ({
|
|
5
5
|
...row,
|
|
6
|
-
...(
|
|
7
|
-
createdAt:
|
|
6
|
+
...(draftId === null ? {} : { draftId }),
|
|
7
|
+
createdAt: createdAt.getTime(),
|
|
8
8
|
});
|
|
9
|
-
/** The chats' store on the
|
|
9
|
+
/** The chats' store on the database `db` gives, the config's database once it is prepared. */
|
|
10
10
|
export function chatStore(db) {
|
|
11
11
|
return {
|
|
12
12
|
async open({ id, person, engine, title }) {
|
|
13
|
-
const
|
|
14
|
-
|
|
13
|
+
const opened = await (await db())
|
|
14
|
+
.insert(chats)
|
|
15
|
+
.values({ id, person, engine, title })
|
|
16
|
+
.onConflictDoNothing()
|
|
17
|
+
.returning({ id: chats.id });
|
|
18
|
+
if (!opened.length)
|
|
15
19
|
throw new SanomaError("invalid_input", `Chat ${id} exists already; open it with another id`);
|
|
16
20
|
},
|
|
17
21
|
async attach(id, draftId) {
|
|
18
|
-
await (await db()).
|
|
22
|
+
await (await db()).update(chats).set({ draftId }).where(eq(chats.id, id));
|
|
19
23
|
},
|
|
20
24
|
async upsert(id, messages) {
|
|
21
25
|
if (!messages.length)
|
|
@@ -23,24 +27,34 @@ export function chatStore(db) {
|
|
|
23
27
|
if (messages.some((m) => !m.id))
|
|
24
28
|
throw new SanomaError("invalid_input", "A chat message needs an id");
|
|
25
29
|
// A new message's place is after the chat's last, in the order given.
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
30
|
+
const last = sql `(SELECT coalesce(max(${chatMessages.seq}), 0) FROM ${chatMessages} WHERE ${eq(chatMessages.chatId, id)})`;
|
|
31
|
+
await (await db())
|
|
32
|
+
.insert(chatMessages)
|
|
33
|
+
.values(messages.map((message, n) => ({ chatId: id, id: message.id, seq: sql `${last} + ${n + 1}`, message })))
|
|
34
|
+
.onConflictDoUpdate({
|
|
35
|
+
target: [chatMessages.chatId, chatMessages.id],
|
|
36
|
+
set: { message: sql `excluded.message` },
|
|
37
|
+
});
|
|
31
38
|
},
|
|
32
39
|
async load(id) {
|
|
33
|
-
const
|
|
40
|
+
const rows = await (await db())
|
|
41
|
+
.select({ message: chatMessages.message })
|
|
42
|
+
.from(chatMessages)
|
|
43
|
+
.where(eq(chatMessages.chatId, id))
|
|
44
|
+
.orderBy(asc(chatMessages.seq), asc(chatMessages.id));
|
|
34
45
|
return rows.map((row) => row.message);
|
|
35
46
|
},
|
|
36
47
|
async get(id) {
|
|
37
|
-
const
|
|
38
|
-
return
|
|
48
|
+
const [row] = await (await db()).select().from(chats).where(eq(chats.id, id));
|
|
49
|
+
return row && chatOf(row);
|
|
39
50
|
},
|
|
40
51
|
async list(person) {
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
52
|
+
const rows = await (await db())
|
|
53
|
+
.select()
|
|
54
|
+
.from(chats)
|
|
55
|
+
.where(eq(chats.person, person))
|
|
56
|
+
.orderBy(desc(chats.createdAt), asc(chats.id))
|
|
57
|
+
.limit(50);
|
|
44
58
|
return rows.map(chatOf);
|
|
45
59
|
},
|
|
46
60
|
};
|
package/dist/chats.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chats.js","sourceRoot":"","sources":["../src/chats.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"chats.js","sourceRoot":"","sources":["../src/chats.ts"],"names":[],"mappings":"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"}
|
package/dist/client.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Pool } from "pg";
|
|
1
2
|
import { type ConnectionRef, type ConnectionSnapshot, type ConnectionTarget, type ConnectionSetup, type ConnectionCard, type ConnectionRule } from "./connection.ts";
|
|
2
3
|
import { type WorkflowStatusString } from "@dbos-inc/dbos-sdk";
|
|
3
4
|
import { z } from "zod";
|
|
@@ -126,6 +127,12 @@ export interface ClientOptions {
|
|
|
126
127
|
* `origin`) and fetches a draft's new commits into. Without one, `remoteEvent` does nothing.
|
|
127
128
|
*/
|
|
128
129
|
source?: Source;
|
|
130
|
+
/**
|
|
131
|
+
* The version each run it starts is queued for, so only a worker of that code runs it: the app
|
|
132
|
+
* bundle's (its snapshot's), which is live beside a worker of another version while a deploy
|
|
133
|
+
* rolls. Without it a run is queued with none, which DBOS gives the app's latest version.
|
|
134
|
+
*/
|
|
135
|
+
appVersion?: string;
|
|
129
136
|
}
|
|
130
137
|
/** Talks to the runtime from another process (the app, a script), through the shared Postgres. */
|
|
131
138
|
export declare class SanomaClient {
|
|
@@ -135,7 +142,7 @@ export declare class SanomaClient {
|
|
|
135
142
|
/** The config's own ledger store, or a Postgres one made for this client, which `close` then closes. */
|
|
136
143
|
private readonly audit;
|
|
137
144
|
/** On the config's database, for the runtime's tables (the credentials, the chats): opened on first use. */
|
|
138
|
-
private readonly
|
|
145
|
+
private readonly database;
|
|
139
146
|
/** What a direct call's driver reads secrets from, as a worker's live driver does. */
|
|
140
147
|
private readonly storedSecrets;
|
|
141
148
|
/** The operator agent's chats: their rows and their transcripts, in the config's database. */
|
|
@@ -154,7 +161,8 @@ export declare class SanomaClient {
|
|
|
154
161
|
* Checks the input against the workflow's schema, then queues a run for the worker. The run
|
|
155
162
|
* gets the input as sent, not as the schema parsed it: the worker parses it once, so a schema
|
|
156
163
|
* with a `.transform` or a default sees the caller's value, and `run.started` records it.
|
|
157
|
-
* `workflow` is a definition, or the name of one in the config's `workflows`.
|
|
164
|
+
* `workflow` is a definition, or the name of one in the config's `workflows`. The run is queued
|
|
165
|
+
* for the client's `appVersion` when it has one.
|
|
158
166
|
*/
|
|
159
167
|
start<S extends z.ZodType>(workflow: WorkflowDefinition<any, S> | string, input: z.input<S>, options: StartOptions): Promise<string>;
|
|
160
168
|
/**
|
|
@@ -213,6 +221,11 @@ export declare class SanomaClient {
|
|
|
213
221
|
by: string;
|
|
214
222
|
expiresAt: Date;
|
|
215
223
|
}): Promise<void>;
|
|
224
|
+
/**
|
|
225
|
+
* Spends a sealed invite request's `nonce` (`openInvite`), good until `expiresAt`: true the first
|
|
226
|
+
* time, false when it was spent already, in this process or another.
|
|
227
|
+
*/
|
|
228
|
+
spendInviteRequest(nonce: string, expiresAt: Date): Promise<boolean>;
|
|
216
229
|
/** When each of `people` last signed in to the staff login, and how, by id (`recordSignIn`); one who never did is left out. */
|
|
217
230
|
lastSignIns(people: readonly string[]): Promise<Record<string, {
|
|
218
231
|
at: number;
|
|
@@ -451,8 +464,8 @@ export declare class SanomaClient {
|
|
|
451
464
|
* `invalid_input` when none does.
|
|
452
465
|
*/
|
|
453
466
|
private declared;
|
|
454
|
-
/** The pool the
|
|
455
|
-
|
|
467
|
+
/** The pool the client reads and writes with, once the database is prepared: what a login runs on (`LoginContext.database`). */
|
|
468
|
+
pool(): Promise<Pool>;
|
|
456
469
|
/**
|
|
457
470
|
* The principal as the data files declare it (`admit`): its id alone is taken, never the groups
|
|
458
471
|
* a caller gives, and an id nobody declares is `not_admitted`.
|
package/dist/client.js
CHANGED
|
@@ -3,6 +3,8 @@ import { encodeCredentials, decodeCredentials, verifyAuthentication, Authenticat
|
|
|
3
3
|
import { createHash, randomUUID } from "node:crypto";
|
|
4
4
|
import { isDeepStrictEqual } from "node:util";
|
|
5
5
|
import { Error as DBOSErrors } from "@dbos-inc/dbos-sdk";
|
|
6
|
+
import { is } from "drizzle-orm";
|
|
7
|
+
import { TransactionRollbackError } from "drizzle-orm/errors";
|
|
6
8
|
import { z } from "zod";
|
|
7
9
|
import { APPROVALS_EVENT, ApprovalMessage, ApprovalWithdrawal, decisionEventOf, messageKeyOf, topicOf, withdrawalRefusal, } from "./approvals.js";
|
|
8
10
|
import { beforeOf, decideCall, decideStart, recorded, startHoldOf } from "./call.js";
|
|
@@ -11,7 +13,7 @@ import { tokenStore } from "./tokens.js";
|
|
|
11
13
|
import { collectionsDriver } from "./collection.js";
|
|
12
14
|
import { resolveConfig } from "./config.js";
|
|
13
15
|
import { configuredSecrets } from "./credentials.js";
|
|
14
|
-
import { dbosClient,
|
|
16
|
+
import { dbosClient, lazyDb, spendInviteRequest } from "./db.js";
|
|
15
17
|
import { credentialsOf } from "./env.js";
|
|
16
18
|
import { Principal } from "./define.js";
|
|
17
19
|
import { APPLY_WORKFLOW } from "./apply.js";
|
|
@@ -79,7 +81,7 @@ export class SanomaClient {
|
|
|
79
81
|
/** The config's own ledger store, or a Postgres one made for this client, which `close` then closes. */
|
|
80
82
|
audit;
|
|
81
83
|
/** On the config's database, for the runtime's tables (the credentials, the chats): opened on first use. */
|
|
82
|
-
|
|
84
|
+
database;
|
|
83
85
|
/** What a direct call's driver reads secrets from, as a worker's live driver does. */
|
|
84
86
|
storedSecrets;
|
|
85
87
|
/** The operator agent's chats: their rows and their transcripts, in the config's database. */
|
|
@@ -93,10 +95,10 @@ export class SanomaClient {
|
|
|
93
95
|
this.drivers = drivers;
|
|
94
96
|
this.options = options;
|
|
95
97
|
this.audit = ownedLedger(config);
|
|
96
|
-
this.
|
|
97
|
-
this.storedSecrets = configuredSecrets(config.secrets, () => this.db());
|
|
98
|
-
this.chats = chatStore(() => this.db());
|
|
99
|
-
this.tokens = tokenStore(() => this.db());
|
|
98
|
+
this.database = lazyDb(config.databaseUrl, "client");
|
|
99
|
+
this.storedSecrets = configuredSecrets(config.secrets, () => this.database.db());
|
|
100
|
+
this.chats = chatStore(() => this.database.db());
|
|
101
|
+
this.tokens = tokenStore(() => this.database.db());
|
|
100
102
|
}
|
|
101
103
|
/**
|
|
102
104
|
* Connects to the runtime's Postgres for the config's app. Checks the config first and throws
|
|
@@ -120,7 +122,8 @@ export class SanomaClient {
|
|
|
120
122
|
* Checks the input against the workflow's schema, then queues a run for the worker. The run
|
|
121
123
|
* gets the input as sent, not as the schema parsed it: the worker parses it once, so a schema
|
|
122
124
|
* with a `.transform` or a default sees the caller's value, and `run.started` records it.
|
|
123
|
-
* `workflow` is a definition, or the name of one in the config's `workflows`.
|
|
125
|
+
* `workflow` is a definition, or the name of one in the config's `workflows`. The run is queued
|
|
126
|
+
* for the client's `appVersion` when it has one.
|
|
124
127
|
*/
|
|
125
128
|
async start(workflow, input, options) {
|
|
126
129
|
const startedBy = await this.withSubject(await this.admitted(parseOrThrow(PrincipalOrId, options?.startedBy, '`startedBy` must be a principal, such as "alice" or { id: "alice" }')));
|
|
@@ -146,11 +149,11 @@ export class SanomaClient {
|
|
|
146
149
|
const args = { input, startedBy, ...(sandbox === undefined ? {} : { sandbox }), ...(hold && { hold }) };
|
|
147
150
|
// Kept on the run's status row too, so a listing tells sandbox runs apart without its input,
|
|
148
151
|
// and says whom an agent's run is for (DBOS keeps the principal's id and groups itself); a
|
|
149
|
-
//
|
|
152
|
+
// built-in run's code, so a worker on another version with the same takes it over.
|
|
150
153
|
const attributes = {
|
|
151
154
|
sandbox,
|
|
152
155
|
onBehalfOf: startedBy.onBehalfOf,
|
|
153
|
-
|
|
156
|
+
builtInCode: this.config.builtInCodes[workflow.name],
|
|
154
157
|
};
|
|
155
158
|
this.options.onWrite?.();
|
|
156
159
|
// An id DBOS has already returns that run, unless it is another workflow's, which DBOS refuses.
|
|
@@ -160,6 +163,7 @@ export class SanomaClient {
|
|
|
160
163
|
workflowName: workflow.name,
|
|
161
164
|
workflowID: runId,
|
|
162
165
|
applicationName: this.config.appName,
|
|
166
|
+
...(this.options.appVersion !== undefined && { appVersion: this.options.appVersion }),
|
|
163
167
|
authenticatedUser: startedBy.id,
|
|
164
168
|
authenticatedRoles: startedBy.groups ?? [],
|
|
165
169
|
...(Object.values(attributes).some((v) => v !== undefined) && { attributes }),
|
|
@@ -281,66 +285,66 @@ export class SanomaClient {
|
|
|
281
285
|
const { dataStore, collections, ledger } = this.config;
|
|
282
286
|
const connectionMutation = op.id.startsWith("sanoma.connection.");
|
|
283
287
|
const transactionStore = connectionMutation
|
|
284
|
-
? this.config.
|
|
288
|
+
? this.config.connectionStore
|
|
285
289
|
: op.vendor === DATA_VENDOR
|
|
286
290
|
? dataStore
|
|
287
291
|
: undefined;
|
|
288
|
-
const
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
op: op.id,
|
|
310
|
-
effect: op.effect,
|
|
311
|
-
input: logged,
|
|
312
|
-
decision,
|
|
313
|
-
...(context.connection && { connection: context.connection }),
|
|
314
|
-
...outcome,
|
|
315
|
-
durationMs: Date.now() - at,
|
|
316
|
-
});
|
|
317
|
-
let output;
|
|
292
|
+
const at = Date.now();
|
|
293
|
+
const record = (outcome) => ({
|
|
294
|
+
v: 1,
|
|
295
|
+
app: this.config.appName,
|
|
296
|
+
id: `${runId}:op.called:0`,
|
|
297
|
+
runId,
|
|
298
|
+
seq: 0,
|
|
299
|
+
at,
|
|
300
|
+
actor,
|
|
301
|
+
workflow: CALL_WORKFLOW,
|
|
302
|
+
type: "op.called",
|
|
303
|
+
op: op.id,
|
|
304
|
+
effect: op.effect,
|
|
305
|
+
input: logged,
|
|
306
|
+
decision,
|
|
307
|
+
...(context.connection && { connection: context.connection }),
|
|
308
|
+
...outcome,
|
|
309
|
+
durationMs: Date.now() - at,
|
|
310
|
+
});
|
|
311
|
+
/** Runs the call through `fn`; a failure is recorded on the store's own connection, outside any transaction. */
|
|
312
|
+
const output = async (fn) => {
|
|
318
313
|
try {
|
|
319
|
-
|
|
314
|
+
return await outputOf(op, () => fn(parsed, context));
|
|
320
315
|
}
|
|
321
316
|
catch (err) {
|
|
322
|
-
// Recorded on the store's own connection; the transaction is rolled back below.
|
|
323
317
|
await this.audit.store.append(record({ error: errorInfo(err) }));
|
|
324
318
|
throw err;
|
|
325
319
|
}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
if (await insertRecord(conn, done)) {
|
|
332
|
-
await conn.query("COMMIT");
|
|
333
|
-
return output;
|
|
334
|
-
}
|
|
335
|
-
await conn.query("ROLLBACK");
|
|
336
|
-
return (await this.recordedCall(runId, op.id, logged, context.connection)).output;
|
|
320
|
+
};
|
|
321
|
+
if (ledger || !transactionStore) {
|
|
322
|
+
const result = await output(call.fn);
|
|
323
|
+
await this.audit.store.append(record({ output: recorded(op, result) }));
|
|
324
|
+
return result;
|
|
337
325
|
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
326
|
+
// The write and its record in one transaction: the metadata or collection driver runs on the
|
|
327
|
+
// transaction's connection (neither has an env guard), and a record another write of the id
|
|
328
|
+
// was first with rolls the write back, for the recorded output to stand.
|
|
329
|
+
const store = await transactionStore.db();
|
|
330
|
+
try {
|
|
331
|
+
return await store.transaction(async (tx) => {
|
|
332
|
+
const fn = (connectionMutation
|
|
333
|
+
? connectionDriver(this.config.connections.onDatabase(tx))
|
|
334
|
+
: collectionsDriver([collections.get(op.resource)], { db: async () => tx })).ops[`${op.resource}.${op.name}`];
|
|
335
|
+
const result = await output(fn);
|
|
336
|
+
if (await insertRecord(tx, record({ output: recorded(op, result) })))
|
|
337
|
+
return result;
|
|
338
|
+
tx.rollback();
|
|
339
|
+
});
|
|
341
340
|
}
|
|
342
|
-
|
|
343
|
-
|
|
341
|
+
catch (err) {
|
|
342
|
+
if (!is(err, TransactionRollbackError))
|
|
343
|
+
throw err;
|
|
344
|
+
const first = await this.recordedCall(runId, op.id, logged, context.connection);
|
|
345
|
+
if (!first)
|
|
346
|
+
throw err;
|
|
347
|
+
return first.output;
|
|
344
348
|
}
|
|
345
349
|
}
|
|
346
350
|
/**
|
|
@@ -413,6 +417,13 @@ export class SanomaClient {
|
|
|
413
417
|
expiresAt: options.expiresAt.toISOString(),
|
|
414
418
|
}));
|
|
415
419
|
}
|
|
420
|
+
/**
|
|
421
|
+
* Spends a sealed invite request's `nonce` (`openInvite`), good until `expiresAt`: true the first
|
|
422
|
+
* time, false when it was spent already, in this process or another.
|
|
423
|
+
*/
|
|
424
|
+
async spendInviteRequest(nonce, expiresAt) {
|
|
425
|
+
return spendInviteRequest(await this.database.db(), nonce, expiresAt);
|
|
426
|
+
}
|
|
416
427
|
/** When each of `people` last signed in to the staff login, and how, by id (`recordSignIn`); one who never did is left out. */
|
|
417
428
|
lastSignIns(people) {
|
|
418
429
|
return lastSignIns(this.audit.store, people);
|
|
@@ -1116,10 +1127,10 @@ export class SanomaClient {
|
|
|
1116
1127
|
this.config.connections.close(),
|
|
1117
1128
|
this.config.identityProvisioning.close(),
|
|
1118
1129
|
this.dbos.destroy(),
|
|
1119
|
-
this.
|
|
1130
|
+
this.database.close(),
|
|
1120
1131
|
this.audit.close(),
|
|
1121
1132
|
this.config.dataStore?.close(),
|
|
1122
|
-
this.config.
|
|
1133
|
+
this.config.connectionStore.close(),
|
|
1123
1134
|
]);
|
|
1124
1135
|
}
|
|
1125
1136
|
/**
|
|
@@ -1140,9 +1151,9 @@ export class SanomaClient {
|
|
|
1140
1151
|
issues: [{ path: ["name"], message, code: "custom" }],
|
|
1141
1152
|
});
|
|
1142
1153
|
}
|
|
1143
|
-
/** The pool the
|
|
1144
|
-
|
|
1145
|
-
return this.pool
|
|
1154
|
+
/** The pool the client reads and writes with, once the database is prepared: what a login runs on (`LoginContext.database`). */
|
|
1155
|
+
pool() {
|
|
1156
|
+
return this.database.pool();
|
|
1146
1157
|
}
|
|
1147
1158
|
/**
|
|
1148
1159
|
* The principal as the data files declare it (`admit`): its id alone is taken, never the groups
|