@htlkg/core 0.0.2 → 0.0.3

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 (35) hide show
  1. package/README.md +51 -0
  2. package/dist/index.d.ts +219 -0
  3. package/dist/index.js +121 -0
  4. package/dist/index.js.map +1 -1
  5. package/package.json +30 -8
  6. package/src/amplify-astro-adapter/amplify-astro-adapter.md +167 -0
  7. package/src/amplify-astro-adapter/createCookieStorageAdapterFromAstroContext.test.ts +296 -0
  8. package/src/amplify-astro-adapter/createCookieStorageAdapterFromAstroContext.ts +97 -0
  9. package/src/amplify-astro-adapter/createRunWithAmplifyServerContext.ts +84 -0
  10. package/src/amplify-astro-adapter/errors.test.ts +115 -0
  11. package/src/amplify-astro-adapter/errors.ts +105 -0
  12. package/src/amplify-astro-adapter/globalSettings.test.ts +78 -0
  13. package/src/amplify-astro-adapter/globalSettings.ts +16 -0
  14. package/src/amplify-astro-adapter/index.ts +14 -0
  15. package/src/amplify-astro-adapter/types.ts +55 -0
  16. package/src/auth/auth.md +178 -0
  17. package/src/auth/index.test.ts +180 -0
  18. package/src/auth/index.ts +294 -0
  19. package/src/constants/constants.md +132 -0
  20. package/src/constants/index.test.ts +116 -0
  21. package/src/constants/index.ts +98 -0
  22. package/src/core-exports.property.test.ts +186 -0
  23. package/src/errors/errors.md +177 -0
  24. package/src/errors/index.test.ts +153 -0
  25. package/src/errors/index.ts +134 -0
  26. package/src/index.ts +65 -0
  27. package/src/routes/index.ts +225 -0
  28. package/src/routes/routes.md +189 -0
  29. package/src/types/index.ts +94 -0
  30. package/src/types/types.md +144 -0
  31. package/src/utils/index.test.ts +257 -0
  32. package/src/utils/index.ts +112 -0
  33. package/src/utils/logger.ts +88 -0
  34. package/src/utils/utils.md +199 -0
  35. package/src/workspace.property.test.ts +235 -0
