@sanoma-ai/workflows 0.10.1 → 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.
- package/CLOUD-IDENTITY.md +140 -0
- package/KMS-SECRETS.md +99 -0
- package/README.md +6 -0
- package/SECRET-SUPPORT.md +1 -1
- package/dist/authentication.d.ts +7 -2
- package/dist/authentication.js +2 -2
- package/dist/authentication.js.map +1 -1
- package/dist/client.js +2 -2
- package/dist/client.js.map +1 -1
- package/dist/cloud-identity.d.ts +52 -0
- package/dist/cloud-identity.js +222 -0
- package/dist/cloud-identity.js.map +1 -0
- package/dist/connection.js +8 -2
- package/dist/connection.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js.map +1 -1
- package/dist/kms-secrets.d.ts +24 -0
- package/dist/kms-secrets.js +209 -0
- package/dist/kms-secrets.js.map +1 -0
- package/oxlint.json +2 -0
- package/package.json +15 -2
- package/src/authentication.ts +9 -2
- package/src/client.ts +2 -0
- package/src/cloud-identity.ts +266 -0
- package/src/connection.ts +8 -2
- package/src/index.ts +1 -0
- package/src/kms-secrets.ts +271 -0
|
@@ -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
|
@@ -348,6 +348,8 @@ A durable call pins its account revision before policy evaluation. A held direct
|
|
|
348
348
|
|
|
349
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
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
|
+
|
|
351
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.
|
|
352
354
|
|
|
353
355
|
Resource declarations use the same identity as an option, loaded with the rest of the data file:
|
|
@@ -388,6 +390,8 @@ Call `sanoma.connection.test` with `{ connection }` to verify the current bundle
|
|
|
388
390
|
|
|
389
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.
|
|
390
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
|
+
|
|
391
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.
|
|
392
396
|
|
|
393
397
|
### Resources
|
|
@@ -808,3 +812,5 @@ Status: early (0.x). The API may change between minor versions.
|
|
|
808
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.
|
|
809
813
|
|
|
810
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.
|
package/SECRET-SUPPORT.md
CHANGED
|
@@ -17,7 +17,7 @@ Configuration and lifecycle contracts remain in [Credentials](README.md#credenti
|
|
|
17
17
|
|
|
18
18
|
## Connector coverage
|
|
19
19
|
|
|
20
|
-
**Neon is the only connector in
|
|
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
21
|
|
|
22
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
23
|
|
package/dist/authentication.d.ts
CHANGED
|
@@ -8,6 +8,11 @@ export interface AuthenticationIdentity {
|
|
|
8
8
|
/** Unix time in milliseconds. Missing means the provider gives no expiry. */
|
|
9
9
|
expiresAt?: number;
|
|
10
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
|
+
}
|
|
11
16
|
export type AuthenticationProblem = "missing" | "expired" | "revoked" | "rejected" | "wrong_account" | "scope_changed" | "uncertain" | "unavailable" | "unsupported";
|
|
12
17
|
/** Safe lifecycle information only: never a provider response, cause or credential. */
|
|
13
18
|
export declare class AuthenticationError extends Error {
|
|
@@ -18,7 +23,7 @@ export declare class AuthenticationError extends Error {
|
|
|
18
23
|
}
|
|
19
24
|
export declare function authenticationProblem(error: unknown): AuthenticationProblem | undefined;
|
|
20
25
|
/** Shared by credential intake and execution; unknown provider failures never imply revocation. */
|
|
21
|
-
export declare function verifyAuthentication(method: Authentication, credentials?: unknown): Promise<AuthenticationIdentity>;
|
|
26
|
+
export declare function verifyAuthentication(method: Authentication, credentials?: unknown, context?: AuthenticationContext): Promise<AuthenticationIdentity>;
|
|
22
27
|
/** Ephemeral material for one authorized driver invocation; never checkpoint this object. */
|
|
23
28
|
export interface CallAuthentication {
|
|
24
29
|
method: string;
|
|
@@ -42,7 +47,7 @@ export type Authentication<S extends z.ZodType = z.ZodType> = {
|
|
|
42
47
|
storage?: never;
|
|
43
48
|
version?: never;
|
|
44
49
|
schema?: never;
|
|
45
|
-
verify?(): Promise<AuthenticationIdentity>;
|
|
50
|
+
verify?(context?: AuthenticationContext): Promise<AuthenticationIdentity>;
|
|
46
51
|
};
|
|
47
52
|
export declare function defineAuthentication<S extends z.ZodType>(definition: Authentication<S>): Authentication<S>;
|
|
48
53
|
/** Typed access inside a driver. A missing or different method never falls back to the environment. */
|
package/dist/authentication.js
CHANGED
|
@@ -29,11 +29,11 @@ export function authenticationProblem(error) {
|
|
|
29
29
|
: undefined;
|
|
30
30
|
}
|
|
31
31
|
/** Shared by credential intake and execution; unknown provider failures never imply revocation. */
|
|
32
|
-
export async function verifyAuthentication(method, credentials) {
|
|
32
|
+
export async function verifyAuthentication(method, credentials, context) {
|
|
33
33
|
if (!method.verify)
|
|
34
34
|
throw new AuthenticationError("unsupported");
|
|
35
35
|
try {
|
|
36
|
-
const identity = await method.verify(credentials);
|
|
36
|
+
const identity = await (method.kind === "workload-identity" ? method.verify(context) : method.verify(credentials));
|
|
37
37
|
if (typeof identity?.account !== "string" ||
|
|
38
38
|
!identity.account.trim() ||
|
|
39
39
|
!Array.isArray(identity.scopes) ||
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"authentication.js","sourceRoot":"","sources":["../src/authentication.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,gBAAgB,EAAgC,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"authentication.js","sourceRoot":"","sources":["../src/authentication.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,gBAAgB,EAAgC,MAAM,mBAAmB,CAAC;AA2BnF,uFAAuF;AACvF,MAAM,OAAO,mBAAoB,SAAQ,KAAK;IACnC,IAAI,GAAG,uBAAuB,CAAC;IAC/B,SAAS,CAAU;IACnB,OAAO,CAAwB;IACxC,YAAY,OAA8B;QACxC,KAAK,CAAC,mBAAmB,OAAO,EAAE,CAAC,CAAC;QACpC,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,IAAI,GAAG,qBAAqB,CAAC;QAClC,IAAI,CAAC,SAAS,GAAG,OAAO,KAAK,aAAa,CAAC;IAC7C,CAAC;CACF;AAED,MAAM,sBAAsB,GAAG,IAAI,GAAG,CAAwB;IAC5D,SAAS;IACT,SAAS;IACT,SAAS;IACT,UAAU;IACV,eAAe;IACf,eAAe;IACf,WAAW;IACX,aAAa;IACb,aAAa;CACd,CAAC,CAAC;AAEH,MAAM,UAAU,qBAAqB,CAAC,KAAc;IAClD,MAAM,OAAO,GAAG,KAA4C,CAAC;IAC7D,OAAO,OAAO,EAAE,IAAI,KAAK,qBAAqB,IAAI,sBAAsB,CAAC,GAAG,CAAC,OAAO,CAAC,OAAQ,CAAC;QAC5F,CAAC,CAAC,OAAO,CAAC,OAAO;QACjB,CAAC,CAAC,SAAS,CAAC;AAChB,CAAC;AAED,mGAAmG;AACnG,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,MAAsB,EACtB,WAAqB,EACrB,OAA+B;IAE/B,IAAI,CAAC,MAAM,CAAC,MAAM;QAAE,MAAM,IAAI,mBAAmB,CAAC,aAAa,CAAC,CAAC;IACjE,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,KAAK,mBAAmB,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,CAAC;QACnH,IACE,OAAO,QAAQ,EAAE,OAAO,KAAK,QAAQ;YACrC,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,EAAE;YACxB,CAAC,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC;YAC/B,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,OAAO,KAAK,KAAK,QAAQ,CAAC;YAC1D,CAAC,QAAQ,CAAC,SAAS,KAAK,SAAS,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC;YAE1E,MAAM,IAAI,mBAAmB,CAAC,aAAa,CAAC,CAAC;QAC/C,IAAI,QAAQ,CAAC,SAAS,KAAK,SAAS,IAAI,QAAQ,CAAC,SAAS,IAAI,IAAI,CAAC,GAAG,EAAE;YAAE,MAAM,IAAI,mBAAmB,CAAC,SAAS,CAAC,CAAC;QACnH,OAAO;YACL,OAAO,EAAE,QAAQ,CAAC,OAAO;YACzB,MAAM,EAAE,CAAC,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ,EAAE;YAChD,GAAG,CAAC,QAAQ,CAAC,SAAS,KAAK,SAAS,IAAI,EAAE,SAAS,EAAE,QAAQ,CAAC,SAAS,EAAE,CAAC;SAC3E,CAAC;IACJ,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,0FAA0F;QAC1F,oFAAoF;QACpF,MAAM,IAAI,mBAAmB,CAAC,qBAAqB,CAAC,KAAK,CAAC,IAAI,aAAa,CAAC,CAAC;IAC/E,CAAC;AACH,CAAC;AA2BD,MAAM,UAAU,oBAAoB,CAAsB,UAA6B;IACrF,IAAI,UAAU,CAAC,MAAM,KAAK,SAAS,IAAI,OAAO,UAAU,CAAC,MAAM,KAAK,UAAU;QAC5E,MAAM,IAAI,gBAAgB,CAAC,uBAAuB,CAAC,CAAC;IACtD,IAAI,UAAU,CAAC,IAAI,KAAK,mBAAmB,EAAE,CAAC;QAC5C,IAAI,UAAU,CAAC,OAAO,KAAK,SAAS,IAAI,UAAU,CAAC,MAAM,KAAK,SAAS,IAAI,UAAU,CAAC,OAAO,KAAK,SAAS;YACzG,MAAM,IAAI,gBAAgB,CAAC,uBAAuB,CAAC,CAAC;QACtD,OAAO,MAAM,CAAC,MAAM,CAAC,EAAE,GAAG,UAAU,EAAE,CAAC,CAAC;IAC1C,CAAC;IACD,IACE,CAAC,CAAC,SAAS,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,CAAC,CAAC,QAAQ,CAAC,UAAU,CAAC,IAAI,CAAC;QACpE,CAAC,MAAM,CAAC,aAAa,CAAC,UAAU,CAAC,OAAO,CAAC;QACzC,UAAU,CAAC,OAAO,GAAG,CAAC;QACtB,OAAO,UAAU,CAAC,MAAM,EAAE,SAAS,KAAK,UAAU;QAClD,CAAC,UAAU,CAAC,OAAO;QACnB,CAAC,MAAM,CAAC,aAAa,CAAC,UAAU,CAAC,OAAO,CAAC,QAAQ,CAAC;QAClD,UAAU,CAAC,OAAO,CAAC,QAAQ,GAAG,CAAC;QAC/B,OAAO,UAAU,CAAC,OAAO,CAAC,QAAQ,KAAK,SAAS;QAChD,OAAO,UAAU,CAAC,OAAO,CAAC,iBAAiB,KAAK,SAAS,EACzD,CAAC;QACD,MAAM,IAAI,gBAAgB,CAAC,uBAAuB,CAAC,CAAC;IACtD,CAAC;IACD,OAAO,MAAM,CAAC,MAAM,CAAC,EAAE,GAAG,UAAU,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,EAAE,GAAG,UAAU,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC;AAC7F,CAAC;AAED,uGAAuG;AACvG,MAAM,UAAU,yBAAyB,CACvC,MAAc,EACd,UAA6B,EAC7B,IAAiB;IAEjB,IACE,UAAU,CAAC,IAAI,KAAK,mBAAmB;QACvC,IAAI,CAAC,cAAc,EAAE,MAAM,KAAK,MAAM;QACtC,IAAI,CAAC,cAAc,CAAC,IAAI,KAAK,UAAU,CAAC,IAAI;QAE5C,MAAM,IAAI,gBAAgB,CAAC,aAAa,CAAC,CAAC;IAC5C,IAAI,CAAC;QACH,OAAO,UAAU,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,cAAc,CAAC,WAAW,CAAC,CAAC;IAClE,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,gBAAgB,CAAC,eAAe,CAAC,CAAC;IAC9C,CAAC;AACH,CAAC;AAED,kGAAkG;AAClG,MAAM,UAAU,wBAAwB,CACtC,IAAoB,EACpB,KAA8B,EAC9B,UAAiC;IAEjC,IAAI,IAAI,CAAC,IAAI,KAAK,mBAAmB;QAAE,OAAO;IAC9C,IAAI,IAAI,CAAC,OAAO,CAAC,QAAQ,GAAG,KAAK,CAAC,QAAQ;QAAE,MAAM,IAAI,gBAAgB,CAAC,WAAW,CAAC,CAAC;IACpF,IAAI,IAAI,CAAC,OAAO,CAAC,QAAQ,IAAI,CAAC,UAAU,KAAK,UAAU,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC;QAAE,MAAM,IAAI,gBAAgB,CAAC,WAAW,CAAC,CAAC;IACrH,IAAI,IAAI,CAAC,OAAO,CAAC,iBAAiB,IAAI,KAAK,CAAC,iBAAiB,KAAK,MAAM;QAAE,MAAM,IAAI,gBAAgB,CAAC,aAAa,CAAC,CAAC;AACtH,CAAC;AAED,mGAAmG;AACnG,MAAM,UAAU,iBAAiB,CAAsB,IAAuB,EAAE,KAAiB;IAC/F,IAAI,IAAI,CAAC,IAAI,KAAK,mBAAmB;QAAE,MAAM,IAAI,gBAAgB,CAAC,aAAa,CAAC,CAAC;IACjF,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;QAC5C,IAAI,CAAC,MAAM,CAAC,OAAO;YAAE,MAAM,IAAI,gBAAgB,CAAC,eAAe,CAAC,CAAC;QACjE,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,KAAK,EAAE,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC;QAC9E,IAAI,MAAM,CAAC,UAAU,CAAC,OAAO,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ;YAAE,MAAM,IAAI,gBAAgB,CAAC,WAAW,CAAC,CAAC;QAChG,OAAO,OAAO,CAAC;IACjB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,KAAK,YAAY,gBAAgB;YAAE,MAAM,IAAI,gBAAgB,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAC9E,MAAM,IAAI,gBAAgB,CAAC,eAAe,CAAC,CAAC;IAC9C,CAAC;AACH,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAsB,IAAuB,EAAE,OAAe;IAC7F,IAAI,IAAI,CAAC,IAAI,KAAK,mBAAmB;QAAE,MAAM,IAAI,gBAAgB,CAAC,aAAa,CAAC,CAAC;IACjF,IAAI,CAAC;QACH,IAAI,MAAM,CAAC,UAAU,CAAC,OAAO,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ;YAAE,MAAM,IAAI,gBAAgB,CAAC,WAAW,CAAC,CAAC;QAChG,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QACnC,IAAI,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC,OAAO;YAAE,MAAM,IAAI,gBAAgB,CAAC,eAAe,CAAC,CAAC;QAClF,OAAO,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IACzC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,KAAK,YAAY,gBAAgB;YAAE,MAAM,IAAI,gBAAgB,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAC9E,MAAM,IAAI,gBAAgB,CAAC,eAAe,CAAC,CAAC;IAC9C,CAAC;AACH,CAAC"}
|
package/dist/client.js
CHANGED
|
@@ -947,7 +947,7 @@ export class SanomaClient {
|
|
|
947
947
|
: external
|
|
948
948
|
? existing.value
|
|
949
949
|
: encodeCredentials(method, credentials);
|
|
950
|
-
const identity = await verifyAuthentication(method, encoded === undefined ? undefined : decodeCredentials(method, encoded));
|
|
950
|
+
const identity = await verifyAuthentication(method, encoded === undefined ? undefined : decodeCredentials(method, encoded), { connectionId: setup.id, vendor: setup.vendor });
|
|
951
951
|
if (setup.expectedAccount && setup.expectedAccount !== identity.account)
|
|
952
952
|
throw new AuthenticationError("wrong_account");
|
|
953
953
|
if (setup.requiredScopes.some((scope) => !identity.scopes.includes(scope)))
|
|
@@ -999,7 +999,7 @@ export class SanomaClient {
|
|
|
999
999
|
: external
|
|
1000
1000
|
? existing.value
|
|
1001
1001
|
: encodeCredentials(method, credentials);
|
|
1002
|
-
const identity = await verifyAuthentication(method, encoded === undefined ? undefined : decodeCredentials(method, encoded));
|
|
1002
|
+
const identity = await verifyAuthentication(method, encoded === undefined ? undefined : decodeCredentials(method, encoded), { connectionId: setup.id, vendor: setup.vendor });
|
|
1003
1003
|
if (identity.account !== review.account)
|
|
1004
1004
|
throw new AuthenticationError("wrong_account");
|
|
1005
1005
|
if (JSON.stringify(identity.scopes) !== JSON.stringify([...new Set(review.scopes)].toSorted()))
|