@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.mjs CHANGED
@@ -1,105 +1,5 @@
1
- //#region package.json
2
- var version = "1.0.0-alpha.3";
3
- //#endregion
4
- //#region src/contracts.ts
5
- /** Valid locations where an addon can be rendered within the Jchat layout. */
6
- const MOUNT_POINTS = {
7
- /** Top of the main chat column */
8
- CHAT_HEADER: "chat-header",
9
- /** Below the main chat header, above messages */
10
- CHAT_TOOLBAR: "chat-toolbar",
11
- /** The persistent right-side sidebar */
12
- CHAT_SIDEBAR: "chat-sidebar",
13
- /** Top of the left sidebar (room list) */
14
- SIDEBAR_TOP: "sidebar-top",
15
- /** Bottom of the left sidebar */
16
- SIDEBAR_BOTTOM: "sidebar-bottom",
17
- /** Action list shown when clicking a user's avatar */
18
- USER_POPOVER_ACTIONS: "user-popover-actions",
19
- /** Menu shown when right-clicking a message */
20
- MESSAGE_CONTEXT_MENU: "message-context-menu",
21
- /** Floating overlay rendered on top of the message stream */
22
- MESSAGE_FEED_OVERLAY: "message-feed-overlay",
23
- /** Main admin dashboard area */
24
- ADMIN_PANEL: "admin-panel",
25
- /** User or site settings dashboard area */
26
- SETTINGS_PANEL: "settings-panel",
27
- /** The 'plus' (+) menu in the message composer */
28
- CHAT_PLUS_MENU: "chat-plus-menu",
29
- /** Custom tab in the emoji/gif selection panel */
30
- EMOJI_PICKER_GIF_TAB: "emoji-picker-gif-tab",
31
- /** Full-screen or modal dialog specifically for TubeScan */
32
- TUBESCAN_DIALOG: "tubescan-dialog"
33
- };
34
- //#endregion
35
- //#region src/index.ts
36
- /** Define a manifest with full validation-friendly typing while preserving literal values. */
37
- function defineAddonManifest(manifest) {
38
- return manifest;
39
- }
40
- /** Define a typed server factory without widening its addon database interface. */
41
- function defineAddonServer(factory) {
42
- return factory;
43
- }
44
- /**
45
- * Access the host application's SDK. Provides authenticated API access, toast notifications, and
46
- * navigation.
47
- *
48
- * This function is a shell; the real implementation is provided by the Jchat host environment at
49
- * runtime.
50
- *
51
- * @example
52
- * ;```tsx
53
- * import { useAddonSDK } from "@jchat/addon-sdk"
54
- *
55
- * export default function MyAddon({ mountPoint }: AddonProps) {
56
- * const sdk = useAddonSDK()
57
- *
58
- * const handleClick = async () => {
59
- * const data = await sdk.api.get("/trending")
60
- * sdk.toast("Fetched trending posts!", "success")
61
- * }
62
- * }
63
- * ```
64
- */
65
- function useAddonSDK() {
66
- throw new Error("AddonSDK must be used within the Jchat host environment.");
67
- }
68
- /**
69
- * Access the host application's active icon pack. Returns the icon set matching the site's
70
- * configured icon pack preference.
71
- *
72
- * This function is a shell; the real implementation is provided by the Jchat host environment at
73
- * runtime.
74
- *
75
- * @example
76
- * ;```tsx
77
- * import { useIcons } from "@jchat/addon-sdk"
78
- *
79
- * export default function MyAddon() {
80
- * const { Icons } = useIcons()
81
- * return <Icons.Send className="size-5" />
82
- * }
83
- * ```
84
- */
85
- function useIcons() {
86
- throw new Error("useIcons must be used within the Jchat host environment.");
87
- }
88
- /**
89
- * Get the version of the addon SDK. This is useful for checking compatibility with the host
90
- * application.
91
- *
92
- * @example
93
- * ;```tsx
94
- * import { getAddonSdkVersion } from "@jchat/addon-sdk"
95
- *
96
- * console.log(getAddonSdkVersion())
97
- * ```
98
- *
99
- * @returns
100
- */
101
- function getAddonSdkVersion() {
102
- return version;
103
- }
104
- //#endregion
105
- export { MOUNT_POINTS, defineAddonManifest, defineAddonServer, getAddonSdkVersion, useAddonSDK, useIcons };
1
+ import { _ as createMongoAbility, a as useAddonSDK, c as useAddonInfiniteQuery, d as useAddonSettings, f as useQueryClient, g as createAddonAbility, h as authorizeResourceOwnership, i as getAddonSdkVersion, l as useAddonMutation, m as ForbiddenError, n as defineAddonManifest, o as verifyAddonEntitlement, p as AbilityBuilder, r as defineAddonServer, s as createMockAddonServerContext, t as AddonPipelineAbortError, u as useAddonQuery, v as hasPermission, y as subject } from "./src.mjs";
2
+ import { HOST_LOCALES, MOUNT_POINTS } from "./contracts.mjs";
3
+ import { i as useTranslation, n as createTranslator, r as useLocale, t as LocaleContext } from "./i18n.mjs";
4
+ import { ADDON_ID_PATTERN, COMPATIBILITY_RANGE_PATTERN, MAX_ADDON_ARCHIVE_BYTES, assertSafeRelativePath, canonicalManifest, createCanonicalAddonDigest, isCompatibilityRange, sortValue, validateAddonManifest } from "./signing.mjs";
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, useAddonSettings, useLocale, useQueryClient, useTranslation, validateAddonManifest, verifyAddonEntitlement };
@@ -0,0 +1,2 @@
1
+ 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";
2
+ export { ADDON_ID_PATTERN, COMPATIBILITY_RANGE_PATTERN, MAX_ADDON_ARCHIVE_BYTES, assertSafeRelativePath, canonicalManifest, createCanonicalAddonDigest, isCompatibilityRange, sortValue, validateAddonManifest };
@@ -0,0 +1,136 @@
1
+ import crypto from "node:crypto";
2
+ import * as path$1 from "node:path";
3
+ //#region src/signing.ts
4
+ /** Maximum allowable byte size for an addon distribution package archive (25 MB). */
5
+ const MAX_ADDON_ARCHIVE_BYTES = 26214400;
6
+ /**
7
+ * Regular expression validating canonical kebab-case addon identifiers (e.g. `'soundboard'`,
8
+ * `'dj-drop'`).
9
+ */
10
+ const ADDON_ID_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
11
+ /** Regular expression validating semver compatibility range expressions. */
12
+ const COMPATIBILITY_RANGE_PATTERN = /^[0-9A-Za-z*^~<>=|.\-\s]+$/;
13
+ /**
14
+ * Asserts that a file path is a safe relative path, guarding against directory traversal attacks,
15
+ * absolute paths, and non-canonical path separators.
16
+ *
17
+ * @param value - File path string to validate.
18
+ * @param label - Human-friendly field name used for error reporting (e.g. `'Logo path'`, `'Bundle
19
+ * path'`).
20
+ * @throws {Error} When the path is empty, absolute, contains backslashes, or attempts directory
21
+ * traversal (`..`).
22
+ */
23
+ function assertSafeRelativePath(value, label) {
24
+ if (!value || path$1.isAbsolute(value) || value.includes("\\") || value.split("/").includes("..")) throw new Error(`${label} must be a safe relative path`);
25
+ }
26
+ /**
27
+ * Type guard verifying whether a value is a valid semver compatibility range string.
28
+ *
29
+ * @param value - The value to check.
30
+ * @returns `true` if the value is a valid non-empty compatibility range string within 64
31
+ * characters.
32
+ */
33
+ function isCompatibilityRange(value) {
34
+ return typeof value === "string" && value.length > 0 && value.length <= 64 && COMPATIBILITY_RANGE_PATTERN.test(value);
35
+ }
36
+ /**
37
+ * Recursively sorts object keys alphabetically to guarantee deterministic, reproducible JSON
38
+ * serialization for cryptographic signing and digest hashing.
39
+ *
40
+ * @param value - Object, array, or primitive value to normalize.
41
+ * @returns A new structure with sorted keys at all nested levels.
42
+ */
43
+ function sortValue(value) {
44
+ if (Array.isArray(value)) return value.map(sortValue);
45
+ if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).toSorted(([left], [right]) => left.localeCompare(right)).map(([key, child]) => [key, sortValue(child)]));
46
+ return value;
47
+ }
48
+ /**
49
+ * Produces a canonical JSON string representation of an {@link AddonManifest} with signature fields
50
+ * blanked and all object keys sorted deterministically.
51
+ *
52
+ * @param manifest - Addon manifest to canonicalize.
53
+ * @returns Normalized JSON string representation.
54
+ */
55
+ function canonicalManifest(manifest) {
56
+ const copy = structuredClone(manifest);
57
+ if (copy.signature) {
58
+ copy.signature.digest = "";
59
+ copy.signature.value = "";
60
+ }
61
+ return JSON.stringify(sortValue(copy));
62
+ }
63
+ /**
64
+ * Computes a deterministic SHA-256 cryptographic digest across an addon archive's contents.
65
+ *
66
+ * Files are processed in lexicographical path order, with manifest signature fields sanitized,
67
+ * producing a tamper-evident package fingerprint.
68
+ *
69
+ * @param manifest - Addon manifest containing metadata and package declarations.
70
+ * @param files - Array of files packaged inside the addon archive.
71
+ * @returns Hex-encoded SHA-256 digest string.
72
+ */
73
+ function createCanonicalAddonDigest(manifest, files) {
74
+ const hash = crypto.createHash("sha256");
75
+ const sortedFiles = [...files].toSorted((left, right) => left.path.localeCompare(right.path));
76
+ for (const file of sortedFiles) {
77
+ hash.update(file.path);
78
+ hash.update("\0");
79
+ hash.update(file.path === "manifest.json" ? canonicalManifest(manifest) : file.contents);
80
+ hash.update("\0");
81
+ }
82
+ return hash.digest("hex");
83
+ }
84
+ /**
85
+ * Validates the structural, security, and schema integrity of an {@link AddonManifest}.
86
+ *
87
+ * Enforces:
88
+ *
89
+ * - Valid kebab-case identifier (`^[a-z0-9]+(?:-[a-z0-9]+)*$`)
90
+ * - Non-empty name and version constraints
91
+ * - Host and SDK compatibility range specifications
92
+ * - Distribution model classification (`'free'` or `'commercial'`)
93
+ * - Logo image file format (`.svg`, `.png`, `.webp`) and safe relative path
94
+ * - Safe relative paths for client/server bundles, migrations, and seeds
95
+ * - Canonical permission namespacing prefix (`addon.<id>.*`)
96
+ *
97
+ * @param manifest - Addon manifest object to inspect and validate.
98
+ * @throws {Error} When any validation rule or security constraint is violated.
99
+ */
100
+ function validateAddonManifest(manifest) {
101
+ if (!manifest.id || manifest.id.length > 40 || !ADDON_ID_PATTERN.test(manifest.id)) throw new Error("Manifest id must contain only lowercase letters, numbers, and hyphens");
102
+ if (!manifest.name || manifest.name.length > 120) throw new Error("Manifest name is invalid");
103
+ if (!manifest.version || String(manifest.version).length > 64) throw new Error("Manifest version is invalid");
104
+ if (!isCompatibilityRange(manifest.compatibility?.jchat) || !isCompatibilityRange(manifest.compatibility?.addonSdk)) throw new Error("Manifest must declare valid JChat and addon SDK compatibility ranges");
105
+ if (manifest.distribution !== void 0 && manifest.distribution !== "free" && manifest.distribution !== "commercial") throw new Error("Manifest distribution must be either 'free' or 'commercial'");
106
+ if (!manifest.logo || typeof manifest.logo !== "string" || !manifest.logo.trim()) throw new Error("Manifest logo is required");
107
+ const cleanLogo = manifest.logo.replace(/^\.\//, "");
108
+ assertSafeRelativePath(cleanLogo, "Logo path");
109
+ const logoExt = path$1.extname(cleanLogo).toLowerCase();
110
+ if (![
111
+ ".svg",
112
+ ".png",
113
+ ".webp"
114
+ ].includes(logoExt)) throw new Error("Manifest logo must be an SVG, PNG, or WebP image file");
115
+ const references = [manifest.frontend?.bundle, manifest.server?.bundle].filter((value) => Boolean(value));
116
+ for (const reference of references) assertSafeRelativePath(reference, "Bundle path");
117
+ for (const migration of manifest.server?.migrations ?? []) {
118
+ assertSafeRelativePath(migration, "Migration path");
119
+ if (!migration.startsWith("migrations/") || !migration.endsWith(".sql")) throw new Error("Migration paths must point to migrations/*.sql");
120
+ }
121
+ for (const seed of manifest.server?.seeds ?? []) {
122
+ assertSafeRelativePath(seed, "Seed path");
123
+ if (!seed.startsWith("seeds/") || !seed.endsWith(".json")) throw new Error("Seed paths must point to seeds/*.json");
124
+ }
125
+ const databaseFiles = [...manifest.server?.migrations ?? [], ...manifest.server?.seeds ?? []];
126
+ if (new Set(databaseFiles).size !== databaseFiles.length) throw new Error("Manifest database file paths must be unique");
127
+ const addonPermPrefix = `addon.${manifest.id}.`;
128
+ const declaredPerms = [
129
+ ...manifest.accessPermission ? [manifest.accessPermission] : [],
130
+ ...manifest.permissions ?? [],
131
+ ...manifest.permissionDefinitions?.map((d) => d.name) ?? []
132
+ ];
133
+ for (const perm of declaredPerms) if (!perm.startsWith(addonPermPrefix)) throw new Error(`Addon permission '${perm}' must start with '${addonPermPrefix}' prefix to prevent permission collisions`);
134
+ }
135
+ //#endregion
136
+ export { ADDON_ID_PATTERN, COMPATIBILITY_RANGE_PATTERN, MAX_ADDON_ARCHIVE_BYTES, assertSafeRelativePath, canonicalManifest, createCanonicalAddonDigest, isCompatibilityRange, sortValue, validateAddonManifest };
@@ -0,0 +1,76 @@
1
+ import { l as AddonManifest, r as AddonArchiveFile } from "./contracts2.mjs";
2
+ //#region src/signing.d.ts
3
+ /** Maximum allowable byte size for an addon distribution package archive (25 MB). */
4
+ declare const MAX_ADDON_ARCHIVE_BYTES: number;
5
+ /**
6
+ * Regular expression validating canonical kebab-case addon identifiers (e.g. `'soundboard'`,
7
+ * `'dj-drop'`).
8
+ */
9
+ declare const ADDON_ID_PATTERN: RegExp;
10
+ /** Regular expression validating semver compatibility range expressions. */
11
+ declare const COMPATIBILITY_RANGE_PATTERN: RegExp;
12
+ /**
13
+ * Asserts that a file path is a safe relative path, guarding against directory traversal attacks,
14
+ * absolute paths, and non-canonical path separators.
15
+ *
16
+ * @param value - File path string to validate.
17
+ * @param label - Human-friendly field name used for error reporting (e.g. `'Logo path'`, `'Bundle
18
+ * path'`).
19
+ * @throws {Error} When the path is empty, absolute, contains backslashes, or attempts directory
20
+ * traversal (`..`).
21
+ */
22
+ declare function assertSafeRelativePath(value: string, label: string): void;
23
+ /**
24
+ * Type guard verifying whether a value is a valid semver compatibility range string.
25
+ *
26
+ * @param value - The value to check.
27
+ * @returns `true` if the value is a valid non-empty compatibility range string within 64
28
+ * characters.
29
+ */
30
+ declare function isCompatibilityRange(value: unknown): value is string;
31
+ /**
32
+ * Recursively sorts object keys alphabetically to guarantee deterministic, reproducible JSON
33
+ * serialization for cryptographic signing and digest hashing.
34
+ *
35
+ * @param value - Object, array, or primitive value to normalize.
36
+ * @returns A new structure with sorted keys at all nested levels.
37
+ */
38
+ declare function sortValue(value: unknown): unknown;
39
+ /**
40
+ * Produces a canonical JSON string representation of an {@link AddonManifest} with signature fields
41
+ * blanked and all object keys sorted deterministically.
42
+ *
43
+ * @param manifest - Addon manifest to canonicalize.
44
+ * @returns Normalized JSON string representation.
45
+ */
46
+ declare function canonicalManifest(manifest: AddonManifest): string;
47
+ /**
48
+ * Computes a deterministic SHA-256 cryptographic digest across an addon archive's contents.
49
+ *
50
+ * Files are processed in lexicographical path order, with manifest signature fields sanitized,
51
+ * producing a tamper-evident package fingerprint.
52
+ *
53
+ * @param manifest - Addon manifest containing metadata and package declarations.
54
+ * @param files - Array of files packaged inside the addon archive.
55
+ * @returns Hex-encoded SHA-256 digest string.
56
+ */
57
+ declare function createCanonicalAddonDigest(manifest: AddonManifest, files: readonly AddonArchiveFile[]): string;
58
+ /**
59
+ * Validates the structural, security, and schema integrity of an {@link AddonManifest}.
60
+ *
61
+ * Enforces:
62
+ *
63
+ * - Valid kebab-case identifier (`^[a-z0-9]+(?:-[a-z0-9]+)*$`)
64
+ * - Non-empty name and version constraints
65
+ * - Host and SDK compatibility range specifications
66
+ * - Distribution model classification (`'free'` or `'commercial'`)
67
+ * - Logo image file format (`.svg`, `.png`, `.webp`) and safe relative path
68
+ * - Safe relative paths for client/server bundles, migrations, and seeds
69
+ * - Canonical permission namespacing prefix (`addon.<id>.*`)
70
+ *
71
+ * @param manifest - Addon manifest object to inspect and validate.
72
+ * @throws {Error} When any validation rule or security constraint is violated.
73
+ */
74
+ declare function validateAddonManifest(manifest: AddonManifest): void;
75
+ //#endregion
76
+ export { canonicalManifest as a, sortValue as c, assertSafeRelativePath as i, validateAddonManifest as l, COMPATIBILITY_RANGE_PATTERN as n, createCanonicalAddonDigest as o, MAX_ADDON_ARCHIVE_BYTES as r, isCompatibilityRange as s, ADDON_ID_PATTERN as t };