@kosha-ai/integration-sdk 0.0.1
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/LICENSE +21 -0
- package/README.md +11 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/lib/define-integration.d.ts +15 -0
- package/dist/lib/define-integration.d.ts.map +1 -0
- package/dist/lib/define-integration.js +67 -0
- package/dist/lib/http.d.ts +18 -0
- package/dist/lib/http.d.ts.map +1 -0
- package/dist/lib/http.js +61 -0
- package/dist/lib/oauth2.d.ts +36 -0
- package/dist/lib/oauth2.d.ts.map +1 -0
- package/dist/lib/oauth2.js +113 -0
- package/dist/lib/types.d.ts +141 -0
- package/dist/lib/types.d.ts.map +1 -0
- package/dist/lib/types.js +6 -0
- package/package.json +44 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Codemancers
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# integration-sdk
|
|
2
|
+
|
|
3
|
+
This library was generated with [Nx](https://nx.dev).
|
|
4
|
+
|
|
5
|
+
## Building
|
|
6
|
+
|
|
7
|
+
Run `nx build integration-sdk` to build the library.
|
|
8
|
+
|
|
9
|
+
## Running unit tests
|
|
10
|
+
|
|
11
|
+
Run `nx test integration-sdk` to execute the unit tests via [Jest](https://jestjs.io).
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export * from './lib/types.js';
|
|
2
|
+
export { defineIntegration, httpFor, IntegrationDefinitionError, } from './lib/define-integration.js';
|
|
3
|
+
export { oauth2, OAuth2Error, s256Challenge, generateCodeVerifier, type OAuth2Options, } from './lib/oauth2.js';
|
|
4
|
+
export { createIntegrationHttp, IntegrationHttpError } from './lib/http.js';
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC;AAC/B,OAAO,EACL,iBAAiB,EACjB,OAAO,EACP,0BAA0B,GAC3B,MAAM,6BAA6B,CAAC;AACrC,OAAO,EACL,MAAM,EACN,WAAW,EACX,aAAa,EACb,oBAAoB,EACpB,KAAK,aAAa,GACnB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export * from './lib/types.js';
|
|
2
|
+
export { defineIntegration, httpFor, IntegrationDefinitionError, } from './lib/define-integration.js';
|
|
3
|
+
export { oauth2, OAuth2Error, s256Challenge, generateCodeVerifier, } from './lib/oauth2.js';
|
|
4
|
+
export { createIntegrationHttp, IntegrationHttpError } from './lib/http.js';
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { IntegrationHttp, KoshaIntegration } from './types.js';
|
|
2
|
+
export declare class IntegrationDefinitionError extends Error {
|
|
3
|
+
constructor(message: string);
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Validates an integration definition and returns it unchanged.
|
|
7
|
+
*
|
|
8
|
+
* Validation runs at definition time — i.e. at server boot, before the first
|
|
9
|
+
* request — so a malformed integration fails startup loudly instead of
|
|
10
|
+
* half-registering and erroring later on someone's OAuth callback.
|
|
11
|
+
*/
|
|
12
|
+
export declare function defineIntegration(definition: KoshaIntegration): KoshaIntegration;
|
|
13
|
+
/** The HTTP client the server passes to `identity.fetchAccount`. */
|
|
14
|
+
export declare function httpFor(integration: KoshaIntegration): IntegrationHttp;
|
|
15
|
+
//# sourceMappingURL=define-integration.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"define-integration.d.ts","sourceRoot":"","sources":["../../src/lib/define-integration.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEpE,qBAAa,0BAA2B,SAAQ,KAAK;gBACvC,OAAO,EAAE,MAAM;CAI5B;AAID;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAC/B,UAAU,EAAE,gBAAgB,GAC3B,gBAAgB,CAqDlB;AAED,oEAAoE;AACpE,wBAAgB,OAAO,CAAC,WAAW,EAAE,gBAAgB,GAAG,eAAe,CAMtE"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { createIntegrationHttp } from './http.js';
|
|
2
|
+
export class IntegrationDefinitionError extends Error {
|
|
3
|
+
constructor(message) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = 'IntegrationDefinitionError';
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
const KEY_PATTERN = /^[a-z][a-z0-9-]*$/;
|
|
9
|
+
/**
|
|
10
|
+
* Validates an integration definition and returns it unchanged.
|
|
11
|
+
*
|
|
12
|
+
* Validation runs at definition time — i.e. at server boot, before the first
|
|
13
|
+
* request — so a malformed integration fails startup loudly instead of
|
|
14
|
+
* half-registering and erroring later on someone's OAuth callback.
|
|
15
|
+
*/
|
|
16
|
+
export function defineIntegration(definition) {
|
|
17
|
+
const problems = [];
|
|
18
|
+
if (!definition.key || !KEY_PATTERN.test(definition.key)) {
|
|
19
|
+
problems.push(`key must be lowercase alphanumeric with dashes, got ${JSON.stringify(definition.key)}`);
|
|
20
|
+
}
|
|
21
|
+
if (!definition.name)
|
|
22
|
+
problems.push('name is required');
|
|
23
|
+
if (!definition.auth) {
|
|
24
|
+
problems.push('auth is required');
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
if (typeof definition.auth.buildAuthorizationUrl !== 'function') {
|
|
28
|
+
problems.push('auth.buildAuthorizationUrl must be a function');
|
|
29
|
+
}
|
|
30
|
+
if (typeof definition.auth.exchangeCode !== 'function') {
|
|
31
|
+
problems.push('auth.exchangeCode must be a function');
|
|
32
|
+
}
|
|
33
|
+
if (!Array.isArray(definition.auth.scopesAvailable)) {
|
|
34
|
+
problems.push('auth.scopesAvailable must be an array');
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (!definition.api?.baseUrl) {
|
|
38
|
+
problems.push('api.baseUrl is required');
|
|
39
|
+
}
|
|
40
|
+
else if (!/^https?:\/\//.test(definition.api.baseUrl)) {
|
|
41
|
+
problems.push(`api.baseUrl must be absolute, got ${definition.api.baseUrl}`);
|
|
42
|
+
}
|
|
43
|
+
if (definition.api && typeof definition.api.authHeaders !== 'function') {
|
|
44
|
+
problems.push('api.authHeaders must be a function');
|
|
45
|
+
}
|
|
46
|
+
if (typeof definition.identity?.fetchAccount !== 'function') {
|
|
47
|
+
problems.push('identity.fetchAccount must be a function');
|
|
48
|
+
}
|
|
49
|
+
for (const field of definition.configFields ?? []) {
|
|
50
|
+
if (!field.key)
|
|
51
|
+
problems.push('configFields[].key is required');
|
|
52
|
+
if (field.type === 'select' && !field.options?.length) {
|
|
53
|
+
problems.push(`configFields.${field.key} is a select with no options`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (problems.length > 0) {
|
|
57
|
+
throw new IntegrationDefinitionError(`Invalid integration "${definition.key ?? '<unnamed>'}":\n - ${problems.join('\n - ')}`);
|
|
58
|
+
}
|
|
59
|
+
return definition;
|
|
60
|
+
}
|
|
61
|
+
/** The HTTP client the server passes to `identity.fetchAccount`. */
|
|
62
|
+
export function httpFor(integration) {
|
|
63
|
+
return createIntegrationHttp({
|
|
64
|
+
baseUrl: integration.api.baseUrl,
|
|
65
|
+
authHeaders: ({ accessToken }) => integration.api.authHeaders({ accessToken }),
|
|
66
|
+
});
|
|
67
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { IntegrationHttp } from './types.js';
|
|
2
|
+
export declare class IntegrationHttpError extends Error {
|
|
3
|
+
readonly status: number;
|
|
4
|
+
readonly body: unknown;
|
|
5
|
+
constructor(message: string, status: number, body: unknown);
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* The HTTP client handed to integrations. Deliberately thin: integrations
|
|
9
|
+
* describe providers, they should not each ship a request layer.
|
|
10
|
+
*/
|
|
11
|
+
export declare function createIntegrationHttp(options: {
|
|
12
|
+
baseUrl: string;
|
|
13
|
+
defaultHeaders?: Record<string, string>;
|
|
14
|
+
authHeaders?: (credential: {
|
|
15
|
+
accessToken: string;
|
|
16
|
+
}) => Record<string, string>;
|
|
17
|
+
}): IntegrationHttp;
|
|
18
|
+
//# sourceMappingURL=http.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../src/lib/http.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAA0B,MAAM,YAAY,CAAC;AAE1E,qBAAa,oBAAqB,SAAQ,KAAK;IAG3C,QAAQ,CAAC,MAAM,EAAE,MAAM;IACvB,QAAQ,CAAC,IAAI,EAAE,OAAO;gBAFtB,OAAO,EAAE,MAAM,EACN,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,OAAO;CAKzB;AAgBD;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE;IAC7C,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACxC,WAAW,CAAC,EAAE,CAAC,UAAU,EAAE;QAAE,WAAW,EAAE,MAAM,CAAA;KAAE,KAAK,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC/E,GAAG,eAAe,CA2ClB"}
|
package/dist/lib/http.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export class IntegrationHttpError extends Error {
|
|
2
|
+
status;
|
|
3
|
+
body;
|
|
4
|
+
constructor(message, status, body) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.status = status;
|
|
7
|
+
this.body = body;
|
|
8
|
+
this.name = 'IntegrationHttpError';
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
function buildUrl(baseUrl, path, query) {
|
|
12
|
+
const url = new URL(path.startsWith('http') ? path : `${baseUrl.replace(/\/$/, '')}${path}`);
|
|
13
|
+
for (const [key, value] of Object.entries(query ?? {})) {
|
|
14
|
+
if (value !== undefined)
|
|
15
|
+
url.searchParams.set(key, String(value));
|
|
16
|
+
}
|
|
17
|
+
return url.toString();
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The HTTP client handed to integrations. Deliberately thin: integrations
|
|
21
|
+
* describe providers, they should not each ship a request layer.
|
|
22
|
+
*/
|
|
23
|
+
export function createIntegrationHttp(options) {
|
|
24
|
+
const request = async (path, init = {}) => {
|
|
25
|
+
const headers = {
|
|
26
|
+
Accept: 'application/json',
|
|
27
|
+
...options.defaultHeaders,
|
|
28
|
+
...(init.accessToken && options.authHeaders
|
|
29
|
+
? options.authHeaders({ accessToken: init.accessToken })
|
|
30
|
+
: {}),
|
|
31
|
+
...init.headers,
|
|
32
|
+
};
|
|
33
|
+
if (init.body !== undefined && !headers['Content-Type']) {
|
|
34
|
+
headers['Content-Type'] = 'application/json';
|
|
35
|
+
}
|
|
36
|
+
const response = await fetch(buildUrl(options.baseUrl, path, init.query), {
|
|
37
|
+
method: init.method ?? (init.body === undefined ? 'GET' : 'POST'),
|
|
38
|
+
headers,
|
|
39
|
+
body: init.body === undefined ? undefined : JSON.stringify(init.body),
|
|
40
|
+
});
|
|
41
|
+
const text = await response.text();
|
|
42
|
+
const parsed = text ? safeJsonParse(text) : undefined;
|
|
43
|
+
if (!response.ok) {
|
|
44
|
+
throw new IntegrationHttpError(`Request to ${path} failed with ${response.status}`, response.status, parsed ?? text);
|
|
45
|
+
}
|
|
46
|
+
return parsed;
|
|
47
|
+
};
|
|
48
|
+
return {
|
|
49
|
+
request,
|
|
50
|
+
get: (path, init) => request(path, { ...init, method: 'GET' }),
|
|
51
|
+
post: (path, init) => request(path, { ...init, method: 'POST' }),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function safeJsonParse(text) {
|
|
55
|
+
try {
|
|
56
|
+
return JSON.parse(text);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return text;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { AuthorizationUrlArgs, ExchangeCodeArgs, IntegrationAuthStrategy, RefreshArgs, TokenSet } from './types.js';
|
|
2
|
+
export interface OAuth2Options {
|
|
3
|
+
authorizationUrl: string;
|
|
4
|
+
tokenUrl: string;
|
|
5
|
+
revokeUrl?: string;
|
|
6
|
+
scopesAvailable: string[];
|
|
7
|
+
/** Separator the provider expects between scopes. GitHub uses a space. */
|
|
8
|
+
scopeSeparator?: string;
|
|
9
|
+
/** Extra static params on the authorize redirect (e.g. `{ prompt: 'consent' }`). */
|
|
10
|
+
authorizationParams?: Record<string, string>;
|
|
11
|
+
/** Providers disagree: GitHub wants JSON, most want form-encoded. */
|
|
12
|
+
tokenRequest?: 'json' | 'form';
|
|
13
|
+
pkce?: boolean;
|
|
14
|
+
/** Per-step overrides for providers that deviate from the spec. */
|
|
15
|
+
buildAuthorizationUrl?(args: AuthorizationUrlArgs): string;
|
|
16
|
+
exchangeCode?(args: ExchangeCodeArgs): Promise<TokenSet>;
|
|
17
|
+
refresh?(args: RefreshArgs): Promise<TokenSet>;
|
|
18
|
+
/** Map a non-standard token response onto {@link TokenSet}. */
|
|
19
|
+
parseTokenResponse?(body: Record<string, unknown>): TokenSet;
|
|
20
|
+
}
|
|
21
|
+
export declare class OAuth2Error extends Error {
|
|
22
|
+
constructor(message: string);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* A complete OAuth2 authorization-code implementation. An integration supplies
|
|
26
|
+
* endpoints and scopes; this handles the redirect, the exchange, and refresh.
|
|
27
|
+
*/
|
|
28
|
+
export declare function oauth2(options: OAuth2Options): IntegrationAuthStrategy;
|
|
29
|
+
/** RFC 7636 S256: base64url(sha256(verifier)). */
|
|
30
|
+
export declare function s256Challenge(verifier: string): string;
|
|
31
|
+
/**
|
|
32
|
+
* A PKCE code verifier: 43-128 unreserved characters. base64url of 32 random
|
|
33
|
+
* bytes lands at 43.
|
|
34
|
+
*/
|
|
35
|
+
export declare function generateCodeVerifier(): string;
|
|
36
|
+
//# sourceMappingURL=oauth2.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oauth2.d.ts","sourceRoot":"","sources":["../../src/lib/oauth2.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,oBAAoB,EACpB,gBAAgB,EAChB,uBAAuB,EACvB,WAAW,EACX,QAAQ,EACT,MAAM,YAAY,CAAC;AAEpB,MAAM,WAAW,aAAa;IAC5B,gBAAgB,EAAE,MAAM,CAAC;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,0EAA0E;IAC1E,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,oFAAoF;IACpF,mBAAmB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7C,qEAAqE;IACrE,YAAY,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC/B,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,mEAAmE;IACnE,qBAAqB,CAAC,CAAC,IAAI,EAAE,oBAAoB,GAAG,MAAM,CAAC;IAC3D,YAAY,CAAC,CAAC,IAAI,EAAE,gBAAgB,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IACzD,OAAO,CAAC,CAAC,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC/C,+DAA+D;IAC/D,kBAAkB,CAAC,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,QAAQ,CAAC;CAC9D;AAED,qBAAa,WAAY,SAAQ,KAAK;gBACxB,OAAO,EAAE,MAAM;CAI5B;AAED;;;GAGG;AACH,wBAAgB,MAAM,CAAC,OAAO,EAAE,aAAa,GAAG,uBAAuB,CAoGtE;AAqBD,kDAAkD;AAClD,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAEtD;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,IAAI,MAAM,CAE7C"}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
2
|
+
export class OAuth2Error extends Error {
|
|
3
|
+
constructor(message) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = 'OAuth2Error';
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* A complete OAuth2 authorization-code implementation. An integration supplies
|
|
10
|
+
* endpoints and scopes; this handles the redirect, the exchange, and refresh.
|
|
11
|
+
*/
|
|
12
|
+
export function oauth2(options) {
|
|
13
|
+
const separator = options.scopeSeparator ?? ' ';
|
|
14
|
+
const tokenRequest = options.tokenRequest ?? 'form';
|
|
15
|
+
const pkce = options.pkce ?? false;
|
|
16
|
+
const parseTokenResponse = options.parseTokenResponse ?? defaultParseTokenResponse;
|
|
17
|
+
const buildAuthorizationUrl = (args) => {
|
|
18
|
+
const url = new URL(options.authorizationUrl);
|
|
19
|
+
url.searchParams.set('response_type', 'code');
|
|
20
|
+
url.searchParams.set('client_id', args.client.clientId);
|
|
21
|
+
url.searchParams.set('redirect_uri', args.client.redirectUri);
|
|
22
|
+
url.searchParams.set('state', args.state);
|
|
23
|
+
if (args.client.scopes.length > 0) {
|
|
24
|
+
url.searchParams.set('scope', args.client.scopes.join(separator));
|
|
25
|
+
}
|
|
26
|
+
for (const [key, value] of Object.entries(options.authorizationParams ?? {})) {
|
|
27
|
+
url.searchParams.set(key, value);
|
|
28
|
+
}
|
|
29
|
+
if (pkce) {
|
|
30
|
+
if (!args.codeVerifier) {
|
|
31
|
+
throw new OAuth2Error('PKCE is enabled for this integration but no code verifier was provided');
|
|
32
|
+
}
|
|
33
|
+
// The challenge is the hash of the verifier, never the verifier itself —
|
|
34
|
+
// sending the verifier here would defeat the point of PKCE.
|
|
35
|
+
url.searchParams.set('code_challenge', s256Challenge(args.codeVerifier));
|
|
36
|
+
url.searchParams.set('code_challenge_method', 'S256');
|
|
37
|
+
}
|
|
38
|
+
return url.toString();
|
|
39
|
+
};
|
|
40
|
+
const postToken = async (params) => {
|
|
41
|
+
const response = await fetch(options.tokenUrl, {
|
|
42
|
+
method: 'POST',
|
|
43
|
+
headers: {
|
|
44
|
+
Accept: 'application/json',
|
|
45
|
+
'Content-Type': tokenRequest === 'json'
|
|
46
|
+
? 'application/json'
|
|
47
|
+
: 'application/x-www-form-urlencoded',
|
|
48
|
+
},
|
|
49
|
+
body: tokenRequest === 'json'
|
|
50
|
+
? JSON.stringify(params)
|
|
51
|
+
: new URLSearchParams(params).toString(),
|
|
52
|
+
});
|
|
53
|
+
const body = (await response.json().catch(() => null));
|
|
54
|
+
if (!response.ok || !body || body['error']) {
|
|
55
|
+
const detail = body?.['error_description'] ??
|
|
56
|
+
body?.['error'] ??
|
|
57
|
+
`HTTP ${response.status}`;
|
|
58
|
+
throw new OAuth2Error(`Token request failed: ${detail}`);
|
|
59
|
+
}
|
|
60
|
+
return parseTokenResponse(body);
|
|
61
|
+
};
|
|
62
|
+
return {
|
|
63
|
+
type: 'OAUTH2',
|
|
64
|
+
authorizationUrl: options.authorizationUrl,
|
|
65
|
+
tokenUrl: options.tokenUrl,
|
|
66
|
+
revokeUrl: options.revokeUrl,
|
|
67
|
+
scopesAvailable: options.scopesAvailable,
|
|
68
|
+
pkce,
|
|
69
|
+
buildAuthorizationUrl: options.buildAuthorizationUrl ?? buildAuthorizationUrl,
|
|
70
|
+
exchangeCode: options.exchangeCode ??
|
|
71
|
+
(({ client, code, codeVerifier }) => postToken({
|
|
72
|
+
grant_type: 'authorization_code',
|
|
73
|
+
client_id: client.clientId,
|
|
74
|
+
client_secret: client.clientSecret,
|
|
75
|
+
redirect_uri: client.redirectUri,
|
|
76
|
+
code,
|
|
77
|
+
...(codeVerifier ? { code_verifier: codeVerifier } : {}),
|
|
78
|
+
})),
|
|
79
|
+
refresh: options.refresh ??
|
|
80
|
+
(({ client, refreshToken }) => postToken({
|
|
81
|
+
grant_type: 'refresh_token',
|
|
82
|
+
client_id: client.clientId,
|
|
83
|
+
client_secret: client.clientSecret,
|
|
84
|
+
refresh_token: refreshToken,
|
|
85
|
+
})),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function defaultParseTokenResponse(body) {
|
|
89
|
+
const accessToken = body['access_token'];
|
|
90
|
+
if (typeof accessToken !== 'string') {
|
|
91
|
+
throw new OAuth2Error('Token response contained no access_token');
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
accessToken,
|
|
95
|
+
refreshToken: typeof body['refresh_token'] === 'string'
|
|
96
|
+
? body['refresh_token']
|
|
97
|
+
: undefined,
|
|
98
|
+
tokenType: typeof body['token_type'] === 'string' ? body['token_type'] : undefined,
|
|
99
|
+
expiresInSeconds: typeof body['expires_in'] === 'number' ? body['expires_in'] : undefined,
|
|
100
|
+
raw: body,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/** RFC 7636 S256: base64url(sha256(verifier)). */
|
|
104
|
+
export function s256Challenge(verifier) {
|
|
105
|
+
return createHash('sha256').update(verifier).digest('base64url');
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* A PKCE code verifier: 43-128 unreserved characters. base64url of 32 random
|
|
109
|
+
* bytes lands at 43.
|
|
110
|
+
*/
|
|
111
|
+
export function generateCodeVerifier() {
|
|
112
|
+
return randomBytes(32).toString('base64url');
|
|
113
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The integration contract. An integration package exports a factory that
|
|
3
|
+
* returns one {@link KoshaIntegration}; the server loads it at boot, syncs it
|
|
4
|
+
* into the provider catalogue, and resolves it per request.
|
|
5
|
+
*/
|
|
6
|
+
export type KoshaAuthType = 'OAUTH2' | 'OAUTH1' | 'API_KEY' | 'BASIC' | 'CUSTOM';
|
|
7
|
+
/**
|
|
8
|
+
* One organization's resolved credentials for a provider — comes from the
|
|
9
|
+
* `configuration` row, never from the integration package itself.
|
|
10
|
+
*/
|
|
11
|
+
export interface OAuthClientContext {
|
|
12
|
+
clientId: string;
|
|
13
|
+
clientSecret: string;
|
|
14
|
+
redirectUri: string;
|
|
15
|
+
scopes: string[];
|
|
16
|
+
}
|
|
17
|
+
export interface TokenSet {
|
|
18
|
+
accessToken: string;
|
|
19
|
+
refreshToken?: string;
|
|
20
|
+
tokenType?: string;
|
|
21
|
+
expiresInSeconds?: number;
|
|
22
|
+
/** Provider's untouched response, for debugging. Never logged. */
|
|
23
|
+
raw?: unknown;
|
|
24
|
+
}
|
|
25
|
+
export interface Credential {
|
|
26
|
+
accessToken: string;
|
|
27
|
+
tokenType?: string | null;
|
|
28
|
+
}
|
|
29
|
+
export interface AccountIdentity {
|
|
30
|
+
externalAccountId: string;
|
|
31
|
+
label?: string;
|
|
32
|
+
}
|
|
33
|
+
/** Minimal HTTP client handed to integrations, pre-bound to `api.baseUrl`. */
|
|
34
|
+
export interface IntegrationHttp {
|
|
35
|
+
get<T = unknown>(path: string, init?: IntegrationRequestInit): Promise<T>;
|
|
36
|
+
post<T = unknown>(path: string, init?: IntegrationRequestInit): Promise<T>;
|
|
37
|
+
request<T = unknown>(path: string, init?: IntegrationRequestInit): Promise<T>;
|
|
38
|
+
}
|
|
39
|
+
export interface IntegrationRequestInit {
|
|
40
|
+
method?: string;
|
|
41
|
+
accessToken?: string;
|
|
42
|
+
headers?: Record<string, string>;
|
|
43
|
+
body?: unknown;
|
|
44
|
+
query?: Record<string, string | number | boolean | undefined>;
|
|
45
|
+
}
|
|
46
|
+
export interface AuthorizationUrlArgs {
|
|
47
|
+
client: OAuthClientContext;
|
|
48
|
+
state: string;
|
|
49
|
+
/** PKCE verifier, generated by the server when `pkce` is enabled. */
|
|
50
|
+
codeVerifier?: string;
|
|
51
|
+
}
|
|
52
|
+
export interface ExchangeCodeArgs {
|
|
53
|
+
client: OAuthClientContext;
|
|
54
|
+
code: string;
|
|
55
|
+
codeVerifier?: string;
|
|
56
|
+
}
|
|
57
|
+
export interface RefreshArgs {
|
|
58
|
+
client: OAuthClientContext;
|
|
59
|
+
refreshToken: string;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* How an integration authenticates. `oauth2()` provides a complete
|
|
63
|
+
* implementation; any step can be replaced for a provider that deviates.
|
|
64
|
+
*/
|
|
65
|
+
export interface IntegrationAuthStrategy {
|
|
66
|
+
type: KoshaAuthType;
|
|
67
|
+
authorizationUrl?: string;
|
|
68
|
+
tokenUrl?: string;
|
|
69
|
+
revokeUrl?: string;
|
|
70
|
+
scopesAvailable: string[];
|
|
71
|
+
pkce: boolean;
|
|
72
|
+
buildAuthorizationUrl(args: AuthorizationUrlArgs): string;
|
|
73
|
+
exchangeCode(args: ExchangeCodeArgs): Promise<TokenSet>;
|
|
74
|
+
refresh?(args: RefreshArgs): Promise<TokenSet>;
|
|
75
|
+
}
|
|
76
|
+
export interface IntegrationApi {
|
|
77
|
+
/** Base URL the proxy forwards requests to. */
|
|
78
|
+
baseUrl: string;
|
|
79
|
+
/** Headers that authenticate a proxied request. */
|
|
80
|
+
authHeaders(credential: Credential): Record<string, string>;
|
|
81
|
+
}
|
|
82
|
+
export interface IntegrationIdentity {
|
|
83
|
+
fetchAccount(args: {
|
|
84
|
+
accessToken: string;
|
|
85
|
+
http: IntegrationHttp;
|
|
86
|
+
}): Promise<AccountIdentity>;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Per-organization settings, described well enough for the admin to render a
|
|
90
|
+
* form without shipping any integration-specific UI code.
|
|
91
|
+
*/
|
|
92
|
+
export interface IntegrationConfigField {
|
|
93
|
+
key: string;
|
|
94
|
+
label: string;
|
|
95
|
+
type: 'string' | 'number' | 'boolean' | 'select';
|
|
96
|
+
required?: boolean;
|
|
97
|
+
secret?: boolean;
|
|
98
|
+
options?: {
|
|
99
|
+
label: string;
|
|
100
|
+
value: string;
|
|
101
|
+
}[];
|
|
102
|
+
help?: string;
|
|
103
|
+
}
|
|
104
|
+
export interface IntegrationWebhooks {
|
|
105
|
+
/** Verify the provider's signature. Returning false rejects the delivery. */
|
|
106
|
+
verify(args: {
|
|
107
|
+
body: string;
|
|
108
|
+
headers: Record<string, string | string[] | undefined>;
|
|
109
|
+
secret?: string;
|
|
110
|
+
}): boolean | Promise<boolean>;
|
|
111
|
+
handle(args: {
|
|
112
|
+
payload: unknown;
|
|
113
|
+
organizationId: string;
|
|
114
|
+
}): Promise<void>;
|
|
115
|
+
}
|
|
116
|
+
export interface IntegrationHooks {
|
|
117
|
+
onConnectionCreated?(args: {
|
|
118
|
+
connectionId: string;
|
|
119
|
+
organizationId: string;
|
|
120
|
+
}): Promise<void> | void;
|
|
121
|
+
onTokenRefreshed?(args: {
|
|
122
|
+
connectionId: string;
|
|
123
|
+
tokens: TokenSet;
|
|
124
|
+
}): Promise<void> | void;
|
|
125
|
+
}
|
|
126
|
+
export interface KoshaIntegration {
|
|
127
|
+
/** Stable identifier; the primary key in the provider catalogue. */
|
|
128
|
+
key: string;
|
|
129
|
+
name: string;
|
|
130
|
+
logo?: string;
|
|
131
|
+
docsUrl?: string;
|
|
132
|
+
auth: IntegrationAuthStrategy;
|
|
133
|
+
api: IntegrationApi;
|
|
134
|
+
identity: IntegrationIdentity;
|
|
135
|
+
configFields?: IntegrationConfigField[];
|
|
136
|
+
webhooks?: IntegrationWebhooks;
|
|
137
|
+
hooks?: IntegrationHooks;
|
|
138
|
+
}
|
|
139
|
+
/** What an integration package's default export is. */
|
|
140
|
+
export type IntegrationFactory<TOptions = void> = TOptions extends void ? () => KoshaIntegration : (options?: TOptions) => KoshaIntegration;
|
|
141
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/lib/types.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kosha-ai/integration-sdk",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "SDK for authoring Kosha integrations",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"kosha",
|
|
7
|
+
"sdk",
|
|
8
|
+
"oauth",
|
|
9
|
+
"integrations"
|
|
10
|
+
],
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"homepage": "https://github.com/codemancers/kosha#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/codemancers/kosha/issues"
|
|
15
|
+
},
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/codemancers/kosha.git",
|
|
19
|
+
"directory": "packages/integration-sdk"
|
|
20
|
+
},
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public"
|
|
23
|
+
},
|
|
24
|
+
"type": "module",
|
|
25
|
+
"main": "./dist/index.js",
|
|
26
|
+
"module": "./dist/index.js",
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
|
+
"exports": {
|
|
29
|
+
"./package.json": "./package.json",
|
|
30
|
+
".": {
|
|
31
|
+
"@org/source": "./src/index.ts",
|
|
32
|
+
"types": "./dist/index.d.ts",
|
|
33
|
+
"import": "./dist/index.js",
|
|
34
|
+
"default": "./dist/index.js"
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"files": [
|
|
38
|
+
"dist",
|
|
39
|
+
"!**/*.tsbuildinfo"
|
|
40
|
+
],
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"tslib": "^2.3.0"
|
|
43
|
+
}
|
|
44
|
+
}
|