@keystrokehq/notion 0.0.9 → 0.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_official/index.d.mts +3 -3
- package/dist/_official/index.mjs +1 -1
- package/dist/_runtime/index.d.mts +25 -25
- package/dist/blocks.d.mts +6 -6
- package/dist/blocks.mjs +7 -7
- package/dist/client.d.mts +1 -1
- package/dist/client.mjs +2 -2
- package/dist/comments.d.mts +4 -4
- package/dist/comments.mjs +5 -5
- package/dist/connection.d.mts +1 -1
- package/dist/connection.mjs +1 -1
- package/dist/data-sources.d.mts +6 -6
- package/dist/data-sources.mjs +7 -7
- package/dist/dist-Bbp6t-WQ.mjs +74 -0
- package/dist/factory-Dw2V3rrS.mjs +7 -0
- package/dist/{integration-D12eY2r0.d.mts → integration-Bc-B-wPX.d.mts} +3 -4
- package/dist/integration-Dqf1rNJ6.mjs +770 -0
- package/dist/pages.d.mts +8 -8
- package/dist/pages.mjs +9 -9
- package/dist/search.d.mts +2 -2
- package/dist/search.mjs +2 -2
- package/dist/triggers.d.mts +2 -3
- package/dist/triggers.mjs +8 -8
- package/dist/users.d.mts +4 -4
- package/dist/users.mjs +5 -5
- package/dist/verification.mjs +86 -1
- package/package.json +5 -5
- package/dist/factory-BvG2wLPI.mjs +0 -8
- package/dist/integration-AVAZpqqY.mjs +0 -93
- /package/dist/{shared-C6A6JPl3.mjs → shared-DwBtvjix.mjs} +0 -0
|
@@ -0,0 +1,770 @@
|
|
|
1
|
+
import { CredentialSet, Operation } from "@keystrokehq/core";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
//#region ../../packages/integration-authoring/dist/official/runtime.mjs
|
|
5
|
+
const REGISTRY_KEY = "__ks_official_integration_metadata_registry";
|
|
6
|
+
function getRegistry() {
|
|
7
|
+
const globalStore = globalThis;
|
|
8
|
+
if (!globalStore[REGISTRY_KEY]) globalStore[REGISTRY_KEY] = /* @__PURE__ */ new WeakMap();
|
|
9
|
+
return globalStore[REGISTRY_KEY];
|
|
10
|
+
}
|
|
11
|
+
function registerOfficialOperation(operation, metadata) {
|
|
12
|
+
getRegistry().set(operation, Object.freeze({ ...metadata }));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
//#endregion
|
|
16
|
+
//#region ../../packages/integration-authoring/dist/official/index.mjs
|
|
17
|
+
const OFFICIAL_CREDENTIAL_SET_ID_PREFIX = "keystroke:";
|
|
18
|
+
function officialCredentialSetId(id) {
|
|
19
|
+
return `${OFFICIAL_CREDENTIAL_SET_ID_PREFIX}${id}`;
|
|
20
|
+
}
|
|
21
|
+
function stripOfficialCredentialSetIdPrefix(id) {
|
|
22
|
+
return id.startsWith(OFFICIAL_CREDENTIAL_SET_ID_PREFIX) ? id.slice(10) : id;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Creates a factory for Keystroke-official integration operations.
|
|
26
|
+
*
|
|
27
|
+
* It keeps the same flat `run(input, credentials)` ergonomics as the generic
|
|
28
|
+
* operation factory, while registering official metadata for builder/runtime
|
|
29
|
+
* operation metadata.
|
|
30
|
+
*/
|
|
31
|
+
function createOfficialOperationFactory(credentialSet) {
|
|
32
|
+
const integrationId = stripOfficialCredentialSetIdPrefix(credentialSet.id);
|
|
33
|
+
return (config) => {
|
|
34
|
+
const wrappedRun = async (input, ctx) => {
|
|
35
|
+
const creds = ctx.credentials[credentialSet.id];
|
|
36
|
+
return config.run(input, creds);
|
|
37
|
+
};
|
|
38
|
+
const operation = new Operation({
|
|
39
|
+
id: config.id,
|
|
40
|
+
name: config.name,
|
|
41
|
+
description: config.description,
|
|
42
|
+
input: config.input,
|
|
43
|
+
output: config.output,
|
|
44
|
+
credentialSets: [credentialSet],
|
|
45
|
+
...config.tags !== void 0 ? { tags: config.tags } : {},
|
|
46
|
+
...config.needsApproval !== void 0 ? { needsApproval: config.needsApproval } : {},
|
|
47
|
+
...config.requiredOAuthScopes !== void 0 ? { requiredOAuthScopes: config.requiredOAuthScopes } : {},
|
|
48
|
+
...config.retries !== void 0 ? { retries: config.retries } : {},
|
|
49
|
+
...config.timeout !== void 0 ? { timeout: config.timeout } : {},
|
|
50
|
+
...config.maxDurationMs !== void 0 ? { maxDurationMs: config.maxDurationMs } : {},
|
|
51
|
+
run: wrappedRun
|
|
52
|
+
});
|
|
53
|
+
registerOfficialOperation(operation, { integrationId });
|
|
54
|
+
return operation;
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Creates an official integration bundle from a single config object.
|
|
59
|
+
*
|
|
60
|
+
* - Creates the public `CredentialSet` internally.
|
|
61
|
+
* - Accepts optional `internal` fields for Keystroke-owned platform credentials.
|
|
62
|
+
*/
|
|
63
|
+
function defineOfficialIntegration(config) {
|
|
64
|
+
const internalCredentialSets = config.internal ?? {};
|
|
65
|
+
const allInternalCredentialSets = [
|
|
66
|
+
...internalCredentialSets.providerApp ? [internalCredentialSets.providerApp] : [],
|
|
67
|
+
...internalCredentialSets.providerAppVariants ?? [],
|
|
68
|
+
...internalCredentialSets.webhookVerification ? [internalCredentialSets.webhookVerification] : [],
|
|
69
|
+
...internalCredentialSets.other ?? []
|
|
70
|
+
];
|
|
71
|
+
const credentialSet = new CredentialSet({
|
|
72
|
+
id: config.id,
|
|
73
|
+
name: config.name,
|
|
74
|
+
description: config.description,
|
|
75
|
+
auth: config.auth,
|
|
76
|
+
...config.connections ? { connections: config.connections } : {},
|
|
77
|
+
...config.proxy ? { proxy: config.proxy } : {},
|
|
78
|
+
...config.needsRawSecret === true ? { needsRawSecret: true } : {}
|
|
79
|
+
});
|
|
80
|
+
return Object.freeze({
|
|
81
|
+
integration: {
|
|
82
|
+
id: config.id,
|
|
83
|
+
name: config.name,
|
|
84
|
+
...config.description !== void 0 ? { description: config.description } : {},
|
|
85
|
+
auth: config.auth,
|
|
86
|
+
...config.proxy ? { proxy: config.proxy } : {},
|
|
87
|
+
...config.needsRawSecret === true ? { needsRawSecret: true } : {},
|
|
88
|
+
...config.connections ? { connections: config.connections } : {}
|
|
89
|
+
},
|
|
90
|
+
credentialSet,
|
|
91
|
+
internalCredentialSets,
|
|
92
|
+
allInternalCredentialSets
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
//#endregion
|
|
97
|
+
//#region src/_official/provider-app.ts
|
|
98
|
+
const notionAppCredentialSet = new CredentialSet({
|
|
99
|
+
id: officialCredentialSetId("notion-app"),
|
|
100
|
+
exposure: "platform-only",
|
|
101
|
+
name: "Notion App",
|
|
102
|
+
auth: z.object({
|
|
103
|
+
clientId: z.string().min(1),
|
|
104
|
+
clientSecret: z.string().min(1)
|
|
105
|
+
})
|
|
106
|
+
});
|
|
107
|
+
const notionOfficialProviderSeed = {
|
|
108
|
+
provider: "notion",
|
|
109
|
+
appRef: "notion-official",
|
|
110
|
+
displayName: "Notion Platform",
|
|
111
|
+
credentialSetName: "Keystroke Notion Platform App",
|
|
112
|
+
envShape: {
|
|
113
|
+
KEYSTROKE_OFFICIAL_NOTION_CLIENT_ID: z.string().optional(),
|
|
114
|
+
KEYSTROKE_OFFICIAL_NOTION_CLIENT_SECRET: z.string().optional()
|
|
115
|
+
},
|
|
116
|
+
requiredEnvKeys: ["KEYSTROKE_OFFICIAL_NOTION_CLIENT_ID", "KEYSTROKE_OFFICIAL_NOTION_CLIENT_SECRET"],
|
|
117
|
+
externalAppIdEnvKey: "KEYSTROKE_OFFICIAL_NOTION_CLIENT_ID",
|
|
118
|
+
buildCredentials: (env) => ({
|
|
119
|
+
clientId: env.KEYSTROKE_OFFICIAL_NOTION_CLIENT_ID,
|
|
120
|
+
clientSecret: env.KEYSTROKE_OFFICIAL_NOTION_CLIENT_SECRET
|
|
121
|
+
})
|
|
122
|
+
};
|
|
123
|
+
const notionWebhookCredentialSet = new CredentialSet({
|
|
124
|
+
id: officialCredentialSetId("notion-webhook"),
|
|
125
|
+
exposure: "platform-only",
|
|
126
|
+
name: "Notion Webhook",
|
|
127
|
+
auth: z.object({ verificationToken: z.string().min(1) })
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
//#endregion
|
|
131
|
+
//#region ../../packages/credential-connection/dist/chunk-DQk6qfdC.mjs
|
|
132
|
+
var __defProp = Object.defineProperty;
|
|
133
|
+
var __exportAll = (all, no_symbols) => {
|
|
134
|
+
let target = {};
|
|
135
|
+
for (var name in all) __defProp(target, name, {
|
|
136
|
+
get: all[name],
|
|
137
|
+
enumerable: true
|
|
138
|
+
});
|
|
139
|
+
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
140
|
+
return target;
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
//#endregion
|
|
144
|
+
//#region ../../packages/credential-connection/dist/defaults-YE9AvLIc.mjs
|
|
145
|
+
/**
|
|
146
|
+
* Shared OAuth 2.0 token-response plumbing.
|
|
147
|
+
*
|
|
148
|
+
* `parseOAuthTokenResponse` handles the two common wire formats (JSON and
|
|
149
|
+
* form-urlencoded, the latter used by GitHub unless you explicitly ask for
|
|
150
|
+
* JSON). `normalizeOAuthTokens` pulls the standard fields out of the parsed
|
|
151
|
+
* body in both the flat form and Slack's nested `authed_user.access_token`
|
|
152
|
+
* form. These helpers are exposed so integration authors overriding
|
|
153
|
+
* `exchangeCode` / `refreshToken` do not have to re-implement them.
|
|
154
|
+
*/
|
|
155
|
+
var TokenExchangeError = class extends Error {
|
|
156
|
+
httpStatus;
|
|
157
|
+
constructor(httpStatus) {
|
|
158
|
+
super(`Token exchange failed: HTTP ${httpStatus}`);
|
|
159
|
+
this.name = "TokenExchangeError";
|
|
160
|
+
this.httpStatus = httpStatus;
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
async function parseOAuthTokenResponse(response) {
|
|
164
|
+
const contentType = response.headers.get("content-type")?.toLowerCase() ?? "";
|
|
165
|
+
if (contentType.includes("application/json")) return await response.json();
|
|
166
|
+
const text = await response.text();
|
|
167
|
+
if (!text.trim()) return {};
|
|
168
|
+
if (contentType.includes("application/x-www-form-urlencoded") || text.includes("=")) {
|
|
169
|
+
const params = new URLSearchParams(text);
|
|
170
|
+
return Object.fromEntries(params.entries());
|
|
171
|
+
}
|
|
172
|
+
throw new Error(`Unsupported OAuth token response content type: ${contentType || "unknown"}`);
|
|
173
|
+
}
|
|
174
|
+
function normalizeOAuthTokens(tokenData) {
|
|
175
|
+
const authedUser = tokenData.authed_user;
|
|
176
|
+
const rawAccessToken = tokenData.access_token ?? authedUser?.access_token;
|
|
177
|
+
const rawRefreshToken = tokenData.refresh_token;
|
|
178
|
+
const rawExpiresIn = tokenData.expires_in;
|
|
179
|
+
const rawInstanceUrl = tokenData.instance_url;
|
|
180
|
+
return {
|
|
181
|
+
accessToken: typeof rawAccessToken === "string" ? rawAccessToken : void 0,
|
|
182
|
+
refreshToken: typeof rawRefreshToken === "string" ? rawRefreshToken : void 0,
|
|
183
|
+
expiresIn: typeof rawExpiresIn === "number" ? rawExpiresIn : typeof rawExpiresIn === "string" ? Number(rawExpiresIn) || void 0 : void 0,
|
|
184
|
+
instanceUrl: typeof rawInstanceUrl === "string" ? rawInstanceUrl : void 0
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
function buildAuthUrl(ctx) {
|
|
188
|
+
const { oauthClient, redirectUri, state, connection } = ctx;
|
|
189
|
+
const scopes = ctx.requestedScopes ?? connection.scopes;
|
|
190
|
+
const url = new URL(connection.authUrl);
|
|
191
|
+
url.searchParams.set("client_id", oauthClient.clientId);
|
|
192
|
+
url.searchParams.set("scope", scopes.join(" "));
|
|
193
|
+
url.searchParams.set("redirect_uri", redirectUri);
|
|
194
|
+
url.searchParams.set("state", state);
|
|
195
|
+
url.searchParams.set("response_type", "code");
|
|
196
|
+
return url.toString();
|
|
197
|
+
}
|
|
198
|
+
async function exchangeToken(params) {
|
|
199
|
+
const response = await fetch(params.tokenUrl, {
|
|
200
|
+
method: "POST",
|
|
201
|
+
headers: {
|
|
202
|
+
Accept: "application/json",
|
|
203
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
204
|
+
},
|
|
205
|
+
body: params.body
|
|
206
|
+
});
|
|
207
|
+
if (!response.ok) throw new TokenExchangeError(response.status);
|
|
208
|
+
const raw = await parseOAuthTokenResponse(response);
|
|
209
|
+
const { accessToken, refreshToken, expiresIn, instanceUrl } = normalizeOAuthTokens(raw);
|
|
210
|
+
if (!accessToken) throw new Error("No access token in response");
|
|
211
|
+
return {
|
|
212
|
+
accessToken,
|
|
213
|
+
refreshToken,
|
|
214
|
+
expiresIn,
|
|
215
|
+
instanceUrl,
|
|
216
|
+
raw
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
async function exchangeCode(ctx) {
|
|
220
|
+
const { oauthClient, code, redirectUri, connection } = ctx;
|
|
221
|
+
if (!code) throw new Error("Missing authorization code");
|
|
222
|
+
return exchangeToken({
|
|
223
|
+
tokenUrl: connection.tokenUrl,
|
|
224
|
+
body: new URLSearchParams({
|
|
225
|
+
code,
|
|
226
|
+
client_id: oauthClient.clientId,
|
|
227
|
+
client_secret: oauthClient.clientSecret,
|
|
228
|
+
redirect_uri: redirectUri,
|
|
229
|
+
grant_type: "authorization_code"
|
|
230
|
+
})
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
async function refreshToken(ctx) {
|
|
234
|
+
const { oauthClient, refreshToken, connection } = ctx;
|
|
235
|
+
return exchangeToken({
|
|
236
|
+
tokenUrl: connection.tokenUrl,
|
|
237
|
+
body: new URLSearchParams({
|
|
238
|
+
grant_type: "refresh_token",
|
|
239
|
+
refresh_token: refreshToken,
|
|
240
|
+
client_id: oauthClient.clientId,
|
|
241
|
+
client_secret: oauthClient.clientSecret
|
|
242
|
+
})
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
const oauthDefaults = {
|
|
246
|
+
buildAuthUrl,
|
|
247
|
+
exchangeCode,
|
|
248
|
+
exchangeToken,
|
|
249
|
+
refreshToken
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
//#endregion
|
|
253
|
+
//#region ../../packages/credential-connection/dist/index.mjs
|
|
254
|
+
/**
|
|
255
|
+
* Compose a base hook with zero or more customizers.
|
|
256
|
+
*
|
|
257
|
+
* Each customizer wraps the result of the previous. Execution order for
|
|
258
|
+
* `pipe(base, a, b, c)` on input `ctx`:
|
|
259
|
+
*
|
|
260
|
+
* ```
|
|
261
|
+
* c-pre → b-pre → a-pre → base(ctx) → a-post → b-post → c-post
|
|
262
|
+
* ```
|
|
263
|
+
*
|
|
264
|
+
* Most presets in `./presets` only post-process, so ordering among them does
|
|
265
|
+
* not matter. Presets that pre-process (rewrite `ctx` before `next`) state
|
|
266
|
+
* their ordering sensitivity in their TSDoc.
|
|
267
|
+
*
|
|
268
|
+
* @example
|
|
269
|
+
* ```ts
|
|
270
|
+
* import { oauthDefaults, oauthPresets as p, pipe } from '@keystrokehq/credential-connection';
|
|
271
|
+
*
|
|
272
|
+
* buildAuthUrl: pipe(
|
|
273
|
+
* oauthDefaults.buildAuthUrl,
|
|
274
|
+
* p.scopeDelimiter(','),
|
|
275
|
+
* p.extraAuthParams({ prompt: 'consent' })
|
|
276
|
+
* );
|
|
277
|
+
* ```
|
|
278
|
+
*/
|
|
279
|
+
function pipe(base, ...customizers) {
|
|
280
|
+
return customizers.reduce((acc, customizer) => customizer(acc), base);
|
|
281
|
+
}
|
|
282
|
+
var presets_exports = /* @__PURE__ */ __exportAll({
|
|
283
|
+
basicAuthJsonBody: () => basicAuthJsonBody,
|
|
284
|
+
dynamicAuthUrlHost: () => dynamicAuthUrlHost,
|
|
285
|
+
dynamicTokenHost: () => dynamicTokenHost,
|
|
286
|
+
extraAuthParams: () => extraAuthParams,
|
|
287
|
+
fetchIdentityAfter: () => fetchIdentityAfter,
|
|
288
|
+
noScopeParam: () => noScopeParam,
|
|
289
|
+
rejectErrorField: () => rejectErrorField,
|
|
290
|
+
requireSuccessFlag: () => requireSuccessFlag,
|
|
291
|
+
scopeDelimiter: () => scopeDelimiter,
|
|
292
|
+
tokenExchangeUsing: () => tokenExchangeUsing
|
|
293
|
+
});
|
|
294
|
+
/**
|
|
295
|
+
* Replace the `scope` query parameter's delimiter on the authorize URL.
|
|
296
|
+
*
|
|
297
|
+
* The default OAuth 2.0 implementation joins scopes with spaces. Slack and
|
|
298
|
+
* Strava require commas.
|
|
299
|
+
*
|
|
300
|
+
* Ordering: post-processing only. Safe to compose in any order with other
|
|
301
|
+
* authorize-URL presets.
|
|
302
|
+
*
|
|
303
|
+
* @example
|
|
304
|
+
* ```ts
|
|
305
|
+
* buildAuthUrl: pipe(oauthDefaults.buildAuthUrl, scopeDelimiter(','));
|
|
306
|
+
* ```
|
|
307
|
+
*
|
|
308
|
+
* @see integrations/slack/src/oauth-connection.ts
|
|
309
|
+
*/
|
|
310
|
+
function scopeDelimiter(delim) {
|
|
311
|
+
return (next) => async (ctx) => {
|
|
312
|
+
const built = await next(ctx);
|
|
313
|
+
const url = new URL(built);
|
|
314
|
+
const scopes = ctx.requestedScopes ?? ctx.connection.scopes;
|
|
315
|
+
url.searchParams.set("scope", scopes.join(delim));
|
|
316
|
+
return url.toString();
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Remove the `scope` query parameter from the authorize URL entirely.
|
|
321
|
+
*
|
|
322
|
+
* Use this for providers that configure scopes at the app level (Notion,
|
|
323
|
+
* Intercom) rather than at authorize time.
|
|
324
|
+
*
|
|
325
|
+
* Ordering: post-processing only.
|
|
326
|
+
*
|
|
327
|
+
* @example
|
|
328
|
+
* ```ts
|
|
329
|
+
* buildAuthUrl: pipe(oauthDefaults.buildAuthUrl, noScopeParam);
|
|
330
|
+
* ```
|
|
331
|
+
*
|
|
332
|
+
* @see integrations/notion/src/oauth-connection.ts
|
|
333
|
+
*/
|
|
334
|
+
const noScopeParam = (next) => async (ctx) => {
|
|
335
|
+
const built = await next(ctx);
|
|
336
|
+
const url = new URL(built);
|
|
337
|
+
url.searchParams.delete("scope");
|
|
338
|
+
return url.toString();
|
|
339
|
+
};
|
|
340
|
+
/**
|
|
341
|
+
* Append extra query parameters to the authorize URL.
|
|
342
|
+
*
|
|
343
|
+
* Accepts a static record (for fixed parameters like Google's
|
|
344
|
+
* `access_type=offline`) or a function of the build context (for parameters
|
|
345
|
+
* that depend on `oauthClient.metadata` or `initiateInput`).
|
|
346
|
+
*
|
|
347
|
+
* Ordering: post-processing only.
|
|
348
|
+
*
|
|
349
|
+
* @example
|
|
350
|
+
* ```ts
|
|
351
|
+
* // Static
|
|
352
|
+
* extraAuthParams({ access_type: 'offline', prompt: 'consent' });
|
|
353
|
+
* // Dynamic (Shopify per-shop)
|
|
354
|
+
* extraAuthParams(({ initiateInput }) => ({ shop: String(initiateInput?.shop) }));
|
|
355
|
+
* ```
|
|
356
|
+
*
|
|
357
|
+
* @see integrations/google/src/oauth-connection.ts
|
|
358
|
+
*/
|
|
359
|
+
function extraAuthParams(params) {
|
|
360
|
+
return (next) => async (ctx) => {
|
|
361
|
+
const built = await next(ctx);
|
|
362
|
+
const url = new URL(built);
|
|
363
|
+
const resolved = typeof params === "function" ? params(ctx) : params;
|
|
364
|
+
for (const [key, value] of Object.entries(resolved)) url.searchParams.set(key, value);
|
|
365
|
+
return url.toString();
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Rewrite the authorize URL's host with a runtime-resolved origin.
|
|
370
|
+
*
|
|
371
|
+
* Use for providers where the OAuth host is tenant-specific (Shopify
|
|
372
|
+
* per-shop) or environment-specific (Salesforce sandbox vs production). The
|
|
373
|
+
* resolver should return a full origin string (`https://host[:port]`); the
|
|
374
|
+
* path, query, and hash from `ctx.connection.authUrl` are preserved.
|
|
375
|
+
*
|
|
376
|
+
* Ordering: pre-processing. Compose BEFORE post-processing presets so the
|
|
377
|
+
* final URL already reflects the resolved host.
|
|
378
|
+
*
|
|
379
|
+
* @example
|
|
380
|
+
* ```ts
|
|
381
|
+
* // Shopify
|
|
382
|
+
* dynamicAuthUrlHost(({ initiateInput }) => `https://${String(initiateInput?.shop)}`);
|
|
383
|
+
* // Salesforce sandbox
|
|
384
|
+
* dynamicAuthUrlHost(({ oauthClient }) =>
|
|
385
|
+
* oauthClient.metadata?.environment === 'sandbox'
|
|
386
|
+
* ? 'https://test.salesforce.com'
|
|
387
|
+
* : 'https://login.salesforce.com'
|
|
388
|
+
* );
|
|
389
|
+
* ```
|
|
390
|
+
*/
|
|
391
|
+
function dynamicAuthUrlHost(resolve) {
|
|
392
|
+
return (next) => async (ctx) => {
|
|
393
|
+
const originalUrl = new URL(ctx.connection.authUrl);
|
|
394
|
+
const newBase = new URL(resolve(ctx));
|
|
395
|
+
originalUrl.host = newBase.host;
|
|
396
|
+
originalUrl.protocol = newBase.protocol;
|
|
397
|
+
if (newBase.port) originalUrl.port = newBase.port;
|
|
398
|
+
const rewrittenConnection = {
|
|
399
|
+
...ctx.connection,
|
|
400
|
+
authUrl: originalUrl.toString()
|
|
401
|
+
};
|
|
402
|
+
return next({
|
|
403
|
+
...ctx,
|
|
404
|
+
connection: rewrittenConnection
|
|
405
|
+
});
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Matched exchange + refresh customizers that POST to the token endpoint
|
|
410
|
+
* using the selected authentication style and body format.
|
|
411
|
+
*
|
|
412
|
+
* This preset REPLACES `next`'s body-building logic entirely. Reach for it
|
|
413
|
+
* when the provider diverges fundamentally (JSON body, HTTP Basic auth)
|
|
414
|
+
* rather than just tweaking behavior around the default. Applying this via
|
|
415
|
+
* `pipe(default, tokenExchangeUsing({...}).exchangeCode)` discards `next`.
|
|
416
|
+
*
|
|
417
|
+
* @remarks
|
|
418
|
+
* Replacement, not wrapping. Stated twice because this is the single most
|
|
419
|
+
* error-prone preset shape in the catalog.
|
|
420
|
+
*/
|
|
421
|
+
function tokenExchangeUsing(options) {
|
|
422
|
+
return {
|
|
423
|
+
exchangeCode: () => async ({ oauthClient, code, redirectUri, connection }) => {
|
|
424
|
+
if (!code) throw new Error("Missing authorization code");
|
|
425
|
+
return doTokenExchange(options.tokenUrl ?? connection.tokenUrl, options, oauthClient, {
|
|
426
|
+
grant_type: "authorization_code",
|
|
427
|
+
code,
|
|
428
|
+
redirect_uri: redirectUri
|
|
429
|
+
});
|
|
430
|
+
},
|
|
431
|
+
refreshToken: () => async ({ oauthClient, refreshToken, connection }) => {
|
|
432
|
+
return doTokenExchange(options.tokenUrl ?? connection.tokenUrl, options, oauthClient, {
|
|
433
|
+
grant_type: "refresh_token",
|
|
434
|
+
refresh_token: refreshToken
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Shorthand for {@link tokenExchangeUsing} with `{ auth: 'basic', bodyFormat: 'json' }`.
|
|
441
|
+
*
|
|
442
|
+
* Notion and Asana both require HTTP Basic auth + a JSON body on their token
|
|
443
|
+
* endpoints.
|
|
444
|
+
*
|
|
445
|
+
* @remarks Replacement semantics inherited from {@link tokenExchangeUsing}.
|
|
446
|
+
*
|
|
447
|
+
* @example
|
|
448
|
+
* ```ts
|
|
449
|
+
* const tokenExchange = basicAuthJsonBody();
|
|
450
|
+
* exchangeCode: pipe(oauthDefaults.exchangeCode, tokenExchange.exchangeCode);
|
|
451
|
+
* refreshToken: pipe(oauthDefaults.refreshToken, tokenExchange.refreshToken);
|
|
452
|
+
* ```
|
|
453
|
+
*
|
|
454
|
+
* @see integrations/notion/src/oauth-connection.ts
|
|
455
|
+
*/
|
|
456
|
+
function basicAuthJsonBody() {
|
|
457
|
+
return tokenExchangeUsing({
|
|
458
|
+
auth: "basic",
|
|
459
|
+
bodyFormat: "json"
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Rewrite the token endpoint's host with a runtime-resolved origin before
|
|
464
|
+
* invoking the downstream exchange / refresh hook.
|
|
465
|
+
*
|
|
466
|
+
* Use for providers where the token endpoint is tenant-specific (Shopify) or
|
|
467
|
+
* environment-specific (Salesforce, Gusto).
|
|
468
|
+
*
|
|
469
|
+
* Ordering: pre-processing. Compose BEFORE other token-exchange presets so
|
|
470
|
+
* they see the rewritten tokenUrl.
|
|
471
|
+
*
|
|
472
|
+
* @example
|
|
473
|
+
* ```ts
|
|
474
|
+
* // Salesforce sandbox
|
|
475
|
+
* dynamicTokenHost(({ oauthClient }) =>
|
|
476
|
+
* oauthClient.metadata?.environment === 'sandbox'
|
|
477
|
+
* ? 'https://test.salesforce.com'
|
|
478
|
+
* : 'https://login.salesforce.com'
|
|
479
|
+
* );
|
|
480
|
+
* ```
|
|
481
|
+
*/
|
|
482
|
+
function dynamicTokenHost(resolve) {
|
|
483
|
+
return {
|
|
484
|
+
exchangeCode: (next) => async (ctx) => {
|
|
485
|
+
return next({
|
|
486
|
+
...ctx,
|
|
487
|
+
connection: rewriteTokenUrl(ctx.connection, resolve(ctx))
|
|
488
|
+
});
|
|
489
|
+
},
|
|
490
|
+
refreshToken: (next) => async (ctx) => {
|
|
491
|
+
return next({
|
|
492
|
+
...ctx,
|
|
493
|
+
connection: rewriteTokenUrl(ctx.connection, resolve(ctx))
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Throw when the token response is missing a truthy success flag.
|
|
500
|
+
*
|
|
501
|
+
* Slack returns `{ ok: true | false, ... }` on its v2 access endpoint; an
|
|
502
|
+
* `ok: false` response still comes back HTTP 200 and often omits the
|
|
503
|
+
* `access_token` field, so the default `!response.ok` and
|
|
504
|
+
* "no access token" checks are both insufficient.
|
|
505
|
+
*
|
|
506
|
+
* @remarks REPLACEMENT, not wrapping. Because the flag check must happen
|
|
507
|
+
* against the raw response body before the normalization step that throws on
|
|
508
|
+
* missing `access_token`, this preset performs its own POST + parse instead
|
|
509
|
+
* of delegating to `next`. Apply via
|
|
510
|
+
* `pipe(oauthDefaults.exchangeCode, requireSuccessFlag('ok'))` for authoring
|
|
511
|
+
* symmetry with other presets; `oauthDefaults.exchangeCode` is effectively
|
|
512
|
+
* discarded.
|
|
513
|
+
*
|
|
514
|
+
* @throws An `Error` when `raw[key]` is falsy. The message includes
|
|
515
|
+
* `raw[errorKey]` when present, so the resulting error surface is
|
|
516
|
+
* actionable.
|
|
517
|
+
*
|
|
518
|
+
* @see integrations/slack/src/oauth-connection.ts
|
|
519
|
+
*/
|
|
520
|
+
function requireSuccessFlag(key = "ok", errorKey = "error") {
|
|
521
|
+
return () => async ({ oauthClient, code, redirectUri, connection }) => {
|
|
522
|
+
if (!code) throw new Error("Missing authorization code");
|
|
523
|
+
const response = await fetch(connection.tokenUrl, {
|
|
524
|
+
method: "POST",
|
|
525
|
+
headers: {
|
|
526
|
+
Accept: "application/json",
|
|
527
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
528
|
+
},
|
|
529
|
+
body: new URLSearchParams({
|
|
530
|
+
client_id: oauthClient.clientId,
|
|
531
|
+
client_secret: oauthClient.clientSecret,
|
|
532
|
+
code,
|
|
533
|
+
redirect_uri: redirectUri,
|
|
534
|
+
grant_type: "authorization_code"
|
|
535
|
+
})
|
|
536
|
+
});
|
|
537
|
+
if (!response.ok) throw new TokenExchangeError(response.status);
|
|
538
|
+
const raw = await parseOAuthTokenResponse(response);
|
|
539
|
+
if (!raw[key]) {
|
|
540
|
+
const error = raw[errorKey];
|
|
541
|
+
const reason = typeof error === "string" ? error : "unknown error";
|
|
542
|
+
throw new Error(`Token exchange failed: ${reason}`);
|
|
543
|
+
}
|
|
544
|
+
const { accessToken, refreshToken, expiresIn, instanceUrl } = normalizeOAuthTokens(raw);
|
|
545
|
+
if (!accessToken) throw new Error("No access token in response");
|
|
546
|
+
return {
|
|
547
|
+
accessToken,
|
|
548
|
+
refreshToken,
|
|
549
|
+
expiresIn,
|
|
550
|
+
instanceUrl,
|
|
551
|
+
raw
|
|
552
|
+
};
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* Throw when the token response carries a truthy `error` field.
|
|
557
|
+
*
|
|
558
|
+
* Intercom and some other providers report failures with HTTP 200 + an
|
|
559
|
+
* `error` / `error_description` pair. This preset converts that shape into a
|
|
560
|
+
* normalized exception.
|
|
561
|
+
*
|
|
562
|
+
* @remarks REPLACEMENT, not wrapping. Mirrors {@link requireSuccessFlag}:
|
|
563
|
+
* the error-field check must happen against the raw response body before
|
|
564
|
+
* the normalization step that throws on missing `access_token`, so this
|
|
565
|
+
* preset performs its own POST + parse instead of delegating to `next`.
|
|
566
|
+
*
|
|
567
|
+
* @throws An `Error` when `raw[errorKey]` is truthy. The message uses
|
|
568
|
+
* `raw[descriptionKey]` when present, otherwise falls back to
|
|
569
|
+
* `raw[errorKey]`.
|
|
570
|
+
*/
|
|
571
|
+
function rejectErrorField(errorKey = "error", descriptionKey = "error_description") {
|
|
572
|
+
return () => async ({ oauthClient, code, redirectUri, connection }) => {
|
|
573
|
+
if (!code) throw new Error("Missing authorization code");
|
|
574
|
+
const response = await fetch(connection.tokenUrl, {
|
|
575
|
+
method: "POST",
|
|
576
|
+
headers: {
|
|
577
|
+
Accept: "application/json",
|
|
578
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
579
|
+
},
|
|
580
|
+
body: new URLSearchParams({
|
|
581
|
+
client_id: oauthClient.clientId,
|
|
582
|
+
client_secret: oauthClient.clientSecret,
|
|
583
|
+
code,
|
|
584
|
+
redirect_uri: redirectUri,
|
|
585
|
+
grant_type: "authorization_code"
|
|
586
|
+
})
|
|
587
|
+
});
|
|
588
|
+
if (!response.ok) throw new TokenExchangeError(response.status);
|
|
589
|
+
const raw = await parseOAuthTokenResponse(response);
|
|
590
|
+
const error = raw[errorKey];
|
|
591
|
+
if (error) {
|
|
592
|
+
const description = raw[descriptionKey];
|
|
593
|
+
const reason = typeof description === "string" ? description : typeof error === "string" ? error : "unknown error";
|
|
594
|
+
throw new Error(`Token exchange failed: ${reason}`);
|
|
595
|
+
}
|
|
596
|
+
const { accessToken, refreshToken, expiresIn, instanceUrl } = normalizeOAuthTokens(raw);
|
|
597
|
+
if (!accessToken) throw new Error("No access token in response");
|
|
598
|
+
return {
|
|
599
|
+
accessToken,
|
|
600
|
+
refreshToken,
|
|
601
|
+
expiresIn,
|
|
602
|
+
instanceUrl,
|
|
603
|
+
raw
|
|
604
|
+
};
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
/**
|
|
608
|
+
* After a successful token exchange, call an identity / me endpoint and stash
|
|
609
|
+
* fields on `tokenResult.raw` under a namespaced prefix. Replaces the
|
|
610
|
+
* duplicated `fetch(...).json()` + `result.raw._providerFoo = ...` pattern in
|
|
611
|
+
* Linear, Google, Sentry, and Strava.
|
|
612
|
+
*
|
|
613
|
+
* Enrichment is best-effort — network errors and non-2xx responses during
|
|
614
|
+
* the identity fetch do not abort the exchange. If the identity data is
|
|
615
|
+
* required for correctness, use a raw {@link ExchangeCodeCustomizer} and
|
|
616
|
+
* throw explicitly.
|
|
617
|
+
*
|
|
618
|
+
* @remarks Stashed values are string-keyed on `raw`; reading them back in
|
|
619
|
+
* `extractInstallationInfo` or in a function-form `vault` requires a cast.
|
|
620
|
+
* That matches the status quo — a typed thread-through is a future
|
|
621
|
+
* improvement.
|
|
622
|
+
*
|
|
623
|
+
* @example
|
|
624
|
+
* ```ts
|
|
625
|
+
* exchangeCode: pipe(
|
|
626
|
+
* oauthDefaults.exchangeCode,
|
|
627
|
+
* fetchIdentityAfter<LinearViewerResponse>({
|
|
628
|
+
* url: 'https://api.linear.app/graphql',
|
|
629
|
+
* method: 'POST',
|
|
630
|
+
* body: { query: '{ viewer { id name } organization { id name } }' },
|
|
631
|
+
* extract: (res) => ({
|
|
632
|
+
* viewer: res.data?.viewer,
|
|
633
|
+
* organization: res.data?.organization,
|
|
634
|
+
* }),
|
|
635
|
+
* namespace: '_linear',
|
|
636
|
+
* }),
|
|
637
|
+
* );
|
|
638
|
+
* ```
|
|
639
|
+
*
|
|
640
|
+
* @see integrations/linear/src/oauth-connection.ts
|
|
641
|
+
*/
|
|
642
|
+
function fetchIdentityAfter(options) {
|
|
643
|
+
return (next) => async (ctx) => {
|
|
644
|
+
const result = await next(ctx);
|
|
645
|
+
const namespace = options.namespace ?? "_identity";
|
|
646
|
+
try {
|
|
647
|
+
const url = typeof options.url === "function" ? options.url(result) : options.url;
|
|
648
|
+
const extraHeaders = typeof options.headers === "function" ? options.headers(result) : options.headers;
|
|
649
|
+
const headers = {
|
|
650
|
+
Authorization: `Bearer ${result.accessToken}`,
|
|
651
|
+
Accept: "application/json",
|
|
652
|
+
...extraHeaders
|
|
653
|
+
};
|
|
654
|
+
const requestInit = {
|
|
655
|
+
method: options.method ?? "GET",
|
|
656
|
+
headers
|
|
657
|
+
};
|
|
658
|
+
if (options.body !== void 0) {
|
|
659
|
+
headers["Content-Type"] = headers["Content-Type"] ?? "application/json";
|
|
660
|
+
requestInit.body = JSON.stringify(options.body);
|
|
661
|
+
}
|
|
662
|
+
const response = await fetch(url, requestInit);
|
|
663
|
+
if (!response.ok) return result;
|
|
664
|
+
const json = await response.json();
|
|
665
|
+
const extracted = options.extract(json);
|
|
666
|
+
for (const [key, value] of Object.entries(extracted)) result.raw[`${namespace}:${key}`] = value;
|
|
667
|
+
} catch {}
|
|
668
|
+
return result;
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
async function doTokenExchange(tokenUrl, options, oauthClient, grantBody) {
|
|
672
|
+
const body = { ...grantBody };
|
|
673
|
+
const headers = { Accept: "application/json" };
|
|
674
|
+
if (options.auth === "basic") headers.Authorization = `Basic ${Buffer.from(`${oauthClient.clientId}:${oauthClient.clientSecret}`, "utf8").toString("base64")}`;
|
|
675
|
+
else {
|
|
676
|
+
body.client_id = oauthClient.clientId;
|
|
677
|
+
body.client_secret = oauthClient.clientSecret;
|
|
678
|
+
}
|
|
679
|
+
let requestBody;
|
|
680
|
+
if (options.bodyFormat === "json") {
|
|
681
|
+
headers["Content-Type"] = "application/json";
|
|
682
|
+
requestBody = JSON.stringify(body);
|
|
683
|
+
} else {
|
|
684
|
+
headers["Content-Type"] = "application/x-www-form-urlencoded";
|
|
685
|
+
requestBody = new URLSearchParams(body);
|
|
686
|
+
}
|
|
687
|
+
const response = await fetch(tokenUrl, {
|
|
688
|
+
method: "POST",
|
|
689
|
+
headers,
|
|
690
|
+
body: requestBody
|
|
691
|
+
});
|
|
692
|
+
if (!response.ok) throw new TokenExchangeError(response.status);
|
|
693
|
+
const raw = await parseOAuthTokenResponse(response);
|
|
694
|
+
const { accessToken, refreshToken, expiresIn, instanceUrl } = normalizeOAuthTokens(raw);
|
|
695
|
+
if (!accessToken) throw new Error("No access token in response");
|
|
696
|
+
return {
|
|
697
|
+
accessToken,
|
|
698
|
+
refreshToken,
|
|
699
|
+
expiresIn,
|
|
700
|
+
instanceUrl,
|
|
701
|
+
raw
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
function rewriteTokenUrl(connection, resolved) {
|
|
705
|
+
const originalUrl = new URL(connection.tokenUrl);
|
|
706
|
+
const newBase = new URL(resolved);
|
|
707
|
+
originalUrl.host = newBase.host;
|
|
708
|
+
originalUrl.protocol = newBase.protocol;
|
|
709
|
+
if (newBase.port) originalUrl.port = newBase.port;
|
|
710
|
+
return {
|
|
711
|
+
...connection,
|
|
712
|
+
tokenUrl: originalUrl.toString()
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
//#endregion
|
|
717
|
+
//#region src/oauth-connection.ts
|
|
718
|
+
const notionTokenExchange = presets_exports.basicAuthJsonBody();
|
|
719
|
+
const notionOAuthConnection = {
|
|
720
|
+
kind: "oauth",
|
|
721
|
+
tokenType: "refreshable",
|
|
722
|
+
authUrl: "https://api.notion.com/v1/oauth/authorize",
|
|
723
|
+
tokenUrl: "https://api.notion.com/v1/oauth/token",
|
|
724
|
+
revokeUrl: "https://api.notion.com/v1/oauth/revoke",
|
|
725
|
+
scopes: [
|
|
726
|
+
"content:read",
|
|
727
|
+
"content:write",
|
|
728
|
+
"comments:read",
|
|
729
|
+
"comments:write",
|
|
730
|
+
"users:read"
|
|
731
|
+
],
|
|
732
|
+
vault: { accessToken: "NOTION_ACCESS_TOKEN" },
|
|
733
|
+
oauth: { id: "official.notion.oauth" },
|
|
734
|
+
buildAuthUrl: pipe(oauthDefaults.buildAuthUrl, presets_exports.noScopeParam, presets_exports.extraAuthParams({ owner: "user" })),
|
|
735
|
+
exchangeCode: pipe(oauthDefaults.exchangeCode, notionTokenExchange.exchangeCode),
|
|
736
|
+
refreshToken: pipe(oauthDefaults.refreshToken, notionTokenExchange.refreshToken)
|
|
737
|
+
};
|
|
738
|
+
|
|
739
|
+
//#endregion
|
|
740
|
+
//#region src/integration.ts
|
|
741
|
+
const notionAuthSchema = z.object({ NOTION_ACCESS_TOKEN: z.string().min(1) });
|
|
742
|
+
const notionOfficialIntegration = {
|
|
743
|
+
id: "notion",
|
|
744
|
+
name: "Notion",
|
|
745
|
+
description: "Notion pages, blocks, data sources, comments, users, search, and triggers",
|
|
746
|
+
auth: notionAuthSchema,
|
|
747
|
+
connections: [{
|
|
748
|
+
id: "oauth",
|
|
749
|
+
label: "Connect Notion workspace",
|
|
750
|
+
recommended: true,
|
|
751
|
+
...notionOAuthConnection
|
|
752
|
+
}, {
|
|
753
|
+
id: "integration-token",
|
|
754
|
+
kind: "manual",
|
|
755
|
+
label: "Use internal integration token",
|
|
756
|
+
advanced: true,
|
|
757
|
+
input: notionAuthSchema
|
|
758
|
+
}]
|
|
759
|
+
};
|
|
760
|
+
const notionBundle = defineOfficialIntegration({
|
|
761
|
+
...notionOfficialIntegration,
|
|
762
|
+
internal: {
|
|
763
|
+
providerApp: notionAppCredentialSet,
|
|
764
|
+
webhookVerification: notionWebhookCredentialSet
|
|
765
|
+
}
|
|
766
|
+
});
|
|
767
|
+
const notion = notionBundle.credentialSet;
|
|
768
|
+
|
|
769
|
+
//#endregion
|
|
770
|
+
export { notionOfficialProviderSeed as a, notionAppCredentialSet as i, notionBundle as n, notionWebhookCredentialSet as o, notionOfficialIntegration as r, createOfficialOperationFactory as s, notion as t };
|