@jchat/addon-sdk 1.0.0-alpha.20 → 1.0.0-alpha.21

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/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # @jchat/addon-sdk
2
2
 
3
+ ## 1.0.0-alpha.21
4
+
5
+ ### Minor Changes
6
+
7
+ - Add message lifecycle interception hooks and chat message card mount point support
8
+
9
+ - Add TypeScript typings for available editor commands in Addon SDK
10
+
11
+ - Add verifyAddonEntitlement runtime helper and AddonServerContext method
12
+
13
+ ### Patch Changes
14
+
15
+ - Remove redundant boilerplate test files
16
+
17
+ - Enhance add-on CSS compilation to ensure consistent styling across dialogs and portals
18
+
19
+ - Bumped `@jchat/*` dependencies:
20
+ - [`icon@1.0.0-alpha.21`](https://github.com/mjjabarullah/jchat/releases/tag/icon@v1.0.0-alpha.21)
21
+ - [`ui@1.0.0-alpha.21`](https://github.com/mjjabarullah/jchat/releases/tag/ui@v1.0.0-alpha.21)
22
+
3
23
  ## 1.0.0-alpha.20
4
24
 
5
25
  ### Patch Changes
@@ -36,15 +36,34 @@ export declare const MOUNT_POINTS: {
36
36
  readonly EMOJI_PICKER_GIF_TAB: "emoji-picker-gif-tab";
37
37
  /** Custom tab in the emoji/sticker selection panel */
38
38
  readonly EMOJI_PICKER_STICKER_TAB: "emoji-picker-sticker-tab";
39
- /** Full-screen or modal dialog specifically for TubeScan */
40
- readonly TUBESCAN_DIALOG: "tubescan-dialog";
41
39
  /** Dedicated settings panel rendered in the Owner Dashboard */
42
40
  readonly OWNER_SETTINGS: "owner-settings";
43
41
  /** Dedicated settings panel rendered in the Site Admin Dashboard */
44
42
  readonly ADDON_SETTINGS: "addon-settings";
43
+ /** Custom interactive card rendered inside an enriched chat message */
44
+ readonly CHAT_MESSAGE_CARD: "chat-message-card";
45
+ /** Custom badge, audio equalizer, or particle aura rendered on a chat bubble */
46
+ readonly CHAT_MESSAGE_FLAIR: "chat-message-flair";
45
47
  };
46
48
  /** Union type of all valid addon mount points. */
47
49
  export type MountPoint = (typeof MOUNT_POINTS)[keyof typeof MOUNT_POINTS];
50
+ /** Parameter specification for an addon slash command. */
51
+ export interface AddonCommandParameter {
52
+ name: string;
53
+ description?: string;
54
+ required?: boolean;
55
+ }
56
+ /** Declaration of an addon slash command available in the chat composer. */
57
+ export interface AddonCommandDefinition {
58
+ /** The command keyword without leading slash (e.g. 'yt', 'roll', 'poll') */
59
+ name: string;
60
+ /** Short summary of what the command does, displayed in autocomplete */
61
+ description: string;
62
+ /** Usage syntax displayed as a hint (e.g. '<query | url>') */
63
+ syntax?: string;
64
+ /** Parameters accepted by this command */
65
+ parameters?: AddonCommandParameter[];
66
+ }
48
67
  /**
49
68
  * The `manifest.json` schema required for every Jchat addon. This file describes how the addon is
50
69
  * identified and mounted.
@@ -117,5 +136,7 @@ export interface AddonManifest {
117
136
  permissions?: string[];
118
137
  /** Custom permissions that should be registered with the host application on installation */
119
138
  permissionDefinitions?: AddonPermissionDefinition[];
139
+ /** Interactive slash commands exposed in the chat composer */
140
+ commands?: AddonCommandDefinition[];
120
141
  }
121
142
  //#endregion
@@ -27,12 +27,14 @@ const MOUNT_POINTS = {
27
27
  EMOJI_PICKER_GIF_TAB: "emoji-picker-gif-tab",
28
28
  /** Custom tab in the emoji/sticker selection panel */
29
29
  EMOJI_PICKER_STICKER_TAB: "emoji-picker-sticker-tab",
30
- /** Full-screen or modal dialog specifically for TubeScan */
31
- TUBESCAN_DIALOG: "tubescan-dialog",
32
30
  /** Dedicated settings panel rendered in the Owner Dashboard */
33
31
  OWNER_SETTINGS: "owner-settings",
34
32
  /** Dedicated settings panel rendered in the Site Admin Dashboard */
35
- ADDON_SETTINGS: "addon-settings"
33
+ ADDON_SETTINGS: "addon-settings",
34
+ /** Custom interactive card rendered inside an enriched chat message */
35
+ CHAT_MESSAGE_CARD: "chat-message-card",
36
+ /** Custom badge, audio equalizer, or particle aura rendered on a chat bubble */
37
+ CHAT_MESSAGE_FLAIR: "chat-message-flair"
36
38
  };
37
39
  //#endregion
38
40
  export { MOUNT_POINTS };
package/dist/index.mjs CHANGED
@@ -2,7 +2,7 @@ import { MOUNT_POINTS } from "./contracts.mjs";
2
2
  import { a as useTranslation, i as useLocale, n as LocaleContext, r as createTranslator, t as HOST_LANGUAGES } from "./i18n.mjs";
3
3
  import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
4
4
  //#region package.json
5
- var version = "1.0.0-alpha.19";
5
+ var version = "1.0.0-alpha.21";
6
6
  //#endregion
7
7
  //#region src/query.ts
8
8
  /**
@@ -145,5 +145,30 @@ function useAddonSDK() {
145
145
  function getAddonSdkVersion() {
146
146
  return version;
147
147
  }
148
+ /**
149
+ * Verifies purchase entitlement and identity integrity for commercial addons. Throws an error if
150
+ * the addon fails entitlement verification or if its manifest ID has been tampered with to
151
+ * impersonate another addon.
152
+ *
153
+ * @example
154
+ * ;```ts
155
+ * export default defineAddonServer((context, app) => {
156
+ * verifyAddonEntitlement("tubescanpro", context)
157
+ * return app.get("/trending", async () => { ... })
158
+ * })
159
+ * ```
160
+ *
161
+ * @param addonId - The canonical addon ID defined by the publisher (e.g. 'tubescanpro')
162
+ * @param context - The AddonServerContext passed to the addon's server factory
163
+ * @returns True if valid
164
+ * @throws Error if entitlement verification fails
165
+ */
166
+ function verifyAddonEntitlement(addonId, context) {
167
+ if (context?.verifyEntitlement) {
168
+ if (!context.verifyEntitlement(addonId)) throw new Error(`addon_entitlement_verification_failed:${addonId}`);
169
+ return true;
170
+ }
171
+ return true;
172
+ }
148
173
  //#endregion
149
- export { HOST_LANGUAGES, LocaleContext, MOUNT_POINTS, createTranslator, defineAddonManifest, defineAddonServer, getAddonSdkVersion, useAddonInfiniteQuery, useAddonMutation, useAddonQuery, useAddonSDK, useLocale, useQueryClient, useTranslation };
174
+ export { HOST_LANGUAGES, LocaleContext, MOUNT_POINTS, createTranslator, defineAddonManifest, defineAddonServer, getAddonSdkVersion, useAddonInfiniteQuery, useAddonMutation, useAddonQuery, useAddonSDK, useLocale, useQueryClient, useTranslation, verifyAddonEntitlement };
package/dist/index2.d.mts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { i as IconPack, n as IconComponent, r as IconName } from "./types.mjs";
2
- import { AddonManifest, AddonPermissionDefinition, MOUNT_POINTS, MountPoint } from "./contracts.mjs";
2
+ import { AddonCommandDefinition, AddonCommandParameter, AddonManifest, AddonPermissionDefinition, MOUNT_POINTS, MountPoint } from "./contracts.mjs";
3
3
  import { InfiniteData, MutationFunction, QueryFunction, QueryFunctionContext, QueryKey, UndefinedInitialDataInfiniteOptions, UndefinedInitialDataOptions, UseInfiniteQueryResult, UseMutationOptions, UseMutationResult, UseQueryResult, useQueryClient } from "@tanstack/react-query";
4
4
  import { Elysia, t } from "elysia";
5
5
  import { ColumnType, Generated, Insertable, JSONColumnType, Kysely, Selectable, Updateable } from "kysely";
@@ -209,6 +209,61 @@ export interface AddonServerEmitTarget {
209
209
  /** Target a specific user */
210
210
  userId?: string;
211
211
  }
212
+ export interface AddonMessageMutableData {
213
+ /** Message content (can be Slate JSON structure or string) */
214
+ content: unknown;
215
+ /** Optional message attachments */
216
+ attachments?: unknown[] | null;
217
+ /**
218
+ * Namespaced metadata bucket for this addon. Keyed as `metadata["addon:<addonId>"]`. Storing data
219
+ * here automatically attaches it to the message broadcast and database record.
220
+ */
221
+ metadata: Record<string, unknown>;
222
+ }
223
+ export interface AddonMessageContext {
224
+ /** The site ID where the message is being sent */
225
+ siteId: string;
226
+ /** The chat room ID */
227
+ roomId: string;
228
+ /** The authenticated user sending the message (read-only) */
229
+ user: AddonRequestUser;
230
+ /** Mutable message payload */
231
+ message: AddonMessageMutableData;
232
+ /** Convenience method to set metadata under this addon's namespace */
233
+ setMetadata: (key: string, value: unknown) => void;
234
+ /**
235
+ * Abort the message sending pipeline with a user-facing error code and reason. Only active during
236
+ * `beforeValidate` and `transformContent`.
237
+ */
238
+ abort: (error: {
239
+ code: string;
240
+ reason?: string;
241
+ }) => never;
242
+ }
243
+ export interface AddonMessageAfterSaveContext {
244
+ siteId: string;
245
+ roomId: string;
246
+ user: AddonRequestUser;
247
+ message: {
248
+ id: string;
249
+ content: unknown;
250
+ attachments?: unknown[] | null;
251
+ metadata?: Record<string, unknown> | null;
252
+ type: string;
253
+ createdAt: Date | string;
254
+ };
255
+ }
256
+ export interface AddonPipelineHooks {
257
+ /**
258
+ * Run before validation to inspect or guard actions (rate-limits, permission gates). Throw or
259
+ * call `context.abort()` to reject the message.
260
+ */
261
+ beforeValidate?: (context: AddonMessageContext) => Promise<void> | void;
262
+ /** Run before persistence to mutate text, parse slash commands, or enrich metadata. */
263
+ transformContent?: (context: AddonMessageContext) => Promise<void> | void;
264
+ /** Run asynchronously after database save (fire-and-forget observers). */
265
+ afterSave?: (context: AddonMessageAfterSaveContext) => Promise<void> | void;
266
+ }
212
267
  /**
213
268
  * Context provided to an addon's server-side factory. Parameterize it with the addon's table
214
269
  * interface: `AddonServerContext<MyAddonDatabase>`.
@@ -249,6 +304,21 @@ export interface AddonServerContext<TDatabase = Record<string, Record<string, un
249
304
  * `sdk.events.on('addon:<addonId>:<event>', callback)` or `sdk.events.on(event, callback)`.
250
305
  */
251
306
  emit: (event: string, payload: unknown, target?: AddonServerEmitTarget) => Promise<void>;
307
+ /**
308
+ * Register lifecycle interception hooks for chat messages. Hooks run in the core chat execution
309
+ * path with strict 50ms timeout guards and fail-open fault tolerance.
310
+ */
311
+ pipeline?: {
312
+ register: (hooks: AddonPipelineHooks) => void;
313
+ };
314
+ /**
315
+ * Cryptographically verify that this addon instance holds a valid host license entitlement.
316
+ * Commercial paid addons should invoke this or call `verifyAddonEntitlement(addonId, context)`.
317
+ *
318
+ * @param expectedAddonId - The canonical addon ID defined by the publisher
319
+ * @returns True if valid, or false if entitlement is missing or invalid
320
+ */
321
+ verifyEntitlement?: (expectedAddonId?: string) => boolean;
252
322
  }
