@jchat/addon-sdk 1.0.0-alpha.27 → 1.0.0-alpha.28

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,18 @@
1
1
  # @jchat/addon-sdk
2
2
 
3
+ ## 1.0.0-alpha.28
4
+
5
+ ### Minor Changes
6
+
7
+ - Add ABAC ability evaluator, authorizeOwnership, and AddonCan UI primitive
8
+
9
+ ### Patch Changes
10
+
11
+ - Add comprehensive JSDoc documentation and align context ownership helpers
12
+
13
+ - Bumped `@jchat/*` dependencies:
14
+ - [`ui@1.0.0-alpha.27`](https://github.com/jchatcloud/jchat/releases/tag/ui@v1.0.0-alpha.27)
15
+
3
16
  ## 1.0.0-alpha.27
4
17
 
5
18
  ### Patch Changes
package/dist/cli.d.mts CHANGED
@@ -1,35 +1,54 @@
1
1
  import { l as AddonManifest } from "./contracts2.mjs";
2
2
  import { InlineConfig } from "vite";
3
3
  //#region src/cli.d.ts
4
- /** Options for resolving addon build configuration. */
4
+ /** Options supplied to {@link resolveAddonBuildConfig} to configure addon compilation and bundling. */
5
5
  export interface AddonCliOptions {
6
+ /** Root workspace directory of the addon (defaults to `process.cwd()`). */
6
7
  rootDir?: string;
8
+ /** CLI execution subcommand (`'build'`, `'dev'`, or `'help'`). */
7
9
  command?: "build" | "dev" | "help";
10
+ /** Vite compilation mode (`'production'` or `'development'`). */
8
11
  mode?: "production" | "development";
12
+ /** Whether to watch filesystem changes and trigger live rebuilds. */
9
13
  watch?: boolean;
14
+ /**
15
+ * Whether to bundle distribution artifacts into an installation `.zip` archive upon build
16
+ * completion.
17
+ */
10
18
  zip?: boolean;
19
+ /** Optional PEM/PKCS8 private key string used to cryptographically sign the distribution archive. */
11
20
  signingPrivateKey?: string;
12
21
  }
13
22
  /**
14
- * Automatically loads the addon manifest from `src/manifest.ts`, `src/manifest.js`, or
15
- * `src/manifest.json`.
23
+ * Dynamically loads and validates the addon manifest from standard file locations:
24
+ * `src/manifest.ts`, `src/manifest.js`, `src/manifest.mjs`, or `src/manifest.json`.
16
25
  *
17
- * @param rootDir - Root directory of the addon workspace.
18
- * @returns Parsed {@link AddonManifest}.
26
+ * @param rootDir - Root directory path of the addon package.
27
+ * @returns A promise resolving to the parsed {@link AddonManifest}.
28
+ * @throws {Error} When no manifest file can be found in the `src/` directory.
19
29
  */
20
30
  export declare function loadAddonManifest(rootDir: string): Promise<AddonManifest>;
21
31
  /**
22
- * Resolves the full Vite configuration for building a JChat addon, merging user custom configs if
23
- * present.
32
+ * Resolves and composes the complete Vite configuration for compiling a JChat addon.
24
33
  *
25
- * @param options - CLI build options.
26
- * @returns Fully merged {@link InlineConfig} for Vite.
34
+ * Automatically integrates:
35
+ *
36
+ * - Tailwind CSS Vite compiler plugin
37
+ * - Core JChat Addon plugin (`jchatAddon`) with asset bundling and signing
38
+ * - Custom user Vite configurations (`vite.config.{ts,js,mjs,cjs}`) with deduplication
39
+ *
40
+ * @param options - Configuration options for the build pipeline.
41
+ * @returns A promise resolving to the fully merged {@link InlineConfig} for Vite.
27
42
  */
28
43
  export declare function resolveAddonBuildConfig(options?: AddonCliOptions): Promise<InlineConfig>;
29
44
  /**
30
- * Main CLI handler for `jchat-addon`.
45
+ * Main CLI entry point handler for the `jchat-addon` binary command.
46
+ *
47
+ * Parses command-line arguments, configures build parameters (mode, watch, zip packaging), resolves
48
+ * configuration via {@link resolveAddonBuildConfig}, and executes the Vite build lifecycle.
31
49
  *
32
- * @param args - CLI arguments (defaults to `process.argv.slice(2)`).
50
+ * @param args - CLI arguments array (defaults to `process.argv.slice(2)`).
51
+ * @returns A promise that resolves when compilation or dev server starts.
33
52
  */
34
53
  export declare function runAddonCli(args?: string[]): Promise<void>;
35
54
  //#endregion
package/dist/cli.mjs CHANGED
@@ -5,6 +5,14 @@ import tailwindcss from "@tailwindcss/vite";
5
5
  import { pathToFileURL } from "node:url";
6
6
  import { build, loadConfigFromFile, mergeConfig } from "vite";
7
7
  //#region src/cli.ts
8
+ /**
9
+ * Recursively inspects a Vite plugin configuration to determine whether a plugin with the specified
10
+ * name is already registered.
11
+ *
12
+ * @param plugins - Vite plugin or array/nested tree of plugins.
13
+ * @param name - Target plugin identifier name to locate (e.g. `'jchat-addon'`).
14
+ * @returns `true` if the plugin is registered, `false` otherwise.
15
+ */
8
16
  function hasPlugin(plugins, name) {
9
17
  if (!plugins) return false;
10
18
  if (Array.isArray(plugins)) return plugins.some((p) => hasPlugin(p, name));
@@ -12,11 +20,12 @@ function hasPlugin(plugins, name) {
12
20
  return false;
13
21
  }
14
22
  /**
15
- * Automatically loads the addon manifest from `src/manifest.ts`, `src/manifest.js`, or
16
- * `src/manifest.json`.
23
+ * Dynamically loads and validates the addon manifest from standard file locations:
24
+ * `src/manifest.ts`, `src/manifest.js`, `src/manifest.mjs`, or `src/manifest.json`.
17
25
  *
18
- * @param rootDir - Root directory of the addon workspace.
19
- * @returns Parsed {@link AddonManifest}.
26
+ * @param rootDir - Root directory path of the addon package.
27
+ * @returns A promise resolving to the parsed {@link AddonManifest}.
28
+ * @throws {Error} When no manifest file can be found in the `src/` directory.
20
29
  */
21
30
  async function loadAddonManifest(rootDir) {
22
31
  const existingPath = [
@@ -36,11 +45,16 @@ async function loadAddonManifest(rootDir) {
36
45
  throw new Error(`Could not find addon manifest in ${rootDir}/src (expected manifest.ts or manifest.json)`);
37
46
  }
38
47
  /**
39
- * Resolves the full Vite configuration for building a JChat addon, merging user custom configs if
40
- * present.
48
+ * Resolves and composes the complete Vite configuration for compiling a JChat addon.
41
49
  *
42
- * @param options - CLI build options.
43
- * @returns Fully merged {@link InlineConfig} for Vite.
50
+ * Automatically integrates:
51
+ *
52
+ * - Tailwind CSS Vite compiler plugin
53
+ * - Core JChat Addon plugin (`jchatAddon`) with asset bundling and signing
54
+ * - Custom user Vite configurations (`vite.config.{ts,js,mjs,cjs}`) with deduplication
55
+ *
56
+ * @param options - Configuration options for the build pipeline.
57
+ * @returns A promise resolving to the fully merged {@link InlineConfig} for Vite.
44
58
  */
45
59
  async function resolveAddonBuildConfig(options = {}) {
46
60
  const rootDir = path.resolve(options.rootDir || process.cwd());
@@ -83,9 +97,13 @@ async function resolveAddonBuildConfig(options = {}) {
83
97
  return baseConfig;
84
98
  }
85
99
  /**
86
- * Main CLI handler for `jchat-addon`.
100
+ * Main CLI entry point handler for the `jchat-addon` binary command.
101
+ *
102
+ * Parses command-line arguments, configures build parameters (mode, watch, zip packaging), resolves
103
+ * configuration via {@link resolveAddonBuildConfig}, and executes the Vite build lifecycle.
87
104
  *
88
- * @param args - CLI arguments (defaults to `process.argv.slice(2)`).
105
+ * @param args - CLI arguments array (defaults to `process.argv.slice(2)`).
106
+ * @returns A promise that resolves when compilation or dev server starts.
89
107
  */
90
108
  async function runAddonCli(args = process.argv.slice(2)) {
91
109
  let command = "build";
@@ -173,6 +173,7 @@ interface AddonRequestUser {
173
173
  id: string;
174
174
  permissions?: string[];
175
175
  siteId?: string;
176
+ isOwner?: boolean;
176
177
  }
177
178
  /** Standard attachment contract for chat messages and addon file attachments. */
178
179
  interface AddonAttachment {
package/dist/index.mjs CHANGED
@@ -1,196 +1,5 @@
1
+ import { _ as hasPermission, a as useAddonSDK, c as useAddonInfiniteQuery, d as useQueryClient, f as AbilityBuilder, g as createMongoAbility, h as createAddonAbility, i as getAddonSdkVersion, l as useAddonMutation, m as authorizeResourceOwnership, n as defineAddonManifest, o as verifyAddonEntitlement, p as ForbiddenError, r as defineAddonServer, s as createMockAddonServerContext, t as AddonPipelineAbortError, u as useAddonQuery, v as subject } from "./src.mjs";
1
2
  import { HOST_LOCALES, MOUNT_POINTS } from "./contracts.mjs";
2
3
  import { i as useTranslation, n as createTranslator, r as useLocale, t as LocaleContext } from "./i18n.mjs";
3
4
  import { ADDON_ID_PATTERN, COMPATIBILITY_RANGE_PATTERN, MAX_ADDON_ARCHIVE_BYTES, assertSafeRelativePath, canonicalManifest, createCanonicalAddonDigest, isCompatibilityRange, sortValue, validateAddonManifest } from "./signing.mjs";
4
- import { createMockAddonServerContext } from "./testing.mjs";
5
- import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
6
- //#region package.json
7
- var version = "1.0.0-alpha.26";
8
- //#endregion
9
- //#region src/query.ts
10
- /**
11
- * Perform a declarative, cached GET request using the host's shared React Query instance.
12
- * Automatically injects authentication headers and prefixes addon paths.
13
- *
14
- * @example
15
- * ;```tsx
16
- * const { data, isLoading } = useAddonQuery<{ polls: Poll[] }>({
17
- * path: "/polls",
18
- * queryKey: [siteId]
19
- * })
20
- * ```
21
- */
22
- function useAddonQuery(options) {
23
- const sdk = useAddonSDK();
24
- const { path, queryKey = [], queryFn, ...queryOptions } = options;
25
- const effectiveKey = path !== void 0 ? [path, ...queryKey] : queryKey;
26
- const effectiveFn = queryFn ?? (() => {
27
- if (!path) throw new Error("useAddonQuery requires either a `path` or a custom `queryFn`");
28
- return sdk.api.get(path);
29
- });
30
- return useQuery({
31
- ...queryOptions,
32
- queryKey: effectiveKey,
33
- queryFn: effectiveFn
34
- });
35
- }
36
- /**
37
- * Perform a paginated or infinite scrolling request using the host's shared React Query instance.
38
- *
39
- * @example
40
- * ;```tsx
41
- * const { data, fetchNextPage, hasNextPage } = useAddonInfiniteQuery<GifResponse, Error, number>({
42
- * path: (page) => `/trending?offset=${page * 20}`,
43
- * initialPageParam: 0,
44
- * getNextPageParam: (lastPage, allPages) => (lastPage.hasMore ? allPages.length : undefined)
45
- * })
46
- * ```
47
- */
48
- function useAddonInfiniteQuery(options) {
49
- const sdk = useAddonSDK();
50
- const { path, queryKey = [], queryFn, ...infiniteOptions } = options;
51
- const effectiveKey = [typeof path === "string" ? path : "infinite", ...queryKey];
52
- const effectiveFn = queryFn ?? (async (context) => {
53
- const pageParam = context.pageParam;
54
- const resolvedPath = typeof path === "function" ? path(pageParam) : path;
55
- if (!resolvedPath) throw new Error("useAddonInfiniteQuery requires either a `path` or a custom `queryFn`");
56
- return sdk.api.get(resolvedPath);
57
- });
58
- return useInfiniteQuery({
59
- ...infiniteOptions,
60
- queryKey: effectiveKey,
61
- queryFn: effectiveFn
62
- });
63
- }
64
- /**
65
- * Perform a declarative mutation using the host's shared React Query instance. Automatically
66
- * executes the request and can invalidate specified query keys upon success.
67
- *
68
- * @example
69
- * ;```tsx
70
- * const { mutate, isPending } = useAddonMutation<Poll, CreatePollInput>({
71
- * path: "/polls",
72
- * method: "POST",
73
- * invalidateQueries: [["/polls"]],
74
- * onSuccess: () => sdk.toast("Poll created!", "success")
75
- * })
76
- * ```
77
- */
78
- function useAddonMutation(options) {
79
- const sdk = useAddonSDK();
80
- const queryClient = useQueryClient();
81
- const { path, method = "POST", invalidateQueries = [], mutationFn, onSuccess, ...mutationOptions } = options;
82
- const effectiveFn = mutationFn ?? (async (variables) => {
83
- const resolvedPath = typeof path === "function" ? path(variables) : path ?? "";
84
- if (!resolvedPath) throw new Error("useAddonMutation requires a valid 'path' or 'mutationFn'.");
85
- if (method === "PUT") return sdk.api.put(resolvedPath, variables);
86
- if (method === "DELETE") return sdk.api.delete(resolvedPath);
87
- return sdk.api.post(resolvedPath, variables);
88
- });
89
- return useMutation({
90
- ...mutationOptions,
91
- mutationFn: effectiveFn,
92
- onSuccess: async (...args) => {
93
- if (invalidateQueries.length > 0) await Promise.all(invalidateQueries.map((key) => queryClient.invalidateQueries({ queryKey: key })));
94
- if (onSuccess) await onSuccess(...args);
95
- }
96
- });
97
- }
98
- //#endregion
99
- //#region src/index.ts
100
- /**
101
- * @jchat/addon-sdk
102
- *
103
- * Lightweight SDK for Jchat addon developers. Provides TypeScript types for addon props, manifest
104
- * schema, and host API access. This package is always EXTERNALISED from addon bundles — the host
105
- * app provides the runtime implementation.
106
- */
107
- /** Standard error thrown or handled when an addon message pipeline hook aborts sending. */
108
- var AddonPipelineAbortError = class extends Error {
109
- code;
110
- reason;
111
- constructor(error) {
112
- super(error.reason || error.code);
113
- this.name = "AddonPipelineAbortError";
114
- this.code = error.code;
115
- this.reason = error.reason;
116
- }
117
- };
118
- /** Define a manifest with full validation-friendly typing while preserving literal values. */
119
- function defineAddonManifest(manifest) {
120
- return manifest;
121
- }
122
- /** Define a typed server factory without widening its addon database interface. */
123
- function defineAddonServer(factory) {
124
- return factory;
125
- }
126
- /**
127
- * Access the host application's SDK. Provides authenticated API access, toast notifications, and
128
- * navigation.
129
- *
130
- * This function is a shell; the real implementation is provided by the Jchat host environment at
131
- * runtime.
132
- *
133
- * @example
134
- * ;```tsx
135
- * import { useAddonSDK } from "@jchat/addon-sdk"
136
- *
137
- * export default function MyAddon({ mountPoint }: AddonProps) {
138
- * const sdk = useAddonSDK()
139
- *
140
- * const handleClick = async () => {
141
- * const data = await sdk.api.get("/trending")
142
- * sdk.toast("Fetched trending posts!", "success")
143
- * }
144
- * }
145
- * ```
146
- */
147
- function useAddonSDK() {
148
- const hostUseAddonSDK = typeof window !== "undefined" ? window.__JCHAT_EXTERNALS__?.JchatAddonSDK?.useAddonSDK : void 0;
149
- if (hostUseAddonSDK && hostUseAddonSDK !== useAddonSDK) return hostUseAddonSDK();
150
- throw new Error("AddonSDK must be used within the Jchat host environment.");
151
- }
152
- /**
153
- * Get the version of the addon SDK. This is useful for checking compatibility with the host
154
- * application.
155
- *
156
- * @example
157
- * ;```tsx
158
- * import { getAddonSdkVersion } from "@jchat/addon-sdk"
159
- *
160
- * console.log(getAddonSdkVersion())
161
- * ```
162
- *
163
- * @returns
164
- */
165
- function getAddonSdkVersion() {
166
- return version;
167
- }
168
- /**
169
- * Verifies purchase entitlement and identity integrity for commercial addons. Throws an error if
170
- * the addon fails entitlement verification or if its manifest ID has been tampered with to
171
- * impersonate another addon.
172
- *
173
- * @example
174
- * ;```ts
175
- * export default ({ verifyEntitlement, db, t }: AddonServerContext, app: AddonApp) => {
176
- * verifyAddonEntitlement("tubescanpro", verifyEntitlement)
177
- * return app.get("/trending", async () => { ... })
178
- * }
179
- * ```
180
- *
181
- * @param addonId - The canonical addon ID defined by the publisher (e.g. 'tubescanpro')
182
- * @param target - Either the `verifyEntitlement` function from context or the `AddonServerContext`
183
- * object itself
184
- * @returns True if valid
185
- * @throws Error if entitlement verification fails
186
- */
187
- function verifyAddonEntitlement(addonId, target) {
188
- const verifyFn = typeof target === "function" ? target : target?.verifyEntitlement;
189
- if (verifyFn) {
190
- if (!verifyFn(addonId)) throw new Error(`addon_entitlement_verification_failed:${addonId}`);
191
- return true;
192
- }
193
- return true;
194
- }
195
- //#endregion
196
- export { ADDON_ID_PATTERN, AddonPipelineAbortError, COMPATIBILITY_RANGE_PATTERN, HOST_LOCALES, LocaleContext, MAX_ADDON_ARCHIVE_BYTES, MOUNT_POINTS, assertSafeRelativePath, canonicalManifest, createCanonicalAddonDigest, createMockAddonServerContext, createTranslator, defineAddonManifest, defineAddonServer, getAddonSdkVersion, isCompatibilityRange, sortValue, useAddonInfiniteQuery, useAddonMutation, useAddonQuery, useAddonSDK, useLocale, useQueryClient, useTranslation, validateAddonManifest, verifyAddonEntitlement };
5
+ export { ADDON_ID_PATTERN, AbilityBuilder, AddonPipelineAbortError, COMPATIBILITY_RANGE_PATTERN, ForbiddenError, HOST_LOCALES, LocaleContext, MAX_ADDON_ARCHIVE_BYTES, MOUNT_POINTS, assertSafeRelativePath, authorizeResourceOwnership, canonicalManifest, createAddonAbility, createCanonicalAddonDigest, createMockAddonServerContext, createMongoAbility, createTranslator, defineAddonManifest, defineAddonServer, getAddonSdkVersion, hasPermission, isCompatibilityRange, sortValue, subject, useAddonInfiniteQuery, useAddonMutation, useAddonQuery, useAddonSDK, useLocale, useQueryClient, useTranslation, validateAddonManifest, verifyAddonEntitlement };
package/dist/index2.d.mts CHANGED
@@ -1,9 +1,115 @@
1
1
  import { i as IconPack, n as IconComponent, r as IconName } from "./types.mjs";
2
2
  import { _ as HOST_LOCALES, a as AddonChatMessage, b as MountPoint, c as AddonDistribution, d as AddonRequestUser, f as AddonRuntimeLog, g as AddonUser, h as AddonRuntimeLogSource, i as AddonAttachment, l as AddonManifest, m as AddonRuntimeLogLevel, n as ActiveAddonFrontend, o as AddonCommandDefinition, p as AddonRuntimeLogFilter, r as AddonArchiveFile, s as AddonCommandParameter, t as ActiveAddon, u as AddonPermissionDefinition, v as HostLocale, y as MOUNT_POINTS } from "./contracts2.mjs";
3
3
  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";
4
+ import { AbilityBuilder, MongoAbility, createMongoAbility, subject } from "@casl/ability";
4
5
  import { InfiniteData, MutationFunction, QueryFunction, QueryFunctionContext, QueryKey, UndefinedInitialDataInfiniteOptions, UndefinedInitialDataOptions, UseInfiniteQueryResult, UseMutationOptions, UseMutationResult, UseQueryResult, useQueryClient } from "@tanstack/react-query";
5
6
  import { Elysia, t } from "elysia";
6
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
7
113
  //#region src/i18n.d.ts
8
114
  export type HostLanguage = HostLocale;
9
115
  export type AddonTranslationDictionary = Record<string, unknown>;
@@ -152,26 +258,57 @@ export declare function useAddonInfiniteQuery<TData = unknown, TError = Error, T
152
258
  export declare function useAddonMutation<TData = unknown, TVariables = unknown, TError = Error, TContext = unknown>(options: UseAddonMutationOptions<TData, TVariables, TError, TContext>): UseMutationResult<TData, TError, TVariables, TContext>;
153
259
  //#endregion
154
260
  //#region src/testing.d.ts
261
+ /** Configuration options supplied to {@link createMockAddonServerContext}. */
155
262
  export interface MockAddonServerContextOptions {
263
+ /** Simulated tenant site identifier (defaults to `'site_test_123'`). */
156
264
  siteId?: string;
265
+ /** Simulated addon configuration settings returned by `context.getConfig()`. */
157
266
  config?: Record<string, unknown>;
267
+ /** Simulated user profile returned by `context.getUser()` and `context.getUsers()`. */
158
268
  user?: Partial<AddonUser>;
269
+ /**
270
+ * Simulated commercial license entitlement status returned by `context.verifyEntitlement()`.
271
+ * Defaults to `true`.
272
+ */
159
273
  isEntitled?: boolean;
160
274
  }
275
+ /** Inspection fixtures and mock context returned by {@link createMockAddonServerContext}. */
161
276
  export interface MockAddonContextResult {
277
+ /** The fully functioning mock {@link AddonServerContext} instance. */
162
278
  context: AddonServerContext;
279
+ /** Array capturing all events emitted via `context.emit()`, enabling assertions in unit tests. */
163
280
  emittedEvents: Array<{
164
281
  event: string;
165
282
  payload: unknown;
166
283
  target?: AddonServerEmitTarget;
167
284
  }>;
285
+ /** Array capturing all pipeline hooks registered via `context.pipeline.register()`. */
168
286
  registeredHooks: AddonPipelineHooks[];
287
+ /** In-memory Map of key-value cache entries written via `context.cache`. */
169
288
  cacheEntries: Map<string, unknown>;
289
+ /** In-memory Map of Redis Set collections written via `context.cache.setAdd()` / `setRemove()`. */
170
290
  cacheSets: Map<string, Set<string>>;
171
291
  }
172
292
  /**
173
- * Creates an in-memory, zero-dependency AddonServerContext mock for testing addon server factories
174
- * and pipeline hooks offline in CI without requiring live databases or Redis.
293
+ * Creates an in-memory, zero-dependency {@link AddonServerContext} mock for testing addon server
294
+ * routes, lifecycle handlers, and pipeline hooks offline in CI without requiring live database
295
+ * connections or Redis.
296
+ *
297
+ * Provides complete mock implementations of:
298
+ *
299
+ * - **Key-Value & Set Cache**: Supports `get`, `set` (with TTL simulation), `del`, `incr`, `expire`,
300
+ * `ttl`, `setAdd`, `setRemove`, and `setMembers` with automatic tenant/addon namespacing.
301
+ * - **Sliding-Window Rate Limiting**: In-memory timestamp-bucket rate limiting via `consume`.
302
+ * - **Event Capturing**: Records emitted events in {@link MockAddonContextResult.emittedEvents} with
303
+ * canonical `addon:<addonId>:<event>` prefixing.
304
+ * - **Pipeline Hook Registration**: Inspects registered hook stages and handlers.
305
+ * - **Fine-Grained ABAC & Ownership**: Enforces CASL `can` attribute queries and BOLA protection via
306
+ * `authorizeOwnership`.
307
+ * - **License Entitlement**: Deterministic commercial entitlement verification.
308
+ *
309
+ * @param addonId - Unique identifier of the addon under test.
310
+ * @param options - Optional mock configuration parameters.
311
+ * @returns A {@link MockAddonContextResult} containing the mock context and inspection fixtures.
175
312
  */
176
313
  export declare function createMockAddonServerContext(addonId: string, options?: MockAddonServerContextOptions): MockAddonContextResult;
177
314
  //#endregion
@@ -371,10 +508,36 @@ export interface AddonServerContext<TDatabase = Record<string, Record<string, un
371
508
  * @returns True if valid, or false if entitlement is missing or invalid
372
509
  */
373
510
  verifyEntitlement?: (expectedAddonId?: string) => boolean;
511
+ /**
512
+ * Enforces resource ownership rules ('own' vs 'any' permissions). Prevents Broken Object-Level
513
+ * Authorization (BOLA/IDOR).
514
+ *
515
+ * @param params - The actor, owner ID, and permission requirements
516
+ * @returns "own" if the actor owns the resource, or "any" if granted via moderator/admin
517
+ * privileges
518
+ * @throws ForbiddenError if neither condition is satisfied
519
+ */
520
+ authorizeOwnership: (params: {
521
+ actor: AddonRequestUser;
522
+ ownerId: string;
523
+ ownPermission: string;
524
+ anyPermission: string;
525
+ resourceLabel?: string;
526
+ }) => ResourceOwnershipScope;
527
+ /**
528
+ * Evaluates fine-grained Attribute-Based Access Control (ABAC) rules for custom addon entities.
529
+ *
530
+ * @param actor - The authenticated request user
531
+ * @param action - Action to perform (e.g. 'read', 'delete', 'close')
532
+ * @param subject - Entity name (e.g. 'poll', 'story', 'track')
533
+ * @param attributes - Resource attributes (e.g. { userId: poll.userId, status: 'open' })
534
+ * @returns True if allowed, false otherwise
535
+ */
536
+ can: (actor: AddonRequestUser | null | undefined, action: string, subject: string, attributes?: Record<string, unknown>) => boolean;
374
537
  }
375
538
  /**
376
539
  * Elysia app instance provided to addon server endpoints with Jchat macro metadata (e.g.
377
- * requirePermission) typed.
540
+ * requirePermission, requireOwnership) typed.
378
541
  */
379
542
  export type AddonApp = Elysia<"", {
380
543
  decorator: {};
@@ -391,6 +554,12 @@ export type AddonApp = Elysia<"", {
391
554
  standaloneSchema: {};
392
555
  macro: {
393
556
  requirePermission?: string | string[];
557
+ requireOwnership?: {
558
+ resolveOwnerId: (context: unknown) => string | Promise<string | null | undefined>;
559
+ ownPermission: string;
560
+ anyPermission: string;
561
+ resourceLabel?: string;
562
+ };
394
563
  };
395
564
  macroFn: {};
396
565
  parser: {};
@@ -653,4 +822,4 @@ export declare function verifyAddonEntitlement(addonId: string, target?: ((expec
653
822
  verifyEntitlement?: (expectedAddonId?: string) => boolean;
654
823
  }): boolean;
655
824
  //#endregion
656
- export { ADDON_ID_PATTERN, type ActiveAddon, type ActiveAddonFrontend, type AddonArchiveFile, type AddonAttachment, type AddonChatMessage, type AddonCommandDefinition, type AddonCommandParameter, type AddonDistribution, type AddonManifest, type AddonPermissionDefinition, type AddonRequestUser, 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 MountPoint, type QueryKey, type Selectable, type Updateable, assertSafeRelativePath, canonicalManifest, createCanonicalAddonDigest, isCompatibilityRange, sortValue, useQueryClient, validateAddonManifest };
825
+ 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 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 Selectable, type Updateable, assertSafeRelativePath, canonicalManifest, createCanonicalAddonDigest, createMongoAbility, isCompatibilityRange, sortValue, subject, useQueryClient, validateAddonManifest };