corsair 0.1.23 → 0.1.25
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/dist/core.js +1807 -17
- package/dist/db/index.d.ts +6 -6
- package/dist/db/kysely/database.d.ts.map +1 -1
- package/dist/db/kysely/sqlite-date-plugin.d.ts +6 -0
- package/dist/db/kysely/sqlite-date-plugin.d.ts.map +1 -0
- package/dist/db.d.ts +1 -0
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +157 -16
- package/dist/index.js +24995 -17
- package/dist/orm.js +845 -28
- package/dist/plugins/github/index.js +2747 -291
- package/dist/plugins/index.js +21282 -21
- package/dist/plugins/linear/index.js +3532 -269
- package/dist/plugins/posthog/index.js +1072 -116
- package/dist/plugins/resend/index.js +1812 -197
- package/dist/plugins/slack/index.js +4251 -504
- package/dist/plugins/spotify/index.js +2550 -382
- package/dist/tsup.config.d.ts.map +1 -1
- package/package.json +1 -1
- package/dist/async-core/ApiError.js +0 -27
- package/dist/async-core/ApiRequestOptions.js +0 -1
- package/dist/async-core/ApiResult.js +0 -1
- package/dist/async-core/CancelablePromise.js +0 -95
- package/dist/async-core/OpenAPI.js +0 -1
- package/dist/async-core/rate-limit.js +0 -79
- package/dist/async-core/request.js +0 -313
- package/dist/async-core/webhook-handler.js +0 -40
- package/dist/async-core/webhook-utils.js +0 -57
- package/dist/core/auth/encryption.js +0 -177
- package/dist/core/auth/errors/index.js +0 -1
- package/dist/core/auth/errors/missing-config.js +0 -25
- package/dist/core/auth/index.js +0 -7
- package/dist/core/auth/key-manager.js +0 -386
- package/dist/core/auth/types.js +0 -28
- package/dist/core/client/index.js +0 -227
- package/dist/core/constants.js +0 -15
- package/dist/core/endpoints/bind.js +0 -154
- package/dist/core/endpoints/index.js +0 -1
- package/dist/core/errors/handler.js +0 -30
- package/dist/core/errors/index.js +0 -1
- package/dist/core/index.js +0 -64
- package/dist/core/inspect/index.js +0 -471
- package/dist/core/permissions/index.js +0 -178
- package/dist/core/plugins/index.js +0 -1
- package/dist/core/utils.js +0 -30
- package/dist/core/webhooks/bind.js +0 -77
- package/dist/core/webhooks/index.js +0 -1
- package/dist/db/index.js +0 -106
- package/dist/db/kysely/database.js +0 -33
- package/dist/db/kysely/orm.js +0 -334
- package/dist/db/kysely/postgres.js +0 -20
- package/dist/db/kysely/sqlite.js +0 -38
- package/dist/db/orm.js +0 -579
- package/dist/permissions/index.js +0 -146
- package/dist/plugins/discord/client.js +0 -42
- package/dist/plugins/discord/endpoints/channels.js +0 -19
- package/dist/plugins/discord/endpoints/guilds.js +0 -25
- package/dist/plugins/discord/endpoints/index.js +0 -8
- package/dist/plugins/discord/endpoints/members.js +0 -55
- package/dist/plugins/discord/endpoints/messages.js +0 -97
- package/dist/plugins/discord/endpoints/reactions.js +0 -23
- package/dist/plugins/discord/endpoints/threads.js +0 -37
- package/dist/plugins/discord/endpoints/types.js +0 -302
- package/dist/plugins/discord/error-handlers.js +0 -134
- package/dist/plugins/discord/index.js +0 -272
- package/dist/plugins/discord/schema/database.js +0 -51
- package/dist/plugins/discord/schema/index.js +0 -10
- package/dist/plugins/discord/webhooks/index.js +0 -8
- package/dist/plugins/discord/webhooks/interactions.js +0 -108
- package/dist/plugins/discord/webhooks/types.js +0 -205
- package/dist/plugins/github/api.test.js +0 -182
- package/dist/plugins/github/client.js +0 -46
- package/dist/plugins/github/endpoints/index.js +0 -37
- package/dist/plugins/github/endpoints/issues.js +0 -94
- package/dist/plugins/github/endpoints/pull-requests.js +0 -60
- package/dist/plugins/github/endpoints/releases.js +0 -66
- package/dist/plugins/github/endpoints/repositories.js +0 -73
- package/dist/plugins/github/endpoints/types.js +0 -651
- package/dist/plugins/github/endpoints/workflows.js +0 -44
- package/dist/plugins/github/integration.test.js +0 -520
- package/dist/plugins/github/schema/database.js +0 -134
- package/dist/plugins/github/schema/index.js +0 -16
- package/dist/plugins/github/types.js +0 -1
- package/dist/plugins/github/webhooks/index.js +0 -16
- package/dist/plugins/github/webhooks/pull-requests.js +0 -200
- package/dist/plugins/github/webhooks/push.js +0 -27
- package/dist/plugins/github/webhooks/stars.js +0 -59
- package/dist/plugins/github/webhooks/types.js +0 -247
- package/dist/plugins/gmail/api.test.js +0 -383
- package/dist/plugins/gmail/client.js +0 -66
- package/dist/plugins/gmail/endpoints/drafts.js +0 -103
- package/dist/plugins/gmail/endpoints/index.js +0 -38
- package/dist/plugins/gmail/endpoints/labels.js +0 -89
- package/dist/plugins/gmail/endpoints/messages.js +0 -213
- package/dist/plugins/gmail/endpoints/threads.js +0 -110
- package/dist/plugins/gmail/endpoints/types.js +0 -330
- package/dist/plugins/gmail/index.js +0 -326
- package/dist/plugins/gmail/integration.test.js +0 -110
- package/dist/plugins/gmail/schema/database.js +0 -59
- package/dist/plugins/gmail/schema/index.js +0 -17
- package/dist/plugins/gmail/types.js +0 -1
- package/dist/plugins/gmail/webhooks/index.js +0 -5
- package/dist/plugins/gmail/webhooks/messages.js +0 -441
- package/dist/plugins/gmail/webhooks/types.js +0 -68
- package/dist/plugins/googlecalendar/api.test.js +0 -134
- package/dist/plugins/googlecalendar/client.js +0 -66
- package/dist/plugins/googlecalendar/endpoints/calendar.js +0 -17
- package/dist/plugins/googlecalendar/endpoints/events.js +0 -123
- package/dist/plugins/googlecalendar/endpoints/index.js +0 -13
- package/dist/plugins/googlecalendar/endpoints/types.js +0 -268
- package/dist/plugins/googlecalendar/index.js +0 -153
- package/dist/plugins/googlecalendar/integration.test.js +0 -149
- package/dist/plugins/googlecalendar/schema/database.js +0 -82
- package/dist/plugins/googlecalendar/schema/index.js +0 -8
- package/dist/plugins/googlecalendar/setup.js +0 -280
- package/dist/plugins/googlecalendar/types.js +0 -1
- package/dist/plugins/googlecalendar/webhooks/events.js +0 -136
- package/dist/plugins/googlecalendar/webhooks/index.js +0 -5
- package/dist/plugins/googlecalendar/webhooks/types.js +0 -75
- package/dist/plugins/googledrive/api.test.js +0 -473
- package/dist/plugins/googledrive/client.js +0 -66
- package/dist/plugins/googledrive/endpoints/files.js +0 -210
- package/dist/plugins/googledrive/endpoints/folders.js +0 -130
- package/dist/plugins/googledrive/endpoints/index.js +0 -34
- package/dist/plugins/googledrive/endpoints/search.js +0 -49
- package/dist/plugins/googledrive/endpoints/sharedDrives.js +0 -124
- package/dist/plugins/googledrive/endpoints/types.js +0 -337
- package/dist/plugins/googledrive/index.js +0 -292
- package/dist/plugins/googledrive/integration.test.js +0 -139
- package/dist/plugins/googledrive/schema/database.js +0 -123
- package/dist/plugins/googledrive/schema/index.js +0 -9
- package/dist/plugins/googledrive/types.js +0 -1
- package/dist/plugins/googledrive/webhooks/changes.js +0 -272
- package/dist/plugins/googledrive/webhooks/index.js +0 -5
- package/dist/plugins/googledrive/webhooks/types.js +0 -82
- package/dist/plugins/googlesheets/api.test.js +0 -344
- package/dist/plugins/googlesheets/client.js +0 -66
- package/dist/plugins/googlesheets/endpoints/index.js +0 -17
- package/dist/plugins/googlesheets/endpoints/sheets.js +0 -325
- package/dist/plugins/googlesheets/endpoints/spreadsheets.js +0 -46
- package/dist/plugins/googlesheets/endpoints/types.js +0 -146
- package/dist/plugins/googlesheets/index.js +0 -184
- package/dist/plugins/googlesheets/integration.test.js +0 -106
- package/dist/plugins/googlesheets/schema/database.js +0 -24
- package/dist/plugins/googlesheets/schema/index.js +0 -9
- package/dist/plugins/googlesheets/types.js +0 -1
- package/dist/plugins/googlesheets/webhooks/index.js +0 -5
- package/dist/plugins/googlesheets/webhooks/rows.js +0 -43
- package/dist/plugins/googlesheets/webhooks/types.js +0 -35
- package/dist/plugins/hubspot/api.test.js +0 -305
- package/dist/plugins/hubspot/client.js +0 -45
- package/dist/plugins/hubspot/endpoints/companies.js +0 -141
- package/dist/plugins/hubspot/endpoints/contact-lists.js +0 -16
- package/dist/plugins/hubspot/endpoints/contacts.js +0 -100
- package/dist/plugins/hubspot/endpoints/deals.js +0 -115
- package/dist/plugins/hubspot/endpoints/engagements.js +0 -71
- package/dist/plugins/hubspot/endpoints/index.js +0 -53
- package/dist/plugins/hubspot/endpoints/tickets.js +0 -94
- package/dist/plugins/hubspot/endpoints/types.js +0 -444
- package/dist/plugins/hubspot/error-handlers.js +0 -55
- package/dist/plugins/hubspot/index.js +0 -448
- package/dist/plugins/hubspot/integration.test.js +0 -590
- package/dist/plugins/hubspot/schema/database.js +0 -48
- package/dist/plugins/hubspot/schema/index.js +0 -15
- package/dist/plugins/hubspot/types.js +0 -1
- package/dist/plugins/hubspot/webhooks/companies.js +0 -127
- package/dist/plugins/hubspot/webhooks/contacts.js +0 -127
- package/dist/plugins/hubspot/webhooks/deals.js +0 -127
- package/dist/plugins/hubspot/webhooks/index.js +0 -25
- package/dist/plugins/hubspot/webhooks/tickets.js +0 -127
- package/dist/plugins/hubspot/webhooks/types.js +0 -148
- package/dist/plugins/linear/api.test.js +0 -705
- package/dist/plugins/linear/client.js +0 -124
- package/dist/plugins/linear/endpoints/comments.js +0 -187
- package/dist/plugins/linear/endpoints/index.js +0 -34
- package/dist/plugins/linear/endpoints/issues.js +0 -362
- package/dist/plugins/linear/endpoints/projects.js +0 -210
- package/dist/plugins/linear/endpoints/teams.js +0 -81
- package/dist/plugins/linear/endpoints/types.js +0 -644
- package/dist/plugins/linear/endpoints/users.js +0 -81
- package/dist/plugins/linear/error-handlers.js +0 -140
- package/dist/plugins/linear/integration.test.js +0 -427
- package/dist/plugins/linear/schema/database.js +0 -93
- package/dist/plugins/linear/schema/index.js +0 -11
- package/dist/plugins/linear/webhooks/comments.js +0 -139
- package/dist/plugins/linear/webhooks/index.js +0 -19
- package/dist/plugins/linear/webhooks/issues.js +0 -167
- package/dist/plugins/linear/webhooks/projects.js +0 -148
- package/dist/plugins/linear/webhooks/types.js +0 -237
- package/dist/plugins/posthog/api.test.js +0 -91
- package/dist/plugins/posthog/client.js +0 -122
- package/dist/plugins/posthog/endpoints/events.js +0 -120
- package/dist/plugins/posthog/endpoints/index.js +0 -9
- package/dist/plugins/posthog/endpoints/types.js +0 -55
- package/dist/plugins/posthog/integration.test.js +0 -132
- package/dist/plugins/posthog/schema/database.js +0 -10
- package/dist/plugins/posthog/schema/index.js +0 -7
- package/dist/plugins/posthog/webhooks/events.js +0 -51
- package/dist/plugins/posthog/webhooks/index.js +0 -5
- package/dist/plugins/posthog/webhooks/types.js +0 -72
- package/dist/plugins/resend/api.test.js +0 -98
- package/dist/plugins/resend/client.js +0 -101
- package/dist/plugins/resend/endpoints/domains.js +0 -87
- package/dist/plugins/resend/endpoints/emails.js +0 -78
- package/dist/plugins/resend/endpoints/index.js +0 -15
- package/dist/plugins/resend/endpoints/types.js +0 -132
- package/dist/plugins/resend/error-handlers.js +0 -39
- package/dist/plugins/resend/integration.test.js +0 -241
- package/dist/plugins/resend/schema/database.js +0 -24
- package/dist/plugins/resend/schema/index.js +0 -8
- package/dist/plugins/resend/webhooks/domains.js +0 -94
- package/dist/plugins/resend/webhooks/emails.js +0 -269
- package/dist/plugins/resend/webhooks/index.js +0 -17
- package/dist/plugins/resend/webhooks/types.js +0 -226
- package/dist/plugins/slack/api.test.js +0 -501
- package/dist/plugins/slack/client.js +0 -125
- package/dist/plugins/slack/endpoints/channels.js +0 -326
- package/dist/plugins/slack/endpoints/files.js +0 -54
- package/dist/plugins/slack/endpoints/index.js +0 -64
- package/dist/plugins/slack/endpoints/messages.js +0 -136
- package/dist/plugins/slack/endpoints/reactions.js +0 -53
- package/dist/plugins/slack/endpoints/stars.js +0 -26
- package/dist/plugins/slack/endpoints/types.js +0 -785
- package/dist/plugins/slack/endpoints/user-groups.js +0 -96
- package/dist/plugins/slack/endpoints/users.js +0 -82
- package/dist/plugins/slack/error-handlers.js +0 -103
- package/dist/plugins/slack/integration.test.js +0 -180
- package/dist/plugins/slack/schema/database.js +0 -108
- package/dist/plugins/slack/schema/index.js +0 -11
- package/dist/plugins/slack/webhooks/challenge.js +0 -31
- package/dist/plugins/slack/webhooks/channels.js +0 -49
- package/dist/plugins/slack/webhooks/files.js +0 -139
- package/dist/plugins/slack/webhooks/index.js +0 -28
- package/dist/plugins/slack/webhooks/messages.js +0 -82
- package/dist/plugins/slack/webhooks/reactions.js +0 -28
- package/dist/plugins/slack/webhooks/types.js +0 -694
- package/dist/plugins/slack/webhooks/users.js +0 -94
- package/dist/plugins/spotify/api.test.js +0 -212
- package/dist/plugins/spotify/client.js +0 -112
- package/dist/plugins/spotify/endpoints/albums.js +0 -63
- package/dist/plugins/spotify/endpoints/artists.js +0 -53
- package/dist/plugins/spotify/endpoints/index.js +0 -52
- package/dist/plugins/spotify/endpoints/library.js +0 -11
- package/dist/plugins/spotify/endpoints/my-data.js +0 -19
- package/dist/plugins/spotify/endpoints/player.js +0 -124
- package/dist/plugins/spotify/endpoints/playlists.js +0 -90
- package/dist/plugins/spotify/endpoints/tracks.js +0 -37
- package/dist/plugins/spotify/endpoints/types.js +0 -572
- package/dist/plugins/spotify/error-handlers.js +0 -124
- package/dist/plugins/spotify/integration.test.js +0 -448
- package/dist/plugins/spotify/schema/database.js +0 -201
- package/dist/plugins/spotify/schema/index.js +0 -12
- package/dist/plugins/spotify/webhooks/example.js +0 -31
- package/dist/plugins/spotify/webhooks/index.js +0 -5
- package/dist/plugins/spotify/webhooks/types.js +0 -99
- package/dist/plugins/tavily/client.js +0 -47
- package/dist/plugins/tavily/endpoints/index.js +0 -5
- package/dist/plugins/tavily/endpoints/search.js +0 -11
- package/dist/plugins/tavily/endpoints/types.js +0 -98
- package/dist/plugins/tavily/error-handlers.js +0 -134
- package/dist/plugins/tavily/index.js +0 -166
- package/dist/plugins/tavily/schema/database.js +0 -10
- package/dist/plugins/tavily/schema/index.js +0 -14
- package/dist/plugins/utils/events.js +0 -57
- package/dist/templates/plugin/generate.js +0 -1023
- package/dist/tests/error-handlers.test.js +0 -454
- package/dist/tests/hooks.test.js +0 -357
- package/dist/tests/plugins-test-utils.js +0 -28
- package/dist/tests/setup-db.js +0 -59
- package/dist/tests/slack-rate-limit-integration.test.js +0 -526
- package/dist/tsup.config.js +0 -31
- package/dist/webhooks/index.js +0 -174
package/dist/core.js
CHANGED
|
@@ -1,17 +1,1807 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
1
|
+
// db/kysely/database.ts
|
|
2
|
+
import { Kysely, PostgresDialect, SqliteDialect } from "kysely";
|
|
3
|
+
|
|
4
|
+
// db/kysely/sqlite-date-plugin.ts
|
|
5
|
+
import {
|
|
6
|
+
OperationNodeTransformer
|
|
7
|
+
} from "kysely";
|
|
8
|
+
function serializeValue(v) {
|
|
9
|
+
if (v instanceof Date) return v.toISOString();
|
|
10
|
+
if (v !== null && typeof v === "object" && !Buffer.isBuffer(v))
|
|
11
|
+
return JSON.stringify(v);
|
|
12
|
+
return v;
|
|
13
|
+
}
|
|
14
|
+
var SqliteSerializingTransformer = class extends OperationNodeTransformer {
|
|
15
|
+
transformValue(node) {
|
|
16
|
+
const serialized = serializeValue(node.value);
|
|
17
|
+
return serialized === node.value ? node : { ...node, value: serialized };
|
|
18
|
+
}
|
|
19
|
+
transformPrimitiveValueList(node) {
|
|
20
|
+
const serialized = node.values.map(serializeValue);
|
|
21
|
+
const changed = serialized.some((v, i) => v !== node.values[i]);
|
|
22
|
+
return changed ? { ...node, values: serialized } : node;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
var transformer = new SqliteSerializingTransformer();
|
|
26
|
+
var SqliteDatePlugin = class {
|
|
27
|
+
transformQuery(args) {
|
|
28
|
+
return transformer.transformNode(args.node);
|
|
29
|
+
}
|
|
30
|
+
async transformResult(args) {
|
|
31
|
+
return args.result;
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// db/kysely/database.ts
|
|
36
|
+
function isPgPool(input) {
|
|
37
|
+
return typeof input.query === "function" && typeof input.connect === "function";
|
|
38
|
+
}
|
|
39
|
+
function isBetterSqlite3(input) {
|
|
40
|
+
const db = input;
|
|
41
|
+
return typeof db.prepare === "function" && typeof db.exec === "function" && typeof db.close === "function" && !("query" in input);
|
|
42
|
+
}
|
|
43
|
+
function isKysely(input) {
|
|
44
|
+
return typeof input.selectFrom === "function";
|
|
45
|
+
}
|
|
46
|
+
function createCorsairDatabase(input) {
|
|
47
|
+
if (isKysely(input)) {
|
|
48
|
+
return { db: input };
|
|
49
|
+
}
|
|
50
|
+
if (isBetterSqlite3(input)) {
|
|
51
|
+
const db = new Kysely({
|
|
52
|
+
dialect: new SqliteDialect({ database: input }),
|
|
53
|
+
plugins: [new SqliteDatePlugin()]
|
|
54
|
+
});
|
|
55
|
+
return { db };
|
|
56
|
+
}
|
|
57
|
+
if (isPgPool(input)) {
|
|
58
|
+
const db = new Kysely({
|
|
59
|
+
dialect: new PostgresDialect({ pool: input })
|
|
60
|
+
});
|
|
61
|
+
return { db };
|
|
62
|
+
}
|
|
63
|
+
throw new Error(
|
|
64
|
+
"Unsupported database input. Expected a pg Pool, better-sqlite3 Database, or a Kysely instance."
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// core/auth/encryption.ts
|
|
69
|
+
import { createCipheriv, createDecipheriv, randomBytes, scrypt } from "crypto";
|
|
70
|
+
import { promisify } from "util";
|
|
71
|
+
var scryptAsync = promisify(scrypt);
|
|
72
|
+
var ALGORITHM = "aes-256-gcm";
|
|
73
|
+
var IV_LENGTH = 12;
|
|
74
|
+
var AUTH_TAG_LENGTH = 16;
|
|
75
|
+
var SALT_LENGTH = 16;
|
|
76
|
+
var KEY_LENGTH = 32;
|
|
77
|
+
function generateDEK() {
|
|
78
|
+
return randomBytes(KEY_LENGTH).toString("base64");
|
|
79
|
+
}
|
|
80
|
+
async function encryptDEK(dek, kek) {
|
|
81
|
+
const salt = randomBytes(SALT_LENGTH);
|
|
82
|
+
const derivedKey = await scryptAsync(kek, salt, KEY_LENGTH);
|
|
83
|
+
const iv = randomBytes(IV_LENGTH);
|
|
84
|
+
const cipher = createCipheriv(ALGORITHM, derivedKey, iv, {
|
|
85
|
+
authTagLength: AUTH_TAG_LENGTH
|
|
86
|
+
});
|
|
87
|
+
const encrypted = Buffer.concat([cipher.update(dek, "utf8"), cipher.final()]);
|
|
88
|
+
const authTag = cipher.getAuthTag();
|
|
89
|
+
return [
|
|
90
|
+
salt.toString("base64"),
|
|
91
|
+
iv.toString("base64"),
|
|
92
|
+
authTag.toString("base64"),
|
|
93
|
+
encrypted.toString("base64")
|
|
94
|
+
].join(":");
|
|
95
|
+
}
|
|
96
|
+
async function decryptDEK(encryptedDek, kek) {
|
|
97
|
+
const [saltB64, ivB64, authTagB64, encryptedB64] = encryptedDek.split(":");
|
|
98
|
+
if (!saltB64 || !ivB64 || !authTagB64 || !encryptedB64) {
|
|
99
|
+
throw new Error("Invalid encrypted DEK format");
|
|
100
|
+
}
|
|
101
|
+
const salt = Buffer.from(saltB64, "base64");
|
|
102
|
+
const iv = Buffer.from(ivB64, "base64");
|
|
103
|
+
const authTag = Buffer.from(authTagB64, "base64");
|
|
104
|
+
const encrypted = Buffer.from(encryptedB64, "base64");
|
|
105
|
+
const derivedKey = await scryptAsync(kek, salt, KEY_LENGTH);
|
|
106
|
+
const decipher = createDecipheriv(ALGORITHM, derivedKey, iv, {
|
|
107
|
+
authTagLength: AUTH_TAG_LENGTH
|
|
108
|
+
});
|
|
109
|
+
decipher.setAuthTag(authTag);
|
|
110
|
+
const decrypted = Buffer.concat([
|
|
111
|
+
decipher.update(encrypted),
|
|
112
|
+
decipher.final()
|
|
113
|
+
]);
|
|
114
|
+
return decrypted.toString("utf8");
|
|
115
|
+
}
|
|
116
|
+
function encryptWithDEK(data, dek) {
|
|
117
|
+
const key = Buffer.from(dek, "base64");
|
|
118
|
+
const iv = randomBytes(IV_LENGTH);
|
|
119
|
+
const cipher = createCipheriv(ALGORITHM, key, iv, {
|
|
120
|
+
authTagLength: AUTH_TAG_LENGTH
|
|
121
|
+
});
|
|
122
|
+
const encrypted = Buffer.concat([
|
|
123
|
+
cipher.update(data, "utf8"),
|
|
124
|
+
cipher.final()
|
|
125
|
+
]);
|
|
126
|
+
const authTag = cipher.getAuthTag();
|
|
127
|
+
return [
|
|
128
|
+
iv.toString("base64"),
|
|
129
|
+
authTag.toString("base64"),
|
|
130
|
+
encrypted.toString("base64")
|
|
131
|
+
].join(":");
|
|
132
|
+
}
|
|
133
|
+
function decryptWithDEK(encryptedData, dek) {
|
|
134
|
+
const [ivB64, authTagB64, encryptedB64] = encryptedData.split(":");
|
|
135
|
+
if (!ivB64 || !authTagB64 || !encryptedB64) {
|
|
136
|
+
throw new Error("Invalid encrypted data format");
|
|
137
|
+
}
|
|
138
|
+
const key = Buffer.from(dek, "base64");
|
|
139
|
+
const iv = Buffer.from(ivB64, "base64");
|
|
140
|
+
const authTag = Buffer.from(authTagB64, "base64");
|
|
141
|
+
const encrypted = Buffer.from(encryptedB64, "base64");
|
|
142
|
+
const decipher = createDecipheriv(ALGORITHM, key, iv, {
|
|
143
|
+
authTagLength: AUTH_TAG_LENGTH
|
|
144
|
+
});
|
|
145
|
+
decipher.setAuthTag(authTag);
|
|
146
|
+
const decrypted = Buffer.concat([
|
|
147
|
+
decipher.update(encrypted),
|
|
148
|
+
decipher.final()
|
|
149
|
+
]);
|
|
150
|
+
return decrypted.toString("utf8");
|
|
151
|
+
}
|
|
152
|
+
function encryptConfig(config, dek) {
|
|
153
|
+
const encrypted = {};
|
|
154
|
+
for (const [key, value] of Object.entries(config)) {
|
|
155
|
+
encrypted[key] = encryptWithDEK(value, dek);
|
|
156
|
+
}
|
|
157
|
+
return encrypted;
|
|
158
|
+
}
|
|
159
|
+
function decryptConfig(encryptedConfig, dek) {
|
|
160
|
+
const decrypted = {};
|
|
161
|
+
for (const [key, value] of Object.entries(encryptedConfig)) {
|
|
162
|
+
decrypted[key] = decryptWithDEK(value, dek);
|
|
163
|
+
}
|
|
164
|
+
return decrypted;
|
|
165
|
+
}
|
|
166
|
+
function reEncryptConfig(encryptedConfig, oldDek, newDek) {
|
|
167
|
+
const decrypted = decryptConfig(encryptedConfig, oldDek);
|
|
168
|
+
return encryptConfig(decrypted, newDek);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// core/auth/errors/missing-config.ts
|
|
172
|
+
function createMissingConfigProxy(hasDatabase, hasKek) {
|
|
173
|
+
const missingConfig = [];
|
|
174
|
+
if (!hasDatabase) missingConfig.push("database");
|
|
175
|
+
if (!hasKek) missingConfig.push("kek");
|
|
176
|
+
const proxyTarget = {};
|
|
177
|
+
return new Proxy(proxyTarget, {
|
|
178
|
+
get(_target, prop) {
|
|
179
|
+
const isPlural = missingConfig.length > 1;
|
|
180
|
+
throw new Error(
|
|
181
|
+
`corsair.keys.${String(prop)}: Cannot access keys because ${missingConfig.join(" and ")} ${isPlural ? "are" : "is"} not configured. Provide both 'database' and 'kek' in createCorsair() to enable key management.
|
|
182
|
+
|
|
183
|
+
To generate a KEK, run: openssl rand -base64 ${KEY_LENGTH}`
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// db/kysely/orm.ts
|
|
190
|
+
import { z } from "zod";
|
|
191
|
+
|
|
192
|
+
// core/utils.ts
|
|
193
|
+
import { v7 } from "uuid";
|
|
194
|
+
function generateUUID() {
|
|
195
|
+
return v7();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// db/kysely/postgres.ts
|
|
199
|
+
import { sql } from "kysely";
|
|
200
|
+
function escapeJsonPath(path) {
|
|
201
|
+
return path.replace(/'/g, "''");
|
|
202
|
+
}
|
|
203
|
+
function jsonbTextField(key) {
|
|
204
|
+
const escapedPath = escapeJsonPath(key);
|
|
205
|
+
return sql`data->>'${sql.raw(escapedPath)}'`;
|
|
206
|
+
}
|
|
207
|
+
function jsonbNumberField(key) {
|
|
208
|
+
const escapedPath = escapeJsonPath(key);
|
|
209
|
+
return sql`(data->>'${sql.raw(escapedPath)}')::numeric`;
|
|
210
|
+
}
|
|
211
|
+
function jsonbBooleanField(key) {
|
|
212
|
+
const escapedPath = escapeJsonPath(key);
|
|
213
|
+
return sql`(data->>'${sql.raw(escapedPath)}')::boolean`;
|
|
214
|
+
}
|
|
215
|
+
function jsonbTimestampField(key) {
|
|
216
|
+
const escapedPath = escapeJsonPath(key);
|
|
217
|
+
return sql`(data->>'${sql.raw(escapedPath)}')::timestamptz`;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// db/kysely/orm.ts
|
|
221
|
+
function parseJsonLike(value) {
|
|
222
|
+
if (typeof value === "string") {
|
|
223
|
+
try {
|
|
224
|
+
return JSON.parse(value);
|
|
225
|
+
} catch {
|
|
226
|
+
return value;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return value;
|
|
230
|
+
}
|
|
231
|
+
function unwrapSchema(schema) {
|
|
232
|
+
let current = schema;
|
|
233
|
+
while (current) {
|
|
234
|
+
if (current instanceof z.ZodOptional || current instanceof z.ZodNullable) {
|
|
235
|
+
current = current._def.innerType;
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
if (current instanceof z.ZodDefault) {
|
|
239
|
+
current = current._def.innerType;
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
if (current instanceof z.ZodEffects) {
|
|
243
|
+
current = current._def.schema;
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
return current;
|
|
249
|
+
}
|
|
250
|
+
function getFieldType(schema) {
|
|
251
|
+
const unwrapped = unwrapSchema(schema);
|
|
252
|
+
if (unwrapped instanceof z.ZodString) return "string";
|
|
253
|
+
if (unwrapped instanceof z.ZodNumber) return "number";
|
|
254
|
+
if (unwrapped instanceof z.ZodBoolean) return "boolean";
|
|
255
|
+
if (unwrapped instanceof z.ZodDate) return "date";
|
|
256
|
+
return void 0;
|
|
257
|
+
}
|
|
258
|
+
function getDataFieldTypes(schema) {
|
|
259
|
+
const unwrapped = unwrapSchema(schema);
|
|
260
|
+
if (!(unwrapped instanceof z.ZodObject)) return {};
|
|
261
|
+
const shape = unwrapped.shape;
|
|
262
|
+
const fieldTypes = {};
|
|
263
|
+
for (const [key, fieldSchema] of Object.entries(shape)) {
|
|
264
|
+
const fieldType = getFieldType(fieldSchema);
|
|
265
|
+
if (fieldType) fieldTypes[key] = fieldType;
|
|
266
|
+
}
|
|
267
|
+
return fieldTypes;
|
|
268
|
+
}
|
|
269
|
+
function applyStringFilter(q, expr, filterValue) {
|
|
270
|
+
if (typeof filterValue === "string") {
|
|
271
|
+
return q.where(expr, "=", filterValue);
|
|
272
|
+
}
|
|
273
|
+
if (typeof filterValue === "object" && filterValue !== null && !Array.isArray(filterValue)) {
|
|
274
|
+
const obj = filterValue;
|
|
275
|
+
if ("equals" in obj && typeof obj.equals === "string") {
|
|
276
|
+
q = q.where(expr, "=", obj.equals);
|
|
277
|
+
}
|
|
278
|
+
if ("contains" in obj && typeof obj.contains === "string") {
|
|
279
|
+
q = q.where(expr, "like", `%${obj.contains}%`);
|
|
280
|
+
}
|
|
281
|
+
if ("startsWith" in obj && typeof obj.startsWith === "string") {
|
|
282
|
+
q = q.where(expr, "like", `${obj.startsWith}%`);
|
|
283
|
+
}
|
|
284
|
+
if ("endsWith" in obj && typeof obj.endsWith === "string") {
|
|
285
|
+
q = q.where(expr, "like", `%${obj.endsWith}`);
|
|
286
|
+
}
|
|
287
|
+
if ("in" in obj && Array.isArray(obj.in)) {
|
|
288
|
+
q = q.where(expr, "in", obj.in);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return q;
|
|
292
|
+
}
|
|
293
|
+
function applyNumberFilter(q, expr, filterValue) {
|
|
294
|
+
if (typeof filterValue === "number") {
|
|
295
|
+
return q.where(expr, "=", filterValue);
|
|
296
|
+
}
|
|
297
|
+
if (typeof filterValue === "object" && filterValue !== null && !Array.isArray(filterValue)) {
|
|
298
|
+
const obj = filterValue;
|
|
299
|
+
if (typeof obj.equals === "number") q = q.where(expr, "=", obj.equals);
|
|
300
|
+
if (typeof obj.gt === "number") q = q.where(expr, ">", obj.gt);
|
|
301
|
+
if (typeof obj.gte === "number") q = q.where(expr, ">=", obj.gte);
|
|
302
|
+
if (typeof obj.lt === "number") q = q.where(expr, "<", obj.lt);
|
|
303
|
+
if (typeof obj.lte === "number") q = q.where(expr, "<=", obj.lte);
|
|
304
|
+
if (Array.isArray(obj.in)) q = q.where(expr, "in", obj.in);
|
|
305
|
+
}
|
|
306
|
+
return q;
|
|
307
|
+
}
|
|
308
|
+
function applyBooleanFilter(q, expr, filterValue) {
|
|
309
|
+
if (typeof filterValue === "boolean") {
|
|
310
|
+
return q.where(expr, "=", filterValue);
|
|
311
|
+
}
|
|
312
|
+
if (typeof filterValue === "object" && filterValue !== null && !Array.isArray(filterValue)) {
|
|
313
|
+
const obj = filterValue;
|
|
314
|
+
if (typeof obj.equals === "boolean") q = q.where(expr, "=", obj.equals);
|
|
315
|
+
}
|
|
316
|
+
return q;
|
|
317
|
+
}
|
|
318
|
+
function applyDateFilter(q, expr, filterValue) {
|
|
319
|
+
if (filterValue instanceof Date) {
|
|
320
|
+
return q.where(expr, "=", filterValue);
|
|
321
|
+
}
|
|
322
|
+
if (typeof filterValue === "object" && filterValue !== null && !Array.isArray(filterValue)) {
|
|
323
|
+
const obj = filterValue;
|
|
324
|
+
if (obj.equals instanceof Date) q = q.where(expr, "=", obj.equals);
|
|
325
|
+
if (obj.before instanceof Date) q = q.where(expr, "<", obj.before);
|
|
326
|
+
if (obj.after instanceof Date) q = q.where(expr, ">", obj.after);
|
|
327
|
+
if (Array.isArray(obj.between) && obj.between.length === 2) {
|
|
328
|
+
const [start, end] = obj.between;
|
|
329
|
+
if (start instanceof Date) q = q.where(expr, ">=", start);
|
|
330
|
+
if (end instanceof Date) q = q.where(expr, "<=", end);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return q;
|
|
334
|
+
}
|
|
335
|
+
function applyDataFilter(q, key, fieldType, filterValue) {
|
|
336
|
+
if (fieldType === "number") {
|
|
337
|
+
return applyNumberFilter(q, jsonbNumberField(key), filterValue);
|
|
338
|
+
}
|
|
339
|
+
if (fieldType === "boolean") {
|
|
340
|
+
return applyBooleanFilter(q, jsonbBooleanField(key), filterValue);
|
|
341
|
+
}
|
|
342
|
+
if (fieldType === "date") {
|
|
343
|
+
return applyDateFilter(q, jsonbTimestampField(key), filterValue);
|
|
344
|
+
}
|
|
345
|
+
return applyStringFilter(q, jsonbTextField(key), filterValue);
|
|
346
|
+
}
|
|
347
|
+
function applyEntityFieldFilter(q, key, filterValue) {
|
|
348
|
+
if (typeof filterValue === "object" && filterValue !== null && !Array.isArray(filterValue)) {
|
|
349
|
+
const obj = filterValue;
|
|
350
|
+
if ("equals" in obj) q = q.where(key, "=", obj.equals);
|
|
351
|
+
if ("contains" in obj && typeof obj.contains === "string") {
|
|
352
|
+
q = q.where(key, "like", `%${obj.contains}%`);
|
|
353
|
+
}
|
|
354
|
+
if ("startsWith" in obj && typeof obj.startsWith === "string") {
|
|
355
|
+
q = q.where(key, "like", `${obj.startsWith}%`);
|
|
356
|
+
}
|
|
357
|
+
if ("endsWith" in obj && typeof obj.endsWith === "string") {
|
|
358
|
+
q = q.where(key, "like", `%${obj.endsWith}`);
|
|
359
|
+
}
|
|
360
|
+
if ("in" in obj && Array.isArray(obj.in)) {
|
|
361
|
+
q = q.where(key, "in", obj.in);
|
|
362
|
+
}
|
|
363
|
+
return q;
|
|
364
|
+
}
|
|
365
|
+
return q.where(key, "=", filterValue);
|
|
366
|
+
}
|
|
367
|
+
function parseCountValue(countVal) {
|
|
368
|
+
if (typeof countVal === "number") return countVal;
|
|
369
|
+
if (typeof countVal === "bigint") return Number(countVal);
|
|
370
|
+
return Number.parseInt(String(countVal ?? 0), 10);
|
|
371
|
+
}
|
|
372
|
+
function baseQuery(db, accountId, entityTypeName) {
|
|
373
|
+
return db.selectFrom("corsair_entities").selectAll().where("account_id", "=", accountId).where("entity_type", "=", entityTypeName);
|
|
374
|
+
}
|
|
375
|
+
function createKyselyEntityClient(db, getAccountId, entityTypeName, version, dataSchema) {
|
|
376
|
+
const dataFieldTypes = getDataFieldTypes(dataSchema);
|
|
377
|
+
function parseRow(row) {
|
|
378
|
+
const data = parseJsonLike(row.data);
|
|
379
|
+
return {
|
|
380
|
+
...row,
|
|
381
|
+
data: dataSchema.parse(data)
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
return {
|
|
385
|
+
findByEntityId: async (entityId) => {
|
|
386
|
+
const accountId = await getAccountId();
|
|
387
|
+
const row = await baseQuery(db, accountId, entityTypeName).where("entity_id", "=", entityId).executeTakeFirst();
|
|
388
|
+
return row ? parseRow(row) : null;
|
|
389
|
+
},
|
|
390
|
+
findById: async (id) => {
|
|
391
|
+
const accountId = await getAccountId();
|
|
392
|
+
const row = await baseQuery(db, accountId, entityTypeName).where("id", "=", id).executeTakeFirst();
|
|
393
|
+
return row ? parseRow(row) : null;
|
|
394
|
+
},
|
|
395
|
+
findManyByEntityIds: async (entityIds) => {
|
|
396
|
+
if (entityIds.length === 0) return [];
|
|
397
|
+
const accountId = await getAccountId();
|
|
398
|
+
const rows = await baseQuery(db, accountId, entityTypeName).where("entity_id", "in", entityIds).execute();
|
|
399
|
+
return rows.map(parseRow);
|
|
400
|
+
},
|
|
401
|
+
list: async (options) => {
|
|
402
|
+
const accountId = await getAccountId();
|
|
403
|
+
let q = baseQuery(db, accountId, entityTypeName);
|
|
404
|
+
if (typeof options?.limit === "number") q = q.limit(options.limit);
|
|
405
|
+
if (typeof options?.offset === "number") q = q.offset(options.offset);
|
|
406
|
+
const rows = await q.execute();
|
|
407
|
+
return rows.map(parseRow);
|
|
408
|
+
},
|
|
409
|
+
search: async (options) => {
|
|
410
|
+
const accountId = await getAccountId();
|
|
411
|
+
let q = baseQuery(db, accountId, entityTypeName);
|
|
412
|
+
const reservedKeys = /* @__PURE__ */ new Set(["data", "limit", "offset"]);
|
|
413
|
+
for (const [key, filterValue] of Object.entries(options)) {
|
|
414
|
+
if (reservedKeys.has(key) || filterValue === void 0) continue;
|
|
415
|
+
q = applyEntityFieldFilter(q, key, filterValue);
|
|
416
|
+
}
|
|
417
|
+
if (options.data && typeof options.data === "object") {
|
|
418
|
+
for (const [key, filterValue] of Object.entries(options.data)) {
|
|
419
|
+
if (filterValue === void 0) continue;
|
|
420
|
+
const fieldType = dataFieldTypes[key] ?? "string";
|
|
421
|
+
q = applyDataFilter(q, key, fieldType, filterValue);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
if (typeof options.limit === "number") q = q.limit(options.limit);
|
|
425
|
+
if (typeof options.offset === "number") q = q.offset(options.offset);
|
|
426
|
+
const rows = await q.execute();
|
|
427
|
+
return rows.map(parseRow);
|
|
428
|
+
},
|
|
429
|
+
upsertByEntityId: async (entityId, data) => {
|
|
430
|
+
const accountId = await getAccountId();
|
|
431
|
+
const parsed = dataSchema.parse(data);
|
|
432
|
+
const now = /* @__PURE__ */ new Date();
|
|
433
|
+
const existing = await baseQuery(db, accountId, entityTypeName).select("id").where("entity_id", "=", entityId).executeTakeFirst();
|
|
434
|
+
if (existing?.id) {
|
|
435
|
+
await db.updateTable("corsair_entities").set({ version, data: parsed, updated_at: now }).where("id", "=", existing.id).execute();
|
|
436
|
+
const updated = await db.selectFrom("corsair_entities").selectAll().where("id", "=", existing.id).executeTakeFirst();
|
|
437
|
+
return parseRow(updated);
|
|
438
|
+
}
|
|
439
|
+
const id = generateUUID();
|
|
440
|
+
await db.insertInto("corsair_entities").values({
|
|
441
|
+
id,
|
|
442
|
+
created_at: now,
|
|
443
|
+
updated_at: now,
|
|
444
|
+
account_id: accountId,
|
|
445
|
+
entity_id: entityId,
|
|
446
|
+
entity_type: entityTypeName,
|
|
447
|
+
version,
|
|
448
|
+
data: parsed
|
|
449
|
+
}).execute();
|
|
450
|
+
const inserted = await db.selectFrom("corsair_entities").selectAll().where("id", "=", id).executeTakeFirst();
|
|
451
|
+
return parseRow(inserted);
|
|
452
|
+
},
|
|
453
|
+
deleteById: async (id) => {
|
|
454
|
+
const accountId = await getAccountId();
|
|
455
|
+
const res = await db.deleteFrom("corsair_entities").where("account_id", "=", accountId).where("entity_type", "=", entityTypeName).where("id", "=", id).executeTakeFirst();
|
|
456
|
+
return Number(res.numDeletedRows) > 0;
|
|
457
|
+
},
|
|
458
|
+
deleteByEntityId: async (entityId) => {
|
|
459
|
+
const accountId = await getAccountId();
|
|
460
|
+
const res = await db.deleteFrom("corsair_entities").where("account_id", "=", accountId).where("entity_type", "=", entityTypeName).where("entity_id", "=", entityId).executeTakeFirst();
|
|
461
|
+
return Number(res.numDeletedRows) > 0;
|
|
462
|
+
},
|
|
463
|
+
count: async () => {
|
|
464
|
+
const accountId = await getAccountId();
|
|
465
|
+
const row = await db.selectFrom("corsair_entities").select((eb) => eb.fn.countAll().as("count")).where("account_id", "=", accountId).where("entity_type", "=", entityTypeName).executeTakeFirst();
|
|
466
|
+
return parseCountValue(row?.count);
|
|
467
|
+
}
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// core/auth/types.ts
|
|
472
|
+
var BASE_AUTH_FIELDS = {
|
|
473
|
+
oauth_2: {
|
|
474
|
+
integration: ["client_id", "client_secret", "redirect_url"],
|
|
475
|
+
account: [
|
|
476
|
+
"access_token",
|
|
477
|
+
"refresh_token",
|
|
478
|
+
"expires_at",
|
|
479
|
+
"scope",
|
|
480
|
+
"webhook_signature"
|
|
481
|
+
]
|
|
482
|
+
},
|
|
483
|
+
api_key: {
|
|
484
|
+
integration: [],
|
|
485
|
+
account: ["api_key", "webhook_signature"]
|
|
486
|
+
},
|
|
487
|
+
bot_token: {
|
|
488
|
+
integration: [],
|
|
489
|
+
account: ["bot_token", "webhook_signature"]
|
|
490
|
+
}
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
// core/auth/key-manager.ts
|
|
494
|
+
function createFieldAccessors(getDecryptedConfig, updateConfig, fields) {
|
|
495
|
+
const accessors = {};
|
|
496
|
+
for (const field of fields) {
|
|
497
|
+
accessors[`get_${field}`] = async () => {
|
|
498
|
+
const config = await getDecryptedConfig();
|
|
499
|
+
return config[field] ?? null;
|
|
500
|
+
};
|
|
501
|
+
accessors[`set_${field}`] = async (value) => {
|
|
502
|
+
await updateConfig({ [field]: value });
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
return accessors;
|
|
506
|
+
}
|
|
507
|
+
var parseConfig = (config) => {
|
|
508
|
+
if (!config) return {};
|
|
509
|
+
if (typeof config === "string") {
|
|
510
|
+
try {
|
|
511
|
+
return JSON.parse(config);
|
|
512
|
+
} catch {
|
|
513
|
+
return {};
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
return config;
|
|
517
|
+
};
|
|
518
|
+
function createIntegrationKeyManager(options) {
|
|
519
|
+
const {
|
|
520
|
+
authType,
|
|
521
|
+
integrationName,
|
|
522
|
+
kek,
|
|
523
|
+
database,
|
|
524
|
+
extraIntegrationFields = []
|
|
525
|
+
} = options;
|
|
526
|
+
const allFields = [
|
|
527
|
+
...BASE_AUTH_FIELDS[authType].integration,
|
|
528
|
+
...extraIntegrationFields
|
|
529
|
+
];
|
|
530
|
+
let cachedIntegration = null;
|
|
531
|
+
const ctx = {
|
|
532
|
+
kek,
|
|
533
|
+
integrationName,
|
|
534
|
+
getIntegration: async () => {
|
|
535
|
+
if (cachedIntegration) return cachedIntegration;
|
|
536
|
+
const integration = await database.db.selectFrom("corsair_integrations").selectAll().where("name", "=", integrationName).executeTakeFirst();
|
|
537
|
+
if (!integration) {
|
|
538
|
+
throw new Error(
|
|
539
|
+
`Integration "${integrationName}" not found. Make sure to create the integration first.`
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
cachedIntegration = {
|
|
543
|
+
id: integration.id,
|
|
544
|
+
config: parseConfig(integration.config),
|
|
545
|
+
dek: integration.dek ?? null
|
|
546
|
+
};
|
|
547
|
+
return cachedIntegration;
|
|
548
|
+
},
|
|
549
|
+
updateIntegration: async (data) => {
|
|
550
|
+
const integration = await ctx.getIntegration();
|
|
551
|
+
await database.db.updateTable("corsair_integrations").set({
|
|
552
|
+
...data.config !== void 0 ? { config: data.config } : {},
|
|
553
|
+
...data.dek !== void 0 ? { dek: data.dek } : {},
|
|
554
|
+
updated_at: /* @__PURE__ */ new Date()
|
|
555
|
+
}).where("id", "=", integration.id).execute();
|
|
556
|
+
cachedIntegration = null;
|
|
557
|
+
}
|
|
558
|
+
};
|
|
559
|
+
let cachedDek = null;
|
|
560
|
+
const getDecryptedDek = async () => {
|
|
561
|
+
if (cachedDek) return cachedDek;
|
|
562
|
+
const integration = await ctx.getIntegration();
|
|
563
|
+
if (!integration.dek) {
|
|
564
|
+
throw new Error(
|
|
565
|
+
`No DEK found for integration "${integrationName}". Initialize the integration first.`
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
cachedDek = await decryptDEK(integration.dek, kek);
|
|
569
|
+
return cachedDek;
|
|
570
|
+
};
|
|
571
|
+
const getDecryptedConfig = async () => {
|
|
572
|
+
const integration = await ctx.getIntegration();
|
|
573
|
+
const dek = await getDecryptedDek();
|
|
574
|
+
const config = integration.config;
|
|
575
|
+
if (!config || Object.keys(config).length === 0) {
|
|
576
|
+
return {};
|
|
577
|
+
}
|
|
578
|
+
return decryptConfig(config, dek);
|
|
579
|
+
};
|
|
580
|
+
const updateConfig = async (updates) => {
|
|
581
|
+
const dek = await getDecryptedDek();
|
|
582
|
+
const currentConfig = await getDecryptedConfig();
|
|
583
|
+
const newConfig = { ...currentConfig };
|
|
584
|
+
for (const [key, value] of Object.entries(updates)) {
|
|
585
|
+
if (value === null) {
|
|
586
|
+
delete newConfig[key];
|
|
587
|
+
} else {
|
|
588
|
+
newConfig[key] = value;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
const encryptedConfig = encryptConfig(newConfig, dek);
|
|
592
|
+
await ctx.updateIntegration({ config: encryptedConfig });
|
|
593
|
+
};
|
|
594
|
+
const manager = {
|
|
595
|
+
get_dek: getDecryptedDek,
|
|
596
|
+
issue_new_dek: async () => {
|
|
597
|
+
const integration = await ctx.getIntegration();
|
|
598
|
+
const newDek = generateDEK();
|
|
599
|
+
let newConfig = {};
|
|
600
|
+
if (integration.dek) {
|
|
601
|
+
const oldDek = await decryptDEK(integration.dek, kek);
|
|
602
|
+
const config = integration.config;
|
|
603
|
+
if (config && Object.keys(config).length > 0) {
|
|
604
|
+
newConfig = reEncryptConfig(config, oldDek, newDek);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
const encryptedNewDek = await encryptDEK(newDek, kek);
|
|
608
|
+
await ctx.updateIntegration({
|
|
609
|
+
config: newConfig,
|
|
610
|
+
dek: encryptedNewDek
|
|
611
|
+
});
|
|
612
|
+
cachedDek = newDek;
|
|
613
|
+
return newDek;
|
|
614
|
+
},
|
|
615
|
+
// Auto-generated field accessors
|
|
616
|
+
...createFieldAccessors(getDecryptedConfig, updateConfig, allFields)
|
|
617
|
+
};
|
|
618
|
+
return manager;
|
|
619
|
+
}
|
|
620
|
+
function createAccountKeyManager(options) {
|
|
621
|
+
const {
|
|
622
|
+
authType,
|
|
623
|
+
integrationName,
|
|
624
|
+
tenantId,
|
|
625
|
+
kek,
|
|
626
|
+
database,
|
|
627
|
+
extraAccountFields = []
|
|
628
|
+
} = options;
|
|
629
|
+
const allFields = [
|
|
630
|
+
...BASE_AUTH_FIELDS[authType].account,
|
|
631
|
+
...extraAccountFields
|
|
632
|
+
];
|
|
633
|
+
let cachedAccount = null;
|
|
634
|
+
let cachedIntegration = null;
|
|
635
|
+
const getIntegration = async () => {
|
|
636
|
+
if (cachedIntegration) return cachedIntegration;
|
|
637
|
+
const integration = await database.db.selectFrom("corsair_integrations").selectAll().where("name", "=", integrationName).executeTakeFirst();
|
|
638
|
+
if (!integration) {
|
|
639
|
+
throw new Error(
|
|
640
|
+
`Integration "${integrationName}" not found. Make sure to create the integration first.`
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
cachedIntegration = {
|
|
644
|
+
id: integration.id,
|
|
645
|
+
config: parseConfig(integration.config),
|
|
646
|
+
dek: integration.dek ?? null
|
|
647
|
+
};
|
|
648
|
+
return cachedIntegration;
|
|
649
|
+
};
|
|
650
|
+
const ctx = {
|
|
651
|
+
kek,
|
|
652
|
+
integrationName,
|
|
653
|
+
tenantId,
|
|
654
|
+
getIntegration,
|
|
655
|
+
getAccount: async () => {
|
|
656
|
+
if (cachedAccount) return cachedAccount;
|
|
657
|
+
const integration = await getIntegration();
|
|
658
|
+
const account = await database.db.selectFrom("corsair_accounts").selectAll().where("tenant_id", "=", tenantId).where("integration_id", "=", integration.id).executeTakeFirst();
|
|
659
|
+
if (!account) {
|
|
660
|
+
throw new Error(
|
|
661
|
+
`Account not found for tenant "${tenantId}" and integration "${integrationName}". Make sure to create the account first.`
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
cachedAccount = {
|
|
665
|
+
id: account.id,
|
|
666
|
+
config: parseConfig(account.config),
|
|
667
|
+
dek: account.dek ?? null
|
|
668
|
+
};
|
|
669
|
+
return cachedAccount;
|
|
670
|
+
},
|
|
671
|
+
updateAccount: async (data) => {
|
|
672
|
+
const account = await ctx.getAccount();
|
|
673
|
+
await database.db.updateTable("corsair_accounts").set({
|
|
674
|
+
...data.config !== void 0 ? { config: data.config } : {},
|
|
675
|
+
...data.dek !== void 0 ? { dek: data.dek } : {},
|
|
676
|
+
updated_at: /* @__PURE__ */ new Date()
|
|
677
|
+
}).where("id", "=", account.id).execute();
|
|
678
|
+
cachedAccount = null;
|
|
679
|
+
}
|
|
680
|
+
};
|
|
681
|
+
let cachedDek = null;
|
|
682
|
+
let cachedIntegrationDek = null;
|
|
683
|
+
const getDecryptedDek = async () => {
|
|
684
|
+
if (cachedDek) return cachedDek;
|
|
685
|
+
const account = await ctx.getAccount();
|
|
686
|
+
if (!account.dek) {
|
|
687
|
+
throw new Error(
|
|
688
|
+
`No DEK found for account (tenant: "${tenantId}", integration: "${integrationName}"). Initialize the account first.`
|
|
689
|
+
);
|
|
690
|
+
}
|
|
691
|
+
cachedDek = await decryptDEK(account.dek, kek);
|
|
692
|
+
return cachedDek;
|
|
693
|
+
};
|
|
694
|
+
const getDecryptedIntegrationDek = async () => {
|
|
695
|
+
if (cachedIntegrationDek) return cachedIntegrationDek;
|
|
696
|
+
const integration = await ctx.getIntegration();
|
|
697
|
+
if (!integration.dek) {
|
|
698
|
+
throw new Error(
|
|
699
|
+
`No DEK found for integration "${integrationName}". Initialize the integration first.`
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
cachedIntegrationDek = await decryptDEK(integration.dek, kek);
|
|
703
|
+
return cachedIntegrationDek;
|
|
704
|
+
};
|
|
705
|
+
const getDecryptedConfig = async () => {
|
|
706
|
+
const account = await ctx.getAccount();
|
|
707
|
+
const dek = await getDecryptedDek();
|
|
708
|
+
const config = account.config;
|
|
709
|
+
if (!config || Object.keys(config).length === 0) {
|
|
710
|
+
return {};
|
|
711
|
+
}
|
|
712
|
+
return decryptConfig(config, dek);
|
|
713
|
+
};
|
|
714
|
+
const getDecryptedIntegrationConfig = async () => {
|
|
715
|
+
const integration = await ctx.getIntegration();
|
|
716
|
+
const dek = await getDecryptedIntegrationDek();
|
|
717
|
+
const config = integration.config;
|
|
718
|
+
if (!config || Object.keys(config).length === 0) {
|
|
719
|
+
return {};
|
|
720
|
+
}
|
|
721
|
+
return decryptConfig(config, dek);
|
|
722
|
+
};
|
|
723
|
+
const updateConfig = async (updates) => {
|
|
724
|
+
const dek = await getDecryptedDek();
|
|
725
|
+
const currentConfig = await getDecryptedConfig();
|
|
726
|
+
const newConfig = { ...currentConfig };
|
|
727
|
+
for (const [key, value] of Object.entries(updates)) {
|
|
728
|
+
if (value === null) {
|
|
729
|
+
delete newConfig[key];
|
|
730
|
+
} else {
|
|
731
|
+
newConfig[key] = value;
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
const encryptedConfig = encryptConfig(newConfig, dek);
|
|
735
|
+
await ctx.updateAccount({ config: encryptedConfig });
|
|
736
|
+
};
|
|
737
|
+
const manager = {
|
|
738
|
+
get_dek: getDecryptedDek,
|
|
739
|
+
issue_new_dek: async () => {
|
|
740
|
+
const account = await ctx.getAccount();
|
|
741
|
+
const newDek = generateDEK();
|
|
742
|
+
let newConfig = {};
|
|
743
|
+
if (account.dek) {
|
|
744
|
+
const oldDek = await decryptDEK(account.dek, kek);
|
|
745
|
+
const config = account.config;
|
|
746
|
+
if (config && Object.keys(config).length > 0) {
|
|
747
|
+
newConfig = reEncryptConfig(config, oldDek, newDek);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
const encryptedNewDek = await encryptDEK(newDek, kek);
|
|
751
|
+
await ctx.updateAccount({
|
|
752
|
+
config: newConfig,
|
|
753
|
+
dek: encryptedNewDek
|
|
754
|
+
});
|
|
755
|
+
cachedDek = newDek;
|
|
756
|
+
return newDek;
|
|
757
|
+
},
|
|
758
|
+
// Auto-generated field accessors
|
|
759
|
+
...createFieldAccessors(getDecryptedConfig, updateConfig, allFields)
|
|
760
|
+
};
|
|
761
|
+
if (authType === "oauth_2") {
|
|
762
|
+
manager.get_integration_credentials = async () => {
|
|
763
|
+
const config = await getDecryptedIntegrationConfig();
|
|
764
|
+
return {
|
|
765
|
+
client_id: config.client_id || null,
|
|
766
|
+
client_secret: config.client_secret || null,
|
|
767
|
+
redirect_url: config.redirect_url ?? null
|
|
768
|
+
};
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
return manager;
|
|
772
|
+
}
|
|
773
|
+
async function initializeIntegrationDEK(database, integrationName, kek) {
|
|
774
|
+
const integration = await database.db.selectFrom("corsair_integrations").selectAll().where("name", "=", integrationName).executeTakeFirst();
|
|
775
|
+
if (!integration) {
|
|
776
|
+
throw new Error(`Integration "${integrationName}" not found.`);
|
|
777
|
+
}
|
|
778
|
+
const dek = generateDEK();
|
|
779
|
+
const encryptedDek = await encryptDEK(dek, kek);
|
|
780
|
+
await database.db.updateTable("corsair_integrations").set({
|
|
781
|
+
dek: encryptedDek,
|
|
782
|
+
updated_at: /* @__PURE__ */ new Date()
|
|
783
|
+
}).where("id", "=", integration.id).execute();
|
|
784
|
+
return dek;
|
|
785
|
+
}
|
|
786
|
+
async function initializeAccountDEK(database, integrationName, tenantId, kek) {
|
|
787
|
+
const integration = await database.db.selectFrom("corsair_integrations").selectAll().where("name", "=", integrationName).executeTakeFirst();
|
|
788
|
+
if (!integration) {
|
|
789
|
+
throw new Error(`Integration "${integrationName}" not found.`);
|
|
790
|
+
}
|
|
791
|
+
const account = await database.db.selectFrom("corsair_accounts").selectAll().where("tenant_id", "=", tenantId).where("integration_id", "=", integration.id).executeTakeFirst();
|
|
792
|
+
if (!account) {
|
|
793
|
+
throw new Error(
|
|
794
|
+
`Account not found for tenant "${tenantId}" and integration "${integrationName}".`
|
|
795
|
+
);
|
|
796
|
+
}
|
|
797
|
+
const dek = generateDEK();
|
|
798
|
+
const encryptedDek = await encryptDEK(dek, kek);
|
|
799
|
+
await database.db.updateTable("corsair_accounts").set({
|
|
800
|
+
dek: encryptedDek,
|
|
801
|
+
updated_at: /* @__PURE__ */ new Date()
|
|
802
|
+
}).where("id", "=", account.id).execute();
|
|
803
|
+
return dek;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
// core/errors/handler.ts
|
|
807
|
+
var defaultErrorHandler = async (error, context) => {
|
|
808
|
+
console.error(`[corsair:${context.pluginId}:${context.operation}]`, {
|
|
809
|
+
error: error.message,
|
|
810
|
+
input: context.input
|
|
811
|
+
});
|
|
812
|
+
return {
|
|
813
|
+
maxRetries: 0
|
|
814
|
+
};
|
|
815
|
+
};
|
|
816
|
+
async function handleCorsairError(error, pluginId, operation, input, errorHandlers) {
|
|
817
|
+
const context = {
|
|
818
|
+
pluginId,
|
|
819
|
+
operation,
|
|
820
|
+
input,
|
|
821
|
+
originalError: error
|
|
822
|
+
};
|
|
823
|
+
const matchingHandlerName = Object.keys(errorHandlers).find(
|
|
824
|
+
(errorName) => errorHandlers[errorName]?.match(error, context)
|
|
825
|
+
);
|
|
826
|
+
const pluginSpecificErrorHandler = errorHandlers[matchingHandlerName || "DEFAULT"]?.handler;
|
|
827
|
+
const handler = pluginSpecificErrorHandler || defaultErrorHandler;
|
|
828
|
+
return await handler(error, context);
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// core/permissions/index.ts
|
|
832
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
833
|
+
import { v4 as uuidv4 } from "uuid";
|
|
834
|
+
var PERMISSION_MATRIX = {
|
|
835
|
+
open: { read: "allow", write: "allow", destructive: "allow" },
|
|
836
|
+
cautious: { read: "allow", write: "allow", destructive: "require_approval" },
|
|
837
|
+
strict: { read: "allow", write: "require_approval", destructive: "deny" },
|
|
838
|
+
readonly: { read: "allow", write: "deny", destructive: "deny" }
|
|
839
|
+
};
|
|
840
|
+
function evaluatePermission(riskLevel, mode, override) {
|
|
841
|
+
if (override !== void 0) return override;
|
|
842
|
+
return PERMISSION_MATRIX[mode][riskLevel];
|
|
843
|
+
}
|
|
844
|
+
function parseDurationMs(duration) {
|
|
845
|
+
const regex = /(\d+)(d|h|m|s)/g;
|
|
846
|
+
let total = 0;
|
|
847
|
+
let match;
|
|
848
|
+
while ((match = regex.exec(duration)) !== null) {
|
|
849
|
+
const value = parseInt(match[1], 10);
|
|
850
|
+
switch (match[2]) {
|
|
851
|
+
case "d":
|
|
852
|
+
total += value * 864e5;
|
|
853
|
+
break;
|
|
854
|
+
case "h":
|
|
855
|
+
total += value * 36e5;
|
|
856
|
+
break;
|
|
857
|
+
case "m":
|
|
858
|
+
total += value * 6e4;
|
|
859
|
+
break;
|
|
860
|
+
case "s":
|
|
861
|
+
total += value * 1e3;
|
|
862
|
+
break;
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
return total > 0 ? total : 10 * 60 * 1e3;
|
|
866
|
+
}
|
|
867
|
+
function buildPermissionsNamespace(db) {
|
|
868
|
+
return {
|
|
869
|
+
async find_by_permission_id(id) {
|
|
870
|
+
if (!db) return void 0;
|
|
871
|
+
return db.db.selectFrom("corsair_permissions").selectAll().where("id", "=", id).executeTakeFirst();
|
|
872
|
+
},
|
|
873
|
+
async find_by_token(token) {
|
|
874
|
+
if (!db) return void 0;
|
|
875
|
+
return db.db.selectFrom("corsair_permissions").selectAll().where("token", "=", token).executeTakeFirst();
|
|
876
|
+
},
|
|
877
|
+
async set_executing(id) {
|
|
878
|
+
if (!db) return;
|
|
879
|
+
await db.db.updateTable("corsair_permissions").set({ status: "executing", updated_at: /* @__PURE__ */ new Date() }).where("id", "=", id).execute();
|
|
880
|
+
},
|
|
881
|
+
async set_completed(id) {
|
|
882
|
+
if (!db) return;
|
|
883
|
+
await db.db.updateTable("corsair_permissions").set({ status: "completed", updated_at: /* @__PURE__ */ new Date() }).where("id", "=", id).execute();
|
|
884
|
+
}
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
async function enforcePermission(opts) {
|
|
888
|
+
const policy = evaluatePermission(opts.riskLevel, opts.mode, opts.override);
|
|
889
|
+
if (policy === "allow") return { result: "allow" };
|
|
890
|
+
const irreversibleNote = opts.meta?.irreversible ? " (irreversible)" : "";
|
|
891
|
+
const description = opts.meta?.description ? `${opts.meta.description}${irreversibleNote}` : `${opts.pluginId}.${opts.endpointPath}${irreversibleNote}`;
|
|
892
|
+
if (policy === "deny" || !opts.db) {
|
|
893
|
+
console.log(
|
|
894
|
+
`[corsair/${opts.pluginId}] '${opts.endpointPath}' blocked \u2014 denied by permission mode '${opts.mode}'.`,
|
|
895
|
+
`
|
|
896
|
+
Action: ${description}`,
|
|
897
|
+
`
|
|
898
|
+
To allow this, update the permission mode or add an override in your corsair config.`
|
|
899
|
+
);
|
|
900
|
+
return { result: "blocked" };
|
|
901
|
+
}
|
|
902
|
+
const argsJson = JSON.stringify(opts.args);
|
|
903
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
904
|
+
const tenantId = opts.tenantId ?? "default";
|
|
905
|
+
const existing = await opts.db.db.selectFrom("corsair_permissions").selectAll().where("plugin", "=", opts.pluginId).where("endpoint", "=", opts.endpointPath).where("args", "=", argsJson).where("tenant_id", "=", tenantId).where("expires_at", ">", now).where("status", "in", ["pending", "approved", "executing"]).orderBy("created_at", "desc").limit(1).executeTakeFirst();
|
|
906
|
+
if (existing) {
|
|
907
|
+
if (existing.status === "approved") {
|
|
908
|
+
const db = opts.db;
|
|
909
|
+
const permissionId = existing.id;
|
|
910
|
+
return {
|
|
911
|
+
result: "allow",
|
|
912
|
+
onComplete: async () => {
|
|
913
|
+
await db.db.updateTable("corsair_permissions").set({ status: "completed", updated_at: /* @__PURE__ */ new Date() }).where("id", "=", permissionId).execute();
|
|
914
|
+
}
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
if (existing.status === "executing") {
|
|
918
|
+
return { result: "allow" };
|
|
919
|
+
}
|
|
920
|
+
console.log(
|
|
921
|
+
`[corsair/${opts.pluginId}] '${opts.endpointPath}' blocked \u2014 approval already pending.`,
|
|
922
|
+
`
|
|
923
|
+
Action: ${description}`,
|
|
924
|
+
`
|
|
925
|
+
Permission ID: ${existing.id}`,
|
|
926
|
+
`
|
|
927
|
+
Use the token to approve or deny this request.`
|
|
928
|
+
);
|
|
929
|
+
return { result: "blocked" };
|
|
930
|
+
}
|
|
931
|
+
const id = uuidv4();
|
|
932
|
+
const token = randomBytes2(32).toString("hex");
|
|
933
|
+
const timeoutMs = opts.timeoutMs ?? 10 * 60 * 1e3;
|
|
934
|
+
const expiresAt = new Date(Date.now() + timeoutMs).toISOString();
|
|
935
|
+
await opts.db.db.insertInto("corsair_permissions").values({
|
|
936
|
+
id,
|
|
937
|
+
created_at: /* @__PURE__ */ new Date(),
|
|
938
|
+
updated_at: /* @__PURE__ */ new Date(),
|
|
939
|
+
token,
|
|
940
|
+
plugin: opts.pluginId,
|
|
941
|
+
endpoint: opts.endpointPath,
|
|
942
|
+
args: argsJson,
|
|
943
|
+
tenant_id: tenantId,
|
|
944
|
+
status: "pending",
|
|
945
|
+
expires_at: expiresAt
|
|
946
|
+
}).execute();
|
|
947
|
+
console.log(
|
|
948
|
+
`[corsair/${opts.pluginId}] '${opts.endpointPath}' blocked \u2014 approval required.`,
|
|
949
|
+
`
|
|
950
|
+
Action: ${description}`,
|
|
951
|
+
`
|
|
952
|
+
Permission ID: ${id}`,
|
|
953
|
+
`
|
|
954
|
+
Permission token: ${token}`,
|
|
955
|
+
`
|
|
956
|
+
Expires at: ${expiresAt}`,
|
|
957
|
+
`
|
|
958
|
+
Use the token to approve or deny this request.`
|
|
959
|
+
);
|
|
960
|
+
return { result: "blocked" };
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
// core/endpoints/bind.ts
|
|
964
|
+
function isEndpoint(value) {
|
|
965
|
+
return typeof value === "function";
|
|
966
|
+
}
|
|
967
|
+
function bindEndpointsRecursively({
|
|
968
|
+
endpoints,
|
|
969
|
+
hooks,
|
|
970
|
+
ctx,
|
|
971
|
+
tree,
|
|
972
|
+
pluginId,
|
|
973
|
+
errorHandlers,
|
|
974
|
+
currentPath = [],
|
|
975
|
+
keyBuilder,
|
|
976
|
+
permissionsConfig,
|
|
977
|
+
endpointMeta,
|
|
978
|
+
database,
|
|
979
|
+
approvalConfig,
|
|
980
|
+
tenantId
|
|
981
|
+
}) {
|
|
982
|
+
for (const [key, value] of Object.entries(endpoints)) {
|
|
983
|
+
const nodeHooks = hooks?.[key];
|
|
984
|
+
if (isEndpoint(value)) {
|
|
985
|
+
const endpointHooks = nodeHooks;
|
|
986
|
+
const operationPath = [...currentPath, key].join(".");
|
|
987
|
+
const boundFn = async (args) => {
|
|
988
|
+
let onPermissionComplete;
|
|
989
|
+
if (permissionsConfig) {
|
|
990
|
+
const meta = endpointMeta?.[operationPath];
|
|
991
|
+
const { result: permResult, onComplete } = await enforcePermission({
|
|
992
|
+
pluginId,
|
|
993
|
+
endpointPath: operationPath,
|
|
994
|
+
args,
|
|
995
|
+
mode: permissionsConfig.mode,
|
|
996
|
+
override: permissionsConfig.overrides?.[operationPath],
|
|
997
|
+
// Default to 'write' when no meta declared — conservative fallback
|
|
998
|
+
riskLevel: meta?.riskLevel ?? "write",
|
|
999
|
+
meta,
|
|
1000
|
+
db: database,
|
|
1001
|
+
timeoutMs: approvalConfig ? parseDurationMs(approvalConfig.timeout) : void 0,
|
|
1002
|
+
tenantId
|
|
1003
|
+
});
|
|
1004
|
+
if (permResult === "blocked") return null;
|
|
1005
|
+
onPermissionComplete = onComplete;
|
|
1006
|
+
}
|
|
1007
|
+
const call = async (attemptNumber, callCtx, callArgs) => {
|
|
1008
|
+
try {
|
|
1009
|
+
return await value(callCtx, callArgs);
|
|
1010
|
+
} catch (error) {
|
|
1011
|
+
if (error instanceof Error) {
|
|
1012
|
+
const retryStrategy = await handleCorsairError(
|
|
1013
|
+
error,
|
|
1014
|
+
pluginId,
|
|
1015
|
+
operationPath,
|
|
1016
|
+
typeof callArgs === "object" && callArgs !== null ? callArgs : { args: callArgs },
|
|
1017
|
+
errorHandlers
|
|
1018
|
+
);
|
|
1019
|
+
if (attemptNumber < (retryStrategy.maxRetries || 0)) {
|
|
1020
|
+
const newAttempt = attemptNumber + 1;
|
|
1021
|
+
console.log(
|
|
1022
|
+
`Retrying (${newAttempt} / ${retryStrategy.maxRetries})...`
|
|
1023
|
+
);
|
|
1024
|
+
let delayMs;
|
|
1025
|
+
if (retryStrategy.headersRetryAfterMs) {
|
|
1026
|
+
delayMs = retryStrategy.headersRetryAfterMs;
|
|
1027
|
+
} else {
|
|
1028
|
+
switch (retryStrategy.retryStrategy) {
|
|
1029
|
+
case "exponential_backoff":
|
|
1030
|
+
delayMs = Math.pow(2, newAttempt - 1) * 1e3;
|
|
1031
|
+
break;
|
|
1032
|
+
case "exponential_backoff_jitter":
|
|
1033
|
+
const baseDelay = Math.pow(2, newAttempt - 1) * 1e3;
|
|
1034
|
+
const jitter = (Math.random() - 0.5) * 1e3;
|
|
1035
|
+
delayMs = Math.max(0, baseDelay + jitter);
|
|
1036
|
+
break;
|
|
1037
|
+
case "linear_1s":
|
|
1038
|
+
delayMs = 1e3;
|
|
1039
|
+
break;
|
|
1040
|
+
case "linear_2s":
|
|
1041
|
+
delayMs = 2e3;
|
|
1042
|
+
break;
|
|
1043
|
+
case "linear_3s":
|
|
1044
|
+
delayMs = 3e3;
|
|
1045
|
+
break;
|
|
1046
|
+
case "linear_4s":
|
|
1047
|
+
delayMs = 4e3;
|
|
1048
|
+
break;
|
|
1049
|
+
default:
|
|
1050
|
+
delayMs = 1e3;
|
|
1051
|
+
break;
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
1055
|
+
await call(newAttempt, callCtx, callArgs);
|
|
1056
|
+
console.log(
|
|
1057
|
+
`[corsair:${pluginId}:${operationPath}] Retry strategy:`,
|
|
1058
|
+
retryStrategy
|
|
1059
|
+
);
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
throw error;
|
|
1063
|
+
}
|
|
1064
|
+
};
|
|
1065
|
+
const key2 = keyBuilder ? await keyBuilder(ctx, "endpoint") : void 0;
|
|
1066
|
+
if (!endpointHooks?.before && !endpointHooks?.after) {
|
|
1067
|
+
const res2 = await call(0, { ...ctx, key: key2 }, args);
|
|
1068
|
+
await onPermissionComplete?.();
|
|
1069
|
+
return res2;
|
|
1070
|
+
}
|
|
1071
|
+
const ctxWithKey = { ...ctx, key: key2 };
|
|
1072
|
+
const beforeResult = endpointHooks.before ? await endpointHooks.before(ctxWithKey, args) : {
|
|
1073
|
+
ctx: ctxWithKey,
|
|
1074
|
+
args,
|
|
1075
|
+
continue: true,
|
|
1076
|
+
passToAfter: void 0
|
|
1077
|
+
};
|
|
1078
|
+
if (beforeResult.continue === false) return;
|
|
1079
|
+
const res = await call(0, beforeResult.ctx, beforeResult.args);
|
|
1080
|
+
await endpointHooks.after?.(
|
|
1081
|
+
beforeResult.ctx,
|
|
1082
|
+
res,
|
|
1083
|
+
beforeResult.passToAfter
|
|
1084
|
+
);
|
|
1085
|
+
await onPermissionComplete?.();
|
|
1086
|
+
return res;
|
|
1087
|
+
};
|
|
1088
|
+
tree[key] = boundFn;
|
|
1089
|
+
} else if (value && typeof value === "object") {
|
|
1090
|
+
const nestedTree = {};
|
|
1091
|
+
bindEndpointsRecursively({
|
|
1092
|
+
endpoints: value,
|
|
1093
|
+
hooks: nodeHooks,
|
|
1094
|
+
ctx,
|
|
1095
|
+
tree: nestedTree,
|
|
1096
|
+
pluginId,
|
|
1097
|
+
errorHandlers,
|
|
1098
|
+
currentPath: [...currentPath, key],
|
|
1099
|
+
keyBuilder,
|
|
1100
|
+
permissionsConfig,
|
|
1101
|
+
endpointMeta,
|
|
1102
|
+
database,
|
|
1103
|
+
approvalConfig,
|
|
1104
|
+
tenantId
|
|
1105
|
+
});
|
|
1106
|
+
tree[key] = nestedTree;
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
// core/constants.ts
|
|
1112
|
+
var BaseProviders = [
|
|
1113
|
+
"discord",
|
|
1114
|
+
"github",
|
|
1115
|
+
"gmail",
|
|
1116
|
+
"googlecalendar",
|
|
1117
|
+
"googledrive",
|
|
1118
|
+
"googlesheets",
|
|
1119
|
+
"hubspot",
|
|
1120
|
+
"linear",
|
|
1121
|
+
"posthog",
|
|
1122
|
+
"resend",
|
|
1123
|
+
"slack",
|
|
1124
|
+
"spotify",
|
|
1125
|
+
"tavily"
|
|
1126
|
+
];
|
|
1127
|
+
|
|
1128
|
+
// core/inspect/index.ts
|
|
1129
|
+
function zodToJsonSchema(schema) {
|
|
1130
|
+
const def = schema._def;
|
|
1131
|
+
const typeName = def.typeName;
|
|
1132
|
+
switch (typeName) {
|
|
1133
|
+
case "ZodString":
|
|
1134
|
+
return { type: "string" };
|
|
1135
|
+
case "ZodNumber":
|
|
1136
|
+
return { type: "number" };
|
|
1137
|
+
case "ZodBoolean":
|
|
1138
|
+
return { type: "boolean" };
|
|
1139
|
+
case "ZodNull":
|
|
1140
|
+
return { type: "null" };
|
|
1141
|
+
case "ZodUnknown":
|
|
1142
|
+
case "ZodAny":
|
|
1143
|
+
return {};
|
|
1144
|
+
case "ZodLiteral":
|
|
1145
|
+
return { const: def.value };
|
|
1146
|
+
case "ZodEnum":
|
|
1147
|
+
return { enum: def.values };
|
|
1148
|
+
case "ZodOptional":
|
|
1149
|
+
return zodToJsonSchema(def.innerType);
|
|
1150
|
+
case "ZodNullable": {
|
|
1151
|
+
const inner = zodToJsonSchema(def.innerType);
|
|
1152
|
+
return { anyOf: [inner, { type: "null" }] };
|
|
1153
|
+
}
|
|
1154
|
+
case "ZodArray":
|
|
1155
|
+
return { type: "array", items: zodToJsonSchema(def.type) };
|
|
1156
|
+
case "ZodRecord":
|
|
1157
|
+
return {
|
|
1158
|
+
type: "object",
|
|
1159
|
+
additionalProperties: zodToJsonSchema(def.valueType)
|
|
1160
|
+
};
|
|
1161
|
+
case "ZodObject": {
|
|
1162
|
+
const shape = def.shape();
|
|
1163
|
+
const properties = {};
|
|
1164
|
+
const required = [];
|
|
1165
|
+
for (const [key, val] of Object.entries(shape)) {
|
|
1166
|
+
properties[key] = zodToJsonSchema(val);
|
|
1167
|
+
const fieldTypeName = val._def.typeName;
|
|
1168
|
+
if (fieldTypeName !== "ZodOptional" && fieldTypeName !== "ZodNullable") {
|
|
1169
|
+
required.push(key);
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
const result = { type: "object", properties };
|
|
1173
|
+
if (required.length > 0) result.required = required;
|
|
1174
|
+
return result;
|
|
1175
|
+
}
|
|
1176
|
+
case "ZodUnion":
|
|
1177
|
+
return { anyOf: def.options.map(zodToJsonSchema) };
|
|
1178
|
+
case "ZodIntersection":
|
|
1179
|
+
return {
|
|
1180
|
+
allOf: [
|
|
1181
|
+
zodToJsonSchema(def.left),
|
|
1182
|
+
zodToJsonSchema(def.right)
|
|
1183
|
+
]
|
|
1184
|
+
};
|
|
1185
|
+
case "ZodEffects":
|
|
1186
|
+
return zodToJsonSchema(def.schema);
|
|
1187
|
+
default:
|
|
1188
|
+
return { type: (typeName ?? "unknown").replace("Zod", "").toLowerCase() };
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
var STRING_OPERATORS = ["equals", "contains", "startsWith", "endsWith", "in"];
|
|
1192
|
+
var NUMBER_OPERATORS = ["equals", "gt", "gte", "lt", "lte", "in"];
|
|
1193
|
+
var BOOLEAN_OPERATORS = ["equals"];
|
|
1194
|
+
var DATE_OPERATORS = ["equals", "before", "after", "between"];
|
|
1195
|
+
function getSchemaLeafType(schema) {
|
|
1196
|
+
const def = schema._def;
|
|
1197
|
+
const typeName = def.typeName;
|
|
1198
|
+
switch (typeName) {
|
|
1199
|
+
case "ZodOptional":
|
|
1200
|
+
case "ZodNullable":
|
|
1201
|
+
case "ZodDefault":
|
|
1202
|
+
return getSchemaLeafType(def.innerType);
|
|
1203
|
+
case "ZodEffects":
|
|
1204
|
+
return getSchemaLeafType(def.schema);
|
|
1205
|
+
case "ZodString":
|
|
1206
|
+
return "string";
|
|
1207
|
+
case "ZodNumber":
|
|
1208
|
+
return "number";
|
|
1209
|
+
case "ZodBoolean":
|
|
1210
|
+
return "boolean";
|
|
1211
|
+
case "ZodDate":
|
|
1212
|
+
return "date";
|
|
1213
|
+
default:
|
|
1214
|
+
return null;
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
function buildFilterableFields(schema) {
|
|
1218
|
+
const def = schema._def;
|
|
1219
|
+
const typeName = def.typeName;
|
|
1220
|
+
if (typeName === "ZodOptional" || typeName === "ZodNullable" || typeName === "ZodDefault") {
|
|
1221
|
+
return buildFilterableFields(def.innerType);
|
|
1222
|
+
}
|
|
1223
|
+
if (typeName === "ZodEffects") {
|
|
1224
|
+
return buildFilterableFields(def.schema);
|
|
1225
|
+
}
|
|
1226
|
+
if (typeName !== "ZodObject") return {};
|
|
1227
|
+
const shape = def.shape();
|
|
1228
|
+
const result = {};
|
|
1229
|
+
for (const [key, fieldSchema] of Object.entries(shape)) {
|
|
1230
|
+
const leafType = getSchemaLeafType(fieldSchema);
|
|
1231
|
+
if (leafType === "string") {
|
|
1232
|
+
result[key] = { type: "string", operators: STRING_OPERATORS };
|
|
1233
|
+
} else if (leafType === "number") {
|
|
1234
|
+
result[key] = { type: "number", operators: NUMBER_OPERATORS };
|
|
1235
|
+
} else if (leafType === "boolean") {
|
|
1236
|
+
result[key] = { type: "boolean", operators: BOOLEAN_OPERATORS };
|
|
1237
|
+
} else if (leafType === "date") {
|
|
1238
|
+
result[key] = { type: "date", operators: DATE_OPERATORS };
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
return result;
|
|
1242
|
+
}
|
|
1243
|
+
function findEntityCaseInsensitive(entities, lowercasedName) {
|
|
1244
|
+
for (const [key, schema] of Object.entries(entities)) {
|
|
1245
|
+
if (key.toLowerCase() === lowercasedName) return [key, schema];
|
|
1246
|
+
}
|
|
1247
|
+
return void 0;
|
|
1248
|
+
}
|
|
1249
|
+
function walkEndpointTree(tree, pathParts, result) {
|
|
1250
|
+
for (const [key, value] of Object.entries(tree)) {
|
|
1251
|
+
const current = [...pathParts, key];
|
|
1252
|
+
if (typeof value === "function") {
|
|
1253
|
+
result.push(current.join("."));
|
|
1254
|
+
} else if (value !== null && typeof value === "object") {
|
|
1255
|
+
walkEndpointTree(value, current, result);
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
function isWebhookLeaf(value) {
|
|
1260
|
+
return value !== null && typeof value === "object" && "match" in value && "handler" in value && typeof value.match === "function" && typeof value.handler === "function";
|
|
1261
|
+
}
|
|
1262
|
+
function walkWebhookTree(tree, pathParts, result) {
|
|
1263
|
+
for (const [key, value] of Object.entries(tree)) {
|
|
1264
|
+
const current = [...pathParts, key];
|
|
1265
|
+
if (isWebhookLeaf(value)) {
|
|
1266
|
+
result.push(current.join("."));
|
|
1267
|
+
} else if (value !== null && typeof value === "object") {
|
|
1268
|
+
walkWebhookTree(value, current, result);
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
function resolveWebhookPathOriginalCase(tree, normalizedParts) {
|
|
1273
|
+
if (normalizedParts.length === 0) return null;
|
|
1274
|
+
const [head, ...tail] = normalizedParts;
|
|
1275
|
+
const entry = Object.entries(tree).find(([k]) => k.toLowerCase() === head);
|
|
1276
|
+
if (!entry) return null;
|
|
1277
|
+
const [originalKey, value] = entry;
|
|
1278
|
+
if (tail.length === 0) {
|
|
1279
|
+
return isWebhookLeaf(value) ? [originalKey] : null;
|
|
1280
|
+
}
|
|
1281
|
+
if (value !== null && typeof value === "object" && !isWebhookLeaf(value)) {
|
|
1282
|
+
const rest = resolveWebhookPathOriginalCase(
|
|
1283
|
+
value,
|
|
1284
|
+
tail
|
|
1285
|
+
);
|
|
1286
|
+
if (rest !== null) return [originalKey, ...rest];
|
|
1287
|
+
}
|
|
1288
|
+
return null;
|
|
1289
|
+
}
|
|
1290
|
+
function buildWebhookUsageExample(pluginId, pathParts, responseSchema) {
|
|
1291
|
+
const lines = [];
|
|
1292
|
+
lines.push(`${pluginId}({`);
|
|
1293
|
+
lines.push(` webhookHooks: {`);
|
|
1294
|
+
for (let i = 0; i < pathParts.length; i++) {
|
|
1295
|
+
const indent = " ".repeat(i + 2);
|
|
1296
|
+
lines.push(`${indent}${pathParts[i]}: {`);
|
|
1297
|
+
}
|
|
1298
|
+
const hookIndent = " ".repeat(pathParts.length + 2);
|
|
1299
|
+
const bodyIndent = hookIndent + " ";
|
|
1300
|
+
lines.push(`${hookIndent}before(ctx, args) {`);
|
|
1301
|
+
lines.push(`${bodyIndent}return { ctx, args };`);
|
|
1302
|
+
lines.push(`${hookIndent}},`);
|
|
1303
|
+
lines.push(`${hookIndent}after(ctx, response) {`);
|
|
1304
|
+
if (responseSchema !== null) {
|
|
1305
|
+
const json = JSON.stringify(responseSchema, null, 2);
|
|
1306
|
+
const commentLines = json.split("\n").map(
|
|
1307
|
+
(l, i) => i === 0 ? `${bodyIndent}// response.data: ${l}` : `${bodyIndent}// ${l}`
|
|
1308
|
+
);
|
|
1309
|
+
lines.push(...commentLines);
|
|
1310
|
+
} else {
|
|
1311
|
+
lines.push(
|
|
1312
|
+
`${bodyIndent}// response.data: unknown (register webhookSchemas to see the type)`
|
|
1313
|
+
);
|
|
1314
|
+
}
|
|
1315
|
+
lines.push(`${hookIndent}},`);
|
|
1316
|
+
for (let i = pathParts.length - 1; i >= 0; i--) {
|
|
1317
|
+
const indent = " ".repeat(i + 2);
|
|
1318
|
+
lines.push(`${indent}},`);
|
|
1319
|
+
}
|
|
1320
|
+
lines.push(` },`);
|
|
1321
|
+
lines.push(`})`);
|
|
1322
|
+
return lines.join("\n");
|
|
1323
|
+
}
|
|
1324
|
+
var KNOWN_PLUGIN_IDS = new Set(BaseProviders);
|
|
1325
|
+
function listOperations(plugins, options) {
|
|
1326
|
+
const type = options?.type ?? "api";
|
|
1327
|
+
const pluginId = options?.plugin;
|
|
1328
|
+
if (pluginId !== void 0) {
|
|
1329
|
+
const found = plugins.find((p) => p.id === pluginId);
|
|
1330
|
+
if (!found) {
|
|
1331
|
+
if (KNOWN_PLUGIN_IDS.has(pluginId)) {
|
|
1332
|
+
return `This plugin (${pluginId}) is not configured. Please add it to the Corsair instance to see its associated methods.`;
|
|
1333
|
+
}
|
|
1334
|
+
return listOperations(plugins);
|
|
1335
|
+
}
|
|
1336
|
+
if (type === "webhooks") {
|
|
1337
|
+
if (!found.webhooks) return [];
|
|
1338
|
+
const paths2 = [];
|
|
1339
|
+
walkWebhookTree(found.webhooks, [], paths2);
|
|
1340
|
+
return paths2.map((path) => `${found.id}.webhooks.${path}`);
|
|
1341
|
+
}
|
|
1342
|
+
if (type === "db") {
|
|
1343
|
+
const entities = found.schema?.entities;
|
|
1344
|
+
if (!entities) return [];
|
|
1345
|
+
return Object.keys(entities).map(
|
|
1346
|
+
(entityName) => `${found.id}.db.${entityName}.search`
|
|
1347
|
+
);
|
|
1348
|
+
}
|
|
1349
|
+
if (!found.endpoints) return [];
|
|
1350
|
+
const paths = [];
|
|
1351
|
+
walkEndpointTree(found.endpoints, [], paths);
|
|
1352
|
+
return paths.map((path) => `${found.id}.api.${path.toLowerCase()}`);
|
|
1353
|
+
}
|
|
1354
|
+
const result = {};
|
|
1355
|
+
if (type === "webhooks") {
|
|
1356
|
+
for (const p of plugins) {
|
|
1357
|
+
if (!p.webhooks) continue;
|
|
1358
|
+
const paths = [];
|
|
1359
|
+
walkWebhookTree(p.webhooks, [], paths);
|
|
1360
|
+
result[p.id] = paths.map((path) => `${p.id}.webhooks.${path}`);
|
|
1361
|
+
}
|
|
1362
|
+
} else if (type === "db") {
|
|
1363
|
+
for (const p of plugins) {
|
|
1364
|
+
const entities = p.schema?.entities;
|
|
1365
|
+
if (!entities) continue;
|
|
1366
|
+
result[p.id] = Object.keys(entities).map(
|
|
1367
|
+
(entityName) => `${p.id}.db.${entityName}.search`
|
|
1368
|
+
);
|
|
1369
|
+
}
|
|
1370
|
+
} else {
|
|
1371
|
+
for (const p of plugins) {
|
|
1372
|
+
if (!p.endpoints) continue;
|
|
1373
|
+
const paths = [];
|
|
1374
|
+
walkEndpointTree(p.endpoints, [], paths);
|
|
1375
|
+
result[p.id] = paths.map((path) => `${p.id}.api.${path.toLowerCase()}`);
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
return result;
|
|
1379
|
+
}
|
|
1380
|
+
function findEndpointCaseInsensitive(record, lowercasedPath) {
|
|
1381
|
+
if (!record) return void 0;
|
|
1382
|
+
for (const [key, value] of Object.entries(record)) {
|
|
1383
|
+
if (key.toLowerCase() === lowercasedPath) return value;
|
|
1384
|
+
}
|
|
1385
|
+
return void 0;
|
|
1386
|
+
}
|
|
1387
|
+
function getSchema(plugins, path) {
|
|
1388
|
+
const normalised = path.toLowerCase();
|
|
1389
|
+
const dotIndex = normalised.indexOf(".");
|
|
1390
|
+
if (dotIndex !== -1) {
|
|
1391
|
+
const pluginId = normalised.slice(0, dotIndex);
|
|
1392
|
+
const remainder = normalised.slice(dotIndex + 1);
|
|
1393
|
+
const plugin = plugins.find((p) => p.id === pluginId);
|
|
1394
|
+
if (plugin) {
|
|
1395
|
+
if (remainder.startsWith("db.")) {
|
|
1396
|
+
const dbPath = remainder.slice(3);
|
|
1397
|
+
const lastDot = dbPath.lastIndexOf(".");
|
|
1398
|
+
if (lastDot !== -1) {
|
|
1399
|
+
const entityNameLower = dbPath.slice(0, lastDot);
|
|
1400
|
+
const method = dbPath.slice(lastDot + 1);
|
|
1401
|
+
const entities = plugin.schema?.entities;
|
|
1402
|
+
if (method === "search" && entities) {
|
|
1403
|
+
const entry = findEntityCaseInsensitive(entities, entityNameLower);
|
|
1404
|
+
if (entry) {
|
|
1405
|
+
const [entityName, entitySchema] = entry;
|
|
1406
|
+
return {
|
|
1407
|
+
description: `Search ${pluginId} ${entityName} stored in the local database. Returns an array of matching records. Pass limit and offset (numbers) for pagination.`,
|
|
1408
|
+
filters: {
|
|
1409
|
+
entity_id: {
|
|
1410
|
+
type: "string",
|
|
1411
|
+
operators: STRING_OPERATORS
|
|
1412
|
+
},
|
|
1413
|
+
data: buildFilterableFields(entitySchema)
|
|
1414
|
+
}
|
|
1415
|
+
};
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
return {
|
|
1420
|
+
availableMethods: listOperations(plugins, {
|
|
1421
|
+
type: "db"
|
|
1422
|
+
})
|
|
1423
|
+
};
|
|
1424
|
+
}
|
|
1425
|
+
if (remainder.startsWith("webhooks.")) {
|
|
1426
|
+
const webhookPathNormalised = remainder.slice(9);
|
|
1427
|
+
if (plugin.webhooks) {
|
|
1428
|
+
const originalPathParts = resolveWebhookPathOriginalCase(
|
|
1429
|
+
plugin.webhooks,
|
|
1430
|
+
webhookPathNormalised.split(".")
|
|
1431
|
+
);
|
|
1432
|
+
if (originalPathParts !== null) {
|
|
1433
|
+
const originalPath = originalPathParts.join(".");
|
|
1434
|
+
const schemas2 = findEndpointCaseInsensitive(
|
|
1435
|
+
plugin.webhookSchemas,
|
|
1436
|
+
originalPath.toLowerCase()
|
|
1437
|
+
);
|
|
1438
|
+
const responseSchema = schemas2?.response ? zodToJsonSchema(schemas2.response) : null;
|
|
1439
|
+
return {
|
|
1440
|
+
description: schemas2?.description,
|
|
1441
|
+
payload: schemas2?.payload ? zodToJsonSchema(schemas2.payload) : void 0,
|
|
1442
|
+
response: responseSchema ?? void 0,
|
|
1443
|
+
usage: buildWebhookUsageExample(
|
|
1444
|
+
pluginId,
|
|
1445
|
+
originalPathParts,
|
|
1446
|
+
responseSchema
|
|
1447
|
+
)
|
|
1448
|
+
};
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
return {
|
|
1452
|
+
availableWebhooks: listOperations(plugins, {
|
|
1453
|
+
type: "webhooks"
|
|
1454
|
+
})
|
|
1455
|
+
};
|
|
1456
|
+
}
|
|
1457
|
+
let endpointPath = remainder;
|
|
1458
|
+
if (endpointPath.startsWith("api.")) {
|
|
1459
|
+
endpointPath = endpointPath.slice(4);
|
|
1460
|
+
}
|
|
1461
|
+
const meta = findEndpointCaseInsensitive(
|
|
1462
|
+
plugin.endpointMeta,
|
|
1463
|
+
endpointPath
|
|
1464
|
+
);
|
|
1465
|
+
const schemas = findEndpointCaseInsensitive(
|
|
1466
|
+
plugin.endpointSchemas,
|
|
1467
|
+
endpointPath
|
|
1468
|
+
);
|
|
1469
|
+
if (meta || schemas) {
|
|
1470
|
+
return {
|
|
1471
|
+
description: meta?.description,
|
|
1472
|
+
riskLevel: meta?.riskLevel,
|
|
1473
|
+
irreversible: meta?.irreversible,
|
|
1474
|
+
input: schemas?.input ? zodToJsonSchema(schemas.input) : void 0,
|
|
1475
|
+
output: schemas?.output ? zodToJsonSchema(schemas.output) : void 0
|
|
1476
|
+
};
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
return {
|
|
1481
|
+
availableMethods: listOperations(plugins)
|
|
1482
|
+
};
|
|
1483
|
+
}
|
|
1484
|
+
function buildInspectMethods(plugins) {
|
|
1485
|
+
return {
|
|
1486
|
+
list_operations(options) {
|
|
1487
|
+
return listOperations(plugins, options);
|
|
1488
|
+
},
|
|
1489
|
+
get_schema(path) {
|
|
1490
|
+
return getSchema(plugins, path);
|
|
1491
|
+
}
|
|
1492
|
+
};
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
// core/webhooks/bind.ts
|
|
1496
|
+
function isWebhook(value) {
|
|
1497
|
+
return value !== null && typeof value === "object" && "match" in value && "handler" in value && typeof value.match === "function" && typeof value.handler === "function";
|
|
1498
|
+
}
|
|
1499
|
+
function bindWebhooksRecursively({
|
|
1500
|
+
webhooks,
|
|
1501
|
+
hooks,
|
|
1502
|
+
ctx,
|
|
1503
|
+
webhooksTree,
|
|
1504
|
+
keyBuilder
|
|
1505
|
+
}) {
|
|
1506
|
+
for (const [key, value] of Object.entries(webhooks)) {
|
|
1507
|
+
const nodeHooks = hooks?.[key];
|
|
1508
|
+
if (isWebhook(value)) {
|
|
1509
|
+
const webhookHooks = nodeHooks;
|
|
1510
|
+
const boundHandler = async (request) => {
|
|
1511
|
+
const call = (callCtx, callRequest) => value.handler(callCtx, callRequest);
|
|
1512
|
+
const key2 = keyBuilder ? await keyBuilder(ctx, "webhook") : void 0;
|
|
1513
|
+
if (!webhookHooks?.before && !webhookHooks?.after) {
|
|
1514
|
+
return call({ ...ctx, key: key2 }, request);
|
|
1515
|
+
}
|
|
1516
|
+
return (async () => {
|
|
1517
|
+
const ctxWithKey = { ...ctx, key: key2 };
|
|
1518
|
+
const beforeResult = webhookHooks.before ? await webhookHooks.before(ctxWithKey, request) : {
|
|
1519
|
+
ctx: ctxWithKey,
|
|
1520
|
+
args: request,
|
|
1521
|
+
continue: true,
|
|
1522
|
+
passToAfter: void 0
|
|
1523
|
+
};
|
|
1524
|
+
if (beforeResult.continue === false) return;
|
|
1525
|
+
const res = await call(beforeResult.ctx, beforeResult.args);
|
|
1526
|
+
if (res?.success === true) {
|
|
1527
|
+
await webhookHooks.after?.(
|
|
1528
|
+
beforeResult.ctx,
|
|
1529
|
+
res,
|
|
1530
|
+
beforeResult.passToAfter
|
|
1531
|
+
);
|
|
1532
|
+
}
|
|
1533
|
+
return res;
|
|
1534
|
+
})();
|
|
1535
|
+
};
|
|
1536
|
+
webhooksTree[key] = {
|
|
1537
|
+
match: value.match,
|
|
1538
|
+
handler: boundHandler
|
|
1539
|
+
};
|
|
1540
|
+
} else if (value && typeof value === "object") {
|
|
1541
|
+
const nestedWebhooksTree = {};
|
|
1542
|
+
bindWebhooksRecursively({
|
|
1543
|
+
webhooks: value,
|
|
1544
|
+
hooks: nodeHooks,
|
|
1545
|
+
ctx,
|
|
1546
|
+
webhooksTree: nestedWebhooksTree,
|
|
1547
|
+
keyBuilder
|
|
1548
|
+
});
|
|
1549
|
+
webhooksTree[key] = nestedWebhooksTree;
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
// core/client/index.ts
|
|
1555
|
+
function createAccountIdResolver(database, integrationName, tenantId) {
|
|
1556
|
+
let cachedAccountId = null;
|
|
1557
|
+
return async () => {
|
|
1558
|
+
if (cachedAccountId) return cachedAccountId;
|
|
1559
|
+
if (!database) {
|
|
1560
|
+
throw new Error("Database not configured");
|
|
1561
|
+
}
|
|
1562
|
+
const integration = await database.db.selectFrom("corsair_integrations").selectAll().where("name", "=", integrationName).executeTakeFirst();
|
|
1563
|
+
if (!integration) {
|
|
1564
|
+
throw new Error(
|
|
1565
|
+
`Integration "${integrationName}" not found. Make sure to create the integration first.`
|
|
1566
|
+
);
|
|
1567
|
+
}
|
|
1568
|
+
const account = await database.db.selectFrom("corsair_accounts").selectAll().where("tenant_id", "=", tenantId).where("integration_id", "=", integration.id).executeTakeFirst();
|
|
1569
|
+
if (!account) {
|
|
1570
|
+
throw new Error(
|
|
1571
|
+
`Account not found for tenant "${tenantId}" and integration "${integrationName}". Make sure to create the account first.`
|
|
1572
|
+
);
|
|
1573
|
+
}
|
|
1574
|
+
cachedAccountId = account.id;
|
|
1575
|
+
return cachedAccountId;
|
|
1576
|
+
};
|
|
1577
|
+
}
|
|
1578
|
+
function createEntityClient(database, getAccountId, entityTypeName, version, dataSchema) {
|
|
1579
|
+
if (database) {
|
|
1580
|
+
return createKyselyEntityClient(
|
|
1581
|
+
database.db,
|
|
1582
|
+
getAccountId,
|
|
1583
|
+
entityTypeName,
|
|
1584
|
+
version,
|
|
1585
|
+
dataSchema
|
|
1586
|
+
);
|
|
1587
|
+
}
|
|
1588
|
+
return {
|
|
1589
|
+
findByEntityId: async () => null,
|
|
1590
|
+
findById: async () => null,
|
|
1591
|
+
findManyByEntityIds: async () => [],
|
|
1592
|
+
list: async () => [],
|
|
1593
|
+
search: async () => [],
|
|
1594
|
+
upsertByEntityId: async () => {
|
|
1595
|
+
throw new Error("Database not configured");
|
|
1596
|
+
},
|
|
1597
|
+
deleteById: async () => false,
|
|
1598
|
+
deleteByEntityId: async () => false,
|
|
1599
|
+
count: async () => 0
|
|
1600
|
+
};
|
|
1601
|
+
}
|
|
1602
|
+
function buildCorsairClient(plugins, options) {
|
|
1603
|
+
const { database, tenantId, kek, rootErrorHandlers, approvalConfig } = options;
|
|
1604
|
+
const apiUnsafe = {};
|
|
1605
|
+
const pluginEntitiesUnsafe = {};
|
|
1606
|
+
for (const plugin of plugins) {
|
|
1607
|
+
apiUnsafe[plugin.id] = {};
|
|
1608
|
+
pluginEntitiesUnsafe[plugin.id] = {};
|
|
1609
|
+
}
|
|
1610
|
+
for (const plugin of plugins) {
|
|
1611
|
+
const schema = plugin.schema;
|
|
1612
|
+
const effectiveTenantId = tenantId ?? "default";
|
|
1613
|
+
const getAccountId = createAccountIdResolver(
|
|
1614
|
+
database,
|
|
1615
|
+
plugin.id,
|
|
1616
|
+
effectiveTenantId
|
|
1617
|
+
);
|
|
1618
|
+
if (schema?.entities) {
|
|
1619
|
+
const dbClients = {};
|
|
1620
|
+
for (const [entityTypeName, dataSchema] of Object.entries(
|
|
1621
|
+
schema.entities
|
|
1622
|
+
)) {
|
|
1623
|
+
const entityClient = database ? createKyselyEntityClient(
|
|
1624
|
+
database.db,
|
|
1625
|
+
getAccountId,
|
|
1626
|
+
entityTypeName,
|
|
1627
|
+
schema.version,
|
|
1628
|
+
dataSchema
|
|
1629
|
+
) : createEntityClient(
|
|
1630
|
+
void 0,
|
|
1631
|
+
getAccountId,
|
|
1632
|
+
entityTypeName,
|
|
1633
|
+
schema.version,
|
|
1634
|
+
dataSchema
|
|
1635
|
+
);
|
|
1636
|
+
dbClients[entityTypeName] = entityClient;
|
|
1637
|
+
}
|
|
1638
|
+
pluginEntitiesUnsafe[plugin.id].db = dbClients;
|
|
1639
|
+
apiUnsafe[plugin.id].db = dbClients;
|
|
1640
|
+
}
|
|
1641
|
+
const pluginOptions = plugin.options;
|
|
1642
|
+
const authConfig = plugin.authConfig;
|
|
1643
|
+
let accountKeyManager;
|
|
1644
|
+
if (database && kek && pluginOptions?.authType) {
|
|
1645
|
+
const extraAccountFields = authConfig?.[pluginOptions.authType]?.account ?? [];
|
|
1646
|
+
accountKeyManager = createAccountKeyManager({
|
|
1647
|
+
authType: pluginOptions.authType,
|
|
1648
|
+
integrationName: plugin.id,
|
|
1649
|
+
tenantId: effectiveTenantId,
|
|
1650
|
+
kek,
|
|
1651
|
+
database,
|
|
1652
|
+
extraAccountFields
|
|
1653
|
+
});
|
|
1654
|
+
apiUnsafe[plugin.id].keys = accountKeyManager;
|
|
1655
|
+
}
|
|
1656
|
+
const ctxForPlugin = {
|
|
1657
|
+
database,
|
|
1658
|
+
db: pluginEntitiesUnsafe[plugin.id]?.db ?? {},
|
|
1659
|
+
$getAccountId: getAccountId,
|
|
1660
|
+
...plugin.options ? { options: plugin.options } : {},
|
|
1661
|
+
// Include keys manager and authType in context so keyBuilder can access and narrow types
|
|
1662
|
+
...accountKeyManager ? { keys: accountKeyManager, authType: pluginOptions?.authType } : {},
|
|
1663
|
+
// Include tenantId in context so it's available in webhook hooks
|
|
1664
|
+
...tenantId ? { tenantId } : {}
|
|
1665
|
+
};
|
|
1666
|
+
const endpoints = plugin.endpoints ?? {};
|
|
1667
|
+
const hooks = plugin.hooks;
|
|
1668
|
+
const allErrorHandlers = {
|
|
1669
|
+
...rootErrorHandlers,
|
|
1670
|
+
...plugin.errorHandlers
|
|
1671
|
+
};
|
|
1672
|
+
const boundTree = {};
|
|
1673
|
+
const pluginPermsConfig = plugin.options?.permissions;
|
|
1674
|
+
bindEndpointsRecursively({
|
|
1675
|
+
endpoints,
|
|
1676
|
+
hooks,
|
|
1677
|
+
ctx: ctxForPlugin,
|
|
1678
|
+
tree: boundTree,
|
|
1679
|
+
pluginId: plugin.id,
|
|
1680
|
+
errorHandlers: allErrorHandlers,
|
|
1681
|
+
currentPath: [],
|
|
1682
|
+
keyBuilder: plugin.keyBuilder,
|
|
1683
|
+
permissionsConfig: pluginPermsConfig,
|
|
1684
|
+
// endpointMeta is typed with plugin-specific literal keys — cast to runtime Record
|
|
1685
|
+
endpointMeta: plugin.endpointMeta,
|
|
1686
|
+
database,
|
|
1687
|
+
approvalConfig,
|
|
1688
|
+
tenantId
|
|
1689
|
+
});
|
|
1690
|
+
if (Object.keys(boundTree).length > 0) {
|
|
1691
|
+
apiUnsafe[plugin.id].api = boundTree;
|
|
1692
|
+
}
|
|
1693
|
+
ctxForPlugin.endpoints = boundTree;
|
|
1694
|
+
const webhooks = plugin.webhooks ?? {};
|
|
1695
|
+
const webhookHooks = plugin.webhookHooks;
|
|
1696
|
+
if (Object.keys(webhooks).length > 0) {
|
|
1697
|
+
const boundWebhooks = {};
|
|
1698
|
+
bindWebhooksRecursively({
|
|
1699
|
+
webhooks,
|
|
1700
|
+
hooks: webhookHooks,
|
|
1701
|
+
ctx: ctxForPlugin,
|
|
1702
|
+
webhooksTree: boundWebhooks,
|
|
1703
|
+
keyBuilder: plugin.keyBuilder
|
|
1704
|
+
});
|
|
1705
|
+
apiUnsafe[plugin.id].webhooks = boundWebhooks;
|
|
1706
|
+
if (plugin.pluginWebhookMatcher) {
|
|
1707
|
+
apiUnsafe[plugin.id].pluginWebhookMatcher = plugin.pluginWebhookMatcher;
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
const api = apiUnsafe;
|
|
1712
|
+
const inspect = buildInspectMethods(plugins);
|
|
1713
|
+
return {
|
|
1714
|
+
...api,
|
|
1715
|
+
...inspect
|
|
1716
|
+
};
|
|
1717
|
+
}
|
|
1718
|
+
function buildIntegrationKeys(plugins, database, kek) {
|
|
1719
|
+
const keysUnsafe = {};
|
|
1720
|
+
for (const plugin of plugins) {
|
|
1721
|
+
const pluginOptions = plugin.options;
|
|
1722
|
+
const authConfig = plugin.authConfig;
|
|
1723
|
+
if (pluginOptions?.authType) {
|
|
1724
|
+
const extraIntegrationFields = authConfig?.[pluginOptions.authType]?.integration ?? [];
|
|
1725
|
+
const integrationKeyManager = createIntegrationKeyManager({
|
|
1726
|
+
authType: pluginOptions.authType,
|
|
1727
|
+
integrationName: plugin.id,
|
|
1728
|
+
kek,
|
|
1729
|
+
database,
|
|
1730
|
+
extraIntegrationFields
|
|
1731
|
+
});
|
|
1732
|
+
keysUnsafe[plugin.id] = integrationKeyManager;
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
return keysUnsafe;
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
// core/index.ts
|
|
1739
|
+
var CORSAIR_INTERNAL = Symbol.for("corsair:internal");
|
|
1740
|
+
function createCorsair(config) {
|
|
1741
|
+
const resolvedDatabase = config.database ? createCorsairDatabase(config.database) : void 0;
|
|
1742
|
+
const integrationKeys = resolvedDatabase && config.kek ? buildIntegrationKeys(config.plugins, resolvedDatabase, config.kek) : createMissingConfigProxy(
|
|
1743
|
+
!!resolvedDatabase,
|
|
1744
|
+
!!config.kek
|
|
1745
|
+
);
|
|
1746
|
+
const internalConfig = {
|
|
1747
|
+
plugins: config.plugins,
|
|
1748
|
+
database: resolvedDatabase,
|
|
1749
|
+
kek: config.kek,
|
|
1750
|
+
multiTenancy: !!config.multiTenancy,
|
|
1751
|
+
approval: config.approval
|
|
1752
|
+
};
|
|
1753
|
+
const permissions = buildPermissionsNamespace(resolvedDatabase);
|
|
1754
|
+
if (config.multiTenancy) {
|
|
1755
|
+
return Object.assign(
|
|
1756
|
+
{
|
|
1757
|
+
withTenant: (tenantId) => {
|
|
1758
|
+
if (!tenantId) {
|
|
1759
|
+
throw new Error(
|
|
1760
|
+
"corsair.withTenant(tenantId): tenantId must be a non-empty string"
|
|
1761
|
+
);
|
|
1762
|
+
}
|
|
1763
|
+
return buildCorsairClient(config.plugins, {
|
|
1764
|
+
database: resolvedDatabase,
|
|
1765
|
+
tenantId,
|
|
1766
|
+
kek: config.kek,
|
|
1767
|
+
rootErrorHandlers: config.errorHandlers,
|
|
1768
|
+
approvalConfig: config.approval
|
|
1769
|
+
});
|
|
1770
|
+
},
|
|
1771
|
+
keys: integrationKeys,
|
|
1772
|
+
permissions,
|
|
1773
|
+
...buildInspectMethods(config.plugins)
|
|
1774
|
+
},
|
|
1775
|
+
{ [CORSAIR_INTERNAL]: internalConfig }
|
|
1776
|
+
);
|
|
1777
|
+
}
|
|
1778
|
+
const client = buildCorsairClient(config.plugins, {
|
|
1779
|
+
database: resolvedDatabase,
|
|
1780
|
+
tenantId: void 0,
|
|
1781
|
+
kek: config.kek,
|
|
1782
|
+
rootErrorHandlers: config.errorHandlers,
|
|
1783
|
+
approvalConfig: config.approval
|
|
1784
|
+
});
|
|
1785
|
+
return Object.assign({}, client, {
|
|
1786
|
+
keys: integrationKeys,
|
|
1787
|
+
permissions,
|
|
1788
|
+
[CORSAIR_INTERNAL]: internalConfig
|
|
1789
|
+
});
|
|
1790
|
+
}
|
|
1791
|
+
export {
|
|
1792
|
+
BASE_AUTH_FIELDS,
|
|
1793
|
+
CORSAIR_INTERNAL,
|
|
1794
|
+
createAccountKeyManager,
|
|
1795
|
+
createCorsair,
|
|
1796
|
+
createIntegrationKeyManager,
|
|
1797
|
+
decryptConfig,
|
|
1798
|
+
decryptDEK,
|
|
1799
|
+
decryptWithDEK,
|
|
1800
|
+
encryptConfig,
|
|
1801
|
+
encryptDEK,
|
|
1802
|
+
encryptWithDEK,
|
|
1803
|
+
generateDEK,
|
|
1804
|
+
initializeAccountDEK,
|
|
1805
|
+
initializeIntegrationDEK,
|
|
1806
|
+
reEncryptConfig
|
|
1807
|
+
};
|