zitejs 0.9.129 → 0.9.131

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>>>>>;
@@ -47,7 +47,7 @@ export type CredentialServiceType = keyof typeof FIELDS_BY_TYPE;
47
47
  export declare const CREDENTIAL_FIELDS: Readonly<Record<CredentialServiceType, readonly string[]>>;
48
48
  /**
49
49
  * Extras the platform omits when the account doesn't have them: a Jira site
50
- * connected before its URL was stored, a Trello key the deployment never set.
50
+ * connected before its URL was stored.
51
51
  */
52
52
  export declare const OPTIONAL_CREDENTIAL_FIELDS: Readonly<Partial<Record<CredentialServiceType, readonly string[]>>>;
53
53
  /**
@@ -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
@@ -54,11 +54,10 @@ const FIELDS_BY_TYPE = {
54
54
  exports.CREDENTIAL_FIELDS = FIELDS_BY_TYPE;
55
55
  /**
56
56
  * Extras the platform omits when the account doesn't have them: a Jira site
57
- * connected before its URL was stored, a Trello key the deployment never set.
57
+ * connected before its URL was stored.
58
58
  */
59
59
  exports.OPTIONAL_CREDENTIAL_FIELDS = {
60
60
  jira: ["siteUrl"],
61
- trello: ["clientApiKey"],
62
61
  };
63
62
  /**
64
63
  * Types the runtime never serves: those with a generated client (Airtable, email,
@@ -105,8 +104,13 @@ const gettableIntegrations = (rootIntegrations, appIntegrations) => {
105
104
  if (typeof entry?.type !== "string")
106
105
  continue;
107
106
  const serviceType = (0, exports.normalizeIntegrationServiceType)(entry.type);
108
- if ((0, exports.isCredentialServiceType)(serviceType))
109
- visible.push({ name, serviceType });
107
+ if ((0, exports.isCredentialServiceType)(serviceType)) {
108
+ visible.push({
109
+ name,
110
+ serviceType,
111
+ perUser: entry.connectionMode === "app-user",
112
+ });
113
+ }
110
114
  }
111
115
  return visible.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
112
116
  };
@@ -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,36 @@ 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 }>;");
509
+ // The runner supplies Trello's key for every account, or refuses get.
510
+ (0, vitest_1.expect)((0, lib_js_1.generateIntegrationsIndex)([{ name: 'boards', serviceType: 'trello', perUser: false }], { reexportAirtable: false })).toContain("'boards': Connected<{ clientApiKey: string }>;");
505
511
  (0, vitest_1.expect)(typeErrorsIn(generated, `import { integrations } from 'zitejs/integrations';
506
512
  const jira = await integrations.get('tickets');
507
- if (jira.status === 'connected') {
508
- const cloudId: string = jira.cloudId;
509
- const siteUrl: string = jira.siteUrl;
510
- }
513
+ const cloudId: string = jira.cloudId;
514
+ const siteUrl: string = jira.siteUrl;
511
515
  `)).toEqual([
512
516
  vitest_1.expect.stringContaining("Type 'string | undefined' is not assignable"),
513
517
  ]);
514
518
  });
519
+ // A workspace entry is used as is; only a per-user one makes code check first.
515
520
  (0, vitest_1.it)('types get per name in app code', () => {
516
521
  const generated = (0, lib_js_1.generateIntegrationsIndex)([
517
- { name: 'crm', serviceType: 'salesforce' },
518
- { name: 'warehouse', serviceType: 'clickhouse' },
522
+ { name: 'crm', serviceType: 'salesforce', perUser: false },
523
+ { name: 'warehouse', serviceType: 'clickhouse', perUser: false },
524
+ { name: 'inbox', serviceType: 'gmail', perUser: true },
519
525
  ], { reexportAirtable: false });
520
526
  (0, vitest_1.expect)(typeErrorsIn(generated, [
521
527
  "import { integrations } from 'zitejs/integrations';",
522
528
  'export const run = async () => {',
523
529
  " const crm = await integrations.get('crm');",
524
- " if (crm.status === 'connected') return crm.instanceUrl + crm.accessToken;",
525
530
  " const warehouse = await integrations.get('warehouse');",
526
- " if (warehouse.status === 'connected') return warehouse.url + warehouse.username;",
527
- ' return crm.connectUrl;',
531
+ " const inbox = await integrations.get('inbox');",
532
+ " if (inbox.status !== 'connected') return inbox.connectUrl;",
533
+ ' return crm.instanceUrl + warehouse.url + inbox.accessToken;',
528
534
  '};',
529
535
  ].join('\n'))).toEqual([]);
536
+ (0, vitest_1.expect)(typeErrorsIn(generated, "import { integrations } from 'zitejs/integrations';\nexport const f = async () => (await integrations.get('inbox')).accessToken;")).toHaveLength(1);
530
537
  (0, vitest_1.expect)(typeErrorsIn(generated, "import { integrations } from 'zitejs/integrations';\nvoid integrations.get('nope');")).toHaveLength(1);
531
538
  });
532
539
  (0, vitest_1.it)('compiles with nothing visible, naming nothing', () => {
@@ -542,13 +549,13 @@ if (jira.status === 'connected') {
542
549
  ['a newline', 'crm\nprod'],
543
550
  ['a trailing backslash', 'crm\\'],
544
551
  ])('emits parseable TypeScript for a name with %s', (_what, name) => {
545
- (0, vitest_1.expect)(syntaxErrorsIn((0, lib_js_1.generateIntegrationsIndex)([{ name, serviceType: 'slack' }], {
552
+ (0, vitest_1.expect)(syntaxErrorsIn((0, lib_js_1.generateIntegrationsIndex)([{ name, serviceType: 'slack', perUser: false }], {
546
553
  reexportAirtable: false,
547
554
  }))).toEqual([]);
548
555
  });
549
556
  // One module for everything integration: the Airtable clients keep resolving.
550
557
  (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 });
558
+ const generated = (0, lib_js_1.generateIntegrationsIndex)([{ name: 'team', serviceType: 'slack', perUser: false }], { reexportAirtable: true });
552
559
  (0, vitest_1.expect)(generated).toContain("export * from './airtable';");
553
560
  (0, vitest_1.expect)(typeErrorsIn(generated, [
554
561
  "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();
@@ -47,7 +47,7 @@ export type CredentialServiceType = keyof typeof FIELDS_BY_TYPE;
47
47
  export declare const CREDENTIAL_FIELDS: Readonly<Record<CredentialServiceType, readonly string[]>>;
48
48
  /**
49
49
  * Extras the platform omits when the account doesn't have them: a Jira site
50
- * connected before its URL was stored, a Trello key the deployment never set.
50
+ * connected before its URL was stored.
51
51
  */
