@sanoma-ai/workflows 0.11.12 → 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.
Files changed (142) hide show
  1. package/CLOUD-IDENTITY.md +12 -4
  2. package/KMS-SECRETS.md +3 -1
  3. package/README.md +4 -2
  4. package/dist/bundled.d.ts +2 -2
  5. package/dist/chats.d.ts +3 -3
  6. package/dist/chats.js +34 -20
  7. package/dist/chats.js.map +1 -1
  8. package/dist/client.d.ts +17 -4
  9. package/dist/client.js +74 -63
  10. package/dist/client.js.map +1 -1
  11. package/dist/cloud-identity.js +14 -12
  12. package/dist/cloud-identity.js.map +1 -1
  13. package/dist/collection.d.ts +2 -2
  14. package/dist/collection.js +53 -42
  15. package/dist/collection.js.map +1 -1
  16. package/dist/config.d.ts +11 -6
  17. package/dist/config.js +15 -13
  18. package/dist/config.js.map +1 -1
  19. package/dist/connection.d.ts +2 -0
  20. package/dist/connection.js +112 -40
  21. package/dist/connection.js.map +1 -1
  22. package/dist/credentials.d.ts +1 -1
  23. package/dist/credentials.js +33 -29
  24. package/dist/credentials.js.map +1 -1
  25. package/dist/db/company.d.ts +1152 -0
  26. package/dist/db/company.js +118 -0
  27. package/dist/db/company.js.map +1 -0
  28. package/dist/db/dbos.d.ts +111 -0
  29. package/dist/db/dbos.js +15 -0
  30. package/dist/db/dbos.js.map +1 -0
  31. package/dist/db/identity.d.ts +367 -0
  32. package/dist/db/identity.js +48 -0
  33. package/dist/db/identity.js.map +1 -0
  34. package/dist/db/index.d.ts +6 -0
  35. package/dist/db/index.js +10 -0
  36. package/dist/db/index.js.map +1 -0
  37. package/dist/db/kms.d.ts +224 -0
  38. package/dist/db/kms.js +29 -0
  39. package/dist/db/kms.js.map +1 -0
  40. package/dist/db/migrate.d.ts +63 -0
  41. package/dist/db/migrate.js +41 -0
  42. package/dist/db/migrate.js.map +1 -0
  43. package/dist/db/wakeup.d.ts +271 -0
  44. package/dist/db/wakeup.js +34 -0
  45. package/dist/db/wakeup.js.map +1 -0
  46. package/dist/db.d.ts +42 -39
  47. package/dist/db.js +129 -169
  48. package/dist/db.js.map +1 -1
  49. package/dist/deadlines.js +8 -4
  50. package/dist/deadlines.js.map +1 -1
  51. package/dist/deployment-key.d.ts +4 -1
  52. package/dist/deployment-key.js +10 -3
  53. package/dist/deployment-key.js.map +1 -1
  54. package/dist/host-derive.d.ts +12 -3
  55. package/dist/host-derive.js +21 -8
  56. package/dist/host-derive.js.map +1 -1
  57. package/dist/identity-provisioning.d.ts +2 -2
  58. package/dist/identity-provisioning.js +75 -42
  59. package/dist/identity-provisioning.js.map +1 -1
  60. package/dist/identity-service.js +18 -15
  61. package/dist/identity-service.js.map +1 -1
  62. package/dist/index.d.ts +2 -2
  63. package/dist/index.js +1 -1
  64. package/dist/index.js.map +1 -1
  65. package/dist/kms-secrets.d.ts +9 -0
  66. package/dist/kms-secrets.js +59 -27
  67. package/dist/kms-secrets.js.map +1 -1
  68. package/dist/ledger.d.ts +1 -1
  69. package/dist/ledger.js +22 -14
  70. package/dist/ledger.js.map +1 -1
  71. package/dist/op.d.ts +27 -6
  72. package/dist/op.js.map +1 -1
  73. package/dist/run-status.js +7 -3
  74. package/dist/run-status.js.map +1 -1
  75. package/dist/schedules.d.ts +1 -1
  76. package/dist/schedules.js +5 -7
  77. package/dist/schedules.js.map +1 -1
  78. package/dist/schema.d.ts +6 -5
  79. package/dist/schema.js +11 -7
  80. package/dist/schema.js.map +1 -1
  81. package/dist/shared.d.ts +6 -23
  82. package/dist/shared.js +6 -5
  83. package/dist/shared.js.map +1 -1
  84. package/dist/storage-provisioning.d.ts +2 -0
  85. package/dist/storage-provisioning.js +51 -50
  86. package/dist/storage-provisioning.js.map +1 -1
  87. package/dist/tokens.d.ts +2 -2
  88. package/dist/tokens.js +31 -20
  89. package/dist/tokens.js.map +1 -1
  90. package/dist/version.d.ts +4 -4
  91. package/dist/version.js +5 -5
  92. package/dist/version.js.map +1 -1
  93. package/dist/wakeup-service.d.ts +0 -4
  94. package/dist/wakeup-service.js +102 -93
  95. package/dist/wakeup-service.js.map +1 -1
  96. package/dist/worker.d.ts +7 -0
  97. package/dist/worker.js +26 -21
  98. package/dist/worker.js.map +1 -1
  99. package/drizzle/company/0000_tables.sql +104 -0
  100. package/drizzle/company/0001_wakeup_row.sql +2 -0
  101. package/drizzle/company/meta/0000_snapshot.json +652 -0
  102. package/drizzle/company/meta/0001_snapshot.json +652 -0
  103. package/drizzle/company/meta/_journal.json +20 -0
  104. package/drizzle/wakeup/0000_tables.sql +26 -0
  105. package/drizzle/wakeup/meta/0000_snapshot.json +200 -0
  106. package/drizzle/wakeup/meta/_journal.json +13 -0
  107. package/oxlint.json +2 -1
  108. package/package.json +11 -2
  109. package/src/bundled.ts +2 -2
  110. package/src/chats.ts +41 -42
  111. package/src/client.ts +81 -64
  112. package/src/cloud-identity.ts +23 -24
  113. package/src/collection.ts +58 -50
  114. package/src/config.ts +29 -19
  115. package/src/connection.ts +132 -102
  116. package/src/credentials.ts +37 -33
  117. package/src/db/company.ts +173 -0
  118. package/src/db/dbos.ts +17 -0
  119. package/src/db/identity.ts +61 -0
  120. package/src/db/index.ts +10 -0
  121. package/src/db/kms.ts +41 -0
  122. package/src/db/migrate.ts +70 -0
  123. package/src/db/wakeup.ts +41 -0
  124. package/src/db.ts +151 -182
  125. package/src/deadlines.ts +10 -4
  126. package/src/deployment-key.ts +11 -3
  127. package/src/host-derive.ts +26 -9
  128. package/src/identity-provisioning.ts +85 -57
  129. package/src/identity-service.ts +19 -19
  130. package/src/index.ts +3 -0
  131. package/src/kms-secrets.ts +64 -56
  132. package/src/ledger.ts +23 -18
  133. package/src/op.ts +28 -6
  134. package/src/run-status.ts +7 -6
  135. package/src/schedules.ts +6 -8
  136. package/src/schema.ts +11 -8
  137. package/src/shared.ts +6 -17
  138. package/src/storage-provisioning.ts +47 -52
  139. package/src/tokens.ts +43 -33
  140. package/src/version.ts +5 -5
  141. package/src/wakeup-service.ts +117 -131
  142. package/src/worker.ts +38 -21
