Skip to main content

Bandcamp

@magistr/bandcampv2026.08.19.1· 23d agoMODELS
01README

Bandcamp for swamp — search artists, albums, and tracks on the public catalog, and fetch full artist/album/track metadata (discography, track listings, tags, art) by URL.

Model:

  • @magistr/bandcampsearch-artist, search-album, and search-track scrape the public Bandcamp search page; get-artist, get-album, and get-track parse JSON-LD plus embedded TralbumData from a Bandcamp URL.

Optional OAuth (clientId/clientSecret) unlocks account methods: my-bands, sales-report, get-merch-details, get-orders, and update-shipped against the Bandcamp sales/merch API. The catalog search and detail methods need no credentials.

02Models1
@magistr/bandcampv2026.08.19.1extensions/models/bandcamp.ts

Resources

search(infinite)— Search results from Bandcamp
artistDetail(infinite)— Artist/band page details with discography
albumDetail(infinite)— Album/release details with track listing
bands(infinite)— Bands/labels associated with account
sales(infinite)— Sales report data
merch(infinite)— Merch details
orders(infinite)— Merch orders
shippingOrigins(infinite)— Shipping origin locations
task(infinite)— Action result
report(infinite)— Generated report info
03Previous Versions5
2026.08.02.1

2026.08.02.1

