@thalorlabs/chucknorris 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/.env.example ADDED
@@ -0,0 +1,6 @@
1
+ # @thalorlabs/chucknorris
2
+ # No environment variables required — api.chucknorris.io is free and unauthenticated.
3
+ # This file exists for consistency with other provider packages.
4
+
5
+ # Optional: Override the API base URL (e.g. for testing with a mock server)
6
+ # CHUCK_NORRIS_BASE_URL=https://api.chucknorris.io
package/.prettierrc ADDED
@@ -0,0 +1,7 @@
1
+ {
2
+ "semi": true,
3
+ "singleQuote": true,
4
+ "trailingComma": "es5",
5
+ "printWidth": 80,
6
+ "tabWidth": 2
7
+ }
package/CLAUDE.md ADDED
@@ -0,0 +1,52 @@
1
+ # @thalorlabs/chucknorris
2
+
3
+ ## What this repo is
4
+
5
+ Provider adapter package for api.chucknorris.io. Calls one external API, normalises the response to `TLJoke`, and returns it. No DB, no cache, no logging.
6
+
7
+ ## Knowledge base
8
+
9
+ Before writing any code, read:
10
+ - TL-Coding vault: `knowledge/multi-provider-pattern.md`
11
+ - TL-Coding vault: `examples/jokes/layer-provider-package.md`
12
+ - TL-Coding vault: `context/packages.md`
13
+
14
+ ## External API
15
+
16
+ - **API:** api.chucknorris.io
17
+ - **Auth:** None
18
+ - **Billable:** No
19
+ - **Joke type:** Always SINGLE (one-liner)
20
+ - **Category:** Always GENERAL
21
+
22
+ ## Build & publish
23
+
24
+ ```bash
25
+ npm run build # tsc → dist/
26
+ npm test # vitest
27
+ npm version patch # or minor/major
28
+ npm publish --access public
29
+ ```
30
+
31
+ ## Folder structure
32
+
33
+ ```
34
+ src/
35
+ index.ts ← exports ChuckNorrisClient, CHUCK_NORRIS_CONFIG
36
+ ChuckNorrisClient.ts ← HTTP adapter, normalises to TLJoke
37
+ ChuckNorrisTypes.ts ← raw API types, local only, never exported
38
+ tests/
39
+ ChuckNorrisClient.test.ts
40
+ ```
41
+
42
+ ## What this package does NOT do
43
+
44
+ - No database access
45
+ - No caching (orchestrator handles this)
46
+ - No logging (orchestrator handles this)
47
+ - Raw types (`ChuckNorrisRawResponse`) never exported, never in `@thalorlabs/types`
48
+ - No provider selection logic — that's the orchestrator's job
49
+
50
+ ## Migration note
51
+
52
+ When `@thalorlabs/api` is published, `ChuckNorrisClient` should extend `BaseHttpClient` instead of managing its own axios instance.
package/README.md ADDED
@@ -0,0 +1,44 @@
1
+ # @thalorlabs/chucknorris
2
+
3
+ Provider adapter for [api.chucknorris.io](https://api.chucknorris.io). Returns normalised `TLJoke` from `@thalorlabs/types`.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @thalorlabs/chucknorris
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```typescript
14
+ import { ChuckNorrisClient, CHUCK_NORRIS_CONFIG } from '@thalorlabs/chucknorris';
15
+
16
+ const client = new ChuckNorrisClient();
17
+ const joke = await client.getJoke();
18
+ // → TLJoke { id, type: 'SINGLE', joke: '...', category: 'GENERAL', safe: false, provider: 'chucknorris' }
19
+ ```
20
+
21
+ ## Configuration
22
+
23
+ | Option | Default | Description |
24
+ |--------|---------|-------------|
25
+ | `baseURL` | `https://api.chucknorris.io` | API base URL |
26
+ | `timeout` | `10000` | Request timeout (ms) |
27
+
28
+ ## Exported Config
29
+
30
+ ```typescript
31
+ CHUCK_NORRIS_CONFIG = {
32
+ cacheTtlMs: 3600000, // 1h
33
+ isBillable: false,
34
+ }
35
+ ```
36
+
37
+ ## Scripts
38
+
39
+ ```bash
40
+ npm run build # tsc → dist/
41
+ npm test # vitest
42
+ npm run lint # eslint
43
+ npm run format:check # prettier
44
+ ```
@@ -0,0 +1,21 @@
1
+ import { TLJoke } from '@thalorlabs/types';
2
+ export interface ChuckNorrisClientConfig {
3
+ baseURL?: string;
4
+ timeout?: number;
5
+ }
6
+ /**
7
+ * HTTP adapter for api.chucknorris.io.
8
+ *
9
+ * Calls the external API and normalises the raw response to TLJoke.
10
+ * No DB, no cache, no logging — pure HTTP adapter.
11
+ *
12
+ * When @thalorlabs/api is available, this should extend BaseHttpClient
13
+ * instead of managing its own axios instance.
14
+ */
15
+ export declare class ChuckNorrisClient {
16
+ readonly serviceName = "chucknorris";
17
+ private readonly axiosInstance;
18
+ constructor(config?: ChuckNorrisClientConfig);
19
+ getJoke(): Promise<TLJoke>;
20
+ private normalise;
21
+ }
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.ChuckNorrisClient = void 0;
7
+ const axios_1 = __importDefault(require("axios"));
8
+ const crypto_1 = require("crypto");
9
+ const types_1 = require("@thalorlabs/types");
10
+ /**
11
+ * HTTP adapter for api.chucknorris.io.
12
+ *
13
+ * Calls the external API and normalises the raw response to TLJoke.
14
+ * No DB, no cache, no logging — pure HTTP adapter.
15
+ *
16
+ * When @thalorlabs/api is available, this should extend BaseHttpClient
17
+ * instead of managing its own axios instance.
18
+ */
19
+ class ChuckNorrisClient {
20
+ constructor(config = {}) {
21
+ this.serviceName = 'chucknorris';
22
+ this.axiosInstance = axios_1.default.create({
23
+ baseURL: config.baseURL ?? 'https://api.chucknorris.io',
24
+ timeout: config.timeout ?? 10000,
25
+ headers: {
26
+ Accept: 'application/json',
27
+ },
28
+ });
29
+ }
30
+ async getJoke() {
31
+ const { data } = await this.axiosInstance.get('/jokes/random');
32
+ return this.normalise(data);
33
+ }
34
+ normalise(raw) {
35
+ return types_1.TLJoke.parse({
36
+ id: (0, crypto_1.createHash)('md5').update(raw.value).digest('hex'),
37
+ type: types_1.EJokeType.SINGLE,
38
+ joke: raw.value,
39
+ category: types_1.EJokeCategory.GENERAL,
40
+ safe: false, // Chuck Norris jokes can contain explicit content
41
+ provider: this.serviceName,
42
+ });
43
+ }
44
+ }
45
+ exports.ChuckNorrisClient = ChuckNorrisClient;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Raw response from api.chucknorris.io.
3
+ *
4
+ * Internal to this package only — never exported from index.ts,
5
+ * never added to @thalorlabs/types.
6
+ */
7
+ export interface ChuckNorrisRawResponse {
8
+ icon_url: string;
9
+ id: string;
10
+ url: string;
11
+ value: string;
12
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,5 @@
1
+ export { ChuckNorrisClient, ChuckNorrisClientConfig, } from './ChuckNorrisClient';
2
+ export declare const CHUCK_NORRIS_CONFIG: {
3
+ readonly cacheTtlMs: number;
4
+ readonly isBillable: false;
5
+ };
package/dist/index.js ADDED
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CHUCK_NORRIS_CONFIG = exports.ChuckNorrisClient = void 0;
4
+ var ChuckNorrisClient_1 = require("./ChuckNorrisClient");
5
+ Object.defineProperty(exports, "ChuckNorrisClient", { enumerable: true, get: function () { return ChuckNorrisClient_1.ChuckNorrisClient; } });
6
+ exports.CHUCK_NORRIS_CONFIG = {
7
+ cacheTtlMs: 1000 * 60 * 60, // 1h
8
+ isBillable: false, // api.chucknorris.io is free
9
+ };
@@ -0,0 +1,21 @@
1
+ import eslint from '@eslint/js';
2
+ import tseslint from 'typescript-eslint';
3
+ import prettier from 'eslint-config-prettier';
4
+ import nodePlugin from 'eslint-plugin-n';
5
+
6
+ export default tseslint.config(
7
+ eslint.configs.recommended,
8
+ ...tseslint.configs.recommended,
9
+ nodePlugin.configs['flat/recommended-module'],
10
+ prettier,
11
+ {
12
+ rules: {
13
+ '@typescript-eslint/no-explicit-any': 'error',
14
+ 'n/no-missing-import': 'off',
15
+ 'n/no-unpublished-import': 'off',
16
+ },
17
+ },
18
+ {
19
+ ignores: ['dist/', 'node_modules/'],
20
+ }
21
+ );
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@thalorlabs/chucknorris",
3
+ "author": "ThalorLabs",
4
+ "private": false,
5
+ "version": "1.0.0",
6
+ "description": "Provider adapter for api.chucknorris.io — returns TLJoke",
7
+ "homepage": "https://github.com/ThalorLabs/chucknorris#readme",
8
+ "bugs": {
9
+ "url": "https://github.com/ThalorLabs/chucknorris/issues"
10
+ },
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/ThalorLabs/chucknorris.git"
14
+ },
15
+ "license": "ISC",
16
+ "main": "dist/index.js",
17
+ "types": "dist/index.d.ts",
18
+ "scripts": {
19
+ "test": "vitest run",
20
+ "test:watch": "vitest",
21
+ "build": "tsc",
22
+ "clean": "rimraf dist",
23
+ "prebuild": "npm run clean",
24
+ "prepublishOnly": "npm run build",
25
+ "lint": "eslint src/",
26
+ "lint:fix": "eslint src/ --fix",
27
+ "format": "prettier --write \"src/**/*.ts\"",
28
+ "format:check": "prettier --check \"src/**/*.ts\""
29
+ },
30
+ "devDependencies": {
31
+ "@eslint/js": "^10.0.1",
32
+ "@types/node": "^25.5.2",
33
+ "eslint": "^10.1.0",
34
+ "eslint-config-prettier": "^10.1.8",
35
+ "eslint-plugin-n": "^17.24.0",
36
+ "prettier": "^3.8.1",
37
+ "rimraf": "^5.0.0",
38
+ "typescript": "^5.0.0",
39
+ "typescript-eslint": "^8.58.0",
40
+ "vitest": "^3.0.0"
41
+ },
42
+ "dependencies": {
43
+ "@thalorlabs/types": "^1.7.0",
44
+ "axios": "^1.7.0"
45
+ }
46
+ }
@@ -0,0 +1,50 @@
1
+ import axios, { AxiosInstance } from 'axios';
2
+ import { createHash } from 'crypto';
3
+ import { TLJoke, EJokeType, EJokeCategory } from '@thalorlabs/types';
4
+ import { ChuckNorrisRawResponse } from './ChuckNorrisTypes';
5
+
6
+ export interface ChuckNorrisClientConfig {
7
+ baseURL?: string;
8
+ timeout?: number;
9
+ }
10
+
11
+ /**
12
+ * HTTP adapter for api.chucknorris.io.
13
+ *
14
+ * Calls the external API and normalises the raw response to TLJoke.
15
+ * No DB, no cache, no logging — pure HTTP adapter.
16
+ *
17
+ * When @thalorlabs/api is available, this should extend BaseHttpClient
18
+ * instead of managing its own axios instance.
19
+ */
20
+ export class ChuckNorrisClient {
21
+ public readonly serviceName = 'chucknorris';
22
+ private readonly axiosInstance: AxiosInstance;
23
+
24
+ constructor(config: ChuckNorrisClientConfig = {}) {
25
+ this.axiosInstance = axios.create({
26
+ baseURL: config.baseURL ?? 'https://api.chucknorris.io',
27
+ timeout: config.timeout ?? 10000,
28
+ headers: {
29
+ Accept: 'application/json',
30
+ },
31
+ });
32
+ }
33
+
34
+ async getJoke(): Promise<TLJoke> {
35
+ const { data } =
36
+ await this.axiosInstance.get<ChuckNorrisRawResponse>('/jokes/random');
37
+ return this.normalise(data);
38
+ }
39
+
40
+ private normalise(raw: ChuckNorrisRawResponse): TLJoke {
41
+ return TLJoke.parse({
42
+ id: createHash('md5').update(raw.value).digest('hex'),
43
+ type: EJokeType.SINGLE,
44
+ joke: raw.value,
45
+ category: EJokeCategory.GENERAL,
46
+ safe: false, // Chuck Norris jokes can contain explicit content
47
+ provider: this.serviceName,
48
+ });
49
+ }
50
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Raw response from api.chucknorris.io.
3
+ *
4
+ * Internal to this package only — never exported from index.ts,
5
+ * never added to @thalorlabs/types.
6
+ */
7
+ export interface ChuckNorrisRawResponse {
8
+ icon_url: string;
9
+ id: string;
10
+ url: string;
11
+ value: string;
12
+ }
package/src/index.ts ADDED
@@ -0,0 +1,9 @@
1
+ export {
2
+ ChuckNorrisClient,
3
+ ChuckNorrisClientConfig,
4
+ } from './ChuckNorrisClient';
5
+
6
+ export const CHUCK_NORRIS_CONFIG = {
7
+ cacheTtlMs: 1000 * 60 * 60, // 1h
8
+ isBillable: false, // api.chucknorris.io is free
9
+ } as const;
@@ -0,0 +1,121 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { ChuckNorrisClient } from '../src/ChuckNorrisClient';
3
+ import { EJokeType, EJokeCategory } from '@thalorlabs/types';
4
+ import axios from 'axios';
5
+
6
+ vi.mock('axios', () => {
7
+ const mockAxiosInstance = {
8
+ get: vi.fn(),
9
+ };
10
+ return {
11
+ default: {
12
+ create: vi.fn(() => mockAxiosInstance),
13
+ },
14
+ };
15
+ });
16
+
17
+ function getMockAxios() {
18
+ const instance = (axios.create as ReturnType<typeof vi.fn>).mock.results[0]
19
+ .value;
20
+ return instance.get as ReturnType<typeof vi.fn>;
21
+ }
22
+
23
+ describe('ChuckNorrisClient', () => {
24
+ let client: ChuckNorrisClient;
25
+
26
+ beforeEach(() => {
27
+ vi.clearAllMocks();
28
+ client = new ChuckNorrisClient();
29
+ });
30
+
31
+ describe('constructor', () => {
32
+ it('sets serviceName to chucknorris', () => {
33
+ expect(client.serviceName).toBe('chucknorris');
34
+ });
35
+
36
+ it('creates axios instance with default config', () => {
37
+ expect(axios.create).toHaveBeenCalledWith({
38
+ baseURL: 'https://api.chucknorris.io',
39
+ timeout: 10000,
40
+ headers: { Accept: 'application/json' },
41
+ });
42
+ });
43
+
44
+ it('accepts custom baseURL and timeout', () => {
45
+ vi.clearAllMocks();
46
+ new ChuckNorrisClient({
47
+ baseURL: 'http://localhost:3000',
48
+ timeout: 5000,
49
+ });
50
+ expect(axios.create).toHaveBeenCalledWith({
51
+ baseURL: 'http://localhost:3000',
52
+ timeout: 5000,
53
+ headers: { Accept: 'application/json' },
54
+ });
55
+ });
56
+ });
57
+
58
+ describe('getJoke', () => {
59
+ it('returns a valid TLJoke with SINGLE type and GENERAL category', async () => {
60
+ const mockGet = getMockAxios();
61
+ mockGet.mockResolvedValueOnce({
62
+ data: {
63
+ icon_url: 'https://api.chucknorris.io/img/avatar/chuck-norris.png',
64
+ id: 'abc123',
65
+ url: '',
66
+ value: 'Chuck Norris can divide by zero.',
67
+ },
68
+ });
69
+
70
+ const joke = await client.getJoke();
71
+
72
+ expect(joke.type).toBe(EJokeType.SINGLE);
73
+ expect(joke.category).toBe(EJokeCategory.GENERAL);
74
+ expect(joke.joke).toBe('Chuck Norris can divide by zero.');
75
+ expect(joke.safe).toBe(false);
76
+ expect(joke.provider).toBe('chucknorris');
77
+ expect(joke.id).toBeDefined();
78
+ });
79
+
80
+ it('generates a deterministic id from joke text', async () => {
81
+ const mockGet = getMockAxios();
82
+ const jokeText = 'Test joke';
83
+ mockGet.mockResolvedValue({
84
+ data: {
85
+ icon_url: 'https://example.com/icon.png',
86
+ id: 'raw-id',
87
+ url: '',
88
+ value: jokeText,
89
+ },
90
+ });
91
+
92
+ const joke1 = await client.getJoke();
93
+ const joke2 = await client.getJoke();
94
+
95
+ expect(joke1.id).toBe(joke2.id);
96
+ });
97
+
98
+ it('calls GET /jokes/random on the API', async () => {
99
+ const mockGet = getMockAxios();
100
+ mockGet.mockResolvedValueOnce({
101
+ data: {
102
+ icon_url: '',
103
+ id: 'abc',
104
+ url: '',
105
+ value: 'A joke',
106
+ },
107
+ });
108
+
109
+ await client.getJoke();
110
+
111
+ expect(mockGet).toHaveBeenCalledWith('/jokes/random');
112
+ });
113
+
114
+ it('propagates API errors', async () => {
115
+ const mockGet = getMockAxios();
116
+ mockGet.mockRejectedValueOnce(new Error('Network error'));
117
+
118
+ await expect(client.getJoke()).rejects.toThrow('Network error');
119
+ });
120
+ });
121
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "CommonJS",
5
+ "declaration": true,
6
+ "outDir": "./dist",
7
+ "strict": true,
8
+ "esModuleInterop": true,
9
+ "skipLibCheck": true,
10
+ "moduleResolution": "node"
11
+ },
12
+ "include": ["src"],
13
+ "exclude": ["node_modules", "dist"]
14
+ }