@medialane/sdk 0.159.0 → 0.161.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Medialane
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 CHANGED
@@ -2,9 +2,11 @@
2
2
 
3
3
  # @medialane/sdk
4
4
 
5
- **Framework-agnostic TypeScript SDK for the Medialane IP marketplace on Starknet**
5
+ **Build on Medialane: the open rails for creators, collectors and creator capital markets.**
6
6
 
7
- The Medialane SDK provides a unified interface for interacting with the Medialane marketplace: both **on-chain operations** (create listings, make offers, fulfill orders, mint IP assets) and **REST API access** (search tokens, manage orders, upload metadata to IPFS). Built for [medialane.io](https://medialane.io), [starknet.medialane.io](https://starknet.medialane.io), [portal.medialane.io](https://portal.medialane.io), and `media-wallet`.
7
+ Everything the Medialane apps can do, your app or AI agent can do too. The SDK covers both **onchain operations** (list, offer, buy, cancel, mint and deploy collections) and the **Medialane API** (assets, collections, orders, activity, search, creator profiles and metadata uploads). It is the same SDK that powers [medialane.io](https://medialane.io), [starknet.medialane.io](https://starknet.medialane.io) and [portal.medialane.io](https://portal.medialane.io).
8
+
9
+ Framework-agnostic TypeScript, designed for many chains, live today on Starknet mainnet.
8
10
 
9
11
  ---
10
12
 
@@ -25,8 +27,7 @@ The Medialane SDK provides a unified interface for interacting with the Medialan
25
27
  - Query orders, tokens, collections, and activities
26
28
  - Full-text search across the marketplace
27
29
  - Intent-based transaction orchestration
28
- - Upload metadata and files to IPFS (Pinata)
29
- - Portal: API keys, credits, usage
30
+ - Upload metadata and files to IPFS
30
31
  - ERC-1155 multi-holder ownership via `token.balances`
31
32
 
32
33
  **IP Metadata Types**
@@ -61,166 +62,62 @@ yarn add @medialane/sdk starknet
61
62
  ### Initialize the Client
62
63
 
63
64
  ```typescript
64
- import { MedialaneClient } from "@medialane/sdk";
65
+ import { MedialaneClient } from "@medialane/sdk/starknet";
65
66
 
66
67
  const client = new MedialaneClient({
67
68
  chain: "STARKNET", // chain-scoped (default "STARKNET"); replaces `network` (v0.37.0)
68
69
  rpcUrl: "https://rpc.starknet.lava.build", // optional; defaults to the chain's registry rpcUrl
69
- backendUrl: "https://medialane-backend-production.up.railway.app", // required for .api methods
70
+ backendUrl: "https://api.medialane.io", // required for .api methods
70
71
  apiKey: "ml_live_...", // from Medialane Portal
71
72
  });
72
73
  ```
73
74
 
74
75
  ---
75
76
 
76
- ## Marketplace Operations (On-Chain)
77
-
78
- All methods require a `starknet.js` `AccountInterface`. SNIP-12 signing and `waitForTransaction` are handled automatically. Fulfilment is **unsigned**: the caller is the fulfiller, so there is no `fulfiller`/`offerer` field to pass; cancellation still signs, but without a nonce (a per-offerer `counter` replaces it, see `incrementCounter`).
79
-
80
- Two marketplace modules are available:
81
- - `client.marketplace`: ERC-721 marketplace (`Medialane721`)
82
- - `client.marketplace1155`: ERC-1155 marketplace (`Medialane1155`)
77
+ ## Onchain actions
83
78
 
84
- ### Create a Listing (ERC-721)
79
+ Every onchain action (list, offer, buy, cancel, checkout, mint, deploy a collection, launch a coin, sponsor) follows the same flow. You ask the API for an **intent**, sign it if it needs a signature, and execute the resulting calls from the user's own account. Nothing is ever signed or sent on the user's behalf.
85
80
 
86
81
  ```typescript
87
- import { Account } from "starknet";
82
+ import { executeIntent } from "@medialane/sdk/starknet";
83
+ import { RpcProvider, Account, stark } from "starknet";
88
84
 
89
- const result = await client.marketplace.createListing(account, {
90
- nftContract: "0x05e73b7...",
91
- tokenId: "42",
92
- currency: "USDC",
93
- price: "1000000", // 1 USDC (6 decimals)
94
- durationSeconds: 86400 * 30, // 30 days
95
- });
96
- console.log("Listed:", result.txHash);
97
- ```
85
+ const provider = new RpcProvider({ nodeUrl: "https://..." });
86
+ const account: Account = /* the user's account */;
98
87
 
99
- ### Make an Offer
88
+ const signer = {
89
+ address: account.address,
90
+ signTypedData: async (data) => stark.formatSignature(await account.signMessage(data)),
91
+ execute: async (calls) => ({ txHash: (await account.execute(calls)).transaction_hash }),
92
+ };
100
93
 
101
- ```typescript
102
- const result = await client.marketplace.makeOffer(account, {
103
- nftContract: "0x05e73b7...",
94
+ const { data: intent } = await client.api.createListingIntent({
95
+ offerer: account.address,
96
+ nftContract: "0x...",
104
97
  tokenId: "42",
105
- currency: "USDC",
106
- price: "500000", // 0.5 USDC
107
- durationSeconds: 86400 * 7,
108
- });
109
- ```
110
-
111
- ### Fulfill an Order
112
-
113
- ```typescript
114
- // Fetch order details first to get paymentToken and totalPrice
115
- const details = await client.api.getOrder(orderHash);
116
-
117
- const result = await client.marketplace.fulfillOrder(account, {
118
- orderHash: "0x...",
119
- paymentToken: "0x033068...", // from order details
120
- totalPrice: "1000000", // raw token units
121
- });
122
- ```
123
-
124
- ### Cart Checkout (Multiple Items)
125
-
126
- ```typescript
127
- const result = await client.marketplace.checkoutCart(account, [
128
- { orderHash: "0x...", considerationToken: "0x033068...", considerationAmount: "1000000" },
129
- { orderHash: "0x...", considerationToken: "0x033068...", considerationAmount: "500000" },
130
- ]);
131
- ```
132
-
133
- ### Cancel an Order
134
-
135
- ```typescript
136
- const result = await client.marketplace.cancelOrder(account, {
137
- orderHash: "0x...",
138
- });
139
- ```
140
-
141
- ### Bulk-Cancel (Invalidate All Open Orders)
142
-
143
- ```typescript
144
- // Bumps the caller's counter: every previously-registered order becomes unfulfillable.
145
- await client.marketplace.incrementCounter(account);
146
- ```
147
-
148
- ### Mint an IP Asset
149
-
150
- ```typescript
151
- const result = await client.marketplace.mint(account, {
152
- collectionId: "1", // collection ID on the registry
153
- recipient: account.address,
154
- tokenUri: "ipfs://...", // IPFS URI of the metadata JSON
155
- royaltyBps: 500, // EIP-2981 secondary-sale royalty, 0-10_000 (required since MIP v0.4.0)
156
- });
157
- ```
158
-
159
- ### Deploy a Collection
160
-
161
- ```typescript
162
- const result = await client.marketplace.createCollection(account, {
163
- name: "My Creative Works",
164
- symbol: "MCW",
165
- baseUri: "",
166
- });
167
- ```
168
-
169
- ---
170
-
171
- ## ERC-1155 Marketplace (Medialane1155)
172
-
173
- For IP assets from ERC-1155 collections (e.g. IP-Programmable-ERC1155-Collections). Contract address: read `getCoordinates("STARKNET").marketplace1155` from `src/chains.ts`, the single source of truth across redeploys.
174
-
175
- ### Create an ERC-1155 Listing
176
-
177
- ```typescript
178
- const result = await client.marketplace1155.createListing(account, {
179
- nftContract: "0x...", // ERC-1155 collection address
180
- tokenId: "1",
181
- amount: "10", // number of tokens to sell
182
- pricePerUnit: "1", // human-readable price per token (e.g. "1" USDC)
183
- currency: "USDC",
184
- durationSeconds: 86400 * 30,
185
- });
186
- ```
187
-
188
- `set_approval_for_all` is granted automatically if not already in place.
189
-
190
- ### Fulfill an ERC-1155 Order
191
-
192
- ```typescript
193
- // Fetch order details first to get paymentToken and totalPrice
194
- const details = await client.api.getOrder(orderHash);
195
-
196
- const result = await client.marketplace1155.fulfillOrder(account, {
197
- orderHash: "0x...",
198
- paymentToken: "0x033068...", // from order details
199
- totalPrice: "10000000", // pricePerUnit × amount in raw token units
98
+ currency: "0x033068...", // USDC
99
+ price: "1000000", // 1 USDC, in the token's smallest unit
100
+ endTime: Math.floor(Date.now() / 1000) + 86400 * 30,
200
101
  });
201
- ```
202
-
203
- ERC-2981 royalties are automatically deducted by the contract at fulfillment.
204
-
205
- ### Cancel an ERC-1155 Order
206
102
 
207
- ```typescript
208
- const result = await client.marketplace1155.cancelOrder(account, {
209
- orderHash: "0x...",
210
- });
103
+ const { txHash } = await executeIntent(provider, signer, client, intent);
211
104
  ```
212
105
 
213
- ### SNIP-12 Typed Data Builders (custodial-wallet / custom flows)
106
+ The same pattern works with `createOfferIntent`, `createFulfillIntent`, `createCancelIntent`, `createCheckoutIntent`, `createMintIntent`, `createCollectionIntent`, `createCoinIntent` and the sponsorship intents.
214
107
 
215
- Listing/offer and cancellation are signed; fulfilment is an **unsigned** call (the buyer is
216
- the fulfiller, since v0.26.0): there is no fulfillment typed-data builder.
108
+ ### Launchpad services
217
109
 
218
- ```typescript
219
- import { build1155OrderTypedData, build1155CancellationTypedData } from "@medialane/sdk";
220
- import { constants } from "starknet";
110
+ Launchpad services are available under `client.services`:
221
111
 
222
- const typedData = build1155OrderTypedData(orderParams, constants.StarknetChainId.SN_MAIN);
223
- ```
112
+ | Service | What it does |
113
+ |---|---|
114
+ | `client.services.pop` | Proof of participation badges: create, claim, issue, burn |
115
+ | `client.services.drop` | Timed collection drops |
116
+ | `client.services.erc1155Collection` | Limited editions collections |
117
+ | `client.services.creatorCoin` | Creator coins with a public trading pool |
118
+ | `client.services.ticket` | IP Tickets |
119
+ | `client.services.club` | IP Club memberships |
120
+ | `client.services.sponsorship` | IP Sponsorship |
224
121
 
225
122
  ---
226
123
 
@@ -330,40 +227,6 @@ const metaResult = await client.api.uploadMetadata({
330
227
  // metaResult.data.url → "ipfs://..."
331
228
  ```
332
229
 
333
- ### Intents (Advanced)
334
-
335
- The intent system handles the SNIP-12 signing flow for marketplace operations:
336
-
337
- ```typescript
338
- // 1. Create intent (gets typedData to sign)
339
- const intent = await client.api.createListingIntent({
340
- offerer: address,
341
- nftContract: "0x...",
342
- tokenId: "42",
343
- currency: "0x033068...",
344
- price: "1000000",
345
- endTime: Math.floor(Date.now() / 1000) + 86400 * 30,
346
- });
347
-
348
- // 2. Sign typedData
349
- const signature = await account.signMessage(intent.data.typedData);
350
-
351
- // 3. Submit signature
352
- await client.api.submitIntentSignature(intent.data.id, toSignatureArray(signature));
353
- ```
354
-
355
- Mint and collection intents are pre-signed: no signature step needed:
356
-
357
- ```typescript
358
- const mintIntent = await client.api.createMintIntent({
359
- owner: ownerAddress,
360
- collectionId: "1",
361
- recipient: recipientAddress,
362
- tokenUri: "ipfs://...",
363
- });
364
- // mintIntent.data.calls → ready to execute
365
- ```
366
-
367
230
  ---
368
231
 
369
232
  ## IP Metadata Types
@@ -445,11 +308,12 @@ import {
445
308
  ## Error Handling
446
309
 
447
310
  ```typescript
448
- import { MedialaneError, MedialaneApiError } from "@medialane/sdk";
311
+ import { MedialaneApiError } from "@medialane/sdk";
312
+ import { MedialaneError, executeIntent } from "@medialane/sdk/starknet";
449
313
 
450
- // On-chain errors (marketplace module)
314
+ // Onchain errors
451
315
  try {
452
- await client.marketplace.createListing(account, params);
316
+ await executeIntent(provider, signer, client, intent);
453
317
  } catch (err) {
454
318
  if (err instanceof MedialaneError) {
455
319
  console.error("On-chain error:", err.message, err.cause);
@@ -476,6 +340,7 @@ message belongs to whatever your screen was trying to do.
476
340
  | `MedialaneApiError` | The API refused the request | `err.message`, which is the reason the API gave or a plain statement of the status |
477
341
  | `UserFacingError` | The SDK got partway and stopped, for example a transaction that reverted | `err.message` |
478
342
  | `PasskeyCancelledError` | The prompt was dismissed | Nothing was submitted, so avoid wording it as a failure |
343
+ | `PasskeyUnsupportedError` | `err.reason`: `"no-webauthn"` or `"no-prf"` | Your own wording |
479
344
 
480
345
  ```typescript
481
346
  import { MedialaneApiError, PasskeyCancelledError, UserFacingError } from "@medialane/sdk";
@@ -521,18 +386,15 @@ without reaching a reader.
521
386
 
522
387
  ## Advanced: SNIP-12 Typed Data Builders
523
388
 
524
- For integrations that handle signing externally (e.g. a custodial wallet service, Cartridge Controller):
389
+ For integrations that build and sign marketplace orders themselves:
525
390
 
526
391
  ```typescript
527
392
  import {
528
393
  buildOrderTypedData,
529
- buildFulfillmentTypedData,
530
394
  buildCancellationTypedData,
531
- } from "@medialane/sdk";
532
-
533
- const typedData = buildOrderTypedData(orderParams, chainId);
534
- const signature = await account.signMessage(typedData);
535
- await client.api.submitIntentSignature(intentId, signatureArray);
395
+ build1155OrderTypedData,
396
+ build1155CancellationTypedData,
397
+ } from "@medialane/sdk/starknet";
536
398
  ```
537
399
 
538
400
  ---
@@ -555,142 +417,14 @@ Built with:
555
417
 
556
418
  ## Changelog
557
419
 
558
- > Full history in [CHANGELOG.md](./CHANGELOG.md). Highlights below.
559
-
560
- ### v0.37.0: multichain readiness (BREAKING)
561
- - **Chain is a first-class axis.** New `chains.ts` `coordinates[chain]` registry is the single source of per-chain service coordinates (`CHAINS`, `getCoordinates`, `DEFAULT_CHAIN`, `Chain`, `ChainCoordinates`); the flat `*_MAINNET` constants derive from it.
562
- - **`MedialaneConfig.chain` replaces `network`**: the client is chain-scoped; `client.network` getter → `client.chain`.
563
- - **`ServiceDefinition.onchain` is per-chain**: `Partial<Record<Chain, …>>`; read `service.onchain?.STARKNET?.factoryAddress`.
564
- - **`normalizeAddress(chain, address)`**: per-chain codec (Starknet pad / EVM EIP-55 / Solana base58; Bitcoin not yet implemented).
565
- - **Removed** `SUPPORTED_NETWORKS`, `DEFAULT_RPC_URL`, `Network` (mainnet-only: coordinates key by chain alone). `getChainId(config)` throws for non-Starknet.
566
-
567
- ### v0.6.7
568
- - **`CollectionRegistryABI`** exported from `@medialane/sdk`: minimal ABI covering `list_user_collections` and `get_collection` on the collection registry contract. Eliminates duplicated inline ABI definitions in consuming apps.
569
-
570
- ### v0.6.6
571
- - **`COLLECTION_CONTRACT_MAINNET`** updated to audited v2 contract address `0x05c49ee5d3208a2c2e150fdd0c247d1195ed9ab54fa2d5dea7a633f39e4b205b`
572
-
573
- ### v0.6.5
574
- - **ERC-1155 support**: `ApiToken.balances: ApiTokenBalance[] | null` replaces the single `owner` field for ownership checks
575
- - **`ApiTokenBalance`** type: `{ owner: string; amount: string }`: each entry represents one holder and their quantity
576
- - **`ApiToken.owner`** deprecated: always `null` after the ERC-1155 migration; use `balances` instead
577
- - **`ApiCollection.standard`**: `"ERC721" | "ERC1155" | "UNKNOWN"` detected via ERC-165 `supportsInterface`
578
- - **`totalSupply` fix**: ERC-1155 collections now report `SUM(holder amounts)` for an accurate circulating total
579
-
580
- ### v0.6.1
581
- - **Collection Drop**: new `DropService` (`client.services.drop`) with full on-chain drop management: `claim`, `adminMint`, `setClaimConditions`, `setAllowlistEnabled`, `addToAllowlist`, `batchAddToAllowlist`, `setPaused`, `withdrawPayments`, `createDrop`
582
- - **`client.api.getDropCollections(opts?)`**: list all `COLLECTION_DROP` collections
583
- - **`client.api.getDropMintStatus(collection, wallet)`**: returns `{ mintedByWallet, totalMinted }`
584
- - **`DropMintStatus`**, **`ClaimConditions`**, **`CreateDropParams`** types exported
585
- - **`DropCollectionABI`** and **`DropFactoryABI`** exported from `@medialane/sdk`
586
- - **`DROP_FACTORY_CONTRACT_MAINNET`** and **`DROP_COLLECTION_CLASS_HASH_MAINNET`** constants exported
587
- - **`CollectionSource`** union extended with `"COLLECTION_DROP"`
588
-
589
- ### v0.6.0
590
- - **POP Protocol**: `PopService` (`client.services.pop`): `claim`, `adminMint`, `addToAllowlist`, `batchAddToAllowlist`, `removeFromAllowlist`, `setTokenUri`, `setPaused`, `createCollection`
591
- - **`client.api.getPopCollections(opts?)`** and **`client.api.getPopEligibility(collection, wallet)`**
592
- - **`POPCollectionABI`** and **`POPFactoryABI`** exported
593
- - **`POP_FACTORY_CONTRACT_MAINNET`** and **`POP_COLLECTION_CLASS_HASH_MAINNET`** constants exported
594
-
595
- ### v0.5.7
596
- - **`ApiCollectionProfile.hasGatedContent: boolean`**: whether the collection has token-gated content configured
597
- - **`ApiCollectionProfile.gatedContentTitle: string | null`**: public title of gated content (shown to all users; URL is accessible to holders only via the backend gated-content endpoint)
598
-
599
- ### v0.5.5
600
- - **`extendRemixOffer(id, days, siwsToken)`**: requester extends expiry of a PENDING/AUTO_PENDING remix offer by 1–30 days (`POST /v1/remix-offers/:id/extend`)
601
- - **`ApiRemixOfferPrice`** type: `{ raw, formatted, currency, decimals }` replaces flat `proposedPrice`/`proposedCurrency` fields on `ApiRemixOffer.price` (visible to participants only)
602
-
603
- ### v0.5.4
604
- - **`ApiRemixOffer.price`** shape introduced: backend now serializes price as a structured object (`raw`, `formatted`, `currency`, `decimals`), replacing raw wei strings
605
-
606
- ### v0.5.3
607
- - **`getTokenComments(contract, tokenId, opts?)`**: fetch on-chain NFT comments for a token (`GET /v1/tokens/:contract/:tokenId/comments`)
608
- - **`ApiComment`** type: `{ id, author, content, txHash, blockNumber, blockTimestamp, isHidden, createdAt }`
609
-
610
- ### v0.5.0
611
- - **Counter-offer support**: `createCounterOfferIntent(params, siwsToken)`, `getCounterOffers(query)`, `ApiCounterOffersQuery`, `CreateCounterOfferIntentParams`
612
- - **`OrderStatus`** extended with `"COUNTER_OFFERED"`; **`IntentType`** with `"COUNTER_OFFER"`
613
- - **`ApiOrder`** extended: `parentOrderHash?: string | null`, `counterOfferMessage?: string | null`
614
- - **Remix licensing**: full set of remix offer methods and types:
615
- - `submitRemixOffer(params, siwsToken)`: custom offer
616
- - `submitAutoRemixOffer(params, siwsToken)`: auto offer for open-license tokens
617
- - `confirmSelfRemix(params, siwsToken)`: record owner self-remix
618
- - `getRemixOffers(query, siwsToken)`: list by role
619
- - `getRemixOffer(id, siwsToken?)`: single offer
620
- - `confirmRemixOffer(id, params, siwsToken)`: creator approves
621
- - `rejectRemixOffer(id, siwsToken)`: creator rejects
622
- - `getTokenRemixes(contract, tokenId, opts?)`: public remix list
623
- - **New types**: `RemixOfferStatus`, `ApiRemixOffer`, `ApiPublicRemix`, `OPEN_LICENSES`, `OpenLicense`, `CreateRemixOfferParams`, `AutoRemixOfferParams`, `ConfirmSelfRemixParams`, `ConfirmRemixOfferParams`, `ApiRemixOffersQuery`
624
-
625
- ### v0.4.8
626
- - **`ApiComment`** type + **`getTokenComments`** (patch release, backported into v0.5.3)
627
-
628
- ### v0.4.7
629
- - **`IPType`** union type exported: `"Audio" | "Art" | "Documents" | "NFT" | "Video" | "Photography" | "Patents" | "Posts" | "Publications" | "RWA" | "Software" | "Custom"`
630
-
631
- ### v0.4.6
632
- - **`ApiUserWallet`** type + `upsertMyWallet(siwsToken)` / `getMyWallet(siwsToken)` for wallet registration fallback (`POST/GET /v1/users/me`)
633
-
634
- ### v0.4.5
635
- - **`ApiSearchCreatorResult`** type + `ApiSearchResult.creators`: creator profiles now included in search results
636
-
637
- ### v0.4.4
638
- - **`ApiCreatorListResult`** + `getCreators(opts?)`: list creators with search/pagination via `GET /v1/creators`
639
-
640
- ### v0.4.3
641
- - **`ApiCreatorProfile.username`** field + `getCreatorByUsername(username)`: resolve username slug to creator profile
642
-
643
- ### v0.4.2
644
- - **WBTC** added to `SUPPORTED_TOKENS` (`0x03fe2b97c1fd336e750087d68b9b867997fd64a2661ff3ca5a7c771641e8e7ac`, 8 decimals)
645
- - **`listable` field** on every `SUPPORTED_TOKENS` entry: controls whether a token appears in listing/offer dialogs vs filter-only
646
- - **`getListableTokens()`**: returns tokens filtered to `listable: true`; exported from package root
647
- - **ETH** promoted to `listable: true`: now available in listing and offer dialogs
648
- - **USDC.e removed**: bridged USDC (`0x053c91...`) removed entirely; only Circle-native USDC remains, to avoid user confusion
649
-
650
- ### v0.4.1
651
- - **Collection claims**: `claimCollection(contractAddress, walletAddress, siwsToken)` for on-chain ownership verification; `requestCollectionClaim({ contractAddress, walletAddress?, email, notes? })` for manual review
652
- - **Collection profiles**: `getCollectionProfile(contractAddress)` and `updateCollectionProfile(contractAddress, data, siwsToken)` for enriched display metadata (displayName, description, image, bannerImage, social links)
653
- - **Creator profiles**: `getCreatorProfile(walletAddress)` and `updateCreatorProfile(walletAddress, data, siwsToken)` for creator display metadata
654
- - **New types**: `ApiCollectionClaim`, `ApiAdminCollectionClaim`, `ApiCollectionProfile`, `ApiCreatorProfile`
655
- - **`ApiCollection`** extended with `source` (`"MEDIALANE_REGISTRY" | "EXTERNAL" | "PARTNERSHIP" | "IP_TICKET" | "IP_CLUB" | "GAME"`) and `claimedBy: string | null`
656
- - `profile?: ApiCollectionProfile | null` optionally embedded on `ApiCollection` when `?include=profile`
657
-
658
- ### v0.4.0
659
- - **Typed error codes**: `MedialaneError` and `MedialaneApiError` now expose a `.code: MedialaneErrorCode` property (`"TOKEN_NOT_FOUND"` | `"RATE_LIMITED"` | `"INTENT_EXPIRED"` | `"UNAUTHORIZED"` | `"INVALID_PARAMS"` | `"NETWORK_NOT_SUPPORTED"` | `"UNKNOWN"`)
660
- - **Automatic retry**: all API requests retry up to 3 times with exponential backoff (300ms base, 5s cap) on transient failures. Configure via `retryOptions` in `MedialaneConfig`
661
- - **`RetryOptions`** type exported from index
662
- - **`CollectionSort`** named union type exported (`"recent" | "supply" | "floor" | "volume" | "name"`)
663
- - **Sepolia guard**: constructing a client with `network: "sepolia"` and no explicit contract addresses now throws `NETWORK_NOT_SUPPORTED` immediately
664
-
665
- ### v0.3.3
666
- - `getCollections(page?, limit?, isKnown?, sort?)`: added `sort` parameter: `"recent"` (default) | `"supply"` | `"floor"` | `"volume"` | `"name"`
667
- - Default sort changed from `totalSupply DESC` to `createdAt DESC` (newest first): matches backend default
668
-
669
- ### v0.3.1
670
- - `ApiCollection.collectionId: string | null`: on-chain registry numeric ID (decimal string). Required for `createMintIntent`. Populated for collections indexed after 2026-03-09.
671
-
672
- ### v0.3.0
673
- - `normalizeAddress()` applied internally before all API calls: callers no longer need to normalize Starknet addresses
674
- - `ApiCollection.owner: string | null`: populated from intent typedData or on-chain `owner()` call
675
- - `getCollectionsByOwner(owner)`: fetch collections by wallet address via `GET /v1/collections?owner=`
676
-
677
- ### v0.2.6
678
- - `ApiOrder.token: ApiOrderTokenMeta | null`: token name/image/description embedded on orders (batchTokenMeta); no per-row `getToken` calls needed
679
-
680
- ### v0.2.0
681
- - `IpAttribute` and `IpNftMetadata` interfaces for IP metadata
682
- - `ApiTokenMetadata.attributes` typed as `IpAttribute[] | null` (was `unknown`)
683
- - `ApiTokenMetadata` extended with `derivatives`, `attribution`, `territory`, `aiPolicy`, `royalty`, `registration`, `standard`
684
- - Added `USDC.e` (bridged USDC via Starkgate) to `SUPPORTED_TOKENS`
685
-
686
- ### v0.1.0
687
- - Initial release: orders, tokens, collections, activities, intents, metadata, portal
420
+ See [CHANGELOG.md](./CHANGELOG.md).
688
421
 
689
422
  ---
690
423
 
691
424
  ## Links
692
425
 
693
- - **Marketplace**: [medialane.io](https://medialane.io)
426
+ - **Medialane**: [medialane.io](https://medialane.io)
427
+ - **Docs**: [docs.medialane.io/dev](https://docs.medialane.io/dev)
694
428
  - **Starknet App**: [starknet.medialane.io](https://starknet.medialane.io)
695
429
  - **Developer Portal**: [portal.medialane.io](https://portal.medialane.io)
696
430
  - **npm**: [npmjs.com/package/@medialane/sdk](https://www.npmjs.com/package/@medialane/sdk)
@@ -1164,6 +1164,15 @@ var PasskeyCancelledError = class extends Error {
1164
1164
  this.name = "PasskeyCancelledError";
1165
1165
  }
1166
1166
  };
1167
+ var PasskeyUnsupportedError = class extends Error {
1168
+ constructor(reason) {
1169
+ super(
1170
+ reason === "no-webauthn" ? "Passkeys are not available in this environment." : "This passkey did not return a PRF secret, so it cannot seal a wallet key."
1171
+ );
1172
+ this.name = "PasskeyUnsupportedError";
1173
+ this.reason = reason;
1174
+ }
1175
+ };
1167
1176
  var encodeBase64 = (buf) => {
1168
1177
  const bytes = buf instanceof Uint8Array ? buf : new Uint8Array(buf);
1169
1178
  let binary = "";
@@ -1184,15 +1193,24 @@ function createPasskeyOwner(config) {
1184
1193
  const randomBytes = config.randomBytes ?? ((length) => crypto.getRandomValues(new Uint8Array(length)));
1185
1194
  const credentialsApi = () => {
1186
1195
  const api = config.credentials ?? (typeof navigator === "undefined" ? void 0 : navigator.credentials);
1187
- if (!api) throw new Error("Passkeys are only available in a browser.");
1196
+ if (!api) throw new PasskeyUnsupportedError("no-webauthn");
1188
1197
  return api;
1189
1198
  };
1190
- const prfUnsupportedMessage = () => {
1191
- const isBrave = typeof navigator !== "undefined" && "brave" in navigator;
1192
- const cause = isBrave ? "Brave doesn't currently support the WebAuthn PRF extension." : "This browser didn't return a passkey PRF secret.";
1193
- return `${cause} ${config.appName} needs it to seal your key. Your device passkey (Touch ID) is fine, the limitation is the browser. Please open this in Safari or Chrome on an up-to-date OS.`;
1199
+ const browserRefusesPrf = async () => {
1200
+ const read = config.clientCapabilities ?? (typeof PublicKeyCredential !== "undefined" && typeof PublicKeyCredential.getClientCapabilities === "function" ? () => PublicKeyCredential.getClientCapabilities() : void 0);
1201
+ if (!read) return false;
1202
+ try {
1203
+ return (await read())["extension:prf"] === false;
1204
+ } catch {
1205
+ return false;
1206
+ }
1207
+ };
1208
+ const ensurePrfPossible = async () => {
1209
+ credentialsApi();
1210
+ if (await browserRefusesPrf()) throw new PasskeyUnsupportedError("no-prf");
1194
1211
  };
1195
1212
  async function registerPasskey() {
1213
+ await ensurePrfPossible();
1196
1214
  let credential;
1197
1215
  try {
1198
1216
  credential = await credentialsApi().create({
@@ -1207,8 +1225,7 @@ function createPasskeyOwner(config) {
1207
1225
  ],
1208
1226
  authenticatorSelection: {
1209
1227
  residentKey: "required",
1210
- userVerification: "required",
1211
- authenticatorAttachment: "platform"
1228
+ userVerification: "required"
1212
1229
  },
1213
1230
  extensions: { prf: { eval: { first: config.prfSalt } } }
1214
1231
  }
@@ -1221,6 +1238,7 @@ function createPasskeyOwner(config) {
1221
1238
  return { credentialId: encodeBase64(credential.rawId), prfFirst: prf?.results?.first ?? null };
1222
1239
  }
1223
1240
  async function prfSecret(credentialId) {
1241
+ await ensurePrfPossible();
1224
1242
  let assertion;
1225
1243
  try {
1226
1244
  assertion = await credentialsApi().get({
@@ -1237,16 +1255,12 @@ function createPasskeyOwner(config) {
1237
1255
  throw err;
1238
1256
  }
1239
1257
  const result = assertion.getClientExtensionResults().prf?.results?.first;
1240
- if (!result) throw new Error("Passkey PRF unavailable on this device/browser.");
1258
+ if (!result) throw new PasskeyUnsupportedError("no-prf");
1241
1259
  return new Uint8Array(result);
1242
1260
  }
1243
1261
  async function secretFromRegistration(registration) {
1244
1262
  if (registration.prfFirst) return new Uint8Array(registration.prfFirst);
1245
- try {
1246
- return await prfSecret(registration.credentialId);
1247
- } catch {
1248
- throw new Error(prfUnsupportedMessage());
1249
- }
1263
+ return prfSecret(registration.credentialId);
1250
1264
  }
1251
1265
  async function seal(secret, privateKeyHex) {
1252
1266
  const aes = await deriveAesKey(secret, config.hkdfInfo);
@@ -1292,6 +1306,26 @@ function createPasskeyOwner(config) {
1292
1306
  };
1293
1307
  }
1294
1308
 
1309
+ // src/routes.ts
1310
+ var SUPPORTED_URL_CHAINS = ["STARKNET"];
1311
+ function chainSlug(chain) {
1312
+ return chain.toLowerCase();
1313
+ }
1314
+ function chainFromSlug(slug) {
1315
+ const upper = slug.toUpperCase();
1316
+ return SUPPORTED_URL_CHAINS.includes(upper) ? upper : null;
1317
+ }
1318
+ var seg = (v) => v == null ? "" : String(v);
1319
+ function assetHref(chain, contract, tokenId) {
1320
+ return `/asset/${chainSlug(chain)}/${seg(contract)}/${seg(tokenId)}`;
1321
+ }
1322
+ function collectionHref(chain, contract) {
1323
+ return `/collections/${chainSlug(chain)}/${seg(contract)}`;
1324
+ }
1325
+ function coinHref(chain, address) {
1326
+ return `/coins/${chainSlug(chain)}/${seg(address)}`;
1327
+ }
1328
+
1295
1329
  // src/services/registry.ts
1296
1330
  var SN2 = chunkH7I6TEOA_cjs.getStarknetCoordinates("STARKNET");
1297
1331
  var SERVICES = {
@@ -1769,6 +1803,7 @@ exports.InvalidStarkPrivateKeyError = InvalidStarkPrivateKeyError;
1769
1803
  exports.MedialaneApiError = MedialaneApiError;
1770
1804
  exports.POLICY_REFUSAL_CODES = POLICY_REFUSAL_CODES;
1771
1805
  exports.PasskeyCancelledError = PasskeyCancelledError;
1806
+ exports.PasskeyUnsupportedError = PasskeyUnsupportedError;
1772
1807
  exports.STARKNET_COLLECTION_1155_CLASS_HASH = STARKNET_COLLECTION_1155_CLASS_HASH;
1773
1808
  exports.STARKNET_COLLECTION_1155_CONTRACT = STARKNET_COLLECTION_1155_CONTRACT;
1774
1809
  exports.STARKNET_COLLECTION_1155_FACTORY_CLASS_HASH = STARKNET_COLLECTION_1155_FACTORY_CLASS_HASH;
@@ -1806,7 +1841,13 @@ exports.STARKNET_POP_COLLECTION_CLASS_HASH = STARKNET_POP_COLLECTION_CLASS_HASH;
1806
1841
  exports.STARKNET_POP_FACTORY_CONTRACT = STARKNET_POP_FACTORY_CONTRACT;
1807
1842
  exports.STARKNET_POP_FACTORY_START_BLOCK = STARKNET_POP_FACTORY_START_BLOCK;
1808
1843
  exports.SUPPORTED_TOKENS = SUPPORTED_TOKENS;
1844
+ exports.SUPPORTED_URL_CHAINS = SUPPORTED_URL_CHAINS;
1809
1845
  exports.UserFacingError = UserFacingError;
1846
+ exports.assetHref = assetHref;
1847
+ exports.chainFromSlug = chainFromSlug;
1848
+ exports.chainSlug = chainSlug;
1849
+ exports.coinHref = coinHref;
1850
+ exports.collectionHref = collectionHref;
1810
1851
  exports.computeAccountAddress = computeAccountAddress;
1811
1852
  exports.createFailoverFetch = createFailoverFetch;
1812
1853
  exports.createPasskeyOwner = createPasskeyOwner;
@@ -1838,5 +1879,5 @@ exports.starkKeyPairFromPrivateKey = starkKeyPairFromPrivateKey;
1838
1879
  exports.stringifyBigInts = stringifyBigInts;
1839
1880
  exports.u256ToBigInt = u256ToBigInt;
1840
1881
  exports.unsealPrivateKey = unsealPrivateKey;
1841
- //# sourceMappingURL=chunk-K5MWWVX5.cjs.map
1842
- //# sourceMappingURL=chunk-K5MWWVX5.cjs.map
1882
+ //# sourceMappingURL=chunk-GQWRNQD2.cjs.map
1883
+ //# sourceMappingURL=chunk-GQWRNQD2.cjs.map