@nebutra/integration-vault 0.1.2 → 0.1.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nebutra/integration-vault",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Tenant-scoped SaaS OAuth and credential vault for tool calling",
5
5
  "private": false,
6
6
  "license": "MIT",
@@ -10,15 +10,19 @@
10
10
  "category": "integrations",
11
11
  "summary": "OAuth, secret storage, and SaaS action invocation boundary"
12
12
  },
13
- "main": "./src/index.ts",
14
- "types": "./src/index.ts",
13
+ "main": "./dist/index.js",
14
+ "types": "./dist/index.d.ts",
15
15
  "exports": {
16
- ".": "./src/index.ts"
16
+ ".": {
17
+ "types": "./dist/index.d.ts",
18
+ "import": "./dist/index.js",
19
+ "default": "./dist/index.js"
20
+ }
17
21
  },
18
22
  "dependencies": {
19
23
  "p-limit": "^7.3.0",
20
- "@nebutra/errors": "0.1.2",
21
- "@nebutra/vault": "0.1.2"
24
+ "@nebutra/vault": "0.1.2",
25
+ "@nebutra/errors": "0.1.2"
22
26
  },
23
27
  "devDependencies": {
24
28
  "@types/node": "^25.9.1",
@@ -38,6 +42,12 @@
38
42
  "publishConfig": {
39
43
  "access": "public"
40
44
  },
45
+ "files": [
46
+ "dist",
47
+ "README.md",
48
+ "LICENSE",
49
+ "CHANGELOG.md"
50
+ ],
41
51
  "scripts": {
42
52
  "build": "tsup",
43
53
  "test": "vitest run",
@@ -1,22 +0,0 @@
1
-
2
- > @nebutra/integration-vault@0.1.2 build /Users/tseka_luk/Documents/Nebutra-SaaS-Lab/Nebutra-Sailor/packages/integrations/integration-vault
3
- > tsup
4
-
5
- CLI Building entry: src/cli.ts, src/index.ts
6
- CLI Using tsconfig: tsconfig.json
7
- CLI tsup v8.5.1
8
- CLI Using tsup config: /Users/tseka_luk/Documents/Nebutra-SaaS-Lab/Nebutra-Sailor/packages/integrations/integration-vault/tsup.config.ts
9
- CLI Target: es2022
10
- CLI Cleaning output folder
11
- ESM Build start
12
- ESM dist/chunk-TQULRIQM.js 8.99 KB
13
- ESM dist/index.js 350.00 B
14
- ESM dist/cli.js 2.07 KB
15
- ESM dist/cli.js.map 3.96 KB
16
- ESM dist/index.js.map 71.00 B
17
- ESM dist/chunk-TQULRIQM.js.map 19.61 KB
18
- ESM ⚡️ Build success in 25ms
19
- DTS Build start
20
- DTS ⚡️ Build success in 3098ms
21
- DTS dist/cli.d.ts 13.00 B
22
- DTS dist/index.d.ts 4.66 KB
@@ -1,14 +0,0 @@
1
-
2
- > @nebutra/integration-vault@0.1.1 test /Users/tseka_luk/Documents/Nebutra-SaaS-Lab/Nebutra-Sailor/packages/integrations/integration-vault
3
- > vitest run
4
-
5
-
6
- RUN v4.1.4 /Users/tseka_luk/Documents/Nebutra-SaaS-Lab/Nebutra-Sailor/packages/integrations/integration-vault
7
-
8
- ✓ src/index.test.ts (4 tests) 17ms
9
-
10
- Test Files 1 passed (1)
11
- Tests 4 passed (4)
12
- Start at 09:38:42
13
- Duration 1.75s (transform 231ms, setup 0ms, import 1.06s, tests 17ms, environment 0ms)
14
-
@@ -1,4 +0,0 @@
1
-
2
- > @nebutra/integration-vault@0.1.2 typecheck /Users/tseka_luk/Documents/Nebutra-SaaS-Lab/Nebutra-Sailor/packages/integrations/integration-vault
3
- > tsc --noEmit
4
-
@@ -1,34 +0,0 @@
1
- import { InMemoryIntegrationProvider, IntegrationVault } from "../src";
2
-
3
- const vault = IntegrationVault.local({
4
- providers: [new InMemoryIntegrationProvider({ id: "local", apps: ["docs"] })],
5
- });
6
-
7
- try {
8
- const flow = await vault.startOAuth({
9
- tenantId: "tenant_demo",
10
- app: "docs",
11
- scopes: ["documents:write"],
12
- });
13
-
14
- const connection = await vault.connectToken({
15
- tenantId: "tenant_demo",
16
- app: "docs",
17
- accessToken: "local-development-token",
18
- scopes: flow.scopes,
19
- });
20
-
21
- process.stdout.write(
22
- `${JSON.stringify(
23
- {
24
- auth_url: flow.url,
25
- token_id: connection.tokenId,
26
- scopes: connection.scopes,
27
- },
28
- null,
29
- 2,
30
- )}\n`,
31
- );
32
- } finally {
33
- await vault.close();
34
- }
@@ -1,32 +0,0 @@
1
- import { InMemoryIntegrationProvider, IntegrationVault } from "../src";
2
-
3
- const vault = IntegrationVault.local({
4
- providers: [new InMemoryIntegrationProvider({ id: "local", apps: ["docs"] })],
5
- });
6
-
7
- try {
8
- await vault.connectToken({
9
- tenantId: "tenant_demo",
10
- app: "docs",
11
- accessToken: "local-development-token",
12
- scopes: ["documents:write"],
13
- });
14
-
15
- await vault.grantConsent({
16
- tenantId: "tenant_demo",
17
- app: "docs",
18
- action: "create_page",
19
- scopes: ["documents:write"],
20
- });
21
-
22
- const result = await vault.invoke({
23
- tenantId: "tenant_demo",
24
- app: "docs",
25
- action: "create_page",
26
- args: { title: "Layer 1", body: "Published through the integration vault." },
27
- });
28
-
29
- process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
30
- } finally {
31
- await vault.close();
32
- }
@@ -1,26 +0,0 @@
1
- import { InMemoryIntegrationProvider, IntegrationVault } from "../src";
2
-
3
- const vault = IntegrationVault.local({
4
- providers: [
5
- new InMemoryIntegrationProvider({ id: "primary", apps: ["crm"] }),
6
- new InMemoryIntegrationProvider({ id: "fallback", apps: ["*"] }),
7
- ],
8
- });
9
-
10
- try {
11
- const direct = await vault.startOAuth({ tenantId: "tenant_demo", app: "crm" });
12
- const fallback = await vault.startOAuth({ tenantId: "tenant_demo", app: "calendar" });
13
-
14
- process.stdout.write(
15
- `${JSON.stringify(
16
- {
17
- direct_provider: direct.provider,
18
- fallback_provider: fallback.provider,
19
- },
20
- null,
21
- 2,
22
- )}\n`,
23
- );
24
- } finally {
25
- await vault.close();
26
- }
package/src/cli.ts DELETED
@@ -1,57 +0,0 @@
1
- import { readFile, writeFile } from "node:fs/promises";
2
- import { dirname, join } from "node:path";
3
- import { IntegrationVault } from "./index";
4
-
5
- const command = process.argv[2] ?? "doctor";
6
- const tenantId = process.env.TENANT_ID ?? "local";
7
- const statePath = join(process.cwd(), ".nebutra", "debug", "integration-vault.jsonl");
8
- const vault = IntegrationVault.local();
9
-
10
- async function appendState(entry: Record<string, unknown>): Promise<void> {
11
- await writeFile(statePath, `${JSON.stringify({ at: new Date().toISOString(), ...entry })}\n`, {
12
- flag: "a",
13
- }).catch(async (error) => {
14
- if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
15
- await import("node:fs/promises").then(({ mkdir }) =>
16
- mkdir(dirname(statePath), { recursive: true }),
17
- );
18
- await writeFile(statePath, `${JSON.stringify({ at: new Date().toISOString(), ...entry })}\n`, {
19
- flag: "a",
20
- });
21
- });
22
- }
23
-
24
- try {
25
- if (command === "doctor") {
26
- process.stdout.write(
27
- `${JSON.stringify({ capability: "integration-vault", ...(await vault.doctor()) }, null, 2)}\n`,
28
- );
29
- } else if (command === "connect") {
30
- const app = process.argv[3];
31
- if (!app) throw new Error("Missing app name");
32
- const flow = await vault.startOAuth({ app, tenantId });
33
- await appendState({ type: "connect", app, tenantId, flow });
34
- process.stdout.write(
35
- `${JSON.stringify({ capability: "integration-vault", auth_url: flow.url, state: flow.state }, null, 2)}\n`,
36
- );
37
- } else if (command === "list") {
38
- process.stdout.write(
39
- `${JSON.stringify({ capability: "integration-vault", connections: await vault.list(tenantId) }, null, 2)}\n`,
40
- );
41
- } else if (command === "debug") {
42
- const raw = await readFile(statePath, "utf8").catch(() => "");
43
- const entries = raw
44
- .trim()
45
- .split("\n")
46
- .filter(Boolean)
47
- .map((line) => JSON.parse(line) as unknown);
48
- process.stdout.write(
49
- `${JSON.stringify({ capability: "integration-vault", entries }, null, 2)}\n`,
50
- );
51
- } else {
52
- process.stderr.write(`Unknown integration-vault command: ${command}\n`);
53
- process.exitCode = 1;
54
- }
55
- } finally {
56
- await vault.close();
57
- }
package/src/index.test.ts DELETED
@@ -1,119 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import {
3
- CapabilityError,
4
- InMemoryIntegrationProvider,
5
- InMemorySaasConsentStore,
6
- type IntegrationProvider,
7
- IntegrationVault,
8
- } from "./index";
9
-
10
- describe("IntegrationVault", () => {
11
- it("requires tenant context before starting OAuth", async () => {
12
- const vault = IntegrationVault.local({
13
- providers: [new InMemoryIntegrationProvider({ id: "local", apps: ["notion"] })],
14
- });
15
-
16
- await expect(vault.startOAuth({ app: "notion", tenantId: "" })).rejects.toThrow(
17
- CapabilityError,
18
- );
19
- });
20
-
21
- it("stores tokens encrypted and lists only token ids and scopes", async () => {
22
- const vault = IntegrationVault.local({
23
- providers: [new InMemoryIntegrationProvider({ id: "local", apps: ["notion"] })],
24
- });
25
- const connection = await vault.connectToken({
26
- tenantId: "tenant_a",
27
- app: "notion",
28
- accessToken: "secret-token",
29
- scopes: ["pages:write"],
30
- });
31
-
32
- await expect(vault.list("tenant_a")).resolves.toEqual([
33
- expect.objectContaining({
34
- app: "notion",
35
- tokenId: connection.tokenId,
36
- scopes: ["pages:write"],
37
- }),
38
- ]);
39
- expect(JSON.stringify(await vault.list("tenant_a"))).not.toContain("secret-token");
40
- });
41
-
42
- it("requires explicit per-action consent before invoking a SaaS action", async () => {
43
- const consent = new InMemorySaasConsentStore();
44
- const vault = IntegrationVault.local({
45
- consent,
46
- providers: [new InMemoryIntegrationProvider({ id: "local", apps: ["notion"] })],
47
- });
48
- await vault.connectToken({
49
- tenantId: "tenant_a",
50
- app: "notion",
51
- accessToken: "secret-token",
52
- scopes: ["pages:write"],
53
- });
54
-
55
- await expect(
56
- vault.invoke({
57
- tenantId: "tenant_a",
58
- app: "notion",
59
- action: "create_page",
60
- args: { title: "Layer 1" },
61
- }),
62
- ).resolves.toMatchObject({
63
- ok: false,
64
- suggestion: expect.stringContaining("vault:connect"),
65
- });
66
-
67
- await consent.grant({
68
- tenantId: "tenant_a",
69
- app: "notion",
70
- action: "create_page",
71
- scopes: ["pages:write"],
72
- });
73
-
74
- await expect(
75
- vault.invoke({
76
- tenantId: "tenant_a",
77
- app: "notion",
78
- action: "create_page",
79
- args: { title: "Layer 1" },
80
- }),
81
- ).resolves.toMatchObject({
82
- ok: true,
83
- provider: "local",
84
- result: { app: "notion", action: "create_page", title: "Layer 1" },
85
- });
86
- });
87
-
88
- it("caps provider doctor concurrency to avoid burst health checks", async () => {
89
- let active = 0;
90
- let maxActive = 0;
91
- const providers = Array.from({ length: 12 }, (_, index): IntegrationProvider => {
92
- const id = `provider_${index}`;
93
- return {
94
- id,
95
- supports: () => true,
96
- startOAuth: async (request) => ({
97
- provider: id,
98
- app: request.app,
99
- url: `https://example.test/${id}`,
100
- state: `state_${id}`,
101
- scopes: request.scopes ?? [],
102
- }),
103
- invoke: async () => ({ provider: id }),
104
- doctor: async () => {
105
- active += 1;
106
- maxActive = Math.max(maxActive, active);
107
- await new Promise((resolve) => setTimeout(resolve, 0));
108
- active -= 1;
109
- return { ok: true, provider: id };
110
- },
111
- };
112
- });
113
-
114
- const vault = IntegrationVault.local({ providers });
115
-
116
- await expect(vault.doctor()).resolves.toMatchObject({ ok: true });
117
- expect(maxActive).toBeLessThanOrEqual(4);
118
- });
119
- });
package/src/index.ts DELETED
@@ -1,413 +0,0 @@
1
- import { randomUUID } from "node:crypto";
2
- import { CapabilityError } from "@nebutra/errors";
3
- import { type EncryptedSecret, LocalProvider, type VaultProvider } from "@nebutra/vault";
4
- import pLimit from "p-limit";
5
-
6
- export { CapabilityError } from "@nebutra/errors";
7
-
8
- export interface OAuthStartRequest {
9
- readonly app: string;
10
- readonly tenantId: string;
11
- readonly scopes?: readonly string[];
12
- }
13
-
14
- export interface OAuthStartResult {
15
- readonly provider: string;
16
- readonly app: string;
17
- readonly url: string;
18
- readonly state: string;
19
- readonly scopes: readonly string[];
20
- }
21
-
22
- export interface ConnectTokenRequest {
23
- readonly tenantId: string;
24
- readonly app: string;
25
- readonly accessToken: string;
26
- readonly scopes: readonly string[];
27
- readonly provider?: string;
28
- }
29
-
30
- export interface InvokeRequest {
31
- readonly tenantId: string;
32
- readonly app: string;
33
- readonly action: string;
34
- readonly args: Record<string, unknown>;
35
- }
36
-
37
- export interface InvokeResult {
38
- readonly ok: boolean;
39
- readonly provider?: string;
40
- readonly result?: unknown;
41
- readonly suggestion?: string;
42
- readonly error?: string;
43
- }
44
-
45
- export interface IntegrationConnection {
46
- readonly id: string;
47
- readonly tenantId: string;
48
- readonly app: string;
49
- readonly provider: string;
50
- readonly tokenId: string;
51
- readonly scopes: readonly string[];
52
- readonly encrypted: EncryptedSecret;
53
- readonly connectedAt: string;
54
- }
55
-
56
- export interface ListedConnection {
57
- readonly id: string;
58
- readonly app: string;
59
- readonly provider: string;
60
- readonly tokenId: string;
61
- readonly scopes: readonly string[];
62
- readonly connectedAt: string;
63
- }
64
-
65
- export interface IntegrationProvider {
66
- readonly id: string;
67
- supports(app: string): boolean;
68
- startOAuth(request: OAuthStartRequest): Promise<OAuthStartResult>;
69
- invoke(request: InvokeRequest, token: string, scopes: readonly string[]): Promise<unknown>;
70
- doctor(): Promise<{ ok: boolean; provider: string; suggestion?: string }>;
71
- }
72
-
73
- export interface SaasConsentGrant {
74
- readonly tenantId: string;
75
- readonly app: string;
76
- readonly action: string;
77
- readonly scopes: readonly string[];
78
- }
79
-
80
- export interface SaasConsentStore {
81
- grant(grant: SaasConsentGrant): Promise<void>;
82
- hasConsent(grant: SaasConsentGrant): Promise<boolean>;
83
- list(tenantId?: string): Promise<SaasConsentGrant[]>;
84
- }
85
-
86
- export interface IntegrationVaultOptions {
87
- readonly vault?: VaultProvider;
88
- readonly providers?: readonly IntegrationProvider[];
89
- readonly consent?: SaasConsentStore;
90
- }
91
-
92
- const PROVIDER_DOCTOR_CONCURRENCY = 4;
93
-
94
- function requireTenant(tenantId: string): void {
95
- if (!tenantId.trim()) {
96
- throw new CapabilityError("integration-vault", "Tenant context is required", {
97
- suggestion:
98
- "Pass tenantId from the Sailor tenant context before starting OAuth or invoking SaaS tools.",
99
- statusCode: 400,
100
- });
101
- }
102
- }
103
-
104
- function consentKey(grant: Omit<SaasConsentGrant, "scopes">): string {
105
- return [grant.tenantId, grant.app, grant.action].join(":");
106
- }
107
-
108
- export class InMemorySaasConsentStore implements SaasConsentStore {
109
- readonly #grants = new Map<string, SaasConsentGrant>();
110
-
111
- async grant(grant: SaasConsentGrant): Promise<void> {
112
- this.#grants.set(consentKey(grant), { ...grant, scopes: [...grant.scopes] });
113
- }
114
-
115
- async hasConsent(grant: SaasConsentGrant): Promise<boolean> {
116
- const existing = this.#grants.get(consentKey(grant));
117
- if (!existing) return false;
118
- const scopes = new Set(existing.scopes);
119
- return grant.scopes.every((scope) => scopes.has(scope));
120
- }
121
-
122
- async list(tenantId?: string): Promise<SaasConsentGrant[]> {
123
- return Array.from(this.#grants.values()).filter(
124
- (grant) => tenantId === undefined || grant.tenantId === tenantId,
125
- );
126
- }
127
- }
128
-
129
- export class InMemoryIntegrationProvider implements IntegrationProvider {
130
- readonly id: string;
131
- readonly #apps: ReadonlySet<string>;
132
-
133
- constructor(options: { id: string; apps: readonly string[] }) {
134
- this.id = options.id;
135
- this.#apps = new Set(options.apps);
136
- }
137
-
138
- supports(app: string): boolean {
139
- return this.#apps.has(app) || this.#apps.has("*");
140
- }
141
-
142
- async startOAuth(request: OAuthStartRequest): Promise<OAuthStartResult> {
143
- return {
144
- provider: this.id,
145
- app: request.app,
146
- url: `http://127.0.0.1:8787/oauth/${request.app}?tenant=${request.tenantId}`,
147
- state: `state_${request.tenantId}_${request.app}`,
148
- scopes: request.scopes ?? [],
149
- };
150
- }
151
-
152
- async invoke(
153
- request: InvokeRequest,
154
- _token: string,
155
- _scopes: readonly string[],
156
- ): Promise<unknown> {
157
- return { app: request.app, action: request.action, ...request.args };
158
- }
159
-
160
- async doctor(): Promise<{ ok: boolean; provider: string }> {
161
- return { ok: true, provider: this.id };
162
- }
163
- }
164
-
165
- export class HttpIntegrationProvider implements IntegrationProvider {
166
- readonly id: string;
167
- readonly #baseUrl: string;
168
- readonly #apps: ReadonlySet<string>;
169
- readonly #fetch: typeof fetch;
170
-
171
- constructor(options: {
172
- id: string;
173
- baseUrl: string;
174
- apps?: readonly string[];
175
- fetch?: typeof fetch;
176
- }) {
177
- this.id = options.id;
178
- this.#baseUrl = options.baseUrl.replace(/\/$/, "");
179
- this.#apps = new Set(options.apps ?? ["*"]);
180
- this.#fetch = options.fetch ?? fetch;
181
- }
182
-
183
- supports(app: string): boolean {
184
- return this.#apps.has(app) || this.#apps.has("*");
185
- }
186
-
187
- async startOAuth(request: OAuthStartRequest): Promise<OAuthStartResult> {
188
- return {
189
- provider: this.id,
190
- app: request.app,
191
- url: `${this.#baseUrl}/oauth/${request.app}/start?tenant=${encodeURIComponent(request.tenantId)}`,
192
- state: `state_${request.tenantId}_${request.app}`,
193
- scopes: request.scopes ?? [],
194
- };
195
- }
196
-
197
- async invoke(request: InvokeRequest, token: string): Promise<unknown> {
198
- const response = await this.#fetch(`${this.#baseUrl}/invoke`, {
199
- method: "POST",
200
- headers: {
201
- authorization: `Bearer ${token}`,
202
- "content-type": "application/json",
203
- },
204
- body: JSON.stringify(request),
205
- });
206
- if (!response.ok) {
207
- throw new CapabilityError(
208
- "integration-vault",
209
- `Provider ${this.id} returned ${response.status}`,
210
- {
211
- suggestion: "Run `pnpm vault:doctor` and refresh or reconnect this SaaS app.",
212
- metadata: { provider: this.id, app: request.app, status: response.status },
213
- },
214
- );
215
- }
216
- return response.json();
217
- }
218
-
219
- async doctor(): Promise<{ ok: boolean; provider: string; suggestion?: string }> {
220
- return {
221
- ok: true,
222
- provider: this.id,
223
- suggestion:
224
- "Network health is checked on invoke; configure provider API keys in the sidecar environment.",
225
- };
226
- }
227
- }
228
-
229
- class InMemoryConnectionStore {
230
- readonly #connections = new Map<string, IntegrationConnection>();
231
-
232
- upsert(connection: IntegrationConnection): IntegrationConnection {
233
- this.#connections.set(`${connection.tenantId}:${connection.app}`, connection);
234
- return connection;
235
- }
236
-
237
- get(tenantId: string, app: string): IntegrationConnection | undefined {
238
- return this.#connections.get(`${tenantId}:${app}`);
239
- }
240
-
241
- list(tenantId: string): ListedConnection[] {
242
- return Array.from(this.#connections.values())
243
- .filter((connection) => connection.tenantId === tenantId)
244
- .map((connection) => ({
245
- id: connection.id,
246
- app: connection.app,
247
- provider: connection.provider,
248
- tokenId: connection.tokenId,
249
- scopes: connection.scopes,
250
- connectedAt: connection.connectedAt,
251
- }));
252
- }
253
- }
254
-
255
- export class IntegrationVault {
256
- readonly #vault: VaultProvider;
257
- readonly #providers: IntegrationProvider[];
258
- readonly #connections = new InMemoryConnectionStore();
259
- readonly #consent: SaasConsentStore;
260
-
261
- constructor(options: IntegrationVaultOptions) {
262
- this.#vault =
263
- options.vault ??
264
- new LocalProvider({
265
- provider: "local",
266
- masterKey: process.env.VAULT_LOCAL_MASTER_KEY ?? "integration-vault-local-development-key",
267
- });
268
- this.#providers =
269
- options.providers && options.providers.length > 0
270
- ? [...options.providers]
271
- : [new InMemoryIntegrationProvider({ id: "local", apps: ["*"] })];
272
- this.#consent = options.consent ?? new InMemorySaasConsentStore();
273
- }
274
-
275
- static local(options: IntegrationVaultOptions = {}): IntegrationVault {
276
- return new IntegrationVault(options);
277
- }
278
-
279
- async startOAuth(request: OAuthStartRequest): Promise<OAuthStartResult> {
280
- requireTenant(request.tenantId);
281
- return this.#providerFor(request.app).startOAuth(request);
282
- }
283
-
284
- async connectToken(request: ConnectTokenRequest): Promise<ListedConnection> {
285
- requireTenant(request.tenantId);
286
- const provider = request.provider
287
- ? this.#providers.find((candidate) => candidate.id === request.provider)
288
- : this.#providerFor(request.app);
289
- if (!provider) {
290
- throw new CapabilityError("integration-vault", "Integration provider not found", {
291
- suggestion: "Configure a provider adapter before connecting this app.",
292
- metadata: { app: request.app, provider: request.provider },
293
- statusCode: 404,
294
- });
295
- }
296
- const tokenId = `tok_${randomUUID()}`;
297
- const encrypted = await this.#vault.encrypt(request.accessToken, {
298
- id: tokenId,
299
- tenantId: request.tenantId,
300
- metadata: { name: `${request.app}:access-token`, type: "oauth_token" },
301
- });
302
- const connection = this.#connections.upsert({
303
- id: `conn_${randomUUID()}`,
304
- tenantId: request.tenantId,
305
- app: request.app,
306
- provider: provider.id,
307
- tokenId,
308
- scopes: [...request.scopes],
309
- encrypted,
310
- connectedAt: new Date().toISOString(),
311
- });
312
- return this.#redact(connection);
313
- }
314
-
315
- async grantConsent(grant: SaasConsentGrant): Promise<void> {
316
- requireTenant(grant.tenantId);
317
- await this.#consent.grant(grant);
318
- }
319
-
320
- async invoke(request: InvokeRequest): Promise<InvokeResult> {
321
- requireTenant(request.tenantId);
322
- const connection = this.#connections.get(request.tenantId, request.app);
323
- if (!connection) {
324
- return {
325
- ok: false,
326
- error: "App is not connected",
327
- suggestion: `Run \`pnpm vault:connect ${request.app}\` before invoking ${request.app}.${request.action}.`,
328
- };
329
- }
330
- const hasConsent = await this.#consent.hasConsent({
331
- tenantId: request.tenantId,
332
- app: request.app,
333
- action: request.action,
334
- scopes: connection.scopes,
335
- });
336
- if (!hasConsent) {
337
- return {
338
- ok: false,
339
- error: "SaaS action is not authorized",
340
- suggestion: `Run \`pnpm vault:connect ${request.app}\` and grant ${request.app}.${request.action} for this tenant.`,
341
- };
342
- }
343
- try {
344
- const token = await this.#vault.decrypt(connection.encrypted, { tenantId: request.tenantId });
345
- const provider = this.#providerById(connection.provider);
346
- const result = await provider.invoke(request, token, connection.scopes);
347
- return { ok: true, provider: provider.id, result };
348
- } catch (error) {
349
- return {
350
- ok: false,
351
- error: error instanceof Error ? error.message : String(error),
352
- suggestion: "Refresh the connection or inspect provider health with `pnpm vault:doctor`.",
353
- };
354
- }
355
- }
356
-
357
- async list(tenantId: string): Promise<ListedConnection[]> {
358
- requireTenant(tenantId);
359
- return this.#connections.list(tenantId);
360
- }
361
-
362
- async doctor(): Promise<{
363
- ok: boolean;
364
- providers: Array<{ ok: boolean; provider: string; suggestion?: string }>;
365
- }> {
366
- const limit = pLimit(PROVIDER_DOCTOR_CONCURRENCY);
367
- const providers = await Promise.all(
368
- this.#providers.map((provider) => limit(() => provider.doctor())),
369
- );
370
- return { ok: providers.some((provider) => provider.ok), providers };
371
- }
372
-
373
- async close(): Promise<void> {
374
- await this.#vault.close();
375
- }
376
-
377
- #providerFor(app: string): IntegrationProvider {
378
- const provider = this.#providers.find((candidate) => candidate.supports(app));
379
- if (!provider) {
380
- throw new CapabilityError("integration-vault", "No provider supports this app", {
381
- suggestion: "Configure a provider adapter or local connector for this SaaS app.",
382
- metadata: { app },
383
- statusCode: 404,
384
- });
385
- }
386
- return provider;
387
- }
388
-
389
- #providerById(id: string): IntegrationProvider {
390
- const provider = this.#providers.find((candidate) => candidate.id === id);
391
- if (!provider) {
392
- throw new CapabilityError("integration-vault", "Connection provider is unavailable", {
393
- suggestion: "Reconnect this app or enable the provider adapter.",
394
- metadata: { provider: id },
395
- statusCode: 404,
396
- });
397
- }
398
- return provider;
399
- }
400
-
401
- #redact(connection: IntegrationConnection): ListedConnection {
402
- return {
403
- id: connection.id,
404
- app: connection.app,
405
- provider: connection.provider,
406
- tokenId: connection.tokenId,
407
- scopes: connection.scopes,
408
- connectedAt: connection.connectedAt,
409
- };
410
- }
411
- }
412
-
413
- export const vault = IntegrationVault.local();
package/tsconfig.json DELETED
@@ -1,12 +0,0 @@
1
- {
2
- "extends": "../../../tsconfig.base.json",
3
- "compilerOptions": {
4
- "module": "ESNext",
5
- "moduleResolution": "bundler",
6
- "target": "esnext",
7
- "types": ["node"],
8
- "incremental": false
9
- },
10
- "include": ["src", "examples"],
11
- "exclude": ["node_modules", "dist"]
12
- }
package/tsup.config.ts DELETED
@@ -1,11 +0,0 @@
1
- import { defineConfig } from "tsup";
2
-
3
- export default defineConfig({
4
- entry: ["src/index.ts", "src/cli.ts"],
5
- format: ["esm"],
6
- dts: true,
7
- sourcemap: true,
8
- clean: true,
9
- target: "es2022",
10
- external: ["@nebutra/errors", "@nebutra/vault"],
11
- });