@@ -0,0 +1,61 @@
1
+ import { sql } from "drizzle-orm";
2
+ import { bigint, boolean, check, pgSchema, text, timestamp, unique, uuid } from "drizzle-orm/pg-core";
3
+
4
+ // The cloud identity broker's tables, in the schema `sanoma_identity` an administrator installs
5
+ // with `CLOUD_IDENTITY_SQL` (`cloud-identity.ts`) and `CLOUD_IDENTITY_AUDIT_SQL`
6
+ // (`identity-service.ts`), with their grants: the runtime never makes them, so no migration is
7
+ // generated from this model. `installed-schemas.test.ts` holds the install SQL to it.
8
+
9
+ export const identity = pgSchema("sanoma_identity");
10
+
11
+ const when = () => timestamp({ withTimezone: true });
12
+
13
+ /** Each enrolled deployment and the service account it may act as. */
14
+ export const identityDeployments = identity.table("deployments", {
15
+ id: text().primaryKey(),
16
+ company: text().notNull(),
17
+ environment: text().notNull(),
18
+ serviceAccount: text().notNull(),
19
+ enabled: boolean().notNull().default(true),
20
+ });
21
+
22
+ /** Each deployment credential's hash, never the credential, with its generation and validity. */
23
+ export const identityCredentials = identity.table(
24
+ "credentials",
25
+ {
26
+ hash: text().primaryKey(),
27
+ deployment: text()
28
+ .notNull()
29
+ .references(() => identityDeployments.id),
30
+ generation: bigint({ mode: "number" }).notNull(),
31
+ validFrom: when().notNull().defaultNow(),
32
+ validUntil: when().notNull(),
33
+ revoked: boolean().notNull().default(false),
34
+ },
35
+ (t) => [
36
+ unique("credentials_deployment_generation").on(t.deployment, t.generation),
37
+ check("credentials_hash", sql`${t.hash} ~ '^[0-9a-f]{64}$'`),
38
+ check("credentials_generation", sql`${t.generation} > 0`),
39
+ check("credentials_validity", sql`${t.validUntil} > ${t.validFrom}`),
40
+ check("credentials_finite", sql`isfinite(${t.validFrom}) AND isfinite(${t.validUntil})`),
41
+ ],
42
+ );
43
+
44
+ /** Each issuance decision, without token values. */
45
+ export const identityAuditEvents = identity.table(
46
+ "audit_events",
47
+ {
48
+ id: uuid().primaryKey().defaultRandom(),
49
+ recordedAt: when()
50
+ .notNull()
51
+ .default(sql`clock_timestamp()`),
52
+ result: text().$type<"issued" | "denied" | "unavailable">().notNull(),
53
+ deployment: text(),
54
+ company: text(),
55
+ environment: text(),
56
+ serviceAccount: text(),
57
+ generation: bigint({ mode: "number" }),
58
+ expiresAt: when(),
59
+ },
60
+ (t) => [check("audit_events_result", sql`${t.result} IN ('issued', 'denied', 'unavailable')`)],
61
+ );
@@ -0,0 +1,10 @@
1
+ // `@sanoma-ai/workflows/db`, server-only: the runtime's tables and the database they are read
2
+ // with, for the packages that share them (the connectors' stores, the app's tests). Never a
3
+ // workflow's import: the shipped `oxlint.json` refuses it, as it does `/wakeup-service`.
4
+
5
+ export { type Db, databaseOn, pgCode } from "../db.ts";
6
+ export * from "./company.ts";
7
+ export * from "./dbos.ts";
8
+ export * from "./identity.ts";
9
+ export * from "./kms.ts";
10
+ export * from "./wakeup.ts";
package/src/db/kms.ts ADDED
@@ -0,0 +1,41 @@
1
+ import { sql } from "drizzle-orm";
2
+ import { boolean, check, jsonb, pgSchema, primaryKey, text, timestamp, uuid } from "drizzle-orm/pg-core";
3
+ import type { Envelope } from "../kms-secrets.ts";
4
+
5
+ // The KMS secret store's tables, in the schema `sanoma_kms` an administrator installs with
6
+ // `KMS_SECRETS_SQL` (`kms-secrets.ts`) and `sanoma.secretStore.prepare` (`storage-provisioning.ts`),
7
+ // with their grants: the runtime never makes them, so no migration is generated from this model.
8
+ // `installed-schemas.test.ts` holds the install SQL to it.
9
+
10
+ export const kms = pgSchema("sanoma_kms");
11
+
12
+ /** Each secret's envelope, by company, namespace and path. */
13
+ export const kmsSecrets = kms.table(
14
+ "secrets",
15
+ {
16
+ company: text().notNull(),
17
+ namespace: text().notNull(),
18
+ path: text().notNull(),
19
+ version: uuid().notNull(),
20
+ envelope: jsonb().$type<Envelope>().notNull(),
21
+ updatedBy: text().notNull(),
22
+ updatedAt: timestamp({ withTimezone: true })
23
+ .notNull()
24
+ .defaultNow()
25
+ .$onUpdate(() => sql`now()`),
26
+ },
27
+ (t) => [primaryKey({ columns: [t.company, t.namespace, t.path] })],
28
+ );
29
+
30
+ /** The one row binding the database to its company and environment, and the runtime role's credential hash. */
31
+ export const kmsDeployment = kms.table(
32
+ "deployment",
33
+ {
34
+ singleton: boolean().primaryKey().default(true),
35
+ company: text().notNull(),
36
+ environment: text().notNull(),
37
+ runtimeRole: text().notNull(),
38
+ credentialHash: text().notNull(),
39
+ },
40
+ (t) => [check("deployment_singleton", sql`${t.singleton}`)],
41
+ );
@@ -0,0 +1,70 @@
1
+ import { fileURLToPath } from "node:url";
2
+ import { sql } from "drizzle-orm";
3
+ import { migrate } from "drizzle-orm/node-postgres/migrator";
4
+ import type { Db } from "../db.ts";
5
+ import companyJournal from "../../drizzle/company/meta/_journal.json" with { type: "json" };
6
+ import wakeupJournal from "../../drizzle/wakeup/meta/_journal.json" with { type: "json" };
7
+
8
+ // The generated migrations (`pnpm --filter @sanoma-ai/workflows generate`), one folder per
9
+ // database the runtime makes tables in, each with its own journal table in the schema `drizzle`,
10
+ // since the wakeup service's database may be a company's own. The Worker, which never migrates,
11
+ // checks a database against the journal shipped with the package (`migrationsApplied`), with no
12
+ // file read: the journal is imported.
13
+
14
+ /** Drizzle's journal, `meta/_journal.json` of a migrations folder. */
15
+ export interface MigrationsJournal {
16
+ entries: { idx: number; when: number; tag: string }[];
17
+ }
18
+
19
+ /** A migrations folder with the journal table recording what of it a database has. */
20
+ export interface Migrations {
21
+ /** The folder's name under the package's `drizzle/`. */
22
+ name: string;
23
+ /** Its journal table in the schema `drizzle`. */
24
+ table: string;
25
+ journal: MigrationsJournal;
26
+ }
27
+
28
+ /** The folder of `of`, from `src/` and from `dist/` alike; read only when migrating, never in the Worker. */
29
+ const folderOf = (of: Migrations): string => fileURLToPath(new URL(`../../drizzle/${of.name}/`, import.meta.url));
30
+
31
+ /** The tables every company database gets (`db/company.ts`). */
32
+ export const COMPANY_MIGRATIONS = {
33
+ name: "company",
34
+ table: "__drizzle_migrations",
35
+ journal: companyJournal,
36
+ } satisfies Migrations;
37
+
38
+ /** The wakeup service's tables (`db/wakeup.ts`). */
39
+ export const WAKEUP_MIGRATIONS = {
40
+ name: "wakeup",
41
+ table: "__drizzle_migrations_wakeup",
42
+ journal: wakeupJournal,
43
+ } satisfies Migrations;
44
+
45
+ /**
46
+ * Applies to `db` the migrations of `of` its database has not had, in one transaction, recording
47
+ * each in the journal table. Drizzle's migrator compares only the newest timestamp, so a migration
48
+ * older than the newest applied is skipped: `scripts/check-journal.ts` keeps a branch from adding
49
+ * one. Two processes migrating one database at once race (the migrator takes no lock): callers
50
+ * hold `underPreparationLock`.
51
+ */
52
+ export const applyMigrations = (db: Db, of: Migrations): Promise<void> =>
53
+ migrate(db, { migrationsFolder: folderOf(of), migrationsTable: of.table });
54
+
55
+ /**
56
+ * Whether the database `db` is on has had every migration of `of`: each journal entry's `when`
57
+ * is a `created_at` of its journal table, whatever else the table holds (a database a newer
58
+ * release migrated serves this one, as after a rollback). False with no journal table.
59
+ */
60
+ export async function migrationsApplied(db: Db, of: Migrations): Promise<boolean> {
61
+ const present = await db.execute<{ present: boolean }>(
62
+ sql`SELECT to_regclass(${`drizzle.${of.table}`}) IS NOT NULL AS present`,
63
+ );
64
+ if (present.rows[0]?.present !== true) return false;
65
+ const { rows } = await db.execute<{ at: string }>(
66
+ sql`SELECT created_at::text AS at FROM drizzle.${sql.identifier(of.table)}`,
67
+ );
68
+ const applied = new Set(rows.map((row) => Number(row.at)));
69
+ return of.journal.entries.every((entry) => applied.has(entry.when));
70
+ }
@@ -0,0 +1,41 @@
1
+ import { sql } from "drizzle-orm";
2
+ import { index, integer, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core";
3
+
4
+ // The wakeup service's tables (`wakeup-service.ts`), in the service's own database: never a
5
+ // company's, which every company database gets. Migrations under `drizzle/wakeup/`.
6
+
7
+ const when = () => timestamp({ withTimezone: true });
8
+
9
+ /** One account per party that registers instances, its token's hash alone. */
10
+ export const wakeupAccounts = pgTable("sanoma_wakeup_accounts", {
11
+ id: uuid().primaryKey(),
12
+ tokenHash: text().notNull().unique(),
13
+ label: text().notNull(),
14
+ createdAt: when().notNull().defaultNow(),
15
+ revokedAt: when(),
16
+ });
17
+
18
+ /** Each registered instance: its URL, its token's hash and its registered wake. */
19
+ export const wakeupInstances = pgTable(
20
+ "sanoma_wakeup_instances",
21
+ {
22
+ id: uuid().primaryKey(),
23
+ accountId: uuid()
24
+ .notNull()
25
+ .references(() => wakeupAccounts.id),
26
+ url: text().notNull(),
27
+ tokenHash: text().notNull().unique(),
28
+ at: when(),
29
+ calledAt: when(),
30
+ attempts: integer().notNull().default(0),
31
+ nextAttemptAt: when(),
32
+ lastError: text(),
33
+ createdAt: when().notNull().defaultNow(),
34
+ },
35
+ (t) => [
36
+ index("sanoma_wakeup_instances_at")
37
+ .on(t.at)
38
+ .where(sql`${t.at} IS NOT NULL`),
39
+ index("sanoma_wakeup_instances_account").on(t.accountId),
40
+ ],
41
+ );