@codelockpro/sdk 0.0.1 → 0.1.11

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.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"kb.d.ts","sourceRoot":"","sources":["../../src/modules/kb.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,86 @@
1
+ function _qs(params) {
2
+ const usp = new URLSearchParams();
3
+ for (const [k, v] of Object.entries(params)) {
4
+ if (v === undefined || v === null || v === "")
5
+ continue;
6
+ usp.set(k, String(v));
7
+ }
8
+ const s = usp.toString();
9
+ return s ? `?${s}` : "";
10
+ }
11
+ /**
12
+ * Module factory. Conforms to {@link ModuleFactory} — the SDK core treats
13
+ * KB exactly like any other module.
14
+ *
15
+ * const client = new CodeLockPro({
16
+ * baseUrl: "https://example.com/my-api",
17
+ * modules: [["kb", createKbModule]],
18
+ * });
19
+ */
20
+ export const createKbModule = (ctx) => {
21
+ return _create(ctx, {});
22
+ };
23
+ /** Variant that accepts module-specific options (e.g. a custom basePath). */
24
+ export function createKbModuleWith(options) {
25
+ return (ctx) => _create(ctx, options);
26
+ }
27
+ function _create(ctx, options) {
28
+ const base = (options.basePath ?? "/kb").replace(/\/+$/, "");
29
+ const ns = ctx.name; // "kb" by convention; whatever the host registered as.
30
+ function _ev(name) {
31
+ return `${ns}.${name}`;
32
+ }
33
+ return {
34
+ async getArticles(opts = {}) {
35
+ const qs = _qs({
36
+ category_id: opts.categoryId,
37
+ category_slug: opts.categorySlug,
38
+ limit: opts.limit,
39
+ starting_after: opts.startingAfter,
40
+ });
41
+ return ctx.request(`${base}/articles${qs}`);
42
+ },
43
+ async getArticle(idOrSlug) {
44
+ if (typeof idOrSlug !== "string" || idOrSlug.length === 0) {
45
+ return Promise.reject(new Error("getArticle: idOrSlug required"));
46
+ }
47
+ return ctx.request(`${base}/articles/${encodeURIComponent(idOrSlug)}`);
48
+ },
49
+ async getCategories() {
50
+ return ctx.request(`${base}/categories`);
51
+ },
52
+ async search(query, limit) {
53
+ const result = await ctx.request(`${base}/search${_qs({ q: query, limit })}`);
54
+ ctx.bus.emit(_ev("search.performed"), {
55
+ query,
56
+ count: Array.isArray(result?.articles) ? result.articles.length : 0,
57
+ });
58
+ return result;
59
+ },
60
+ async trackView(articleId, opts = {}) {
61
+ if (typeof articleId !== "string" || articleId.length === 0) {
62
+ throw new Error("trackView: articleId required");
63
+ }
64
+ // Bus event fires synchronously so host apps update immediately.
65
+ ctx.bus.emit(_ev("article.viewed"), {
66
+ articleId,
67
+ slug: opts.slug,
68
+ });
69
+ // Fire-and-forget on the wire: do **not** await, so rendering
70
+ // is never blocked, and swallow errors so view-tracking is
71
+ // never user-visible.
72
+ void ctx
73
+ .request(`${base}/articles/${encodeURIComponent(articleId)}/track-view`, { method: "POST" })
74
+ .catch(() => {
75
+ /* swallow — see docstring */
76
+ });
77
+ },
78
+ on(event, handler) {
79
+ return ctx.bus.on(_ev(event), handler);
80
+ },
81
+ off(event, handler) {
82
+ ctx.bus.off(_ev(event), handler);
83
+ },
84
+ };
85
+ }
86
+ //# sourceMappingURL=kb.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"kb.js","sourceRoot":"","sources":["../../src/modules/kb.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,46 @@
1
1
  {
2
- "name": "@codelockpro/sdk",
3
- "version": "0.0.1",
4
- "private": false
2
+ "name": "@codelockpro/sdk",
3
+ "version": "0.1.11",
4
+ "description": "Framework-agnostic, modular client SDK for CodeLockPro. Module one: Knowledge base. Configured with the developer's own base URL — never communicates directly with the CodeLockPro API.",
5
+ "license": "SEE LICENSE IN LICENSE",
6
+ "homepage": "https://github.com/mbos01/codelockpro/tree/main/sdk/js#readme",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/mbos01/codelockpro.git",
10
+ "directory": "sdk/js"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/mbos01/codelockpro/issues"
14
+ },
15
+ "type": "module",
16
+ "main": "./dist/index.cjs",
17
+ "module": "./dist/index.mjs",
18
+ "types": "./dist/index.d.ts",
19
+ "exports": {
20
+ ".": {
21
+ "types": "./dist/index.d.ts",
22
+ "import": "./dist/index.mjs",
23
+ "require": "./dist/index.cjs"
24
+ }
25
+ },
26
+ "files": [
27
+ "dist",
28
+ "src",
29
+ "README.md",
30
+ "LICENSE"
31
+ ],
32
+ "scripts": {
33
+ "build": "tsc -p tsconfig.json",
34
+ "test": "node --test"
35
+ },
36
+ "keywords": [
37
+ "codelockpro",
38
+ "sdk",
39
+ "knowledge-base",
40
+ "licensing"
41
+ ],
42
+ "engines": {
43
+ "node": ">=18"
44
+ },
45
+ "sideEffects": false
5
46
  }
