@ecosplay/e-brocante 1.0.2

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.
Files changed (110) hide show
  1. package/AGENTS.md +141 -0
  2. package/CHANGELOG.md +76 -0
  3. package/LICENSE +64 -0
  4. package/README.md +246 -0
  5. package/dist/EBrocanteClient.d.ts +38 -0
  6. package/dist/EBrocanteClient.d.ts.map +1 -0
  7. package/dist/EBrocanteClient.js +99 -0
  8. package/dist/EBrocanteClient.js.map +1 -0
  9. package/dist/cache/CacheBackend.d.ts +20 -0
  10. package/dist/cache/CacheBackend.d.ts.map +1 -0
  11. package/dist/cache/CacheBackend.js +6 -0
  12. package/dist/cache/CacheBackend.js.map +1 -0
  13. package/dist/cache/LocalCache.d.ts +22 -0
  14. package/dist/cache/LocalCache.d.ts.map +1 -0
  15. package/dist/cache/LocalCache.js +53 -0
  16. package/dist/cache/LocalCache.js.map +1 -0
  17. package/dist/cache/NullCache.d.ts +17 -0
  18. package/dist/cache/NullCache.d.ts.map +1 -0
  19. package/dist/cache/NullCache.js +26 -0
  20. package/dist/cache/NullCache.js.map +1 -0
  21. package/dist/cache/RedisCache.d.ts +39 -0
  22. package/dist/cache/RedisCache.d.ts.map +1 -0
  23. package/dist/cache/RedisCache.js +104 -0
  24. package/dist/cache/RedisCache.js.map +1 -0
  25. package/dist/cache/RedisConfig.d.ts +17 -0
  26. package/dist/cache/RedisConfig.d.ts.map +1 -0
  27. package/dist/cache/RedisConfig.js +13 -0
  28. package/dist/cache/RedisConfig.js.map +1 -0
  29. package/dist/cache/stableHashKey.d.ts +11 -0
  30. package/dist/cache/stableHashKey.d.ts.map +1 -0
  31. package/dist/cache/stableHashKey.js +27 -0
  32. package/dist/cache/stableHashKey.js.map +1 -0
  33. package/dist/exceptions/index.d.ts +41 -0
  34. package/dist/exceptions/index.d.ts.map +1 -0
  35. package/dist/exceptions/index.js +62 -0
  36. package/dist/exceptions/index.js.map +1 -0
  37. package/dist/http/HttpClient.d.ts +41 -0
  38. package/dist/http/HttpClient.d.ts.map +1 -0
  39. package/dist/http/HttpClient.js +222 -0
  40. package/dist/http/HttpClient.js.map +1 -0
  41. package/dist/index.d.ts +22 -0
  42. package/dist/index.d.ts.map +1 -0
  43. package/dist/index.js +17 -0
  44. package/dist/index.js.map +1 -0
  45. package/dist/internal/safeStrings.d.ts +52 -0
  46. package/dist/internal/safeStrings.d.ts.map +1 -0
  47. package/dist/internal/safeStrings.js +156 -0
  48. package/dist/internal/safeStrings.js.map +1 -0
  49. package/dist/logger/DebugWriter.d.ts +24 -0
  50. package/dist/logger/DebugWriter.d.ts.map +1 -0
  51. package/dist/logger/DebugWriter.js +62 -0
  52. package/dist/logger/DebugWriter.js.map +1 -0
  53. package/dist/logger/FileLogger.d.ts +26 -0
  54. package/dist/logger/FileLogger.d.ts.map +1 -0
  55. package/dist/logger/FileLogger.js +96 -0
  56. package/dist/logger/FileLogger.js.map +1 -0
  57. package/dist/logger/Logger.d.ts +12 -0
  58. package/dist/logger/Logger.d.ts.map +1 -0
  59. package/dist/logger/Logger.js +6 -0
  60. package/dist/logger/Logger.js.map +1 -0
  61. package/dist/logger/NullLogger.d.ts +13 -0
  62. package/dist/logger/NullLogger.d.ts.map +1 -0
  63. package/dist/logger/NullLogger.js +20 -0
  64. package/dist/logger/NullLogger.js.map +1 -0
  65. package/dist/models/Event.d.ts +37 -0
  66. package/dist/models/Event.d.ts.map +1 -0
  67. package/dist/models/Event.js +42 -0
  68. package/dist/models/Event.js.map +1 -0
  69. package/dist/models/Orga.d.ts +33 -0
  70. package/dist/models/Orga.d.ts.map +1 -0
  71. package/dist/models/Orga.js +39 -0
  72. package/dist/models/Orga.js.map +1 -0
  73. package/dist/models/PaginatedResult.d.ts +27 -0
  74. package/dist/models/PaginatedResult.d.ts.map +1 -0
  75. package/dist/models/PaginatedResult.js +46 -0
  76. package/dist/models/PaginatedResult.js.map +1 -0
  77. package/dist/resources/EventsResource.d.ts +20 -0
  78. package/dist/resources/EventsResource.d.ts.map +1 -0
  79. package/dist/resources/EventsResource.js +46 -0
  80. package/dist/resources/EventsResource.js.map +1 -0
  81. package/dist/resources/OrgaResource.d.ts +20 -0
  82. package/dist/resources/OrgaResource.d.ts.map +1 -0
  83. package/dist/resources/OrgaResource.js +51 -0
  84. package/dist/resources/OrgaResource.js.map +1 -0
  85. package/dist/types.d.ts +81 -0
  86. package/dist/types.d.ts.map +1 -0
  87. package/dist/types.js +8 -0
  88. package/dist/types.js.map +1 -0
  89. package/package.json +72 -0
  90. package/src/EBrocanteClient.ts +110 -0
  91. package/src/cache/CacheBackend.ts +20 -0
  92. package/src/cache/LocalCache.ts +64 -0
  93. package/src/cache/NullCache.ts +32 -0
  94. package/src/cache/RedisCache.ts +123 -0
  95. package/src/cache/RedisConfig.ts +23 -0
  96. package/src/cache/stableHashKey.ts +28 -0
  97. package/src/exceptions/index.ts +75 -0
  98. package/src/http/HttpClient.ts +266 -0
  99. package/src/index.ts +42 -0
  100. package/src/internal/safeStrings.ts +154 -0
  101. package/src/logger/DebugWriter.ts +61 -0
  102. package/src/logger/FileLogger.ts +106 -0
  103. package/src/logger/Logger.ts +13 -0
  104. package/src/logger/NullLogger.ts +22 -0
  105. package/src/models/Event.ts +92 -0
  106. package/src/models/Orga.ts +76 -0
  107. package/src/models/PaginatedResult.ts +62 -0
  108. package/src/resources/EventsResource.ts +48 -0
  109. package/src/resources/OrgaResource.ts +53 -0
  110. package/src/types.ts +86 -0
