@yougrowai/node 0.3.0 → 0.4.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/README.md +42 -13
- package/dist/cjs/index.d.ts +40 -6
- package/dist/cjs/index.js +28 -5
- package/dist/cjs/server.d.ts +24 -0
- package/dist/index.d.ts +40 -6
- package/dist/index.js +28 -5
- package/dist/server.d.ts +24 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -10,8 +10,8 @@ https://yougrow.ai/developers are the contract. The source on GitHub isn't: its
|
|
|
10
10
|
main branch can be ahead of what's been released.
|
|
11
11
|
|
|
12
12
|
> **2026-09-25:** API v1 (`POST /api/v1/events`, HMAC signing) was removed.
|
|
13
|
-
> 0.3.0
|
|
14
|
-
> sending. See [Upgrading from 0.1.x](#upgrading-from-01x).
|
|
13
|
+
> 0.3.0 and later are the client for API v2; 0.1.x only spoke v1, so upgrade to
|
|
14
|
+
> keep sending. See [Upgrading from 0.1.x](#upgrading-from-01x).
|
|
15
15
|
|
|
16
16
|
```sh
|
|
17
17
|
npm install @yougrowai/node
|
|
@@ -41,6 +41,9 @@ const yg = new YouGrow({
|
|
|
41
41
|
origin: process.env.YOUGROW_ORIGIN, // optional; defaults to https://yougrow.ai
|
|
42
42
|
});
|
|
43
43
|
|
|
44
|
+
// Check the key (e.g. when you deploy): which connection and environment it's for
|
|
45
|
+
const { connection } = await yg.me(); // { name, environment: "production", status: "active", … }
|
|
46
|
+
|
|
44
47
|
// At sign-up: who they are, and the basis for emailing them
|
|
45
48
|
await yg.users.update(user.id, {
|
|
46
49
|
email: user.email,
|
|
@@ -135,8 +138,10 @@ record an event twice. A key that was already recorded resolves
|
|
|
135
138
|
- A step's value is when it was done (`null`: not done). Timestamps are ISO 8601
|
|
136
139
|
with a zone (`Z` or `+01:00`), e.g. from `toISOString()`.
|
|
137
140
|
- `email`, `firstName`, `lastName`, `timezone` and `locale` are fields of their
|
|
138
|
-
own, never traits.
|
|
139
|
-
|
|
141
|
+
own, never traits. An invalid one doesn't sink the update: the rest applies,
|
|
142
|
+
that field keeps its stored value, and the result lists it in
|
|
143
|
+
`ignoredFields`. Anything else invalid, including an unknown field, is
|
|
144
|
+
refused (400), so a typo can't be silently dropped.
|
|
140
145
|
- Up to 50 steps, 50 facts and 50 traits per user. Step ids are lower-case
|
|
141
146
|
letters, digits, `_` and `-`; trait keys start with a letter.
|
|
142
147
|
|
|
@@ -178,8 +183,13 @@ Every method returns a promise; await it.
|
|
|
178
183
|
`code` (the API's `error`, e.g. `invalid`, `unauthorized`, `body_too_large`),
|
|
179
184
|
`fields` for a 400, and a message such as
|
|
180
185
|
`YouGrow API 400 invalid: traits.plan: …`. A 429 or 5xx that outlasts the
|
|
181
|
-
retries rejects the same way
|
|
182
|
-
error
|
|
186
|
+
retries rejects the same way, and so does a network failure or timeout
|
|
187
|
+
(`status` 0, `code` `network_error` or `timeout`, the original error as
|
|
188
|
+
`cause`).
|
|
189
|
+
- `err.retryable` says whether trying again later may work: true for a 429, a
|
|
190
|
+
5xx, a timeout or a network failure. In a queue worker, or a trigger with
|
|
191
|
+
retries on, rethrow those so the work is redelivered, and log the rest: a
|
|
192
|
+
400 won't succeed as it is. Every write is safe to repeat.
|
|
183
193
|
- A user id the API can't take (empty, over 256 characters, `"batch"`, `"."`
|
|
184
194
|
or `".."`) rejects with a `TypeError` before anything is sent.
|
|
185
195
|
|
|
@@ -189,8 +199,9 @@ import { YouGrowError } from "@yougrowai/node";
|
|
|
189
199
|
try {
|
|
190
200
|
await yg.users.update(user.id, patch);
|
|
191
201
|
} catch (err) {
|
|
192
|
-
if (err instanceof YouGrowError && err.
|
|
193
|
-
|
|
202
|
+
if (err instanceof YouGrowError && err.retryable) throw err; // 429, 5xx, timeout, network: let it be retried
|
|
203
|
+
if (err instanceof YouGrowError) console.error(err.message, err.fields); // e.g. a 400: [{ path, message }]; fix it
|
|
204
|
+
else throw err;
|
|
194
205
|
}
|
|
195
206
|
```
|
|
196
207
|
|
|
@@ -285,10 +296,20 @@ never invents them.
|
|
|
285
296
|
|
|
286
297
|
### Webhooks
|
|
287
298
|
|
|
288
|
-
YouGrow tells your webhook endpoint about
|
|
289
|
-
|
|
290
|
-
`
|
|
291
|
-
|
|
299
|
+
YouGrow tells your webhook endpoint about changes to mirror. Verify each request
|
|
300
|
+
with `verifier.verify({ …, direction: "webhook" })`, then read the body as a
|
|
301
|
+
`WebhookEvent` (a type from `@yougrowai/node/server`):
|
|
302
|
+
|
|
303
|
+
- `email_preferences.updated`: the person unsubscribed from one of YouGrow's
|
|
304
|
+
emails (`data.scope` is `all` or `category`).
|
|
305
|
+
- `email.suppressed`: YouGrow stopped emailing them, because their address
|
|
306
|
+
hard-bounced (`data.reason` `hard_bounce`) or they reported an email as spam
|
|
307
|
+
(`complaint`).
|
|
308
|
+
- `connection.test`: the Test webhook button.
|
|
309
|
+
|
|
310
|
+
Reply 2xx to any type you don't handle. Webhook ids (`jti` in the token, `id` in
|
|
311
|
+
the body) are unique and the same on every retry, so you can drop duplicates.
|
|
312
|
+
API writes never trigger a webhook, so echoing a change back can't loop.
|
|
292
313
|
|
|
293
314
|
## Without the SDK
|
|
294
315
|
|
|
@@ -322,9 +343,17 @@ at https://yougrow.ai/developers.
|
|
|
322
343
|
`test/vectors.json` (included in this package) holds reference tokens for
|
|
323
344
|
checking your own verifier.
|
|
324
345
|
|
|
346
|
+
## Upgrading from 0.3
|
|
347
|
+
|
|
348
|
+
- A network failure or timeout that outlasts the retries now rejects with a
|
|
349
|
+
`YouGrowError` (`status` 0, `code` `network_error` or `timeout`) instead of
|
|
350
|
+
the raw error, which is its `cause`.
|
|
351
|
+
- New: `yg.me()`, `err.retryable`, `ignoredFields` on update results (and
|
|
352
|
+
`fields_ignored` in batch results), and the `WebhookEvent` type.
|
|
353
|
+
|
|
325
354
|
## Upgrading from 0.1.x
|
|
326
355
|
|
|
327
|
-
| 0.1.x (API v1) | 0.3.0 (API v2) |
|
|
356
|
+
| 0.1.x (API v1) | 0.3.0 and later (API v2) |
|
|
328
357
|
|---|---|
|
|
329
358
|
| `identify({ userId, traits, consent })` | `users.update(userId, { email, firstName, …, consent, traits })`: profile fields are top-level, and `consent` is the basis itself, e.g. `"soft_opt_in"` |
|
|
330
359
|
| `track({ event: "user.signed_up" })` | `signedUpAt` in `users.update` |
|
package/dist/cjs/index.d.ts
CHANGED
|
@@ -81,9 +81,12 @@ export interface UserView extends UserState {
|
|
|
81
81
|
}
|
|
82
82
|
/** Why a write was ignored: older than the stored state, or than the user's deletion. */
|
|
83
83
|
export type SkipReason = "stale_write" | "deleted_later";
|
|
84
|
-
export type PatchResponse =
|
|
84
|
+
export type PatchResponse =
|
|
85
|
+
/** `ignoredFields`: profile fields whose value was invalid, so left as they were; the rest applied. */
|
|
86
|
+
{
|
|
85
87
|
applied: true;
|
|
86
88
|
user: UserState;
|
|
89
|
+
ignoredFields?: FieldError[];
|
|
87
90
|
} | {
|
|
88
91
|
applied: false;
|
|
89
92
|
reason: SkipReason;
|
|
@@ -103,15 +106,30 @@ export interface BatchResponse {
|
|
|
103
106
|
applied: number;
|
|
104
107
|
ignored: number;
|
|
105
108
|
failed: number;
|
|
106
|
-
/**
|
|
109
|
+
/**
|
|
110
|
+
* The ignored and failed items, and applied ones whose invalid profile fields were
|
|
111
|
+
* left as they were (reason `fields_ignored`). `index` is the item's position in your array.
|
|
112
|
+
*/
|
|
107
113
|
results: Array<{
|
|
108
114
|
index: number;
|
|
109
115
|
userId: string | null;
|
|
110
|
-
status: "ignored" | "failed";
|
|
116
|
+
status: "applied" | "ignored" | "failed";
|
|
111
117
|
reason: string;
|
|
112
118
|
fields?: FieldError[];
|
|
113
119
|
}>;
|
|
114
120
|
}
|
|
121
|
+
/** The connection behind your key (`GET /api/v2/me`). */
|
|
122
|
+
export interface MeResponse {
|
|
123
|
+
connection: {
|
|
124
|
+
id: string;
|
|
125
|
+
name: string;
|
|
126
|
+
environment: "staging" | "production" | null;
|
|
127
|
+
status: "active" | "paused" | "revoked";
|
|
128
|
+
};
|
|
129
|
+
keyId: string;
|
|
130
|
+
/** A new secret was issued in the last 24 hours; the previous one works until then. */
|
|
131
|
+
rotating: boolean;
|
|
132
|
+
}
|
|
115
133
|
export interface EventResult {
|
|
116
134
|
recorded: boolean;
|
|
117
135
|
duplicate: boolean;
|
|
@@ -166,15 +184,26 @@ export interface EventsApi {
|
|
|
166
184
|
/** Record a milestone, e.g. "report.exported". Optional: journeys run on state. */
|
|
167
185
|
track(userId: string, event: string, opts?: TrackOptions): Promise<EventResult>;
|
|
168
186
|
}
|
|
169
|
-
/**
|
|
187
|
+
/**
|
|
188
|
+
* The API refused a request, or it still failed after the retries: a 429, a 5xx,
|
|
189
|
+
* or a timeout or network failure (`status` 0, `code` `timeout` or `network_error`).
|
|
190
|
+
*/
|
|
170
191
|
export declare class YouGrowError extends Error {
|
|
171
192
|
readonly status: number;
|
|
172
193
|
readonly body?: unknown | undefined;
|
|
173
|
-
/** The API's error code (`invalid`, `unauthorized`, `rate_limited`…), or `http_<status
|
|
194
|
+
/** The API's error code (`invalid`, `unauthorized`, `rate_limited`…), `timeout` or `network_error`, or `http_<status>`. */
|
|
174
195
|
readonly code: string;
|
|
175
196
|
/** For a 400: which fields were wrong, and why. */
|
|
176
197
|
readonly fields?: FieldError[];
|
|
177
|
-
|
|
198
|
+
/**
|
|
199
|
+
* True for a 429, a 5xx, a timeout or a network failure: trying again later may
|
|
200
|
+
* work, so a queue or trigger should rethrow it for redelivery. Anything else
|
|
201
|
+
* (a 400, 401, 404…) won't succeed as it is.
|
|
202
|
+
*/
|
|
203
|
+
readonly retryable: boolean;
|
|
204
|
+
constructor(message: string, status: number, body?: unknown | undefined, options?: {
|
|
205
|
+
cause?: unknown;
|
|
206
|
+
});
|
|
178
207
|
}
|
|
179
208
|
/** From `users.batch` with `throwOnItemError`: some items failed. The rest were applied; `result` says which. */
|
|
180
209
|
export declare class YouGrowBatchError extends Error {
|
|
@@ -186,4 +215,9 @@ export declare class YouGrow {
|
|
|
186
215
|
readonly users: UsersApi;
|
|
187
216
|
readonly events: EventsApi;
|
|
188
217
|
constructor(opts: YouGrowOptions);
|
|
218
|
+
/**
|
|
219
|
+
* The connection behind your key: its name, environment and status. A credential
|
|
220
|
+
* check — and a way to catch a key from the wrong environment — before you send.
|
|
221
|
+
*/
|
|
222
|
+
me(): Promise<MeResponse>;
|
|
189
223
|
}
|
package/dist/cjs/index.js
CHANGED
|
@@ -11,16 +11,25 @@ const BATCH_ENVELOPE_BYTES = '{"users":[]}'.length;
|
|
|
11
11
|
const MAX_USER_ID = 256;
|
|
12
12
|
/** The longest delay a Node timer accepts. */
|
|
13
13
|
const MAX_TIMER_MS = 2_147_483_647;
|
|
14
|
-
/**
|
|
14
|
+
/**
|
|
15
|
+
* The API refused a request, or it still failed after the retries: a 429, a 5xx,
|
|
16
|
+
* or a timeout or network failure (`status` 0, `code` `timeout` or `network_error`).
|
|
17
|
+
*/
|
|
15
18
|
class YouGrowError extends Error {
|
|
16
19
|
status;
|
|
17
20
|
body;
|
|
18
|
-
/** The API's error code (`invalid`, `unauthorized`, `rate_limited`…), or `http_<status
|
|
21
|
+
/** The API's error code (`invalid`, `unauthorized`, `rate_limited`…), `timeout` or `network_error`, or `http_<status>`. */
|
|
19
22
|
code;
|
|
20
23
|
/** For a 400: which fields were wrong, and why. */
|
|
21
24
|
fields;
|
|
22
|
-
|
|
23
|
-
|
|
25
|
+
/**
|
|
26
|
+
* True for a 429, a 5xx, a timeout or a network failure: trying again later may
|
|
27
|
+
* work, so a queue or trigger should rethrow it for redelivery. Anything else
|
|
28
|
+
* (a 400, 401, 404…) won't succeed as it is.
|
|
29
|
+
*/
|
|
30
|
+
retryable;
|
|
31
|
+
constructor(message, status, body, options) {
|
|
32
|
+
super(message, options);
|
|
24
33
|
this.status = status;
|
|
25
34
|
this.body = body;
|
|
26
35
|
this.name = "YouGrowError";
|
|
@@ -28,6 +37,7 @@ class YouGrowError extends Error {
|
|
|
28
37
|
this.code = typeof b?.error === "string" ? b.error : `http_${status}`;
|
|
29
38
|
if (Array.isArray(b?.fields))
|
|
30
39
|
this.fields = b.fields;
|
|
40
|
+
this.retryable = status === 0 || status === 429 || status >= 500;
|
|
31
41
|
}
|
|
32
42
|
}
|
|
33
43
|
exports.YouGrowError = YouGrowError;
|
|
@@ -90,6 +100,13 @@ class YouGrow {
|
|
|
90
100
|
},
|
|
91
101
|
};
|
|
92
102
|
}
|
|
103
|
+
/**
|
|
104
|
+
* The connection behind your key: its name, environment and status. A credential
|
|
105
|
+
* check — and a way to catch a key from the wrong environment — before you send.
|
|
106
|
+
*/
|
|
107
|
+
async me() {
|
|
108
|
+
return (await this.#send("GET", "/api/v2/me"));
|
|
109
|
+
}
|
|
93
110
|
async #patchMany(items, opts = {}) {
|
|
94
111
|
if (!Array.isArray(items))
|
|
95
112
|
throw new TypeError("YouGrow: users.batch takes an array of { userId, ...patch }");
|
|
@@ -137,7 +154,7 @@ class YouGrow {
|
|
|
137
154
|
catch (err) {
|
|
138
155
|
// Network error or timeout.
|
|
139
156
|
if (attempt >= this.#maxRetries)
|
|
140
|
-
throw err;
|
|
157
|
+
throw transportError(err);
|
|
141
158
|
await sleep(this.#retryWait(attempt));
|
|
142
159
|
continue;
|
|
143
160
|
}
|
|
@@ -212,6 +229,12 @@ function failureSummary(r, count) {
|
|
|
212
229
|
return `users.batch: ${r.failed} of ${count} users failed${shown.length > 0 ? ` (${shown.join(", ")})` : ""}`;
|
|
213
230
|
}
|
|
214
231
|
/** e.g. "YouGrow API 400 invalid: traits.plan: …". */
|
|
232
|
+
/** A timeout or network failure that outlasted the retries: status 0, retryable, the original error as `cause`. */
|
|
233
|
+
function transportError(err) {
|
|
234
|
+
const code = err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError") ? "timeout" : "network_error";
|
|
235
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
236
|
+
return new YouGrowError(`YouGrow API ${code}: ${detail}`, 0, { error: code }, { cause: err });
|
|
237
|
+
}
|
|
215
238
|
function apiError(status, body) {
|
|
216
239
|
const b = asRecord(body);
|
|
217
240
|
const fields = Array.isArray(b?.fields) ? b.fields : [];
|
package/dist/cjs/server.d.ts
CHANGED
|
@@ -28,6 +28,30 @@ export type VerifyResult = {
|
|
|
28
28
|
ok: false;
|
|
29
29
|
reason: JwtFailure | "keys_unavailable";
|
|
30
30
|
};
|
|
31
|
+
/** What YouGrow POSTs to your webhook endpoint: one of these, by `type`. Reply 2xx to any type you don't handle. */
|
|
32
|
+
export type WebhookEvent = WebhookEnvelope<"email_preferences.updated",
|
|
33
|
+
/** The person unsubscribed from one of YouGrow's emails: from one category, or all of them. */
|
|
34
|
+
{
|
|
35
|
+
userId: string;
|
|
36
|
+
category: string;
|
|
37
|
+
subscribed: false;
|
|
38
|
+
scope: "all" | "category";
|
|
39
|
+
source: string;
|
|
40
|
+
}>
|
|
41
|
+
/** YouGrow stopped emailing the person: their address hard-bounced, or they reported an email as spam. */
|
|
42
|
+
| WebhookEnvelope<"email.suppressed", {
|
|
43
|
+
userId: string;
|
|
44
|
+
reason: "hard_bounce" | "complaint";
|
|
45
|
+
}>
|
|
46
|
+
/** The Test webhook button. */
|
|
47
|
+
| WebhookEnvelope<"connection.test", Record<string, never>>;
|
|
48
|
+
export interface WebhookEnvelope<T extends string, D> {
|
|
49
|
+
/** Unique per webhook, and the same on every retry: drop ones you've seen. */
|
|
50
|
+
id: string;
|
|
51
|
+
type: T;
|
|
52
|
+
createdAt: string;
|
|
53
|
+
data: D;
|
|
54
|
+
}
|
|
31
55
|
export interface VerifierOptions {
|
|
32
56
|
/** Your connection's key id — the token's audience. */
|
|
33
57
|
keyId: string;
|
package/dist/index.d.ts
CHANGED
|
@@ -81,9 +81,12 @@ export interface UserView extends UserState {
|
|
|
81
81
|
}
|
|
82
82
|
/** Why a write was ignored: older than the stored state, or than the user's deletion. */
|
|
83
83
|
export type SkipReason = "stale_write" | "deleted_later";
|
|
84
|
-
export type PatchResponse =
|
|
84
|
+
export type PatchResponse =
|
|
85
|
+
/** `ignoredFields`: profile fields whose value was invalid, so left as they were; the rest applied. */
|
|
86
|
+
{
|
|
85
87
|
applied: true;
|
|
86
88
|
user: UserState;
|
|
89
|
+
ignoredFields?: FieldError[];
|
|
87
90
|
} | {
|
|
88
91
|
applied: false;
|
|
89
92
|
reason: SkipReason;
|
|
@@ -103,15 +106,30 @@ export interface BatchResponse {
|
|
|
103
106
|
applied: number;
|
|
104
107
|
ignored: number;
|
|
105
108
|
failed: number;
|
|
106
|
-
/**
|
|
109
|
+
/**
|
|
110
|
+
* The ignored and failed items, and applied ones whose invalid profile fields were
|
|
111
|
+
* left as they were (reason `fields_ignored`). `index` is the item's position in your array.
|
|
112
|
+
*/
|
|
107
113
|
results: Array<{
|
|
108
114
|
index: number;
|
|
109
115
|
userId: string | null;
|
|
110
|
-
status: "ignored" | "failed";
|
|
116
|
+
status: "applied" | "ignored" | "failed";
|
|
111
117
|
reason: string;
|
|
112
118
|
fields?: FieldError[];
|
|
113
119
|
}>;
|
|
114
120
|
}
|
|
121
|
+
/** The connection behind your key (`GET /api/v2/me`). */
|
|
122
|
+
export interface MeResponse {
|
|
123
|
+
connection: {
|
|
124
|
+
id: string;
|
|
125
|
+
name: string;
|
|
126
|
+
environment: "staging" | "production" | null;
|
|
127
|
+
status: "active" | "paused" | "revoked";
|
|
128
|
+
};
|
|
129
|
+
keyId: string;
|
|
130
|
+
/** A new secret was issued in the last 24 hours; the previous one works until then. */
|
|
131
|
+
rotating: boolean;
|
|
132
|
+
}
|
|
115
133
|
export interface EventResult {
|
|
116
134
|
recorded: boolean;
|
|
117
135
|
duplicate: boolean;
|
|
@@ -166,15 +184,26 @@ export interface EventsApi {
|
|
|
166
184
|
/** Record a milestone, e.g. "report.exported". Optional: journeys run on state. */
|
|
167
185
|
track(userId: string, event: string, opts?: TrackOptions): Promise<EventResult>;
|
|
168
186
|
}
|
|
169
|
-
/**
|
|
187
|
+
/**
|
|
188
|
+
* The API refused a request, or it still failed after the retries: a 429, a 5xx,
|
|
189
|
+
* or a timeout or network failure (`status` 0, `code` `timeout` or `network_error`).
|
|
190
|
+
*/
|
|
170
191
|
export declare class YouGrowError extends Error {
|
|
171
192
|
readonly status: number;
|
|
172
193
|
readonly body?: unknown | undefined;
|
|
173
|
-
/** The API's error code (`invalid`, `unauthorized`, `rate_limited`…), or `http_<status
|
|
194
|
+
/** The API's error code (`invalid`, `unauthorized`, `rate_limited`…), `timeout` or `network_error`, or `http_<status>`. */
|
|
174
195
|
readonly code: string;
|
|
175
196
|
/** For a 400: which fields were wrong, and why. */
|
|
176
197
|
readonly fields?: FieldError[];
|
|
177
|
-
|
|
198
|
+
/**
|
|
199
|
+
* True for a 429, a 5xx, a timeout or a network failure: trying again later may
|
|
200
|
+
* work, so a queue or trigger should rethrow it for redelivery. Anything else
|
|
201
|
+
* (a 400, 401, 404…) won't succeed as it is.
|
|
202
|
+
*/
|
|
203
|
+
readonly retryable: boolean;
|
|
204
|
+
constructor(message: string, status: number, body?: unknown | undefined, options?: {
|
|
205
|
+
cause?: unknown;
|
|
206
|
+
});
|
|
178
207
|
}
|
|
179
208
|
/** From `users.batch` with `throwOnItemError`: some items failed. The rest were applied; `result` says which. */
|
|
180
209
|
export declare class YouGrowBatchError extends Error {
|
|
@@ -186,4 +215,9 @@ export declare class YouGrow {
|
|
|
186
215
|
readonly users: UsersApi;
|
|
187
216
|
readonly events: EventsApi;
|
|
188
217
|
constructor(opts: YouGrowOptions);
|
|
218
|
+
/**
|
|
219
|
+
* The connection behind your key: its name, environment and status. A credential
|
|
220
|
+
* check — and a way to catch a key from the wrong environment — before you send.
|
|
221
|
+
*/
|
|
222
|
+
me(): Promise<MeResponse>;
|
|
189
223
|
}
|
package/dist/index.js
CHANGED
|
@@ -8,16 +8,25 @@ const BATCH_ENVELOPE_BYTES = '{"users":[]}'.length;
|
|
|
8
8
|
const MAX_USER_ID = 256;
|
|
9
9
|
/** The longest delay a Node timer accepts. */
|
|
10
10
|
const MAX_TIMER_MS = 2_147_483_647;
|
|
11
|
-
/**
|
|
11
|
+
/**
|
|
12
|
+
* The API refused a request, or it still failed after the retries: a 429, a 5xx,
|
|
13
|
+
* or a timeout or network failure (`status` 0, `code` `timeout` or `network_error`).
|
|
14
|
+
*/
|
|
12
15
|
export class YouGrowError extends Error {
|
|
13
16
|
status;
|
|
14
17
|
body;
|
|
15
|
-
/** The API's error code (`invalid`, `unauthorized`, `rate_limited`…), or `http_<status
|
|
18
|
+
/** The API's error code (`invalid`, `unauthorized`, `rate_limited`…), `timeout` or `network_error`, or `http_<status>`. */
|
|
16
19
|
code;
|
|
17
20
|
/** For a 400: which fields were wrong, and why. */
|
|
18
21
|
fields;
|
|
19
|
-
|
|
20
|
-
|
|
22
|
+
/**
|
|
23
|
+
* True for a 429, a 5xx, a timeout or a network failure: trying again later may
|
|
24
|
+
* work, so a queue or trigger should rethrow it for redelivery. Anything else
|
|
25
|
+
* (a 400, 401, 404…) won't succeed as it is.
|
|
26
|
+
*/
|
|
27
|
+
retryable;
|
|
28
|
+
constructor(message, status, body, options) {
|
|
29
|
+
super(message, options);
|
|
21
30
|
this.status = status;
|
|
22
31
|
this.body = body;
|
|
23
32
|
this.name = "YouGrowError";
|
|
@@ -25,6 +34,7 @@ export class YouGrowError extends Error {
|
|
|
25
34
|
this.code = typeof b?.error === "string" ? b.error : `http_${status}`;
|
|
26
35
|
if (Array.isArray(b?.fields))
|
|
27
36
|
this.fields = b.fields;
|
|
37
|
+
this.retryable = status === 0 || status === 429 || status >= 500;
|
|
28
38
|
}
|
|
29
39
|
}
|
|
30
40
|
/** From `users.batch` with `throwOnItemError`: some items failed. The rest were applied; `result` says which. */
|
|
@@ -85,6 +95,13 @@ export class YouGrow {
|
|
|
85
95
|
},
|
|
86
96
|
};
|
|
87
97
|
}
|
|
98
|
+
/**
|
|
99
|
+
* The connection behind your key: its name, environment and status. A credential
|
|
100
|
+
* check — and a way to catch a key from the wrong environment — before you send.
|
|
101
|
+
*/
|
|
102
|
+
async me() {
|
|
103
|
+
return (await this.#send("GET", "/api/v2/me"));
|
|
104
|
+
}
|
|
88
105
|
async #patchMany(items, opts = {}) {
|
|
89
106
|
if (!Array.isArray(items))
|
|
90
107
|
throw new TypeError("YouGrow: users.batch takes an array of { userId, ...patch }");
|
|
@@ -132,7 +149,7 @@ export class YouGrow {
|
|
|
132
149
|
catch (err) {
|
|
133
150
|
// Network error or timeout.
|
|
134
151
|
if (attempt >= this.#maxRetries)
|
|
135
|
-
throw err;
|
|
152
|
+
throw transportError(err);
|
|
136
153
|
await sleep(this.#retryWait(attempt));
|
|
137
154
|
continue;
|
|
138
155
|
}
|
|
@@ -206,6 +223,12 @@ function failureSummary(r, count) {
|
|
|
206
223
|
return `users.batch: ${r.failed} of ${count} users failed${shown.length > 0 ? ` (${shown.join(", ")})` : ""}`;
|
|
207
224
|
}
|
|
208
225
|
/** e.g. "YouGrow API 400 invalid: traits.plan: …". */
|
|
226
|
+
/** A timeout or network failure that outlasted the retries: status 0, retryable, the original error as `cause`. */
|
|
227
|
+
function transportError(err) {
|
|
228
|
+
const code = err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError") ? "timeout" : "network_error";
|
|
229
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
230
|
+
return new YouGrowError(`YouGrow API ${code}: ${detail}`, 0, { error: code }, { cause: err });
|
|
231
|
+
}
|
|
209
232
|
function apiError(status, body) {
|
|
210
233
|
const b = asRecord(body);
|
|
211
234
|
const fields = Array.isArray(b?.fields) ? b.fields : [];
|
package/dist/server.d.ts
CHANGED
|
@@ -28,6 +28,30 @@ export type VerifyResult = {
|
|
|
28
28
|
ok: false;
|
|
29
29
|
reason: JwtFailure | "keys_unavailable";
|
|
30
30
|
};
|
|
31
|
+
/** What YouGrow POSTs to your webhook endpoint: one of these, by `type`. Reply 2xx to any type you don't handle. */
|
|
32
|
+
export type WebhookEvent = WebhookEnvelope<"email_preferences.updated",
|
|
33
|
+
/** The person unsubscribed from one of YouGrow's emails: from one category, or all of them. */
|
|
34
|
+
{
|
|
35
|
+
userId: string;
|
|
36
|
+
category: string;
|
|
37
|
+
subscribed: false;
|
|
38
|
+
scope: "all" | "category";
|
|
39
|
+
source: string;
|
|
40
|
+
}>
|
|
41
|
+
/** YouGrow stopped emailing the person: their address hard-bounced, or they reported an email as spam. */
|
|
42
|
+
| WebhookEnvelope<"email.suppressed", {
|
|
43
|
+
userId: string;
|
|
44
|
+
reason: "hard_bounce" | "complaint";
|
|
45
|
+
}>
|
|
46
|
+
/** The Test webhook button. */
|
|
47
|
+
| WebhookEnvelope<"connection.test", Record<string, never>>;
|
|
48
|
+
export interface WebhookEnvelope<T extends string, D> {
|
|
49
|
+
/** Unique per webhook, and the same on every retry: drop ones you've seen. */
|
|
50
|
+
id: string;
|
|
51
|
+
type: T;
|
|
52
|
+
createdAt: string;
|
|
53
|
+
data: D;
|
|
54
|
+
}
|
|
31
55
|
export interface VerifierOptions {
|
|
32
56
|
/** Your connection's key id — the token's audience. */
|
|
33
57
|
keyId: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yougrowai/node",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Keep your users' state in YouGrow lifecycle journeys, and verify the requests YouGrow sends you.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "YouGrow.AI Limited",
|
|
@@ -22,7 +22,8 @@
|
|
|
22
22
|
"./server": {
|
|
23
23
|
"import": { "types": "./dist/server.d.ts", "default": "./dist/server.js" },
|
|
24
24
|
"require": { "types": "./dist/cjs/server.d.ts", "default": "./dist/cjs/server.js" }
|
|
25
|
-
}
|
|
25
|
+
},
|
|
26
|
+
"./package.json": "./package.json"
|
|
26
27
|
},
|
|
27
28
|
"typesVersions": {
|
|
28
29
|
"*": { "server": ["./dist/cjs/server.d.ts"] }
|