zitejs 0.9.119 → 0.9.120

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.
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,96 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vitest_1 = require("vitest");
4
+ /**
5
+ * The signed-out redirect on internal apps.
6
+ *
7
+ * A separate file from `index.test.ts` because it has to replace
8
+ * `better-auth/react` wholesale to drive `useSession`, and that would strip the
9
+ * real client the export tests next door assert against.
10
+ *
11
+ * The value here is entirely in WHICH signed-out renders it reacts to. An
12
+ * external app has a real logged-out state, and an app built before the
13
+ * platform sent an access mode tells us nothing — redirecting either turns a
14
+ * working public page into a forced sign-in.
15
+ */
16
+ const { useSessionMock } = vitest_1.vi.hoisted(() => ({ useSessionMock: vitest_1.vi.fn() }));
17
+ vitest_1.vi.mock('better-auth/react', () => ({
18
+ createAuthClient: () => ({
19
+ useSession: useSessionMock,
20
+ signIn: {},
21
+ signUp: {},
22
+ signOut: vitest_1.vi.fn(),
23
+ updateUser: vitest_1.vi.fn(),
24
+ }),
25
+ }));
26
+ vitest_1.vi.mock('better-auth/client/plugins', () => ({
27
+ magicLinkClient: () => ({}),
28
+ inferAdditionalFields: () => ({}),
29
+ }));
30
+ // `useAuth` takes only `useEffect` from React. Running it inline is the whole
31
+ // of what a render would do here, and avoids pulling in a renderer.
32
+ vitest_1.vi.mock('react', () => ({ useEffect: (fn) => fn() }));
33
+ const index_js_1 = require("./index.js");
34
+ const APP_URL = 'https://app.zite.so/dashboard';
35
+ function stubLocation(href) {
36
+ const location = { pathname: new URL(href).pathname, href };
37
+ vitest_1.vi.stubGlobal('window', { location });
38
+ return location;
39
+ }
40
+ const signedOut = { data: null, isPending: false };
41
+ (0, vitest_1.beforeEach)(() => {
42
+ delete process.env.ZITE_ACCESS_MODE;
43
+ });
44
+ (0, vitest_1.afterEach)(() => {
45
+ delete process.env.ZITE_ACCESS_MODE;
46
+ vitest_1.vi.unstubAllGlobals();
47
+ useSessionMock.mockReset();
48
+ });
49
+ const render = (session, { accessMode, href = APP_URL } = {}) => {
50
+ if (accessMode !== undefined)
51
+ process.env.ZITE_ACCESS_MODE = accessMode;
52
+ const location = stubLocation(href);
53
+ useSessionMock.mockReturnValue(session);
54
+ return { result: (0, index_js_1.useAuth)(), location };
55
+ };
56
+ (0, vitest_1.describe)('useAuth signed-out handling', () => {
57
+ (0, vitest_1.it)('sends a signed-out visitor on an internal app to sign-in', () => {
58
+ const { result, location } = render(signedOut, { accessMode: 'internal' });
59
+ (0, vitest_1.expect)(location.href.startsWith('/auth/login')).toBe(true);
60
+ // Reported as loading rather than signed out: the signed-out branch of an
61
+ // internal app is the empty screen this exists to prevent.
62
+ (0, vitest_1.expect)(result.isLoading).toBe(true);
63
+ });
64
+ (0, vitest_1.it)('leaves a signed-out visitor on an external app alone', () => {
65
+ const { result, location } = render(signedOut, { accessMode: 'external' });
66
+ (0, vitest_1.expect)(location.href).toBe(APP_URL);
67
+ (0, vitest_1.expect)(result.isLoading).toBe(false);
68
+ (0, vitest_1.expect)(result.user).toBe(null);
69
+ });
70
+ // Every app published before the platform started sending an access mode.
71
+ (0, vitest_1.it)('leaves an app that never declared an access mode alone', () => {
72
+ const { result, location } = render(signedOut);
73
+ (0, vitest_1.expect)(location.href).toBe(APP_URL);
74
+ (0, vitest_1.expect)(result.isLoading).toBe(false);
75
+ });
76
+ (0, vitest_1.it)('does not redirect while the session is still resolving', () => {
77
+ const { result, location } = render({ data: null, isPending: true }, { accessMode: 'internal' });
78
+ (0, vitest_1.expect)(location.href).toBe(APP_URL);
79
+ (0, vitest_1.expect)(result.isLoading).toBe(true);
80
+ });
81
+ (0, vitest_1.it)('does not redirect a signed-in visitor', () => {
82
+ const { result, location } = render({ data: { user: { id: 'u1', email: 'a@b.com' } }, isPending: false }, { accessMode: 'internal' });
83
+ (0, vitest_1.expect)(location.href).toBe(APP_URL);
84
+ (0, vitest_1.expect)(result.isLoading).toBe(false);
85
+ (0, vitest_1.expect)(result.user).toMatchObject({ id: 'u1' });
86
+ });
87
+ // `loginWithRedirect` refuses to navigate away from an auth page, so claiming
88
+ // a redirect here would hang the caller on `isLoading` forever.
89
+ (0, vitest_1.it)('does not claim to be loading on an auth page it cannot leave', () => {
90
+ const { result } = render(signedOut, {
91
+ accessMode: 'internal',
92
+ href: 'https://app.zite.so/auth/login',
93
+ });
94
+ (0, vitest_1.expect)(result.isLoading).toBe(false);
95
+ });
96
+ });
@@ -33,24 +33,18 @@ function getFlowId(appDir) {
33
33
  catch { }
34
34
  return undefined;
35
35
  }
