Bandcamp
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/bandcamp—search-artist,search-album, andsearch-trackscrape the public Bandcamp search page;get-artist,get-album, andget-trackparse 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.
Resources
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):parseAlbumPagenow tries a directJSON.parseof the TralbumData blob FIRST (real Bandcamp TralbumData is valid JSON). Only on failure does a cleanup fallback run, and that fallback now PROTECTS anyscheme://value (.replace(/(^|[^:])\/\/.*$/gm, "$1")) instead of unconditionally stripping from the first//to end-of-line -- the old cleanup truncated the JSON at the first embeddedhttps://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):
parseAlbumPageandparseArtistPagenow accept an optionalwarn?: (msg: string) => voidcallback, invoked from each of the three emptycatchblocks (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 threeget-*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
timedFetchhelper (AbortController+setTimeout(..., 30_000),clearTimeoutin afinallyso it fires on success, on a thrown error, AND on every redirect hop) and routedfetchPage(per hop),getToken, andbcPostthrough it. A hung/slow upstream can no longer block a call -- and the model's lock -- indefinitely. instanceName60-char truncation collision (LOW, bandcamp-latent-bugs #6):get-artist/get-album/get-tracknow derive the written resource's instance name via a newurlResourceNamehelper: 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 aget-*method against the same URL still idempotently overwrites its own prior resource.slice()surrogate split (LOW, bandcamp-latent-bugs #7):aboutandbionow 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
globalArgumentschange. Added a no-opupgrades[]entry (fromVersion: "2026.07.31.1",toVersion: "2026.08.02.1",upgradeAttributes: (old) => old) documenting the bump. - Tests: flipped all 5 remaining
bandcamp-latent-bugspins acrossbandcamp_test.ts(contract-fixture, #3's and #6's concrete VALUE pins) andbandcamp_adversarial_test.ts(#3 x2, #4 x2, #5 x2, #6, #7 x2) to assert the FIXED behavior;bandcamp_adversarial_test.ts'smakeCtxnow captures logger calls (mirroringbandcamp_methods_test.ts's pattern). Added: a hung-upstreamFakeTime-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
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-trackused to pass the caller-suppliedurlstraight tofetch()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. AddedassertAllowedHost(), enforced insidefetchPagebefore every fetch: onlybandcamp.comor a*.bandcamp.comsubdomain is allowed (case-insensitive, one trailing dot stripped, http/https only).fetchPagenow fetches withredirect: "manual"and manually follows up to 5 redirect hops, re-validating theLocationhost 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
cachedTokensingleton was keyed only onDate.now(), never on whichclientId/clientSecretproduced it, so two swamp instances of@magistr/bandcampconfigured 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 withtokenCache, aMapkeyed 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, samerefresh_tokenbranch, 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,instanceNametruncation collision,slice()surrogate split) remain deferred/accepted and are unaffected by this change; in particular noAbortSignal/timeout was added anywhere (#5's pins still assertinit.signal === undefined). - Tests: flipped the three
bandcamp-latent-bugs #1SSRF pins and the#2token-bleed pin inbandcamp_adversarial_test.tsto 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.comrejected, real*.bandcamp.comstill succeeds). Migrated every get-artist/get-album/get-track fetch target from a*.example.comhost to*.bandcamp.comacross the adversarial/contract/methods/coverage/property suites (the allowlist now rejectsexample.com); fixture files themselves stay byte-frozen since their embeddedexample.comcontent is parsed data, never a fetch target.
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.
Maintenance version bump. No functional changes since 2026.05.25.1.
Merge pull request #5 from umag/extensions/jscad-stl-pair
extensions: add 15 more @magistr extensions + auto-discover CI
- 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