@pilely/simple-email 0.1.0 → 0.2.1
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/README.md +38 -0
- package/dist/index.d.ts +7 -3
- package/dist/index.js +30 -13
- package/dist/types.d.ts +23 -4
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -34,3 +34,41 @@ all reach it that way.
|
|
|
34
34
|
|
|
35
35
|
This is a pre-1.0 package and moves with the platform. Nothing here is
|
|
36
36
|
published to the registry yet. Zero runtime dependencies.
|
|
37
|
+
|
|
38
|
+
## Templates can be nameless, and can lose their body
|
|
39
|
+
|
|
40
|
+
Two degraded shapes the service keeps deliberately visible, both reflected
|
|
41
|
+
in the types:
|
|
42
|
+
|
|
43
|
+
- `name` is `string | null`. A nameless template is legal — `createTemplate`
|
|
44
|
+
takes `string | null` and hands the `null` straight back.
|
|
45
|
+
- `html` is **optional, not nullable**. When the stored object behind a
|
|
46
|
+
template is missing, `templateInfo` omits the key rather than sending
|
|
47
|
+
`null`. Discriminate with `"html" in template`, and never assume a body.
|
|
48
|
+
|
|
49
|
+
## Error codes
|
|
50
|
+
|
|
51
|
+
Refusals arrive as `PilelyError` with a `code` drawn from
|
|
52
|
+
`SimpleEmailErrorCode`. Here matching on the code is not optional:
|
|
53
|
+
|
|
54
|
+
**`daily_cap_exceeded` and `rate_limited` both answer 429.** One means the
|
|
55
|
+
app has spent its UTC-day send allowance (retry tomorrow, or not at all);
|
|
56
|
+
the other is an ordinary burst window (retry shortly). The status cannot
|
|
57
|
+
tell them apart — only the code can.
|
|
58
|
+
|
|
59
|
+
`send` can also answer `phone_verification_required` (403) and
|
|
60
|
+
`phone_verification_unavailable` (503) from the phone gate, and
|
|
61
|
+
`out_of_traffic_credits` (503). Provisioning adds `account_exists`,
|
|
62
|
+
`account_limit_reached` and `template_limit_reached` (all 409). The
|
|
63
|
+
remainder: `not_found` (404, the uniform hide), `bad_request` (400),
|
|
64
|
+
`unauthenticated` (401), `bad_forwarded_identity` (401), `internal` (500).
|
|
65
|
+
|
|
66
|
+
A member-role caller sees a uniform 404 where the owner would see a 503
|
|
67
|
+
`out_of_traffic_credits` — worth knowing before debugging a member's send.
|
|
68
|
+
|
|
69
|
+
## Limits worth knowing before you call `send`
|
|
70
|
+
|
|
71
|
+
- Billed **per recipient**, not per call.
|
|
72
|
+
- At most 50 recipients per send.
|
|
73
|
+
- 200 sends per app per UTC day — the `daily_cap_exceeded` wall above.
|
|
74
|
+
- `send_id` is the idempotency key.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
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";
|
|
2
|
+
export type { Account, SimpleEmailErrorCode, AccountCursor, AccountInfoEnvelope, SendCursor, SendRow, Template, TemplateCursor, TemplateMeta, } from "./types.js";
|
|
3
3
|
/** `to` is capped at the service's `max_recipients_per_send` (50 by default). */
|
|
4
4
|
export type SendContent = {
|
|
5
5
|
template_id: string;
|
|
@@ -72,8 +72,12 @@ export declare function deleteAccount(): Promise<void>;
|
|
|
72
72
|
* means owner-only. */
|
|
73
73
|
export declare function setAccountAccess(sendGroup: string | null): Promise<Account>;
|
|
74
74
|
/** Returns metadata only — no `html`. Use `templateInfo` to read the body
|
|
75
|
-
* back.
|
|
76
|
-
|
|
75
|
+
* back.
|
|
76
|
+
*
|
|
77
|
+
* `name` is nullable because the service accepts a nameless template. It
|
|
78
|
+
* stays a required POSITIONAL parameter — pass an explicit `null`, not
|
|
79
|
+
* nothing — and `TemplateMeta.name` then comes back `null`. */
|
|
80
|
+
export declare function createTemplate(name: string | null, subject: string, html: string): Promise<TemplateMeta>;
|
|
77
81
|
/** The paged primitive; use `listAllTemplates` to walk to the end. Rows
|
|
78
82
|
* carry metadata only — no `html`. */
|
|
79
83
|
export declare function listTemplates(options?: ListTemplatesOptions): Promise<{
|
package/dist/index.js
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
// POST routes. See README.md for where app_id lives on each route: this is
|
|
3
3
|
// the one service where it moves between the body and the path.
|
|
4
4
|
import { appId, call, collectPages } from "@pilely/core";
|
|
5
|
+
/**
|
|
6
|
+
* Every route on this service needs an app id on the wire (body or path).
|
|
7
|
+
* `@pilely/core`'s `appId()` can return `null` (no tag, `ready()` not yet
|
|
8
|
+
* resolved) — this service cannot send that: a `null` in a path template
|
|
9
|
+
* would silently become the literal string `"null"`. Fail loudly instead.
|
|
10
|
+
*/
|
|
11
|
+
function requireAppId() {
|
|
12
|
+
const id = appId();
|
|
13
|
+
if (!id) {
|
|
14
|
+
throw new Error('pilely client not loaded, <meta name="pilely-app"> missing, and no token pile_id claim available yet — call after ready()');
|
|
15
|
+
}
|
|
16
|
+
return id;
|
|
17
|
+
}
|
|
5
18
|
// ── Sending ──────────────────────────────────────────────────────────────
|
|
6
19
|
/**
|
|
7
20
|
* `app_id` is added from core's `appId()` — it is not a caller parameter.
|
|
@@ -12,8 +25,8 @@ import { appId, call, collectPages } from "@pilely/core";
|
|
|
12
25
|
*/
|
|
13
26
|
export async function send(input) {
|
|
14
27
|
const body = "template_id" in input
|
|
15
|
-
? { app_id:
|
|
16
|
-
: { app_id:
|
|
28
|
+
? { app_id: requireAppId(), to: input.to, template_id: input.template_id, variables: input.variables }
|
|
29
|
+
: { app_id: requireAppId(), to: input.to, subject: input.subject, html: input.html };
|
|
17
30
|
return call({
|
|
18
31
|
service: "simple-email",
|
|
19
32
|
path: "/send",
|
|
@@ -27,7 +40,7 @@ export async function listSends(options = {}) {
|
|
|
27
40
|
service: "simple-email",
|
|
28
41
|
path: "/sends/list",
|
|
29
42
|
body: {
|
|
30
|
-
app_id:
|
|
43
|
+
app_id: requireAppId(),
|
|
31
44
|
limit: options.limit,
|
|
32
45
|
after_created_time_stamp: options.after_created_time_stamp,
|
|
33
46
|
after_send_id: options.after_send_id,
|
|
@@ -46,7 +59,7 @@ export async function listAllSends() {
|
|
|
46
59
|
* key is left off the wire, meaning owner-only. `setAccountAccess` below
|
|
47
60
|
* is the opposite: its key is always required. */
|
|
48
61
|
export async function createAccount(sendGroup) {
|
|
49
|
-
const body = { app_id:
|
|
62
|
+
const body = { app_id: requireAppId() };
|
|
50
63
|
if (sendGroup !== undefined) {
|
|
51
64
|
body.send_group = sendGroup;
|
|
52
65
|
}
|
|
@@ -83,7 +96,7 @@ export async function listAllAccounts() {
|
|
|
83
96
|
export async function accountInfo() {
|
|
84
97
|
const json = await call({
|
|
85
98
|
service: "simple-email",
|
|
86
|
-
path: `/accounts/${
|
|
99
|
+
path: `/accounts/${requireAppId()}/info`,
|
|
87
100
|
});
|
|
88
101
|
return {
|
|
89
102
|
account: json.account,
|
|
@@ -94,7 +107,7 @@ export async function accountInfo() {
|
|
|
94
107
|
};
|
|
95
108
|
}
|
|
96
109
|
export async function deleteAccount() {
|
|
97
|
-
await call({ service: "simple-email", path: `/accounts/${
|
|
110
|
+
await call({ service: "simple-email", path: `/accounts/${requireAppId()}/delete` });
|
|
98
111
|
}
|
|
99
112
|
/** `sendGroup` is REQUIRED here and its key is always sent, even `null` —
|
|
100
113
|
* the opposite of `createAccount`, where the key is optional. `null`
|
|
@@ -102,18 +115,22 @@ export async function deleteAccount() {
|
|
|
102
115
|
export async function setAccountAccess(sendGroup) {
|
|
103
116
|
const json = await call({
|
|
104
117
|
service: "simple-email",
|
|
105
|
-
path: `/accounts/${
|
|
118
|
+
path: `/accounts/${requireAppId()}/access/set`,
|
|
106
119
|
body: { send_group: sendGroup },
|
|
107
120
|
});
|
|
108
121
|
return json.account;
|
|
109
122
|
}
|
|
110
123
|
// ── Templates (nested under the account) ────────────────────────────────
|
|
111
124
|
/** Returns metadata only — no `html`. Use `templateInfo` to read the body
|
|
112
|
-
* back.
|
|
125
|
+
* back.
|
|
126
|
+
*
|
|
127
|
+
* `name` is nullable because the service accepts a nameless template. It
|
|
128
|
+
* stays a required POSITIONAL parameter — pass an explicit `null`, not
|
|
129
|
+
* nothing — and `TemplateMeta.name` then comes back `null`. */
|
|
113
130
|
export async function createTemplate(name, subject, html) {
|
|
114
131
|
const json = await call({
|
|
115
132
|
service: "simple-email",
|
|
116
|
-
path: `/accounts/${
|
|
133
|
+
path: `/accounts/${requireAppId()}/templates/create`,
|
|
117
134
|
body: { name, subject, html },
|
|
118
135
|
});
|
|
119
136
|
return json.template;
|
|
@@ -123,7 +140,7 @@ export async function createTemplate(name, subject, html) {
|
|
|
123
140
|
export async function listTemplates(options = {}) {
|
|
124
141
|
const json = await call({
|
|
125
142
|
service: "simple-email",
|
|
126
|
-
path: `/accounts/${
|
|
143
|
+
path: `/accounts/${requireAppId()}/templates/list`,
|
|
127
144
|
body: {
|
|
128
145
|
limit: options.limit,
|
|
129
146
|
after_created_time_stamp: options.after_created_time_stamp,
|
|
@@ -142,7 +159,7 @@ export async function listAllTemplates() {
|
|
|
142
159
|
export async function templateInfo(templateNanoid) {
|
|
143
160
|
const json = await call({
|
|
144
161
|
service: "simple-email",
|
|
145
|
-
path: `/accounts/${
|
|
162
|
+
path: `/accounts/${requireAppId()}/templates/${templateNanoid}/info`,
|
|
146
163
|
});
|
|
147
164
|
return json.template;
|
|
148
165
|
}
|
|
@@ -154,7 +171,7 @@ export async function templateInfo(templateNanoid) {
|
|
|
154
171
|
export async function updateTemplate(templateNanoid, body) {
|
|
155
172
|
const json = await call({
|
|
156
173
|
service: "simple-email",
|
|
157
|
-
path: `/accounts/${
|
|
174
|
+
path: `/accounts/${requireAppId()}/templates/${templateNanoid}/update`,
|
|
158
175
|
body,
|
|
159
176
|
});
|
|
160
177
|
return json.template;
|
|
@@ -162,6 +179,6 @@ export async function updateTemplate(templateNanoid, body) {
|
|
|
162
179
|
export async function deleteTemplate(templateNanoid) {
|
|
163
180
|
await call({
|
|
164
181
|
service: "simple-email",
|
|
165
|
-
path: `/accounts/${
|
|
182
|
+
path: `/accounts/${requireAppId()}/templates/${templateNanoid}/delete`,
|
|
166
183
|
});
|
|
167
184
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
/** Every `code` the simple_email service can put in a `{ok:false, code, reason}`
|
|
2
|
+
* refusal. Exhaustive as of the 2026-09 review, checked against the service's
|
|
3
|
+
* `response_json.rs` and `render.rs`.
|
|
4
|
+
*
|
|
5
|
+
* This is the union that most earns its keep: `daily_cap_exceeded` and
|
|
6
|
+
* `rate_limited` BOTH answer 429, so a consumer cannot tell a burst limit
|
|
7
|
+
* (retry shortly) from the per-app daily send cap (retry tomorrow) by status
|
|
8
|
+
* alone — only by matching this code. */
|
|
9
|
+
export type SimpleEmailErrorCode = "unauthenticated" | "bad_forwarded_identity" | "bad_request" | "not_found" | "account_exists" | "account_limit_reached" | "template_limit_reached"
|
|
10
|
+
/** 429, same status as `rate_limited` — the per-app UTC-day send cap. */
|
|
11
|
+
| "daily_cap_exceeded" | "phone_verification_required" | "phone_verification_unavailable" | "out_of_traffic_credits" | "rate_limited" | "internal";
|
|
1
12
|
/** `address` is derived at read time, never stored, and `null` when the
|
|
2
13
|
* app it names is gone. */
|
|
3
14
|
export interface Account {
|
|
@@ -21,15 +32,23 @@ export interface AccountInfoEnvelope {
|
|
|
21
32
|
/** What `templates/create` and `templates/update` answer with — no `html`. */
|
|
22
33
|
export interface TemplateMeta {
|
|
23
34
|
template_id: string;
|
|
24
|
-
/** Not updatable through `updateTemplate` — only `subject` and `html` are.
|
|
25
|
-
|
|
35
|
+
/** Not updatable through `updateTemplate` — only `subject` and `html` are.
|
|
36
|
+
* `null` when the template was created without one: the service accepts a
|
|
37
|
+
* nameless template and returns the name back as `null`. */
|
|
38
|
+
name: string | null;
|
|
26
39
|
subject: string;
|
|
27
40
|
created_time_stamp: number;
|
|
28
41
|
updated_time_stamp: number;
|
|
29
42
|
}
|
|
30
|
-
/** What `templates/{t}/info` answers with — `TemplateMeta` plus the body.
|
|
43
|
+
/** What `templates/{t}/info` answers with — `TemplateMeta` plus the body.
|
|
44
|
+
*
|
|
45
|
+
* `html` is OPTIONAL, not nullable: when the R2 object behind the template is
|
|
46
|
+
* missing (the compensated-create residue the service keeps visible so an
|
|
47
|
+
* owner can spot it) or its body is not valid UTF-8, the service omits the key
|
|
48
|
+
* entirely rather than sending `null`. Test with `"html" in template` or a
|
|
49
|
+
* plain falsy check — never assume the body is there. */
|
|
31
50
|
export interface Template extends TemplateMeta {
|
|
32
|
-
html
|
|
51
|
+
html?: string;
|
|
33
52
|
}
|
|
34
53
|
export interface SendRow {
|
|
35
54
|
send_id: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pilely/simple-email",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "A typed client for the simple_email service.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -21,11 +21,11 @@
|
|
|
21
21
|
"directory": "npm/simple-email"
|
|
22
22
|
},
|
|
23
23
|
"peerDependencies": {
|
|
24
|
-
"@pilely/core": "^0.
|
|
24
|
+
"@pilely/core": "^0.2.0"
|
|
25
25
|
},
|
|
26
26
|
"scripts": {
|
|
27
27
|
"build": "tsc -p tsconfig.json",
|
|
28
|
-
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
28
|
+
"typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.test.json",
|
|
29
29
|
"test": "vitest run"
|
|
30
30
|
}
|
|
31
31
|
}
|