@mycreat-labs/integrations-sdk 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +54 -2
- package/dist/adapters/bullmq.cjs +110 -0
- package/dist/adapters/bullmq.cjs.map +1 -0
- package/dist/adapters/bullmq.d.cts +11 -0
- package/dist/adapters/bullmq.d.ts +11 -0
- package/dist/adapters/bullmq.js +85 -0
- package/dist/adapters/bullmq.js.map +1 -0
- package/dist/adapters/postgres.cjs +396 -0
- package/dist/adapters/postgres.cjs.map +1 -0
- package/dist/adapters/postgres.d.cts +51 -0
- package/dist/adapters/postgres.d.ts +51 -0
- package/dist/adapters/postgres.js +368 -0
- package/dist/adapters/postgres.js.map +1 -0
- package/dist/adapters/prisma.cjs +414 -0
- package/dist/adapters/prisma.cjs.map +1 -0
- package/dist/adapters/prisma.d.cts +15 -0
- package/dist/adapters/prisma.d.ts +15 -0
- package/dist/adapters/prisma.js +386 -0
- package/dist/adapters/prisma.js.map +1 -0
- package/dist/adapters/redis.cjs +125 -0
- package/dist/adapters/redis.cjs.map +1 -0
- package/dist/adapters/redis.d.cts +20 -0
- package/dist/adapters/redis.d.ts +20 -0
- package/dist/adapters/redis.js +98 -0
- package/dist/adapters/redis.js.map +1 -0
- package/dist/errors-3hDEtzuZ.d.cts +11 -0
- package/dist/errors-3hDEtzuZ.d.ts +11 -0
- package/dist/index.cjs +1328 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +128 -0
- package/dist/index.d.ts +128 -0
- package/dist/index.js +1297 -0
- package/dist/index.js.map +1 -0
- package/dist/providers/index.cjs +1287 -0
- package/dist/providers/index.cjs.map +1 -0
- package/dist/providers/index.d.cts +144 -0
- package/dist/providers/index.d.ts +144 -0
- package/dist/providers/index.js +1252 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/testing.cjs +275 -0
- package/dist/testing.cjs.map +1 -0
- package/dist/testing.d.cts +20 -0
- package/dist/testing.d.ts +20 -0
- package/dist/testing.js +248 -0
- package/dist/testing.js.map +1 -0
- package/dist/types-Y4-DoJ1A.d.cts +260 -0
- package/dist/types-Y4-DoJ1A.d.ts +260 -0
- package/docs/migration.md +23 -0
- package/docs/security.md +26 -0
- package/docs/verification.md +22 -0
- package/examples/embedded-host.ts +62 -0
- package/migrations/0001_initial.sql +40 -0
- package/package.json +90 -4
- package/schema.prisma +80 -0
|
@@ -0,0 +1,1252 @@
|
|
|
1
|
+
// src/providers/identity.ts
|
|
2
|
+
import {
|
|
3
|
+
createLocalJWKSet,
|
|
4
|
+
importPKCS8,
|
|
5
|
+
jwtVerify,
|
|
6
|
+
SignJWT
|
|
7
|
+
} from "jose";
|
|
8
|
+
import { timingSafeEqual } from "crypto";
|
|
9
|
+
|
|
10
|
+
// src/errors.ts
|
|
11
|
+
var IntegrationError = class extends Error {
|
|
12
|
+
constructor(code, disposition = "permanent_failure", retryAfterMs) {
|
|
13
|
+
super(code);
|
|
14
|
+
this.code = code;
|
|
15
|
+
this.disposition = disposition;
|
|
16
|
+
this.retryAfterMs = retryAfterMs;
|
|
17
|
+
this.name = "IntegrationError";
|
|
18
|
+
}
|
|
19
|
+
code;
|
|
20
|
+
disposition;
|
|
21
|
+
retryAfterMs;
|
|
22
|
+
sdkError = "@mycreat-labs/integrations-sdk/v1";
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// src/providers/http.ts
|
|
26
|
+
var ProviderHttpError = class extends IntegrationError {
|
|
27
|
+
status;
|
|
28
|
+
providerCode;
|
|
29
|
+
definite;
|
|
30
|
+
committedResourceId;
|
|
31
|
+
committedChildIds;
|
|
32
|
+
constructor(code, disposition, options = {}) {
|
|
33
|
+
super(code, disposition, options.retryAfterMs);
|
|
34
|
+
this.name = "ProviderHttpError";
|
|
35
|
+
this.status = options.status ?? null;
|
|
36
|
+
this.providerCode = options.providerCode;
|
|
37
|
+
this.definite = options.definite ?? false;
|
|
38
|
+
this.committedResourceId = options.committedResourceId;
|
|
39
|
+
this.committedChildIds = options.committedChildIds;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
function record(value) {
|
|
43
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
44
|
+
throw new IntegrationError("invalid_response");
|
|
45
|
+
return value;
|
|
46
|
+
}
|
|
47
|
+
function text(value, max = 4096) {
|
|
48
|
+
if (typeof value !== "string" || value.length === 0 || value.length > max)
|
|
49
|
+
throw new IntegrationError("invalid_response");
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
52
|
+
function optionalText(value, max = 4096) {
|
|
53
|
+
return value === void 0 || value === null ? void 0 : text(value, max);
|
|
54
|
+
}
|
|
55
|
+
function inputRecord(value, keys2) {
|
|
56
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
57
|
+
throw new IntegrationError("invalid_input");
|
|
58
|
+
const input = value;
|
|
59
|
+
if (Object.keys(input).some((key) => !keys2.includes(key)))
|
|
60
|
+
throw new IntegrationError("invalid_input");
|
|
61
|
+
return input;
|
|
62
|
+
}
|
|
63
|
+
function inputText(value, maximum, required = true) {
|
|
64
|
+
if (value === void 0 && !required) return void 0;
|
|
65
|
+
if (typeof value !== "string" || required && !value.trim() || value.length > maximum)
|
|
66
|
+
throw new IntegrationError("invalid_input");
|
|
67
|
+
return value;
|
|
68
|
+
}
|
|
69
|
+
function webUrl(value) {
|
|
70
|
+
const result = inputText(value, 1e4);
|
|
71
|
+
try {
|
|
72
|
+
const url = new URL(result);
|
|
73
|
+
if (!["https:", "http:"].includes(url.protocol) || url.username || url.password)
|
|
74
|
+
throw new Error();
|
|
75
|
+
} catch {
|
|
76
|
+
throw new IntegrationError("invalid_input");
|
|
77
|
+
}
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
function jsonMetadata(value) {
|
|
81
|
+
if (value === void 0) return void 0;
|
|
82
|
+
const visit = (item, depth) => depth <= 10 && (item === null || typeof item === "string" || typeof item === "boolean" || typeof item === "number" && Number.isFinite(item) || Array.isArray(item) && item.every((child) => visit(child, depth + 1)) || typeof item === "object" && item !== null && Object.getPrototypeOf(item) === Object.prototype && Object.values(item).every((child) => visit(child, depth + 1)));
|
|
83
|
+
if (!visit(value, 0) || JSON.stringify(value).length > 16384)
|
|
84
|
+
throw new IntegrationError("invalid_input");
|
|
85
|
+
return value;
|
|
86
|
+
}
|
|
87
|
+
var safeCodes = /* @__PURE__ */ new Set([
|
|
88
|
+
"invalid_client",
|
|
89
|
+
"unauthorized_client",
|
|
90
|
+
"invalid_grant",
|
|
91
|
+
"bad_authorization_code",
|
|
92
|
+
"invalid_request",
|
|
93
|
+
"invalid_json",
|
|
94
|
+
"validation_error",
|
|
95
|
+
"restricted_resource",
|
|
96
|
+
"unauthorized",
|
|
97
|
+
"object_not_found",
|
|
98
|
+
"rate_limited",
|
|
99
|
+
"conflict_error",
|
|
100
|
+
"internal_server_error",
|
|
101
|
+
"service_unavailable",
|
|
102
|
+
"gateway_timeout",
|
|
103
|
+
"service_overload",
|
|
104
|
+
"row_limit_exceeded",
|
|
105
|
+
"invalid_token",
|
|
106
|
+
"bad_verification_code",
|
|
107
|
+
"incorrect_client_credentials",
|
|
108
|
+
"redirect_uri_mismatch",
|
|
109
|
+
"access_denied"
|
|
110
|
+
]);
|
|
111
|
+
var uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
112
|
+
var isUuid = (value) => typeof value === "string" && uuid.test(value);
|
|
113
|
+
function retryDelay(value) {
|
|
114
|
+
if (!value) return void 0;
|
|
115
|
+
const milliseconds = /^\d+(?:\.\d+)?$/.test(value) ? Number(value) * 1e3 : Date.parse(value) - Date.now();
|
|
116
|
+
return Number.isFinite(milliseconds) ? Math.max(0, Math.min(milliseconds, 864e5)) : void 0;
|
|
117
|
+
}
|
|
118
|
+
function failure(status, body, write, retryAfterMs) {
|
|
119
|
+
const payload = body && typeof body === "object" && !Array.isArray(body) ? body : {};
|
|
120
|
+
const vendorCode = typeof payload.code === "string" ? payload.code : typeof payload.error === "string" ? payload.error : void 0;
|
|
121
|
+
const providerCode = vendorCode && safeCodes.has(vendorCode) ? vendorCode : void 0;
|
|
122
|
+
const extra = payload.additional_data && typeof payload.additional_data === "object" ? payload.additional_data : {};
|
|
123
|
+
const committedResourceId = isUuid(extra.committed_resource_id) ? extra.committed_resource_id : void 0;
|
|
124
|
+
const committedChildIds = Array.isArray(extra.committed_child_ids) && extra.committed_child_ids.length <= 100 && extra.committed_child_ids.every(isUuid) ? extra.committed_child_ids : void 0;
|
|
125
|
+
const definite = status >= 400 && status < 500 && status !== 408 && !committedResourceId && !committedChildIds?.length;
|
|
126
|
+
let code = status === 401 ? "auth_required" : status === 404 ? "resource_unavailable" : status === 403 ? "permission_required" : status === 429 || status === 529 ? "rate_limited" : status >= 500 ? "upstream_unavailable" : "invalid_payload";
|
|
127
|
+
if (providerCode === "invalid_client" || providerCode === "unauthorized_client" || providerCode === "incorrect_client_credentials" || payload.errorMessage === "client_id or client_secret is invalid")
|
|
128
|
+
code = "application_unavailable";
|
|
129
|
+
else if (providerCode === "invalid_grant" || providerCode === "bad_authorization_code" || providerCode === "invalid_token" || providerCode === "bad_verification_code")
|
|
130
|
+
code = "auth_required";
|
|
131
|
+
else if (extra.rate_limit_reason === "public_api_request_blocked")
|
|
132
|
+
code = "access_restricted";
|
|
133
|
+
else if (status === 403 && providerCode === "restricted_resource")
|
|
134
|
+
code = "resource_unavailable";
|
|
135
|
+
else if (status === 403 && typeof payload.message === "string" && /block.*limit|limit.*block/i.test(payload.message))
|
|
136
|
+
code = "workspace_capacity";
|
|
137
|
+
const disposition = code === "auth_required" ? "requires_reauth" : write && !definite ? "outcome_unknown" : code === "rate_limited" || status >= 500 || status === 408 ? "retryable" : "permanent_failure";
|
|
138
|
+
return new ProviderHttpError(code, disposition, {
|
|
139
|
+
status,
|
|
140
|
+
providerCode,
|
|
141
|
+
definite,
|
|
142
|
+
retryAfterMs,
|
|
143
|
+
committedResourceId,
|
|
144
|
+
committedChildIds
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
async function requestJson(url, init, fetcher, options = {}) {
|
|
148
|
+
const write = options.write ?? !["GET", "HEAD"].includes(init.method ?? "GET");
|
|
149
|
+
const signal = init.signal ? AbortSignal.any([init.signal, AbortSignal.timeout(3e4)]) : AbortSignal.timeout(3e4);
|
|
150
|
+
let response;
|
|
151
|
+
try {
|
|
152
|
+
response = await fetcher(url, { ...init, signal, redirect: "error" });
|
|
153
|
+
} catch (error) {
|
|
154
|
+
if (error instanceof Error && "sdkError" in error && error.sdkError === "@mycreat-labs/integrations-sdk/v1" && "code" in error && error.code === "rate_limited" && "definite" in error && error.definite === true) {
|
|
155
|
+
const delay = "retryAfterMs" in error && typeof error.retryAfterMs === "number" && Number.isFinite(error.retryAfterMs) ? Math.max(0, Math.min(error.retryAfterMs, 864e5)) : void 0;
|
|
156
|
+
throw new ProviderHttpError("rate_limited", "retryable", {
|
|
157
|
+
status: 429,
|
|
158
|
+
definite: true,
|
|
159
|
+
retryAfterMs: delay
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
throw new ProviderHttpError(
|
|
163
|
+
"upstream_unavailable",
|
|
164
|
+
write ? "outcome_unknown" : "retryable"
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
if (response.status === 204 && response.ok) return {};
|
|
168
|
+
let body;
|
|
169
|
+
try {
|
|
170
|
+
if (options.requireJson && !response.headers.get("content-type")?.toLowerCase().includes("json"))
|
|
171
|
+
throw new Error();
|
|
172
|
+
if (!response.body) throw new Error();
|
|
173
|
+
const reader = response.body.getReader();
|
|
174
|
+
const chunks = [];
|
|
175
|
+
let size = 0;
|
|
176
|
+
try {
|
|
177
|
+
for (; ; ) {
|
|
178
|
+
const part = await reader.read();
|
|
179
|
+
if (part.done) break;
|
|
180
|
+
size += part.value.length;
|
|
181
|
+
if (size > 2097152) {
|
|
182
|
+
await reader.cancel();
|
|
183
|
+
throw new Error();
|
|
184
|
+
}
|
|
185
|
+
chunks.push(part.value);
|
|
186
|
+
}
|
|
187
|
+
} finally {
|
|
188
|
+
reader.releaseLock();
|
|
189
|
+
}
|
|
190
|
+
body = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
191
|
+
} catch {
|
|
192
|
+
if (!response.ok)
|
|
193
|
+
throw failure(
|
|
194
|
+
response.status,
|
|
195
|
+
{},
|
|
196
|
+
write,
|
|
197
|
+
retryDelay(response.headers.get("retry-after"))
|
|
198
|
+
);
|
|
199
|
+
throw new ProviderHttpError(
|
|
200
|
+
"invalid_response",
|
|
201
|
+
write ? "outcome_unknown" : "retryable",
|
|
202
|
+
{ status: response.status }
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
if (!response.ok)
|
|
206
|
+
throw failure(
|
|
207
|
+
response.status,
|
|
208
|
+
body,
|
|
209
|
+
write,
|
|
210
|
+
retryDelay(response.headers.get("retry-after"))
|
|
211
|
+
);
|
|
212
|
+
if (body && typeof body === "object" && !Array.isArray(body)) {
|
|
213
|
+
const value = body;
|
|
214
|
+
if (typeof value.error === "string" || options.raindrop && value.result === false) {
|
|
215
|
+
const status = typeof value.status === "number" && Number.isInteger(value.status) && value.status >= 400 && value.status < 600 ? value.status : 400;
|
|
216
|
+
throw failure(
|
|
217
|
+
status,
|
|
218
|
+
body,
|
|
219
|
+
write,
|
|
220
|
+
retryDelay(response.headers.get("retry-after"))
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return body;
|
|
225
|
+
}
|
|
226
|
+
function providerPath(path, prefix) {
|
|
227
|
+
if (path.length > 4096 || !/^\/[a-zA-Z0-9_/-]+(?:\?[a-zA-Z0-9_%=&.~-]+)?$/.test(path) || path.includes("//") || path.includes(".."))
|
|
228
|
+
throw new ProviderHttpError("invalid_upstream_path", "permanent_failure", {
|
|
229
|
+
status: 400,
|
|
230
|
+
definite: true
|
|
231
|
+
});
|
|
232
|
+
return path.startsWith(`${prefix}/`) || path === prefix ? path : `${prefix}${path}`;
|
|
233
|
+
}
|
|
234
|
+
function notionRequest(path, options) {
|
|
235
|
+
const normalized = providerPath(path, "/v1");
|
|
236
|
+
if (!options.accessToken && (!options.clientId || !options.clientSecret))
|
|
237
|
+
throw new ProviderHttpError(
|
|
238
|
+
"application_unavailable",
|
|
239
|
+
"permanent_failure",
|
|
240
|
+
{ definite: true }
|
|
241
|
+
);
|
|
242
|
+
const authorization = options.accessToken ? `Bearer ${options.accessToken}` : `Basic ${Buffer.from(`${options.clientId}:${options.clientSecret}`).toString("base64")}`;
|
|
243
|
+
return requestJson(
|
|
244
|
+
`https://api.notion.com${normalized}`,
|
|
245
|
+
{
|
|
246
|
+
method: options.method ?? (options.body === void 0 ? "GET" : "POST"),
|
|
247
|
+
headers: {
|
|
248
|
+
"Notion-Version": options.apiVersion ?? "2026-03-11",
|
|
249
|
+
"Content-Type": "application/json",
|
|
250
|
+
Accept: "application/json",
|
|
251
|
+
Authorization: authorization
|
|
252
|
+
},
|
|
253
|
+
...options.body === void 0 ? {} : { body: JSON.stringify(options.body) },
|
|
254
|
+
...options.signal ? { signal: options.signal } : {}
|
|
255
|
+
},
|
|
256
|
+
options.fetch ?? globalThis.fetch,
|
|
257
|
+
{
|
|
258
|
+
write: normalized !== "/v1/search" && (options.method ?? (options.body === void 0 ? "GET" : "POST")) !== "GET"
|
|
259
|
+
}
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
function raindropRequest(path, options) {
|
|
263
|
+
const normalized = path === "/v1/oauth/access_token" ? path : providerPath(path, "/rest/v1");
|
|
264
|
+
if (normalized !== "/v1/oauth/access_token" && !options.accessToken)
|
|
265
|
+
throw new ProviderHttpError("auth_required", "requires_reauth", {
|
|
266
|
+
status: 401,
|
|
267
|
+
definite: true
|
|
268
|
+
});
|
|
269
|
+
return requestJson(
|
|
270
|
+
`https://api.raindrop.io${normalized}`,
|
|
271
|
+
{
|
|
272
|
+
method: options.method ?? (options.body === void 0 ? "GET" : "POST"),
|
|
273
|
+
headers: {
|
|
274
|
+
"Content-Type": "application/json",
|
|
275
|
+
Accept: "application/json",
|
|
276
|
+
...options.accessToken ? { Authorization: `Bearer ${options.accessToken}` } : {}
|
|
277
|
+
},
|
|
278
|
+
...options.body === void 0 ? {} : { body: JSON.stringify(options.body) },
|
|
279
|
+
...options.signal ? { signal: options.signal } : {}
|
|
280
|
+
},
|
|
281
|
+
options.fetch ?? globalThis.fetch,
|
|
282
|
+
{ raindrop: true, requireJson: true }
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// src/providers/identity.ts
|
|
287
|
+
function configure(config) {
|
|
288
|
+
if (!config.registrationId || !config.clientId || !config.redirectUri || config.identityNamespace === "")
|
|
289
|
+
throw new IntegrationError("invalid_provider_configuration");
|
|
290
|
+
let url;
|
|
291
|
+
try {
|
|
292
|
+
url = new URL(config.redirectUri);
|
|
293
|
+
} catch {
|
|
294
|
+
throw new IntegrationError("invalid_provider_configuration");
|
|
295
|
+
}
|
|
296
|
+
if (url.username || url.password || url.hash || url.protocol !== "https:" && !(url.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname)))
|
|
297
|
+
throw new IntegrationError("invalid_provider_configuration");
|
|
298
|
+
}
|
|
299
|
+
function identityPurpose(purpose) {
|
|
300
|
+
if (purpose !== "identity") throw new IntegrationError("unsupported_purpose");
|
|
301
|
+
}
|
|
302
|
+
function authorizationURL(base, parameters) {
|
|
303
|
+
const url = new URL(base);
|
|
304
|
+
for (const [key, value] of Object.entries(parameters))
|
|
305
|
+
url.searchParams.set(key, value);
|
|
306
|
+
return url.toString();
|
|
307
|
+
}
|
|
308
|
+
var keyCaches = /* @__PURE__ */ new WeakMap();
|
|
309
|
+
var keyLoads = /* @__PURE__ */ new WeakMap();
|
|
310
|
+
async function keys(url, fetcher, force = false) {
|
|
311
|
+
let cache = keyCaches.get(fetcher);
|
|
312
|
+
if (!cache) {
|
|
313
|
+
cache = /* @__PURE__ */ new Map();
|
|
314
|
+
keyCaches.set(fetcher, cache);
|
|
315
|
+
}
|
|
316
|
+
const existing = cache.get(url);
|
|
317
|
+
if (!force && existing && existing.expires > Date.now())
|
|
318
|
+
return existing.resolve;
|
|
319
|
+
if (force && existing && existing.refreshedAt + 3e4 > Date.now())
|
|
320
|
+
return existing.resolve;
|
|
321
|
+
let loads = keyLoads.get(fetcher);
|
|
322
|
+
if (!loads) {
|
|
323
|
+
loads = /* @__PURE__ */ new Map();
|
|
324
|
+
keyLoads.set(fetcher, loads);
|
|
325
|
+
}
|
|
326
|
+
const pending = loads.get(url);
|
|
327
|
+
if (pending) return pending;
|
|
328
|
+
const load = (async () => {
|
|
329
|
+
try {
|
|
330
|
+
const value = record(await requestJson(url, {}, fetcher));
|
|
331
|
+
if (!Array.isArray(value.keys) || value.keys.length > 100)
|
|
332
|
+
throw new Error();
|
|
333
|
+
const resolve = createLocalJWKSet(value);
|
|
334
|
+
const now = Date.now();
|
|
335
|
+
cache.set(url, { expires: now + 3e5, refreshedAt: now, resolve });
|
|
336
|
+
return resolve;
|
|
337
|
+
} catch {
|
|
338
|
+
throw new IntegrationError("identity_unavailable", "retryable");
|
|
339
|
+
}
|
|
340
|
+
})();
|
|
341
|
+
loads.set(url, load);
|
|
342
|
+
try {
|
|
343
|
+
return await load;
|
|
344
|
+
} finally {
|
|
345
|
+
loads.delete(url);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
async function verifyToken(input, config, fetcher, options) {
|
|
349
|
+
if (!input.nonce || input.nonce.length > 1024 || !input.idToken || input.idToken.length > 32768 || !options.audiences.length || options.audiences.some((item) => !item))
|
|
350
|
+
throw new IntegrationError("invalid_identity");
|
|
351
|
+
const google = options.provider === "google";
|
|
352
|
+
const issuer = google ? "https://accounts.google.com" : "https://appleid.apple.com";
|
|
353
|
+
const jwks = google ? "https://www.googleapis.com/oauth2/v3/certs" : "https://appleid.apple.com/auth/keys";
|
|
354
|
+
const verificationOptions = {
|
|
355
|
+
algorithms: ["RS256"],
|
|
356
|
+
issuer: google ? [issuer, "accounts.google.com"] : issuer,
|
|
357
|
+
audience: [...options.audiences],
|
|
358
|
+
requiredClaims: ["sub", "exp", "iat", "nonce"],
|
|
359
|
+
clockTolerance: 30
|
|
360
|
+
};
|
|
361
|
+
let payload;
|
|
362
|
+
try {
|
|
363
|
+
let resolve = await keys(jwks, fetcher);
|
|
364
|
+
try {
|
|
365
|
+
({ payload } = await jwtVerify(
|
|
366
|
+
input.idToken,
|
|
367
|
+
resolve,
|
|
368
|
+
verificationOptions
|
|
369
|
+
));
|
|
370
|
+
} catch (error) {
|
|
371
|
+
if (!(error && typeof error === "object" && "code" in error && error.code === "ERR_JWKS_NO_MATCHING_KEY"))
|
|
372
|
+
throw error;
|
|
373
|
+
resolve = await keys(jwks, fetcher, true);
|
|
374
|
+
({ payload } = await jwtVerify(
|
|
375
|
+
input.idToken,
|
|
376
|
+
resolve,
|
|
377
|
+
verificationOptions
|
|
378
|
+
));
|
|
379
|
+
}
|
|
380
|
+
const expected = Buffer.from(input.nonce);
|
|
381
|
+
const actual = Buffer.from(
|
|
382
|
+
typeof payload.nonce === "string" ? payload.nonce : ""
|
|
383
|
+
);
|
|
384
|
+
if (expected.length !== actual.length || !timingSafeEqual(expected, actual) || typeof payload.iat !== "number" || payload.iat > Date.now() / 1e3 + 30 || typeof payload.sub !== "string" || !payload.sub || payload.sub.length > 255)
|
|
385
|
+
throw new Error();
|
|
386
|
+
if (Array.isArray(payload.aud) && payload.aud.length > 1 && (typeof payload.azp !== "string" || !options.audiences.includes(payload.azp)))
|
|
387
|
+
throw new Error();
|
|
388
|
+
} catch (error) {
|
|
389
|
+
if (error instanceof IntegrationError && (error.disposition === "retryable" || error.disposition === "outcome_unknown"))
|
|
390
|
+
throw new IntegrationError("identity_unavailable", "retryable");
|
|
391
|
+
throw new IntegrationError("invalid_identity");
|
|
392
|
+
}
|
|
393
|
+
const email = optionalText(payload.email, 320);
|
|
394
|
+
const name = optionalText(payload.name, 1024);
|
|
395
|
+
return {
|
|
396
|
+
provider: options.provider,
|
|
397
|
+
issuer,
|
|
398
|
+
subject: payload.sub,
|
|
399
|
+
identityNamespace: config.identityNamespace ?? config.registrationId,
|
|
400
|
+
emailVerified: payload.email_verified === true || options.provider === "apple" && payload.email_verified === "true",
|
|
401
|
+
...email ? { email } : {},
|
|
402
|
+
...name ? { name } : {}
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
function createGoogleProvider(config) {
|
|
406
|
+
configure(config);
|
|
407
|
+
return {
|
|
408
|
+
key: "google",
|
|
409
|
+
registrationId: config.registrationId,
|
|
410
|
+
purposes: ["identity"],
|
|
411
|
+
authorization: {
|
|
412
|
+
url(input) {
|
|
413
|
+
identityPurpose(input.purpose);
|
|
414
|
+
return authorizationURL(
|
|
415
|
+
"https://accounts.google.com/o/oauth2/v2/auth",
|
|
416
|
+
{
|
|
417
|
+
client_id: config.clientId,
|
|
418
|
+
redirect_uri: config.redirectUri,
|
|
419
|
+
response_type: "code",
|
|
420
|
+
scope: "openid email profile",
|
|
421
|
+
state: input.state,
|
|
422
|
+
nonce: input.nonce,
|
|
423
|
+
code_challenge: input.challenge,
|
|
424
|
+
code_challenge_method: "S256"
|
|
425
|
+
}
|
|
426
|
+
);
|
|
427
|
+
},
|
|
428
|
+
async exchange(input, fetcher) {
|
|
429
|
+
identityPurpose(input.purpose);
|
|
430
|
+
if (!config.clientSecret)
|
|
431
|
+
throw new IntegrationError("application_unavailable");
|
|
432
|
+
const tokens = record(
|
|
433
|
+
await requestJson(
|
|
434
|
+
"https://oauth2.googleapis.com/token",
|
|
435
|
+
{
|
|
436
|
+
method: "POST",
|
|
437
|
+
headers: {
|
|
438
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
439
|
+
Accept: "application/json"
|
|
440
|
+
},
|
|
441
|
+
body: new URLSearchParams({
|
|
442
|
+
client_id: config.clientId,
|
|
443
|
+
client_secret: config.clientSecret,
|
|
444
|
+
redirect_uri: config.redirectUri,
|
|
445
|
+
grant_type: "authorization_code",
|
|
446
|
+
code: input.code,
|
|
447
|
+
code_verifier: input.verifier
|
|
448
|
+
}),
|
|
449
|
+
...input.signal ? { signal: input.signal } : {}
|
|
450
|
+
},
|
|
451
|
+
fetcher
|
|
452
|
+
)
|
|
453
|
+
);
|
|
454
|
+
const identity = await verifyToken(
|
|
455
|
+
{ idToken: text(tokens.id_token, 32768), nonce: input.nonce },
|
|
456
|
+
config,
|
|
457
|
+
fetcher,
|
|
458
|
+
{ provider: "google", audiences: [config.clientId] }
|
|
459
|
+
);
|
|
460
|
+
return { identity };
|
|
461
|
+
}
|
|
462
|
+
},
|
|
463
|
+
verifyNative(input, fetcher) {
|
|
464
|
+
return verifyToken(input, config, fetcher, {
|
|
465
|
+
provider: "google",
|
|
466
|
+
audiences: config.nativeClientIds ?? [config.clientId]
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
function createAppleProvider(config) {
|
|
472
|
+
configure(config);
|
|
473
|
+
async function clientSecret() {
|
|
474
|
+
if (!config.teamId || !config.keyId || !config.privateKey)
|
|
475
|
+
throw new IntegrationError("application_unavailable");
|
|
476
|
+
try {
|
|
477
|
+
const key = await importPKCS8(config.privateKey, "ES256");
|
|
478
|
+
return await new SignJWT({}).setProtectedHeader({ alg: "ES256", kid: config.keyId }).setIssuer(config.teamId).setSubject(config.clientId).setAudience("https://appleid.apple.com").setIssuedAt().setExpirationTime("5m").sign(key);
|
|
479
|
+
} catch {
|
|
480
|
+
throw new IntegrationError("application_unavailable");
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
return {
|
|
484
|
+
key: "apple",
|
|
485
|
+
registrationId: config.registrationId,
|
|
486
|
+
purposes: ["identity"],
|
|
487
|
+
authorization: {
|
|
488
|
+
url(input) {
|
|
489
|
+
identityPurpose(input.purpose);
|
|
490
|
+
return authorizationURL("https://appleid.apple.com/auth/authorize", {
|
|
491
|
+
client_id: config.clientId,
|
|
492
|
+
redirect_uri: config.redirectUri,
|
|
493
|
+
response_type: "code",
|
|
494
|
+
response_mode: "form_post",
|
|
495
|
+
scope: "name email",
|
|
496
|
+
state: input.state,
|
|
497
|
+
nonce: input.nonce
|
|
498
|
+
});
|
|
499
|
+
},
|
|
500
|
+
async exchange(input, fetcher) {
|
|
501
|
+
identityPurpose(input.purpose);
|
|
502
|
+
const result = record(
|
|
503
|
+
await requestJson(
|
|
504
|
+
"https://appleid.apple.com/auth/token",
|
|
505
|
+
{
|
|
506
|
+
method: "POST",
|
|
507
|
+
headers: {
|
|
508
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
509
|
+
Accept: "application/json"
|
|
510
|
+
},
|
|
511
|
+
body: new URLSearchParams({
|
|
512
|
+
client_id: config.clientId,
|
|
513
|
+
client_secret: await clientSecret(),
|
|
514
|
+
redirect_uri: config.redirectUri,
|
|
515
|
+
grant_type: "authorization_code",
|
|
516
|
+
code: input.code
|
|
517
|
+
}),
|
|
518
|
+
...input.signal ? { signal: input.signal } : {}
|
|
519
|
+
},
|
|
520
|
+
fetcher
|
|
521
|
+
)
|
|
522
|
+
);
|
|
523
|
+
return {
|
|
524
|
+
identity: await verifyToken(
|
|
525
|
+
{ idToken: text(result.id_token, 32768), nonce: input.nonce },
|
|
526
|
+
config,
|
|
527
|
+
fetcher,
|
|
528
|
+
{ provider: "apple", audiences: [config.clientId] }
|
|
529
|
+
)
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
},
|
|
533
|
+
// nonce is the actual expected token nonce; a native host using SHA256 supplies that hash.
|
|
534
|
+
verifyNative(input, fetcher) {
|
|
535
|
+
return verifyToken(input, config, fetcher, {
|
|
536
|
+
provider: "apple",
|
|
537
|
+
audiences: config.nativeClientIds ?? [config.clientId]
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
function createGitHubProvider(config) {
|
|
543
|
+
configure(config);
|
|
544
|
+
if (!config.clientSecret)
|
|
545
|
+
throw new IntegrationError("invalid_provider_configuration");
|
|
546
|
+
return {
|
|
547
|
+
key: "github",
|
|
548
|
+
registrationId: config.registrationId,
|
|
549
|
+
purposes: ["identity"],
|
|
550
|
+
authorization: {
|
|
551
|
+
url(input) {
|
|
552
|
+
identityPurpose(input.purpose);
|
|
553
|
+
return authorizationURL("https://github.com/login/oauth/authorize", {
|
|
554
|
+
client_id: config.clientId,
|
|
555
|
+
redirect_uri: config.redirectUri,
|
|
556
|
+
scope: "read:user user:email",
|
|
557
|
+
state: input.state,
|
|
558
|
+
code_challenge: input.challenge,
|
|
559
|
+
code_challenge_method: "S256"
|
|
560
|
+
});
|
|
561
|
+
},
|
|
562
|
+
async exchange(input, fetcher) {
|
|
563
|
+
identityPurpose(input.purpose);
|
|
564
|
+
const tokens = record(
|
|
565
|
+
await requestJson(
|
|
566
|
+
"https://github.com/login/oauth/access_token",
|
|
567
|
+
{
|
|
568
|
+
method: "POST",
|
|
569
|
+
headers: {
|
|
570
|
+
Accept: "application/json",
|
|
571
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
572
|
+
},
|
|
573
|
+
body: new URLSearchParams({
|
|
574
|
+
client_id: config.clientId,
|
|
575
|
+
client_secret: config.clientSecret,
|
|
576
|
+
redirect_uri: config.redirectUri,
|
|
577
|
+
code: input.code,
|
|
578
|
+
code_verifier: input.verifier
|
|
579
|
+
}),
|
|
580
|
+
...input.signal ? { signal: input.signal } : {}
|
|
581
|
+
},
|
|
582
|
+
fetcher
|
|
583
|
+
)
|
|
584
|
+
);
|
|
585
|
+
const accessToken = text(tokens.access_token);
|
|
586
|
+
const headers = {
|
|
587
|
+
Accept: "application/vnd.github+json",
|
|
588
|
+
Authorization: `Bearer ${accessToken}`,
|
|
589
|
+
"X-GitHub-Api-Version": "2026-03-10",
|
|
590
|
+
"User-Agent": "mycreat-labs-integrations-sdk"
|
|
591
|
+
};
|
|
592
|
+
const user = record(
|
|
593
|
+
await requestJson(
|
|
594
|
+
"https://api.github.com/user",
|
|
595
|
+
{ headers, ...input.signal ? { signal: input.signal } : {} },
|
|
596
|
+
fetcher
|
|
597
|
+
)
|
|
598
|
+
);
|
|
599
|
+
if (!Number.isSafeInteger(user.id) || Number(user.id) <= 0)
|
|
600
|
+
throw new IntegrationError("invalid_identity");
|
|
601
|
+
const emails = await requestJson(
|
|
602
|
+
"https://api.github.com/user/emails?per_page=100",
|
|
603
|
+
{ headers, ...input.signal ? { signal: input.signal } : {} },
|
|
604
|
+
fetcher
|
|
605
|
+
);
|
|
606
|
+
if (!Array.isArray(emails) || emails.length > 100)
|
|
607
|
+
throw new IntegrationError("invalid_identity");
|
|
608
|
+
const primary = emails.find(
|
|
609
|
+
(item) => item && typeof item === "object" && item.primary === true && item.verified === true
|
|
610
|
+
);
|
|
611
|
+
const email = primary ? text(primary.email, 320) : void 0;
|
|
612
|
+
const name = optionalText(user.name, 1024);
|
|
613
|
+
const username = text(user.login, 255);
|
|
614
|
+
return {
|
|
615
|
+
identity: {
|
|
616
|
+
provider: "github",
|
|
617
|
+
issuer: "https://github.com",
|
|
618
|
+
subject: String(user.id),
|
|
619
|
+
identityNamespace: config.identityNamespace ?? config.registrationId,
|
|
620
|
+
emailVerified: Boolean(email),
|
|
621
|
+
username,
|
|
622
|
+
...email ? { email } : {},
|
|
623
|
+
...name ? { name } : {}
|
|
624
|
+
}
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// src/providers/notion.ts
|
|
632
|
+
var NOTION_API_VERSION = "2026-03-11";
|
|
633
|
+
function uuid2(value) {
|
|
634
|
+
if (!isUuid(value)) throw new IntegrationError("invalid_input");
|
|
635
|
+
return value;
|
|
636
|
+
}
|
|
637
|
+
var validCursor = (value) => typeof value === "string" && value.length > 0 && value.length <= 4096;
|
|
638
|
+
function validateNotionCreatePage(value) {
|
|
639
|
+
const input = inputRecord(value, [
|
|
640
|
+
"parentId",
|
|
641
|
+
"parentType",
|
|
642
|
+
"title",
|
|
643
|
+
"content",
|
|
644
|
+
"url",
|
|
645
|
+
"metadata"
|
|
646
|
+
]);
|
|
647
|
+
const result = {
|
|
648
|
+
parentId: uuid2(input.parentId),
|
|
649
|
+
title: inputText(input.title, 2e3)
|
|
650
|
+
};
|
|
651
|
+
if (input.parentType !== void 0) {
|
|
652
|
+
if (input.parentType !== "page" && input.parentType !== "data_source")
|
|
653
|
+
throw new IntegrationError("invalid_input");
|
|
654
|
+
result.parentType = input.parentType;
|
|
655
|
+
}
|
|
656
|
+
if (input.content !== void 0)
|
|
657
|
+
result.content = inputText(input.content, 1e5, false);
|
|
658
|
+
if (input.url !== void 0) result.url = webUrl(input.url);
|
|
659
|
+
if (input.metadata !== void 0) {
|
|
660
|
+
const metadata = jsonMetadata(input.metadata);
|
|
661
|
+
if (!metadata || Array.isArray(metadata) || typeof metadata !== "object")
|
|
662
|
+
throw new IntegrationError("invalid_input");
|
|
663
|
+
result.metadata = metadata;
|
|
664
|
+
}
|
|
665
|
+
return result;
|
|
666
|
+
}
|
|
667
|
+
function richText(value) {
|
|
668
|
+
const result = [];
|
|
669
|
+
for (let index = 0; index < value.length; ) {
|
|
670
|
+
let end = Math.min(index + 2e3, value.length);
|
|
671
|
+
if (end < value.length && /[\uD800-\uDBFF]/.test(value.charAt(end - 1)))
|
|
672
|
+
end--;
|
|
673
|
+
result.push({ type: "text", text: { content: value.slice(index, end) } });
|
|
674
|
+
index = end;
|
|
675
|
+
}
|
|
676
|
+
return result;
|
|
677
|
+
}
|
|
678
|
+
function resourceTitle(value) {
|
|
679
|
+
if (!Array.isArray(value)) throw new IntegrationError("invalid_response");
|
|
680
|
+
return value.map((item) => {
|
|
681
|
+
const piece = record(item);
|
|
682
|
+
if (typeof piece.plain_text === "string") return piece.plain_text;
|
|
683
|
+
return text(record(piece.text).content, 2e3);
|
|
684
|
+
}).join("");
|
|
685
|
+
}
|
|
686
|
+
function pageResult(value) {
|
|
687
|
+
try {
|
|
688
|
+
const page = record(value);
|
|
689
|
+
if (page.object !== "page" || !isUuid(page.id)) throw new Error();
|
|
690
|
+
const url = text(page.url, 4096);
|
|
691
|
+
if (new URL(url).protocol !== "https:") throw new Error();
|
|
692
|
+
return { id: page.id, url };
|
|
693
|
+
} catch {
|
|
694
|
+
throw new IntegrationError("invalid_response", "outcome_unknown");
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
function createNotionClient(config) {
|
|
698
|
+
if (!config.accessToken)
|
|
699
|
+
throw new IntegrationError("auth_required", "requires_reauth");
|
|
700
|
+
const request = (path, options = {}) => notionRequest(path, { ...config, ...options });
|
|
701
|
+
async function listResources(input = {}) {
|
|
702
|
+
if (input.parentId !== void 0)
|
|
703
|
+
throw new IntegrationError("unsupported_resource_filter");
|
|
704
|
+
if (input.cursor !== void 0 && !validCursor(input.cursor))
|
|
705
|
+
throw new IntegrationError("invalid_input");
|
|
706
|
+
if (input.query !== void 0) inputText(input.query, 1e3, false);
|
|
707
|
+
const response = record(
|
|
708
|
+
await request("/search", {
|
|
709
|
+
body: {
|
|
710
|
+
page_size: 100,
|
|
711
|
+
...input.cursor ? { start_cursor: input.cursor } : {},
|
|
712
|
+
...input.query !== void 0 ? { query: input.query } : {}
|
|
713
|
+
}
|
|
714
|
+
})
|
|
715
|
+
);
|
|
716
|
+
if (!Array.isArray(response.results) || response.results.length > 100 || typeof response.has_more !== "boolean")
|
|
717
|
+
throw new IntegrationError("invalid_response");
|
|
718
|
+
if (response.request_status !== void 0 && record(response.request_status).type !== "complete")
|
|
719
|
+
throw new IntegrationError("resource_listing_incomplete");
|
|
720
|
+
if (response.has_more && !validCursor(response.next_cursor))
|
|
721
|
+
throw new IntegrationError("invalid_response");
|
|
722
|
+
const items = [];
|
|
723
|
+
for (const item of response.results) {
|
|
724
|
+
const entry = record(item);
|
|
725
|
+
if (!isUuid(entry.id) || entry.object !== "page" && entry.object !== "data_source")
|
|
726
|
+
throw new IntegrationError("invalid_response");
|
|
727
|
+
if (entry.in_trash === true || entry.archived === true || entry.is_archived === true)
|
|
728
|
+
continue;
|
|
729
|
+
let name;
|
|
730
|
+
if (entry.object === "data_source") name = resourceTitle(entry.title);
|
|
731
|
+
else {
|
|
732
|
+
const properties = record(entry.properties);
|
|
733
|
+
const title = Object.values(properties).map(record).find((property) => property.type === "title");
|
|
734
|
+
name = title ? resourceTitle(title.title) : "";
|
|
735
|
+
}
|
|
736
|
+
const url = optionalText(entry.url);
|
|
737
|
+
items.push({
|
|
738
|
+
id: entry.id,
|
|
739
|
+
type: entry.object,
|
|
740
|
+
name: name || "Untitled",
|
|
741
|
+
...url ? { url } : {}
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
return {
|
|
745
|
+
items,
|
|
746
|
+
...response.has_more ? { nextCursor: response.next_cursor } : {}
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
async function createPage(value) {
|
|
750
|
+
const input = validateNotionCreatePage(value);
|
|
751
|
+
const parentType = input.parentType ?? "page";
|
|
752
|
+
let titleKey = "title";
|
|
753
|
+
if (parentType === "data_source") {
|
|
754
|
+
const source = record(await request(`/data_sources/${input.parentId}`));
|
|
755
|
+
if (source.id !== input.parentId || source.object !== "data_source")
|
|
756
|
+
throw new IntegrationError("invalid_response");
|
|
757
|
+
const property = Object.entries(record(source.properties)).find(
|
|
758
|
+
([, definition]) => record(definition).type === "title"
|
|
759
|
+
);
|
|
760
|
+
if (!property) throw new IntegrationError("invalid_resource");
|
|
761
|
+
titleKey = property[0];
|
|
762
|
+
}
|
|
763
|
+
const children = [];
|
|
764
|
+
if (input.content) {
|
|
765
|
+
for (const part of richText(input.content))
|
|
766
|
+
children.push({
|
|
767
|
+
object: "block",
|
|
768
|
+
type: "paragraph",
|
|
769
|
+
paragraph: { rich_text: [part] }
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
if (input.url)
|
|
773
|
+
children.push({
|
|
774
|
+
object: "block",
|
|
775
|
+
type: "bookmark",
|
|
776
|
+
bookmark: { url: input.url }
|
|
777
|
+
});
|
|
778
|
+
return pageResult(
|
|
779
|
+
await request("/pages", {
|
|
780
|
+
body: {
|
|
781
|
+
parent: parentType === "page" ? { page_id: input.parentId } : { data_source_id: input.parentId },
|
|
782
|
+
properties: {
|
|
783
|
+
[titleKey]: { type: "title", title: richText(input.title) }
|
|
784
|
+
},
|
|
785
|
+
...children.length ? { children } : {}
|
|
786
|
+
}
|
|
787
|
+
})
|
|
788
|
+
);
|
|
789
|
+
}
|
|
790
|
+
return { request, listResources, resources: listResources, createPage };
|
|
791
|
+
}
|
|
792
|
+
function tokenCredentials(value, previous) {
|
|
793
|
+
try {
|
|
794
|
+
const response = record(value);
|
|
795
|
+
const accessToken = text(response.access_token, 8192);
|
|
796
|
+
const refreshToken = text(response.refresh_token, 8192);
|
|
797
|
+
if (typeof response.token_type !== "string" || response.token_type.toLowerCase() !== "bearer")
|
|
798
|
+
throw new Error();
|
|
799
|
+
const expires = response.expires_in;
|
|
800
|
+
if (expires !== void 0 && (typeof expires !== "number" || !Number.isInteger(expires) || expires <= 0 || expires > 31536e3))
|
|
801
|
+
throw new Error();
|
|
802
|
+
return {
|
|
803
|
+
accessToken,
|
|
804
|
+
refreshToken,
|
|
805
|
+
...expires !== void 0 ? { expiresAt: Date.now() + Number(expires) * 1e3 } : {},
|
|
806
|
+
...previous?.scopes ? { scopes: previous.scopes } : {},
|
|
807
|
+
...previous?.extra ? { extra: previous.extra } : {}
|
|
808
|
+
};
|
|
809
|
+
} catch {
|
|
810
|
+
throw new IntegrationError("invalid_response", "outcome_unknown");
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
function createNotionProvider(config) {
|
|
814
|
+
if (!config.registrationId || !config.clientId || !config.clientSecret || !config.redirectUri)
|
|
815
|
+
throw new IntegrationError("invalid_provider_configuration");
|
|
816
|
+
const tokenRequest = (body, fetcher, signal) => notionRequest("/oauth/token", {
|
|
817
|
+
clientId: config.clientId,
|
|
818
|
+
clientSecret: config.clientSecret,
|
|
819
|
+
apiVersion: config.apiVersion,
|
|
820
|
+
body,
|
|
821
|
+
fetch: fetcher,
|
|
822
|
+
...signal ? { signal } : {}
|
|
823
|
+
});
|
|
824
|
+
return {
|
|
825
|
+
key: "notion",
|
|
826
|
+
registrationId: config.registrationId,
|
|
827
|
+
purposes: ["connect"],
|
|
828
|
+
authorization: {
|
|
829
|
+
url(input) {
|
|
830
|
+
if (input.purpose !== "connect")
|
|
831
|
+
throw new IntegrationError("unsupported_purpose");
|
|
832
|
+
const url = new URL("https://api.notion.com/v1/oauth/authorize");
|
|
833
|
+
url.search = new URLSearchParams({
|
|
834
|
+
client_id: config.clientId,
|
|
835
|
+
redirect_uri: config.redirectUri,
|
|
836
|
+
response_type: "code",
|
|
837
|
+
owner: "user",
|
|
838
|
+
state: input.state
|
|
839
|
+
}).toString();
|
|
840
|
+
return url.toString();
|
|
841
|
+
},
|
|
842
|
+
async exchange(input, fetcher) {
|
|
843
|
+
if (input.purpose !== "connect")
|
|
844
|
+
throw new IntegrationError("unsupported_purpose");
|
|
845
|
+
const response = record(
|
|
846
|
+
await tokenRequest(
|
|
847
|
+
{
|
|
848
|
+
grant_type: "authorization_code",
|
|
849
|
+
code: input.code,
|
|
850
|
+
redirect_uri: config.redirectUri
|
|
851
|
+
},
|
|
852
|
+
fetcher,
|
|
853
|
+
input.signal
|
|
854
|
+
)
|
|
855
|
+
);
|
|
856
|
+
const credentials = tokenCredentials(response);
|
|
857
|
+
const workspaceId = text(response.workspace_id);
|
|
858
|
+
const botId = text(response.bot_id);
|
|
859
|
+
if (!isUuid(workspaceId) || !isUuid(botId))
|
|
860
|
+
throw new IntegrationError("invalid_response", "outcome_unknown");
|
|
861
|
+
const owner = record(response.owner);
|
|
862
|
+
if (owner.type !== "user")
|
|
863
|
+
throw new IntegrationError("invalid_response");
|
|
864
|
+
const userId = text(record(owner.user).id);
|
|
865
|
+
if (!isUuid(userId)) throw new IntegrationError("invalid_response");
|
|
866
|
+
const label = optionalText(response.workspace_name) ?? "Notion workspace";
|
|
867
|
+
credentials.extra = { workspaceId, botId, ownerId: userId };
|
|
868
|
+
return {
|
|
869
|
+
credentials,
|
|
870
|
+
account: {
|
|
871
|
+
id: `${workspaceId}:${botId}:${userId}`,
|
|
872
|
+
label,
|
|
873
|
+
metadata: { workspaceId, botId, ownerId: userId }
|
|
874
|
+
}
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
},
|
|
878
|
+
resources(input, context) {
|
|
879
|
+
return createNotionClient({
|
|
880
|
+
accessToken: context.credentials.accessToken,
|
|
881
|
+
fetch: context.fetch,
|
|
882
|
+
signal: context.signal,
|
|
883
|
+
apiVersion: config.apiVersion
|
|
884
|
+
}).listResources(input);
|
|
885
|
+
},
|
|
886
|
+
async refresh(credentials, context) {
|
|
887
|
+
if (!credentials.refreshToken)
|
|
888
|
+
throw new IntegrationError("auth_required", "requires_reauth");
|
|
889
|
+
return tokenCredentials(
|
|
890
|
+
await tokenRequest(
|
|
891
|
+
{
|
|
892
|
+
grant_type: "refresh_token",
|
|
893
|
+
refresh_token: credentials.refreshToken
|
|
894
|
+
},
|
|
895
|
+
context.fetch,
|
|
896
|
+
context.signal
|
|
897
|
+
),
|
|
898
|
+
credentials
|
|
899
|
+
);
|
|
900
|
+
},
|
|
901
|
+
async revoke(credentials, context) {
|
|
902
|
+
await notionRequest("/oauth/revoke", {
|
|
903
|
+
clientId: config.clientId,
|
|
904
|
+
clientSecret: config.clientSecret,
|
|
905
|
+
apiVersion: config.apiVersion,
|
|
906
|
+
body: { token: credentials.accessToken },
|
|
907
|
+
fetch: context.fetch,
|
|
908
|
+
signal: context.signal
|
|
909
|
+
});
|
|
910
|
+
},
|
|
911
|
+
actions: {
|
|
912
|
+
create_page: {
|
|
913
|
+
validate(value) {
|
|
914
|
+
return validateNotionCreatePage(value);
|
|
915
|
+
},
|
|
916
|
+
async execute(value, context) {
|
|
917
|
+
const checkpoint = context.checkpoint && typeof context.checkpoint === "object" && !Array.isArray(context.checkpoint) ? context.checkpoint : {};
|
|
918
|
+
if (typeof checkpoint.id === "string" && typeof checkpoint.url === "string")
|
|
919
|
+
return { id: checkpoint.id, url: checkpoint.url };
|
|
920
|
+
const result = await createNotionClient({
|
|
921
|
+
accessToken: context.credentials.accessToken,
|
|
922
|
+
fetch: context.fetch,
|
|
923
|
+
signal: context.signal,
|
|
924
|
+
apiVersion: config.apiVersion
|
|
925
|
+
}).createPage(value);
|
|
926
|
+
await context.saveCheckpoint(result);
|
|
927
|
+
return result;
|
|
928
|
+
},
|
|
929
|
+
async reconcile(_value, context) {
|
|
930
|
+
const checkpoint = context.checkpoint && typeof context.checkpoint === "object" && !Array.isArray(context.checkpoint) ? context.checkpoint : {};
|
|
931
|
+
if (!isUuid(checkpoint.id)) return { resolved: false };
|
|
932
|
+
const result = pageResult(
|
|
933
|
+
await notionRequest(`/pages/${checkpoint.id}`, {
|
|
934
|
+
accessToken: context.credentials.accessToken,
|
|
935
|
+
fetch: context.fetch,
|
|
936
|
+
signal: context.signal,
|
|
937
|
+
apiVersion: config.apiVersion
|
|
938
|
+
})
|
|
939
|
+
);
|
|
940
|
+
if (result.id !== checkpoint.id)
|
|
941
|
+
throw new IntegrationError("invalid_response");
|
|
942
|
+
return { resolved: true, result };
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
};
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
// src/providers/raindrop.ts
|
|
950
|
+
var RAINDROP_TOKEN_ENDPOINT = "https://api.raindrop.io/v1/oauth/access_token";
|
|
951
|
+
function collectionId(value) {
|
|
952
|
+
if (!Number.isSafeInteger(value) || Number(value) !== -1 && Number(value) <= 0)
|
|
953
|
+
throw new IntegrationError("invalid_input");
|
|
954
|
+
return value;
|
|
955
|
+
}
|
|
956
|
+
function validateRaindropCreateBookmark(value) {
|
|
957
|
+
const input = inputRecord(value, [
|
|
958
|
+
"collectionId",
|
|
959
|
+
"url",
|
|
960
|
+
"title",
|
|
961
|
+
"note",
|
|
962
|
+
"excerpt",
|
|
963
|
+
"tags",
|
|
964
|
+
"metadata"
|
|
965
|
+
]);
|
|
966
|
+
const result = { url: webUrl(input.url) };
|
|
967
|
+
if (input.collectionId !== void 0)
|
|
968
|
+
result.collectionId = collectionId(input.collectionId);
|
|
969
|
+
if (input.title !== void 0)
|
|
970
|
+
result.title = inputText(input.title, 1e4, false);
|
|
971
|
+
if (input.note !== void 0)
|
|
972
|
+
result.note = inputText(input.note, 1e5, false);
|
|
973
|
+
if (input.excerpt !== void 0)
|
|
974
|
+
result.excerpt = inputText(input.excerpt, 1e4, false);
|
|
975
|
+
if (input.tags !== void 0) {
|
|
976
|
+
if (!Array.isArray(input.tags) || input.tags.length > 100 || input.tags.some(
|
|
977
|
+
(tag) => typeof tag !== "string" || !tag.trim() || tag.length > 255
|
|
978
|
+
))
|
|
979
|
+
throw new IntegrationError("invalid_input");
|
|
980
|
+
result.tags = [...new Set(input.tags)];
|
|
981
|
+
}
|
|
982
|
+
if (input.metadata !== void 0) {
|
|
983
|
+
const metadata = jsonMetadata(input.metadata);
|
|
984
|
+
if (!metadata || Array.isArray(metadata) || typeof metadata !== "object")
|
|
985
|
+
throw new IntegrationError("invalid_input");
|
|
986
|
+
result.metadata = metadata;
|
|
987
|
+
}
|
|
988
|
+
return result;
|
|
989
|
+
}
|
|
990
|
+
function bookmarkResult(value, input) {
|
|
991
|
+
try {
|
|
992
|
+
const response = record(value);
|
|
993
|
+
if (response.result !== true) throw new Error();
|
|
994
|
+
const item = record(response.item);
|
|
995
|
+
if (!Number.isSafeInteger(item._id) || Number(item._id) <= 0)
|
|
996
|
+
throw new Error();
|
|
997
|
+
const destination = collectionId(record(item.collection).$id);
|
|
998
|
+
const link = text(item.link, 1e4);
|
|
999
|
+
if (input && (new URL(link).toString() !== new URL(input.url).toString() || destination !== (input.collectionId ?? -1)))
|
|
1000
|
+
throw new Error();
|
|
1001
|
+
return {
|
|
1002
|
+
id: String(item._id),
|
|
1003
|
+
collectionId: destination,
|
|
1004
|
+
url: `https://app.raindrop.io/my/${destination}/item/${item._id}`
|
|
1005
|
+
};
|
|
1006
|
+
} catch {
|
|
1007
|
+
throw new IntegrationError("invalid_response", "outcome_unknown");
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
function createRaindropClient(config) {
|
|
1011
|
+
if (!config.accessToken)
|
|
1012
|
+
throw new IntegrationError("auth_required", "requires_reauth");
|
|
1013
|
+
const request = (path, options = {}) => raindropRequest(path, { ...config, ...options });
|
|
1014
|
+
async function listCollections(input = {}) {
|
|
1015
|
+
if (input.cursor !== void 0) throw new IntegrationError("invalid_input");
|
|
1016
|
+
if (input.query !== void 0) inputText(input.query, 1e3, false);
|
|
1017
|
+
if (input.parentId !== void 0 && !/^[1-9][0-9]*$/.test(input.parentId))
|
|
1018
|
+
throw new IntegrationError("invalid_input");
|
|
1019
|
+
const pages = await Promise.all([
|
|
1020
|
+
request("/collections"),
|
|
1021
|
+
request("/collections/childrens")
|
|
1022
|
+
]);
|
|
1023
|
+
const resources = /* @__PURE__ */ new Map();
|
|
1024
|
+
resources.set("-1", { id: "-1", type: "collection", name: "Unsorted" });
|
|
1025
|
+
for (const value of pages) {
|
|
1026
|
+
const response = record(value);
|
|
1027
|
+
if (response.result !== true || !Array.isArray(response.items) || response.items.length > 1e4)
|
|
1028
|
+
throw new IntegrationError("invalid_response");
|
|
1029
|
+
for (const value2 of response.items) {
|
|
1030
|
+
const item = record(value2);
|
|
1031
|
+
if (!Number.isSafeInteger(item._id) || Number(item._id) <= 0)
|
|
1032
|
+
throw new IntegrationError("invalid_response");
|
|
1033
|
+
const level = record(item.access).level;
|
|
1034
|
+
if (!Number.isInteger(level) || Number(level) < 1 || Number(level) > 4)
|
|
1035
|
+
throw new IntegrationError("invalid_response");
|
|
1036
|
+
if (Number(level) < 3) continue;
|
|
1037
|
+
let parentId;
|
|
1038
|
+
if (item.parent !== void 0 && item.parent !== null) {
|
|
1039
|
+
const parent = record(item.parent);
|
|
1040
|
+
const id = parent.$id ?? parent._id;
|
|
1041
|
+
if (!Number.isSafeInteger(id) || Number(id) <= 0)
|
|
1042
|
+
throw new IntegrationError("invalid_response");
|
|
1043
|
+
parentId = String(id);
|
|
1044
|
+
}
|
|
1045
|
+
resources.set(String(item._id), {
|
|
1046
|
+
id: String(item._id),
|
|
1047
|
+
type: "collection",
|
|
1048
|
+
name: text(item.title),
|
|
1049
|
+
metadata: {
|
|
1050
|
+
accessLevel: Number(level),
|
|
1051
|
+
...parentId ? { parentId } : {}
|
|
1052
|
+
}
|
|
1053
|
+
});
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
return {
|
|
1057
|
+
items: [...resources.values()].filter(
|
|
1058
|
+
(item) => (!input.parentId || item.metadata?.parentId === input.parentId) && (!input.query || item.name.toLocaleLowerCase().includes(input.query.toLocaleLowerCase()))
|
|
1059
|
+
)
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
1062
|
+
async function createBookmark(value) {
|
|
1063
|
+
const input = validateRaindropCreateBookmark(value);
|
|
1064
|
+
const id = input.collectionId ?? -1;
|
|
1065
|
+
if (id !== -1) {
|
|
1066
|
+
const response = record(await request(`/collection/${id}`));
|
|
1067
|
+
const item = record(response.item);
|
|
1068
|
+
if (response.result !== true || item._id !== id)
|
|
1069
|
+
throw new IntegrationError("invalid_response");
|
|
1070
|
+
const level = record(item.access).level;
|
|
1071
|
+
if (level !== 3 && level !== 4)
|
|
1072
|
+
throw new IntegrationError("permission_required");
|
|
1073
|
+
}
|
|
1074
|
+
const payload = {
|
|
1075
|
+
link: input.url,
|
|
1076
|
+
collection: { $id: id },
|
|
1077
|
+
...input.title !== void 0 ? { title: input.title } : {},
|
|
1078
|
+
...input.note !== void 0 ? { note: input.note } : {},
|
|
1079
|
+
...input.excerpt !== void 0 ? { excerpt: input.excerpt } : {},
|
|
1080
|
+
...input.tags ? { tags: input.tags } : {}
|
|
1081
|
+
};
|
|
1082
|
+
return bookmarkResult(await request("/raindrop", { body: payload }), input);
|
|
1083
|
+
}
|
|
1084
|
+
return {
|
|
1085
|
+
request,
|
|
1086
|
+
listCollections,
|
|
1087
|
+
resources: listCollections,
|
|
1088
|
+
createBookmark
|
|
1089
|
+
};
|
|
1090
|
+
}
|
|
1091
|
+
function tokenCredentials2(value, previous) {
|
|
1092
|
+
try {
|
|
1093
|
+
const response = record(value);
|
|
1094
|
+
const accessToken = text(response.access_token, 8192);
|
|
1095
|
+
const refreshToken = text(response.refresh_token, 8192);
|
|
1096
|
+
const expires = response.expires_in;
|
|
1097
|
+
if (typeof response.token_type !== "string" || response.token_type.toLowerCase() !== "bearer" || typeof expires !== "number" || !Number.isInteger(expires) || expires <= 0 || expires > 31536e3)
|
|
1098
|
+
throw new Error();
|
|
1099
|
+
return {
|
|
1100
|
+
accessToken,
|
|
1101
|
+
refreshToken,
|
|
1102
|
+
expiresAt: Date.now() + expires * 1e3,
|
|
1103
|
+
...previous?.extra ? { extra: previous.extra } : {}
|
|
1104
|
+
};
|
|
1105
|
+
} catch {
|
|
1106
|
+
throw new IntegrationError("invalid_response", "outcome_unknown");
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
function createRaindropProvider(config) {
|
|
1110
|
+
if (!config.registrationId || !config.clientId || !config.clientSecret || !config.redirectUri)
|
|
1111
|
+
throw new IntegrationError("invalid_provider_configuration");
|
|
1112
|
+
const tokenRequest = (body, fetcher, signal) => raindropRequest("/v1/oauth/access_token", {
|
|
1113
|
+
body: {
|
|
1114
|
+
client_id: config.clientId,
|
|
1115
|
+
client_secret: config.clientSecret,
|
|
1116
|
+
...body
|
|
1117
|
+
},
|
|
1118
|
+
fetch: fetcher,
|
|
1119
|
+
...signal ? { signal } : {}
|
|
1120
|
+
});
|
|
1121
|
+
return {
|
|
1122
|
+
key: "raindrop",
|
|
1123
|
+
registrationId: config.registrationId,
|
|
1124
|
+
purposes: ["connect"],
|
|
1125
|
+
authorization: {
|
|
1126
|
+
url(input) {
|
|
1127
|
+
if (input.purpose !== "connect")
|
|
1128
|
+
throw new IntegrationError("unsupported_purpose");
|
|
1129
|
+
const url = new URL("https://raindrop.io/oauth/authorize");
|
|
1130
|
+
url.search = new URLSearchParams({
|
|
1131
|
+
client_id: config.clientId,
|
|
1132
|
+
redirect_uri: config.redirectUri,
|
|
1133
|
+
response_type: "code",
|
|
1134
|
+
state: input.state
|
|
1135
|
+
}).toString();
|
|
1136
|
+
return url.toString();
|
|
1137
|
+
},
|
|
1138
|
+
async exchange(input, fetcher) {
|
|
1139
|
+
if (input.purpose !== "connect")
|
|
1140
|
+
throw new IntegrationError("unsupported_purpose");
|
|
1141
|
+
const credentials = tokenCredentials2(
|
|
1142
|
+
await tokenRequest(
|
|
1143
|
+
{
|
|
1144
|
+
grant_type: "authorization_code",
|
|
1145
|
+
code: input.code,
|
|
1146
|
+
redirect_uri: config.redirectUri
|
|
1147
|
+
},
|
|
1148
|
+
fetcher,
|
|
1149
|
+
input.signal
|
|
1150
|
+
)
|
|
1151
|
+
);
|
|
1152
|
+
const response = record(
|
|
1153
|
+
await raindropRequest("/user", {
|
|
1154
|
+
accessToken: credentials.accessToken,
|
|
1155
|
+
fetch: fetcher,
|
|
1156
|
+
signal: input.signal
|
|
1157
|
+
})
|
|
1158
|
+
);
|
|
1159
|
+
const user = record(response.user);
|
|
1160
|
+
if (response.result !== true || !Number.isSafeInteger(user._id) || Number(user._id) <= 0)
|
|
1161
|
+
throw new IntegrationError("invalid_response");
|
|
1162
|
+
const id = String(user._id);
|
|
1163
|
+
return {
|
|
1164
|
+
credentials,
|
|
1165
|
+
account: {
|
|
1166
|
+
id,
|
|
1167
|
+
label: optionalText(user.fullName) ?? `Raindrop ${id}`
|
|
1168
|
+
}
|
|
1169
|
+
};
|
|
1170
|
+
}
|
|
1171
|
+
},
|
|
1172
|
+
resources(input, context) {
|
|
1173
|
+
return createRaindropClient({
|
|
1174
|
+
accessToken: context.credentials.accessToken,
|
|
1175
|
+
fetch: context.fetch,
|
|
1176
|
+
signal: context.signal
|
|
1177
|
+
}).listCollections(input);
|
|
1178
|
+
},
|
|
1179
|
+
async refresh(credentials, context) {
|
|
1180
|
+
if (!credentials.refreshToken)
|
|
1181
|
+
throw new IntegrationError("auth_required", "requires_reauth");
|
|
1182
|
+
return tokenCredentials2(
|
|
1183
|
+
await tokenRequest(
|
|
1184
|
+
{
|
|
1185
|
+
grant_type: "refresh_token",
|
|
1186
|
+
refresh_token: credentials.refreshToken
|
|
1187
|
+
},
|
|
1188
|
+
context.fetch,
|
|
1189
|
+
context.signal
|
|
1190
|
+
),
|
|
1191
|
+
credentials
|
|
1192
|
+
);
|
|
1193
|
+
},
|
|
1194
|
+
// Raindrop does not document a revocation API; local disconnect must not pretend to revoke the upstream grant.
|
|
1195
|
+
actions: {
|
|
1196
|
+
create_bookmark: {
|
|
1197
|
+
validate(value) {
|
|
1198
|
+
return validateRaindropCreateBookmark(value);
|
|
1199
|
+
},
|
|
1200
|
+
async execute(value, context) {
|
|
1201
|
+
const checkpoint = context.checkpoint && typeof context.checkpoint === "object" && !Array.isArray(context.checkpoint) ? context.checkpoint : {};
|
|
1202
|
+
if (typeof checkpoint.id === "string" && typeof checkpoint.url === "string" && typeof checkpoint.collectionId === "number")
|
|
1203
|
+
return {
|
|
1204
|
+
id: checkpoint.id,
|
|
1205
|
+
url: checkpoint.url,
|
|
1206
|
+
collectionId: checkpoint.collectionId
|
|
1207
|
+
};
|
|
1208
|
+
const result = await createRaindropClient({
|
|
1209
|
+
accessToken: context.credentials.accessToken,
|
|
1210
|
+
fetch: context.fetch,
|
|
1211
|
+
signal: context.signal
|
|
1212
|
+
}).createBookmark(value);
|
|
1213
|
+
await context.saveCheckpoint(result);
|
|
1214
|
+
return result;
|
|
1215
|
+
},
|
|
1216
|
+
async reconcile(value, context) {
|
|
1217
|
+
const checkpoint = context.checkpoint && typeof context.checkpoint === "object" && !Array.isArray(context.checkpoint) ? context.checkpoint : {};
|
|
1218
|
+
if (typeof checkpoint.id !== "string" || !/^[1-9][0-9]*$/.test(checkpoint.id))
|
|
1219
|
+
return { resolved: false };
|
|
1220
|
+
const result = bookmarkResult(
|
|
1221
|
+
await raindropRequest(`/raindrop/${checkpoint.id}`, {
|
|
1222
|
+
accessToken: context.credentials.accessToken,
|
|
1223
|
+
fetch: context.fetch,
|
|
1224
|
+
signal: context.signal
|
|
1225
|
+
}),
|
|
1226
|
+
validateRaindropCreateBookmark(value)
|
|
1227
|
+
);
|
|
1228
|
+
if (result.id !== checkpoint.id)
|
|
1229
|
+
throw new IntegrationError("invalid_response");
|
|
1230
|
+
return { resolved: true, result };
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
};
|
|
1235
|
+
}
|
|
1236
|
+
export {
|
|
1237
|
+
NOTION_API_VERSION,
|
|
1238
|
+
ProviderHttpError,
|
|
1239
|
+
RAINDROP_TOKEN_ENDPOINT,
|
|
1240
|
+
createAppleProvider,
|
|
1241
|
+
createGitHubProvider,
|
|
1242
|
+
createGoogleProvider,
|
|
1243
|
+
createNotionClient,
|
|
1244
|
+
createNotionProvider,
|
|
1245
|
+
createRaindropClient,
|
|
1246
|
+
createRaindropProvider,
|
|
1247
|
+
notionRequest,
|
|
1248
|
+
raindropRequest,
|
|
1249
|
+
validateNotionCreatePage,
|
|
1250
|
+
validateRaindropCreateBookmark
|
|
1251
|
+
};
|
|
1252
|
+
//# sourceMappingURL=index.js.map
|