36
- /**
37
- * Find the connected email integration's key in an app's zite.config.json,
38
- * if any. The key is the integrationId used by the runtime SDK bridge.
39
- */
40
- function getEmailIntegrationId(appDir) {
36
+ const readJsonFile = (path) => {
41
37
  try {
42
- const configPath = (0, path_1.join)("apps", appDir, "zite.config.json");
43
- if (!(0, fs_2.existsSync)(configPath))
44
- return undefined;
45
- const config = JSON.parse((0, fs_2.readFileSync)(configPath, "utf-8"));
46
- const integrations = config.integrations ?? {};
47
- for (const [id, int] of Object.entries(integrations)) {
48
- if (int?.type === "email")
49
- return id;
50
- }
38
+ return (0, fs_2.existsSync)(path)
39
+ ? JSON.parse((0, fs_2.readFileSync)(path, "utf-8"))
40
+ : undefined;
51
41
  }
52
- catch { }
53
- return undefined;
42
+ catch {
43
+ return undefined;
44
+ }
45
+ };
46
+ function getEmailIntegrationId(appDir) {
47
+ return (0, lib_js_1.findEmailIntegrationId)(readJsonFile((0, path_1.join)("apps", appDir, "zite.config.json")), readJsonFile("zite.config.json"));
54
48
  }
55
49
  function getDeclaredEnvVarNames(appDir) {
56
50
  try {
@@ -0,0 +1,14 @@
1
+ export type ZiteProjectUser = {
2
+ uuid: string;
3
+ firstName: string | null;
4
+ lastName: string | null;
5
+ email: string;
6
+ profilePictureUrl: string | null;
7
+ };
8
+ export type MetaListUsersResult = {
9
+ users: ZiteProjectUser[];
10
+ };
11
+ export declare class ZiteMeta {
12
+ static listUsers(): Promise<MetaListUsersResult>;
13
+ }
14
+ export declare const Meta: typeof ZiteMeta;
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Meta = exports.ZiteMeta = void 0;
4
+ const sdkCall_js_1 = require("../internal/sdkCall.js");
5
+ const META_SDK_INTEGRATION_ID = '__meta__';
6
+ class ZiteMeta {
7
+ static listUsers() {
8
+ return (0, sdkCall_js_1.getSdkCall)()(META_SDK_INTEGRATION_ID, 'ZiteMeta', 'listUsers', {});
9
+ }
10
+ }
11
+ exports.ZiteMeta = ZiteMeta;
12
+ exports.Meta = ZiteMeta;
@@ -0,0 +1,25 @@
1
+ export interface NotificationLink {
2
+ path?: string;
3
+ params?: Record<string, string>;
4
+ }
5
+ export type NotificationsCreateParams = {
6
+ recipients: string[];
7
+ title: string;
8
+ body?: string;
9
+ link?: NotificationLink;
10
+ path?: string;
11
+ params?: Record<string, string>;
12
+ payload?: Record<string, unknown>;
13
+ idempotencyKey?: string;
14
+ };
15
+ export type NotificationsCreateResult = {
16
+ created: number;
17
+ } | {
18
+ created: 0;
19
+ preview: true;
20
+ wouldCreate: number;
21
+ };
22
+ export declare class ZiteNotifications {
23
+ static create(params: NotificationsCreateParams): Promise<NotificationsCreateResult>;
24
+ }
25
+ export declare const Notifications: typeof ZiteNotifications;
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Notifications = exports.ZiteNotifications = void 0;
4
+ const sdkCall_js_1 = require("../internal/sdkCall.js");
5
+ const NOTIFICATIONS_SDK_INTEGRATION_ID = '__notifications__';
6
+ class ZiteNotifications {
7
+ static create(params) {
8
+ return (0, sdkCall_js_1.getSdkCall)()(NOTIFICATIONS_SDK_INTEGRATION_ID, 'ZiteNotifications', 'create', params);
9
+ }
10
+ }
11
+ exports.ZiteNotifications = ZiteNotifications;
12
+ exports.Notifications = ZiteNotifications;
@@ -82,6 +82,12 @@ export declare function normalizeAirtableLockNames(lock: AirtableLock): {
82
82
  };
83
83
  export declare function generateAirtableTs(inputLock: AirtableLock): string | null;
84
84
  export declare function generateBackendWrapperTs(envVarNames?: string[]): string;
85
+ /**
86
+ * The key of the email integration an app sends through, which is the
87
+ * integrationId the runtime SDK bridge dispatches on. The app's own entry wins;
88
+ * otherwise it is the workspace email the app has `integrationSettings` for.
89
+ */
90
+ export declare function findEmailIntegrationId(appConfig: unknown, workspaceConfig: unknown): string | undefined;
85
91
  /**
86
92
  * Generate `.zite/integrations/email.ts` — the `Email` client for an app with
87
93
  * an email integration connected. Mirrors the airtable SDK generation: a thin
@@ -7,6 +7,7 @@ exports.generateApiTs = generateApiTs;
7
7
  exports.normalizeAirtableLockNames = normalizeAirtableLockNames;
8
8
  exports.generateAirtableTs = generateAirtableTs;
9
9
  exports.generateBackendWrapperTs = generateBackendWrapperTs;
10
+ exports.findEmailIntegrationId = findEmailIntegrationId;
10
11
  exports.generateEmailSdk = generateEmailSdk;
11
12
  const parser_1 = require("@babel/parser");
12
13
  const sdkNames_js_1 = require("./sdkNames.js");
@@ -1346,6 +1347,18 @@ function generateBackendWrapperTs(envVarNames = []) {
1346
1347
  "",
1347
1348
  ].join("\n");
1348
1349
  }
1350
+ /**
1351
+ * The key of the email integration an app sends through, which is the
1352
+ * integrationId the runtime SDK bridge dispatches on. The app's own entry wins;
1353
+ * otherwise it is the workspace email the app has `integrationSettings` for.
1354
+ */
1355
+ function findEmailIntegrationId(appConfig, workspaceConfig) {
1356
+ const app = (appConfig ?? {});
1357
+ const workspace = (workspaceConfig ?? {});
1358
+ const isEmail = (config, id) => config.integrations?.[id]?.type === "email";
1359
+ return (Object.keys(app.integrations ?? {}).find((id) => isEmail(app, id)) ??
1360
+ Object.keys(app.integrationSettings ?? {}).find((id) => isEmail(workspace, id)));
1361
+ }
1349
1362
  /**
1350
1363
  * Generate `.zite/integrations/email.ts` — the `Email` client for an app with
1351
1364
  * an email integration connected. Mirrors the airtable SDK generation: a thin
@@ -385,3 +385,31 @@ const duplicateIdentifierErrorsIn = (source) => {
385
385
  (0, vitest_1.expect)(output).toContain('ZiteError');
386
386
  });
387
387
  });
388
+ (0, vitest_1.describe)('findEmailIntegrationId', () => {
389
+ const workspace = {
390
+ integrations: {
391
+ slack: { type: 'slack' },
392
+ 'team-email': { type: 'email' },
393
+ },
394
+ };
395
+ (0, vitest_1.it)('finds the app\'s own email entry', () => {
396
+ (0, vitest_1.expect)((0, lib_js_1.findEmailIntegrationId)({ integrations: { mail: { type: 'email' } } }, workspace)).toBe('mail');
397
+ });
398
+ (0, vitest_1.it)('finds the workspace email the app has settings for', () => {
399
+ (0, vitest_1.expect)((0, lib_js_1.findEmailIntegrationId)({ integrationSettings: { slack: {}, 'team-email': { fromName: 'Ops' } } }, workspace)).toBe('team-email');
400
+ });
401
+ // An app that never opted in generates no client, so `zitejs/email` stays
402
+ // unresolved there instead of sending as a sender it never chose.
403
+ (0, vitest_1.it)('ignores a workspace email the app has no settings for', () => {
404
+ (0, vitest_1.expect)((0, lib_js_1.findEmailIntegrationId)({}, workspace)).toBeUndefined();
405
+ });
406
+ (0, vitest_1.it)('prefers the app\'s own entry over a workspace one', () => {
407
+ (0, vitest_1.expect)((0, lib_js_1.findEmailIntegrationId)({
408
+ integrations: { mail: { type: 'email' } },
409
+ integrationSettings: { 'team-email': {} },
410
+ }, workspace)).toBe('mail');
411
+ });
412
+ (0, vitest_1.it)('tolerates missing configs', () => {
413
+ (0, vitest_1.expect)((0, lib_js_1.findEmailIntegrationId)(undefined, undefined)).toBeUndefined();
414
+ });
415
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,94 @@
1
+ import { describe, it, expect, afterEach, beforeEach, vi } from 'vitest';
2
+ /**
3
+ * The signed-out redirect on internal apps.
4
+ *
5
+ * A separate file from `index.test.ts` because it has to replace
6
+ * `better-auth/react` wholesale to drive `useSession`, and that would strip the
7
+ * real client the export tests next door assert against.
8
+ *
9
+ * The value here is entirely in WHICH signed-out renders it reacts to. An
10
+ * external app has a real logged-out state, and an app built before the
11
+ * platform sent an access mode tells us nothing — redirecting either turns a
12
+ * working public page into a forced sign-in.
13
+ */
14
+ const { useSessionMock } = vi.hoisted(() => ({ useSessionMock: vi.fn() }));
15
+ vi.mock('better-auth/react', () => ({
16
+ createAuthClient: () => ({
17
+ useSession: useSessionMock,
18
+ signIn: {},
19
+ signUp: {},
20
+ signOut: vi.fn(),
21
+ updateUser: vi.fn(),
22
+ }),
23
+ }));
24
+ vi.mock('better-auth/client/plugins', () => ({
25
+ magicLinkClient: () => ({}),
26
+ inferAdditionalFields: () => ({}),
27
+ }));
28
+ // `useAuth` takes only `useEffect` from React. Running it inline is the whole
29
+ // of what a render would do here, and avoids pulling in a renderer.
30
+ vi.mock('react', () => ({ useEffect: (fn) => fn() }));
31
+ import { useAuth } from './index.js';
32
+ const APP_URL = 'https://app.zite.so/dashboard';
33
+ function stubLocation(href) {
34
+ const location = { pathname: new URL(href).pathname, href };
35
+ vi.stubGlobal('window', { location });
36
+ return location;
37
+ }
38
+ const signedOut = { data: null, isPending: false };
39
+ beforeEach(() => {
40
+ delete process.env.ZITE_ACCESS_MODE;
41
+ });
42
+ afterEach(() => {
43
+ delete process.env.ZITE_ACCESS_MODE;
44
+ vi.unstubAllGlobals();
45
+ useSessionMock.mockReset();
46
+ });
47
+ const render = (session, { accessMode, href = APP_URL } = {}) => {
48
+ if (accessMode !== undefined)
49
+ process.env.ZITE_ACCESS_MODE = accessMode;
50
+ const location = stubLocation(href);
51
+ useSessionMock.mockReturnValue(session);
52
+ return { result: useAuth(), location };
53
+ };
54
+ describe('useAuth signed-out handling', () => {
55
+ it('sends a signed-out visitor on an internal app to sign-in', () => {
56
+ const { result, location } = render(signedOut, { accessMode: 'internal' });
57
+ expect(location.href.startsWith('/auth/login')).toBe(true);
58
+ // Reported as loading rather than signed out: the signed-out branch of an
59
+ // internal app is the empty screen this exists to prevent.
60
+ expect(result.isLoading).toBe(true);
61
+ });
62
+ it('leaves a signed-out visitor on an external app alone', () => {
63
+ const { result, location } = render(signedOut, { accessMode: 'external' });
64
+ expect(location.href).toBe(APP_URL);
65
+ expect(result.isLoading).toBe(false);
66
+ expect(result.user).toBe(null);
67
+ });
68
+ // Every app published before the platform started sending an access mode.
69
+ it('leaves an app that never declared an access mode alone', () => {
70
+ const { result, location } = render(signedOut);
71
+ expect(location.href).toBe(APP_URL);
72
+ expect(result.isLoading).toBe(false);
73
+ });
74
+ it('does not redirect while the session is still resolving', () => {
75
+ const { result, location } = render({ data: null, isPending: true }, { accessMode: 'internal' });
76
+ expect(location.href).toBe(APP_URL);
77
+ expect(result.isLoading).toBe(true);
78
+ });
79
+ it('does not redirect a signed-in visitor', () => {
80
+ const { result, location } = render({ data: { user: { id: 'u1', email: 'a@b.com' } }, isPending: false }, { accessMode: 'internal' });
81
+ expect(location.href).toBe(APP_URL);
82
+ expect(result.isLoading).toBe(false);
83
+ expect(result.user).toMatchObject({ id: 'u1' });
84
+ });
85
+ // `loginWithRedirect` refuses to navigate away from an auth page, so claiming
86
+ // a redirect here would hang the caller on `isLoading` forever.
87
+ it('does not claim to be loading on an auth page it cannot leave', () => {
88
+ const { result } = render(signedOut, {
89
+ accessMode: 'internal',
90
+ href: 'https://app.zite.so/auth/login',
91
+ });
92
+ expect(result.isLoading).toBe(false);
93
+ });
94
+ });
@@ -2,7 +2,7 @@ import { watch } from "fs";
2
2
  import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync, } from "fs";
3
3
  import { join } from "path";
4
4
  import { runSync } from "../sync/index.js";
5
- import { generateDbTs, generateApiTs, generateBackendWrapperTs, generateAirtableTs, generateEmailSdk, } from "../sync/lib.js";
5
+ import { generateDbTs, generateApiTs, generateBackendWrapperTs, generateAirtableTs, findEmailIntegrationId, generateEmailSdk, } from "../sync/lib.js";
6
6
  const debounceTimers = new Map();
7
7
  function debounce(key, fn, ms) {
8
8
  const existing = debounceTimers.get(key);
@@ -29,24 +29,18 @@ function getFlowId(appDir) {
29
29
  catch { }
30
30
  return undefined;
31
31
  }
32
- /**
33
- * Find the connected email integration's key in an app's zite.config.json,
34
- * if any. The key is the integrationId used by the runtime SDK bridge.
35
- */
36
- function getEmailIntegrationId(appDir) {
32
+ const readJsonFile = (path) => {
37
33
  try {
38
- const configPath = join("apps", appDir, "zite.config.json");
39
- if (!existsSync(configPath))
40
- return undefined;
41
- const config = JSON.parse(readFileSync(configPath, "utf-8"));
42
- const integrations = config.integrations ?? {};
43
- for (const [id, int] of Object.entries(integrations)) {
44
- if (int?.type === "email")
45
- return id;
46
- }
34
+ return existsSync(path)
35
+ ? JSON.parse(readFileSync(path, "utf-8"))
36
+ : undefined;
47
37
  }
48
- catch { }
49
- return undefined;
38
+ catch {
39
+ return undefined;
40
+ }
41
+ };
42
+ function getEmailIntegrationId(appDir) {
43
+ return findEmailIntegrationId(readJsonFile(join("apps", appDir, "zite.config.json")), readJsonFile("zite.config.json"));
50
44
  }
51
45
  function getDeclaredEnvVarNames(appDir) {
52
46
  try {
@@ -0,0 +1,14 @@
1
+ export type ZiteProjectUser = {
2
+ uuid: string;
3
+ firstName: string | null;
4
+ lastName: string | null;
5
+ email: string;
6
+ profilePictureUrl: string | null;
7
+ };
8
+ export type MetaListUsersResult = {
9
+ users: ZiteProjectUser[];
10
+ };
11
+ export declare class ZiteMeta {
12
+ static listUsers(): Promise<MetaListUsersResult>;
13
+ }
14
+ export declare const Meta: typeof ZiteMeta;
@@ -0,0 +1,8 @@
1
+ import { getSdkCall } from '../internal/sdkCall.js';
2
+ const META_SDK_INTEGRATION_ID = '__meta__';
3
+ export class ZiteMeta {
4
+ static listUsers() {
5
+ return getSdkCall()(META_SDK_INTEGRATION_ID, 'ZiteMeta', 'listUsers', {});
6
+ }
7
+ }
8
+ export const Meta = ZiteMeta;
@@ -0,0 +1,25 @@
1
+ export interface NotificationLink {
2
+ path?: string;
3
+ params?: Record<string, string>;
4
+ }
5
+ export type NotificationsCreateParams = {
6
+ recipients: string[];
7
+ title: string;
8
+ body?: string;
9
+ link?: NotificationLink;
10
+ path?: string;
11
+ params?: Record<string, string>;
12
+ payload?: Record<string, unknown>;
13
+ idempotencyKey?: string;
14
+ };
15
+ export type NotificationsCreateResult = {
16
+ created: number;
17
+ } | {
18
+ created: 0;
19
+ preview: true;
20
+ wouldCreate: number;
21
+ };
22
+ export declare class ZiteNotifications {
23
+ static create(params: NotificationsCreateParams): Promise<NotificationsCreateResult>;
24
+ }
25
+ export declare const Notifications: typeof ZiteNotifications;
@@ -0,0 +1,8 @@
1
+ import { getSdkCall } from '../internal/sdkCall.js';
2
+ const NOTIFICATIONS_SDK_INTEGRATION_ID = '__notifications__';
3
+ export class ZiteNotifications {
4
+ static create(params) {
5
+ return getSdkCall()(NOTIFICATIONS_SDK_INTEGRATION_ID, 'ZiteNotifications', 'create', params);
6
+ }
7
+ }
8
+ export const Notifications = ZiteNotifications;
@@ -82,6 +82,12 @@ export declare function normalizeAirtableLockNames(lock: AirtableLock): {
82
82
  };
83
83
  export declare function generateAirtableTs(inputLock: AirtableLock): string | null;
84
84
  export declare function generateBackendWrapperTs(envVarNames?: string[]): string;
85
+ /**
86
+ * The key of the email integration an app sends through, which is the
87
+ * integrationId the runtime SDK bridge dispatches on. The app's own entry wins;
88
+ * otherwise it is the workspace email the app has `integrationSettings` for.
89
+ */
90
+ export declare function findEmailIntegrationId(appConfig: unknown, workspaceConfig: unknown): string | undefined;
85
91
  /**
86
92
  * Generate `.zite/integrations/email.ts` — the `Email` client for an app with
87
93
  * an email integration connected. Mirrors the airtable SDK generation: a thin
@@ -1333,6 +1333,18 @@ export function generateBackendWrapperTs(envVarNames = []) {
1333
1333
  "",
1334
1334
  ].join("\n");
1335
1335
  }
1336
+ /**
1337
+ * The key of the email integration an app sends through, which is the
1338
+ * integrationId the runtime SDK bridge dispatches on. The app's own entry wins;
1339
+ * otherwise it is the workspace email the app has `integrationSettings` for.
1340
+ */
1341
+ export function findEmailIntegrationId(appConfig, workspaceConfig) {
1342
+ const app = (appConfig ?? {});
1343
+ const workspace = (workspaceConfig ?? {});
1344
+ const isEmail = (config, id) => config.integrations?.[id]?.type === "email";
1345
+ return (Object.keys(app.integrations ?? {}).find((id) => isEmail(app, id)) ??
1346
+ Object.keys(app.integrationSettings ?? {}).find((id) => isEmail(workspace, id)));
1347
+ }
1336
1348
  /**
1337
1349
  * Generate `.zite/integrations/email.ts` — the `Email` client for an app with
1338
1350
  * an email integration connected. Mirrors the airtable SDK generation: a thin
@@ -1,6 +1,6 @@
1
1
  import { describe, it, expect } from 'vitest';
2
2
  import ts from 'typescript';
3
- import { generateAirtableTs, generateApiTs, generateBackendWrapperTs, generateDbTs, generateEmailSdk, } from './lib.js';
3
+ import { findEmailIntegrationId, generateAirtableTs, generateApiTs, generateBackendWrapperTs, generateDbTs, generateEmailSdk, } from './lib.js';
4
4
  /**
5
5
  * Syntax errors in the emitted source. `.zite/db.ts` sits at the repo root and
6
6
  * every app imports it, so anything unparseable here fails typecheck for the
@@ -380,3 +380,31 @@ describe('generateBackendWrapperTs', () => {
380
380
  expect(output).toContain('ZiteError');
381
381
  });
382
382
  });
383
+ describe('findEmailIntegrationId', () => {
384
+ const workspace = {
385
+ integrations: {
386
+ slack: { type: 'slack' },
387
+ 'team-email': { type: 'email' },
388
+ },
389
+ };
390
+ it('finds the app\'s own email entry', () => {
391
+ expect(findEmailIntegrationId({ integrations: { mail: { type: 'email' } } }, workspace)).toBe('mail');
392
+ });
393
+ it('finds the workspace email the app has settings for', () => {
394
+ expect(findEmailIntegrationId({ integrationSettings: { slack: {}, 'team-email': { fromName: 'Ops' } } }, workspace)).toBe('team-email');
395
+ });
396
+ // An app that never opted in generates no client, so `zitejs/email` stays
397
+ // unresolved there instead of sending as a sender it never chose.
398
+ it('ignores a workspace email the app has no settings for', () => {
399
+ expect(findEmailIntegrationId({}, workspace)).toBeUndefined();
400
+ });
401
+ it('prefers the app\'s own entry over a workspace one', () => {
402
+ expect(findEmailIntegrationId({
403
+ integrations: { mail: { type: 'email' } },
404
+ integrationSettings: { 'team-email': {} },
405
+ }, workspace)).toBe('mail');
406
+ });
407
+ it('tolerates missing configs', () => {
408
+ expect(findEmailIntegrationId(undefined, undefined)).toBeUndefined();
409
+ });
410
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zitejs",
3
- "version": "0.9.119",
3
+ "version": "0.9.120",
4
4
  "description": "The Zite framework — build apps on Zite Database",
5
5
  "type": "module",
6
6
  "main": "./dist/cjs/index.js",