@jchat/addon-sdk 1.0.0-alpha.3 → 1.0.0-alpha.31

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/dist/index.d.mts CHANGED
@@ -1,15 +1,333 @@
1
- import { a as MOUNT_POINTS, i as ConfigSchemaField, n as AddonPermission, o as MountPoint, r as AddonPermissionDefinition, t as AddonManifest } from "./contracts.mjs";
2
- import { ComponentType, SVGProps } from "react";
3
- import { Elysia } from "elysia";
4
- import { Kysely } from "kysely";
5
- //#region ../icon/src/types.d.ts
6
- type IconComponent = ComponentType<SVGProps<SVGSVGElement>>;
7
- declare const ICON_NAMES: readonly ["ProfileMen", "ProfileWomen", "Stories", "Search", "AdminPanel", "Investigation", "Logout", "Users", "Settings", "RoomList", "AdminShip", "Announcement", "NewsFeed", "Ranks", "TopPoints", "Like", "DisLike", "Love", "Funny", "Comment", "Chat", "Brush", "Image", "Mic", "More", "Palette", "Smiley", "Send", "Private", "Action", "View", "Female", "Male", "LuSend", "Heart", "Trash", "HorizontalOption", "Menu", "UserGroup", "Globe", "Plus", "Loader", "X", "UserRoundPlus", "Footprints", "Calendar", "Volume", "Youtube", "Upload", "Play", "Pause", "Stop", "Ok", "ImageFill", "MicFill", "YoutubeFill", "PlusFill", "Music", "Gift", "Camera", "Envelope", "Key", "Signature", "UserPen", "Edit", "AddressCard", "Star", "StatusOnline", "StatusAway", "StatusBusy", "StatusEating", "StatusGaming", "StatusSinging", "StatusListening", "StatusStay"];
8
- type IconName = (typeof ICON_NAMES)[number];
9
- type IconPack = Record<IconName, IconComponent>;
1
+ import { S as ResilientFetchOptions, _ as AddonUser, a as AddonChatMessage, b as MOUNT_POINTS, c as AddonDistribution, d as AddonRequestUser, f as AddonResilientFetch, g as AddonRuntimeLogSource, h as AddonRuntimeLogLevel, i as AddonAttachment, l as AddonManifest, m as AddonRuntimeLogFilter, n as ActiveAddonFrontend, o as AddonCommandDefinition, p as AddonRuntimeLog, r as AddonArchiveFile, s as AddonCommandParameter, t as ActiveAddon, u as AddonPermissionDefinition, v as HOST_LOCALES, x as MountPoint, y as HostLocale } from "./contracts2.mjs";
2
+ import { a as canonicalManifest, c as sortValue, i as assertSafeRelativePath, l as validateAddonManifest, n as COMPATIBILITY_RANGE_PATTERN, o as createCanonicalAddonDigest, r as MAX_ADDON_ARCHIVE_BYTES, s as isCompatibilityRange, t as ADDON_ID_PATTERN } from "./signing2.mjs";
3
+ import { AbilityBuilder, MongoAbility, createMongoAbility, subject } from "@casl/ability";
4
+ import { InfiniteData, MutationFunction, QueryFunction, QueryFunctionContext, QueryKey, UndefinedInitialDataInfiniteOptions, UndefinedInitialDataOptions, UseInfiniteQueryResult, UseMutationOptions, UseMutationResult, UseQueryResult, useQueryClient } from "@tanstack/react-query";
5
+ import { IconComponent, IconName, IconPack } from "@jchat/ui";
6
+ import { Elysia, t } from "elysia";
7
+ import { ColumnType, Generated, Insertable, JSONColumnType, Kysely, Selectable, Updateable } from "kysely";
8
+ //#region src/permissions.d.ts
9
+ /**
10
+ * Standard forbidden exception thrown when an actor lacks required permissions or fails ownership
11
+ * checks.
12
+ */
13
+ export declare class ForbiddenError extends Error {
14
+ readonly status = 403;
15
+ readonly code = "FORBIDDEN";
16
+ /**
17
+ * Initializes a new ForbiddenError instance.
18
+ *
19
+ * @param message - User-friendly error message detailing the authorization failure.
20
+ */
21
+ constructor(message?: string);
22
+ }
23
+ /**
24
+ * Authorization scope granted when accessing or mutating a resource:
25
+ *
26
+ * - `"own"`: Actor is the author/owner possessing the `.own` permission (or platform owner).
27
+ * - `"any"`: Actor is an administrator/moderator overriding ownership via the `.any` permission.
28
+ */
29
+ export type ResourceOwnershipScope = "own" | "any";
30
+ /** Configuration payload supplied to {@link authorizeResourceOwnership}. */
31
+ export interface ResourceOwnershipInput {
32
+ /** Unique identifier of the authenticated user performing the action. */
33
+ actorId: string;
34
+ /** Array of effective permission strings held by the actor in the current tenancy context. */
35
+ actorPermissions: string[];
36
+ /** Unique identifier of the user who owns or created the target resource. */
37
+ ownerId: string;
38
+ /**
39
+ * Permission string required if the actor is the resource owner (e.g.
40
+ * `'addon.pollwise.close.own'`).
41
+ */
42
+ ownPermission: string;
43
+ /**
44
+ * Permission string required if the actor is mutating another user's resource (e.g.
45
+ * `'addon.pollwise.close.any'`).
46
+ */
47
+ anyPermission: string;
48
+ /**
49
+ * Human-readable entity noun used in error messages (e.g. `"poll"`, `"story"`, `"comment"`).
50
+ * Defaults to `"resource"`.
51
+ */
52
+ resourceLabel?: string;
53
+ /**
54
+ * Optional factory function creating a custom error instance on authorization denial. Defaults to
55
+ * {@link ForbiddenError}.
56
+ */
57
+ errorFactory?: (message: string) => Error;
58
+ /**
59
+ * Whether the actor possesses site owner / super admin status, granting unconditional bypass.
60
+ * Defaults to `false`.
61
+ */
62
+ isOwner?: boolean;
63
+ }
64
+ /**
65
+ * Checks whether an actor holds a specific permission with wildcard support.
66
+ *
67
+ * Evaluation rules:
68
+ *
69
+ * 1. Exact match (`'addon.pollwise.close.own'` matches `'addon.pollwise.close.own'`).
70
+ * 2. Global wildcard (`'*'` grants all actions).
71
+ * 3. Segment wildcards (`'addon.pollwise.*'` grants `'addon.pollwise.close.own'`).
72
+ *
73
+ * @param userPermissions - Array of permissions possessed by the user.
74
+ * @param required - Permission string required for the requested action.
75
+ * @returns `true` if the user is granted the required permission, `false` otherwise.
76
+ */
77
+ export declare function hasPermission(userPermissions: string[], required: string): boolean;
78
+ /**
79
+ * Evaluates Attribute-Based Access Control (ABAC) ownership rules to prevent Broken Object-Level
80
+ * Authorization (BOLA / IDOR) vulnerabilities.
81
+ *
82
+ * Resolution flow:
83
+ *
84
+ * 1. **Site Owner Bypass**: If `isOwner === true` or holding `'*'`, access is granted unconditionally.
85
+ * 2. **Author Ownership**: If `actorId === ownerId` and actor holds `ownPermission`, grants `"own"`.
86
+ * 3. **Moderator Override**: If actor holds `anyPermission`, grants `"any"`.
87
+ * 4. **Denial**: Throws an error generated by `errorFactory` (defaults to {@link ForbiddenError}).
88
+ *
89
+ * @param input - Ownership parameters, actor credentials, and required permissions.
90
+ * @returns The resolved {@link ResourceOwnershipScope} (`"own"` or `"any"`).
91
+ * @throws {ForbiddenError} When the actor lacks required ownership or moderator privileges.
92
+ */
93
+ export declare function authorizeResourceOwnership({ actorId, actorPermissions, ownerId, ownPermission, anyPermission, resourceLabel, errorFactory, isOwner: actorIsOwner }: ResourceOwnershipInput): ResourceOwnershipScope;
94
+ /** CASL MongoAbility contract for Addon entities and attribute evaluation. */
95
+ export type AddonAbility = MongoAbility<[string, string | Record<string, unknown>]>;
96
+ /**
97
+ * Creates a CASL Ability instance for an addon actor with automated permission convention mapping
98
+ * and optional custom rule extensions.
99
+ *
100
+ * Convention mapping:
101
+ *
102
+ * - Global `'*'` or `isOwner: true` $\rightarrow$ `can('manage', 'all')`
103
+ * - `*.any` (e.g. `'addon.pollwise.close.any'`) $\rightarrow$ `can('close', 'all')`
104
+ * - `*.own` (e.g. `'addon.pollwise.close.own'`) $\rightarrow$ `can('close', 'all', { userId: actor.id
105
+ * })`
106
+ *
107
+ * @param actor - Authenticated request user context.
108
+ * @param defineRules - Optional callback to register custom CASL ability rules.
109
+ * @returns A frozen CASL {@link AddonAbility} instance.
110
+ */
111
+ export declare function createAddonAbility(actor: AddonRequestUser | null | undefined, defineRules?: (builder: AbilityBuilder<AddonAbility>) => void): AddonAbility;
112
+ //#endregion
113
+ //#region src/i18n.d.ts
114
+ export type HostLanguage = HostLocale;
115
+ export type AddonTranslationDictionary = Record<string, unknown>;
116
+ export type AddonTranslationResources = Record<string, AddonTranslationDictionary>;
117
+ export type AddonLocaleLoader = (locale: string) => Promise<AddonTranslationDictionary> | AddonTranslationDictionary;
118
+ export interface LocaleContextValue {
119
+ locale: string;
120
+ dir: "ltr" | "rtl";
121
+ }
122
+ export declare const LocaleContext: import("react").Context<LocaleContextValue>;
123
+ export interface TranslatorOptions {
124
+ resources?: AddonTranslationResources;
125
+ defaultLocale?: string;
126
+ fallbackLocale?: string;
127
+ }
128
+ export type TranslateFunction<TKeys extends string = string> = (key: TKeys | (string & {}), paramsOrLang?: Record<string, unknown> | string, maybeLang?: string) => string;
129
+ /**
130
+ * Creates a standalone translation function for backend server routes (`src/api.ts`) or utilities.
131
+ *
132
+ * @param resources - Dictionary of translation resources (e.g., `{ en: enJson, ta: taJson }`).
133
+ * @param defaultLocale - Default locale to use if not specified per call (defaults to "en").
134
+ * @param fallbackLocale - Fallback locale if a key is missing in the target language (defaults to
135
+ * "en").
136
+ */
137
+ export declare function createTranslator<TResources extends AddonTranslationResources = AddonTranslationResources>(resources?: TResources, defaultLocale?: string, fallbackLocale?: string): TranslateFunction<TResources extends {
138
+ en: infer TEn;
139
+ } ? TEn extends Record<string, unknown> ? Extract<keyof TEn, string> : string : string>;
140
+ /**
141
+ * Hook to access the current active host locale and text direction in an addon component.
142
+ *
143
+ * @example
144
+ * ;```tsx
145
+ * import { useLocale } from "@jchat/addon-sdk"
146
+ *
147
+ * export default function MyAddon() {
148
+ * const { locale, dir, isRtl } = useLocale()
149
+ * return <div dir={dir}>Current language: {locale}</div>
150
+ * }
151
+ * ```
152
+ */
153
+ export declare function useLocale(): {
154
+ locale: string;
155
+ dir: "ltr" | "rtl";
156
+ isRtl: boolean;
157
+ };
158
+ /**
159
+ * Hook to access translation function and locale metadata inside addon components. Automatically
160
+ * synchronizes with host language switches and falls back to English. Supports static dictionary
161
+ * resources or dynamic async locale loaders.
162
+ *
163
+ * @example
164
+ * ;```tsx
165
+ * import { useTranslation } from "@jchat/addon-sdk"
166
+ * import locales from "./locales"
167
+ *
168
+ * export default function MyAddon() {
169
+ * const { t, locale, dir } = useTranslation(locales)
170
+ * return (
171
+ * <div dir={dir}>
172
+ * <h1>{t("welcome", { name: "Guest" })}</h1>
173
+ * </div>
174
+ * )
175
+ * }
176
+ * ```
177
+ *
178
+ * @param resourcesOrLoader - Addon-scoped translation dictionaries or async chunk loader.
179
+ * @param fallbackLocale - Fallback language code when a string is missing (default: "en").
180
+ */
181
+ export declare function useTranslation<TResources extends AddonTranslationResources | AddonLocaleLoader = AddonTranslationResources>(resourcesOrLoader?: TResources, fallbackLocale?: string): {
182
+ t: TranslateFunction<TResources extends {
183
+ en: infer TEn;
184
+ } ? TEn extends Record<string, unknown> ? Extract<keyof TEn, string> : string : string>;
185
+ locale: string;
186
+ dir: "ltr" | "rtl";
187
+ isRtl: boolean;
188
+ isReady: boolean;
189
+ };
190
+ //#endregion
191
+ //#region src/query.d.ts
192
+ export interface UseAddonQueryOptions<TData = unknown, TError = Error> extends Omit<UndefinedInitialDataOptions<TData, TError, TData, QueryKey>, "queryKey" | "queryFn"> {
193
+ /** The API path to fetch, automatically prefixed with `/api/v1/addons/<addonId>` */
194
+ path?: string;
195
+ /** Custom query key elements appended to the automatic path cache key */
196
+ queryKey?: QueryKey;
197
+ /** Custom query function override */
198
+ queryFn?: QueryFunction<TData, QueryKey>;
199
+ }
200
+ export interface UseAddonMutationOptions<TData = unknown, TVariables = unknown, TError = Error, TContext = unknown> extends Omit<UseMutationOptions<TData, TError, TVariables, TContext>, "mutationFn"> {
201
+ /** The target API path, or a dynamic function of mutation variables */
202
+ path?: string | ((variables: TVariables) => string);
203
+ /** HTTP method to use (defaults to 'POST') */
204
+ method?: "POST" | "PUT" | "DELETE";
205
+ /** Query keys to automatically invalidate upon successful mutation */
206
+ invalidateQueries?: QueryKey[];
207
+ /** Custom mutation function override */
208
+ mutationFn?: MutationFunction<TData, TVariables>;
209
+ }
210
+ export interface UseAddonInfiniteQueryOptions<TData = unknown, TError = Error, TPageParam = unknown> extends Omit<UndefinedInitialDataInfiniteOptions<TData, TError, InfiniteData<TData, TPageParam>, QueryKey, TPageParam>, "queryKey" | "queryFn"> {
211
+ /** The API path to fetch, or a dynamic function that receives pageParam and returns the path */
212
+ path?: string | ((pageParam: TPageParam) => string);
213
+ /** Custom query key elements appended to the automatic cache key */
214
+ queryKey?: QueryKey;
215
+ /** Custom query function override */
216
+ queryFn?: (context: QueryFunctionContext<QueryKey, TPageParam>) => Promise<TData>;
217
+ }
218
+ /**
219
+ * Perform a declarative, cached GET request using the host's shared React Query instance.
220
+ * Automatically injects authentication headers and prefixes addon paths.
221
+ *
222
+ * @example
223
+ * ;```tsx
224
+ * const { data, isLoading } = useAddonQuery<{ polls: Poll[] }>({
225
+ * path: "/polls",
226
+ * queryKey: [siteId]
227
+ * })
228
+ * ```
229
+ */
230
+ export declare function useAddonQuery<TData = unknown, TError = Error>(options: UseAddonQueryOptions<TData, TError>): UseQueryResult<TData, TError>;
231
+ /**
232
+ * Perform a paginated or infinite scrolling request using the host's shared React Query instance.
233
+ *
234
+ * @example
235
+ * ;```tsx
236
+ * const { data, fetchNextPage, hasNextPage } = useAddonInfiniteQuery<GifResponse, Error, number>({
237
+ * path: (page) => `/trending?offset=${page * 20}`,
238
+ * initialPageParam: 0,
239
+ * getNextPageParam: (lastPage, allPages) => (lastPage.hasMore ? allPages.length : undefined)
240
+ * })
241
+ * ```
242
+ */
243
+ export declare function useAddonInfiniteQuery<TData = unknown, TError = Error, TPageParam = unknown>(options: UseAddonInfiniteQueryOptions<TData, TError, TPageParam>): UseInfiniteQueryResult<InfiniteData<TData, TPageParam>, TError>;
244
+ /**
245
+ * Perform a declarative mutation using the host's shared React Query instance. Automatically
246
+ * executes the request and can invalidate specified query keys upon success.
247
+ *
248
+ * @example
249
+ * ;```tsx
250
+ * const { mutate, isPending } = useAddonMutation<Poll, CreatePollInput>({
251
+ * path: "/polls",
252
+ * method: "POST",
253
+ * invalidateQueries: [["/polls"]],
254
+ * onSuccess: () => sdk.toast("Poll created!", "success")
255
+ * })
256
+ * ```
257
+ */
258
+ export declare function useAddonMutation<TData = unknown, TVariables = unknown, TError = Error, TContext = unknown>(options: UseAddonMutationOptions<TData, TVariables, TError, TContext>): UseMutationResult<TData, TError, TVariables, TContext>;
259
+ export interface UseAddonSettingsResult<TConfig> {
260
+ settings: TConfig | null;
261
+ isLoading: boolean;
262
+ isSaving: boolean;
263
+ error: Error | null;
264
+ saveSettings: (settings: TConfig) => void;
265
+ saveSettingsAsync: (settings: TConfig) => Promise<void>;
266
+ refetch: () => Promise<unknown>;
267
+ }
268
+ /**
269
+ * Ergonomic hook for loading and updating site configuration for an addon. Handles fetching,
270
+ * caching, pending states, and automatic query cache invalidation.
271
+ */
272
+ export declare function useAddonSettings<TConfig = Record<string, unknown>>(siteId?: string): UseAddonSettingsResult<TConfig>;
273
+ //#endregion
274
+ //#region src/testing.d.ts
275
+ /** Configuration options supplied to {@link createMockAddonServerContext}. */
276
+ export interface MockAddonServerContextOptions {
277
+ /** Simulated tenant site identifier (defaults to `'site_test_123'`). */
278
+ siteId?: string;
279
+ /** Simulated addon configuration settings returned by `context.getConfig()`. */
280
+ config?: Record<string, unknown>;
281
+ /** Simulated user profile returned by `context.getUser()` and `context.getUsers()`. */
282
+ user?: Partial<AddonUser>;
283
+ /**
284
+ * Simulated commercial license entitlement status returned by `context.verifyEntitlement()`.
285
+ * Defaults to `true`.
286
+ */
287
+ isEntitled?: boolean;
288
+ }
289
+ /** Inspection fixtures and mock context returned by {@link createMockAddonServerContext}. */
290
+ export interface MockAddonContextResult {
291
+ /** The fully functioning mock {@link AddonServerContext} instance. */
292
+ context: AddonServerContext;
293
+ /** Array capturing all events emitted via `context.emit()`, enabling assertions in unit tests. */
294
+ emittedEvents: Array<{
295
+ event: string;
296
+ payload: unknown;
297
+ target?: AddonServerEmitTarget;
298
+ }>;
299
+ /** Array capturing all pipeline hooks registered via `context.pipeline.register()`. */
300
+ registeredHooks: AddonPipelineHooks[];
301
+ /** In-memory Map of key-value cache entries written via `context.cache`. */
302
+ cacheEntries: Map<string, unknown>;
303
+ /** In-memory Map of Redis Set collections written via `context.cache.setAdd()` / `setRemove()`. */
304
+ cacheSets: Map<string, Set<string>>;
305
+ }
306
+ /**
307
+ * Creates an in-memory, zero-dependency {@link AddonServerContext} mock for testing addon server
308
+ * routes, lifecycle handlers, and pipeline hooks offline in CI without requiring live database
309
+ * connections or Redis.
310
+ *
311
+ * Provides complete mock implementations of:
312
+ *
313
+ * - **Key-Value & Set Cache**: Supports `get`, `set` (with TTL simulation), `del`, `incr`, `expire`,
314
+ * `ttl`, `setAdd`, `setRemove`, and `setMembers` with automatic tenant/addon namespacing.
315
+ * - **Sliding-Window Rate Limiting**: In-memory timestamp-bucket rate limiting via `consume`.
316
+ * - **Event Capturing**: Records emitted events in {@link MockAddonContextResult.emittedEvents} with
317
+ * canonical `addon:<addonId>:<event>` prefixing.
318
+ * - **Pipeline Hook Registration**: Inspects registered hook stages and handlers.
319
+ * - **Fine-Grained ABAC & Ownership**: Enforces CASL `can` attribute queries and BOLA protection via
320
+ * `authorizeOwnership`.
321
+ * - **License Entitlement**: Deterministic commercial entitlement verification.
322
+ *
323
+ * @param addonId - Unique identifier of the addon under test.
324
+ * @param options - Optional mock configuration parameters.
325
+ * @returns A {@link MockAddonContextResult} containing the mock context and inspection fixtures.
326
+ */
327
+ export declare function createMockAddonServerContext(addonId: string, options?: MockAddonServerContextOptions): MockAddonContextResult;
10
328
  //#endregion