52
52
  export declare const OPTIONAL_CREDENTIAL_FIELDS: Readonly<Partial<Record<CredentialServiceType, readonly string[]>>>;
53
53
  /**
@@ -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
@@ -51,11 +51,10 @@ const FIELDS_BY_TYPE = {
51
51
  export const CREDENTIAL_FIELDS = FIELDS_BY_TYPE;
52
52
  /**
53
53
  * Extras the platform omits when the account doesn't have them: a Jira site
54
- * connected before its URL was stored, a Trello key the deployment never set.
54
+ * connected before its URL was stored.
55
55
  */
56
56
  export const OPTIONAL_CREDENTIAL_FIELDS = {
57
57
  jira: ["siteUrl"],
58
- trello: ["clientApiKey"],
59
58
  };
60
59
  /**
61
60
  * Types the runtime never serves: those with a generated client (Airtable, email,
@@ -100,8 +99,13 @@ export const gettableIntegrations = (rootIntegrations, appIntegrations) => {
100
99
  if (typeof entry?.type !== "string")
101
100
  continue;
102
101
  const serviceType = normalizeIntegrationServiceType(entry.type);
103
- if (isCredentialServiceType(serviceType))
104
- visible.push({ name, serviceType });
102
+ if (isCredentialServiceType(serviceType)) {
103
+ visible.push({
104
+ name,
105
+ serviceType,
106
+ perUser: entry.connectionMode === "app-user",
107
+ });
108
+ }
105
109
  }
106
110
  return visible.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
107
111
  };
@@ -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,36 @@ 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 }>;");
504
+ // The runner supplies Trello's key for every account, or refuses get.
505
+ expect(generateIntegrationsIndex([{ name: 'boards', serviceType: 'trello', perUser: false }], { reexportAirtable: false })).toContain("'boards': Connected<{ clientApiKey: string }>;");
500
506
  expect(typeErrorsIn(generated, `import { integrations } from 'zitejs/integrations';
501
507
  const jira = await integrations.get('tickets');
502
- if (jira.status === 'connected') {
503
- const cloudId: string = jira.cloudId;
504
- const siteUrl: string = jira.siteUrl;
505
- }
508
+ const cloudId: string = jira.cloudId;
509
+ const siteUrl: string = jira.siteUrl;
506
510
  `)).toEqual([
507
511
  expect.stringContaining("Type 'string | undefined' is not assignable"),
508
512
  ]);
509
513
  });
514
+ // A workspace entry is used as is; only a per-user one makes code check first.
510
515
  it('types get per name in app code', () => {
511
516
  const generated = generateIntegrationsIndex([
512
- { name: 'crm', serviceType: 'salesforce' },
513
- { name: 'warehouse', serviceType: 'clickhouse' },
517
+ { name: 'crm', serviceType: 'salesforce', perUser: false },
518
+ { name: 'warehouse', serviceType: 'clickhouse', perUser: false },
519
+ { name: 'inbox', serviceType: 'gmail', perUser: true },
514
520
  ], { reexportAirtable: false });
515
521
  expect(typeErrorsIn(generated, [
516
522
  "import { integrations } from 'zitejs/integrations';",
517
523
  'export const run = async () => {',
518
524
  " const crm = await integrations.get('crm');",
519
- " if (crm.status === 'connected') return crm.instanceUrl + crm.accessToken;",
520
525
  " const warehouse = await integrations.get('warehouse');",
521
- " if (warehouse.status === 'connected') return warehouse.url + warehouse.username;",
522
- ' return crm.connectUrl;',
526
+ " const inbox = await integrations.get('inbox');",
527
+ " if (inbox.status !== 'connected') return inbox.connectUrl;",
528
+ ' return crm.instanceUrl + warehouse.url + inbox.accessToken;',
523
529
  '};',
524
530
  ].join('\n'))).toEqual([]);
531
+ expect(typeErrorsIn(generated, "import { integrations } from 'zitejs/integrations';\nexport const f = async () => (await integrations.get('inbox')).accessToken;")).toHaveLength(1);
525
532
  expect(typeErrorsIn(generated, "import { integrations } from 'zitejs/integrations';\nvoid integrations.get('nope');")).toHaveLength(1);
526
533
  });
527
534
  it('compiles with nothing visible, naming nothing', () => {
@@ -537,13 +544,13 @@ if (jira.status === 'connected') {
537
544
  ['a newline', 'crm\nprod'],
538
545
  ['a trailing backslash', 'crm\\'],
539
546
  ])('emits parseable TypeScript for a name with %s', (_what, name) => {
540
- expect(syntaxErrorsIn(generateIntegrationsIndex([{ name, serviceType: 'slack' }], {
547
+ expect(syntaxErrorsIn(generateIntegrationsIndex([{ name, serviceType: 'slack', perUser: false }], {
541
548
  reexportAirtable: false,
542
549
  }))).toEqual([]);
543
550
  });
544
551
  // One module for everything integration: the Airtable clients keep resolving.
545
552
  it('re-exports the generated Airtable clients beside the integrations client', () => {
546
- const generated = generateIntegrationsIndex([{ name: 'team', serviceType: 'slack' }], { reexportAirtable: true });
553
+ const generated = generateIntegrationsIndex([{ name: 'team', serviceType: 'slack', perUser: false }], { reexportAirtable: true });
547
554
  expect(generated).toContain("export * from './airtable';");
548
555
  expect(typeErrorsIn(generated, [
549
556
  "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.131",
4
4
  "description": "The Zite framework — build apps on Zite Database",
5
5
  "type": "module",
6
6
  "sideEffects": false,