zitejs 0.9.129 → 0.9.130

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.
@@ -3,12 +3,14 @@ export declare const INTEGRATIONS_CLASS_NAME = "Connections";
3
3
  /**
4
4
  * `accessToken` is the service's primary credential (for Twilio the account SID).
5
5
  * Call `get` where the value is used: a token refreshed mid-run is what the next call returns.
6
+ * For a workspace connection that isn't connected, `get` throws a `ZiteError`
7
+ * whose `userFacingMessage` is safe to show visitors.
6
8
  */
7
9
  export type Connected<Extra = {}> = {
8
10
  status: "connected";
9
11
  accessToken: string;
10
12
  } & Extra;
11
- /** No account behind the entry, or its token could not be refreshed. `connectUrl` is where a person fixes that. */
13
+ /** A per-user entry the signed-in user hasn't connected, or whose token expired: send them to `connectUrl`. */
12
14
  export type NeedsConnect = {
13
15
  status: "missing" | "expired";
14
16
  connectUrl: string;
@@ -19,7 +21,8 @@ export interface IntegrationsClient<ByName extends Record<string, unknown>> {
19
21
  }
20
22
  /**
21
23
  * `ByName` maps each entry name to its answer, e.g.
22
- * `{ crm: Connected<{ instanceUrl: string }> | NeedsConnect }`; `zitejs generate`
23
- * writes it per app into `.zite/integrations/index.ts`.
24
+ * `{ crm: Connected<{ instanceUrl: string }>; inbox: Connected | NeedsConnect }`
25
+ * (only per-user entries can need connecting); `zitejs generate` writes it per
26
+ * app into `.zite/integrations/index.ts`.
24
27
  */
25
28
  export declare function createIntegrationsClient<ByName extends Record<string, unknown>>(): IntegrationsClient<ByName>;
@@ -3,14 +3,14 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.INTEGRATIONS_CLASS_NAME = exports.INTEGRATIONS_SDK_INTEGRATION_ID = void 0;
4
4
  exports.createIntegrationsClient = createIntegrationsClient;
5
5
  const sdkCall_js_1 = require("../internal/sdkCall.js");
6
- // The worker and runner answer this call under its original name; renaming the
7
- // wire protocol would break every deployed bundle for no user-visible gain.
6
+ // The platform id the worker answers `get` under, as `__pdf__` is for PDFs.
8
7
  exports.INTEGRATIONS_SDK_INTEGRATION_ID = "__connections__";
9
8
  exports.INTEGRATIONS_CLASS_NAME = "Connections";
10
9
  /**
11
10
  * `ByName` maps each entry name to its answer, e.g.
12
- * `{ crm: Connected<{ instanceUrl: string }> | NeedsConnect }`; `zitejs generate`
13
- * writes it per app into `.zite/integrations/index.ts`.
11
+ * `{ crm: Connected<{ instanceUrl: string }>; inbox: Connected | NeedsConnect }`
12
+ * (only per-user entries can need connecting); `zitejs generate` writes it per
13
+ * app into `.zite/integrations/index.ts`.
14
14
  */
15
15
  function createIntegrationsClient() {
16
16
  return {
@@ -8,6 +8,7 @@ const index_js_2 = require("../index.js");
8
8
  const client = (0, client_js_1.createIntegrationsClient)();
9
9
  (0, vitest_1.it)("types each name's answer", () => {
10
10
  (0, vitest_1.expectTypeOf)(client.get("crm")).resolves.toEqualTypeOf();
11
+ (0, vitest_1.expectTypeOf)(client.get("inbox")).resolves.toEqualTypeOf();
11
12
  });
12
13
  (0, vitest_1.it)("rejects a name the app cannot see", () => {
13
14
  // @ts-expect-error not a visible entry
@@ -18,23 +19,21 @@ const index_js_2 = require("../index.js");
18
19
  const none = (0, client_js_1.createIntegrationsClient)();
19
20
  (0, vitest_1.expectTypeOf)().toEqualTypeOf();
20
21
  });
21
- (0, vitest_1.it)("narrows to the connected fields on status", async () => {
22
- const crm = await client.get("crm");
23
- if (crm.status === "connected") {
24
- (0, vitest_1.expectTypeOf)(crm.instanceUrl).toEqualTypeOf();
22
+ (0, vitest_1.it)("narrows a per-user entry to the connected fields on status", async () => {
23
+ const inbox = await client.get("inbox");
24
+ if (inbox.status === "connected") {
25
+ (0, vitest_1.expectTypeOf)(inbox.accessToken).toEqualTypeOf();
25
26
  }
26
27
  else {
27
- (0, vitest_1.expectTypeOf)(crm.connectUrl).toEqualTypeOf();
28
+ (0, vitest_1.expectTypeOf)(inbox.connectUrl).toEqualTypeOf();
28
29
  }
29
30
  });
30
31
  });
31
32
  (0, vitest_1.describe)("the untyped zitejs/integrations client", () => {
32
33
  (0, vitest_1.it)("takes any name; only accessToken is certain", async () => {
33
34
  const entry = await index_js_1.integrations.get("anything");
34
- if (entry.status === "connected") {
35
- (0, vitest_1.expectTypeOf)(entry.accessToken).toEqualTypeOf();
36
- (0, vitest_1.expectTypeOf)(entry.instanceUrl).toEqualTypeOf();
37
- }
35
+ (0, vitest_1.expectTypeOf)(entry.accessToken).toEqualTypeOf();
36
+ (0, vitest_1.expectTypeOf)(entry.instanceUrl).toEqualTypeOf();
38
37
  });
39
38
  });
40
39
  (0, vitest_1.describe)("CREDENTIAL_FIELDS", () => {
@@ -1,3 +1,3 @@
1
- import { type Connected, type NeedsConnect } from "./client.js";
1
+ import { type Connected } from "./client.js";
2
2
  export type { Connected, NeedsConnect } from "./client.js";
3
- export declare const integrations: import("./client.js").IntegrationsClient<Record<string, NeedsConnect | Connected<Partial<Record<string, string>>>>>;
3
+ export declare const integrations: import("./client.js").IntegrationsClient<Record<string, Connected<Partial<Record<string, string>>>>>;
@@ -61,9 +61,12 @@ export declare const isCredentialServiceType: (type: string) => type is Credenti
61
61
  export type GettableIntegration = {
62
62
  name: string;
63
63
  serviceType: CredentialServiceType;
64
+ /** Each signed-in user connects their own account, so `get` can answer that they haven't. */
65
+ perUser: boolean;
64
66
  };
65
67
  export type ConfigIntegrations = Readonly<Record<string, {
66
68
  type?: unknown;
69
+ connectionMode?: unknown;
67
70
  } | undefined>>;
68
71
  /**
69
72
  * The entries a source dir can name, by name: its own, then the workspace's under
@@ -105,8 +105,13 @@ const gettableIntegrations = (rootIntegrations, appIntegrations) => {
105
105
  if (typeof entry?.type !== "string")
106
106
  continue;
107
107
  const serviceType = (0, exports.normalizeIntegrationServiceType)(entry.type);
108
- if ((0, exports.isCredentialServiceType)(serviceType))
109
- visible.push({ name, serviceType });
108
+ if ((0, exports.isCredentialServiceType)(serviceType)) {
109
+ visible.push({
110
+ name,
111
+ serviceType,
112
+ perUser: entry.connectionMode === "app-user",
113
+ });
114
+ }
110
115
  }
111
116
  return visible.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
112
117
  };
@@ -47,9 +47,9 @@ const index_js_1 = require("../index.js");
47
47
  (0, vitest_1.describe)("gettableIntegrations", () => {
48
48
  (0, vitest_1.it)("lists the app's entries, then the workspace's under untaken keys, by name", () => {
49
49
  (0, vitest_1.expect)((0, index_js_1.gettableIntegrations)({ crm: { type: "salesforce" }, team: { type: "slack" } }, { billing: { type: "stripe" }, team: { type: "microsoftTeams" } })).toEqual([
50
- { name: "billing", serviceType: "stripe" },
51
- { name: "crm", serviceType: "salesforce" },
52
- { name: "team", serviceType: "microsoftTeams" },
50
+ { name: "billing", serviceType: "stripe", perUser: false },
51
+ { name: "crm", serviceType: "salesforce", perUser: false },
52
+ { name: "team", serviceType: "microsoftTeams", perUser: false },
53
53
  ]);
54
54
  });
55
55
  (0, vitest_1.it)("lets an app entry of a generated-client type shadow a workspace credential entry", () => {
@@ -70,7 +70,7 @@ const index_js_1 = require("../index.js");
70
70
  });
71
71
  (0, vitest_1.it)("normalizes legacy type spellings", () => {
72
72
  (0, vitest_1.expect)((0, index_js_1.gettableIntegrations)({ cal: { type: "google-calendar" } })).toEqual([
73
- { name: "cal", serviceType: "googleCalendar" },
73
+ { name: "cal", serviceType: "googleCalendar", perUser: false },
74
74
  ]);
75
75
  });
76
76
  (0, vitest_1.it)("drops a type it does not know, as the runtime does", () => {
@@ -78,7 +78,16 @@ const index_js_1 = require("../index.js");
78
78
  });
79
79
  (0, vitest_1.it)("shadows by own keys only", () => {
80
80
  (0, vitest_1.expect)((0, index_js_1.gettableIntegrations)({ constructor: { type: "slack" } }, {})).toEqual([
81
- { name: "constructor", serviceType: "slack" },
81
+ { name: "constructor", serviceType: "slack", perUser: false },
82
+ ]);
83
+ });
84
+ (0, vitest_1.it)("marks an entry each signed-in user connects for themselves", () => {
85
+ (0, vitest_1.expect)((0, index_js_1.gettableIntegrations)({
86
+ inbox: { type: "gmail", connectionMode: "app-user" },
87
+ support: { type: "gmail" },
88
+ })).toEqual([
89
+ { name: "inbox", serviceType: "gmail", perUser: true },
90
+ { name: "support", serviceType: "gmail", perUser: false },
82
91
  ]);
83
92
  });
84
93
  (0, vitest_1.it)("works with no configs", () => {
@@ -1427,7 +1427,7 @@ function generateIntegrationsIndex(entries, opts) {
1427
1427
  ? ["export type IntegrationByName = {};"]
1428
1428
  : [
1429
1429
  "export type IntegrationByName = {",
1430
- ...sorted.map(({ name, serviceType }) => ` ${tsSingleQuoted(name)}: ${connectedTypeFor(serviceType)} | NeedsConnect;`),
1430
+ ...sorted.map(({ name, serviceType, perUser }) => ` ${tsSingleQuoted(name)}: ${connectedTypeFor(serviceType)}${perUser ? " | NeedsConnect" : ""};`),
1431
1431
  "};",
1432
1432
  ];
1433
1433
  return [
@@ -1437,8 +1437,10 @@ function generateIntegrationsIndex(entries, opts) {
1437
1437
  "// Usage in endpoints (src/api/*.ts) and automations (src/index.ts):",
1438
1438
  "// import { integrations } from 'zitejs/integrations';",
1439
1439
  "//",
1440
- "// const crm = await integrations.get('<entry name>');",
1441
- "// if (crm.status !== 'connected') { /* crm.connectUrl is where a person reconnects */ }",
1440
+ "// const { accessToken } = await integrations.get('<entry name>');",
1441
+ "//",
1442
+ "// A workspace connection that isn't connected throws a ZiteError. A per-user",
1443
+ "// entry answers `missing`/`expired` with a connectUrl for the signed-in user.",
1442
1444
  "//",
1443
1445
  "// Call get where the value is used, not once at module scope: a token refreshed",
1444
1446
  "// mid-run is what the next call returns.",
@@ -467,8 +467,9 @@ const duplicateIdentifierErrorsIn = (source) => {
467
467
  };
468
468
  (0, vitest_1.it)('emits the client typed by name', () => {
469
469
  (0, vitest_1.expect)((0, lib_js_1.generateIntegrationsIndex)([
470
- { name: 'team', serviceType: 'slack' },
471
- { name: 'crm', serviceType: 'salesforce' },
470
+ { name: 'team', serviceType: 'slack', perUser: false },
471
+ { name: 'crm', serviceType: 'salesforce', perUser: false },
472
+ { name: 'inbox', serviceType: 'gmail', perUser: true },
472
473
  ], { reexportAirtable: false })).toMatchInlineSnapshot(`
473
474
  "// Auto-generated by zitejs generate from zite.config.json. Do not edit manually.
474
475
  // zitejs/integrations — an integration's credentials by its zite.config.json entry name.
@@ -476,8 +477,10 @@ const duplicateIdentifierErrorsIn = (source) => {
476
477
  // Usage in endpoints (src/api/*.ts) and automations (src/index.ts):
477
478
  // import { integrations } from 'zitejs/integrations';
478
479
  //
479
- // const crm = await integrations.get('<entry name>');
480
- // if (crm.status !== 'connected') { /* crm.connectUrl is where a person reconnects */ }
480
+ // const { accessToken } = await integrations.get('<entry name>');
481
+ //
482
+ // A workspace connection that isn't connected throws a ZiteError. A per-user
483
+ // entry answers \`missing\`/\`expired\` with a connectUrl for the signed-in user.
481
484
  //
482
485
  // Call get where the value is used, not once at module scope: a token refreshed
483
486
  // mid-run is what the next call returns.
@@ -487,11 +490,12 @@ const duplicateIdentifierErrorsIn = (source) => {
487
490
 
488
491
  export type { Connected, NeedsConnect };
489
492
 
490
- export type IntegrationName = 'crm' | 'team';
493
+ export type IntegrationName = 'crm' | 'inbox' | 'team';
491
494
 
492
495
  export type IntegrationByName = {
493
- 'crm': Connected<{ instanceUrl: string }> | NeedsConnect;
494
- 'team': Connected | NeedsConnect;
496
+ 'crm': Connected<{ instanceUrl: string }>;
497
+ 'inbox': Connected | NeedsConnect;
498
+ 'team': Connected;
495
499
  };
496
500
 
497
501
  export const integrations = createIntegrationsClient<IntegrationByName>();
@@ -500,33 +504,34 @@ const duplicateIdentifierErrorsIn = (source) => {
500
504
  });
501
505
  // The platform leaves these out for some accounts, so the type can't promise them.
502
506
  (0, vitest_1.it)('marks an extra the platform may omit as optional', () => {
503
- const generated = (0, lib_js_1.generateIntegrationsIndex)([{ name: 'tickets', serviceType: 'jira' }], { reexportAirtable: false });
504
- (0, vitest_1.expect)(generated).toContain("'tickets': Connected<{ cloudId: string; siteUrl?: string }> | NeedsConnect;");
507
+ const generated = (0, lib_js_1.generateIntegrationsIndex)([{ name: 'tickets', serviceType: 'jira', perUser: false }], { reexportAirtable: false });
508
+ (0, vitest_1.expect)(generated).toContain("'tickets': Connected<{ cloudId: string; siteUrl?: string }>;");
505
509
  (0, vitest_1.expect)(typeErrorsIn(generated, `import { integrations } from 'zitejs/integrations';
506
510
  const jira = await integrations.get('tickets');
507
- if (jira.status === 'connected') {
508
- const cloudId: string = jira.cloudId;
509
- const siteUrl: string = jira.siteUrl;
510
- }
511
+ const cloudId: string = jira.cloudId;
512
+ const siteUrl: string = jira.siteUrl;
511
513
  `)).toEqual([
512
514
  vitest_1.expect.stringContaining("Type 'string | undefined' is not assignable"),
513
515
  ]);
514
516
  });
517
+ // A workspace entry is used as is; only a per-user one makes code check first.
515
518
  (0, vitest_1.it)('types get per name in app code', () => {
516
519
  const generated = (0, lib_js_1.generateIntegrationsIndex)([
517
- { name: 'crm', serviceType: 'salesforce' },
518
- { name: 'warehouse', serviceType: 'clickhouse' },
520
+ { name: 'crm', serviceType: 'salesforce', perUser: false },
521
+ { name: 'warehouse', serviceType: 'clickhouse', perUser: false },
522
+ { name: 'inbox', serviceType: 'gmail', perUser: true },
519
523
  ], { reexportAirtable: false });
520
524
  (0, vitest_1.expect)(typeErrorsIn(generated, [
521
525
  "import { integrations } from 'zitejs/integrations';",
522
526
  'export const run = async () => {',
523
527
  " const crm = await integrations.get('crm');",
524
- " if (crm.status === 'connected') return crm.instanceUrl + crm.accessToken;",
525
528
  " const warehouse = await integrations.get('warehouse');",
526
- " if (warehouse.status === 'connected') return warehouse.url + warehouse.username;",
527
- ' return crm.connectUrl;',
529
+ " const inbox = await integrations.get('inbox');",
530
+ " if (inbox.status !== 'connected') return inbox.connectUrl;",
531
+ ' return crm.instanceUrl + warehouse.url + inbox.accessToken;',
528
532
  '};',
529
533
  ].join('\n'))).toEqual([]);
534
+ (0, vitest_1.expect)(typeErrorsIn(generated, "import { integrations } from 'zitejs/integrations';\nexport const f = async () => (await integrations.get('inbox')).accessToken;")).toHaveLength(1);
530
535
  (0, vitest_1.expect)(typeErrorsIn(generated, "import { integrations } from 'zitejs/integrations';\nvoid integrations.get('nope');")).toHaveLength(1);
531
536
  });
532
537
  (0, vitest_1.it)('compiles with nothing visible, naming nothing', () => {
@@ -542,13 +547,13 @@ if (jira.status === 'connected') {
542
547
  ['a newline', 'crm\nprod'],
543
548
  ['a trailing backslash', 'crm\\'],
544
549
  ])('emits parseable TypeScript for a name with %s', (_what, name) => {
545
- (0, vitest_1.expect)(syntaxErrorsIn((0, lib_js_1.generateIntegrationsIndex)([{ name, serviceType: 'slack' }], {
550
+ (0, vitest_1.expect)(syntaxErrorsIn((0, lib_js_1.generateIntegrationsIndex)([{ name, serviceType: 'slack', perUser: false }], {
546
551
  reexportAirtable: false,
547
552
  }))).toEqual([]);
548
553
  });
549
554
  // One module for everything integration: the Airtable clients keep resolving.
550
555
  (0, vitest_1.it)('re-exports the generated Airtable clients beside the integrations client', () => {
551
- const generated = (0, lib_js_1.generateIntegrationsIndex)([{ name: 'team', serviceType: 'slack' }], { reexportAirtable: true });
556
+ const generated = (0, lib_js_1.generateIntegrationsIndex)([{ name: 'team', serviceType: 'slack', perUser: false }], { reexportAirtable: true });
552
557
  (0, vitest_1.expect)(generated).toContain("export * from './airtable';");
553
558
  (0, vitest_1.expect)(typeErrorsIn(generated, [
554
559
  "import { Customers, integrations } from 'zitejs/integrations';",
@@ -3,12 +3,14 @@ export declare const INTEGRATIONS_CLASS_NAME = "Connections";
3
3
  /**
4
4
  * `accessToken` is the service's primary credential (for Twilio the account SID).
5
5
  * Call `get` where the value is used: a token refreshed mid-run is what the next call returns.
6
+ * For a workspace connection that isn't connected, `get` throws a `ZiteError`
7
+ * whose `userFacingMessage` is safe to show visitors.
6
8
  */
7
9
  export type Connected<Extra = {}> = {
8
10
  status: "connected";
9
11
  accessToken: string;
10
12
  } & Extra;
11
- /** No account behind the entry, or its token could not be refreshed. `connectUrl` is where a person fixes that. */
13
+ /** A per-user entry the signed-in user hasn't connected, or whose token expired: send them to `connectUrl`. */
12
14
  export type NeedsConnect = {
13
15
  status: "missing" | "expired";
14
16
  connectUrl: string;
@@ -19,7 +21,8 @@ export interface IntegrationsClient<ByName extends Record<string, unknown>> {
19
21
  }
20
22
  /**
21
23
  * `ByName` maps each entry name to its answer, e.g.
22
- * `{ crm: Connected<{ instanceUrl: string }> | NeedsConnect }`; `zitejs generate`
23
- * writes it per app into `.zite/integrations/index.ts`.
24
+ * `{ crm: Connected<{ instanceUrl: string }>; inbox: Connected | NeedsConnect }`
25
+ * (only per-user entries can need connecting); `zitejs generate` writes it per
26
+ * app into `.zite/integrations/index.ts`.
24
27
  */
25
28
  export declare function createIntegrationsClient<ByName extends Record<string, unknown>>(): IntegrationsClient<ByName>;
@@ -1,12 +1,12 @@
1
1
  import { getSdkCall } from "../internal/sdkCall.js";
2
- // The worker and runner answer this call under its original name; renaming the
3
- // wire protocol would break every deployed bundle for no user-visible gain.
2
+ // The platform id the worker answers `get` under, as `__pdf__` is for PDFs.
4
3
  export const INTEGRATIONS_SDK_INTEGRATION_ID = "__connections__";
5
4
  export const INTEGRATIONS_CLASS_NAME = "Connections";
6
5
  /**
7
6
  * `ByName` maps each entry name to its answer, e.g.
8
- * `{ crm: Connected<{ instanceUrl: string }> | NeedsConnect }`; `zitejs generate`
9
- * writes it per app into `.zite/integrations/index.ts`.
7
+ * `{ crm: Connected<{ instanceUrl: string }>; inbox: Connected | NeedsConnect }`
8
+ * (only per-user entries can need connecting); `zitejs generate` writes it per
9
+ * app into `.zite/integrations/index.ts`.
10
10
  */
11
11
  export function createIntegrationsClient() {
12
12
  return {
@@ -6,6 +6,7 @@ describe("createIntegrationsClient", () => {
6
6
  const client = createIntegrationsClient();
7
7
  it("types each name's answer", () => {
8
8
  expectTypeOf(client.get("crm")).resolves.toEqualTypeOf();
9
+ expectTypeOf(client.get("inbox")).resolves.toEqualTypeOf();
9
10
  });
10
11
  it("rejects a name the app cannot see", () => {
11
12
  // @ts-expect-error not a visible entry
@@ -16,23 +17,21 @@ describe("createIntegrationsClient", () => {
16
17
  const none = createIntegrationsClient();
17
18
  expectTypeOf().toEqualTypeOf();
18
19
  });
19
- it("narrows to the connected fields on status", async () => {
20
- const crm = await client.get("crm");
21
- if (crm.status === "connected") {
22
- expectTypeOf(crm.instanceUrl).toEqualTypeOf();
20
+ it("narrows a per-user entry to the connected fields on status", async () => {
21
+ const inbox = await client.get("inbox");
22
+ if (inbox.status === "connected") {
23
+ expectTypeOf(inbox.accessToken).toEqualTypeOf();
23
24
  }
24
25
  else {
25
- expectTypeOf(crm.connectUrl).toEqualTypeOf();
26
+ expectTypeOf(inbox.connectUrl).toEqualTypeOf();
26
27
  }
27
28
  });
28
29
  });
29
30
  describe("the untyped zitejs/integrations client", () => {
30
31
  it("takes any name; only accessToken is certain", async () => {
31
32
  const entry = await untyped.get("anything");
32
- if (entry.status === "connected") {
33
- expectTypeOf(entry.accessToken).toEqualTypeOf();
34
- expectTypeOf(entry.instanceUrl).toEqualTypeOf();
35
- }
33
+ expectTypeOf(entry.accessToken).toEqualTypeOf();
34
+ expectTypeOf(entry.instanceUrl).toEqualTypeOf();
36
35
  });
37
36
  });
38
37
  describe("CREDENTIAL_FIELDS", () => {
@@ -1,3 +1,3 @@
1
- import { type Connected, type NeedsConnect } from "./client.js";
1
+ import { type Connected } from "./client.js";
2
2
  export type { Connected, NeedsConnect } from "./client.js";
3
- export declare const integrations: import("./client.js").IntegrationsClient<Record<string, NeedsConnect | Connected<Partial<Record<string, string>>>>>;
3
+ export declare const integrations: import("./client.js").IntegrationsClient<Record<string, Connected<Partial<Record<string, string>>>>>;
@@ -1,3 +1,3 @@
1
- import { createIntegrationsClient, } from "./client.js";
1
+ import { createIntegrationsClient } from "./client.js";
2
2
  // What code with no generated `.zite/integrations/index.ts` gets, e.g. a repo-root script.
3
3
  export const integrations = createIntegrationsClient();
@@ -61,9 +61,12 @@ export declare const isCredentialServiceType: (type: string) => type is Credenti
61
61
  export type GettableIntegration = {
62
62
  name: string;
63
63
  serviceType: CredentialServiceType;
64
+ /** Each signed-in user connects their own account, so `get` can answer that they haven't. */
65
+ perUser: boolean;
64
66
  };
65
67
  export type ConfigIntegrations = Readonly<Record<string, {
66
68
  type?: unknown;
69
+ connectionMode?: unknown;
67
70
  } | undefined>>;
68
71
  /**
69
72
  * The entries a source dir can name, by name: its own, then the workspace's under
@@ -100,8 +100,13 @@ export const gettableIntegrations = (rootIntegrations, appIntegrations) => {
100
100
  if (typeof entry?.type !== "string")
101
101
  continue;
102
102
  const serviceType = normalizeIntegrationServiceType(entry.type);
103
- if (isCredentialServiceType(serviceType))
104
- visible.push({ name, serviceType });
103
+ if (isCredentialServiceType(serviceType)) {
104
+ visible.push({
105
+ name,
106
+ serviceType,
107
+ perUser: entry.connectionMode === "app-user",
108
+ });
109
+ }
105
110
  }
106
111
  return visible.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
107
112
  };
@@ -45,9 +45,9 @@ describe("normalizeIntegrationServiceType", () => {
45
45
  describe("gettableIntegrations", () => {
46
46
  it("lists the app's entries, then the workspace's under untaken keys, by name", () => {
47
47
  expect(gettableIntegrations({ crm: { type: "salesforce" }, team: { type: "slack" } }, { billing: { type: "stripe" }, team: { type: "microsoftTeams" } })).toEqual([
48
- { name: "billing", serviceType: "stripe" },
49
- { name: "crm", serviceType: "salesforce" },
50
- { name: "team", serviceType: "microsoftTeams" },
48
+ { name: "billing", serviceType: "stripe", perUser: false },
49
+ { name: "crm", serviceType: "salesforce", perUser: false },
50
+ { name: "team", serviceType: "microsoftTeams", perUser: false },
51
51
  ]);
52
52
  });
53
53
  it("lets an app entry of a generated-client type shadow a workspace credential entry", () => {
@@ -68,7 +68,7 @@ describe("gettableIntegrations", () => {
68
68
  });
69
69
  it("normalizes legacy type spellings", () => {
70
70
  expect(gettableIntegrations({ cal: { type: "google-calendar" } })).toEqual([
71
- { name: "cal", serviceType: "googleCalendar" },
71
+ { name: "cal", serviceType: "googleCalendar", perUser: false },
72
72
  ]);
73
73
  });
74
74
  it("drops a type it does not know, as the runtime does", () => {
@@ -76,7 +76,16 @@ describe("gettableIntegrations", () => {
76
76
  });
77
77
  it("shadows by own keys only", () => {
78
78
  expect(gettableIntegrations({ constructor: { type: "slack" } }, {})).toEqual([
79
- { name: "constructor", serviceType: "slack" },
79
+ { name: "constructor", serviceType: "slack", perUser: false },
80
+ ]);
81
+ });
82
+ it("marks an entry each signed-in user connects for themselves", () => {
83
+ expect(gettableIntegrations({
84
+ inbox: { type: "gmail", connectionMode: "app-user" },
85
+ support: { type: "gmail" },
86
+ })).toEqual([
87
+ { name: "inbox", serviceType: "gmail", perUser: true },
88
+ { name: "support", serviceType: "gmail", perUser: false },
80
89
  ]);
81
90
  });
82
91
  it("works with no configs", () => {
@@ -1412,7 +1412,7 @@ export function generateIntegrationsIndex(entries, opts) {
1412
1412
  ? ["export type IntegrationByName = {};"]
1413
1413
  : [
1414
1414
  "export type IntegrationByName = {",
1415
- ...sorted.map(({ name, serviceType }) => ` ${tsSingleQuoted(name)}: ${connectedTypeFor(serviceType)} | NeedsConnect;`),
1415
+ ...sorted.map(({ name, serviceType, perUser }) => ` ${tsSingleQuoted(name)}: ${connectedTypeFor(serviceType)}${perUser ? " | NeedsConnect" : ""};`),
1416
1416
  "};",
1417
1417
  ];
1418
1418
  return [
@@ -1422,8 +1422,10 @@ export function generateIntegrationsIndex(entries, opts) {
1422
1422
  "// Usage in endpoints (src/api/*.ts) and automations (src/index.ts):",
1423
1423
  "// import { integrations } from 'zitejs/integrations';",
1424
1424
  "//",
1425
- "// const crm = await integrations.get('<entry name>');",
1426
- "// if (crm.status !== 'connected') { /* crm.connectUrl is where a person reconnects */ }",
1425
+ "// const { accessToken } = await integrations.get('<entry name>');",
1426
+ "//",
1427
+ "// A workspace connection that isn't connected throws a ZiteError. A per-user",
1428
+ "// entry answers `missing`/`expired` with a connectUrl for the signed-in user.",
1427
1429
  "//",
1428
1430
  "// Call get where the value is used, not once at module scope: a token refreshed",
1429
1431
  "// mid-run is what the next call returns.",
@@ -462,8 +462,9 @@ describe('generateIntegrationsIndex', () => {
462
462
  };
463
463
  it('emits the client typed by name', () => {
464
464
  expect(generateIntegrationsIndex([
465
- { name: 'team', serviceType: 'slack' },
466
- { name: 'crm', serviceType: 'salesforce' },
465
+ { name: 'team', serviceType: 'slack', perUser: false },
466
+ { name: 'crm', serviceType: 'salesforce', perUser: false },
467
+ { name: 'inbox', serviceType: 'gmail', perUser: true },
467
468
  ], { reexportAirtable: false })).toMatchInlineSnapshot(`
468
469
  "// Auto-generated by zitejs generate from zite.config.json. Do not edit manually.
469
470
  // zitejs/integrations — an integration's credentials by its zite.config.json entry name.
@@ -471,8 +472,10 @@ describe('generateIntegrationsIndex', () => {
471
472
  // Usage in endpoints (src/api/*.ts) and automations (src/index.ts):
472
473
  // import { integrations } from 'zitejs/integrations';
473
474
  //
474
- // const crm = await integrations.get('<entry name>');
475
- // if (crm.status !== 'connected') { /* crm.connectUrl is where a person reconnects */ }
475
+ // const { accessToken } = await integrations.get('<entry name>');
476
+ //
477
+ // A workspace connection that isn't connected throws a ZiteError. A per-user
478
+ // entry answers \`missing\`/\`expired\` with a connectUrl for the signed-in user.
476
479
  //
477
480
  // Call get where the value is used, not once at module scope: a token refreshed
478
481
  // mid-run is what the next call returns.
@@ -482,11 +485,12 @@ describe('generateIntegrationsIndex', () => {
482
485
 
483
486
  export type { Connected, NeedsConnect };
484
487
 
485
- export type IntegrationName = 'crm' | 'team';
488
+ export type IntegrationName = 'crm' | 'inbox' | 'team';
486
489
 
487
490
  export type IntegrationByName = {
488
- 'crm': Connected<{ instanceUrl: string }> | NeedsConnect;
489
- 'team': Connected | NeedsConnect;
491
+ 'crm': Connected<{ instanceUrl: string }>;
492
+ 'inbox': Connected | NeedsConnect;
493
+ 'team': Connected;
490
494
  };
491
495
 
492
496
  export const integrations = createIntegrationsClient<IntegrationByName>();
@@ -495,33 +499,34 @@ describe('generateIntegrationsIndex', () => {
495
499
  });
496
500
  // The platform leaves these out for some accounts, so the type can't promise them.
497
501
  it('marks an extra the platform may omit as optional', () => {
498
- const generated = generateIntegrationsIndex([{ name: 'tickets', serviceType: 'jira' }], { reexportAirtable: false });
499
- expect(generated).toContain("'tickets': Connected<{ cloudId: string; siteUrl?: string }> | NeedsConnect;");
502
+ const generated = generateIntegrationsIndex([{ name: 'tickets', serviceType: 'jira', perUser: false }], { reexportAirtable: false });
503
+ expect(generated).toContain("'tickets': Connected<{ cloudId: string; siteUrl?: string }>;");
500
504
  expect(typeErrorsIn(generated, `import { integrations } from 'zitejs/integrations';
501
505
  const jira = await integrations.get('tickets');
502
- if (jira.status === 'connected') {
503
- const cloudId: string = jira.cloudId;
504
- const siteUrl: string = jira.siteUrl;
505
- }
506
+ const cloudId: string = jira.cloudId;
507
+ const siteUrl: string = jira.siteUrl;
506
508
  `)).toEqual([
507
509
  expect.stringContaining("Type 'string | undefined' is not assignable"),
508
510
  ]);
509
511
  });
512
+ // A workspace entry is used as is; only a per-user one makes code check first.
510
513
  it('types get per name in app code', () => {
511
514
  const generated = generateIntegrationsIndex([
512
- { name: 'crm', serviceType: 'salesforce' },
513
- { name: 'warehouse', serviceType: 'clickhouse' },
515
+ { name: 'crm', serviceType: 'salesforce', perUser: false },
516
+ { name: 'warehouse', serviceType: 'clickhouse', perUser: false },
517
+ { name: 'inbox', serviceType: 'gmail', perUser: true },
514
518
  ], { reexportAirtable: false });
515
519
  expect(typeErrorsIn(generated, [
516
520
  "import { integrations } from 'zitejs/integrations';",
517
521
  'export const run = async () => {',
518
522
  " const crm = await integrations.get('crm');",
519
- " if (crm.status === 'connected') return crm.instanceUrl + crm.accessToken;",
520
523
  " const warehouse = await integrations.get('warehouse');",
521
- " if (warehouse.status === 'connected') return warehouse.url + warehouse.username;",
522
- ' return crm.connectUrl;',
524
+ " const inbox = await integrations.get('inbox');",
525
+ " if (inbox.status !== 'connected') return inbox.connectUrl;",
526
+ ' return crm.instanceUrl + warehouse.url + inbox.accessToken;',
523
527
  '};',
524
528
  ].join('\n'))).toEqual([]);
529
+ expect(typeErrorsIn(generated, "import { integrations } from 'zitejs/integrations';\nexport const f = async () => (await integrations.get('inbox')).accessToken;")).toHaveLength(1);
525
530
  expect(typeErrorsIn(generated, "import { integrations } from 'zitejs/integrations';\nvoid integrations.get('nope');")).toHaveLength(1);
526
531
  });
527
532
  it('compiles with nothing visible, naming nothing', () => {
@@ -537,13 +542,13 @@ if (jira.status === 'connected') {
537
542
  ['a newline', 'crm\nprod'],
538
543
  ['a trailing backslash', 'crm\\'],
539
544
  ])('emits parseable TypeScript for a name with %s', (_what, name) => {
540
- expect(syntaxErrorsIn(generateIntegrationsIndex([{ name, serviceType: 'slack' }], {
545
+ expect(syntaxErrorsIn(generateIntegrationsIndex([{ name, serviceType: 'slack', perUser: false }], {
541
546
  reexportAirtable: false,
542
547
  }))).toEqual([]);
543
548
  });
544
549
  // One module for everything integration: the Airtable clients keep resolving.
545
550
  it('re-exports the generated Airtable clients beside the integrations client', () => {
546
- const generated = generateIntegrationsIndex([{ name: 'team', serviceType: 'slack' }], { reexportAirtable: true });
551
+ const generated = generateIntegrationsIndex([{ name: 'team', serviceType: 'slack', perUser: false }], { reexportAirtable: true });
547
552
  expect(generated).toContain("export * from './airtable';");
548
553
  expect(typeErrorsIn(generated, [
549
554
  "import { Customers, integrations } from 'zitejs/integrations';",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zitejs",
3
- "version": "0.9.129",
3
+ "version": "0.9.130",
4
4
  "description": "The Zite framework — build apps on Zite Database",
5
5
  "type": "module",
6
6
  "sideEffects": false,