@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.
Files changed (54) hide show
  1. package/README.md +54 -2
  2. package/dist/adapters/bullmq.cjs +110 -0
  3. package/dist/adapters/bullmq.cjs.map +1 -0
  4. package/dist/adapters/bullmq.d.cts +11 -0
  5. package/dist/adapters/bullmq.d.ts +11 -0
  6. package/dist/adapters/bullmq.js +85 -0
  7. package/dist/adapters/bullmq.js.map +1 -0
  8. package/dist/adapters/postgres.cjs +396 -0
  9. package/dist/adapters/postgres.cjs.map +1 -0
  10. package/dist/adapters/postgres.d.cts +51 -0
  11. package/dist/adapters/postgres.d.ts +51 -0
  12. package/dist/adapters/postgres.js +368 -0
  13. package/dist/adapters/postgres.js.map +1 -0
  14. package/dist/adapters/prisma.cjs +414 -0
  15. package/dist/adapters/prisma.cjs.map +1 -0
  16. package/dist/adapters/prisma.d.cts +15 -0
  17. package/dist/adapters/prisma.d.ts +15 -0
  18. package/dist/adapters/prisma.js +386 -0
  19. package/dist/adapters/prisma.js.map +1 -0
  20. package/dist/adapters/redis.cjs +125 -0
  21. package/dist/adapters/redis.cjs.map +1 -0
  22. package/dist/adapters/redis.d.cts +20 -0
  23. package/dist/adapters/redis.d.ts +20 -0
  24. package/dist/adapters/redis.js +98 -0
  25. package/dist/adapters/redis.js.map +1 -0
  26. package/dist/errors-3hDEtzuZ.d.cts +11 -0
  27. package/dist/errors-3hDEtzuZ.d.ts +11 -0
  28. package/dist/index.cjs +1328 -0
  29. package/dist/index.cjs.map +1 -0
  30. package/dist/index.d.cts +128 -0
  31. package/dist/index.d.ts +128 -0
  32. package/dist/index.js +1297 -0
  33. package/dist/index.js.map +1 -0
  34. package/dist/providers/index.cjs +1287 -0
  35. package/dist/providers/index.cjs.map +1 -0
  36. package/dist/providers/index.d.cts +144 -0
  37. package/dist/providers/index.d.ts +144 -0
  38. package/dist/providers/index.js +1252 -0
  39. package/dist/providers/index.js.map +1 -0
  40. package/dist/testing.cjs +275 -0
  41. package/dist/testing.cjs.map +1 -0
  42. package/dist/testing.d.cts +20 -0
  43. package/dist/testing.d.ts +20 -0
  44. package/dist/testing.js +248 -0
  45. package/dist/testing.js.map +1 -0
  46. package/dist/types-Y4-DoJ1A.d.cts +260 -0
  47. package/dist/types-Y4-DoJ1A.d.ts +260 -0
  48. package/docs/migration.md +23 -0
  49. package/docs/security.md +26 -0
  50. package/docs/verification.md +22 -0
  51. package/examples/embedded-host.ts +62 -0
  52. package/migrations/0001_initial.sql +40 -0
  53. package/package.json +90 -4
  54. 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