11
329
  //#region src/index.d.ts
12
- type AddonJsonBody = Record<string, unknown> | unknown[] | null;
330
+ export type AddonJsonBody = Record<string, unknown> | unknown[] | null;
13
331
  /**
14
332
  * @jchat/addon-sdk
15
333
  *
@@ -17,41 +335,158 @@ type AddonJsonBody = Record<string, unknown> | unknown[] | null;
17
335
  * schema, and host API access. This package is always EXTERNALISED from addon bundles — the host
18
336
  * app provides the runtime implementation.
19
337
  */
20
- /** Minimal user profile accessible to addon backends. */
21
- interface AddonUser {
22
- /** Unique user identifier */
23
- id: string;
24
- /** Display name */
25
- name: string;
26
- /** Full display name */
27
- displayName: string;
28
- /** Full URL to the user's avatar image */
29
- avatar: string;
30
- /** User's gender identifier */
31
- gender: string;
32
- }
33
- interface AddonRequestUser {
34
- id?: string;
35
- permissions?: string[];
36
- siteId?: string;
338
+ /** Standard error thrown or handled when an addon message pipeline hook aborts sending. */
339
+ export declare class AddonPipelineAbortError extends Error {
340
+ readonly code: string;
341
+ readonly reason?: string;
342
+ constructor(error: {
343
+ code: string;
344
+ reason?: string;
345
+ });
37
346
  }
38
347
  /**
39
348
  * Typed, schema-scoped database facade for an addon. The host deliberately omits raw execution,
40
349
  * schema switching, dialect/connection access, and lifecycle methods.
41
350
  */
42
- type AddonDatabase<TDatabase = Record<string, Record<string, unknown>>> = Pick<Kysely<TDatabase>, "deleteFrom" | "fn" | "insertInto" | "selectFrom" | "transaction" | "updateTable">;
351
+ export type AddonDatabase<TDatabase = Record<string, Record<string, unknown>>> = Pick<Kysely<TDatabase>, "deleteFrom" | "fn" | "insertInto" | "selectFrom" | "transaction" | "updateTable">;
352
+ /**
353
+ * Scoped caching and key-value client provided to addon server instances. Keys are automatically
354
+ * namespaced by the host as `addon:<addonId>:<key>`.
355
+ */
356
+ export interface AddonCacheClient {
357
+ /** Retrieve a string or JSON-parsed value by key */
358
+ get: <T = string>(key: string) => Promise<T | null>;
359
+ /** Store a value with an optional time-to-live in seconds */
360
+ set: (key: string, value: unknown, ttlSeconds?: number) => Promise<void>;
361
+ /** Delete one or more keys */
362
+ del: (...keys: string[]) => Promise<number>;
363
+ /** Atomically increment a numeric counter and return the new value */
364
+ incr: (key: string) => Promise<number>;
365
+ /** Set a timeout on a key in seconds */
366
+ expire: (key: string, ttlSeconds: number) => Promise<boolean>;
367
+ /** Get the remaining time-to-live of a key in seconds (-2 if key does not exist, -1 if no TTL) */
368
+ ttl: (key: string) => Promise<number>;
369
+ /** Add a member to a set stored at key */
370
+ setAdd: (key: string, member: string) => Promise<number>;
371
+ /** Remove a member from a set stored at key */
372
+ setRemove: (key: string, member: string) => Promise<number>;
373
+ /** Retrieve all members of a set stored at key */
374
+ setMembers: (key: string) => Promise<string[]>;
375
+ }
376
+ export interface AddonRateLimitOptions {
377
+ /** Maximum number of allowed requests within the sliding window */
378
+ limit: number;
379
+ /** Time window duration in milliseconds */
380
+ windowMs: number;
381
+ }
382
+ /**
383
+ * High-performance sliding-window rate limiter scoped to this addon. Keys are automatically
384
+ * prefixed by the host as `addon:<addonId>:ratelimit:<key>`.
385
+ */
386
+ export interface AddonRateLimiter {
387
+ /**
388
+ * Consume a rate limit token using an atomic sliding window.
389
+ *
390
+ * @param key - Identifier within the addon (e.g. `user:${userId}:audio`, `command:${cmd}`)
391
+ * @param options - Limit and window duration in milliseconds
392
+ * @returns `true` if within the rate limit, `false` if limit exceeded
393
+ */
394
+ consume: (key: string, options: AddonRateLimitOptions) => Promise<boolean>;
395
+ }
396
+ export interface AddonServerEmitTarget {
397
+ /** Target a specific site (defaults to the request user's siteId) */
398
+ siteId?: string;
399
+ /** Target a specific chat room */
400
+ roomId?: string;
401
+ /** Target a specific user */
402
+ userId?: string;
403
+ }
404
+ export interface AddonMessageMutableData {
405
+ /** Message content (can be Slate JSON structure or string) */
406
+ content: unknown;
407
+ /** Optional message attachments */
408
+ attachments?: AddonAttachment[] | unknown[] | null;
409
+ /**
410
+ * Namespaced metadata bucket for this addon. Keyed as `metadata["addon:<addonId>"]`. Storing data
411
+ * here automatically attaches it to the message broadcast and database record.
412
+ */
413
+ metadata: Record<string, unknown>;
414
+ /** Target specific user recipient for whispers/directed messages */
415
+ receiverId?: string | null;
416
+ }
417
+ export interface AddonMessageContext {
418
+ /** The site ID where the message is being sent */
419
+ siteId: string;
420
+ /** The chat room ID */
421
+ roomId: string;
422
+ /** The authenticated user sending the message (read-only) */
423
+ user: AddonRequestUser;
424
+ /** Mutable message payload */
425
+ message: AddonMessageMutableData;
426
+ /** Convenience method to set metadata under this addon's namespace */
427
+ setMetadata: (key: string, value: unknown) => void;
428
+ /**
429
+ * Abort the message sending pipeline with a user-facing error code and reason. Only active during
430
+ * `beforeValidate` and `transformContent`.
431
+ */
432
+ abort: (error: {
433
+ code: string;
434
+ reason?: string;
435
+ }) => never;
436
+ /**
437
+ * AbortSignal triggered if this interceptor times out or the message pipeline is cancelled. Can
438
+ * be passed to `fetch(url, { signal: context.signal })` or async operations.
439
+ */
440
+ signal?: AbortSignal;
441
+ }
442
+ export interface AddonMessageAfterSaveContext {
443
+ siteId: string;
444
+ roomId: string;
445
+ user: AddonRequestUser;
446
+ message: {
447
+ id: string;
448
+ content: unknown;
449
+ attachments?: AddonAttachment[] | unknown[] | null;
450
+ metadata?: Record<string, unknown> | null;
451
+ type: string;
452
+ createdAt: Date | string;
453
+ };
454
+ /** AbortSignal triggered if the observer times out or is cancelled. */
455
+ signal?: AbortSignal;
456
+ }
457
+ export interface AddonPipelineHooks {
458
+ /** Optional priority ordering. Lower numbers execute first (e.g. 10 runs before 100). Default is 100. */
459
+ priority?: number;
460
+ /**
461
+ * Run before validation to inspect or guard actions (rate-limits, permission gates). Throw or
462
+ * call `context.abort()` to reject the message.
463
+ */
464
+ beforeValidate?: (context: AddonMessageContext) => Promise<void> | void;
465
+ /** Run before persistence to mutate text, parse slash commands, or enrich metadata. */
466
+ transformContent?: (context: AddonMessageContext) => Promise<void> | void;
467
+ /** Run asynchronously after database save (fire-and-forget observers). */
468
+ afterSave?: (context: AddonMessageAfterSaveContext) => Promise<void> | void;
469
+ }
43
470
  /**
44
471
  * Context provided to an addon's server-side factory. Parameterize it with the addon's table
45
472
  * interface: `AddonServerContext<MyAddonDatabase>`.
46
473
  */
47
- interface AddonServerContext<TDatabase = Record<string, Record<string, unknown>>> {
474
+ export interface AddonServerContext<TDatabase = Record<string, Record<string, unknown>>> {
475
+ /**
476
+ * Elysia / TypeBox schema validator for defining route validation schemas (`body`, `query`,
477
+ * `params`, `headers`). Provided directly by the host runtime so your addon server bundle does
478
+ * not need to bundle Elysia.
479
+ */
480
+ t: typeof t;
48
481
  /**
49
482
  * Retrieve the encrypted settings for this addon for a specific site. Internal secrets are
50
483
  * automatically decrypted using the host's master key.
51
484
  */
52
- getConfig: (siteId: string) => Promise<Record<string, unknown>>;
485
+ getConfig: <T = Record<string, unknown>>(siteId: string) => Promise<T>;
53
486
  /** Fetch a specific user's public profile */
54
487
  getUser: (userId: string) => Promise<AddonUser | null>;
488
+ /** Find a user by their unique username */
489
+ findUserByName: (name: string) => Promise<AddonUser | null>;
55
490
  /** Efficiently fetch multiple user profiles in a single query. Returns a Map keyed by user ID. */
56
491
  getUsers: (userIds: string[]) => Promise<Map<string, AddonUser>>;
57
492
  /**
@@ -61,13 +496,99 @@ interface AddonServerContext<TDatabase = Record<string, Record<string, unknown>>
61
496
  * @param user - The user object from the request context
62
497
  * @param permission - The permission to check, e.g., 'message.send' or 'addon.stories.view'
63
498
  */
64
- hasPermission: (user: AddonRequestUser | null | undefined, permission: AddonPermission) => boolean;
499
+ hasPermission: (user: AddonRequestUser | null | undefined, permission: string) => boolean;
65
500
  /**
66
501
  * A Kysely database instance scoped entirely to your addon's tables. You cannot access core Jchat
67
502
  * tables (Users, Rooms, etc.) through this instance.
68
503
  */
69
504
  db: AddonDatabase<TDatabase>;
505
+ /** A secure, namespaced key-value cache client scoped to your addon (`addon:<addonId>:<key>`). */
506
+ cache: AddonCacheClient;
507
+ /**
508
+ * Broadcast a real-time event to connected clients across the site or room. Clients listen via
509
+ * `sdk.events.on('addon:<addonId>:<event>', callback)` or `sdk.events.on(event, callback)`.
510
+ */
511
+ emit: (event: string, payload: unknown, target?: AddonServerEmitTarget) => Promise<void>;
512
+ /**
513
+ * Register lifecycle interception hooks for chat messages. Hooks run in the core chat execution
514
+ * path with strict 50ms timeout guards and fail-open fault tolerance.
515
+ */
516
+ pipeline?: {
517
+ register: (hooks: AddonPipelineHooks) => void;
518
+ };
519
+ /** An atomic sliding-window rate limiter scoped to your addon (`addon:<addonId>:ratelimit:<key>`). */
520
+ rateLimiter?: AddonRateLimiter;
521
+ /**
522
+ * Cryptographically verify that this addon instance holds a valid host license entitlement.
523
+ * Commercial paid addons should invoke this or call `verifyAddonEntitlement(addonId, context)`.
524
+ *
525
+ * @param expectedAddonId - The canonical addon ID defined by the publisher
526
+ * @returns True if valid, or false if entitlement is missing or invalid
527
+ */
528
+ verifyEntitlement?: (expectedAddonId?: string) => boolean;
529
+ /**
530
+ * Enforces resource ownership rules ('own' vs 'any' permissions). Prevents Broken Object-Level
531
+ * Authorization (BOLA/IDOR).
532
+ *
533
+ * @param params - The actor, owner ID, and permission requirements
534
+ * @returns "own" if the actor owns the resource, or "any" if granted via moderator/admin
535
+ * privileges
536
+ * @throws ForbiddenError if neither condition is satisfied
537
+ */
538
+ authorizeOwnership: (params: {
539
+ actor: AddonRequestUser;
540
+ ownerId: string;
541
+ ownPermission: string;
542
+ anyPermission: string;
543
+ resourceLabel?: string;
544
+ }) => ResourceOwnershipScope;
545
+ /**
546
+ * Evaluates fine-grained Attribute-Based Access Control (ABAC) rules for custom addon entities.
547
+ *
548
+ * @param actor - The authenticated request user
549
+ * @param action - Action to perform (e.g. 'read', 'delete', 'close')
550
+ * @param subject - Entity name (e.g. 'poll', 'story', 'track')
551
+ * @param attributes - Resource attributes (e.g. { userId: poll.userId, status: 'open' })
552
+ * @returns True if allowed, false otherwise
553
+ */
554
+ can: (actor: AddonRequestUser | null | undefined, action: string, subject: string, attributes?: Record<string, unknown>) => boolean;
555
+ /**
556
+ * A resilient HTTP fetch client with built-in exponential backoff, jitter, and timeouts.
557
+ * Automatically retries on transient network disconnects and HTTP 5xx responses without requiring
558
+ * external dependencies in addons.
559
+ */
560
+ fetch: AddonResilientFetch;
70
561
  }
562
+ /**
563
+ * Elysia app instance provided to addon server endpoints with Jchat macro metadata (e.g.
564
+ * requirePermission, requireOwnership) typed.
565
+ */
566
+ export type AddonApp = Elysia<"", {
567
+ decorator: {};
568
+ store: {};
569
+ derive: {};
570
+ resolve: {
571
+ user: AddonRequestUser;
572
+ };
573
+ }, {
574
+ typebox: {};
575
+ error: {};
576
+ }, {
577
+ schema: {};
578
+ standaloneSchema: {};
579
+ macro: {
580
+ requirePermission?: string | string[];
581
+ requireOwnership?: {
582
+ resolveOwnerId: (context: unknown) => string | Promise<string | null | undefined>;
583
+ ownPermission: string;
584
+ anyPermission: string;
585
+ resourceLabel?: string;
586
+ };
587
+ };
588
+ macroFn: {};
589
+ parser: {};
590
+ response: {};
591
+ }>;
71
592
  /**
72
593
  * The entry point for your addon's server bundle. You should default-export a function matching
73
594
  * this signature.
@@ -82,9 +603,9 @@ interface AddonServerContext<TDatabase = Record<string, Record<string, unknown>>
82
603
  * export default factory
83
604
  * ```
84
605
  */
85
- type AddonServerFactory<TDatabase = Record<string, Record<string, unknown>>> = (ctx: AddonServerContext<TDatabase>, app: Elysia) => Elysia;
606
+ export type AddonServerFactory<TDatabase = Record<string, Record<string, unknown>>> = (ctx: AddonServerContext<TDatabase>, app: AddonApp) => Elysia;
86
607
  /** Props passed to your addon's root React component by the Jchat host. */
87
- interface AddonProps {
608
+ export interface AddonProps {
88
609
  /** The mount point where this addon instance is rendered (e.g., 'chat-sidebar') */
89
610
  mountPoint: MountPoint;
90
611
  /** Information about the currently logged-in user (if any) */
@@ -99,16 +620,33 @@ interface AddonProps {
99
620
  roomId?: string;
100
621
  /** The ID of the active Jchat site */
101
622
  siteId?: string;
623
+ /** The active user locale (e.g. 'en', 'ta', 'ar') */
624
+ locale?: string;
625
+ /** Text direction for the current locale */
626
+ dir?: "ltr" | "rtl";
627
+ /** The message object when rendered inside 'chat-message-card' or 'chat-message-flair' */
628
+ message?: AddonChatMessage | unknown;
629
+ /** The addon's specific payload extracted from message.metadata */
630
+ addonData?: unknown;
631
+ /** List of active addon IDs enabled on the current site */
632
+ activeAddonIds?: string[];
102
633
  }
103
- interface AddonApi {
634
+ export interface AddonApi {
104
635
  get: <TResponse = unknown>(path: string) => Promise<TResponse>;
105
636
  post: <TResponse = unknown>(path: string, body?: AddonJsonBody) => Promise<TResponse>;
106
637
  put: <TResponse = unknown>(path: string, body?: AddonJsonBody) => Promise<TResponse>;
107
638
  delete: <TResponse = unknown>(path: string) => Promise<TResponse>;
108
639
  }
109
- type AddonEventListener = (data: unknown) => void;
640
+ /** First-class addon settings management interface provided by the host. */
641
+ export interface AddonSettingsApi {
642
+ /** Fetch the current site's saved configuration for this addon */
643
+ get: <T = Record<string, unknown>>() => Promise<T | null>;
644
+ /** Save updated configuration for this addon for the current site */
645
+ save: <T = Record<string, unknown>>(settings: T) => Promise<void>;
646
+ }
647
+ export type AddonEventListener = (data: unknown) => void;
110
648
  /** A font option provided by the JChat host. */
111
- interface AddonFont {
649
+ export interface AddonFont {
112
650
  /** Display label shown to users (e.g. 'Modern', 'Elegant') */
113
651
  label: string;
114
652
  /** Font identifier stored in the database (e.g. 'poppins', 'lato') */
@@ -120,16 +658,75 @@ interface AddonFont {
120
658
  /** Relative size multiplier — apply this to base font sizes to match the host's rendering */
121
659
  scale: number;
122
660
  }
661
+ export interface AddonEditorInsertImagePayload {
662
+ src: string;
663
+ altText: string;
664
+ emoji?: {
665
+ col: number;
666
+ row: number;
667
+ size: number;
668
+ };
669
+ className?: string;
670
+ }
671
+ export interface AddonEditorInsertEmojiPayload {
672
+ col: number;
673
+ row: number;
674
+ unicode: string;
675
+ shortcode?: string;
676
+ }
677
+ export interface AddonEditorCommandMap {
678
+ INSERT_YOUTUBE_COMMAND: string;
679
+ INSERT_TEXT: string;
680
+ INSERT_TEXT_COMMAND: string;
681
+ INSERT_MENTION_COMMAND: string;
682
+ INSERT_IMAGE_COMMAND: AddonEditorInsertImagePayload;
683
+ INSERT_EMOJI_COMMAND: AddonEditorInsertEmojiPayload;
684
+ CLEAR_EDITOR: void;
685
+ CLEAR_EDITOR_COMMAND: void;
686
+ }
687
+ export type AddonEditorCommand = keyof AddonEditorCommandMap;
688
+ export interface AddonEditorApi {
689
+ /**
690
+ * Trigger a specialized editor command defined by the host.
691
+ *
692
+ * @example
693
+ * sdk.editor.dispatchCommand("INSERT_YOUTUBE_COMMAND", "dQw4w9WgXcQ")
694
+ */
695
+ dispatchCommand: <C extends AddonEditorCommand>(command: C, ...args: AddonEditorCommandMap[C] extends void ? [payload?: undefined] : [payload: AddonEditorCommandMap[C]]) => void;
696
+ /**
697
+ * Insert a low-level Slate node directly at the user's cursor position.
698
+ *
699
+ * @param node - A valid Slate Descendant or Fragment object
700
+ */
701
+ insertNode: (node: unknown) => void;
702
+ /**
703
+ * Upload and attach a file to the current message without inserting it inline. Users will see the
704
+ * file in the attachment bar below the editor.
705
+ */
706
+ addAttachment: (attachment: AddonAttachment | {
707
+ url: string;
708
+ name: string;
709
+ type: string;
710
+ }) => void;
711
+ /** Force the browser focus back to the editor input */
712
+ focus: () => void;
713
+ }
123
714
  /**
124
715
  * The bridge between your addon and the Jchat host application. Use the `useAddonSDK()` hook to
125
716
  * access this interface in your components.
126
717
  */
127
- interface AddonSDK {
718
+ export interface AddonSDK {
128
719
  /**
129
720
  * Make authenticated API calls to your addon's backend or core Jchat endpoints. All calls
130
- * automatically include the current user's sessionId and site validation headers.
721
+ * automatically include the current user's sessionId and site validation headers. Relative paths
722
+ * are automatically prefixed with `/api/v1/addons/<addonId>`.
131
723
  */
132
724
  api: AddonApi;
725
+ /**
726
+ * First-class settings API to load and save addon configuration without boilerplate path
727
+ * construction.
728
+ */
729
+ settings: AddonSettingsApi;
133
730
  /**
134
731
  * Display a temporary notification toast in the host UI.
135
732
  *
@@ -147,32 +744,7 @@ interface AddonSDK {
147
744
  * Interaction utilities for the active Slate message editor. Only available if your addon is
148
745
  * mounted near the chat input.
149
746
  */
150
- editor: {
151
- /**
152
- * Trigger a specialized editor command defined by the host.
153
- *
154
- * @example
155
- * sdk.editor.dispatchCommand("INSERT_YOUTUBE_COMMAND", "dQw4w9WgXcQ")
156
- */
157
- dispatchCommand: (command: string, payload?: unknown) => void;
158
- /**
159
- * Insert a low-level Slate node directly at the user's cursor position.
160
- *
161
- * @param node - A valid Slate Descendant or Fragment object
162
- */
163
- insertNode: (node: unknown) => void;
164
- /**
165
- * Upload and attach a file to the current message without inserting it inline. Users will see
166
- * the file in the attachment bar below the editor.
167
- */
168
- addAttachment: (attachment: {
169
- url: string;
170
- name: string;
171
- type: string;
172
- }) => void;
173
- /** Force the browser focus back to the editor input */
174
- focus: () => void;
175
- };
747
+ editor: AddonEditorApi;
176
748
  /** Event Bus for reacting to real-time application state changes. */
177
749
  events: {
178
750
  /**
@@ -189,6 +761,10 @@ interface AddonSDK {
189
761
  * Use this to build a font picker or apply a matching font family in your addon UI.
190
762
  */
191
763
  supportedFonts?: AddonFont[];
764
+ /** The active user locale (e.g. 'en', 'ta', 'ar') */
765
+ locale: string;
766
+ /** Text direction for the active locale ('ltr' or 'rtl') */
767
+ dir: "ltr" | "rtl";
192
768
  }
193
769
  /**
194
770
  * Seed data for your addon's tables, provided inside the addon's isolated schema.
@@ -197,7 +773,7 @@ interface AddonSDK {
197
773
  * provide default values, references, or supporting data. The host guarantees that these documents
198
774
  * do not conflict with core schemas or other addons.
199
775
  */
200
- interface AddonSeedDocument {
776
+ export interface AddonSeedDocument {
201
777
  /** Addon-owned table name. Core prefixes the addon's isolated PostgreSQL schema. */
202
778
  table: string;
203
779
  /** Behavior when a row conflicts with an existing unique or primary key. */
@@ -208,9 +784,9 @@ interface AddonSeedDocument {
208
784
  rows: Record<string, unknown>[];
209
785
  }
210
786
  /** Define a manifest with full validation-friendly typing while preserving literal values. */
211
- declare function defineAddonManifest<const TManifest extends AddonManifest>(manifest: TManifest): TManifest;
787
+ export declare function defineAddonManifest<const TManifest extends AddonManifest>(manifest: TManifest): TManifest;
212
788
  /** Define a typed server factory without widening its addon database interface. */
213
- declare function defineAddonServer<TDatabase = Record<string, Record<string, unknown>>>(factory: AddonServerFactory<TDatabase>): AddonServerFactory<TDatabase>;
789
+ export declare function defineAddonServer<TDatabase = Record<string, Record<string, unknown>>>(factory: AddonServerFactory<TDatabase>): AddonServerFactory<TDatabase>;
214
790
  /**
215
791
  * Access the host application's SDK. Provides authenticated API access, toast notifications, and
216
792
  * navigation.
@@ -232,27 +808,7 @@ declare function defineAddonServer<TDatabase = Record<string, Record<string, unk
232
808
  * }
233
809
  * ```
234
810
  */
235
- declare function useAddonSDK(): AddonSDK;
236
- /**
237
- * Access the host application's active icon pack. Returns the icon set matching the site's
238
- * configured icon pack preference.
239
- *
240
- * This function is a shell; the real implementation is provided by the Jchat host environment at
241
- * runtime.
242
- *
243
- * @example
244
- * ;```tsx
245
- * import { useIcons } from "@jchat/addon-sdk"
246
- *
247
- * export default function MyAddon() {
248
- * const { Icons } = useIcons()
249
- * return <Icons.Send className="size-5" />
250
- * }
251
- * ```
252
- */
253
- declare function useIcons(): {
254
- Icons: IconPack;
255
- };
811
+ export declare function useAddonSDK(): AddonSDK;
256
812
  /**
257
813
  * Get the version of the addon SDK. This is useful for checking compatibility with the host
258
814
  * application.
@@ -266,6 +822,28 @@ declare function useIcons(): {
266
822
  *
267
823
  * @returns
268
824
  */
269
- declare function getAddonSdkVersion(): string;
825
+ export declare function getAddonSdkVersion(): string;
826
+ /**
827
+ * Verifies purchase entitlement and identity integrity for commercial addons. Throws an error if
828
+ * the addon fails entitlement verification or if its manifest ID has been tampered with to
829
+ * impersonate another addon.
830
+ *
831
+ * @example
832
+ * ;```ts
833
+ * export default ({ verifyEntitlement, db, t }: AddonServerContext, app: AddonApp) => {
834
+ * verifyAddonEntitlement("tubescanpro", verifyEntitlement)
835
+ * return app.get("/trending", async () => { ... })
836
+ * }
837
+ * ```
838
+ *
839
+ * @param addonId - The canonical addon ID defined by the publisher (e.g. 'tubescanpro')
840
+ * @param target - Either the `verifyEntitlement` function from context or the `AddonServerContext`
841
+ * object itself
842
+ * @returns True if valid
843
+ * @throws Error if entitlement verification fails
844
+ */
845
+ export declare function verifyAddonEntitlement(addonId: string, target?: ((expectedAddonId?: string) => boolean) | {
846
+ verifyEntitlement?: (expectedAddonId?: string) => boolean;
847
+ }): boolean;
270
848
  //#endregion
271
- export { AddonApi, AddonDatabase, AddonEventListener, AddonFont, AddonJsonBody, type AddonManifest, type AddonPermission, type AddonPermissionDefinition, AddonProps, AddonRequestUser, AddonSDK, AddonSeedDocument, AddonServerContext, AddonServerFactory, AddonUser, type ConfigSchemaField, MOUNT_POINTS, type MountPoint, defineAddonManifest, defineAddonServer, getAddonSdkVersion, useAddonSDK, useIcons };
849
+ export { ADDON_ID_PATTERN, AbilityBuilder, type ActiveAddon, type ActiveAddonFrontend, type AddonArchiveFile, type AddonAttachment, type AddonChatMessage, type AddonCommandDefinition, type AddonCommandParameter, type AddonDistribution, type AddonManifest, type AddonPermissionDefinition, type AddonRequestUser, type AddonResilientFetch, type AddonRuntimeLog, type AddonRuntimeLogFilter, type AddonRuntimeLogLevel, type AddonRuntimeLogSource, type AddonUser, COMPATIBILITY_RANGE_PATTERN, type ColumnType, type Generated, HOST_LOCALES, type IconComponent, type IconName, type IconPack, type InfiniteData, type Insertable, type JSONColumnType, MAX_ADDON_ARCHIVE_BYTES, MOUNT_POINTS, type MongoAbility, type MountPoint, type QueryKey, type ResilientFetchOptions, type Selectable, type Updateable, assertSafeRelativePath, canonicalManifest, createCanonicalAddonDigest, createMongoAbility, isCompatibilityRange, sortValue, subject, useQueryClient, validateAddonManifest };