@@ -0,0 +1,153 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import {
3
+ AppError,
4
+ AuthError,
5
+ ValidationError,
6
+ NotFoundError,
7
+ } from "./index";
8
+
9
+ describe("Error Classes", () => {
10
+ describe("AppError", () => {
11
+ it("should create error with default values", () => {
12
+ const error = new AppError("Test error");
13
+
14
+ expect(error.message).toBe("Test error");
15
+ expect(error.code).toBe("APP_ERROR");
16
+ expect(error.statusCode).toBe(500);
17
+ expect(error.name).toBe("AppError");
18
+ expect(error.details).toBeUndefined();
19
+ });
20
+
21
+ it("should create error with custom values", () => {
22
+ const details = { userId: "123" };
23
+ const error = new AppError("Custom error", "CUSTOM_CODE", 400, details);
24
+
25
+ expect(error.message).toBe("Custom error");
26
+ expect(error.code).toBe("CUSTOM_CODE");
27
+ expect(error.statusCode).toBe(400);
28
+ expect(error.details).toEqual(details);
29
+ });
30
+
31
+ it("should be instanceof Error", () => {
32
+ const error = new AppError("Test");
33
+ expect(error).toBeInstanceOf(Error);
34
+ });
35
+
36
+ it("should convert to JSON", () => {
37
+ const error = new AppError("Test", "CODE", 400, { key: "value" });
38
+ const json = error.toJSON();
39
+
40
+ expect(json).toEqual({
41
+ name: "AppError",
42
+ message: "Test",
43
+ code: "CODE",
44
+ statusCode: 400,
45
+ details: { key: "value" },
46
+ });
47
+ });
48
+ });
49
+
50
+ describe("AuthError", () => {
51
+ it("should create auth error with default values", () => {
52
+ const error = new AuthError("Unauthorized");
53
+
54
+ expect(error.message).toBe("Unauthorized");
55
+ expect(error.code).toBe("AUTH_ERROR");
56
+ expect(error.statusCode).toBe(401);
57
+ expect(error.name).toBe("AuthError");
58
+ });
59
+
60
+ it("should create auth error with custom values", () => {
61
+ const error = new AuthError("Invalid token", "INVALID_TOKEN", 403);
62
+
63
+ expect(error.message).toBe("Invalid token");
64
+ expect(error.code).toBe("INVALID_TOKEN");
65
+ expect(error.statusCode).toBe(403);
66
+ });
67
+
68
+ it("should be instanceof AppError", () => {
69
+ const error = new AuthError("Test");
70
+ expect(error).toBeInstanceOf(AppError);
71
+ });
72
+ });
73
+
74
+ describe("ValidationError", () => {
75
+ it("should create validation error with default values", () => {
76
+ const error = new ValidationError("Validation failed");
77
+
78
+ expect(error.message).toBe("Validation failed");
79
+ expect(error.code).toBe("VALIDATION_ERROR");
80
+ expect(error.statusCode).toBe(400);
81
+ expect(error.name).toBe("ValidationError");
82
+ expect(error.errors).toEqual([]);
83
+ });
84
+
85
+ it("should create validation error with field errors", () => {
86
+ const fieldErrors = [
87
+ { field: "email", message: "Invalid email" },
88
+ { field: "password", message: "Too short" },
89
+ ];
90
+ const error = new ValidationError("Validation failed", fieldErrors);
91
+
92
+ expect(error.errors).toEqual(fieldErrors);
93
+ expect(error.details).toEqual({ errors: fieldErrors });
94
+ });
95
+
96
+ it("should convert to JSON with errors", () => {
97
+ const fieldErrors = [{ field: "email", message: "Invalid" }];
98
+ const error = new ValidationError("Failed", fieldErrors);
99
+ const json = error.toJSON();
100
+
101
+ expect(json.errors).toEqual(fieldErrors);
102
+ expect(json.name).toBe("ValidationError");
103
+ });
104
+
105
+ it("should be instanceof AppError", () => {
106
+ const error = new ValidationError("Test");
107
+ expect(error).toBeInstanceOf(AppError);
108
+ });
109
+ });
110
+
111
+ describe("NotFoundError", () => {
112
+ it("should create not found error with resource only", () => {
113
+ const error = new NotFoundError("User");
114
+
115
+ expect(error.message).toBe("User not found");
116
+ expect(error.code).toBe("NOT_FOUND");
117
+ expect(error.statusCode).toBe(404);
118
+ expect(error.name).toBe("NotFoundError");
119
+ expect(error.resource).toBe("User");
120
+ expect(error.resourceId).toBeUndefined();
121
+ });
122
+
123
+ it("should create not found error with resource and ID", () => {
124
+ const error = new NotFoundError("User", "123");
125
+
126
+ expect(error.message).toBe("User with id '123' not found");
127
+ expect(error.resource).toBe("User");
128
+ expect(error.resourceId).toBe("123");
129
+ });
130
+
131
+ it("should create not found error with custom message", () => {
132
+ const error = new NotFoundError("User", "123", "Custom not found message");
133
+
134
+ expect(error.message).toBe("Custom not found message");
135
+ expect(error.resource).toBe("User");
136
+ expect(error.resourceId).toBe("123");
137
+ });
138
+
139
+ it("should convert to JSON with resource info", () => {
140
+ const error = new NotFoundError("Brand", "456");
141
+ const json = error.toJSON();
142
+
143
+ expect(json.resource).toBe("Brand");
144
+ expect(json.resourceId).toBe("456");
145
+ expect(json.name).toBe("NotFoundError");
146
+ });
147
+
148
+ it("should be instanceof AppError", () => {
149
+ const error = new NotFoundError("Test");
150
+ expect(error).toBeInstanceOf(AppError);
151
+ });
152
+ });
153
+ });
@@ -0,0 +1,134 @@
1
+ /**
2
+ * @htlkg/core/errors
3
+ * Core error classes for Hotelinking applications
4
+ */
5
+
6
+ /**
7
+ * Base application error class
8
+ */
9
+ export class AppError extends Error {
10
+ public readonly code: string;
11
+ public readonly statusCode: number;
12
+ public readonly details?: Record<string, any>;
13
+
14
+ constructor(
15
+ message: string,
16
+ code = "APP_ERROR",
17
+ statusCode = 500,
18
+ details?: Record<string, any>,
19
+ ) {
20
+ super(message);
21
+ this.name = "AppError";
22
+ this.code = code;
23
+ this.statusCode = statusCode;
24
+ this.details = details;
25
+
26
+ // Maintains proper stack trace for where our error was thrown (only available on V8)
27
+ if (Error.captureStackTrace) {
28
+ Error.captureStackTrace(this, AppError);
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Convert error to JSON representation
34
+ */
35
+ toJSON(): Record<string, any> {
36
+ return {
37
+ name: this.name,
38
+ message: this.message,
39
+ code: this.code,
40
+ statusCode: this.statusCode,
41
+ details: this.details,
42
+ };
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Authentication error class
48
+ */
49
+ export class AuthError extends AppError {
50
+ constructor(
51
+ message: string,
52
+ code = "AUTH_ERROR",
53
+ statusCode = 401,
54
+ details?: Record<string, any>,
55
+ ) {
56
+ super(message, code, statusCode, details);
57
+ this.name = "AuthError";
58
+
59
+ if (Error.captureStackTrace) {
60
+ Error.captureStackTrace(this, AuthError);
61
+ }
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Validation error class
67
+ */
68
+ export class ValidationError extends AppError {
69
+ public readonly errors: Array<{ field: string; message: string }>;
70
+
71
+ constructor(
72
+ message: string,
73
+ errors: Array<{ field: string; message: string }> = [],
74
+ code = "VALIDATION_ERROR",
75
+ statusCode = 400,
76
+ ) {
77
+ super(message, code, statusCode, { errors });
78
+ this.name = "ValidationError";
79
+ this.errors = errors;
80
+
81
+ if (Error.captureStackTrace) {
82
+ Error.captureStackTrace(this, ValidationError);
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Convert error to JSON representation
88
+ */
89
+ toJSON(): Record<string, any> {
90
+ return {
91
+ ...super.toJSON(),
92
+ errors: this.errors,
93
+ };
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Not found error class
99
+ */
100
+ export class NotFoundError extends AppError {
101
+ public readonly resource: string;
102
+ public readonly resourceId?: string;
103
+
104
+ constructor(
105
+ resource: string,
106
+ resourceId?: string,
107
+ message?: string,
108
+ code = "NOT_FOUND",
109
+ statusCode = 404,
110
+ ) {
111
+ const defaultMessage = resourceId
112
+ ? `${resource} with id '${resourceId}' not found`
113
+ : `${resource} not found`;
114
+ super(message || defaultMessage, code, statusCode, { resource, resourceId });
115
+ this.name = "NotFoundError";
116
+ this.resource = resource;
117
+ this.resourceId = resourceId;
118
+
119
+ if (Error.captureStackTrace) {
120
+ Error.captureStackTrace(this, NotFoundError);
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Convert error to JSON representation
126
+ */
127
+ toJSON(): Record<string, any> {
128
+ return {
129
+ ...super.toJSON(),
130
+ resource: this.resource,
131
+ resourceId: this.resourceId,
132
+ };
133
+ }
134
+ }
package/src/index.ts ADDED
@@ -0,0 +1,65 @@
1
+ /**
2
+ * @htlkg/core
3
+ * Core utilities, types, and authentication for Hotelinking applications
4
+ */
5
+
6
+ // Auth exports
7
+ export {
8
+ getUser,
9
+ getClientUser,
10
+ hasAccessToBrand,
11
+ hasAccessToAccount,
12
+ isAdminUser,
13
+ isSuperAdminUser,
14
+ requireAuth,
15
+ requireAdminAccess,
16
+ requireBrandAccess,
17
+ type User,
18
+ type APIContext,
19
+ } from "./auth/index.js";
20
+
21
+ // Type exports
22
+ export type {
23
+ Brand,
24
+ Account,
25
+ Product,
26
+ ProductInstance,
27
+ BrandUser,
28
+ } from "./types/index.js";
29
+
30
+ // Utility exports
31
+ export {
32
+ formatDate,
33
+ truncateText,
34
+ groupBy,
35
+ debounce,
36
+ throttle,
37
+ } from "./utils/index.js";
38
+
39
+ // Constants exports
40
+ export { ROUTES, PRODUCTS, PERMISSIONS, USER_ROLES } from "./constants/index.js";
41
+
42
+ // Error exports
43
+ export {
44
+ AppError,
45
+ AuthError,
46
+ ValidationError,
47
+ NotFoundError,
48
+ } from "./errors/index.js";
49
+
50
+ // Type-safe routes
51
+ export {
52
+ routes,
53
+ adminRoutes,
54
+ portalRoutes,
55
+ authRoutes,
56
+ apiRoutes,
57
+ createRoute,
58
+ matchesRoute,
59
+ extractRouteParams,
60
+ navigateTo,
61
+ buildUrl,
62
+ type Route,
63
+ type RouteParams,
64
+ type Routes,
65
+ } from "./routes/index.js";
@@ -0,0 +1,225 @@
1
+ /**
2
+ * Type-Safe Routes
3
+ *
4
+ * Centralized route definitions with type safety and parameter interpolation.
5
+ * Eliminates string literals scattered across the codebase.
6
+ */
7
+
8
+ /**
9
+ * Route parameters type
10
+ */
11
+ export type RouteParams = Record<string, string | number>;
12
+
13
+ /**
14
+ * Route function interface - can be called with params or used as path
15
+ */
16
+ export interface Route<TParams extends RouteParams = RouteParams> {
17
+ /** Base path of the route */
18
+ readonly path: string;
19
+ /** Generate URL with interpolated parameters */
20
+ (params?: TParams): string;
21
+ }
22
+
23
+ /**
24
+ * Create a route with optional parameters
25
+ *
26
+ * @example
27
+ * ```typescript
28
+ * const userRoute = createRoute<{ id: string }>('/users/:id');
29
+ * userRoute({ id: '123' }); // "/users/123"
30
+ * userRoute.path; // "/users/:id"
31
+ * ```
32
+ */
33
+ export function createRoute<TParams extends RouteParams = Record<string, never>>(path: string): Route<TParams> {
34
+ const routeFn = (params?: TParams): string => {
35
+ if (!params) return path;
36
+
37
+ let result = path;
38
+ for (const [key, value] of Object.entries(params)) {
39
+ result = result.replace(`:${key}`, String(value));
40
+ }
41
+ return result;
42
+ };
43
+
44
+ // Add path as a property
45
+ Object.defineProperty(routeFn, "path", {
46
+ value: path,
47
+ writable: false,
48
+ enumerable: true,
49
+ });
50
+
51
+ return routeFn as Route<TParams>;
52
+ }
53
+
54
+ /**
55
+ * Admin routes
56
+ */
57
+ export const adminRoutes = {
58
+ /** Dashboard */
59
+ dashboard: createRoute("/admin"),
60
+
61
+ /** Accounts list */
62
+ accounts: createRoute("/admin/accounts"),
63
+
64
+ /** Account detail */
65
+ account: createRoute<{ id: string | number }>("/admin/accounts/:id"),
66
+
67
+ /** Account brands */
68
+ accountBrands: createRoute<{ id: string | number }>("/admin/accounts/:id/brands"),
69
+
70
+ /** Brands list */
71
+ brands: createRoute("/admin/brands"),
72
+
73
+ /** Brand detail */
74
+ brand: createRoute<{ id: string | number }>("/admin/brands/:id"),
75
+
76
+ /** Brand settings */
77
+ brandSettings: createRoute<{ id: string | number }>("/admin/brands/:id/settings"),
78
+
79
+ /** Users list */
80
+ users: createRoute("/admin/users"),
81
+
82
+ /** User detail */
83
+ user: createRoute<{ id: string }>("/admin/users/:id"),
84
+
85
+ /** Current user profile */
86
+ profile: createRoute("/admin/user"),
87
+
88
+ /** Analytics */
89
+ analytics: createRoute("/admin/analytics"),
90
+ } as const;
91
+
92
+ /**
93
+ * Portal routes (customer-facing)
94
+ */
95
+ export const portalRoutes = {
96
+ /** Portal home */
97
+ home: createRoute("/"),
98
+
99
+ /** Brand portal */
100
+ brand: createRoute<{ brandId: string | number }>("/brands/:brandId"),
101
+
102
+ /** Brand settings */
103
+ brandSettings: createRoute<{ brandId: string | number }>("/brands/:brandId/settings"),
104
+
105
+ /** Brand dashboard */
106
+ brandDashboard: createRoute<{ brandId: string | number }>("/brands/:brandId/dashboard"),
107
+
108
+ /** Brand analytics */
109
+ brandAnalytics: createRoute<{ brandId: string | number }>("/brands/:brandId/analytics"),
110
+ } as const;
111
+
112
+ /**
113
+ * Auth routes
114
+ */
115
+ export const authRoutes = {
116
+ /** Login page */
117
+ login: createRoute("/login"),
118
+
119
+ /** Logout endpoint */
120
+ logout: createRoute("/api/auth/logout"),
121
+
122
+ /** Confirm sign in */
123
+ confirmSignIn: createRoute("/api/auth/confirm-signin"),
124
+ } as const;
125
+
126
+ /**
127
+ * API routes
128
+ */
129
+ export const apiRoutes = {
130
+ /** Auth endpoints */
131
+ auth: {
132
+ logout: createRoute("/api/auth/logout"),
133
+ confirmSignIn: createRoute("/api/auth/confirm-signin"),
134
+ },
135
+ } as const;
136
+
137
+ /**
138
+ * All routes combined
139
+ */
140
+ export const routes = {
141
+ admin: adminRoutes,
142
+ portal: portalRoutes,
143
+ auth: authRoutes,
144
+ api: apiRoutes,
145
+ } as const;
146
+
147
+ /**
148
+ * Type for all routes
149
+ */
150
+ export type Routes = typeof routes;
151
+
152
+ /**
153
+ * Helper to check if a path matches a route pattern
154
+ */
155
+ export function matchesRoute(path: string, route: Route): boolean {
156
+ const pattern = route.path
157
+ .replace(/:[^/]+/g, "[^/]+") // Replace :param with regex
158
+ .replace(/\//g, "\\/"); // Escape slashes
159
+
160
+ const regex = new RegExp(`^${pattern}$`);
161
+ return regex.test(path);
162
+ }
163
+
164
+ /**
165
+ * Extract parameters from a path given a route pattern
166
+ */
167
+ export function extractRouteParams<TParams extends RouteParams>(
168
+ path: string,
169
+ route: Route<TParams>
170
+ ): TParams | null {
171
+ const paramNames: string[] = [];
172
+ const pattern = route.path.replace(/:([^/]+)/g, (_, name) => {
173
+ paramNames.push(name);
174
+ return "([^/]+)";
175
+ });
176
+
177
+ const regex = new RegExp(`^${pattern}$`);
178
+ const match = path.match(regex);
179
+
180
+ if (!match) return null;
181
+
182
+ const params: RouteParams = {};
183
+ paramNames.forEach((name, index) => {
184
+ params[name] = match[index + 1];
185
+ });
186
+
187
+ return params as TParams;
188
+ }
189
+
190
+ /**
191
+ * Navigate to a route (client-side helper)
192
+ *
193
+ * @example
194
+ * ```typescript
195
+ * navigateTo(routes.admin.account({ id: '123' }));
196
+ * ```
197
+ */
198
+ export function navigateTo(url: string): void {
199
+ if (typeof window !== "undefined") {
200
+ window.location.href = url;
201
+ }
202
+ }
203
+
204
+ /**
205
+ * Build a URL with query parameters
206
+ *
207
+ * @example
208
+ * ```typescript
209
+ * const url = buildUrl(routes.admin.users(), { page: 2, status: 'active' });
210
+ * // "/admin/users?page=2&status=active"
211
+ * ```
212
+ */
213
+ export function buildUrl(path: string, params?: Record<string, string | number | boolean | null | undefined>): string {
214
+ if (!params) return path;
215
+
216
+ const searchParams = new URLSearchParams();
217
+ for (const [key, value] of Object.entries(params)) {
218
+ if (value !== null && value !== undefined && value !== "") {
219
+ searchParams.set(key, String(value));
220
+ }
221
+ }
222
+
223
+ const queryString = searchParams.toString();
224
+ return queryString ? `${path}?${queryString}` : path;
225
+ }