@durgadigital/narada 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 +21 -0
- package/README.md +89 -2
- package/dist/index.d.ts +128 -0
- package/dist/index.js +140 -0
- package/dist/types.d.ts +215 -0
- package/dist/types.js +1 -0
- package/package.json +32 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Durga Digital
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,90 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Narada Node SDK
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Typed, dependency-free, server-side client for your Narada installation. Requires Node.js 22+. MIT licensed.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
npm install @durgadigital/narada
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
From the monorepo:
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
pnpm --filter @durgadigital/narada build
|
|
13
|
+
pnpm --filter @durgadigital/narada pack
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Install the resulting `durgadigital-narada-0.1.0.tgz` in another application with `npm install /path/to/durgadigital-narada-0.1.0.tgz`. Inside this workspace, declare `"@durgadigital/narada": "workspace:*"` and build the SDK first. The archive includes JavaScript and TypeScript declarations; it does not require the Narada server source or TypeScript at runtime. This release provides an ESM entry point.
|
|
17
|
+
|
|
18
|
+
```js
|
|
19
|
+
import { Narada, NaradaError } from "@durgadigital/narada";
|
|
20
|
+
|
|
21
|
+
const narada = new Narada({
|
|
22
|
+
baseUrl: "http://localhost:5173", // dashboard origin, without /api/v1
|
|
23
|
+
apiKey: process.env.NARADA_API_KEY,
|
|
24
|
+
timeoutMs: 30000,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const result = await narada.emails.send(
|
|
28
|
+
{
|
|
29
|
+
from: "hello@narada.test",
|
|
30
|
+
to: "alex@example.com",
|
|
31
|
+
subject: "Welcome",
|
|
32
|
+
text: "Your account is ready.",
|
|
33
|
+
},
|
|
34
|
+
{ idempotencyKey: "welcome-user-123" },
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
console.log(await narada.emails.get(result.id));
|
|
39
|
+
} catch (error) {
|
|
40
|
+
if (error instanceof NaradaError) {
|
|
41
|
+
console.error(error.status, error.code, error.requestId);
|
|
42
|
+
} else throw error;
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Resources: `templates`, `emails`, `broadcasts`, `contacts`, `contactProperties`, `segments`, and `topics`. `send` remains an alias for `emails.send`. Management resources require a **Full access** API key; existing and default keys retain email sending/status access only. Use keys on your server, never in a browser bundle.
|
|
47
|
+
|
|
48
|
+
List methods return `{ data, nextCursor }`. Pass `{ limit: 20, cursor: previous.nextCursor }` to continue. A null cursor ends the list. Lists are eventually consistent, not a transactionally frozen view.
|
|
49
|
+
|
|
50
|
+
Update methods require the latest `revision` and a full resource input (`draft`, `contact`, `property`, `segment`, or `topic`), not a patch. Fetch again after a 409; do not overwrite another writer blindly. Contacts are addressed by email; properties by key; other resources by UUID.
|
|
51
|
+
|
|
52
|
+
```js
|
|
53
|
+
const topic = await narada.topics.create({ name: "Product updates" });
|
|
54
|
+
const segment = await narada.segments.create({
|
|
55
|
+
name: "Customers",
|
|
56
|
+
kind: "manual",
|
|
57
|
+
});
|
|
58
|
+
await narada.segments.import(segment.id, {
|
|
59
|
+
csv: "email,first_name\nalex@example.com,Alex",
|
|
60
|
+
consentSource: "Explicit signup form",
|
|
61
|
+
consentConfirmed: true,
|
|
62
|
+
topicIds: [topic.id],
|
|
63
|
+
});
|
|
64
|
+
const broadcast = await narada.broadcasts.create({
|
|
65
|
+
name: "October update",
|
|
66
|
+
audienceId: segment.id,
|
|
67
|
+
topicId: topic.id,
|
|
68
|
+
from: "hello@narada.test",
|
|
69
|
+
subject: "Hello {{contact.first_name}}",
|
|
70
|
+
text: "Here is the latest news.",
|
|
71
|
+
organization: "Example company",
|
|
72
|
+
postalAddress: "123 Example Street, Sydney NSW 2000",
|
|
73
|
+
});
|
|
74
|
+
console.log(await narada.broadcasts.preview(broadcast.id));
|
|
75
|
+
// Queues delivery to eligible recipients; use the current revision after any edits.
|
|
76
|
+
await narada.broadcasts.send(broadcast.id, {
|
|
77
|
+
revision: broadcast.revision,
|
|
78
|
+
confirmed: true,
|
|
79
|
+
});
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Requests time out after 30 seconds by default and are **never retried automatically**. Network/abort errors propagate; HTTP errors throw `NaradaError` with `status`, `code`, `requestId`, and optional `retryAfter` seconds. A timeout does not imply the server rejected a mutation. Only email submission supports `Idempotency-Key`; reuse it for retries. Broadcast launch is guarded by revision and state—retrieve it after an uncertain outcome. Creating resources, duplicating broadcasts, and test sends have no idempotency guarantee.
|
|
83
|
+
|
|
84
|
+
Use `fetch` in constructor options to inject a test transport. An optional `signal` is supported on `emails.send`. API keys are not forwarded across redirects.
|
|
85
|
+
|
|
86
|
+
See the dashboard API reference for copyable Node.js and cURL examples for every supported operation. Automations, property deletion, and whole-contact/segment deletion are not supported in this release. Topics can be archived with `topics.update`.
|
|
87
|
+
|
|
88
|
+
## Templates
|
|
89
|
+
|
|
90
|
+
Use `templates.create`, `get`, `list`, `update`, `publish`, `preview`, `test`, `duplicate`, `archive`, and `createBroadcast` with a Full access key. Updates/publication require the latest revision. Sending keys can use `emails.send({from, to, template: {id: "welcome-email", version: 1, variables: {first_name: "Alex"}}})`. Pin the publication version to keep retries stable. Draft changes do not affect published content or already accepted emails.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import type * as T from "./types.js";
|
|
2
|
+
export type * from "./types.js";
|
|
3
|
+
export declare class NaradaError extends Error {
|
|
4
|
+
readonly status: number;
|
|
5
|
+
readonly code?: string | undefined;
|
|
6
|
+
readonly requestId?: string | undefined;
|
|
7
|
+
readonly retryAfter?: number | undefined;
|
|
8
|
+
constructor(message: string, status: number, code?: string | undefined, requestId?: string | undefined, retryAfter?: number | undefined);
|
|
9
|
+
}
|
|
10
|
+
export interface NaradaOptions {
|
|
11
|
+
apiKey: string;
|
|
12
|
+
baseUrl: string;
|
|
13
|
+
timeoutMs?: number;
|
|
14
|
+
fetch?: typeof fetch;
|
|
15
|
+
}
|
|
16
|
+
export interface RequestOptions {
|
|
17
|
+
idempotencyKey?: string;
|
|
18
|
+
signal?: AbortSignal;
|
|
19
|
+
}
|
|
20
|
+
/** Server-side client. Requests are never retried implicitly, including timeouts. */
|
|
21
|
+
export declare class Narada {
|
|
22
|
+
private readonly options;
|
|
23
|
+
private readonly baseUrl;
|
|
24
|
+
private readonly transport;
|
|
25
|
+
constructor(options: NaradaOptions);
|
|
26
|
+
private request;
|
|
27
|
+
/** Backward-compatible alias for emails.send. */
|
|
28
|
+
send: (input: T.EmailInput | T.TemplateEmailInput, options?: RequestOptions) => Promise<T.EmailAccepted>;
|
|
29
|
+
templates: {
|
|
30
|
+
list: (options?: T.ListOptions) => Promise<T.Page<T.Template>>;
|
|
31
|
+
get: (id: string) => Promise<T.Template>;
|
|
32
|
+
create: (input: T.TemplateContent & {
|
|
33
|
+
alias: string;
|
|
34
|
+
}) => Promise<T.Template>;
|
|
35
|
+
update: (id: string, input: {
|
|
36
|
+
draft: T.TemplateContent;
|
|
37
|
+
revision: number;
|
|
38
|
+
}) => Promise<T.Template>;
|
|
39
|
+
publish: (id: string, input: {
|
|
40
|
+
revision: number;
|
|
41
|
+
}) => Promise<T.Template>;
|
|
42
|
+
archive: (id: string, input: {
|
|
43
|
+
archived: boolean;
|
|
44
|
+
revision: number;
|
|
45
|
+
}) => Promise<T.Template>;
|
|
46
|
+
duplicate: (id: string, input: {
|
|
47
|
+
alias: string;
|
|
48
|
+
}) => Promise<T.Template>;
|
|
49
|
+
preview: (id: string, input?: T.TemplatePreviewInput) => Promise<{
|
|
50
|
+
subject: string;
|
|
51
|
+
html: string;
|
|
52
|
+
text: string;
|
|
53
|
+
}>;
|
|
54
|
+
test: (id: string, input: {
|
|
55
|
+
from: string;
|
|
56
|
+
to: string;
|
|
57
|
+
variables?: Record<string, T.PropertyValue>;
|
|
58
|
+
}) => Promise<T.EmailAccepted>;
|
|
59
|
+
createBroadcast: (id: string, input?: T.TemplateBroadcastInput) => Promise<T.Broadcast>;
|
|
60
|
+
};
|
|
61
|
+
emails: {
|
|
62
|
+
send: (input: T.EmailInput | T.TemplateEmailInput, options?: RequestOptions) => Promise<T.EmailAccepted>;
|
|
63
|
+
get: (id: string) => Promise<T.EmailStatus>;
|
|
64
|
+
};
|
|
65
|
+
contactProperties: {
|
|
66
|
+
list: (options?: T.ListOptions) => Promise<T.Page<T.Property>>;
|
|
67
|
+
get: (key: string) => Promise<T.Property>;
|
|
68
|
+
create: (input: T.PropertyInput) => Promise<T.Property>;
|
|
69
|
+
update: (key: string, input: {
|
|
70
|
+
property: T.PropertyInput;
|
|
71
|
+
revision: number;
|
|
72
|
+
}) => Promise<T.Property>;
|
|
73
|
+
};
|
|
74
|
+
topics: {
|
|
75
|
+
list: (options?: T.ListOptions) => Promise<T.Page<T.Topic>>;
|
|
76
|
+
get: (id: string) => Promise<T.Topic>;
|
|
77
|
+
create: (input: T.TopicInput) => Promise<T.Topic>;
|
|
78
|
+
update: (id: string, input: {
|
|
79
|
+
topic: T.TopicInput;
|
|
80
|
+
revision: number;
|
|
81
|
+
}) => Promise<T.Topic>;
|
|
82
|
+
};
|
|
83
|
+
contacts: {
|
|
84
|
+
list: (options?: T.ListOptions) => Promise<T.Page<T.Contact>>;
|
|
85
|
+
create: (input: T.ContactInput) => Promise<T.Contact>;
|
|
86
|
+
get: (email: string) => Promise<T.ContactDetails>;
|
|
87
|
+
update: (email: string, input: {
|
|
88
|
+
contact: Omit<T.Contact, "email" | "revision" | "addedAt">;
|
|
89
|
+
revision: number;
|
|
90
|
+
}) => Promise<T.Contact>;
|
|
91
|
+
subscribe: (email: string, topicId: string, input: T.SubscriptionInput) => Promise<T.Ok>;
|
|
92
|
+
unsubscribe: (email: string) => Promise<T.Ok>;
|
|
93
|
+
};
|
|
94
|
+
segments: {
|
|
95
|
+
list: (options?: T.ListOptions) => Promise<T.Page<T.Segment>>;
|
|
96
|
+
get: (id: string) => Promise<T.Segment>;
|
|
97
|
+
create: (input: T.SegmentInput) => Promise<T.Segment>;
|
|
98
|
+
update: (id: string, input: {
|
|
99
|
+
segment: T.SegmentInput;
|
|
100
|
+
revision: number;
|
|
101
|
+
}) => Promise<T.Segment>;
|
|
102
|
+
contacts: (id: string, options?: T.ListOptions) => Promise<T.Page<T.SegmentMember>>;
|
|
103
|
+
import: (id: string, input: T.ImportInput) => Promise<{
|
|
104
|
+
added: number;
|
|
105
|
+
duplicates: number;
|
|
106
|
+
total: number;
|
|
107
|
+
}>;
|
|
108
|
+
removeContact: (id: string, email: string) => Promise<T.Ok>;
|
|
109
|
+
};
|
|
110
|
+
broadcasts: {
|
|
111
|
+
list: (options?: T.ListOptions) => Promise<T.Page<T.Broadcast>>;
|
|
112
|
+
get: (id: string) => Promise<T.Broadcast>;
|
|
113
|
+
create: (input: T.BroadcastInput) => Promise<T.Broadcast>;
|
|
114
|
+
update: (id: string, input: {
|
|
115
|
+
draft: T.BroadcastInput;
|
|
116
|
+
revision: number;
|
|
117
|
+
}) => Promise<T.Broadcast>;
|
|
118
|
+
recipients: (id: string, options?: T.ListOptions) => Promise<T.Page<T.Recipient>>;
|
|
119
|
+
preview: (id: string, contact?: string) => Promise<T.Preview>;
|
|
120
|
+
test: (id: string, input: {
|
|
121
|
+
to: string;
|
|
122
|
+
previewEmail?: string;
|
|
123
|
+
}) => Promise<T.EmailAccepted>;
|
|
124
|
+
send: (id: string, input: T.SendBroadcastInput) => Promise<T.Broadcast>;
|
|
125
|
+
cancel: (id: string) => Promise<T.Broadcast>;
|
|
126
|
+
duplicate: (id: string) => Promise<T.Broadcast>;
|
|
127
|
+
};
|
|
128
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
export class NaradaError extends Error {
|
|
2
|
+
status;
|
|
3
|
+
code;
|
|
4
|
+
requestId;
|
|
5
|
+
retryAfter;
|
|
6
|
+
constructor(message, status, code, requestId, retryAfter) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.status = status;
|
|
9
|
+
this.code = code;
|
|
10
|
+
this.requestId = requestId;
|
|
11
|
+
this.retryAfter = retryAfter;
|
|
12
|
+
this.name = "NaradaError";
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
const path = encodeURIComponent;
|
|
16
|
+
function query(options = {}) {
|
|
17
|
+
const params = new URLSearchParams();
|
|
18
|
+
if (options.limit !== undefined)
|
|
19
|
+
params.set("limit", String(options.limit));
|
|
20
|
+
if (options.cursor)
|
|
21
|
+
params.set("cursor", options.cursor);
|
|
22
|
+
return params.size ? `?${params}` : "";
|
|
23
|
+
}
|
|
24
|
+
/** Server-side client. Requests are never retried implicitly, including timeouts. */
|
|
25
|
+
export class Narada {
|
|
26
|
+
options;
|
|
27
|
+
baseUrl;
|
|
28
|
+
transport;
|
|
29
|
+
constructor(options) {
|
|
30
|
+
this.options = options;
|
|
31
|
+
const url = new URL(options.baseUrl);
|
|
32
|
+
if (!["http:", "https:"].includes(url.protocol) ||
|
|
33
|
+
url.username ||
|
|
34
|
+
url.password ||
|
|
35
|
+
url.search ||
|
|
36
|
+
url.hash)
|
|
37
|
+
throw new Error("baseUrl must be an HTTP(S) origin or path without credentials, query, or fragment");
|
|
38
|
+
if (!options.apiKey)
|
|
39
|
+
throw new Error("apiKey is required");
|
|
40
|
+
if (options.timeoutMs !== undefined &&
|
|
41
|
+
(!Number.isInteger(options.timeoutMs) || options.timeoutMs <= 0))
|
|
42
|
+
throw new Error("timeoutMs must be a positive integer");
|
|
43
|
+
this.baseUrl = options.baseUrl.replace(/\/+$/, "");
|
|
44
|
+
this.transport = options.fetch ?? globalThis.fetch;
|
|
45
|
+
}
|
|
46
|
+
async request(method, endpoint, body, options = {}) {
|
|
47
|
+
const timeout = AbortSignal.timeout(this.options.timeoutMs ?? 30000);
|
|
48
|
+
const response = await this.transport(`${this.baseUrl}/api/v1${endpoint}`, {
|
|
49
|
+
method,
|
|
50
|
+
redirect: "error",
|
|
51
|
+
signal: options.signal
|
|
52
|
+
? AbortSignal.any([timeout, options.signal])
|
|
53
|
+
: timeout,
|
|
54
|
+
headers: {
|
|
55
|
+
authorization: `Bearer ${this.options.apiKey}`,
|
|
56
|
+
accept: "application/json",
|
|
57
|
+
...(body !== undefined ? { "content-type": "application/json" } : {}),
|
|
58
|
+
...(options.idempotencyKey
|
|
59
|
+
? { "idempotency-key": options.idempotencyKey }
|
|
60
|
+
: {}),
|
|
61
|
+
},
|
|
62
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
63
|
+
});
|
|
64
|
+
const raw = await response.text();
|
|
65
|
+
let data;
|
|
66
|
+
try {
|
|
67
|
+
data = raw ? JSON.parse(raw) : undefined;
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
throw new NaradaError(`Narada returned a non-JSON response (HTTP ${response.status})`, response.status, "invalid_response", response.headers.get("x-request-id") ?? undefined);
|
|
71
|
+
}
|
|
72
|
+
if (!response.ok) {
|
|
73
|
+
const error = data;
|
|
74
|
+
const retry = response.headers.get("retry-after");
|
|
75
|
+
throw new NaradaError(typeof error?.error === "string"
|
|
76
|
+
? error.error
|
|
77
|
+
: `Narada HTTP ${response.status}`, response.status, error?.code, error?.requestId ?? response.headers.get("x-request-id") ?? undefined, retry && /^\d+$/.test(retry) ? Number(retry) : undefined);
|
|
78
|
+
}
|
|
79
|
+
return data;
|
|
80
|
+
}
|
|
81
|
+
/** Backward-compatible alias for emails.send. */
|
|
82
|
+
send = (input, options = {}) => this.emails.send(input, options);
|
|
83
|
+
templates = {
|
|
84
|
+
list: (options) => this.request("GET", `/templates${query(options)}`),
|
|
85
|
+
get: (id) => this.request("GET", `/templates/${path(id)}`),
|
|
86
|
+
create: (input) => this.request("POST", "/templates", input),
|
|
87
|
+
update: (id, input) => this.request("PUT", `/templates/${path(id)}`, input),
|
|
88
|
+
publish: (id, input) => this.request("POST", `/templates/${path(id)}/publish`, input),
|
|
89
|
+
archive: (id, input) => this.request("POST", `/templates/${path(id)}/archive`, input),
|
|
90
|
+
duplicate: (id, input) => this.request("POST", `/templates/${path(id)}/duplicate`, input),
|
|
91
|
+
preview: (id, input = {}) => this.request("POST", `/templates/${path(id)}/preview`, input),
|
|
92
|
+
test: (id, input) => this.request("POST", `/templates/${path(id)}/test`, input),
|
|
93
|
+
createBroadcast: (id, input = {}) => this.request("POST", `/templates/${path(id)}/broadcasts`, input),
|
|
94
|
+
};
|
|
95
|
+
emails = {
|
|
96
|
+
send: (input, options = {}) => this.request("POST", "/emails", input, options),
|
|
97
|
+
get: (id) => this.request("GET", `/emails/${path(id)}`),
|
|
98
|
+
};
|
|
99
|
+
contactProperties = {
|
|
100
|
+
list: (options) => this.request("GET", `/properties${query(options)}`),
|
|
101
|
+
get: (key) => this.request("GET", `/properties/${path(key)}`),
|
|
102
|
+
create: (input) => this.request("POST", "/properties", input),
|
|
103
|
+
update: (key, input) => this.request("PUT", `/properties/${path(key)}`, input),
|
|
104
|
+
};
|
|
105
|
+
topics = {
|
|
106
|
+
list: (options) => this.request("GET", `/topics${query(options)}`),
|
|
107
|
+
get: (id) => this.request("GET", `/topics/${path(id)}`),
|
|
108
|
+
create: (input) => this.request("POST", "/topics", input),
|
|
109
|
+
update: (id, input) => this.request("PUT", `/topics/${path(id)}`, input),
|
|
110
|
+
};
|
|
111
|
+
contacts = {
|
|
112
|
+
list: (options) => this.request("GET", `/contacts${query(options)}`),
|
|
113
|
+
create: (input) => this.request("POST", "/contacts", input),
|
|
114
|
+
get: (email) => this.request("GET", `/contacts/${path(email)}`),
|
|
115
|
+
update: (email, input) => this.request("PUT", `/contacts/${path(email)}`, input),
|
|
116
|
+
subscribe: (email, topicId, input) => this.request("POST", `/contacts/${path(email)}/topics/${path(topicId)}`, input),
|
|
117
|
+
unsubscribe: (email) => this.request("POST", `/contacts/${path(email)}/unsubscribe`),
|
|
118
|
+
};
|
|
119
|
+
segments = {
|
|
120
|
+
list: (options) => this.request("GET", `/segments${query(options)}`),
|
|
121
|
+
get: (id) => this.request("GET", `/segments/${path(id)}`),
|
|
122
|
+
create: (input) => this.request("POST", "/segments", input),
|
|
123
|
+
update: (id, input) => this.request("PUT", `/segments/${path(id)}`, input),
|
|
124
|
+
contacts: (id, options) => this.request("GET", `/segments/${path(id)}/contacts${query(options)}`),
|
|
125
|
+
import: (id, input) => this.request("POST", `/segments/${path(id)}/import`, input),
|
|
126
|
+
removeContact: (id, email) => this.request("DELETE", `/segments/${path(id)}/contacts/${path(email)}`),
|
|
127
|
+
};
|
|
128
|
+
broadcasts = {
|
|
129
|
+
list: (options) => this.request("GET", `/broadcasts${query(options)}`),
|
|
130
|
+
get: (id) => this.request("GET", `/broadcasts/${path(id)}`),
|
|
131
|
+
create: (input) => this.request("POST", "/broadcasts", input),
|
|
132
|
+
update: (id, input) => this.request("PUT", `/broadcasts/${path(id)}`, input),
|
|
133
|
+
recipients: (id, options) => this.request("GET", `/broadcasts/${path(id)}/recipients${query(options)}`),
|
|
134
|
+
preview: (id, contact) => this.request("GET", `/broadcasts/${path(id)}/preview${contact ? `?contact=${path(contact)}` : ""}`),
|
|
135
|
+
test: (id, input) => this.request("POST", `/broadcasts/${path(id)}/test`, input),
|
|
136
|
+
send: (id, input) => this.request("POST", `/broadcasts/${path(id)}/send`, input),
|
|
137
|
+
cancel: (id) => this.request("POST", `/broadcasts/${path(id)}/cancel`),
|
|
138
|
+
duplicate: (id) => this.request("POST", `/broadcasts/${path(id)}/duplicate`),
|
|
139
|
+
};
|
|
140
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/** Public v1 contracts. No database or server-runtime dependencies. */
|
|
2
|
+
export type PropertyValue = string | number | boolean;
|
|
3
|
+
export interface Page<T> {
|
|
4
|
+
data: T[];
|
|
5
|
+
nextCursor: string | null;
|
|
6
|
+
}
|
|
7
|
+
export interface ListOptions {
|
|
8
|
+
limit?: number;
|
|
9
|
+
cursor?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface EmailInput {
|
|
12
|
+
from: string;
|
|
13
|
+
to: string | string[];
|
|
14
|
+
subject: string;
|
|
15
|
+
html?: string;
|
|
16
|
+
text?: string;
|
|
17
|
+
reply_to?: string;
|
|
18
|
+
expires_in?: number;
|
|
19
|
+
}
|
|
20
|
+
export interface EmailAccepted {
|
|
21
|
+
id: string;
|
|
22
|
+
replayed: boolean;
|
|
23
|
+
}
|
|
24
|
+
export interface EmailStatus {
|
|
25
|
+
id: string;
|
|
26
|
+
status: string;
|
|
27
|
+
created_at: string;
|
|
28
|
+
events: {
|
|
29
|
+
id: string;
|
|
30
|
+
type: string;
|
|
31
|
+
at: string;
|
|
32
|
+
recipients: string[];
|
|
33
|
+
reason?: string;
|
|
34
|
+
permanent?: boolean;
|
|
35
|
+
}[];
|
|
36
|
+
}
|
|
37
|
+
export interface PropertyInput {
|
|
38
|
+
key: string;
|
|
39
|
+
type: "string" | "number" | "boolean" | "date";
|
|
40
|
+
fallback?: PropertyValue;
|
|
41
|
+
}
|
|
42
|
+
export interface Property extends PropertyInput {
|
|
43
|
+
revision: number;
|
|
44
|
+
createdAt: string;
|
|
45
|
+
}
|
|
46
|
+
export interface ContactInput {
|
|
47
|
+
email: string;
|
|
48
|
+
name?: string;
|
|
49
|
+
first_name?: string;
|
|
50
|
+
last_name?: string;
|
|
51
|
+
properties?: Record<string, PropertyValue>;
|
|
52
|
+
}
|
|
53
|
+
export interface Contact {
|
|
54
|
+
email: string;
|
|
55
|
+
name: string;
|
|
56
|
+
first_name: string;
|
|
57
|
+
last_name: string;
|
|
58
|
+
properties: Record<string, PropertyValue>;
|
|
59
|
+
revision: number;
|
|
60
|
+
addedAt: string;
|
|
61
|
+
}
|
|
62
|
+
export interface TopicInput {
|
|
63
|
+
name: string;
|
|
64
|
+
description?: string;
|
|
65
|
+
visibility?: "public" | "private";
|
|
66
|
+
active?: boolean;
|
|
67
|
+
}
|
|
68
|
+
export interface Topic {
|
|
69
|
+
id: string;
|
|
70
|
+
name: string;
|
|
71
|
+
description: string;
|
|
72
|
+
visibility: "public" | "private";
|
|
73
|
+
active: boolean;
|
|
74
|
+
revision: number;
|
|
75
|
+
createdAt: string;
|
|
76
|
+
}
|
|
77
|
+
export interface ContactDetails {
|
|
78
|
+
contact: Contact;
|
|
79
|
+
topics: (Topic & {
|
|
80
|
+
subscribed: boolean;
|
|
81
|
+
})[];
|
|
82
|
+
unsubscribedAll: boolean;
|
|
83
|
+
}
|
|
84
|
+
export interface SegmentRule {
|
|
85
|
+
property: string;
|
|
86
|
+
operator: "equals" | "not_equals" | "contains" | "greater_than" | "less_than" | "is_set" | "is_not_set";
|
|
87
|
+
value?: PropertyValue;
|
|
88
|
+
}
|
|
89
|
+
export interface SegmentInput {
|
|
90
|
+
name: string;
|
|
91
|
+
kind: "manual" | "dynamic";
|
|
92
|
+
match?: "all" | "any";
|
|
93
|
+
rules?: SegmentRule[];
|
|
94
|
+
}
|
|
95
|
+
export interface Segment {
|
|
96
|
+
id: string;
|
|
97
|
+
name: string;
|
|
98
|
+
kind: "manual" | "dynamic";
|
|
99
|
+
match: "all" | "any";
|
|
100
|
+
rules: SegmentRule[];
|
|
101
|
+
revision: number;
|
|
102
|
+
total: number;
|
|
103
|
+
updatedAt: string;
|
|
104
|
+
}
|
|
105
|
+
export interface SegmentMember {
|
|
106
|
+
email: string;
|
|
107
|
+
name: string;
|
|
108
|
+
consentSource: string;
|
|
109
|
+
consentedAt: string;
|
|
110
|
+
}
|
|
111
|
+
export interface ImportInput {
|
|
112
|
+
csv: string;
|
|
113
|
+
consentSource: string;
|
|
114
|
+
consentConfirmed: true;
|
|
115
|
+
topicIds?: string[];
|
|
116
|
+
}
|
|
117
|
+
export interface BroadcastInput {
|
|
118
|
+
name: string;
|
|
119
|
+
audienceId?: string;
|
|
120
|
+
topicId?: string;
|
|
121
|
+
from?: string;
|
|
122
|
+
subject?: string;
|
|
123
|
+
text?: string;
|
|
124
|
+
html?: string;
|
|
125
|
+
organization?: string;
|
|
126
|
+
postalAddress?: string;
|
|
127
|
+
replyTo?: string;
|
|
128
|
+
}
|
|
129
|
+
export interface Broadcast {
|
|
130
|
+
id: string;
|
|
131
|
+
draft: BroadcastInput;
|
|
132
|
+
status: "draft" | "scheduled" | "sending" | "completed" | "cancelled";
|
|
133
|
+
revision: number;
|
|
134
|
+
total: number;
|
|
135
|
+
createdAt: string;
|
|
136
|
+
updatedAt: string;
|
|
137
|
+
scheduledAt?: number;
|
|
138
|
+
}
|
|
139
|
+
export interface Recipient {
|
|
140
|
+
email: string;
|
|
141
|
+
name: string;
|
|
142
|
+
messageId: string;
|
|
143
|
+
status: string;
|
|
144
|
+
error?: string;
|
|
145
|
+
}
|
|
146
|
+
export interface Preview {
|
|
147
|
+
subject: string;
|
|
148
|
+
html: string;
|
|
149
|
+
text?: string;
|
|
150
|
+
eligible: number;
|
|
151
|
+
total: number;
|
|
152
|
+
excluded: number;
|
|
153
|
+
previewEmail?: string;
|
|
154
|
+
}
|
|
155
|
+
export interface SendBroadcastInput {
|
|
156
|
+
revision: number;
|
|
157
|
+
confirmed: true;
|
|
158
|
+
scheduledAt?: number;
|
|
159
|
+
}
|
|
160
|
+
export interface SubscriptionInput {
|
|
161
|
+
subscribed: boolean;
|
|
162
|
+
source: string;
|
|
163
|
+
confirmed: true;
|
|
164
|
+
}
|
|
165
|
+
export interface Ok {
|
|
166
|
+
ok: true;
|
|
167
|
+
}
|
|
168
|
+
export interface TemplateVariable {
|
|
169
|
+
key: string;
|
|
170
|
+
type?: "string" | "number" | "boolean" | "url";
|
|
171
|
+
fallback?: PropertyValue;
|
|
172
|
+
}
|
|
173
|
+
export interface TemplateContent {
|
|
174
|
+
name: string;
|
|
175
|
+
kind?: "transactional" | "broadcast";
|
|
176
|
+
subject?: string;
|
|
177
|
+
html?: string;
|
|
178
|
+
text?: string;
|
|
179
|
+
variables?: TemplateVariable[];
|
|
180
|
+
}
|
|
181
|
+
export interface Template {
|
|
182
|
+
id: string;
|
|
183
|
+
alias: string;
|
|
184
|
+
draft: TemplateContent;
|
|
185
|
+
revision: number;
|
|
186
|
+
publishedVersion?: number;
|
|
187
|
+
archived: boolean;
|
|
188
|
+
status: "draft" | "published" | "archived";
|
|
189
|
+
hasUnpublishedChanges: boolean;
|
|
190
|
+
createdAt: string;
|
|
191
|
+
updatedAt: string;
|
|
192
|
+
}
|
|
193
|
+
export interface TemplateEmailInput {
|
|
194
|
+
from: string;
|
|
195
|
+
to: string | string[];
|
|
196
|
+
reply_to?: string;
|
|
197
|
+
expires_in?: number;
|
|
198
|
+
template: {
|
|
199
|
+
id: string;
|
|
200
|
+
version?: number;
|
|
201
|
+
variables?: Record<string, PropertyValue>;
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
export interface TemplatePreviewInput {
|
|
205
|
+
variables?: Record<string, PropertyValue>;
|
|
206
|
+
version?: number;
|
|
207
|
+
}
|
|
208
|
+
export interface TemplateBroadcastInput extends TemplatePreviewInput {
|
|
209
|
+
name?: string;
|
|
210
|
+
audienceId?: string;
|
|
211
|
+
topicId?: string;
|
|
212
|
+
from?: string;
|
|
213
|
+
organization?: string;
|
|
214
|
+
postalAddress?: string;
|
|
215
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,34 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@durgadigital/narada",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Typed server-side SDK for a self-hosted Narada API",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=22"
|
|
8
|
+
},
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"README.md",
|
|
18
|
+
"LICENSE"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsc -p tsconfig.build.json",
|
|
22
|
+
"prepack": "pnpm build"
|
|
23
|
+
},
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "git+https://github.com/Durga-Digital/narada.git",
|
|
28
|
+
"directory": "packages/sdk"
|
|
29
|
+
},
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public",
|
|
32
|
+
"registry": "https://registry.npmjs.org/"
|
|
33
|
+
}
|
|
34
|
+
}
|