@sanoma-ai/workflows 0.10.0 → 0.10.2

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 (99) hide show
  1. package/CLOUD-IDENTITY.md +140 -0
  2. package/KMS-SECRETS.md +99 -0
  3. package/README.md +39 -7
  4. package/SECRET-SUPPORT.md +33 -0
  5. package/dist/apps.d.ts +6 -0
  6. package/dist/apps.js +18 -1
  7. package/dist/apps.js.map +1 -1
  8. package/dist/authentication.d.ts +35 -0
  9. package/dist/authentication.js +71 -1
  10. package/dist/authentication.js.map +1 -1
  11. package/dist/call.js +7 -5
  12. package/dist/call.js.map +1 -1
  13. package/dist/client.d.ts +54 -6
  14. package/dist/client.js +177 -5
  15. package/dist/client.js.map +1 -1
  16. package/dist/cloud-identity.d.ts +52 -0
  17. package/dist/cloud-identity.js +222 -0
  18. package/dist/cloud-identity.js.map +1 -0
  19. package/dist/config.d.ts +7 -4
  20. package/dist/config.js +44 -12
  21. package/dist/config.js.map +1 -1
  22. package/dist/connection.d.ts +223 -2
  23. package/dist/connection.js +340 -4
  24. package/dist/connection.js.map +1 -1
  25. package/dist/db.js +15 -2
  26. package/dist/db.js.map +1 -1
  27. package/dist/describe.d.ts +8 -0
  28. package/dist/describe.js +51 -1
  29. package/dist/describe.js.map +1 -1
  30. package/dist/direct.js +4 -0
  31. package/dist/direct.js.map +1 -1
  32. package/dist/fake.d.ts +13 -1
  33. package/dist/fake.js +70 -0
  34. package/dist/fake.js.map +1 -1
  35. package/dist/index.d.ts +4 -4
  36. package/dist/index.js +2 -2
  37. package/dist/index.js.map +1 -1
  38. package/dist/kms-secrets.d.ts +24 -0
  39. package/dist/kms-secrets.js +209 -0
  40. package/dist/kms-secrets.js.map +1 -0
  41. package/dist/lint.js +1 -1
  42. package/dist/lint.js.map +1 -1
  43. package/dist/load.d.ts +11 -0
  44. package/dist/load.js +42 -8
  45. package/dist/load.js.map +1 -1
  46. package/dist/op.d.ts +14 -6
  47. package/dist/op.js +20 -2
  48. package/dist/op.js.map +1 -1
  49. package/dist/outline.d.ts +5 -0
  50. package/dist/outline.js +33 -1
  51. package/dist/outline.js.map +1 -1
  52. package/dist/paths.d.ts +2 -4
  53. package/dist/paths.js +2 -8
  54. package/dist/paths.js.map +1 -1
  55. package/dist/run.d.ts +3 -1
  56. package/dist/run.js.map +1 -1
  57. package/dist/sandbox.d.ts +4 -1
  58. package/dist/sandbox.js +51 -5
  59. package/dist/sandbox.js.map +1 -1
  60. package/dist/scenario.d.ts +5 -0
  61. package/dist/scenario.js +3 -2
  62. package/dist/scenario.js.map +1 -1
  63. package/dist/secret.d.ts +2 -0
  64. package/dist/secret.js +32 -0
  65. package/dist/secret.js.map +1 -1
  66. package/dist/shared.d.ts +4 -0
  67. package/dist/shared.js +7 -0
  68. package/dist/shared.js.map +1 -1
  69. package/dist/sites.js +18 -3
  70. package/dist/sites.js.map +1 -1
  71. package/dist/steps.d.ts +3 -0
  72. package/dist/steps.js.map +1 -1
  73. package/oxlint.json +3 -0
  74. package/package.json +16 -2
  75. package/src/apps.ts +17 -1
  76. package/src/authentication.ts +126 -2
  77. package/src/call.ts +4 -5
  78. package/src/client.ts +242 -6
  79. package/src/cloud-identity.ts +266 -0
  80. package/src/config.ts +45 -11
  81. package/src/connection.ts +478 -6
  82. package/src/db.ts +15 -2
  83. package/src/describe.ts +52 -1
  84. package/src/direct.ts +4 -0
  85. package/src/fake.ts +71 -0
  86. package/src/index.ts +13 -1
  87. package/src/kms-secrets.ts +271 -0
  88. package/src/lint.ts +1 -1
  89. package/src/load.ts +36 -7
  90. package/src/op.ts +33 -8
  91. package/src/outline.ts +35 -2
  92. package/src/paths.ts +2 -9
  93. package/src/run.ts +3 -1
  94. package/src/sandbox.ts +55 -5
  95. package/src/scenario.ts +24 -6
  96. package/src/secret.ts +29 -0
  97. package/src/shared.ts +9 -0
  98. package/src/sites.ts +19 -3
  99. package/src/steps.ts +3 -2
