{"version":3,"sources":["/home/kyle/code/cosmo-ts/dist/index.cjs","../src/api/artist.ts","../src/errors.ts","../src/api/base-api.ts","../src/api/auth.ts","../src/api/grid.ts","../src/api/news.ts","../src/api/objekt.ts","../src/api/rewards.ts","../src/api/season.ts","../src/api/user.ts","../src/client.ts","../src/zod/artist.ts","../src/zod/auth.ts","../src/zod/news.ts","../src/zod/objekt.ts"],"names":[],"mappings":"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","file":"/home/kyle/code/cosmo-ts/dist/index.cjs","sourcesContent":[null,"import { randomUUID } from \"crypto\";\nimport { BaseAPI } from \"./base-api\";\nimport { AccessTokenMissing } from \"../errors\";\n\nexport class ArtistAPI extends BaseAPI {\n  /**\n   * Get the list of artists available.\n   *\n   * Authentication is not required.\n   */\n  async list() {\n    const response = await this.request<{\n      artists: Artist.Artist[];\n    }>(\"/artist/v1\");\n\n    if (!response.artists) {\n      return [];\n    }\n\n    return response.artists;\n  }\n\n  /**\n   * Get a single artist.\n   *\n   * Authentication is not required.\n   */\n  async get(artist: ValidArtist) {\n    return await this.request<{\n      artist: Artist.ArtistWithMembers;\n    }>(`/artist/v1/${artist}`).then((res) => res.artist);\n  }\n\n  /**\n   * Get a single artist from the backend for frontend endpoint.\n   *\n   * Authentication is required.\n   */\n  async bffGet(artist: ValidArtist) {\n    if (!this.config.accessToken) {\n      throw new AccessTokenMissing();\n    }\n\n    const params = new URLSearchParams({\n      artistName: artist,\n      tid: randomUUID(),\n    });\n\n    return await this.request<{\n      artist: Artist.BFFArtist;\n    }>(`/bff/v1/artist?${params.toString()}`);\n  }\n}\n\n/**\n * The list of artists available.\n *\n * `(string & {})` allows any string through while giving typesafety on known artists.\n */\nexport const validArtists = [\"tripleS\", \"ARTMS\"] as const;\nexport type ValidArtist = (typeof validArtists)[number] | (string & {});\n\nexport namespace Artist {\n  export type Artist = {\n    name: string;\n    title: string;\n    fandomName: string;\n    logoImageUrl: string;\n    contracts: {\n      Como: string;\n      Objekt: string;\n      ObjektMinter: string;\n      Governor: string;\n      CommunityPool: string;\n      ComoMinter: string;\n    };\n  };\n\n  export type Member = {\n    id: number;\n    name: string;\n    artist: ValidArtist;\n    units: string[];\n    alias: string;\n    profileImageUrl: string;\n    mainObjektImageUrl: string;\n    order: number;\n  };\n\n  export type ArtistWithMembers = Artist & {\n    members: Member[];\n  };\n\n  export type BFFArtist = Artist & {\n    createdAt: string;\n    createdBy: string;\n    updatedAt: string;\n    activeAt: string;\n    order: number;\n  };\n}\n","export type CosmoErrorResponse = {\n  error: {\n    message: string;\n    details?: string;\n    name: string;\n    payload: {};\n    status: number;\n  };\n};\n\nexport type CosmoBFFErrorResponse = {\n  code: number;\n  message: string;\n};\n\nexport class CosmoError extends Error {\n  constructor(public code: number, message: string) {\n    super(message);\n  }\n}\n\nexport class BadRequestError extends CosmoError {\n  constructor(message?: string) {\n    super(404, message ?? \"bad request\");\n  }\n}\n\nexport class UnauthorizedError extends CosmoError {\n  constructor(message?: string) {\n    super(401, message ?? \"unauthorized\");\n  }\n}\n\nexport class TokenExpiredError extends CosmoError {\n  constructor(message?: string) {\n    super(403, message ?? \"token expired\");\n  }\n}\n\nexport class NotFoundError extends CosmoError {\n  constructor(message?: string) {\n    super(404, message ?? \"not found\");\n  }\n}\n\nexport class AccessTokenMissing extends CosmoError {\n  constructor() {\n    super(401, \"A valid access token is required for this endpoint.\");\n  }\n}\n","import { Config } from \"../config\";\nimport {\n  BadRequestError,\n  CosmoBFFErrorResponse,\n  CosmoError,\n  CosmoErrorResponse,\n  NotFoundError,\n  TokenExpiredError,\n  UnauthorizedError,\n} from \"../errors\";\n\nexport class BaseAPI {\n  constructor(protected config: Config) {}\n\n  /**\n   * Make a request to the API.\n   */\n  async request<T>(url: string, options: RequestInit = {}) {\n    const response = await fetch(new URL(url, this.config.baseUrl), {\n      ...options,\n      headers: {\n        ...options.headers,\n        ...(this.config.accessToken\n          ? { Authorization: `Bearer ${this.config.accessToken}` }\n          : {}),\n        ...(this.config.userAgent\n          ? { \"User-Agent\": this.config.userAgent }\n          : {}),\n      },\n    });\n\n    if (response.ok === false) {\n      try {\n        if (url.includes(\"/bff/v1/\")) {\n          const body: CosmoBFFErrorResponse = await response.json();\n\n          switch (response.status) {\n            case 400:\n              throw new BadRequestError(body.message);\n            case 401:\n              throw new UnauthorizedError(body.message);\n            case 403:\n              throw new TokenExpiredError(body.message);\n            case 404:\n              throw new NotFoundError(body.message);\n          }\n        } else {\n          const body: CosmoErrorResponse = await response.json();\n\n          switch (response.status) {\n            case 400:\n              throw new BadRequestError(body.error.message);\n            case 401:\n              throw new UnauthorizedError(body.error.details);\n            case 403:\n              throw new TokenExpiredError(body.error.message);\n            case 404:\n              throw new NotFoundError(body.error.message);\n          }\n        }\n      } catch (err) {\n        if (err instanceof CosmoError) {\n          throw err;\n        }\n\n        throw new CosmoError(response.status, \"unknown error\");\n      }\n    }\n\n    // probably shouldn't do this\n    if (response.status === 204) {\n      return true as T;\n    }\n\n    return response.json() as T;\n  }\n}\n","import { z } from \"zod\";\nimport { BaseAPI } from \"./base-api\";\nimport { signInSchema } from \"../zod/auth\";\n\nexport class AuthAPI extends BaseAPI {\n  /**\n   * Sign in with Ramper credentials.\n   *\n   * Authentication is not required.\n   */\n  async signIn(payload: Auth.LoginPayload) {\n    return await this.request<Auth.LoginResult>(\"/auth/v1/signin\", {\n      method: \"POST\",\n      body: JSON.stringify(payload),\n    });\n  }\n\n  /**\n   * Refresh an access token.\n   *\n   * Authentication is not required.\n   */\n  async refreshToken(refreshToken: string) {\n    return await this.request<Auth.RefreshResult>(\"/auth/v1/refresh\", {\n      method: \"POST\",\n      body: JSON.stringify({ refreshToken }),\n    });\n  }\n}\n\nexport namespace Auth {\n  export type LoginPayload = z.infer<typeof signInSchema>;\n\n  export type LoginResult = {\n    user: {\n      id: number;\n      email: string;\n      nickname: string;\n      address: string;\n      profileImageUrl: string;\n    };\n    credentials: {\n      accessToken: string;\n      refreshToken: string;\n    };\n  };\n\n  export type RefreshResult = {\n    credentials: {\n      refreshToken: string;\n      accessToken: string;\n    };\n  };\n}\n","import { BaseAPI } from \"./base-api\";\nimport { AccessTokenMissing, BadRequestError, CosmoError } from \"../errors\";\nimport { ValidArtist } from \"./artist\";\n\nexport class GridAPI extends BaseAPI {\n  /**\n   * Get the grid status/counts for the given artist.\n   *\n   * Authentication is required.\n   */\n  async artistStatus(artist: ValidArtist) {\n    if (!this.config.accessToken) {\n      throw new AccessTokenMissing();\n    }\n\n    return await this.request<Grid.Status>(`/grid/v3/${artist}/status`);\n  }\n\n  /**\n   * Get the list of editions for the given artist.\n   *\n   * Authentication is required.\n   */\n  async editions(artist: ValidArtist) {\n    if (!this.config.accessToken) {\n      throw new AccessTokenMissing();\n    }\n\n    return await this.request<{ editions: Grid.Edition[] }>(\n      `/grid/v3/${artist}/edition`\n    ).then((res) => res.editions);\n  }\n\n  /**\n   * Get the required objekts and rewards for the given edition.\n   *\n   * Authentication is required.\n   */\n  async editionGrids(edition: string) {\n    if (!this.config.accessToken) {\n      throw new AccessTokenMissing();\n    }\n\n    return await this.request<{ grids: Grid.Grid[] }>(\n      `/grid/v2/edition/${edition}`\n    ).then((res) => res.grids);\n  }\n\n  /**\n   * Get the current state of the given grid.\n   *\n   * Authentication is required.\n   */\n  async gridStatus(gridId: string) {\n    if (!this.config.accessToken) {\n      throw new AccessTokenMissing();\n    }\n\n    return await this.request<{ grids: Grid.OngoingGrid }>(\n      `/grid/v1/${gridId}/status`\n    );\n  }\n\n  /**\n   * Complete a grid.\n   *\n   * Authentication is required.\n   */\n  async complete(gridId: string, slots: Grid.SlotCompletion[]) {\n    if (!this.config.accessToken) {\n      throw new AccessTokenMissing();\n    }\n\n    // check for duplicate slot numbers\n    const objektNos = new Set(slots.map((s) => s.no));\n    if (objektNos.size !== slots.length) {\n      throw new Grid.DuplicateSlotError();\n    }\n\n    // check for duplicate tokenIds\n    const tokenIds = new Set(slots.map((s) => s.tokenIdToUse));\n    if (tokenIds.size !== slots.length) {\n      throw new Grid.DuplicateTokenError();\n    }\n\n    // check for lengths\n    if (slots.length !== 4 && slots.length !== 8) {\n      throw new Grid.InvalidSlotLengthError();\n    }\n\n    return await this.request<boolean>(`/grid/v1/${gridId}/complete`, {\n      method: \"POST\",\n      body: JSON.stringify({ slots }),\n    }).then(() => true);\n  }\n\n  /**\n   * Claim a grid reward.\n   * Must be called after {@link complete}.\n   *\n   * Authentication is required.\n   */\n  async claim(gridId: string) {\n    if (!this.config.accessToken) {\n      throw new AccessTokenMissing();\n    }\n\n    return await this.request<Grid.RewardClaim>(\n      `/grid/v1/${gridId}/claim-reward`,\n      {\n        method: \"POST\",\n      }\n    ).catch((err) => {\n      if (err instanceof BadRequestError) {\n        throw new Grid.NotCompletedError(gridId);\n      }\n      throw err;\n    });\n  }\n}\n\nexport namespace Grid {\n  export type Status = {\n    totalCompletedGrids: number;\n    totalSpecialObjekts: number;\n  };\n\n  export type Edition = {\n    id: string;\n    artist: ValidArtist;\n    title: string;\n    subtitle: string;\n    image: string;\n    order: number;\n    createdAt: string;\n    status: {\n      totalGrids: number;\n      completedGrids: number;\n    };\n    season: {\n      createdAt: string;\n      createdBy: string;\n      updatedAt: string;\n      activeAt: string;\n      order: number;\n      id: number;\n      artist: ValidArtist;\n      title: string;\n      image: string | null;\n      startDate: string;\n      endDate: string;\n      ongoing: boolean;\n    };\n  };\n\n  export type GridSlot = {\n    no: number;\n    collections: string[];\n  };\n\n  export type GridReward = {\n    collectionId: string;\n  };\n\n  export type Grid = {\n    id: string;\n    member: string;\n    memberImage: string;\n    class: \"First\";\n    edition: {\n      createdAt: string;\n      createdBy: string;\n      updatedAt: string;\n      activeAt: string;\n      order: number;\n      id: string;\n      artist: ValidArtist;\n      title: string;\n      subtitle: string;\n      image: string;\n      season: {\n        createdAt: string;\n        createdBy: string;\n        updatedAt: string;\n        activeAt: string;\n        order: number;\n        id: number;\n        artist: ValidArtist;\n        title: string;\n        image: string | null;\n        startDate: string;\n        endDate: string | null;\n      };\n    };\n    slots: GridSlot[];\n    reward: GridReward[];\n  };\n\n  export type SlotObjekt = {\n    collectionId: string;\n    season: string;\n    member: string;\n    collectionNo: string;\n    class: string;\n    artists: ValidArtist[];\n    thumbnailImage: string;\n    frontImage: string;\n    backImage: string;\n    accentColor: string;\n    backgroundColor: string;\n    textColor: string;\n    comoAmount: number;\n    transferableByDefault: boolean;\n    tokenId: string;\n    tokenAddress: string;\n    objektNo: number;\n    transferable: boolean;\n  };\n\n  export type OngoingSlotWithoutPreferred = {\n    no: number;\n    completed: false;\n    preferredObjekt: undefined;\n  };\n\n  export type OngoingSlotWithPreferred = {\n    no: number;\n    completed: false;\n    preferredObjekt: SlotObjekt;\n  };\n\n  export type OngoingSlot =\n    | OngoingSlotWithoutPreferred\n    | OngoingSlotWithPreferred;\n\n  export type OngoingGrid = {\n    ongoing: {\n      id: number;\n      slotStatuses: OngoingSlot[];\n      allCompleted: boolean;\n      rewardClaimed: boolean;\n    };\n    ownedRewardObjektCount: number;\n  };\n\n  export type SlotCompletion = {\n    no: number;\n    tokenIdToUse: string;\n  };\n\n  export type RewardClaim = {\n    objekt: {\n      collectionId: string;\n      season: string;\n      member: string;\n      collectionNo: string;\n      class: \"Special\";\n      artists: ValidArtist[];\n      thumbnailImage: string;\n      frontImage: string;\n      backImage: string;\n      accentColor: string;\n      backgroundColor: string;\n      textColor: string;\n      comoAmount: number;\n      transferableByDefault: boolean;\n      tokenId: string;\n      tokenAddress: string;\n      objektNo: number;\n      transferable: boolean;\n    };\n    transaction: {\n      txId: string;\n      chainId: string;\n      ref: string;\n    };\n  };\n\n  export class DuplicateSlotError extends CosmoError {\n    constructor() {\n      super(400, `duplicate objekt slots in use`);\n    }\n  }\n\n  export class DuplicateTokenError extends CosmoError {\n    constructor() {\n      super(400, `duplicate token ids in use`);\n    }\n  }\n\n  export class InvalidSlotLengthError extends CosmoError {\n    constructor() {\n      super(400, `grids must have 4 or 8 slots`);\n    }\n  }\n\n  export class NotCompletedError extends CosmoError {\n    constructor(public gridId: string) {\n      super(400, `grid \"${gridId}\" has not been completed yet`);\n    }\n  }\n}\n","import { z } from \"zod\";\nimport { Artist, ValidArtist } from \"./artist\";\nimport { BaseAPI } from \"./base-api\";\nimport { bffNewsFeedSchema, newsFeedSchema } from \"../zod/news\";\nimport { AccessTokenMissing } from \"../errors\";\nimport { randomUUID } from \"crypto\";\n\nexport class NewsAPI extends BaseAPI {\n  /**\n   * Get home page news sections for the given artist.\n   *\n   * Authentication is required.\n   */\n  async home(artist: ValidArtist) {\n    if (!this.config.accessToken) {\n      throw new AccessTokenMissing();\n    }\n\n    return await this.request<{ sections: News.Section[] }>(\n      `/news/v1?artist=${artist}`\n    ).then((res) => res.sections);\n  }\n\n  /**\n   * Get the news feed for the given artist.\n   *\n   * Authentication is not required.\n   */\n  async feed({ artist, startAfter = 0, limit = 10 }: News.Payload) {\n    const params = new URLSearchParams({\n      artist: artist,\n      start_after: startAfter.toString(),\n      limit: limit.toString(),\n    });\n\n    return await this.request<News.FeedResult<News.SectionFeedContent>>(\n      `/news/v1/feed?${params.toString()}`\n    );\n  }\n\n  /**\n   * Get the exclusive news feed for the given artist.\n   *\n   * Authentication is not required.\n   */\n  async exclusive({ artist, startAfter = 0, limit = 10 }: News.Payload) {\n    const params = new URLSearchParams({\n      artist,\n      start_after: startAfter.toString(),\n      limit: limit.toString(),\n    });\n\n    return await this.request<News.FeedResult<News.SectionExclusiveContent>>(\n      `/news/v1/exclusive?${params.toString()}`\n    );\n  }\n\n  /**\n   * Get the news feed for the given artist via the backend for frontend endpoint.\n   *\n   * Authentication is required.\n   */\n  async feedBff({ artistName, page = 1, size = 10 }: NewsBFF.Payload) {\n    if (!this.config.accessToken) {\n      throw new AccessTokenMissing();\n    }\n\n    const params = new URLSearchParams({\n      artistName: artistName,\n      tid: randomUUID(),\n      page: page.toString(),\n      size: size.toString(),\n    });\n\n    return await this.request<NewsBFF.FeedResult<NewsBFF.FeedItem>>(\n      `/bff/v1/news/feed?${params.toString()}`\n    );\n  }\n}\n\nexport namespace News {\n  export type Payload = z.infer<typeof newsFeedSchema>;\n\n  export type SectionBar = {\n    type: \"bar\";\n    artist: ValidArtist;\n    contents: [];\n  };\n\n  export type SectionBanner = {\n    type: \"banner\";\n    artist: ValidArtist;\n    contents: SectionBannerContent[];\n  };\n  export type SectionBannerContent = {\n    id: number;\n    url: string;\n    createdAt: string;\n    label: \"release\" | \"event\" | \"notice\";\n    order: number;\n    body: string;\n    imageUrl: string;\n  };\n\n  export type SectionFeed = {\n    type: \"feed\";\n    artist: ValidArtist;\n    title: string;\n    contents: SectionFeedContent[];\n  };\n  export type SectionFeedContent = {\n    id: number;\n    url: string;\n    createdAt: string;\n    artist: ValidArtist;\n    logoImageUrl: string;\n    body: string;\n    imageUrls: string[];\n  };\n\n  export type SectionExclusive = {\n    type: \"exclusive\";\n    artist: ValidArtist;\n    title: string;\n    contents: SectionExclusiveContent[];\n  };\n  export type SectionExclusiveContent = {\n    id: number;\n    url: string;\n    createdAt: string;\n    title: string;\n    body: string;\n    thumbnailImageUrl: string;\n    nativeVideoUrl: string;\n  };\n\n  export type Section =\n    | SectionBar\n    | SectionBanner\n    | SectionFeed\n    | SectionExclusive;\n\n  export type FeedResult<TPostType> = {\n    hasNext: boolean;\n    total: number;\n    nextStartAfter: string;\n    results: TPostType[];\n  };\n}\n\nexport namespace NewsBFF {\n  export type Payload = z.infer<typeof bffNewsFeedSchema>;\n\n  export type FeedResult<TPostType> = {\n    count: number;\n    sets: TPostType[];\n  };\n\n  type FeedItemImage = {\n    thumbnail: string;\n    original: string;\n  };\n\n  export type FeedItem = {\n    data: {\n      activeAt: string;\n      updatedAt: string;\n      createdAt: string;\n      artistMembers: unknown[];\n      artist: string;\n      body: string;\n      id: number;\n      totalLikeCount: number;\n      url: string;\n    };\n    artist: Artist.Artist;\n    images: FeedItemImage[];\n    isLiked: boolean;\n  };\n}\n","import { z } from \"zod\";\nimport { BaseAPI } from \"./base-api\";\nimport { objektFilterSchema } from \"../zod/objekt\";\nimport { AccessTokenMissing } from \"../errors\";\n\nexport class ObjektAPI extends BaseAPI {\n  /**\n   * Get the available objekt filters.\n   *\n   * Authentication is required.\n   */\n  async filters() {\n    if (!this.config.accessToken) {\n      throw new AccessTokenMissing();\n    }\n\n    return await this.request<{\n      sorts: Filters.Sort[];\n      filters: Filters.Filter[];\n    }>(`/objekt/v2/filters`);\n  }\n\n  /**\n   * Get an objekt by its QR/odmq code.\n   *\n   * Authentication is required.\n   */\n  async getBySerial(serial: string) {\n    if (!this.config.accessToken) {\n      throw new AccessTokenMissing();\n    }\n\n    return await this.request<{\n      objekt: Objekt.Objekt;\n      isClaimed: boolean;\n    }>(`/objekt/v1/by-serial/${serial}`);\n  }\n\n  /**\n   * Claim an objekt by its QR/odmq code.\n   *\n   * Authentication is required.\n   */\n  async claimBySerial(serial: string) {\n    if (!this.config.accessToken) {\n      throw new AccessTokenMissing();\n    }\n\n    throw new Error(\"not implemented\");\n    return await this.request<boolean>(`/objekt/v1/by-serial/${serial}/claim`, {\n      method: \"POST\",\n    });\n  }\n\n  /**\n   * Get the objekts owned by the given address.\n   *\n   * Authentication is not required.\n   */\n  async ownedBy(address: Objekt.OwnedBy, filters?: Objekt.CollectionParams) {\n    throw new Error(\"not implemented\");\n    const params = toSearchParams(\n      filters ?? {\n        sort: \"newest\",\n        season: [],\n        class: [],\n        on_offline: [],\n      }\n    );\n\n    return await this.request(\n      `/objekt/v1/owned-by/${address}?${params.toString()}`\n    );\n  }\n\n  /**\n   * Get a single token by its ID.\n   *\n   * Authentication is not required.\n   */\n  async token(tokenId: Objekt.TokenId) {\n    return await this.request<Objekt.Token>(`/objekt/v1/token/${tokenId}`);\n  }\n\n  /**\n   * Put two tokens into a lenticular pair.\n   *\n   * Authentication is required.\n   */\n  async applyLenticular(tokenA: Objekt.TokenId, tokenB: Objekt.TokenId) {\n    if (!this.config.accessToken) {\n      throw new AccessTokenMissing();\n    }\n\n    throw new Error(\"not implemented\");\n    return await this.request<boolean>(`/lenticular/v1`, {\n      method: \"POST\",\n      body: JSON.stringify({\n        pairATokenId: tokenA,\n        pairBTokenId: tokenB,\n      }),\n    });\n  }\n\n  /**\n   * Remove a token from a lenticular pair.\n   *\n   * Authentication is required.\n   */\n  async removeLenticular(tokenId: Objekt.TokenId) {\n    if (!this.config.accessToken) {\n      throw new AccessTokenMissing();\n    }\n\n    throw new Error(\"not implemented\");\n    return await this.request<boolean>(`/lenticular/v1/${tokenId}`, {\n      method: \"DELETE\",\n    });\n  }\n}\n\n/**\n * Converts a parsed query string into a Cosmo-compatible URLSearchParams.\n */\nfunction toSearchParams(input: Objekt.CollectionParams): URLSearchParams {\n  const query = new URLSearchParams();\n\n  for (const [key, value] of Object.entries(input)) {\n    // filter out empty values\n    if (\n      value === null ||\n      value === \"\" ||\n      (Array.isArray(value) && value.length === 0)\n    ) {\n      continue;\n    }\n\n    switch (typeof value) {\n      case \"string\":\n        query.set(key, value);\n        break;\n      case \"boolean\":\n        if (value) {\n          query.set(key, \"true\");\n        }\n        break;\n      case \"object\":\n        if (Array.isArray(value)) {\n          query.set(key, value.join(\",\"));\n        }\n        break;\n    }\n  }\n\n  return query;\n}\n\nexport namespace Filters {\n  export type Sort = {\n    title: string;\n    value: string;\n  };\n\n  export type ChipValue = {\n    title: string;\n    value: string;\n    image?: string;\n  };\n\n  export type Chip = {\n    key: string;\n    title: string;\n    type: \"chip\";\n    values: ChipValue[];\n  };\n\n  export type CategoryToggleValue = {\n    title: string;\n    value: string;\n    image: string;\n    category: string;\n  };\n\n  export type CategoryToggle = {\n    key: string;\n    title: string;\n    type: \"toggle\";\n    values: CategoryToggle[];\n  };\n\n  export type BooleanToggleValue = {\n    title: string;\n    value: string;\n  };\n\n  export type BooleanToggle = {\n    key: string;\n    title: string;\n    type: \"toggle\";\n    values: BooleanToggle[];\n  };\n\n  export type Filter = Chip | CategoryToggle | BooleanToggle;\n}\n\nexport namespace Objekt {\n  export type TokenId = string | number;\n  export type OwnedBy = \"me\" | (string & {});\n\n  export type Objekt = {\n    collectionId: string;\n    season: string;\n    member: string;\n    collectionNo: string;\n    class: string;\n    artists: string[];\n    thumbnailImage: string;\n    frontImage: string;\n    backImage: string;\n    accentColor: string;\n    backgroundColor: string;\n    textColor: string;\n    comoAmount: number;\n    transferableByDefault: boolean;\n    tokenId: string;\n    tokenAddress: string;\n    objektNo: number;\n    transferable: boolean;\n  };\n\n  export type Token = {\n    name: string;\n    description: string;\n    image: string;\n    background_color: string;\n    attributes: Attribute[];\n    objekt: Objekt;\n  };\n\n  export type Attribute = {\n    trait_type: string;\n    value: string;\n  };\n\n  export type CollectionParams = z.infer<typeof objektFilterSchema>;\n}\n","import { randomUUID } from \"crypto\";\nimport { BaseAPI } from \"./base-api\";\nimport { AccessTokenMissing, BadRequestError } from \"../errors\";\n\nexport class RewardsAPI extends BaseAPI {\n  /**\n   * Display the list of pending rewards.\n   *\n   * Authentication is required.\n   */\n  async list() {\n    if (!this.config.accessToken) {\n      throw new AccessTokenMissing();\n    }\n\n    const params = new URLSearchParams({\n      tid: randomUUID(),\n    });\n\n    return await this.request<Rewards.RewardList>(\n      `/bff/v1/event-rewards?${params.toString()}`\n    );\n  }\n\n  /**\n   * Check if there are any pending rewards.\n   *\n   * Authentication is required.\n   */\n  async check() {\n    if (!this.config.accessToken) {\n      throw new AccessTokenMissing();\n    }\n\n    const params = new URLSearchParams({\n      tid: randomUUID(),\n    });\n\n    return await this.request<{ isClaimable: boolean }>(\n      `/bff/v1/check-event-rewards?${params.toString()}`\n    ).then((res) => res.isClaimable);\n  }\n\n  /**\n   * Claim all pending rewards.\n   *\n   * Authentication is required.\n   */\n  async claim() {\n    if (!this.config.accessToken) {\n      throw new AccessTokenMissing();\n    }\n\n    const params = new URLSearchParams({\n      tid: randomUUID(),\n    });\n\n    return await this.request<boolean>(\n      `/bff/v1/event-rewards?${params.toString()}`,\n      {\n        method: \"POST\",\n      }\n    )\n      .then(() => true)\n      .catch((err) => {\n        if (err instanceof BadRequestError) {\n          return false;\n        }\n        throw err;\n      });\n  }\n}\n\nexport namespace Rewards {\n  export type Item = {\n    id: number;\n    isClaimed: boolean;\n    thumbnailImage: string;\n    title: string;\n    category: string;\n    endedAt: string;\n  };\n\n  export type RewardList = {\n    count: number;\n    items: Rewards.Item[];\n    claimCount: number;\n  };\n}\n","import { ValidArtist } from \"./artist\";\nimport { BaseAPI } from \"./base-api\";\n\nexport class SeasonAPI extends BaseAPI {\n  /**\n   * Get the seasons for the given artist.\n   *\n   * Authentication is not required.\n   */\n  async list(artist: ValidArtist) {\n    return await this.request<{\n      seasons: Season.Season[];\n      currentSeason: Season.OngoingSeason;\n    }>(`/season/v2/${artist}`);\n  }\n}\n\nexport namespace Season {\n  export type OngoingSeason = {\n    artist: string;\n    title: string;\n    image: string | null;\n    startDate: string;\n    endDate: null;\n    ongoing: true;\n  };\n\n  export type EndedSeason = {\n    artist: string;\n    title: string;\n    image: string | null;\n    startDate: string;\n    endDate: string;\n    ongoing: false;\n  };\n\n  export type Season = OngoingSeason | EndedSeason;\n}\n","import { AccessTokenMissing } from \"../errors\";\nimport { Artist, ValidArtist } from \"./artist\";\nimport { BaseAPI } from \"./base-api\";\n\nexport class UserAPI extends BaseAPI {\n  /**\n   * Get the currently authenticated user.\n   *\n   * Authentication is required.\n   */\n  async me() {\n    if (!this.config.accessToken) {\n      throw new AccessTokenMissing();\n    }\n\n    return await this.request<{ profile: User.User }>(`/user/v1/me`).then(\n      (res) => res.profile\n    );\n  }\n\n  /**\n   * Search for users.\n   *\n   * Authentication is required.\n   */\n  async search(query: string) {\n    if (!this.config.accessToken) {\n      throw new AccessTokenMissing();\n    }\n\n    return await this.request<Search.SearchResponse>(\n      `/user/v1/search?query=${query}`\n    );\n  }\n\n  /**\n   * Get a search result by nickname.\n   *\n   * Authentication is not required.\n   */\n  async byNickname(nickname: string) {\n    return await this.request<Omit<Search.SearchResult, \"profile\">>(\n      `/user/v1/by-nickname/${nickname}`\n    );\n  }\n}\n\nexport namespace User {\n  export type User = {\n    id: number;\n    email: string;\n    nickname: string;\n    address: string;\n    birth: string;\n    profileImageUrl: string;\n    isEligibleForWelcomeObjekt: boolean;\n    followingArtists: FollowedArtist[];\n    lastViewedArtist: ValidArtist;\n    marketingConsentDate: string | null;\n    createdAt: string;\n    profile: Profile[];\n  };\n\n  export type FollowedArtist = Artist.Artist & {\n    receivedWelcomeObjekt: boolean;\n    assetBalance: {\n      totalComo: number;\n      totalObjekt: number;\n    };\n  };\n\n  export type Profile = {\n    artistName: ValidArtist;\n    image: {\n      original: string;\n      thumbnail: string;\n    };\n  };\n}\n\nexport namespace Search {\n  export type SearchResponse =\n    | {\n        hasNext: true;\n        nextStartAfter: string;\n        results: SearchResult[];\n      }\n    | {\n        hasNext: false;\n        results: SearchResult[];\n      };\n\n  export type SearchResult = {\n    nickname: string;\n    address: string;\n    profileImageUrl: string;\n    profile: User.Profile[];\n  };\n}\n","import { Config } from \"./config\";\nimport { ArtistAPI } from \"./api/artist\";\nimport { AuthAPI } from \"./api/auth\";\nimport { GridAPI } from \"./api/grid\";\nimport { NewsAPI } from \"./api/news\";\nimport { ObjektAPI } from \"./api/objekt\";\nimport { RewardsAPI } from \"./api/rewards\";\nimport { SeasonAPI } from \"./api/season\";\nimport { UserAPI } from \"./api/user\";\n\nexport class CosmoClient {\n  private config: Config;\n  public artists: ArtistAPI;\n  public auth: AuthAPI;\n  public seasons: SeasonAPI;\n  public users: UserAPI;\n  public news: NewsAPI;\n  public objekts: ObjektAPI;\n  public rewards: RewardsAPI;\n  public grid: GridAPI;\n\n  constructor(config: Config) {\n    this.config = {\n      baseUrl: \"https://api.cosmo.fans\",\n      ...config,\n    };\n\n    this.artists = new ArtistAPI(this.config);\n    this.auth = new AuthAPI(this.config);\n    this.seasons = new SeasonAPI(this.config);\n    this.users = new UserAPI(this.config);\n    this.news = new NewsAPI(this.config);\n    this.objekts = new ObjektAPI(this.config);\n    this.rewards = new RewardsAPI(this.config);\n    this.grid = new GridAPI(this.config);\n  }\n\n  /**\n   * Set the access token for the client.\n   */\n  setAccessToken(accessToken: string | undefined) {\n    this.config.accessToken = accessToken;\n  }\n}\n\n/**\n * Create a new instance of the client.\n */\nexport function createClient(config?: Config): CosmoClient {\n  return new CosmoClient(config ?? {});\n}\n","import { z } from \"zod\";\n\nexport const validArtistSchema = z.union([\n  z.literal(\"ARTMS\"),\n  z.literal(\"tripleS\"),\n]);\n","import { z } from \"zod\";\n\nexport const signInSchema = z.object({\n  channel: z.literal(\"email\"),\n  email: z.string(),\n  accessToken: z.string(),\n});\n","import { z } from \"zod\";\nimport { validArtistSchema } from \"./artist\";\n\nexport const newsFeedSchema = z.object({\n  artist: validArtistSchema,\n  startAfter: z.coerce.number().optional(),\n  limit: z.coerce.number().optional(),\n});\n\nexport const bffNewsFeedSchema = z.object({\n  artistName: validArtistSchema,\n  page: z.coerce.number().optional(),\n  size: z.coerce.number().optional(),\n});\n","import { z } from \"zod\";\nimport { validArtistSchema } from \"./artist\";\n\n/**\n * COSMO expects comma-separated values for array filters like:\n * - `?season=Atom01,Binary01`\n * URLSearchParams.getAll() returns `Atom01,Binary01` as it expects arrays in the format of:\n * - `?season=Atom01&season=Binary01`\n * Provides a helper function to cast the value to an array before running it through validation.\n */\nfunction castToArray<TSchema extends z.Schema>(schema: TSchema) {\n  return z.preprocess((val) => {\n    const str = String(val);\n    if (str === \"\") return [];\n    return str.includes(\",\") ? str.split(\",\") : [str];\n  }, z.array(schema));\n}\n\n/**\n * For boolean values, COSMO expects only `true` or `undefined`, `false` is not valid.\n */\nconst nullableTrue = z.preprocess((val) => {\n  const str = String(val);\n  if (str === \"true\") return true;\n  return undefined;\n}, z.union([z.literal(\"true\"), z.literal(undefined)]));\n\nexport const objektFilterSchema = z.object({\n  start_after: z.coerce.number().optional(),\n  sort: z\n    .enum([\"newest\", \"oldest\", \"noAscending\", \"noDescending\"])\n    .optional()\n    .default(\"newest\"),\n  season: castToArray(z.string()).optional().default([]),\n  class: castToArray(z.enum([\"First\", \"Special\", \"Double\", \"Welcome\", \"Zero\"]))\n    .optional()\n    .default([]),\n  on_offline: castToArray(z.enum([\"online\", \"offline\"]))\n    .optional()\n    .default([]),\n  member: z.string().optional(),\n  artist: validArtistSchema.optional(),\n  transferable: nullableTrue,\n  gridable: nullableTrue,\n  collection: z.string().optional(),\n});\n"]}