perspectapi-ts-sdk 3.5.0 → 3.6.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 +32 -4
- package/dist/index.d.mts +75 -9
- package/dist/index.d.ts +75 -9
- package/dist/index.js +229 -3
- package/dist/index.mjs +229 -3
- package/package.json +1 -1
- package/src/client/newsletter-client.ts +343 -3
- package/src/client/site-users-client.ts +18 -0
- package/src/index.ts +3 -0
- package/src/types/index.ts +31 -0
- package/src/utils/image-transform.ts +7 -7
package/dist/index.mjs
CHANGED
|
@@ -2003,11 +2003,123 @@ var NewsletterClient = class extends BaseClient {
|
|
|
2003
2003
|
/**
|
|
2004
2004
|
* Get available newsletter lists
|
|
2005
2005
|
*/
|
|
2006
|
-
async getLists(siteName) {
|
|
2007
|
-
|
|
2008
|
-
|
|
2006
|
+
async getLists(siteName, cachePolicy) {
|
|
2007
|
+
const endpoint = this.newsletterEndpoint(siteName, "/newsletter/lists");
|
|
2008
|
+
const path = this.buildPath(endpoint);
|
|
2009
|
+
return this.fetchWithCache(
|
|
2010
|
+
endpoint,
|
|
2011
|
+
void 0,
|
|
2012
|
+
this.buildNewsletterTags(siteName, [
|
|
2013
|
+
"newsletter:lists",
|
|
2014
|
+
`newsletter:lists:site:${siteName}`
|
|
2015
|
+
]),
|
|
2016
|
+
cachePolicy,
|
|
2017
|
+
() => this.http.get(path)
|
|
2009
2018
|
);
|
|
2010
2019
|
}
|
|
2020
|
+
/**
|
|
2021
|
+
* List publicly available (sent) newsletter campaigns
|
|
2022
|
+
*/
|
|
2023
|
+
async getPublishedCampaigns(siteName, params, cachePolicy) {
|
|
2024
|
+
const endpoint = this.newsletterEndpoint(siteName, "/newsletter/campaigns");
|
|
2025
|
+
const path = this.buildPath(endpoint);
|
|
2026
|
+
const normalizedParams = params ? { ...params } : void 0;
|
|
2027
|
+
if (normalizedParams) {
|
|
2028
|
+
const validatedLimit = validateOptionalLimit(
|
|
2029
|
+
normalizedParams.limit,
|
|
2030
|
+
"newsletter campaigns query"
|
|
2031
|
+
);
|
|
2032
|
+
if (validatedLimit !== void 0) {
|
|
2033
|
+
normalizedParams.limit = validatedLimit;
|
|
2034
|
+
} else {
|
|
2035
|
+
delete normalizedParams.limit;
|
|
2036
|
+
}
|
|
2037
|
+
if (typeof normalizedParams.search === "string" && normalizedParams.search.trim().length === 0) {
|
|
2038
|
+
delete normalizedParams.search;
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
return this.fetchWithCache(
|
|
2042
|
+
endpoint,
|
|
2043
|
+
normalizedParams,
|
|
2044
|
+
this.buildNewsletterTags(siteName, [
|
|
2045
|
+
"newsletter:campaigns",
|
|
2046
|
+
`newsletter:campaigns:list:${siteName}`
|
|
2047
|
+
]),
|
|
2048
|
+
cachePolicy,
|
|
2049
|
+
() => this.http.get(path, normalizedParams)
|
|
2050
|
+
);
|
|
2051
|
+
}
|
|
2052
|
+
/**
|
|
2053
|
+
* Fetch a publicly available (sent) newsletter campaign by slug
|
|
2054
|
+
*/
|
|
2055
|
+
async getPublishedCampaignBySlug(siteName, slug, optionsOrPolicy, cachePolicy) {
|
|
2056
|
+
let normalizedSlug = slug.trim();
|
|
2057
|
+
if (!normalizedSlug) {
|
|
2058
|
+
throw new Error("slug is required");
|
|
2059
|
+
}
|
|
2060
|
+
const isCachePolicyArg = this.isCachePolicy(optionsOrPolicy);
|
|
2061
|
+
const providedSlugPrefix = !isCachePolicyArg && optionsOrPolicy ? this.pickString(optionsOrPolicy, ["slugPrefix", "slug_prefix"]) : void 0;
|
|
2062
|
+
const resolvedCachePolicy = isCachePolicyArg ? optionsOrPolicy : cachePolicy;
|
|
2063
|
+
let slugPrefix = providedSlugPrefix;
|
|
2064
|
+
if (!slugPrefix) {
|
|
2065
|
+
const parts = normalizedSlug.split("/").filter(Boolean);
|
|
2066
|
+
if (parts.length > 1) {
|
|
2067
|
+
slugPrefix = parts.slice(0, -1).join("/");
|
|
2068
|
+
normalizedSlug = parts[parts.length - 1];
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
normalizedSlug = normalizedSlug.trim();
|
|
2072
|
+
if (!normalizedSlug) {
|
|
2073
|
+
throw new Error("slug is required");
|
|
2074
|
+
}
|
|
2075
|
+
const endpoint = this.newsletterEndpoint(
|
|
2076
|
+
siteName,
|
|
2077
|
+
`/newsletter/campaigns/${encodeURIComponent(normalizedSlug)}`
|
|
2078
|
+
);
|
|
2079
|
+
const path = this.buildPath(endpoint);
|
|
2080
|
+
const queryParams = slugPrefix ? { slug_prefix: slugPrefix } : void 0;
|
|
2081
|
+
return this.fetchWithCache(
|
|
2082
|
+
endpoint,
|
|
2083
|
+
queryParams,
|
|
2084
|
+
this.buildNewsletterTags(
|
|
2085
|
+
siteName,
|
|
2086
|
+
[
|
|
2087
|
+
"newsletter:campaigns",
|
|
2088
|
+
`newsletter:campaigns:slug:${siteName}:${normalizedSlug}`,
|
|
2089
|
+
`newsletter:campaigns:detail:${siteName}`
|
|
2090
|
+
],
|
|
2091
|
+
slugPrefix
|
|
2092
|
+
),
|
|
2093
|
+
resolvedCachePolicy,
|
|
2094
|
+
() => this.http.get(path, queryParams)
|
|
2095
|
+
);
|
|
2096
|
+
}
|
|
2097
|
+
/**
|
|
2098
|
+
* Invalidate cached published newsletter campaign data from a webhook payload.
|
|
2099
|
+
* Only publish events (and legacy sent aliases) trigger invalidation.
|
|
2100
|
+
*/
|
|
2101
|
+
async invalidatePublishedCampaignCacheFromWebhook(payload, fallbackSiteName) {
|
|
2102
|
+
const normalized = this.normalizeNewsletterWebhookPayload(payload, fallbackSiteName);
|
|
2103
|
+
if (!normalized.shouldInvalidate) {
|
|
2104
|
+
return {
|
|
2105
|
+
invalidated: false,
|
|
2106
|
+
tags: [],
|
|
2107
|
+
reason: normalized.reason || "event_not_publish_related"
|
|
2108
|
+
};
|
|
2109
|
+
}
|
|
2110
|
+
const tags = this.buildNewsletterTags(
|
|
2111
|
+
normalized.siteName,
|
|
2112
|
+
[
|
|
2113
|
+
"newsletter:campaigns",
|
|
2114
|
+
`newsletter:campaigns:list:${normalized.siteName}`,
|
|
2115
|
+
normalized.slug ? `newsletter:campaigns:slug:${normalized.siteName}:${normalized.slug}` : "",
|
|
2116
|
+
normalized.campaignId ? `newsletter:campaigns:id:${normalized.siteName}:${normalized.campaignId}` : ""
|
|
2117
|
+
],
|
|
2118
|
+
normalized.slugPrefix
|
|
2119
|
+
);
|
|
2120
|
+
await this.invalidateCache({ tags });
|
|
2121
|
+
return { invalidated: true, tags };
|
|
2122
|
+
}
|
|
2011
2123
|
/**
|
|
2012
2124
|
* Check subscription status by email
|
|
2013
2125
|
*/
|
|
@@ -2203,6 +2315,107 @@ var NewsletterClient = class extends BaseClient {
|
|
|
2203
2315
|
59
|
|
2204
2316
|
]);
|
|
2205
2317
|
}
|
|
2318
|
+
buildNewsletterTags(siteName, extraTags = [], slugPrefix) {
|
|
2319
|
+
const tags = /* @__PURE__ */ new Set(["newsletter"]);
|
|
2320
|
+
if (siteName) {
|
|
2321
|
+
tags.add(`newsletter:site:${siteName}`);
|
|
2322
|
+
}
|
|
2323
|
+
if (slugPrefix) {
|
|
2324
|
+
tags.add(`newsletter:prefix:${slugPrefix}`);
|
|
2325
|
+
}
|
|
2326
|
+
extraTags.filter(Boolean).forEach((tag) => tags.add(tag));
|
|
2327
|
+
return Array.from(tags.values());
|
|
2328
|
+
}
|
|
2329
|
+
extractSlugPrefix(slug) {
|
|
2330
|
+
const normalized = slug.trim();
|
|
2331
|
+
if (!normalized.includes("/")) {
|
|
2332
|
+
return void 0;
|
|
2333
|
+
}
|
|
2334
|
+
const [prefix] = normalized.split("/").filter(Boolean);
|
|
2335
|
+
return prefix || void 0;
|
|
2336
|
+
}
|
|
2337
|
+
normalizeNewsletterWebhookPayload(payload, fallbackSiteName) {
|
|
2338
|
+
const eventType = (this.pickString(payload, ["event_type", "type"]) || "").toLowerCase();
|
|
2339
|
+
const dataRaw = payload.data;
|
|
2340
|
+
const data = dataRaw && typeof dataRaw === "object" && !Array.isArray(dataRaw) ? dataRaw : payload;
|
|
2341
|
+
const originType = (this.pickString(data, ["origin_type", "originType"]) || "").toLowerCase();
|
|
2342
|
+
const status = (this.pickString(data, ["status", "campaign_status", "campaignStatus"]) || "").toLowerCase();
|
|
2343
|
+
if (!this.isPublishEvent(eventType, status, originType)) {
|
|
2344
|
+
return {
|
|
2345
|
+
shouldInvalidate: false,
|
|
2346
|
+
reason: "not_publish_or_sent_event",
|
|
2347
|
+
siteName: fallbackSiteName || ""
|
|
2348
|
+
};
|
|
2349
|
+
}
|
|
2350
|
+
const siteName = this.pickString(payload, ["site", "site_name", "siteName"]) || this.pickString(data, ["site", "site_name", "siteName"]) || fallbackSiteName || "";
|
|
2351
|
+
if (!siteName) {
|
|
2352
|
+
return {
|
|
2353
|
+
shouldInvalidate: false,
|
|
2354
|
+
reason: "missing_site_name",
|
|
2355
|
+
siteName: ""
|
|
2356
|
+
};
|
|
2357
|
+
}
|
|
2358
|
+
let slug = this.pickString(data, ["slug", "canonical_slug", "canonicalSlug"]) || this.pickString(payload, ["slug", "canonical_slug", "canonicalSlug"]) || void 0;
|
|
2359
|
+
let slugPrefix = this.pickString(data, ["slug_prefix", "slugPrefix"]) || this.pickString(payload, ["slug_prefix", "slugPrefix"]) || (slug ? this.extractSlugPrefix(slug) : void 0);
|
|
2360
|
+
if (slug) {
|
|
2361
|
+
const slugParts = slug.split("/").filter(Boolean);
|
|
2362
|
+
if (slugParts.length > 1) {
|
|
2363
|
+
if (!slugPrefix) {
|
|
2364
|
+
slugPrefix = slugParts.slice(0, -1).join("/");
|
|
2365
|
+
}
|
|
2366
|
+
slug = slugParts[slugParts.length - 1];
|
|
2367
|
+
}
|
|
2368
|
+
}
|
|
2369
|
+
const campaignId = this.pickString(data, ["campaign_id", "campaignId"]) || this.pickString(payload, ["campaign_id", "campaignId"]) || void 0;
|
|
2370
|
+
return {
|
|
2371
|
+
shouldInvalidate: true,
|
|
2372
|
+
siteName,
|
|
2373
|
+
slug,
|
|
2374
|
+
slugPrefix,
|
|
2375
|
+
campaignId
|
|
2376
|
+
};
|
|
2377
|
+
}
|
|
2378
|
+
isPublishEvent(eventType, status, originType) {
|
|
2379
|
+
const knownPublishEvents = /* @__PURE__ */ new Set([
|
|
2380
|
+
"newsletter.published",
|
|
2381
|
+
"newsletter.sent",
|
|
2382
|
+
"newsletter_campaign.published",
|
|
2383
|
+
"newsletter_campaign.sent",
|
|
2384
|
+
"newsletter.campaign.published",
|
|
2385
|
+
"newsletter.campaign.sent",
|
|
2386
|
+
"campaign.published",
|
|
2387
|
+
"campaign.sent"
|
|
2388
|
+
]);
|
|
2389
|
+
if (knownPublishEvents.has(eventType)) {
|
|
2390
|
+
return true;
|
|
2391
|
+
}
|
|
2392
|
+
if (eventType === "content.published" && originType === "newsletter_campaign") {
|
|
2393
|
+
return true;
|
|
2394
|
+
}
|
|
2395
|
+
if ((eventType.includes("newsletter") || eventType.includes("campaign")) && eventType.endsWith(".updated") && (status === "published" || status === "sent")) {
|
|
2396
|
+
return true;
|
|
2397
|
+
}
|
|
2398
|
+
return false;
|
|
2399
|
+
}
|
|
2400
|
+
pickString(source, keys) {
|
|
2401
|
+
if (!source) {
|
|
2402
|
+
return void 0;
|
|
2403
|
+
}
|
|
2404
|
+
for (const key of keys) {
|
|
2405
|
+
const value = source[key];
|
|
2406
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
2407
|
+
return value.trim();
|
|
2408
|
+
}
|
|
2409
|
+
}
|
|
2410
|
+
return void 0;
|
|
2411
|
+
}
|
|
2412
|
+
isCachePolicy(value) {
|
|
2413
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
2414
|
+
return false;
|
|
2415
|
+
}
|
|
2416
|
+
const record = value;
|
|
2417
|
+
return "ttlSeconds" in record || "tags" in record || "metadata" in record || "skipCache" in record;
|
|
2418
|
+
}
|
|
2206
2419
|
};
|
|
2207
2420
|
|
|
2208
2421
|
// src/client/site-users-client.ts
|
|
@@ -2466,6 +2679,19 @@ var SiteUsersClient = class extends BaseClient {
|
|
|
2466
2679
|
csrfToken
|
|
2467
2680
|
);
|
|
2468
2681
|
}
|
|
2682
|
+
/**
|
|
2683
|
+
* Create a Stripe Billing Portal session for updating payment methods
|
|
2684
|
+
* @param siteName - The site name
|
|
2685
|
+
* @param returnUrl - URL to redirect back to after portal session
|
|
2686
|
+
* @param csrfToken - CSRF token (required)
|
|
2687
|
+
*/
|
|
2688
|
+
async createBillingPortalSession(siteName, returnUrl, csrfToken) {
|
|
2689
|
+
return this.create(
|
|
2690
|
+
this.siteUserEndpoint(siteName, "/users/me/billing-portal"),
|
|
2691
|
+
{ return_url: returnUrl },
|
|
2692
|
+
csrfToken
|
|
2693
|
+
);
|
|
2694
|
+
}
|
|
2469
2695
|
/**
|
|
2470
2696
|
* Get linked newsletter subscriptions
|
|
2471
2697
|
* @param siteName - The site name
|
package/package.json
CHANGED
|
@@ -4,10 +4,13 @@
|
|
|
4
4
|
|
|
5
5
|
import { BaseClient } from './base-client';
|
|
6
6
|
import type { CacheManager } from '../cache/cache-manager';
|
|
7
|
+
import type { CachePolicy } from '../cache/types';
|
|
7
8
|
import type {
|
|
8
9
|
NewsletterSubscription,
|
|
9
10
|
CreateNewsletterSubscriptionRequest,
|
|
10
11
|
NewsletterList,
|
|
12
|
+
NewsletterCampaignDetail,
|
|
13
|
+
NewsletterCampaignListResponse,
|
|
11
14
|
NewsletterPreferences,
|
|
12
15
|
NewsletterStatusResponse,
|
|
13
16
|
NewsletterSubscribeResponse,
|
|
@@ -17,6 +20,7 @@ import type {
|
|
|
17
20
|
PaginatedResponse,
|
|
18
21
|
ApiResponse,
|
|
19
22
|
} from '../types';
|
|
23
|
+
import { validateOptionalLimit } from '../utils/validators';
|
|
20
24
|
|
|
21
25
|
export class NewsletterClient extends BaseClient {
|
|
22
26
|
constructor(http: any, cache?: CacheManager) {
|
|
@@ -119,15 +123,174 @@ export class NewsletterClient extends BaseClient {
|
|
|
119
123
|
/**
|
|
120
124
|
* Get available newsletter lists
|
|
121
125
|
*/
|
|
122
|
-
async getLists(
|
|
126
|
+
async getLists(
|
|
127
|
+
siteName: string,
|
|
128
|
+
cachePolicy?: CachePolicy
|
|
129
|
+
): Promise<ApiResponse<{
|
|
123
130
|
lists: NewsletterList[];
|
|
124
131
|
total: number;
|
|
125
132
|
}>> {
|
|
126
|
-
|
|
127
|
-
|
|
133
|
+
const endpoint = this.newsletterEndpoint(siteName, '/newsletter/lists');
|
|
134
|
+
const path = this.buildPath(endpoint);
|
|
135
|
+
|
|
136
|
+
return this.fetchWithCache<ApiResponse<{
|
|
137
|
+
lists: NewsletterList[];
|
|
138
|
+
total: number;
|
|
139
|
+
}>>(
|
|
140
|
+
endpoint,
|
|
141
|
+
undefined,
|
|
142
|
+
this.buildNewsletterTags(siteName, [
|
|
143
|
+
'newsletter:lists',
|
|
144
|
+
`newsletter:lists:site:${siteName}`,
|
|
145
|
+
]),
|
|
146
|
+
cachePolicy,
|
|
147
|
+
() => this.http.get(path)
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* List publicly available (sent) newsletter campaigns
|
|
153
|
+
*/
|
|
154
|
+
async getPublishedCampaigns(
|
|
155
|
+
siteName: string,
|
|
156
|
+
params?: {
|
|
157
|
+
page?: number;
|
|
158
|
+
limit?: number;
|
|
159
|
+
search?: string;
|
|
160
|
+
},
|
|
161
|
+
cachePolicy?: CachePolicy
|
|
162
|
+
): Promise<ApiResponse<NewsletterCampaignListResponse>> {
|
|
163
|
+
const endpoint = this.newsletterEndpoint(siteName, '/newsletter/campaigns');
|
|
164
|
+
const path = this.buildPath(endpoint);
|
|
165
|
+
const normalizedParams = params ? { ...params } : undefined;
|
|
166
|
+
|
|
167
|
+
if (normalizedParams) {
|
|
168
|
+
const validatedLimit = validateOptionalLimit(
|
|
169
|
+
normalizedParams.limit,
|
|
170
|
+
'newsletter campaigns query',
|
|
171
|
+
);
|
|
172
|
+
if (validatedLimit !== undefined) {
|
|
173
|
+
normalizedParams.limit = validatedLimit;
|
|
174
|
+
} else {
|
|
175
|
+
delete normalizedParams.limit;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (
|
|
179
|
+
typeof normalizedParams.search === 'string' &&
|
|
180
|
+
normalizedParams.search.trim().length === 0
|
|
181
|
+
) {
|
|
182
|
+
delete normalizedParams.search;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return this.fetchWithCache<ApiResponse<NewsletterCampaignListResponse>>(
|
|
187
|
+
endpoint,
|
|
188
|
+
normalizedParams,
|
|
189
|
+
this.buildNewsletterTags(siteName, [
|
|
190
|
+
'newsletter:campaigns',
|
|
191
|
+
`newsletter:campaigns:list:${siteName}`,
|
|
192
|
+
]),
|
|
193
|
+
cachePolicy,
|
|
194
|
+
() => this.http.get<NewsletterCampaignListResponse>(path, normalizedParams)
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Fetch a publicly available (sent) newsletter campaign by slug
|
|
200
|
+
*/
|
|
201
|
+
async getPublishedCampaignBySlug(
|
|
202
|
+
siteName: string,
|
|
203
|
+
slug: string,
|
|
204
|
+
optionsOrPolicy?: { slugPrefix?: string } | CachePolicy,
|
|
205
|
+
cachePolicy?: CachePolicy
|
|
206
|
+
): Promise<ApiResponse<NewsletterCampaignDetail>> {
|
|
207
|
+
let normalizedSlug = slug.trim();
|
|
208
|
+
if (!normalizedSlug) {
|
|
209
|
+
throw new Error('slug is required');
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const isCachePolicyArg = this.isCachePolicy(optionsOrPolicy);
|
|
213
|
+
const providedSlugPrefix =
|
|
214
|
+
!isCachePolicyArg && optionsOrPolicy
|
|
215
|
+
? this.pickString(optionsOrPolicy as Record<string, unknown>, ['slugPrefix', 'slug_prefix'])
|
|
216
|
+
: undefined;
|
|
217
|
+
const resolvedCachePolicy = isCachePolicyArg
|
|
218
|
+
? (optionsOrPolicy as CachePolicy)
|
|
219
|
+
: cachePolicy;
|
|
220
|
+
|
|
221
|
+
let slugPrefix = providedSlugPrefix;
|
|
222
|
+
if (!slugPrefix) {
|
|
223
|
+
const parts = normalizedSlug.split('/').filter(Boolean);
|
|
224
|
+
if (parts.length > 1) {
|
|
225
|
+
slugPrefix = parts.slice(0, -1).join('/');
|
|
226
|
+
normalizedSlug = parts[parts.length - 1];
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
normalizedSlug = normalizedSlug.trim();
|
|
230
|
+
if (!normalizedSlug) {
|
|
231
|
+
throw new Error('slug is required');
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const endpoint = this.newsletterEndpoint(
|
|
235
|
+
siteName,
|
|
236
|
+
`/newsletter/campaigns/${encodeURIComponent(normalizedSlug)}`
|
|
237
|
+
);
|
|
238
|
+
const path = this.buildPath(endpoint);
|
|
239
|
+
const queryParams = slugPrefix ? { slug_prefix: slugPrefix } : undefined;
|
|
240
|
+
|
|
241
|
+
return this.fetchWithCache<ApiResponse<NewsletterCampaignDetail>>(
|
|
242
|
+
endpoint,
|
|
243
|
+
queryParams,
|
|
244
|
+
this.buildNewsletterTags(
|
|
245
|
+
siteName,
|
|
246
|
+
[
|
|
247
|
+
'newsletter:campaigns',
|
|
248
|
+
`newsletter:campaigns:slug:${siteName}:${normalizedSlug}`,
|
|
249
|
+
`newsletter:campaigns:detail:${siteName}`,
|
|
250
|
+
],
|
|
251
|
+
slugPrefix
|
|
252
|
+
),
|
|
253
|
+
resolvedCachePolicy,
|
|
254
|
+
() => this.http.get<NewsletterCampaignDetail>(path, queryParams)
|
|
128
255
|
);
|
|
129
256
|
}
|
|
130
257
|
|
|
258
|
+
/**
|
|
259
|
+
* Invalidate cached published newsletter campaign data from a webhook payload.
|
|
260
|
+
* Only publish events (and legacy sent aliases) trigger invalidation.
|
|
261
|
+
*/
|
|
262
|
+
async invalidatePublishedCampaignCacheFromWebhook(
|
|
263
|
+
payload: Record<string, unknown>,
|
|
264
|
+
fallbackSiteName?: string,
|
|
265
|
+
): Promise<{ invalidated: boolean; tags: string[]; reason?: string }> {
|
|
266
|
+
const normalized = this.normalizeNewsletterWebhookPayload(payload, fallbackSiteName);
|
|
267
|
+
if (!normalized.shouldInvalidate) {
|
|
268
|
+
return {
|
|
269
|
+
invalidated: false,
|
|
270
|
+
tags: [],
|
|
271
|
+
reason: normalized.reason || 'event_not_publish_related',
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const tags = this.buildNewsletterTags(
|
|
276
|
+
normalized.siteName,
|
|
277
|
+
[
|
|
278
|
+
'newsletter:campaigns',
|
|
279
|
+
`newsletter:campaigns:list:${normalized.siteName}`,
|
|
280
|
+
normalized.slug
|
|
281
|
+
? `newsletter:campaigns:slug:${normalized.siteName}:${normalized.slug}`
|
|
282
|
+
: '',
|
|
283
|
+
normalized.campaignId
|
|
284
|
+
? `newsletter:campaigns:id:${normalized.siteName}:${normalized.campaignId}`
|
|
285
|
+
: '',
|
|
286
|
+
],
|
|
287
|
+
normalized.slugPrefix,
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
await this.invalidateCache({ tags });
|
|
291
|
+
return { invalidated: true, tags };
|
|
292
|
+
}
|
|
293
|
+
|
|
131
294
|
/**
|
|
132
295
|
* Check subscription status by email
|
|
133
296
|
*/
|
|
@@ -432,4 +595,181 @@ export class NewsletterClient extends BaseClient {
|
|
|
432
595
|
0x44, 0x01, 0x00, 0x3b,
|
|
433
596
|
]);
|
|
434
597
|
}
|
|
598
|
+
|
|
599
|
+
private buildNewsletterTags(
|
|
600
|
+
siteName?: string,
|
|
601
|
+
extraTags: string[] = [],
|
|
602
|
+
slugPrefix?: string
|
|
603
|
+
): string[] {
|
|
604
|
+
const tags = new Set<string>(['newsletter']);
|
|
605
|
+
if (siteName) {
|
|
606
|
+
tags.add(`newsletter:site:${siteName}`);
|
|
607
|
+
}
|
|
608
|
+
if (slugPrefix) {
|
|
609
|
+
tags.add(`newsletter:prefix:${slugPrefix}`);
|
|
610
|
+
}
|
|
611
|
+
extraTags.filter(Boolean).forEach(tag => tags.add(tag));
|
|
612
|
+
return Array.from(tags.values());
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
private extractSlugPrefix(slug: string): string | undefined {
|
|
616
|
+
const normalized = slug.trim();
|
|
617
|
+
if (!normalized.includes('/')) {
|
|
618
|
+
return undefined;
|
|
619
|
+
}
|
|
620
|
+
const [prefix] = normalized.split('/').filter(Boolean);
|
|
621
|
+
return prefix || undefined;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
private normalizeNewsletterWebhookPayload(
|
|
625
|
+
payload: Record<string, unknown>,
|
|
626
|
+
fallbackSiteName?: string
|
|
627
|
+
): {
|
|
628
|
+
shouldInvalidate: boolean;
|
|
629
|
+
reason?: string;
|
|
630
|
+
siteName: string;
|
|
631
|
+
slug?: string;
|
|
632
|
+
slugPrefix?: string;
|
|
633
|
+
campaignId?: string;
|
|
634
|
+
} {
|
|
635
|
+
const eventType = (
|
|
636
|
+
this.pickString(payload, ['event_type', 'type']) || ''
|
|
637
|
+
).toLowerCase();
|
|
638
|
+
|
|
639
|
+
const dataRaw = payload.data;
|
|
640
|
+
const data = (dataRaw && typeof dataRaw === 'object' && !Array.isArray(dataRaw))
|
|
641
|
+
? (dataRaw as Record<string, unknown>)
|
|
642
|
+
: payload;
|
|
643
|
+
|
|
644
|
+
const originType = (
|
|
645
|
+
this.pickString(data, ['origin_type', 'originType']) || ''
|
|
646
|
+
).toLowerCase();
|
|
647
|
+
const status = (
|
|
648
|
+
this.pickString(data, ['status', 'campaign_status', 'campaignStatus']) || ''
|
|
649
|
+
).toLowerCase();
|
|
650
|
+
|
|
651
|
+
if (!this.isPublishEvent(eventType, status, originType)) {
|
|
652
|
+
return {
|
|
653
|
+
shouldInvalidate: false,
|
|
654
|
+
reason: 'not_publish_or_sent_event',
|
|
655
|
+
siteName: fallbackSiteName || '',
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
const siteName =
|
|
660
|
+
this.pickString(payload, ['site', 'site_name', 'siteName']) ||
|
|
661
|
+
this.pickString(data, ['site', 'site_name', 'siteName']) ||
|
|
662
|
+
fallbackSiteName ||
|
|
663
|
+
'';
|
|
664
|
+
|
|
665
|
+
if (!siteName) {
|
|
666
|
+
return {
|
|
667
|
+
shouldInvalidate: false,
|
|
668
|
+
reason: 'missing_site_name',
|
|
669
|
+
siteName: '',
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
let slug =
|
|
674
|
+
this.pickString(data, ['slug', 'canonical_slug', 'canonicalSlug']) ||
|
|
675
|
+
this.pickString(payload, ['slug', 'canonical_slug', 'canonicalSlug']) ||
|
|
676
|
+
undefined;
|
|
677
|
+
|
|
678
|
+
let slugPrefix =
|
|
679
|
+
this.pickString(data, ['slug_prefix', 'slugPrefix']) ||
|
|
680
|
+
this.pickString(payload, ['slug_prefix', 'slugPrefix']) ||
|
|
681
|
+
(slug ? this.extractSlugPrefix(slug) : undefined);
|
|
682
|
+
|
|
683
|
+
if (slug) {
|
|
684
|
+
const slugParts = slug.split('/').filter(Boolean);
|
|
685
|
+
if (slugParts.length > 1) {
|
|
686
|
+
if (!slugPrefix) {
|
|
687
|
+
slugPrefix = slugParts.slice(0, -1).join('/');
|
|
688
|
+
}
|
|
689
|
+
slug = slugParts[slugParts.length - 1];
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
const campaignId =
|
|
694
|
+
this.pickString(data, ['campaign_id', 'campaignId']) ||
|
|
695
|
+
this.pickString(payload, ['campaign_id', 'campaignId']) ||
|
|
696
|
+
undefined;
|
|
697
|
+
|
|
698
|
+
return {
|
|
699
|
+
shouldInvalidate: true,
|
|
700
|
+
siteName,
|
|
701
|
+
slug,
|
|
702
|
+
slugPrefix,
|
|
703
|
+
campaignId,
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
private isPublishEvent(
|
|
708
|
+
eventType: string,
|
|
709
|
+
status: string,
|
|
710
|
+
originType: string
|
|
711
|
+
): boolean {
|
|
712
|
+
const knownPublishEvents = new Set([
|
|
713
|
+
'newsletter.published',
|
|
714
|
+
'newsletter.sent',
|
|
715
|
+
'newsletter_campaign.published',
|
|
716
|
+
'newsletter_campaign.sent',
|
|
717
|
+
'newsletter.campaign.published',
|
|
718
|
+
'newsletter.campaign.sent',
|
|
719
|
+
'campaign.published',
|
|
720
|
+
'campaign.sent',
|
|
721
|
+
]);
|
|
722
|
+
|
|
723
|
+
if (knownPublishEvents.has(eventType)) {
|
|
724
|
+
return true;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// Content webhook event for unified newsletter campaign content.
|
|
728
|
+
if (eventType === 'content.published' && originType === 'newsletter_campaign') {
|
|
729
|
+
return true;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// Some systems emit update events with final status.
|
|
733
|
+
if (
|
|
734
|
+
(eventType.includes('newsletter') || eventType.includes('campaign')) &&
|
|
735
|
+
eventType.endsWith('.updated') &&
|
|
736
|
+
(status === 'published' || status === 'sent')
|
|
737
|
+
) {
|
|
738
|
+
return true;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
return false;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
private pickString(
|
|
745
|
+
source: Record<string, unknown> | undefined,
|
|
746
|
+
keys: string[]
|
|
747
|
+
): string | undefined {
|
|
748
|
+
if (!source) {
|
|
749
|
+
return undefined;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
for (const key of keys) {
|
|
753
|
+
const value = source[key];
|
|
754
|
+
if (typeof value === 'string' && value.trim().length > 0) {
|
|
755
|
+
return value.trim();
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
return undefined;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
private isCachePolicy(value: unknown): value is CachePolicy {
|
|
763
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
764
|
+
return false;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
const record = value as Record<string, unknown>;
|
|
768
|
+
return (
|
|
769
|
+
'ttlSeconds' in record ||
|
|
770
|
+
'tags' in record ||
|
|
771
|
+
'metadata' in record ||
|
|
772
|
+
'skipCache' in record
|
|
773
|
+
);
|
|
774
|
+
}
|
|
435
775
|
}
|
|
@@ -355,6 +355,24 @@ export class SiteUsersClient extends BaseClient {
|
|
|
355
355
|
);
|
|
356
356
|
}
|
|
357
357
|
|
|
358
|
+
/**
|
|
359
|
+
* Create a Stripe Billing Portal session for updating payment methods
|
|
360
|
+
* @param siteName - The site name
|
|
361
|
+
* @param returnUrl - URL to redirect back to after portal session
|
|
362
|
+
* @param csrfToken - CSRF token (required)
|
|
363
|
+
*/
|
|
364
|
+
async createBillingPortalSession(
|
|
365
|
+
siteName: string,
|
|
366
|
+
returnUrl: string,
|
|
367
|
+
csrfToken?: string
|
|
368
|
+
): Promise<ApiResponse<{ url: string }>> {
|
|
369
|
+
return this.create<{ return_url: string }, { url: string }>(
|
|
370
|
+
this.siteUserEndpoint(siteName, '/users/me/billing-portal'),
|
|
371
|
+
{ return_url: returnUrl },
|
|
372
|
+
csrfToken
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
|
|
358
376
|
/**
|
|
359
377
|
* Get linked newsletter subscriptions
|
|
360
378
|
* @param siteName - The site name
|
package/src/index.ts
CHANGED
|
@@ -100,6 +100,9 @@ export type {
|
|
|
100
100
|
NewsletterSubscription,
|
|
101
101
|
CreateNewsletterSubscriptionRequest,
|
|
102
102
|
NewsletterList,
|
|
103
|
+
NewsletterCampaignSummary,
|
|
104
|
+
NewsletterCampaignDetail,
|
|
105
|
+
NewsletterCampaignListResponse,
|
|
103
106
|
NewsletterPreferences,
|
|
104
107
|
NewsletterStatusResponse,
|
|
105
108
|
NewsletterSubscribeResponse,
|