blocket.js 1.0.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) 2025 Philip Rutberg
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,109 @@
1
+ # blocket.js
2
+
3
+ blocket.js is a lightweight and easy-to-use npm package that provides a TypeScript interface to the unofficial Blocket API. It allows you to search and retrieve ads from blocket.se with automatic token management and error handling, so you can focus on building your application without worrying about the underlying API token management.
4
+
5
+ ## Features
6
+
7
+ - **Simple API**: Import the client and call methods like `find` directly.
8
+ - **Automatic Token Management**: Handles token retrieval, caching, and refreshing automatically when a token expires (detected via 401 errors).
9
+ - **Configurable**: Global and per-request configuration options let you override API endpoints, logging preferences, retry attempts, and more.
10
+ - **TypeScript Support**: Fully typed interfaces for query parameters, API responses, and advertisements.
11
+ - **Robust Error Handling & Logging**: Automatic retries on token expiry with configurable logging to assist in debugging.
12
+
13
+ ## Installation
14
+
15
+ Install blocket.js via npm:
16
+
17
+ ```bash
18
+ npm install blocket.js
19
+ ```
20
+
21
+ or using yarn:
22
+
23
+ ```bash
24
+ yarn add blocket.js
25
+ ```
26
+
27
+ ## Usage
28
+
29
+ ### Basic Usage
30
+
31
+ After installing the package, import the client and start using its methods immediately:
32
+
33
+ ```ts
34
+ import client from 'blocket.js';
35
+
36
+ (async () => {
37
+ try {
38
+ const ads = await client.find({ query: 'macbook air' });
39
+ console.log(ads);
40
+ } catch (error) {
41
+ console.error('Error fetching ads:', error);
42
+ }
43
+ })();
44
+ ```
45
+
46
+ ### Advanced Usage
47
+
48
+ #### Global Configuration
49
+
50
+ Customize the package behavior with the global configuration. This allows you to override default values such as the API base URL, token endpoint, log level, and retry attempts.
51
+
52
+ ```ts
53
+ import client, { configure } from 'blocket.js';
54
+
55
+ configure({
56
+ apiBaseUrl: 'https://api.blocket.se/v1', // API base URL (default)
57
+ tokenEndpoint:
58
+ 'https://www.blocket.se/api/adout-api-route/refresh-token-and-validate-session', // Token endpoint
59
+ logLevel: 'debug', // Options: 'none', 'error', 'info', 'debug'
60
+ retryAttempts: 3, // Maximum retry attempts on 401 errors
61
+ });
62
+
63
+ (async () => {
64
+ try {
65
+ const ads = await client.find({ query: 'macbook air', limit: 10 });
66
+ console.log(ads);
67
+ } catch (error) {
68
+ console.error('Error fetching ads:', error);
69
+ }
70
+ })();
71
+ ```
72
+
73
+ #### Per Request Configuration
74
+
75
+ You can also pass additional fetch options for individual requests to customize headers or other request parameters:
76
+
77
+ ```ts
78
+ import client from 'blocket.js';
79
+
80
+ (async () => {
81
+ try {
82
+ const ads = await client.find(
83
+ { query: 'macbook air', limit: 10 },
84
+ { headers: { 'Custom-Header': 'customValue' } }
85
+ );
86
+ console.log(ads);
87
+ } catch (error) {
88
+ console.error('Error fetching ads:', error);
89
+ }
90
+ })();
91
+ ```
92
+
93
+ ## API Reference
94
+
95
+ `client.find(query: BlocketQueryParams, fetchOptions?: FetchOptions): Promise<BlocketAd[]>`
96
+
97
+ Searches for ads on Blocket based on the provided query parameters.
98
+
99
+ - Parameters:
100
+ - `query`: An object conforming to the `BlocketQueryParams` interface:
101
+ - `query` (string): The search query (e.g., `'macbook air'`).
102
+ - `limit` (number, optional): Maximum number of results to return (default: 60).
103
+ - `sort` (string, optional): Sorting order (default: `'rel'`).
104
+ - `listingType` (string, optional): Listing type; `'s'` for selling, `'b'` for buying (default: `'s'`).
105
+ - `status` (string, optional): Ad status (`'active'` or `'inactive'`, default: `'active'`).
106
+ - `gl` (number, optional): Maximum distance in kilometers.
107
+ - `include` (string, optional): Additional filters or fields to include (e.g., 'image,description').
108
+ - `fetchOptions` (optional): Additional options to pass to the underlying fetch request.
109
+ - Returns: A promise that resolves to an array of `BlocketAd` objects.
package/dist/index.js ADDED
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.configure = void 0;
37
+ const client = __importStar(require("./client"));
38
+ const config_1 = require("./config");
39
+ Object.defineProperty(exports, "configure", { enumerable: true, get: function () { return config_1.configure; } });
40
+ exports.default = client;
41
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,63 @@
1
+ import { apiRequest } from './request';
2
+ import { getConfig } from '@/config';
3
+
4
+ import type { FetchOptions } from 'ofetch';
5
+ import type { BlocketQueryParams, BlocketAd, BlocketResponse } from '@/types';
6
+
7
+ /**
8
+ * Remap BlocketQueryParams to API query parameters.
9
+ * @param params Blocket query parameters.
10
+ * @returns Remapped query parameters.
11
+ */
12
+ function remapQueryParams(params: BlocketQueryParams): Record<string, any> {
13
+ return {
14
+ q: params.query,
15
+ lim: params.limit,
16
+ sort: params.sort,
17
+ st: params.listingType,
18
+ status: params.status,
19
+ gl: params.gl,
20
+ include: params.include,
21
+ };
22
+ }
23
+
24
+ /**
25
+ * Find ads on Blocket based on query parameters.
26
+ * @param query Blocket query parameters.
27
+ * @param fetchOptions Additional fetch options.
28
+ * @returns Array of Blocket ads.
29
+ */
30
+ export async function find(
31
+ query: BlocketQueryParams,
32
+ fetchOptions?: FetchOptions<'json', any>
33
+ ): Promise<BlocketAd[]> {
34
+ const config = getConfig();
35
+ const response = await apiRequest<BlocketResponse>(config.apiBaseUrl, {
36
+ query: remapQueryParams(query),
37
+ ...fetchOptions,
38
+ });
39
+
40
+ if (!response || !response.data || !Array.isArray(response.data)) {
41
+ throw new Error(
42
+ `Unexpected Blocket API response structure, expected array of ads, got: ${typeof response?.data}`
43
+ );
44
+ }
45
+
46
+ return response.data;
47
+ }
48
+
49
+ /**
50
+ * Get details of a specific ad by its ID.
51
+ * @param adId Advertisement ID.
52
+ * @param fetchOptions Additional fetch options.
53
+ * @returns Blocket ad details.
54
+ */
55
+ export async function getAd(
56
+ adId: string,
57
+ fetchOptions?: FetchOptions<'json', any>
58
+ ): Promise<BlocketAd> {
59
+ const config = getConfig();
60
+ const url = `${config.apiBaseUrl}/ad/${adId}`;
61
+
62
+ return await apiRequest<BlocketAd>(url, fetchOptions);
63
+ }
@@ -0,0 +1,43 @@
1
+ import { ofetch, type FetchOptions } from 'ofetch';
2
+
3
+ import { fetchToken, setCachedToken } from './token';
4
+ import { getConfig, logger } from '@/config';
5
+
6
+ /**
7
+ * Make an API request with automatic token handling and retry on 401 errors.
8
+ * @param url URL to fetch.
9
+ * @param options Fetch options.
10
+ * @param retryCount Current retry count.
11
+ * @returns Parsed response of type T.
12
+ */
13
+ export async function apiRequest<T>(
14
+ url: string,
15
+ options: FetchOptions<'json', any> = {},
16
+ retryCount: number = 0
17
+ ): Promise<T> {
18
+ const config = getConfig();
19
+ const token = await fetchToken();
20
+
21
+ try {
22
+ return await ofetch<T>(url, {
23
+ ...options,
24
+ headers: {
25
+ ...options.headers,
26
+ Authorization: `Bearer ${token}`,
27
+ },
28
+ });
29
+ } catch (error: any) {
30
+ if (error?.data?.status_code === 401 && retryCount < config.retryAttempts) {
31
+ logger(
32
+ 'info',
33
+ `Token expired. Retrying request (${retryCount + 1}/${
34
+ config.retryAttempts
35
+ }).`
36
+ );
37
+ const newToken = await fetchToken(true);
38
+ setCachedToken(newToken);
39
+ return apiRequest<T>(url, options, retryCount + 1);
40
+ }
41
+ throw error;
42
+ }
43
+ }
@@ -0,0 +1,43 @@
1
+ import { ofetch } from 'ofetch';
2
+ import { getConfig, logger } from '@/config';
3
+
4
+ import type { BlocketAccessToken } from '@/types';
5
+
6
+ let cachedToken: string | null = null;
7
+
8
+ /**
9
+ * Get the cached token.
10
+ * @returns Cached bearer token if available.
11
+ */
12
+ export const getCachedToken = (): string | null => cachedToken;
13
+
14
+ /**
15
+ * Set the cached token.
16
+ * @param token Bearer token.
17
+ */
18
+ export const setCachedToken = (token: string): void => {
19
+ cachedToken = token;
20
+ };
21
+
22
+ /**
23
+ * Fetch a new token from Blocket API.
24
+ * @param forceRefresh If true, ignores cached token and fetches a new one.
25
+ * @returns The bearer token.
26
+ */
27
+ export const fetchToken = async (
28
+ forceRefresh: boolean = false
29
+ ): Promise<string> => {
30
+ if (!forceRefresh && cachedToken) return cachedToken;
31
+
32
+ logger('debug', 'Fetching new Blocket API token.');
33
+
34
+ const config = getConfig();
35
+
36
+ const tokenData = await ofetch<BlocketAccessToken>(config.tokenEndpoint);
37
+ if (!tokenData || !tokenData.bearerToken) {
38
+ throw new Error('Failed to retrieve Blocket API token.');
39
+ }
40
+
41
+ cachedToken = tokenData.bearerToken;
42
+ return cachedToken;
43
+ };
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Configuration interface.
3
+ */
4
+ export interface BlocketConfig {
5
+ /**
6
+ * Base URL for the Blocket API.
7
+ * @default 'https://api.blocket.se/v1'
8
+ */
9
+ apiBaseUrl: string;
10
+ /**
11
+ * Endpoint URL to fetch the token.
12
+ * @default 'https://www.blocket.se/api/adout-api-route/refresh-token-and-validate-session'
13
+ */
14
+ tokenEndpoint: string;
15
+ /**
16
+ * Log level for debugging.
17
+ * Options: 'none', 'error', 'info', 'debug'
18
+ * @default 'error'
19
+ */
20
+ logLevel: 'none' | 'error' | 'info' | 'debug';
21
+ /**
22
+ * Maximum number of retry attempts on 401 error.
23
+ * @default 3
24
+ */
25
+ retryAttempts: number;
26
+ }
27
+
28
+ /**
29
+ * Default configuration.
30
+ */
31
+ export const defaultConfig: BlocketConfig = {
32
+ apiBaseUrl: 'https://api.blocket.se/v1',
33
+ tokenEndpoint:
34
+ 'https://www.blocket.se/api/adout-api-route/refresh-token-and-validate-session',
35
+ logLevel: 'error',
36
+ retryAttempts: 3,
37
+ };
38
+
39
+ let currentConfig: BlocketConfig = { ...defaultConfig };
40
+
41
+ /**
42
+ * Configure Blocket.js globally.
43
+ * @param config Partial configuration to override defaults.
44
+ */
45
+ export const configure = (config: Partial<BlocketConfig>): void => {
46
+ currentConfig = { ...currentConfig, ...config };
47
+ };
48
+
49
+ /**
50
+ * Get the current configuration.
51
+ * @returns The current Blocket.js configuration.
52
+ */
53
+ export const getConfig = (): BlocketConfig => currentConfig;
54
+
55
+ /**
56
+ * Simple logger function based on log level.
57
+ * @param level Log level of the message.
58
+ * @param message Message to log.
59
+ */
60
+ export const logger = (
61
+ level: 'error' | 'info' | 'debug',
62
+ message: string
63
+ ): void => {
64
+ const levels = { none: 0, error: 1, info: 2, debug: 3 };
65
+ if (levels[getConfig().logLevel] >= levels[level]) {
66
+ console[level === 'error' ? 'error' : 'log'](message);
67
+ }
68
+ };
package/lib/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ import * as client from './client';
2
+ import { configure } from './config';
3
+
4
+ export { configure };
5
+ export default client;
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Access token.
3
+ */
4
+ export type BlocketAccessToken = {
5
+ user: null;
6
+ isLoggedIn: false;
7
+ bearerToken: string;
8
+ };
9
+
10
+ /**
11
+ * Blocket API response containing an array of ads.
12
+ */
13
+ export interface BlocketResponse {
14
+ data: BlocketAd[];
15
+ }
16
+
17
+ /**
18
+ * Blocket advertisement object.
19
+ */
20
+ export interface BlocketAd {
21
+ ad_id: string;
22
+ ad_status: 'active' | 'inactive' | string;
23
+ advertiser: {
24
+ account_id: string;
25
+ contact_methods: Record<string, any>;
26
+ name: string;
27
+ public_profile: Record<string, any>;
28
+ type: 'private' | 'business';
29
+ };
30
+ body: string;
31
+ category: Record<string, any>[];
32
+ co2_text: string;
33
+ images: Record<string, any>[];
34
+ list_id: string;
35
+ list_time: string; // ISO date string
36
+ location: Record<string, any>[];
37
+ map_url: string;
38
+ parameter_groups: Record<string, any>[];
39
+ parameters_raw: {
40
+ is_shipping_buy_now_enabled: Record<string, any>;
41
+ shipping_enabled: Record<string, any>;
42
+ };
43
+ price: {
44
+ suffix: string;
45
+ value: number;
46
+ };
47
+ price_badge?: {
48
+ icon: Record<string, any>;
49
+ id: string;
50
+ label: string;
51
+ };
52
+ share_url: string;
53
+ state_id: string;
54
+ subject: string;
55
+ type: string;
56
+ zipcode: string;
57
+ }
58
+
59
+ /**
60
+ * Parameters for querying Blocket ads.
61
+ */
62
+ export interface BlocketQueryParams {
63
+ /**
64
+ * The search query.
65
+ * @example 'macbook air'
66
+ */
67
+ query: string;
68
+ /**
69
+ * The maximum number of results to return.
70
+ * @example 10
71
+ * @default 60
72
+ */
73
+ limit?: number;
74
+ /**
75
+ * The sorting order of the results.
76
+ * @example 'rel'
77
+ * @default 'rel'
78
+ */
79
+ sort?: 'rel';
80
+ /**
81
+ * The type of listing to search for. 's' for selling, 'b' for buying.
82
+ * @example 's'
83
+ * @default 's'
84
+ * @options 's' | 'b'
85
+ */
86
+ listingType?: 's' | 'b';
87
+ /**
88
+ * The status of the ad.
89
+ * @example 'active'
90
+ * @default 'active'
91
+ */
92
+ status?: 'active' | 'inactive' | string;
93
+ /**
94
+ * The maximum distance in kilometers from the search location.
95
+ * @example 10
96
+ */
97
+ gl?: number;
98
+ /**
99
+ * Additional filters or fields to include in the response.
100
+ * @example 'image,description'
101
+ */
102
+ include?: string;
103
+ }
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "blocket.js",
3
+ "version": "1.0.0",
4
+ "description": "A user-friendly js wrapper for blocket.se",
5
+ "keywords": [
6
+ "blocket",
7
+ "javascript",
8
+ "api",
9
+ "wrapper",
10
+ "blocket.js"
11
+ ],
12
+ "license": "MIT",
13
+ "author": "Philip Rutberg <philiprutberg00@gmail.com> (https://philiprutberg.com/)",
14
+ "homepage": "https://github.com/rutbergphilip/blocket.js#readme",
15
+ "main": "dist/index.js",
16
+ "directories": {
17
+ "lib": "lib"
18
+ },
19
+ "files": [
20
+ "lib"
21
+ ],
22
+ "publishConfig": {
23
+ "registry": "https://registry.npmjs.org/"
24
+ },
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/rutbergphilip/blocket.js.git"
28
+ },
29
+ "scripts": {
30
+ "test": "echo \"Error: no test yet\" && exit 1",
31
+ "build": "tsc"
32
+ },
33
+ "bugs": {
34
+ "url": "https://github.com/rutbergphilip/blocket.js/issues"
35
+ },
36
+ "pre-push": [
37
+ "build"
38
+ ],
39
+ "dependencies": {
40
+ "ofetch": "^1.4.1"
41
+ },
42
+ "devDependencies": {
43
+ "pre-push": "^0.1.4",
44
+ "typescript": "^5.7.3"
45
+ }
46
+ }