@api-emulator/core 0.5.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.
package/README.md ADDED
@@ -0,0 +1,51 @@
1
+ # @api-emulator/core
2
+
3
+ HTTP server, in-memory store, plugin interface, and middleware for emulate service plugins.
4
+
5
+ Part of [emulate](https://github.com/jsj/api-emulator) — local drop-in replacement services for CI and no-network sandboxes.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install @api-emulator/core
11
+ ```
12
+
13
+ ## Overview
14
+
15
+ The core provides the shared infrastructure that every service plugin builds on:
16
+
17
+ - **Store** — a generic in-memory store with typed `Collection<T>` instances supporting CRUD, indexing, filtering, and pagination
18
+ - **Server** — Hono-based HTTP server with automatic port management
19
+ - **Middleware** — bearer token auth, error handling, CORS
20
+ - **UI** — shared authorization/consent page rendering with bundled fonts
21
+ - **Persistence** — pluggable save/load adapters for state durability
22
+
23
+ ## Persistence
24
+
25
+ ### File persistence
26
+
27
+ For local development, use the built-in file adapter:
28
+
29
+ ```typescript
30
+ import { filePersistence } from '@api-emulator/core'
31
+
32
+ persistence: filePersistence('.api-emulator/state.json')
33
+ ```
34
+
35
+ ### Custom adapter
36
+
37
+ Any object with `load` and `save` methods works:
38
+
39
+ ```typescript
40
+ const kvAdapter = {
41
+ async load() { return await kv.get('api-emulator-state') },
42
+ async save(data: string) { await kv.set('api-emulator-state', data) },
43
+ }
44
+ ```
45
+
46
+ The persistence adapter is called on cold start (load) and after every mutating request (save). Saves are serialized via an internal queue to prevent race conditions.
47
+
48
+ ## Links
49
+
50
+ - [Full documentation](https://api-emulator.jsj.sh)
51
+ - [GitHub](https://github.com/jsj/api-emulator)
Binary file
Binary file
@@ -0,0 +1,317 @@
1
+ import * as hono_types from 'hono/types';
2
+ import * as hono from 'hono';
3
+ import { Context, Next, Hono, ErrorHandler, MiddlewareHandler } from 'hono';
4
+
5
+ interface Entity {
6
+ id: number;
7
+ created_at: string;
8
+ updated_at: string;
9
+ }
10
+ type InsertInput<T extends Entity> = Omit<T, "id" | "created_at" | "updated_at"> & {
11
+ id?: number;
12
+ };
13
+ type FilterFn<T> = (item: T) => boolean;
14
+ type SortFn<T> = (a: T, b: T) => number;
15
+ interface QueryOptions<T> {
16
+ filter?: FilterFn<T>;
17
+ sort?: SortFn<T>;
18
+ page?: number;
19
+ per_page?: number;
20
+ }
21
+ interface PaginatedResult<T> {
22
+ items: T[];
23
+ total_count: number;
24
+ page: number;
25
+ per_page: number;
26
+ has_next: boolean;
27
+ has_prev: boolean;
28
+ }
29
+ interface CollectionSnapshot<T extends Entity = Entity> {
30
+ items: T[];
31
+ autoId: number;
32
+ indexFields: string[];
33
+ }
34
+ interface StoreSnapshot {
35
+ collections: Record<string, CollectionSnapshot>;
36
+ data: Record<string, unknown>;
37
+ }
38
+ declare function serializeValue(value: unknown): unknown;
39
+ declare function deserializeValue(value: unknown): unknown;
40
+ declare class Collection<T extends Entity> {
41
+ private indexFields;
42
+ private items;
43
+ private indexes;
44
+ private autoId;
45
+ readonly fieldNames: string[];
46
+ constructor(indexFields?: (keyof T)[]);
47
+ private addToIndex;
48
+ private removeFromIndex;
49
+ insert(data: InsertInput<T>): T;
50
+ get(id: number): T | undefined;
51
+ findBy(field: keyof T, value: T[keyof T] | string | number): T[];
52
+ findOneBy(field: keyof T, value: T[keyof T] | string | number): T | undefined;
53
+ update(id: number, data: Partial<T>): T | undefined;
54
+ delete(id: number): boolean;
55
+ all(): T[];
56
+ query(options?: QueryOptions<T>): PaginatedResult<T>;
57
+ count(filter?: FilterFn<T>): number;
58
+ clear(): void;
59
+ snapshot(): CollectionSnapshot<T>;
60
+ restore(snap: CollectionSnapshot<T>): void;
61
+ }
62
+ declare class Store {
63
+ private collections;
64
+ private _data;
65
+ collection<T extends Entity>(name: string, indexFields?: (keyof T)[]): Collection<T>;
66
+ getData<V>(key: string): V | undefined;
67
+ setData<V>(key: string, value: V): void;
68
+ reset(): void;
69
+ snapshot(): StoreSnapshot;
70
+ restore(snap: StoreSnapshot): void;
71
+ }
72
+
73
+ interface WebhookSubscription {
74
+ id: number;
75
+ url: string;
76
+ events: string[];
77
+ active: boolean;
78
+ secret?: string;
79
+ owner: string;
80
+ repo?: string;
81
+ }
82
+ interface WebhookDelivery {
83
+ id: number;
84
+ hook_id: number;
85
+ event: string;
86
+ action?: string;
87
+ payload: unknown;
88
+ status_code: number | null;
89
+ delivered_at: string;
90
+ duration: number | null;
91
+ success: boolean;
92
+ }
93
+ declare class WebhookDispatcher {
94
+ private subscriptions;
95
+ private deliveries;
96
+ private subscriptionIdCounter;
97
+ private deliveryIdCounter;
98
+ register(sub: Omit<WebhookSubscription, "id"> & {
99
+ id?: number;
100
+ }): WebhookSubscription;
101
+ unregister(id: number): boolean;
102
+ getSubscription(id: number): WebhookSubscription | undefined;
103
+ getSubscriptions(owner?: string, repo?: string): WebhookSubscription[];
104
+ updateSubscription(id: number, data: Partial<Pick<WebhookSubscription, "url" | "events" | "active" | "secret">>): WebhookSubscription | undefined;
105
+ dispatch(event: string, action: string | undefined, payload: unknown, owner: string, repo?: string): Promise<void>;
106
+ getDeliveries(hookId?: number): WebhookDelivery[];
107
+ clear(): void;
108
+ }
109
+
110
+ interface AuthUser {
111
+ login: string;
112
+ id: number;
113
+ scopes: string[];
114
+ }
115
+ interface AuthApp {
116
+ appId: number;
117
+ slug: string;
118
+ name: string;
119
+ }
120
+ interface AuthInstallation {
121
+ installationId: number;
122
+ appId: number;
123
+ permissions: Record<string, string>;
124
+ repositoryIds: number[];
125
+ repositorySelection: "all" | "selected";
126
+ }
127
+ type TokenMap = Map<string, AuthUser>;
128
+ interface TokenEntry {
129
+ token: string;
130
+ login: string;
131
+ id: number;
132
+ scopes: string[];
133
+ }
134
+ declare function serializeTokenMap(tokenMap: TokenMap): TokenEntry[];
135
+ declare function restoreTokenMap(tokenMap: TokenMap, tokens: TokenEntry[]): void;
136
+ type AppEnv = {
137
+ Variables: {
138
+ authUser?: AuthUser;
139
+ authApp?: AuthApp;
140
+ authToken?: string;
141
+ authScopes?: string[];
142
+ docsUrl?: string;
143
+ };
144
+ };
145
+ interface AppKeyResolver {
146
+ (appId: number): {
147
+ privateKey: string;
148
+ slug: string;
149
+ name: string;
150
+ } | null;
151
+ }
152
+ interface AuthFallback {
153
+ login: string;
154
+ id: number;
155
+ scopes: string[];
156
+ }
157
+ declare function authMiddleware(tokens: TokenMap, appKeyResolver?: AppKeyResolver, fallbackUser?: AuthFallback): (c: Context, next: Next) => Promise<void>;
158
+ declare function requireAuth(): (c: Context, next: Next) => Promise<(Response & hono.TypedResponse<{
159
+ message: string;
160
+ documentation_url: string;
161
+ }, 401, "json">) | undefined>;
162
+ declare function requireAppAuth(): (c: Context, next: Next) => Promise<(Response & hono.TypedResponse<{
163
+ message: string;
164
+ documentation_url: string;
165
+ }, 401, "json">) | undefined>;
166
+
167
+ interface RouteContext {
168
+ app: Hono<AppEnv>;
169
+ store: Store;
170
+ webhooks: WebhookDispatcher;
171
+ baseUrl: string;
172
+ tokenMap?: TokenMap;
173
+ }
174
+ interface ServicePlugin {
175
+ name: string;
176
+ register(app: Hono<AppEnv>, store: Store, webhooks: WebhookDispatcher, baseUrl: string, tokenMap?: TokenMap): void;
177
+ seed?(store: Store, baseUrl: string): void;
178
+ }
179
+
180
+ interface ServerOptions {
181
+ port?: number;
182
+ baseUrl?: string;
183
+ docsUrl?: string;
184
+ tokens?: Record<string, {
185
+ login: string;
186
+ id: number;
187
+ scopes?: string[];
188
+ }>;
189
+ appKeyResolver?: AppKeyResolver;
190
+ fallbackUser?: AuthFallback;
191
+ }
192
+ declare function createServer(plugin: ServicePlugin, options?: ServerOptions): {
193
+ app: Hono<AppEnv, hono_types.BlankSchema, "/">;
194
+ store: Store;
195
+ webhooks: WebhookDispatcher;
196
+ port: number;
197
+ baseUrl: string;
198
+ tokenMap: TokenMap;
199
+ };
200
+
201
+ interface FixtureInteraction {
202
+ service: string;
203
+ method: string;
204
+ endpoint: string;
205
+ request: unknown;
206
+ response: unknown;
207
+ status?: number;
208
+ recordedAt?: string;
209
+ metadata?: Record<string, unknown>;
210
+ }
211
+ interface StoreFixture {
212
+ version: 1;
213
+ service: string;
214
+ capturedAt: string;
215
+ store: StoreSnapshot;
216
+ interactions?: FixtureInteraction[];
217
+ metadata?: Record<string, unknown>;
218
+ }
219
+ interface StoreFixtureOptions {
220
+ capturedAt?: string;
221
+ interactions?: FixtureInteraction[];
222
+ metadata?: Record<string, unknown>;
223
+ }
224
+ type FixtureSource = StoreSnapshot | StoreFixture;
225
+ declare function createStoreFixture(service: string, store: StoreSnapshot, options?: StoreFixtureOptions): StoreFixture;
226
+ declare function isStoreFixture(source: FixtureSource): source is StoreFixture;
227
+ declare function fixtureStoreSnapshot(source: FixtureSource): StoreSnapshot;
228
+
229
+ /**
230
+ * Use with `app.onError(...)`. Hono routes handler throws to the app error handler, not to outer middleware try/catch.
231
+ */
232
+ declare function createApiErrorHandler(documentationUrl?: string): ErrorHandler;
233
+ /** Sets `docsUrl` on the context for successful responses; register `createApiErrorHandler` for thrown `ApiError`s. */
234
+ declare function createErrorHandler(documentationUrl?: string): MiddlewareHandler;
235
+ declare const errorHandler: MiddlewareHandler;
236
+ declare class ApiError extends Error {
237
+ status: number;
238
+ errors?: Array<{
239
+ resource: string;
240
+ field: string;
241
+ code: string;
242
+ }> | undefined;
243
+ constructor(status: number, message: string, errors?: Array<{
244
+ resource: string;
245
+ field: string;
246
+ code: string;
247
+ }> | undefined);
248
+ }
249
+ declare function notFound(resource?: string): ApiError;
250
+ declare function validationError(message: string, errors?: ApiError["errors"]): ApiError;
251
+ declare function unauthorized(): ApiError;
252
+ declare function forbidden(): ApiError;
253
+ declare function parseJsonBody(c: Context): Promise<Record<string, unknown>>;
254
+
255
+ interface PaginationParams {
256
+ page: number;
257
+ per_page: number;
258
+ }
259
+ declare function parsePagination(c: Context): PaginationParams;
260
+ declare function setLinkHeader(c: Context, totalCount: number, page: number, perPage: number): void;
261
+
262
+ declare function escapeHtml(s: string): string;
263
+ declare function escapeAttr(s: string): string;
264
+ declare function renderCardPage(title: string, subtitle: string, body: string, service?: string): string;
265
+ declare function renderErrorPage(title: string, message: string, service?: string): string;
266
+ declare function renderSettingsPage(title: string, sidebarHtml: string, bodyHtml: string, service?: string): string;
267
+ interface InspectorTab {
268
+ id: string;
269
+ label: string;
270
+ href: string;
271
+ }
272
+ declare function renderInspectorPage(title: string, tabs: InspectorTab[], activeTab: string, body: string, service?: string): string;
273
+ declare function renderFormPostPage(action: string, fields: Record<string, string>, service?: string): string;
274
+ interface CheckoutLineItem {
275
+ name: string;
276
+ quantity: number;
277
+ unitPrice: number;
278
+ totalPrice: number;
279
+ currency: string;
280
+ }
281
+ interface CheckoutPageOptions {
282
+ merchantName?: string;
283
+ lineItems: CheckoutLineItem[];
284
+ subtotal: number;
285
+ total: number;
286
+ currency: string;
287
+ sessionId: string;
288
+ cancelUrl?: string | null;
289
+ }
290
+ declare function renderCheckoutPage(opts: CheckoutPageOptions, service?: string): string;
291
+ interface UserButtonOptions {
292
+ letter: string;
293
+ login: string;
294
+ name?: string;
295
+ email?: string;
296
+ formAction: string;
297
+ hiddenFields: Record<string, string>;
298
+ }
299
+ declare function renderUserButton(opts: UserButtonOptions): string;
300
+
301
+ declare function registerFontRoutes(app: Hono<AppEnv>): void;
302
+
303
+ declare function normalizeUri(uri: string): string;
304
+ declare function matchesRedirectUri(incoming: string, registered: string[]): boolean;
305
+ declare function constantTimeSecretEqual(a: string, b: string): boolean;
306
+ declare function bodyStr(v: unknown): string;
307
+ declare function parseCookies(header: string): Record<string, string>;
308
+
309
+ declare function debug(label: string, ...args: unknown[]): void;
310
+
311
+ interface PersistenceAdapter {
312
+ load(): Promise<string | null>;
313
+ save(data: string): Promise<void>;
314
+ }
315
+ declare function filePersistence(path: string): PersistenceAdapter;
316
+
317
+ export { ApiError, type AppEnv, type AppKeyResolver, type AuthApp, type AuthFallback, type AuthInstallation, type AuthUser, type CheckoutLineItem, type CheckoutPageOptions, Collection, type CollectionSnapshot, type Entity, type FilterFn, type FixtureInteraction, type FixtureSource, type InsertInput, type InspectorTab, type PaginatedResult, type PaginationParams, type PersistenceAdapter, type QueryOptions, type RouteContext, type ServerOptions, type ServicePlugin, type SortFn, Store, type StoreFixture, type StoreFixtureOptions, type StoreSnapshot, type TokenEntry, type TokenMap, type UserButtonOptions, type WebhookDelivery, WebhookDispatcher, type WebhookSubscription, authMiddleware, bodyStr, constantTimeSecretEqual, createApiErrorHandler, createErrorHandler, createServer, createStoreFixture, debug, deserializeValue, errorHandler, escapeAttr, escapeHtml, filePersistence, fixtureStoreSnapshot, forbidden, isStoreFixture, matchesRedirectUri, normalizeUri, notFound, parseCookies, parseJsonBody, parsePagination, registerFontRoutes, renderCardPage, renderCheckoutPage, renderErrorPage, renderFormPostPage, renderInspectorPage, renderSettingsPage, renderUserButton, requireAppAuth, requireAuth, restoreTokenMap, serializeTokenMap, serializeValue, setLinkHeader, unauthorized, validationError };