6
-
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Tiny synchronous event bus shared by all SDK modules.
3
+ *
4
+ * The bus is intentionally minimal — no priority, no async fan-out, no
5
+ * wildcard subscriptions — because the SDK is a thin data/action layer
6
+ * that the host app (Vue, React, Next.js, plain DOM) decorates with its
7
+ * own state-management primitives. Modules emit semantic namespaced
8
+ * events (e.g. ``kb.article.viewed``) and host code subscribes via
9
+ * :meth:`CodeLockPro.on`.
10
+ *
11
+ * The bus has no awareness of which module emits what — it is a
12
+ * module-agnostic primitive owned by the core.
13
+ */
14
+ export type EventHandler<T = unknown> = (payload: T) => void;
15
+
16
+ export interface EventBus {
17
+ on<T = unknown>(event: string, handler: EventHandler<T>): () => void;
18
+ off<T = unknown>(event: string, handler: EventHandler<T>): void;
19
+ emit<T = unknown>(event: string, payload?: T): void;
20
+ }
21
+
22
+ export function createEventBus(): EventBus {
23
+ const handlers = new Map<string, Set<EventHandler>>();
24
+
25
+ function on<T>(event: string, handler: EventHandler<T>): () => void {
26
+ if (typeof event !== "string" || event.length === 0) {
27
+ throw new Error("CodeLockPro.on: event name is required");
28
+ }
29
+ if (typeof handler !== "function") {
30
+ throw new Error("CodeLockPro.on: handler must be a function");
31
+ }
32
+ let set = handlers.get(event);
33
+ if (!set) {
34
+ set = new Set();
35
+ handlers.set(event, set);
36
+ }
37
+ set.add(handler as EventHandler);
38
+ return () => off(event, handler);
39
+ }
40
+
41
+ function off<T>(event: string, handler: EventHandler<T>): void {
42
+ const set = handlers.get(event);
43
+ if (!set) return;
44
+ set.delete(handler as EventHandler);
45
+ if (set.size === 0) handlers.delete(event);
46
+ }
47
+
48
+ function emit<T>(event: string, payload?: T): void {
49
+ const set = handlers.get(event);
50
+ if (!set || set.size === 0) return;
51
+ // Snapshot so handlers may unsubscribe themselves safely.
52
+ for (const h of Array.from(set)) {
53
+ try {
54
+ h(payload);
55
+ } catch (err) {
56
+ // Never let one handler break another. Surface to the
57
+ // host via ``console.error`` rather than swallowing.
58
+ // eslint-disable-next-line no-console
59
+ console.error(`CodeLockPro: handler for "${event}" threw`, err);
60
+ }
61
+ }
62
+ }
63
+
64
+ return { on, off, emit };
65
+ }
@@ -0,0 +1,62 @@
1
+ import { EventBus } from "./events.js";
2
+
3
+ /**
4
+ * Module contract.
5
+ *
6
+ * A module is anything the host code wants to attach to a
7
+ * :class:`CodeLockPro` instance. The core does not know about any
8
+ * specific module; modules are produced by user-supplied factories.
9
+ *
10
+ * A factory receives the {@link ModuleContext} and returns the
11
+ * module's public surface (an object). The shape is entirely up to the
12
+ * module author — by convention it exposes data accessors
13
+ * (``getArticles``), actions (``trackView``), and emits semantic
14
+ * events on the shared bus.
15
+ */
16
+ export interface ModuleContext {
17
+ /** Module name as registered (e.g. ``"kb"``). */
18
+ readonly name: string;
19
+ /** HTTP helper bound to the developer's own ``baseUrl``. */
20
+ request<T = unknown>(path: string, init?: RequestInit): Promise<T>;
21
+ /** Shared event bus. Modules namespace their events as ``"<name>.…"``. */
22
+ readonly bus: EventBus;
23
+ }
24
+
25
+ export type ModuleFactory<T = unknown> = (ctx: ModuleContext) => T;
26
+
27
+ /**
28
+ * Internal module registry. Exposed via :meth:`CodeLockPro.register` and
29
+ * :meth:`CodeLockPro.module`.
30
+ */
31
+ export class ModuleRegistry {
32
+ private readonly _modules = new Map<string, unknown>();
33
+
34
+ register<T>(name: string, instance: T): T {
35
+ if (typeof name !== "string" || name.length === 0) {
36
+ throw new Error("CodeLockPro.register: module name is required");
37
+ }
38
+ if (this._modules.has(name)) {
39
+ throw new Error(
40
+ `CodeLockPro.register: module "${name}" is already registered`,
41
+ );
42
+ }
43
+ this._modules.set(name, instance);
44
+ return instance;
45
+ }
46
+
47
+ get<T>(name: string): T {
48
+ const mod = this._modules.get(name);
49
+ if (mod === undefined) {
50
+ throw new Error(`CodeLockPro.module: no module registered as "${name}"`);
51
+ }
52
+ return mod as T;
53
+ }
54
+
55
+ has(name: string): boolean {
56
+ return this._modules.has(name);
57
+ }
58
+
59
+ names(): string[] {
60
+ return Array.from(this._modules.keys());
61
+ }
62
+ }
package/src/index.ts ADDED
@@ -0,0 +1,182 @@
1
+ /**
2
+ * `@codelockpro/sdk` — the CodeLockPro client framework.
3
+ *
4
+ * A generic, modular foundation developers use to build a complete
5
+ * user experience on their own site by composing CodeLockPro modules.
6
+ * The package ships with a single built-in module (``kb`` — knowledge
7
+ * base) and exposes the same registration surface for any additional
8
+ * modules the developer authors or that ship in future package
9
+ * versions.
10
+ *
11
+ * The core in this file has no knowledge of any specific module.
12
+ * Modules are attached at construction time (or later via
13
+ * :meth:`register`) and are looked up by name through :meth:`module`.
14
+ * Each module gets a shared {@link EventBus} and an HTTP helper bound
15
+ * to the developer's own backend URL.
16
+ *
17
+ * Architecture invariants (see /docs/sdk/README.md):
18
+ *
19
+ * 1. **Modular foundation.** No coupling to any specific module.
20
+ * 2. **Server-mediated access only.** The client SDK is configured
21
+ * with the developer's *own* base URL. It must never talk
22
+ * directly to the CodeLockPro API.
23
+ * 3. **Plain vanilla.** No framework binding. Hosts (Vue / React /
24
+ * Next.js / plain DOM) bind their own state on top of the SDK's
25
+ * data, events, and actions.
26
+ */
27
+ import { createEventBus, EventBus, EventHandler } from "./core/events.js";
28
+ import { ModuleContext, ModuleFactory, ModuleRegistry } from "./core/module.js";
29
+ import { createKbModule, KnowledgeBaseModule } from "./modules/kb.js";
30
+
31
+ export interface CodeLockProConfig {
32
+ /**
33
+ * Base URL of the developer's own backend, e.g.
34
+ * ``https://example.com/my-api``. Module paths are appended to it
35
+ * (``/kb/articles`` etc.). Direct access to ``https://api.codelock.pro``
36
+ * is intentionally not supported — see invariant #2.
37
+ */
38
+ baseUrl: string;
39
+ /**
40
+ * Modules to register at construction time. Use ``false`` to skip the
41
+ * default registration (KB) and register everything explicitly via
42
+ * :meth:`register`.
43
+ *
44
+ * Each entry is a ``[name, factory]`` tuple — exactly the same shape
45
+ * the registry uses internally — so the same surface registers any
46
+ * future module the same way as KB:
47
+ *
48
+ * new CodeLockPro({
49
+ * baseUrl: "…",
50
+ * modules: [
51
+ * ["kb", createKbModule],
52
+ * ["checkout", createCheckoutModule],
53
+ * ],
54
+ * });
55
+ */
56
+ modules?: false | Array<[string, ModuleFactory<unknown>]>;
57
+ /** Optional fetch implementation override (tests / Node 16). */
58
+ fetch?: typeof fetch;
59
+ /** Optional default headers merged into every request. */
60
+ headers?: Record<string, string>;
61
+ }
62
+
63
+ export class CodeLockPro {
64
+ readonly baseUrl: string;
65
+ private readonly _fetch: typeof fetch;
66
+ private readonly _headers: Record<string, string>;
67
+ private readonly _bus: EventBus;
68
+ private readonly _registry: ModuleRegistry;
69
+
70
+ constructor(config: CodeLockProConfig) {
71
+ if (!config || typeof config.baseUrl !== "string" || !config.baseUrl) {
72
+ throw new Error("CodeLockPro: baseUrl is required");
73
+ }
74
+ this.baseUrl = config.baseUrl.replace(/\/+$/, "");
75
+ this._fetch = config.fetch ?? globalThis.fetch;
76
+ this._headers = config.headers ?? {};
77
+ if (typeof this._fetch !== "function") {
78
+ throw new Error("CodeLockPro: no fetch implementation available");
79
+ }
80
+ this._bus = createEventBus();
81
+ this._registry = new ModuleRegistry();
82
+
83
+ // Default registration: KB is module one. Pass ``modules: false``
84
+ // (or override with an explicit list) to opt out — the core
85
+ // itself has no knowledge-base coupling.
86
+ const defaultModules: Array<[string, ModuleFactory<unknown>]> = [
87
+ ["kb", createKbModule],
88
+ ];
89
+ const toRegister: Array<[string, ModuleFactory<unknown>]> =
90
+ config.modules === false ? [] : config.modules ?? defaultModules;
91
+
92
+ for (const [name, factory] of toRegister) {
93
+ this.register(name, factory);
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Register a module after construction. Modules can be defined by
99
+ * any caller — KB, future first-party modules, and even host-app
100
+ * extensions all use the same entry point.
101
+ */
102
+ register<T>(name: string, factory: ModuleFactory<T>): T {
103
+ const ctx: ModuleContext = {
104
+ name,
105
+ request: this._request.bind(this),
106
+ bus: this._bus,
107
+ };
108
+ const instance = factory(ctx);
109
+ return this._registry.register(name, instance);
110
+ }
111
+
112
+ /** Look up a previously registered module by name. */
113
+ module<T = unknown>(name: string): T {
114
+ return this._registry.get<T>(name);
115
+ }
116
+
117
+ /** Names of every currently-registered module. */
118
+ moduleNames(): string[] {
119
+ return this._registry.names();
120
+ }
121
+
122
+ /** Subscribe to an event on the shared bus. Returns an unsubscribe fn. */
123
+ on<T = unknown>(event: string, handler: EventHandler<T>): () => void {
124
+ return this._bus.on(event, handler);
125
+ }
126
+
127
+ off<T = unknown>(event: string, handler: EventHandler<T>): void {
128
+ this._bus.off(event, handler);
129
+ }
130
+
131
+ emit<T = unknown>(event: string, payload?: T): void {
132
+ this._bus.emit(event, payload);
133
+ }
134
+
135
+ /**
136
+ * Convenience accessor for the knowledge-base module. Equivalent to
137
+ * ``client.module<KnowledgeBaseModule>("kb")``. Provided as ergonomic
138
+ * sugar so existing call sites read naturally; it is not part of the
139
+ * core's invariants — if KB is unregistered this throws.
140
+ */
141
+ get kb(): KnowledgeBaseModule {
142
+ return this.module<KnowledgeBaseModule>("kb");
143
+ }
144
+
145
+ /** Internal HTTP helper. Modules call this rather than fetch directly so
146
+ * cross-cutting concerns (auth, telemetry) can be added in one place. */
147
+ async _request<T = unknown>(path: string, init: RequestInit = {}): Promise<T> {
148
+ const url = `${this.baseUrl}${path.startsWith("/") ? "" : "/"}${path}`;
149
+ const res = await this._fetch(url, {
150
+ ...init,
151
+ headers: {
152
+ Accept: "application/json",
153
+ ...this._headers,
154
+ ...(init.headers || {}),
155
+ },
156
+ });
157
+ if (!res.ok) {
158
+ const text = await res.text().catch(() => "");
159
+ throw new CodeLockProApiError(res.status, res.statusText, text);
160
+ }
161
+ if (res.status === 204) return undefined as T;
162
+ return (await res.json()) as T;
163
+ }
164
+ }
165
+
166
+ export class CodeLockProApiError extends Error {
167
+ constructor(public status: number, statusText: string, public body: string) {
168
+ super(`CodeLockPro API ${status} ${statusText}: ${body}`);
169
+ this.name = "CodeLockProApiError";
170
+ }
171
+ }
172
+
173
+ export type { EventBus, EventHandler } from "./core/events.js";
174
+ export type { ModuleContext, ModuleFactory } from "./core/module.js";
175
+ export type {
176
+ KnowledgeBaseModule,
177
+ KbArticle,
178
+ KbCategory,
179
+ KbListOptions,
180
+ } from "./modules/kb.js";
181
+ export { createKbModule };
182
+ export default CodeLockPro;
@@ -0,0 +1,205 @@
1
+ /**
2
+ * Knowledge base — the built-in module of the CodeLockPro SDK.
3
+ *
4
+ * Wraps the developer's server-side proxy of the unauthenticated public
5
+ * KB endpoints. This module is **not** privileged inside the SDK core —
6
+ * it uses the same {@link ModuleFactory} contract any additional
7
+ * module the developer registers will use.
8
+ *
9
+ * Default expected paths under the configured ``baseUrl`` (override via
10
+ * ``options.basePath`` if your proxy mounts elsewhere):
11
+ *
12
+ * GET {basePath}/articles → list articles
13
+ * GET {basePath}/articles/{idOrSlug} → single article
14
+ * GET {basePath}/categories → list categories
15
+ * GET {basePath}/search?q=… → full-text search
16
+ * POST {basePath}/articles/{id}/track-view → record a view (action)
17
+ *
18
+ * Events emitted on the shared bus (subscribe via ``client.on(...)``):
19
+ *
20
+ * ``kb.article.viewed`` — payload ``{ articleId: string, slug?: string }``
21
+ * ``kb.search.performed`` — payload ``{ query: string, count: number }``
22
+ *
23
+ * Custom events from host apps may follow the same ``kb.*`` namespace.
24
+ */
25
+ import type { ModuleContext, ModuleFactory } from "../core/module.js";
26
+
27
+ export interface KbArticle {
28
+ id: string;
29
+ application_id: string;
30
+ category_id: string | null;
31
+ developer_id: string;
32
+ slug: string;
33
+ title: string;
34
+ excerpt: string | null;
35
+ content?: string;
36
+ status: "draft" | "published";
37
+ published_at: string | null;
38
+ created_at: string | null;
39
+ updated_at: string | null;
40
+ }
41
+
42
+ export interface KbCategory {
43
+ id: string;
44
+ application_id: string;
45
+ developer_id: string;
46
+ name: string;
47
+ slug: string;
48
+ description: string | null;
49
+ created_at: string | null;
50
+ updated_at: string | null;
51
+ }
52
+
53
+ export interface KbListOptions {
54
+ categoryId?: string;
55
+ categorySlug?: string;
56
+ limit?: number;
57
+ startingAfter?: string;
58
+ }
59
+
60
+ export interface KbArticleViewedPayload {
61
+ articleId: string;
62
+ slug?: string;
63
+ }
64
+
65
+ export interface KbSearchPerformedPayload {
66
+ query: string;
67
+ count: number;
68
+ }
69
+
70
+ export interface KnowledgeBaseModule {
71
+ // --- data accessors ----------------------------------------------------
72
+ getArticles(opts?: KbListOptions): Promise<{ articles: KbArticle[] }>;
73
+ getArticle(idOrSlug: string): Promise<KbArticle>;
74
+ getCategories(): Promise<{ categories: KbCategory[] }>;
75
+ search(
76
+ query: string,
77
+ limit?: number,
78
+ ): Promise<{ query: string; articles: KbArticle[] }>;
79
+
80
+ // --- actions -----------------------------------------------------------
81
+ /**
82
+ * Record a view of an article via the developer's proxy and emit
83
+ * ``kb.article.viewed`` on the shared event bus. Errors from the
84
+ * server are swallowed so view-tracking never blocks rendering;
85
+ * the bus event always fires.
86
+ */
87
+ trackView(articleId: string, opts?: { slug?: string }): Promise<void>;
88
+
89
+ // --- events ------------------------------------------------------------
90
+ /** Subscribe to an event scoped to this module (``kb.<event>``). */
91
+ on<T = unknown>(event: string, handler: (payload: T) => void): () => void;
92
+ off<T = unknown>(event: string, handler: (payload: T) => void): void;
93
+ }
94
+
95
+ export interface KbModuleOptions {
96
+ /** Override the proxy mount-point (default: ``/kb``). */
97
+ basePath?: string;
98
+ }
99
+
100
+ function _qs(params: Record<string, unknown>): string {
101
+ const usp = new URLSearchParams();
102
+ for (const [k, v] of Object.entries(params)) {
103
+ if (v === undefined || v === null || v === "") continue;
104
+ usp.set(k, String(v));
105
+ }
106
+ const s = usp.toString();
107
+ return s ? `?${s}` : "";
108
+ }
109
+
110
+ /**
111
+ * Module factory. Conforms to {@link ModuleFactory} — the SDK core treats
112
+ * KB exactly like any other module.
113
+ *
114
+ * const client = new CodeLockPro({
115
+ * baseUrl: "https://example.com/my-api",
116
+ * modules: [["kb", createKbModule]],
117
+ * });
118
+ */
119
+ export const createKbModule: ModuleFactory<KnowledgeBaseModule> = (
120
+ ctx: ModuleContext,
121
+ ): KnowledgeBaseModule => {
122
+ return _create(ctx, {});
123
+ };
124
+
125
+ /** Variant that accepts module-specific options (e.g. a custom basePath). */
126
+ export function createKbModuleWith(
127
+ options: KbModuleOptions,
128
+ ): ModuleFactory<KnowledgeBaseModule> {
129
+ return (ctx) => _create(ctx, options);
130
+ }
131
+
132
+ function _create(
133
+ ctx: ModuleContext,
134
+ options: KbModuleOptions,
135
+ ): KnowledgeBaseModule {
136
+ const base = (options.basePath ?? "/kb").replace(/\/+$/, "");
137
+ const ns = ctx.name; // "kb" by convention; whatever the host registered as.
138
+
139
+ function _ev(name: string): string {
140
+ return `${ns}.${name}`;
141
+ }
142
+
143
+ return {
144
+ async getArticles(opts: KbListOptions = {}) {
145
+ const qs = _qs({
146
+ category_id: opts.categoryId,
147
+ category_slug: opts.categorySlug,
148
+ limit: opts.limit,
149
+ starting_after: opts.startingAfter,
150
+ });
151
+ return ctx.request(`${base}/articles${qs}`);
152
+ },
153
+
154
+ async getArticle(idOrSlug: string) {
155
+ if (typeof idOrSlug !== "string" || idOrSlug.length === 0) {
156
+ return Promise.reject(new Error("getArticle: idOrSlug required"));
157
+ }
158
+ return ctx.request(`${base}/articles/${encodeURIComponent(idOrSlug)}`);
159
+ },
160
+
161
+ async getCategories() {
162
+ return ctx.request(`${base}/categories`);
163
+ },
164
+
165
+ async search(query: string, limit?: number) {
166
+ const result = await ctx.request<{ query: string; articles: KbArticle[] }>(
167
+ `${base}/search${_qs({ q: query, limit })}`,
168
+ );
169
+ ctx.bus.emit<KbSearchPerformedPayload>(_ev("search.performed"), {
170
+ query,
171
+ count: Array.isArray(result?.articles) ? result.articles.length : 0,
172
+ });
173
+ return result;
174
+ },
175
+
176
+ async trackView(articleId: string, opts: { slug?: string } = {}) {
177
+ if (typeof articleId !== "string" || articleId.length === 0) {
178
+ throw new Error("trackView: articleId required");
179
+ }
180
+ // Bus event fires synchronously so host apps update immediately.
181
+ ctx.bus.emit<KbArticleViewedPayload>(_ev("article.viewed"), {
182
+ articleId,
183
+ slug: opts.slug,
184
+ });
185
+ // Fire-and-forget on the wire: do **not** await, so rendering
186
+ // is never blocked, and swallow errors so view-tracking is
187
+ // never user-visible.
188
+ void ctx
189
+ .request(
190
+ `${base}/articles/${encodeURIComponent(articleId)}/track-view`,
191
+ { method: "POST" },
192
+ )
193
+ .catch(() => {
194
+ /* swallow — see docstring */
195
+ });
196
+ },
197
+
198
+ on<T = unknown>(event: string, handler: (payload: T) => void): () => void {
199
+ return ctx.bus.on<T>(_ev(event), handler);
200
+ },
201
+ off<T = unknown>(event: string, handler: (payload: T) => void): void {
202
+ ctx.bus.off<T>(_ev(event), handler);
203
+ },
204
+ };
205
+ }