@@ -0,0 +1,140 @@
1
+ # Managed Cloud runtime identity
2
+
3
+ `@sanoma-ai/workflows/cloud-identity` supplies the identity-broker handler and refreshable Google SDK authentication for [D1, #492](https://github.com/Sanoma-AI/sanoma/issues/492). It is a reusable implementation boundary. Isolated live checks on a temporary Cloud Run service in `sanoma-513217` have verified attached-identity issuance, scoped KMS use and separate database permissions; production deployment remains open.
4
+
5
+ This runtime route is separate from the provisioning OIDC identity in [#489](https://github.com/Sanoma-AI/sanoma/issues/489). Provisioning manages enrollment and infrastructure. The company runtime receives only its deployment's bootstrap credential and short-lived access tokens. The broker issues identity; it does not read company credential rows, decrypt credentials or unwrap data keys.
6
+
7
+ ## Request and authority
8
+
9
+ ```mermaid
10
+ flowchart LR
11
+ runtime[Company runtime] -->|POST /token: deployment bearer| broker[Trusted Cloud Run broker]
12
+ broker -->|Hash lookup and fixed mapping| enrollments[(Broker enrollment database)]
13
+ broker -->|Impersonate mapped service account| iam[Google IAM Credentials]
14
+ iam -->|600-second OAuth access token| broker
15
+ broker -->|Token and expiry| runtime
16
+ runtime -->|Company-scoped identity| kms[Company/environment CryptoKey]
17
+ runtime -->|Company-scoped database login| db[(Company Postgres ciphertext)]
18
+ ```
19
+
20
+ `identityBroker` accepts **POST `/token`**, with `Authorization: Bearer <deployment credential>`, no query string and no request body. Do not send `{}`. The request cannot select company, service account, key, scope or token lifetime. Wrong routes/methods return 404, a query/body returns 400, invalid enrollment returns 401, and dependency/audit failures return a sanitized 503. Responses prohibit caching.
21
+
22
+ The broker hashes the bearer, loads its active enrollment, issues for the mapped service account, then checks the enrollment again before delivery. `postgresIdentityEnrollments` reads on each check; it does not cache enrollment authority. Audit must durably record issuance/denial/unavailability metadata, never tokens or authorization headers. Failure to record a successful issuance prevents delivery, even if Google already minted the token.
23
+
24
+ `attachedGoogleTokenIssuer` explicitly uses the Cloud Run attached identity through `google-auth-library`'s `Compute` client. It requests `generateAccessToken` for the enrollment's service account, with the fixed `cloud-platform` scope and `600s` lifetime. It returns an OAuth API access token, not an OIDC ID token. No service-account private key or ambient credential-file fallback is used. The attached account needs `iam.serviceAccounts.getAccessToken` on the intended target accounts; this permission is documented by [Google IAM Credentials](https://docs.cloud.google.com/iam/docs/reference/credentials/rest/v1/projects.serviceAccounts/generateAccessToken).
25
+
26
+ The broker's attached identity must have no company database access, direct KMS decrypt grant or IAM-policy mutation authority. Each target service account must have encrypt/decrypt access only to its company/environment key, with no inherited fleet grants or impersonation authority. These are deployment requirements; constructing the handler does not establish them.
27
+
28
+ ## Enrollment database and role separation
29
+
30
+ Install `CLOUD_IDENTITY_SQL` with an administrator in a **broker-owned database**, never a company database. It creates `sanoma_identity.deployments` (deployment → company/environment/service account and enabled state) and `sanoma_identity.credentials` (SHA-256 hash, deployment, positive generation, validity interval and revoked state). It revokes public schema/table access. It does not create logins or grant broker/provisioner privileges.
31
+
32
+ Example roles, to adapt and install with administrative authority:
33
+
34
+ ```sql
35
+ -- Synthetic role names; supply login authentication through protected deployment setup.
36
+ CREATE ROLE identity_broker_reader NOLOGIN;
37
+ CREATE ROLE identity_enrollment_writer NOLOGIN;
38
+ GRANT USAGE ON SCHEMA sanoma_identity
39
+ TO identity_broker_reader, identity_enrollment_writer;
40
+ GRANT SELECT ON sanoma_identity.deployments, sanoma_identity.credentials
41
+ TO identity_broker_reader;
42
+ GRANT SELECT, INSERT, UPDATE, DELETE
43
+ ON sanoma_identity.deployments, sanoma_identity.credentials
44
+ TO identity_enrollment_writer;
45
+ -- Separate pre-created logins, neither superuser nor schema/table owner:
46
+ GRANT identity_broker_reader TO example_broker_login;
47
+ GRANT identity_enrollment_writer TO example_provisioner_login;
48
+ ```
49
+
50
+ The broker login must have no writer-role membership, inherited write grants or authority to grant itself privileges. The provisioning login stays outside company runtimes and outside the broker's serving process. Database connection/authentication is separate bootstrap material; it cannot be fetched from the same service it unlocks.
51
+
52
+ Trusted provisioning calls `createDeploymentCredential()` to generate 32 random bytes encoded as base64url plus its SHA-256 hash. Deliver the credential once through that deployment's protected secret channel; persist only its hash. Enrollment writes should be parameterized. For example, with synthetic deployment identifiers and the generated hash supplied as `$1`:
53
+
54
+ ```sql
55
+ BEGIN;
56
+ INSERT INTO sanoma_identity.deployments
57
+ (id, company, environment, service_account)
58
+ VALUES ('example-deployment', 'example-company', 'production',
59
+ 'runtime@example-project.iam.gserviceaccount.com');
60
+ INSERT INTO sanoma_identity.credentials
61
+ (hash, deployment, generation, valid_from, valid_until)
62
+ VALUES ($1, 'example-deployment', 1, clock_timestamp(),
63
+ clock_timestamp() + interval '1 day');
64
+ COMMIT;
65
+ ```
66
+
67
+ The interval is illustrative. The SQL schema requires finite timestamps and checks that `valid_until` follows `valid_from`; provisioning must enforce its maximum validity and operational renewal policy. There is no HTTP enrollment endpoint.
68
+
69
+ For rotation, provision a new random credential under a higher unused generation, deliver it to the deployment, and set the old generation's `valid_until` to a bounded overlap deadline. Revoke a generation with `revoked = true`, or disable the deployment to prevent renewal for all its generations. Do not repoint an existing deployment to another company's service account as a credential-rotation shortcut. Restore enrollment backups carefully so revoked credentials do not become active again.
70
+
71
+ Broker composition uses its read-only database pool and a separate durable audit sink:
72
+
73
+ ```ts
74
+ import {
75
+ identityBroker,
76
+ postgresIdentityEnrollments,
77
+ attachedGoogleTokenIssuer,
78
+ } from "@sanoma-ai/workflows/cloud-identity";
79
+
80
+ const handle = identityBroker({
81
+ enrollments: postgresIdentityEnrollments(async () => brokerReadOnlyPool),
82
+ issue: attachedGoogleTokenIssuer(),
83
+ audit: writeDurableIdentityAudit,
84
+ });
85
+ // The deployment's HTTP server passes the incoming Request to handle.
86
+ // Pool, audit sink and server lifecycle belong to trusted deployment composition.
87
+ ```
88
+
89
+ ## Runtime refresh and KMS integration
90
+
91
+ Create one `cloudIdentityAuth` client per runtime deployment. It returns an `OAuth2Client` whose refresh handler coalesces concurrent renewal requests and renews 60 seconds before expiry. Each renewal calls the bootstrap reader again, so mounted-credential rotation can take effect without caching the old bearer. Tokens remain only in that client's memory. The endpoint must be HTTPS `/token`, with no embedded credentials, query or fragment; redirects are refused. Broker calls time out after ten seconds, malformed/near-expiry replies fail, and errors expose no response body or credential. There is no fallback identity.
92
+
93
+ The following composition joins D1 to the separate [#470 KMS adapter](https://github.com/Sanoma-AI/sanoma/issues/470); it requires both entries to be available in the installed package. Company database/key provisioning remains separate:
94
+
95
+ ```ts
96
+ import { readFile } from "node:fs/promises";
97
+ import { KeyManagementServiceClient } from "@google-cloud/kms";
98
+ import { cloudIdentityAuth } from "@sanoma-ai/workflows/cloud-identity";
99
+ import { kmsPostgresStore } from "@sanoma-ai/workflows/kms-secrets";
100
+
101
+ const authClient = cloudIdentityAuth({
102
+ endpoint: "https://identity.example.test/token",
103
+ credential: async () => (await readFile("/run/sanoma/deployment-credential", "utf8")).trim(),
104
+ });
105
+ const kms = new KeyManagementServiceClient({ authClient });
106
+ const store = kmsPostgresStore({
107
+ company: "example-company", // Fixed by trusted bootstrap.
108
+ keyName: "projects/example-project/locations/us-central1/keyRings/production/cryptoKeys/example-company",
109
+ kms,
110
+ database: async () => companyScopedPool,
111
+ });
112
+ // Supply store to defineConfig's secrets configuration.
113
+ // The deployment owns kms.close() and the database pool lifecycle.
114
+ ```
115
+
116
+ Credential ciphertext stays in the company's Postgres database. Caching the short-lived Google token does not authorize caching decrypted credential bundles or unwrapped data keys. App signing material remains independent. Neither this route nor the KMS adapter needs `SANOMA_SECRETS_KEY`.
117
+
118
+ ## Isolated live verification
119
+
120
+ The verification used D1 source `244fb3e78c66c5d3d6bb22cacad73acbb1726cb3` and KMS source `c3c293d0bc48cbcf66adf8e0cb0c57f500e686bc`, with a temporary Cloud Run broker and separate disposable remote Postgres databases. The broker image contained the compiled handler and a small HTTP adapter; its attached service account had only `iam.serviceAccounts.getAccessToken` on three test runtime accounts. It had no company database credentials or direct KMS grant. Database enrollment used a read-only login and audit writes used a separate insert-only login.
121
+
122
+ The actual Google SDK and KMS store passed own-key encrypted writes/reads, company A/B and production/preview encrypt/decrypt denial, separate company-database access checks, enrollment-write refusal, invalid bearer and caller-target override refusal. Revoking the broker's enrollment-read or audit-insert permission caused a sanitized 503 without a delivered token; restoring the permission restored service. These are permission-failure checks, not a measurement of provider-wide outages.
123
+
124
+ A manual primary-version rotation kept old ciphertext readable and new writes adopted the new primary after propagation. Revoking a deployment credential rejected new issuance while the SDK's already issued token continued working until renewal. The ten-minute runtime renewal result is tracked in [#492](https://github.com/Sanoma-AI/sanoma/issues/492), separately from [provisioning OIDC verification](../../connectors/gcp/README.md#testing-and-current-evidence).
125
+
126
+ This proof uses test infrastructure and compiled source, not published-package consumption in sanoma-ops. Cloudflare cold start/resume, bootstrap delivery, production monitoring/quotas, IAM/KMS audit reconciliation and backup recovery remain rollout gates.
127
+
128
+ ## Residual authority and rollout gates
129
+
130
+ Possession of the deployment bearer proves possession, not host attestation. A stolen bearer can renew that deployment's tokens while its enrollment remains active. Revoking enrollment stops subsequent renewal checks; **already issued tokens can remain usable until expiry**. IAM changes have their own propagation behavior, and neither revocation nor expiry recalls plaintext already obtained.
131
+
132
+ A compromised broker can impersonate every service account its attached identity may mint for. Per-company keys do not remove that fleet-level issuer risk. A compromised enrollment writer can change mappings within the broker's permitted targets. Keep both outside company-authored code and public company Workers; give each company runtime only its own bearer, database authority and key access.
133
+
134
+ Before rollout:
135
+
136
+ - Provision the Cloud Run service, attached issuer account, broker database roles, durable audit sink and per-company/environment keys and service accounts. Enable billing and required APIs. The temporary verification deployment below does not provision a production service.
137
+ - Make the TLS endpoint reachable without first demanding a Google invocation token; the application bearer authenticates it. This requires an explicit [Cloud Run invocation configuration](https://docs.cloud.google.com/run/docs/authenticating/public), plus appropriate request limits and quota monitoring. Do not add this endpoint to a company Worker that holds fleet authority.
138
+ - Deliver each bootstrap credential only to its intended deployment. For Cloudflare, explicitly pass its protected Worker binding to that company's container. Prove cold start, resume, refresh through token expiry, bounded credential rotation, revoked/expired generations and broker/audit outages.
139
+ - Prove company A's bearer cannot select B via body/query, A's token cannot use B's key, A's database login cannot read B's rows, and preview cannot access production. Synthetic handler tests are insufficient evidence for Google IAM or database isolation.
140
+ - Verify issuance and KMS audit attribution, credential/header redaction, service quotas, backup recovery and retained key versions. Record the actual deployment evidence before declaring D1 or managed Cloud ready.
package/KMS-SECRETS.md ADDED
@@ -0,0 +1,99 @@
1
+ # GCP KMS-protected credential storage
2
+
3
+ `@sanoma-ai/workflows/kms-secrets` implements the proposed managed Cloud store for #470. Encrypted credentials live in **Postgres**, in `sanoma_kms.secrets` in the company database supplied by trusted bootstrap. Supabase can host that Postgres database; no separate key-value database is needed. GCP KMS protects encryption keys, not the credential records. BYOC Supabase Vault is a separate adapter. AWS Secrets Manager remains deferred.
4
+
5
+ ## Runtime boundary
6
+
7
+ One company runs in one process with its own database authority and **GCP identity permitted to use only that company's CryptoKeys**. Trusted deployment bootstrap supplies the fixed company ID, CryptoKey resource name, SDK client and database connection. Never give company code fleet-wide decrypt authority or an administrator database connection. A company's code and installed connectors can use its authorized credentials; encryption does not protect those credentials from its own compromised runtime.
8
+
9
+ The proposed default is one symmetric SOFTWARE CryptoKey per company and environment, with keys grouped in a shared environment key ring/project. HSM protection is an independent provisioning choice, with different pricing; per-company software keys do not imply HSM protection. Separate production and test identities, keys and databases.
10
+
11
+ This adapter is not a production Cloud deployment. D1 implementation is tracked in [#492](https://github.com/Sanoma-AI/sanoma/issues/492) and [PR #496](https://github.com/Sanoma-AI/sanoma/pull/496). Isolated live IAM tests have passed; intended-host deployment and rollout checks remain separate. A Cloudflare deployment does not automatically have a Google identity. Workload Identity Federation or another approved refreshable credential route must bind the authenticated workload to its company; tenant code cannot select or impersonate other companies. The adapter provisions no identities, keys, database grants or shared service.
12
+
13
+ ```ts
14
+ import { KeyManagementServiceClient } from "@google-cloud/kms";
15
+ import { KMS_SECRETS_SQL, kmsPostgresStore } from "@sanoma-ai/workflows/kms-secrets";
16
+
17
+ // Privileged deployment setup, once per company database:
18
+ await adminDb.query(KMS_SECRETS_SQL);
19
+ // Grant runtime USAGE on sanoma_kms and SELECT/INSERT/UPDATE/DELETE on sanoma_kms.secrets.
20
+
21
+ // Supply the company-scoped refreshable auth client from trusted bootstrap.
22
+ // D1 provides cloudIdentityAuth in the separate /cloud-identity entry once released.
23
+ const kms = new KeyManagementServiceClient({ authClient: companyAuthClient });
24
+ const store = kmsPostgresStore({
25
+ company: companyIdFromTrustedBootstrap,
26
+ keyName: "projects/cloud-project/locations/us-central1/keyRings/production/cryptoKeys/company-id",
27
+ kms,
28
+ database: async () => companyDatabase,
29
+ });
30
+ await store.validate("production");
31
+ // Deployment config: secrets: { store, namespace: "production" }
32
+ ```
33
+
34
+ The caller owns client/database lifecycle, including `kms.close()`. `SANOMA_SIGNING_KEY` stays independent. This store does not use `SANOMA_SECRETS_KEY`. Workflow code cannot import this backend entry. Secret intake must remain outside durable workflow inputs/outputs.
35
+
36
+ ## One shared key versus a key per company
37
+
38
+ | Event | One shared CryptoKey | Per-company CryptoKeys with scoped identities |
39
+ | ------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
40
+ | Database backup stolen without KMS authority | Encrypted credentials remain protected | Same protection |
41
+ | Company A runtime identity compromised | If it can call the shared key directly, that grant also authorizes decryption of B's ciphertext when available with its AAD | A's key grant cannot decrypt B's ciphertext under B's key |
42
+ | Fleet decrypt identity or IAM administrator compromised | Fleet exposure | Fleet exposure still possible; separate keys cannot fix broad credentials or an attacker able to grant access |
43
+ | Disable a company or respond to an incident | Key disable affects everyone; tenant access needs a broker/policy boundary | Revoke the company's identity or disable its key versions independently |
44
+ | Destroy keys after retention ends | Shared key versions cannot be destroyed while any company still needs them | Independent company retention; all versions/copies/backups must be accounted for |
45
+ | Rotation and audits | Shared lifecycle; attribution relies on identities and application records | Separate key lifecycle and key resource in audit events; configure appropriate audit logging |
46
+
47
+ ```mermaid
48
+ flowchart LR
49
+ A[Company A runtime identity] --> KA[GCP CryptoKey A]
50
+ A --> PA[Company A Postgres encrypted rows]
51
+ B[Company B runtime identity] --> KB[GCP CryptoKey B]
52
+ B --> PB[Company B Postgres encrypted rows]
53
+ ```
54
+
55
+ A shared key can be safe behind a trusted broker that independently authorizes every company request and keeps shared-key permission out of company runtimes. That broker is a different architecture and is not implemented here. In the direct-runtime design, use per-company CryptoKeys and grant `roles/cloudkms.cryptoKeyEncrypterDecrypter` on those keys only. Avoid inherited project/key-ring grants and permissions to change IAM or impersonate other identities. [GCP IAM](https://cloud.google.com/kms/docs/iam) defines these resource boundaries.
56
+
57
+ GCP additional authenticated data (AAD) protects ciphertext binding; it is **not tenant authorization**. Unlike an AWS policy conditioned on encryption context, this GCP design does not claim an IAM restriction on the company string in AAD. A caller with permission to decrypt a shared key can supply another company's known AAD. See [GCP AAD](https://cloud.google.com/kms/docs/additional-authenticated-data).
58
+
59
+ `validate` checks an own-key encryption/decryption round trip and database schema access. It cannot certify that the identity lacks permission on every other company's key. Deployment evidence must inspect grants and prove cross-company denial using existing keys, including inherited permissions. The live test checks encrypt/decrypt denial in both directions between two test identities.
60
+
61
+ The database login must separately prevent cross-company access. The company column and key allowlist are defense in depth, not a replacement for database permissions or IAM. The SQL installer uses a private schema and removes public/API-role grants; never expose it through a browser Data API. Keep database/SDK payloads and credentials out of tracing and logs.
62
+
63
+ ## Envelope format and rotation
64
+
65
+ Each independent secret or atomic token bundle occupies one row. `sanoma-gcp-kms-v1` generates a fresh random 256-bit data key locally for each write attempt. AES-256-GCM encrypts the value; GCP `Encrypt` wraps only the data key using the CryptoKey's primary version. Postgres stores the ciphertext, nonce, tag, wrapped data key, CryptoKey and version names, and an opaque UUID revision for compare-and-swap updates.
66
+
67
+ Both GCP AAD and local AES-GCM bind company, namespace, path, row revision and format. AES-GCM also authenticates the returned CryptoKeyVersion name. Reads accept only configured CryptoKeys. RPCs send CRC32C checksums and verify response integrity, including the version resource returned by encryption. Provider errors become sanitized store error codes. See Google's [encryption API](https://cloud.google.com/kms/docs/reference/rest/v1/projects.locations.keyRings.cryptoKeys/encrypt) and [integrity guidance](https://cloud.google.com/kms/docs/data-integrity-guidelines).
68
+
69
+ Values larger than 65,536 UTF-8 bytes are rejected. No plaintext or data-key cache is kept. Temporary mutable key buffers are cleared in `finally`; JavaScript strings and SDK-internal copies cannot be guaranteed erased. CAS conflicts leave committed values intact. Deleting a row does not revoke its vendor token or erase copies in backups.
70
+
71
+ There are three different rotations:
72
+
73
+ - **KMS version rotation:** a new primary version under the same CryptoKey. After the primary change propagates, new writes use it; existing rows still decrypt through their original enabled versions, without configuration changes. Automatic rotation does not rewrite old rows.
74
+ - **CryptoKey replacement:** change `keyName` and retain old company keys in `decryptKeyNames`. Read and rewrite old rows with their expected versions; conflict means reread rather than overwrite. This draft supports those primitives, not an automatic migration worker. Backups may still need the old keys.
75
+ - **Vendor credential rotation:** obtain a new API key or refresh an OAuth token, then save the new value. KMS rotation does not rotate vendor credentials or remove a compromised runtime's access.
76
+
77
+ Recovery needs database backups plus every enabled key version needed by retained envelopes. Test restores before retiring keys. Rewriting current rows is insufficient if retained backups contain old envelopes. Disabling is reversible but makes dependent ciphertext unreadable; destruction is irreversible after the provider's destruction window. AAD/version binding detects substitution, but does not prevent an authorized database writer from rolling back a complete old valid row; CAS assumes trusted database state. [GCP rotation](https://cloud.google.com/kms/docs/key-rotation).
78
+
79
+ ## Rotation costs
80
+
81
+ GCP bills retained key **versions**, not just logical keys. Current list pricing is about $0.06 per SOFTWARE symmetric version per month, or $1 per HSM symmetric version; encrypt/decrypt operations cost $0.03 per 10,000. Creating a rotation is free. Enabled, disabled and scheduled-for-destruction versions all count until destroyed; partial months are prorated. [GCP pricing](https://cloud.google.com/kms/pricing).
82
+
83
+ | Retained versions per key | One shared SOFTWARE key | 1,000 company SOFTWARE keys | 1,000 company HSM keys |
84
+ | ------------------------- | ----------------------: | --------------------------: | ---------------------: |
85
+ | 1 | $0.06/month | $60/month | $1,000/month |
86
+ | 2 | $0.12/month | $120/month | $2,000/month |
87
+ | 5 | $0.30/month | $300/month | $5,000/month |
88
+
89
+ These examples exclude operations, database costs, free-tier eligibility and replicas. One million symmetric KMS operations adds $3. Each store write attempt uses one encryption operation; each successful credential read requires one decryption. Validation uses both. Rewriting an existing row uses a read plus a write, so two operations before retries: $6 per million rows, plus database/compute work. A future data-key rewrap also requires decrypt/encrypt calls but is not implemented. Five retained versions could mean an initial version plus four rotations; this is an example, not a retention recommendation. Retention follows credential and backup recovery requirements.
90
+
91
+ ## Verification
92
+
93
+ Focused tests use real local Postgres and AES-GCM with a GCP transport/IAM model. They cover atomic store conformance, tampering, checksums, key revocation, key replacement, primary-version rotation and restoration of old rows. They explicitly show that AAD cannot substitute for IAM.
94
+
95
+ The opt-in live suite requires two pre-provisioned test CryptoKeys and separate identities, each authorized only on its own key. Configure `SANOMA_GCP_KMS_LIVE=1`, `SANOMA_GCP_KMS_LIVE_KEY`, `SANOMA_GCP_KMS_LIVE_OTHER_KEY`, `SANOMA_GCP_KMS_LIVE_CREDENTIALS` and `SANOMA_GCP_KMS_LIVE_OTHER_CREDENTIALS`; the latter two name ignored SDK credential/federation configuration files. Use the isolated `SANOMA_TEST_DATABASE_URL` and run `pnpm vitest run packages/workflows/test/kms-secrets-live.test.ts`.
96
+
97
+ The suite creates no cloud resources and cleans only its own synthetic Postgres rows. Its positive checks prevent nonexistent/disabled keys from masquerading as an isolation proof. Local modeled tests do not establish real IAM isolation.
98
+
99
+ Live verification on `sanoma-513217` passed at `c3c293d0bc48cbcf66adf8e0cb0c57f500e686bc`: store conformance and two-way encrypt/decrypt denial between two existing company keys under distinct scoped identities. A separate run also proved production/preview denial in both directions. Positive own-key operations passed before each negative check. The local SDK used explicit service-account impersonation through the administrator’s OAuth session, with no service-account private keys. A separate temporary Cloud Run broker proof exercised the SDK/store against remote company databases, cross-key denials and KMS primary-version rotation: old ciphertext remained readable and new writes adopted the new primary after propagation. These are isolated synthetic resources, not a managed-hosting rollout; runtime renewal and production deployment evidence belong to D1.
package/README.md CHANGED
@@ -122,7 +122,7 @@ What an app's folder declares is named with the folder, from where its file is (
122
122
 
123
123
  The people are the company's: an app's data files declare none (`readPeople` refuses one, naming its file). A workflow may import the root's `resources/data.ts` and any app's. A relative import from one app's files into another's folder is a warning (`crossAppImports(root, apps)`, from `@sanoma-ai/workflows/describe`, which `sanoma check` prints), since a company may take one app without the other: reach the other through `ctx` or `api` instead. A collection or site at the root named as an app is a problem: `api.data.<app>` and `/sites/<app>/` are the app's.
124
124
 
125
- Taking an app is a git merge into a draft of the company's `Source` (the `sanoma` package's `localGitSource`). `draft.take({ url, ref?, folder?, author })` fetches the source repository, a public company repo holding one `apps/<folder>/` (`folder` names it when it holds more), by https alone, and merges it with its history as one merge commit. `draft.update({ folder, author })` merges the new commits of the default branch of the URL the app's `app.ts` names, and `source.behind(folder)` counts them, fetched now (both refuse a source that shares no history with the checkout). An update reads the source from `main`'s `app.ts`, never the draft's, since a draft's files are never loaded: an app only in a draft is refused until its take is published. Either merge takes `apps/<folder>/` (a take, the source's folder whole) and `package.json` (its connectors, both sides kept); every other file stays the company's, as does a file one side deleted, since the source's root is the starter's. Both return an `AppMerge` (`{ folder, revision, merged, conflicts }`): a line both sides changed is committed with git's markers (diff3: ours, the base, theirs) and its file named in `conflicts`, so the draft's checks fail until the agent resolves it. An update needs the take's merge commit in the history: a draft that takes an app is merged into `main` as a merge, not squashed.
125
+ Taking an app is a git merge into a draft of the company's `Source` (the `sanoma` package's `localGitSource`). `draft.take({ url, ref?, folder?, author })` fetches the source repository, a public company repo holding one `apps/<folder>/` (`folder` names it when it holds more), by https alone, and merges it with its history as one merge commit. `draft.update({ folder, author })` merges the new commits of the default branch of the URL the app's `app.ts` names (`appSourceOf`: an `app.ts` that throws, or is not `defineApp({ source })`, is refused with its problem, and one that names no source as the company's own), and `source.behind(folder)` counts them, fetched now (both refuse a source that shares no history with the checkout). An update reads the source from `main`'s `app.ts`, never the draft's, since a draft's files are never loaded: an app only in a draft is refused until its take is published. Either merge takes `apps/<folder>/` (a take, the source's folder whole) and `package.json` (its connectors, both sides kept); every other file stays the company's, as does a file one side deleted, since the source's root is the starter's. Both return an `AppMerge` (`{ folder, revision, merged, conflicts }`): a line both sides changed is committed with git's markers (diff3: ours, the base, theirs) and its file named in `conflicts`, so the draft's checks fail until the agent resolves it. An update needs the take's merge commit in the history: a draft that takes an app is merged into `main` as a merge, not squashed.
126
126
 
127
127
  ### People
128
128
 
@@ -271,7 +271,7 @@ export default defineConfig({ connectors: [], secrets });
271
271
 
272
272
  Without `store`, the runtime uses its encrypted Postgres adapter. Without `namespace`, it uses `default`. An unbound name resolves to the same path in that namespace, managed by Sanoma. A `{ path, management: "external" }` binding is read-only to Sanoma even when its store can write. An `{ env }` binding reads that exact variable, is read-only, and does not fall back when absent. A `{ bootstrap }` binding explicitly exposes one deployment key to trusted host-secret resolution; it reads the corresponding environment or mounted-file input and never stores the key in Postgres. Bootstrap keys and recovery are described [below](#bootstrap-keys).
273
273
 
274
- An external adapter is a `SecretStore` instance supplied as `secrets.store`. It authenticates to one deployment's authorized storage before Sanoma can read values. Prefer a scoped workload identity where the adapter supports it; otherwise deliver its bootstrap credential through the host. Do not make that credential depend on reading the same store it unlocks. AWS Secrets Manager, Supabase Vault, HashiCorp Vault, Cloudflare Secrets Store, managed KMS storage and transfer tooling are not shipped by this foundation.
274
+ An external adapter is a `SecretStore` instance supplied as `secrets.store`. It authenticates to one deployment's authorized storage before Sanoma can read values. Prefer a scoped workload identity where the adapter supports it; otherwise deliver its bootstrap credential through the host. Do not make that credential depend on reading the same store it unlocks. The [Supabase Vault adapter](../../connectors/supabase/README.md#supabase-vault-as-the-secret-store) provides this boundary for BYOC. See the [support and release-readiness matrix](https://github.com/Sanoma-AI/sanoma/blob/main/packages/workflows/SECRET-SUPPORT.md) for merged capabilities, connector coverage, publication status and remaining providers/lifecycle work.
275
275
 
276
276
  Keep deployment composition in config code, outside workflows. For example, given an adapter instance already authenticated as `liveStore`:
277
277
 
@@ -298,7 +298,7 @@ Pass exactly the selected configuration to `defineConfig`. A namespace is a rout
298
298
 
299
299
  The adapter contract uses `read({ namespace, path })`, `write(address, value, { expectedVersion, by })`, `delete(address, { expectedVersion, by })` and `validate(namespace)`. A read returns an opaque version beside the value; `expectedVersion: null` creates only when absent, and a version replaces or deletes only that version. `ensure` uses create-if-absent and reads the winner of a concurrent creation. Providers advertise writable access, a byte limit, conditional-write guarantees (`atomic`, `coordinated` or `none`), deletion behavior and change detection. `coordinated` covers cooperating Sanoma writers only; it cannot promise safety against external writers. Unsupported conditional writes are refused, not emulated with an unsafe read followed by a write.
300
300
 
301
- The built-in adapter stores one encrypted value or credential bundle per namespace/path, with a fresh version on each write. AES-256-GCM authenticates the complete namespace/path identity. It rejects plaintext rows and ciphertext moved to another address. `SecretStoreError` returns fixed codes without provider responses or secret values. Connector authentication declares its own versioned schema with `defineAuthentication`: `api-key`, `fields`, `oauth2` or `signed`, with storage requirements; `workload-identity` needs no persisted credential bundle. Connector authors use `checkAuthenticationStore`, `encodeCredentials` and `decodeCredentials` inside trusted code. These contracts do not implement OAuth consent, refresh or vendor account verification.
301
+ The built-in adapter stores one encrypted value or credential bundle per namespace/path, with a fresh version on each write. AES-256-GCM authenticates the complete namespace/path identity. It rejects plaintext rows and ciphertext moved to another address. `SecretStoreError` returns fixed codes without provider responses or secret values. Connector authentication declares its own versioned schema with `defineAuthentication`: `api-key`, `fields`, `oauth2` or `signed`, with storage requirements; `workload-identity` needs no persisted credential bundle. Connector authors use `checkAuthenticationStore`, `encodeCredentials` and `decodeCredentials` inside trusted code. OAuth consent and refresh remain separate lifecycle work.
302
302
 
303
303
  Adapter authors can run `checkSecretStore(store, disposableNamespace)` from `@sanoma-ai/workflows`. It checks missing values, ownership, contention, stale versions, namespace separation, size limits and the advertised deletion behavior. It mutates test records; use a scoped test identity and clean up retained versions according to the provider. `memorySecretStore()` is the in-process reference implementation, not persistent deployment storage.
304
304
 
@@ -342,10 +342,16 @@ export default defineConfig({
342
342
 
343
343
  Connection creation rules accept principal IDs or `{ group: "visitors" }` for authenticated customer sign-ups. The runtime uses admitted groups and owns each dynamic connection by the initiating principal. Disconnect invalidates the account revision and blocks new selection; vendor revocation and credential retirement are separate lifecycle work. Visitors also need a Cedar permit for the relevant operation; a creation rule does not bypass policy.
344
344
 
345
- `sanoma.connection.bind` is a policy-controlled access operation. Its input contains a declared reference, the granted account and scopes, and `expectedRevision` (`null` for an initial binding). It persists metadata and a new revision, never credential values. `sanoma.connection.create` takes a declared rule ID, account and scopes; the actor must be in `createBy`. It creates an owner-scoped dynamic ID without a repository commit. Trusted code can make a typed reference with `connectionRef(rule, id)`, but the runtime still verifies the stored owner and declaration on use. These metadata operations are not proof of vendor consent; connector intake must verify the account before binding it.
345
+ `sanoma.connection.bind` is a policy-controlled access operation. Its input contains a declared reference, the granted account and scopes, and `expectedRevision` (`null` for an initial binding). It persists metadata and a new revision, never credential values. `sanoma.connection.create` takes a declared rule ID, account and scopes; the actor must be in `createBy`. It creates an owner-scoped dynamic ID without a repository commit. Trusted code can make a typed reference with `connectionRef(rule, id)`, but the runtime still verifies the stored owner and declaration on use. These low-level metadata operations are not a credential-entry interface or proof of vendor consent. Use the secure intake APIs below for user-entered values; each named invocation also verifies the current bundle.
346
346
 
347
347
  A durable call pins its account revision before policy evaluation. A held direct call carries the same pin into its durable continuation. The runtime checks that the binding and definition still match before execution; rebinding or changing requirements requires a fresh call and authorization. Policy approvals cannot silently authorize a different account. The ledger records connection metadata, not its credential bundle. This changes the durable step layout; old runs are not replayed against the new layout.
348
348
 
349
+ Named drivers explicitly opt into methods with `defineDriver(connector, implementation, { authentication: ["apiKey"] })`. Each method supplies `verify(credentials)`, which checks the provider and returns `{ account, scopes, expiresAt? }`; expiry is Unix milliseconds. Workload identity verifies the deployment identity without a stored bundle. A missing verifier or unsupported driver is refused. After policy allows the operation, the runtime reads only the selected bundle, validates its schema, verifies that its account and grants still match the authorized metadata, and rechecks the binding after verification. Changed grants or expired credentials require reconnection. Verifier exceptions expose no SDK messages or causes.
350
+
351
+ Workload-identity methods receive `verify(context?: AuthenticationContext)` instead of a credential bundle. The runtime supplies `{ connectionId, vendor }` from its selected connection during secure intake, Test and execution. Use that reference to select a trusted deployment identity mapping, then verify the identity with the provider; it is not itself proof of cloud authority. The method stores no credential bundle and must not accept a caller-selected credential file or fall back to ambient identity. Bundle-based methods still receive `verify(credentials)`. See [GCP provisioning identity](../../connectors/gcp/README.md#deployment-identity) for an implementation.
352
+
353
+ The driver reads `authenticationCredentials("apiKey", definition, call)` inside its operation and creates a client for that invocation. `defineDriver` also accepts a factory `(call) => implementation`, invoked inside that boundary, for operations sharing one SDK client during a call. `call.authentication.credentialVersion` identifies the version read. The runtime keeps no credential or SDK cache: replacement affects the next invocation, while a call already executing retains its original bundle. Never put this material in an operation result. Named calls' `call.secrets` can resolve only auxiliary secret markers in their authorized input, and `ensure` additionally requires a marker with `generate: true`. An operation may declare generated destinations with `createsSecrets(input)`, such as a database connector storing a URL under the authorized resource export's name; these are exact logical names, not wildcard store access. Driver failures retain retryability and HTTP status, with a fixed message and no provider cause. Direct calls and durable calls use the same driver boundary.
354
+
349
355
  Resource declarations use the same identity as an option, loaded with the rest of the data file:
350
356
 
351
357
  ```ts
@@ -354,7 +360,29 @@ import { mail } from "@acme/mail/resources";
354
360
  export const inbox = mail.inbox({ name: "support" }, { connection: { vendor: "mail", $connection: "mail/support" } });
355
361
  ```
356
362
 
357
- The resource type must belong to that connector. Apply and drift pin the selected revision when they read declarations, then carry it through imports, plans and effects. The resource lockfile records the connection alongside the vendor object ID. Changing the account of a bound resource requires an explicit unbind/rebind; a default change or a same-name account replacement cannot redirect a later apply. Named sandbox account state and the connection-management UI remain follow-up work.
363
+ The resource type must belong to that connector. Apply and drift pin the selected revision when they read declarations, then carry it through imports, plans and effects. The resource lockfile records the connection alongside the vendor object ID. Changing the account of a bound resource requires an explicit unbind/rebind; a default change or a same-name account replacement cannot redirect a later apply. The app exposes these declarations through its [connection management](../app/README.md#connection-management) forms.
364
+
365
+ Sandbox runs use synthetic identities for declared static connections, with separate fake state, faults and remembered replies for each account and run. They never read production connection records, invoke authentication verifiers or read production credentials. A production revision or an undeclared dynamic account is refused. Scenario steps can select an account in their returned seed, such as `{ op: mail.message.create, input: { title }, connection: support }`; absent a selector, only an explicitly declared default is used. Fault seeds accept the same `connection` field. Standalone driver tests can use `connectionFakes(makeFake)` from `@sanoma-ai/workflows/fake`: its `driver` requires `call.connection`, `account(ref)` exposes just that account's fake for seeding and fault injection, and `calls` is the combined log with connection IDs. Rebinding a reference to a different vendor account starts fresh fake state.
366
+
367
+ #### Connecting and managing accounts
368
+
369
+ Use `SanomaClient.previewConnection(target, credentials, { actor, id })` and `connectConnection(review, credentials, { actor })` for secure account intake. These are trusted server APIs, not workflow operations: credential values must never be passed to `client.call`, workflow arguments or an approval. `target` is either `{ connection: ref }` for a declared account or `{ rule: "vendor/rule" }` for an authorized dynamic account. Keep the same request ID while completing one review; a new account or a changed review starts a new request.
370
+
371
+ Preview runs the metadata-only `sanoma.connection.prepare` access gate before reading a store or verifying submitted credentials. The connector verifies the account and granted scopes against the code's requirements. The returned `ConnectionReview` contains metadata only: the verified account and scopes, destination, existing account/destination when present, expected revisions and the authorization fingerprint. It never returns credentials. An externally managed or read-only binding accepts no pasted replacement; preview reads its existing bundle. Workload identity verifies the deployment without a stored bundle.
372
+
373
+ Save rechecks the review, policy and destination, verifies the credentials again, then calls `sanoma.connection.configure` to reserve a `pending` account revision. It writes a managed bundle directly through the selected store with the reviewed expected version, outside durable execution. An opaque stored-version receipt records a successful write so resuming the same approved setup does not blindly repeat it. `sanoma.connection.activate` is a separate access gate: it verifies the saved bundle before making that revision usable. These gates, Test and Disconnect use ordinary policy decisions and ledger records containing metadata only. When a gate asks for approval, return to the secure form and retry after the decision; an approval never stores the form's credential values for later replay.
374
+
375
+ `pending` and `uncertain` accounts cannot run operations. An ambiguous store write or a failure after writing can leave `uncertain`; it must be inspected and reverified rather than treated as a successful connection or blindly written again. A policy, account, scope, destination or binding revision change invalidates the relevant review and requires fresh verification. Changing a destination does not copy or delete the previous value: the review identifies both locations, and a store migration remains an explicit separate action.
376
+
377
+ The read APIs are scoped to the admitted actor:
378
+
379
+ - `client.connections({ actor })` returns `ConnectionCard` metadata for company declarations and that actor's dynamic accounts, including requirements, account, scopes, ownership, current/bound storage location, change attribution and last-tested health. Listing cards does not read credentials or contact a vendor.
380
+ - `client.connectionRules({ actor })` returns creation choices permitted by the current code's `createBy` person IDs or `{ group }` entries. Creating under a rule still passes policy and ownership checks.
381
+ - `client.secretStoreStatus({ actor })` validates the selected store's bootstrap access and reports its ID, namespace, capabilities and a sanitized readiness problem. It neither enumerates nor reads credential values.
382
+
383
+ Call `sanoma.connection.test` with `{ connection }` to verify the current bundle under policy. Its health result is a timestamped observation, not a live guarantee or background monitor: it can report expired, revoked, wrong-account, changed-scope, invalid, unavailable or store-denied states. An external bundle whose opaque version differs from the prior test is marked externally renewed. A later vendor or store change is detected on the next Test or authorized invocation.
384
+
385
+ `sanoma.connection.disconnect` takes `{ connection, expectedRevision }` and makes the binding unusable under a new revision. It does not delete any stored value or revoke the token at the vendor; externally managed values remain with their owner. Deletion, vendor revocation, OAuth consent/refresh and provider-store migration require their own lifecycle support. The app's [connection forms](../app/README.md#connection-management) implement this secure intake and show these states; they do not add a second configuration source.
358
386
 
359
387
  #### Bootstrap keys
360
388
 
@@ -362,6 +390,8 @@ The resource type must belong to that connector. Apply and drift pin the selecte
362
390
 
363
391
  For a host deployment, deliver signing material, database authentication and the adapter's bootstrap through that host's secret delivery mechanism; include the encryption key only for the built-in store. A mount path is local to the process reading it: do not send a laptop's `_FILE` path to a remote container. Explicit bootstrap bindings let a host resource receive the key value through its secret channel while code keeps only the reference.
364
392
 
393
+ The managed Cloud [runtime identity broker](https://github.com/Sanoma-AI/sanoma/blob/main/packages/workflows/CLOUD-IDENTITY.md) supplies refreshable, deployment-bound Google access tokens; its enrollment, bootstrap and rollout requirements are separate from credential storage and provisioning identity.
394
+
365
395
  Back up ciphertext, its namespace/path and version metadata, and the matching encryption key separately. A database-only backup cannot recover encrypted values. Preserve the signing key across ordinary restarts; replacing it invalidates sessions and pending signed authorization state. Changing a vendor token updates a stored value and its version; it does not rotate the row-encryption key. Replacing the encryption key without re-encrypting every retained value makes those values unreadable. Automated encryption-key rotation, external-store migration and OAuth reauthorization tooling are not included here.
366
396
 
367
397
  ### Resources
@@ -470,7 +500,7 @@ Another data file names `website` by importing it: `import { website } from "./g
470
500
 
471
501
  A data file is TypeScript like any other: it may compute a value, spread a shared object or import a helper of its own. Each exported value that a resource type's constructor made (`<vendor>.<type>({ … })`, the vendor's constructors imported from its connector's `<info.package>/resources` entry, the people's from `sanoma/resources`) is one declaration, `<file>#<export>`, in the file that made it (another file re-exporting it declares nothing), under its non-default name when the file also exports it as `default`; anything else the file exports, such as the list `export default [a, b]` that Sanoma writes, declares nothing. A field the type's `fields.references` allows a resource in is given the declared value itself, and names the declaration that exports that very object. A field the type's `fields.writeOnly` lists takes `secret("NAME")` or `secret("NAME", { generate: true })`, imported from `sanoma/resources` (or `@sanoma-ai/workflows/resources`; see [Secrets](#secrets)). `resources/data.ts` is the exception: it holds the [collections](#collections) and nothing else.
472
502
 
473
- The loader (`loadModules`) loads synchronously, since `describeConfig` and `readPeople` are: `require` of an ES module, Node's type stripping and `module.registerHooks`, so a data file may not use top-level `await`. It serves the modules a company file imports from the runtime itself, from its own objects: each connector's `<package>/resources` with the config's constructors, `sanoma/resources`, `sanoma/data` (`collection`), `sanoma/site` (`defineSite`) and, to an app's `app.ts`, `sanoma` (`defineApp`); so a data file never needs the package installed, and each value they make records where it was made. Each load is a generation: a file is loaded again, with what it imports, once it, a company file it imports or a directory they are in has changed (by size and modification time), so an edit is read without a restart; every other call answers from the generation, at the cost of a `stat` per file. A superseded generation's modules stay in Node's cache, a copy of the files per edit.
503
+ The loader (`loadModules`) loads synchronously, since `describeConfig` and `readPeople` are: `require` of an ES module, Node's type stripping and `module.registerHooks`, so a data file may not use top-level `await`. It serves the modules a company file imports from the runtime itself, from its own objects: each connector's `<package>/resources` with the config's constructors, `sanoma/resources`, `sanoma/data` (`collection`), `sanoma/site` (`defineSite`) and, to an app's `app.ts`, `sanoma` (`defineApp`); so a data file never needs the package installed, and each value they make records where it was made. Each load is a generation: a file is loaded again, with what it imports, once it, a company file it imports or a directory they are in has changed (by size and modification time), so an edit is read without a restart; every other call answers from the generation, at the cost of a `stat` per file. `liveDescription(config)` (`@sanoma-ai/workflows/describe`) keeps a `describeConfig` and makes it again only once a file a load read (`stampsOf`), the lockfile, or a folder a new data file, app or site would appear in has changed: the app reads its description through it, so a saved field shows on the next page without a restart. A superseded generation's modules stay in Node's cache, a copy of the files per edit.
474
504
 
475
505
  The data files are found from the config's root: `defineConfig` records the file it is called from as the config's `file`, and `resolveConfig` gives its directory as `root`. A config bundled into one file sets `root` itself. A config with neither has no data files, and says so, rather than guess the working directory.
476
506
 
@@ -749,7 +779,7 @@ A sandbox run does not survive a worker restart: its fakes keep their state in t
749
779
  `@sanoma-ai/app` is one UI over a config; another (a Slack bot, an internal tool) can be built on the same pieces, which the package exports for that:
750
780
 
751
781
  - `describeConfig(config)` and its types (`ConfigDescription`, `WorkflowEntry`, `OpEntry`, `VendorEntry`, `ResourceTypeEntry`, `DeclaredResource`, `ResourceProblem`), and `loadDataFiles(root, connectors)` and `readResources(config)`, from `@sanoma-ai/workflows/describe`: what to render, as plain JSON, with each workflow's outline. Its shape is `ConfigDescriptionSchema`, from `@sanoma-ai/workflows/description` (zod and nothing of Node, so a browser may import it): each entry type is inferred from its schema, and the schema refuses a key it does not name. Beside what the sections above say, a resource type carries its `capabilities` (`host`, `database` and `wakeup` from its spec, `source` for a type a host type's `source` field names, such as `github.repository`), a vendor its `credentials` (each variable its driver's `env` declares: its name, description and whether it is optional, never a value or a status), and the policy its `files` (each `.cedar` file's path and text, the root's then each app's). It is a separate entry so the worker never loads the parser the outline uses, and the lint refuses it in workflow files. `resolveConfig(config)` returns the checked config as a `ResolvedConfig`, with the operations and drivers by id, the resource types by `<vendor>.<type>` (`types`), the workflows by name (the built-ins `drift`, `apply` and `call` among them), its `root` and each vendor's `credentials` (`CredentialStatus`); `isOp(x)` tells an operation from a built-in in a workflow's `uses`.
752
- - `readSites(root, description, connectors, policies?)`, from `@sanoma-ai/workflows/describe`, which `describeConfig` returns as `sites`: what each site calls, read from its source with oxc and never run (its pages are browser code: what they call is the site's outline), for a workflow's **Used by**; its `site.ts` is loaded. A site is a folder of `sites/` with a `routes/`, named as a URL segment; its `routes/**` and `components/**` (`.ts`, `.tsx`, less tests) are read for each `<RunForm workflow="…" />` and each use of `api`, both imported from `sanoma/site`, called or passed on (`queryFn: api.data.leads.list`): `api.<name>` and `api.<name>.<method>` are the workflow whose `apiNameOf` is `<name>`, `api.data.<collection>.<op>` a collection, `api.<vendor>.<resource>.<op>` the operation of that id. Each is a `SiteCall` (`kind`, `target`, `method`, what it calls of a workflow or collection as written (`start`, `latest`, `list`), `via`, `file`, `line`, `column`, and `page`, the page's path from its `createFileRoute` or `createLazyFileRoute` id, in a page's file); a `SiteEntry` holds a site's `name`, its `title` (`defineSite({ title })` in `site.ts`, the file's default export, which a zod schema checks: a key it does not name, a `login` naming no login half, or `locales` that are no language tags or not on a public site without a login, is a problem), its `pages` (each `SitePage` a `path` from a route file's id, one per path, a layout and its index being one; and on a path with one parameter, such as `/sites/www/blog/$slug`, `content`, the one kind its file reads an entry of by that parameter, `content.<kind>.get(params.slug)` (or `slug`, destructured) with `content` from `sanoma/site`, for the app's sitemap and its pages' languages, which `siteContentPages(root)` gives the app's build alone, path to kind), its `calls`, its `warnings` (on a public site, each page with one parameter whose file reads content but not by that parameter, so the sitemap lists none of its pages: a warning `sanoma check` prints and does not fail on) and its `problems`: each call to a workflow, collection or operation the config does not have (`calls workflow "<name>", which does not exist`), or to an operation a page may not call directly, and, given the policies (`describeConfig` passes them unless they have problems), on a public site each start (`api.<name>(…)`, `.start(…)`, a `RunForm`) and direct call that no company `permit` could let a visitor make, its `when` and `unless` aside (one naming `anonymous`; on a site with a `login`, `anonymous` or the group `visitors`), whose message ends with the permit to add, and, whatever the policies, its call of a built-in workflow or read of a workflow's runs but `.result` (`.latest`, `.runs`, `.approvals`, `.ledger`), which the app refuses a visitor; which `sanoma check` fails on. A name not written out (a variable, a template with holes, a computed member) is skipped, as is a call through a namespace import or a destructured `api`.
782
+ - `readSites(root, description, connectors, policies?)`, from `@sanoma-ai/workflows/describe`, which `describeConfig` returns as `sites`: what each site calls, read from its source with oxc and never run (its pages are browser code: what they call is the site's outline), for a workflow's **Used by**; its `site.ts` is loaded. A site is a folder of `sites/` with a `routes/`, named as a URL segment; its `routes/**` and `components/**` (`.ts`, `.tsx`, less tests) are read for each `<RunForm workflow="…" />` and each use of `api`, both imported from `sanoma/site`, called or passed on (`queryFn: api.data.leads.list`): `api.<name>` and `api.<name>.<method>` are the workflow whose `apiNameOf` is `<name>`, `api.data.<collection>.<op>` a collection, `api.<vendor>.<resource>.<op>` the operation of that id. Each is a `SiteCall` (`kind`, `target`, `method`, what it calls of a workflow or collection as written (`start`, `latest`, `list`), `via`, `file`, `line`, `column`, and `page`, the page's path from its `createFileRoute` or `createLazyFileRoute` id, in a page's file); a `SiteEntry` holds a site's `name`, its `title` (`defineSite({ title })` in `site.ts`, the file's default export, which a zod schema checks: a key it does not name or a field of the wrong type (a number for `icon`) is a problem and that field is left out, the rest still read, so a bad `icon` keeps a site public, though a wrong `public`, `login` or `locales` leaves out all three; a `login` naming no login half, or `locales` that are no language tags or not on a public site without a login, is a problem too), its `pages` (each `SitePage` a `path` from a route file's id, one per path, a layout and its index being one; and on a path with one parameter, such as `/sites/www/blog/$slug`, `content`, the one kind its file reads an entry of by that parameter, `content.<kind>.get(params.slug)` (or `slug`, destructured) with `content` from `sanoma/site`, for the app's sitemap and its pages' languages, which `siteContentPages(root)` gives the app's build alone, path to kind), its `calls`, its `warnings` (on a public site, each page with one parameter whose file reads content but not by that parameter, so the sitemap lists none of its pages: a warning `sanoma check` prints and does not fail on) and its `problems`: each call to a workflow, collection or operation the config does not have (`calls workflow "<name>", which does not exist`), or to an operation a page may not call directly, and, given the policies (`describeConfig` passes them unless they have problems), on a public site each start (`api.<name>(…)`, `.start(…)`, a `RunForm`) and direct call that no company `permit` could let a visitor make, its `when` and `unless` aside (one naming `anonymous`; on a site with a `login`, `anonymous` or the group `visitors`), whose message ends with the permit to add, and, whatever the policies, its call of a built-in workflow or read of a workflow's runs but `.result` (`.latest`, `.runs`, `.approvals`, `.ledger`), which the app refuses a visitor; which `sanoma check` fails on. A name not written out (a variable, a template with holes, a computed member) is skipped, as is a call through a namespace import or a destructured `api`.
753
783
  - `generateApi(description)` and `writeApi(root, description)`, from `@sanoma-ai/workflows/describe`: a site's typed `api`, `.sanoma/api.ts` (`API_FILE`), from a `ConfigDescription`, types and JSDoc only and the same text for the same description. It holds a record type per collection and `CompanyApi`: `<apiNameOf(workflow)>: WorkflowFn<input>` per workflow, built-ins too; `<vendor>: { <resource>: { <op>: OpFn<input> } }` for the operations `direct` marks; `data: { <collection>: CollectionApi<record> }`; each member documented from its title or description. Then it augments `sanoma/site`'s `Register`. Inputs are typed from their JSON Schema for the kinds a start form draws, constants, and unions of them (`null` among them); anything else, and every output, is `unknown`. A workflow with `describeConfig`'s api-name problem is left out (`problems`). `writeApi` writes only when the text differs (`wrote`).
754
784
  - `SanomaClient`: call an operation directly (`call`, see [Direct calls](#direct-calls)), start runs, list them (`runs({ limit, status, workflow })`) or read a workflow's last result (`latest`), read a run's ledger and approvals, decide approvals, with the checks described above, and start a drift check or an apply and read its report (`drift`, `driftReport`, `apply`, `applyReport`, see [Drift](#drift) and [Apply](#apply)). `client.chats` keeps the operator agent's chats in the config's database: `open({ id, person, engine, title })` (the id the browser gave the new chat), `attach(id, draftId)`, `upsert(id, messages)` (the AI SDK's UI messages, each by its id, a new one after the last), `load(id)`, `get(id)` and `list(person)` (the newest first, `Chat`).
755
785
  - `APPROVALS_EVENT` and `decisionEventOf(approvalId)`: the DBOS events a run publishes its approvals and each decision on, for a UI that reads DBOS directly. `ApprovalMessage` is the zod schema of a decision as a run reads it. `RunArgs` is what a run receives: its input, `startedBy` and, for a sandbox run, its scenario as `sandbox`.
@@ -782,3 +812,5 @@ Status: early (0.x). The API may change between minor versions.
782
812
  TODO: redact fields a schema marks `.meta({ sensitive: true })` from the ledger, the approvals event and DBOS's step records, so a secret passed by mistake is not kept.
783
813
 
784
814
  License: FSL-1.1-ALv2.
815
+
816
+ The server-only [GCP KMS credential store](KMS-SECRETS.md) is a managed-backend option with explicit company-scoped bootstrap. It requires reviewed IAM/database isolation and does not provision Cloud infrastructure.
@@ -0,0 +1,33 @@
1
+ # Secret storage and named-account support
2
+
3
+ Release checkpoint: `sanoma`, `@sanoma-ai/workflows` and `@sanoma-ai/app` **0.10.1**, `@sanoma-ai/connector-supabase` **0.4.9**, `@sanoma-ai/bridge` **0.5** with its matching platform packages, and `@sanoma-ai/connector-neon` **0.2**. Publication and consumer verification remain pending in [#471](https://github.com/Sanoma-AI/sanoma/issues/471); check installed versions before relying on these capabilities. The client pins [provider-bridge v0.2.0](https://github.com/Sanoma-AI/provider-bridge/releases/tag/v0.2.0); the Go binary release alone does not update installed npm packages. GCP provisioning and D1 runtime identity are separate work, outside this 0.10.1 checkpoint.
4
+
5
+ ## Support matrix
6
+
7
+ | Surface | Merged support | Boundary |
8
+ | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
9
+ | Built-in secret store | Encrypted Postgres values/bundles, logical names, explicit bindings, ownership and atomic version-checked writes/deletes. | Requires `SANOMA_SECRETS_KEY` outside the database. `SANOMA_SIGNING_KEY` is separate. This is not managed KMS storage. |
10
+ | Supabase Vault for BYOC | Server-only `@sanoma-ai/connector-supabase/vault` adapter, private SQL gateway, deployment-scoped database login, managed values and read-only mappings to existing secrets. | Requires gateway installation/grants and bootstrap database authentication. No `SANOMA_SECRETS_KEY` for Vault. Project-wide locks serialize Vault writes; project administrators remain trusted. |
11
+ | Named accounts | Static declarations, code-defined dynamic creation rules, explicit defaults, account/revision selection through policy and execution, and isolated sandbox accounts. | The connector must declare and verify its authentication method, and its driver must explicitly support that method. Declaring an account alone does not add driver support. |
12
+ | Connection management | Secure Verify/Review/Save intake, metadata-only prepare/configure/activate gates, ownership-aware controls, Test/Disconnect, storage review and workflow dependency labels. | Health is last-tested. Pending/uncertain connections cannot execute. Disconnect neither deletes stored credentials nor revokes a vendor token. |
13
+ | Neon | Named `apiKey` connections, verified organization identity, and a new SDK client per authorized invocation. | The verifier requires exactly one organization. It does not attest granular permissions or expiry; Neon enforces permissions on each request. |
14
+ | Provider bridge | UUID session and fresh provider process per operation, protocol-1 negotiation, process-bound plan tokens, shared release/schema caches and session cleanup. | At most 64 active/configuring sessions. No implicit mutation replay. Vendor-specific named authentication still needs an explicit adapter opt-in. |
15
+
16
+ Configuration and lifecycle contracts remain in [Credentials](README.md#credentials). Follow the [Supabase Vault setup and recovery instructions](../../connectors/supabase/README.md#supabase-vault-as-the-secret-store), [connection UI documentation](../app/README.md#connection-management) and [bridge session contract](../bridge/README.md).
17
+
18
+ ## Connector coverage
19
+
20
+ **Neon is the only connector in the 0.10.1 checkpoint declaring a named authentication method.** Its [authentication documentation](../../connectors/neon/README.md#authentication) describes the supported bundle and provider verification.
21
+
22
+ GitHub, Stripe, Supabase and Docker resource drivers use the session-capable bridge, but do not yet opt into named authentication. Supabase Vault is a storage adapter; it does not migrate the Supabase Management API/resource driver. Other connectors retain their existing authentication paths. Login, model and remote halves are also distinct from named operation accounts. This release does not promise multiple named accounts for every connector, nor Gmail, LinkedIn, Reddit or Instagram OAuth connections.
23
+
24
+ For a new named account, the connector's authentication definition, live verifier, supported driver methods and per-call credential use must all be present. Bridge session isolation supplies the provider-process boundary; it does not supply those vendor-specific pieces.
25
+
26
+ ## Remaining delivery and verification
27
+
28
+ - Run the release's affected-package and repository checks against its final commit, verify packed installation and the pinned bridge binary, then publish the coordinated npm packages. Source merge and the Go release alone do not complete these steps.
29
+ - Supabase Vault has an actual-extension proof using standard non-superuser administrator permissions in `supabase/postgres:17.6.1.136`. That is not proof of every hosted project's grants/version; validate the target deployment's gateway and scoped login. The [verification procedure](../../connectors/supabase/README.md#verification) uses a disposable database, not production.
30
+ - Managed Cloud GCP KMS storage remains separate work in [#470](https://github.com/Sanoma-AI/sanoma/issues/470). This release does not establish a deployed Cloud identity route, tenant IAM isolation or the required live deployment evidence.
31
+ - OAuth consent, token refresh, OAuth app ownership and reauthorization remain lifecycle work. Declaring an `oauth2` credential shape does not implement those flows.
32
+ - Cross-store migration remains [#468](https://github.com/Sanoma-AI/sanoma/issues/468). Changing a binding or namespace does not transfer credentials or remove the old copy; automated encryption-key rotation is also absent.
33
+ - AWS Secrets Manager [#462](https://github.com/Sanoma-AI/sanoma/issues/462) and HashiCorp Vault [#464](https://github.com/Sanoma-AI/sanoma/issues/464) are deferred. Cloudflare host bootstrap-secret delivery is distinct from its optional account-wide secret-store adapter; this matrix does not claim that adapter is implemented.
package/dist/apps.d.ts CHANGED
@@ -17,6 +17,12 @@ export declare function appsAt(root: string | undefined): {
17
17
  apps: AppEntry[];
18
18
  problems: AppProblem[];
19
19
  };
20
+ /**
21
+ * The source URL the app `folder` under `root` is taken from, as its `app.ts` names it; an error
22
+ * saying why there is none: no such app, its `app.ts` does not load or is not `defineApp({ source })`
23
+ * (what is wrong, where), or it names none (the company's own app).
24
+ */
25
+ export declare function appSourceOf(root: string, folder: string): string;
20
26
  /**
21
27
  * Each import, from a file under one app's folder, of a file under another's, at its place: a
22
28
  * warning, since a company may take one app without the other. Static imports, re-exports and
package/dist/apps.js CHANGED
@@ -5,7 +5,7 @@ import { childrenOf, readSource } from "./ast.js";
5
5
  import { exportsOf, loadModules } from "./load.js";
6
6
  import { stringValue } from "./outline.js";
7
7
  import { APPS_DIR, filesUnder, inside, isDirectory, RELATIVE, relPath } from "./paths.js";
8
- import { APP_NAME } from "./shared.js";
8
+ import { APP_NAME, problemLine } from "./shared.js";
9
9
  /** The apps under `root` (`appsAt`), without what is wrong with them. */
10
10
  export const readApps = (root) => appsAt(root).apps;
11
11
  /**
@@ -52,6 +52,23 @@ export function appsAt(root) {
52
52
  problems.sort((a, b) => a.file.localeCompare(b.file));
53
53
  return { apps, problems };
54
54
  }
55
+ /**
56
+ * The source URL the app `folder` under `root` is taken from, as its `app.ts` names it; an error
57
+ * saying why there is none: no such app, its `app.ts` does not load or is not `defineApp({ source })`
58
+ * (what is wrong, where), or it names none (the company's own app).
59
+ */
60
+ export function appSourceOf(root, folder) {
61
+ if (!APP_NAME.test(folder) || !isDirectory(join(root, APPS_DIR, folder)))
62
+ throw new Error(`There is no app ${APPS_DIR}/${folder}/`);
63
+ const file = `${APPS_DIR}/${folder}/app.ts`;
64
+ const path = join(root, file);
65
+ const read = existsSync(path) ? appDefinitionOf(file, loadModules(root, [path], [])) : { problems: [] };
66
+ if (read.problems.length)
67
+ throw new Error(read.problems.map(problemLine).join("; "));
68
+ if (read.source === undefined)
69
+ throw new Error(`${file} names no source: the app is the company's own`);
70
+ return read.source;
71
+ }
55
72
  /** What `app.ts`'s default export, `defineApp({ … })`, may say. */
56
73
  const AppDefinitionSchema = z.strictObject({ source: z.string().optional() });
57
74
  /** `app.ts`'s `source`, as loading it gave, and what is wrong there; nothing for no file. */
package/dist/apps.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"apps.js","sourceRoot":"","sources":["../src/apps.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"apps.js","sourceRoot":"","sources":["../src/apps.ts"],"names":[],"mappings":"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"}
@@ -1,5 +1,36 @@
1
1
  import type { z } from "zod";
2
+ import type { CallContext } from "./op.ts";
2
3
  import { type SecretStoreCapabilities } from "./secret-store.ts";
4
+ /** Facts verified with the provider, not account claims supplied by the caller. */
5
+ export interface AuthenticationIdentity {
6
+ account: string;
7
+ scopes: readonly string[];
8
+ /** Unix time in milliseconds. Missing means the provider gives no expiry. */
9
+ expiresAt?: number;
10
+ }
11
+ /** Server-selected account reference for deployment-bound workload identity resolution. */
12
+ export interface AuthenticationContext {
13
+ readonly connectionId: string;
14
+ readonly vendor: string;
15
+ }
16
+ export type AuthenticationProblem = "missing" | "expired" | "revoked" | "rejected" | "wrong_account" | "scope_changed" | "uncertain" | "unavailable" | "unsupported";
17
+ /** Safe lifecycle information only: never a provider response, cause or credential. */
18
+ export declare class AuthenticationError extends Error {
19
+ readonly code = "authentication_failed";
20
+ readonly retryable: boolean;
21
+ readonly problem: AuthenticationProblem;
22
+ constructor(problem: AuthenticationProblem);
23
+ }
24
+ export declare function authenticationProblem(error: unknown): AuthenticationProblem | undefined;
25
+ /** Shared by credential intake and execution; unknown provider failures never imply revocation. */
26
+ export declare function verifyAuthentication(method: Authentication, credentials?: unknown, context?: AuthenticationContext): Promise<AuthenticationIdentity>;
27
+ /** Ephemeral material for one authorized driver invocation; never checkpoint this object. */
28
+ export interface CallAuthentication {
29
+ method: string;
30
+ kind: Authentication["kind"];
31
+ credentialVersion?: string;
32
+ credentials?: unknown;
33
+ }
3
34
  /** Credential schemas and lifecycle are owned by authentication, never by the store provider. */
4
35
  export type Authentication<S extends z.ZodType = z.ZodType> = {
5
36
  kind: "api-key" | "fields" | "oauth2" | "signed";
@@ -10,13 +41,17 @@ export type Authentication<S extends z.ZodType = z.ZodType> = {
10
41
  maxBytes: number;
11
42
  conditionalWrites: boolean;
12
43
  };
44
+ verify?(credentials: z.output<S>): Promise<AuthenticationIdentity>;
13
45
  } | {
14
46
  kind: "workload-identity";
15
47
  storage?: never;
16
48
  version?: never;
17
49
  schema?: never;
50
+ verify?(context?: AuthenticationContext): Promise<AuthenticationIdentity>;
18
51
  };
19
52
  export declare function defineAuthentication<S extends z.ZodType>(definition: Authentication<S>): Authentication<S>;
53
+ /** Typed access inside a driver. A missing or different method never falls back to the environment. */
54
+ export declare function authenticationCredentials<S extends z.ZodType>(method: string, definition: Authentication<S>, call: CallContext): z.output<S>;
20
55
  /** Compatibility check before consent, including externally renewed (read-only) OAuth bundles. */
21
56
  export declare function checkAuthenticationStore(auth: Authentication, store: SecretStoreCapabilities, management: "sanoma" | "external"): void;
22
57
  /** Serialized only at the trusted store boundary, never returned by an operation to a workflow. */