@nebutra/integration-vault 0.1.1 → 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/CHANGELOG.md +24 -0
- package/LICENSE +21 -676
- package/dist/{chunk-6YC3QBSB.js → chunk-TQULRIQM.js} +7 -2
- package/dist/chunk-TQULRIQM.js.map +1 -0
- package/dist/cli.js +1 -1
- package/dist/index.js +1 -1
- package/package.json +18 -7
- package/.turbo/turbo-build.log +0 -22
- package/.turbo/turbo-test.log +0 -14
- package/.turbo/turbo-typecheck.log +0 -4
- package/dist/chunk-6YC3QBSB.js.map +0 -1
- package/examples/connect-token.ts +0 -34
- package/examples/invoke-consented.ts +0 -32
- package/examples/provider-fallback.ts +0 -26
- package/src/cli.ts +0 -57
- package/src/index.test.ts +0 -86
- package/src/index.ts +0 -407
- package/tsconfig.json +0 -12
- package/tsup.config.ts +0 -11
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
import { randomUUID } from "crypto";
|
|
3
3
|
import { CapabilityError } from "@nebutra/errors";
|
|
4
4
|
import { LocalProvider } from "@nebutra/vault";
|
|
5
|
+
import pLimit from "p-limit";
|
|
5
6
|
import { CapabilityError as CapabilityError2 } from "@nebutra/errors";
|
|
7
|
+
var PROVIDER_DOCTOR_CONCURRENCY = 4;
|
|
6
8
|
function requireTenant(tenantId) {
|
|
7
9
|
if (!tenantId.trim()) {
|
|
8
10
|
throw new CapabilityError("integration-vault", "Tenant context is required", {
|
|
@@ -222,7 +224,10 @@ var IntegrationVault = class _IntegrationVault {
|
|
|
222
224
|
return this.#connections.list(tenantId);
|
|
223
225
|
}
|
|
224
226
|
async doctor() {
|
|
225
|
-
const
|
|
227
|
+
const limit = pLimit(PROVIDER_DOCTOR_CONCURRENCY);
|
|
228
|
+
const providers = await Promise.all(
|
|
229
|
+
this.#providers.map((provider) => limit(() => provider.doctor()))
|
|
230
|
+
);
|
|
226
231
|
return { ok: providers.some((provider) => provider.ok), providers };
|
|
227
232
|
}
|
|
228
233
|
async close() {
|
|
@@ -271,4 +276,4 @@ export {
|
|
|
271
276
|
vault,
|
|
272
277
|
CapabilityError2 as CapabilityError
|
|
273
278
|
};
|
|
274
|
-
//# sourceMappingURL=chunk-
|
|
279
|
+
//# sourceMappingURL=chunk-TQULRIQM.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\";\nimport pLimit from \"p-limit\";\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\nconst PROVIDER_DOCTOR_CONCURRENCY = 4;\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 limit = pLimit(PROVIDER_DOCTOR_CONCURRENCY);\n const providers = await Promise.all(\n this.#providers.map((provider) => limit(() => provider.doctor())),\n );\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":";AAAA,SAAS,kBAAkB;AAC3B,SAAS,uBAAuB;AAChC,SAA+B,qBAAyC;AACxE,OAAO,YAAY;AAEnB,SAAS,mBAAAA,wBAAuB;AAsFhC,IAAM,8BAA8B;AAEpC,SAAS,cAAc,UAAwB;AAC7C,MAAI,CAAC,SAAS,KAAK,GAAG;AACpB,UAAM,IAAI,gBAAgB,qBAAqB,8BAA8B;AAAA,MAC3E,YACE;AAAA,MACF,YAAY;AAAA,IACd,CAAC;AAAA,EACH;AACF;AAEA,SAAS,WAAW,OAAiD;AACnE,SAAO,CAAC,MAAM,UAAU,MAAM,KAAK,MAAM,MAAM,EAAE,KAAK,GAAG;AAC3D;AAEO,IAAM,2BAAN,MAA2D;AAAA,EACvD,UAAU,oBAAI,IAA8B;AAAA,EAErD,MAAM,MAAM,OAAwC;AAClD,SAAK,QAAQ,IAAI,WAAW,KAAK,GAAG,EAAE,GAAG,OAAO,QAAQ,CAAC,GAAG,MAAM,MAAM,EAAE,CAAC;AAAA,EAC7E;AAAA,EAEA,MAAM,WAAW,OAA2C;AAC1D,UAAM,WAAW,KAAK,QAAQ,IAAI,WAAW,KAAK,CAAC;AACnD,QAAI,CAAC,SAAU,QAAO;AACtB,UAAM,SAAS,IAAI,IAAI,SAAS,MAAM;AACtC,WAAO,MAAM,OAAO,MAAM,CAAC,UAAU,OAAO,IAAI,KAAK,CAAC;AAAA,EACxD;AAAA,EAEA,MAAM,KAAK,UAAgD;AACzD,WAAO,MAAM,KAAK,KAAK,QAAQ,OAAO,CAAC,EAAE;AAAA,MACvC,CAAC,UAAU,aAAa,UAAa,MAAM,aAAa;AAAA,IAC1D;AAAA,EACF;AACF;AAEO,IAAM,8BAAN,MAAiE;AAAA,EAC7D;AAAA,EACA;AAAA,EAET,YAAY,SAAkD;AAC5D,SAAK,KAAK,QAAQ;AAClB,SAAK,QAAQ,IAAI,IAAI,QAAQ,IAAI;AAAA,EACnC;AAAA,EAEA,SAAS,KAAsB;AAC7B,WAAO,KAAK,MAAM,IAAI,GAAG,KAAK,KAAK,MAAM,IAAI,GAAG;AAAA,EAClD;AAAA,EAEA,MAAM,WAAW,SAAuD;AACtE,WAAO;AAAA,MACL,UAAU,KAAK;AAAA,MACf,KAAK,QAAQ;AAAA,MACb,KAAK,+BAA+B,QAAQ,GAAG,WAAW,QAAQ,QAAQ;AAAA,MAC1E,OAAO,SAAS,QAAQ,QAAQ,IAAI,QAAQ,GAAG;AAAA,MAC/C,QAAQ,QAAQ,UAAU,CAAC;AAAA,IAC7B;AAAA,EACF;AAAA,EAEA,MAAM,OACJ,SACA,QACA,SACkB;AAClB,WAAO,EAAE,KAAK,QAAQ,KAAK,QAAQ,QAAQ,QAAQ,GAAG,QAAQ,KAAK;AAAA,EACrE;AAAA,EAEA,MAAM,SAAqD;AACzD,WAAO,EAAE,IAAI,MAAM,UAAU,KAAK,GAAG;AAAA,EACvC;AACF;AAEO,IAAM,0BAAN,MAA6D;AAAA,EACzD;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAET,YAAY,SAKT;AACD,SAAK,KAAK,QAAQ;AAClB,SAAK,WAAW,QAAQ,QAAQ,QAAQ,OAAO,EAAE;AACjD,SAAK,QAAQ,IAAI,IAAI,QAAQ,QAAQ,CAAC,GAAG,CAAC;AAC1C,SAAK,SAAS,QAAQ,SAAS;AAAA,EACjC;AAAA,EAEA,SAAS,KAAsB;AAC7B,WAAO,KAAK,MAAM,IAAI,GAAG,KAAK,KAAK,MAAM,IAAI,GAAG;AAAA,EAClD;AAAA,EAEA,MAAM,WAAW,SAAuD;AACtE,WAAO;AAAA,MACL,UAAU,KAAK;AAAA,MACf,KAAK,QAAQ;AAAA,MACb,KAAK,GAAG,KAAK,QAAQ,UAAU,QAAQ,GAAG,iBAAiB,mBAAmB,QAAQ,QAAQ,CAAC;AAAA,MAC/F,OAAO,SAAS,QAAQ,QAAQ,IAAI,QAAQ,GAAG;AAAA,MAC/C,QAAQ,QAAQ,UAAU,CAAC;AAAA,IAC7B;AAAA,EACF;AAAA,EAEA,MAAM,OAAO,SAAwB,OAAiC;AACpE,UAAM,WAAW,MAAM,KAAK,OAAO,GAAG,KAAK,QAAQ,WAAW;AAAA,MAC5D,QAAQ;AAAA,MACR,SAAS;AAAA,QACP,eAAe,UAAU,KAAK;AAAA,QAC9B,gBAAgB;AAAA,MAClB;AAAA,MACA,MAAM,KAAK,UAAU,OAAO;AAAA,IAC9B,CAAC;AACD,QAAI,CAAC,SAAS,IAAI;AAChB,YAAM,IAAI;AAAA,QACR;AAAA,QACA,YAAY,KAAK,EAAE,aAAa,SAAS,MAAM;AAAA,QAC/C;AAAA,UACE,YAAY;AAAA,UACZ,UAAU,EAAE,UAAU,KAAK,IAAI,KAAK,QAAQ,KAAK,QAAQ,SAAS,OAAO;AAAA,QAC3E;AAAA,MACF;AAAA,IACF;AACA,WAAO,SAAS,KAAK;AAAA,EACvB;AAAA,EAEA,MAAM,SAA0E;AAC9E,WAAO;AAAA,MACL,IAAI;AAAA,MACJ,UAAU,KAAK;AAAA,MACf,YACE;AAAA,IACJ;AAAA,EACF;AACF;AAEA,IAAM,0BAAN,MAA8B;AAAA,EACnB,eAAe,oBAAI,IAAmC;AAAA,EAE/D,OAAO,YAA0D;AAC/D,SAAK,aAAa,IAAI,GAAG,WAAW,QAAQ,IAAI,WAAW,GAAG,IAAI,UAAU;AAC5E,WAAO;AAAA,EACT;AAAA,EAEA,IAAI,UAAkB,KAAgD;AACpE,WAAO,KAAK,aAAa,IAAI,GAAG,QAAQ,IAAI,GAAG,EAAE;AAAA,EACnD;AAAA,EAEA,KAAK,UAAsC;AACzC,WAAO,MAAM,KAAK,KAAK,aAAa,OAAO,CAAC,EACzC,OAAO,CAAC,eAAe,WAAW,aAAa,QAAQ,EACvD,IAAI,CAAC,gBAAgB;AAAA,MACpB,IAAI,WAAW;AAAA,MACf,KAAK,WAAW;AAAA,MAChB,UAAU,WAAW;AAAA,MACrB,SAAS,WAAW;AAAA,MACpB,QAAQ,WAAW;AAAA,MACnB,aAAa,WAAW;AAAA,IAC1B,EAAE;AAAA,EACN;AACF;AAEO,IAAM,mBAAN,MAAM,kBAAiB;AAAA,EACnB;AAAA,EACA;AAAA,EACA,eAAe,IAAI,wBAAwB;AAAA,EAC3C;AAAA,EAET,YAAY,SAAkC;AAC5C,SAAK,SACH,QAAQ,SACR,IAAI,cAAc;AAAA,MAChB,UAAU;AAAA,MACV,WAAW,QAAQ,IAAI,0BAA0B;AAAA,IACnD,CAAC;AACH,SAAK,aACH,QAAQ,aAAa,QAAQ,UAAU,SAAS,IAC5C,CAAC,GAAG,QAAQ,SAAS,IACrB,CAAC,IAAI,4BAA4B,EAAE,IAAI,SAAS,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC;AACpE,SAAK,WAAW,QAAQ,WAAW,IAAI,yBAAyB;AAAA,EAClE;AAAA,EAEA,OAAO,MAAM,UAAmC,CAAC,GAAqB;AACpE,WAAO,IAAI,kBAAiB,OAAO;AAAA,EACrC;AAAA,EAEA,MAAM,WAAW,SAAuD;AACtE,kBAAc,QAAQ,QAAQ;AAC9B,WAAO,KAAK,aAAa,QAAQ,GAAG,EAAE,WAAW,OAAO;AAAA,EAC1D;AAAA,EAEA,MAAM,aAAa,SAAyD;AAC1E,kBAAc,QAAQ,QAAQ;AAC9B,UAAM,WAAW,QAAQ,WACrB,KAAK,WAAW,KAAK,CAAC,cAAc,UAAU,OAAO,QAAQ,QAAQ,IACrE,KAAK,aAAa,QAAQ,GAAG;AACjC,QAAI,CAAC,UAAU;AACb,YAAM,IAAI,gBAAgB,qBAAqB,kCAAkC;AAAA,QAC/E,YAAY;AAAA,QACZ,UAAU,EAAE,KAAK,QAAQ,KAAK,UAAU,QAAQ,SAAS;AAAA,QACzD,YAAY;AAAA,MACd,CAAC;AAAA,IACH;AACA,UAAM,UAAU,OAAO,WAAW,CAAC;AACnC,UAAM,YAAY,MAAM,KAAK,OAAO,QAAQ,QAAQ,aAAa;AAAA,MAC/D,IAAI;AAAA,MACJ,UAAU,QAAQ;AAAA,MAClB,UAAU,EAAE,MAAM,GAAG,QAAQ,GAAG,iBAAiB,MAAM,cAAc;AAAA,IACvE,CAAC;AACD,UAAM,aAAa,KAAK,aAAa,OAAO;AAAA,MAC1C,IAAI,QAAQ,WAAW,CAAC;AAAA,MACxB,UAAU,QAAQ;AAAA,MAClB,KAAK,QAAQ;AAAA,MACb,UAAU,SAAS;AAAA,MACnB;AAAA,MACA,QAAQ,CAAC,GAAG,QAAQ,MAAM;AAAA,MAC1B;AAAA,MACA,cAAa,oBAAI,KAAK,GAAE,YAAY;AAAA,IACtC,CAAC;AACD,WAAO,KAAK,QAAQ,UAAU;AAAA,EAChC;AAAA,EAEA,MAAM,aAAa,OAAwC;AACzD,kBAAc,MAAM,QAAQ;AAC5B,UAAM,KAAK,SAAS,MAAM,KAAK;AAAA,EACjC;AAAA,EAEA,MAAM,OAAO,SAA+C;AAC1D,kBAAc,QAAQ,QAAQ;AAC9B,UAAM,aAAa,KAAK,aAAa,IAAI,QAAQ,UAAU,QAAQ,GAAG;AACtE,QAAI,CAAC,YAAY;AACf,aAAO;AAAA,QACL,IAAI;AAAA,QACJ,OAAO;AAAA,QACP,YAAY,4BAA4B,QAAQ,GAAG,sBAAsB,QAAQ,GAAG,IAAI,QAAQ,MAAM;AAAA,MACxG;AAAA,IACF;AACA,UAAM,aAAa,MAAM,KAAK,SAAS,WAAW;AAAA,MAChD,UAAU,QAAQ;AAAA,MAClB,KAAK,QAAQ;AAAA,MACb,QAAQ,QAAQ;AAAA,MAChB,QAAQ,WAAW;AAAA,IACrB,CAAC;AACD,QAAI,CAAC,YAAY;AACf,aAAO;AAAA,QACL,IAAI;AAAA,QACJ,OAAO;AAAA,QACP,YAAY,4BAA4B,QAAQ,GAAG,gBAAgB,QAAQ,GAAG,IAAI,QAAQ,MAAM;AAAA,MAClG;AAAA,IACF;AACA,QAAI;AACF,YAAM,QAAQ,MAAM,KAAK,OAAO,QAAQ,WAAW,WAAW,EAAE,UAAU,QAAQ,SAAS,CAAC;AAC5F,YAAM,WAAW,KAAK,cAAc,WAAW,QAAQ;AACvD,YAAM,SAAS,MAAM,SAAS,OAAO,SAAS,OAAO,WAAW,MAAM;AACtE,aAAO,EAAE,IAAI,MAAM,UAAU,SAAS,IAAI,OAAO;AAAA,IACnD,SAAS,OAAO;AACd,aAAO;AAAA,QACL,IAAI;AAAA,QACJ,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAAA,QAC5D,YAAY;AAAA,MACd;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAM,KAAK,UAA+C;AACxD,kBAAc,QAAQ;AACtB,WAAO,KAAK,aAAa,KAAK,QAAQ;AAAA,EACxC;AAAA,EAEA,MAAM,SAGH;AACD,UAAM,QAAQ,OAAO,2BAA2B;AAChD,UAAM,YAAY,MAAM,QAAQ;AAAA,MAC9B,KAAK,WAAW,IAAI,CAAC,aAAa,MAAM,MAAM,SAAS,OAAO,CAAC,CAAC;AAAA,IAClE;AACA,WAAO,EAAE,IAAI,UAAU,KAAK,CAAC,aAAa,SAAS,EAAE,GAAG,UAAU;AAAA,EACpE;AAAA,EAEA,MAAM,QAAuB;AAC3B,UAAM,KAAK,OAAO,MAAM;AAAA,EAC1B;AAAA,EAEA,aAAa,KAAkC;AAC7C,UAAM,WAAW,KAAK,WAAW,KAAK,CAAC,cAAc,UAAU,SAAS,GAAG,CAAC;AAC5E,QAAI,CAAC,UAAU;AACb,YAAM,IAAI,gBAAgB,qBAAqB,iCAAiC;AAAA,QAC9E,YAAY;AAAA,QACZ,UAAU,EAAE,IAAI;AAAA,QAChB,YAAY;AAAA,MACd,CAAC;AAAA,IACH;AACA,WAAO;AAAA,EACT;AAAA,EAEA,cAAc,IAAiC;AAC7C,UAAM,WAAW,KAAK,WAAW,KAAK,CAAC,cAAc,UAAU,OAAO,EAAE;AACxE,QAAI,CAAC,UAAU;AACb,YAAM,IAAI,gBAAgB,qBAAqB,sCAAsC;AAAA,QACnF,YAAY;AAAA,QACZ,UAAU,EAAE,UAAU,GAAG;AAAA,QACzB,YAAY;AAAA,MACd,CAAC;AAAA,IACH;AACA,WAAO;AAAA,EACT;AAAA,EAEA,QAAQ,YAAqD;AAC3D,WAAO;AAAA,MACL,IAAI,WAAW;AAAA,MACf,KAAK,WAAW;AAAA,MAChB,UAAU,WAAW;AAAA,MACrB,SAAS,WAAW;AAAA,MACpB,QAAQ,WAAW;AAAA,MACnB,aAAa,WAAW;AAAA,IAC1B;AAAA,EACF;AACF;AAEO,IAAM,QAAQ,iBAAiB,MAAM;","names":["CapabilityError"]}
|
package/dist/cli.js
CHANGED
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nebutra/integration-vault",
|
|
3
|
-
"version": "0.1.
|
|
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,17 +10,22 @@
|
|
|
10
10
|
"category": "integrations",
|
|
11
11
|
"summary": "OAuth, secret storage, and SaaS action invocation boundary"
|
|
12
12
|
},
|
|
13
|
-
"main": "./
|
|
14
|
-
"types": "./
|
|
13
|
+
"main": "./dist/index.js",
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
15
|
"exports": {
|
|
16
|
-
".":
|
|
16
|
+
".": {
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"import": "./dist/index.js",
|
|
19
|
+
"default": "./dist/index.js"
|
|
20
|
+
}
|
|
17
21
|
},
|
|
18
22
|
"dependencies": {
|
|
19
|
-
"
|
|
20
|
-
"@nebutra/vault": "0.1.
|
|
23
|
+
"p-limit": "^7.3.0",
|
|
24
|
+
"@nebutra/vault": "0.1.2",
|
|
25
|
+
"@nebutra/errors": "0.1.2"
|
|
21
26
|
},
|
|
22
27
|
"devDependencies": {
|
|
23
|
-
"@types/node": "^
|
|
28
|
+
"@types/node": "^25.9.1",
|
|
24
29
|
"tsup": "^8.5.1",
|
|
25
30
|
"typescript": "^5.9.3",
|
|
26
31
|
"vitest": "^4.1.4"
|
|
@@ -37,6 +42,12 @@
|
|
|
37
42
|
"publishConfig": {
|
|
38
43
|
"access": "public"
|
|
39
44
|
},
|
|
45
|
+
"files": [
|
|
46
|
+
"dist",
|
|
47
|
+
"README.md",
|
|
48
|
+
"LICENSE",
|
|
49
|
+
"CHANGELOG.md"
|
|
50
|
+
],
|
|
40
51
|
"scripts": {
|
|
41
52
|
"build": "tsup",
|
|
42
53
|
"test": "vitest run",
|
package/.turbo/turbo-build.log
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
> @nebutra/integration-vault@0.1.1 build /home/runner/work/Nebutra-Sailor/Nebutra-Sailor/packages/integrations/integration-vault
|
|
3
|
-
> tsup
|
|
4
|
-
|
|
5
|
-
[34mCLI[39m Building entry: src/cli.ts, src/index.ts
|
|
6
|
-
[34mCLI[39m Using tsconfig: tsconfig.json
|
|
7
|
-
[34mCLI[39m tsup v8.5.1
|
|
8
|
-
[34mCLI[39m Using tsup config: /home/runner/work/Nebutra-Sailor/Nebutra-Sailor/packages/integrations/integration-vault/tsup.config.ts
|
|
9
|
-
[34mCLI[39m Target: es2022
|
|
10
|
-
[34mCLI[39m Cleaning output folder
|
|
11
|
-
[34mESM[39m Build start
|
|
12
|
-
[32mESM[39m [1mdist/cli.js [22m[32m2.07 KB[39m
|
|
13
|
-
[32mESM[39m [1mdist/index.js [22m[32m350.00 B[39m
|
|
14
|
-
[32mESM[39m [1mdist/chunk-6YC3QBSB.js [22m[32m8.84 KB[39m
|
|
15
|
-
[32mESM[39m [1mdist/cli.js.map [22m[32m3.96 KB[39m
|
|
16
|
-
[32mESM[39m [1mdist/index.js.map [22m[32m71.00 B[39m
|
|
17
|
-
[32mESM[39m [1mdist/chunk-6YC3QBSB.js.map [22m[32m19.37 KB[39m
|
|
18
|
-
[32mESM[39m ⚡️ Build success in 82ms
|
|
19
|
-
[34mDTS[39m Build start
|
|
20
|
-
[32mDTS[39m ⚡️ Build success in 13234ms
|
|
21
|
-
[32mDTS[39m [1mdist/cli.d.ts [22m[32m13.00 B[39m
|
|
22
|
-
[32mDTS[39m [1mdist/index.d.ts [22m[32m4.66 KB[39m
|
package/.turbo/turbo-test.log
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
> @nebutra/integration-vault@0.1.1 test /home/runner/work/Nebutra-Sailor/Nebutra-Sailor/packages/integrations/integration-vault
|
|
3
|
-
> vitest run
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
[1m[30m[46m RUN [49m[39m[22m [36mv4.1.4 [39m[90m/home/runner/work/Nebutra-Sailor/Nebutra-Sailor/packages/integrations/integration-vault[39m
|
|
7
|
-
|
|
8
|
-
[32m✓[39m src/index.test.ts [2m([22m[2m3 tests[22m[2m)[22m[32m 49[2mms[22m[39m
|
|
9
|
-
|
|
10
|
-
[2m Test Files [22m [1m[32m1 passed[39m[22m[90m (1)[39m
|
|
11
|
-
[2m Tests [22m [1m[32m3 passed[39m[22m[90m (3)[39m
|
|
12
|
-
[2m Start at [22m 08:15:06
|
|
13
|
-
[2m Duration [22m 3.41s[2m (transform 1.01s, setup 0ms, import 2.23s, tests 49ms, environment 0ms)[22m
|
|
14
|
-
|
|
@@ -1 +0,0 @@
|
|
|
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"]}
|
|
@@ -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,86 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import {
|
|
3
|
-
CapabilityError,
|
|
4
|
-
InMemoryIntegrationProvider,
|
|
5
|
-
InMemorySaasConsentStore,
|
|
6
|
-
IntegrationVault,
|
|
7
|
-
} from "./index";
|
|
8
|
-
|
|
9
|
-
describe("IntegrationVault", () => {
|
|
10
|
-
it("requires tenant context before starting OAuth", async () => {
|
|
11
|
-
const vault = IntegrationVault.local({
|
|
12
|
-
providers: [new InMemoryIntegrationProvider({ id: "local", apps: ["notion"] })],
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
await expect(vault.startOAuth({ app: "notion", tenantId: "" })).rejects.toThrow(
|
|
16
|
-
CapabilityError,
|
|
17
|
-
);
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
it("stores tokens encrypted and lists only token ids and scopes", async () => {
|
|
21
|
-
const vault = IntegrationVault.local({
|
|
22
|
-
providers: [new InMemoryIntegrationProvider({ id: "local", apps: ["notion"] })],
|
|
23
|
-
});
|
|
24
|
-
const connection = await vault.connectToken({
|
|
25
|
-
tenantId: "tenant_a",
|
|
26
|
-
app: "notion",
|
|
27
|
-
accessToken: "secret-token",
|
|
28
|
-
scopes: ["pages:write"],
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
await expect(vault.list("tenant_a")).resolves.toEqual([
|
|
32
|
-
expect.objectContaining({
|
|
33
|
-
app: "notion",
|
|
34
|
-
tokenId: connection.tokenId,
|
|
35
|
-
scopes: ["pages:write"],
|
|
36
|
-
}),
|
|
37
|
-
]);
|
|
38
|
-
expect(JSON.stringify(await vault.list("tenant_a"))).not.toContain("secret-token");
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it("requires explicit per-action consent before invoking a SaaS action", async () => {
|
|
42
|
-
const consent = new InMemorySaasConsentStore();
|
|
43
|
-
const vault = IntegrationVault.local({
|
|
44
|
-
consent,
|
|
45
|
-
providers: [new InMemoryIntegrationProvider({ id: "local", apps: ["notion"] })],
|
|
46
|
-
});
|
|
47
|
-
await vault.connectToken({
|
|
48
|
-
tenantId: "tenant_a",
|
|
49
|
-
app: "notion",
|
|
50
|
-
accessToken: "secret-token",
|
|
51
|
-
scopes: ["pages:write"],
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
await expect(
|
|
55
|
-
vault.invoke({
|
|
56
|
-
tenantId: "tenant_a",
|
|
57
|
-
app: "notion",
|
|
58
|
-
action: "create_page",
|
|
59
|
-
args: { title: "Layer 1" },
|
|
60
|
-
}),
|
|
61
|
-
).resolves.toMatchObject({
|
|
62
|
-
ok: false,
|
|
63
|
-
suggestion: expect.stringContaining("vault:connect"),
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
await consent.grant({
|
|
67
|
-
tenantId: "tenant_a",
|
|
68
|
-
app: "notion",
|
|
69
|
-
action: "create_page",
|
|
70
|
-
scopes: ["pages:write"],
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
await expect(
|
|
74
|
-
vault.invoke({
|
|
75
|
-
tenantId: "tenant_a",
|
|
76
|
-
app: "notion",
|
|
77
|
-
action: "create_page",
|
|
78
|
-
args: { title: "Layer 1" },
|
|
79
|
-
}),
|
|
80
|
-
).resolves.toMatchObject({
|
|
81
|
-
ok: true,
|
|
82
|
-
provider: "local",
|
|
83
|
-
result: { app: "notion", action: "create_page", title: "Layer 1" },
|
|
84
|
-
});
|
|
85
|
-
});
|
|
86
|
-
});
|