@@ -0,0 +1,81 @@
1
+ /**
2
+ * @copyright 2026 Association E-Cosplay
3
+ * @license Proprietary — see LICENSE
4
+ *
5
+ * Public type definitions exposed by the SDK.
6
+ */
7
+ import type { CacheBackend } from './cache/CacheBackend.js';
8
+ import type { RedisConfig } from './cache/RedisConfig.js';
9
+ import type { Logger } from './logger/Logger.js';
10
+ export type Mode = 'prod' | 'test';
11
+ export type CacheType = 'local' | 'redis';
12
+ export interface EBrocanteClientOptions {
13
+ /** API key (eb_prod_…, eb_test_…, eb_mock_…). */
14
+ apiKey: string;
15
+ /** Account email sent in the X-BROC header. */
16
+ brocEmail: string;
17
+ /** prod (live data) or test (sandbox). Defaults to 'prod'. */
18
+ mode?: Mode;
19
+ /** API base URL (override for the bundled mock server). */
20
+ baseUrl?: string;
21
+ /** HTTP timeout in milliseconds. Default: 10_000. */
22
+ timeoutMs?: number;
23
+ /** Enable response cache. Default: true. */
24
+ cache?: boolean;
25
+ /** Cache backend. Default: 'local'. */
26
+ cacheType?: CacheType;
27
+ /** Cache entry TTL in seconds. Default: 300. */
28
+ cacheTtl?: number;
29
+ /** Redis config (required when cacheType='redis'). */
30
+ cacheRedis?: RedisConfig;
31
+ /** Cache key prefix. Default: 'ebrocante:'. */
32
+ cacheKeyPrefix?: string;
33
+ /** Custom backend implementation (full override). */
34
+ cacheImpl?: CacheBackend;
35
+ /** Directory to write JSON-lines log files (one per day). */
36
+ logPath?: string;
37
+ /** Custom Logger implementation (overrides logPath). */
38
+ logger?: Logger;
39
+ /** Append one line per SDK action to a debug file. Default: false. */
40
+ debug?: boolean;
41
+ /** Debug file path when `debug` is true. Default: './DEBUG.TXT'. */
42
+ debugFile?: string;
43
+ /** Number of retries on 429 / 5xx. Default: 3. */
44
+ maxRetries?: number;
45
+ /** Custom fetch implementation (defaults to global fetch). */
46
+ fetch?: typeof fetch;
47
+ }
48
+ export interface EventFilters {
49
+ city?: string;
50
+ department?: string;
51
+ region?: string;
52
+ from?: string;
53
+ to?: string;
54
+ category?: string;
55
+ has_available_booths?: boolean;
56
+ outside?: boolean;
57
+ inside?: boolean;
58
+ since?: string;
59
+ q?: string;
60
+ sort?: string;
61
+ page?: number;
62
+ per_page?: number;
63
+ lat?: number;
64
+ lng?: number;
65
+ radius?: number;
66
+ }
67
+ export interface OrgaFilters {
68
+ city?: string;
69
+ legal_form?: string;
70
+ q?: string;
71
+ has_upcoming_events?: boolean;
72
+ since?: string;
73
+ sort?: string;
74
+ page?: number;
75
+ per_page?: number;
76
+ }
77
+ export interface RequestOptions {
78
+ skipCache?: boolean;
79
+ ttl?: number;
80
+ }
81
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAEjD,MAAM,MAAM,IAAI,GAAG,MAAM,GAAG,MAAM,CAAC;AACnC,MAAM,MAAM,SAAS,GAAG,OAAO,GAAG,OAAO,CAAC;AAE1C,MAAM,WAAW,sBAAsB;IACnC,iDAAiD;IACjD,MAAM,EAAE,MAAM,CAAC;IACf,+CAA+C;IAC/C,SAAS,EAAE,MAAM,CAAC;IAClB,8DAA8D;IAC9D,IAAI,CAAC,EAAE,IAAI,CAAC;IACZ,2DAA2D;IAC3D,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,qDAAqD;IACrD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,4CAA4C;IAC5C,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,uCAAuC;IACvC,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,gDAAgD;IAChD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,sDAAsD;IACtD,UAAU,CAAC,EAAE,WAAW,CAAC;IACzB,+CAA+C;IAC/C,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,qDAAqD;IACrD,SAAS,CAAC,EAAE,YAAY,CAAC;IACzB,6DAA6D;IAC7D,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,wDAAwD;IACxD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,sEAAsE;IACtE,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,oEAAoE;IACpE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,kDAAkD;IAClD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,8DAA8D;IAC9D,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;CACxB;AAED,MAAM,WAAW,YAAY;IACzB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,CAAC,CAAC,EAAE,MAAM,CAAC;IACX,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,WAAW;IACxB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,CAAC,CAAC,EAAE,MAAM,CAAC;IACX,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,cAAc;IAC3B,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,GAAG,CAAC,EAAE,MAAM,CAAC;CAChB"}
package/dist/types.js ADDED
@@ -0,0 +1,8 @@
1
+ /**
2
+ * @copyright 2026 Association E-Cosplay
3
+ * @license Proprietary — see LICENSE
4
+ *
5
+ * Public type definitions exposed by the SDK.
6
+ */
7
+ export {};
8
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG"}
package/package.json ADDED
@@ -0,0 +1,72 @@
1
+ {
2
+ "name": "@ecosplay/e-brocante",
3
+ "version": "1.0.2",
4
+ "description": "Official JavaScript / TypeScript client for the public e-brocante.enum.fr API",
5
+ "type": "module",
6
+ "license": "SEE LICENSE IN LICENSE",
7
+ "homepage": "https://e-brocante.enum.fr",
8
+ "keywords": ["brocante", "flea-market", "api", "sdk", "e-brocante", "ecosplay"],
9
+ "author": {
10
+ "name": "Association E-Cosplay",
11
+ "email": "contact@e-cosplay.fr",
12
+ "url": "https://e-cosplay.fr"
13
+ },
14
+ "publishConfig": {
15
+ "access": "public"
16
+ },
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "https://code.e-cosplay.fr/shoko/e-brocante-js.git"
20
+ },
21
+ "bugs": {
22
+ "email": "contact@e-cosplay.fr",
23
+ "url": "https://code.e-cosplay.fr/shoko/e-brocante-js/issues"
24
+ },
25
+ "main": "./dist/index.js",
26
+ "module": "./dist/index.js",
27
+ "types": "./dist/index.d.ts",
28
+ "exports": {
29
+ ".": {
30
+ "types": "./dist/index.d.ts",
31
+ "import": "./dist/index.js"
32
+ }
33
+ },
34
+ "files": [
35
+ "dist",
36
+ "src",
37
+ "LICENSE",
38
+ "README.md",
39
+ "AGENTS.md",
40
+ "CHANGELOG.md"
41
+ ],
42
+ "engines": {
43
+ "node": ">=22",
44
+ "bun": ">=1.1"
45
+ },
46
+ "scripts": {
47
+ "build": "tsc -p tsconfig.build.json",
48
+ "test": "vitest run",
49
+ "test:unit": "vitest run tests/unit",
50
+ "test:integration": "vitest run tests/integration",
51
+ "test:coverage": "vitest run --coverage",
52
+ "coverage:check": "node bin/check-coverage.mjs coverage/coverage-final.json 100",
53
+ "lint": "biome check src tests samples",
54
+ "lint:fix": "biome check --write src tests samples",
55
+ "typecheck": "tsc --noEmit",
56
+ "mock-api": "cd tools/mock-api && bun run src/server.ts",
57
+ "ci": "bun run lint && bun run typecheck && bun run test:coverage && bun run coverage:check",
58
+ "prepublishOnly": "bun run ci && bun run build"
59
+ },
60
+ "dependencies": {},
61
+ "optionalDependencies": {
62
+ "ioredis": "^5.4.0"
63
+ },
64
+ "devDependencies": {
65
+ "@biomejs/biome": "^1.9.0",
66
+ "@types/node": "^22.10.0",
67
+ "@vitest/coverage-v8": "^2.1.0",
68
+ "ioredis": "^5.4.0",
69
+ "typescript": "^5.6.0",
70
+ "vitest": "^2.1.0"
71
+ }
72
+ }
@@ -0,0 +1,110 @@
1
+ /**
2
+ * e-brocante JavaScript / TypeScript SDK
3
+ *
4
+ * @copyright 2026 Association E-Cosplay
5
+ * @author Association E-Cosplay <contact@e-cosplay.fr>
6
+ * @license Proprietary — see LICENSE
7
+ *
8
+ * Redistribution and modification forbidden without prior written agreement.
9
+ * Contact: contact@e-cosplay.fr
10
+ */
11
+
12
+ import type { CacheBackend } from './cache/CacheBackend.js';
13
+ import { LocalCache } from './cache/LocalCache.js';
14
+ import { NullCache } from './cache/NullCache.js';
15
+ import { RedisCache } from './cache/RedisCache.js';
16
+ import { HttpClient, SDK_VERSION } from './http/HttpClient.js';
17
+ import { DebugWriter } from './logger/DebugWriter.js';
18
+ import { FileLogger } from './logger/FileLogger.js';
19
+ import type { Logger } from './logger/Logger.js';
20
+ import { NullLogger } from './logger/NullLogger.js';
21
+ import { EventsResource } from './resources/EventsResource.js';
22
+ import { OrgaResource } from './resources/OrgaResource.js';
23
+ import type { EBrocanteClientOptions } from './types.js';
24
+
25
+ import { isValidEmail } from './internal/safeStrings.js';
26
+
27
+ const DEFAULT_BASE_URL = 'https://e-brocante.enum.fr';
28
+ const DEFAULT_TIMEOUT_MS = 10_000;
29
+ const DEFAULT_CACHE_TTL = 300;
30
+
31
+ /**
32
+ * Main client for the e-brocante.enum.fr public API.
33
+ *
34
+ * const client = new EBrocanteClient({
35
+ * apiKey: 'eb_prod_…',
36
+ * brocEmail: 'orga@example.com',
37
+ * });
38
+ * const { data: events } = await client.events.list({ city: 'paris' });
39
+ * const orga = await client.orga.get('asso-sakura');
40
+ */
41
+ export class EBrocanteClient {
42
+ static readonly VERSION: string = SDK_VERSION;
43
+
44
+ readonly events: EventsResource;
45
+ readonly orga: OrgaResource;
46
+ readonly cache: CacheBackend;
47
+ readonly logger: Logger;
48
+ readonly mode: 'prod' | 'test';
49
+
50
+ private readonly http: HttpClient;
51
+
52
+ constructor(options: EBrocanteClientOptions) {
53
+ if (!options.apiKey || typeof options.apiKey !== 'string') {
54
+ throw new TypeError('apiKey is required and must be a string');
55
+ }
56
+ if (!options.brocEmail || !isValidEmail(options.brocEmail)) {
57
+ throw new TypeError(`Invalid brocEmail: '${options.brocEmail}'`);
58
+ }
59
+ const mode = options.mode ?? 'prod';
60
+ if (mode !== 'prod' && mode !== 'test') {
61
+ throw new TypeError(`mode must be 'prod' or 'test', got '${String(mode)}'`);
62
+ }
63
+ this.mode = mode;
64
+
65
+ this.cache = this.buildCache(options);
66
+ this.logger = this.buildLogger(options);
67
+ const debugWriter = options.debug === true ? new DebugWriter(options.debugFile ?? 'DEBUG.TXT') : null;
68
+
69
+ this.http = new HttpClient({
70
+ apiKey: options.apiKey,
71
+ brocEmail: options.brocEmail,
72
+ mode,
73
+ baseUrl: options.baseUrl ?? DEFAULT_BASE_URL,
74
+ timeoutMs: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
75
+ cache: this.cache,
76
+ logger: this.logger,
77
+ debugWriter,
78
+ maxRetries: options.maxRetries ?? 3,
79
+ fetchImpl: options.fetch ?? globalThis.fetch.bind(globalThis),
80
+ });
81
+
82
+ this.events = new EventsResource(this.http);
83
+ this.orga = new OrgaResource(this.http);
84
+ }
85
+
86
+ private buildCache(options: EBrocanteClientOptions): CacheBackend {
87
+ if (options.cacheImpl) return options.cacheImpl;
88
+ if (options.cache === false) return new NullCache();
89
+
90
+ const ttl = options.cacheTtl ?? DEFAULT_CACHE_TTL;
91
+ const mode = options.mode ?? 'prod';
92
+ const keyPrefix = `${options.cacheKeyPrefix ?? 'ebrocante:'}${mode}:`;
93
+ const type = options.cacheType ?? 'local';
94
+
95
+ if (type === 'local') return new LocalCache(ttl, keyPrefix);
96
+ if (type === 'redis') {
97
+ if (!options.cacheRedis) {
98
+ throw new TypeError("cacheRedis is required when cacheType='redis'");
99
+ }
100
+ return new RedisCache(options.cacheRedis, ttl, keyPrefix);
101
+ }
102
+ throw new TypeError(`cacheType must be 'local' or 'redis', got '${String(type)}'`);
103
+ }
104
+
105
+ private buildLogger(options: EBrocanteClientOptions): Logger {
106
+ if (options.logger) return options.logger;
107
+ if (options.logPath) return new FileLogger(options.logPath);
108
+ return new NullLogger();
109
+ }
110
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * @copyright 2026 Association E-Cosplay
3
+ * @license Proprietary — see LICENSE
4
+ */
5
+
6
+ /**
7
+ * Minimal cache interface used by the SDK. Implementations are local
8
+ * (in-memory), Redis, or a no-op when caching is disabled.
9
+ */
10
+ export interface CacheBackend {
11
+ get(key: string): Promise<unknown>;
12
+ set(key: string, value: unknown, ttlSeconds?: number): Promise<void>;
13
+ delete(key: string): Promise<void>;
14
+ purge(): Promise<void>;
15
+ /**
16
+ * Bulk purge keys matching a wildcard pattern (`*` at the end). Used
17
+ * by the public `client.cache.purgeKey()` shortcut.
18
+ */
19
+ purgeKey(endpoint: string, params?: Record<string, unknown>): Promise<void>;
20
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * @copyright 2026 Association E-Cosplay
3
+ * @license Proprietary — see LICENSE
4
+ */
5
+
6
+ import type { CacheBackend } from './CacheBackend.js';
7
+ import { stableHashKey } from './stableHashKey.js';
8
+
9
+ interface Entry {
10
+ value: unknown;
11
+ expiresAt: number;
12
+ }
13
+
14
+ /**
15
+ * In-memory cache backend. Lives for the duration of a single Node/Bun
16
+ * process. Suitable for short scripts and single-instance apps. For
17
+ * multi-process deployments use RedisCache.
18
+ */
19
+ export class LocalCache implements CacheBackend {
20
+ private readonly store = new Map<string, Entry>();
21
+
22
+ constructor(
23
+ private readonly defaultTtl: number = 300,
24
+ private readonly keyPrefix: string = '',
25
+ ) {}
26
+
27
+ async get(key: string): Promise<unknown> {
28
+ const fullKey = this.keyPrefix + key;
29
+ const entry = this.store.get(fullKey);
30
+ if (!entry) {
31
+ return null;
32
+ }
33
+ if (entry.expiresAt < Date.now()) {
34
+ this.store.delete(fullKey);
35
+ return null;
36
+ }
37
+ return entry.value;
38
+ }
39
+
40
+ async set(key: string, value: unknown, ttlSeconds?: number): Promise<void> {
41
+ const ttl = ttlSeconds ?? this.defaultTtl;
42
+ this.store.set(this.keyPrefix + key, {
43
+ value,
44
+ expiresAt: Date.now() + ttl * 1000,
45
+ });
46
+ }
47
+
48
+ async delete(key: string): Promise<void> {
49
+ this.store.delete(this.keyPrefix + key);
50
+ }
51
+
52
+ async purge(): Promise<void> {
53
+ this.store.clear();
54
+ }
55
+
56
+ async purgeKey(endpoint: string, params: Record<string, unknown> = {}): Promise<void> {
57
+ const hashed = stableHashKey(endpoint, params);
58
+ for (const key of this.store.keys()) {
59
+ if (key.endsWith(hashed)) {
60
+ this.store.delete(key);
61
+ }
62
+ }
63
+ }
64
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * @copyright 2026 Association E-Cosplay
3
+ * @license Proprietary — see LICENSE
4
+ */
5
+
6
+ import type { CacheBackend } from './CacheBackend.js';
7
+
8
+ /**
9
+ * No-op cache used when the SDK is constructed with `cache: false`.
10
+ * Every request hits the network; nothing is stored.
11
+ */
12
+ export class NullCache implements CacheBackend {
13
+ async get(_key: string): Promise<null> {
14
+ return null;
15
+ }
16
+
17
+ async set(_key: string, _value: unknown, _ttlSeconds?: number): Promise<void> {
18
+ /* intentional no-op */
19
+ }
20
+
21
+ async delete(_key: string): Promise<void> {
22
+ /* intentional no-op */
23
+ }
24
+
25
+ async purge(): Promise<void> {
26
+ /* intentional no-op */
27
+ }
28
+
29
+ async purgeKey(_endpoint: string, _params?: Record<string, unknown>): Promise<void> {
30
+ /* intentional no-op */
31
+ }
32
+ }
@@ -0,0 +1,123 @@
1
+ /**
2
+ * @copyright 2026 Association E-Cosplay
3
+ * @license Proprietary — see LICENSE
4
+ */
5
+
6
+ import { EBrocanteError } from '../exceptions/index.js';
7
+ import type { CacheBackend } from './CacheBackend.js';
8
+ import { DEFAULT_REDIS_CONFIG, type RedisConfig } from './RedisConfig.js';
9
+ import { stableHashKey } from './stableHashKey.js';
10
+
11
+ /**
12
+ * Lightweight Redis client surface required by RedisCache. Compatible
13
+ * with `ioredis`'s public API (and any equivalent client).
14
+ */
15
+ export interface RedisLikeClient {
16
+ get(key: string): Promise<string | null>;
17
+ set(key: string, value: string, mode: 'EX', seconds: number): Promise<unknown>;
18
+ del(...keys: string[]): Promise<number>;
19
+ scan(cursor: string | number, ...args: unknown[]): Promise<[string, string[]]>;
20
+ quit(): Promise<unknown>;
21
+ }
22
+
23
+ /**
24
+ * Redis cache backend. Uses `ioredis` by default. The connection is
25
+ * lazily established on the first get/set and reused for the lifetime
26
+ * of the cache instance.
27
+ */
28
+ export class RedisCache implements CacheBackend {
29
+ private client: RedisLikeClient | null = null;
30
+
31
+ constructor(
32
+ private readonly config: RedisConfig,
33
+ private readonly defaultTtl: number = 300,
34
+ private readonly keyPrefix: string = '',
35
+ private readonly clientFactory?: () => Promise<RedisLikeClient>,
36
+ ) {}
37
+
38
+ async get(key: string): Promise<unknown> {
39
+ const raw = await (await this.connect()).get(this.fullKey(key));
40
+ if (raw === null) return null;
41
+ try {
42
+ return JSON.parse(raw);
43
+ } catch {
44
+ return null;
45
+ }
46
+ }
47
+
48
+ async set(key: string, value: unknown, ttlSeconds?: number): Promise<void> {
49
+ const encoded = JSON.stringify(value);
50
+ await (await this.connect()).set(this.fullKey(key), encoded, 'EX', ttlSeconds ?? this.defaultTtl);
51
+ }
52
+
53
+ async delete(key: string): Promise<void> {
54
+ await (await this.connect()).del(this.fullKey(key));
55
+ }
56
+
57
+ async purge(): Promise<void> {
58
+ const client = await this.connect();
59
+ const pattern = this.fullKey('*');
60
+ let cursor = '0';
61
+ do {
62
+ const [next, keys] = await client.scan(cursor, 'MATCH', pattern, 'COUNT', '100');
63
+ cursor = next;
64
+ if (keys.length > 0) {
65
+ await client.del(...keys);
66
+ }
67
+ } while (cursor !== '0');
68
+ }
69
+
70
+ async purgeKey(endpoint: string, params: Record<string, unknown> = {}): Promise<void> {
71
+ const hashed = stableHashKey(endpoint, params);
72
+ const client = await this.connect();
73
+ const pattern = this.fullKey(`*${hashed}`);
74
+ let cursor = '0';
75
+ do {
76
+ const [next, keys] = await client.scan(cursor, 'MATCH', pattern, 'COUNT', '100');
77
+ cursor = next;
78
+ if (keys.length > 0) {
79
+ await client.del(...keys);
80
+ }
81
+ } while (cursor !== '0');
82
+ }
83
+
84
+ private async connect(): Promise<RedisLikeClient> {
85
+ if (this.client) return this.client;
86
+ /* v8 ignore start — live-Redis fallback path is integration-tested only */
87
+ if (!this.clientFactory) {
88
+ this.client = await this.realConnect();
89
+ return this.client;
90
+ }
91
+ /* v8 ignore stop */
92
+ this.client = await this.clientFactory();
93
+ return this.client;
94
+ }
95
+
96
+ /* v8 ignore start — exercised only against a live Redis instance */
97
+ private async realConnect(): Promise<RedisLikeClient> {
98
+ const cfg = { ...DEFAULT_REDIS_CONFIG, ...this.config };
99
+ let Redis: new (...args: unknown[]) => RedisLikeClient;
100
+ try {
101
+ ({ default: Redis } = (await import('ioredis')) as unknown as {
102
+ default: new (...args: unknown[]) => RedisLikeClient;
103
+ });
104
+ } catch {
105
+ throw new EBrocanteError(
106
+ "RedisCache requires the 'ioredis' package. Install it with: bun add ioredis",
107
+ );
108
+ }
109
+ return new Redis({
110
+ host: cfg.host,
111
+ port: cfg.port,
112
+ password: cfg.password,
113
+ db: cfg.db,
114
+ connectTimeout: cfg.timeoutMs,
115
+ tls: cfg.tls ? {} : undefined,
116
+ });
117
+ }
118
+ /* v8 ignore stop */
119
+
120
+ private fullKey(key: string): string {
121
+ return `${this.config.prefix ?? DEFAULT_REDIS_CONFIG.prefix}${this.keyPrefix}${key}`;
122
+ }
123
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * @copyright 2026 Association E-Cosplay
3
+ * @license Proprietary — see LICENSE
4
+ */
5
+
6
+ export interface RedisConfig {
7
+ host?: string;
8
+ port?: number;
9
+ password?: string;
10
+ db?: number;
11
+ prefix?: string;
12
+ timeoutMs?: number;
13
+ tls?: boolean;
14
+ }
15
+
16
+ export const DEFAULT_REDIS_CONFIG: Required<Omit<RedisConfig, 'password'>> & { password?: string } = {
17
+ host: '127.0.0.1',
18
+ port: 6379,
19
+ db: 0,
20
+ prefix: 'ebrocante:',
21
+ timeoutMs: 1500,
22
+ tls: false,
23
+ };
@@ -0,0 +1,28 @@
1
+ /**
2
+ * @copyright 2026 Association E-Cosplay
3
+ * @license Proprietary — see LICENSE
4
+ */
5
+
6
+ import { createHash } from 'node:crypto';
7
+ import { slashesToDots, stringifyPrimitive, trimSlashes } from '../internal/safeStrings.js';
8
+
9
+ /**
10
+ * Deterministic, key-order-independent hash for cache lookups.
11
+ * Mirrors the PHP SDK's makeCacheKey() so both clients can share
12
+ * a Redis instance without collisions.
13
+ */
14
+ export function stableHashKey(endpoint: string, params: Record<string, unknown>): string {
15
+ const sortedKeys = Object.keys(params).sort((a, b) => a.localeCompare(b, 'en'));
16
+ const queryString = sortedKeys
17
+ .map((k) => {
18
+ const v = params[k];
19
+ if (v === undefined || v === null || v === '') return null;
20
+ return `${encodeURIComponent(k)}=${encodeURIComponent(stringifyPrimitive(v))}`;
21
+ })
22
+ .filter((s): s is string => s !== null)
23
+ .join('&');
24
+
25
+ const cleanEndpoint = slashesToDots(trimSlashes(endpoint));
26
+ const digest = createHash('sha256').update(queryString).digest('hex').slice(0, 32);
27
+ return `${cleanEndpoint}:${digest}`;
28
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * @copyright 2026 Association E-Cosplay
3
+ * @license Proprietary — see LICENSE
4
+ *
5
+ * Typed exception hierarchy. Catch `EBrocanteError` to handle any SDK
6
+ * failure, or a more specific subclass for finer control.
7
+ */
8
+
9
+ export class EBrocanteError extends Error {
10
+ constructor(message: string, options?: ErrorOptions) {
11
+ super(message, options);
12
+ this.name = this.constructor.name;
13
+ Object.setPrototypeOf(this, new.target.prototype);
14
+ }
15
+ }
16
+
17
+ /** HTTP 401 / 403 — invalid key, wrong scope, or mode mismatch. */
18
+ export class AuthenticationError extends EBrocanteError {
19
+ constructor(
20
+ message: string,
21
+ readonly status: number,
22
+ options?: ErrorOptions,
23
+ ) {
24
+ super(message, options);
25
+ }
26
+ }
27
+
28
+ /** HTTP 404 / 410 — resource missing or gone. */
29
+ export class NotFoundError extends EBrocanteError {
30
+ constructor(
31
+ message: string,
32
+ readonly status: number,
33
+ options?: ErrorOptions,
34
+ ) {
35
+ super(message, options);
36
+ }
37
+ }
38
+
39
+ /** HTTP 422 — request schema rejected. Carries the raw error body. */
40
+ export class ValidationError extends EBrocanteError {
41
+ constructor(
42
+ message: string,
43
+ readonly status: number,
44
+ readonly body: string,
45
+ options?: ErrorOptions,
46
+ ) {
47
+ super(message, options);
48
+ }
49
+ }
50
+
51
+ /** HTTP 429 — rate limit exhausted. `retryAfter` in seconds. */
52
+ export class RateLimitError extends EBrocanteError {
53
+ constructor(
54
+ message: string,
55
+ readonly status: number,
56
+ readonly retryAfter: number,
57
+ options?: ErrorOptions,
58
+ ) {
59
+ super(message, options);
60
+ }
61
+ }
62
+
63
+ /** HTTP 5xx (or any unmapped status) once retries have been exhausted. */
64
+ export class ServerError extends EBrocanteError {
65
+ constructor(
66
+ message: string,
67
+ readonly status: number,
68
+ options?: ErrorOptions,
69
+ ) {
70
+ super(message, options);
71
+ }
72
+ }
73
+
74
+ /** Transport-level failure (DNS, TCP timeout, broken pipe, abort, …). */
75
+ export class NetworkError extends EBrocanteError {}