@retinue/agentkit 0.2.0 → 0.3.0
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/README.md +1 -1
- package/dist/adapters/audio/openai.d.ts +36 -0
- package/dist/adapters/audio/openai.js +136 -0
- package/dist/adapters/bullmq/lock.d.ts +26 -2
- package/dist/adapters/memory/connections.d.ts +10 -0
- package/dist/adapters/memory/connections.js +87 -0
- package/dist/adapters/memory/graph.d.ts +17 -0
- package/dist/adapters/memory/graph.js +267 -0
- package/dist/adapters/memory/index.d.ts +3 -0
- package/dist/adapters/memory/index.js +3 -0
- package/dist/adapters/memory/rate-limit.d.ts +14 -0
- package/dist/adapters/memory/rate-limit.js +28 -0
- package/dist/adapters/memory/runtime.js +0 -0
- package/dist/adapters/postgres/connections.d.ts +15 -0
- package/dist/adapters/postgres/connections.js +134 -0
- package/dist/adapters/postgres/graph.d.ts +24 -0
- package/dist/adapters/postgres/graph.js +325 -0
- package/dist/adapters/postgres/index.d.ts +2 -0
- package/dist/adapters/postgres/index.js +2 -0
- package/dist/adapters/postgres/migrations.js +198 -0
- package/dist/adapters/postgres/run-store.js +26 -2
- package/dist/adapters/postgres/schema.d.ts +61 -2
- package/dist/adapters/postgres/schema.js +95 -10
- package/dist/adapters/redis/index.d.ts +1 -0
- package/dist/adapters/redis/index.js +1 -0
- package/dist/adapters/redis/rate-limit.d.ts +46 -0
- package/dist/adapters/redis/rate-limit.js +55 -0
- package/dist/adapters/supabase/index.d.ts +15 -0
- package/dist/adapters/supabase/index.js +15 -0
- package/dist/adapters/supabase/rls.js +33 -0
- package/dist/agents/agent.d.ts +78 -1
- package/dist/agents/agent.js +118 -5
- package/dist/agents/define.js +24 -4
- package/dist/agents/engine.d.ts +20 -0
- package/dist/agents/engine.js +177 -6
- package/dist/audio/index.d.ts +151 -0
- package/dist/audio/index.js +183 -0
- package/dist/connections/cipher.d.ts +103 -0
- package/dist/connections/cipher.js +141 -0
- package/dist/connections/index.d.ts +156 -0
- package/dist/connections/index.js +72 -0
- package/dist/connections/oauth/client.d.ts +84 -0
- package/dist/connections/oauth/client.js +141 -0
- package/dist/connections/oauth/index.d.ts +166 -0
- package/dist/connections/oauth/index.js +226 -0
- package/dist/connections/oauth/service.d.ts +90 -0
- package/dist/connections/oauth/service.js +132 -0
- package/dist/connections/pause.d.ts +68 -0
- package/dist/connections/pause.js +97 -0
- package/dist/connections/resolver.d.ts +59 -0
- package/dist/connections/resolver.js +138 -0
- package/dist/connections/resume.d.ts +64 -0
- package/dist/connections/resume.js +99 -0
- package/dist/connections/vault-cipher.d.ts +95 -0
- package/dist/connections/vault-cipher.js +206 -0
- package/dist/core/content-parts.d.ts +18 -2
- package/dist/core/content-parts.js +1 -0
- package/dist/core/context.d.ts +25 -0
- package/dist/core/errors.d.ts +1 -1
- package/dist/core/errors.js +15 -0
- package/dist/core/events.d.ts +44 -2
- package/dist/core/events.js +31 -2
- package/dist/core/validation.js +19 -0
- package/dist/entries/connections.d.ts +10 -0
- package/dist/entries/connections.js +10 -0
- package/dist/entries/knowledge.d.ts +8 -0
- package/dist/entries/knowledge.js +8 -0
- package/dist/entries/mcp-server.d.ts +9 -0
- package/dist/entries/mcp-server.js +9 -0
- package/dist/entries/runtime.d.ts +10 -0
- package/dist/entries/runtime.js +8 -0
- package/dist/entries/testing.d.ts +28 -0
- package/dist/entries/testing.js +28 -0
- package/dist/graphql/resolvers.d.ts +3 -1
- package/dist/graphql/resolvers.js +14 -0
- package/dist/knowledge/communities.d.ts +166 -0
- package/dist/knowledge/communities.js +377 -0
- package/dist/knowledge/graph-global.d.ts +153 -0
- package/dist/knowledge/graph-global.js +246 -0
- package/dist/knowledge/graph-retrieval.d.ts +150 -0
- package/dist/knowledge/graph-retrieval.js +303 -0
- package/dist/knowledge/graph.d.ts +223 -0
- package/dist/knowledge/graph.js +369 -0
- package/dist/knowledge/index.d.ts +24 -0
- package/dist/knowledge/index.js +36 -1
- package/dist/knowledge/retrieval.d.ts +26 -1
- package/dist/knowledge/retrieval.js +77 -1
- package/dist/mcp-server/index.d.ts +168 -0
- package/dist/mcp-server/index.js +175 -0
- package/dist/models/extraction.d.ts +73 -0
- package/dist/models/extraction.js +118 -0
- package/dist/models/index.d.ts +40 -1
- package/dist/models/index.js +22 -1
- package/dist/models/pricing.d.ts +2 -0
- package/dist/models/pricing.js +17 -1
- package/dist/models/provider-factory.js +9 -7
- package/dist/models/streaming.d.ts +89 -0
- package/dist/models/streaming.js +179 -2
- package/dist/persistence/index.d.ts +230 -0
- package/dist/persistence/index.js +2 -0
- package/dist/runtime/index.d.ts +1 -1
- package/dist/runtime/index.js +12 -0
- package/dist/runtime/worker.d.ts +36 -1
- package/dist/runtime/worker.js +37 -4
- package/dist/security/checklist.js +46 -0
- package/dist/server/bin.d.ts +19 -0
- package/dist/server/bin.js +226 -0
- package/dist/server/boot.d.ts +20 -2
- package/dist/server/boot.js +29 -3
- package/dist/server/cli.d.ts +13 -0
- package/dist/server/cli.js +7 -4
- package/dist/server/doctor.d.ts +74 -0
- package/dist/server/doctor.js +280 -0
- package/dist/skills/context.d.ts +61 -0
- package/dist/skills/context.js +107 -0
- package/dist/skills/index.d.ts +1 -0
- package/dist/skills/index.js +1 -0
- package/dist/telemetry/spans.js +5 -0
- package/dist/testing/conformance/artifact-exports.d.ts +20 -0
- package/dist/testing/conformance/artifact-exports.js +172 -0
- package/dist/testing/conformance/artifacts.d.ts +11 -0
- package/dist/testing/conformance/artifacts.js +316 -0
- package/dist/testing/conformance/audio.d.ts +38 -0
- package/dist/testing/conformance/audio.js +145 -0
- package/dist/testing/conformance/capability.d.ts +23 -0
- package/dist/testing/conformance/capability.js +27 -0
- package/dist/testing/conformance/checkpoint-store.d.ts +15 -0
- package/dist/testing/conformance/checkpoint-store.js +72 -0
- package/dist/testing/conformance/connections.d.ts +14 -0
- package/dist/testing/conformance/connections.js +171 -0
- package/dist/testing/conformance/conversation-store.d.ts +10 -0
- package/dist/testing/conformance/conversation-store.js +60 -0
- package/dist/testing/conformance/evaluation.d.ts +10 -0
- package/dist/testing/conformance/evaluation.js +295 -0
- package/dist/testing/conformance/files.d.ts +19 -0
- package/dist/testing/conformance/files.js +454 -0
- package/dist/testing/conformance/flows.d.ts +16 -0
- package/dist/testing/conformance/flows.js +193 -0
- package/dist/testing/conformance/graph.d.ts +22 -0
- package/dist/testing/conformance/graph.js +500 -0
- package/dist/testing/conformance/hitl.d.ts +25 -0
- package/dist/testing/conformance/hitl.js +523 -0
- package/dist/testing/conformance/index.d.ts +163 -0
- package/dist/testing/conformance/index.js +321 -0
- package/dist/testing/conformance/invariants.d.ts +23 -0
- package/dist/testing/conformance/invariants.js +80 -0
- package/dist/testing/conformance/knowledge.d.ts +41 -0
- package/dist/testing/conformance/knowledge.js +592 -0
- package/dist/testing/conformance/parents.d.ts +50 -0
- package/dist/testing/conformance/parents.js +39 -0
- package/dist/testing/conformance/rate-limit.d.ts +25 -0
- package/dist/testing/conformance/rate-limit.js +71 -0
- package/dist/testing/conformance/records.d.ts +36 -0
- package/dist/testing/conformance/records.js +400 -0
- package/dist/testing/conformance/rollups.d.ts +30 -0
- package/dist/testing/conformance/rollups.js +425 -0
- package/dist/testing/conformance/run-coordinator.d.ts +27 -0
- package/dist/testing/conformance/run-coordinator.js +150 -0
- package/dist/testing/conformance/run-event-log.d.ts +9 -0
- package/dist/testing/conformance/run-event-log.js +138 -0
- package/dist/testing/conformance/run-store.d.ts +12 -0
- package/dist/testing/conformance/run-store.js +294 -0
- package/dist/testing/conformance/session-state.d.ts +38 -0
- package/dist/testing/conformance/session-state.js +198 -0
- package/dist/testing/conformance/usage-limits.d.ts +10 -0
- package/dist/testing/conformance/usage-limits.js +309 -0
- package/dist/testing/conformance.d.ts +8 -0
- package/dist/testing/conformance.js +8 -0
- package/dist/testing/memory-backend.d.ts +39 -0
- package/dist/testing/memory-backend.js +41 -0
- package/dist/testing/pglite.d.ts +55 -0
- package/dist/testing/pglite.js +78 -0
- package/dist/testing/stub-model.d.ts +58 -0
- package/dist/testing/stub-model.js +71 -0
- package/dist/testing/supabase-storage-double.d.ts +35 -0
- package/dist/testing/supabase-storage-double.js +101 -0
- package/dist/toolkit/http.d.ts +27 -0
- package/dist/toolkit/http.js +48 -0
- package/dist/toolkit/index.d.ts +27 -1
- package/dist/toolkit/index.js +24 -1
- package/dist/toolkit/mime.d.ts +97 -0
- package/dist/toolkit/mime.js +233 -0
- package/dist/toolkit/ssrf.d.ts +139 -0
- package/dist/toolkit/ssrf.js +349 -0
- package/dist/toolkit/vendor.d.ts +91 -0
- package/dist/toolkit/vendor.js +153 -0
- package/dist/tools/credentials.d.ts +265 -6
- package/dist/tools/credentials.js +318 -3
- package/dist/tools/define.d.ts +14 -0
- package/dist/tools/define.js +3 -0
- package/dist/tools/index.d.ts +21 -0
- package/dist/tools/index.js +7 -0
- package/dist/tools/library/audio.d.ts +63 -0
- package/dist/tools/library/audio.js +148 -0
- package/dist/tools/library/index.d.ts +8 -0
- package/dist/tools/library/index.js +7 -0
- package/dist/tools/registry.d.ts +33 -0
- package/dist/tools/registry.js +72 -2
- package/dist/usage/index.d.ts +21 -0
- package/dist/usage/index.js +1 -0
- package/dist/usage/rate-limit.d.ts +122 -0
- package/dist/usage/rate-limit.js +131 -0
- package/dist/usage/recorder.js +3 -0
- package/package.json +29 -2
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `SecretCipher` — encryption at rest for third-party credentials, REQ-063 (#259), task #261.
|
|
3
|
+
*
|
|
4
|
+
* `docs/21`'s Connections section sets the requirement, and it is precise enough to decide the design:
|
|
5
|
+
*
|
|
6
|
+
* > encryption at rest with **a key the application database cannot decrypt on its own**
|
|
7
|
+
*
|
|
8
|
+
* That sentence rules out the two obvious implementations.
|
|
9
|
+
*
|
|
10
|
+
* ## Why not `pgcrypto`
|
|
11
|
+
*
|
|
12
|
+
* `pgp_sym_encrypt(data, key)` needs the key passed into each query. A key stored in the same database as the
|
|
13
|
+
* ciphertext satisfies nothing — it protects against a stolen backup and nothing else, and the sentence above
|
|
14
|
+
* rules it out by name. A key passed as a SQL literal is worse: it can land in `pg_stat_statements`, in the
|
|
15
|
+
* server log and in a slow-query log, which are three places nobody thinks of as credential stores.
|
|
16
|
+
*
|
|
17
|
+
* `pgsodium` and its transparent column encryption are deprecated; do not build on them.
|
|
18
|
+
*
|
|
19
|
+
* ## Why not Supabase Vault alone
|
|
20
|
+
*
|
|
21
|
+
* Supabase Vault is genuinely good — libsodium, and a root key held outside the database, so it satisfies the
|
|
22
|
+
* requirement. It is also available only on Supabase. Building on it would leave the Postgres and in-memory
|
|
23
|
+
* adapters storing plaintext, so the conformance suite would be asserting *different guarantees per adapter* —
|
|
24
|
+
* which is the one thing a conformance suite must never do.
|
|
25
|
+
*
|
|
26
|
+
* ## So: encrypt in the application, behind a seam
|
|
27
|
+
*
|
|
28
|
+
* The store holds an opaque ciphertext blob and knows nothing about how it was produced. Every adapter family
|
|
29
|
+
* gets the identical property, no key passes through SQL, and Vault becomes one *implementation* of this seam
|
|
30
|
+
* rather than the foundation — a choice a deployment makes, not a dependency it inherits.
|
|
31
|
+
*
|
|
32
|
+
* The honest trade-off, which belongs in the docs rather than in a footnote: app-side encryption means the
|
|
33
|
+
* application process holds the key in memory. Vault means the database performs the decryption and therefore
|
|
34
|
+
* sees plaintext. Neither is strictly better, and a deployment should pick knowing that.
|
|
35
|
+
*
|
|
36
|
+
* **The Vault implementation shipped in #268**, in `vault-cipher.ts`, and every clause of this file's test
|
|
37
|
+
* suite was run against a real Vault before it landed — which is why it was held back until one was available.
|
|
38
|
+
* Two properties differ from app-side sealing and are argued there rather than here: a Vault-sealed row holds
|
|
39
|
+
* a **pointer, not ciphertext**, which changes what a backup contains; and a pointer cannot be authenticated
|
|
40
|
+
* by what it points at, so integrity is provided by a binding token rather than by AEAD.
|
|
41
|
+
*/
|
|
42
|
+
/**
|
|
43
|
+
* A stored secret: ciphertext plus what is needed to read it back.
|
|
44
|
+
*
|
|
45
|
+
* `keyId` is the reason rotation is possible at all — see `createAesGcmCipher`. Everything here is safe to
|
|
46
|
+
* store beside the ciphertext; none of it is secret on its own.
|
|
47
|
+
*/
|
|
48
|
+
export type SealedSecret = {
|
|
49
|
+
/** Which key sealed this. A design with no key identity has a first key that is permanent. */
|
|
50
|
+
readonly keyId: string;
|
|
51
|
+
/** Algorithm, so a future one can be added without guessing what old rows used. */
|
|
52
|
+
readonly algorithm: string;
|
|
53
|
+
/** Base64. */
|
|
54
|
+
readonly nonce: string;
|
|
55
|
+
/** Base64, including the authentication tag. */
|
|
56
|
+
readonly ciphertext: string;
|
|
57
|
+
};
|
|
58
|
+
export interface SecretCipher {
|
|
59
|
+
seal(plaintext: string): Promise<SealedSecret>;
|
|
60
|
+
open(sealed: SealedSecret): Promise<string>;
|
|
61
|
+
/** The key new secrets are sealed with. Exposed so a rotation job can tell what still needs re-sealing. */
|
|
62
|
+
currentKeyId(): string;
|
|
63
|
+
/**
|
|
64
|
+
* Destroy whatever the sealed value refers to — **optional, and found by #268.**
|
|
65
|
+
*
|
|
66
|
+
* App-side sealing has nothing to implement: the ciphertext *is* the stored value, so deleting the row
|
|
67
|
+
* deletes the secret. A cipher that stores a **pointer** — Supabase Vault returns an id and keeps the
|
|
68
|
+
* ciphertext itself — does not, and dropping a connection row there leaves the credential behind, which is
|
|
69
|
+
* the opposite of what `docs/18` promises.
|
|
70
|
+
*
|
|
71
|
+
* Optional rather than required because most implementations genuinely have nothing to do, and a required
|
|
72
|
+
* no-op is a method every future cipher has to write in order to say "not applicable". A caller deleting a
|
|
73
|
+
* connection should call it when present.
|
|
74
|
+
*/
|
|
75
|
+
forget?(sealed: SealedSecret): Promise<void>;
|
|
76
|
+
}
|
|
77
|
+
export declare const AES_GCM: "aes-256-gcm";
|
|
78
|
+
export type AesKey = {
|
|
79
|
+
readonly id: string;
|
|
80
|
+
/** 32 bytes. A shorter key is a configuration error, not a weaker mode. */
|
|
81
|
+
readonly key: Buffer;
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* AES-256-GCM, with a key set rather than a key.
|
|
85
|
+
*
|
|
86
|
+
* **Rotation is why this takes a list.** A design with one key has a first key that is permanent: re-encrypting
|
|
87
|
+
* every stored secret requires reading them, which requires the old key, which the process no longer has. So
|
|
88
|
+
* every sealed secret records its `keyId`, `open` looks the key up, and rotation is: add a new key, make it
|
|
89
|
+
* current, re-seal in the background, retire the old one. A deployment that never rotates passes one key and
|
|
90
|
+
* notices nothing.
|
|
91
|
+
*
|
|
92
|
+
* **Authenticated encryption, and the authentication is the point.** GCM's tag means a tampered ciphertext
|
|
93
|
+
* *fails* rather than decrypting to something else. Without it, a byte flipped in a database row would produce
|
|
94
|
+
* garbage that gets sent to a vendor as a token — a failure that looks like an expired credential and is not.
|
|
95
|
+
*/
|
|
96
|
+
export declare const createAesGcmCipher: (input: {
|
|
97
|
+
readonly keys: readonly AesKey[];
|
|
98
|
+
readonly currentKeyId?: string;
|
|
99
|
+
readonly randomNonce?: () => Buffer;
|
|
100
|
+
}) => SecretCipher;
|
|
101
|
+
/** Constant-time comparison, for callers checking a sealed secret against another without leaking timing. */
|
|
102
|
+
export declare const sealedEquals: (a: SealedSecret, b: SealedSecret) => boolean;
|
|
103
|
+
//# sourceMappingURL=cipher.d.ts.map
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `SecretCipher` — encryption at rest for third-party credentials, REQ-063 (#259), task #261.
|
|
3
|
+
*
|
|
4
|
+
* `docs/21`'s Connections section sets the requirement, and it is precise enough to decide the design:
|
|
5
|
+
*
|
|
6
|
+
* > encryption at rest with **a key the application database cannot decrypt on its own**
|
|
7
|
+
*
|
|
8
|
+
* That sentence rules out the two obvious implementations.
|
|
9
|
+
*
|
|
10
|
+
* ## Why not `pgcrypto`
|
|
11
|
+
*
|
|
12
|
+
* `pgp_sym_encrypt(data, key)` needs the key passed into each query. A key stored in the same database as the
|
|
13
|
+
* ciphertext satisfies nothing — it protects against a stolen backup and nothing else, and the sentence above
|
|
14
|
+
* rules it out by name. A key passed as a SQL literal is worse: it can land in `pg_stat_statements`, in the
|
|
15
|
+
* server log and in a slow-query log, which are three places nobody thinks of as credential stores.
|
|
16
|
+
*
|
|
17
|
+
* `pgsodium` and its transparent column encryption are deprecated; do not build on them.
|
|
18
|
+
*
|
|
19
|
+
* ## Why not Supabase Vault alone
|
|
20
|
+
*
|
|
21
|
+
* Supabase Vault is genuinely good — libsodium, and a root key held outside the database, so it satisfies the
|
|
22
|
+
* requirement. It is also available only on Supabase. Building on it would leave the Postgres and in-memory
|
|
23
|
+
* adapters storing plaintext, so the conformance suite would be asserting *different guarantees per adapter* —
|
|
24
|
+
* which is the one thing a conformance suite must never do.
|
|
25
|
+
*
|
|
26
|
+
* ## So: encrypt in the application, behind a seam
|
|
27
|
+
*
|
|
28
|
+
* The store holds an opaque ciphertext blob and knows nothing about how it was produced. Every adapter family
|
|
29
|
+
* gets the identical property, no key passes through SQL, and Vault becomes one *implementation* of this seam
|
|
30
|
+
* rather than the foundation — a choice a deployment makes, not a dependency it inherits.
|
|
31
|
+
*
|
|
32
|
+
* The honest trade-off, which belongs in the docs rather than in a footnote: app-side encryption means the
|
|
33
|
+
* application process holds the key in memory. Vault means the database performs the decryption and therefore
|
|
34
|
+
* sees plaintext. Neither is strictly better, and a deployment should pick knowing that.
|
|
35
|
+
*
|
|
36
|
+
* **The Vault implementation shipped in #268**, in `vault-cipher.ts`, and every clause of this file's test
|
|
37
|
+
* suite was run against a real Vault before it landed — which is why it was held back until one was available.
|
|
38
|
+
* Two properties differ from app-side sealing and are argued there rather than here: a Vault-sealed row holds
|
|
39
|
+
* a **pointer, not ciphertext**, which changes what a backup contains; and a pointer cannot be authenticated
|
|
40
|
+
* by what it points at, so integrity is provided by a binding token rather than by AEAD.
|
|
41
|
+
*/
|
|
42
|
+
import { createCipheriv, createDecipheriv, randomBytes, timingSafeEqual } from "node:crypto";
|
|
43
|
+
import { AgentPlatformError } from "../core/errors.js";
|
|
44
|
+
export const AES_GCM = "aes-256-gcm";
|
|
45
|
+
/** 96 bits, which is what GCM is specified for; a different length changes the security argument. */
|
|
46
|
+
const NONCE_BYTES = 12;
|
|
47
|
+
const TAG_BYTES = 16;
|
|
48
|
+
/**
|
|
49
|
+
* AES-256-GCM, with a key set rather than a key.
|
|
50
|
+
*
|
|
51
|
+
* **Rotation is why this takes a list.** A design with one key has a first key that is permanent: re-encrypting
|
|
52
|
+
* every stored secret requires reading them, which requires the old key, which the process no longer has. So
|
|
53
|
+
* every sealed secret records its `keyId`, `open` looks the key up, and rotation is: add a new key, make it
|
|
54
|
+
* current, re-seal in the background, retire the old one. A deployment that never rotates passes one key and
|
|
55
|
+
* notices nothing.
|
|
56
|
+
*
|
|
57
|
+
* **Authenticated encryption, and the authentication is the point.** GCM's tag means a tampered ciphertext
|
|
58
|
+
* *fails* rather than decrypting to something else. Without it, a byte flipped in a database row would produce
|
|
59
|
+
* garbage that gets sent to a vendor as a token — a failure that looks like an expired credential and is not.
|
|
60
|
+
*/
|
|
61
|
+
export const createAesGcmCipher = (input) => {
|
|
62
|
+
if (input.keys.length === 0)
|
|
63
|
+
throw new Error("SecretCipher needs at least one key");
|
|
64
|
+
for (const { id, key } of input.keys) {
|
|
65
|
+
if (key.length !== 32)
|
|
66
|
+
throw new Error(`SecretCipher key "${id}" is ${key.length} bytes; AES-256 needs 32`);
|
|
67
|
+
}
|
|
68
|
+
const byId = new Map(input.keys.map((k) => [k.id, k.key]));
|
|
69
|
+
const currentId = input.currentKeyId ?? input.keys[0].id;
|
|
70
|
+
if (!byId.has(currentId))
|
|
71
|
+
throw new Error(`SecretCipher currentKeyId "${currentId}" is not among the keys`);
|
|
72
|
+
const nonceOf = input.randomNonce ?? (() => randomBytes(NONCE_BYTES));
|
|
73
|
+
return {
|
|
74
|
+
currentKeyId: () => currentId,
|
|
75
|
+
async seal(plaintext) {
|
|
76
|
+
const nonce = nonceOf();
|
|
77
|
+
if (nonce.length !== NONCE_BYTES)
|
|
78
|
+
throw new Error(`GCM nonce must be ${NONCE_BYTES} bytes`);
|
|
79
|
+
const cipher = createCipheriv(AES_GCM, byId.get(currentId), nonce);
|
|
80
|
+
const body = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
|
|
81
|
+
// Tag appended rather than stored separately: one field cannot be reunited with the wrong other field.
|
|
82
|
+
return {
|
|
83
|
+
keyId: currentId,
|
|
84
|
+
algorithm: AES_GCM,
|
|
85
|
+
nonce: nonce.toString("base64"),
|
|
86
|
+
ciphertext: Buffer.concat([body, cipher.getAuthTag()]).toString("base64"),
|
|
87
|
+
};
|
|
88
|
+
},
|
|
89
|
+
async open(sealed) {
|
|
90
|
+
if (sealed.algorithm !== AES_GCM)
|
|
91
|
+
throw new AgentPlatformError({
|
|
92
|
+
code: "capability_unavailable",
|
|
93
|
+
message: `sealed with ${sealed.algorithm}, which this cipher cannot open`,
|
|
94
|
+
retryable: false,
|
|
95
|
+
});
|
|
96
|
+
const key = byId.get(sealed.keyId);
|
|
97
|
+
if (key === undefined)
|
|
98
|
+
throw new AgentPlatformError({
|
|
99
|
+
code: "capability_unavailable",
|
|
100
|
+
// Named, because the remedy is specific: the key was retired before everything sealed with it was
|
|
101
|
+
// re-sealed, and a rotation job needs to know which one to put back.
|
|
102
|
+
message: `no key "${sealed.keyId}" is configured, so this secret cannot be opened. It was sealed with a key ` +
|
|
103
|
+
"this process does not have — retire a key only after re-sealing everything that used it.",
|
|
104
|
+
retryable: false,
|
|
105
|
+
});
|
|
106
|
+
const raw = Buffer.from(sealed.ciphertext, "base64");
|
|
107
|
+
if (raw.length < TAG_BYTES)
|
|
108
|
+
throw new AgentPlatformError({ code: "invalid_input", message: "ciphertext is too short to carry a tag", retryable: false });
|
|
109
|
+
const decipher = createDecipheriv(AES_GCM, key, Buffer.from(sealed.nonce, "base64"));
|
|
110
|
+
decipher.setAuthTag(raw.subarray(raw.length - TAG_BYTES));
|
|
111
|
+
try {
|
|
112
|
+
return Buffer.concat([decipher.update(raw.subarray(0, raw.length - TAG_BYTES)), decipher.final()]).toString("utf8");
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
/**
|
|
116
|
+
* A tampered or corrupt secret **fails**, and this is the branch that matters.
|
|
117
|
+
*
|
|
118
|
+
* Without authentication a flipped byte decrypts to garbage, and garbage gets sent to a vendor as a
|
|
119
|
+
* token — which looks like an expired credential and sends an operator to rotate something that was
|
|
120
|
+
* never wrong. The message says what happened rather than what it looks like.
|
|
121
|
+
*/
|
|
122
|
+
throw new AgentPlatformError({
|
|
123
|
+
code: "invalid_input",
|
|
124
|
+
message: "this secret failed authentication: it was modified after being sealed, or was sealed with a " +
|
|
125
|
+
"different key. It is deliberately not returned — an unauthenticated decryption yields bytes that " +
|
|
126
|
+
"would be sent to a vendor as a credential.",
|
|
127
|
+
retryable: false,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
};
|
|
133
|
+
/** Constant-time comparison, for callers checking a sealed secret against another without leaking timing. */
|
|
134
|
+
export const sealedEquals = (a, b) => {
|
|
135
|
+
if (a.keyId !== b.keyId || a.algorithm !== b.algorithm || a.nonce !== b.nonce)
|
|
136
|
+
return false;
|
|
137
|
+
const left = Buffer.from(a.ciphertext, "base64");
|
|
138
|
+
const right = Buffer.from(b.ciphertext, "base64");
|
|
139
|
+
return left.length === right.length && timingSafeEqual(left, right);
|
|
140
|
+
};
|
|
141
|
+
//# sourceMappingURL=cipher.js.map
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Connections — a tenant's link to a third-party provider. REQ-063 (#259), task #261.
|
|
3
|
+
*
|
|
4
|
+
* `docs/21` assigned this to the platform repository and `docs/21`'s amendment moved the runtime half here,
|
|
5
|
+
* because eight toolkits ship in this package and none of them was usable by a second tenant: every one takes a
|
|
6
|
+
* `credentialRef` and the only resolver that shipped was a static map.
|
|
7
|
+
*
|
|
8
|
+
* ## What a connection is, and is not
|
|
9
|
+
*
|
|
10
|
+
* It is **one tenant's authorisation to reach one provider**. It is not a toolkit's configuration: a base URL
|
|
11
|
+
* or a page size is the same for every tenant and belongs where the toolkit is wired. The dividing line is
|
|
12
|
+
* whether two tenants using the same toolkit would differ, and for a credential they always do.
|
|
13
|
+
*
|
|
14
|
+
* ## Several per tenant, per provider
|
|
15
|
+
*
|
|
16
|
+
* A customer with three GitHub organisations or two Slack workspaces is the normal case, not an edge one. So a
|
|
17
|
+
* connection has its own id, a provider, and an optional label a person chose — and `credentialRef` addresses a
|
|
18
|
+
* **connection**, with a per-tenant default per provider so the common single-connection case stays a one-word
|
|
19
|
+
* ref.
|
|
20
|
+
*
|
|
21
|
+
* ## The secret is opaque here
|
|
22
|
+
*
|
|
23
|
+
* The store holds a `SealedSecret` and cannot read it. That is what lets the same port sit over Postgres,
|
|
24
|
+
* Supabase and memory with the identical guarantee, and it is why `SecretCipher` is a separate seam rather than
|
|
25
|
+
* a detail of one adapter — see `cipher.ts` for why `pgcrypto` and Vault-only were both rejected.
|
|
26
|
+
*/
|
|
27
|
+
import type { TenantScope } from "../core/context.js";
|
|
28
|
+
import type { AuthMode, CredentialScheme } from "../tools/credentials.js";
|
|
29
|
+
import type { SealedSecret } from "./cipher.js";
|
|
30
|
+
export type ConnectionId = string;
|
|
31
|
+
/**
|
|
32
|
+
* A tenant's connection to a provider.
|
|
33
|
+
*
|
|
34
|
+
* Everything here is safe to read and to log, including `sealed` — which is ciphertext, not a secret. The name
|
|
35
|
+
* is the point: a connection list is a screen a person looks at, and rendering it must not need a key.
|
|
36
|
+
*/
|
|
37
|
+
/**
|
|
38
|
+
* What a stored row *is* — REQ-063 (#259), task #263.
|
|
39
|
+
*
|
|
40
|
+
* A tenant's own OAuth **app registration** is the same kind of record as a connection: tenant-scoped,
|
|
41
|
+
* provider-keyed, holding exactly one sealed secret, with metadata beside it, revocable, and covered by the same
|
|
42
|
+
* retention promise. So it is the same table with a discriminator rather than a second port, a second adapter
|
|
43
|
+
* family, a second migration, a second RLS policy and a second conformance suite for a record of identical
|
|
44
|
+
* shape.
|
|
45
|
+
*
|
|
46
|
+
* The discriminator is load-bearing, not cosmetic: `list` and `get` return **connections only** unless asked
|
|
47
|
+
* otherwise, so a credential resolver can never hand a toolkit a client secret by accident.
|
|
48
|
+
*/
|
|
49
|
+
export declare const CONNECTION_KINDS: readonly ["connection", "oauth-app"];
|
|
50
|
+
export type ConnectionKind = (typeof CONNECTION_KINDS)[number];
|
|
51
|
+
export type Connection = {
|
|
52
|
+
readonly id: ConnectionId;
|
|
53
|
+
/** Defaults to `"connection"`, so every existing row and caller means what it always did. */
|
|
54
|
+
readonly kind?: ConnectionKind;
|
|
55
|
+
/** `github`, `slack`, `google` — the toolkit's own name, so a ref can be resolved without a second lookup. */
|
|
56
|
+
readonly provider: string;
|
|
57
|
+
/** What a person called it: "Acme org", "#support workspace". Absent for a connection nobody named. */
|
|
58
|
+
readonly label?: string;
|
|
59
|
+
readonly mode: AuthMode;
|
|
60
|
+
readonly scheme: CredentialScheme;
|
|
61
|
+
/**
|
|
62
|
+
* Non-secret vendor identifiers discovered at connection time — Atlassian's cloud id, WhatsApp's phone
|
|
63
|
+
* number id. Stored beside the secret rather than inside it, so listing connections needs no key.
|
|
64
|
+
*/
|
|
65
|
+
readonly metadata?: Readonly<Record<string, string>>;
|
|
66
|
+
/**
|
|
67
|
+
* Scopes the tenant actually granted.
|
|
68
|
+
*
|
|
69
|
+
* Recorded because a scope check must read what was *granted*, not what the deployment's app requests: a
|
|
70
|
+
* tenant using their own OAuth app (#263) may have granted a different set, and #259's AC-7 — telling someone
|
|
71
|
+
* to *reconnect and grant X* rather than surfacing a 403 — is answerable only from this.
|
|
72
|
+
*/
|
|
73
|
+
readonly grantedScopes?: readonly string[];
|
|
74
|
+
readonly sealed: SealedSecret;
|
|
75
|
+
/** Present for a credential that expires; `undefined` for a long-lived token. Refresh is #233's. */
|
|
76
|
+
readonly expiresAt?: string;
|
|
77
|
+
readonly createdAt: string;
|
|
78
|
+
readonly updatedAt: string;
|
|
79
|
+
/**
|
|
80
|
+
* Set when a connection is revoked but kept for the audit trail.
|
|
81
|
+
*
|
|
82
|
+
* Soft, because "who connected this and when was it removed" is a question a security review asks, and a
|
|
83
|
+
* hard delete makes it unanswerable. A revoked connection never resolves.
|
|
84
|
+
*/
|
|
85
|
+
readonly revokedAt?: string;
|
|
86
|
+
};
|
|
87
|
+
export type ConnectionInput = Omit<Connection, "createdAt" | "updatedAt" | "revokedAt">;
|
|
88
|
+
export type ConnectionPatch = {
|
|
89
|
+
readonly label?: string;
|
|
90
|
+
readonly sealed?: SealedSecret;
|
|
91
|
+
readonly metadata?: Readonly<Record<string, string>>;
|
|
92
|
+
readonly grantedScopes?: readonly string[];
|
|
93
|
+
readonly expiresAt?: string;
|
|
94
|
+
};
|
|
95
|
+
/**
|
|
96
|
+
* Where a tenant's connections live.
|
|
97
|
+
*
|
|
98
|
+
* Tenant-scoped on every method, with no `findById(id)` — governing principle 1. The store never sees a
|
|
99
|
+
* plaintext secret, so an adapter cannot leak one it does not have.
|
|
100
|
+
*/
|
|
101
|
+
export interface ConnectionStore {
|
|
102
|
+
create(input: TenantScope & {
|
|
103
|
+
readonly connection: ConnectionInput;
|
|
104
|
+
}): Promise<Connection>;
|
|
105
|
+
get(input: TenantScope & {
|
|
106
|
+
readonly id: ConnectionId;
|
|
107
|
+
readonly kind?: ConnectionKind;
|
|
108
|
+
}): Promise<Connection | null>;
|
|
109
|
+
/**
|
|
110
|
+
* Every live connection, optionally narrowed to one provider. Revoked ones are excluded.
|
|
111
|
+
*
|
|
112
|
+
* `kind` defaults to `"connection"`, so a caller that does not ask cannot receive an OAuth app registration —
|
|
113
|
+
* which is what stops a credential resolver handing a toolkit a client secret.
|
|
114
|
+
*/
|
|
115
|
+
list(input: TenantScope & {
|
|
116
|
+
readonly provider?: string;
|
|
117
|
+
readonly kind?: ConnectionKind;
|
|
118
|
+
}): Promise<readonly Connection[]>;
|
|
119
|
+
update(input: TenantScope & {
|
|
120
|
+
readonly id: ConnectionId;
|
|
121
|
+
readonly patch: ConnectionPatch;
|
|
122
|
+
}): Promise<Connection>;
|
|
123
|
+
/** Marks it revoked. The row stays, so the audit trail survives. */
|
|
124
|
+
revoke(input: TenantScope & {
|
|
125
|
+
readonly id: ConnectionId;
|
|
126
|
+
}): Promise<void>;
|
|
127
|
+
/**
|
|
128
|
+
* Removes a tenant's connections entirely — `docs/18`'s retention, and the one hard delete.
|
|
129
|
+
*
|
|
130
|
+
* Deleting a tenant must remove their secrets: a soft-deleted credential is still a credential, and "we kept
|
|
131
|
+
* it for the audit trail" is not an answer to "delete my data".
|
|
132
|
+
*/
|
|
133
|
+
purge(input: TenantScope): Promise<number>;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Which connection a `credentialRef` names.
|
|
137
|
+
*
|
|
138
|
+
* Two shapes, and both are needed. `provider` alone means "this tenant's default connection for GitHub", which
|
|
139
|
+
* keeps the single-connection case a one-word ref and is what almost every deployment writes. `provider:id`
|
|
140
|
+
* addresses one of several, which is what a customer with three organisations needs.
|
|
141
|
+
*
|
|
142
|
+
* Parsed rather than guessed, so a provider name containing a colon fails loudly instead of resolving to
|
|
143
|
+
* something surprising.
|
|
144
|
+
*/
|
|
145
|
+
export declare const parseCredentialRef: (ref: string) => {
|
|
146
|
+
readonly provider: string;
|
|
147
|
+
readonly id?: ConnectionId;
|
|
148
|
+
};
|
|
149
|
+
export * from "./cipher.js";
|
|
150
|
+
export * from "./resolver.js";
|
|
151
|
+
export * from "./oauth/index.js";
|
|
152
|
+
export * from "./oauth/service.js";
|
|
153
|
+
export * from "./oauth/client.js";
|
|
154
|
+
export * from "./pause.js";
|
|
155
|
+
export * from "./resume.js";
|
|
156
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Connections — a tenant's link to a third-party provider. REQ-063 (#259), task #261.
|
|
3
|
+
*
|
|
4
|
+
* `docs/21` assigned this to the platform repository and `docs/21`'s amendment moved the runtime half here,
|
|
5
|
+
* because eight toolkits ship in this package and none of them was usable by a second tenant: every one takes a
|
|
6
|
+
* `credentialRef` and the only resolver that shipped was a static map.
|
|
7
|
+
*
|
|
8
|
+
* ## What a connection is, and is not
|
|
9
|
+
*
|
|
10
|
+
* It is **one tenant's authorisation to reach one provider**. It is not a toolkit's configuration: a base URL
|
|
11
|
+
* or a page size is the same for every tenant and belongs where the toolkit is wired. The dividing line is
|
|
12
|
+
* whether two tenants using the same toolkit would differ, and for a credential they always do.
|
|
13
|
+
*
|
|
14
|
+
* ## Several per tenant, per provider
|
|
15
|
+
*
|
|
16
|
+
* A customer with three GitHub organisations or two Slack workspaces is the normal case, not an edge one. So a
|
|
17
|
+
* connection has its own id, a provider, and an optional label a person chose — and `credentialRef` addresses a
|
|
18
|
+
* **connection**, with a per-tenant default per provider so the common single-connection case stays a one-word
|
|
19
|
+
* ref.
|
|
20
|
+
*
|
|
21
|
+
* ## The secret is opaque here
|
|
22
|
+
*
|
|
23
|
+
* The store holds a `SealedSecret` and cannot read it. That is what lets the same port sit over Postgres,
|
|
24
|
+
* Supabase and memory with the identical guarantee, and it is why `SecretCipher` is a separate seam rather than
|
|
25
|
+
* a detail of one adapter — see `cipher.ts` for why `pgcrypto` and Vault-only were both rejected.
|
|
26
|
+
*/
|
|
27
|
+
/**
|
|
28
|
+
* A tenant's connection to a provider.
|
|
29
|
+
*
|
|
30
|
+
* Everything here is safe to read and to log, including `sealed` — which is ciphertext, not a secret. The name
|
|
31
|
+
* is the point: a connection list is a screen a person looks at, and rendering it must not need a key.
|
|
32
|
+
*/
|
|
33
|
+
/**
|
|
34
|
+
* What a stored row *is* — REQ-063 (#259), task #263.
|
|
35
|
+
*
|
|
36
|
+
* A tenant's own OAuth **app registration** is the same kind of record as a connection: tenant-scoped,
|
|
37
|
+
* provider-keyed, holding exactly one sealed secret, with metadata beside it, revocable, and covered by the same
|
|
38
|
+
* retention promise. So it is the same table with a discriminator rather than a second port, a second adapter
|
|
39
|
+
* family, a second migration, a second RLS policy and a second conformance suite for a record of identical
|
|
40
|
+
* shape.
|
|
41
|
+
*
|
|
42
|
+
* The discriminator is load-bearing, not cosmetic: `list` and `get` return **connections only** unless asked
|
|
43
|
+
* otherwise, so a credential resolver can never hand a toolkit a client secret by accident.
|
|
44
|
+
*/
|
|
45
|
+
export const CONNECTION_KINDS = ["connection", "oauth-app"];
|
|
46
|
+
/**
|
|
47
|
+
* Which connection a `credentialRef` names.
|
|
48
|
+
*
|
|
49
|
+
* Two shapes, and both are needed. `provider` alone means "this tenant's default connection for GitHub", which
|
|
50
|
+
* keeps the single-connection case a one-word ref and is what almost every deployment writes. `provider:id`
|
|
51
|
+
* addresses one of several, which is what a customer with three organisations needs.
|
|
52
|
+
*
|
|
53
|
+
* Parsed rather than guessed, so a provider name containing a colon fails loudly instead of resolving to
|
|
54
|
+
* something surprising.
|
|
55
|
+
*/
|
|
56
|
+
export const parseCredentialRef = (ref) => {
|
|
57
|
+
const parts = ref.split(":");
|
|
58
|
+
if (parts.length === 1)
|
|
59
|
+
return { provider: parts[0] };
|
|
60
|
+
if (parts.length === 2 && parts[0] !== "" && parts[1] !== "")
|
|
61
|
+
return { provider: parts[0], id: parts[1] };
|
|
62
|
+
throw new Error(`credential reference "${ref}" is not "<provider>" or "<provider>:<connectionId>". A provider name ` +
|
|
63
|
+
"containing a colon would otherwise resolve to a connection nobody meant.");
|
|
64
|
+
};
|
|
65
|
+
export * from "./cipher.js";
|
|
66
|
+
export * from "./resolver.js";
|
|
67
|
+
export * from "./oauth/index.js";
|
|
68
|
+
export * from "./oauth/service.js";
|
|
69
|
+
export * from "./oauth/client.js";
|
|
70
|
+
export * from "./pause.js";
|
|
71
|
+
export * from "./resume.js";
|
|
72
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which OAuth app a tenant connects through — REQ-063 (#259), task #263.
|
|
3
|
+
*
|
|
4
|
+
* Two ways to reach a provider, and both must work.
|
|
5
|
+
*
|
|
6
|
+
* **The deployment's shared app.** One registration per provider; every tenant consents to it. Simplest for the
|
|
7
|
+
* customer — nothing to register — and the path most will take.
|
|
8
|
+
*
|
|
9
|
+
* **The tenant's own app.** A customer who already has a registered Slack app, GitHub App or Google Cloud
|
|
10
|
+
* project uses their own client id and secret. This is not a nicety. Meta's app review is per app, so a shared
|
|
11
|
+
* app's approved use case may not cover a customer's; X's access tier is per app, so a customer paying for a
|
|
12
|
+
* higher tier gains nothing from a shared one on a lower; and an enterprise whose security team will not
|
|
13
|
+
* approve a third-party app in their Google tenant has no other route.
|
|
14
|
+
*
|
|
15
|
+
* ## The fallback is explicit, never silent
|
|
16
|
+
*
|
|
17
|
+
* `resolveOAuthClient` reports **which** app it chose. A caller that logs or displays it can see the difference;
|
|
18
|
+
* one that ignores it behaves as before. The reason is AC-7: removing a tenant's app configuration must not
|
|
19
|
+
* quietly migrate their live connections to the shared app, because that is a credential swap nobody
|
|
20
|
+
* authorised — the tokens were issued by a client that no longer participates, and the shared app cannot
|
|
21
|
+
* refresh or revoke them.
|
|
22
|
+
*/
|
|
23
|
+
import type { ExecutionContext } from "../../core/context.js";
|
|
24
|
+
import type { SecretCipher } from "../cipher.js";
|
|
25
|
+
import type { Connection, ConnectionStore } from "../index.js";
|
|
26
|
+
import { type OAuthProviderConfig } from "./index.js";
|
|
27
|
+
/** Which registration a flow is running under, and where it came from. */
|
|
28
|
+
export type ResolvedOAuthClient = {
|
|
29
|
+
readonly clientId: string;
|
|
30
|
+
readonly clientSecret?: string;
|
|
31
|
+
readonly redirectUris: readonly string[];
|
|
32
|
+
readonly scopes: readonly string[];
|
|
33
|
+
readonly source: "tenant" | "deployment";
|
|
34
|
+
/** The connection id of the tenant's registration, when `source` is `"tenant"`. */
|
|
35
|
+
readonly registrationId?: string;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* A tenant's registration, stored as an `oauth-app` row.
|
|
39
|
+
*
|
|
40
|
+
* The client secret is sealed by the same `SecretCipher` as every token — AC-2 — because it is exactly as much
|
|
41
|
+
* a secret and exactly as much the tenant's. The client id, redirect URI and scopes are metadata, readable,
|
|
42
|
+
* because a settings screen should render without a key.
|
|
43
|
+
*/
|
|
44
|
+
export declare const registerTenantOAuthApp: (input: {
|
|
45
|
+
readonly store: ConnectionStore;
|
|
46
|
+
readonly cipher: SecretCipher;
|
|
47
|
+
readonly context: ExecutionContext;
|
|
48
|
+
readonly provider: string;
|
|
49
|
+
readonly clientId: string;
|
|
50
|
+
readonly clientSecret: string;
|
|
51
|
+
readonly redirectUris: readonly string[];
|
|
52
|
+
readonly scopes?: readonly string[];
|
|
53
|
+
readonly id?: string;
|
|
54
|
+
readonly label?: string;
|
|
55
|
+
}) => Promise<Connection>;
|
|
56
|
+
/**
|
|
57
|
+
* The client this tenant connects through — theirs if registered, otherwise the deployment's.
|
|
58
|
+
*
|
|
59
|
+
* A tenant's own app brings its **own** redirect URIs, and they are still allowlisted: this is the obvious place
|
|
60
|
+
* that check gets loosened into a wildcard to make BYO work, and the whole of #262's second defence would go
|
|
61
|
+
* with it. The registration's URIs simply *become* the allowlist for that tenant's flow — an allowlist chosen by
|
|
62
|
+
* the customer, not a pattern.
|
|
63
|
+
*/
|
|
64
|
+
export declare const resolveOAuthClient: (input: {
|
|
65
|
+
readonly store: ConnectionStore;
|
|
66
|
+
readonly cipher: SecretCipher;
|
|
67
|
+
readonly context: ExecutionContext;
|
|
68
|
+
readonly config: OAuthProviderConfig;
|
|
69
|
+
}) => Promise<ResolvedOAuthClient>;
|
|
70
|
+
/** The provider config a flow should run with, for this tenant. */
|
|
71
|
+
export declare const configForTenant: (base: OAuthProviderConfig, client: ResolvedOAuthClient) => OAuthProviderConfig;
|
|
72
|
+
/**
|
|
73
|
+
* Refuses to use a connection with a client that did not issue it — AC-4.
|
|
74
|
+
*
|
|
75
|
+
* A token obtained through one app is not usable through another: refresh and revocation both authenticate as
|
|
76
|
+
* the client, so a connection whose client changed underneath it fails at the provider with a message about an
|
|
77
|
+
* invalid client — which reads as "your integration is broken" and sends nobody to the actual cause.
|
|
78
|
+
*
|
|
79
|
+
* Failing loudly here, naming both clients, is the difference between a five-minute fix and an afternoon.
|
|
80
|
+
*/
|
|
81
|
+
export declare const assertClientMatches: (connection: Connection, client: ResolvedOAuthClient) => void;
|
|
82
|
+
/** Whether a redirect is allowed for this tenant's resolved client. Exactly matched, as always. */
|
|
83
|
+
export declare const isAllowedRedirectForClient: (client: ResolvedOAuthClient, candidate: string) => boolean;
|
|
84
|
+
//# sourceMappingURL=client.d.ts.map
|