253
323
  /**
254
324
  * Elysia app instance provided to addon server endpoints with Jchat macro metadata (e.g.
@@ -309,6 +379,10 @@ export interface AddonProps {
309
379
  locale?: string;
310
380
  /** Text direction for the current locale */
311
381
  dir?: "ltr" | "rtl";
382
+ /** The message object when rendered inside 'chat-message-card' or 'chat-message-flair' */
383
+ message?: unknown;
384
+ /** The addon's specific payload extracted from message.metadata */
385
+ addonData?: unknown;
312
386
  }
313
387
  export interface AddonApi {
314
388
  get: <TResponse = unknown>(path: string) => Promise<TResponse>;
@@ -337,6 +411,59 @@ export interface AddonFont {
337
411
  /** Relative size multiplier — apply this to base font sizes to match the host's rendering */
338
412
  scale: number;
339
413
  }
414
+ export interface AddonEditorInsertImagePayload {
415
+ src: string;
416
+ altText: string;
417
+ emoji?: {
418
+ col: number;
419
+ row: number;
420
+ size: number;
421
+ };
422
+ className?: string;
423
+ }
424
+ export interface AddonEditorInsertEmojiPayload {
425
+ col: number;
426
+ row: number;
427
+ unicode: string;
428
+ shortcode?: string;
429
+ }
430
+ export interface AddonEditorCommandMap {
431
+ INSERT_YOUTUBE_COMMAND: string;
432
+ INSERT_TEXT: string;
433
+ INSERT_TEXT_COMMAND: string;
434
+ INSERT_MENTION_COMMAND: string;
435
+ INSERT_IMAGE_COMMAND: AddonEditorInsertImagePayload;
436
+ INSERT_EMOJI_COMMAND: AddonEditorInsertEmojiPayload;
437
+ CLEAR_EDITOR: void;
438
+ CLEAR_EDITOR_COMMAND: void;
439
+ }
440
+ export type AddonEditorCommand = keyof AddonEditorCommandMap;
441
+ export interface AddonEditorApi {
442
+ /**
443
+ * Trigger a specialized editor command defined by the host.
444
+ *
445
+ * @example
446
+ * sdk.editor.dispatchCommand("INSERT_YOUTUBE_COMMAND", "dQw4w9WgXcQ")
447
+ */
448
+ dispatchCommand: <C extends AddonEditorCommand>(command: C, ...args: AddonEditorCommandMap[C] extends void ? [payload?: undefined] : [payload: AddonEditorCommandMap[C]]) => void;
449
+ /**
450
+ * Insert a low-level Slate node directly at the user's cursor position.
451
+ *
452
+ * @param node - A valid Slate Descendant or Fragment object
453
+ */
454
+ insertNode: (node: unknown) => void;
455
+ /**
456
+ * Upload and attach a file to the current message without inserting it inline. Users will see the
457
+ * file in the attachment bar below the editor.
458
+ */
459
+ addAttachment: (attachment: {
460
+ url: string;
461
+ name: string;
462
+ type: string;
463
+ }) => void;
464
+ /** Force the browser focus back to the editor input */
465
+ focus: () => void;
466
+ }
340
467
  /**
341
468
  * The bridge between your addon and the Jchat host application. Use the `useAddonSDK()` hook to
342
469
  * access this interface in your components.
@@ -370,32 +497,7 @@ export interface AddonSDK {
370
497
  * Interaction utilities for the active Slate message editor. Only available if your addon is
371
498
  * mounted near the chat input.
372
499
  */
373
- editor: {
374
- /**
375
- * Trigger a specialized editor command defined by the host.
376
- *
377
- * @example
378
- * sdk.editor.dispatchCommand("INSERT_YOUTUBE_COMMAND", "dQw4w9WgXcQ")
379
- */
380
- dispatchCommand: (command: string, payload?: unknown) => void;
381
- /**
382
- * Insert a low-level Slate node directly at the user's cursor position.
383
- *
384
- * @param node - A valid Slate Descendant or Fragment object
385
- */
386
- insertNode: (node: unknown) => void;
387
- /**
388
- * Upload and attach a file to the current message without inserting it inline. Users will see
389
- * the file in the attachment bar below the editor.
390
- */
391
- addAttachment: (attachment: {
392
- url: string;
393
- name: string;
394
- type: string;
395
- }) => void;
396
- /** Force the browser focus back to the editor input */
397
- focus: () => void;
398
- };
500
+ editor: AddonEditorApi;
399
501
  /** Event Bus for reacting to real-time application state changes. */
400
502
  events: {
401
503
  /**
@@ -474,5 +576,26 @@ export declare function useAddonSDK(): AddonSDK;
474
576
  * @returns
475
577
  */
476
578
  export declare function getAddonSdkVersion(): string;
579
+ /**
580
+ * Verifies purchase entitlement and identity integrity for commercial addons. Throws an error if
581
+ * the addon fails entitlement verification or if its manifest ID has been tampered with to
582
+ * impersonate another addon.
583
+ *
584
+ * @example
585
+ * ;```ts
586
+ * export default defineAddonServer((context, app) => {
587
+ * verifyAddonEntitlement("tubescanpro", context)
588
+ * return app.get("/trending", async () => { ... })
589
+ * })
590
+ * ```
591
+ *
592
+ * @param addonId - The canonical addon ID defined by the publisher (e.g. 'tubescanpro')
593
+ * @param context - The AddonServerContext passed to the addon's server factory
594
+ * @returns True if valid
595
+ * @throws Error if entitlement verification fails
596
+ */
597
+ export declare function verifyAddonEntitlement(addonId: string, context?: {
598
+ verifyEntitlement?: (expectedAddonId?: string) => boolean;
599
+ }): boolean;
477
600
  //#endregion
478
- export { type AddonManifest, type AddonPermissionDefinition, type ColumnType, type Generated, type IconComponent, type IconName, type IconPack, type InfiniteData, type Insertable, type JSONColumnType, MOUNT_POINTS, type MountPoint, type QueryKey, type Selectable, type Updateable, useQueryClient };
601
+ export { type AddonCommandDefinition, type AddonCommandParameter, type AddonManifest, type AddonPermissionDefinition, type ColumnType, type Generated, type IconComponent, type IconName, type IconPack, type InfiniteData, type Insertable, type JSONColumnType, MOUNT_POINTS, type MountPoint, type QueryKey, type Selectable, type Updateable, useQueryClient };
package/dist/vite.mjs CHANGED
@@ -82,7 +82,7 @@ function jchatAddon(options) {
82
82
  if (!fs.existsSync(buildDir)) return;
83
83
  const bundleFile = fs.readdirSync(buildDir).find((f) => f.includes("ui") && f.endsWith(".js"));
84
84
  if (!bundleFile) return;
85
- if (options.css !== false) embedCssIntoBundle(buildDir, manifestData.id);
85
+ if (options.css !== false) embedCssIntoBundle(buildDir);
86
86
  let manifest = {
87
87
  ...manifestData,
88
88
  version: pkg.version,
@@ -243,7 +243,7 @@ function requireAddonSigningKey(signingPrivateKey, developmentBuild) {
243
243
  if (developmentBuild) return void 0;
244
244
  throw new Error("Database-bearing addon builds require JCHAT_ADDON_SIGNING_PRIVATE_KEY");
245
245
  }
246
- function embedCssIntoBundle(buildDir, addonId) {
246
+ function embedCssIntoBundle(buildDir) {
247
247
  const files = fs.readdirSync(buildDir);
248
248
  const cssFile = files.find((f) => f.endsWith(".css"));
249
249
  const jsFile = files.find((f) => f.includes("ui") && f.endsWith(".js"));
@@ -274,7 +274,7 @@ function embedCssIntoBundle(buildDir, addonId) {
274
274
  css = css.replace(/\.user-border\s*\{[^}]+\}/g, "");
275
275
  css = css.replace(/\.scrollbar-hide[^{]*\{[^}]+\}/g, "");
276
276
  css = css.replace(/@layer\s+components(?:\s*\{[^}]*\}|;)/g, "");
277
- css = minifyCss(`@scope (#addon-${addonId}) { ${css} }`);
277
+ css = minifyCss(css);
278
278
  }
279
279
  if (css.trim().length > 0) {
280
280
  const injection = `
@@ -294,12 +294,14 @@ function embedCssIntoBundle(buildDir, addonId) {
294
294
  } catch {}
295
295
  }
296
296
  function stripLayer(css, layerName) {
297
- const marker = `@layer ${layerName} {`;
298
- let idx = css.indexOf(marker);
299
- while (idx !== -1) {
300
- let braceCount = 0;
301
- let endIdx = idx + marker.length - 1;
302
- for (let i = idx + marker.length - 1; i < css.length; i++) if (css[i] === "{") braceCount++;
297
+ const regex = new RegExp(`@layer\\s+${layerName}\\s*\\{`);
298
+ let match = regex.exec(css);
299
+ while (match) {
300
+ const startIdx = match.index;
301
+ const openBraceIdx = startIdx + match[0].length - 1;
302
+ let braceCount = 1;
303
+ let endIdx = -1;
304
+ for (let i = openBraceIdx + 1; i < css.length; i++) if (css[i] === "{") braceCount++;
303
305
  else if (css[i] === "}") {
304
306
  braceCount--;
305
307
  if (braceCount === 0) {
@@ -307,8 +309,9 @@ function stripLayer(css, layerName) {
307
309
  break;
308
310
  }
309
311
  }
310
- css = css.slice(0, idx) + css.slice(endIdx + 1);
311
- idx = css.indexOf(marker);
312
+ if (endIdx === -1) break;
313
+ css = css.slice(0, startIdx) + css.slice(endIdx + 1);
314
+ match = regex.exec(css);
312
315
  }
313
316
  return css;
314
317
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jchat/addon-sdk",
3
- "version": "1.0.0-alpha.20",
3
+ "version": "1.0.0-alpha.21",
4
4
  "private": false,
5
5
  "description": "JChat Addon SDK for building JChat addons.",
6
6
  "keywords": [