Real-fixes the remaining 5 latent bugs tracked in the local bandcamp-latent-bugs issue-lifecycle model (bugs #3-#7 of the 7 characterized in the wave-3 test backfill; #1 and #2 already shipped in 2026.07.31.1; NEVER filed to the swamp.club Lab -- see CLAUDE.md's anti-bypass rule). After this release all 7 tracked findings are resolved. No resource-schema or globalArguments change -- model identity is preserved via a no-op upgrades[] entry.

  • TralbumData //-strip corruption (MEDIUM, bandcamp-latent-bugs #3): parseAlbumPage now tries a direct JSON.parse of the TralbumData blob FIRST (real Bandcamp TralbumData is valid JSON). Only on failure does a cleanup fallback run, and that fallback now PROTECTS any scheme:// value (.replace(/(^|[^:])\/\/.*$/gm, "$1")) instead of unconditionally stripping from the first // to end-of-line -- the old cleanup truncated the JSON at the first embedded https:// value, silently losing every track. The trailing-comma cleanups are unchanged and still run on the fallback path.
  • Silent all-clear on parse failure (MEDIUM, bandcamp-latent-bugs #4): parseAlbumPage and parseArtistPage now accept an optional warn?: (msg: string) => void callback, invoked from each of the three empty catch blocks (album JSON-LD, album TralbumData, artist JSON-LD) on a GENUINE parse failure (script present, parse threw) -- never when the script is simply absent. The three get-* methods pass (m) => context.logger?.warning?.(m). The message is a fixed, generic string naming only the error's .name (e.g. SyntaxError) -- never the raw blob content or any credential.
  • No fetch timeout/backoff (MEDIUM, bandcamp-latent-bugs #5): added a timedFetch helper (AbortController + setTimeout(..., 30_000), clearTimeout in a finally so it fires on success, on a thrown error, AND on every redirect hop) and routed fetchPage (per hop), getToken, and bcPost through it. A hung/slow upstream can no longer block a call -- and the model's lock -- indefinitely.
  • instanceName 60-char truncation collision (LOW, bandcamp-latent-bugs #6): get-artist/get-album/get-track now derive the written resource's instance name via a new urlResourceName helper: a 47-char sanitized slug plus a 12-hex-char SHA-256 suffix of the FULL source URL (still <= 60 chars total). Two different URLs sharing the same first 47 sanitized characters no longer collide on the identical resource name -- each gets its own collision-resistant suffix. The same URL always hashes to the same suffix, so re-running a get-* method against the same URL still idempotently overwrites its own prior resource.
  • slice() surrogate split (LOW, bandcamp-latent-bugs #7): about and bio now truncate by CODE POINT (Array.from(x).slice(0, 500).join("")) instead of by UTF-16 code unit, so an astral character (e.g. an emoji) straddling the boundary is kept or dropped WHOLE, never split into a lone unpaired surrogate. Note the invariant changes shape: it is now "<= 500 CODE POINTS", not "<= 500 UTF-16 code units" -- for astral input the returned string's .length (code units) can exceed 500.
  • No method contract or wire body changes; no resource-schema or globalArguments change. Added a no-op upgrades[] entry (fromVersion: "2026.07.31.1", toVersion: "2026.08.02.1", upgradeAttributes: (old) => old) documenting the bump.
  • Tests: flipped all 5 remaining bandcamp-latent-bugs pins across bandcamp_test.ts (contract-fixture, #3's and #6's concrete VALUE pins) and bandcamp_adversarial_test.ts (#3 x2, #4 x2, #5 x2, #6, #7 x2) to assert the FIXED behavior; bandcamp_adversarial_test.ts's makeCtx now captures logger calls (mirroring bandcamp_methods_test.ts's pattern). Added: a hung-upstream FakeTime-driven abort test for #5; a JSON-LD parse-failure leak test and an artist-page parse-failure warning test for #4; an astral-heavy property test for #7 (additive, existing ASCII-only properties untouched). quality.yaml's header comment and this file's/ the adversarial suite's/the methods suite's/fixtures/PROVENANCE.md's "byte-frozen"/"unmodified"/"deferred" wording is updated to reflect that all 7 bugs are now fixed; fixture FILES themselves stay byte-identical.
2026.07.31.1

2026.07.31.1

Fixes the CRITICAL SSRF and HIGH cross-instance OAuth token-cache bleed tracked in the local bandcamp-latent-bugs issue-lifecycle model (bugs #1 and #2 of the 7 characterized in the wave-3 test backfill below; NEVER filed to the swamp.club Lab -- see CLAUDE.md's anti-bypass rule).

  • SSRF (CRITICAL, bandcamp-latent-bugs #1): get-artist/get-album/ get-track used to pass the caller-supplied url straight to fetch() with no host allowlist, so a link-local (169.254.169.254) or loopback (127.0.0.1) target was reached exactly like a real Bandcamp URL. Added assertAllowedHost(), enforced inside fetchPage before every fetch: only bandcamp.com or a *.bandcamp.com subdomain is allowed (case-insensitive, one trailing dot stripped, http/https only). fetchPage now fetches with redirect: "manual" and manually follows up to 5 redirect hops, re-validating the Location host against the same allowlist on every hop, so a 3xx bounce to an internal host is rejected exactly like a direct request to it. Custom-domain Bandcamp artist pages are no longer fetched -- a deliberate, accepted scope narrowing.
  • Cross-instance OAuth token-cache bleed (HIGH, bandcamp-latent-bugs #2): the module-level cachedToken singleton was keyed only on Date.now(), never on which clientId/clientSecret produced it, so two swamp instances of @magistr/bandcamp configured with DIFFERENT OAuth credentials but sharing one running swamp process silently reused each other's bearer/refresh token for up to an hour. Replaced it with tokenCache, a Map keyed on credential identity (clientId + clientSecret); a different identity now misses the cache and fetches its own token. Behavior for a single credential (the common case) is unchanged -- same key, same time-based validity check, same refresh_token branch, same write-back.
  • No method contract, resource schema, or wire body changes. Latent bugs #3-#7 (TralbumData //-strip corruption, silent parse-failure all-clear, no fetch timeout/backoff, instanceName truncation collision, slice() surrogate split) remain deferred/accepted and are unaffected by this change; in particular no AbortSignal/timeout was added anywhere (#5's pins still assert init.signal === undefined).
  • Tests: flipped the three bandcamp-latent-bugs #1 SSRF pins and the #2 token-bleed pin in bandcamp_adversarial_test.ts to assert the FIXED behavior (rejection + zero fetches for internal targets; two distinct per-credential token fetches); added a redirect-revalidation SSRF test and a positive allowlist test (example.com rejected, real *.bandcamp.com still succeeds). Migrated every get-artist/get-album/get-track fetch target from a *.example.com host to *.bandcamp.com across the adversarial/contract/methods/coverage/property suites (the allowlist now rejects example.com); fixture files themselves stay byte-frozen since their embedded example.com content is parsed data, never a fetch target.
2026.07.16.2

Release 2026.07.16.2 — align model versions with manifests

Maintenance release across the @magistr extensions. For most packages this carries no functional change: the only edit is the model's version: field, brought back in line with its manifest version so the published model type version and the package version no longer drift.

Functional changes in this release are limited to:

  • anime-cron: normalizeTitle now strips a ": subtitle" suffix and a trailing parenthesized year before comparison, fixing dedup false-misses where the torrent title carries a subtitle or year that the AniList romaji does not.

  • arckit: first publish. Standalone ArcKit port — a 12-phase architecture governance state machine with 65 bundled templates, driven by a bundled skill.

Also tracks three extensions (kaiten, observability-agent, music-library) that previously existed only as untracked working-tree directories, recovered from stashes.

2026.07.16.1

Maintenance version bump. No functional changes since 2026.05.25.1.

2026.05.25.1

Merge pull request #5 from umag/extensions/jscad-stl-pair

extensions: add 15 more @magistr extensions + auto-discover CI

04Stats
A
100 / 100
Downloads
2
Archive size
126.1 KB
  • Has README or module doc2/2earned
  • README has a code example1/1earned
  • README is substantive1/1earned
  • Most symbols documented1/1earned
  • No slow types (deprecated)1/1earned
  • Dependencies pass trust audit2/2earned
  • Has description1/1earned
  • Platform support declared (or universal)2/2earned
  • License declared1/1earned
  • Verified public repository2/2earned
05Platforms
06Labels