@notionhq/workers 0.8.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.
@@ -1,11 +1,26 @@
1
1
  import { describe, expect, it } from "vitest";
2
2
 
3
+ import { CREDENTIAL_VALUE } from "./credential.js";
3
4
  import * as Schema from "./schema.js";
4
5
  import { Worker } from "./worker.js";
5
6
 
6
7
  describe("Worker", () => {
7
- it("includes sdkVersion, databases, and capabilities in manifest", () => {
8
+ it("includes sdkVersion, credentials, databases, and capabilities in manifest", () => {
8
9
  const worker = new Worker();
10
+ worker.credential("LINEAR_API_TOKEN", {
11
+ network: [
12
+ {
13
+ domain: "api.linear.com",
14
+ transform: [
15
+ {
16
+ headers: {
17
+ Authorization: `Bearer ${CREDENTIAL_VALUE}`,
18
+ },
19
+ },
20
+ ],
21
+ },
22
+ ],
23
+ });
9
24
  const tasks = worker.database("tasks", {
10
25
  type: "managed",
11
26
  initialTitle: "Tasks",
@@ -26,6 +41,23 @@ describe("Worker", () => {
26
41
  });
27
42
 
28
43
  expect(worker.manifest.sdkVersion).toMatch(/^\d+\.\d+\.\d+/);
44
+ expect(worker.manifest.credentials).toEqual([
45
+ {
46
+ key: "LINEAR_API_TOKEN",
47
+ network: [
48
+ {
49
+ domain: "api.linear.com",
50
+ transform: [
51
+ {
52
+ headers: {
53
+ Authorization: "Bearer {{credential}}",
54
+ },
55
+ },
56
+ ],
57
+ },
58
+ ],
59
+ },
60
+ ]);
29
61
  expect(worker.manifest.databases).toEqual([
30
62
  {
31
63
  key: "tasks",
@@ -288,7 +320,7 @@ describe("Worker", () => {
288
320
  );
289
321
  });
290
322
 
291
- it("rejects duplicate keys across databases, pacers, and capabilities", () => {
323
+ it("rejects duplicate keys across credentials, databases, pacers, and capabilities", () => {
292
324
  const worker = new Worker();
293
325
  const db = worker.database("shared-key", {
294
326
  type: "managed",
@@ -301,6 +333,23 @@ describe("Worker", () => {
301
333
  },
302
334
  });
303
335
 
336
+ expect(() =>
337
+ worker.credential("shared-key", {
338
+ network: [
339
+ {
340
+ domain: "api.example.com",
341
+ transform: [
342
+ {
343
+ headers: {
344
+ Authorization: CREDENTIAL_VALUE,
345
+ },
346
+ },
347
+ ],
348
+ },
349
+ ],
350
+ }),
351
+ ).toThrow('Worker item with key "shared-key" already registered');
352
+
304
353
  expect(() =>
305
354
  worker.sync("shared-key", {
306
355
  database: db,
@@ -311,4 +360,106 @@ describe("Worker", () => {
311
360
  }),
312
361
  ).toThrow('Worker item with key "shared-key" already registered');
313
362
  });
363
+
364
+ it("does not reserve a key when registration fails", () => {
365
+ const worker = new Worker();
366
+
367
+ expect(() =>
368
+ worker.credential("REUSABLE_KEY", {
369
+ network: [],
370
+ }),
371
+ ).toThrow("must declare at least one network rule");
372
+
373
+ expect(() =>
374
+ worker.pacer("REUSABLE_KEY", {
375
+ allowedRequests: 10,
376
+ intervalMs: 1_000,
377
+ }),
378
+ ).not.toThrow();
379
+ });
380
+
381
+ it("rejects credentials that target the same domain and header", () => {
382
+ const worker = new Worker();
383
+ worker.credential("FIRST_TOKEN", {
384
+ network: [
385
+ {
386
+ domain: "api.example.com",
387
+ transform: [{ headers: { Authorization: `Bearer ${CREDENTIAL_VALUE}` } }],
388
+ },
389
+ ],
390
+ });
391
+
392
+ expect(() =>
393
+ worker.credential("SECOND_TOKEN", {
394
+ network: [
395
+ {
396
+ domain: "API.EXAMPLE.COM",
397
+ transform: [{ headers: { authorization: CREDENTIAL_VALUE } }],
398
+ },
399
+ ],
400
+ }),
401
+ ).toThrow('conflicts with credential "FIRST_TOKEN"');
402
+ });
403
+
404
+ it("limits the number of credential declarations", () => {
405
+ const worker = new Worker();
406
+ for (let index = 0; index < 100; index += 1) {
407
+ worker.credential(`TOKEN_${index}`, {
408
+ network: [
409
+ {
410
+ domain: `api${index}.example.com`,
411
+ transform: [{ headers: { Authorization: CREDENTIAL_VALUE } }],
412
+ },
413
+ ],
414
+ });
415
+ }
416
+
417
+ expect(() =>
418
+ worker.credential("ONE_TOO_MANY", {
419
+ network: [
420
+ {
421
+ domain: "overflow.example.com",
422
+ transform: [{ headers: { Authorization: CREDENTIAL_VALUE } }],
423
+ },
424
+ ],
425
+ }),
426
+ ).toThrow("cannot declare more than 100 credentials");
427
+ });
428
+
429
+ it("matches the service limit for the credential declaration payload", () => {
430
+ const worker = new Worker();
431
+ for (let index = 0; index < 32; index += 1) {
432
+ worker.credential(`TOKEN_${index}`, {
433
+ network: [
434
+ {
435
+ domain: `api${index}.example.com`,
436
+ transform: [
437
+ {
438
+ headers: {
439
+ Authorization: `${"x".repeat(8_000)}${CREDENTIAL_VALUE}`,
440
+ },
441
+ },
442
+ ],
443
+ },
444
+ ],
445
+ });
446
+ }
447
+
448
+ expect(() =>
449
+ worker.credential("TOKEN_32", {
450
+ network: [
451
+ {
452
+ domain: "api32.example.com",
453
+ transform: [
454
+ {
455
+ headers: {
456
+ Authorization: `${"x".repeat(8_000)}${CREDENTIAL_VALUE}`,
457
+ },
458
+ },
459
+ ],
460
+ },
461
+ ],
462
+ }),
463
+ ).toThrow("credential declarations must be at most 262144 bytes");
464
+ });
314
465
  });
package/src/worker.ts CHANGED
@@ -39,6 +39,17 @@ import type {
39
39
  WorkflowEvent,
40
40
  } from "./capabilities/workflow.js";
41
41
  import { createWorkflowCapability } from "./capabilities/workflow.js";
42
+ import type {
43
+ CredentialConfiguration,
44
+ CredentialDeclaration,
45
+ CredentialKey,
46
+ } from "./credential.js";
47
+ import {
48
+ createCredentialDeclaration,
49
+ MAX_CREDENTIAL_DECLARATIONS_BYTES,
50
+ MAX_CREDENTIALS,
51
+ validateCredentialDeclarationConflicts,
52
+ } from "./credential.js";
42
53
  import type { PacerDeclaration } from "./pacer_internal.js";
43
54
  import { pacerWait } from "./pacer_internal.js";
44
55
  import type { Schema } from "./schema.js";
@@ -212,6 +223,7 @@ type CapabilityManifestEntry = Pick<Capability, "_tag" | "key" | "config">;
212
223
  */
213
224
  export type WorkerManifest = {
214
225
  readonly sdkVersion: string;
226
+ readonly credentials: readonly CredentialDeclaration[];
215
227
  readonly databases: readonly RegisteredDatabase[];
216
228
  readonly pacers: readonly PacerDeclaration[];
217
229
  readonly capabilities: readonly CapabilityManifestEntry[];
@@ -243,9 +255,33 @@ const SDK_VERSION = (() => {
243
255
 
244
256
  export class Worker {
245
257
  #capabilities: Map<string, StoredCapability> = new Map();
258
+ #credentials: Map<string, CredentialDeclaration> = new Map();
246
259
  #databases: Map<string, RegisteredDatabase> = new Map();
247
260
  #pacers: Map<string, PacerDeclaration> = new Map();
248
261
 
262
+ /**
263
+ * Declare a credential that is injected into matching outbound HTTPS
264
+ * requests without exposing its value to Worker code.
265
+ */
266
+ credential(key: CredentialKey, config: CredentialConfiguration): void {
267
+ this.#validateUniqueKey(key);
268
+ if (this.#credentials.size >= MAX_CREDENTIALS) {
269
+ throw new Error(`Worker cannot declare more than ${MAX_CREDENTIALS} credentials`);
270
+ }
271
+ const declaration = createCredentialDeclaration(key, config);
272
+ validateCredentialDeclarationConflicts(this.#credentials.values(), declaration);
273
+ const declarations = [...this.#credentials.values(), declaration];
274
+ if (
275
+ new TextEncoder().encode(JSON.stringify(declarations)).byteLength >
276
+ MAX_CREDENTIAL_DECLARATIONS_BYTES
277
+ ) {
278
+ throw new Error(
279
+ `Worker credential declarations must be at most ${MAX_CREDENTIAL_DECLARATIONS_BYTES} bytes`,
280
+ );
281
+ }
282
+ this.#credentials.set(key, declaration);
283
+ }
284
+
249
285
  database<PK extends string, S extends Schema<PK>>(
250
286
  key: string,
251
287
  config: DatabaseConfig<PK, S>,
@@ -675,6 +711,7 @@ export class Worker {
675
711
  get manifest(): WorkerManifest {
676
712
  return {
677
713
  sdkVersion: SDK_VERSION,
714
+ credentials: Array.from(this.#credentials.values()),
678
715
  databases: Array.from(this.#databases.values()),
679
716
  pacers: Array.from(this.#pacers.values()),
680
717
  capabilities: this.capabilities,
@@ -723,7 +760,12 @@ export class Worker {
723
760
  if (!key || typeof key !== "string") {
724
761
  throw new Error("Capability key must be a non-empty string");
725
762
  }
726
- if (this.#capabilities.has(key) || this.#databases.has(key) || this.#pacers.has(key)) {
763
+ if (
764
+ this.#capabilities.has(key) ||
765
+ this.#credentials.has(key) ||
766
+ this.#databases.has(key) ||
767
+ this.#pacers.has(key)
768
+ ) {
727
769
  throw new Error(`Worker item with key "${key}" already registered`);
728
770
  }
729
771
  }