@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 +13 -0
- package/dist/cli.d.mts +30 -11
- package/dist/cli.mjs +28 -10
- package/dist/contracts2.d.mts +1 -0
- package/dist/index.mjs +2 -193
- package/dist/index2.d.mts +173 -4
- package/dist/signing.mjs +63 -0
- package/dist/signing2.d.mts +63 -0
- package/dist/src.mjs +467 -0
- package/dist/testing.mjs +1 -143
- package/dist/ui.d.mts +42 -1
- package/dist/ui.mjs +32 -1
- package/package.json +3 -1
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
|
|
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
|
-
*
|
|
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
|
|
18
|
-
* @returns
|
|
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
|
|
23
|
-
* present.
|
|
32
|
+
* Resolves and composes the complete Vite configuration for compiling a JChat addon.
|
|
24
33
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
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
|
-
*
|
|
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
|
|
19
|
-
* @returns
|
|
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
|
|
40
|
-
* present.
|
|
48
|
+
* Resolves and composes the complete Vite configuration for compiling a JChat addon.
|
|
41
49
|
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
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";
|
package/dist/contracts2.d.mts
CHANGED
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
|
-
|
|
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
|
|
174
|
-
* and pipeline hooks offline in CI without requiring live
|
|
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 };
|