@notionhq/workers 0.7.0 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/alpha/workflow.d.ts +30 -2
- package/dist/alpha/workflow.d.ts.map +1 -1
- package/dist/alpha/workflow.js +51 -1
- package/dist/capabilities/custom-block.d.ts +124 -0
- package/dist/capabilities/custom-block.d.ts.map +1 -0
- package/dist/capabilities/custom-block.js +22 -0
- package/dist/credential.d.ts +26 -0
- package/dist/credential.d.ts.map +1 -0
- package/dist/credential.js +176 -0
- package/dist/credential.test.d.ts +2 -0
- package/dist/credential.test.d.ts.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/worker.d.ts +48 -1
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +78 -1
- package/package.json +1 -1
- package/src/alpha/workflow.test.ts +124 -2
- package/src/alpha/workflow.ts +92 -3
- package/src/capabilities/custom-block.ts +181 -0
- package/src/credential.test.ts +324 -0
- package/src/credential.ts +238 -0
- package/src/index.ts +15 -0
- package/src/worker.test.ts +300 -2
- package/src/worker.ts +102 -2
- package/dist/block.d.ts +0 -321
- package/dist/block.d.ts.map +0 -1
- package/dist/block.js +0 -0
- package/src/block.ts +0 -525
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
CredentialConfiguration,
|
|
5
|
+
CredentialHeaderTransform,
|
|
6
|
+
CredentialKey,
|
|
7
|
+
CredentialNetworkRule,
|
|
8
|
+
} from "./credential.js";
|
|
9
|
+
import {
|
|
10
|
+
CREDENTIAL_VALUE,
|
|
11
|
+
createCredentialDeclaration,
|
|
12
|
+
validateCredentialDeclarationConflicts,
|
|
13
|
+
} from "./credential.js";
|
|
14
|
+
|
|
15
|
+
describe("credential declarations", () => {
|
|
16
|
+
it("serializes symbolic header templates without a credential value", () => {
|
|
17
|
+
const key: CredentialKey = "LINEAR_API_TOKEN";
|
|
18
|
+
const network = [
|
|
19
|
+
{
|
|
20
|
+
domain: "API.LINEAR.COM.",
|
|
21
|
+
transform: [
|
|
22
|
+
{
|
|
23
|
+
headers: {
|
|
24
|
+
Authorization: `Bearer ${CREDENTIAL_VALUE}`,
|
|
25
|
+
"x-api-key": CREDENTIAL_VALUE,
|
|
26
|
+
Cookie: `session=${CREDENTIAL_VALUE}; Secure`,
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
},
|
|
31
|
+
] satisfies readonly CredentialNetworkRule[];
|
|
32
|
+
const declaration = createCredentialDeclaration(key, { network });
|
|
33
|
+
|
|
34
|
+
expect(declaration).toEqual({
|
|
35
|
+
key: "LINEAR_API_TOKEN",
|
|
36
|
+
network: [
|
|
37
|
+
{
|
|
38
|
+
domain: "api.linear.com",
|
|
39
|
+
transform: [
|
|
40
|
+
{
|
|
41
|
+
headers: {
|
|
42
|
+
Authorization: "Bearer {{credential}}",
|
|
43
|
+
"x-api-key": "{{credential}}",
|
|
44
|
+
Cookie: "session={{credential}}; Secure",
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
});
|
|
51
|
+
expect(JSON.stringify(declaration)).not.toContain("lin_api_123");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("supports pre-encoded Basic auth and Vercel wildcard domains", () => {
|
|
55
|
+
const declaration = createCredentialDeclaration("BASIC_CREDENTIAL", {
|
|
56
|
+
network: [
|
|
57
|
+
{
|
|
58
|
+
domain: "*.github.com",
|
|
59
|
+
transform: [
|
|
60
|
+
{
|
|
61
|
+
headers: {
|
|
62
|
+
Authorization: `Basic ${CREDENTIAL_VALUE}`,
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
expect(declaration.network[0]?.domain).toBe("*.github.com");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it.each([
|
|
74
|
+
["invalid key", "1TOKEN"],
|
|
75
|
+
["reserved key", "NOTION_PRIVATE_TOKEN"],
|
|
76
|
+
["case-insensitive reserved key", "notion_private_token"],
|
|
77
|
+
])("rejects %s", (_testCase, key) => {
|
|
78
|
+
expect(() =>
|
|
79
|
+
createCredentialDeclaration(key, {
|
|
80
|
+
network: [
|
|
81
|
+
{
|
|
82
|
+
domain: "api.example.com",
|
|
83
|
+
transform: [
|
|
84
|
+
{
|
|
85
|
+
headers: { Authorization: CREDENTIAL_VALUE },
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
}),
|
|
91
|
+
).toThrow();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it.each(["NOTION_API_TOKEN", "notion_api_token"])(
|
|
95
|
+
"allows credential keys supported by the existing secret store: %s",
|
|
96
|
+
(key) => {
|
|
97
|
+
expect(
|
|
98
|
+
createCredentialDeclaration(key, {
|
|
99
|
+
network: [
|
|
100
|
+
{
|
|
101
|
+
domain: "api.notion.com",
|
|
102
|
+
transform: [{ headers: { Authorization: CREDENTIAL_VALUE } }],
|
|
103
|
+
},
|
|
104
|
+
],
|
|
105
|
+
}).key,
|
|
106
|
+
).toBe(key);
|
|
107
|
+
},
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
it("rejects unsafe domains, header names, and header templates", () => {
|
|
111
|
+
const validTransform = [
|
|
112
|
+
{
|
|
113
|
+
headers: { Authorization: CREDENTIAL_VALUE },
|
|
114
|
+
},
|
|
115
|
+
] satisfies readonly CredentialHeaderTransform[];
|
|
116
|
+
const configuration: CredentialConfiguration = {
|
|
117
|
+
network: [
|
|
118
|
+
{
|
|
119
|
+
domain: "api.example.com",
|
|
120
|
+
transform: validTransform,
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
expect(() =>
|
|
126
|
+
createCredentialDeclaration("TOKEN", {
|
|
127
|
+
...configuration,
|
|
128
|
+
network: [
|
|
129
|
+
{
|
|
130
|
+
domain: "https://evil.com",
|
|
131
|
+
transform: validTransform,
|
|
132
|
+
},
|
|
133
|
+
],
|
|
134
|
+
}),
|
|
135
|
+
).toThrow("must be an exact hostname");
|
|
136
|
+
|
|
137
|
+
expect(() =>
|
|
138
|
+
createCredentialDeclaration("TOKEN", {
|
|
139
|
+
...configuration,
|
|
140
|
+
network: [
|
|
141
|
+
{
|
|
142
|
+
domain: "api.example.com",
|
|
143
|
+
transform: [
|
|
144
|
+
{
|
|
145
|
+
headers: { "bad header": CREDENTIAL_VALUE },
|
|
146
|
+
},
|
|
147
|
+
],
|
|
148
|
+
},
|
|
149
|
+
],
|
|
150
|
+
}),
|
|
151
|
+
).toThrow("Invalid credential header name");
|
|
152
|
+
|
|
153
|
+
expect(() =>
|
|
154
|
+
createCredentialDeclaration("TOKEN", {
|
|
155
|
+
...configuration,
|
|
156
|
+
network: [
|
|
157
|
+
{
|
|
158
|
+
domain: "api.example.com",
|
|
159
|
+
transform: [
|
|
160
|
+
{
|
|
161
|
+
headers: {
|
|
162
|
+
Authorization: `Bearer\r\nX-Leak: ${CREDENTIAL_VALUE}`,
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
],
|
|
166
|
+
},
|
|
167
|
+
],
|
|
168
|
+
}),
|
|
169
|
+
).toThrow("cannot contain control characters");
|
|
170
|
+
|
|
171
|
+
expect(() =>
|
|
172
|
+
createCredentialDeclaration("TOKEN", {
|
|
173
|
+
...configuration,
|
|
174
|
+
network: [
|
|
175
|
+
{
|
|
176
|
+
domain: "api.example.com",
|
|
177
|
+
transform: [
|
|
178
|
+
{
|
|
179
|
+
headers: {
|
|
180
|
+
Authorization: `Bearer\t${CREDENTIAL_VALUE}`,
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
],
|
|
184
|
+
},
|
|
185
|
+
],
|
|
186
|
+
}),
|
|
187
|
+
).toThrow("cannot contain control characters");
|
|
188
|
+
|
|
189
|
+
for (const template of ["Bearer token", `${CREDENTIAL_VALUE}${CREDENTIAL_VALUE}`]) {
|
|
190
|
+
expect(() =>
|
|
191
|
+
createCredentialDeclaration("TOKEN", {
|
|
192
|
+
...configuration,
|
|
193
|
+
network: [
|
|
194
|
+
{
|
|
195
|
+
domain: "api.example.com",
|
|
196
|
+
transform: [{ headers: { Authorization: template } }],
|
|
197
|
+
},
|
|
198
|
+
],
|
|
199
|
+
}),
|
|
200
|
+
).toThrow("must contain CREDENTIAL_VALUE exactly once");
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("detects header conflicts across overlapping wildcard domains", () => {
|
|
205
|
+
const existing = createCredentialDeclaration("FIRST_TOKEN", {
|
|
206
|
+
network: [
|
|
207
|
+
{
|
|
208
|
+
domain: "*.example.com",
|
|
209
|
+
transform: [{ headers: { Authorization: CREDENTIAL_VALUE } }],
|
|
210
|
+
},
|
|
211
|
+
],
|
|
212
|
+
});
|
|
213
|
+
const overlapping = createCredentialDeclaration("SECOND_TOKEN", {
|
|
214
|
+
network: [
|
|
215
|
+
{
|
|
216
|
+
domain: "api.example.com",
|
|
217
|
+
transform: [{ headers: { authorization: CREDENTIAL_VALUE } }],
|
|
218
|
+
},
|
|
219
|
+
],
|
|
220
|
+
});
|
|
221
|
+
const unrelated = createCredentialDeclaration("THIRD_TOKEN", {
|
|
222
|
+
network: [
|
|
223
|
+
{
|
|
224
|
+
domain: "api.example.net",
|
|
225
|
+
transform: [{ headers: { Authorization: CREDENTIAL_VALUE } }],
|
|
226
|
+
},
|
|
227
|
+
],
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
expect(() => validateCredentialDeclarationConflicts([existing], overlapping)).toThrow(
|
|
231
|
+
'conflicts with credential "FIRST_TOKEN"',
|
|
232
|
+
);
|
|
233
|
+
expect(() => validateCredentialDeclarationConflicts([existing], unrelated)).not.toThrow();
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it("rejects duplicate domains and empty transforms", () => {
|
|
237
|
+
const transform = [{ headers: { Authorization: CREDENTIAL_VALUE } }];
|
|
238
|
+
|
|
239
|
+
expect(() =>
|
|
240
|
+
createCredentialDeclaration("TOKEN", {
|
|
241
|
+
network: [
|
|
242
|
+
{ domain: "API.EXAMPLE.COM", transform },
|
|
243
|
+
{ domain: "api.example.com.", transform },
|
|
244
|
+
],
|
|
245
|
+
}),
|
|
246
|
+
).toThrow("more than once");
|
|
247
|
+
|
|
248
|
+
expect(() =>
|
|
249
|
+
createCredentialDeclaration("TOKEN", {
|
|
250
|
+
network: [{ domain: "api.example.com", transform: [] }],
|
|
251
|
+
}),
|
|
252
|
+
).toThrow("must declare 1-20 transforms");
|
|
253
|
+
|
|
254
|
+
expect(() =>
|
|
255
|
+
createCredentialDeclaration("TOKEN", {
|
|
256
|
+
network: [
|
|
257
|
+
{
|
|
258
|
+
domain: "api.example.com",
|
|
259
|
+
transform: [
|
|
260
|
+
{ headers: { Authorization: CREDENTIAL_VALUE } },
|
|
261
|
+
{ headers: { authorization: CREDENTIAL_VALUE } },
|
|
262
|
+
],
|
|
263
|
+
},
|
|
264
|
+
],
|
|
265
|
+
}),
|
|
266
|
+
).toThrow("declares header");
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it("matches the service bounds for transforms, headers, and strings", () => {
|
|
270
|
+
const tooManyTransforms = Array.from({ length: 21 }, (_, index) => ({
|
|
271
|
+
headers: { [`X-Token-${index}`]: CREDENTIAL_VALUE },
|
|
272
|
+
}));
|
|
273
|
+
const tooManyHeaders = Object.fromEntries(
|
|
274
|
+
Array.from({ length: 21 }, (_, index) => [`X-Token-${index}`, CREDENTIAL_VALUE]),
|
|
275
|
+
);
|
|
276
|
+
const configurations: CredentialConfiguration[] = [
|
|
277
|
+
{
|
|
278
|
+
network: [{ domain: "api.example.com", transform: tooManyTransforms }],
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
network: [
|
|
282
|
+
{
|
|
283
|
+
domain: "api.example.com",
|
|
284
|
+
transform: [{ headers: tooManyHeaders }],
|
|
285
|
+
},
|
|
286
|
+
],
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
network: [
|
|
290
|
+
{
|
|
291
|
+
domain: `${"a.".repeat(126)}com`,
|
|
292
|
+
transform: [{ headers: { Authorization: CREDENTIAL_VALUE } }],
|
|
293
|
+
},
|
|
294
|
+
],
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
network: [
|
|
298
|
+
{
|
|
299
|
+
domain: "api.example.com",
|
|
300
|
+
transform: [{ headers: { ["X".repeat(257)]: CREDENTIAL_VALUE } }],
|
|
301
|
+
},
|
|
302
|
+
],
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
network: [
|
|
306
|
+
{
|
|
307
|
+
domain: "api.example.com",
|
|
308
|
+
transform: [
|
|
309
|
+
{
|
|
310
|
+
headers: {
|
|
311
|
+
Authorization: `${"x".repeat(8 * 1024)}${CREDENTIAL_VALUE}`,
|
|
312
|
+
},
|
|
313
|
+
},
|
|
314
|
+
],
|
|
315
|
+
},
|
|
316
|
+
],
|
|
317
|
+
},
|
|
318
|
+
];
|
|
319
|
+
|
|
320
|
+
for (const configuration of configurations) {
|
|
321
|
+
expect(() => createCredentialDeclaration("TOKEN", configuration)).toThrow();
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
});
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A symbolic marker for the declared credential value. It is safe to include
|
|
3
|
+
* in Worker source and manifests because it never contains the stored secret.
|
|
4
|
+
*/
|
|
5
|
+
export const CREDENTIAL_VALUE = "{{credential}}";
|
|
6
|
+
|
|
7
|
+
/** @internal */
|
|
8
|
+
export const MAX_CREDENTIALS = 100;
|
|
9
|
+
|
|
10
|
+
/** @internal */
|
|
11
|
+
export const MAX_CREDENTIAL_DECLARATIONS_BYTES = 256 * 1024;
|
|
12
|
+
|
|
13
|
+
const MAX_NETWORK_RULES_PER_CREDENTIAL = 20;
|
|
14
|
+
const MAX_TRANSFORMS_PER_NETWORK_RULE = 20;
|
|
15
|
+
const MAX_HEADERS_PER_NETWORK_RULE = 20;
|
|
16
|
+
const MAX_DOMAIN_LENGTH = 253;
|
|
17
|
+
const MAX_HEADER_NAME_LENGTH = 256;
|
|
18
|
+
const MAX_HEADER_TEMPLATE_BYTES = 8 * 1024;
|
|
19
|
+
const ALLOWED_NOTION_CREDENTIAL_KEYS = new Set(["NOTION_API_TOKEN", "NOTION_API_BASE_URL"]);
|
|
20
|
+
const CREDENTIAL_DOMAIN_PATTERN =
|
|
21
|
+
/^(?:\*\.)?(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
22
|
+
|
|
23
|
+
export type CredentialKey = string;
|
|
24
|
+
|
|
25
|
+
/** A Vercel-compatible collection of headers to add or replace. */
|
|
26
|
+
export type CredentialHeaderTransform = {
|
|
27
|
+
readonly headers: Readonly<Record<string, string>>;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Limits credential injection to a domain. This does not grant or deny general
|
|
32
|
+
* network egress; the sandbox's egress policy remains a separate concern.
|
|
33
|
+
*/
|
|
34
|
+
export type CredentialNetworkRule = {
|
|
35
|
+
readonly domain: string;
|
|
36
|
+
readonly transform: readonly CredentialHeaderTransform[];
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export type CredentialConfiguration = {
|
|
40
|
+
/** Destinations and header templates where the credential may be injected. */
|
|
41
|
+
readonly network: readonly CredentialNetworkRule[];
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export type CredentialDeclaration = CredentialConfiguration & {
|
|
45
|
+
readonly key: CredentialKey;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/** @internal */
|
|
49
|
+
export function createCredentialDeclaration(
|
|
50
|
+
key: CredentialKey,
|
|
51
|
+
config: CredentialConfiguration,
|
|
52
|
+
): CredentialDeclaration {
|
|
53
|
+
validateCredentialKey(key);
|
|
54
|
+
|
|
55
|
+
if (config.network.length === 0) {
|
|
56
|
+
throw new Error(`Credential "${key}" must declare at least one network rule`);
|
|
57
|
+
}
|
|
58
|
+
if (config.network.length > MAX_NETWORK_RULES_PER_CREDENTIAL) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
`Credential "${key}" cannot declare more than ${MAX_NETWORK_RULES_PER_CREDENTIAL} domains`,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const domains = new Set<string>();
|
|
65
|
+
const network = config.network.map((rule) => {
|
|
66
|
+
const domain = normalizeDomain(rule.domain);
|
|
67
|
+
if (domains.has(domain)) {
|
|
68
|
+
throw new Error(`Credential "${key}" declares domain "${domain}" more than once`);
|
|
69
|
+
}
|
|
70
|
+
domains.add(domain);
|
|
71
|
+
|
|
72
|
+
if (rule.transform.length === 0 || rule.transform.length > MAX_TRANSFORMS_PER_NETWORK_RULE) {
|
|
73
|
+
throw new Error(
|
|
74
|
+
`Credential "${key}" must declare 1-${MAX_TRANSFORMS_PER_NETWORK_RULE} transforms for domain "${domain}"`,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const headerNames = new Set<string>();
|
|
79
|
+
return {
|
|
80
|
+
domain,
|
|
81
|
+
transform: rule.transform.map((transform) => {
|
|
82
|
+
const headers = normalizeHeaders({
|
|
83
|
+
credentialKey: key,
|
|
84
|
+
domain,
|
|
85
|
+
headers: transform.headers,
|
|
86
|
+
});
|
|
87
|
+
for (const headerName of Object.keys(headers)) {
|
|
88
|
+
const normalizedHeaderName = headerName.toLowerCase();
|
|
89
|
+
if (headerNames.has(normalizedHeaderName)) {
|
|
90
|
+
throw new Error(
|
|
91
|
+
`Credential "${key}" declares header "${headerName}" more than once for domain "${domain}"`,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
headerNames.add(normalizedHeaderName);
|
|
95
|
+
if (headerNames.size > MAX_HEADERS_PER_NETWORK_RULE) {
|
|
96
|
+
throw new Error(
|
|
97
|
+
`Credential "${key}" cannot inject more than ${MAX_HEADERS_PER_NETWORK_RULE} headers for domain "${domain}"`,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return { headers };
|
|
102
|
+
}),
|
|
103
|
+
};
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
key,
|
|
108
|
+
network,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** @internal */
|
|
113
|
+
export function validateCredentialDeclarationConflicts(
|
|
114
|
+
existingDeclarations: Iterable<CredentialDeclaration>,
|
|
115
|
+
declaration: CredentialDeclaration,
|
|
116
|
+
): void {
|
|
117
|
+
for (const existing of existingDeclarations) {
|
|
118
|
+
for (const rule of declaration.network) {
|
|
119
|
+
for (const existingRule of existing.network) {
|
|
120
|
+
if (!credentialDomainsOverlap(rule.domain, existingRule.domain)) {
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const existingHeaders = new Set(
|
|
125
|
+
existingRule.transform.flatMap((transform) =>
|
|
126
|
+
Object.keys(transform.headers).map((name) => name.toLowerCase()),
|
|
127
|
+
),
|
|
128
|
+
);
|
|
129
|
+
for (const transform of rule.transform) {
|
|
130
|
+
for (const headerName of Object.keys(transform.headers)) {
|
|
131
|
+
if (existingHeaders.has(headerName.toLowerCase())) {
|
|
132
|
+
throw new Error(
|
|
133
|
+
`Credential "${declaration.key}" conflicts with credential "${existing.key}" for header "${headerName}" on overlapping domains "${rule.domain}" and "${existingRule.domain}"`,
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function validateCredentialKey(key: CredentialKey): void {
|
|
144
|
+
if (!/^[A-Za-z][A-Za-z0-9_]{0,63}$/.test(key)) {
|
|
145
|
+
throw new Error(
|
|
146
|
+
`Credential key "${key}" must start with a letter, contain only letters, numbers, or underscores, and be at most 64 characters`,
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
const normalizedKey = key.toUpperCase();
|
|
150
|
+
if (normalizedKey.startsWith("NOTION_") && !ALLOWED_NOTION_CREDENTIAL_KEYS.has(normalizedKey)) {
|
|
151
|
+
throw new Error(`Credential key "${key}" uses the reserved NOTION_ prefix`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function normalizeDomain(value: string): string {
|
|
156
|
+
const domain = value.trim().toLowerCase().replace(/\.$/, "");
|
|
157
|
+
if (domain.length > MAX_DOMAIN_LENGTH || !CREDENTIAL_DOMAIN_PATTERN.test(domain)) {
|
|
158
|
+
throw new Error(
|
|
159
|
+
`Credential network domain "${value}" must be an exact hostname or a leading wildcard such as "*.example.com"`,
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
return domain;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function credentialDomainsOverlap(first: string, second: string): boolean {
|
|
166
|
+
if (first === second) {
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const firstBase = first.startsWith("*.") ? first.slice(2) : undefined;
|
|
171
|
+
const secondBase = second.startsWith("*.") ? second.slice(2) : undefined;
|
|
172
|
+
if (firstBase === undefined && secondBase === undefined) {
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
if (firstBase !== undefined && secondBase === undefined) {
|
|
176
|
+
return second.endsWith(`.${firstBase}`);
|
|
177
|
+
}
|
|
178
|
+
if (firstBase === undefined && secondBase !== undefined) {
|
|
179
|
+
return first.endsWith(`.${secondBase}`);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return (
|
|
183
|
+
firstBase !== undefined &&
|
|
184
|
+
secondBase !== undefined &&
|
|
185
|
+
(firstBase.endsWith(`.${secondBase}`) || secondBase.endsWith(`.${firstBase}`))
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function normalizeHeaders(args: {
|
|
190
|
+
credentialKey: string;
|
|
191
|
+
domain: string;
|
|
192
|
+
headers: Readonly<Record<string, string>>;
|
|
193
|
+
}): Readonly<Record<string, string>> {
|
|
194
|
+
const entries = Object.entries(args.headers);
|
|
195
|
+
if (entries.length === 0) {
|
|
196
|
+
throw new Error(
|
|
197
|
+
`Credential "${args.credentialKey}" has an empty header transform for domain "${args.domain}"`,
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return Object.fromEntries(
|
|
202
|
+
entries.map(([name, template]) => {
|
|
203
|
+
if (name.length > MAX_HEADER_NAME_LENGTH || !/^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/.test(name)) {
|
|
204
|
+
throw new Error(`Invalid credential header name "${name}"`);
|
|
205
|
+
}
|
|
206
|
+
if (typeof template !== "string") {
|
|
207
|
+
throw new Error(`Credential header "${name}" must use a string template`);
|
|
208
|
+
}
|
|
209
|
+
validateHeaderTemplate(name, template);
|
|
210
|
+
return [name, template];
|
|
211
|
+
}),
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function validateHeaderTemplate(name: string, template: string): void {
|
|
216
|
+
if (hasControlCharacters(template)) {
|
|
217
|
+
throw new Error(`Credential header "${name}" template cannot contain control characters`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const markerCount = template.split(CREDENTIAL_VALUE).length - 1;
|
|
221
|
+
if (markerCount !== 1) {
|
|
222
|
+
throw new Error(
|
|
223
|
+
`Credential header "${name}" template must contain CREDENTIAL_VALUE exactly once`,
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
if (new TextEncoder().encode(template).byteLength > MAX_HEADER_TEMPLATE_BYTES) {
|
|
227
|
+
throw new Error(
|
|
228
|
+
`Credential header "${name}" template must be at most ${MAX_HEADER_TEMPLATE_BYTES} bytes`,
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function hasControlCharacters(value: string): boolean {
|
|
234
|
+
return Array.from(value).some((character) => {
|
|
235
|
+
const codePoint = character.codePointAt(0);
|
|
236
|
+
return codePoint !== undefined && (codePoint <= 0x1f || codePoint === 0x7f);
|
|
237
|
+
});
|
|
238
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
export type {
|
|
2
|
+
CustomBlockCapability,
|
|
3
|
+
CustomBlockCapabilityConfig,
|
|
4
|
+
CustomBlockConfiguration,
|
|
5
|
+
CustomBlockManifest,
|
|
6
|
+
CustomBlockManifestDataSource,
|
|
7
|
+
} from "./capabilities/custom-block.js";
|
|
1
8
|
export { emojiIcon, imageCover, imageIcon, notionIcon, place } from "./builder.js";
|
|
2
9
|
export type {
|
|
3
10
|
AiConnectorArchetype,
|
|
@@ -51,6 +58,14 @@ export type {
|
|
|
51
58
|
WorkflowEventForTrigger,
|
|
52
59
|
WorkflowEventForTriggers,
|
|
53
60
|
} from "./capabilities/workflow.js";
|
|
61
|
+
export type {
|
|
62
|
+
CredentialConfiguration,
|
|
63
|
+
CredentialDeclaration,
|
|
64
|
+
CredentialHeaderTransform,
|
|
65
|
+
CredentialKey,
|
|
66
|
+
CredentialNetworkRule,
|
|
67
|
+
} from "./credential.js";
|
|
68
|
+
export { CREDENTIAL_VALUE } from "./credential.js";
|
|
54
69
|
export { RateLimitError } from "./error.js";
|
|
55
70
|
export type { AnyJSONSchema, JSONSchema } from "./json-schema.js";
|
|
56
71
|
export type { Infer, SchemaBuilder } from "./schema-builder.js";
|