@pilely/simple-email 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 +36 -0
- package/dist/index.d.ts +92 -0
- package/dist/index.js +167 -0
- package/dist/types.d.ts +58 -0
- package/dist/types.js +1 -0
- package/package.json +31 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Pilely
|
|
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
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# `@pilely/simple-email`
|
|
2
|
+
|
|
3
|
+
A typed client for the `simple_email` service — the platform's managed
|
|
4
|
+
transactional email. Wraps all 12 of its POST routes: sending, account
|
|
5
|
+
provisioning, and templates.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm install @pilely/simple-email @pilely/core
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Always install with the scope — `pilely-email` and similar unscoped names
|
|
14
|
+
are not owned by this project and are squattable. `@pilely/core` is a peer
|
|
15
|
+
dependency; an app has exactly one facade, never two.
|
|
16
|
+
|
|
17
|
+
## Where the app id lives
|
|
18
|
+
|
|
19
|
+
Unlike the other three service packages, this one splits: `send()` and
|
|
20
|
+
`listSends()` put the app id in the request **body**; every account and
|
|
21
|
+
template method puts it in the request **path**. `listAccounts()` takes no
|
|
22
|
+
app id at all — it lists every account the caller owns. Every method adds
|
|
23
|
+
the id itself from `@pilely/core`'s `appId()`; it is never a parameter you
|
|
24
|
+
pass.
|
|
25
|
+
|
|
26
|
+
## Templates live under the account
|
|
27
|
+
|
|
28
|
+
There is no top-level `templates/...` route. Every template call is
|
|
29
|
+
`accounts/{app}/templates/...` — a template belongs to one account, and
|
|
30
|
+
`createTemplate`/`listTemplates`/`templateInfo`/`updateTemplate`/`deleteTemplate`
|
|
31
|
+
all reach it that way.
|
|
32
|
+
|
|
33
|
+
## Status
|
|
34
|
+
|
|
35
|
+
This is a pre-1.0 package and moves with the platform. Nothing here is
|
|
36
|
+
published to the registry yet. Zero runtime dependencies.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { Account, AccountCursor, AccountInfoEnvelope, SendCursor, SendRow, Template, TemplateCursor, TemplateMeta } from "./types.js";
|
|
2
|
+
export type { Account, AccountCursor, AccountInfoEnvelope, SendCursor, SendRow, Template, TemplateCursor, TemplateMeta, } from "./types.js";
|
|
3
|
+
/** `to` is capped at the service's `max_recipients_per_send` (50 by default). */
|
|
4
|
+
export type SendContent = {
|
|
5
|
+
template_id: string;
|
|
6
|
+
variables?: Record<string, string>;
|
|
7
|
+
} | {
|
|
8
|
+
subject: string;
|
|
9
|
+
html: string;
|
|
10
|
+
};
|
|
11
|
+
export type SendInput = {
|
|
12
|
+
to: string[];
|
|
13
|
+
} & SendContent;
|
|
14
|
+
export interface ListSendsOptions {
|
|
15
|
+
limit?: number;
|
|
16
|
+
after_created_time_stamp?: number;
|
|
17
|
+
after_send_id?: string;
|
|
18
|
+
}
|
|
19
|
+
export interface ListAccountsOptions {
|
|
20
|
+
limit?: number;
|
|
21
|
+
after_created_time_stamp?: number;
|
|
22
|
+
after_app_id?: string;
|
|
23
|
+
}
|
|
24
|
+
export interface ListTemplatesOptions {
|
|
25
|
+
limit?: number;
|
|
26
|
+
after_created_time_stamp?: number;
|
|
27
|
+
after_template_id?: string;
|
|
28
|
+
}
|
|
29
|
+
/** Both keys are REQUIRED — the server rejects either one missing with a
|
|
30
|
+
* 400 (`ContentField::validate`), matching `updateTemplate`'s full-replace
|
|
31
|
+
* contract: there is no patch form on this route. */
|
|
32
|
+
export interface UpdateTemplateBody {
|
|
33
|
+
subject: string;
|
|
34
|
+
html: string;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* `app_id` is added from core's `appId()` — it is not a caller parameter.
|
|
38
|
+
* Body size is capped on this route (like `createTemplate`/`updateTemplate`):
|
|
39
|
+
* an oversize body is rejected by the server before any handler runs, so
|
|
40
|
+
* the failure is not an `{ok:false}` envelope and the thrown `PilelyError`
|
|
41
|
+
* carries a null `code`.
|
|
42
|
+
*/
|
|
43
|
+
export declare function send(input: SendInput): Promise<{
|
|
44
|
+
send_id: string;
|
|
45
|
+
recipient_count: number;
|
|
46
|
+
}>;
|
|
47
|
+
/** `app_id` lives in the body here, like `send` — not in the path. The
|
|
48
|
+
* paged primitive; use `listAllSends` to walk to the end. */
|
|
49
|
+
export declare function listSends(options?: ListSendsOptions): Promise<{
|
|
50
|
+
sends: SendRow[];
|
|
51
|
+
next_cursor: SendCursor | null;
|
|
52
|
+
}>;
|
|
53
|
+
export declare function listAllSends(): Promise<SendRow[]>;
|
|
54
|
+
/** `sendGroup` is optional here — omit it (or pass `undefined`) and the
|
|
55
|
+
* key is left off the wire, meaning owner-only. `setAccountAccess` below
|
|
56
|
+
* is the opposite: its key is always required. */
|
|
57
|
+
export declare function createAccount(sendGroup?: string | null): Promise<Account>;
|
|
58
|
+
/** No `app_id` anywhere on this route — `after_app_id` is a cursor field,
|
|
59
|
+
* not a selector. The paged primitive; use `listAllAccounts` to walk to
|
|
60
|
+
* the end. */
|
|
61
|
+
export declare function listAccounts(options?: ListAccountsOptions): Promise<{
|
|
62
|
+
accounts: Account[];
|
|
63
|
+
next_cursor: AccountCursor | null;
|
|
64
|
+
}>;
|
|
65
|
+
export declare function listAllAccounts(): Promise<Account[]>;
|
|
66
|
+
/** Returns the account plus its live operational state — the four extra
|
|
67
|
+
* fields are nullable siblings of `account`, not nested inside it. */
|
|
68
|
+
export declare function accountInfo(): Promise<AccountInfoEnvelope>;
|
|
69
|
+
export declare function deleteAccount(): Promise<void>;
|
|
70
|
+
/** `sendGroup` is REQUIRED here and its key is always sent, even `null` —
|
|
71
|
+
* the opposite of `createAccount`, where the key is optional. `null`
|
|
72
|
+
* means owner-only. */
|
|
73
|
+
export declare function setAccountAccess(sendGroup: string | null): Promise<Account>;
|
|
74
|
+
/** Returns metadata only — no `html`. Use `templateInfo` to read the body
|
|
75
|
+
* back. */
|
|
76
|
+
export declare function createTemplate(name: string, subject: string, html: string): Promise<TemplateMeta>;
|
|
77
|
+
/** The paged primitive; use `listAllTemplates` to walk to the end. Rows
|
|
78
|
+
* carry metadata only — no `html`. */
|
|
79
|
+
export declare function listTemplates(options?: ListTemplatesOptions): Promise<{
|
|
80
|
+
templates: TemplateMeta[];
|
|
81
|
+
next_cursor: TemplateCursor | null;
|
|
82
|
+
}>;
|
|
83
|
+
export declare function listAllTemplates(): Promise<TemplateMeta[]>;
|
|
84
|
+
/** The only template route whose answer carries `html`. */
|
|
85
|
+
export declare function templateInfo(templateNanoid: string): Promise<Template>;
|
|
86
|
+
/**
|
|
87
|
+
* A FULL REPLACE of both `subject` and `html` — never a patch — and `name`
|
|
88
|
+
* is not updatable at all. Returns metadata only, like `createTemplate`.
|
|
89
|
+
* Subject to the same oversize-body caveat as `send`/`createTemplate`.
|
|
90
|
+
*/
|
|
91
|
+
export declare function updateTemplate(templateNanoid: string, body: UpdateTemplateBody): Promise<TemplateMeta>;
|
|
92
|
+
export declare function deleteTemplate(templateNanoid: string): Promise<void>;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// @pilely/simple-email — a typed wrapper over the simple_email service's 12
|
|
2
|
+
// POST routes. See README.md for where app_id lives on each route: this is
|
|
3
|
+
// the one service where it moves between the body and the path.
|
|
4
|
+
import { appId, call, collectPages } from "@pilely/core";
|
|
5
|
+
// ── Sending ──────────────────────────────────────────────────────────────
|
|
6
|
+
/**
|
|
7
|
+
* `app_id` is added from core's `appId()` — it is not a caller parameter.
|
|
8
|
+
* Body size is capped on this route (like `createTemplate`/`updateTemplate`):
|
|
9
|
+
* an oversize body is rejected by the server before any handler runs, so
|
|
10
|
+
* the failure is not an `{ok:false}` envelope and the thrown `PilelyError`
|
|
11
|
+
* carries a null `code`.
|
|
12
|
+
*/
|
|
13
|
+
export async function send(input) {
|
|
14
|
+
const body = "template_id" in input
|
|
15
|
+
? { app_id: appId(), to: input.to, template_id: input.template_id, variables: input.variables }
|
|
16
|
+
: { app_id: appId(), to: input.to, subject: input.subject, html: input.html };
|
|
17
|
+
return call({
|
|
18
|
+
service: "simple-email",
|
|
19
|
+
path: "/send",
|
|
20
|
+
body,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
/** `app_id` lives in the body here, like `send` — not in the path. The
|
|
24
|
+
* paged primitive; use `listAllSends` to walk to the end. */
|
|
25
|
+
export async function listSends(options = {}) {
|
|
26
|
+
const json = await call({
|
|
27
|
+
service: "simple-email",
|
|
28
|
+
path: "/sends/list",
|
|
29
|
+
body: {
|
|
30
|
+
app_id: appId(),
|
|
31
|
+
limit: options.limit,
|
|
32
|
+
after_created_time_stamp: options.after_created_time_stamp,
|
|
33
|
+
after_send_id: options.after_send_id,
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
return { sends: json.sends, next_cursor: json.next_cursor };
|
|
37
|
+
}
|
|
38
|
+
export async function listAllSends() {
|
|
39
|
+
return collectPages(async (cursor) => {
|
|
40
|
+
const page = await listSends({ limit: 100, ...(cursor ?? {}) });
|
|
41
|
+
return { rows: page.sends, nextCursor: page.next_cursor };
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
// ── Accounts ─────────────────────────────────────────────────────────────
|
|
45
|
+
/** `sendGroup` is optional here — omit it (or pass `undefined`) and the
|
|
46
|
+
* key is left off the wire, meaning owner-only. `setAccountAccess` below
|
|
47
|
+
* is the opposite: its key is always required. */
|
|
48
|
+
export async function createAccount(sendGroup) {
|
|
49
|
+
const body = { app_id: appId() };
|
|
50
|
+
if (sendGroup !== undefined) {
|
|
51
|
+
body.send_group = sendGroup;
|
|
52
|
+
}
|
|
53
|
+
const json = await call({
|
|
54
|
+
service: "simple-email",
|
|
55
|
+
path: "/accounts/create",
|
|
56
|
+
body,
|
|
57
|
+
});
|
|
58
|
+
return json.account;
|
|
59
|
+
}
|
|
60
|
+
/** No `app_id` anywhere on this route — `after_app_id` is a cursor field,
|
|
61
|
+
* not a selector. The paged primitive; use `listAllAccounts` to walk to
|
|
62
|
+
* the end. */
|
|
63
|
+
export async function listAccounts(options = {}) {
|
|
64
|
+
const json = await call({
|
|
65
|
+
service: "simple-email",
|
|
66
|
+
path: "/accounts/list",
|
|
67
|
+
body: {
|
|
68
|
+
limit: options.limit,
|
|
69
|
+
after_created_time_stamp: options.after_created_time_stamp,
|
|
70
|
+
after_app_id: options.after_app_id,
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
return { accounts: json.accounts, next_cursor: json.next_cursor };
|
|
74
|
+
}
|
|
75
|
+
export async function listAllAccounts() {
|
|
76
|
+
return collectPages(async (cursor) => {
|
|
77
|
+
const page = await listAccounts({ limit: 100, ...(cursor ?? {}) });
|
|
78
|
+
return { rows: page.accounts, nextCursor: page.next_cursor };
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
/** Returns the account plus its live operational state — the four extra
|
|
82
|
+
* fields are nullable siblings of `account`, not nested inside it. */
|
|
83
|
+
export async function accountInfo() {
|
|
84
|
+
const json = await call({
|
|
85
|
+
service: "simple-email",
|
|
86
|
+
path: `/accounts/${appId()}/info`,
|
|
87
|
+
});
|
|
88
|
+
return {
|
|
89
|
+
account: json.account,
|
|
90
|
+
daily_cap: json.daily_cap,
|
|
91
|
+
sent_today: json.sent_today,
|
|
92
|
+
credit_blocked: json.credit_blocked,
|
|
93
|
+
phone_verified: json.phone_verified,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
export async function deleteAccount() {
|
|
97
|
+
await call({ service: "simple-email", path: `/accounts/${appId()}/delete` });
|
|
98
|
+
}
|
|
99
|
+
/** `sendGroup` is REQUIRED here and its key is always sent, even `null` —
|
|
100
|
+
* the opposite of `createAccount`, where the key is optional. `null`
|
|
101
|
+
* means owner-only. */
|
|
102
|
+
export async function setAccountAccess(sendGroup) {
|
|
103
|
+
const json = await call({
|
|
104
|
+
service: "simple-email",
|
|
105
|
+
path: `/accounts/${appId()}/access/set`,
|
|
106
|
+
body: { send_group: sendGroup },
|
|
107
|
+
});
|
|
108
|
+
return json.account;
|
|
109
|
+
}
|
|
110
|
+
// ── Templates (nested under the account) ────────────────────────────────
|
|
111
|
+
/** Returns metadata only — no `html`. Use `templateInfo` to read the body
|
|
112
|
+
* back. */
|
|
113
|
+
export async function createTemplate(name, subject, html) {
|
|
114
|
+
const json = await call({
|
|
115
|
+
service: "simple-email",
|
|
116
|
+
path: `/accounts/${appId()}/templates/create`,
|
|
117
|
+
body: { name, subject, html },
|
|
118
|
+
});
|
|
119
|
+
return json.template;
|
|
120
|
+
}
|
|
121
|
+
/** The paged primitive; use `listAllTemplates` to walk to the end. Rows
|
|
122
|
+
* carry metadata only — no `html`. */
|
|
123
|
+
export async function listTemplates(options = {}) {
|
|
124
|
+
const json = await call({
|
|
125
|
+
service: "simple-email",
|
|
126
|
+
path: `/accounts/${appId()}/templates/list`,
|
|
127
|
+
body: {
|
|
128
|
+
limit: options.limit,
|
|
129
|
+
after_created_time_stamp: options.after_created_time_stamp,
|
|
130
|
+
after_template_id: options.after_template_id,
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
return { templates: json.templates, next_cursor: json.next_cursor };
|
|
134
|
+
}
|
|
135
|
+
export async function listAllTemplates() {
|
|
136
|
+
return collectPages(async (cursor) => {
|
|
137
|
+
const page = await listTemplates({ limit: 100, ...(cursor ?? {}) });
|
|
138
|
+
return { rows: page.templates, nextCursor: page.next_cursor };
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
/** The only template route whose answer carries `html`. */
|
|
142
|
+
export async function templateInfo(templateNanoid) {
|
|
143
|
+
const json = await call({
|
|
144
|
+
service: "simple-email",
|
|
145
|
+
path: `/accounts/${appId()}/templates/${templateNanoid}/info`,
|
|
146
|
+
});
|
|
147
|
+
return json.template;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* A FULL REPLACE of both `subject` and `html` — never a patch — and `name`
|
|
151
|
+
* is not updatable at all. Returns metadata only, like `createTemplate`.
|
|
152
|
+
* Subject to the same oversize-body caveat as `send`/`createTemplate`.
|
|
153
|
+
*/
|
|
154
|
+
export async function updateTemplate(templateNanoid, body) {
|
|
155
|
+
const json = await call({
|
|
156
|
+
service: "simple-email",
|
|
157
|
+
path: `/accounts/${appId()}/templates/${templateNanoid}/update`,
|
|
158
|
+
body,
|
|
159
|
+
});
|
|
160
|
+
return json.template;
|
|
161
|
+
}
|
|
162
|
+
export async function deleteTemplate(templateNanoid) {
|
|
163
|
+
await call({
|
|
164
|
+
service: "simple-email",
|
|
165
|
+
path: `/accounts/${appId()}/templates/${templateNanoid}/delete`,
|
|
166
|
+
});
|
|
167
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/** `address` is derived at read time, never stored, and `null` when the
|
|
2
|
+
* app it names is gone. */
|
|
3
|
+
export interface Account {
|
|
4
|
+
app_id: string;
|
|
5
|
+
address: string | null;
|
|
6
|
+
display_name: string | null;
|
|
7
|
+
/** `null` means owner-only — the opposite polarity from simple_db's
|
|
8
|
+
* `read_group`/`write_group`, where `null` means everyone. */
|
|
9
|
+
send_group: string | null;
|
|
10
|
+
created_time_stamp: number;
|
|
11
|
+
}
|
|
12
|
+
/** `daily_cap`'s three siblings are nullable and sit BESIDE `account`, not
|
|
13
|
+
* inside it — the whole reason an app calls `accountInfo`. */
|
|
14
|
+
export interface AccountInfoEnvelope {
|
|
15
|
+
account: Account;
|
|
16
|
+
daily_cap: number;
|
|
17
|
+
sent_today: number | null;
|
|
18
|
+
credit_blocked: boolean | null;
|
|
19
|
+
phone_verified: boolean | null;
|
|
20
|
+
}
|
|
21
|
+
/** What `templates/create` and `templates/update` answer with — no `html`. */
|
|
22
|
+
export interface TemplateMeta {
|
|
23
|
+
template_id: string;
|
|
24
|
+
/** Not updatable through `updateTemplate` — only `subject` and `html` are. */
|
|
25
|
+
name: string;
|
|
26
|
+
subject: string;
|
|
27
|
+
created_time_stamp: number;
|
|
28
|
+
updated_time_stamp: number;
|
|
29
|
+
}
|
|
30
|
+
/** What `templates/{t}/info` answers with — `TemplateMeta` plus the body. */
|
|
31
|
+
export interface Template extends TemplateMeta {
|
|
32
|
+
html: string;
|
|
33
|
+
}
|
|
34
|
+
export interface SendRow {
|
|
35
|
+
send_id: string;
|
|
36
|
+
requester_user_id: string;
|
|
37
|
+
via_app: boolean;
|
|
38
|
+
kind: string;
|
|
39
|
+
template_id: string | null;
|
|
40
|
+
subject: string;
|
|
41
|
+
recipients: unknown;
|
|
42
|
+
recipient_count: number;
|
|
43
|
+
outcome: string;
|
|
44
|
+
outcome_reason: string | null;
|
|
45
|
+
fired_at: number;
|
|
46
|
+
}
|
|
47
|
+
export interface SendCursor {
|
|
48
|
+
after_created_time_stamp: number;
|
|
49
|
+
after_send_id: string;
|
|
50
|
+
}
|
|
51
|
+
export interface AccountCursor {
|
|
52
|
+
after_created_time_stamp: number;
|
|
53
|
+
after_app_id: string;
|
|
54
|
+
}
|
|
55
|
+
export interface TemplateCursor {
|
|
56
|
+
after_created_time_stamp: number;
|
|
57
|
+
after_template_id: string;
|
|
58
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pilely/simple-email",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A typed client for the simple_email service.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"default": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"sideEffects": false,
|
|
16
|
+
"files": ["dist", "README.md", "LICENSE"],
|
|
17
|
+
"publishConfig": { "access": "public" },
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/Pile-ly/reference-code.git",
|
|
21
|
+
"directory": "npm/simple-email"
|
|
22
|
+
},
|
|
23
|
+
"peerDependencies": {
|
|
24
|
+
"@pilely/core": "^0.1.0"
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "tsc -p tsconfig.json",
|
|
28
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
29
|
+
"test": "vitest run"
|
|
30
|
+
}
|
|
31
|
+
}
|