@linked.cm/zoho-cpaas 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,15 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2026 Linked contributors
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,40 @@
1
- # Temporary Holding Version
1
+ # @linked.cm/zoho-cpaas
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ An email provider for LINKED apps: it sends `LinkedEmail` messages (`@_linked/server-utils`), such
4
+ as `@_linked/auth`'s sign-in codes and verification links, through
5
+ [Zoho CPaaS](https://www.zoho.com/zeptomail/) (called ZeptoMail until September 2026). The API
6
+ is unchanged: `POST https://api.zeptomail.com/v1.1/email` with `Authorization: Zoho-enczapikey …`.
7
+
8
+ ## Use
9
+
10
+ ```bash
11
+ npm install @linked.cm/zoho-cpaas
12
+ ```
13
+
14
+ The package's `./backend` is a `BackendProvider`. At boot it registers the provider with
15
+ `LinkedEmail` **only when `ZEPTOMAIL_API_KEY` is set**. Without the key nothing is registered:
16
+ in development `@_linked/auth` then prints what it would have emailed to the server log.
17
+
18
+ | Variable | Required | Meaning |
19
+ |---|---|---|
20
+ | `ZEPTOMAIL_API_KEY` | to send | The Mail Agent's send token, with or without its `Zoho-enczapikey ` prefix. |
21
+ | `ZEPTOMAIL_API_URL` | no | The send endpoint of your region. Default `https://api.zeptomail.com/v1.1/email` (US); `https` only, `http` only on localhost. |
22
+ | `EMAIL_FROM` | no | The sender address, on a domain verified in the Mail Agent. Default `noreply@` the host of `SITE_ROOT`. |
23
+ | `EMAIL_FROM_NAME` | no | The sender's display name. Default none. |
24
+
25
+ A message's own `from` and `bounce_address` (in `EmailOptions`) win over these. Each email gets a
26
+ plain text part made from its HTML; click and open tracking stay off, so links are not
27
+ rewritten. A setting that cannot work stops the boot with a message naming the variable.
28
+
29
+ A failed send rejects with the HTTP status and the API's error codes only, for example
30
+ `Zoho CPaaS refused the email: 401 (TM_3201, SERR_157)`: never the key, the address, the subject
31
+ or the body.
32
+
33
+ Without the framework, the root entry exposes the pieces: `emailProviderFromEnv(env)`,
34
+ `zohoCpaasProvider({transport})`, `zohoCpaasTransport({settings, fetch?})`, `settingsFromEnv(env)`,
35
+ `htmlToText(html)`.
36
+
37
+ ## Develop
38
+
39
+ `npm install && npm run build && npm test && npm run typecheck`. The tests load the built `lib/`
40
+ with a mocked `fetch`; nothing is sent.
@@ -0,0 +1,14 @@
1
+ import { BackendProvider } from '@_linked/server-utils/utils/BackendProvider';
2
+ import './package.js';
3
+ /**
4
+ * Registers Zoho CPaaS as `LinkedEmail`'s provider when `ZEPTOMAIL_API_KEY` is set. Without it
5
+ * nothing is registered, so in development `@_linked/auth` prints what it would have emailed.
6
+ * A setting that cannot work stops the boot with a message naming the variable.
7
+ */
8
+ export default class ZohoCpaasBackendProvider extends BackendProvider {
9
+ setupBeforeControllers(): void;
10
+ }
11
+ /** What `setupBeforeControllers` does, for an environment; returns whether it registered. */
12
+ export declare function registerFromEnv(env: Record<string, string | undefined>, options?: {
13
+ fetch?: typeof globalThis.fetch;
14
+ }): boolean;
@@ -0,0 +1,26 @@
1
+ import { BackendProvider } from '@_linked/server-utils/utils/BackendProvider';
2
+ import { LinkedEmail } from '@_linked/server-utils/utils/LinkedEmail';
3
+ import { zohoCpaasProvider } from './provider.js';
4
+ import './package.js';
5
+ import { settingsFromEnv, zohoCpaasTransport } from './transport.js';
6
+ /**
7
+ * Registers Zoho CPaaS as `LinkedEmail`'s provider when `ZEPTOMAIL_API_KEY` is set. Without it
8
+ * nothing is registered, so in development `@_linked/auth` prints what it would have emailed.
9
+ * A setting that cannot work stops the boot with a message naming the variable.
10
+ */
11
+ export default class ZohoCpaasBackendProvider extends BackendProvider {
12
+ setupBeforeControllers() {
13
+ registerFromEnv(process.env);
14
+ }
15
+ }
16
+ /** What `setupBeforeControllers` does, for an environment; returns whether it registered. */
17
+ export function registerFromEnv(env, options = {}) {
18
+ const settings = settingsFromEnv(env);
19
+ if (!settings)
20
+ return false;
21
+ const transport = zohoCpaasTransport({ settings, ...(options.fetch ? { fetch: options.fetch } : {}) });
22
+ LinkedEmail.setDefaultProvider(zohoCpaasProvider({ transport }));
23
+ console.log(`[zoho-cpaas] email is sent through Zoho CPaaS from ${settings.from.address}`);
24
+ return true;
25
+ }
26
+ //# sourceMappingURL=backend.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"backend.js","sourceRoot":"","sources":["../../src/backend.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,6CAA6C,CAAC;AAC9E,OAAO,EAAE,WAAW,EAAE,MAAM,yCAAyC,CAAC;AACtE,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,cAAc,CAAC;AACtB,OAAO,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAErE;;;;GAIG;AACH,MAAM,CAAC,OAAO,OAAO,wBAAyB,SAAQ,eAAe;IACnE,sBAAsB;QACpB,eAAe,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IAC/B,CAAC;CACF;AAED,6FAA6F;AAC7F,MAAM,UAAU,eAAe,CAC7B,GAAuC,EACvC,UAA+C,EAAE;IAEjD,MAAM,QAAQ,GAAG,eAAe,CAAC,GAAG,CAAC,CAAC;IACtC,IAAI,CAAC,QAAQ;QAAE,OAAO,KAAK,CAAC;IAC5B,MAAM,SAAS,GAAG,kBAAkB,CAAC,EAAE,QAAQ,EAAE,GAAG,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;IACvG,WAAW,CAAC,kBAAkB,CAAC,iBAAiB,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC;IACjE,OAAO,CAAC,GAAG,CAAC,sDAAsD,QAAQ,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC;IAC3F,OAAO,IAAI,CAAC;AACd,CAAC"}
@@ -0,0 +1,3 @@
1
+ import './package.js';
2
+ export { DEFAULT_API_URL, KEY_PREFIX, SEND_TIMEOUT_MS, requestBody, settingsFromEnv, zohoCpaasTransport, type EmailTransport, type InlineImage, type OutgoingEmail, type Sender, type ZohoCpaasSettings, } from './transport.js';
3
+ export { emailProviderFromEnv, htmlToText, toOutgoingEmail, zohoCpaasProvider } from './provider.js';
@@ -0,0 +1,7 @@
1
+ // Registers the package with @_linked/core whichever entry a consumer imports.
2
+ import './package.js';
3
+ // The root entry: the transport, the settings and the provider, with no server dependency.
4
+ // `./backend` is the BackendProvider an app loads to register the provider with LinkedEmail.
5
+ export { DEFAULT_API_URL, KEY_PREFIX, SEND_TIMEOUT_MS, requestBody, settingsFromEnv, zohoCpaasTransport, } from './transport.js';
6
+ export { emailProviderFromEnv, htmlToText, toOutgoingEmail, zohoCpaasProvider } from './provider.js';
7
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,OAAO,cAAc,CAAC;AACtB,2FAA2F;AAC3F,6FAA6F;AAC7F,OAAO,EACL,eAAe,EACf,UAAU,EACV,eAAe,EACf,WAAW,EACX,eAAe,EACf,kBAAkB,GAMnB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,oBAAoB,EAAE,UAAU,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC"}
@@ -0,0 +1,5 @@
1
+ /** This package's own linked identity. */
2
+ export declare const linkedShape: {
3
+ <T extends typeof import("@_linked/core/shapes/Shape.js").Shape>(constructor: T): void;
4
+ <T extends typeof import("@_linked/core/shapes/Shape.js").Shape>(config?: import("@_linked/core/utils/Package").ShapeConfig): (constructor: T) => void;
5
+ }, linkedUtil: (constructor: any) => any, linkedOntology: (allFileExports: any, nameSpace: (term: string) => import("@_linked/core/utils/NodeReference.js").NodeReferenceValue, suggestedPrefixAndFileName: string, loadDataFunction?: () => Promise<unknown>, dataSource?: string | string[]) => void, registerPackageExport: (exportedObject: any) => void, registerPackageModule: (_module: any) => void, getPackageShape: (name: string) => import("@_linked/core/shapes/Shape.js").ShapeConstructor | undefined, packageExports: any, packageName: string;
@@ -0,0 +1,4 @@
1
+ import { linkedPackage } from '@_linked/core/utils/Package';
2
+ /** This package's own linked identity. */
3
+ export const { linkedShape, linkedUtil, linkedOntology, registerPackageExport, registerPackageModule, getPackageShape, packageExports, packageName, } = linkedPackage('@linked.cm/zoho-cpaas');
4
+ //# sourceMappingURL=package.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"package.js","sourceRoot":"","sources":["../../src/package.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAE5D,0CAA0C;AAC1C,MAAM,CAAC,MAAM,EACX,WAAW,EACX,UAAU,EACV,cAAc,EACd,qBAAqB,EACrB,qBAAqB,EACrB,eAAe,EACf,cAAc,EACd,WAAW,GACZ,GAAG,aAAa,CAAC,uBAAuB,CAAC,CAAC"}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * The `LinkedEmail` provider: takes the message the app (or `@_linked/auth`) built, adds a plain
3
+ * text part made from its HTML, and sends it through Zoho CPaaS.
4
+ *
5
+ * Only type imports from the framework, so it loads without a server.
6
+ */
7
+ import type { EmailOptions, IEmailProvider } from '@_linked/server-utils/utils/LinkedEmail';
8
+ import { type EmailTransport, type Env, type OutgoingEmail } from './transport.js';
9
+ /** A plain text version of an HTML email: links kept as their URLs, tags dropped. */
10
+ export declare function htmlToText(html: string): string;
11
+ /** `LinkedEmail`'s options as one outgoing message, with a text part from the HTML. */
12
+ export declare function toOutgoingEmail(options: EmailOptions): OutgoingEmail;
13
+ export declare function zohoCpaasProvider({ transport }: {
14
+ transport: EmailTransport;
15
+ }): IEmailProvider;
16
+ /**
17
+ * The provider for this environment, or `undefined` without `ZEPTOMAIL_API_KEY`. Throws when a
18
+ * setting cannot work.
19
+ */
20
+ export declare function emailProviderFromEnv(env: Env, { fetch }?: {
21
+ fetch?: typeof globalThis.fetch;
22
+ }): IEmailProvider | undefined;
@@ -0,0 +1,52 @@
1
+ import { settingsFromEnv, zohoCpaasTransport } from './transport.js';
2
+ /** A plain text version of an HTML email: links kept as their URLs, tags dropped. */
3
+ export function htmlToText(html) {
4
+ const decode = (text) => text
5
+ .replace(/&lt;/g, '<')
6
+ .replace(/&gt;/g, '>')
7
+ .replace(/&quot;/g, '"')
8
+ .replace(/&#39;/g, "'")
9
+ .replace(/&nbsp;/g, ' ')
10
+ .replace(/&amp;/g, '&');
11
+ return decode(html
12
+ .replace(/<(style|head)[^>]*>[\s\S]*?<\/\1>/gi, '')
13
+ .replace(/<a\b[^>]*\bhref=(['"])(.*?)\1[^>]*>([\s\S]*?)<\/a>/gi, (_m, _q, href, label) => {
14
+ const text = label.replace(/<[^>]+>/g, '').trim();
15
+ return text && text !== href ? `${text}: ${href}` : href;
16
+ })
17
+ .replace(/<br\s*\/?>/gi, '\n')
18
+ .replace(/<\/(p|div|tr|h[1-6]|li|table)>/gi, '\n')
19
+ .replace(/<[^>]+>/g, ''))
20
+ .split('\n')
21
+ .map((line) => line.replace(/[ \t]+/g, ' ').trim())
22
+ .join('\n')
23
+ .replace(/\n{3,}/g, '\n\n')
24
+ .trim();
25
+ }
26
+ /** `LinkedEmail`'s options as one outgoing message, with a text part from the HTML. */
27
+ export function toOutgoingEmail(options) {
28
+ return {
29
+ to: options.to.map(({ email_address }) => ({ address: email_address.address, name: email_address.name })),
30
+ subject: options.subject,
31
+ html: options.htmlbody,
32
+ text: htmlToText(options.htmlbody),
33
+ ...(options.from ? { from: { address: options.from.address, name: options.from.name } } : {}),
34
+ ...(options.bounce_address ? { bounceAddress: options.bounce_address } : {}),
35
+ };
36
+ }
37
+ export function zohoCpaasProvider({ transport }) {
38
+ return {
39
+ send: (options) => transport.send(toOutgoingEmail(options)),
40
+ };
41
+ }
42
+ /**
43
+ * The provider for this environment, or `undefined` without `ZEPTOMAIL_API_KEY`. Throws when a
44
+ * setting cannot work.
45
+ */
46
+ export function emailProviderFromEnv(env, { fetch } = {}) {
47
+ const settings = settingsFromEnv(env);
48
+ if (!settings)
49
+ return undefined;
50
+ return zohoCpaasProvider({ transport: zohoCpaasTransport({ settings, ...(fetch ? { fetch } : {}) }) });
51
+ }
52
+ //# sourceMappingURL=provider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider.js","sourceRoot":"","sources":["../../src/provider.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Zoho CPaaS email (formerly ZeptoMail): its settings from the environment and a transport that
3
+ * sends one message through its send API (`POST /v1.1/email`, JSON). The API kept ZeptoMail's
4
+ * host, auth scheme and variable names when the product was renamed.
5
+ *
6
+ * Pure Node (global `fetch`, injectable): no framework imports.
7
+ */
8
+ /** The send endpoint in the US data centre; other regions have their own host. */
9
+ export declare const DEFAULT_API_URL = "https://api.zeptomail.com/v1.1/email";
10
+ /** How a Mail Agent's send token is presented. */
11
+ export declare const KEY_PREFIX = "Zoho-enczapikey ";
12
+ /** How long one send may take before it counts as failed. */
13
+ export declare const SEND_TIMEOUT_MS = 10000;
14
+ export interface Sender {
15
+ address: string;
16
+ name?: string;
17
+ }
18
+ export interface ZohoCpaasSettings {
19
+ /** The send token, without its `Zoho-enczapikey ` prefix. */
20
+ apiKey: string;
21
+ apiUrl: string;
22
+ from: Sender;
23
+ }
24
+ /** An image the HTML shows as `cid:{cid}`. */
25
+ export interface InlineImage {
26
+ cid: string;
27
+ mimeType: string;
28
+ content: Buffer;
29
+ }
30
+ /** One message as this package sends it. */
31
+ export interface OutgoingEmail {
32
+ to: {
33
+ address: string;
34
+ name?: string;
35
+ }[];
36
+ subject: string;
37
+ html: string;
38
+ text: string;
39
+ /** Overrides the sender from the settings. */
40
+ from?: Sender;
41
+ bounceAddress?: string;
42
+ inlineImages?: InlineImage[];
43
+ }
44
+ export interface EmailTransport {
45
+ send(email: OutgoingEmail): Promise<void>;
46
+ }
47
+ export type Env = Record<string, string | undefined>;
48
+ /**
49
+ * The settings, or `undefined` when `ZEPTOMAIL_API_KEY` is not set (no provider is registered).
50
+ * Throws, naming the variable, for a value that cannot work.
51
+ */
52
+ export declare function settingsFromEnv(env: Env): ZohoCpaasSettings | undefined;
53
+ /** The API's JSON for one message. Tracking stays off: it would rewrite sign-in links. */
54
+ export declare function requestBody(email: OutgoingEmail, from: Sender): Record<string, unknown>;
55
+ /**
56
+ * Send through Zoho CPaaS. A failure rejects with the HTTP status and the API's error codes
57
+ * only: never the key, the address, the subject or the body (which may carry links and codes).
58
+ */
59
+ export declare function zohoCpaasTransport({ settings, fetch: fetchImpl, timeoutMs, }: {
60
+ settings: ZohoCpaasSettings;
61
+ fetch?: typeof fetch;
62
+ timeoutMs?: number;
63
+ }): EmailTransport;
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Zoho CPaaS email (formerly ZeptoMail): its settings from the environment and a transport that
3
+ * sends one message through its send API (`POST /v1.1/email`, JSON). The API kept ZeptoMail's
4
+ * host, auth scheme and variable names when the product was renamed.
5
+ *
6
+ * Pure Node (global `fetch`, injectable): no framework imports.
7
+ */
8
+ /** The send endpoint in the US data centre; other regions have their own host. */
9
+ export const DEFAULT_API_URL = 'https://api.zeptomail.com/v1.1/email';
10
+ /** How a Mail Agent's send token is presented. */
11
+ export const KEY_PREFIX = 'Zoho-enczapikey ';
12
+ /** How long one send may take before it counts as failed. */
13
+ export const SEND_TIMEOUT_MS = 10_000;
14
+ const ADDRESS = /^[^\s@<>"]+@[^\s@<>"]+\.[^\s@<>"]+$/;
15
+ /**
16
+ * The settings, or `undefined` when `ZEPTOMAIL_API_KEY` is not set (no provider is registered).
17
+ * Throws, naming the variable, for a value that cannot work.
18
+ */
19
+ export function settingsFromEnv(env) {
20
+ let apiKey = (env.ZEPTOMAIL_API_KEY ?? '').trim();
21
+ if (!apiKey)
22
+ return undefined;
23
+ // the Mail Agent shows the token with its scheme; either form is accepted
24
+ apiKey = apiKey.replace(/^zoho-enczapikey(\s+|$)/i, '');
25
+ if (!apiKey || /\s/.test(apiKey))
26
+ throw new Error('ZEPTOMAIL_API_KEY is not a Zoho CPaaS send token');
27
+ const apiUrl = (env.ZEPTOMAIL_API_URL ?? '').trim() || DEFAULT_API_URL;
28
+ let url;
29
+ try {
30
+ url = new URL(apiUrl);
31
+ }
32
+ catch {
33
+ throw new Error(`ZEPTOMAIL_API_URL is not a URL: ${apiUrl}`);
34
+ }
35
+ const local = url.hostname === 'localhost' || url.hostname === '127.0.0.1';
36
+ if (url.protocol !== 'https:' && !(url.protocol === 'http:' && local)) {
37
+ throw new Error(`ZEPTOMAIL_API_URL must be https (http only on localhost): ${apiUrl}`);
38
+ }
39
+ let address = (env.EMAIL_FROM ?? '').trim();
40
+ if (!address) {
41
+ let host = '';
42
+ try {
43
+ host = new URL(env.SITE_ROOT ?? '').hostname;
44
+ }
45
+ catch {
46
+ // reported below
47
+ }
48
+ address = host ? `noreply@${host}` : '';
49
+ }
50
+ if (!ADDRESS.test(address)) {
51
+ throw new Error(`EMAIL_FROM must be an address on a sender domain verified in Zoho CPaaS (got "${address}")`);
52
+ }
53
+ const name = (env.EMAIL_FROM_NAME ?? '').trim();
54
+ return { apiKey, apiUrl: url.toString(), from: name ? { address, name } : { address } };
55
+ }
56
+ /** The API's JSON for one message. Tracking stays off: it would rewrite sign-in links. */
57
+ export function requestBody(email, from) {
58
+ const sender = email.from ?? from;
59
+ return {
60
+ from: sender.name ? { address: sender.address, name: sender.name } : { address: sender.address },
61
+ to: email.to.map(({ address, name }) => ({ email_address: { address, name: name || address } })),
62
+ subject: email.subject,
63
+ htmlbody: email.html,
64
+ textbody: email.text,
65
+ ...(email.bounceAddress ? { bounce_address: email.bounceAddress } : {}),
66
+ ...(email.inlineImages?.length
67
+ ? {
68
+ inline_images: email.inlineImages.map((image) => ({
69
+ cid: image.cid,
70
+ mime_type: image.mimeType,
71
+ content: image.content.toString('base64'),
72
+ })),
73
+ }
74
+ : {}),
75
+ track_clicks: false,
76
+ track_opens: false,
77
+ };
78
+ }
79
+ /** The API's error codes from a failed answer (`{error: {code, details: [{code}]}}`). */
80
+ async function errorCodes(response) {
81
+ try {
82
+ const body = (await response.json());
83
+ const codes = [body.error?.code, ...(body.error?.details ?? []).map((d) => d?.code)].filter((code) => typeof code === 'string' && /^[A-Za-z0-9_]{1,32}$/.test(code));
84
+ return codes.length ? ` (${codes.join(', ')})` : '';
85
+ }
86
+ catch {
87
+ return '';
88
+ }
89
+ }
90
+ /**
91
+ * Send through Zoho CPaaS. A failure rejects with the HTTP status and the API's error codes
92
+ * only: never the key, the address, the subject or the body (which may carry links and codes).
93
+ */
94
+ export function zohoCpaasTransport({ settings, fetch: fetchImpl = fetch, timeoutMs = SEND_TIMEOUT_MS, }) {
95
+ return {
96
+ async send(email) {
97
+ let response;
98
+ const controller = new AbortController();
99
+ const timer = setTimeout(() => controller.abort(new DOMException('timed out', 'TimeoutError')), timeoutMs);
100
+ try {
101
+ response = await fetchImpl(settings.apiUrl, {
102
+ method: 'POST',
103
+ headers: {
104
+ accept: 'application/json',
105
+ 'content-type': 'application/json',
106
+ authorization: `${KEY_PREFIX}${settings.apiKey}`,
107
+ },
108
+ body: JSON.stringify(requestBody(email, settings.from)),
109
+ signal: controller.signal,
110
+ redirect: 'error',
111
+ });
112
+ }
113
+ catch (err) {
114
+ const name = err?.name;
115
+ throw new Error(name === 'TimeoutError'
116
+ ? `Zoho CPaaS did not answer in ${timeoutMs} ms`
117
+ : `Zoho CPaaS could not be reached (${name ?? 'error'})`);
118
+ }
119
+ finally {
120
+ clearTimeout(timer);
121
+ }
122
+ if (!response.ok)
123
+ throw new Error(`Zoho CPaaS refused the email: ${response.status}${await errorCodes(response)}`);
124
+ await response.body?.cancel().catch(() => { });
125
+ },
126
+ };
127
+ }
128
+ //# sourceMappingURL=transport.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transport.js","sourceRoot":"","sources":["../../src/transport.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,75 @@
1
1
  {
2
2
  "name": "@linked.cm/zoho-cpaas",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Zoho CPaaS (formerly ZeptoMail) email provider for LINKED apps: registers with LinkedEmail when ZEPTOMAIL_API_KEY is set.",
5
+ "type": "module",
6
+ "linked": {
7
+ "kind": "library"
8
+ },
9
+ "main": "lib/esm/index.js",
10
+ "module": "lib/esm/index.js",
11
+ "types": "lib/esm/index.d.ts",
12
+ "license": "ISC",
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/linked-cm/zoho-cpaas.git"
16
+ },
17
+ "homepage": "https://github.com/linked-cm/zoho-cpaas#readme",
18
+ "bugs": {
19
+ "url": "https://github.com/linked-cm/zoho-cpaas/issues"
20
+ },
21
+ "files": [
22
+ "lib",
23
+ "README.md",
24
+ "LICENSE"
25
+ ],
26
+ "publishConfig": {
27
+ "access": "public"
28
+ },
29
+ "scripts": {
30
+ "start": "npm exec linked dev",
31
+ "build": "linked build",
32
+ "typecheck": "tsc -p tsconfig.json --noEmit",
33
+ "test": "node --test test/*.test.mjs"
34
+ },
35
+ "exports": {
36
+ ".": {
37
+ "types": "./lib/esm/index.d.ts",
38
+ "import": "./lib/esm/index.js"
39
+ },
40
+ "./backend": {
41
+ "types": "./lib/esm/backend.d.ts",
42
+ "import": "./lib/esm/backend.js"
43
+ },
44
+ "./package": {
45
+ "types": "./lib/esm/package.d.ts",
46
+ "import": "./lib/esm/package.js"
47
+ }
48
+ },
49
+ "typesVersions": {
50
+ "*": {
51
+ "*": [
52
+ "lib/esm/*"
53
+ ]
54
+ }
55
+ },
56
+ "dependencies": {
57
+ "@_linked/core": "^2.45.0",
58
+ "@_linked/server-utils": "^1.17.0"
59
+ },
60
+ "devDependencies": {
61
+ "@_linked/cli": "^1.60.0",
62
+ "@changesets/changelog-github": "^1.0.0",
63
+ "@changesets/cli": "^3.0.0",
64
+ "@types/node": "^22.0.0",
65
+ "typescript": "^5.9.2"
66
+ },
67
+ "keywords": [
68
+ "linked",
69
+ "email",
70
+ "zoho",
71
+ "zeptomail",
72
+ "cpaas"
73
+ ],
74
+ "packageManager": "npm@12.2.0"
75
+ }