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

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,11 @@
1
1
  # @jchat/addon-sdk
2
2
 
3
+ ## 1.0.0-alpha.4
4
+
5
+ ### Patch Changes
6
+
7
+ - ada430f: Formatted type declarations in the Addon SDK to align with project style guidelines.
8
+
3
9
  ## 1.0.0-alpha.3
4
10
 
5
11
  ### Patch Changes
package/README.md CHANGED
@@ -40,6 +40,30 @@ export default {
40
40
  }
41
41
  ```
42
42
 
43
+ ## Database Architecture & Schema Isolation
44
+
45
+ JChat database-bearing addons use **Kysely** for server-side database access:
46
+
47
+ - **0 Bundle Overhead**: Table definitions are pure TypeScript interfaces (`src/db.ts`), resulting in 0 runtime JavaScript bundle footprint.
48
+ - **Shared `addons` Schema**: All addon tables are placed inside the dedicated `"addons"` schema (e.g. `addons.DjDropTracks`, `addons.PollwisePolls`, `addons.GlimpseStories`), keeping them cleanly isolated from core `public` application tables without requiring dynamic PostgreSQL role creation (`CREATE ROLE`).
49
+ - **Mandatory Table Naming**: Table names **must start with a Capital letter** and **must be prefixed with your capitalized `addonId`** (e.g. `addons.MyAddonSettings`). Unprefixed tables are rejected by the host migration validator.
50
+ - **SQL Migrations**: Addon migrations are stored as plain SQL files (`migrations/001_*.sql`) listed in `manifest.ts` under `server.migrations`.
51
+ - **ID Column Convention**: Use `text` / `varchar(36)` for host-referencing IDs (`siteId`, `roomId`, `userId`) to ensure driver-agnostic, cast-free query execution with UUID v7 strings.
52
+
53
+ ```ts
54
+ import type { AddonDatabase } from "@jchat/addon-sdk"
55
+
56
+ export interface MyAddonDatabase {
57
+ MyAddonSettings: {
58
+ id: string
59
+ siteId: string
60
+ createdAt: Date
61
+ }
62
+ }
63
+
64
+ export type MyAddonContext = AddonServerContext<MyAddonDatabase>
65
+ ```
66
+
43
67
  ## License
44
68
 
45
69
  Apache-2.0
@@ -1,22 +1,69 @@
1
1
  import { t as AddonManifest } from "./contracts.mjs";
2
2
  //#region src/diagnostics.d.ts
3
+ /**
4
+ * Diagnostic result status indicator for addon validation checks. - `"pass"`: The diagnostic check
5
+ * passed successfully. - `"warning"`: Non-critical warning (e.g. source maps present in
6
+ * production). - `"error"`: Critical failure blocking addon installation or distribution.
7
+ */
3
8
  type AddonDiagnosticStatus = "pass" | "warning" | "error";
9
+ /** Represents an individual diagnostic check result for an addon package. */
4
10
  interface AddonPackageDiagnostic {
11
+ /** Unique identifier for the diagnostic check (e.g. `"jchat"`, `"package-size"`). */
5
12
  id: string;
13
+ /** Human-readable title for the diagnostic check. */
6
14
  label: string;
15
+ /** Severity status outcome of the check. */
7
16
  status: AddonDiagnosticStatus;
17
+ /** Descriptive message or metric value providing check details. */
8
18
  message: string;
9
19
  }
20
+ /** Options required to run diagnostic validation on an addon build package. */
10
21
  interface AddonPackageDiagnosticOptions {
22
+ /** Absolute path to the compiled addon build output directory (`build/`). */
11
23
  buildDir: string;
24
+ /** Parsed addon manifest object (`manifest.json`). */
12
25
  manifest: AddonManifest;
26
+ /** Semantic version of the host JChat platform instance. */
13
27
  jchatVersion: string;
28
+ /** Semantic version of the `@jchat/addon-sdk` package. */
14
29
  addonSdkVersion: string;
30
+ /** Whether the diagnostic target is a production `.zip` package build. Defaults to `false`. */
15
31
  productionPackage?: boolean;
32
+ /** Maximum allowable package size in bytes. Defaults to 25MB (`26,214,400` bytes). */
16
33
  maxBytes?: number;
17
34
  }
35
+ /**
36
+ * Constructs a semantic version compatibility range covering all minor/patch versions of the
37
+ * current major version.
38
+ *
39
+ * @example
40
+ * ```ts
41
+ * createCompatibleMajorRange("1.4.2") // returns ">=1.4.2 <2.0.0"
42
+ * ```
43
+ *
44
+ * @param version - Valid semver string (e.g. `"1.2.0"`).
45
+ * @returns Semver range string matching the current major series (e.g. `">=1.2.0 <2.0.0"`).
46
+ * @throws {@link Error} If the input is not a valid semantic version string.
47
+ */
18
48
  declare function createCompatibleMajorRange(version: string): string;
49
+ /**
50
+ * Performs comprehensive diagnostic validation checks on an addon package build directory.
51
+ *
52
+ * Validates version compatibility ranges, verifies required build files (frontend/server bundles,
53
+ * migrations, seeds), checks total package size limits, flags production source maps, and verifies
54
+ * publisher digital signatures.
55
+ *
56
+ * @param options - Configuration options specifying build directory, manifest, and environment
57
+ * versions.
58
+ * @returns Array of {@link AddonPackageDiagnostic} results.
59
+ */
19
60
  declare function diagnoseAddonPackage(options: AddonPackageDiagnosticOptions): AddonPackageDiagnostic[];
61
+ /**
62
+ * Checks whether any diagnostic check in a diagnostic list returned a critical error.
63
+ *
64
+ * @param diagnostics - Array of diagnostic check results to evaluate.
65
+ * @returns `true` if at least one check has `status === "error"`, otherwise `false`.
66
+ */
20
67
  declare function hasAddonDiagnosticErrors(diagnostics: readonly AddonPackageDiagnostic[]): boolean;
21
68
  //#endregion
22
69
  export { AddonDiagnosticStatus, AddonPackageDiagnostic, AddonPackageDiagnosticOptions, createCompatibleMajorRange, diagnoseAddonPackage, hasAddonDiagnosticErrors };
@@ -1,6 +1,19 @@
1
1
  import * as fs$1 from "node:fs";
2
2
  import * as path$1 from "node:path";
3
3
  //#region src/diagnostics.ts
4
+ /**
5
+ * Constructs a semantic version compatibility range covering all minor/patch versions of the
6
+ * current major version.
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * createCompatibleMajorRange("1.4.2") // returns ">=1.4.2 <2.0.0"
11
+ * ```
12
+ *
13
+ * @param version - Valid semver string (e.g. `"1.2.0"`).
14
+ * @returns Semver range string matching the current major series (e.g. `">=1.2.0 <2.0.0"`).
15
+ * @throws {@link Error} If the input is not a valid semantic version string.
16
+ */
4
17
  function createCompatibleMajorRange(version) {
5
18
  try {
6
19
  Bun.semver.order(version, version);
@@ -12,6 +25,17 @@ function createCompatibleMajorRange(version) {
12
25
  if (!Number.isSafeInteger(major)) throw new Error(`Invalid semantic version: ${version}`);
13
26
  return `>=${version} <${major + 1}.0.0`;
14
27
  }
28
+ /**
29
+ * Performs comprehensive diagnostic validation checks on an addon package build directory.
30
+ *
31
+ * Validates version compatibility ranges, verifies required build files (frontend/server bundles,
32
+ * migrations, seeds), checks total package size limits, flags production source maps, and verifies
33
+ * publisher digital signatures.
34
+ *
35
+ * @param options - Configuration options specifying build directory, manifest, and environment
36
+ * versions.
37
+ * @returns Array of {@link AddonPackageDiagnostic} results.
38
+ */
15
39
  function diagnoseAddonPackage(options) {
16
40
  const { buildDir, manifest } = options;
17
41
  const diagnostics = [];
@@ -59,6 +83,18 @@ function diagnoseAddonPackage(options) {
59
83
  status: totalBytes <= maxBytes ? "pass" : "error",
60
84
  message: `${totalBytes} of ${maxBytes} bytes`
61
85
  });
86
+ const addonPermPrefix = `addon.${manifest.id}.`;
87
+ const invalidPerms = Array.from(/* @__PURE__ */ new Set([
88
+ ...manifest.accessPermission ? [manifest.accessPermission] : [],
89
+ ...manifest.permissions ?? [],
90
+ ...manifest.permissionDefinitions?.map((d) => d.name) ?? []
91
+ ])).filter((perm) => !perm.startsWith(addonPermPrefix));
92
+ diagnostics.push({
93
+ id: "permission-namespacing",
94
+ label: "Permission namespacing",
95
+ status: invalidPerms.length === 0 ? "pass" : "error",
96
+ message: invalidPerms.length === 0 ? `All declared permissions start with ${addonPermPrefix}` : `Permission(s) do not follow naming convention ${addonPermPrefix}*: ${invalidPerms.join(", ")}`
97
+ });
62
98
  const sourceMaps = files.filter((file) => file.endsWith(".map"));
63
99
  diagnostics.push({
64
100
  id: "source-maps",
@@ -81,9 +117,24 @@ function diagnoseAddonPackage(options) {
81
117
  });
82
118
  return diagnostics;
83
119
  }
120
+ /**
121
+ * Checks whether any diagnostic check in a diagnostic list returned a critical error.
122
+ *
123
+ * @param diagnostics - Array of diagnostic check results to evaluate.
124
+ * @returns `true` if at least one check has `status === "error"`, otherwise `false`.
125
+ */
84
126
  function hasAddonDiagnosticErrors(diagnostics) {
85
127
  return diagnostics.some(({ status }) => status === "error");
86
128
  }
129
+ /**
130
+ * Evaluates semantic version compatibility between a target version and a semver range string.
131
+ *
132
+ * @param id - Diagnostic check identifier.
133
+ * @param label - Diagnostic check label.
134
+ * @param version - Environment component version string.
135
+ * @param range - Expected semver range string from manifest compatibility declarations.
136
+ * @returns An {@link AddonPackageDiagnostic} result object.
137
+ */
87
138
  function compatibilityDiagnostic(id, label, version, range) {
88
139
  const compatible = Boolean(range && Bun.semver.satisfies(version, range));
89
140
  return {
@@ -93,12 +144,26 @@ function compatibilityDiagnostic(id, label, version, range) {
93
144
  message: range ? `${version} ${compatible ? "satisfies" : "does not satisfy"} ${range}` : "Compatibility range is missing"
94
145
  };
95
146
  }
147
+ /**
148
+ * Safely resolves a relative build file reference within a target build directory, guarding against
149
+ * directory traversal attacks.
150
+ *
151
+ * @param buildDir - Absolute path to the build directory.
152
+ * @param reference - Relative file path declared in manifest.
153
+ * @returns Resolved absolute file path if safe and inside `buildDir`, or `null` if unsafe.
154
+ */
96
155
  function resolveBuildFile(buildDir, reference) {
97
156
  if (path$1.isAbsolute(reference) || reference.includes("\\") || reference.split("/").includes("..")) return null;
98
157
  const target = path$1.resolve(buildDir, ...reference.replace(/^\.\//, "").split("/"));
99
158
  const relative = path$1.relative(path$1.resolve(buildDir), target);
100
159
  return relative && !relative.startsWith("..") && !path$1.isAbsolute(relative) ? target : null;
101
160
  }
161
+ /**
162
+ * Recursively lists all regular files within a root directory.
163
+ *
164
+ * @param root - Path to search.
165
+ * @returns Array of absolute file paths.
166
+ */
102
167
  function listRegularFiles(root) {
103
168
  if (!fs$1.existsSync(root)) return [];
104
169
  return fs$1.readdirSync(root, {
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  //#region package.json
2
- var version = "1.0.0-alpha.3";
2
+ var version = "1.0.0-alpha.4";
3
3
  //#endregion
4
4
  //#region src/contracts.ts
5
5
  /** Valid locations where an addon can be rendered within the Jchat layout. */
package/dist/vite.d.mts CHANGED
@@ -2,13 +2,41 @@ import { t as AddonManifest } from "./contracts.mjs";
2
2
  import { Plugin } from "vite";
3
3
  //#region src/vite.d.ts
4
4
  /**
5
- * Vite plugin to configure Jchat addon manifest generation, bundle backend scripts, copy
6
- * migrations, declarative seed data, and zip production build artifacts.
5
+ * Vite plugin for building JChat sandboxed addon extensions.
6
+ *
7
+ * Configures automatic manifest generation (`manifest.json`), bundles server-side API scripts
8
+ * (`src/api.ts`), embeds scoped Tailwind CSS into the frontend bundle, packages database migrations
9
+ * and seed files, copies documentation (`README.md`), signs database-bearing addon manifests, and
10
+ * zips production build artifacts.
11
+ *
12
+ * @example
13
+ * ;```ts
14
+ * import { jchatAddon } from "@jchat/addon-sdk/vite"
15
+ * import manifest from "./src/manifest"
16
+ * import { defineConfig } from "vite"
17
+ *
18
+ * export default defineConfig({
19
+ * plugins: [jchatAddon({ manifest })]
20
+ * })
21
+ * ```
22
+ *
23
+ * @param options - Configuration options for the JChat addon Vite plugin.
24
+ * @returns A Vite {@link Plugin} instance configured for addon compilation.
7
25
  */
8
26
  declare function jchatAddon(options: JchatAddonPluginOptions): Plugin;
27
+ /** Configuration options for the {@link jchatAddon} Vite plugin. */
9
28
  interface JchatAddonPluginOptions {
29
+ /** The base manifest specification for the addon. */
10
30
  manifest: AddonManifest;
31
+ /**
32
+ * Whether to package the build output into a `.zip` archive upon build completion. Defaults to
33
+ * `true` during production builds and `false` during development builds.
34
+ */
11
35
  zip?: boolean;
36
+ /**
37
+ * Optional RSA private key string used to cryptographically sign database-bearing addons. If
38
+ * omitted, falls back to the `JCHAT_ADDON_SIGNING_PRIVATE_KEY` environment variable.
39
+ */
12
40
  signingPrivateKey?: string;
13
41
  /**
14
42
  * Embed Tailwind CSS output into the JS bundle and scope it to the addon's DOM container.
@@ -16,11 +44,36 @@ interface JchatAddonPluginOptions {
16
44
  */
17
45
  css?: boolean;
18
46
  }
47
+ /**
48
+ * Generates CLI argument parameters for bundling server-side Elysia API scripts (`src/api.ts`) via
49
+ * Bun.
50
+ *
51
+ * @param entryPath - Absolute file path to the server entry script (`src/api.ts`).
52
+ * @param outputPath - Absolute destination file path for the bundled output (`build/api.js`).
53
+ * @param developmentBuild - Indicates whether to enable external sourcemaps or minification.
54
+ * @returns Array of command line arguments for running `bun build`.
55
+ */
19
56
  declare function createAddonServerBuildArguments(entryPath: string, outputPath: string, developmentBuild: boolean): string[];
57
+ /**
58
+ * Cryptographically signs an addon manifest using RSA-SHA256 over all archive file contents.
59
+ *
60
+ * @param manifest - Unsigned or base addon manifest.
61
+ * @param files - List of archive file paths and raw contents to digest.
62
+ * @param privateKey - RSA private key in PEM format.
63
+ * @returns Updated {@link AddonManifest} containing an RSA signature block.
64
+ */
20
65
  declare function signAddonManifest(manifest: AddonManifest, files: readonly {
21
66
  path: string;
22
67
  contents: Buffer;
23
68
  }[], privateKey: string): AddonManifest;
69
+ /**
70
+ * Resolves the required RSA signing key for database-bearing addons.
71
+ *
72
+ * @param signingPrivateKey - Provided signing key or undefined.
73
+ * @param developmentBuild - Whether the current build is targeting development mode.
74
+ * @returns Resolved signing key string or undefined (in dev builds).
75
+ * @throws {@link Error} If building a database-bearing addon in production without a signing key.
76
+ */
24
77
  declare function requireAddonSigningKey(signingPrivateKey: string | undefined, developmentBuild: boolean): string | undefined;
25
78
  //#endregion
26
79
  export { JchatAddonPluginOptions, createAddonServerBuildArguments, jchatAddon, requireAddonSigningKey, signAddonManifest };
package/dist/vite.mjs CHANGED
@@ -4,8 +4,26 @@ import { execFileSync } from "node:child_process";
4
4
  import crypto from "node:crypto";
5
5
  //#region src/vite.ts
6
6
  /**
7
- * Vite plugin to configure Jchat addon manifest generation, bundle backend scripts, copy
8
- * migrations, declarative seed data, and zip production build artifacts.
7
+ * Vite plugin for building JChat sandboxed addon extensions.
8
+ *
9
+ * Configures automatic manifest generation (`manifest.json`), bundles server-side API scripts
10
+ * (`src/api.ts`), embeds scoped Tailwind CSS into the frontend bundle, packages database migrations
11
+ * and seed files, copies documentation (`README.md`), signs database-bearing addon manifests, and
12
+ * zips production build artifacts.
13
+ *
14
+ * @example
15
+ * ;```ts
16
+ * import { jchatAddon } from "@jchat/addon-sdk/vite"
17
+ * import manifest from "./src/manifest"
18
+ * import { defineConfig } from "vite"
19
+ *
20
+ * export default defineConfig({
21
+ * plugins: [jchatAddon({ manifest })]
22
+ * })
23
+ * ```
24
+ *
25
+ * @param options - Configuration options for the JChat addon Vite plugin.
26
+ * @returns A Vite {@link Plugin} instance configured for addon compilation.
9
27
  */
10
28
  function jchatAddon(options) {
11
29
  const { manifest: manifestData } = options;
@@ -81,12 +99,15 @@ function jchatAddon(options) {
81
99
  })), signingPrivateKey);
82
100
  }
83
101
  }
102
+ const readmeSource = path.resolve(rootDir, "README.md");
103
+ if (fs.existsSync(readmeSource)) fs.copyFileSync(readmeSource, path.resolve(buildDir, "README.md"));
84
104
  const manifestJson = JSON.stringify(manifest, null, 2);
85
105
  fs.writeFileSync(path.resolve(buildDir, "manifest.json"), manifestJson);
86
106
  if (shouldZip) {
87
107
  const zipName = `${manifest.id}-${pkg.version}.zip`;
88
108
  const filesToZip = ["manifest.json", bundleFile];
89
109
  if (fs.existsSync(path.resolve(buildDir, "api.js"))) filesToZip.push("api.js");
110
+ if (fs.existsSync(path.resolve(buildDir, "README.md"))) filesToZip.push("README.md");
90
111
  const migrationsDest = path.resolve(buildDir, "migrations");
91
112
  if (fs.existsSync(migrationsDest)) filesToZip.push("migrations");
92
113
  const seedsDest = path.resolve(buildDir, "seeds");
@@ -105,6 +126,15 @@ function jchatAddon(options) {
105
126
  }
106
127
  };
107
128
  }
129
+ /**
130
+ * Generates CLI argument parameters for bundling server-side Elysia API scripts (`src/api.ts`) via
131
+ * Bun.
132
+ *
133
+ * @param entryPath - Absolute file path to the server entry script (`src/api.ts`).
134
+ * @param outputPath - Absolute destination file path for the bundled output (`build/api.js`).
135
+ * @param developmentBuild - Indicates whether to enable external sourcemaps or minification.
136
+ * @returns Array of command line arguments for running `bun build`.
137
+ */
108
138
  function createAddonServerBuildArguments(entryPath, outputPath, developmentBuild) {
109
139
  return [
110
140
  "build",
@@ -112,9 +142,17 @@ function createAddonServerBuildArguments(entryPath, outputPath, developmentBuild
112
142
  "--target=bun",
113
143
  "--format=esm",
114
144
  `--outfile=${outputPath}`,
115
- developmentBuild ? "--sourcemap=external" : "--minify"
145
+ developmentBuild ? "--sourcemap=inline" : "--minify"
116
146
  ];
117
147
  }
148
+ /**
149
+ * Cryptographically signs an addon manifest using RSA-SHA256 over all archive file contents.
150
+ *
151
+ * @param manifest - Unsigned or base addon manifest.
152
+ * @param files - List of archive file paths and raw contents to digest.
153
+ * @param privateKey - RSA private key in PEM format.
154
+ * @returns Updated {@link AddonManifest} containing an RSA signature block.
155
+ */
118
156
  function signAddonManifest(manifest, files, privateKey) {
119
157
  if (!manifest.publisher?.id) throw new Error("Signed addons require manifest.publisher.id");
120
158
  const unsigned = {
@@ -142,6 +180,14 @@ function signAddonManifest(manifest, files, privateKey) {
142
180
  }
143
181
  };
144
182
  }
183
+ /**
184
+ * Resolves the required RSA signing key for database-bearing addons.
185
+ *
186
+ * @param signingPrivateKey - Provided signing key or undefined.
187
+ * @param developmentBuild - Whether the current build is targeting development mode.
188
+ * @returns Resolved signing key string or undefined (in dev builds).
189
+ * @throws {@link Error} If building a database-bearing addon in production without a signing key.
190
+ */
145
191
  function requireAddonSigningKey(signingPrivateKey, developmentBuild) {
146
192
  if (signingPrivateKey) return signingPrivateKey;
147
193
  if (developmentBuild) return void 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jchat/addon-sdk",
3
- "version": "1.0.0-alpha.3",
3
+ "version": "1.0.0-alpha.4",
4
4
  "private": false,
5
5
  "keywords": [
6
6
  "addon",
@@ -44,7 +44,7 @@
44
44
  },
45
45
  "scripts": {
46
46
  "build": "tsdown",
47
- "lint": "oxlint . --config=../../oxlint.json",
47
+ "lint": "oxlint .",
48
48
  "format": "oxfmt . --write",
49
49
  "typecheck": "tsc --noEmit",
50
50
  "prepack": "bun run build"