@stephensauceda/apple-news 1.0.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 ADDED
@@ -0,0 +1,223 @@
1
+ # @stephensauceda/apple-news
2
+
3
+ [![CI](https://github.com/stephensauceda/apple-news/actions/workflows/ci.yml/badge.svg)](https://github.com/stephensauceda/apple-news/actions/workflows/ci.yml)
4
+ [![codecov](https://codecov.io/github/stephensauceda/apple-news/graph/badge.svg?token=CWIFXDRP77)](https://codecov.io/github/stephensauceda/apple-news)
5
+
6
+ Modern Node.js client for the Apple News API.
7
+
8
+ - ESM-first package
9
+ - Native fetch transport
10
+ - Promise-based API
11
+ - Apple HHMAC request signing
12
+ - Co-located Vitest test suite with high coverage thresholds
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ npm install @stephensauceda/apple-news
18
+ ```
19
+
20
+ ## Requirements
21
+
22
+ - Node.js 22+
23
+ - Apple News API key ID and secret from Apple News Publisher
24
+
25
+ ## Quick Start
26
+
27
+ ```js
28
+ import { AppleNewsClient } from '@stephensauceda/apple-news'
29
+
30
+ const client = new AppleNewsClient({
31
+ apiId: process.env.APPLE_NEWS_API_ID,
32
+ apiSecret: process.env.APPLE_NEWS_API_SECRET
33
+ })
34
+
35
+ const channel = await client.readChannel({ channelId: 'your-channel-id' })
36
+ console.log(channel)
37
+ ```
38
+
39
+ ## API
40
+
41
+ The client supports the following methods:
42
+
43
+ - readChannel
44
+ - readChannelQuota
45
+ - listSections
46
+ - readSection
47
+ - createArticle
48
+ - readArticle
49
+ - updateArticle
50
+ - deleteArticle
51
+ - searchArticles
52
+ - promoteArticles
53
+
54
+ ### Constructor
55
+
56
+ ```js
57
+ const client = new AppleNewsClient({
58
+ apiId: 'your-api-key-id',
59
+ apiSecret: 'your-base64-secret',
60
+ host: 'news-api.apple.com' // optional
61
+ })
62
+ ```
63
+
64
+ ### readChannel
65
+
66
+ ```js
67
+ await client.readChannel({ channelId: 'channel-id' })
68
+ ```
69
+
70
+ ### listSections
71
+
72
+ ```js
73
+ await client.listSections({ channelId: 'channel-id' })
74
+ ```
75
+
76
+ ### readSection
77
+
78
+ ```js
79
+ await client.readSection({ sectionId: 'section-id' })
80
+ ```
81
+
82
+ ### readArticle
83
+
84
+ ```js
85
+ await client.readArticle({ articleId: 'article-id' })
86
+ ```
87
+
88
+ ### deleteArticle
89
+
90
+ ```js
91
+ await client.deleteArticle({ articleId: 'article-id' })
92
+ ```
93
+
94
+ ### searchArticles
95
+
96
+ Provide exactly one of channelId or sectionId.
97
+
98
+ ```js
99
+ await client.searchArticles({ channelId: 'channel-id', limit: 25 })
100
+ await client.searchArticles({ sectionId: 'section-id', offset: 50 })
101
+ ```
102
+
103
+ ### createArticle
104
+
105
+ ```js
106
+ await client.createArticle({
107
+ channelId: 'channel-id',
108
+ article: {
109
+ identifier: 'article-id',
110
+ title: 'Title'
111
+ },
112
+ sections: ['section-id'],
113
+ isPreview: true,
114
+ isSponsored: false,
115
+ maturityRating: 'GENERAL',
116
+ bundleFiles: {
117
+ 'image.jpg': {
118
+ data: Buffer.from([
119
+ /* bytes */
120
+ ]),
121
+ mimeType: 'image/jpeg'
122
+ }
123
+ }
124
+ })
125
+ ```
126
+
127
+ ### readChannelQuota
128
+
129
+ ```js
130
+ await client.readChannelQuota({ channelId: 'channel-id' })
131
+ ```
132
+
133
+ Returns the channel's remaining create/update quota, queue size, and estimated wait time.
134
+
135
+ ### promoteArticles
136
+
137
+ Sets the promoted articles list for a section (up to 6). Pass an empty array to clear promotions.
138
+
139
+ ```js
140
+ await client.promoteArticles({
141
+ sectionId: 'section-id',
142
+ articleIds: ['article-id-1', 'article-id-2']
143
+ })
144
+
145
+ // Clear all promoted articles
146
+ await client.promoteArticles({ sectionId: 'section-id', articleIds: [] })
147
+ ```
148
+
149
+ ### updateArticle
150
+
151
+ revision is required.
152
+
153
+ ```js
154
+ await client.updateArticle({
155
+ articleId: 'article-id',
156
+ revision: 'current-revision',
157
+ article: {
158
+ identifier: 'article-id',
159
+ title: 'Updated title'
160
+ },
161
+ bundleFiles: {
162
+ 'image.jpg': {
163
+ data: Buffer.from([
164
+ /* bytes */
165
+ ]),
166
+ mimeType: 'image/jpeg'
167
+ }
168
+ }
169
+ })
170
+ ```
171
+
172
+ ## Optional Helper: Fetch Bundle Files From URLs
173
+
174
+ Core upload behavior expects buffer/file payloads for deterministic multipart signing.
175
+ If you want convenience URL downloading, use the helper:
176
+
177
+ ```js
178
+ import { fetchBundleFiles } from '@stephensauceda/apple-news'
179
+
180
+ const bundleFiles = await fetchBundleFiles({
181
+ 'image.jpg': 'https://example.com/image.jpg',
182
+ 'font.otf': 'https://example.com/font.otf'
183
+ })
184
+ ```
185
+
186
+ ## Error Handling
187
+
188
+ Non-2xx responses throw AppleNewsApiError with:
189
+
190
+ - status
191
+ - method
192
+ - url
193
+ - apiErrors (when present in Apple response)
194
+ - responseBody
195
+
196
+ ```js
197
+ import { AppleNewsApiError } from '@stephensauceda/apple-news'
198
+
199
+ try {
200
+ await client.readChannel({ channelId: 'channel-id' })
201
+ } catch (error) {
202
+ if (error instanceof AppleNewsApiError) {
203
+ console.error(error.status, error.apiErrors)
204
+ }
205
+ throw error
206
+ }
207
+ ```
208
+
209
+ ## Development
210
+
211
+ ```bash
212
+ npm install
213
+ npm run lint
214
+ npm run test
215
+ npm run coverage
216
+ npm run build
217
+ ```
218
+
219
+ ## Community
220
+
221
+ - Contributing guide: see CONTRIBUTING.md
222
+ - Code of Conduct: see CODE_OF_CONDUCT.md
223
+ - Security policy: see SECURITY.md
@@ -0,0 +1,24 @@
1
+ // src/helpers/fetchBundleFiles.js
2
+ async function fetchBundleFiles(urlMap) {
3
+ const result = {};
4
+ for (const [filename, url] of Object.entries(urlMap)) {
5
+ const response = await fetch(url);
6
+ if (!response.ok) {
7
+ throw new Error(
8
+ `Failed to fetch bundle file ${filename} from ${url}: ${response.status}`
9
+ );
10
+ }
11
+ const arrayBuffer = await response.arrayBuffer();
12
+ const data = Buffer.from(arrayBuffer);
13
+ const mimeType = response.headers.get("content-type") ?? "application/octet-stream";
14
+ result[filename] = {
15
+ data,
16
+ mimeType
17
+ };
18
+ }
19
+ return result;
20
+ }
21
+ export {
22
+ fetchBundleFiles
23
+ };
24
+ //# sourceMappingURL=helpers.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/helpers/fetchBundleFiles.js"],
4
+ "sourcesContent": ["/**\n * @typedef BundleFetchResult\n * @property {Buffer} data\n * @property {string} mimeType\n */\n\n/**\n * @param {Record<string, string>} urlMap\n * @returns {Promise<Record<string, BundleFetchResult>>}\n */\nexport async function fetchBundleFiles(urlMap) {\n const result = {}\n\n for (const [filename, url] of Object.entries(urlMap)) {\n const response = await fetch(url)\n\n if (!response.ok) {\n throw new Error(\n `Failed to fetch bundle file ${filename} from ${url}: ${response.status}`\n )\n }\n\n const arrayBuffer = await response.arrayBuffer()\n const data = Buffer.from(arrayBuffer)\n const mimeType =\n response.headers.get('content-type') ?? 'application/octet-stream'\n\n result[filename] = {\n data,\n mimeType\n }\n }\n\n return result\n}\n"],
5
+ "mappings": ";AAUA,eAAsB,iBAAiB,QAAQ;AAC7C,QAAM,SAAS,CAAC;AAEhB,aAAW,CAAC,UAAU,GAAG,KAAK,OAAO,QAAQ,MAAM,GAAG;AACpD,UAAM,WAAW,MAAM,MAAM,GAAG;AAEhC,QAAI,CAAC,SAAS,IAAI;AAChB,YAAM,IAAI;AAAA,QACR,+BAA+B,QAAQ,SAAS,GAAG,KAAK,SAAS,MAAM;AAAA,MACzE;AAAA,IACF;AAEA,UAAM,cAAc,MAAM,SAAS,YAAY;AAC/C,UAAM,OAAO,OAAO,KAAK,WAAW;AACpC,UAAM,WACJ,SAAS,QAAQ,IAAI,cAAc,KAAK;AAE1C,WAAO,QAAQ,IAAI;AAAA,MACjB;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AACT;",
6
+ "names": []
7
+ }
package/dist/index.js ADDED
@@ -0,0 +1,544 @@
1
+ // src/auth.js
2
+ import { createHmac } from "node:crypto";
3
+ function formatAppleDate(date) {
4
+ const iso = date.toISOString();
5
+ return iso.replace(/\.\d{3}Z$/, "Z");
6
+ }
7
+ function toBuffer(value) {
8
+ if (value == null) {
9
+ return Buffer.alloc(0);
10
+ }
11
+ if (Buffer.isBuffer(value)) {
12
+ return value;
13
+ }
14
+ if (value instanceof Uint8Array) {
15
+ return Buffer.from(value);
16
+ }
17
+ return Buffer.from(String(value), "utf8");
18
+ }
19
+ function buildCanonicalRequest(input) {
20
+ const method = input.method.toUpperCase();
21
+ const contentType = input.contentType ?? "";
22
+ const prefix = `${method}${input.url}${input.date}${contentType}`;
23
+ const prefixBuffer = Buffer.from(prefix, "utf8");
24
+ const bodyBuffer = toBuffer(input.body);
25
+ if (bodyBuffer.length === 0) {
26
+ return prefixBuffer;
27
+ }
28
+ return Buffer.concat([prefixBuffer, bodyBuffer]);
29
+ }
30
+ function signCanonicalRequest(apiSecret, canonicalPayload) {
31
+ const key = Buffer.from(apiSecret, "base64");
32
+ return createHmac("sha256", key).update(canonicalPayload).digest("base64");
33
+ }
34
+ function buildAuthorizationHeader(input) {
35
+ return `HHMAC; key="${input.apiId}"; signature="${input.signature}"; date="${input.date}"`;
36
+ }
37
+ function createSignedHeaders(input) {
38
+ const date = typeof input.date === "string" ? input.date : formatAppleDate(input.date ?? /* @__PURE__ */ new Date());
39
+ const contentType = input.contentType ?? "";
40
+ const canonicalPayload = buildCanonicalRequest({
41
+ method: input.method,
42
+ url: input.url,
43
+ date,
44
+ contentType,
45
+ body: input.body
46
+ });
47
+ const signature = signCanonicalRequest(input.apiSecret, canonicalPayload);
48
+ const authorization = buildAuthorizationHeader({
49
+ apiId: input.apiId,
50
+ signature,
51
+ date
52
+ });
53
+ const headers = {
54
+ Accept: "application/json",
55
+ Authorization: authorization
56
+ };
57
+ if (contentType) {
58
+ headers["Content-Type"] = contentType;
59
+ }
60
+ return {
61
+ headers,
62
+ date,
63
+ signature,
64
+ canonicalPayload
65
+ };
66
+ }
67
+
68
+ // src/request.js
69
+ var AppleNewsApiError = class extends Error {
70
+ /**
71
+ * @param {string} message
72
+ * @param {Object} details
73
+ * @param {number} details.status
74
+ * @param {string} details.method
75
+ * @param {string} details.url
76
+ * @param {unknown[]} [details.apiErrors]
77
+ * @param {unknown} [details.responseBody]
78
+ */
79
+ constructor(message, details) {
80
+ super(message);
81
+ this.name = "AppleNewsApiError";
82
+ this.status = details.status;
83
+ this.method = details.method;
84
+ this.url = details.url;
85
+ this.apiErrors = details.apiErrors;
86
+ this.responseBody = details.responseBody;
87
+ }
88
+ };
89
+ function buildRequestUrl(host, endpoint, query) {
90
+ const url = new URL(`https://${host}${endpoint}`);
91
+ if (query) {
92
+ for (const [key, value] of Object.entries(query)) {
93
+ if (value === void 0) {
94
+ continue;
95
+ }
96
+ url.searchParams.set(key, String(value));
97
+ }
98
+ }
99
+ return url.toString();
100
+ }
101
+ function isObject(value) {
102
+ return value !== null && typeof value === "object";
103
+ }
104
+ function getApiErrors(parsed) {
105
+ if (!isObject(parsed)) {
106
+ return void 0;
107
+ }
108
+ const maybeErrors = parsed.errors;
109
+ if (!Array.isArray(maybeErrors)) {
110
+ return void 0;
111
+ }
112
+ return maybeErrors;
113
+ }
114
+ function unwrapData(parsed) {
115
+ if (!isObject(parsed)) {
116
+ return parsed;
117
+ }
118
+ if ("data" in parsed) {
119
+ return parsed.data;
120
+ }
121
+ return parsed;
122
+ }
123
+ async function requestSigned(options) {
124
+ const host = options.host ?? "news-api.apple.com";
125
+ const url = buildRequestUrl(host, options.endpoint, options.query);
126
+ const signed = createSignedHeaders({
127
+ apiId: options.apiId,
128
+ apiSecret: options.apiSecret,
129
+ method: options.method,
130
+ url,
131
+ date: options.date,
132
+ contentType: options.contentType,
133
+ body: options.body
134
+ });
135
+ const response = await fetch(url, {
136
+ method: options.method,
137
+ headers: signed.headers,
138
+ body: options.body ?? void 0
139
+ });
140
+ const rawBody = await response.text();
141
+ let parsedBody = null;
142
+ if (rawBody.length > 0) {
143
+ try {
144
+ parsedBody = JSON.parse(rawBody);
145
+ } catch {
146
+ parsedBody = rawBody;
147
+ }
148
+ }
149
+ if (!response.ok) {
150
+ const apiErrors = getApiErrors(parsedBody);
151
+ throw new AppleNewsApiError(
152
+ `${options.method.toUpperCase()} ${options.endpoint} failed with status ${response.status}`,
153
+ {
154
+ status: response.status,
155
+ method: options.method.toUpperCase(),
156
+ url,
157
+ apiErrors,
158
+ responseBody: parsedBody
159
+ }
160
+ );
161
+ }
162
+ if (parsedBody === null) {
163
+ return null;
164
+ }
165
+ return unwrapData(parsedBody);
166
+ }
167
+
168
+ // src/multipart.js
169
+ var CRLF = "\r\n";
170
+ function toBuffer2(data) {
171
+ if (Buffer.isBuffer(data)) {
172
+ return data;
173
+ }
174
+ if (data instanceof Uint8Array) {
175
+ return Buffer.from(data);
176
+ }
177
+ return Buffer.from(String(data), "utf8");
178
+ }
179
+ function buildPart(boundary, name, value, contentType, filename) {
180
+ const lines = [`--${boundary}`];
181
+ if (filename) {
182
+ lines.push(
183
+ `Content-Disposition: form-data; name="${name}"; filename="${filename}"`
184
+ );
185
+ } else {
186
+ lines.push(`Content-Disposition: form-data; name="${name}"`);
187
+ }
188
+ lines.push(`Content-Type: ${contentType}`);
189
+ lines.push("");
190
+ return Buffer.concat([
191
+ Buffer.from(lines.join(CRLF) + CRLF, "utf8"),
192
+ value,
193
+ Buffer.from(CRLF, "utf8")
194
+ ]);
195
+ }
196
+ function validateBundleFiles(bundleFiles) {
197
+ if (!bundleFiles) {
198
+ return;
199
+ }
200
+ for (const name of Object.keys(bundleFiles)) {
201
+ if (name === "article.json" || name === "metadata") {
202
+ throw new TypeError(
203
+ "bundleFiles cannot contain reserved names: article.json or metadata"
204
+ );
205
+ }
206
+ }
207
+ }
208
+ function buildArticleMultipartBody(input) {
209
+ if (!input.article || typeof input.article !== "object") {
210
+ throw new TypeError("article is required and must be an object");
211
+ }
212
+ validateBundleFiles(input.bundleFiles);
213
+ const boundary = input.boundary ?? `apple-news-${Date.now().toString(16)}`;
214
+ const metadata = input.metadata ?? {};
215
+ const parts = [];
216
+ parts.push(
217
+ buildPart(
218
+ boundary,
219
+ "article.json",
220
+ Buffer.from(JSON.stringify(input.article), "utf8"),
221
+ "application/json"
222
+ )
223
+ );
224
+ parts.push(
225
+ buildPart(
226
+ boundary,
227
+ "metadata",
228
+ Buffer.from(JSON.stringify({ data: metadata }), "utf8"),
229
+ "application/json"
230
+ )
231
+ );
232
+ if (input.bundleFiles) {
233
+ let index = 0;
234
+ for (const [filename, file] of Object.entries(input.bundleFiles)) {
235
+ const fieldName = `file${index}`;
236
+ index += 1;
237
+ parts.push(
238
+ buildPart(
239
+ boundary,
240
+ fieldName,
241
+ toBuffer2(file.data),
242
+ file.mimeType ?? "application/octet-stream",
243
+ filename
244
+ )
245
+ );
246
+ }
247
+ }
248
+ parts.push(Buffer.from(`--${boundary}--${CRLF}`, "utf8"));
249
+ return {
250
+ contentType: `multipart/form-data; boundary=${boundary}`,
251
+ body: Buffer.concat(parts)
252
+ };
253
+ }
254
+
255
+ // src/client.js
256
+ function assertRequired(value, name) {
257
+ if (!value || typeof value !== "string") {
258
+ throw new TypeError(`${name} is required and must be a non-empty string`);
259
+ }
260
+ }
261
+ function assertId(value, name) {
262
+ if (!value || typeof value !== "string") {
263
+ throw new TypeError(`${name} is required and must be a non-empty string`);
264
+ }
265
+ }
266
+ function buildMetadata(options, host, includeRevision = false) {
267
+ const metadata = {
268
+ isPreview: options.isPreview ?? true,
269
+ isSponsored: options.isSponsored ?? false,
270
+ isPaid: options.isPaid ?? false
271
+ };
272
+ if (options.sections !== void 0) {
273
+ metadata.links = {
274
+ sections: options.sections.map(
275
+ (sectionId) => `https://${host}/sections/${encodeURIComponent(sectionId)}`
276
+ )
277
+ };
278
+ }
279
+ if (options.maturityRating !== void 0) {
280
+ metadata.maturityRating = options.maturityRating;
281
+ }
282
+ if (includeRevision) {
283
+ metadata.revision = options.revision;
284
+ }
285
+ return metadata;
286
+ }
287
+ function getSharedOptions(options) {
288
+ return {
289
+ date: options?.date
290
+ };
291
+ }
292
+ var AppleNewsClient = class {
293
+ /**
294
+ * @param {AppleNewsClientConfig} config
295
+ */
296
+ constructor(config) {
297
+ assertRequired(config?.apiId, "apiId");
298
+ assertRequired(config?.apiSecret, "apiSecret");
299
+ this.apiId = config.apiId;
300
+ this.apiSecret = config.apiSecret;
301
+ this.host = config.host ?? "news-api.apple.com";
302
+ }
303
+ /**
304
+ * @param {Object} options
305
+ * @param {string} options.channelId
306
+ * @param {string|Date} [options.date]
307
+ */
308
+ async readChannel(options) {
309
+ assertId(options?.channelId, "channelId");
310
+ return this.#request(
311
+ "GET",
312
+ `/channels/${options.channelId}`,
313
+ getSharedOptions(options)
314
+ );
315
+ }
316
+ /**
317
+ * @param {Object} options
318
+ * @param {string} options.channelId
319
+ * @param {string|Date} [options.date]
320
+ */
321
+ async listSections(options) {
322
+ assertId(options?.channelId, "channelId");
323
+ return this.#request(
324
+ "GET",
325
+ `/channels/${options.channelId}/sections`,
326
+ getSharedOptions(options)
327
+ );
328
+ }
329
+ /**
330
+ * @param {Object} options
331
+ * @param {string} options.sectionId
332
+ * @param {string|Date} [options.date]
333
+ */
334
+ async readSection(options) {
335
+ assertId(options?.sectionId, "sectionId");
336
+ return this.#request(
337
+ "GET",
338
+ `/sections/${options.sectionId}`,
339
+ getSharedOptions(options)
340
+ );
341
+ }
342
+ /**
343
+ * @param {Object} options
344
+ * @param {string} options.articleId
345
+ * @param {string|Date} [options.date]
346
+ */
347
+ async readArticle(options) {
348
+ assertId(options?.articleId, "articleId");
349
+ return this.#request(
350
+ "GET",
351
+ `/articles/${options.articleId}`,
352
+ getSharedOptions(options)
353
+ );
354
+ }
355
+ /**
356
+ * @param {Object} options
357
+ * @param {string} options.articleId
358
+ * @param {string|Date} [options.date]
359
+ */
360
+ async deleteArticle(options) {
361
+ assertId(options?.articleId, "articleId");
362
+ return this.#request(
363
+ "DELETE",
364
+ `/articles/${options.articleId}`,
365
+ getSharedOptions(options)
366
+ );
367
+ }
368
+ /**
369
+ * @param {Record<string, unknown>} options
370
+ * @param {string} [options.channelId]
371
+ * @param {string} [options.sectionId]
372
+ * @param {string|Date} [options.date]
373
+ */
374
+ async searchArticles(options) {
375
+ const hasChannelId = typeof options?.channelId === "string" && options.channelId.length > 0;
376
+ const hasSectionId = typeof options?.sectionId === "string" && options.sectionId.length > 0;
377
+ if (!hasChannelId && !hasSectionId) {
378
+ throw new TypeError(
379
+ "searchArticles requires either channelId or sectionId"
380
+ );
381
+ }
382
+ if (hasChannelId && hasSectionId) {
383
+ throw new TypeError(
384
+ "searchArticles accepts either channelId or sectionId, not both"
385
+ );
386
+ }
387
+ const endpoint = hasChannelId ? `/channels/${options.channelId}/articles` : `/sections/${options.sectionId}/articles`;
388
+ const query = { ...options };
389
+ const date = query.date;
390
+ delete query.channelId;
391
+ delete query.sectionId;
392
+ delete query.date;
393
+ return this.#request("GET", endpoint, {
394
+ date,
395
+ query
396
+ });
397
+ }
398
+ /**
399
+ * @param {Object} options
400
+ * @param {string} options.channelId
401
+ * @param {Record<string, unknown>} options.article
402
+ * @param {Record<string, ArticleBundleFile>} [options.bundleFiles]
403
+ * @param {boolean} [options.isPreview=true]
404
+ * @param {boolean} [options.isSponsored=false]
405
+ * @param {boolean} [options.isPaid=false]
406
+ * @param {string[]} [options.sections]
407
+ * @param {'KIDS'|'MATURE'|'GENERAL'} [options.maturityRating]
408
+ * @param {string|Date} [options.date]
409
+ */
410
+ async createArticle(options) {
411
+ assertId(options?.channelId, "channelId");
412
+ const multipart = buildArticleMultipartBody({
413
+ article: options.article,
414
+ metadata: buildMetadata(options, this.host),
415
+ bundleFiles: options.bundleFiles
416
+ });
417
+ return this.#request("POST", `/channels/${options.channelId}/articles`, {
418
+ date: options.date,
419
+ contentType: multipart.contentType,
420
+ body: multipart.body
421
+ });
422
+ }
423
+ /**
424
+ * @param {Object} options
425
+ * @param {string} options.articleId
426
+ * @param {string} options.revision
427
+ * @param {Record<string, unknown>} options.article
428
+ * @param {Record<string, ArticleBundleFile>} [options.bundleFiles]
429
+ * @param {boolean} [options.isPreview=true]
430
+ * @param {boolean} [options.isSponsored=false]
431
+ * @param {boolean} [options.isPaid=false]
432
+ * @param {string[]} [options.sections]
433
+ * @param {'KIDS'|'MATURE'|'GENERAL'} [options.maturityRating]
434
+ * @param {string|Date} [options.date]
435
+ */
436
+ async updateArticle(options) {
437
+ assertId(options?.articleId, "articleId");
438
+ assertId(options?.revision, "revision");
439
+ const multipart = buildArticleMultipartBody({
440
+ article: options.article,
441
+ metadata: buildMetadata(options, this.host, true),
442
+ bundleFiles: options.bundleFiles
443
+ });
444
+ return this.#request("POST", `/articles/${options.articleId}`, {
445
+ date: options.date,
446
+ contentType: multipart.contentType,
447
+ body: multipart.body
448
+ });
449
+ }
450
+ /**
451
+ * @param {Object} options
452
+ * @param {string} options.channelId
453
+ * @param {string|Date} [options.date]
454
+ */
455
+ async readChannelQuota(options) {
456
+ assertId(options?.channelId, "channelId");
457
+ return this.#request(
458
+ "GET",
459
+ `/channels/${options.channelId}/quota`,
460
+ getSharedOptions(options)
461
+ );
462
+ }
463
+ /**
464
+ * @param {Object} options
465
+ * @param {string} options.sectionId
466
+ * @param {string[]} options.articleIds
467
+ * @param {string|Date} [options.date]
468
+ */
469
+ async promoteArticles(options) {
470
+ assertId(options?.sectionId, "sectionId");
471
+ if (!Array.isArray(options?.articleIds)) {
472
+ throw new TypeError("articleIds is required and must be an array");
473
+ }
474
+ const body = JSON.stringify({
475
+ data: { promotedArticles: options.articleIds }
476
+ });
477
+ return this.#request(
478
+ "POST",
479
+ `/sections/${options.sectionId}/promotedArticles`,
480
+ {
481
+ date: options.date,
482
+ contentType: "application/json",
483
+ body
484
+ }
485
+ );
486
+ }
487
+ /**
488
+ * @param {string} method
489
+ * @param {string} endpoint
490
+ * @param {Object} [options={}]
491
+ * @param {string|Date} [options.date]
492
+ * @param {Record<string, string|number|boolean|undefined>} [options.query]
493
+ * @param {string} [options.contentType]
494
+ * @param {string|Buffer|Uint8Array|null} [options.body]
495
+ */
496
+ async #request(method, endpoint, options = {}) {
497
+ return requestSigned({
498
+ apiId: this.apiId,
499
+ apiSecret: this.apiSecret,
500
+ host: this.host,
501
+ method,
502
+ endpoint,
503
+ date: options.date,
504
+ query: options.query,
505
+ contentType: options.contentType,
506
+ body: options.body
507
+ });
508
+ }
509
+ };
510
+
511
+ // src/helpers/fetchBundleFiles.js
512
+ async function fetchBundleFiles(urlMap) {
513
+ const result = {};
514
+ for (const [filename, url] of Object.entries(urlMap)) {
515
+ const response = await fetch(url);
516
+ if (!response.ok) {
517
+ throw new Error(
518
+ `Failed to fetch bundle file ${filename} from ${url}: ${response.status}`
519
+ );
520
+ }
521
+ const arrayBuffer = await response.arrayBuffer();
522
+ const data = Buffer.from(arrayBuffer);
523
+ const mimeType = response.headers.get("content-type") ?? "application/octet-stream";
524
+ result[filename] = {
525
+ data,
526
+ mimeType
527
+ };
528
+ }
529
+ return result;
530
+ }
531
+ export {
532
+ AppleNewsApiError,
533
+ AppleNewsClient,
534
+ buildArticleMultipartBody,
535
+ buildAuthorizationHeader,
536
+ buildCanonicalRequest,
537
+ buildRequestUrl,
538
+ createSignedHeaders,
539
+ fetchBundleFiles,
540
+ formatAppleDate,
541
+ requestSigned,
542
+ signCanonicalRequest
543
+ };
544
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/auth.js", "../src/request.js", "../src/multipart.js", "../src/client.js", "../src/helpers/fetchBundleFiles.js"],
4
+ "sourcesContent": ["import { createHmac } from 'node:crypto'\n\n/**\n * @param {Date} date\n * @returns {string}\n */\nexport function formatAppleDate(date) {\n const iso = date.toISOString()\n return iso.replace(/\\.\\d{3}Z$/, 'Z')\n}\n\n/**\n * @param {string|Uint8Array|Buffer|undefined|null} value\n * @returns {Buffer}\n */\nfunction toBuffer(value) {\n if (value == null) {\n return Buffer.alloc(0)\n }\n\n if (Buffer.isBuffer(value)) {\n return value\n }\n\n if (value instanceof Uint8Array) {\n return Buffer.from(value)\n }\n\n return Buffer.from(String(value), 'utf8')\n}\n\n/**\n * @typedef BuildCanonicalRequestInput\n * @property {string} method\n * @property {string} url\n * @property {string} date\n * @property {string} [contentType]\n * @property {string|Uint8Array|Buffer|undefined|null} [body]\n */\n\n/**\n * Build canonical payload for Apple HHMAC signing.\n *\n * @param {BuildCanonicalRequestInput} input\n * @returns {Buffer}\n */\nexport function buildCanonicalRequest(input) {\n const method = input.method.toUpperCase()\n const contentType = input.contentType ?? ''\n const prefix = `${method}${input.url}${input.date}${contentType}`\n const prefixBuffer = Buffer.from(prefix, 'utf8')\n const bodyBuffer = toBuffer(input.body)\n\n if (bodyBuffer.length === 0) {\n return prefixBuffer\n }\n\n return Buffer.concat([prefixBuffer, bodyBuffer])\n}\n\n/**\n * @param {string} apiSecret Base64 encoded Apple News API secret\n * @param {Buffer} canonicalPayload\n * @returns {string}\n */\nexport function signCanonicalRequest(apiSecret, canonicalPayload) {\n const key = Buffer.from(apiSecret, 'base64')\n\n return createHmac('sha256', key).update(canonicalPayload).digest('base64')\n}\n\n/**\n * @param {{ apiId: string, signature: string, date: string }} input\n * @returns {string}\n */\nexport function buildAuthorizationHeader(input) {\n return `HHMAC; key=\"${input.apiId}\"; signature=\"${input.signature}\"; date=\"${input.date}\"`\n}\n\n/**\n * @typedef CreateSignedHeadersInput\n * @property {string} apiId\n * @property {string} apiSecret\n * @property {string} method\n * @property {string} url\n * @property {string|Date} [date]\n * @property {string} [contentType]\n * @property {string|Uint8Array|Buffer|undefined|null} [body]\n */\n\n/**\n * @param {CreateSignedHeadersInput} input\n * @returns {{ headers: Record<string, string>, date: string, signature: string, canonicalPayload: Buffer }}\n */\nexport function createSignedHeaders(input) {\n const date =\n typeof input.date === 'string'\n ? input.date\n : formatAppleDate(input.date ?? new Date())\n const contentType = input.contentType ?? ''\n\n const canonicalPayload = buildCanonicalRequest({\n method: input.method,\n url: input.url,\n date,\n contentType,\n body: input.body\n })\n\n const signature = signCanonicalRequest(input.apiSecret, canonicalPayload)\n const authorization = buildAuthorizationHeader({\n apiId: input.apiId,\n signature,\n date\n })\n\n const headers = {\n Accept: 'application/json',\n Authorization: authorization\n }\n\n if (contentType) {\n headers['Content-Type'] = contentType\n }\n\n return {\n headers,\n date,\n signature,\n canonicalPayload\n }\n}\n", "import { createSignedHeaders } from './auth.js'\n\n/**\n * @typedef RequestOptions\n * @property {string} apiId\n * @property {string} apiSecret\n * @property {string} method\n * @property {string} endpoint\n * @property {string} [host]\n * @property {Record<string, string | number | boolean | undefined>} [query]\n * @property {string} [contentType]\n * @property {string|Buffer|Uint8Array|null} [body]\n * @property {string|Date} [date]\n */\n\nexport class AppleNewsApiError extends Error {\n /**\n * @param {string} message\n * @param {Object} details\n * @param {number} details.status\n * @param {string} details.method\n * @param {string} details.url\n * @param {unknown[]} [details.apiErrors]\n * @param {unknown} [details.responseBody]\n */\n constructor(message, details) {\n super(message)\n this.name = 'AppleNewsApiError'\n this.status = details.status\n this.method = details.method\n this.url = details.url\n this.apiErrors = details.apiErrors\n this.responseBody = details.responseBody\n }\n}\n\n/**\n * @param {string} host\n * @param {string} endpoint\n * @param {Record<string, string | number | boolean | undefined>} [query]\n * @returns {string}\n */\nexport function buildRequestUrl(host, endpoint, query) {\n const url = new URL(`https://${host}${endpoint}`)\n\n if (query) {\n for (const [key, value] of Object.entries(query)) {\n if (value === undefined) {\n continue\n }\n\n url.searchParams.set(key, String(value))\n }\n }\n\n return url.toString()\n}\n\n/**\n * @param {unknown} value\n * @returns {value is Record<string, unknown>}\n */\nfunction isObject(value) {\n return value !== null && typeof value === 'object'\n}\n\n/**\n * @param {unknown} parsed\n * @returns {unknown[]|undefined}\n */\nfunction getApiErrors(parsed) {\n if (!isObject(parsed)) {\n return undefined\n }\n\n const maybeErrors = parsed.errors\n if (!Array.isArray(maybeErrors)) {\n return undefined\n }\n\n return maybeErrors\n}\n\n/**\n * @param {unknown} parsed\n * @returns {unknown}\n */\nfunction unwrapData(parsed) {\n if (!isObject(parsed)) {\n return parsed\n }\n\n if ('data' in parsed) {\n return parsed.data\n }\n\n return parsed\n}\n\n/**\n * Execute one signed Apple News API request.\n *\n * @param {RequestOptions} options\n * @returns {Promise<unknown>}\n */\nexport async function requestSigned(options) {\n const host = options.host ?? 'news-api.apple.com'\n\n const url = buildRequestUrl(host, options.endpoint, options.query)\n const signed = createSignedHeaders({\n apiId: options.apiId,\n apiSecret: options.apiSecret,\n method: options.method,\n url,\n date: options.date,\n contentType: options.contentType,\n body: options.body\n })\n\n const response = await fetch(url, {\n method: options.method,\n headers: signed.headers,\n body: options.body ?? undefined\n })\n\n const rawBody = await response.text()\n let parsedBody = null\n\n if (rawBody.length > 0) {\n try {\n parsedBody = JSON.parse(rawBody)\n } catch {\n parsedBody = rawBody\n }\n }\n\n if (!response.ok) {\n const apiErrors = getApiErrors(parsedBody)\n throw new AppleNewsApiError(\n `${options.method.toUpperCase()} ${options.endpoint} failed with status ${response.status}`,\n {\n status: response.status,\n method: options.method.toUpperCase(),\n url,\n apiErrors,\n responseBody: parsedBody\n }\n )\n }\n\n if (parsedBody === null) {\n return null\n }\n\n return unwrapData(parsedBody)\n}\n", "const CRLF = '\\r\\n'\n\n/**\n * @typedef BundleFile\n * @property {Buffer|Uint8Array|string} data\n * @property {string} [mimeType]\n */\n\n/**\n * @param {Buffer|Uint8Array|string} data\n * @returns {Buffer}\n */\nfunction toBuffer(data) {\n if (Buffer.isBuffer(data)) {\n return data\n }\n\n if (data instanceof Uint8Array) {\n return Buffer.from(data)\n }\n\n return Buffer.from(String(data), 'utf8')\n}\n\n/**\n * @param {string} boundary\n * @param {string} name\n * @param {Buffer} value\n * @param {string} contentType\n * @param {string} [filename]\n * @returns {Buffer}\n */\nfunction buildPart(boundary, name, value, contentType, filename) {\n const lines = [`--${boundary}`]\n\n if (filename) {\n lines.push(\n `Content-Disposition: form-data; name=\"${name}\"; filename=\"${filename}\"`\n )\n } else {\n lines.push(`Content-Disposition: form-data; name=\"${name}\"`)\n }\n\n lines.push(`Content-Type: ${contentType}`)\n lines.push('')\n\n return Buffer.concat([\n Buffer.from(lines.join(CRLF) + CRLF, 'utf8'),\n value,\n Buffer.from(CRLF, 'utf8')\n ])\n}\n\n/**\n * @param {Record<string, BundleFile>|undefined} bundleFiles\n */\nfunction validateBundleFiles(bundleFiles) {\n if (!bundleFiles) {\n return\n }\n\n for (const name of Object.keys(bundleFiles)) {\n if (name === 'article.json' || name === 'metadata') {\n throw new TypeError(\n 'bundleFiles cannot contain reserved names: article.json or metadata'\n )\n }\n }\n}\n\n/**\n * @param {Object} input\n * @param {Record<string, unknown>} input.article\n * @param {Record<string, unknown>} [input.metadata]\n * @param {Record<string, BundleFile>} [input.bundleFiles]\n * @param {string} [input.boundary]\n * @returns {{ contentType: string, body: Buffer }}\n */\nexport function buildArticleMultipartBody(input) {\n if (!input.article || typeof input.article !== 'object') {\n throw new TypeError('article is required and must be an object')\n }\n\n validateBundleFiles(input.bundleFiles)\n\n const boundary = input.boundary ?? `apple-news-${Date.now().toString(16)}`\n const metadata = input.metadata ?? {}\n\n const parts = []\n parts.push(\n buildPart(\n boundary,\n 'article.json',\n Buffer.from(JSON.stringify(input.article), 'utf8'),\n 'application/json'\n )\n )\n\n parts.push(\n buildPart(\n boundary,\n 'metadata',\n Buffer.from(JSON.stringify({ data: metadata }), 'utf8'),\n 'application/json'\n )\n )\n\n if (input.bundleFiles) {\n let index = 0\n for (const [filename, file] of Object.entries(input.bundleFiles)) {\n const fieldName = `file${index}`\n index += 1\n parts.push(\n buildPart(\n boundary,\n fieldName,\n toBuffer(file.data),\n file.mimeType ?? 'application/octet-stream',\n filename\n )\n )\n }\n }\n\n parts.push(Buffer.from(`--${boundary}--${CRLF}`, 'utf8'))\n\n return {\n contentType: `multipart/form-data; boundary=${boundary}`,\n body: Buffer.concat(parts)\n }\n}\n", "import { requestSigned } from './request.js'\nimport { buildArticleMultipartBody } from './multipart.js'\n\n/**\n * @typedef AppleNewsClientConfig\n * @property {string} apiId\n * @property {string} apiSecret\n * @property {string} [host]\n */\n\nfunction assertRequired(value, name) {\n if (!value || typeof value !== 'string') {\n throw new TypeError(`${name} is required and must be a non-empty string`)\n }\n}\n\nfunction assertId(value, name) {\n if (!value || typeof value !== 'string') {\n throw new TypeError(`${name} is required and must be a non-empty string`)\n }\n}\n\n/**\n * @typedef {Object} ArticleBundleFile\n * @property {Buffer|Uint8Array|string} data\n * @property {string} [mimeType]\n */\n\n/**\n * @param {Object} options\n * @param {boolean} [options.isPreview=true]\n * @param {boolean} [options.isSponsored=false]\n * @param {boolean} [options.isPaid=false]\n * @param {string[]} [options.sections]\n * @param {'KIDS'|'MATURE'|'GENERAL'} [options.maturityRating]\n * @param {string} [options.revision]\n * @param {string} host\n * @param {boolean} includeRevision\n */\nfunction buildMetadata(options, host, includeRevision = false) {\n const metadata = {\n isPreview: options.isPreview ?? true,\n isSponsored: options.isSponsored ?? false,\n isPaid: options.isPaid ?? false\n }\n\n if (options.sections !== undefined) {\n metadata.links = {\n sections: options.sections.map(\n (sectionId) =>\n `https://${host}/sections/${encodeURIComponent(sectionId)}`\n )\n }\n }\n\n if (options.maturityRating !== undefined) {\n metadata.maturityRating = options.maturityRating\n }\n\n if (includeRevision) {\n metadata.revision = options.revision\n }\n\n return metadata\n}\n\n/**\n * @typedef {Object} SharedOptions\n * @property {string|Date} [date]\n */\n\n/**\n * @param {Record<string, unknown>|undefined} options\n * @returns {SharedOptions}\n */\nfunction getSharedOptions(options) {\n return {\n date: options?.date\n }\n}\n\nexport class AppleNewsClient {\n /**\n * @param {AppleNewsClientConfig} config\n */\n constructor(config) {\n assertRequired(config?.apiId, 'apiId')\n assertRequired(config?.apiSecret, 'apiSecret')\n\n this.apiId = config.apiId\n this.apiSecret = config.apiSecret\n this.host = config.host ?? 'news-api.apple.com'\n }\n\n /**\n * @param {Object} options\n * @param {string} options.channelId\n * @param {string|Date} [options.date]\n */\n async readChannel(options) {\n assertId(options?.channelId, 'channelId')\n return this.#request(\n 'GET',\n `/channels/${options.channelId}`,\n getSharedOptions(options)\n )\n }\n\n /**\n * @param {Object} options\n * @param {string} options.channelId\n * @param {string|Date} [options.date]\n */\n async listSections(options) {\n assertId(options?.channelId, 'channelId')\n return this.#request(\n 'GET',\n `/channels/${options.channelId}/sections`,\n getSharedOptions(options)\n )\n }\n\n /**\n * @param {Object} options\n * @param {string} options.sectionId\n * @param {string|Date} [options.date]\n */\n async readSection(options) {\n assertId(options?.sectionId, 'sectionId')\n return this.#request(\n 'GET',\n `/sections/${options.sectionId}`,\n getSharedOptions(options)\n )\n }\n\n /**\n * @param {Object} options\n * @param {string} options.articleId\n * @param {string|Date} [options.date]\n */\n async readArticle(options) {\n assertId(options?.articleId, 'articleId')\n return this.#request(\n 'GET',\n `/articles/${options.articleId}`,\n getSharedOptions(options)\n )\n }\n\n /**\n * @param {Object} options\n * @param {string} options.articleId\n * @param {string|Date} [options.date]\n */\n async deleteArticle(options) {\n assertId(options?.articleId, 'articleId')\n return this.#request(\n 'DELETE',\n `/articles/${options.articleId}`,\n getSharedOptions(options)\n )\n }\n\n /**\n * @param {Record<string, unknown>} options\n * @param {string} [options.channelId]\n * @param {string} [options.sectionId]\n * @param {string|Date} [options.date]\n */\n async searchArticles(options) {\n const hasChannelId =\n typeof options?.channelId === 'string' && options.channelId.length > 0\n const hasSectionId =\n typeof options?.sectionId === 'string' && options.sectionId.length > 0\n\n if (!hasChannelId && !hasSectionId) {\n throw new TypeError(\n 'searchArticles requires either channelId or sectionId'\n )\n }\n\n if (hasChannelId && hasSectionId) {\n throw new TypeError(\n 'searchArticles accepts either channelId or sectionId, not both'\n )\n }\n\n const endpoint = hasChannelId\n ? `/channels/${options.channelId}/articles`\n : `/sections/${options.sectionId}/articles`\n\n const query = { ...options }\n const date = query.date\n delete query.channelId\n delete query.sectionId\n delete query.date\n\n return this.#request('GET', endpoint, {\n date,\n query\n })\n }\n\n /**\n * @param {Object} options\n * @param {string} options.channelId\n * @param {Record<string, unknown>} options.article\n * @param {Record<string, ArticleBundleFile>} [options.bundleFiles]\n * @param {boolean} [options.isPreview=true]\n * @param {boolean} [options.isSponsored=false]\n * @param {boolean} [options.isPaid=false]\n * @param {string[]} [options.sections]\n * @param {'KIDS'|'MATURE'|'GENERAL'} [options.maturityRating]\n * @param {string|Date} [options.date]\n */\n async createArticle(options) {\n assertId(options?.channelId, 'channelId')\n\n const multipart = buildArticleMultipartBody({\n article: options.article,\n metadata: buildMetadata(options, this.host),\n bundleFiles: options.bundleFiles\n })\n\n return this.#request('POST', `/channels/${options.channelId}/articles`, {\n date: options.date,\n contentType: multipart.contentType,\n body: multipart.body\n })\n }\n\n /**\n * @param {Object} options\n * @param {string} options.articleId\n * @param {string} options.revision\n * @param {Record<string, unknown>} options.article\n * @param {Record<string, ArticleBundleFile>} [options.bundleFiles]\n * @param {boolean} [options.isPreview=true]\n * @param {boolean} [options.isSponsored=false]\n * @param {boolean} [options.isPaid=false]\n * @param {string[]} [options.sections]\n * @param {'KIDS'|'MATURE'|'GENERAL'} [options.maturityRating]\n * @param {string|Date} [options.date]\n */\n async updateArticle(options) {\n assertId(options?.articleId, 'articleId')\n assertId(options?.revision, 'revision')\n\n const multipart = buildArticleMultipartBody({\n article: options.article,\n metadata: buildMetadata(options, this.host, true),\n bundleFiles: options.bundleFiles\n })\n\n return this.#request('POST', `/articles/${options.articleId}`, {\n date: options.date,\n contentType: multipart.contentType,\n body: multipart.body\n })\n }\n\n /**\n * @param {Object} options\n * @param {string} options.channelId\n * @param {string|Date} [options.date]\n */\n async readChannelQuota(options) {\n assertId(options?.channelId, 'channelId')\n return this.#request(\n 'GET',\n `/channels/${options.channelId}/quota`,\n getSharedOptions(options)\n )\n }\n\n /**\n * @param {Object} options\n * @param {string} options.sectionId\n * @param {string[]} options.articleIds\n * @param {string|Date} [options.date]\n */\n async promoteArticles(options) {\n assertId(options?.sectionId, 'sectionId')\n\n if (!Array.isArray(options?.articleIds)) {\n throw new TypeError('articleIds is required and must be an array')\n }\n\n const body = JSON.stringify({\n data: { promotedArticles: options.articleIds }\n })\n\n return this.#request(\n 'POST',\n `/sections/${options.sectionId}/promotedArticles`,\n {\n date: options.date,\n contentType: 'application/json',\n body\n }\n )\n }\n\n /**\n * @param {string} method\n * @param {string} endpoint\n * @param {Object} [options={}]\n * @param {string|Date} [options.date]\n * @param {Record<string, string|number|boolean|undefined>} [options.query]\n * @param {string} [options.contentType]\n * @param {string|Buffer|Uint8Array|null} [options.body]\n */\n async #request(method, endpoint, options = {}) {\n return requestSigned({\n apiId: this.apiId,\n apiSecret: this.apiSecret,\n host: this.host,\n method,\n endpoint,\n date: options.date,\n query: options.query,\n contentType: options.contentType,\n body: options.body\n })\n }\n}\n", "/**\n * @typedef BundleFetchResult\n * @property {Buffer} data\n * @property {string} mimeType\n */\n\n/**\n * @param {Record<string, string>} urlMap\n * @returns {Promise<Record<string, BundleFetchResult>>}\n */\nexport async function fetchBundleFiles(urlMap) {\n const result = {}\n\n for (const [filename, url] of Object.entries(urlMap)) {\n const response = await fetch(url)\n\n if (!response.ok) {\n throw new Error(\n `Failed to fetch bundle file ${filename} from ${url}: ${response.status}`\n )\n }\n\n const arrayBuffer = await response.arrayBuffer()\n const data = Buffer.from(arrayBuffer)\n const mimeType =\n response.headers.get('content-type') ?? 'application/octet-stream'\n\n result[filename] = {\n data,\n mimeType\n }\n }\n\n return result\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["toBuffer"]
7
+ }
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "@stephensauceda/apple-news",
3
+ "version": "1.0.0",
4
+ "description": "Modern Apple News API client for Node.js",
5
+ "author": "Stephen Sauceda",
6
+ "type": "module",
7
+ "main": "./dist/index.js",
8
+ "sideEffects": false,
9
+ "exports": {
10
+ ".": "./dist/index.js",
11
+ "./helpers": "./dist/helpers.js"
12
+ },
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/stephensauceda/apple-news.git"
16
+ },
17
+ "bugs": {
18
+ "url": "https://github.com/stephensauceda/apple-news/issues"
19
+ },
20
+ "homepage": "https://github.com/stephensauceda/apple-news#readme",
21
+ "funding": {
22
+ "type": "github",
23
+ "url": "https://github.com/sponsors/stephensauceda"
24
+ },
25
+ "files": [
26
+ "dist"
27
+ ],
28
+ "engines": {
29
+ "node": ">=22"
30
+ },
31
+ "publishConfig": {
32
+ "access": "public"
33
+ },
34
+ "scripts": {
35
+ "build": "node ./local/scripts/build.mjs",
36
+ "clean": "rm -rf dist coverage",
37
+ "lint": "eslint .",
38
+ "format": "prettier --write .",
39
+ "format:check": "prettier --check .",
40
+ "test": "vitest run",
41
+ "test:watch": "vitest",
42
+ "coverage": "vitest run --coverage",
43
+ "prepare": "npm run build"
44
+ },
45
+ "keywords": [
46
+ "apple-news",
47
+ "apple-news-api",
48
+ "news",
49
+ "api-client",
50
+ "nodejs",
51
+ "publishing"
52
+ ],
53
+ "license": "MIT",
54
+ "devDependencies": {
55
+ "@eslint/js": "^10.0.1",
56
+ "@vitest/coverage-v8": "^5.0.3",
57
+ "esbuild": "^0.28.0",
58
+ "eslint": "^10.2.0",
59
+ "globals": "^17.4.0",
60
+ "prettier": "^3.5.3",
61
+ "vitest": "^5.0.3"
62
+ }
63
+ }