@usebetterdev/plugin 0.5.0 → 0.7.0

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.
@@ -0,0 +1,23 @@
1
+ import type { BetterConfig } from "./config-types.js";
2
+ /** Thrown when no `better.config.*` file is found in the working directory. */
3
+ export declare class BetterConfigNotFoundError extends Error {
4
+ readonly name = "BetterConfigNotFoundError";
5
+ readonly cwd: string;
6
+ constructor(cwd: string);
7
+ }
8
+ export interface LoadBetterConfigOptions {
9
+ /** Working directory to search for the config file. Defaults to `process.cwd()`. */
10
+ cwd?: string;
11
+ /** Explicit path to a config file, bypassing discovery. Maps to c12's `configFile` option. */
12
+ configPath?: string;
13
+ }
14
+ /**
15
+ * Loads a `better.config.{ts,js,mjs}` file using c12.
16
+ * Throws {@link BetterConfigNotFoundError} when no config file is found.
17
+ *
18
+ * The returned config is **unvalidated**. Pass it through
19
+ * {@link requireTenantConfig} or {@link requireAuditConfig} to validate
20
+ * product-specific sections.
21
+ */
22
+ export declare function loadBetterConfig(options?: LoadBetterConfigOptions): Promise<BetterConfig>;
23
+ //# sourceMappingURL=config-loader.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config-loader.d.ts","sourceRoot":"","sources":["../src/config-loader.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEtD,+EAA+E;AAC/E,qBAAa,yBAA0B,SAAQ,KAAK;IAClD,SAAkB,IAAI,+BAA+B;IACrD,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;gBAET,GAAG,EAAE,MAAM;CAgBxB;AAED,MAAM,WAAW,uBAAuB;IACtC,oFAAoF;IACpF,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,8FAA8F;IAC9F,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;;GAOG;AACH,wBAAsB,gBAAgB,CACpC,OAAO,CAAC,EAAE,uBAAuB,GAChC,OAAO,CAAC,YAAY,CAAC,CAsBvB"}
@@ -0,0 +1,2 @@
1
+ export type { TelemetryConfig, PathResolverConfig, SubdomainResolverConfig, StaticJwtResolverConfig, StaticTenantResolverConfig, BetterTenantStaticConfig, RetentionPolicy, BetterAuditStaticConfig, StaticMagicLinkConfig, StaticConsoleSessionConfig, BetterConsoleStaticConfig, BetterConfig, } from "@usebetterdev/contract/config-types";
2
+ //# sourceMappingURL=config-types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config-types.d.ts","sourceRoot":"","sources":["../src/config-types.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,eAAe,EACf,kBAAkB,EAClB,uBAAuB,EACvB,uBAAuB,EACvB,0BAA0B,EAC1B,wBAAwB,EACxB,eAAe,EACf,uBAAuB,EACvB,qBAAqB,EACrB,0BAA0B,EAC1B,yBAAyB,EACzB,YAAY,GACb,MAAM,qCAAqC,CAAC"}
@@ -0,0 +1,19 @@
1
+ import type { BetterConfig, BetterTenantStaticConfig, BetterAuditStaticConfig } from "./config-types.js";
2
+ /** Thrown when a config section fails validation. */
3
+ export declare class ConfigValidationError extends Error {
4
+ readonly name = "ConfigValidationError";
5
+ readonly field: string;
6
+ constructor(field: string, message: string);
7
+ }
8
+ /**
9
+ * Validates that `config.tenant` exists and its `tenantTables` is a non-empty
10
+ * array of non-empty strings. Returns the narrowed tenant config.
11
+ */
12
+ export declare function requireTenantConfig(config: BetterConfig): BetterTenantStaticConfig;
13
+ /**
14
+ * Validates that `config.audit` exists and its `auditTables` is a non-empty
15
+ * array of non-empty strings. When `retention` is present, validates `days` is
16
+ * a positive integer and `tables` (if present) is a non-empty string array.
17
+ */
18
+ export declare function requireAuditConfig(config: BetterConfig): BetterAuditStaticConfig;
19
+ //# sourceMappingURL=config-validation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config-validation.d.ts","sourceRoot":"","sources":["../src/config-validation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,YAAY,EACZ,wBAAwB,EACxB,uBAAuB,EACxB,MAAM,mBAAmB,CAAC;AAE3B,qDAAqD;AACrD,qBAAa,qBAAsB,SAAQ,KAAK;IAC9C,SAAkB,IAAI,2BAA2B;IACjD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;gBAEX,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM;CAI3C;AA4BD;;;GAGG;AACH,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,YAAY,GACnB,wBAAwB,CAM1B;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,YAAY,GACnB,uBAAuB,CAoBzB"}
@@ -0,0 +1,142 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/config.ts
21
+ var config_exports = {};
22
+ __export(config_exports, {
23
+ BetterConfigNotFoundError: () => BetterConfigNotFoundError,
24
+ ConfigValidationError: () => ConfigValidationError,
25
+ defineBetterConfig: () => defineBetterConfig,
26
+ loadBetterConfig: () => loadBetterConfig,
27
+ requireAuditConfig: () => requireAuditConfig,
28
+ requireTenantConfig: () => requireTenantConfig
29
+ });
30
+ module.exports = __toCommonJS(config_exports);
31
+
32
+ // src/define-config.ts
33
+ function defineBetterConfig(config) {
34
+ return config;
35
+ }
36
+
37
+ // src/config-loader.ts
38
+ var import_c12 = require("c12");
39
+ var BetterConfigNotFoundError = class extends Error {
40
+ name = "BetterConfigNotFoundError";
41
+ cwd;
42
+ constructor(cwd) {
43
+ super(
44
+ [
45
+ `No better.config.{ts,js,mjs} file found in ${cwd}.`,
46
+ "",
47
+ "Create one with:",
48
+ "",
49
+ ' import { defineBetterConfig } from "@usebetterdev/plugin/config";',
50
+ "",
51
+ " export default defineBetterConfig({",
52
+ ' tenant: { tenantTables: ["organizations"] },',
53
+ " });"
54
+ ].join("\n")
55
+ );
56
+ this.cwd = cwd;
57
+ }
58
+ };
59
+ async function loadBetterConfig(options) {
60
+ const cwd = options?.cwd ?? process.cwd();
61
+ const configFile = options?.configPath;
62
+ const result = await (0, import_c12.loadConfig)({
63
+ name: "better",
64
+ cwd,
65
+ ...configFile !== void 0 ? { configFile } : {},
66
+ rcFile: false,
67
+ globalRc: false,
68
+ packageJson: false,
69
+ dotenv: false
70
+ });
71
+ const layers = result.layers ?? [];
72
+ if (layers.length === 0) {
73
+ throw new BetterConfigNotFoundError(cwd);
74
+ }
75
+ return result.config ?? {};
76
+ }
77
+
78
+ // src/config-validation.ts
79
+ var ConfigValidationError = class extends Error {
80
+ name = "ConfigValidationError";
81
+ field;
82
+ constructor(field, message) {
83
+ super(`Config error: "${field}" ${message}`);
84
+ this.field = field;
85
+ }
86
+ };
87
+ function isNonEmptyString(value) {
88
+ return typeof value === "string" && value.length > 0;
89
+ }
90
+ function validateStringArray(items, fieldName, sectionName) {
91
+ const qualifiedName = `${sectionName}.${fieldName}`;
92
+ if (!Array.isArray(items)) {
93
+ throw new ConfigValidationError(qualifiedName, "must be an array");
94
+ }
95
+ if (items.length === 0) {
96
+ throw new ConfigValidationError(qualifiedName, "must not be empty");
97
+ }
98
+ for (const item of items) {
99
+ if (!isNonEmptyString(item)) {
100
+ throw new ConfigValidationError(
101
+ qualifiedName,
102
+ "entries must be non-empty strings"
103
+ );
104
+ }
105
+ }
106
+ }
107
+ function requireTenantConfig(config) {
108
+ if (config.tenant === void 0) {
109
+ throw new ConfigValidationError("tenant", "section is required");
110
+ }
111
+ validateStringArray(config.tenant.tenantTables, "tenantTables", "tenant");
112
+ return config.tenant;
113
+ }
114
+ function requireAuditConfig(config) {
115
+ if (config.audit === void 0) {
116
+ throw new ConfigValidationError("audit", "section is required");
117
+ }
118
+ validateStringArray(config.audit.auditTables, "auditTables", "audit");
119
+ if (config.audit.retention !== void 0) {
120
+ const { days, tables } = config.audit.retention;
121
+ if (!Number.isInteger(days) || days <= 0) {
122
+ throw new ConfigValidationError(
123
+ "audit.retention.days",
124
+ "must be a positive integer"
125
+ );
126
+ }
127
+ if (tables !== void 0) {
128
+ validateStringArray(tables, "retention.tables", "audit");
129
+ }
130
+ }
131
+ return config.audit;
132
+ }
133
+ // Annotate the CommonJS export names for ESM import in node:
134
+ 0 && (module.exports = {
135
+ BetterConfigNotFoundError,
136
+ ConfigValidationError,
137
+ defineBetterConfig,
138
+ loadBetterConfig,
139
+ requireAuditConfig,
140
+ requireTenantConfig
141
+ });
142
+ //# sourceMappingURL=config.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/config.ts","../src/define-config.ts","../src/config-loader.ts","../src/config-validation.ts"],"sourcesContent":["// ── Config helpers ───────────────────────────────────────────────────────────\n\nexport { defineBetterConfig } from \"./define-config.js\";\n\nexport {\n loadBetterConfig,\n BetterConfigNotFoundError,\n} from \"./config-loader.js\";\nexport type { LoadBetterConfigOptions } from \"./config-loader.js\";\n\nexport {\n requireTenantConfig,\n requireAuditConfig,\n ConfigValidationError,\n} from \"./config-validation.js\";\n\n// ── Re-export config types ──────────────────────────────────────────────────\n\nexport type {\n BetterConfig,\n BetterTenantStaticConfig,\n BetterAuditStaticConfig,\n BetterConsoleStaticConfig,\n TelemetryConfig,\n PathResolverConfig,\n SubdomainResolverConfig,\n StaticJwtResolverConfig,\n StaticTenantResolverConfig,\n RetentionPolicy,\n StaticMagicLinkConfig,\n StaticConsoleSessionConfig,\n} from \"./config-types.js\";\n","import type { BetterConfig } from \"./config-types.js\";\n\n/**\n * Identity helper that provides TypeScript autocomplete for `better.config.ts`.\n *\n * ```ts\n * // better.config.ts\n * import { defineBetterConfig } from \"@usebetterdev/plugin/config\";\n *\n * export default defineBetterConfig({\n * tenant: { tenantTables: [\"organizations\"] },\n * });\n * ```\n */\nexport function defineBetterConfig(config: BetterConfig): BetterConfig {\n return config;\n}\n","import { loadConfig } from \"c12\";\nimport type { BetterConfig } from \"./config-types.js\";\n\n/** Thrown when no `better.config.*` file is found in the working directory. */\nexport class BetterConfigNotFoundError extends Error {\n override readonly name = \"BetterConfigNotFoundError\";\n readonly cwd: string;\n\n constructor(cwd: string) {\n super(\n [\n `No better.config.{ts,js,mjs} file found in ${cwd}.`,\n \"\",\n \"Create one with:\",\n \"\",\n ' import { defineBetterConfig } from \"@usebetterdev/plugin/config\";',\n \"\",\n \" export default defineBetterConfig({\",\n ' tenant: { tenantTables: [\"organizations\"] },',\n \" });\",\n ].join(\"\\n\"),\n );\n this.cwd = cwd;\n }\n}\n\nexport interface LoadBetterConfigOptions {\n /** Working directory to search for the config file. Defaults to `process.cwd()`. */\n cwd?: string;\n /** Explicit path to a config file, bypassing discovery. Maps to c12's `configFile` option. */\n configPath?: string;\n}\n\n/**\n * Loads a `better.config.{ts,js,mjs}` file using c12.\n * Throws {@link BetterConfigNotFoundError} when no config file is found.\n *\n * The returned config is **unvalidated**. Pass it through\n * {@link requireTenantConfig} or {@link requireAuditConfig} to validate\n * product-specific sections.\n */\nexport async function loadBetterConfig(\n options?: LoadBetterConfigOptions,\n): Promise<BetterConfig> {\n const cwd = options?.cwd ?? process.cwd();\n\n // configPath maps to c12's configFile — spread conditionally to satisfy exactOptionalPropertyTypes\n const configFile = options?.configPath;\n\n const result = await loadConfig<BetterConfig>({\n name: \"better\",\n cwd,\n ...(configFile !== undefined ? { configFile } : {}),\n rcFile: false,\n globalRc: false,\n packageJson: false,\n dotenv: false,\n });\n\n const layers = result.layers ?? [];\n if (layers.length === 0) {\n throw new BetterConfigNotFoundError(cwd);\n }\n\n return result.config ?? {};\n}\n","import type {\n BetterConfig,\n BetterTenantStaticConfig,\n BetterAuditStaticConfig,\n} from \"./config-types.js\";\n\n/** Thrown when a config section fails validation. */\nexport class ConfigValidationError extends Error {\n override readonly name = \"ConfigValidationError\";\n readonly field: string;\n\n constructor(field: string, message: string) {\n super(`Config error: \"${field}\" ${message}`);\n this.field = field;\n }\n}\n\nfunction isNonEmptyString(value: unknown): value is string {\n return typeof value === \"string\" && value.length > 0;\n}\n\nfunction validateStringArray(\n items: unknown,\n fieldName: string,\n sectionName: string,\n): void {\n const qualifiedName = `${sectionName}.${fieldName}`;\n if (!Array.isArray(items)) {\n throw new ConfigValidationError(qualifiedName, \"must be an array\");\n }\n if (items.length === 0) {\n throw new ConfigValidationError(qualifiedName, \"must not be empty\");\n }\n for (const item of items) {\n if (!isNonEmptyString(item)) {\n throw new ConfigValidationError(\n qualifiedName,\n \"entries must be non-empty strings\",\n );\n }\n }\n}\n\n/**\n * Validates that `config.tenant` exists and its `tenantTables` is a non-empty\n * array of non-empty strings. Returns the narrowed tenant config.\n */\nexport function requireTenantConfig(\n config: BetterConfig,\n): BetterTenantStaticConfig {\n if (config.tenant === undefined) {\n throw new ConfigValidationError(\"tenant\", \"section is required\");\n }\n validateStringArray(config.tenant.tenantTables, \"tenantTables\", \"tenant\");\n return config.tenant;\n}\n\n/**\n * Validates that `config.audit` exists and its `auditTables` is a non-empty\n * array of non-empty strings. When `retention` is present, validates `days` is\n * a positive integer and `tables` (if present) is a non-empty string array.\n */\nexport function requireAuditConfig(\n config: BetterConfig,\n): BetterAuditStaticConfig {\n if (config.audit === undefined) {\n throw new ConfigValidationError(\"audit\", \"section is required\");\n }\n validateStringArray(config.audit.auditTables, \"auditTables\", \"audit\");\n\n if (config.audit.retention !== undefined) {\n const { days, tables } = config.audit.retention;\n if (!Number.isInteger(days) || days <= 0) {\n throw new ConfigValidationError(\n \"audit.retention.days\",\n \"must be a positive integer\",\n );\n }\n if (tables !== undefined) {\n validateStringArray(tables, \"retention.tables\", \"audit\");\n }\n }\n\n return config.audit;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACcO,SAAS,mBAAmB,QAAoC;AACrE,SAAO;AACT;;;AChBA,iBAA2B;AAIpB,IAAM,4BAAN,cAAwC,MAAM;AAAA,EACjC,OAAO;AAAA,EAChB;AAAA,EAET,YAAY,KAAa;AACvB;AAAA,MACE;AAAA,QACE,8CAA8C,GAAG;AAAA,QACjD;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF,EAAE,KAAK,IAAI;AAAA,IACb;AACA,SAAK,MAAM;AAAA,EACb;AACF;AAiBA,eAAsB,iBACpB,SACuB;AACvB,QAAM,MAAM,SAAS,OAAO,QAAQ,IAAI;AAGxC,QAAM,aAAa,SAAS;AAE5B,QAAM,SAAS,UAAM,uBAAyB;AAAA,IAC5C,MAAM;AAAA,IACN;AAAA,IACA,GAAI,eAAe,SAAY,EAAE,WAAW,IAAI,CAAC;AAAA,IACjD,QAAQ;AAAA,IACR,UAAU;AAAA,IACV,aAAa;AAAA,IACb,QAAQ;AAAA,EACV,CAAC;AAED,QAAM,SAAS,OAAO,UAAU,CAAC;AACjC,MAAI,OAAO,WAAW,GAAG;AACvB,UAAM,IAAI,0BAA0B,GAAG;AAAA,EACzC;AAEA,SAAO,OAAO,UAAU,CAAC;AAC3B;;;AC1DO,IAAM,wBAAN,cAAoC,MAAM;AAAA,EAC7B,OAAO;AAAA,EAChB;AAAA,EAET,YAAY,OAAe,SAAiB;AAC1C,UAAM,kBAAkB,KAAK,KAAK,OAAO,EAAE;AAC3C,SAAK,QAAQ;AAAA,EACf;AACF;AAEA,SAAS,iBAAiB,OAAiC;AACzD,SAAO,OAAO,UAAU,YAAY,MAAM,SAAS;AACrD;AAEA,SAAS,oBACP,OACA,WACA,aACM;AACN,QAAM,gBAAgB,GAAG,WAAW,IAAI,SAAS;AACjD,MAAI,CAAC,MAAM,QAAQ,KAAK,GAAG;AACzB,UAAM,IAAI,sBAAsB,eAAe,kBAAkB;AAAA,EACnE;AACA,MAAI,MAAM,WAAW,GAAG;AACtB,UAAM,IAAI,sBAAsB,eAAe,mBAAmB;AAAA,EACpE;AACA,aAAW,QAAQ,OAAO;AACxB,QAAI,CAAC,iBAAiB,IAAI,GAAG;AAC3B,YAAM,IAAI;AAAA,QACR;AAAA,QACA;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;AAMO,SAAS,oBACd,QAC0B;AAC1B,MAAI,OAAO,WAAW,QAAW;AAC/B,UAAM,IAAI,sBAAsB,UAAU,qBAAqB;AAAA,EACjE;AACA,sBAAoB,OAAO,OAAO,cAAc,gBAAgB,QAAQ;AACxE,SAAO,OAAO;AAChB;AAOO,SAAS,mBACd,QACyB;AACzB,MAAI,OAAO,UAAU,QAAW;AAC9B,UAAM,IAAI,sBAAsB,SAAS,qBAAqB;AAAA,EAChE;AACA,sBAAoB,OAAO,MAAM,aAAa,eAAe,OAAO;AAEpE,MAAI,OAAO,MAAM,cAAc,QAAW;AACxC,UAAM,EAAE,MAAM,OAAO,IAAI,OAAO,MAAM;AACtC,QAAI,CAAC,OAAO,UAAU,IAAI,KAAK,QAAQ,GAAG;AACxC,YAAM,IAAI;AAAA,QACR;AAAA,QACA;AAAA,MACF;AAAA,IACF;AACA,QAAI,WAAW,QAAW;AACxB,0BAAoB,QAAQ,oBAAoB,OAAO;AAAA,IACzD;AAAA,EACF;AAEA,SAAO,OAAO;AAChB;","names":[]}
@@ -0,0 +1,6 @@
1
+ export { defineBetterConfig } from "./define-config.js";
2
+ export { loadBetterConfig, BetterConfigNotFoundError, } from "./config-loader.js";
3
+ export type { LoadBetterConfigOptions } from "./config-loader.js";
4
+ export { requireTenantConfig, requireAuditConfig, ConfigValidationError, } from "./config-validation.js";
5
+ export type { BetterConfig, BetterTenantStaticConfig, BetterAuditStaticConfig, BetterConsoleStaticConfig, TelemetryConfig, PathResolverConfig, SubdomainResolverConfig, StaticJwtResolverConfig, StaticTenantResolverConfig, RetentionPolicy, StaticMagicLinkConfig, StaticConsoleSessionConfig, } from "./config-types.js";
6
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAExD,OAAO,EACL,gBAAgB,EAChB,yBAAyB,GAC1B,MAAM,oBAAoB,CAAC;AAC5B,YAAY,EAAE,uBAAuB,EAAE,MAAM,oBAAoB,CAAC;AAElE,OAAO,EACL,mBAAmB,EACnB,kBAAkB,EAClB,qBAAqB,GACtB,MAAM,wBAAwB,CAAC;AAIhC,YAAY,EACV,YAAY,EACZ,wBAAwB,EACxB,uBAAuB,EACvB,yBAAyB,EACzB,eAAe,EACf,kBAAkB,EAClB,uBAAuB,EACvB,uBAAuB,EACvB,0BAA0B,EAC1B,eAAe,EACf,qBAAqB,EACrB,0BAA0B,GAC3B,MAAM,mBAAmB,CAAC"}
package/dist/config.js ADDED
@@ -0,0 +1,110 @@
1
+ // src/define-config.ts
2
+ function defineBetterConfig(config) {
3
+ return config;
4
+ }
5
+
6
+ // src/config-loader.ts
7
+ import { loadConfig } from "c12";
8
+ var BetterConfigNotFoundError = class extends Error {
9
+ name = "BetterConfigNotFoundError";
10
+ cwd;
11
+ constructor(cwd) {
12
+ super(
13
+ [
14
+ `No better.config.{ts,js,mjs} file found in ${cwd}.`,
15
+ "",
16
+ "Create one with:",
17
+ "",
18
+ ' import { defineBetterConfig } from "@usebetterdev/plugin/config";',
19
+ "",
20
+ " export default defineBetterConfig({",
21
+ ' tenant: { tenantTables: ["organizations"] },',
22
+ " });"
23
+ ].join("\n")
24
+ );
25
+ this.cwd = cwd;
26
+ }
27
+ };
28
+ async function loadBetterConfig(options) {
29
+ const cwd = options?.cwd ?? process.cwd();
30
+ const configFile = options?.configPath;
31
+ const result = await loadConfig({
32
+ name: "better",
33
+ cwd,
34
+ ...configFile !== void 0 ? { configFile } : {},
35
+ rcFile: false,
36
+ globalRc: false,
37
+ packageJson: false,
38
+ dotenv: false
39
+ });
40
+ const layers = result.layers ?? [];
41
+ if (layers.length === 0) {
42
+ throw new BetterConfigNotFoundError(cwd);
43
+ }
44
+ return result.config ?? {};
45
+ }
46
+
47
+ // src/config-validation.ts
48
+ var ConfigValidationError = class extends Error {
49
+ name = "ConfigValidationError";
50
+ field;
51
+ constructor(field, message) {
52
+ super(`Config error: "${field}" ${message}`);
53
+ this.field = field;
54
+ }
55
+ };
56
+ function isNonEmptyString(value) {
57
+ return typeof value === "string" && value.length > 0;
58
+ }
59
+ function validateStringArray(items, fieldName, sectionName) {
60
+ const qualifiedName = `${sectionName}.${fieldName}`;
61
+ if (!Array.isArray(items)) {
62
+ throw new ConfigValidationError(qualifiedName, "must be an array");
63
+ }
64
+ if (items.length === 0) {
65
+ throw new ConfigValidationError(qualifiedName, "must not be empty");
66
+ }
67
+ for (const item of items) {
68
+ if (!isNonEmptyString(item)) {
69
+ throw new ConfigValidationError(
70
+ qualifiedName,
71
+ "entries must be non-empty strings"
72
+ );
73
+ }
74
+ }
75
+ }
76
+ function requireTenantConfig(config) {
77
+ if (config.tenant === void 0) {
78
+ throw new ConfigValidationError("tenant", "section is required");
79
+ }
80
+ validateStringArray(config.tenant.tenantTables, "tenantTables", "tenant");
81
+ return config.tenant;
82
+ }
83
+ function requireAuditConfig(config) {
84
+ if (config.audit === void 0) {
85
+ throw new ConfigValidationError("audit", "section is required");
86
+ }
87
+ validateStringArray(config.audit.auditTables, "auditTables", "audit");
88
+ if (config.audit.retention !== void 0) {
89
+ const { days, tables } = config.audit.retention;
90
+ if (!Number.isInteger(days) || days <= 0) {
91
+ throw new ConfigValidationError(
92
+ "audit.retention.days",
93
+ "must be a positive integer"
94
+ );
95
+ }
96
+ if (tables !== void 0) {
97
+ validateStringArray(tables, "retention.tables", "audit");
98
+ }
99
+ }
100
+ return config.audit;
101
+ }
102
+ export {
103
+ BetterConfigNotFoundError,
104
+ ConfigValidationError,
105
+ defineBetterConfig,
106
+ loadBetterConfig,
107
+ requireAuditConfig,
108
+ requireTenantConfig
109
+ };
110
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/define-config.ts","../src/config-loader.ts","../src/config-validation.ts"],"sourcesContent":["import type { BetterConfig } from \"./config-types.js\";\n\n/**\n * Identity helper that provides TypeScript autocomplete for `better.config.ts`.\n *\n * ```ts\n * // better.config.ts\n * import { defineBetterConfig } from \"@usebetterdev/plugin/config\";\n *\n * export default defineBetterConfig({\n * tenant: { tenantTables: [\"organizations\"] },\n * });\n * ```\n */\nexport function defineBetterConfig(config: BetterConfig): BetterConfig {\n return config;\n}\n","import { loadConfig } from \"c12\";\nimport type { BetterConfig } from \"./config-types.js\";\n\n/** Thrown when no `better.config.*` file is found in the working directory. */\nexport class BetterConfigNotFoundError extends Error {\n override readonly name = \"BetterConfigNotFoundError\";\n readonly cwd: string;\n\n constructor(cwd: string) {\n super(\n [\n `No better.config.{ts,js,mjs} file found in ${cwd}.`,\n \"\",\n \"Create one with:\",\n \"\",\n ' import { defineBetterConfig } from \"@usebetterdev/plugin/config\";',\n \"\",\n \" export default defineBetterConfig({\",\n ' tenant: { tenantTables: [\"organizations\"] },',\n \" });\",\n ].join(\"\\n\"),\n );\n this.cwd = cwd;\n }\n}\n\nexport interface LoadBetterConfigOptions {\n /** Working directory to search for the config file. Defaults to `process.cwd()`. */\n cwd?: string;\n /** Explicit path to a config file, bypassing discovery. Maps to c12's `configFile` option. */\n configPath?: string;\n}\n\n/**\n * Loads a `better.config.{ts,js,mjs}` file using c12.\n * Throws {@link BetterConfigNotFoundError} when no config file is found.\n *\n * The returned config is **unvalidated**. Pass it through\n * {@link requireTenantConfig} or {@link requireAuditConfig} to validate\n * product-specific sections.\n */\nexport async function loadBetterConfig(\n options?: LoadBetterConfigOptions,\n): Promise<BetterConfig> {\n const cwd = options?.cwd ?? process.cwd();\n\n // configPath maps to c12's configFile — spread conditionally to satisfy exactOptionalPropertyTypes\n const configFile = options?.configPath;\n\n const result = await loadConfig<BetterConfig>({\n name: \"better\",\n cwd,\n ...(configFile !== undefined ? { configFile } : {}),\n rcFile: false,\n globalRc: false,\n packageJson: false,\n dotenv: false,\n });\n\n const layers = result.layers ?? [];\n if (layers.length === 0) {\n throw new BetterConfigNotFoundError(cwd);\n }\n\n return result.config ?? {};\n}\n","import type {\n BetterConfig,\n BetterTenantStaticConfig,\n BetterAuditStaticConfig,\n} from \"./config-types.js\";\n\n/** Thrown when a config section fails validation. */\nexport class ConfigValidationError extends Error {\n override readonly name = \"ConfigValidationError\";\n readonly field: string;\n\n constructor(field: string, message: string) {\n super(`Config error: \"${field}\" ${message}`);\n this.field = field;\n }\n}\n\nfunction isNonEmptyString(value: unknown): value is string {\n return typeof value === \"string\" && value.length > 0;\n}\n\nfunction validateStringArray(\n items: unknown,\n fieldName: string,\n sectionName: string,\n): void {\n const qualifiedName = `${sectionName}.${fieldName}`;\n if (!Array.isArray(items)) {\n throw new ConfigValidationError(qualifiedName, \"must be an array\");\n }\n if (items.length === 0) {\n throw new ConfigValidationError(qualifiedName, \"must not be empty\");\n }\n for (const item of items) {\n if (!isNonEmptyString(item)) {\n throw new ConfigValidationError(\n qualifiedName,\n \"entries must be non-empty strings\",\n );\n }\n }\n}\n\n/**\n * Validates that `config.tenant` exists and its `tenantTables` is a non-empty\n * array of non-empty strings. Returns the narrowed tenant config.\n */\nexport function requireTenantConfig(\n config: BetterConfig,\n): BetterTenantStaticConfig {\n if (config.tenant === undefined) {\n throw new ConfigValidationError(\"tenant\", \"section is required\");\n }\n validateStringArray(config.tenant.tenantTables, \"tenantTables\", \"tenant\");\n return config.tenant;\n}\n\n/**\n * Validates that `config.audit` exists and its `auditTables` is a non-empty\n * array of non-empty strings. When `retention` is present, validates `days` is\n * a positive integer and `tables` (if present) is a non-empty string array.\n */\nexport function requireAuditConfig(\n config: BetterConfig,\n): BetterAuditStaticConfig {\n if (config.audit === undefined) {\n throw new ConfigValidationError(\"audit\", \"section is required\");\n }\n validateStringArray(config.audit.auditTables, \"auditTables\", \"audit\");\n\n if (config.audit.retention !== undefined) {\n const { days, tables } = config.audit.retention;\n if (!Number.isInteger(days) || days <= 0) {\n throw new ConfigValidationError(\n \"audit.retention.days\",\n \"must be a positive integer\",\n );\n }\n if (tables !== undefined) {\n validateStringArray(tables, \"retention.tables\", \"audit\");\n }\n }\n\n return config.audit;\n}\n"],"mappings":";AAcO,SAAS,mBAAmB,QAAoC;AACrE,SAAO;AACT;;;AChBA,SAAS,kBAAkB;AAIpB,IAAM,4BAAN,cAAwC,MAAM;AAAA,EACjC,OAAO;AAAA,EAChB;AAAA,EAET,YAAY,KAAa;AACvB;AAAA,MACE;AAAA,QACE,8CAA8C,GAAG;AAAA,QACjD;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF,EAAE,KAAK,IAAI;AAAA,IACb;AACA,SAAK,MAAM;AAAA,EACb;AACF;AAiBA,eAAsB,iBACpB,SACuB;AACvB,QAAM,MAAM,SAAS,OAAO,QAAQ,IAAI;AAGxC,QAAM,aAAa,SAAS;AAE5B,QAAM,SAAS,MAAM,WAAyB;AAAA,IAC5C,MAAM;AAAA,IACN;AAAA,IACA,GAAI,eAAe,SAAY,EAAE,WAAW,IAAI,CAAC;AAAA,IACjD,QAAQ;AAAA,IACR,UAAU;AAAA,IACV,aAAa;AAAA,IACb,QAAQ;AAAA,EACV,CAAC;AAED,QAAM,SAAS,OAAO,UAAU,CAAC;AACjC,MAAI,OAAO,WAAW,GAAG;AACvB,UAAM,IAAI,0BAA0B,GAAG;AAAA,EACzC;AAEA,SAAO,OAAO,UAAU,CAAC;AAC3B;;;AC1DO,IAAM,wBAAN,cAAoC,MAAM;AAAA,EAC7B,OAAO;AAAA,EAChB;AAAA,EAET,YAAY,OAAe,SAAiB;AAC1C,UAAM,kBAAkB,KAAK,KAAK,OAAO,EAAE;AAC3C,SAAK,QAAQ;AAAA,EACf;AACF;AAEA,SAAS,iBAAiB,OAAiC;AACzD,SAAO,OAAO,UAAU,YAAY,MAAM,SAAS;AACrD;AAEA,SAAS,oBACP,OACA,WACA,aACM;AACN,QAAM,gBAAgB,GAAG,WAAW,IAAI,SAAS;AACjD,MAAI,CAAC,MAAM,QAAQ,KAAK,GAAG;AACzB,UAAM,IAAI,sBAAsB,eAAe,kBAAkB;AAAA,EACnE;AACA,MAAI,MAAM,WAAW,GAAG;AACtB,UAAM,IAAI,sBAAsB,eAAe,mBAAmB;AAAA,EACpE;AACA,aAAW,QAAQ,OAAO;AACxB,QAAI,CAAC,iBAAiB,IAAI,GAAG;AAC3B,YAAM,IAAI;AAAA,QACR;AAAA,QACA;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;AAMO,SAAS,oBACd,QAC0B;AAC1B,MAAI,OAAO,WAAW,QAAW;AAC/B,UAAM,IAAI,sBAAsB,UAAU,qBAAqB;AAAA,EACjE;AACA,sBAAoB,OAAO,OAAO,cAAc,gBAAgB,QAAQ;AACxE,SAAO,OAAO;AAChB;AAOO,SAAS,mBACd,QACyB;AACzB,MAAI,OAAO,UAAU,QAAW;AAC9B,UAAM,IAAI,sBAAsB,SAAS,qBAAqB;AAAA,EAChE;AACA,sBAAoB,OAAO,MAAM,aAAa,eAAe,OAAO;AAEpE,MAAI,OAAO,MAAM,cAAc,QAAW;AACxC,UAAM,EAAE,MAAM,OAAO,IAAI,OAAO,MAAM;AACtC,QAAI,CAAC,OAAO,UAAU,IAAI,KAAK,QAAQ,GAAG;AACxC,YAAM,IAAI;AAAA,QACR;AAAA,QACA;AAAA,MACF;AAAA,IACF;AACA,QAAI,WAAW,QAAW;AACxB,0BAAoB,QAAQ,oBAAoB,OAAO;AAAA,IACzD;AAAA,EACF;AAEA,SAAO,OAAO;AAChB;","names":[]}
@@ -0,0 +1,15 @@
1
+ import type { BetterConfig } from "./config-types.js";
2
+ /**
3
+ * Identity helper that provides TypeScript autocomplete for `better.config.ts`.
4
+ *
5
+ * ```ts
6
+ * // better.config.ts
7
+ * import { defineBetterConfig } from "@usebetterdev/plugin/config";
8
+ *
9
+ * export default defineBetterConfig({
10
+ * tenant: { tenantTables: ["organizations"] },
11
+ * });
12
+ * ```
13
+ */
14
+ export declare function defineBetterConfig(config: BetterConfig): BetterConfig;
15
+ //# sourceMappingURL=define-config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"define-config.d.ts","sourceRoot":"","sources":["../src/define-config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEtD;;;;;;;;;;;GAWG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,YAAY,GAAG,YAAY,CAErE"}
@@ -0,0 +1,34 @@
1
+ export declare const authEvents: () => {
2
+ id: string;
3
+ schema: {
4
+ tables: {
5
+ auth_events: {
6
+ fields: {
7
+ id: {
8
+ type: "string";
9
+ required: true;
10
+ unique: true;
11
+ };
12
+ event_type: {
13
+ type: "string";
14
+ required: true;
15
+ };
16
+ user_id: {
17
+ type: "string";
18
+ required: true;
19
+ index: true;
20
+ };
21
+ timestamp: {
22
+ type: "date";
23
+ required: true;
24
+ defaultValue: () => Date;
25
+ };
26
+ metadata: {
27
+ type: "json";
28
+ };
29
+ };
30
+ };
31
+ };
32
+ };
33
+ };
34
+ //# sourceMappingURL=auth-events.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth-events.d.ts","sourceRoot":"","sources":["../../src/demo/auth-events.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAsBtB,CAAC"}
@@ -0,0 +1,74 @@
1
+ export type AuditHookContract = {
2
+ afterLog: {
3
+ log: Readonly<{
4
+ id: string;
5
+ tableName: string;
6
+ }>;
7
+ };
8
+ };
9
+ export interface WebhookDispatcherConfig {
10
+ /** Target URL to POST event payloads to. */
11
+ url: string;
12
+ /** Optional list of event types to forward. When omitted, all events are forwarded. */
13
+ events?: string[];
14
+ }
15
+ export declare const webhookDispatcher: (config: WebhookDispatcherConfig) => {
16
+ id: string;
17
+ hooks: {
18
+ afterLog: (_payload: {
19
+ log: Readonly<{
20
+ id: string;
21
+ tableName: string;
22
+ }>;
23
+ }) => Promise<void>;
24
+ };
25
+ schema: {
26
+ tables: {
27
+ webhook_deliveries: {
28
+ fields: {
29
+ id: {
30
+ type: "string";
31
+ required: true;
32
+ unique: true;
33
+ };
34
+ endpoint_url: {
35
+ type: "string";
36
+ required: true;
37
+ index: true;
38
+ };
39
+ event_type: {
40
+ type: "string";
41
+ required: true;
42
+ };
43
+ status: {
44
+ type: "string";
45
+ required: true;
46
+ };
47
+ http_status: {
48
+ type: "number";
49
+ };
50
+ attempt_count: {
51
+ type: "number";
52
+ required: true;
53
+ defaultValue: number;
54
+ };
55
+ last_attempt_at: {
56
+ type: "date";
57
+ };
58
+ created_at: {
59
+ type: "date";
60
+ required: true;
61
+ };
62
+ };
63
+ };
64
+ };
65
+ extend: {
66
+ audit_log: {
67
+ webhook_delivered_at: {
68
+ type: "date";
69
+ };
70
+ };
71
+ };
72
+ };
73
+ };
74
+ //# sourceMappingURL=webhook-dispatcher.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhook-dispatcher.d.ts","sourceRoot":"","sources":["../../src/demo/webhook-dispatcher.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,iBAAiB,GAAG;IAC9B,QAAQ,EAAE;QAAE,GAAG,EAAE,QAAQ,CAAC;YAAE,EAAE,EAAE,MAAM,CAAC;YAAC,SAAS,EAAE,MAAM,CAAA;SAAE,CAAC,CAAA;KAAE,CAAC;CAChE,CAAC;AAEF,MAAM,WAAW,uBAAuB;IACtC,4CAA4C;IAC5C,GAAG,EAAE,MAAM,CAAC;IACZ,uFAAuF;IACvF,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,eAAO,MAAM,iBAAiB,WAAY,uBAAuB;;;;iBAV9C,QAAQ,CAAC;gBAAE,EAAE,EAAE,MAAM,CAAC;gBAAC,SAAS,EAAE,MAAM,CAAA;aAAE,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6C7D,CAAC"}
@@ -0,0 +1,68 @@
1
+ import type { HookContract, HookHandlers } from "./types.js";
2
+ /**
3
+ * Error captured when a plugin hook handler fails during `invokeAfter`.
4
+ */
5
+ export interface PluginError {
6
+ pluginId: string;
7
+ hook: string;
8
+ error: unknown;
9
+ }
10
+ /**
11
+ * Error thrown by `invokeBefore` when a plugin hook handler fails.
12
+ * Wraps the original error with plugin and hook context so callers
13
+ * can identify which plugin caused the failure.
14
+ */
15
+ export declare class PluginInvokeError extends Error {
16
+ readonly name = "PluginInvokeError";
17
+ readonly pluginId: string;
18
+ readonly hook: string;
19
+ constructor(pluginId: string, hook: string, cause: unknown);
20
+ }
21
+ /**
22
+ * Runtime registry that products use to register plugin hooks and invoke them
23
+ * at lifecycle points.
24
+ *
25
+ * Generic over THooks so products get type-safe hook names and payloads.
26
+ */
27
+ export declare class HookRegistry<THooks extends HookContract> {
28
+ private hooks;
29
+ private registeredPlugins;
30
+ /**
31
+ * Register all hooks declared by a plugin. Skips entries whose value is not
32
+ * a function (defensive against partial/optional declarations).
33
+ *
34
+ * Throws if a plugin with the same ID has already been registered.
35
+ */
36
+ register(pluginId: string, hooks: Partial<HookHandlers<THooks>>): void;
37
+ /**
38
+ * Sequential, fail-fast invocation — for "before" hooks.
39
+ * Runs handlers in registration order. Aborts on first throw and wraps
40
+ * the error in a {@link PluginInvokeError} so the caller knows which
41
+ * plugin failed. Later handlers are **not** called.
42
+ *
43
+ * Uses a snapshot of the handler list so that registrations during
44
+ * async execution do not affect the current invocation.
45
+ */
46
+ invokeBefore<K extends keyof THooks & string>(hook: K, payload: THooks[K]): Promise<void>;
47
+ /**
48
+ * Sequential, collect-errors invocation — for "after" hooks.
49
+ * Runs **all** handlers even when some fail. Failures are wrapped in
50
+ * {@link PluginError} and returned as an array. Returns an empty array
51
+ * when all handlers succeed or when no handlers are registered.
52
+ *
53
+ * Uses a snapshot of the handler list so that registrations during
54
+ * async execution do not affect the current invocation.
55
+ */
56
+ invokeAfter<K extends keyof THooks & string>(hook: K, payload: THooks[K]): Promise<PluginError[]>;
57
+ /**
58
+ * Calls a stored handler with the given payload.
59
+ *
60
+ * Handlers are stored as `(payload: never) => …` for variance compatibility
61
+ * (see {@link RegisteredHook}). At runtime the payload always matches
62
+ * `THooks[K]` — guaranteed by the generic constraint on {@link register}.
63
+ * This single assertion is the only place where TypeScript's type system
64
+ * cannot statically verify the heterogeneous map access.
65
+ */
66
+ private callHandler;
67
+ }
68
+ //# sourceMappingURL=hook-registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hook-registry.d.ts","sourceRoot":"","sources":["../src/hook-registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE7D;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,OAAO,CAAC;CAChB;AAED;;;;GAIG;AACH,qBAAa,iBAAkB,SAAQ,KAAK;IAC1C,SAAkB,IAAI,uBAAuB;IAC7C,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;gBAEV,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO;CAQ3D;AAYD;;;;;GAKG;AACH,qBAAa,YAAY,CAAC,MAAM,SAAS,YAAY;IACnD,OAAO,CAAC,KAAK,CAAuC;IACpD,OAAO,CAAC,iBAAiB,CAAqB;IAE9C;;;;;OAKG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC,GAAG,IAAI;IAoBtE;;;;;;;;OAQG;IACG,YAAY,CAAC,CAAC,SAAS,MAAM,MAAM,GAAG,MAAM,EAChD,IAAI,EAAE,CAAC,EACP,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC,GACjB,OAAO,CAAC,IAAI,CAAC;IAehB;;;;;;;;OAQG;IACG,WAAW,CAAC,CAAC,SAAS,MAAM,MAAM,GAAG,MAAM,EAC/C,IAAI,EAAE,CAAC,EACP,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC,GACjB,OAAO,CAAC,WAAW,EAAE,CAAC;IAsBzB;;;;;;;;OAQG;IACH,OAAO,CAAC,WAAW;CAMpB"}