@nebutra/integration-vault 0.1.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/.turbo/turbo-build.log +22 -0
- package/.turbo/turbo-test.log +14 -0
- package/.turbo/turbo-typecheck.log +4 -0
- package/LICENSE +676 -0
- package/dist/chunk-6YC3QBSB.js +274 -0
- package/dist/chunk-6YC3QBSB.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +62 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +141 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/examples/connect-token.ts +34 -0
- package/examples/invoke-consented.ts +32 -0
- package/examples/provider-fallback.ts +26 -0
- package/package.json +45 -0
- package/src/cli.ts +57 -0
- package/src/index.test.ts +86 -0
- package/src/index.ts +407 -0
- package/tsconfig.json +12 -0
- package/tsup.config.ts +11 -0
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { randomUUID } from "crypto";
|
|
3
|
+
import { CapabilityError } from "@nebutra/errors";
|
|
4
|
+
import { LocalProvider } from "@nebutra/vault";
|
|
5
|
+
import { CapabilityError as CapabilityError2 } from "@nebutra/errors";
|
|
6
|
+
function requireTenant(tenantId) {
|
|
7
|
+
if (!tenantId.trim()) {
|
|
8
|
+
throw new CapabilityError("integration-vault", "Tenant context is required", {
|
|
9
|
+
suggestion: "Pass tenantId from the Sailor tenant context before starting OAuth or invoking SaaS tools.",
|
|
10
|
+
statusCode: 400
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function consentKey(grant) {
|
|
15
|
+
return [grant.tenantId, grant.app, grant.action].join(":");
|
|
16
|
+
}
|
|
17
|
+
var InMemorySaasConsentStore = class {
|
|
18
|
+
#grants = /* @__PURE__ */ new Map();
|
|
19
|
+
async grant(grant) {
|
|
20
|
+
this.#grants.set(consentKey(grant), { ...grant, scopes: [...grant.scopes] });
|
|
21
|
+
}
|
|
22
|
+
async hasConsent(grant) {
|
|
23
|
+
const existing = this.#grants.get(consentKey(grant));
|
|
24
|
+
if (!existing) return false;
|
|
25
|
+
const scopes = new Set(existing.scopes);
|
|
26
|
+
return grant.scopes.every((scope) => scopes.has(scope));
|
|
27
|
+
}
|
|
28
|
+
async list(tenantId) {
|
|
29
|
+
return Array.from(this.#grants.values()).filter(
|
|
30
|
+
(grant) => tenantId === void 0 || grant.tenantId === tenantId
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
var InMemoryIntegrationProvider = class {
|
|
35
|
+
id;
|
|
36
|
+
#apps;
|
|
37
|
+
constructor(options) {
|
|
38
|
+
this.id = options.id;
|
|
39
|
+
this.#apps = new Set(options.apps);
|
|
40
|
+
}
|
|
41
|
+
supports(app) {
|
|
42
|
+
return this.#apps.has(app) || this.#apps.has("*");
|
|
43
|
+
}
|
|
44
|
+
async startOAuth(request) {
|
|
45
|
+
return {
|
|
46
|
+
provider: this.id,
|
|
47
|
+
app: request.app,
|
|
48
|
+
url: `http://127.0.0.1:8787/oauth/${request.app}?tenant=${request.tenantId}`,
|
|
49
|
+
state: `state_${request.tenantId}_${request.app}`,
|
|
50
|
+
scopes: request.scopes ?? []
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
async invoke(request, _token, _scopes) {
|
|
54
|
+
return { app: request.app, action: request.action, ...request.args };
|
|
55
|
+
}
|
|
56
|
+
async doctor() {
|
|
57
|
+
return { ok: true, provider: this.id };
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
var HttpIntegrationProvider = class {
|
|
61
|
+
id;
|
|
62
|
+
#baseUrl;
|
|
63
|
+
#apps;
|
|
64
|
+
#fetch;
|
|
65
|
+
constructor(options) {
|
|
66
|
+
this.id = options.id;
|
|
67
|
+
this.#baseUrl = options.baseUrl.replace(/\/$/, "");
|
|
68
|
+
this.#apps = new Set(options.apps ?? ["*"]);
|
|
69
|
+
this.#fetch = options.fetch ?? fetch;
|
|
70
|
+
}
|
|
71
|
+
supports(app) {
|
|
72
|
+
return this.#apps.has(app) || this.#apps.has("*");
|
|
73
|
+
}
|
|
74
|
+
async startOAuth(request) {
|
|
75
|
+
return {
|
|
76
|
+
provider: this.id,
|
|
77
|
+
app: request.app,
|
|
78
|
+
url: `${this.#baseUrl}/oauth/${request.app}/start?tenant=${encodeURIComponent(request.tenantId)}`,
|
|
79
|
+
state: `state_${request.tenantId}_${request.app}`,
|
|
80
|
+
scopes: request.scopes ?? []
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
async invoke(request, token) {
|
|
84
|
+
const response = await this.#fetch(`${this.#baseUrl}/invoke`, {
|
|
85
|
+
method: "POST",
|
|
86
|
+
headers: {
|
|
87
|
+
authorization: `Bearer ${token}`,
|
|
88
|
+
"content-type": "application/json"
|
|
89
|
+
},
|
|
90
|
+
body: JSON.stringify(request)
|
|
91
|
+
});
|
|
92
|
+
if (!response.ok) {
|
|
93
|
+
throw new CapabilityError(
|
|
94
|
+
"integration-vault",
|
|
95
|
+
`Provider ${this.id} returned ${response.status}`,
|
|
96
|
+
{
|
|
97
|
+
suggestion: "Run `pnpm vault:doctor` and refresh or reconnect this SaaS app.",
|
|
98
|
+
metadata: { provider: this.id, app: request.app, status: response.status }
|
|
99
|
+
}
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
return response.json();
|
|
103
|
+
}
|
|
104
|
+
async doctor() {
|
|
105
|
+
return {
|
|
106
|
+
ok: true,
|
|
107
|
+
provider: this.id,
|
|
108
|
+
suggestion: "Network health is checked on invoke; configure provider API keys in the sidecar environment."
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
var InMemoryConnectionStore = class {
|
|
113
|
+
#connections = /* @__PURE__ */ new Map();
|
|
114
|
+
upsert(connection) {
|
|
115
|
+
this.#connections.set(`${connection.tenantId}:${connection.app}`, connection);
|
|
116
|
+
return connection;
|
|
117
|
+
}
|
|
118
|
+
get(tenantId, app) {
|
|
119
|
+
return this.#connections.get(`${tenantId}:${app}`);
|
|
120
|
+
}
|
|
121
|
+
list(tenantId) {
|
|
122
|
+
return Array.from(this.#connections.values()).filter((connection) => connection.tenantId === tenantId).map((connection) => ({
|
|
123
|
+
id: connection.id,
|
|
124
|
+
app: connection.app,
|
|
125
|
+
provider: connection.provider,
|
|
126
|
+
tokenId: connection.tokenId,
|
|
127
|
+
scopes: connection.scopes,
|
|
128
|
+
connectedAt: connection.connectedAt
|
|
129
|
+
}));
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
var IntegrationVault = class _IntegrationVault {
|
|
133
|
+
#vault;
|
|
134
|
+
#providers;
|
|
135
|
+
#connections = new InMemoryConnectionStore();
|
|
136
|
+
#consent;
|
|
137
|
+
constructor(options) {
|
|
138
|
+
this.#vault = options.vault ?? new LocalProvider({
|
|
139
|
+
provider: "local",
|
|
140
|
+
masterKey: process.env.VAULT_LOCAL_MASTER_KEY ?? "integration-vault-local-development-key"
|
|
141
|
+
});
|
|
142
|
+
this.#providers = options.providers && options.providers.length > 0 ? [...options.providers] : [new InMemoryIntegrationProvider({ id: "local", apps: ["*"] })];
|
|
143
|
+
this.#consent = options.consent ?? new InMemorySaasConsentStore();
|
|
144
|
+
}
|
|
145
|
+
static local(options = {}) {
|
|
146
|
+
return new _IntegrationVault(options);
|
|
147
|
+
}
|
|
148
|
+
async startOAuth(request) {
|
|
149
|
+
requireTenant(request.tenantId);
|
|
150
|
+
return this.#providerFor(request.app).startOAuth(request);
|
|
151
|
+
}
|
|
152
|
+
async connectToken(request) {
|
|
153
|
+
requireTenant(request.tenantId);
|
|
154
|
+
const provider = request.provider ? this.#providers.find((candidate) => candidate.id === request.provider) : this.#providerFor(request.app);
|
|
155
|
+
if (!provider) {
|
|
156
|
+
throw new CapabilityError("integration-vault", "Integration provider not found", {
|
|
157
|
+
suggestion: "Configure a provider adapter before connecting this app.",
|
|
158
|
+
metadata: { app: request.app, provider: request.provider },
|
|
159
|
+
statusCode: 404
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
const tokenId = `tok_${randomUUID()}`;
|
|
163
|
+
const encrypted = await this.#vault.encrypt(request.accessToken, {
|
|
164
|
+
id: tokenId,
|
|
165
|
+
tenantId: request.tenantId,
|
|
166
|
+
metadata: { name: `${request.app}:access-token`, type: "oauth_token" }
|
|
167
|
+
});
|
|
168
|
+
const connection = this.#connections.upsert({
|
|
169
|
+
id: `conn_${randomUUID()}`,
|
|
170
|
+
tenantId: request.tenantId,
|
|
171
|
+
app: request.app,
|
|
172
|
+
provider: provider.id,
|
|
173
|
+
tokenId,
|
|
174
|
+
scopes: [...request.scopes],
|
|
175
|
+
encrypted,
|
|
176
|
+
connectedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
177
|
+
});
|
|
178
|
+
return this.#redact(connection);
|
|
179
|
+
}
|
|
180
|
+
async grantConsent(grant) {
|
|
181
|
+
requireTenant(grant.tenantId);
|
|
182
|
+
await this.#consent.grant(grant);
|
|
183
|
+
}
|
|
184
|
+
async invoke(request) {
|
|
185
|
+
requireTenant(request.tenantId);
|
|
186
|
+
const connection = this.#connections.get(request.tenantId, request.app);
|
|
187
|
+
if (!connection) {
|
|
188
|
+
return {
|
|
189
|
+
ok: false,
|
|
190
|
+
error: "App is not connected",
|
|
191
|
+
suggestion: `Run \`pnpm vault:connect ${request.app}\` before invoking ${request.app}.${request.action}.`
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
const hasConsent = await this.#consent.hasConsent({
|
|
195
|
+
tenantId: request.tenantId,
|
|
196
|
+
app: request.app,
|
|
197
|
+
action: request.action,
|
|
198
|
+
scopes: connection.scopes
|
|
199
|
+
});
|
|
200
|
+
if (!hasConsent) {
|
|
201
|
+
return {
|
|
202
|
+
ok: false,
|
|
203
|
+
error: "SaaS action is not authorized",
|
|
204
|
+
suggestion: `Run \`pnpm vault:connect ${request.app}\` and grant ${request.app}.${request.action} for this tenant.`
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
try {
|
|
208
|
+
const token = await this.#vault.decrypt(connection.encrypted, { tenantId: request.tenantId });
|
|
209
|
+
const provider = this.#providerById(connection.provider);
|
|
210
|
+
const result = await provider.invoke(request, token, connection.scopes);
|
|
211
|
+
return { ok: true, provider: provider.id, result };
|
|
212
|
+
} catch (error) {
|
|
213
|
+
return {
|
|
214
|
+
ok: false,
|
|
215
|
+
error: error instanceof Error ? error.message : String(error),
|
|
216
|
+
suggestion: "Refresh the connection or inspect provider health with `pnpm vault:doctor`."
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
async list(tenantId) {
|
|
221
|
+
requireTenant(tenantId);
|
|
222
|
+
return this.#connections.list(tenantId);
|
|
223
|
+
}
|
|
224
|
+
async doctor() {
|
|
225
|
+
const providers = await Promise.all(this.#providers.map((provider) => provider.doctor()));
|
|
226
|
+
return { ok: providers.some((provider) => provider.ok), providers };
|
|
227
|
+
}
|
|
228
|
+
async close() {
|
|
229
|
+
await this.#vault.close();
|
|
230
|
+
}
|
|
231
|
+
#providerFor(app) {
|
|
232
|
+
const provider = this.#providers.find((candidate) => candidate.supports(app));
|
|
233
|
+
if (!provider) {
|
|
234
|
+
throw new CapabilityError("integration-vault", "No provider supports this app", {
|
|
235
|
+
suggestion: "Configure a provider adapter or local connector for this SaaS app.",
|
|
236
|
+
metadata: { app },
|
|
237
|
+
statusCode: 404
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
return provider;
|
|
241
|
+
}
|
|
242
|
+
#providerById(id) {
|
|
243
|
+
const provider = this.#providers.find((candidate) => candidate.id === id);
|
|
244
|
+
if (!provider) {
|
|
245
|
+
throw new CapabilityError("integration-vault", "Connection provider is unavailable", {
|
|
246
|
+
suggestion: "Reconnect this app or enable the provider adapter.",
|
|
247
|
+
metadata: { provider: id },
|
|
248
|
+
statusCode: 404
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
return provider;
|
|
252
|
+
}
|
|
253
|
+
#redact(connection) {
|
|
254
|
+
return {
|
|
255
|
+
id: connection.id,
|
|
256
|
+
app: connection.app,
|
|
257
|
+
provider: connection.provider,
|
|
258
|
+
tokenId: connection.tokenId,
|
|
259
|
+
scopes: connection.scopes,
|
|
260
|
+
connectedAt: connection.connectedAt
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
var vault = IntegrationVault.local();
|
|
265
|
+
|
|
266
|
+
export {
|
|
267
|
+
InMemorySaasConsentStore,
|
|
268
|
+
InMemoryIntegrationProvider,
|
|
269
|
+
HttpIntegrationProvider,
|
|
270
|
+
IntegrationVault,
|
|
271
|
+
vault,
|
|
272
|
+
CapabilityError2 as CapabilityError
|
|
273
|
+
};
|
|
274
|
+
//# sourceMappingURL=chunk-6YC3QBSB.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport { CapabilityError } from \"@nebutra/errors\";\nimport { type EncryptedSecret, LocalProvider, type VaultProvider } from \"@nebutra/vault\";\n\nexport { CapabilityError } from \"@nebutra/errors\";\n\nexport interface OAuthStartRequest {\n readonly app: string;\n readonly tenantId: string;\n readonly scopes?: readonly string[];\n}\n\nexport interface OAuthStartResult {\n readonly provider: string;\n readonly app: string;\n readonly url: string;\n readonly state: string;\n readonly scopes: readonly string[];\n}\n\nexport interface ConnectTokenRequest {\n readonly tenantId: string;\n readonly app: string;\n readonly accessToken: string;\n readonly scopes: readonly string[];\n readonly provider?: string;\n}\n\nexport interface InvokeRequest {\n readonly tenantId: string;\n readonly app: string;\n readonly action: string;\n readonly args: Record<string, unknown>;\n}\n\nexport interface InvokeResult {\n readonly ok: boolean;\n readonly provider?: string;\n readonly result?: unknown;\n readonly suggestion?: string;\n readonly error?: string;\n}\n\nexport interface IntegrationConnection {\n readonly id: string;\n readonly tenantId: string;\n readonly app: string;\n readonly provider: string;\n readonly tokenId: string;\n readonly scopes: readonly string[];\n readonly encrypted: EncryptedSecret;\n readonly connectedAt: string;\n}\n\nexport interface ListedConnection {\n readonly id: string;\n readonly app: string;\n readonly provider: string;\n readonly tokenId: string;\n readonly scopes: readonly string[];\n readonly connectedAt: string;\n}\n\nexport interface IntegrationProvider {\n readonly id: string;\n supports(app: string): boolean;\n startOAuth(request: OAuthStartRequest): Promise<OAuthStartResult>;\n invoke(request: InvokeRequest, token: string, scopes: readonly string[]): Promise<unknown>;\n doctor(): Promise<{ ok: boolean; provider: string; suggestion?: string }>;\n}\n\nexport interface SaasConsentGrant {\n readonly tenantId: string;\n readonly app: string;\n readonly action: string;\n readonly scopes: readonly string[];\n}\n\nexport interface SaasConsentStore {\n grant(grant: SaasConsentGrant): Promise<void>;\n hasConsent(grant: SaasConsentGrant): Promise<boolean>;\n list(tenantId?: string): Promise<SaasConsentGrant[]>;\n}\n\nexport interface IntegrationVaultOptions {\n readonly vault?: VaultProvider;\n readonly providers?: readonly IntegrationProvider[];\n readonly consent?: SaasConsentStore;\n}\n\nfunction requireTenant(tenantId: string): void {\n if (!tenantId.trim()) {\n throw new CapabilityError(\"integration-vault\", \"Tenant context is required\", {\n suggestion:\n \"Pass tenantId from the Sailor tenant context before starting OAuth or invoking SaaS tools.\",\n statusCode: 400,\n });\n }\n}\n\nfunction consentKey(grant: Omit<SaasConsentGrant, \"scopes\">): string {\n return [grant.tenantId, grant.app, grant.action].join(\":\");\n}\n\nexport class InMemorySaasConsentStore implements SaasConsentStore {\n readonly #grants = new Map<string, SaasConsentGrant>();\n\n async grant(grant: SaasConsentGrant): Promise<void> {\n this.#grants.set(consentKey(grant), { ...grant, scopes: [...grant.scopes] });\n }\n\n async hasConsent(grant: SaasConsentGrant): Promise<boolean> {\n const existing = this.#grants.get(consentKey(grant));\n if (!existing) return false;\n const scopes = new Set(existing.scopes);\n return grant.scopes.every((scope) => scopes.has(scope));\n }\n\n async list(tenantId?: string): Promise<SaasConsentGrant[]> {\n return Array.from(this.#grants.values()).filter(\n (grant) => tenantId === undefined || grant.tenantId === tenantId,\n );\n }\n}\n\nexport class InMemoryIntegrationProvider implements IntegrationProvider {\n readonly id: string;\n readonly #apps: ReadonlySet<string>;\n\n constructor(options: { id: string; apps: readonly string[] }) {\n this.id = options.id;\n this.#apps = new Set(options.apps);\n }\n\n supports(app: string): boolean {\n return this.#apps.has(app) || this.#apps.has(\"*\");\n }\n\n async startOAuth(request: OAuthStartRequest): Promise<OAuthStartResult> {\n return {\n provider: this.id,\n app: request.app,\n url: `http://127.0.0.1:8787/oauth/${request.app}?tenant=${request.tenantId}`,\n state: `state_${request.tenantId}_${request.app}`,\n scopes: request.scopes ?? [],\n };\n }\n\n async invoke(\n request: InvokeRequest,\n _token: string,\n _scopes: readonly string[],\n ): Promise<unknown> {\n return { app: request.app, action: request.action, ...request.args };\n }\n\n async doctor(): Promise<{ ok: boolean; provider: string }> {\n return { ok: true, provider: this.id };\n }\n}\n\nexport class HttpIntegrationProvider implements IntegrationProvider {\n readonly id: string;\n readonly #baseUrl: string;\n readonly #apps: ReadonlySet<string>;\n readonly #fetch: typeof fetch;\n\n constructor(options: {\n id: string;\n baseUrl: string;\n apps?: readonly string[];\n fetch?: typeof fetch;\n }) {\n this.id = options.id;\n this.#baseUrl = options.baseUrl.replace(/\\/$/, \"\");\n this.#apps = new Set(options.apps ?? [\"*\"]);\n this.#fetch = options.fetch ?? fetch;\n }\n\n supports(app: string): boolean {\n return this.#apps.has(app) || this.#apps.has(\"*\");\n }\n\n async startOAuth(request: OAuthStartRequest): Promise<OAuthStartResult> {\n return {\n provider: this.id,\n app: request.app,\n url: `${this.#baseUrl}/oauth/${request.app}/start?tenant=${encodeURIComponent(request.tenantId)}`,\n state: `state_${request.tenantId}_${request.app}`,\n scopes: request.scopes ?? [],\n };\n }\n\n async invoke(request: InvokeRequest, token: string): Promise<unknown> {\n const response = await this.#fetch(`${this.#baseUrl}/invoke`, {\n method: \"POST\",\n headers: {\n authorization: `Bearer ${token}`,\n \"content-type\": \"application/json\",\n },\n body: JSON.stringify(request),\n });\n if (!response.ok) {\n throw new CapabilityError(\n \"integration-vault\",\n `Provider ${this.id} returned ${response.status}`,\n {\n suggestion: \"Run `pnpm vault:doctor` and refresh or reconnect this SaaS app.\",\n metadata: { provider: this.id, app: request.app, status: response.status },\n },\n );\n }\n return response.json();\n }\n\n async doctor(): Promise<{ ok: boolean; provider: string; suggestion?: string }> {\n return {\n ok: true,\n provider: this.id,\n suggestion:\n \"Network health is checked on invoke; configure provider API keys in the sidecar environment.\",\n };\n }\n}\n\nclass InMemoryConnectionStore {\n readonly #connections = new Map<string, IntegrationConnection>();\n\n upsert(connection: IntegrationConnection): IntegrationConnection {\n this.#connections.set(`${connection.tenantId}:${connection.app}`, connection);\n return connection;\n }\n\n get(tenantId: string, app: string): IntegrationConnection | undefined {\n return this.#connections.get(`${tenantId}:${app}`);\n }\n\n list(tenantId: string): ListedConnection[] {\n return Array.from(this.#connections.values())\n .filter((connection) => connection.tenantId === tenantId)\n .map((connection) => ({\n id: connection.id,\n app: connection.app,\n provider: connection.provider,\n tokenId: connection.tokenId,\n scopes: connection.scopes,\n connectedAt: connection.connectedAt,\n }));\n }\n}\n\nexport class IntegrationVault {\n readonly #vault: VaultProvider;\n readonly #providers: IntegrationProvider[];\n readonly #connections = new InMemoryConnectionStore();\n readonly #consent: SaasConsentStore;\n\n constructor(options: IntegrationVaultOptions) {\n this.#vault =\n options.vault ??\n new LocalProvider({\n provider: \"local\",\n masterKey: process.env.VAULT_LOCAL_MASTER_KEY ?? \"integration-vault-local-development-key\",\n });\n this.#providers =\n options.providers && options.providers.length > 0\n ? [...options.providers]\n : [new InMemoryIntegrationProvider({ id: \"local\", apps: [\"*\"] })];\n this.#consent = options.consent ?? new InMemorySaasConsentStore();\n }\n\n static local(options: IntegrationVaultOptions = {}): IntegrationVault {\n return new IntegrationVault(options);\n }\n\n async startOAuth(request: OAuthStartRequest): Promise<OAuthStartResult> {\n requireTenant(request.tenantId);\n return this.#providerFor(request.app).startOAuth(request);\n }\n\n async connectToken(request: ConnectTokenRequest): Promise<ListedConnection> {\n requireTenant(request.tenantId);\n const provider = request.provider\n ? this.#providers.find((candidate) => candidate.id === request.provider)\n : this.#providerFor(request.app);\n if (!provider) {\n throw new CapabilityError(\"integration-vault\", \"Integration provider not found\", {\n suggestion: \"Configure a provider adapter before connecting this app.\",\n metadata: { app: request.app, provider: request.provider },\n statusCode: 404,\n });\n }\n const tokenId = `tok_${randomUUID()}`;\n const encrypted = await this.#vault.encrypt(request.accessToken, {\n id: tokenId,\n tenantId: request.tenantId,\n metadata: { name: `${request.app}:access-token`, type: \"oauth_token\" },\n });\n const connection = this.#connections.upsert({\n id: `conn_${randomUUID()}`,\n tenantId: request.tenantId,\n app: request.app,\n provider: provider.id,\n tokenId,\n scopes: [...request.scopes],\n encrypted,\n connectedAt: new Date().toISOString(),\n });\n return this.#redact(connection);\n }\n\n async grantConsent(grant: SaasConsentGrant): Promise<void> {\n requireTenant(grant.tenantId);\n await this.#consent.grant(grant);\n }\n\n async invoke(request: InvokeRequest): Promise<InvokeResult> {\n requireTenant(request.tenantId);\n const connection = this.#connections.get(request.tenantId, request.app);\n if (!connection) {\n return {\n ok: false,\n error: \"App is not connected\",\n suggestion: `Run \\`pnpm vault:connect ${request.app}\\` before invoking ${request.app}.${request.action}.`,\n };\n }\n const hasConsent = await this.#consent.hasConsent({\n tenantId: request.tenantId,\n app: request.app,\n action: request.action,\n scopes: connection.scopes,\n });\n if (!hasConsent) {\n return {\n ok: false,\n error: \"SaaS action is not authorized\",\n suggestion: `Run \\`pnpm vault:connect ${request.app}\\` and grant ${request.app}.${request.action} for this tenant.`,\n };\n }\n try {\n const token = await this.#vault.decrypt(connection.encrypted, { tenantId: request.tenantId });\n const provider = this.#providerById(connection.provider);\n const result = await provider.invoke(request, token, connection.scopes);\n return { ok: true, provider: provider.id, result };\n } catch (error) {\n return {\n ok: false,\n error: error instanceof Error ? error.message : String(error),\n suggestion: \"Refresh the connection or inspect provider health with `pnpm vault:doctor`.\",\n };\n }\n }\n\n async list(tenantId: string): Promise<ListedConnection[]> {\n requireTenant(tenantId);\n return this.#connections.list(tenantId);\n }\n\n async doctor(): Promise<{\n ok: boolean;\n providers: Array<{ ok: boolean; provider: string; suggestion?: string }>;\n }> {\n const providers = await Promise.all(this.#providers.map((provider) => provider.doctor()));\n return { ok: providers.some((provider) => provider.ok), providers };\n }\n\n async close(): Promise<void> {\n await this.#vault.close();\n }\n\n #providerFor(app: string): IntegrationProvider {\n const provider = this.#providers.find((candidate) => candidate.supports(app));\n if (!provider) {\n throw new CapabilityError(\"integration-vault\", \"No provider supports this app\", {\n suggestion: \"Configure a provider adapter or local connector for this SaaS app.\",\n metadata: { app },\n statusCode: 404,\n });\n }\n return provider;\n }\n\n #providerById(id: string): IntegrationProvider {\n const provider = this.#providers.find((candidate) => candidate.id === id);\n if (!provider) {\n throw new CapabilityError(\"integration-vault\", \"Connection provider is unavailable\", {\n suggestion: \"Reconnect this app or enable the provider adapter.\",\n metadata: { provider: id },\n statusCode: 404,\n });\n }\n return provider;\n }\n\n #redact(connection: IntegrationConnection): ListedConnection {\n return {\n id: connection.id,\n app: connection.app,\n provider: connection.provider,\n tokenId: connection.tokenId,\n scopes: connection.scopes,\n connectedAt: connection.connectedAt,\n };\n }\n}\n\nexport const vault = IntegrationVault.local();\n"],"mappings":"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","names":["CapabilityError"]}
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import {
|
|
2
|
+
IntegrationVault
|
|
3
|
+
} from "./chunk-6YC3QBSB.js";
|
|
4
|
+
|
|
5
|
+
// src/cli.ts
|
|
6
|
+
import { readFile, writeFile } from "fs/promises";
|
|
7
|
+
import { dirname, join } from "path";
|
|
8
|
+
var command = process.argv[2] ?? "doctor";
|
|
9
|
+
var tenantId = process.env.TENANT_ID ?? "local";
|
|
10
|
+
var statePath = join(process.cwd(), ".nebutra", "debug", "integration-vault.jsonl");
|
|
11
|
+
var vault = IntegrationVault.local();
|
|
12
|
+
async function appendState(entry) {
|
|
13
|
+
await writeFile(statePath, `${JSON.stringify({ at: (/* @__PURE__ */ new Date()).toISOString(), ...entry })}
|
|
14
|
+
`, {
|
|
15
|
+
flag: "a"
|
|
16
|
+
}).catch(async (error) => {
|
|
17
|
+
if (error.code !== "ENOENT") throw error;
|
|
18
|
+
await import("fs/promises").then(
|
|
19
|
+
({ mkdir }) => mkdir(dirname(statePath), { recursive: true })
|
|
20
|
+
);
|
|
21
|
+
await writeFile(statePath, `${JSON.stringify({ at: (/* @__PURE__ */ new Date()).toISOString(), ...entry })}
|
|
22
|
+
`, {
|
|
23
|
+
flag: "a"
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
if (command === "doctor") {
|
|
29
|
+
process.stdout.write(
|
|
30
|
+
`${JSON.stringify({ capability: "integration-vault", ...await vault.doctor() }, null, 2)}
|
|
31
|
+
`
|
|
32
|
+
);
|
|
33
|
+
} else if (command === "connect") {
|
|
34
|
+
const app = process.argv[3];
|
|
35
|
+
if (!app) throw new Error("Missing app name");
|
|
36
|
+
const flow = await vault.startOAuth({ app, tenantId });
|
|
37
|
+
await appendState({ type: "connect", app, tenantId, flow });
|
|
38
|
+
process.stdout.write(
|
|
39
|
+
`${JSON.stringify({ capability: "integration-vault", auth_url: flow.url, state: flow.state }, null, 2)}
|
|
40
|
+
`
|
|
41
|
+
);
|
|
42
|
+
} else if (command === "list") {
|
|
43
|
+
process.stdout.write(
|
|
44
|
+
`${JSON.stringify({ capability: "integration-vault", connections: await vault.list(tenantId) }, null, 2)}
|
|
45
|
+
`
|
|
46
|
+
);
|
|
47
|
+
} else if (command === "debug") {
|
|
48
|
+
const raw = await readFile(statePath, "utf8").catch(() => "");
|
|
49
|
+
const entries = raw.trim().split("\n").filter(Boolean).map((line) => JSON.parse(line));
|
|
50
|
+
process.stdout.write(
|
|
51
|
+
`${JSON.stringify({ capability: "integration-vault", entries }, null, 2)}
|
|
52
|
+
`
|
|
53
|
+
);
|
|
54
|
+
} else {
|
|
55
|
+
process.stderr.write(`Unknown integration-vault command: ${command}
|
|
56
|
+
`);
|
|
57
|
+
process.exitCode = 1;
|
|
58
|
+
}
|
|
59
|
+
} finally {
|
|
60
|
+
await vault.close();
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/cli.ts"],"sourcesContent":["import { readFile, writeFile } from \"node:fs/promises\";\nimport { dirname, join } from \"node:path\";\nimport { IntegrationVault } from \"./index\";\n\nconst command = process.argv[2] ?? \"doctor\";\nconst tenantId = process.env.TENANT_ID ?? \"local\";\nconst statePath = join(process.cwd(), \".nebutra\", \"debug\", \"integration-vault.jsonl\");\nconst vault = IntegrationVault.local();\n\nasync function appendState(entry: Record<string, unknown>): Promise<void> {\n await writeFile(statePath, `${JSON.stringify({ at: new Date().toISOString(), ...entry })}\\n`, {\n flag: \"a\",\n }).catch(async (error) => {\n if ((error as NodeJS.ErrnoException).code !== \"ENOENT\") throw error;\n await import(\"node:fs/promises\").then(({ mkdir }) =>\n mkdir(dirname(statePath), { recursive: true }),\n );\n await writeFile(statePath, `${JSON.stringify({ at: new Date().toISOString(), ...entry })}\\n`, {\n flag: \"a\",\n });\n });\n}\n\ntry {\n if (command === \"doctor\") {\n process.stdout.write(\n `${JSON.stringify({ capability: \"integration-vault\", ...(await vault.doctor()) }, null, 2)}\\n`,\n );\n } else if (command === \"connect\") {\n const app = process.argv[3];\n if (!app) throw new Error(\"Missing app name\");\n const flow = await vault.startOAuth({ app, tenantId });\n await appendState({ type: \"connect\", app, tenantId, flow });\n process.stdout.write(\n `${JSON.stringify({ capability: \"integration-vault\", auth_url: flow.url, state: flow.state }, null, 2)}\\n`,\n );\n } else if (command === \"list\") {\n process.stdout.write(\n `${JSON.stringify({ capability: \"integration-vault\", connections: await vault.list(tenantId) }, null, 2)}\\n`,\n );\n } else if (command === \"debug\") {\n const raw = await readFile(statePath, \"utf8\").catch(() => \"\");\n const entries = raw\n .trim()\n .split(\"\\n\")\n .filter(Boolean)\n .map((line) => JSON.parse(line) as unknown);\n process.stdout.write(\n `${JSON.stringify({ capability: \"integration-vault\", entries }, null, 2)}\\n`,\n );\n } else {\n process.stderr.write(`Unknown integration-vault command: ${command}\\n`);\n process.exitCode = 1;\n }\n} finally {\n await vault.close();\n}\n"],"mappings":";;;;;AAAA,SAAS,UAAU,iBAAiB;AACpC,SAAS,SAAS,YAAY;AAG9B,IAAM,UAAU,QAAQ,KAAK,CAAC,KAAK;AACnC,IAAM,WAAW,QAAQ,IAAI,aAAa;AAC1C,IAAM,YAAY,KAAK,QAAQ,IAAI,GAAG,YAAY,SAAS,yBAAyB;AACpF,IAAM,QAAQ,iBAAiB,MAAM;AAErC,eAAe,YAAY,OAA+C;AACxE,QAAM,UAAU,WAAW,GAAG,KAAK,UAAU,EAAE,KAAI,oBAAI,KAAK,GAAE,YAAY,GAAG,GAAG,MAAM,CAAC,CAAC;AAAA,GAAM;AAAA,IAC5F,MAAM;AAAA,EACR,CAAC,EAAE,MAAM,OAAO,UAAU;AACxB,QAAK,MAAgC,SAAS,SAAU,OAAM;AAC9D,UAAM,OAAO,aAAkB,EAAE;AAAA,MAAK,CAAC,EAAE,MAAM,MAC7C,MAAM,QAAQ,SAAS,GAAG,EAAE,WAAW,KAAK,CAAC;AAAA,IAC/C;AACA,UAAM,UAAU,WAAW,GAAG,KAAK,UAAU,EAAE,KAAI,oBAAI,KAAK,GAAE,YAAY,GAAG,GAAG,MAAM,CAAC,CAAC;AAAA,GAAM;AAAA,MAC5F,MAAM;AAAA,IACR,CAAC;AAAA,EACH,CAAC;AACH;AAEA,IAAI;AACF,MAAI,YAAY,UAAU;AACxB,YAAQ,OAAO;AAAA,MACb,GAAG,KAAK,UAAU,EAAE,YAAY,qBAAqB,GAAI,MAAM,MAAM,OAAO,EAAG,GAAG,MAAM,CAAC,CAAC;AAAA;AAAA,IAC5F;AAAA,EACF,WAAW,YAAY,WAAW;AAChC,UAAM,MAAM,QAAQ,KAAK,CAAC;AAC1B,QAAI,CAAC,IAAK,OAAM,IAAI,MAAM,kBAAkB;AAC5C,UAAM,OAAO,MAAM,MAAM,WAAW,EAAE,KAAK,SAAS,CAAC;AACrD,UAAM,YAAY,EAAE,MAAM,WAAW,KAAK,UAAU,KAAK,CAAC;AAC1D,YAAQ,OAAO;AAAA,MACb,GAAG,KAAK,UAAU,EAAE,YAAY,qBAAqB,UAAU,KAAK,KAAK,OAAO,KAAK,MAAM,GAAG,MAAM,CAAC,CAAC;AAAA;AAAA,IACxG;AAAA,EACF,WAAW,YAAY,QAAQ;AAC7B,YAAQ,OAAO;AAAA,MACb,GAAG,KAAK,UAAU,EAAE,YAAY,qBAAqB,aAAa,MAAM,MAAM,KAAK,QAAQ,EAAE,GAAG,MAAM,CAAC,CAAC;AAAA;AAAA,IAC1G;AAAA,EACF,WAAW,YAAY,SAAS;AAC9B,UAAM,MAAM,MAAM,SAAS,WAAW,MAAM,EAAE,MAAM,MAAM,EAAE;AAC5D,UAAM,UAAU,IACb,KAAK,EACL,MAAM,IAAI,EACV,OAAO,OAAO,EACd,IAAI,CAAC,SAAS,KAAK,MAAM,IAAI,CAAY;AAC5C,YAAQ,OAAO;AAAA,MACb,GAAG,KAAK,UAAU,EAAE,YAAY,qBAAqB,QAAQ,GAAG,MAAM,CAAC,CAAC;AAAA;AAAA,IAC1E;AAAA,EACF,OAAO;AACL,YAAQ,OAAO,MAAM,sCAAsC,OAAO;AAAA,CAAI;AACtE,YAAQ,WAAW;AAAA,EACrB;AACF,UAAE;AACA,QAAM,MAAM,MAAM;AACpB;","names":[]}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { EncryptedSecret, VaultProvider } from '@nebutra/vault';
|
|
2
|
+
export { CapabilityError } from '@nebutra/errors';
|
|
3
|
+
|
|
4
|
+
interface OAuthStartRequest {
|
|
5
|
+
readonly app: string;
|
|
6
|
+
readonly tenantId: string;
|
|
7
|
+
readonly scopes?: readonly string[];
|
|
8
|
+
}
|
|
9
|
+
interface OAuthStartResult {
|
|
10
|
+
readonly provider: string;
|
|
11
|
+
readonly app: string;
|
|
12
|
+
readonly url: string;
|
|
13
|
+
readonly state: string;
|
|
14
|
+
readonly scopes: readonly string[];
|
|
15
|
+
}
|
|
16
|
+
interface ConnectTokenRequest {
|
|
17
|
+
readonly tenantId: string;
|
|
18
|
+
readonly app: string;
|
|
19
|
+
readonly accessToken: string;
|
|
20
|
+
readonly scopes: readonly string[];
|
|
21
|
+
readonly provider?: string;
|
|
22
|
+
}
|
|
23
|
+
interface InvokeRequest {
|
|
24
|
+
readonly tenantId: string;
|
|
25
|
+
readonly app: string;
|
|
26
|
+
readonly action: string;
|
|
27
|
+
readonly args: Record<string, unknown>;
|
|
28
|
+
}
|
|
29
|
+
interface InvokeResult {
|
|
30
|
+
readonly ok: boolean;
|
|
31
|
+
readonly provider?: string;
|
|
32
|
+
readonly result?: unknown;
|
|
33
|
+
readonly suggestion?: string;
|
|
34
|
+
readonly error?: string;
|
|
35
|
+
}
|
|
36
|
+
interface IntegrationConnection {
|
|
37
|
+
readonly id: string;
|
|
38
|
+
readonly tenantId: string;
|
|
39
|
+
readonly app: string;
|
|
40
|
+
readonly provider: string;
|
|
41
|
+
readonly tokenId: string;
|
|
42
|
+
readonly scopes: readonly string[];
|
|
43
|
+
readonly encrypted: EncryptedSecret;
|
|
44
|
+
readonly connectedAt: string;
|
|
45
|
+
}
|
|
46
|
+
interface ListedConnection {
|
|
47
|
+
readonly id: string;
|
|
48
|
+
readonly app: string;
|
|
49
|
+
readonly provider: string;
|
|
50
|
+
readonly tokenId: string;
|
|
51
|
+
readonly scopes: readonly string[];
|
|
52
|
+
readonly connectedAt: string;
|
|
53
|
+
}
|
|
54
|
+
interface IntegrationProvider {
|
|
55
|
+
readonly id: string;
|
|
56
|
+
supports(app: string): boolean;
|
|
57
|
+
startOAuth(request: OAuthStartRequest): Promise<OAuthStartResult>;
|
|
58
|
+
invoke(request: InvokeRequest, token: string, scopes: readonly string[]): Promise<unknown>;
|
|
59
|
+
doctor(): Promise<{
|
|
60
|
+
ok: boolean;
|
|
61
|
+
provider: string;
|
|
62
|
+
suggestion?: string;
|
|
63
|
+
}>;
|
|
64
|
+
}
|
|
65
|
+
interface SaasConsentGrant {
|
|
66
|
+
readonly tenantId: string;
|
|
67
|
+
readonly app: string;
|
|
68
|
+
readonly action: string;
|
|
69
|
+
readonly scopes: readonly string[];
|
|
70
|
+
}
|
|
71
|
+
interface SaasConsentStore {
|
|
72
|
+
grant(grant: SaasConsentGrant): Promise<void>;
|
|
73
|
+
hasConsent(grant: SaasConsentGrant): Promise<boolean>;
|
|
74
|
+
list(tenantId?: string): Promise<SaasConsentGrant[]>;
|
|
75
|
+
}
|
|
76
|
+
interface IntegrationVaultOptions {
|
|
77
|
+
readonly vault?: VaultProvider;
|
|
78
|
+
readonly providers?: readonly IntegrationProvider[];
|
|
79
|
+
readonly consent?: SaasConsentStore;
|
|
80
|
+
}
|
|
81
|
+
declare class InMemorySaasConsentStore implements SaasConsentStore {
|
|
82
|
+
#private;
|
|
83
|
+
grant(grant: SaasConsentGrant): Promise<void>;
|
|
84
|
+
hasConsent(grant: SaasConsentGrant): Promise<boolean>;
|
|
85
|
+
list(tenantId?: string): Promise<SaasConsentGrant[]>;
|
|
86
|
+
}
|
|
87
|
+
declare class InMemoryIntegrationProvider implements IntegrationProvider {
|
|
88
|
+
#private;
|
|
89
|
+
readonly id: string;
|
|
90
|
+
constructor(options: {
|
|
91
|
+
id: string;
|
|
92
|
+
apps: readonly string[];
|
|
93
|
+
});
|
|
94
|
+
supports(app: string): boolean;
|
|
95
|
+
startOAuth(request: OAuthStartRequest): Promise<OAuthStartResult>;
|
|
96
|
+
invoke(request: InvokeRequest, _token: string, _scopes: readonly string[]): Promise<unknown>;
|
|
97
|
+
doctor(): Promise<{
|
|
98
|
+
ok: boolean;
|
|
99
|
+
provider: string;
|
|
100
|
+
}>;
|
|
101
|
+
}
|
|
102
|
+
declare class HttpIntegrationProvider implements IntegrationProvider {
|
|
103
|
+
#private;
|
|
104
|
+
readonly id: string;
|
|
105
|
+
constructor(options: {
|
|
106
|
+
id: string;
|
|
107
|
+
baseUrl: string;
|
|
108
|
+
apps?: readonly string[];
|
|
109
|
+
fetch?: typeof fetch;
|
|
110
|
+
});
|
|
111
|
+
supports(app: string): boolean;
|
|
112
|
+
startOAuth(request: OAuthStartRequest): Promise<OAuthStartResult>;
|
|
113
|
+
invoke(request: InvokeRequest, token: string): Promise<unknown>;
|
|
114
|
+
doctor(): Promise<{
|
|
115
|
+
ok: boolean;
|
|
116
|
+
provider: string;
|
|
117
|
+
suggestion?: string;
|
|
118
|
+
}>;
|
|
119
|
+
}
|
|
120
|
+
declare class IntegrationVault {
|
|
121
|
+
#private;
|
|
122
|
+
constructor(options: IntegrationVaultOptions);
|
|
123
|
+
static local(options?: IntegrationVaultOptions): IntegrationVault;
|
|
124
|
+
startOAuth(request: OAuthStartRequest): Promise<OAuthStartResult>;
|
|
125
|
+
connectToken(request: ConnectTokenRequest): Promise<ListedConnection>;
|
|
126
|
+
grantConsent(grant: SaasConsentGrant): Promise<void>;
|
|
127
|
+
invoke(request: InvokeRequest): Promise<InvokeResult>;
|
|
128
|
+
list(tenantId: string): Promise<ListedConnection[]>;
|
|
129
|
+
doctor(): Promise<{
|
|
130
|
+
ok: boolean;
|
|
131
|
+
providers: Array<{
|
|
132
|
+
ok: boolean;
|
|
133
|
+
provider: string;
|
|
134
|
+
suggestion?: string;
|
|
135
|
+
}>;
|
|
136
|
+
}>;
|
|
137
|
+
close(): Promise<void>;
|
|
138
|
+
}
|
|
139
|
+
declare const vault: IntegrationVault;
|
|
140
|
+
|
|
141
|
+
export { type ConnectTokenRequest, HttpIntegrationProvider, InMemoryIntegrationProvider, InMemorySaasConsentStore, type IntegrationConnection, type IntegrationProvider, IntegrationVault, type IntegrationVaultOptions, type InvokeRequest, type InvokeResult, type ListedConnection, type OAuthStartRequest, type OAuthStartResult, type SaasConsentGrant, type SaasConsentStore, vault };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CapabilityError,
|
|
3
|
+
HttpIntegrationProvider,
|
|
4
|
+
InMemoryIntegrationProvider,
|
|
5
|
+
InMemorySaasConsentStore,
|
|
6
|
+
IntegrationVault,
|
|
7
|
+
vault
|
|
8
|
+
} from "./chunk-6YC3QBSB.js";
|
|
9
|
+
export {
|
|
10
|
+
CapabilityError,
|
|
11
|
+
HttpIntegrationProvider,
|
|
12
|
+
InMemoryIntegrationProvider,
|
|
13
|
+
InMemorySaasConsentStore,
|
|
14
|
+
IntegrationVault,
|
|
15
|
+
vault
|
|
16
|
+
};
|
|
17
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -0,0 +1,34 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
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
|
+
}
|