@pipelab/shared 2.0.1-beta.20

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.
Files changed (54) hide show
  1. package/.turbo/turbo-build.log +41 -0
  2. package/.turbo/turbo-lint.log +136 -0
  3. package/CHANGELOG.md +167 -0
  4. package/LICENSE +110 -0
  5. package/LICENSE.md +110 -0
  6. package/README.md +3 -0
  7. package/dist/index.d.mts +3603 -0
  8. package/dist/index.d.mts.map +1 -0
  9. package/dist/index.mjs +43763 -0
  10. package/dist/index.mjs.map +1 -0
  11. package/package.json +53 -0
  12. package/src/apis.ts +191 -0
  13. package/src/build-history.ts +127 -0
  14. package/src/config/migrators.ts +308 -0
  15. package/src/config/projects-definition.ts +25 -0
  16. package/src/config/projects-types.ts +25 -0
  17. package/src/config/projects.ts +1 -0
  18. package/src/config/settings-definition.ts +21 -0
  19. package/src/config/settings.ts +21 -0
  20. package/src/config.schema.ts +149 -0
  21. package/src/database.types.ts +95 -0
  22. package/src/errors.ts +11 -0
  23. package/src/evaluator.ts +58 -0
  24. package/src/fmt.ts +5 -0
  25. package/src/graph.ts +207 -0
  26. package/src/i18n/de_DE.json +86 -0
  27. package/src/i18n/en_US.json +147 -0
  28. package/src/i18n/es_ES.json +86 -0
  29. package/src/i18n/fr_FR.json +89 -0
  30. package/src/i18n/pt_BR.json +86 -0
  31. package/src/i18n/zh_CN.json +86 -0
  32. package/src/i18n-utils.ts +10 -0
  33. package/src/index.ts +51 -0
  34. package/src/ipc.types.ts +73 -0
  35. package/src/logger.ts +20 -0
  36. package/src/migrations/model.ts +1 -0
  37. package/src/migrations/projects.ts +1 -0
  38. package/src/migrations/settings.ts +1 -0
  39. package/src/model.test.ts +214 -0
  40. package/src/model.ts +233 -0
  41. package/src/plugins/definitions.ts +472 -0
  42. package/src/plugins.ts +20 -0
  43. package/src/quickjs.ts +98 -0
  44. package/src/save-location.ts +42 -0
  45. package/src/subscription-errors.ts +87 -0
  46. package/src/supabase.ts +28 -0
  47. package/src/tests/helpers.ts +5 -0
  48. package/src/types.ts +3 -0
  49. package/src/utils.ts +3 -0
  50. package/src/validation.ts +16 -0
  51. package/src/variables.ts +27 -0
  52. package/src/wasm.d.ts +9 -0
  53. package/src/websocket.types.ts +186 -0
  54. package/tsconfig.json +13 -0
