discogs-typescript 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +396 -0
- package/dist/index.d.ts +2573 -0
- package/dist/index.js +1739 -0
- package/dist/index.js.map +1 -0
- package/package.json +79 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Thijs Wijnmaalen
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
# discogs-typescript
|
|
2
|
+
|
|
3
|
+
A modern, fully typed, zero-dependency TypeScript client for the [Discogs API v2](https://www.discogs.com/developers/).
|
|
4
|
+
|
|
5
|
+
Covers **every endpoint in the Discogs documentation** — all 60 of them, across Database,
|
|
6
|
+
Marketplace, Inventory Export, Inventory Upload, User Identity, User Collection, User Wantlist
|
|
7
|
+
and User Lists — plus all three authentication schemes.
|
|
8
|
+
|
|
9
|
+
- **Zero runtime dependencies.** Nothing but the platform.
|
|
10
|
+
- **Isomorphic.** Global `fetch` and Web Crypto only — Node 18+, Deno, Bun, browsers and edge
|
|
11
|
+
runtimes. No Node built-ins are imported.
|
|
12
|
+
- **Fully typed.** Hand-written interfaces for every request and response, with string-literal
|
|
13
|
+
unions for conditions, currencies, order statuses and sort keys.
|
|
14
|
+
- **ESM only**, with a single rolled-up `.d.ts`. There is no CommonJS build, though Node
|
|
15
|
+
22.12+ can still `require()` it via `require(esm)` — the bundle has no top-level await.
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pnpm add discogs-typescript
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Quick start
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
import { DiscogsClient } from 'discogs-typescript'
|
|
27
|
+
|
|
28
|
+
const client = new DiscogsClient({
|
|
29
|
+
// Required — Discogs returns an empty response to requests without a User-Agent.
|
|
30
|
+
userAgent: 'MyApp/1.0 +https://example.com',
|
|
31
|
+
auth: { token: process.env.DISCOGS_TOKEN! }
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
const release = await client.database.getRelease(249504)
|
|
35
|
+
console.log(release.title, release.artists[0]?.name)
|
|
36
|
+
|
|
37
|
+
const results = await client.database.search({ artist: 'nirvana', type: 'release' })
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The `userAgent` is not optional politeness — Discogs answers requests without one with an
|
|
41
|
+
empty body, and rejects strings that look like a browser or a generic HTTP library. Use
|
|
42
|
+
something like `MyDiscogsClient/1.0 +https://mydiscogsclient.org`.
|
|
43
|
+
|
|
44
|
+
## Authentication
|
|
45
|
+
|
|
46
|
+
All three schemes from the [authentication docs](https://www.discogs.com/developers/#page:authentication)
|
|
47
|
+
are supported. What you can do depends on which you pick:
|
|
48
|
+
|
|
49
|
+
| Credentials | Rate limit | Image URLs | Acts as a user |
|
|
50
|
+
| ----------------------- | ---------- | ---------- | --------------------------- |
|
|
51
|
+
| none | 25/min | no | no |
|
|
52
|
+
| consumer key + secret | 60/min | yes | no |
|
|
53
|
+
| personal access token | 60/min | yes | the token holder |
|
|
54
|
+
| OAuth 1.0a access token | 60/min | yes | any user who granted access |
|
|
55
|
+
|
|
56
|
+
### Personal access token
|
|
57
|
+
|
|
58
|
+
The simplest option for scripts acting on your own account. Generate one under
|
|
59
|
+
[Developer Settings](https://www.discogs.com/settings/developers).
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
new DiscogsClient({ userAgent, auth: { token: 'abcxyz123456' } })
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Consumer key and secret
|
|
66
|
+
|
|
67
|
+
Raises your rate limit and unlocks image URLs, but authenticates you as nobody — endpoints
|
|
68
|
+
that touch a user's data still need a token or OAuth.
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
new DiscogsClient({ userAgent, auth: { consumerKey: 'foo123', consumerSecret: 'bar456' } })
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### OAuth 1.0a
|
|
75
|
+
|
|
76
|
+
To act on behalf of other users, run the three-legged flow with `DiscogsOAuth`:
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
import { DiscogsClient, DiscogsOAuth } from 'discogs-typescript'
|
|
80
|
+
|
|
81
|
+
const oauth = new DiscogsOAuth({ consumerKey, consumerSecret, userAgent })
|
|
82
|
+
|
|
83
|
+
// 1. Temporary request token (valid 15 minutes). Pass 'oob' if you have no callback URL.
|
|
84
|
+
const request = await oauth.getRequestToken('https://example.com/callback')
|
|
85
|
+
|
|
86
|
+
// 2. Send the user here to approve your app.
|
|
87
|
+
console.log(oauth.getAuthorizeUrl(request.oauthToken))
|
|
88
|
+
|
|
89
|
+
// 3. Discogs redirects back with ?oauth_verifier=… — exchange it.
|
|
90
|
+
const access = await oauth.getAccessToken({
|
|
91
|
+
oauthToken: request.oauthToken,
|
|
92
|
+
oauthTokenSecret: request.oauthTokenSecret,
|
|
93
|
+
verifier
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
// 4. Access tokens do not expire unless the user revokes them. Store and reuse.
|
|
97
|
+
const client = new DiscogsClient({
|
|
98
|
+
userAgent,
|
|
99
|
+
auth: {
|
|
100
|
+
consumerKey,
|
|
101
|
+
consumerSecret,
|
|
102
|
+
accessToken: access.oauthToken,
|
|
103
|
+
accessTokenSecret: access.oauthTokenSecret
|
|
104
|
+
}
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
await client.user.getIdentity() // confirms who you are authenticated as
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Requests are signed with `PLAINTEXT` by default, which is what the Discogs docs recommend —
|
|
111
|
+
everything runs over HTTPS anyway. Pass `signatureMethod: 'HMAC-SHA1'` in the auth object to
|
|
112
|
+
sign with HMAC-SHA1 instead (computed via Web Crypto).
|
|
113
|
+
|
|
114
|
+
You can also supply your own strategy:
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
const client = new DiscogsClient({
|
|
118
|
+
userAgent,
|
|
119
|
+
auth: { authorize: ({ headers }) => headers.set('Authorization', 'Discogs token=…') }
|
|
120
|
+
})
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Endpoints
|
|
124
|
+
|
|
125
|
+
Every resource mirrors a section of the Discogs documentation.
|
|
126
|
+
|
|
127
|
+
### `client.database`
|
|
128
|
+
|
|
129
|
+
`getRelease` · `getReleaseRating` · `updateReleaseRating` · `deleteReleaseRating` ·
|
|
130
|
+
`getCommunityReleaseRating` · `getReleaseStats` · `getMaster` · `getMasterVersions` ·
|
|
131
|
+
`getArtist` · `getArtistReleases` · `getLabel` · `getLabelReleases` · `search`
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
const versions = await client.database.getMasterVersions(1000, {
|
|
135
|
+
country: 'Belgium',
|
|
136
|
+
sort: 'released',
|
|
137
|
+
sort_order: 'asc'
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
// Artist discographies mix masters and releases, discriminated by `type`.
|
|
141
|
+
const { releases } = await client.database.getArtistReleases(108713, { sort: 'year' })
|
|
142
|
+
for (const item of releases) {
|
|
143
|
+
if (item.type === 'master') console.log(item.main_release)
|
|
144
|
+
else console.log(item.format, item.label)
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
`search` requires authentication as any user — an unauthenticated search fails with a 401.
|
|
149
|
+
|
|
150
|
+
### `client.marketplace`
|
|
151
|
+
|
|
152
|
+
`getInventory` · `getListing` · `createListing` · `editListing` · `deleteListing` ·
|
|
153
|
+
`getOrder` · `editOrder` · `listOrders` · `getOrderMessages` · `addOrderMessage` · `getFee` ·
|
|
154
|
+
`getPriceSuggestions` · `getReleaseStats`
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
const { listing_id } = await client.marketplace.createListing({
|
|
158
|
+
release_id: 249504,
|
|
159
|
+
condition: 'Near Mint (NM or M-)',
|
|
160
|
+
sleeve_condition: 'Very Good Plus (VG+)',
|
|
161
|
+
price: 12.5,
|
|
162
|
+
status: 'For Sale',
|
|
163
|
+
weight: 'auto' // or a number of grams
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
const fee = await client.marketplace.getFee(20, 'EUR') // omit the currency for USD
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Order ids are **strings** of the form `"1-1"`, not numbers. Which statuses an order can move
|
|
170
|
+
to is decided per order — read `order.next_status` rather than assuming a fixed table. Setting
|
|
171
|
+
`shipping` invoices the buyer and forces the status to `Invoice Sent`, so `shipping` and
|
|
172
|
+
`status` cannot be sent in the same `editOrder` call.
|
|
173
|
+
|
|
174
|
+
Order messages are a discriminated union on `type`:
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
const { messages } = await client.marketplace.getOrderMessages('1-1')
|
|
178
|
+
for (const message of messages) {
|
|
179
|
+
if (message.type === 'shipping') console.log(message.original, '→', message.new)
|
|
180
|
+
else if (message.type === 'status') console.log(message.status_id, message.actor.username)
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### `client.user`
|
|
185
|
+
|
|
186
|
+
`getIdentity` · `getProfile` · `editProfile` · `getSubmissions` · `getContributions`
|
|
187
|
+
|
|
188
|
+
### `client.collection`
|
|
189
|
+
|
|
190
|
+
`getFolders` · `createFolder` · `getFolder` · `editFolder` · `deleteFolder` ·
|
|
191
|
+
`getItemsByRelease` · `getItemsByFolder` · `addReleaseToFolder` · `changeInstance` ·
|
|
192
|
+
`deleteInstance` · `getFields` · `editFieldInstance` · `getValue`
|
|
193
|
+
|
|
194
|
+
Folder `0` is the permanent "All" folder (nothing can be added to it) and folder `1` is
|
|
195
|
+
"Uncategorized"; both are exported as `FOLDER_ALL` and `FOLDER_UNCATEGORIZED`. Because a user
|
|
196
|
+
may own several copies of the same release, each copy in a folder is an _instance_ with its
|
|
197
|
+
own `instance_id`.
|
|
198
|
+
|
|
199
|
+
```ts
|
|
200
|
+
const { instance_id } = await client.collection.addReleaseToFolder(username, 1, 249504)
|
|
201
|
+
await client.collection.changeInstance(username, 1, 249504, instance_id, { rating: 5 })
|
|
202
|
+
// Move it elsewhere by passing the destination as folder_id in the body:
|
|
203
|
+
await client.collection.changeInstance(username, 1, 249504, instance_id, { folder_id: 4 })
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### `client.wantlist`
|
|
207
|
+
|
|
208
|
+
`getWants` · `addToWantlist` · `editWantlistItem` · `removeFromWantlist`
|
|
209
|
+
|
|
210
|
+
Note that `notes` is a plain string on wantlist items, but an array of custom-field values on
|
|
211
|
+
collection items. That asymmetry is in the API, and the types reflect it.
|
|
212
|
+
|
|
213
|
+
### `client.lists`
|
|
214
|
+
|
|
215
|
+
`getUserLists` · `getList`
|
|
216
|
+
|
|
217
|
+
The index and detail endpoints name their fields differently — `date_added`/`date_changed`/
|
|
218
|
+
`id`/`uri` versus `created_ts`/`modified_ts`/`list_id`/`url`. Again, that is the API, not a
|
|
219
|
+
transcription slip.
|
|
220
|
+
|
|
221
|
+
### `client.inventoryExport` and `client.inventoryUpload`
|
|
222
|
+
|
|
223
|
+
`create` · `list` · `get` · `downloadCsv` · `downloadRaw`, and
|
|
224
|
+
`add` · `change` · `delete` · `list` · `get`.
|
|
225
|
+
|
|
226
|
+
Both are asynchronous job APIs: submit, then poll.
|
|
227
|
+
|
|
228
|
+
```ts
|
|
229
|
+
const { id } = await client.inventoryExport.create() // 409 if one is already running
|
|
230
|
+
const status = await client.inventoryExport.get(id!)
|
|
231
|
+
if (status?.finished_ts) {
|
|
232
|
+
const csv = await client.inventoryExport.downloadCsv(id!)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
await client.inventoryUpload.add(
|
|
236
|
+
'release_id,price,media_condition\n249504,12.50,Near Mint (NM or M-)\n'
|
|
237
|
+
)
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Upload CSVs must be comma-separated with a lower-case header row. `add` requires `release_id`,
|
|
241
|
+
`price` and `media_condition`; `change` requires `release_id` plus at least one field to
|
|
242
|
+
change; `delete` takes only `listing_id`.
|
|
243
|
+
|
|
244
|
+
Both `get` methods accept `ifModifiedSince` and resolve to `null` on a `304 Not Modified`.
|
|
245
|
+
|
|
246
|
+
## Pagination
|
|
247
|
+
|
|
248
|
+
Paginated endpoints take `page` and `per_page` (default 50, maximum 100) and return a
|
|
249
|
+
`pagination` object. Walking pages is left to you:
|
|
250
|
+
|
|
251
|
+
```ts
|
|
252
|
+
let page = 1
|
|
253
|
+
for (;;) {
|
|
254
|
+
const result = await client.database.getLabelReleases(1, { page, per_page: 100 })
|
|
255
|
+
for (const release of result.releases) console.log(release.title)
|
|
256
|
+
if (page >= result.pagination.pages) break
|
|
257
|
+
page++
|
|
258
|
+
}
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
Discogs also sends an RFC 5988 `Link` header, which `parseLinkHeader` will read if you are
|
|
262
|
+
working with a raw response.
|
|
263
|
+
|
|
264
|
+
## Rate limits
|
|
265
|
+
|
|
266
|
+
Discogs throttles by source IP over a rolling 60-second window: 60 requests per minute
|
|
267
|
+
authenticated, 25 unauthenticated. Exceeding it returns a 429, which this client raises as a
|
|
268
|
+
`DiscogsRateLimitError` carrying the headers that came with it.
|
|
269
|
+
|
|
270
|
+
This client does **not** queue or retry for you — it reports what the server said and lets
|
|
271
|
+
you decide. There are three ways to read that, in rough order of how often you will want them.
|
|
272
|
+
|
|
273
|
+
**`onResponse`** — fires after every request, before the body is read. This is the one to
|
|
274
|
+
reach for. It is the only accurate option while requests overlap, and it gives you the whole
|
|
275
|
+
response, so you always know which call the numbers belong to:
|
|
276
|
+
|
|
277
|
+
```ts
|
|
278
|
+
const client = new DiscogsClient({
|
|
279
|
+
userAgent,
|
|
280
|
+
onResponse: ({ response, rateLimit }) => {
|
|
281
|
+
// response.url, .status and .headers are all available here
|
|
282
|
+
if (rateLimit && rateLimit.remaining < 5) console.warn('Slow down —', response.url)
|
|
283
|
+
}
|
|
284
|
+
})
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
**`client.rateLimit`** — the most recent response's values, for a quick check between batches.
|
|
288
|
+
Because it only ever reflects the last response, it is racy under concurrency: with several
|
|
289
|
+
requests in flight you cannot tell which one it came from. Fine for a sequential script,
|
|
290
|
+
wrong for anything parallel.
|
|
291
|
+
|
|
292
|
+
```ts
|
|
293
|
+
await client.database.getRelease(249504)
|
|
294
|
+
console.log(client.rateLimit) // { limit: 60, used: 13, remaining: 47 }
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
**[`client.request()`](#escape-hatch)** — returns `{ data, response, rateLimit }` for a single
|
|
298
|
+
call, when you want the metadata inline rather than in a hook.
|
|
299
|
+
|
|
300
|
+
Resource methods deliberately return the parsed body rather than that envelope: paying a
|
|
301
|
+
`.data` on all 60 of them to carry metadata most calls ignore is not a good trade, and
|
|
302
|
+
`onResponse` covers the case better anyway.
|
|
303
|
+
|
|
304
|
+
## Errors
|
|
305
|
+
|
|
306
|
+
Every non-2xx response throws a `DiscogsError` subclass carrying the status, the parsed body
|
|
307
|
+
and the raw `Response`. The message is taken from the API's `{ "message": … }` payload.
|
|
308
|
+
|
|
309
|
+
```ts
|
|
310
|
+
import { DiscogsError, DiscogsNotFoundError, DiscogsRateLimitError } from 'discogs-typescript'
|
|
311
|
+
|
|
312
|
+
try {
|
|
313
|
+
await client.database.getRelease(1)
|
|
314
|
+
} catch (error) {
|
|
315
|
+
if (error instanceof DiscogsNotFoundError)
|
|
316
|
+
console.log(error.message) // "Release not found."
|
|
317
|
+
else if (error instanceof DiscogsRateLimitError) console.log(error.rateLimit)
|
|
318
|
+
else if (error instanceof DiscogsError) console.log(error.status, error.body)
|
|
319
|
+
else throw error
|
|
320
|
+
}
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
`DiscogsAuthenticationError` (401), `DiscogsPermissionError` (403), `DiscogsNotFoundError`
|
|
324
|
+
(404), `DiscogsMethodNotAllowedError` (405), `DiscogsValidationError` (422),
|
|
325
|
+
`DiscogsRateLimitError` (429) and `DiscogsServerError` (5xx) all extend `DiscogsError`.
|
|
326
|
+
|
|
327
|
+
## Escape hatch
|
|
328
|
+
|
|
329
|
+
Anything the typed resources do not cover — and the response headers they discard — is
|
|
330
|
+
reachable through `request`:
|
|
331
|
+
|
|
332
|
+
```ts
|
|
333
|
+
const { data, response, rateLimit } = await client.request<Release>({
|
|
334
|
+
method: 'GET',
|
|
335
|
+
path: '/releases/249504',
|
|
336
|
+
query: { curr_abbr: 'EUR' }
|
|
337
|
+
})
|
|
338
|
+
console.log(response.headers.get('Link'))
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
## Other options
|
|
342
|
+
|
|
343
|
+
```ts
|
|
344
|
+
new DiscogsClient({
|
|
345
|
+
userAgent,
|
|
346
|
+
auth,
|
|
347
|
+
baseUrl: 'https://api.discogs.com', // point at a proxy or a mock server
|
|
348
|
+
mediaType: 'plaintext', // 'discogs' (default) | 'html' | 'plaintext'
|
|
349
|
+
fetch: myFetch, // inject a custom fetch
|
|
350
|
+
onResponse
|
|
351
|
+
})
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
`mediaType` selects the `Accept` header, which controls how Discogs renders markup inside text
|
|
355
|
+
fields such as release notes and artist profiles.
|
|
356
|
+
|
|
357
|
+
## Development
|
|
358
|
+
|
|
359
|
+
```bash
|
|
360
|
+
pnpm install
|
|
361
|
+
pnpm test # vitest, no network access
|
|
362
|
+
pnpm typecheck
|
|
363
|
+
pnpm lint
|
|
364
|
+
pnpm build # vite library build → dist/
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
Runnable examples live in [`examples/`](./examples) — `pnpm tsx examples/search.ts` and
|
|
368
|
+
friends. They talk to the real API and need credentials in the environment.
|
|
369
|
+
|
|
370
|
+
TypeScript is deliberately held at 5.x while the rest of the toolchain tracks latest:
|
|
371
|
+
typescript-eslint refuses to load under TS 7 ([#10940](https://github.com/typescript-eslint/typescript-eslint/issues/10940))
|
|
372
|
+
and `@microsoft/api-extractor` cannot bundle declarations it emits, which would cost both
|
|
373
|
+
type-aware linting and the single rolled-up `.d.ts`. Worth revisiting once both support it.
|
|
374
|
+
|
|
375
|
+
## Releasing
|
|
376
|
+
|
|
377
|
+
Releases are cut from a git tag. `npm version` writes the new version to `package.json`,
|
|
378
|
+
commits it, and creates the matching tag:
|
|
379
|
+
|
|
380
|
+
```bash
|
|
381
|
+
npm version patch # or minor / major
|
|
382
|
+
git push --follow-tags
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
Pushing a `v*` tag runs [`.github/workflows/release.yml`](./.github/workflows/release.yml),
|
|
386
|
+
which checks the tag against `package.json`, runs lint, typecheck, tests and the build,
|
|
387
|
+
publishes to npm, and opens a GitHub Release with generated notes.
|
|
388
|
+
|
|
389
|
+
The workflow publishes over [npm trusted publishing](https://docs.npmjs.com/trusted-publishers),
|
|
390
|
+
so there is no npm token in the repository — CI exchanges a short-lived GitHub OIDC token for
|
|
391
|
+
publish rights, and every release carries a provenance attestation linking the tarball back to
|
|
392
|
+
the commit and workflow run that produced it.
|
|
393
|
+
|
|
394
|
+
## License
|
|
395
|
+
|
|
396
|
+
MIT
|