@@ -0,0 +1,472 @@
1
+ import type { ConditionalPick } from "type-fest";
2
+ import type { BrowserWindow, OpenDialogOptions } from "electron";
3
+
4
+ export type PathOptions = {
5
+ filter?: RegExp;
6
+ type?: "file" | "folder";
7
+ };
8
+
9
+ export type PropType =
10
+ | "string"
11
+ | "number"
12
+ | "boolean"
13
+ | "array"
14
+ | {
15
+ type: "array";
16
+ of: PropType;
17
+ }
18
+ | "any"
19
+ | PropType[];
20
+
21
+ export interface ControlTypeBase {
22
+ type: string;
23
+ }
24
+
25
+ export interface ControlTypeInput extends ControlTypeBase {
26
+ type: "input";
27
+ options: {
28
+ kind: "number" | "text";
29
+ validator?: string;
30
+ placeholder?: string;
31
+ password?: boolean;
32
+ };
33
+ }
34
+
35
+ export interface ControlTypeExpression extends ControlTypeBase {
36
+ type: "expression";
37
+ options: {};
38
+ }
39
+
40
+ export interface ControlTypeNetlifySite extends ControlTypeBase {
41
+ type: "netlify-site";
42
+ options: {
43
+ allowCreate?: boolean;
44
+ placeholder?: string;
45
+ tokenKey: string;
46
+ };
47
+ }
48
+
49
+ export interface PipelabSelectOption {
50
+ label: string;
51
+ value: string;
52
+ }
53
+
54
+ export interface ControlTypeSelect extends ControlTypeBase {
55
+ type: "select";
56
+ options: {
57
+ options: Array<PipelabSelectOption>;
58
+ placeholder: string;
59
+ };
60
+ }
61
+
62
+ export interface ControlTypeMultiSelect extends ControlTypeBase {
63
+ type: "multi-select";
64
+ options: {
65
+ options: Array<PipelabSelectOption>;
66
+ placeholder: string;
67
+ };
68
+ }
69
+
70
+ export interface ControlTypeBoolean extends ControlTypeBase {
71
+ type: "boolean";
72
+ }
73
+
74
+ export interface ControlTypeCheckbox extends ControlTypeBase {
75
+ type: "checkbox";
76
+ }
77
+
78
+ export interface ControlTypePath extends ControlTypeBase {
79
+ type: "path";
80
+ options: OpenDialogOptions;
81
+ label?: string;
82
+ }
83
+
84
+ export interface ControlTypeJSON extends ControlTypeBase {
85
+ type: "json";
86
+ }
87
+
88
+ export interface ControlTypeArray extends ControlTypeBase {
89
+ type: "array";
90
+ options: {
91
+ kind: "number" | "text";
92
+ };
93
+ }
94
+
95
+ export interface ControlTypeColor extends ControlTypeBase {
96
+ type: "color";
97
+ }
98
+
99
+ export interface ControlTypeElectronConfigureV2 extends ControlTypeBase {
100
+ type: "electron:configure:v2";
101
+ }
102
+
103
+ export type ControlType =
104
+ | ControlTypeInput
105
+ | ControlTypeSelect
106
+ | ControlTypeMultiSelect
107
+ | ControlTypeBoolean
108
+ | ControlTypeCheckbox
109
+ | ControlTypePath
110
+ | ControlTypeJSON
111
+ | ControlTypeExpression
112
+ | ControlTypeNetlifySite
113
+ | ControlTypeArray
114
+ | ControlTypeColor
115
+ | ControlTypeElectronConfigureV2;
116
+
117
+ export type InputDefinition<T extends ControlType = ControlType> = {
118
+ label: string;
119
+ description?: string;
120
+ validator?: () => any;
121
+ required: boolean;
122
+ control: T;
123
+ value: unknown;
124
+ platforms?: NodeJS.Platform[];
125
+ onNodeUpdate?: (value: any, settings: InputDefinition<any>) => void;
126
+ };
127
+
128
+ export type InputsDefinition = Record<string, InputDefinition>;
129
+ export type Meta = Record<string, unknown>;
130
+
131
+ export interface OutputDefinition {
132
+ label: string;
133
+ description?: string;
134
+ deprecated?: boolean;
135
+ validator?: (value: any) => any;
136
+ control?: ControlType;
137
+ value: unknown;
138
+ }
139
+
140
+ export type OutputsDefinition = Record<string, OutputDefinition>;
141
+
142
+ export type InputOutputDefinition = InputDefinition | OutputDefinition;
143
+
144
+ export type IconType =
145
+ | {
146
+ type: "image";
147
+ /**
148
+ * base64 image
149
+ */
150
+ image: string;
151
+ }
152
+ | {
153
+ type: "icon";
154
+ icon: string;
155
+ };
156
+ export interface PluginDefinition {
157
+ id: string;
158
+ name: string;
159
+ icon: IconType;
160
+ description: string;
161
+ }
162
+
163
+ export type RendererNodeDefinition = {
164
+ node: PipelabNode;
165
+ };
166
+
167
+ export interface RendererPluginDefinition extends PluginDefinition {
168
+ nodes: Array<RendererNodeDefinition>;
169
+ }
170
+
171
+ export interface MainPluginDefinition extends PluginDefinition {
172
+ nodes: ({
173
+ runner: any; // We use 'any' here to avoid importing Node runners in the safe definition
174
+ } & RendererNodeDefinition)[];
175
+ validators?: Array<{
176
+ id: string;
177
+ description: string;
178
+ validator: (options: any) => any;
179
+ }>;
180
+ }
181
+
182
+ export const createNodeDefinition = (def: MainPluginDefinition) => {
183
+ return def;
184
+ };
185
+
186
+ export type InputsOutputsDefinition = InputsDefinition | OutputsDefinition;
187
+
188
+ export type GetFlowEntries<T extends InputsOutputsDefinition> = ConditionalPick<
189
+ T,
190
+ { type: "flow" }
191
+ >;
192
+ export type GetDataEntries<T extends InputsOutputsDefinition> = ConditionalPick<
193
+ T,
194
+ { type: "data" }
195
+ >;
196
+
197
+ export type GetFlowKeys<T extends InputsOutputsDefinition> = keyof GetFlowEntries<T>;
198
+ export type GetDataKeys<T extends InputsOutputsDefinition> = keyof GetDataEntries<T>;
199
+
200
+ export type DataResult = Record<string, any>;
201
+
202
+ export type SetOutputActionFn<T extends Action> = (
203
+ key: keyof T["outputs"],
204
+ value: T["outputs"][typeof key]["value"],
205
+ ) => void;
206
+ export type SetOutputLoopFn<T extends Loop> = (
207
+ key: keyof T["outputs"],
208
+ value: T["outputs"][typeof key]["value"],
209
+ ) => void;
210
+ export type SetOutputExpressionFn<T extends Expression> = (
211
+ key: keyof T["outputs"],
212
+ value: T["outputs"][typeof key]["value"],
213
+ ) => void;
214
+
215
+ export type ParamsToInput<PARAMS extends InputsDefinition> = {
216
+ [index in keyof PARAMS]: PARAMS[index]["required"] extends true
217
+ ? PARAMS[index]["value"]
218
+ : PARAMS[index]["value"] | null;
219
+ };
220
+
221
+ export interface BaseNode {
222
+ disabled?: boolean | string;
223
+ advanced?: boolean | string;
224
+ updateAvailable?: boolean | string;
225
+ }
226
+
227
+ export interface Action extends BaseNode {
228
+ id: string;
229
+ type: "action";
230
+ version?: number;
231
+ displayString: string;
232
+ icon: string;
233
+ name: string;
234
+ description: string;
235
+ params: InputsDefinition;
236
+ meta: Meta;
237
+ outputs: OutputsDefinition;
238
+ platforms?: NodeJS.Platform[];
239
+ deprecated?: boolean;
240
+ deprecatedMessage?: string;
241
+ }
242
+
243
+ export type ExtractInputsFromAction<ACTION extends Action> = {
244
+ [index in keyof ACTION["params"]]: ACTION["params"][index]["value"];
245
+ };
246
+
247
+ export type ExtractInputsFromCondition<CONDITION extends Condition> = {
248
+ [index in keyof CONDITION["params"]]: CONDITION["params"][index]["value"];
249
+ };
250
+ export type ExtractInputsFromLoop<LOOP extends Loop> = {
251
+ [index in keyof LOOP["params"]]: LOOP["params"][index]["value"];
252
+ };
253
+ export type ExtractInputsFromEvent<EVENT extends Event> = {
254
+ [index in keyof EVENT["params"]]: EVENT["params"][index]["value"];
255
+ };
256
+ export type ExtractInputsFromExpression<EXPRESSION extends Expression> = {
257
+ [index in keyof EXPRESSION["params"]]: EXPRESSION["params"][index]["value"];
258
+ };
259
+
260
+ export interface Condition extends BaseNode {
261
+ id: string;
262
+ type: "condition";
263
+ version?: number;
264
+ displayString: string;
265
+ icon: string;
266
+ name: string;
267
+ description: string;
268
+ params: InputsDefinition;
269
+ meta?: Meta;
270
+ platforms?: NodeJS.Platform[];
271
+ }
272
+
273
+ export interface Loop extends BaseNode {
274
+ id: string;
275
+ type: "loop";
276
+ version?: number;
277
+ displayString: string;
278
+ icon: string;
279
+ name: string;
280
+ description: string;
281
+ params: InputsDefinition;
282
+ meta?: Meta;
283
+ outputs: OutputsDefinition;
284
+ }
285
+
286
+ export interface Expression extends BaseNode {
287
+ id: string;
288
+ type: "expression";
289
+ displayString: string;
290
+ version?: number;
291
+ icon: string;
292
+ name: string;
293
+ description: string;
294
+ params: InputsDefinition;
295
+ meta?: Meta;
296
+ outputs: OutputsDefinition;
297
+ }
298
+
299
+ export interface Event extends BaseNode {
300
+ id: string;
301
+ type: "event";
302
+ version?: number;
303
+ displayString: string;
304
+ icon: string;
305
+ name: string;
306
+ description: string;
307
+ params: InputsDefinition;
308
+ meta?: Meta;
309
+ platforms?: NodeJS.Platform[];
310
+ }
311
+
312
+ export type PipelabNode = Event | Condition | Expression | Action | Loop;
313
+
314
+ export const createDefinition = <T extends MainPluginDefinition>(definition: T) => {
315
+ return definition satisfies T;
316
+ };
317
+
318
+ export const createAction = <T extends Omit<Action, "type">>(action: T) => {
319
+ return {
320
+ ...action,
321
+ type: "action",
322
+ } satisfies Action;
323
+ };
324
+
325
+ export const createStringParam = (
326
+ value: string,
327
+ definition: Omit<InputDefinition<ControlTypeInput>, "value" | "control">,
328
+ ) => {
329
+ return {
330
+ ...definition,
331
+ control: {
332
+ type: "input",
333
+ options: {
334
+ kind: "text",
335
+ },
336
+ },
337
+ value: `"${value}"`,
338
+ } satisfies InputDefinition<ControlTypeInput>;
339
+ };
340
+
341
+ export const createColorPicker = (
342
+ value: string,
343
+ definition: Omit<InputDefinition<ControlTypeColor>, "value" | "control">,
344
+ ) => {
345
+ return {
346
+ ...definition,
347
+ control: {
348
+ type: "color",
349
+ },
350
+ value: `"${value}"`,
351
+ } satisfies InputDefinition<ControlTypeColor>;
352
+ };
353
+
354
+ export const createPasswordParam = (
355
+ value: string,
356
+ definition: Omit<InputDefinition<ControlTypeInput>, "value" | "control">,
357
+ ) => {
358
+ return {
359
+ ...definition,
360
+ control: {
361
+ type: "input",
362
+ options: {
363
+ kind: "text",
364
+ password: true,
365
+ },
366
+ },
367
+ value: `"${value}"`,
368
+ } satisfies InputDefinition<ControlTypeInput>;
369
+ };
370
+
371
+ export const createPathParam = (
372
+ value: string | undefined,
373
+ definition: Omit<InputDefinition<ControlTypePath>, "value">,
374
+ ) => {
375
+ return {
376
+ ...definition,
377
+ value: value ? `"${value}"` : value,
378
+ } satisfies InputDefinition<ControlTypePath>;
379
+ };
380
+
381
+ export const createArray = <T extends unknown[]>(
382
+ value: string | Array<unknown>,
383
+ definition: Omit<InputDefinition<ControlTypeArray>, "value">,
384
+ ) => {
385
+ return {
386
+ ...definition,
387
+ value: (Array.isArray(value) ? `"${value}"` : value) as unknown as T,
388
+ } satisfies InputDefinition<ControlTypeArray>;
389
+ };
390
+
391
+ export const createNetlifySiteParam = (
392
+ value: string,
393
+ tokenKey: string,
394
+ definition: Omit<InputDefinition<ControlTypeNetlifySite>, "value" | "control">,
395
+ ) => {
396
+ return {
397
+ ...definition,
398
+ control: {
399
+ type: "netlify-site",
400
+ options: {
401
+ allowCreate: true,
402
+ placeholder: "Select a site",
403
+ tokenKey,
404
+ },
405
+ },
406
+ value: `"${value}"`,
407
+ } satisfies InputDefinition<ControlTypeNetlifySite>;
408
+ };
409
+
410
+ export const createNumberParam = (
411
+ value: number,
412
+ definition: Omit<InputDefinition<ControlTypeInput>, "value" | "control">,
413
+ ) => {
414
+ return {
415
+ ...definition,
416
+ control: {
417
+ type: "input",
418
+ options: {
419
+ kind: "number",
420
+ },
421
+ },
422
+ value,
423
+ } satisfies InputDefinition<ControlTypeInput>;
424
+ };
425
+
426
+ export const createBooleanParam = (
427
+ value: boolean,
428
+ definition: Omit<InputDefinition<ControlTypeBoolean>, "value" | "control">,
429
+ ) => {
430
+ return {
431
+ ...definition,
432
+ control: {
433
+ type: "boolean",
434
+ },
435
+ value,
436
+ } satisfies InputDefinition<ControlTypeBoolean>;
437
+ };
438
+
439
+ export const createRawParam = <T>(value: T, definition: Omit<InputDefinition, "value">) => {
440
+ return {
441
+ ...definition,
442
+ value,
443
+ };
444
+ };
445
+
446
+ export const createExpression = <T extends Omit<Expression, "type">>(expression: T) => {
447
+ return {
448
+ ...expression,
449
+ type: "expression",
450
+ } satisfies Expression;
451
+ };
452
+
453
+ export const createCondition = <T extends Omit<Condition, "type">>(condition: T) => {
454
+ return {
455
+ ...condition,
456
+ type: "condition",
457
+ } satisfies Condition;
458
+ };
459
+
460
+ export const createLoop = <T extends Omit<Loop, "type">>(loop: T) => {
461
+ return {
462
+ ...loop,
463
+ type: "loop",
464
+ } satisfies Loop;
465
+ };
466
+
467
+ export const createEvent = <T extends Omit<Event, "type">>(event: T) => {
468
+ return {
469
+ ...event,
470
+ type: "event",
471
+ } satisfies Event;
472
+ };
package/src/plugins.ts ADDED
@@ -0,0 +1,20 @@
1
+ import { shallowRef } from "vue";
2
+ import type { RendererPluginDefinition } from "./plugins/definitions.js";
3
+
4
+ type Plugin = RendererPluginDefinition;
5
+
6
+ const plugins = shallowRef<Plugin[]>([]);
7
+
8
+ export const usePlugins = () => {
9
+ const load = () => {};
10
+
11
+ const registerPlugins = (newPlugins: Plugin[]) => {
12
+ plugins.value.push(...newPlugins);
13
+ };
14
+
15
+ return {
16
+ load,
17
+ registerPlugins,
18
+ plugins,
19
+ };
20
+ };
package/src/quickjs.ts ADDED
@@ -0,0 +1,98 @@
1
+ import { useLogger } from "./logger";
2
+ import { newQuickJSWASMModuleFromVariant, newVariant, RELEASE_SYNC } from "quickjs-emscripten";
3
+ import { Arena } from "quickjs-emscripten-sync";
4
+ import { fmt } from "./fmt";
5
+
6
+ class EvaluationError extends Error {
7
+ constructor(
8
+ public name: string,
9
+ public description: string,
10
+ ) {
11
+ super(description);
12
+ this.name = name;
13
+ }
14
+ }
15
+
16
+ /**
17
+ * Creates a QuickJS instance from an already-resolved variant.
18
+ * Callers are responsible for importing and passing the correct variant
19
+ * for their environment (Node vs browser) using a static import.
20
+ */
21
+ export const createQuickJsFromVariant = async (variant: any) => {
22
+ const { logger } = useLogger();
23
+
24
+ const quickjs = await newQuickJSWASMModuleFromVariant(variant);
25
+
26
+ const createContext = () => {
27
+ const vm = quickjs.newContext();
28
+ const arena = new Arena(vm, { isMarshalable: true });
29
+
30
+ const run = (code: string, params: Record<string, unknown>) => {
31
+ const exposed = {
32
+ fmt,
33
+ ...params,
34
+ };
35
+ arena.expose(exposed);
36
+
37
+ const finalCode = `(() => {
38
+ return ${code};
39
+ })()`;
40
+
41
+ try {
42
+ return arena.evalCode(finalCode);
43
+ } catch (e) {
44
+ logger().error("error", e);
45
+ logger().error("Final code was", finalCode);
46
+ throw new EvaluationError(e.name, e.message);
47
+ }
48
+ };
49
+
50
+ return {
51
+ run,
52
+ dispose: () => {
53
+ try {
54
+ arena.dispose();
55
+ } catch (e) {
56
+ logger().error("Failed to dispose arena", e);
57
+ }
58
+ try {
59
+ vm.dispose();
60
+ } catch (e) {
61
+ logger().error("Failed to dispose VM", e);
62
+ }
63
+ },
64
+ };
65
+ };
66
+
67
+ const run = (code: string, params: Record<string, unknown>) => {
68
+ const ctx = createContext();
69
+ try {
70
+ return ctx.run(code, params);
71
+ } finally {
72
+ try {
73
+ ctx.dispose();
74
+ } catch (e) {
75
+ logger().error("Failed to dispose context", e);
76
+ }
77
+ }
78
+ };
79
+
80
+ return {
81
+ run,
82
+ createContext,
83
+ };
84
+ };
85
+
86
+ // ---------------------------------------------------------------------------
87
+ // Node.js variant — statically imported, safe in the Electron main process
88
+ // ---------------------------------------------------------------------------
89
+ import nodeVariant from "@jitl/quickjs-singlefile-mjs-release-sync";
90
+
91
+ export const createQuickJs = () => createQuickJsFromVariant(nodeVariant);
92
+
93
+ export type CreateQuickJSFn = ReturnType<typeof createQuickJs>;
94
+
95
+ // ---------------------------------------------------------------------------
96
+ // newVariant / RELEASE_SYNC re-exported for callers that build their own variant
97
+ // ---------------------------------------------------------------------------
98
+ export { newVariant, RELEASE_SYNC };
@@ -0,0 +1,42 @@
1
+ import { array, literal, object, string, union, type InferInput } from "valibot";
2
+
3
+ export const SaveLocationInternalValidator = object({
4
+ id: string(),
5
+ project: string(),
6
+ lastModified: string(),
7
+ type: literal("internal"),
8
+ configName: string(),
9
+ });
10
+ export type SaveLocationInternal = InferInput<typeof SaveLocationInternalValidator>;
11
+
12
+ /** @deprecated External pipeline files are deprecated and will be removed in future versions. */
13
+ export const SaveLocationExternalValidator = object({
14
+ id: string(),
15
+ project: string(),
16
+ path: string(),
17
+ lastModified: string(),
18
+ type: literal("external"),
19
+ summary: object({
20
+ plugins: array(string()),
21
+ name: string(),
22
+ description: string(),
23
+ }),
24
+ });
25
+ /** @deprecated External pipeline files are deprecated and will be removed in future versions. */
26
+ export type SaveLocationExternal = InferInput<typeof SaveLocationExternalValidator>;
27
+
28
+ export const SaveLocationPipelabCloudValidator = object({
29
+ id: string(),
30
+ project: string(),
31
+ type: literal("pipelab-cloud"),
32
+ });
33
+
34
+ export type SaveLocationPipelabCloud = InferInput<typeof SaveLocationPipelabCloudValidator>;
35
+
36
+ export const SaveLocationValidator = union([
37
+ SaveLocationExternalValidator,
38
+ SaveLocationInternalValidator,
39
+ SaveLocationPipelabCloudValidator,
40
+ ]);
41
+
42
+ export type SaveLocation = InferInput<typeof SaveLocationValidator>;
@@ -0,0 +1,87 @@
1
+ import { SubscriptionError } from "./build-history";
2
+
3
+ export class SubscriptionRequiredError extends Error implements SubscriptionError {
4
+ public readonly code = "SUBSCRIPTION_REQUIRED" as const;
5
+ public benefit: string;
6
+ public userMessage: string;
7
+
8
+ constructor(benefit?: string) {
9
+ super(`Subscription required${benefit ? ` for benefit: ${benefit}` : ""}`);
10
+ this.name = "SubscriptionRequiredError";
11
+ this.benefit = benefit || "build-history";
12
+ this.userMessage = `Build history is a premium feature. Please upgrade your subscription to access this feature.`;
13
+ }
14
+ }
15
+
16
+ export class SubscriptionExpiredError extends Error implements SubscriptionError {
17
+ public readonly code = "SUBSCRIPTION_EXPIRED" as const;
18
+ public benefit: string;
19
+ public userMessage: string;
20
+
21
+ constructor(benefit?: string) {
22
+ super(`Subscription expired${benefit ? ` for benefit: ${benefit}` : ""}`);
23
+ this.name = "SubscriptionExpiredError";
24
+ this.benefit = benefit || "build-history";
25
+ this.userMessage = `Your subscription has expired. Please renew your subscription to continue using build history.`;
26
+ }
27
+ }
28
+
29
+ export class BenefitNotFoundError extends Error implements SubscriptionError {
30
+ public readonly code = "BENEFIT_NOT_FOUND" as const;
31
+ public benefit: string;
32
+ public userMessage: string;
33
+
34
+ constructor(benefit: string) {
35
+ super(`Benefit not found: ${benefit}`);
36
+ this.name = "BenefitNotFoundError";
37
+ this.benefit = benefit;
38
+ this.userMessage = `The requested feature (${benefit}) is not available in your current subscription.`;
39
+ }
40
+ }
41
+
42
+ export class UnauthorizedError extends Error implements SubscriptionError {
43
+ public readonly code = "UNAUTHORIZED" as const;
44
+ public benefit?: string;
45
+ public userMessage: string;
46
+
47
+ constructor(message?: string, benefit?: string) {
48
+ super(message || "Unauthorized access");
49
+ this.name = "UnauthorizedError";
50
+ this.benefit = benefit;
51
+ this.userMessage = message || "You do not have permission to access this feature.";
52
+ }
53
+ }
54
+
55
+ export function createSubscriptionError(
56
+ code: SubscriptionError["code"],
57
+ benefit?: string,
58
+ customMessage?: string,
59
+ ): SubscriptionError {
60
+ switch (code) {
61
+ case "SUBSCRIPTION_REQUIRED":
62
+ return new SubscriptionRequiredError(benefit);
63
+ case "SUBSCRIPTION_EXPIRED":
64
+ return new SubscriptionExpiredError(benefit);
65
+ case "BENEFIT_NOT_FOUND":
66
+ if (!benefit) throw new Error("Benefit is required for BENEFIT_NOT_FOUND error");
67
+ return new BenefitNotFoundError(benefit);
68
+ case "UNAUTHORIZED":
69
+ return new UnauthorizedError(customMessage, benefit);
70
+ default:
71
+ throw new Error(`Unknown subscription error code: ${code}`);
72
+ }
73
+ }
74
+
75
+ export function isSubscriptionError(error: unknown): error is SubscriptionError {
76
+ return error instanceof Error && "code" in error && "userMessage" in error;
77
+ }
78
+
79
+ export function getSubscriptionErrorMessage(error: unknown): string {
80
+ if (isSubscriptionError(error)) {
81
+ return error.userMessage;
82
+ }
83
+ if (error instanceof Error) {
84
+ return error.message;
85
+ }
86
+ return "An unknown error occurred";
87
+ }